Skip to content

plan: int two-heap durable-turn suite (locks #859) #860

Description

@btipling

Plan header

Field Value
Status HANDOFF-READY
Date 2026-08-27
Type single (one PR; not a parent)
Parent #859 (source bug). Umbrella: #794
Branch plan/int-durable-860
Layers DOM (boot driver extract, SessionStore) · harness (Wasm = assertion surface only, no Zig) · Vercel backend (in-process persist seam + envelope; no route change)
Reusability impact none — config-only CI (GITHUB_TOKEN / existing fetch-harness). No origin URL, droplet, or World secret hard-bind
Production mutate? no
Cloud ops path N/A for Production data/secret/env. CI ships: new GHA int-durable (ubuntu-latest, PR + main)
Living docs AGENTS.md tests bullet: npm run test:int + npm test --project flags. No issue numbers. Other surfaces N/A

Review notes (2026-08-27)

Field Value
Reviewed 2026-08-27
Verdict HANDOFF-READY
Reviewer plan-review (mode=fix)

Majors landed: (1) vitest run runs all projects — npm test must pass --project default --project tenancy or the int project is collected. (2) it.fails would swallow a missing-wasm throw — fail-closed load is a green test; only contract expects are it.fails. (3) Do not add lib/turnBoot.ts — extract into existing modules (sessionRepository / turnAttach). Minors: GHA permissions: actions: read; AGENTS.md timeless (no issue numbers).

Source issue #860 is this issue. Body below is the implementable plan. Product patches for #859 stay on later PRs.

Summary

Ship a process-level integration suite that reproduces #859 as failing desired-contract tests before any persist/attach product patch. Wasm is the operator-visible ring after hydrate, not the SUT. The SUT is persist + envelope + boot attach + C15 Send remap. One PR, one review, merge. Default npm test stays green. npm run test:int stays green because of it.fails, not because the contract holds.

Goals

# Goal Success signal
1 Lock #859 row 1 (pointer clobber) as an int test Worker persist blob round-trips parseCloudSessionSnapshot (desired). On main: it.fails
2 Lock F5 attach + Send remap (rows 2–3) on two JS heaps + real Wasm Stale local completed + envelope running → cold attach, not none; Send → GET not POST. it.fails on main
3 Lock quota + stream-drop + persist-hole (rows 4–6) Silent quota save is a failed contract; abort keeps envelope running; mid-turn blob is not live interleaving. it.fails on main
4 Isolate the suite from the merge-green gate Vitest project int excluded from npm test. New GHA int-durable on ubuntu-latest
5 Fail-closed Wasm supply Missing/stale harness.wasm throws, never it.skip (same as lib/harnessChat.wasm-int.test.ts)

Non-goals / out of scope

  • Product patches for bug: F5 after a live durable turn restores a stale snapshot; C15 409 is the last click, not the cause #859 rows 1–5 (schema unify, envelope-wins boot, quota visibility, continuous persist). Those PRs drop the matching it.fails in the same PR as the fix.
  • Playwright / Chromium / pixel QA / canvas screenshots.
  • wasmtime / a second Zig/WASI harness target.
  • next start, auth cookie, local Workflow World, Vercel World, workflows-smoke extension.
  • DO droplet / invincible-do-1 / self-hosted runner (Zig compile only).
  • Rewriting turnLoop.test.ts (it locks { deltas } as persist content until a product PR).
  • React mount of HarnessHost.
  • Layer 2/3 (HTTP POST /api/turns, preview Vercel smoke).
  • Forbidden wiring: dual DOM chat · secrets in Wasm · weakening C15 · /api/agent fallback · wrapping runAgentStream in one 'use step' · Wasm/protocol change · nested 'use step' · continue-on-error on the int job · it.skip when wasm is missing · laptop-only Production ops · phase/issue theater in docs/*

Intent lock

In scope: vitest project int + int/** helpers/tests; behavior-preserving extract of boot/attach spec from HarnessHost so the driver does not mount React; GHA int-durable.yml; one AGENTS.md line.

Out of scope: anything that makes a #859 row pass (except the extract remaining behavior-identical to main).

Forbidden: dual DOM chat · secrets in Wasm · laptop-only Production ops · phase/issue theater in product docs

Architectural decisions

Decision Options considered Choice Why
Runtime Playwright next start / wasmtime / Node WebAssembly.instantiate Node + WebAssembly.instantiate, reuse wasm-int dvui stub harness.wasm imports dvui. Node already instantiates it (lib/harnessChat.wasm-int.test.ts). wasmtime would be a new product and would not have caught pointer clobber / C15 remap
F5 Document reload / new tab folklore / new JS heap New JS heap (new wasm instance + load envelope/blob + hydrate + attach spec) E19 decideAttachClass is heap state (heapApplied), not a document. Matches #857
Stores Mock fetch / Vercel Blob+Redis / in-memory Blob + envelope MemoryBlobTranscriptStore + MemorySessionStore + real createTurnPersistSeam / persistStep Real persist + parser. Fake the model and tools only. Do not stub the persist seam or parseCloudSessionSnapshot
HTTP Drive POST /api/turns / in-process persist In-process persist + parser + boot Layer 2 (next start + Workflow World) is a later issue. Local World is the false-green workflows-smoke forbids
Host driver New lib/turnBoot.ts / mount React / extract into existing modules bootCloudSnapshot in lib/sessionRepository.ts (next to parseCloudSessionSnapshot / overlayEnvelopeMeta; mirrors inner getEnvelope two-step, no HTTP). coldAttachFromSnapshot in lib/turnAttach.ts (next to decideAttachClass). HarnessHost keeps const kickColdAttach = useCallback lib/sessionBoot.ts is URL/mint/adopt identity — do not overload it. Do not add a third boot module. Extract is behavior-preserving of today's boot (parse fail → keep local; overlay never runs). Do not overlay envelope running on parse error — that is the #859 product fix
Red ratchet continue-on-error / it.skip / wrap the whole file in it.fails / split load vs contract Green it('loads harness.wasm') fail-closed (throw, never skip). it.fails only around desired-contract expects, tagged by #859 row. beforeAll / loadBridge() stay outside it.fails If loadBridge throws inside it.fails, missing wasm looks like the expected failure (false green). Product PR that fixes a row must drop that it.fails in the same PR or unexpected-pass reds CI
Gate vs npm test Exclude int/** from default only / separate config file / explicit --project on npm test Vitest project int + default exclude: ['int/**'] and "test": "… && vitest run --project default --project tenancy". npm run test:int = vitest run --project int (no wrapper script) Vitest 3 vitest run with test.projects runs every project. Excluding int/** from default is not enough — the int project would still run under naked vitest run
CI runner DO self-hosted / ubuntu-latest ubuntu-latest only. Fetch main harness-wasm via existing scripts/fetch-harness-artifact.mjs DO is Zig compile (docs/runner.md). Fork PRs never run there. App int is trusted Node
Artifact SHA Wait for this PR's GITHUB_SHA / compile Zig here / latest main harness-wasm Latest main artifact. Unset HARNESS_COMMIT_SHA. HARNESS_WAIT_MS=0. HARNESS_REQUIRE=1 This PR does not touch Zig. fetch-harness already ignores harness-wasm-pr-*. Waiting on the PR SHA would fail-closed or stall

Layer placement

Concern Layer Path(s) Rationale
Persist blob write Vercel backend (in-process) lib/workflows/persistStep.ts, lib/agent/turnPersistSeam.ts, lib/agent/turnWorkerPersist.ts Real B13 seam; no 'use workflow' entry, no World
Envelope + parser DOM host + server stores (test doubles) lib/sessionRepository.ts parseCloudSessionSnapshot / overlayEnvelopeMeta / bootCloudSnapshot; inner getEnvelope two-step mirrored (not HTTP) Parser is the lock B13 never had. lib/sessionBoot.ts stays identity-only
Boot attach / Send remap DOM lib/turnAttach.ts decideAttachClass / decideSendAttach / coldAttachFromSnapshot; app/harness/HarnessHost.tsx kickColdAttach (hook stays) E19 already pure; boot still keys off restored snapshot
Ring assertion harness Wasm int/loadBridge.ts + inv_message_*_at Operator-visible surface; no Zig change, no protocol bump
Quota DOM lib/sessionStore.ts LocalStorageSessionStore.save empty catch Bounded fake localStorage, not ~5MB folklore
CI GHA .github/workflows/int-durable.yml ubuntu-latest; not self-hosted

Current baseline (live code)

Grounded on main @ a10dad73b6c21e2f3badab5d45562d9fb20862a7.

Claim Path / symbol Notes
Worker persist body is { deltas } lib/workflows/persistStep.ts content = JSON.stringify({ deltas: args.deltas }) verified
B13 seam advances meta.transcriptPointer on that body lib/agent/turnPersistSeam.ts createTurnPersistSeampersistTranscriptSegment verified
Host parser requires { id, updatedAt, messages[] } lib/sessionRepository.ts parseCloudSessionSnapshot { deltas }null
Envelope GET is a two-step and is not exported inner getEnvelope in createHttpSessionRepository (~L637) Blob fetch → parse → overlayEnvelopeMeta. Parse fail → { action: 'error', message: 'Invalid transcript body.' }overlay never runs, checkpointPointer unread
overlayEnvelopeMeta can copy envelope turnStatus lib/sessionRepository.ts overlayEnvelopeMeta Only reached on parse ok
Cold attach keys off restored snapshot app/harness/HarnessHost.tsx kickColdAttach (~L371): if (s.turnStatus !== 'running' || !s.turnRunId) return; then attach: { runId, startIndex: 0, dedup: true } Source-scanned by lib/turnAttach.test.ts (const kickColdAttach = useCallback)
Send remap same predicate lib/turnAttach.ts decideSendAttachdecideAttachClass; turnStatus !== 'running'{ kind: 'none' } verified
wasm-int = real Wasm, mocked sendAgentStream lib/harnessChat.wasm-int.test.ts loadBridge Fail-closed missing wasm. No envelope, no Blob, no second heap
In-memory doubles exist lib/sessions/blobStores.ts MemoryBlobTranscriptStore; lib/sessions/memorySessionStore.ts MemorySessionStore Already used by turnPersistSeam.test.ts / turnWorkerPersist.test.ts
Quota swallow lib/sessionStore.ts LocalStorageSessionStore.save catch { /* quota / private mode — ignore */ } verified
Vitest projects vitest.config.ts default + tenancy No int project. npm test = di-gate + drizzle-journal-gate + vitest run
test:int script package.json absent
vitest package.json vitest: ^3.0.5 it.fails is available
wasm-int already in default suite **/*.{test,spec} includes lib/harnessChat.wasm-int.test.ts Leave it there. Int suite is additive
fetch-harness token scripts/fetch-harness-artifact.mjs HARNESS_ARTIFACT_TOKEN || GH_TOKEN || GITHUB_TOKEN. Artifact name harness-wasm (main only)
Existing GHA .github/workflows/* No npm test / wasm-int GHA today. build-harness is Zig on self-hosted; workflows-smoke is dispatch + Vercel World. Neither is this job
HARNESS_ARTIFACT_TOKEN AGENTS.md infra Done (Vercel prebuild) GHA job uses default GITHUB_TOKEN (same repo). No new secret
DO runner AGENTS.md / docs/runner.md Zig compile only. Out of scope
pushSessionToBridge lib/harnessChat.ts (exported) Hydrate path for heap B ring

Design

Driver shape (heap A → drop → heap B)

heap A (originating tab)
  MemoryBlobTranscriptStore + MemorySessionStore
  createTurnPersistSeam / persistStep (real)
  optional: loadBridge() + runHarnessTurn with fake model
    → host-shaped envelope upsert (turnStatus: running, turnRunId)
    → drop the reader (AbortController) before persistStep  // rows 5–6
    → or let persistStep run                                 // row 1 fixture

heap B (F5 = new JS heap)
  new wasm instance (rows that assert the ring)
  bootCloudSnapshot({ local, envelopeMeta, blobJson })
    // mirrors inner getEnvelope: parseCloudSessionSnapshot(blob)
    //   ok  → overlayEnvelopeMeta(parsed, envelopeMeta)
    //   fail → action error, snapshot = local  (TODAY)
  pushSessionToBridge(wasmB, snapshot)
  coldAttachFromSnapshot(snapshot)
  decideSendAttach({ turnStatus, turnRunId, heapApplied: null, ... })
  assert inv_message_*_at

Fake model: deterministic tool_start / text_delta / done (second prompt for the turn-2 user line). Fake sandbox tools. Do not mock persist or the parser.

getEnvelope stays unexported. The int driver calls parseCloudSessionSnapshot + overlayEnvelopeMeta directly with in-memory envelope + blob bytes — the same two-step, no fetch.

Behavior-preserving extract (existing modules — not lib/turnBoot.ts)

Export Semantics (must match main today)
bootCloudSnapshot({ id, local, envelopeMeta, blobJson }) Parse blob as parseCloudSessionSnapshot(blobJson, id). Null → { action: 'error', message: 'Invalid transcript body.', snapshot: local } (host keeps local; no overlay, no checkpointPointer fetch). Ok → { action: 'ok', snapshot: overlayEnvelopeMeta(parsed, envelopeMeta) }
coldAttachFromSnapshot(s) s.turnStatus === 'running' && s.turnRunId{ runId, startIndex: 0, dedup: true }; else null. Exact predicate of kickColdAttach

HarnessHost kickColdAttach remains a useCallback that calls coldAttachFromSnapshot(sessionRef.current) then runPrompt('', { attach }). Do not rename the hook; lib/turnAttach.test.ts source-scan stays green. Green unit tests of today's extract semantics live in lib/sessionRepository.test.ts + lib/turnAttach.test.ts (parse fail keeps local; coldAttachFromSnapshot none when completed).

Do not change overlay-on-error, envelope-wins, or Send remap. That is #859 product work.

Test matrix (desired contract; must fail on main today)

# #859 row Case Assert (desired) Wasm? File
1 1 pointer clobber persistStep / createTurnPersistSeam.persist writes a blob; parseCloudSessionSnapshot(blob) not null. Envelope GET/bootCloudSnapshot hydrates latest user line + assistant. checkpointPointer is either unnecessary or boot-readable no int/persist-parse.int.test.ts
2 3 stale local Heap A leaves envelope turnStatus: running + turnRunId. Heap B local is completed / missing turn 2. Blob may be {deltas} (unreadable) coldAttachFromSnapshot / decideAttachClasscold, not none. Ring is not stuck on turn-1-only yes int/f5-attach.int.test.ts
3 C15 last click Heap B Send while envelope live, local completed decideSendAttach → GET-attach, not { kind: 'none' } (POST). A true second POST while live still 409s (C15 preserved — unit already exists; do not weaken) yes int/f5-attach.int.test.ts
4 5 quota Polyfill localStorage that throws QuotaExceededError on a later LocalStorageSessionStore.save of running + turn-2 user Failure is not silent. Boot must not trust a truncated snapshot as if the save succeeded. No Chromium no int/quota.int.test.ts
5 4 stream-drop Abort after headers, no SSE done Envelope stays running (overlap #849). Heap B attaches. Empty-EOF does not spin (decideHotResume already refuses; assert it) yes int/f5-attach.int.test.ts
6 2 persist not continuous Mid-turn abort Blob/envelope have the user line + whatever host persist actually wrote. Heap-B ring is not live interleaving. Documents the hole; may stay it.fails until a later continuous-persist issue yes int/f5-attach.int.test.ts

Row 1 first in the PR. It is the one-file test that would have blocked B13. No Wasm.

Realistic fixture for rows 2–3–5 (incident shape): turn 1 persistStep completed ({ deltas } on transcriptPointer); turn 2 started (envelope running + turnRunId); reader aborted; heap B local is turn-1 completed.

Files

Path Role
vitest.config.ts Add project int: include: ['int/**/*.int.test.ts'], environment: 'node', testTimeout: 30000. Default exclude += int/**
package.json "test": "… && vitest run --project default --project tenancy" (required). "test:int": "vitest run --project int". Optionally prefix test:int with node scripts/di-gate.mjs &&. Not a wrapper around vitest
lib/sessionRepository.ts Add bootCloudSnapshot. Unit-test today's semantics (parse fail keeps local) in lib/sessionRepository.test.tsgreen
lib/turnAttach.ts Add coldAttachFromSnapshot. Unit-test in lib/turnAttach.test.tsgreen
app/harness/HarnessHost.tsx Hook body calls coldAttachFromSnapshot. Behavior-identical
int/loadBridge.ts Same fail-closed loader as wasm-int (dvui stub + WebAssembly.instantiate). Duplicate OK; do not churn the green wasm-int file unless a one-line import is trivial
int/stores.ts Wire MemoryBlobTranscriptStore + MemorySessionStore + createTurnPersistSeam like prod
int/driver.ts Heap A/B: bootCloudSnapshot, hydrate, attach spec, ring readback
int/persist-parse.int.test.ts Matrix row 1 it.fails
int/f5-attach.int.test.ts Rows 2, 3, 5, 6 it.fails
int/quota.int.test.ts Row 4 it.fails
.github/workflows/int-durable.yml See Cloud ops
AGENTS.md One line under Working rules / tests: npm run test:int is the durable-turn int project; excluded from npm test; fail-closed wasm; it.fails until the matching #859 product PR

