fix(statusline): sticky telemetry collection, footer print-through, EOF fix

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>
This commit is contained in:
timkjr
2026-09-07 20:37:29 -05:00
co-authored by Claude Sonnet 5
parent e15e8e43e8
commit d5b75af628
17 changed files with 288 additions and 143 deletions
+13 -13
View File
@@ -947,18 +947,19 @@ export function registerSessionRoutes(
await updateCaseModel(workingDir, body.modelOverride || null);
}
// Plan-usage telemetry request (App Settings → header chip). NO LONGER a
// disk write here — a settings.local.json statusLine took precedence over
// the user's own global/project statusLine for ANY `claude` run in that
// directory, including entirely outside Codeman, with no disclosure and no
// way to undo it (real bug, found 2026-08-31). The request now flows
// through as an ordinary session field (statusLineTelemetryRequested below)
// and Session/TmuxManager resolve it into an EPHEMERAL `claude --settings`
// CLI flag at actual spawn time (resolveStatusLineCliCommand in
// hooks-config.ts) — never written to disk, so a plain `claude` run outside
// Codeman is untouched. That resolution also self-heals: it strips any
// legacy disk-written exporter an older Codeman build left behind.
const statusLineTelemetryRequested = body.statusLineTelemetry === true;
// Plan-usage telemetry (App Settings → header chip): no request-time field
// here anymore, and NO disk write — a settings.local.json statusLine used
// to take precedence over the user's own global/project statusLine for ANY
// `claude` run in that directory, including entirely outside Codeman, with
// no disclosure and no way to undo it (real bug, found 2026-08-31).
// TmuxManager.createSession reads the persisted `showPlanUsageLimits`
// setting FRESH at spawn (readPlanUsageTelemetryEnabled in hooks-config.ts)
// and resolves it into an EPHEMERAL `claude --settings` CLI flag — never
// written to disk, so a plain `claude` run outside Codeman is untouched —
// and applies uniformly to every claude creation path (this route, cron,
// the Ralph Loop API, quick-start), not just this one. That resolution
// also self-heals: it strips any legacy disk-written exporter an older
// Codeman build left behind.
// Hooks for the workspace this session runs in (install vs refresh-only is the
// `workspaceHooksEnabled` setting; see applyWorkspaceHooks). Never for a remote
@@ -1092,7 +1093,6 @@ export function registerSessionRoutes(
resumeSessionId: validatedResumeId,
envOverrides: await clampEnvOverridesForOwner(owner, body.envOverrides),
effort: body.effort,
statusLineTelemetry: statusLineTelemetryRequested,
tmuxHistoryLimit: terminalHistoryConfig.tmuxHistoryLimit,
remote,
owner,
+21 -19
View File
@@ -990,11 +990,9 @@ export function registerSystemRoutes(
} catch {
/* ignore */
}
// statusLineTelemetry and acknowledgeUnauthTunnel are ACTION fields (not stored
// settings) — strip them before persisting so settings.json stays clean.
// statusLineTelemetry is an action field only (see below); it must never
// land in settingsToStore.
const { statusLineTelemetry: _statusLineTelemetry, acknowledgeUnauthTunnel, ...settingsToStore } = settings;
// acknowledgeUnauthTunnel is an ACTION field (not a stored setting) — strip
// it before persisting so settings.json stays clean.
const { acknowledgeUnauthTunnel, ...settingsToStore } = settings;
const merged = { ...existing, ...settingsToStore };
await fs.writeFile(SETTINGS_PATH, JSON.stringify(merged, null, 2));
@@ -1007,7 +1005,7 @@ export function registerSystemRoutes(
// Service toggles resolve from `merged` (existing + incoming), NEVER from the
// raw request body. A PARTIAL PUT omits keys it does not intend to change, and
// reading the body directly turned every omission into "apply the default":
// a body of just `{statusLineTelemetry:true}` would START the subagent watcher
// a body of just `{showPlanUsageLimits:true}` would START the subagent watcher
// (`?? true`) and STOP the workflow + image watchers (`?? false`), silently
// undoing the user's persisted config. Reading `merged` makes any PUT reconcile
// services to the effective stored settings instead, which also self-heals
@@ -1033,19 +1031,23 @@ export function registerSystemRoutes(
}
});
// Plan-usage chip: its DISPLAY is per-device (client-side, see settings-ui.js).
// Telemetry COLLECTION was previously server-side and enable-sticky here —
// toggling the chip ON re-injected a statusLine.command into every ACTIVE
// Claude session's settings.local.json so live % started flowing without a
// new session. That disk write is exactly the bug fixed 2026-08-31 (it took
// precedence over the user's own statusline for ANY `claude` run in that
// directory, including outside Codeman, with no way to undo it). Telemetry
// is now requested per-session at CREATE/RESPAWN time only (statusLineTelemetry
// threaded through cron/ralph-loop/quick-start/interactive-create, see those
// route handlers), resolved into an ephemeral `--settings` CLI flag — fixed at
// spawn, so there is nothing to (re)inject into an ALREADY-RUNNING session
// here, unlike the old disk mechanism. The action field above is received and
// discarded; flipping the chip ON only affects sessions created from now on.
// Plan-usage chip: its DISPLAY is per-device (client-side, see settings-ui.js),
// but `showPlanUsageLimits` ALSO doubles as the telemetry COLLECTION switch,
// persisted here in settingsToStore like any other setting (no special-casing
// needed — see readPlanUsageTelemetryEnabled's doc comment in hooks-config.ts).
// Telemetry COLLECTION used to be a SEPARATE, action-only, sticky mechanism
// here: toggling the chip ON re-injected a statusLine.command into every
// ACTIVE Claude session's settings.local.json so live % started flowing
// without a new session. That disk write was the bug fixed 2026-08-31 (it
// took precedence over the user's own statusline for ANY `claude` run in
// that directory, including outside Codeman, with no way to undo it).
// Collection is now decided by TmuxManager.createSession/respawnPane reading
// `showPlanUsageLimits` FRESH from settings.json at spawn time — no
// per-session field, no per-request threading through cron/Ralph-loop/
// quick-start/interactive-create (they all reach the same read), and no
// (re)injection into an already-running session needed here: the NEXT
// respawn (a Ralph cycle, `/clear`, a PTY-exit restart) already picks up
// whatever this PUT just persisted.
// Handle tunnel toggle dynamically
if ('tunnelEnabled' in settings) {