instar 1.3.110 → 1.3.112

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 (32) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +20 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/StopGateDb.d.ts +1 -1
  5. package/dist/core/StopGateDb.d.ts.map +1 -1
  6. package/dist/core/StopGateDb.js.map +1 -1
  7. package/dist/core/UnjustifiedStopGate.d.ts +23 -1
  8. package/dist/core/UnjustifiedStopGate.d.ts.map +1 -1
  9. package/dist/core/UnjustifiedStopGate.js +54 -3
  10. package/dist/core/UnjustifiedStopGate.js.map +1 -1
  11. package/dist/core/types.d.ts +7 -0
  12. package/dist/core/types.d.ts.map +1 -1
  13. package/dist/core/types.js.map +1 -1
  14. package/dist/monitoring/SessionWatchdog.d.ts +27 -4
  15. package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
  16. package/dist/monitoring/SessionWatchdog.js +52 -30
  17. package/dist/monitoring/SessionWatchdog.js.map +1 -1
  18. package/dist/monitoring/rateLimitDetection.d.ts +51 -3
  19. package/dist/monitoring/rateLimitDetection.d.ts.map +1 -1
  20. package/dist/monitoring/rateLimitDetection.js +61 -4
  21. package/dist/monitoring/rateLimitDetection.js.map +1 -1
  22. package/dist/server/routes.d.ts.map +1 -1
  23. package/dist/server/routes.js +24 -9
  24. package/dist/server/routes.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/data/builtin-manifest.json +46 -46
  27. package/upgrades/1.3.111.md +129 -0
  28. package/upgrades/1.3.112.md +54 -0
  29. package/upgrades/NEXT.eli16.md +69 -60
  30. package/upgrades/side-effects/real-world-state-fixture-framework.md +137 -0
  31. package/upgrades/side-effects/stopgate-cli-circuit-breaker.md +62 -0
  32. package/upgrades/side-effects/throttle-settled-detection.md +70 -0
@@ -2,78 +2,87 @@
2
2
 
3
3
  ## What this change is
4
4
 
