Skip to content

persist: write SessionSnapshot on transcriptPointer - #868

Open
btipling wants to merge 10 commits into
mainfrom
plan/f5-readable-blob
Open

persist: write SessionSnapshot on transcriptPointer#868
btipling wants to merge 10 commits into
mainfrom
plan/f5-readable-blob

Conversation

@btipling

Copy link
Copy Markdown
Owner

Summary

Readable worker blob (plan #864, parent #862).

persistStep was JSON.stringify({ deltas }), so parseCloudSessionSnapshot returned null and F5 after a finished durable turn could not LWW-adopt the worker transcript.

This PR writes a SessionSnapshot shape (id, messages, extra deltas key) from the bounded checkpoint. The persist seam stamps updatedAt with the B8 overlay clock once (same number as the envelope upsert). persistStep never calls Date.now(). Checkpoint tool maps to session tool_run so tool rows are not dropped. Empty-envelope B7 pointer mint uses updatedAt: 0 so that overlay clock is strictly newer (append-only blob cannot be rewritten after B7).

Locks

  • persistStep stringifies { id, messages, deltas } only — no updatedAt, no Date.now().
  • Missing/blank scope.sessionId{ ok: false, code: 'invalid_scope' } before seam.persist.
  • Seam: OverlayClock once → stamp JSON → B7 → B8 with the same number.
  • 'tool''tool_run'; other unknown checkpoint roles dropped.
  • Parser unchanged: { deltas }-only still null. No second schema. No blob backfill.
  • Helper is runtime-pure (no sessionRepository import).

Tests

  • checkpointToSnapshotMessages: +4 (happy, tooltool_run, drop unknown, empty).
  • persist seam: +1 (blob updatedAt === envelope clock; clock invoked once).
  • persistStep: +1 (missing sessionId fail-closed).
  • persist-parse int: dropped it.fails; both rows green.
  • Remaining it.fails: TURN2_USER, LIVE_ASSISTANT (phase 4), quota (phase 3).
  • F5 attach / C15 rows stay green (phase 1).

Gates: npm run typecheck green. vitest run --changed 36 files / 377 passed. vitest run --project int 11 passed. npm run build N/A (no Wasm change).

Expected default-suite delta: +6 tests vs main (4 helper + 1 clock + 1 invalid_scope).

Docs

  • docs/session-model.md Reload/restore: transcriptPointer body is a session snapshot JSON; legacy { deltas }-only fails parse (timeless).

Fixes #864
Refs #862
Refs #859

Worker persist was stringify({ deltas }), so boot parse-fail left F5 on
stale local. persistStep now writes { id, messages, deltas }; the seam
stamps the B8 overlay clock once onto that JSON. Checkpoint `tool` maps
to `tool_run`. Drop persist-parse it.fails.
@btipling btipling self-assigned this Aug 28, 2026
@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 8:17am

Request Review

Copy link
Copy Markdown
Owner Author

Opened from plan #864 (not merged).

  • typecheck green
  • vitest run --changed: 36 files / 377 passed
  • vitest run --project int: 11 passed (persist-parse no longer it.fails)
  • +6 unit tests; remaining int it.fails: TURN2_USER / LIVE_ASSISTANT / quota

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

@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 #868

Verdict: BLOCK
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer schema
Lenses run: L1, L6, L8 (skip: L2 no secret/runner/client-bundle surface; L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L5 one persist stringify, not a poll/alloc storm; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Blocker L1 persistStep writes this-run fold.checkpoint as the Blob SessionSnapshot (lib/workflows/persistStep.ts · checkpointToSnapshotMessages; lib/workflows/turnLoop.ts · derivePersistFold / messages = [{ role:'user', content: input.userMessage }]). The seam then stamps the B8 overlay clock (max(now, stored+1)) onto that body (lib/agent/turnPersistSeam.ts). shouldAdoptServer adopts any strictly-newer server whole-array (lib/sessionRepository.ts). Complete durable turn 1, then turn 2. Worker persist 2 body is [user2, assistant2] only, updatedAt strictly newer than the host envelope/local. F5 → bootCloudSnapshot parses (this PR’s DoD) → action: 'ok'shouldAdoptServer true → canvas drops turn 1. Same-device F5 after a finished second turn, or any empty other-device local, hydrates the suffix as the full session. Main today fail-closes parse ({ deltas }) and keeps local; this PR turns that clobber into a successful LWW replace. Plan #864 / parent #862 want a parseable pointer and to “hydrate that checkpoint, not turn-1-only local.” That is the missing turn 2 content, not a license to delete turn 1. Empty-checkpoint “keeps local via shouldAdoptServer” is false: newer updatedAt wins before the empty-dialogue clause. TURN2_USER remaining it.fails is phase 4 cadence, not a merge of prior blob messages. persist-parse only exercises persistTurn1 (2 rows). Defender “local clock is usually newer” fails the PR’s own lock: overlay clock is designed to beat stored/local completed. high
Major L6 No test that a second persistStep / seam persist retains turn-1 messages (or suffix-merges a host-shaped prior). New rows prove parse, 'tool''tool_run', one-clock stamp, missing sessionId. Merge this PR. vitest run --changed and int/persist-parse stay green while F5 after two completed durable turns wipes history. CI cannot catch the Blocker. “Phase 4 int rows cover turn 2” — those rows are still it.fails and do not assert LWW of a completed two-turn blob. DI/cost is clean (memory stores). high
Minor L8 docs/session-model.md Reload/restore now says the pointer body is “the same shape a full-record GET returns.” After this PR it is this-run checkpoint + deltas, not the accumulated host transcript. Operator/agent reads living docs, treats worker blob as full GET, ships F5 against it, loses earlier turns. Extra-keys sentence is accurate. The “full-record GET” claim is not. high

Residual risk

Even with a prior-blob merge, a first persist over a leftover { deltas } object still starts from this run only (no backfill — accepted). B7/B8 remain two envelope upserts; a concurrent host LWW on the B8 overlay still leaves the pointer advanced (pre-existing partial-commit). Oversize merged JSON can still write_failed at the 8 MiB object ceiling.

Merge guidance

  • BLOCK: do not merge until the Blocker is fixed in createTurnPersistSeam (read current transcriptPointer body; suffix-merge this-run messages onto a parseable prior; never LWW-publish a this-run-only array under a newer overlay clock when a readable prior exists). Add a two-persist / host-prior unit that still parses and keeps the turn-1 user line. Living-docs sentence must match the accumulated body.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob, real F5 against Production Redis, Wasm hydrate paint, host putEnvelopeOnce race on a live session.

This-run checkpoint plus a newer overlay clock LWW-replaced turn-1
history on F5. The persist seam reads the current transcriptPointer,
suffix-merges this-run messages onto a parseable prior, then stamps
the same B8 clock. Leftover { deltas } objects are not merged.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial BLOCK: suffix-merge is in c043e9b.

  • createTurnPersistSeam reads the current bound transcriptPointer, suffix-merges this-run messages onto a parseable prior (role+text overlap), then stamps the same B8 overlay clock.
  • Leftover { deltas } / foreign pointer still starts from this run only (no backfill).
  • Tests: two-persist keeps turn-1 user; host-shaped prior is not duplicated; unreadable prior stays this-run; int persist-parse second persist asserts the four-row body.

--request-changes is not available on an own-PR; the review body above still stands as BLOCK until this commit is considered.

@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 #868

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer SessionSnapshot + suffix-merge
Lenses run: L1, L2, L5, L6, L8 (skip: L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

c043e9b closed the previous BLOCK (this-run-only array under a newer overlay clock wiping turn 1 on the two-persist happy path). Remaining findings are on the merge predicate and the bound-pointer miss path.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 mergeCheckpointOntoPrior (lib/agent/messageCheckpoint.ts) overlaps only on exact role+text. Host tool_run rows are encodeToolRun payloads (lib/harnessChat.ts livePaintToolRun); worker checkpoint tooltool_run rows are raw result strings (turnLoop.ts checkpointRow). Those texts never match, so overlap collapses to 0 and this-run is appended. Durable tool turn. Host persistTurn / onSessionPatch wins the pointer (turn-end PUT or mid-turn change_dir / usage after appendMessage bumps updatedAt). Worker persist reads that host snapshot, suffix-merges [user, raw_tool, assistant] onto […, user, encoded_tool, …], publishes the duplicate under the B8 overlay clock. F5 LWW-adopts doubled user + mixed tool cards. Same hole for a trailing partial assistant whose text ≠ the final checkpoint. Tests only cover exact role+text (turn-2 user/assistant, host prior that already equals this turn). Text-only turns with identical assistant strings still overlap. Plan #864 did not mention merge; c043e9b added it to stop clobber, not to survive the host encoder. Host PUT often 409s against the running envelope clock — not on the turn-end path once local updatedAt is Date.now(). high
Major L1 Bound transcriptPointer + blobStore.readnull is treated like leftover { deltas }: priorMessages = null → this-run-only body → overlay clock LWW-clobbers history (createTurnPersistSeam prior-read try/catch). VercelBlobTranscriptStore.read maps fetch/timeout/non-2xx to null (does not throw). Complete turn 1 (readable snapshot at a bound pointer). Turn 2 persist: signed GET of that pointer fails transiently → raw === null → merge skipped → B7 writes [user2, assistant2] as a new object and B8 stamps a newer clock. F5 adopts turn 2 only. Distinct from leftover { deltas } (that path still has a JSON body). Unbound seed pointers (t_old_ptr_0000) already skip the read. Missing blob vs timeout is indistinguishable in the Vercel store — fail-closed persist (leave the pointer) is still better than a successful LWW replace. { deltas } remains this-run-only because snapshotMessagesFromUnknown rejects it after a successful read. high
Minor L6 No unit/seam row for (a) host-encoded tool_run prior, (b) bound pointer + read() === null must {ok:false} and not advance. Two-persist / host-prior rows stay green while both Majors ship. Merge this PR. vitest run --changed and persist-parse int remain green. Helper overlap tests exist for equal text. DI/cost is clean (memory stores). high

Residual risk

Leftover { deltas } first persist is still this-run-only (locked, no backfill). B7/B8 remain two envelope upserts; concurrent host LWW on B8 still leaves the pointer advanced. Oversize merged JSON still write_failed at 8 MiB. Assistant text that is a proper prefix of the final checkpoint (mid-stream host PUT) can still duplicate if overlap requires exact assistant text — accepted unless a later phase persists mid-turn (phase 4). Envelope reserved meta is overlaid on boot (overlayEnvelopeMeta); dropping meta from the worker blob is not a carrier wipe.

Merge guidance

  • CONCERNS: do not merge until both Majors are fixed. mergeCheckpointOntoPrior must treat tool_run as role-equal (ignore payload text) so a host-encoded prior does not duplicate; a bound pointer whose read() is null (or whose JSON throws) must {ok:false, code:'write_failed'} and must not stamp a this-run-only snapshot. Add those two tests. Leftover { deltas } JSON must stay this-run-only.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob signed GET against Production, real F5 vs Production Redis, Wasm hydrate paint, host putEnvelopeOnce vs B8 overlay on a live session.

Host encodeToolRun payloads never equal checkpoint result text, so
suffix-merge appended a duplicate this-run. tool_run now matches by
role. A bound pointer whose object is missing or not JSON fails
persist instead of LWW-publishing this-run-only.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial CONCERNS (b0bf473):

  • mergeCheckpointOntoPrior treats tool_run as role-equal (host encodeToolRun payload vs worker raw result) so a host-shaped prior does not duplicate this-run.
  • Bound transcriptPointer whose object is missing or not JSON → {ok:false, code:'write_failed'}, pointer unchanged. Leftover { deltas } JSON still starts from this run only.
  • Prior-read gate runs before B6 so a miss does not orphan a checkpoint blob.
  • Tests: encoded tool_run host prior; bound missing blob; bound non-JSON body.

npm run typecheck green. vitest run --changed: 33 files / 374 passed. persist-parse int: 3 passed.

@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 #868

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer SessionSnapshot + suffix-merge
Lenses run: L1, L2, L5, L6, L8 (skip: L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

b0bf473 closed the previous CONCERNS pair (role-equal tool_run; bound pointer miss/non-JSON fail-closed). Remaining finding is the overlap shape: 1:1 row alignment still cannot absorb the host this-run window.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 mergeCheckpointOntoPrior (lib/agent/messageCheckpoint.ts) still aligns equal-length prefixes. tool_run is role-equal 1:1. Host livePaintToolRun (lib/harnessChat.ts) grows one session row for a consecutive tool streak (updateLastMessage / same openToolRunId); worker checkpointRow (lib/workflows/turnLoop.ts) emits one row per tool. Host also inserts skill_attached (pushSkillRow) — and can insert system/error — inside that window. Any extra incoming tool_run or host-only role zeros overlap. Durable turn with ≥2 tools. Host persistTurn / envelope carrier wins transcriptPointer first (terminal persist after stream; putEnvelopeOnce writes {id,updatedAt,messages}). Prior = […, user2, ONE encodeToolRun card, assistant2]. Incoming = [user2, raw_t1, raw_t2, assistant2]. k=4 fails (lengths / assistant vs tool); k=3 fails (t2 vs assistant); k=2 fails (user vs tool card once assistant is last). overlap=0 → this-run appended under the B8 overlay clock. F5 shouldAdoptServer adopts doubled user + mixed encoded/raw tool cards + doubled assistant. Same wipe-to-duplicate with /skill (skill_attached between user and tool). N=1 encoded-tool tests (host-encoded tool_run prior is not duplicated, helper does not duplicate a host prior whose tool_run payload differs) stay green — one host card vs one checkpoint tool is 1:1. Worker-to-worker two-persist (no host coalesce) also stays green because prior tools stay N raw rows. Envelope carrier off → host never writes this pointer and the hole is latent; this PR’s F5 path is that pointer, and turn-end host PUT is the common writer before the worker step. high
Minor L6 No unit/seam row for (a) host one tool_run vs checkpoint N≥2 tools, (b) skill_attached (or system/error) interrupting the this-run suffix. The N=1 encoded-tool tests do not catch the Major. Merge this PR. vitest run --changed and persist-parse int remain green. DI/cost is clean (memory stores, no extra PGlite). high

Residual risk

Leftover { deltas } first persist is still this-run-only (locked, no backfill). Bound miss/non-JSON stays fail-closed. B7/B8 remain two envelope upserts; concurrent host LWW on B8 still leaves the pointer advanced. Oversize merged JSON still write_failed at 8 MiB. A host mid-stream assistant prefix whose text ≠ the final checkpoint still fails exact role+text on that row (phase 4 mid-turn persist). Envelope reserved meta is overlaid on boot (overlayEnvelopeMeta); dropping meta from the worker blob is not a carrier wipe.

Merge guidance

  • CONCERNS: do not merge until the Major is fixed. Suffix overlap must treat a single prior tool_run as covering a run of incoming tool_run rows when the next prior row is not tool_run (host live-paint card), keep 1:1 when both sides have consecutive tool_run (worker-to-worker), and skip host-only skill_attached / system / error in the prior suffix so they cannot zero overlap. Add tests for coalesced N≥2 and a skill_attached interrupt. Leftover { deltas } JSON must stay this-run-only; bound miss/non-JSON must stay {ok:false}.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob signed GET against Production, real F5 vs Production Redis, Wasm hydrate paint, host putEnvelopeOnce vs B8 overlay on a live session, NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE off (rollforward PUT) as a substitute pointer writer.

Host live-paint grows one encodeToolRun card for N tools, and may
insert skill_attached in the this-run window. 1:1 suffix overlap
zeroed and duplicated this-run under the overlay clock. One prior
tool_run now covers an incoming tool run when the next prior row is
not tool_run; worker consecutive tool_run stays 1:1. Host-only
skill_attached/system/error rows are skipped so they cannot zero
overlap.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial CONCERNS (19a27ea):

  • Suffix overlap is no longer 1:1 length. A single prior tool_run covers a run of incoming tool_run rows when the next prior row is not tool_run (host livePaintToolRun card). Consecutive prior tool_run rows stay 1:1 (worker-to-worker).
  • Host-only skill_attached / system / error in the prior suffix are skipped so they cannot zero overlap.
  • Tests: host one card vs N≥2 checkpoint tools (helper + seam); mid-turn card appends only the trailing assistant; skill_attached interrupt; worker-to-worker N tools stay 1:1.

npm run typecheck green. messageCheckpoint + turnPersistSeam 49 passed. persist-parse int: 3 passed.

@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 #868

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer SessionSnapshot + suffix-merge
Lenses run: L1, L6, L8 (skip: L2 no secret/runner/client-bundle surface; L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L5 one persist stringify / bounded merge, not a poll/alloc storm; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

19a27ea closed the previous CONCERNS (host one tool_run card covering N checkpoint tools; host-only skill_attached / system / error skip). Remaining hole is the assistant shape: checkpoint is per-round, host session is not.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 mergeCheckpointOntoPrior / flexMatchExact (lib/agent/messageCheckpoint.ts) still assume this-run assistant rows line up with the host snapshot. They do not. derivePersistFold / checkpointRow (lib/workflows/turnLoop.ts) emit one assistant per model round (delta.text is always a string, including '' for tool-only rounds — lib/agent/generateOneRound.ts). Host runHarnessTurn (lib/harnessChat.ts) does not appendMessage(..., 'assistant') on text_delta; live assistant is bridge-only until the terminal appendMessage of concatenated done.text. Mid-turn onSessionPatch / usage / change_dir persist the session as […, user, encodeToolRun card(s)] with no assistant. Incoming [user, assistant(preamble or ''), tool…, assistant(final)] vs that prior: user matches, next incoming assistant vs host tool_run zeros overlap. Durable tool turn. Host onTurnStarted then a live usage or change_dir patchSession writes the pointer (HarnessHost onSessionPatch: persistTurn) after tools have painted. Worker persistStep runs before done (turnLoop awaits persist, then doneLine). Seam reads that host this-run suffix, overlap=0, appends the whole checkpoint under the B8 overlay clock. F5 / other-device shouldAdoptServer adopts duplicated user + mixed encoded/raw tools + split assistants. Same zero-overlap when the model emits a visible preamble (text: 'tooling' + toolCalls — already a turnLoop.test.ts shape) or a tool-only round (text: ''). Coalesced-tool tests stay green because incoming is [user, tool…, assistant] with no round-1 assistant row. Real generateOneRound always sets delta.text (typed string, default ''). “Host persistTurn at end overwrites” is a later best-effort PUT after done; it does not un-publish the merged blob, 409/network/detach leave it, and other-device F5 in the persist-before-done window reads it. pi === prior.length does not save this: the mismatch is inside the this-run suffix, not a short prefix of history. high
Minor L6 No helper/seam row for (a) incoming assistant before tools vs host tool card, (b) empty-string round-1 assistant, (c) host trailing concatenated done.text vs worker last-round assistant. N-tool tests omit the per-round assistant rows checkpointRow actually emits. Merge this PR. vitest run --changed and persist-parse int stay green. DI/cost is clean (memory stores). high

Residual risk

Leftover { deltas } first persist is still this-run-only (locked, no backfill). Bound miss/non-JSON stays fail-closed. B7/B8 remain two envelope upserts; concurrent host LWW on B8 still leaves the pointer advanced. Oversize merged JSON still write_failed at 8 MiB. Identical consecutive turns (same user+assistant text, prior is exactly this-run) still suffix-match as a retry and will not append — indistinguishable from persist retry without a turn boundary. Worker-shaped tool_run text is still the raw result, not encodeToolRun; hydrate coalesce may not decode those rows (accepted mapping). A host mid-stream assistant prefix in the session (if a future path writes live assistant text into SessionSnapshot) still fails exact role+text unless trailing-assistant coalesce is role-only after a tool match.

Merge guidance

  • CONCERNS: do not merge until the Major is fixed. Suffix overlap must (1) not treat empty-text checkpoint assistants as rows the host snapshot can match, (2) skip incoming assistant while the current prior row is tool_run (preamble / empty rounds are not in the mid-turn host snapshot), (3) after a this-run tool match, let a single trailing prior assistant cover remaining incoming assistant rows (host done.text vs last-round text) the same way one host tool_run covers a run of checkpoint tools. Keep user as role+text. Worker-to-worker consecutive assistants that actually exist on both sides stay 1:1 via text equality when no skip applies. Add tests for preamble-before-tools, empty round-1 assistant, and host concatenated trailing assistant. Leftover { deltas } JSON must stay this-run-only; bound miss/non-JSON must stay {ok:false}.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob signed GET against Production, real F5 vs Production Redis, Wasm hydrate paint, host putEnvelopeOnce vs B8 overlay on a live session, NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE off (rollforward PUT) as a substitute pointer writer.

Checkpoint emits one assistant per model round (including empty
tool-only text). Host mid-turn persist has tools and no assistant.
Skip incoming assistants while prior is tool_run; trailing host
done.text covers remaining this-run assistants.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial CONCERNS (92e7863):

  • Empty-text checkpoint assistants are dropped (generateOneRound always sets delta.text, including '' for tool-only rounds).
  • Suffix overlap skips incoming assistant while the current prior row is tool_run (per-round preamble is not in the mid-turn host snapshot).
  • After a this-run tool match, a trailing prior assistant covers remaining incoming assistants (host concatenated done.text vs last-round checkpoint text). Worker-to-worker preamble stays 1:1.
  • Tests: empty-text drop; preamble vs host tool card (helper + seam); host concatenated trailing assistant; worker-to-worker preamble.

npm run typecheck green. messageCheckpoint + turnPersistSeam 54 passed. persist-parse int: 3 passed.

@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 #868

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer SessionSnapshot + suffix-merge
Lenses run: L1, L6, L8 (skip: L2 no secret/runner/client-bundle surface; L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L5 one persist stringify / bounded merge, not a poll/alloc storm; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

92e7863 closed the previous CONCERNS (empty-text drop; skip incoming assistant while prior is tool_run; trailing host done.text covers remaining this-run assistants). Remaining hole is that after-tool assistant cover is role-only, so a completed prior tool-turn with the same user prompt swallows a new turn.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 flexMatchExact (lib/agent/messageCheckpoint.ts) sets afterToolAssistant when matchedTool && both roles are assistant, then matches role only and may consume remaining incoming assistants. Combined with tool_run role-equal, the only remaining discriminator for a completed tool-turn is user text. Durable tool turn 1, user prompt "continue" (or any retry of the same line). Worker/host pointer is [user, tool_run, assistant_A]. Turn 2 uses the same prompt, different tools, different reply assistant_B. k=len(incoming) flex-matches the entire prior: user text equal, tool role-equal, assistants role-only. overlap=full → prior unchanged, B8 overlay clock still advances. F5 shouldAdoptServer adopts turn 1 as the full session. Reproduced: mergeCheckpointOntoPrior([continue, tool, first], [continue, tool, second]) returns length 3. “Identical consecutive turns are accepted residual” — that residual required equal assistant text. 92e7863 widened it to any two tool-turns that share a user line. Host concatenated done.text vs last-round checkpoint is a suffix/equal cover (Let me read that\nfile looks good ends with file looks good), not a license to match unrelated replies. Persist-retry against a host window that already ends with this turn still matches via equal or suffix. Tests use distinct 'turn-1 user' / 'turn-2 user' and never hit this. high
Minor L6 No helper/seam row for two completed tool-turns with the same user text and different assistants. Existing two-persist / host-prior rows stay green while the Major ships. Merge this PR. vitest run --changed and persist-parse int remain green. DI/cost is clean (memory stores). Concatenated-trailing-assistant test is a different shape (this-run window, not a prior completed turn). high

Residual risk

Leftover { deltas } first persist is still this-run-only (locked, no backfill). Bound miss/non-JSON stays fail-closed. B7/B8 remain two envelope upserts; concurrent host LWW on B8 still leaves the pointer advanced. Oversize merged JSON still write_failed at 8 MiB. A later assistant that is an accidental suffix of an earlier reply can still false-cover after a same-user tool match (narrower than role-only). Worker-shaped tool_run text is still the raw result, not encodeToolRun. Envelope reserved meta is overlaid on boot (overlayEnvelopeMeta); dropping meta from the worker blob is not a carrier wipe.

Merge guidance

  • CONCERNS: do not merge until the Major is fixed. After a this-run tool match, a trailing prior assistant may cover remaining incoming assistants only when text is equal or a suffix/prefix cover (host concatenated done.text vs last-round checkpoint). Different replies with the same user prompt must append. Keep skip-incoming-assistant while prior is tool_run (preamble vs host card). Add a same-user-text / different-assistant tool-turn test (helper + seam). Leftover { deltas } JSON must stay this-run-only; bound miss/non-JSON must stay {ok:false}.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob signed GET against Production, real F5 vs Production Redis, Wasm hydrate paint, host putEnvelopeOnce vs B8 overlay on a live session, NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE off (rollforward PUT) as a substitute pointer writer.

Role-only trailing-assistant match after a tool_run let two completed
tool-turns that share a user prompt collapse to the first reply under
the overlay clock. Cover is now equal or suffix/prefix (host concatenated
done.text vs last-round checkpoint). Same-user different-reply appends.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial CONCERNS (e18d4f9):

  • After a this-run tool_run match, a trailing prior assistant covers remaining incoming assistants only when text is equal or a suffix/prefix (host concatenated done.text vs last-round checkpoint). Role-only is gone — two tool-turns that share a user prompt keep both replies.
  • Empty strings never cover (endsWith('') would match every haystack).
  • Tests: same-user-text / different-assistant tool-turn (helper + seam). Existing concatenated-trailing-assistant and preamble-skip rows stay green.

npm run typecheck green. vitest run --changed: 33 files / 386 passed. persist-parse int: 3 passed.

@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 #868

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer SessionSnapshot + suffix-merge
Lenses run: L1, L6, L8 (skip: L2 no secret/runner/client-bundle surface; L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L5 one persist stringify / bounded merge, not a poll/alloc storm; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

e18d4f9 closed the previous CONCERNS (after-tool assistant cover is equal/suffix/prefix, not role-only). Remaining holes are the real checkpoint shape (turnLoop interleaves per-round assistants with tools) and the reverse endsWith on that cover.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 flexMatchExact host-card coalesce (lib/agent/messageCheckpoint.ts) only consumes consecutive incoming tool_run rows. runTurnLoop pushes {role:'assistant', delta} every round, then tools (lib/workflows/turnLoop.ts). Checkpoint is [user, asst1, tool1, asst2, tool2, asst3], not [user, preamble, tools…, final]. After matching the first tool against a host live-paint card, the next incoming row is assistant, so later tools are not coalesced. Durable two-round tool turn. Host onSessionPatch writes […, user, encodeToolRun(read+exec)]. Worker persist reads that pointer. Mid-turn merge keeps the card and appends asst2 + raw tool2 + asst3. F5 / other-device adopt shows a duplicate exec row (card + raw result). If a trailing host done.text concat is already on the pointer (persist retry), cover checks concat vs first remaining assistant (I will run the tests), fails, overlap=0, and the entire this-run is appended (duplicated user + mixed tools). Reproduced against HEAD. Tests only use preamble-then-all-tools ([u, pre, t1, t2, a]). “Preamble skip handles per-round assistants” — that skip runs only while prior[pi] is still tool_run. After the first tool match, pi has advanced off the card. Empty-text drop only removes '' rounds; generateOneRound routinely sets non-empty delta.text before more tools (“Let me read”, “I’ll run tests”). Worker persist is before done, so the live path is the mid-turn host card, not an after-the-fact host PUT. high
Major L1 assistantCovers (lib/agent/messageCheckpoint.ts) treats either text as a suffix of the other. Incoming is always the worker checkpoint (last-round / per-round text). The host-concat case only needs prior.endsWith(incoming). Reverse incoming.endsWith(prior) swallows a longer new reply that happens to end with the previous short ack. Durable tool turn 1, prompt "continue", assistant "OK". Turn 2, same prompt, different tools, assistant "All tests passed. OK". flexMatchExact user-text + tool role + reverse endsWith → overlap=full → prior unchanged, B8 clock still advances. F5 adopts turn 1 as the full session. Reproduced: merge of [continue, tool, OK] vs [continue, tool, All tests passed. OK] returns length 3. Last review locked “equal or suffix/prefix (host concatenated done.text vs last-round checkpoint).” That example is prior.endsWith(incoming) (concat is longer). Reverse is not required for worker incoming. Equal still covers persist-retry. Tests used unrelated long strings (here is the first analysis / now I will edit the file) and never a short ack. high
Minor L6 No helper/seam row for (a) interleaved per-round assistants vs a host tool card, (b) the same shape vs a trailing host concat, (c) same-user tool-turns where the new assistant ends with the prior assistant. Existing preamble/N-tool/same-user rows stay green. Merge this PR. vitest run --changed and persist-parse int remain green. DI/cost is clean (memory stores). high

Residual risk

Leftover { deltas } first persist is still this-run-only (locked, no backfill) — first persist after upgrade of a multi-turn session can LWW-publish this run under a newer clock. Bound miss/non-JSON stays fail-closed. B7/B8 remain two envelope upserts; concurrent host LWW on B8 still leaves the pointer advanced. Oversize merged JSON still write_failed at 8 MiB. A new last-round that is an accidental suffix of a prior host concat (the remaining prior.endsWith(incoming) direction) can still false-cover after a same-user tool match. Worker-shaped tool_run text is still the raw result, not encodeToolRun. Same-device F5 may keep a newer host persistTurn local and hide the mid-turn mixed blob; other-device / worker-pointer-wins will not.

Merge guidance

  • CONCERNS: do not merge until both Majors are fixed.
    1. When a prior tool_run is a coalesced host card (no later prior tool_run in the matched suffix), skip incoming assistant rows that are still followed by a tool_run and consume those tools — same as consecutive-tool coalesce. Worker-to-worker stays 1:1 when a later prior tool_run exists (interleaved [t1, asst, t2]). After that, trailing assistants still use equal / prior.endsWith(incoming) cover (or append).
    2. assistantCovers: equal or priorText.endsWith(incomingText) only. Drop reverse incoming.endsWith(prior). Empty strings still never cover.
    3. Tests: interleaved two-round tools vs mid-turn host card (helper + seam); same shape vs trailing concat (must not duplicate the user); reverse-endsWith short ack must append. Leftover { deltas } JSON must stay this-run-only; bound miss/non-JSON must stay {ok:false}.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob signed GET against Production, real F5 vs Production Redis, Wasm hydrate paint of raw vs encodeToolRun rows, host putEnvelopeOnce vs B8 overlay on a live session, NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE off (rollforward PUT) as a substitute pointer writer.

The turn loop emits an assistant delta every model round, so a host
live-paint card vs [asst, tool, asst, tool, asst] was not coalesced
and appended duplicate tools (or the whole this-run when a trailing
concat was present). Reverse endsWith also swallowed a longer new
reply that ended with a previous short ack. Cover is equal or prior
ends with incoming only. Worker interleaved tools stay 1:1.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial CONCERNS (5f27f48):

  • Host-card coalesce (no later prior tool_run in the matched suffix) skips incoming assistant rows that are still followed by a tool_run and consumes those tools. Interleaved per-round checkpoint text (turnLoop assistant-delta every round) no longer duplicates the host card or the user.
  • Worker-to-worker interleaved [t1, asst, t2] stays 1:1 (later prior tool_run exists).
  • assistantCovers is equal or prior.endsWith(incoming) only. Reverse dropped — "OK" vs "All tests passed. OK" appends.
  • Tests: interleaved vs mid-turn host card (helper + seam); same vs trailing concat (no duplicate user); reverse-endsWith short ack appends; worker-to-worker interleaved retry.

npm run typecheck green. messageCheckpoint + turnPersistSeam + turnLoop 93 passed. persist-parse int: 3 passed.

@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 #868

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer SessionSnapshot + suffix-merge
Lenses run: L1, L6, L8 (skip: L2 no secret/runner/client-bundle surface; L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L5 one persist stringify / bounded merge, not a poll/alloc storm; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

5f27f48 closed the previous CONCERNS (host-card coalesce skips interleaved round assistants; assistantCovers is equal / prior.endsWith(incoming) only). Remaining hole: skipped incoming assistants are consumed as overlap, so they never reach the remainder.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 flexMatchExact (lib/agent/messageCheckpoint.ts) skips incoming assistant rows to let a host tool_run card match (preamble while prior[pi] is tool_run; interleaved assts still followed by a tool_run under host-card coalesce). mergeCheckpointOntoPrior then takes incoming.slice(overlap) — those skipped rows sit inside the matched prefix, so they are discarded. runTurnLoop persists before done SSE (lib/workflows/turnLoop.ts). Host onTurnStarted / live usage / change_dir / meta_sandbox_switch all patchSessionputEnvelopeOnce (lib/harnessChat.ts), so a bound pointer after tools is commonly […, user, encodeToolRun card(s)] with no trailing assistant (live asst is bridge-only until done.text). Durable multi-round tool turn. First tools paint; a usage (or change_dir) persist writes the mid-turn card. Worker persistStep reads that pointer, suffix-merges, publishes […, user, card, last-round text] under the B8 overlay clock. F5 / other-device parseCloudSessionSnapshot + shouldAdoptServer adopts it. Per-round prose (Let me read the file, I will run the tests) is gone — only 3 passed remains. Reproduced: existing helper/seam rows already assert this (interleaved per-round assistants vs mid-turn host card → trailing '3 passed' only). “Host persistTurn after done writes the full concat and should LWW-win” — worker persist is before done; overlay clock is max(worker now, stored+1) on the Vercel clock, so the later browser PUT can 409/adopt the incomplete blob. Other-device F5 never sees host persistTurn; the worker blob is the DoD transcript. “Skip is required so overlap does not zero” — skip is valid for matching; it is not a license to drop unmatched assts from the remainder when the prior has no covering assistant. Trailing-concat cover (prior already has done.text) is a different path and must stay no-append. high
Minor L6 Helper + seam rows for interleaved vs mid-turn host card lock in last-round-only ('3 passed' / 'file looks good'). No row that skipped this-run assistant text survives onto the pointer when the prior ends on a tool card. Merge this PR. messageCheckpoint / turnPersistSeam stay green while other-device F5 after a usage mid-turn persist hydrates last-round-only. DI/cost is clean (memory stores). Trailing-concat rows are a different shape (prior already has done.text). high

Residual risk

Leftover { deltas } first persist is still this-run-only (locked, no backfill). Bound miss/non-JSON stays fail-closed. B7/B8 remain two envelope upserts; concurrent host LWW on B8 still leaves the pointer advanced. Oversize merged JSON still write_failed at 8 MiB. assistantCovers prior.endsWith(incoming) can still false-cover a new last-round that is an accidental suffix of a prior host concat after a same-user tool match. Worker-shaped tool_run text is still the raw result; coalesceToolRunMessages fail-open keeps it as plain text if the worker blob wins before a host encodeToolRun PUT. Suffix search is O(incoming × prior) on an 8 MiB object — fine for typical sessions, not a poll loop.

Merge guidance

  • CONCERNS: do not merge until the Major is fixed.
    1. Skipping incoming assistants to match a host tool card must not drop them from the remainder when the prior suffix has no covering assistant. Fold those this-run assistant texts into the appended tail (one concatenated row matching host done.text += of per-round delta.text, or the skipped rows in order). Prior that already ends with a covering concat stays no-append.
    2. Tests: interleaved vs mid-turn host card (helper + seam) must keep all this-run assistant prose, not last-round-only. Trailing-concat / reverse-endsWith / same-user-different-reply / leftover { deltas } / bound miss rows stay as they are.
    3. Leftover { deltas } JSON must stay this-run-only; bound miss/non-JSON must stay {ok:false}.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob signed GET against Production, real F5 vs Production Redis, Wasm hydrate paint of raw vs encodeToolRun rows, host putEnvelopeOnce vs B8 overlay on a live session, NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE off (rollforward PUT) as a substitute pointer writer.

Skipped incoming assistants sat inside the matched prefix, so
incoming.slice(overlap) dropped them. A mid-turn host pointer
(usage / change_dir) then LWW-published last-round-only under
the overlay clock. Fold this-run assistant texts into one
appended row (+= of delta.text) when prior ends on tool_run.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial CONCERNS (8570ec3):

  • Skipping incoming assistants to match a host tool card no longer drops them from the remainder. When the prior suffix ends on tool_run (mid-turn usage / change_dir pointer, no covering assistant), this-run assistant texts are folded into one appended row (+= of per-round delta.text, same as host done.text).
  • Trailing-concat cover stays no-append. Reverse-endsWith short ack still appends. Same-user different reply still appends the whole this-run.
  • Tests: interleaved / preamble vs mid-turn host card keep all this-run assistant prose (helper + seam).

npm run typecheck green. messageCheckpoint + turnPersistSeam + turnLoop 93 passed. persist-parse int: 3 passed.

@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 #868

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer SessionSnapshot + suffix-merge
Lenses run: L1, L2, L5, L6, L8 (skip: L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

8570ec3 closed the previous CONCERNS (fold skipped per-round assistants onto a mid-turn host card when the remainder is trailing assistants). Remaining hole: that fold is gated on a nonempty remainder, so a full-prefix match that ends on tool_run still drops the skipped prose.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 shouldFoldSkippedAssistants (lib/agent/messageCheckpoint.ts) returns false when appended.length === 0. checkpointToSnapshotMessages drops empty-text last-round assistants (generateOneRound uses (result.text ?? '').trim(), thinking-only / whitespace finish). Incoming then ends on tool_run. flexMatchExact matches the full prefix against a mid-turn host card (skip preambles, coalesce tools), overlap=incoming.length, remainder empty, fold never runs, skipped assts are discarded. Durable multi-round tool turn. Host usage / change_dir persist writes […, user, encodeToolRun card]. Last model round is empty/whitespace (no tools). Worker persistStep incoming is [user, asst1, t1, asst2, t2] (empty last asst dropped). Merge publishes the prior card and drops asst1/asst2. F5 / other-device shouldAdoptServer adopts user + tool card, no this-run prose. Reproduced on HEAD: mergeCheckpointOntoPrior([…, user, card], checkpointToSnapshotMessages([user, 'Let me read the file', tool, 'I will run the tests', tool, ''])) → texts are turn-1 + user + card only. 8570ec3 folds skipped assts” — only when the remainder is trailing assistants. The existing interleaved helper/seam rows keep a nonempty last-round (3 passed), so they do not hit full-prefix overlap. Covering concat is already excluded by lastKeptRole(prior) !== 'tool_run'; appended.length === 0 is not that path. “Host persistTurn after done writes concat” — worker persist is still before done; overlay clock can 409/beat the later browser PUT; other-device F5 never sees host persistTurn. high
Major L1 createTurnPersistSeam (lib/agent/turnPersistSeam.ts) catch-swallows readEnvelope throw as stored = null, then skips the prior-read (no pointer) and suffix-merges onto empty. Production Redis rejects commands when unreachable (lib/sessions/redisSessionStore.ts fail-closed, no offline queue). A first-read reject + B7 retry success publishes this-run-only under clock(0) = Date.now(). Complete turn 1 (bound readable pointer). Turn 2 persist: first readEnvelope rejects (socket blip / reconnect). Seam treats no envelope, writes [user2, assistant2], B7’s second GET succeeds and advances transcriptPointer, B8 stamps a newer clock. F5 LWW-adopts turn 2 only. Distinct from bound blob read() === null (already {ok:false}). “If Redis is down B7 also fails” — node-redis reconnects; first command reject + second success is the documented reconnect path. null (missing envelope, first persist) is not throw (read failed). The catch existed for the overlay clock; this PR tied merge skip to the same swallow. high
Minor L6 No helper/seam row for (a) empty last-round / incoming ending on tool_run vs mid-turn host card, (b) readEnvelope throw must {ok:false} and leave the pointer. Interleaved-with-trailing-asst rows stay green. Merge this PR. messageCheckpoint / turnPersistSeam stay green while other-device F5 after a thinking-only finish hydrates no this-run prose, or a Redis blip clobbers history. DI/cost is clean (memory stores). high

Residual risk

Leftover { deltas } first persist is still this-run-only (locked, no backfill). Bound miss/non-JSON stays fail-closed. B7/B8 remain two envelope upserts; concurrent host LWW on B8 still leaves the pointer advanced. Oversize merged JSON still write_failed at 8 MiB. assistantCovers prior.endsWith(incoming) can still false-cover a new last-round that is an accidental suffix of a prior host concat after a same-user tool match. Worker-shaped tool_run text is still the raw result. Suffix search is O(incoming × prior) on an 8 MiB object — fine for typical sessions, not a poll loop.

Merge guidance

  • CONCERNS: do not merge until both Majors are fixed.
    1. shouldFoldSkippedAssistants: drop the appended.length === 0 gate. Fold when lastKeptRole(prior) === 'tool_run' and this-run had assistants (covering concat already fails lastKeptRole). Empty last-round vs mid-turn card must keep all this-run assistant prose.
    2. readEnvelope throw → {ok:false, code:'write_failed'}, pointer unchanged. Missing envelope (null) still first-persist / this-run-only.
    3. Tests: empty last-round vs mid-turn host card (helper + seam); envelope-throw fail-closed. Trailing-concat / reverse-endsWith / leftover { deltas } / bound miss rows stay as they are.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob signed GET against Production, real F5 vs Production Redis, Wasm hydrate paint of raw vs encodeToolRun rows, host putEnvelopeOnce vs B8 overlay on a live session, NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE off (rollforward PUT) as a substitute pointer writer.

shouldFoldSkippedAssistants no longer requires a nonempty remainder, so a
full-prefix match that ends on tool_run (empty last-round assistant dropped)
still folds skipped this-run prose onto a mid-turn host card.

readEnvelope throw is write_failed (pointer unchanged), not stored=null
this-run-only. Missing envelope (null) stays first persist.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial CONCERNS (120b2ef):

  • shouldFoldSkippedAssistants no longer requires a nonempty remainder. A full-prefix match that ends on tool_run (empty last-round assistant dropped by checkpointToSnapshotMessages) still folds skipped this-run assistant texts onto a mid-turn host card. Covering concat stays no-append (lastKeptRole !== 'tool_run').
  • readEnvelope throw → {ok:false, code:'write_failed'}, pointer unchanged. Missing envelope (null) still first-persist / this-run-only.
  • Tests: empty last-round vs mid-turn host card (helper + seam); envelope-throw fail-closed.

npm run typecheck green. messageCheckpoint + turnPersistSeam + turnLoop 96 passed. vitest run --changed: 33 files / 396 passed. persist-parse int: 3 passed.

@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 #868

Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer SessionSnapshot + suffix-merge
Lenses run: L1, L2, L5, L6, L8 (skip: L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

120b2ef closed the previous CONCERNS pair (empty last-round full-prefix fold; readEnvelope throw fail-closed). This pass re-attacked HEAD as if those fixes were wrong, then enumerated remaining production host/checkpoint shapes that prior rounds never executed: skill-attached between two host cards, consecutive group-full cards vs interleaved tools, usage-before-first-tool (user-only prior), error-after-card empty-finish, text-only turn after a tool-turn, same-user no-tools different assistant, user-prompt substring. All held. Attack did not breach with current evidence.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
(none survived self-refutation)

Residual risk

Leftover { deltas } first persist is still this-run-only (locked, no backfill). Bound miss/non-JSON and envelope throw stay fail-closed. B7/B8 remain two envelope upserts; concurrent host LWW on B8 still leaves the pointer advanced. Oversize merged JSON still write_failed at 8 MiB. assistantCovers prior.endsWith(incoming) can still false-cover a new last-round that is an accidental suffix of a prior host concat after a same-user tool match. Two consecutive tool-only turns that share a user prompt and have no assistant prose are indistinguishable from a persist retry (tool_run is role-equal) — no turn-boundary id on the blob. Worker-shaped tool_run text is still the raw result; hydrate coalesce may keep it as plain text if the worker blob wins before a host encodeToolRun PUT. Suffix search is O(incoming × prior) on an 8 MiB object — fine for typical sessions, not a poll loop.

Merge guidance

  • PASS WITH NOTES: safe to merge from this attack; nits optional. Do not start another adversarial round unless the merge predicate or persist seam changes again.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob signed GET against Production, real F5 vs Production Redis, Wasm hydrate paint of raw vs encodeToolRun rows, host putEnvelopeOnce vs B8 overlay on a live session, NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE off (rollforward PUT) as a substitute pointer writer.

@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 #868

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/f5-readable-blob · 10 files · worker transcriptPointer SessionSnapshot + suffix-merge
Lenses run: L1, L5, L6, L8 (skip: L2 no secret/runner/client-bundle surface; L3 no DOM/Wasm chat path; L4 no workflow/artifact change; L7 no host/tenant hardcoding; L9 no UI)
AGENTS.md read: yes

120b2ef closed the previous CONCERNS pair (empty last-round full-prefix fold; readEnvelope throw fail-closed). This pass re-attacked HEAD as if those fixes were wrong. Mid-turn host-card fold, covering concat, reverse-endsWith, same-user-with-trailing-assistant, leftover { deltas }, bound miss/non-JSON, envelope throw all still hold. Remaining hole is that the empty-remainder fold cannot tell a mid-turn host card from a worker-shaped prior that already is this run.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 shouldFoldSkippedAssistants (lib/agent/messageCheckpoint.ts) fires on lastKeptRole(prior)==='tool_run' + incoming has any assistant, including when that prior already is this-run (1:1 worker rows, no skipped assts). 120b2ef dropped the nonempty-remainder gate so a full-prefix match still folds. Fold then appends += of this-run assistant texts onto a blob that already contains them. Durable tool turn, last model round empty/whitespace (checkpointToSnapshotMessages drops it). First persist (empty prior / leftover { deltas }) writes [user, preamble, tool_run]. B7 advances transcriptPointer; the 'use step' then times out before B8 returns (the PR’s own B7/B8 partial-commit residual). Vercel retries persistStep. Second merge: prior ends on tool_run, overlap=full, remainder empty, fold appends the preamble again. F5 / other-device shouldAdoptServer adopts [user, preamble, tool, preamble]. Reproduced: mergeCheckpointOntoPrior([u1,a1, ...incoming], incoming) with empty last-round incoming → duplicate 'Let me'. “Persist is once per turn and overlayWorkerMeta never throws” — retry is timeout-after-B7, not a throw. The existing worker-to-worker interleaved tools stay 1:1 on persist retry row keeps a trailing assistant so lastKeptRole is assistant and never hits this gate. Mid-turn host-card fold does skip incoming assts against a tool_run card; worker 1:1 does not. Those are distinguishable: fold only when the winning flexMatchExact actually skipped an incoming assistant. high
Minor L6 No helper/seam row for worker-to-worker empty last-round persist retry. The 1:1 retry row has a nonempty trailing assistant; the empty-last-round rows use a host mid-turn card, not a worker prior that already contains this run. Merge this PR. messageCheckpoint / turnPersistSeam stay green while a B7-then-retry duplicates preamble prose. DI/cost is clean (memory stores). high

Residual risk

Leftover { deltas } first persist is still this-run-only (locked, no backfill). Bound miss/non-JSON and envelope throw stay fail-closed. B7/B8 remain two envelope upserts; concurrent host LWW on B8 still leaves the pointer advanced. Oversize merged JSON still write_failed at 8 MiB. assistantCovers prior.endsWith(incoming) can still false-cover a new last-round that is an accidental suffix of a prior host concat after a same-user tool match. Two consecutive tool-only turns that share a user prompt are indistinguishable from a persist retry without a turn-boundary id — including a completed empty-finish turn ([user, card]) followed by a new same-prompt turn with a reply (that reply glues onto the previous card; fixing it would re-break the mid-turn host-card fold). Worker-shaped tool_run text is still the raw result. Suffix search is O(incoming × prior) on an 8 MiB object — fine for typical sessions, not a poll loop.

Merge guidance

  • CONCERNS: do not merge until the Major is fixed.
    1. shouldFoldSkippedAssistants: fold only when the winning suffix match skipped incoming assistant rows against a prior tool_run (mid-turn host card / host-card coalesce). A 1:1 worker prior that already ends on tool_run must keep prior (no-append), including empty last-round.
    2. Tests: worker-to-worker empty last-round persist retry (helper + seam) must not duplicate this-run assistant prose. Existing mid-turn host-card empty-last-round / interleaved fold rows stay green.
    3. Leftover { deltas } JSON must stay this-run-only; bound miss/non-JSON must stay {ok:false}.

What was not attacked

Live DO runner, prod Gateway, Vercel Blob signed GET against Production, real F5 vs Production Redis, Wasm hydrate paint of raw vs encodeToolRun rows, host putEnvelopeOnce vs B8 overlay on a live session, NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE off (rollforward PUT) as a substitute pointer writer, live 'use step' timeout-retry against Vercel Workflows.

Empty-remainder fold treated a worker 1:1 prior that already ends on
tool_run as a mid-turn host card, so a persist-step retry after B7
duplicated this-run assistant prose. Fold only when flexMatchExact
skipped an incoming assistant against a prior tool_run.

Copy link
Copy Markdown
Owner Author

Follow-up on the adversarial CONCERNS (9b41884):

  • shouldFoldSkippedAssistants now requires the winning flexMatchExact to have skipped an incoming assistant against a prior tool_run (mid-turn host card / host-card coalesce). A worker 1:1 prior that already ends on tool_run (empty last-round persist retry after B7) stays no-append.
  • Tests: worker-to-worker empty last-round persist retry (helper + seam) does not duplicate this-run assistant prose. Existing mid-turn host-card empty-last-round / interleaved fold rows stay green.

npm run typecheck green. messageCheckpoint + turnPersistSeam + turnLoop 98 passed. persist-parse int: 3 passed.

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 readable worker blob — phase 2 — one transcript schema

1 participant