Skip to content
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
47e7d61
docs(plan): split wt3 roadmap into per-bug decade docs with researche…
lidge-jun Aug 2, 2026
2bb3d91
docs(plan): fold audit round-1 blockers into wt3 bug-a docs
lidge-jun Aug 2, 2026
6a9b79e
docs(plan): fold audit round-2 blockers — sol in built-in set, additi…
lidge-jun Aug 2, 2026
62bbeb5
docs(plan): fold audit round-3 — uniform seven-model count, honest pr…
lidge-jun Aug 2, 2026
14c8214
fix(providers): route Copilot Responses-only models off chat completions
lidge-jun Aug 2, 2026
202ff8a
docs(plan): wt2 root-cause delta + diff-level decade docs for the six…
lidge-jun Aug 2, 2026
123cf9e
docs(plan): fold audit-round-1 FAIL blockers into wt2 decade docs
lidge-jun Aug 2, 2026
001daef
docs(plan): fold audit-round-2 blockers into wt2 decade docs
lidge-jun Aug 2, 2026
18289dc
fix(responses): bound continuation admission, snapshot read, and spil…
lidge-jun Aug 2, 2026
e67c0df
docs(providers): Copilot Responses-only GPT-5 defaults + modelAdapter…
lidge-jun Aug 2, 2026
8c3681e
fix(responses): close the four audit blockers in continuation admission
lidge-jun Aug 2, 2026
cbd345b
docs(plan): wp-b P — concrete file map and reviewed service_tier sema…
lidge-jun Aug 2, 2026
eb44a77
fix(responses): never read non-regular snapshot targets
lidge-jun Aug 2, 2026
198a317
docs(plan): fold wp-b audit round-1 blockers
lidge-jun Aug 2, 2026
b09596e
docs(plan): fold wp-b audit round-2 residual — fail-closed scope word…
lidge-jun Aug 2, 2026
9c400f5
fix(chat): scope collector tool args per call and normalize overflow …
lidge-jun Aug 2, 2026
b42d573
fix(responses): gate service_tier by provider capability, preserve De…
lidge-jun Aug 2, 2026
1e3c050
docs(providers): supportsServiceTier + preserveResponsesReasoningCont…
lidge-jun Aug 2, 2026
30bf3af
fix(chat): close the three audit blockers in collector/bridge bounds
lidge-jun Aug 2, 2026
51cf17c
docs(plan): record 030 round-2 accepted residuals
lidge-jun Aug 2, 2026
3185610
fix(chat): close the cancel race and the last 413 in replay
lidge-jun Aug 2, 2026
9f801e3
fix(providers): keep service-tier/reasoning capabilities registry-only
lidge-jun Aug 2, 2026
c985863
fix(chat): exit the bridge step without EOF synthesis after cancel
lidge-jun Aug 2, 2026
8d31b82
docs(plan): wp4 P re-verification — cursor transport implementation d…
lidge-jun Aug 2, 2026
a0ea408
docs(plan): wp-c P — stale check narrows scope to registry map + agen…
lidge-jun Aug 2, 2026
10388e1
fix(cursor): decode Connect frames from a cursor backlog and classify…
lidge-jun Aug 2, 2026
2843eb5
docs(plan): fold wp-c audit round-1 — generated metadata + pinned tes…
lidge-jun Aug 2, 2026
0b30283
fix(providers): give Claude 4.6/4.7 their 1M context windows; authori…
lidge-jun Aug 2, 2026
e290550
fix(cursor): zero-copy frame handoff and terminal backlog cleanup
lidge-jun Aug 2, 2026
4f1f059
fix(cursor): never release an uncharged rejected chunk
lidge-jun Aug 2, 2026
956d871
fix(cursor): never charge stream data after terminal settlement
lidge-jun Aug 2, 2026
cfdad39
fix(cursor): bound the blob-ID key channel
lidge-jun Aug 2, 2026
e71bd10
fix(cursor): domain-separate blob key namespaces and count keys in sn…
lidge-jun Aug 2, 2026
93b8811
fix(cursor): classify blob key bytes with their entries
lidge-jun Aug 2, 2026
a18d1fd
test(cursor): cross the 4096-entry boundary and use the payload-only …
lidge-jun Aug 2, 2026
687ae1c
fix(antigravity): derive fixed-size replay key identities
lidge-jun Aug 2, 2026
dc71043
fix(antigravity): stream canonical escaping and drop zero-call shells
lidge-jun Aug 2, 2026
00cf454
fix(antigravity): escape lone surrogates in canonical keys (ES2019)
lidge-jun Aug 2, 2026
e448abd
fix(antigravity): make key hashing injective and prove bounded canoni…
lidge-jun Aug 2, 2026
2101d50
chore: untrack accidentally committed retired-Go file
lidge-jun Aug 2, 2026
2c2c113
docs(plan): record UTF-16 key framing as the final antigravity design
lidge-jun Aug 2, 2026
c8ee260
fix(windows): release ephemeral ACL memos and key timeouts by destina…
lidge-jun Aug 2, 2026
0ec6023
fix(antigravity): bound canonical key collection and prove walk abort
lidge-jun Aug 2, 2026
d9d2eb3
test(antigravity): make the canonical-abort assertions regression-tight
lidge-jun Aug 2, 2026
a26b379
fix(windows): scope ephemeral ACL release to temps, keep stable-path …
lidge-jun Aug 2, 2026
1c55ca8
test(windows): pin ACL release contracts directly; prompt-journal use…
lidge-jun Aug 2, 2026
d5b8863
fix(windows): close the three residual ACL lifecycle edges
lidge-jun Aug 2, 2026
f7f5b1b
fix(windows): gate the migration memo release on both residual classes
lidge-jun Aug 2, 2026
e456db5
docs(plan): wt2 campaign close-out (070)
lidge-jun Aug 2, 2026
c1098af
docs(plan): record final full-suite and privacy gates for wt2
lidge-jun Aug 2, 2026
fd72782
fix(antigravity): throttle the lazy expiry scan (CI ubuntu timeout fix)
lidge-jun Aug 2, 2026
601c36a
fix(update): compare installed previews by base version on latest cha…
lidge-jun Aug 2, 2026
17ec899
fix(cli): bound the star-prompt agent deferral (#879)
lidge-jun Aug 2, 2026
19d002b
fix(cli): reject future-dated star deferral records on the version-ma…
lidge-jun Aug 2, 2026
ab5a20c
fix(server): match browser extension CORS origins by scheme+authority
lidge-jun Aug 2, 2026
3bba8e7
fix(config): preserve symlinked destinations in atomic writes
lidge-jun Aug 2, 2026
b1f299d
fix(config): complete wt4 cherry-pick resolution — keep ephemeral ACL…
lidge-jun Aug 2, 2026
89ae572
fix(config): guard first writes beneath symlinked parent dirs
lidge-jun Aug 2, 2026
5d6bd71
fix(config): guard before directory mutation in pid/port writers and …
lidge-jun Aug 2, 2026
d83a07c
fix(windows): let post-create scheduler verification settle before ro…
lidge-jun Aug 2, 2026
22e156e
fix(doctor): report how the service actually got its Bun runtime
lidge-jun Aug 2, 2026
aeb16e1
fix(doctor): carry Bun provenance through execPath relaunches too
lidge-jun Aug 2, 2026
2ade543
fix(codex): scope the Bun provenance marker to the ensure call
lidge-jun Aug 2, 2026
77f5ae9
fix(runtime): record which binary the provenance marker describes
lidge-jun Aug 2, 2026
58bb9a6
fix(doctor): require the binary half for Bun provenance; never settle…
lidge-jun Aug 2, 2026
a468652
test(windows): pin transient-vs-invalid fixtures and the zero-delay p…
lidge-jun Aug 2, 2026
622d60a
Merge remote-tracking branch 'origin/codex/wt3-exec' into codex/wt2-z…
lidge-jun Aug 2, 2026
0f8c332
fix(responses): tri-state service_tier gate and authoritative-only [1…
lidge-jun Aug 2, 2026
155fc9c
docs(responses): align service_tier contract across types, docs, and …
lidge-jun Aug 2, 2026
6c3930f
chore(claude): drop the unused shouldMarkOneMillion import from the p…
lidge-jun Aug 2, 2026
f7d8bcd
docs(ru): drop the redundant classification phrase in the models guide
lidge-jun Aug 2, 2026
58e8718
chore: untrack the retired-Go file again (re-added by 2c2c11357's bro…
lidge-jun Aug 2, 2026
a86ee03
fix: fold the CodeRabbit review round (journal hardening, recovery TO…
lidge-jun Aug 2, 2026
16099bb
Merge remote-tracking branch 'origin/dev' into codex/wt2-zero-leak-impl
lidge-jun Aug 2, 2026
8ede130
test(integrations): build the GUI before the wire-level CSRF suite wh…
lidge-jun Aug 2, 2026
831283d
fix(kiro): never split a surrogate pair at the instruction budget
lidge-jun Aug 2, 2026
eeef7a3
fix: never split surrogate pairs at the compaction and kiro boundaries
lidge-jun Aug 2, 2026
0793edc
fix(cursor): send Grok 4.5 Fast parameters
jbaehova Aug 2, 2026
05bb74f
docs(cursor): sync Grok Fast translations
jbaehova Aug 2, 2026
ab6a1ed
docs(cursor): name Grok Fast parameter field
jbaehova Aug 2, 2026
538d755
fix(anthropic): complete AgentRouter streams that end before terminal…
Yuxin-Qiao Aug 2, 2026
ffc24bc
preserve foreground Luna turns
kimrinking-cell Aug 2, 2026
5dd965a
fix DeepSeek Responses over Codex WebSocket
atian8179 Aug 2, 2026
cbb198d
fix: preserve routed model identity in Codex
0xWinner98 Aug 1, 2026
9469422
fix: align routed model identity across adapters
0xWinner98 Aug 2, 2026
9794e24
fix: fold the wp6 reviewer round (bounded upstream JSON body, backwar…
lidge-jun Aug 2, 2026
d8b707e
fix: fold the wp6 re-audit round (house bounded-body primitive, exact…
lidge-jun Aug 2, 2026
daf7069
fix: count malformed surrogate pairs as separate U+FFFD replacements …
lidge-jun Aug 2, 2026
a8e0fe0
test(memory): give the #848 provenance test headroom on slow CI runners
lidge-jun Aug 2, 2026
0bccc8a
test(responses-state): probe a genuinely dead pid for the symlinked-s…
lidge-jun Aug 2, 2026
80c49cf
test(storage): budget the two cleanup-mode tests on loaded Windows ru…
lidge-jun Aug 2, 2026
6ec6ffc
test(app-server): tolerate a thrown CIM deadline in the live enumerat…
lidge-jun Aug 2, 2026
a4ff77c
Merge remote-tracking branch 'origin/dev' into codex/wt2-zero-leak-impl
lidge-jun Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions devlog/_plan/260802_wt2_zero_leak_bounds/001_root_cause_delta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 001 — Root-cause delta: what wave 1 already landed vs what remains

Date: 2026-08-02. Basis: three read-only explorer passes over `codex/wt2-zero-leak-impl` @ `478354ee8` (= dev tip), plus `gh pr diff` for #840-#847. This doc SUPERSEDES the assumptions in `000_plan.md` where they conflict.

## Wave-1 landings (already on dev, do NOT re-implement)

| Commit | What landed |
|--------|-------------|
| `77243d932` | app-owned retained-state byte budget framework (`src/lib/app-owned-memory.ts`: 256 MiB eviction target, 512 MiB worst-case pinned ceiling, category-ordered eviction, re-snapshot-after-evict honesty) |
| `d1408b92f` | Responses continuation hard cap + durable spill (`src/responses/state.ts`, `src/responses/spill-store.ts`) |
| `034d320b8` | byte caps for blob, replay, vision, image caches |
| `a61607894` | translator turn budgets: 2 MiB/tool call, 32 MiB/turn, 32 MiB SSE logical event (`src/lib/translator-budget.ts`) |
| `17faddd24` | benchmark gap closure |

Framework note (`src/lib/app-owned-memory.ts:43`): the budget is an eviction target, not an admission boundary — it runs AFTER an owner allocated, cannot prevent a single oversized allocation, sees only owner-reported bytes, and cannot evict pinned state. Per-store admission caps remain necessary. That is the frame for every delta below.

## True remaining deltas (the actual work of this unit)

### #841 — Responses state: admission boundary, not rejection (refinement, NOT wave-1 redo)

Current: `setResidentEntry()` (`src/responses/state.ts:243`) fully materializes + measures the candidate, inserts it as resident, THEN prunes — an oversized candidate is fully allocated and older UNRELATED residents may be demoted first. Remaining gaps:

1. Oversized candidate (`sizeBytes > 64 MiB cap`) should go DIRECTLY to durable spill and install only its stub — never resident, never demoting unrelated chains. Keep spill (replay availability), do not adopt PR #841's plain rejection.
2. Snapshot input not size-bounded before `readFileSync`/`JSON.parse` (`src/responses/state.ts:453`) — an externally oversized `responses-state.json` is parsed whole.
3. Spill replay materialization unbounded: `readResponseSpill` (`src/responses/spill-store.ts:307`) reads+parses with no replay ceiling and does not charge `storedResponseBytes` (`src/responses/state.ts:666`).
4. `writeBoundedSnapshot` (`src/responses/state.ts:485`) uses JS string length, not UTF-8 bytes, for the 2 MiB/24 MiB limits.

### #847 — tool-argument bounds: two narrow gaps (mostly landed)

Current: translator budget (2 MiB/call, 32 MiB/turn, 32 MiB SSE) covers OpenAI Chat (`src/adapters/openai-chat.ts:801`), streaming+batch bridge (`src/bridge.ts:851`, `:1468`), Responses-to-Chat streaming (`src/chat/outbound.ts:168-198`). Remaining gaps:

1. Non-stream collector `collectChatCompletion()` charges tool args to generic `retained_collectors` scope (`src/chat/outbound.ts:621`, `:700`) — one call can consume nearly the full 32 MiB turn budget instead of the 2 MiB per-call limit. Fix: per-call ownership by stable index/call ID.
2. `translatorBudget` is OPTIONAL in the bridge option type (`src/bridge.ts:136`) — a future caller omitting it gets an unbounded append helper. Make it mandatory (all production callers pass one today).
3. Overflow contract inconsistency: Chat outbound maps translator overflow to 413 `invalid_request_error`; adapter/bridge use 502 `upstream_error`. Normalize to 502.

Decisions (recorded, not silent): keep the shared SSE record ceiling at 32 MiB (PR #847's 4 MiB could reject legitimate large compatible-provider records); keep typed `translation_buffer_limit` overflow (no `arguments.done`, no completed item, no clean Chat DONE — already the bridge behavior).

### #844 — Cursor Connect frames: incremental remainder + partial-EOF (refinement)

Current: declared-length validation at header arrival exists (`src/adapters/cursor/framing.ts:171`), 32 MiB declared / 16 MiB effective caps exist (`src/lib/translator-budget.ts:4`), 1,024-frame flow control exists. Remaining gaps:

1. Concat-first pending handling (`src/adapters/cursor/live-transport.ts:894`, `concatBytes()` at :906-918): every chunk is concatenated with the ENTIRE pending remainder. Fix: complete only the missing header/payload portion incrementally; carry at most one bounded incomplete frame.
2. Partial-EOF (`live-transport.ts:949`): complete frame(s) + trailing incomplete frame settles SUCCESSFULLY and silently discards the remainder. Fix: fail the turn with typed `frame_incomplete` on non-expected EOF when pending bytes remain (after accounting for queued async frame work; expected client-tool cancellation must NOT error).

Decision: do NOT adopt PR #844's flat 32 MiB effective inbound — current 16 MiB effective preserves the copy-overlap budget inside the 32 MiB transport budget.

### #845 — Cursor blob store: payload bounded, KEYS UNBOUNDED (audit round 1 refuted the NOOP)

`src/adapters/cursor/native-exec.ts` already has: 16 MiB/entry, 64 MiB aggregate, 4,096 entries, 15-min TTL, request-scope pinning with seal/rollback (`:351`), typed atomic admission failures (`entry_too_large`, `pinned_saturation`, `request_pinned_conflict`, `:219`), protobuf error acknowledgement for rejected `setBlobArgs` (`:551`), per-key hydration release (`:537`), app-owned-memory integration.

**Audit blocker (Critical, accepted):** the caps account only `blobData`. A remote `blobId` of arbitrary length becomes an unbounded, UNCOUNTED `Map` key (`:219`, `:551`) — a near-16 MiB raw ID becomes a ~32 MiB hex-expanded key (`key(blobId)` at `:331`, before admission), retainable across 4,096 entries (~128 GiB worst case of pure key strings). The NOOP verdict was wrong. Fix in `045`: validate/digest IDs from raw bytes before hex expansion, with a SEPARATE key-bytes counter so the 64 MiB payload cap is unchanged. Accepted residual (unchanged): remote `setBlobArgs` after scope sealing is TTL-protected only; PR has the same limitation.

### #843 — Antigravity replay: fixed-size identities (refinement)

Current: caps exist (10,240 sessions, 256 calls/session, 2 MiB/session, 64 MiB global counted, 64 KiB signature — `src/adapters/google-antigravity-replay.ts:29`), 1h TTL + centralized sweep. Remaining gaps:

1. Outer key retains raw `model`/`sessionId` (`replayKey`, `:57`) and inner key raw function name + canonical args (`functionCallKey`, `:61`/`:70`) — key bytes are NOT counted in `replayBytes`. **Audit sharpening (Critical, accepted):** this means the advertised 64 MiB global / 2 MiB per-session caps do NOT cap total retained memory at all — keys are outside them. Fix: SHA-256 fixed-size identities with LENGTH-PREFIXED UTF-8 components (NUL separators are collision-ambiguous: `("a\0b","c")` vs `("a","b\0c")` serialize identically), preserving native `touchedAtMs`, exact deletion accounting, retained-store snapshot, sweeper, and shared-budget call. Worst-case pinned-cap test must cover key storage, not payload constants alone.
2. Transient canonical JSON allocation before admission checks — large arguments produce an unbounded temporary string. Fix: bounded recursive/streaming canonicalization (a `JSON.stringify` size precheck would itself allocate the temporary we are avoiding). Red-green seam: `snapshot.bytes` already excludes outer keys, so the fixed-key regression needs a test-only key-derivation seam, not a bytes assertion.

Decision: keep native TTL-refresh-on-duplicate-observation (PR #843 does not refresh; changing it alters TTL semantics for no leak benefit).

### #840 — Windows ACL memos: timeout release + destination keying (refinement)

Current: success memos already released after rename/confirmed removal (`src/config.ts:120`, `:137`); async writer keys timeouts by destination (`src/config.ts:187`); residual-file retention is fail-closed (`tests/config.test.ts:1536`). Remaining gaps:

1. Sync `atomicWriteFile` hardens the unique temp WITHOUT a destination memo key (`src/config.ts:107-109`) — a timeout retains `required:<unique-temp>` forever even after cleanup removes the temp. Fix: pass `timeoutMemoKey: destination` (matches async).
2. `forgetHardenedSecretPath` (`src/lib/windows-secret-acl.ts:171`) clears only the success set, not timeout state. Fix: ephemeral release clearing `hardenedPaths` + `timedOutPaths` in BOTH namespaces (`required:`/`optional:`), invoked ONLY after proven absence (successful rename, successful unlink, ENOENT, or explicit `existsSync === false`) at `src/config.ts:125`, `:152`, `:211`, `:238` + management-token/tray temp writers.

Guardrails: never clear the stable DESTINATION timeout memo (intentional anti-restall state); retain memos when a residual temp remains on disk; preserve required/optional namespace isolation. Store F is not registered with the framework — registration is out of scope (memos become self-releasing instead).

## Revised landing order

020 (#841) → 030 (#847) → 040 (#844) → 045 (#845 NOOP record) → 050 (#843) → 060 (#840). Order is dependency-free across subsystems; sequence keeps the prep doc's order minus the NOOP. wt3 coordination stands: #847 edits stay in the translator-budget/collector paths, not `service_tier` injection sites.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 020 — Fix #841: Responses state admission boundary (direct-spill oversized, bounded snapshot read, bounded replay)

Depends on: 001 root-cause delta. NOT a redo of wave-1 (`d1408b92f` hard cap + spill already landed).

## P re-verification note (2026-08-02, wp2 cycle — supersedes details below where they conflict)

- `byteCap()` (64 MiB default, `MAX_STORED_RESPONSE_BYTES`) is the TOTAL resident-map cap, not per-entry. "Oversized candidate" = `candidate.sizeBytes > byteCap()` — it can never fit as resident even alone.
- `expected?.kind === "spill"` already direct-spills atomically via `replaceSpillEntryAtomically` (`state.ts:212`) with deferred old-generation unlink — the new branch REUSES it for oversized candidates; scenario 8b's machinery exists.
- Single constant decision: `MAX_RESPONSE_SPILL_PAYLOAD_BYTES = 256 MiB` in `spill-store.ts`, used BOTH as direct-spill admission ceiling and replay read ceiling (candidates above it are tombstoned at admission with `admissionCounters.oversizedDrops` — retaining an unreadable spill would be write-only waste). 256 MiB bounds the replay transient under the 512 MiB `APP_OWNED_WORST_CASE_PINNED_BYTES` ceiling. This replaces the earlier "recommend the same 64 MiB" line, which contradicted direct-spill preservation.
- `readResponseSpill` already verifies `stat.size === ref.payloadBytes`; the new `too_large` reason is checked on `ref.payloadBytes` BEFORE any read. Wire-safe: `core.ts:1188` maps every replay failure to the same 400 `previous_response_not_found`; the internal reason union gains `spill_too_large`.
- Snapshot file ceiling: `SNAPSHOT_FILE_MAX_BYTES = 32 MiB` (> 24 MiB write bound), checked via `statSync` before parse; refusal recorded in a test-visible counter.
- Test hooks available: `setResponseStateByteCapForTests`, `setSpillIoForTest` (write-failure injection), `noteStubSwapForTest`. A payload-ceiling override for tests is added alongside the new constant.

## File map

- MODIFY `src/responses/state.ts`
- `setResidentEntry()` (~:243): before `replaceMapEntry()`, when `candidate.sizeBytes > byteCap()`, write the candidate DIRECTLY to spill and atomically install only its measured stub. Never insert the oversized candidate as resident; never demote unrelated residents to make room for it.
- `ensureLoaded()` (~:453): bound the snapshot file read — `statSync` first, refuse (or truncate-refuse with typed error + quarantine) a `responses-state.json` above an explicit ceiling (recommend 32 MiB, above the 24 MiB write bound). Enforce direct-spill/reject for oversized resident rows BEFORE map admission in `loadSnapshotEntry()` (~:301).
- `writeBoundedSnapshot()` (~:485): use `Buffer.byteLength(value, "utf8")` instead of `.length` for the 2 MiB/24 MiB limits.
- MODIFY `src/responses/spill-store.ts`
- `readResponseSpill()` (~:307): reject `payloadBytes` above an explicit replay ceiling BEFORE read/parse (recommend the same 64 MiB as the store cap), typed error `spill_payload_too_large`; the continuation then fails as a structured `previous_response_not_found`-class miss rather than an unbounded allocation.
- MODIFY `tests/responses-state.test.ts` — new regressions (below).

Scope OUT: changing TTL (1h), count cap (1,000), stub/tombstone semantics, Windows ACL/fsync behavior, `previous_response_not_found` wire shape.

## Acceptance + activation scenarios

1. Oversized candidate (sizeBytes > cap) with two unrelated small residents present: candidate lands as spill stub only; both unrelated residents remain resident (not demoted). Activation: test asserting map contents + stub presence + spill file exists; replay of the stub still works.
2. At-cap-minus-epsilon candidate: admitted resident as today. Activation: boundary test.
3. Externally oversized snapshot file (> ceiling): load refuses with typed error, process starts with empty state, no giant parse allocation. Activation: fixture writing a >ceiling `responses-state.json` in a temp config dir.
4. Oversized spill payload on disk: replay rejects typed before read; no unbounded allocation; error surfaces as structured continuation miss. Activation: fixture spill file over the replay ceiling.
5. Multibyte snapshot: entries whose UTF-8 bytes exceed 2 MiB but whose `.length` does not are now correctly excluded from snapshot output. Activation: multibyte fixture + byte-length assertion.
6. Red-green: each new test fails on the pre-fix tree (verify at least #1, #3, #4 red first).
7. Direct-spill WRITE FAILURE for an oversized candidate: bounded tombstone installed, candidate never resident, unrelated residents untouched. Activation: fault-injected spill write (chmod/readonly dir or mock) asserting tombstone + resident map unchanged (audit round 1 gap).
8. Same-ID replacement: an oversized candidate replacing an existing resident ID installs the stub and unlinks the old generation in the existing deferred order. Activation: replace-then-crash-ordering fixture (audit round 1 gap).

## Typed observability (audit round 1 blocker — current contracts swallow the new outcomes)

- Snapshot load errors are swallowed today (`state.ts:453` catch-all) and spill reads expose only `missing | corrupt` (`spill-store.ts:48`). The new outcomes need INTERNAL reason seams, not wire changes: extend the spill-read reason union with `too_large` (still surfaced upstream as the existing structured continuation miss), and record snapshot-load refusal as a typed metric/log (`snapshot_oversized`) plus a test-visible reason. No response-shape changes.

## Regression risks (watch in C)

- Continuation misses if direct-spill breaks same-ID crash consistency or deferred old-generation unlink ordering.
- Stubs must stay NON-evictable in `responseContinuationRetainedStoreSnapshot()` (counted as pinned) or the shared budget spins.
- v1/v2 snapshot compatibility; provider continuation metadata replay.
Loading
Loading