5
- In JavaScript/TypeScript, you can write `try { ... } catch {}` — a block
6
- that says "do this, and if anything goes wrong, throw the error away
7
- and continue silently." It's three characters of body and zero
8
- explanation, and it's a really cheap shortcut that almost always means
9
- "I don't want to think about this right now."
10
-
11
- The problem: when an error gets thrown away silently, you don't find
12
- out about it from a stack trace or a log line. You find out about it
13
- when something downstream breaks, often hours later, often from a user.
14
-
15
- The worst recent example was the **PromptGate $452 incident**. There
16
- was a 5-second loop checking whether the LLM had hit its rate limit.
17
- Inside that loop, a bare `catch {}` was swallowing every actual
18
- rate-limit error. The loop kept burning credits at full speed —
19
- hundreds of times per minute, sometimes for hours — because nothing
20
- ever surfaced the problem. By the time someone noticed, we'd burned
21
- $452 we shouldn't have.
22
-
23
- This PR adds a unit-test lint that refuses any bare `catch {}` in
24
- production code (`src/**/*.ts`, excluding tests and code-generation
25
- hosts) unless it carries a comment annotation explaining WHY the
26
- silent swallow is safe.
5
+ The post-mortem this morning named five recurring bug classes. Four of
6
+ the five PRs landed today (#542, #545, #550, #551, #552) each closed
7
+ one specific incident — silent 403s, missing wiring, watch-exit
8
+ merges, bare catches. This is the fifth and final post-mortem PR, and
9
+ it closes the broader pattern that produced them:
10
+
11
+ **Tested on fresh state, not real-world state.**
12
+
13
+ That class is the one where the test suite passes because it uses
14
+ small, fresh, just-created fixtures, but the actual deployed agent has
15
+ a 202MB token-ledger.db / an externalized secret config / a
16
+ wrong-ABI binary / 8 concurrent jobs — and the boot path breaks in
17
+ production despite tests being green. Every fix-shape PR I named in
18
+ the post-mortem analysis had at least one test that PASSED on fresh
19
+ state and would have FAILED on real state.
20
+
21
+ This PR adds a new test category called `tests/real-world-state/`
22
+ specifically for that class. It sits alongside `unit/`,
23
+ `integration/`, and `e2e/` as a peer.
27
24
 
28
25
  ## What already exists
29
26
 
30
- - An older lint called `no-silent-fallbacks.test.ts` that catches a
31
- related pattern: catches that return a degraded value (`return
32
- null`, `return []`, etc.).
33
- - The `@silent-fallback-ok` comment annotation, already used in
34
- several places (`TrustRecovery.ts`, `SyncOrchestrator.ts`, etc.).
35
- - `DegradationReporter` for emitting properly-tracked degradation
36
- signals.
27
+ - The three existing test categories. None of them load real-shaped
28
+ state — they all use small fresh fixtures (`tests/fixtures/`).
29
+ - Some adjacent code that COULD be tested at real-world state (the
30
+ SecretMigrator + SecretStore have unit tests but their merge layer
31
+ was never exercised end-to-end against the externalized shape until
32
+ this PR).
37
33
 
38
34
  ## What's new
39
35
 
40
- - A new lint test `tests/unit/no-empty-catch-blocks.test.ts` that
41
- scans for truly empty `catch{}` blocks and fails on any new ones.
42
- - The seven existing bare `catch{}` sites are annotated in this same
43
- PR — five in `src/paste/PasteManager.ts` (file cleanup that's
44
- genuinely safe to silently fail) and three elsewhere. Each carries
45
- a one-line rationale.
46
- - A focused regression check on `src/core/PromptGate.ts` — the file
47
- that gave the post-mortem its poster-child incident. It can never
48
- silently regress.
49
- - The lint's ratchet baseline starts at zero. New bare `catch{}` in
50
- unannotated form fails the unit suite at commit time.
36
+ - A new directory `tests/real-world-state/`.
37
+ - A small framework helper (`_framework.ts`) with two things:
38
+ - A two-tier system. Small/fast fixtures run on every PR. Big/slow
39
+ fixtures (multi-100MB DBs, concurrency at scale) are gated on
40
+ `INSTAR_REAL_WORLD_BIG=1` env, default off, so CI cost stays
41
+ bounded.
42
+ - A `makeAgentFixture()` helper that gives each test a real-shape
43
+ on-disk agent home (`projectDir` + `.instar/`) for setting up the
44
+ scenario.
45
+ - The first scenario: `externalized-config-boot.test.ts`. Five tests
46
+ that target the #542 incident class — making sure `loadConfig()`
47
+ correctly merges the real authToken back from the secret store when
48
+ the on-disk config holds the placeholder. This is the in-process
49
+ Node side of the bug; the existing PR #542 tests cover only the
50
+ shell-script side.
51
+ - Vitest config updated to include the new directory.
51
52
 
52
53
  ## What you need to decide
53
54
 
54
- Nothing. Lint-only. No runtime change, no config, no fleet migration.
55
+ Nothing. Test-only change. No runtime code modified. CI cost negligible
56
+ (the PR-tier fixture is tiny; nightly tier is opt-in).
55
57
 
56
58
  ## How to verify it worked after deploy
57
59
 
58
- Try writing a function with a bare `catch{}` in `src/`. Stage it. The
59
- pre-commit gate's test step will fail with a clear message naming
60
- PromptGate and the post-mortem. Fix: add `// @silent-fallback-ok —
61
- <why>` above the catch, OR put a real body inside it.
60
+ In CI, the new tests will appear in the unit-shard results as
61
+ `[real-world-state:pr] externalized-config-boot — ...`. The
62
+ `[real-world-state:nightly]` blocks (when added in future PRs) will
63
+ appear as "skipped (set INSTAR_REAL_WORLD_BIG=1 to run)".
64
+
65
+ Locally: `npm test` runs the PR tier. `INSTAR_REAL_WORLD_BIG=1 npm test`
66
+ runs everything.
62
67
 
63
68
  ## Why this matters more than it might look
64
69
 
65
- This is the fifth post-mortem PR landing in roughly four hours. Each
66
- one closes a recurring bug class, not just one incident:
67
-
68
- - #542 — silent-403 on secret-externalization (config-reader didn't
69
- survive the security upgrade).
70
- - #545 — failure-learning loop wiring + migration-parity tests.
71
- - #550 — failure-learning git reads opt into the source-tree read
72
- escape hatch.
73
- - #551 — `gh pr merge` refused on red checks (the watch-exit-merge
74
- class).
75
- - THIS — bare `catch {}` refused in production paths.
76
-
77
- Each of these patterns has cost real time + real money. Closing them
78
- at the structural level (a lint, a test, a hook) is much cheaper than
79
- relying on "we'll be careful next time."
70
+ This is the framework for catching the broader class. Future PRs will
71
+ add scenarios that target the other patterns I named in the
72
+ post-mortem:
73
+
74
+ - Multi-100MB token-ledger.db boot (catches the #534 class).
75
+ - Wrong-ABI better-sqlite3 binary swap (catches #539).
76
+ - Concurrent-job restart-during-tick (catches the class behind
77
+ several silent-stop incidents).
78
+
79
+ Each future scenario follows the same `describeAtTier(...)` shape as
80
+ the first one. Adding the next one is small (~30 minutes of test
81
+ writing); designing the framework was the hard part and it's done.
82
+
83
+ This is the LAST recommended fix from the post-mortem. Six PRs in <7
84
+ hours total. The fix-rate of 19% over the last 14 days won't drop
85
+ overnight, but each of these closes a class structurally — which means
86
+ the rate drops the next time someone tries to ship one of those
87
+ patterns again, not because they remember to be careful but because
88
+ the lint or the test or the gate refuses to let them.
@@ -0,0 +1,137 @@
1
+ # Side-effects review — Real-World-State fixture framework + first scenario (post-mortem lever B)
2
+
3
+ ## What changed
4
+
5
+ A new test category `tests/real-world-state/` for scenarios that exercise
6
+ instar against state that LOOKS LIKE a real production agent — not the
7
+ small fresh-fixture state the rest of the suite uses.
8
+
9
+ - **`tests/real-world-state/_framework.ts`** — fixture-loader helpers,
10
+ tier system, agent-fixture builder.
11
+ - **`tests/real-world-state/externalized-config-boot.test.ts`** — the
12
+ first scenario (5 tests). Targets the post-mortem's #542 incident
13
+ class: `loadConfig()` MUST merge the externalized authToken back from
14
+ the secret store, or every downstream consumer that doesn't go
15
+ through `loadConfig()` silently 403s.
16
+ - **`vitest.config.ts`** — the new directory wired into the
17
+ `include` array (alongside unit / integration / e2e).
18
+
19
+ The framework introduces a **two-tier system**:
20
+
21
+ - **'pr' tier** — small fixtures (< ~5 MB, < ~30s setup). Runs on
22
+ every PR / every CI shard. The first scenario lives here.
23
+ - **'nightly' tier** — large fixtures (multi-100MB DBs, generated
24
+ JSONL volumes, environment-specific shapes like wrong-ABI binaries).
25
+ Gated on `INSTAR_REAL_WORLD_BIG=1` env. Default OFF; default behavior
26
+ is `describe.skip` with a clear "skipped (set ... to run)" message so
27
+ the coverage gap is visible, not silently absent.
28
+
29
+ Helper API:
30
+
31
+ ```ts
32
+ import { describeAtTier, makeAgentFixture } from './_framework.js';
33
+
34
+ describeAtTier('pr', 'scenario name', () => {
35
+ let fx;
36
+ afterEach(() => fx?.cleanup());
37
+ it('asserts X about Y', () => {
38
+ fx = makeAgentFixture();
39
+ // fx.projectDir + fx.stateDir are a real on-disk agent home shape
40
+ // … set up scenario, run code-under-test, assert
41
+ });
42
+ });
43
+ ```
44
+
45
+ ## Why
46
+
47
+ Per the 2026-05-29 pipeline post-mortem (PR #545), pattern #1 — "Tested
48
+ on fresh state, not real-world state" — was the **largest** of the five
49
+ named bug classes. Bugs in this class shipped in the last 2 weeks:
50
+ - #534 (TokenLedger backfill bricked boot at 202MB; fixture was tiny).
51
+ - #512 (TokenLedger schema-order died on pre-existing DB but passed on
52
+ fresh).
53
+ - #509 (heal-then-heal failed; heal-once passed).
54
+ - #501 (SleepWakeDetector false-wake under load).
55
+ - #503 (PresenceProxy redundant after ack).
56
+ - And the originator of this whole arc — #542 (silent-403 secret
57
+ externalization). The in-process Node side's `loadConfig()` wasn't
58
+ exercised against the externalized shape; only the SHELL hook/script
59
+ side was, by PR #542's tests.
60
+
61
+ Lever B is the only post-mortem lever that closes the broader pattern
62
+ rather than a specific incident shape. The other four (#542, #545,
63
+ #550, #551, #552) each closed individual classes; this one builds the
64
+ infrastructure to catch the class FAMILY going forward.
65
+
66
+ ## Risk surface
67
+
68
+ - **Test-only change.** No runtime code modified. Existing test suites
69
+ unchanged.
70
+ - **vitest.config.ts include list extended** — additive only; the three
71
+ existing entries (unit / integration / e2e) remain unchanged. CI
72
+ shards will pick up the new path automatically.
73
+ - **PR-tier fixture is tiny.** The externalized-config-boot scenario
74
+ writes a ~1KB JSON file and runs the SecretMigrator + SecretStore
75
+ paths. Test runtime: ~200ms. CI impact: negligible.
76
+ - **Nightly tier defaults OFF.** No big-fixture cost on PR runs until
77
+ someone deliberately opts in. The skip is loud (`describe.skip` with
78
+ a message), not silent — the coverage gap is visible in every CI
79
+ report.
80
+ - **No fixture generation in this PR.** The first scenario uses
81
+ in-test fixture builders (write JSON file, run SecretMigrator). When
82
+ the nightly tier ships its first scenario (multi-100MB DB → catches
83
+ #534), it will need a `scripts/build-real-world-fixtures.mjs`
84
+ generator + a git-ignored `tests/fixtures/real-world-state/` cache.
85
+ Deferred to that PR.
86
+
87
+ ## Bug surfaces eliminated
88
+
89
+ - A future regression in `mergeConfigWithSecrets` (or the call from
90
+ `Config.ts:loadConfig`) that re-introduces the placeholder-leak shape
91
+ fails the 5 new tests with clear messages. Verified destructive-
92
+ negative: disabling the merge call in `Config.ts` trips 4 of the 5
93
+ scenario tests with the exact failure modes the bug class produced.
94
+ - The full-shape sub-test asserts that telegram token, telegram chatId,
95
+ dashboard PIN, AND tunnel token all merge — not just authToken. A
96
+ future externalization that adds a new secret field without wiring
97
+ the merge fails immediately.
98
+ - The idempotency sub-test asserts that re-running pairing on an
99
+ already-externalized agent doesn't double-extract or lose state.
100
+
101
+ ## Testing
102
+
103
+ - **5 tests, all green** on the current main:
104
+ - Disk shape sanity (post-pairing layout).
105
+ - `loadConfig()` returns merged real authToken (the headline check).
106
+ - `loadConfig()` never leaks the placeholder shape (regression).
107
+ - Full-shape merge (telegram + dashboard + tunnel + auth).
108
+ - Idempotency on re-run.
109
+ - **Tier-gating verified both directions**. With `INSTAR_REAL_WORLD_BIG`
110
+ unset, `nightly`-tier blocks skip with a visible message; with it set
111
+ to `1`, they execute. Verified with a sentinel test that
112
+ intentionally fails — proves the gating respects the env.
113
+ - **Destructive-negative verified**. Disabling
114
+ `mergeConfigWithSecrets()` in `Config.ts` fails 4 of 5 scenario tests
115
+ with the failure messages the bug class produced
116
+ (`{ secret: true }` returned as authToken, telegram token leaks the
117
+ placeholder, etc.).
118
+ - **No regression in existing related tests** —
119
+ `secret-migrator.test.ts` (40), `secret-store.test.ts` (presumed
120
+ green), `config-secret-merge.test.ts` (4) all still pass.
121
+ - `tsc --noEmit` clean.
122
+
123
+ ## Follow-ups
124
+
125
+ - **First nightly-tier scenario** — multi-100MB token-ledger.db boot
126
+ test that would have caught #534. Needs a fixture generator
127
+ (`scripts/build-real-world-fixtures.mjs`) and a git-ignored cache
128
+ (`tests/fixtures/real-world-state/`). Separate PR.
129
+ - **Wrong-ABI better-sqlite3 scenario** — catches #539's class. Needs
130
+ the framework to support "environment fixtures" (specific Node + ABI
131
+ combos), which is a bigger design step.
132
+ - **Concurrency-at-scale scenarios** — multi-job + multi-session under
133
+ restart-during-tick. Bigger design step (probably needs a tmux
134
+ harness).
135
+
136
+ This PR ships the FRAMEWORK + ONE scenario as a working backstop.
137
+ Subsequent PRs add scenarios at the same incremental pace.
@@ -0,0 +1,62 @@
1
+ # Side-effects review — UnjustifiedStopGate circuit breaker
2
+
3
+ **Spec:** `docs/specs/STOPGATE-CLI-CIRCUIT-BREAKER-SPEC.md`
4
+ **Change:** a self-circuit-breaker on the stop gate's LLM path so subscription
5
+ agents (where a `claude -p` judgment call takes ~5-6s but the budget is ~2s) stop
6
+ churning doomed subprocesses and flooding /health with per-stop degradations.
7
+ **Class:** robustness fix (found by the 12h session's "load is suspect" investigation).
8
+
9
+ ## What changed
10
+
11
+ - **`src/core/UnjustifiedStopGate.ts`** — new config `breakerThreshold` (default 3),
12
+ `breakerCooldownMs` (default 5 min), injectable `now`. New `breakerOpen` failure
13
+ kind + `breakerState()` telemetry. `evaluate()` short-circuits (no LLM call) while
14
+ the breaker is open; counts provider failures (timeout/llmUnavailable); resets on
15
+ a reachable provider.
16
+ - **`src/core/StopGateDb.ts`** — `InvalidKind` gains `breakerOpen`.
17
+ - **`src/server/routes.ts`** — `/internal/stop-gate/evaluate` fail-opens on
18
+ `breakerOpen` (as for any failure) but skips the failure DB record + the
19
+ DegradationReport for it (deliberate short-circuit, not an evaluation failure).
20
+
21
+ ## Blast radius
22
+
23
+ - **Stop decisions:** unchanged. `breakerOpen` allows the stop exactly like a
24
+ `timeout` fail-open — the breaker can never make a stop decision worse, only the
25
+ fail-open faster + quieter. (This is what keeps the change low-risk despite the
26
+ stop gate being safety-adjacent.)
27
+ - **Fast-provider (API-key) agents:** the breaker never opens (calls succeed) →
28
+ identical behaviour to today. Only slow/unavailable-provider agents see the
29
+ short-circuit.
30
+ - **Stop-gate analytics:** `breakerOpen` is excluded from the failure rollup (it
31
+ is not an evaluation failure), so the gate's failure-rate metric is no longer
32
+ skewed by the very churn the breaker stops.
33
+ - **/health:** stops flooding — at most `breakerThreshold` degradations per
34
+ cooldown window instead of one per stop event.
35
+ - **Config / schema / migration:** none. Safe in-code defaults; no PostUpdateMigrator
36
+ entry. Operators may pass `breakerThreshold`/`breakerCooldownMs`/`clientTimeoutMs`
37
+ via the gate constructor if they want to tune it.
38
+
39
+ ## What could break (and why it doesn't)
40
+
41
+ - **Breaker stuck open forever?** No — it half-opens after the cooldown and a
42
+ single reachable response closes it. A genuinely recovered provider self-heals.
43
+ - **A transient blip opens it?** Only after `breakerThreshold` (3) CONSECUTIVE
44
+ failures; a single success in between resets the counter.
45
+ - **Lost audit of fail-opens?** Real evaluation failures (timeout, malformed, etc.)
46
+ are still recorded + reported exactly as before; only the deliberate breaker
47
+ short-circuit is excluded.
48
+
49
+ ## Security
50
+
51
+ No new external input / network / auth / fs surface. Pure in-memory breaker state.
52
+
53
+ ## Rollback
54
+
55
+ Revert the commit, or set `breakerThreshold: 0` (disables the breaker). No state.
56
+
57
+ ## Tests
58
+
59
+ `tests/unit/UnjustifiedStopGate-breaker.test.ts` (+5): opens after K + stops
60
+ calling the provider; half-open retry after cooldown; reachable-provider reset;
61
+ threshold=0 disables; real-timeout counts. Existing gate/route/db suites (42) green.
62
+ `tsc` + lint clean.
@@ -0,0 +1,70 @@
1
+ # Side-Effects Review — settled-throttle detection (RateLimitSentinel actually fires)
2
+
3
+ **Version / slug:** `throttle-settled-detection`
4
+ **Date:** `2026-05-30`
5
+ **Author:** `echo`
6
+ **Driving spec:** `docs/specs/rate-limit-sentinel.md` (§Settled-throttle detection — the live fix)
7
+ **Second-pass reviewer:** `not required` (single-author behavioral fix to an already-shipped sentinel; no new external surface)
8
+
9
+ ## Summary of the change
10
+
11
+ The RateLimitSentinel (shipped PR #528, live at v1.3.108) was built, wired, and
12
+ enabled but fired **zero** times in the field — throttled sessions hung 5–10
13
+ minutes until the 15-minute silence fallback. Root cause was the detection
14
+ preconditions in `SessionWatchdog.checkRateLimited`, not the recovery machinery:
15
+ it required a session be "cleanly idle, no active child processes, at a prompt,
16
+ throttle string in the last 20 lines." Busy dev sessions never satisfy that —
17
+ they have a live background shell/MCP process, and Claude Code's input box pushes
18
+ the `API Error:` line out of the 20-line window.
19
+
20
+ The fix replaces those gates with a **settled-output signal**: throttle string in
21
+ a widened 45-line window + pane byte-identical across two consecutive watchdog
22
+ polls (an actively-working session animates its spinner/elapsed-timer every tick,
23
+ so a frozen pane is a reliable "turn ended, stuck" signal). No process-tree
24
+ inspection, no at-prompt heuristic. After a recovery cycle escalates and clears
25
+ (~30s), a still-stuck pane re-emits past the 60s cooldown → **unbounded retry**
26
+ until the throttle lifts (the "never hang forever" guarantee Justin asked for).
27
+
28
+ ## Files touched
29
+
30
+ - `src/monitoring/rateLimitDetection.ts` — `detectRateLimited` gains an optional
31
+ `captureLines` param (default 20, unchanged for existing callers); new
32
+ `RATE_LIMIT_SETTLED_CAPTURE_LINES=45`, `RATE_LIMIT_DEFAULT_SETTLE_MS=20000`,
33
+ `throttleSignature()`, and the pure clock-injected `evaluateThrottleSettle()`.
34
+ - `src/monitoring/SessionWatchdog.ts` — `checkRateLimited` rewritten to the
35
+ settled-output decision; removed the active-child-process and at-prompt gates;
36
+ added `throttleSettle` tracking map + `rateLimitSettleMs` (config-read).
37
+ - `src/core/types.ts` — `monitoring.watchdog.rateLimitSettleMs?: number`.
38
+ - `src/commands/server.ts` — wire RateLimitSentinel lifecycle events
39
+ (detected/resuming/recovered/escalated) into `logs/sentinel-events.jsonl`.
40
+ - `tests/unit/rate-limit-detection.test.ts` — +16 cases (widened-window
41
+ regression reproducing the incident; throttleSignature; evaluateThrottleSettle
42
+ both sides of every branch).
43
+ - `tests/unit/SessionWatchdog-rate-limit-settle.test.ts` — new, 8 wiring cases.
44
+ - `upgrades/NEXT.md`, `docs/specs/rate-limit-sentinel.md` (+eli16) — docs.
45
+
46
+ ## Decision-point inventory
47
+
48
+ - **Stuck-vs-working discriminator** — *modify*. Was process-tree + at-prompt +
49
+ 20-line window; is now byte-identical-pane across polls + 45-line window. Pure
50
+ function (`evaluateThrottleSettle`), no judgment, fully unit-tested.
51
+ - **Widened capture window (45 lines)** — *new, bounded*. Only the watchdog
52
+ settled path uses it; `detectRateLimited`'s default stays 20 for every other
53
+ caller (SessionManager idle path unchanged). Old-throttle-scrolled-away and
54
+ usage-limit/mid-retry exclusions still hold at 45 lines (tested).
55
+ - **Unbounded re-emit after escalation** — *behavioral, intentional*. Bounded by
56
+ the 60s watchdog cooldown + the sentinel's own dedupe; the throttle is
57
+ Anthropic's short-lived shared-capacity signal, so cycles are few in practice.
58
+ This is the load-bearing "never hang forever" property.
59
+ - **Audit-log wiring** — *additive observability*. Best-effort `appendFileSync`
60
+ to the existing sentinel-events.jsonl; annotated `@silent-fallback-ok` (never
61
+ crashes the monitor path). No new route, no new external surface.
62
+ - **`rateLimitSettleMs` config** — *additive, back-compat*. Optional, code
63
+ default 20000; configs without it are unaffected → no migration required.
64
+
65
+ ## Blast radius / rollback
66
+
67
+ Pure monitoring-path change; no API routes, no schema, no state-file format
68
+ change. The `RateLimitSentinel.enabled` master switch (existing) disables the
69
+ whole path. Reverting this commit restores the prior gates verbatim. Existing
70
+ 121 rate-limit/watchdog tests pass unchanged (event contract preserved).