restore: overlay live envelope carriers when the blob is unreadable - #867
Conversation
F5 after a durable turn was restoring a stale completed local because parse-fail GET dropped envelope turnStatus/turnRunId. Thread those carriers through cloudGetFromEnvelopeMeta, merge them in snapshotAfterRepoGet, and cold-attach / Send-remap. Empty sessions without a live envelope still 404-mint. Drops the matching int it.fails (attach, C15 remap, stream-drop F5).
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Opened from implement-plan for #863 (not merged). Verified in agent workspace: Next: adversarial-review on this PR, then an explicit merge request. Do not squash. |
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #867
Verdict: CONCERNS
Repo: btipling/invincible
Scope: main ← plan/f5-envelope-wins · 7 files · envelope-wins restore (live carriers on unreadable blob)
Lenses run: L1, L6, L8 (skip: L2 no new trust boundary / secrets; L3 HarnessHost comment-only, no DOM chat; L4 no CI/wasm; L5 no new polls/caps; L7 no origin-host bind; L9 no UI/palette)
AGENTS.md read: yes (docs/feature-divide.md skimmed — host still shell-only)
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Major | L1 | HarnessHost.onGetMiss only returns 'adopted' when snapshotAfterRepoGet changes the snapshot. Same-tab F5 during a live turn already has running+turnRunId in localStorage (onTurnStarted → patchSession/persist). Overlay is identity → onGetMiss returns void → bootCloudSession onUrlUpdate(null) (lib/sessionBoot.ts · bootCloudSession error arm). Docs (docs/session-model.md) and #861 lock say ?s= stays pinned. |
1. Start a durable turn (host folds running + wr_* locally). 2. Worker persist overwrites the blob with { deltas } (unreadable). 3. F5. 4. GET is error + same carriers. 5. Address bar drops ?s=; bootCloudSession result is kind: 'local'. Attach still happens via the post-boot kickColdAttach, so the turn reconnects, but the pin is gone. Copy/share/reload-from-URL identity is lost until something re-pins. |
Defender: “no-op same-ref is the plan lock so onGetMiss does not re-hydrate.” That is right for activateSession. It is wrong to couple pin to “snapshot mutated.” #861: a successful restore must not be followed by onUrlUpdate(null). Local already matching the live envelope is a successful restore of this id. |
high |
| Minor | L6 | HTTP getEnvelope blob 401 / !ok is not source-locked to cloudGetFromEnvelopeMeta. The new source-scan only asserts the missing-pointer block. The existing HTTP 401 test (lib/sessionRepository.test.ts “a 401 from the Blob object host…”) has no live meta and only checks action === 'error' + repo.enabled. |
Restore the pre-PR 401 branch (return { action: 'error', status, message } with no carriers). Source-scan still green (helper remains on the JSON path). HTTP 401 test still green. Stale-completed local + live envelope + expired signed URL — the realistic short-TTL Blob miss — would not overlay and would not attach. |
Defender: helper unit test with blobJson: null is what 401 calls today. True for current code. The ratchet does not cover the branch this PR actually rewired. Plan text said “no other bare error after env.meta”; the scan does not enforce that. |
high |
Residual risk
Stale Redis running after the worker died still cold-attaches (attach 404 / empty-EOF must stay non-spinning — covered for stream-drop). Envelope cancelling is not live by lock, so a missing pointer during cancel still 404-mints. Carrier merge does not copy cwd/persona/skills; those stay stale local until SSE. Parse-fail ring stays turn-1-only until phase 2 blob schema. True C15 double-POST 409 is untouched.
Merge guidance
- CONCERNS: do not merge until the Major is fixed (keep
?s=when GET is a live envelope, even if overlay is identity). Minor 401/live HTTP ratchet should land in the same patch. - Do not squash.
What was not attacked
Live DO runner, prod Gateway, Vercel Blob TTL, Playwright, next start, Wasm/protocol, worker { deltas } schema (phase 2), quota/TURN2_USER/LIVE_ASSISTANT it.fails (phases 2–4). Int driver never calls HTTP getEnvelope (accepted plan lock: helper + source-scan).
Adversarial-review on #867: same-tab F5 during a live turn already has running+turnRunId locally, so snapshotAfterRepoGet is identity and onGetMiss skipped — bootCloudSession then cleared ?s=. restoreOnGetMiss pins on live identity without rehydrating. HTTP live Blob-401 / missing pointer now asserted; int driver uses cloudGetFromEnvelopeMeta.
|
Follow-up on the adversarial review (CONCERNS):
Commit:
|
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #867
Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: main ← plan/f5-envelope-wins · 9 files · envelope-wins restore (live carriers on unreadable blob) + pin-on-identity follow-up 56a6eaa
Lenses run: L1, L6, L8 (skip: L2 no new trust boundary / secrets; L3 host restore only, no DOM chat; L4 no CI/wasm; L5 no new polls/caps; L7 no origin-host bind; L9 no palette)
AGENTS.md read: yes (docs/feature-divide.md — host still shell-only)
Prior CONCERNS on 2de0f5e (?s= cleared on live identity; Blob 401 unratcheted) are addressed on 56a6eaa. Re-attacked HEAD.
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Minor | L1 | HarnessHost.onGetMiss pin does not setActiveSessionId. Ok-path identity does (onAdopt · restored === local → setActiveSessionId(id)). Error-path identity (the new pin) only returns 'adopted' (URL) and leaves React activeSessionId null. Catalog effect then setSessionCatalog(entries, null) → Wasm currentIndex() == null, no rail highlight, current session not forced to the top (lib/sessionSummaryLabel.ts · buildSessionCatalogEntries). |
1. Start a durable turn (local already running+wr_*). 2. Worker blob is { deltas }. 3. F5. 4. restoreOnGetMiss → pin. 5. ?s= stays; attach still kicks. 6. Session rail paints with no current row. |
Defender: “sessionRef is already that id; onSwitchSession no-ops same id; attach does not need the rail.” True for attach/?s=. False for the rail: ok-path identity exists specifically to mark current without rehydrate. Pin is that path on error GET and omits the setter. refreshSessions only replaces the list, not current. |
high |
| Minor | L6 | Host source-scan (lib/turnAttach.test.ts) locks skip / adopt / return 'adopted' but not that 'adopted' is returned on pin. Policy tests cover restoreOnGetMiss + a simulated onGetMiss in bootCloudSession, not the HarnessHost fallthrough. |
Move return 'adopted' inside the adopt branch (natural “cleanup”). Scan still green (toContain("return 'adopted'")). Same-tab F5 live identity regresses to onUrlUpdate(null) — the Major this follow-up fixed. |
Defender: “unit tests of restoreOnGetMiss + boot pin test are the contract.” They are the policy. The host can still drop pin→'adopted' without touching those tests. This is the same class of ratchet hole the prior review called on the 401 branch. |
high |
| Nit | L8 | snapshotAfterCloudGet JSDoc still says return 'adopted' when restore changed the snapshot (lib/sessionBoot.ts). That policy moved to restoreOnGetMiss (identity live → pin). |
A later reader “fixes” host onGetMiss to match the stale comment and re-breaks the pin. |
Comment-only; restoreOnGetMiss doc is correct. |
high |
Residual risk
Stale Redis running after the worker died still cold-attaches (stream-drop empty-EOF must stay non-spinning — covered). Envelope cancelling is not live, so a missing pointer during cancel still 404-mints. Carrier merge does not copy cwd/persona/skills; those stay stale local until SSE. Parse-fail ring stays turn-1-only until phase 2 blob schema. Blob 401/!ok now maps through the helper as status: 0 + Invalid transcript body. (host does not display GET status). Send-before-boot-GET still POSTs (C15 409) — #849 reconnect-without-F5. True C15 double-POST 409 is untouched.
Merge guidance
- PASS WITH NOTES: safe to merge from this attack; nits/minors optional. Prefer landing the rail-current + host source-scan lock in the same PR so the pin fix cannot silently regress.
- Do not squash.
What was not attacked
Live DO runner, prod Gateway, Vercel Blob TTL, Playwright, next start, Wasm/protocol, worker { deltas } schema (phase 2), quota/TURN2_USER/LIVE_ASSISTANT it.fails (phases 2–4). Int driver does not call HTTP getEnvelope (plan lock: helper + source-scan).
Adversarial-review on #867 (PASS WITH NOTES): same-tab F5 identity left activeSessionId null so the rail had no current. Pin now setActiveSessionId like ok-path identity. Source-scan locks pin → setActiveSessionId → return 'adopted'. Stale snapshotAfterCloudGet JSDoc no longer says pin only when the snapshot mutates.
|
Follow-up on the adversarial review (PASS WITH NOTES):
Commit:
|
Summary
Envelope-wins restore (plan #863, parent #862).
F5 after a live durable turn was restoring a stale
completedlocal becausegetEnvelopeparse-fail (worker{ deltas }blob) dropped envelopeturnStatus/turnRunId. Cold-attach and Send→GET remap never fired; the next prompt 409'd (C15).This PR overlays live envelope carriers onto the kept local transcript and threads them through the HTTP path so
onGetMissactivates +kickColdAttach. Messages stay local (worker blob schema is phase 2). Empty sessions without a live envelope still 404-mint.Locks
cloudGetFromEnvelopeMetais the post-env.metamapping (HTTP + unit tests).sanitizeTurnStatus === 'running'ANDsanitizeTurnRunIddefined.error+ carriers, nevernotfound.snapshotAfterRepoGetno-op is the same reference (soonGetMissstays a no-op).onGetMissdoes notrepo.putthe overlay.action: 'ok'.Tests
npm test).sessionBoot.test.ts29 (was 27, +2).sessionRepository.test.ts69 (was 67, +2). +4 tests.npm run test:int). Droppedit.failson attach, C15 Send remap, stream-drop F5 attach. Remainingit.fails: persist-parse,TURN2_USER,LIVE_ASSISTANT, quota (phases 2–4).npm run typecheckgreen.npm run buildN/A (no Wasm change; needs harness token).Docs
docs/session-model.mdreload: envelope liveness vs transcript LWW (timeless, no issue numbers).Fixes #863
Refs #862
Refs #859