instar 1.3.110 → 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.
@@ -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).