instar 1.3.229 → 1.3.231

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cli.js +11 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/devProfileNode.d.ts +62 -0
  4. package/dist/commands/devProfileNode.d.ts.map +1 -0
  5. package/dist/commands/devProfileNode.js +179 -0
  6. package/dist/commands/devProfileNode.js.map +1 -0
  7. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  8. package/dist/config/ConfigDefaults.js +8 -0
  9. package/dist/config/ConfigDefaults.js.map +1 -1
  10. package/dist/core/LlmCircuitBreaker.d.ts +15 -0
  11. package/dist/core/LlmCircuitBreaker.d.ts.map +1 -1
  12. package/dist/core/LlmCircuitBreaker.js +37 -1
  13. package/dist/core/LlmCircuitBreaker.js.map +1 -1
  14. package/dist/monitoring/ResourceLedger.d.ts +78 -0
  15. package/dist/monitoring/ResourceLedger.d.ts.map +1 -0
  16. package/dist/monitoring/ResourceLedger.js +163 -0
  17. package/dist/monitoring/ResourceLedger.js.map +1 -0
  18. package/dist/monitoring/ResourceLedgerPoller.d.ts +64 -0
  19. package/dist/monitoring/ResourceLedgerPoller.d.ts.map +1 -0
  20. package/dist/monitoring/ResourceLedgerPoller.js +83 -0
  21. package/dist/monitoring/ResourceLedgerPoller.js.map +1 -0
  22. package/dist/scaffold/templates.d.ts.map +1 -1
  23. package/dist/scaffold/templates.js +7 -0
  24. package/dist/scaffold/templates.js.map +1 -1
  25. package/dist/server/AgentServer.d.ts +2 -0
  26. package/dist/server/AgentServer.d.ts.map +1 -1
  27. package/dist/server/AgentServer.js +49 -0
  28. package/dist/server/AgentServer.js.map +1 -1
  29. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  30. package/dist/server/CapabilityIndex.js +11 -0
  31. package/dist/server/CapabilityIndex.js.map +1 -1
  32. package/dist/server/routes.d.ts +1 -0
  33. package/dist/server/routes.d.ts.map +1 -1
  34. package/dist/server/routes.js +18 -0
  35. package/dist/server/routes.js.map +1 -1
  36. package/package.json +1 -1
  37. package/scripts/docs-coverage.mjs +14 -4
  38. package/scripts/generate-builtin-manifest.cjs +1 -0
  39. package/src/data/builtin-manifest.json +87 -79
  40. package/src/scaffold/templates.ts +7 -0
  41. package/upgrades/1.3.230.md +66 -0
  42. package/upgrades/1.3.231.md +39 -0
  43. package/upgrades/dev-profile-node-tool.eli16.md +42 -0
  44. package/upgrades/side-effects/dev-profile-node-tool.md +85 -0
  45. package/upgrades/side-effects/resource-ledger-phase-a.md +101 -0
