Skip to content

restore: overlay live envelope carriers when the blob is unreadable - #867

Merged
btipling merged 3 commits into
mainfrom
plan/f5-envelope-wins
Aug 28, 2026
Merged

restore: overlay live envelope carriers when the blob is unreadable#867
btipling merged 3 commits into
mainfrom
plan/f5-envelope-wins

Conversation

@btipling

Copy link
Copy Markdown
Owner

Summary

Envelope-wins restore (plan #863, parent #862).

F5 after a live durable turn was restoring a stale completed local because getEnvelope parse-fail (worker { deltas } blob) dropped envelope turnStatus / 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 onGetMiss activates + kickColdAttach. Messages stay local (worker blob schema is phase 2). Empty sessions without a live envelope still 404-mint.

Locks

  • cloudGetFromEnvelopeMeta is the post-env.meta mapping (HTTP + unit tests).
  • Live = sanitizeTurnStatus === 'running' AND sanitizeTurnRunId defined.
  • Live envelope + missing/failed blob → error + carriers, never notfound.
  • snapshotAfterRepoGet no-op is the same reference (so onGetMiss stays a no-op).
  • onGetMiss does not repo.put the overlay.
  • Parse fail never becomes action: 'ok'.
  • C15 409 for a true double-POST is unchanged.

Tests

  • Default suite: 2475 passed (npm test). sessionBoot.test.ts 29 (was 27, +2). sessionRepository.test.ts 69 (was 67, +2). +4 tests.
  • Int: 11 passed (npm run test:int). Dropped it.fails on attach, C15 Send remap, stream-drop F5 attach. Remaining it.fails: persist-parse, TURN2_USER, LIVE_ASSISTANT, quota (phases 2–4).
  • npm run typecheck green.
  • npm run build N/A (no Wasm change; needs harness token).

Docs

  • docs/session-model.md reload: envelope liveness vs transcript LWW (timeless, no issue numbers).

Fixes #863
Refs #862
Refs #859

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).
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
invincible Ignored Ignored Aug 28, 2026 2:30am

Request Review

Copy link
Copy Markdown
Owner Author

Opened from implement-plan for #863 (not merged).

Verified in agent workspace: npm run typecheck · npm test 2475 passed · npm run test:int 11 passed (3 former it.fails now green: attach, C15 remap, stream-drop F5). Remaining it.fails belong to phases 2–4.

Next: adversarial-review on this PR, then an explicit merge request. Do not squash.

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #867

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/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 (onTurnStartedpatchSession/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.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial review (CONCERNS):

  • Major (L1): restoreOnGetMiss now returns pin when overlay is identity but the GET is live, so onGetMiss still returns 'adopted' and bootCloudSession keeps ?s=. Rehydrate (adopt) only when carriers actually change. Same-tab F5 during a live turn no longer strips the pin.
  • Minor (L6): HTTP tests for live envelope + Blob 401 and live/not-live missing pointer; source-scan locks the 401 || !ok branch to cloudGetFromEnvelopeMeta. Int driver now calls that helper (no third mapping).

Commit: 56a6eaarestore: pin ?s= when live overlay is identity.

npm run typecheck green. Changed-file units green (165). Full npm test 2472 passed here; 13 harnessChat.wasm-int rows fail-closed on missing wasm in this workspace (no HARNESS_ARTIFACT_TOKEN) — same 13 as on a clone without the artifact, not this diff. GitHub Actions still has the fetch path.

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #867

Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: mainplan/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 === localsetActiveSessionId(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. restoreOnGetMisspin. 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.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial review (PASS WITH NOTES):

  • Minor (L1): onGetMiss pin now setActiveSessionId(id) so the session rail marks current after same-tab F5 identity (same as ok-path identity, without rehydrate / repo.put).
  • Minor (L6): Host source-scan locks pinsetActiveSessionId(id)return 'adopted' inside the onGetMiss block, so moving 'adopted' into the adopt branch fails the ratchet.
  • Nit (L8): snapshotAfterCloudGet JSDoc no longer says pin only when the snapshot mutates.

Commit: e27d9c0restore: pin path sets session rail current.

npm run typecheck green. lib/turnAttach.test.ts + lib/sessionBoot.test.ts 93 passed.

@btipling
btipling merged commit 1dc1014 into main Aug 28, 2026
3 checks passed
@btipling
btipling deleted the plan/f5-envelope-wins branch August 28, 2026 02:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plan: F5 envelope-wins restore — phase 1 — attach after stale local

1 participant