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.
- package/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/devProfileNode.d.ts +62 -0
- package/dist/commands/devProfileNode.d.ts.map +1 -0
- package/dist/commands/devProfileNode.js +179 -0
- package/dist/commands/devProfileNode.js.map +1 -0
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +8 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/LlmCircuitBreaker.d.ts +15 -0
- package/dist/core/LlmCircuitBreaker.d.ts.map +1 -1
- package/dist/core/LlmCircuitBreaker.js +37 -1
- package/dist/core/LlmCircuitBreaker.js.map +1 -1
- package/dist/monitoring/ResourceLedger.d.ts +78 -0
- package/dist/monitoring/ResourceLedger.d.ts.map +1 -0
- package/dist/monitoring/ResourceLedger.js +163 -0
- package/dist/monitoring/ResourceLedger.js.map +1 -0
- package/dist/monitoring/ResourceLedgerPoller.d.ts +64 -0
- package/dist/monitoring/ResourceLedgerPoller.d.ts.map +1 -0
- package/dist/monitoring/ResourceLedgerPoller.js +83 -0
- package/dist/monitoring/ResourceLedgerPoller.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +49 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +11 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +18 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/docs-coverage.mjs +14 -4
- package/scripts/generate-builtin-manifest.cjs +1 -0
- package/src/data/builtin-manifest.json +87 -79
- package/src/scaffold/templates.ts +7 -0
- package/upgrades/1.3.230.md +66 -0
- package/upgrades/1.3.231.md +39 -0
- package/upgrades/dev-profile-node-tool.eli16.md +42 -0
- package/upgrades/side-effects/dev-profile-node-tool.md +85 -0
- 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).
|