@@ -650,6 +650,11 @@ This routes feedback to the Instar maintainers automatically. Valid types: \`bug
650
650
  - Returns \`{ totals, features: [{ feature, calls, tokensIn, tokensOut, fired, noop, fireRate, p50LatencyMs, p95LatencyMs, ... }] }\` — one row per system (e.g. MessagingToneGate, CoherenceReviewer). Filter with \`?feature=<name>\`.
651
651
  - **When to use**: when asked "which checks cost the most / fire the least?", "is this gate worth it?", or before tuning a sentinel/gate — read the numbers instead of guessing. (Spec: \`docs/specs/llm-feature-metrics-spec.md\`.)
652
652
 
653
+ **Resource Usage (rate-limit events)** — Durable per-agent record of rate-limit events, so they survive restarts instead of vanishing. Every circuit-breaker trip (the account got throttled) and session-sentinel detection is written down. Read-only observability — it never gates. (Phase A; CPU/memory tracking is a follow-up.)
654
+ - Check: \`curl -H "Authorization: Bearer $AUTH" "http://localhost:${port}/resources/rate-limits?sinceHours=24"\`
655
+ - Returns \`{ windowHours, summary: { circuitOpenCount, circuitRecoverCount, sentinelCount, tripsPerHour, ... }, byKind, events }\`. \`circuitOpenCount\` is the headline (breaker trips); \`sentinelCount\` is session-scoped detections, counted separately.
656
+ - **When to use**: when asked "how many times were we throttled today?", "is the rate-limit pressure getting worse?", or before/while tuning anything that reacts to rate limits — read the durable count instead of guessing. (Spec: \`docs/specs/per-agent-resource-ledger.md\`.)
657
+
653
658
  **Session Clock** — How long have you been running, and how much time is left? For any active time-boxed (autonomous) session, this returns the computed elapsed + remaining so you never have to guess or compute it yourself. Read-only observability.
654
659
  - Check: \`curl -H "Authorization: Bearer $AUTH" "http://localhost:${port}/session/clock"\` (optional \`?topic=<N>\` to bind to one session)
655
660
  - Returns \`{ now, nowIso, sessions: [{ label, kind, startedAt, endsAt, elapsedSeconds, remainingSeconds, elapsedHuman, remainingHuman, percentElapsed, status }] }\`; \`{ sessions: [] }\` when nothing is time-boxed. Per-machine (the record is local).
@@ -755,6 +760,8 @@ Instar contributors can run \`instar dev:preflight\` before opening PRs to run l
755
760
 
756
761
  Run \`instar dev:ci-failures <pr>\` to print a red PR's exact failing tests (file:line + assertion) via the GitHub check-run annotations API — handy when \`gh run view --log\` returns nothing.
757
762
 
763
+ Run \`instar dev:profile-node [pid]\` to CPU-profile a hot RUNNING node process and print its hottest JS functions (function + file:line + self-time %). It uses SIGUSR1 + node's inspector + a CDP CPU profile, so it sees the JS frames macOS \`sample\` can't symbolicate — the way to pin which function a busy agent server is burning CPU in. No pid → it profiles the hottest node process.
764
+
758
765
  **Critical rule**: If this CLAUDE.md says a feature is "for standalone agents" or "when configured" or uses any qualifier — do NOT conclude you lack the feature. Check \`/capabilities\` instead. Documentation describes features in general; the API tells you what's actually running for YOU right now. When they conflict, the API wins.
759
766
 
760
767
  ### Registry First, Explore Second
@@ -0,0 +1,66 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ Adds `instar dev:profile-node [pid]` — a contributor/agent dev command that
9
+ CPU-profiles a **running** node process and prints its hottest JS functions
10
+ (function + `file:line` + self-time %).
11
+
12
+ **Why this exists (the technique that cracked a real bug).** macOS `sample` — and
13
+ lsof snapshots, empty `gh run --log`, port-grep — cannot symbolicate a node
14
+ process's **JS** frames; they show only native/V8 frames, so a busy agent server's
15
+ actual hot *function* stays invisible. The way to see it is the node process's own
16
+ introspection: `SIGUSR1` opens node's inspector on the running process, then a CDP
17
+ CPU profile over the inspector websocket reports the exact JS call frames. This is
18
+ literally how the systemic agent-server hot-loop was pinned — `StateManager.listSessions`
19
+ burning ~30% of CPU in `readFileUtf8`, invisible to every other tool. This command
20
+ turns that hard-won procedure into one reusable command.
21
+
22
+ **What it does:** resolves the pid (or auto-finds the hottest `node` process) →
23
+ `SIGUSR1` → probes `127.0.0.1:9229-9235` for the inspector target → captures a CPU
24
+ profile (default 5s, `--duration`) → prints the top frames by self-time (`--top`).
25
+ The biggest non-idle frame is the hot path. Read-only aside from the SIGUSR1, which
26
+ opens the inspector on localhost for the process's lifetime (it closes on the next
27
+ restart) — the command says so in its output.
28
+
29
+ Pairs with `dev:ci-failures` and `dev:preflight` as the contributor dev-loop
30
+ toolkit (friction → tooling).
31
+
32
+ ## What to Tell Your User
33
+
34
+ Nothing required — it is a developer/agent tool for working on Instar. When an
35
+ agent server is mysteriously burning CPU, the instar dev:profile-node command
36
+ prints the exact JS function responsible, which native profilers cannot show.
37
+
38
+ ## Summary of New Capabilities
39
+
40
+ - `instar dev:profile-node [pid] [--duration <sec>] [--top <n>]` — CPU-profile a
41
+ running node process (SIGUSR1 + inspector + CDP) and print its hottest JS frames.
42
+ No pid → profiles the hottest node process.
43
+
44
+ ## Evidence
45
+
46
+ - Built directly from the technique that pinned the real hot-loop: after `sample`,
47
+ lsof, grep, and log-diffing all failed, SIGUSR1 + a CDP CPU profile showed 30.8%
48
+ `readFileUtf8` + 7% `listSessions` (StateManager) — the JS frame nothing else
49
+ could see — leading to the cache fix.
50
+ - Live end-to-end smoke: profiling a spawned CPU-busy node correctly reported the
51
+ hot frame (`listOnTimeout`, where the busy loop ran).
52
+ - Tests: `tests/unit/devProfileNode.test.ts` — pure `aggregateHotFrames` (ranking,
53
+ file:line normalization, anonymous-frame fallback, empty profile, topN) +
54
+ `findInspectorTarget` (first-hit / none) + `runDevProfileNode` orchestration with
55
+ injected deps (happy path, no-pid→hottest, no-node→exit1, signal-fail→exit1,
56
+ no-inspector→exit1, capture-throw→exit1).
57
+
58
+ ## Also in this change
59
+
60
+ Fixes a real false-negative in `scripts/docs-coverage.mjs`: a command's coverage
61
+ was keyed on its camelCase **file** name (`devProfileNode`), so the needle
62
+ `instar devProfileNode` never matched the shipped colon-command
63
+ `instar dev:profile-node` — every `dev:*` command (ci-failures, preflight,
64
+ profile-node) read as undocumented. The matcher now also tries a kebab form and
65
+ treats `:`/`-` as interchangeable, so colon-commands count. (The new command
66
+ surfaced this — friction → structural fix.)
@@ -0,0 +1,39 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ **Rate-limit events are now recorded durably (per-agent ResourceLedger, Phase A).**
9
+ Until now, every time the account got throttled — a circuit-breaker trip, or a
10
+ session hitting Anthropic's server-side rate limit — was counted only in
11
+ process-local memory and lost on restart, so "how many times were we throttled
12
+ today?" had no answer. This adds a small read-only SQLite ledger
13
+ (`server-data/resource-ledger.db`, same pattern as the TokenLedger) that persists
14
+ each rate-limit event, fed by a minimal new trip/recover observer on the circuit
15
+ breaker plus the existing RateLimitSentinel.
16
+
17
+ New read-only route `GET /resources/rate-limits?sinceHours=N` returns the durable
18
+ count + rate (breaker trips as the headline; session-sentinel detections counted
19
+ separately) and recent events. Default-on (event-driven, negligible cost);
20
+ `monitoring.resourceLedger.enabled: false` opts out. It never gates or changes any
21
+ behavior — the breaker observer is wrapped so a listener error can never affect
22
+ the breaker. CPU/memory tracking and a unified `/resources` snapshot are Phase B.
23
+
24
+ Spec: `docs/specs/per-agent-resource-ledger.md`.
25
+
26
+ ## What to Tell Your User
27
+
28
+ Nothing to configure. Your agent now keeps a durable record of how often it's
29
+ being rate-limited, so that history survives restarts instead of resetting to
30
+ zero. You can ask it how many times it was throttled today, or whether the
31
+ pressure is getting worse, and it can answer from real recorded numbers.
32
+
33
+ ## Summary of New Capabilities
34
+
35
+ - New read-only endpoint GET /resources/rate-limits (durable rate-limit-event
36
+ count, rate, and recent events).
37
+ - Circuit-breaker trips and session rate-limit detections are now persisted
38
+ across restarts instead of being lost.
39
+ - New config flag monitoring.resourceLedger.enabled (default on).
@@ -0,0 +1,42 @@
1
+ # A stethoscope for a node process that's running hot
2
+
3
+ ## The one-sentence version
4
+ When one of our programs is burning the CPU for no obvious reason, the usual tools can tell you *that* it's busy but not *what line of code* is doing it. This new command — `instar dev:profile-node` — presses a stethoscope to the running program and prints the exact function that's hogging the CPU.
5
+
6
+ ## The problem it solves
7
+ Picture a car idling in the driveway but somehow guzzling gas. You pop the hood with your usual gauge — it says "engine's working hard" but won't tell you *which part*. That's macOS's built-in profiler on a Node.js program: it sees the engine block (the low-level machinery) but can't read the actual code, so the real culprit stays hidden.
8
+
9
+ This bit us for real: three of our agent servers were each burning ~50–60% CPU, bouncing them never helped, and four different tools all came back "busy, but I can't tell you why."
10
+
11
+ ## The trick that worked (now a one-liner)
12
+ Node programs have a hidden superpower: send them a specific nudge (the SIGUSR1 signal) and they open a little diagnostic port on themselves. Connect to that port, record a few seconds of "what function is running right now," and it tells you — in plain code terms — exactly where the time goes.
13
+
14
+ That's how we finally caught it: **30% of the CPU was in `readFileUtf8`, inside `listSessions`** — a function re-reading every session file off the disk, over and over. Invisible to everything else; obvious once we looked through the right lens.
15
+
16
+ `instar dev:profile-node` bakes that whole trick into one command:
17
+ - Point it at a process (or let it auto-find the hottest one).
18
+ - It nudges the process, records ~5 seconds, and prints the hottest functions with their percentages.
19
+ - The biggest non-idle line is your culprit.
20
+
21
+ ## What it looks like
22
+ ```
23
+ Hottest JS frames (self-time) for pid 12673:
24
+ 87.6% listOnTimeout node:internal/timers:546
25
+ 9.9% (program)
26
+ 2.5% (garbage collector)
27
+ ```
28
+ (That's from a test process running a busy loop inside a timer — and it correctly fingered the timer callback.)
29
+
30
+ ## The safety notes
31
+ 1. The only thing it changes is opening that diagnostic port — and only on **localhost** (your own machine), only until the process next restarts. The command tells you it did this.
32
+ 2. It **samples** the process — it never kills it, restarts it, or edits anything.
33
+ 3. It's a developer tool, run on demand — nothing in the running system uses it.
34
+
35
+ ## Why it matters
36
+ The hard part of fixing a mystery slowdown isn't the fix — it's *finding* the cause. This turns a clever, hard-won debugging trick into a reusable one-liner, so the next time any node process runs hot, the answer is one command away instead of an afternoon of dead ends. (Same spirit as our `dev:ci-failures` tool: when a workaround saves the day, make it permanent.)
37
+
38
+ ---
39
+
40
+ **Rendered (verified HTTP 200):** https://echo.dawn-tunnel.dev/view/e5433071-bf6f-4cf0-b97a-5e16d9f7b160?sig=2c015aaeedc96ebb20d57b326f44d88eaa93dd2bcba0b4e8347a33d23f4d2b4b
41
+
42
+ *See also: the full success-story + meta-insights writeup for this run.*
@@ -0,0 +1,85 @@
1
+ # Side-Effects Review — `instar dev:profile-node`
2
+
3
+ **Version / slug:** `dev-profile-node-tool`
4
+ **Date:** `2026-06-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (dev-only CLI; read-only sampling; no server/runtime surface)`
7
+
8
+ ## Summary of the change
9
+
10
+ A new dev CLI command that CPU-profiles a running node process via SIGUSR1 + the
11
+ node inspector + a CDP CPU profile, and prints the hottest JS frames. New file
12
+ `src/commands/devProfileNode.ts` + a cli.ts registration + a manifest entry +
13
+ awareness lines. No server, route, config, or runtime-behavior change.
14
+
15
+ ## Decision-point inventory
16
+
17
+ 1. **pid resolution** — explicit `[pid]` digits win; otherwise auto-pick the
18
+ hottest `node` process (`ps -Aceo pcpu,pid,comm`). No node found → exit 1.
19
+ 2. **Inspector discovery** — probe ports 9229–9235 (node's default + increments
20
+ when the default is taken); first target with a `webSocketDebuggerUrl` wins;
21
+ none → exit 1 with a clear message.
22
+ 3. **Profile aggregation** — `aggregateHotFrames` sums per-node `hitCount` by call
23
+ frame, normalizes the path to `dist/…`/`src/…`, +1 to the 0-based line, and
24
+ returns top-N by self-time. Idle/native frames are kept on purpose (seeing
25
+ "48% idle, 30% readFileUtf8" is the signal).
26
+ 4. **Exit codes** — 0 on a successful print (even "no samples"); 1 only on an
27
+ operational failure (no node, signal failed, no inspector, capture threw).
28
+
29
+ ## 1. The one real side effect — SIGUSR1 opens the inspector
30
+
31
+ SIGUSR1 tells node to start its inspector on `127.0.0.1` (localhost-only) for the
32
+ life of the process. Implications, all bounded:
33
+ - **Localhost-only** — the inspector binds 127.0.0.1, not a public interface; it is
34
+ not remotely reachable.
35
+ - **Transient** — it closes when the process restarts; it does not persist.
36
+ - **Disclosed** — the command prints that it left the inspector open on the pid.
37
+ - **No state mutation** — profiling samples the process; it does not change its
38
+ behavior, memory, or files.
39
+
40
+ This is the same standard procedure node documents for live debugging; it is the
41
+ *only* way to symbolicate JS frames on a running process. The benefit (pinning a
42
+ hot loop that no other tool can see) outweighs a transient localhost debug port on
43
+ a dev box. Operators who object can simply not run the command.
44
+
45
+ ## 2. Over/under-block
46
+
47
+ Not applicable — it gates nothing and changes no behavior. A wrong pid or a
48
+ non-node target fails cleanly with exit 1; it never acts on the target beyond
49
+ SIGUSR1 + reading the profile.
50
+
51
+ ## 3. Blast radius
52
+
53
+ Zero runtime surface. The command runs only when a contributor/agent invokes it.
54
+ No server code path imports it; it is a leaf CLI command behind a dynamic import
55
+ (like `dev:ci-failures`). The `ws` dependency is already present (`^8.19.0`).
56
+
57
+ ## 4. Migration parity
58
+
59
+ CLAUDE.md template (`generateClaudeMd`) + `site/.../cli.md` + the builtin-manifest
60
+ (`cli:dev-profile-node`) are updated in lockstep so the awareness + discoverability
61
+ checks stay green. No config/hook/skill migration — it is a pure CLI addition
62
+ (same shape as `dev:ci-failures`, which needed none).
63
+
64
+ ## 5. Testability
65
+
66
+ `aggregateHotFrames` and `findInspectorTarget` are pure/dep-injected; the
67
+ SIGUSR1 / inspector-http / ws-CDP boundary is behind `ProfileNodeDeps` so the
68
+ orchestration is unit-tested without a real process. A live end-to-end smoke
69
+ (profiling a spawned busy node) confirmed the real path.
70
+
71
+ ## 6. What it does NOT do
72
+
73
+ - Does not kill, restart, or modify the target (only SIGUSR1 + sample).
74
+ - Does not run as part of the server or any job — invoked on demand only.
75
+ - Does not expose a remote/public debug port (inspector is localhost).
76
+
77
+ ## 7. Collateral fix — docs-coverage matcher
78
+
79
+ Adding the command surfaced a false-negative in `scripts/docs-coverage.mjs`: it
80
+ matched a command by its camelCase file name, so colon-commands (`dev:*`) never
81
+ matched their documented form and read as undocumented (the floor was sitting
82
+ right at the limit, so the new command tipped it). The matcher now also tries a
83
+ kebab form and normalizes `:`↔`-`. Audit-only script (the weekly docs-coverage
84
+ report); no runtime/behavior effect. It re-classifies the three `dev:*` commands
85
+ as documented, which is correct (they ARE in cli.md + the CLAUDE.md template).
@@ -0,0 +1,101 @@
1
+ # Side-Effects Review — Per-agent ResourceLedger, Phase A (durable rate-limit events)
2
+
3
+ **Version / slug:** `resource-ledger-phase-a`
4
+ **Date:** `2026-06-03`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Phase A of `docs/specs/per-agent-resource-ledger.md`: a durable, read-only store
11
+ for rate-limit events, so breaker trips + session-sentinel detections survive
12
+ restarts (today they're process-local and vanish). New:
13
+ - `src/monitoring/ResourceLedger.ts` — SQLite store (`rate_limit_events`),
14
+ TokenLedger pattern (NativeModuleHealer/WAL/registerSqliteHandle), swallow-on-write.
15
+ - `src/monitoring/ResourceLedgerPoller.ts` — event-driven; subscribes to the
16
+ breaker observer (+ optional RateLimitSentinel) and writes one row per emission.
17
+ - `src/core/LlmCircuitBreaker.ts` — added a minimal `onTrip`/`onRecover` observer
18
+ (emit in `onRateLimited` and on open→closed `onResolved`).
19
+ - `src/server/AgentServer.ts` — construct ledger (own try/catch) + start poller
20
+ (breaker = `getLlmCircuitBreaker()`); inject into route ctx; stop on shutdown.
21
+ - `src/server/routes.ts` — `GET /resources/rate-limits` (503 on null ledger).
22
+ - `src/config/ConfigDefaults.ts` — `monitoring.resourceLedger.enabled` (default on).
23
+ - `src/scaffold/templates.ts` — CLAUDE.md Capabilities entry (Agent Awareness).
24
+
25
+ ## Decision-point inventory
26
+ - `LlmCircuitBreaker.onRateLimited / onResolved` — **modify (additive emit only)** —
27
+ fire observer callbacks; no change to gating/state logic.
28
+ - `ResourceLedger` writes, route, poller, config default, template — **add**.
29
+
30
+ ## 1. Over-block
31
+ No block/allow surface — not applicable (read-only observability).
32
+
33
+ ## 2. Under-block
34
+ No block/allow surface. (Phase A captures breaker trips + sentinel detections; it
35
+ does not claim to capture every provider 429 — the breaker trip is the canonical
36
+ account-level signal, sentinel detections are counted separately by `source`.)
37
+
38
+ ## 3. Level-of-abstraction fit
39
+ Correct. The ledger is a recorder, not an authority; it FEEDS the read-only
40
+ `/resources` surface and never gates. The breaker observer is a pure side-channel
41
+ on a component that DOES hold gating authority — see §4.
42
+
43
+ > CI-fix addendum (full-suite caught two regressions the local subset missed,
44
+ > both no-behavior-change): (a) the new `/resources` route prefix was unclassified
45
+ > in `CapabilityIndex` — added a read-only `resourceLedger` capability entry
46
+ > (enabled only when `ctx.resourceLedger` exists); (b) `ResourceLedger.ts`
47
+ > registers a SQLite handle so it was added to the `SqliteRegistry-wiring` test's
48
+ > `LONG_LIVED_STORES` allowlist. No new surface; purely discoverability/test wiring.
49
+
50
+ ## 4. Signal vs authority compliance
51
+ **Required reference:** docs/signal-vs-authority.md
52
+ - [x] No — this change has no block/allow surface of its own.
53
+
54
+ The one sensitive point: `LlmCircuitBreaker` holds real blocking authority (it
55
+ gates ALL LLM work). I added an *observer* to it, NOT logic that affects gating.
56
+ Safety properties, all tested (`tests/unit/llm-circuit-breaker-observers.test.ts`):
57
+ - `emitTrip`/`emitRecover` iterate listeners inside a per-listener `try/catch`, so
58
+ a throwing observer **cannot** propagate into the breaker — verified by a test
59
+ that registers a throwing listener and asserts `onRateLimited` doesn't throw and
60
+ `tripCount` is still correct.
61
+ - The emit happens AFTER all state mutation (last line of `onRateLimited`; end of
62
+ `onResolved`), so an observer can never see/affect an intermediate state.
63
+ - The 30 existing breaker tests still pass unchanged — no behavioral drift.
64
+
65
+ ## 5. Interactions
66
+ - **Shadowing/double-fire:** none. The observer is additive; the breaker's
67
+ acquire/onRateLimited/onResolved behavior is byte-identical to before.
68
+ - **Races:** the poller's per-source `seq` + the ledger's `(source,ts,seq)` id +
69
+ `INSERT OR IGNORE` make writes idempotent; a replay can't double-count.
70
+ - **Lifecycle:** ledger constructed in its OWN try/catch in AgentServer (cascade
71
+ isolation — a failure can't 503 TokenLedger/FeatureMetricsLedger); poller
72
+ `stop()` unsubscribes the breaker observer + the ledger closes on shutdown.
73
+ - **Feedback loops:** none — observe-only.
74
+
75
+ ## 6. External surfaces
76
+ - New route `GET /resources/rate-limits` (additive; Bearer-auth; read-only).
77
+ - Persistent state: a new `server-data/resource-ledger.db` per agent. No schema
78
+ change to existing DBs. Naturally bounded (~hundreds of events/day) — no
79
+ retention needed for `rate_limit_events` (the high-frequency `resource_samples`
80
+ table that WILL need retention is Phase B, not in this PR).
81
+ - Config: `monitoring.resourceLedger.enabled` default true (ships ON — event-driven,
82
+ negligible cost; `false` leaves the ledger null and the route 503s).
83
+
84
+ ## 7. Rollback cost
85
+ Pure additive code + a new DB file. Revert and ship a patch; the orphaned
86
+ `resource-ledger.db` is harmless (nothing else reads it). No migration of existing
87
+ state, no agent-state repair, no user-visible regression.
88
+
89
+ ## Conclusion
90
+ Complete, working Phase A with all three test tiers green (unit store+emitter+poller
91
+ incl. a real-breaker→real-ledger wiring test; integration route 200/503; e2e
92
+ "feature is alive" 200 on the real AgentServer init path). The only authority-
93
+ adjacent edit (the breaker observer) is provably unable to affect gating. CPU/mem
94
+ sampling + retention + the unified `/resources` snapshot + dashboard are Phase B
95
+ (specced, not in this PR). Clear to ship.
96
+
97
+ ## Evidence pointers
98
+ - `tsc --noEmit` clean; vitest green: `ResourceLedger.test.ts` (6),
99
+ `ResourceLedgerPoller.test.ts` (4), `llm-circuit-breaker-observers.test.ts` (4),
100
+ `llm-circuit-breaker-wait.test.ts` (30, unchanged),
101
+ `resources-rate-limits-routes.test.ts` (2), `resources-rate-limits-lifecycle.test.ts` (3).