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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +20 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/StopGateDb.d.ts +1 -1
- package/dist/core/StopGateDb.d.ts.map +1 -1
- package/dist/core/StopGateDb.js.map +1 -1
- package/dist/core/UnjustifiedStopGate.d.ts +23 -1
- package/dist/core/UnjustifiedStopGate.d.ts.map +1 -1
- package/dist/core/UnjustifiedStopGate.js +54 -3
- package/dist/core/UnjustifiedStopGate.js.map +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/SessionWatchdog.d.ts +27 -4
- package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
- package/dist/monitoring/SessionWatchdog.js +52 -30
- package/dist/monitoring/SessionWatchdog.js.map +1 -1
- package/dist/monitoring/rateLimitDetection.d.ts +51 -3
- package/dist/monitoring/rateLimitDetection.d.ts.map +1 -1
- package/dist/monitoring/rateLimitDetection.js +61 -4
- package/dist/monitoring/rateLimitDetection.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +24 -9
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/1.3.111.md +129 -0
- package/upgrades/1.3.112.md +54 -0
- package/upgrades/NEXT.eli16.md +69 -60
- package/upgrades/side-effects/real-world-state-fixture-framework.md +137 -0
- package/upgrades/side-effects/stopgate-cli-circuit-breaker.md +62 -0
- package/upgrades/side-effects/throttle-settled-detection.md +70 -0
package/upgrades/NEXT.eli16.md
CHANGED
|
@@ -2,78 +2,87 @@
|
|
|
2
2
|
|
|
3
3
|
## What this change is
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- A
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- The
|
|
50
|
-
|
|
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.
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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).
|