feat(cli): roll circuit-breaker state over across config wipes

The DE parallel-router breaker's tripped state lived in the same config
file as the install id, so the most common identity reset — deleting
~/.hyperframes — also re-enrolled the machine into an experimental path
that had already failed on it.

Mirror exactly two facts into a machine-local state file
(~/.local/state/hyperframes/install-state.json) that a config wipe does
not touch:

- markerAt: written unconditionally on every install, so the fraction of
  fresh mints that find it directly measures recoverable id churn
  (config wiped, machine persisted) vs unrecoverable (fresh
  machine/container/new user). Emitted as install_predecessor_found on
  telemetry events; absent (not false) on configs predating the field.
- deParallelRouterTrialFired: a breaker tripped by a previous install
  stays tripped for the new one. Config corruption takes the same mint
  path, so it survives that too.

The file deliberately holds NO identity — no anonymousId, no counters.
A wiped config still gets a fresh id unconditionally; only the safety
fact about the machine survives. Sync happens inside writeConfig so no
breaker write site can forget it; failures are swallowed (telemetry
must never break the CLI) but leave the memo unset so a later write
retries. `hyperframes telemetry` lists the state path for transparency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-29 00:46:41 -07:00
co-authored by Claude Opus 5
parent 0d42d65525
commit dfe92b2aab
5 changed files with 272 additions and 10 deletions
@@ -27,6 +27,7 @@ async function loadTelemetryCommand(options?: {
vi.resetModules();
vi.doMock("../telemetry/config.js", () => ({
CONFIG_PATH: "/test/.hyperframes/config.json",
STATE_PATH: "/test/.local/state/hyperframes/install-state.json",
readConfig: () => {
throw new Error("telemetry commands must bypass stale cached config");
},
+9 -1
View File
@@ -1,5 +1,10 @@
import { defineCommand } from "citty";
import { writeConfigWithResult, readConfigFresh, CONFIG_PATH } from "../telemetry/config.js";
import {
writeConfigWithResult,
readConfigFresh,
CONFIG_PATH,
STATE_PATH,
} from "../telemetry/config.js";
import { effectiveTelemetryStatus, type TelemetryStatusSource } from "../telemetry/policy.js";
import { c } from "../ui/colors.js";
import { failCommand } from "../utils/commandResult.js";
@@ -56,6 +61,9 @@ function runStatus(): void {
console.log(` ${c.dim("Status:")} ${status}`);
console.log(` ${c.dim("Source:")} ${effective.source}`);
console.log(` ${c.dim("Config:")} ${c.accent(CONFIG_PATH)}`);
// Machine-local safety state (no identity): survives a config wipe so a
// tripped experiment circuit breaker stays tripped. Listed for transparency.
console.log(` ${c.dim("State:")} ${c.accent(STATE_PATH)}`);
console.log(` ${c.dim("Tracked commands:")} ${c.bold(String(config.commandCount))}`);
console.log();
console.log(` ${c.dim("Disable:")} ${c.accent("hyperframes telemetry disable")}`);