Do not rewrite turnLoop.test.ts. Do not add docs/*.

it.fails convention

it('loads harness.wasm (fail-closed)', async () => {
  const bridge = await loadBridge(); // throw if missing — NOT it.fails
  expect(bridge.protocolVersion()).toBeGreaterThanOrEqual(11);
});

it.fails('#859 row 1: worker persist blob parses as SessionSnapshot', async () => {
  expect(parseCloudSessionSnapshot(JSON.parse(blobContent), sessionId)).not.toBeNull();
});

Wasm rows: await loadBridge() (or beforeAll) above the it.fails body. Only the desired-contract expects sit inside it.fails.

When the product PR makes the expect pass, it.fails fails (unexpected pass). That PR drops it.fails (becomes it) in the same commit. Never continue-on-error. Never it.skip.

Cloud ops path

Production mutate: N/A. No migrate / backfill / seed / env cutover / secret dual-store.

CI (not a human Production button, but the cloud operator path for the lock):

Item Lock
Primary operator surface GitHub Actions → int-durable on pull_request + push main (not dispatch-only). Origin required check is GitHub branch-protection UI — out of this PR; shipping the workflow is enough
Workflow to add .github/workflows/int-durable.yml
runs-on ubuntu-latest only. Never self-hosted / invincible-do-1 / vars.RUNNER_LABELS
Same-repo only if: skip when pull_request.head.repo.full_name != github.repository (public-repo policy; forks cannot read origin harness-wasm)
What the job runs actions/checkout@v4actions/setup-node@v4 (Node 22) → npm cinpm run fetch-harnessnpm run test:int
Wasm supply scripts/fetch-harness-artifact.mjs. Token: default GITHUB_TOKEN (already in GHA). Do not set HARNESS_COMMIT_SHA. HARNESS_WAIT_MS=0. HARNESS_REQUIRE=1. Artifact name harness-wasm from main. Fail-closed if missing
Secrets (names only) none new. Do not add VERCEL_TOKEN / World / DATABASE_URL. Do not require HARNESS_ARTIFACT_TOKEN (that's Vercel prebuild)
permissions contents: read + actions: read (artifact list/download). No write, no id-token
Guards timeout-minutes: 15; concurrency group int-durable-${{ github.ref }} cancel-in-progress; never continue-on-error
Explicit non-paths not build-harness; not workflows-smoke; not DO; not npm test (would exclude int anyway); not personal laptop npm as the prove
After job none (no deploy, no env flip)

Living docs plan

Surface Change Notes
docs/* N/A No operator product behavior change. Session-model / runner / feature-divide stay with H23–H25. Do not narrate #860 / phases
AGENTS.md Update Working rules tests bullet: npm test = di-gate + drizzle-journal-gate + vitest run --project default --project tenancy; npm run test:int = vitest run --project int; fail-closed wasm; it.fails only on persist/boot contract rows Timeless. Do not cite issue numbers
README.md N/A Visitors do not run this suite
SECURITY.md N/A No new secret, no runner-policy change (ubuntu-latest, same-repo)
.env.example N/A No new env. Reuses GITHUB_TOKEN / fetch-harness

Caps table

No existing cap is raised or lowered. No product cap added.

Cap / ceiling Value Rationale Code location
HARNESS_SESSION_MAX_BODY_BYTES unchanged Not touched lib/sessionCloudCaps.ts
TURN_STREAM_CURSOR_MAX unchanged Not touched lib/sessionCloudCaps.ts
wasm-int fail-closed supply throw, never it.skip Reuse int/loadBridge.ts (same rule as lib/harnessChat.wasm-int.test.ts)
NEW int project testTimeout 30000 ms Wasm instantiate + two heaps; generous NEW; not a product/transport cap vitest.config.ts project int
GHA timeout-minutes 15 Generous NEW job budget .github/workflows/int-durable.yml

Quota test uses a tiny fake store cap as a test double (not a product cap, not HARNESS_SESSION_MAX_BODY_BYTES). Do not change the ~5MB browser folklore into a shipped constant.

Implementation order

  1. vitest.config.ts project int + default exclude int/** + package.json test gains --project default --project tenancy and test:int. Confirm vitest run --project default --project tenancy collect does not pick up int/**. Naked vitest run would — that is why npm test must pass the flags.
  2. int/stores.ts + int/persist-parse.int.test.ts (row 1 it.fails). Prove: persistStep content → parseCloudSessionSnapshot is null on main, wrapped in it.fails. No wasm in this file.
  3. bootCloudSnapshot + coldAttachFromSnapshot + thin HarnessHost call. Default-suite unit tests: parse fail keeps local (green). lib/turnAttach.test.ts source-scan still green.
  4. int/loadBridge.ts + int/driver.ts + int/f5-attach.int.test.ts (rows 2, 3, 5, 6 it.fails).
  5. int/quota.int.test.ts (row 4 it.fails) against real LocalStorageSessionStore + throwing polyfill.
  6. .github/workflows/int-durable.yml (same PR — do not defer CI).
  7. AGENTS.md one line.
  8. Agent workspace: npm test green, npm run typecheck green, npm run test:int green via it.fails.

Testing

# Case Layer Type Command / method
1 Row 1 pointer clobber API/persist integration (it.fails) npm run test:int
2 Row 2 stale local → cold attach DOM + harness integration (it.fails) npm run test:int
3 Row 3 Send → GET-attach DOM + harness integration (it.fails) npm run test:int
4 True double-POST still 409 API unit (already exists) npm test — do not weaken
5 Row 4 quota not silent DOM integration (it.fails) npm run test:int
6 Row 5 stream-drop keeps running DOM + harness integration (it.fails) npm run test:int
7 Row 6 persist hole documented DOM + harness integration (it.fails) npm run test:int
8 Missing wasm throws harness integration (green, not it.fails) npm run test:int without public/harness/harness.wasm → non-zero, not skip, not swallowed
9 Default suite unchanged unit + wasm-int npm test (--project default --project tenancy) green (no int/**)
10 Extract semantics DOM unit bootCloudSnapshot parse-fail keeps local; coldAttachFromSnapshot none on completedgreen in default suite
11 kickColdAttach source-scan DOM unit lib/turnAttach.test.ts still finds the hook
12 typecheck gate npm run typecheck
13 GHA job GHA operator int-durable on the PR; ubuntu-latest; fetch-harness + test:int

Minimum locked for DoD: rows 1, 2, 3, 9, 12, 13. Rows 4–6 ship in the same PR (source lock) but 1–3 are the B13/F5/C15 ratchet.

Definition of done

  • Vitest project int + npm run test:int (vitest run --project int). npm test = di-gate + drizzle-journal-gate + vitest run --project default --project tenancy (does not collect int/**; still green on main).
  • Matrix row 1 ships and is it.fails on main: worker persist blob is not a SessionSnapshot today.
  • Matrix rows 2–3 ship (real wasm, two heaps): stale local completed + envelope running → attach, not none; Send → GET not POST. it.fails on main.
  • Matrix row 4: quota-bounded store, silent save is a failed contract. it.fails on main.
  • Matrix rows 5–6: stream-drop keeps running; mid-turn persist hole documented. it.fails on main.
  • Extract is behavior-preserving (bootCloudSnapshot / coldAttachFromSnapshot in existing modules; no lib/turnBoot.ts). HarnessHost keeps const kickColdAttach = useCallback; lib/turnAttach.test.ts still green.
  • Fail-closed wasm supply: a green it('loads harness.wasm') throws if missing (never it.skip; never inside it.fails).
  • GHA int-durable on ubuntu-latest (same-repo PR + main). Not self-hosted. Not continue-on-error. Fetches main harness-wasm.
  • No Playwright, no wasmtime, no next start, no Vercel World, no product persist/attach patch, no C15 weaken, no /api/agent fallback, no Wasm/protocol change, no nested 'use step'.
  • npm run typecheck + default vitest run green. npm run test:int green because of it.fails, not because the contract holds.
  • Cloud ops: Production mutate N/A. CI workflow shipped in this PR.
  • Living docs: AGENTS.md tests bullet (commands + --project flags + fail-closed / it.fails split). Timeless — no issue numbers. Other surfaces N/A.
  • Maps to parent bug: F5 after a live durable turn restores a stale snapshot; C15 409 is the last click, not the cause #859: this is the missing lock, not the product fix. bug: F5 after a live durable turn restores a stale snapshot; C15 409 is the last click, not the cause #859 DoD tests ("worker blob ⊂ host parser; boot + stale local + live envelope → attach; F5 then Send → GET not POST") become drop-it.fails work on later PRs.

Risks & mitigations

Risk Mitigation
Extract accidentally "fixes" overlay-on-error Lock: parse fail returns local, no overlay. Green unit test of today's semantics. Desired contract lives only in it.fails
it.fails silently ignored / job continue-on-error / it.fails swallows missing wasm Green load test outside it.fails. Forbid continue-on-error. Product PR that fixes a row must drop it.fails or CI reds on unexpected pass
Default npm test collects int/** Default exclude: ['int/**'] plus vitest run --project default --project tenancy. Verify collect before merge
fetch-harness waits on PR GITHUB_SHA and times out HARNESS_WAIT_MS=0, do not set HARNESS_COMMIT_SHA; latest main harness-wasm
Fork PR cannot read origin artifacts Same-repo if: skip (existing public-repo policy)
Wasm instantiate flaky / slow on ubuntu-latest testTimeout 30s; job timeout 15m; reuse wasm-int stub (already works in Node)
Scope bleed into #859 product rows Non-goals + DoD forbid persist schema / envelope-wins / quota product change
Laptop-only cutover / missing GHA N/A mutate. CI workflow is in this PR, not "later docs"
lib/turnAttach.test.ts source-scan breaks Keep the hook name/shape in HarnessHost
Reusability / World secrets Job uses only GITHUB_TOKEN. No origin URL

Open questions

None. In-scope engineering choices are locked above. Origin branch-protection "required check" is operator UI, not a plan blocker.

Phase map

N/A — single issue, one PR. Do not split into phase children.

Corrections / refinements vs source #860

Topic Source #860 said This plan locks
Living docs "none here" AGENTS.md one agent-rule line (skill: agent rules changed). docs/* still N/A
Cloud ops N/A Production mutate N/A. CI workflow ships in this PR (ubuntu-latest int-durable)
getEnvelope "GET envelope" Inner, unexported. Driver mirrors the two-step with Memory stores (no HTTP)
kickColdAttach extract "extract if stuck in the component" Spec → coldAttachFromSnapshot in lib/turnAttach.ts; hook stays in HarnessHost (source-scan). No lib/turnBoot.ts
DO droplet Out of scope Confirmed: compile runner only
test:int new script vitest run --project int — no wrapper script (AGENTS.md vitest rule)
vitest run vs projects Exclude int/** from default Also change npm test to --project default --project tenancy (vitest runs every project otherwise)
it.fails vs missing wasm Fail-closed throw Green load test; it.fails only on contract expects
lib/turnBoot.ts New module NobootCloudSnapshotsessionRepository.ts; coldAttachFromSnapshotturnAttach.ts. sessionBoot.ts stays identity/mint
GHA permissions (unspecified) contents: read + actions: read
AGENTS.md issue numbers “until matching #859 product PR” Timeless wording, no issue numbers

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    backend-agentsClient-independent agent turns — backend-run, survive tab close / refreshbugSomething isn't workingharnessHarness / agent UIinfra

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions