instar 1.3.109 → 1.3.111
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/SessionManager.d.ts +11 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +18 -15
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/frameworkSessionLaunch.d.ts +20 -0
- package/dist/core/frameworkSessionLaunch.d.ts.map +1 -1
- package/dist/core/frameworkSessionLaunch.js +27 -0
- package/dist/core/frameworkSessionLaunch.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/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +6 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +4 -4
- package/upgrades/1.3.110.md +55 -0
- package/upgrades/1.3.111.md +129 -0
- package/upgrades/NEXT.eli16.md +69 -60
- package/upgrades/side-effects/loop-session-no-mcp.md +65 -0
- package/upgrades/side-effects/real-world-state-fixture-framework.md +137 -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,65 @@
|
|
|
1
|
+
# Side-effects review — Mentor loop session spawns with no project MCP
|
|
2
|
+
|
|
3
|
+
**Spec:** `docs/specs/LOOP-SESSION-NO-MCP-SPEC.md`
|
|
4
|
+
**Change:** the mentor autonomous-fix loop spawns its per-cycle Opus session with
|
|
5
|
+
no project MCP servers, so a headless spawn doesn't hang on auth-required remote
|
|
6
|
+
MCP boot. Found by the live dogfood of the loop (#554).
|
|
7
|
+
**Class:** mentor-loop reliability fix (the feature ships dark).
|
|
8
|
+
|
|
9
|
+
## What changed
|
|
10
|
+
|
|
11
|
+
- **`src/core/frameworkSessionLaunch.ts`** (+1 export) — `claudeHeadlessExtraFlags(opts)`:
|
|
12
|
+
the single pure builder for the claude-code headless flags spliced before `-p`
|
|
13
|
+
(the existing `--allowedTools` scope + the new `--strict-mcp-config --mcp-config
|
|
14
|
+
'{"mcpServers":{}}'` no-MCP spawn). Returns `[]` for non-claude / no options.
|
|
15
|
+
- **`src/core/SessionManager.ts`** — new opt-in `spawnSession({ disableProjectMcp })`;
|
|
16
|
+
the two inline flag-splice blocks (`--allowedTools` + the new MCP-disable) are
|
|
17
|
+
replaced by one call to `claudeHeadlessExtraFlags`.
|
|
18
|
+
- **`src/server/AgentServer.ts`** — `spawnLoopSession` passes `disableProjectMcp: true`.
|
|
19
|
+
|
|
20
|
+
## Blast radius
|
|
21
|
+
|
|
22
|
+
- **Other spawn callers:** unchanged. `disableProjectMcp` is opt-in (default
|
|
23
|
+
unset). Interactive sessions, mentee-handle spawns, Stage-A, and jobs leave it
|
|
24
|
+
unset ⇒ byte-identical argv to today (project MCP loads as before).
|
|
25
|
+
- **`--allowedTools` behaviour:** preserved exactly — same flags, now built by the
|
|
26
|
+
shared helper instead of an inline splice. Covered by the existing
|
|
27
|
+
framework-launch unit suite (56 tests still green) + the new helper test.
|
|
28
|
+
- **Codex spawns:** unaffected — the helper returns `[]` for non-claude
|
|
29
|
+
frameworks (Codex MCP wiring is separate, via sandbox modes).
|
|
30
|
+
- **MCP for the loop session:** the loop drives the mentee over Telegram (bash +
|
|
31
|
+
relay / a2a HTTP) and ships fixes with built-in tools — it used no MCP, so
|
|
32
|
+
removing it has no functional loss, only removes the boot hazard.
|
|
33
|
+
|
|
34
|
+
## What could break (and why it doesn't)
|
|
35
|
+
|
|
36
|
+
- **Loop needs an MCP tool?** It doesn't — it uses built-in Bash/Edit/Read/Write
|
|
37
|
+
+ git/gh via Bash, and Telegram via the relay script. If a future loop variant
|
|
38
|
+
needs a specific MCP, pass a curated `--mcp-config` rather than the project one.
|
|
39
|
+
- **`--mcp-config` inline-string support?** Verified against the installed claude
|
|
40
|
+
CLI: `--mcp-config '{"mcpServers":{}}'` is accepted and starts zero servers
|
|
41
|
+
(a headless `-p` run that otherwise stalled booted in ~9s).
|
|
42
|
+
- **Splice position?** The helper's flags are spliced before `-p` exactly like the
|
|
43
|
+
prior `--allowedTools` code; argv structure is unchanged otherwise.
|
|
44
|
+
|
|
45
|
+
## Security
|
|
46
|
+
|
|
47
|
+
No new external input / network / auth / fs surface. The change REMOVES MCP
|
|
48
|
+
servers from one spawn path (strictly fewer external connections). The empty MCP
|
|
49
|
+
config is a constant inline string.
|
|
50
|
+
|
|
51
|
+
## Migration parity
|
|
52
|
+
|
|
53
|
+
Server-internal session-spawn code — every agent gets it by running the new
|
|
54
|
+
build. No config, schema, hook, or CLAUDE.md change. No PostUpdateMigrator entry.
|
|
55
|
+
|
|
56
|
+
## Rollback
|
|
57
|
+
|
|
58
|
+
Revert the commit, or stop passing `disableProjectMcp`. No persisted state.
|
|
59
|
+
|
|
60
|
+
## Tests
|
|
61
|
+
|
|
62
|
+
`tests/unit/claude-headless-extra-flags.test.ts` (+6): every helper branch incl.
|
|
63
|
+
the empty-MCP-config validity. `tests/e2e/mentor-onboarding-lifecycle.test.ts`
|
|
64
|
+
(+1 assertion): the production loop spawn passes `disableProjectMcp: true`.
|
|
65
|
+
Framework-launch regression suite (56 tests) green. `tsc` + `npm run lint` clean.
|
|
@@ -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,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).
|