Plan header
| Field |
Value |
| Status |
HANDOFF-READY — implement after #959 lands |
| Date |
2026-09-07 |
| Type |
phase 2 — one issue / one PR |
| Parent / source |
#958, #553 |
| Depends on |
#959, bounded best-effort read/transport |
| Closes bug |
#924 only when the actual production-controller real-Wasm int gate is green and this PR ships |
| Baseline / branch |
main b31ca512e7ddee162fa8313cc52f549822e2d23a; branch feat/a5-tail-cold-attach after dependency |
| Layers |
Host controller/parser/apply; backend narrow metadata sync; existing Wasm paint exports |
| Reusability / ops |
Scoped injected services; no admin migrate/backfill/env cutover; existing Git deploy/int artifact |
| Living docs |
Session/stream/limits/feature-divide, AGENTS, README, SECURITY |
Review notes — pragmatic revision (2026-09-07)
Operator explicitly chose best effort, sensible bounded cost, incremental usability over exact history. This replaces this issue's previous certified-checkpoint/whole-system-CAS dependency. Parent and #959 now define recent sampled display and tail skipping. Do not revive the removed archive/proof/transaction work.
| Finding |
Resolution |
| Legacy restoration cannot be perfectly aligned |
No exact merge. Adopt sample/head or retain cached paint, mark partial, begin fresh continuation rows after handoff. |
| Cold JSON→GET can replay backlog |
Consume #959's one negotiated snapshot-first SSE response, not a second cold raw GET. |
| Partial snapshots can overwrite canonical history |
Separate display from durable snapshots; all new production host persistence uses a narrow metadata-only API, no transcript body upload. |
| Queue still needs reload continuity |
Persist the existing host-known desired queue in meta.queueMirror, explicit [] clear. No transaction journal/exactly-once claim. |
| Whole Redis CAS rewrite was scope inflation |
No source-generation/CAS/worker B7/B8 refactor. Use a small field-patch store primitive that changes only allowed metadata fields in the current stored envelope; see below. |
Scores: correctness4, performance5, architecture4, testing5, security4, reusability5, parent adherence5, layers5, docs5, cap governance5, harness UX/layout4. Administrative cloud ops N/A. Verdict HANDOFF-READY subject to #959 merge, not blocked on a design question. No code/tests run during review.
Goals / scope
- G1: real running F5/new tab/login/switch lands on recent useful rows, no prefix rewind or historical chronological replay.
- G2: no sampled historical reasoning paint; post-handoff reasoning is live, even if continuing an old segment.
- G3: Busy/Stop restored promptly for running/cancelling; explicit Stop works during recovery; restore never starts a second turn.
- G4: partial displays never upload as full transcript or become model promptHistory; menus/host-known queue remain usable.
In: production shared controller, #959 parser/negotiation, attach/apply lifecycle, metadata PATCH + queueMirror, tests/docs.
Out: native exports, scrollbar/cache format, exact dedup/terminal commit proof, global CAS, changing worker persistence/inference/queue semantics, SDK changes.
Baseline / ownership
Read on stated main: turnAttach.ts cold decisions hardcode zero; harnessChat.ts calls prefix wipe; turnApply.ts increments callback cursor and assumes last visible live row; HarnessHost cold adopt resets heap then kicks attach; session repo whole-snapshot PUT serves metadata updates too. Existing queue helpers maintain host-known mirror/FIFO; body sanitization drops empty queue whereas the new metadata mirror needs an explicit empty tombstone. Existing Redis store has get/set, no narrow field-patch method; memory adapter must match new method.
| Concern |
Files |
| Pure production controller |
new lib/viewportBootstrap.ts, lib/sessionBoot.ts, app/harness/HarnessHost.tsx |
| Negotiated clients/parser |
lib/turnApi.ts, #959 lib/viewportStreamProtocol.ts, lib/sessionRepository.ts |
| Apply/lifecycle |
lib/turnAttach.ts, lib/turnApply.ts, lib/harnessChat.ts, existing queue/detach helpers |
| Metadata API |
app/api/sessions/[id]/envelope/route.ts PATCH, new lib/sessions/patchSessionMeta.ts |
| Store primitive/carrier |
lib/sessions/{sessionStore,redisSessionStore,memorySessionStore}.ts, lib/sessionCloudCaps.ts, lib/turnQueue.ts |
| Int |
int/{driver,stores,f5-attach.int.test,quota.int.test}.ts, real int/loadBridge.ts |
1. Production cold controller
Extract a framework-independent controller used by HarnessHost and int tests; do not copy its decisions into test driver. States: cached → recovering → attached, or terminal/error/detached. Inputs: session epoch/id, bridge, scoped clients, abort, local writers.
- Cached/local paint immediately; avoid full repo.get/reconstruction before cold attach. Read small envelope for liveness, then
viewportVersion=1&hydrate=tail for a live run. Envelope/snapshot turnStreamCursor is only a hint; never trust it as a complete display prefix.
- On
viewport_state, set live run id and Busy/Stop immediately. No blank/prompt-only rebuild. Keep composer draft/focus and existing FIFO. Cancelling remains live until actual terminal status.
- On snapshot
replace:true, paint rows once through canonical image/math scheduling and queue-preserving ring replace. replace:false keeps cached rows. Add one neutral incomplete-history System note when needed. Snapshot rows are already projected; no second full-history fold/coalescer.
- Set heap next index to snapshot.resumeIndex; reset live assistant/tool/thinking mutable buffers and start fresh live continuation rows. Never update the last restored tool/card by name or append full
done.text over sampled text. Modest fragmented/duplicate rows accepted; no historical wipe/replay.
turn_event advances absolute cursor only after parsing/apply; typed skipped records advance without arbitrary paint. Index unknown after decoder/transport failure → detach and preserve last applied state. Hot GET resumes from same-heap raw index; cold reopen resamples, not callback count.
- Negotiate indexed durable POST for normal new turns too, so later hot cursor has one domain. New turn starts at raw origin legitimately; cold attach doesn't. Legacy injected
/api/agent/test clients stay explicit unindexed paths.
- Cold Send-while-running attaches without a new POST, preserving current follow-up-note behavior. True terminal state permits the next actual prompt. No auto-retry that reruns tools after a durable start.
- Terminal stored done/error and synthetic viewport_end control lifecycle using actual run status; do not demand a successful exact terminal Blob proof. Retain displayed content, stop busy, follow existing success/error/queue-promote rules. On later idle reload the bounded latest head is best effort, never full replace upload.
- All switch/New/Clear/unmount/logout epochs ignore late data and abort readers only. Explicit Stop uses existing cancel API then releases UI appropriately; no recovery-side cancellation. Preserve FIFO/pause/no auto-drain on Stop/error.
- No automatic full-history materialization in new production path. For rare next-turn missing model seed, send raw prompt and omit partial promptHistory; server's normal modelMessages/compaction selection remains unchanged. Legacy complete-snapshot test path stays supported.
2. Partial-view persistence firewall
Use explicit historyComplete:false/view type, not length-based inference. Central guard rejects passing any view to put, trimForCloudPut, mint/upload/flatten path. Apply across success/done/error/Stop, subscription failure, queue/menu/cwd/provider/usage updates and 409 adoption. Read-only view remains separate even if it happens to contain all rows.
Replace new production host metadata calls with PATCH /api/sessions/:id/envelope:
{selectedModel?: string|null, reasoningEffort?: string|null, queueMirror?: string[]}.
Only these fields in this cut; unknown keys reject (including messages, transcriptPointer, turnRunId/turnStatus, arbitrary meta and worker-owned pointers). Persona selection remains existing New-session creation semantics, not a new editable PATCH feature. Worker-owned cwd/bind/provider/usage/notes/model/compaction state reads locally and stays server-written, not echoed by partial host. Standard session/user/tenant validation, invalid fields400, unauth401, missing/foreign404, oversized full meta413, unavailable503. Return only small sanitized acknowledgement/carriers, not arbitrary full meta/secret content.
Narrow field patch — not a history transaction
Add patchEnvelopeMetaFields(key, changes) to session store and injected Redis adapter. On Redis use one small Lua operation that reads the current envelope, changes only the validated selectedModel/reasoningEffort/queueMirror fields, advances updatedAt server-side, preserves every other field and existing TTL. Missing row returns notfound and must not create/resurrect it. Validate the same scope/record and full serialized1 MiB meta/2 MiB envelope rails inside the primitive; malformed existing data fails closed. Changes already schema-sanitized by pure helper; Lua enforces the allowlist too. Memory implementation same semantics. This avoids a stale host read copying an older transcriptPointer over the current one, without adding source-base CAS, retries, candidate journals, worker protocol changes or multi-store transactions. Normal complete legacy writers remain unchanged.
A single field-patch operation is idempotent desired-state/LWW. Existing concurrent old worker writes may later overwrite same host metadata; accepted cross-version residual, preserve local queue/preferences and reassert on later normal coalesced updates. Do not rewrite every worker, retry forever or rerun inference. On response loss retrying the same desired array does not append a duplicate command. No history bytes are read, written or replaced by PATCH.
3. Queue mirror
New reserved scalar key queueMirror stores JSON of the existing bounded queue including valid []. Add typed parser preserving the difference between absent/invalid and present empty; use existing16×5000 sanitization/multiplicity, but serialize empty array instead of dropping it. Excess invalid client input rejects rather than silently converting to a different desired queue.
- Copy forward current queueMirror on omit in ordinary new-code envelope upserts (compatibility like other reserved host-carried exceptions); explicit new valid value wins. Old deployed workers are not magically upgraded; live FIFO/local mirror protects best-effort continuity.
- Reads prefer valid present queueMirror (including
[]), else legacy Blob queue. Do not resurrect Blob queue after a new explicit empty mirror.
- Existing host queue helpers compute desired mirror. Strip the promoted prompt once before POST, schedule that desired state through the coalesced metadata channel, restore head on a failed pre-start send as today. No worker-side extra strip of the new mirror, avoiding duplicate-text double removal.
- One in-flight metadata request per session channel, latest desired state wins; no tight retry loop. On a later normal update send newest full desired mirror. Queue edits inside Wasm that host cannot observe remain the existing documented limitation.
- On cold activation rearm exactly once; snapshot/live ring replacement doesn't rearm. Current pause/promote/band interaction unchanged.
Caps
No existing cap changes, no new history-retention quota. Parent/operator best-effort defaults inherited.
| Value |
Limit / rationale |
Location |
| Inherited recovery |
2048 frames /8 MiB sample /5000ms optional work /1000ms final probe /one head |
#959 caps; this phase consumes, not retries |
| Snapshot / row / ring |
2 MiB /262144B /2048 rows |
Existing and phase1 view rails; include omission note |
| New queueMirror carrier |
Existing16×5000 chars JSON |
Inside unchanged1 MiB meta; worst escape~480KiB, combined meta may reject without clipping unrelated fields |
| PATCH body/envelope |
Existing2 MiB Function body,1 MiB meta, below4.5 MB real ceiling |
Existing caps; reject overbudget, no source body transport |
| Channels |
One in-flight metadata write and one attach per session epoch |
Coalesce latest desired state, no new retry attempt/time budget |
| Other turn/queue/route/protocol |
All unchanged |
No new native export or step |
Testing — #924 close gate
No tests run in planning. All cases ordinary green assertions, real protocol-current Wasm fails closed.
| # |
Case |
Required evidence |
| R1 |
Long running F5/new tab/switch from cached tail |
Production controller+actual client parser+apply→real Wasm ring stays recent throughout state/snapshot/attach; old prefix wipe unreachable |
| R2 |
Large historical reasoning sample, omitted gap; new post-H reasoning |
No historical reasoning body delivered/painted; post-H thinking does paint; no exact legacy segment proof required |
| R3 |
running/cancelling during delayed recovery |
Busy+Stop restored before snapshot, accepted explicit cancel works; server H not naked envelope/local C; no second POST |
| R4 |
Idle/completed head restore, stream drop hot resume, EOF/no progress, synthetic end, known skipped frame |
No idle stream; raw cursor correct; no spin or fake history completeness |
| R5 |
Missing prompt/head/corrupt history, partial tools/oversize row, done aggregate |
Useful partial or cached paint; no full done duplication, no global history reconstruction |
| R6 |
User Stop/error/success/menu/queue/cwd updates after partial restore |
Spy on mint/upload/PUT: zero transcript uploads; only allowed metadata PATCH; canonical Blob unchanged |
| R7 |
Metadata field patch races worker pointer update/delete; invalid/foreign/malicious fields; combined meta pressure |
Redis Lua and memory parity: current transcript pointer/worker siblings unchanged, no recreate, no keys leaked; no retry loop |
| R8 |
queueMirror absent/invalid/[], legacy fallback, repeated text, strip-before-send/failed-start restore, response loss, coalescing |
No ordinary double strip or duplicate append; explicit[] overrides old queue; best-effort cross-tab LWW documented |
| R9 |
Switch/Clear/unmount during I/O, queue rearm/pause/promote, late acknowledgement |
Old epoch cannot repaint/update selected view/rearm old FIFO; abort≠cancel |
| R10 |
Required gates |
npm test, npm run test:int, npm run typecheck, npm run build; keep #959 foundation green |
| R11 |
Hosted browser desktop/~390px |
F5/Stop/new thinking/two-turn conversation; composer/status geometry stable, no React fallback |
Rewrite old unconditional startIndex:0 cold expectations. Unit helper-only tests, old harnessChat.wasm-int or screenshots alone don't satisfy R1–R3. #924 closes only with these production-path real-ring tests and shipping this PR.
Docs / operations / implementation order
| Surface |
Change |
docs/session-model.md |
Active best-effort tail policy, partial-write firewall, metadata PATCH/queueMirror and accepted LWW residual |
docs/agent-stream.md |
Negotiated production cold/indexed apply, lifecycle and post-handoff thinking |
docs/feature-divide.md |
Host I/O / canvas lifecycle and no second UI |
docs/harness-limits.md |
Recovery budgets/excerpts, no full-history accuracy claim |
AGENTS.md |
Controller/store primitive ownership and real-Wasm no-replay gate |
README.md |
Reload resumes recent view without historical replay; link guide |
SECURITY.md |
Allowlisted owned metadata PATCH, no client transcript pointer or secrets |
.env.example, native protocol guide |
N/A no new env/secret/export |
No administrative mutate/migration/backfill; existing Git deployment and int-durable current artifact. Small new metadata lazily appears; old readers ignore it; source Blob unchanged. No new dashboard setting/laptop ops. Build/compile only usual agent/CI path; no native changes planned.
Implementation: parser/client and partial types → metadata field primitive/PATCH/queue tests → shared controller + all cold callsites → apply/finalization/queue lifecycle → real-Wasm int → docs/gates. Exactly one PR.
DoD / residuals
Open questions: none. Accepted: imperfect recent history, fragmented continuation rows, missed legacy model context when no server seed, best-effort cross-tab/old-worker preference/queue synchronization. Not accepted: cross-session data exposure, accidental partial transcript replacement, hidden rerun/cancel, or failed no-replay integration gates.
Refinement vs parent / old draft
The global source-generation/CAS and exact terminal-commit requirements are removed under the operator's best-effort decision. The replacement is a small allowlisted field-only patch, not a full-envelope read-copy-upsert, so a preference update cannot roll back a concurrently advanced transcript pointer. Parent map is aligned to this concrete refinement. No dependency on perfect legacy history remains.
Plan header
mainb31ca512e7ddee162fa8313cc52f549822e2d23a; branchfeat/a5-tail-cold-attachafter dependencyReview notes — pragmatic revision (2026-09-07)
Operator explicitly chose best effort, sensible bounded cost, incremental usability over exact history. This replaces this issue's previous certified-checkpoint/whole-system-CAS dependency. Parent and #959 now define recent sampled display and tail skipping. Do not revive the removed archive/proof/transaction work.
meta.queueMirror, explicit[]clear. No transaction journal/exactly-once claim.Scores: correctness4, performance5, architecture4, testing5, security4, reusability5, parent adherence5, layers5, docs5, cap governance5, harness UX/layout4. Administrative cloud ops N/A. Verdict HANDOFF-READY subject to #959 merge, not blocked on a design question. No code/tests run during review.
Goals / scope
In: production shared controller, #959 parser/negotiation, attach/apply lifecycle, metadata PATCH + queueMirror, tests/docs.
Out: native exports, scrollbar/cache format, exact dedup/terminal commit proof, global CAS, changing worker persistence/inference/queue semantics, SDK changes.
Baseline / ownership
Read on stated main:
turnAttach.tscold decisions hardcode zero;harnessChat.tscalls prefix wipe;turnApply.tsincrements callback cursor and assumes last visible live row;HarnessHostcold adopt resets heap then kicks attach; session repo whole-snapshot PUT serves metadata updates too. Existing queue helpers maintain host-known mirror/FIFO; body sanitization drops empty queue whereas the new metadata mirror needs an explicit empty tombstone. Existing Redis store has get/set, no narrow field-patch method; memory adapter must match new method.lib/viewportBootstrap.ts,lib/sessionBoot.ts,app/harness/HarnessHost.tsxlib/turnApi.ts, #959lib/viewportStreamProtocol.ts,lib/sessionRepository.tslib/turnAttach.ts,lib/turnApply.ts,lib/harnessChat.ts, existing queue/detach helpersapp/api/sessions/[id]/envelope/route.tsPATCH, newlib/sessions/patchSessionMeta.tslib/sessions/{sessionStore,redisSessionStore,memorySessionStore}.ts,lib/sessionCloudCaps.ts,lib/turnQueue.tsint/{driver,stores,f5-attach.int.test,quota.int.test}.ts, realint/loadBridge.ts1. Production cold controller
Extract a framework-independent controller used by HarnessHost and int tests; do not copy its decisions into test driver. States: cached → recovering → attached, or terminal/error/detached. Inputs: session epoch/id, bridge, scoped clients, abort, local writers.
viewportVersion=1&hydrate=tailfor a live run. Envelope/snapshotturnStreamCursoris only a hint; never trust it as a complete display prefix.viewport_state, set live run id and Busy/Stop immediately. No blank/prompt-only rebuild. Keep composer draft/focus and existing FIFO. Cancelling remains live until actual terminal status.replace:true, paint rows once through canonical image/math scheduling and queue-preserving ring replace.replace:falsekeeps cached rows. Add one neutral incomplete-history System note when needed. Snapshot rows are already projected; no second full-history fold/coalescer.done.textover sampled text. Modest fragmented/duplicate rows accepted; no historical wipe/replay.turn_eventadvances absolute cursor only after parsing/apply; typed skipped records advance without arbitrary paint. Index unknown after decoder/transport failure → detach and preserve last applied state. Hot GET resumes from same-heap raw index; cold reopen resamples, not callback count./api/agent/test clients stay explicit unindexed paths.2. Partial-view persistence firewall
Use explicit
historyComplete:false/view type, not length-based inference. Central guard rejects passing any view toput,trimForCloudPut, mint/upload/flatten path. Apply across success/done/error/Stop, subscription failure, queue/menu/cwd/provider/usage updates and 409 adoption. Read-only view remains separate even if it happens to contain all rows.Replace new production host metadata calls with PATCH /api/sessions/:id/envelope:
{selectedModel?: string|null, reasoningEffort?: string|null, queueMirror?: string[]}.Only these fields in this cut; unknown keys reject (including messages, transcriptPointer, turnRunId/turnStatus, arbitrary meta and worker-owned pointers). Persona selection remains existing New-session creation semantics, not a new editable PATCH feature. Worker-owned cwd/bind/provider/usage/notes/model/compaction state reads locally and stays server-written, not echoed by partial host. Standard session/user/tenant validation, invalid fields400, unauth401, missing/foreign404, oversized full meta413, unavailable503. Return only small sanitized acknowledgement/carriers, not arbitrary full meta/secret content.
Narrow field patch — not a history transaction
Add
patchEnvelopeMetaFields(key, changes)to session store and injected Redis adapter. On Redis use one small Lua operation that reads the current envelope, changes only the validated selectedModel/reasoningEffort/queueMirror fields, advances updatedAt server-side, preserves every other field and existing TTL. Missing row returns notfound and must not create/resurrect it. Validate the same scope/record and full serialized1 MiB meta/2 MiB envelope rails inside the primitive; malformed existing data fails closed. Changes already schema-sanitized by pure helper; Lua enforces the allowlist too. Memory implementation same semantics. This avoids a stale host read copying an older transcriptPointer over the current one, without adding source-base CAS, retries, candidate journals, worker protocol changes or multi-store transactions. Normal complete legacy writers remain unchanged.A single field-patch operation is idempotent desired-state/LWW. Existing concurrent old worker writes may later overwrite same host metadata; accepted cross-version residual, preserve local queue/preferences and reassert on later normal coalesced updates. Do not rewrite every worker, retry forever or rerun inference. On response loss retrying the same desired array does not append a duplicate command. No history bytes are read, written or replaced by PATCH.
3. Queue mirror
New reserved scalar key
queueMirrorstores JSON of the existing bounded queue including valid[]. Add typed parser preserving the difference between absent/invalid and present empty; use existing16×5000 sanitization/multiplicity, but serialize empty array instead of dropping it. Excess invalid client input rejects rather than silently converting to a different desired queue.[]), else legacy Blob queue. Do not resurrect Blob queue after a new explicit empty mirror.Caps
No existing cap changes, no new history-retention quota. Parent/operator best-effort defaults inherited.
Testing — #924 close gate
No tests run in planning. All cases ordinary green assertions, real protocol-current Wasm fails closed.
npm test,npm run test:int,npm run typecheck,npm run build; keep #959 foundation greenRewrite old unconditional
startIndex:0cold expectations. Unit helper-only tests, oldharnessChat.wasm-intor screenshots alone don't satisfy R1–R3. #924 closes only with these production-path real-ring tests and shipping this PR.Docs / operations / implementation order
docs/session-model.mddocs/agent-stream.mddocs/feature-divide.mddocs/harness-limits.mdAGENTS.mdREADME.mdSECURITY.md.env.example, native protocol guideNo administrative mutate/migration/backfill; existing Git deployment and
int-durablecurrent artifact. Small new metadata lazily appears; old readers ignore it; source Blob unchanged. No new dashboard setting/laptop ops. Build/compile only usual agent/CI path; no native changes planned.Implementation: parser/client and partial types → metadata field primitive/PATCH/queue tests → shared controller + all cold callsites → apply/finalization/queue lifecycle → real-Wasm int → docs/gates. Exactly one PR.
DoD / residuals
Fixes #960,Fixes #924only with gate evidence;Refs #958 #553, no source-spike close yet.Open questions: none. Accepted: imperfect recent history, fragmented continuation rows, missed legacy model context when no server seed, best-effort cross-tab/old-worker preference/queue synchronization. Not accepted: cross-session data exposure, accidental partial transcript replacement, hidden rerun/cancel, or failed no-replay integration gates.
Refinement vs parent / old draft
The global source-generation/CAS and exact terminal-commit requirements are removed under the operator's best-effort decision. The replacement is a small allowlisted field-only patch, not a full-envelope read-copy-upsert, so a preference update cannot roll back a concurrently advanced transcript pointer. Parent map is aligned to this concrete refinement. No dependency on perfect legacy history remains.