Claude Code's tracker nulls recache_tokens_if_cold at a compaction or a tool-result clearing and only counts the expected rebuild at the next request, which also closes the window, so no report carried both and the old rule never fired. Stamp on the first warm null report after one that carried a value, carry it through idle re-reports and drop it at the next request; the readout now says "Context compacted at HH:MM" since a clearing opens the same window.
Also persist a changed report so a crash restores the latest one, rename the session field to statusLineExporter (statusLineTelemetry is the registry capability) and document its values, word the stake as re-cached, move the readout block above the SSE staleness heading, and drop keep-warm mentions until it lands.
Claude Code's statusline has carried a prompt_cache object since v2.1.251, and Codeman's statusline exporter already received it, but StatusTelemetrySchema stripped the whole object, so Codeman never knew whether a session's conversation was still cached. Keep it and show it.
StatusTelemetrySchema now lists every documented scalar field null-tolerant (several are documented as possibly null, and the object gains fields across CLI versions, so a strict shape would 400 the post and blank the footer), while parsePromptCache normalizes only the subset Codeman stores, so a hostile localhost payload cannot bloat state. Session.promptCache rides toState with a change-only session:promptCache event, and the route broadcasts the stored object.
The Session Options (Respawn tab) readout shows warm-until or cold with the re-read stake, the cache hit rate, the last miss's causes, and, for Claude Code's own idle compaction (recache_tokens_if_cold null while expected_rebuilds rose), a "compacted at HH:MM" note that is never raised as an alert. setPromptCache stamps that on a new object rather than mutating the parsed report, so the stamp reaches the broadcast. Sessions without a statusline of their own also get a cache:until HH:MM / cache:cold group in the terminal footer. The last report is restored on a Codeman restart (like displayModel) so the readout is not blank until the pane's next render; the readout is time-aware, so a value whose expiry has passed reads as cold, not stale-warm. A launch without the exporter records why (collection off, workspace statusline, remote) so the row says the cause instead of "not reported yet" forever.
This is the sensor a later opt-in keep-warm would read, and the TTL it reports is the source of truth rather than the plan tier.
Two follow-ups to #361's sticky telemetry switch.
GET /api/settings reconciled an absent showPlanUsageLimits by persisting
true, but readJsonConfig() answers {} for ANY read failure (a parse
error, EACCES, EMFILE, a read landing inside PUT's non-atomic write), not
only ENOENT, and every page load calls this route, so one unlucky read
replaced the whole settings file with a one-key file. The route is a
plain read again and the default moved into the reader:
readPlanUsageTelemetryEnabled() treats an absent key as ON, the same way
readWorkspaceHooksEnabled() does, which is what the desktop chip already
shows for an install that never touched the setting.
saveAppSettings() sent showPlanUsageLimits on every save. The chip
defaults OFF on handhelds, so a phone saving its font size persisted
false and switched collection off for every desktop, whose chip then
went stale with no error anywhere. The key is now stripped like the
other per-device display keys and re-added only when the save FLIPS the
chip relative to what the device had (planUsageCollectionFlip), so an
explicit toggle on any device still writes it in either direction.
Tests pin both: the GET route with a mocked filesystem (absent, missing,
EACCES, garbage, explicit), the reader default, and the flip helper plus
its wiring in saveAppSettings.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Responds to Ark0N's review round on the ephemeral-CLI-flag statusline
injection rework:
- Rebase-detail fixes: registry-gated telemetry eligibility via
getCli(mode)?.capabilities.statusLineTelemetry instead of a hardcoded
mode === 'claude' check, using the capability flag master's CLI-registry
refactor already declares for exactly this purpose.
- Design question settled: sticky (a). Rather than persisting the toggle
as a new field and threading it through every session-creation path
(cron, Ralph Loop API, quick-start), eliminated the per-session field
entirely. readPlanUsageTelemetryEnabled() (hooks-config.ts) reads the
existing showPlanUsageLimits setting fresh from settings.json at every
claude create/respawn (TmuxManager.createSession/respawnPane) - no
per-session state to survive a restart, and it applies uniformly to
every creation path for free, since they all flow through the same
TmuxManager methods.
This required fixing a real bug found along the way: showPlanUsageLimits
was not actually round-tripping through settings.json on save -
settings-ui.js explicitly excluded it from the PUT body as a pure
per-device display key. It now flows through normally (both true and
false); the load-side per-device merge behavior is unchanged.
Removed entirely as a result: the statusLineTelemetry field from
CreateSessionSchema/SettingsUpdateSchema, CreateSessionOptions/
RespawnPaneOptions, Session._statusLineTelemetry (this is what makes
the restart-persistence bug moot rather than patched), and the
frontend send sites.
- Footer print-through restored: the no-user-statusline branch of the
exporter script now runs the telemetry POST in the foreground so its
own stdout becomes the in-terminal footer, falling back to a plain
"codeman" marker only on curl failure.
- Background-subshell EOF fix: the wrap-a-real-statusline branch closes
stdin too, not just stdout/stderr (`>/dev/null 2>&1 </dev/null &`) -
the un-redirected subshell process itself, not curl, was what held a
reader-to-EOF's pipe open for however long curl took to finish. Added
curl --max-time 5 so a hung (not just refused) Codeman cannot wedge
the render.
Tests: real-shell-execution tests for the footer/EOF fixes (fake curl
stand-in on PATH, real sh subprocess spawns, real elapsed-time
measurements - verified non-vacuous against a hand-reconstructed
old-style script), unit tests for readPlanUsageTelemetryEnabled.
Adapted two existing tests whose payloads referenced the removed field.
Fixed during independent code review: a stray indentation break and a
test exercising the wrong (legacy) exporter code path.
Docs synced: CLAUDE.md, docs/usage-limits-display-plan.md (old
disk-based section marked superseded, kept for history),
docs/architecture-invariants.md.
Full suite green: 352 files, 6780 passed, 12 skipped, 0 failed.
tsc/lint/format:check/frontend-syntax all clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plan-usage chip defaults ON on desktop (handhelds stay OFF), resolved
through a single planUsageChipEnabled() helper so the checkbox, the chip
and the create-time statusLineTelemetry flag cannot disagree. Correct the
stale "Cron button defaults ON" comment (it is OFF in code, template and
CSS) and the styles.css comment claiming the server strips the chip's
hidden class.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Surface Claude subscription plan usage limits (5-hour rolling + 7-day
weekly: percent used + reset time) in the header, opt-in via App Settings
→ Display → "Plan Usage Limits" (default OFF, no behavior change when off).
A Codeman-managed Claude statusLine exporter forwards the rate_limits JSON
to a new auth-exempt POST /api/status-telemetry (same loopback + hook-secret
gate as /api/hook-event); parsed telemetry broadcasts over SSE
session:statusTelemetry to a header chip (amber >=80%, red >=95%, reset
times on hover). The exporter prints the same summary back as the
in-terminal footer (print-through).
- src/usage-telemetry.ts: pure parser/formatter (epoch-sec -> ms, clamp,
change signature) + test/usage-telemetry.test.ts
- hooks-config.ts: generateStatusLineCommand + applyStatusLineConfig
(add/remove; never clobbers a user's own statusLine)
- session-routes.ts: inject gate (Claude-only, Codeman-managed cases),
driven by create-payload statusLineTelemetry (session-ui.js)
- schemas.ts: StatusTelemetrySchema + showPlanUsageLimits + payload field
- frontend: header chip, applyHeaderVisibilitySettings toggle,
renderIndexHtml strip, _onSessionStatusTelemetry handler
Schema empirically confirmed against Claude Code 2.1.177 (Claude Max):
only five_hour/seven_day windows exist (no Opus-weekly field); rate_limits
is absent before the first API response and for non-subscriber auth. Design
+ verification method in docs/usage-limits-display-plan.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>