Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.
This repository was archived by the owner on Sep 8, 2026. It is now read-only.

spike: viewport hydrate — latest-page F5 (#924), slim local, scrollbar (A5 rebase) #553

Open

Description

@btipling

Parent

#548 (agent session architecture) — A5 viewport

Type

Spike — design + maybe a throwaway prototype. Not a ship-or-fail
feature until a follow-up create-plan locks the winner. Do not convert
this issue into a plan.

Rebase (2026-09-06)

This spike was filed before durable async workflow inference. The original
problem (one JSON messages[] Blob, host Stop persist of the whole snapshot,
“do we HTTP Range or page?”) does not match main today.

Hard requirement: the winner of this spike must close
#924
(F5 / reload
fast-forward-replays the entire durable stream). A viewport design that leaves
that bug standing is not done.

Hard requirement: the follow-up plan’s DoD must include green
end-to-end integration tests on the durable-turn int project
(npm run test:int = vitest run --project int) that prove the #924
bullets (and any leftover that ships in the same cut) on a real Wasm
ring
. Unit tests of coldAttachFromSnapshot / turnApply alone are
not sufficient. See End-to-end integration tests below.

A4 compaction (#552 / plan #947) is the model seed, not the canvas. This
spike is the paint window. Do not re-plan inference fold.

Leftover (this is the spike)

Three holes remain. #924 is the user-visible one and is non-negotiable.
Closing it (and any leftover that ships with it) requires e2e int rows,
not a unit-only gate.

# Leftover Status
1 Live F5 / cold open replays the Workflows user-stream from index 0, including every reasoning_delta #924 — must close
2 localStorage still JSON.stringifys the whole SessionSnapshot leftover of original #553
3 Load-earlier is an in-memory slice, not a Blob page fetch; no intelligent scrollbar leftover of original #553

Idle / completed F5 already lands on the latest ring page. Do not re-solve that.

Verified F5 path (why #924 still happens)

Same-tab F5 of a live (running / cancelling) turn on current main:

store.load()                         // full localStorage JSON
hydrateRingWindow(latest ≤2048)      // Wasm paints immediately (looks right)
bootCloudSession                     // envelope + full Blob GET (no Range)
adoptCloudSession                    // re-hydrate latest ring; heapAppliedRef = null
kickColdAttach
  coldAttachFromSnapshot → { startIndex: 0, dedup: true }
  // envelope turnStreamCursor is persisted, then ignored
prefixThroughLastUser                // strip this-run tool_run / assistant
  // ring = prior turns + this prompt  ← operator sees the starting prompt
GET /api/turns/:runId/stream?startIndex=0
  apply every SSE frame
  reasoning_delta → growThinking     // NEVER skipped
  tools / text skip only against the (now-empty) this-run window

The lock that causes the replay is intentional in today’s code:

  • decideAttachClass / coldAttachFromSnapshot: new heap (F5 / login / new
    tab / switch) → always { kind: 'cold', startIndex: 0 } even when
    envelope C is large. Comment: “Heap has not applied this turnRunId.”
  • adoptCloudSession / kickColdAttach set heapAppliedRef = null, so F5
    never takes the hot path (startIndex = heap.count).
  • prefixThroughLastUser (adversarial feat(harness): E19 attach handshake — Blob hydrate + stream attach #857): Blob this-run suffix has no
    thinking; leaving it on the ring would append reasoning_delta after the
    answer. Cold attach therefore wipes tools/text off the ring and rebuilds
    them from the stream — which is why the operator sees prompt → sped-up
    thinking → tools → text, not “thinking on top of an already-painted tail.”
  • lib/turnApply.ts: “Thinking is never in Blob — never skip, even on cold
    attach.”

Hot resume at heap C already works same-tab stream drop. F5 is a new
heap, so it never uses it.

'cancelling' stays attachable so F5 after an accepted cancel restores
Busy + Stop. That constraint stays. The replay does not.

Idle / completed F5: coldAttachFromSnapshot → null. Latest 2048 from
local then Blob. No stream, no thinking (thinking is not in SessionStore).
Expected #924 bullets 1–2 are already true for idle sessions.

Already shipped (do not re-solve)

Claim Where Notes
Workflow run owns the turn; viewport attaches docs/feature-divide.md, POST /api/turns Detach closes a reader; it does not stop the run. Worker is the Blob writer.
Attach cursor on the wire meta.turnStreamCursor, GET /api/turns/:runId/stream?startIndex=C #813 / #797. Hot resume (same-heap) starts at heap-applied C. F5 refuses C.
Dedup on cold attach lib/turnApply.ts shouldSkipToolStart / textDeltaDedup Tools + assistant text can skip — but prefixThroughLastUser empties the this-run window first, so they replay too.
Thinking not in SessionStore / Blob docs/session-model.md Correct for paint storage. Replay still streams thinking from the durable run.
Transcript chunks + flatten transcriptPointer + prev / depth (max 256); terminal persist writes a flatten root Original Q1 (one JSON array + HTTP Range) is obsolete. Blob PUT is whole-object. prev is mid-turn worker durability, not a viewport pager; GET reconstructs into one snapshot.
Ring window + “Load earlier” button lib/sessionWindow.ts, protocol v6, native/harness/src/ui.zig Latest 2048; button steps −512 through already-in-memory session.messages. Not overscroll, not document-length scrollbar, not a Blob fetch.
Inference seed ≠ paint meta.modelMessagesPointer, A3 trim, A4 compaction (#947 p1–p4 on main) Model memory is a different pipe. Compaction does not shrink localStorage, Blob GET, or F5 replay.
Completed-session boot does not attach decideAttachClass → none when not live Idle F5 is hydrate cost, not stream replay.

There is no “hydrate tail page from Blob Range” API and no “skip
reasoning on attach” flag. #924 is a policy change on
kickColdAttach / prefixThroughLastUser / reasoning_delta, not a new
transport.

#924 requirement (non-negotiable)

The spike winner, then the follow-up plan, must deliver all three:

  1. Refresh / cold open of a large session lands on the latest viewport
    (tail)
    — not a chronological replay from the starting prompt.
  2. Thinking rows are not streamed or replayed on hydrate (collapsed /
    omitted until the operator explicitly expands or a new live thinking
    segment starts).
  3. Live attach (turn still running / cancelling) resumes from a cursor
    / tail
    without replaying the whole history at UI speed. Busy + Stop must
    still restore.

Closing this spike’s follow-up plan closes #924. #924 stays open until
that ships and the e2e int rows below are green.

End-to-end integration tests (non-negotiable)

The follow-up create-plan DoD must include durable-turn int coverage.
This is a close gate, same class as “must close #924”: a merge that
flips attach policy without these rows is not done.

Where: int/ (npm run test:int). Load a real protocol-current
harness.wasm via int/loadBridge.ts. Fail closed if Wasm is missing
or stale (throw, never it.skip). Helpers already in tree:
bootFromMemory, hydrateRing, ringTexts, int/stores.ts.

Required rows (map 1:1 onto the three #924 bullets):

# Prove Fail if
1 Live F5 / cold open of a running turn hydrates the tail (Wasm ring is not this-prompt-only) Ring is the starting prompt; chronological replay
2 Historical reasoning_delta is not applied on hydrate; a new live thinking segment after attach still paints Fast-forward thinking on refresh, or live thinking after attach is dropped
3 Stream attach uses resume C (envelope / heap tail), not startIndex=0 when C is known; 'cancelling' still attachable so Busy + Stop restore Cold attach ignores turnStreamCursor; cancel F5 loses Busy + Stop

Keep an idle / completed F5 regression: no stream, latest page (already true
on main; do not drop it).

If leftover 2 (slim local) or 3 (scrollbar / load-earlier) ships in
the same cut, those need int rows too: persist/hydrate of a large transcript
must not require the full snapshot in localStorage; load-earlier prepend
must come from a Blob page, not “host already holds all rows.”

Existing int/f5-attach.int.test.ts currently locks the #859
startIndex: 0 cold-attach contract.
That expect is #924. The
follow-up must rewrite those rows to the new contract (or wrap only the
pre-flip expects in it.fails until the phase that changes them). Do not
leave the old startIndex: 0 assert green as “coverage.”

Do not substitute lib/harnessChat.wasm-int.test.ts (protocol supply) or
app/api/turns/[runId]/stream/route.test.ts (HTTP startIndex parsing)
for this gate. Those stay; they do not prove F5 hydrate.

Likely multi-phase follow-up (not required)

Leftovers 1 / 2 / 3 are separable cuts. #924 (live F5 policy) can ship
without slim-local and without a document scrollbar.

The spike write-up decides whether they are one cut. If they are not,
the follow-up should be a parent create-plan with phase issues (same
shape as A4 #947 → #948–#952) so #924 is not blocked on scrollbar work.
If they are one cut, a single plan is fine.

Multi-phase is the likely shape, not a mandate. Do not invent phases
before the spike names the cut.

Reminder — one GitHub issue per phase / per PR. Invincible ships one
PR per GitHub issue
. If this is multi-phase, each phase MUST be its own
issue
(own DRAFT → HANDOFF-READY → IMPLEMENTED lifecycle, own PR). A
single issue that lists several phases shipping in separate PRs is
forbidden — when Phase 1 is marked IMPLEMENTED the issue closes and
Phase 2 has no home. N PRs → N phase issues plus a parent that is
tracking-only (never implemented). Do not implement this spike, the
parent, or “phase 1+2 on one issue.”

Spike questions (answer these against main)

  1. F5 of a live turn (bug: F5 / reload fast-forward replays entire durable stream (need latest-page hydrate, no thinking replay) #924-1, bug: F5 / reload fast-forward replays entire durable stream (need latest-page hydrate, no thinking replay) #924-3) — Hydrate the Blob/checkpoint
    tail into Wasm (do not prefixThroughLastUser wipe), then
    GET .../stream?startIndex=C for new frames only. Can Busy + Stop
    restore without applying historical reasoning_delta? What is C when
    local turnStreamCursor is stale vs envelope C vs heap 0?
  2. Thinking on hydrate (bug: F5 / reload fast-forward replays entire durable stream (need latest-page hydrate, no thinking replay) #924-2) — Skip reasoning_delta on cold attach,
    or do not subscribe the historical stream at all? Collapsed “thought”
    placeholder vs omit. Must not lose live thinking that starts after
    attach. Today’s lock (“never skip, thinking is not in Blob”) is the bug,
    not a feature to preserve.
  3. Slim local (leftover spike: viewport hydrate — latest-page F5 (#924), slim local, scrollbar (A5 rebase) #553) — Persist only the latest ring window +
    envelope carriers (transcriptPointer, turn ids/status/cursor). Full
    history stays on Blob. First paint reads the slim tail; 409 adopt still
    the latest window. Does Stop persist still need a full snapshot locally?
    Independent of Range; can ship with the F5 fix if the spike says they are
    the same cut.
  4. Load-earlier / scrollbar (leftover spike: viewport hydrate — latest-page F5 (#924), slim local, scrollbar (A5 rebase) #553) — Keep stepping
    session.messages (requires the host to already hold older rows — fights
    slim-local), or host-GET the previous Blob chunk via prev and prepend
    a page, Wasm paints the window only, jump-to-latest is O(1) (re-hydrate
    tail). Preference: feels like one document; almost never scroll to the
    beginning. Do not treat the existing “Load earlier” button as this
    question already answered.
  5. Completed long session — No stream. Blob GET is still the full
    flatten-root JSON (up to 8 MiB) then the host slices 2048. Is reconstruct
    still stuffing the full chain into snapshot.messages before the ring
    slice? Slim-local + chunk walk on demand is the leftover of original
    spike: viewport hydrate — latest-page F5 (#924), slim local, scrollbar (A5 rebase) #553 Q2/Q3.

Dropped as a primary question: HTTP Range over one messages[] object
(original Q1). Whole-object PUT + existing prev/depth already answered it.
The three original Blob options are stale as a “does this exist?” question:
main already picked a hybrid of rewrite-OK flatten (option 2) + mid-turn
prev chunks (a bit of option 3). Remaining work is viewport policy
(#924) + slim local + scrollbar, not a new object layout.

Preference (still)

  • One logical transcript (flatten root + mid-turn chunks), not thousands of
    part/*.json / SQLite-in-browser.
  • Do not want 8 MiB localStorage for first paint.
  • Almost never scroll to the beginning; if A1–A4 work, ask the agent instead.
  • Feature-divide: host fetches, Wasm paints the window only. No dual
    DOM transcript.

Out of spike scope

Suggested next

Spike write-up on this issue (decision table + recommended option) that
explicitly maps to the three #924 expected bullets and names the e2e int
rows above. Then a separate create-plan for the winner; that plan’s DoD
Closes #924 (and this spike) and requires the int rows green.
Slim-local tail can ship with the F5 fix if the spike says they are the
same cut; otherwise expect a parent + phases (likely, not required) —
one GitHub issue per phase, because one GitHub issue per PR.

Peer harness notes

OpenCode before / after / limit + RFC 8288 is a useful host API
shape
if we page Blob chunks — not a reason to shard parts or run SQLite
in the browser.

Pi stream offset via SessionManager is closer to resume at startIndex=C
(already on the wire) than to byte-Range on one object.

References

Activity

  1. btipling commented on Sep 1, 2026

    @btipling
    OwnerAuthor

    User-visible failure mode of slim/first-paint viewport: F5 fast-forward-replays the entire durable stream (prompt → thinking → tools → text) on long sessions.

    Filed as #924 — latest-page hydrate, do not replay thinking on refresh. This spike remains the architecture parent.

  2. changed the title [-]spike: Blob viewport — turn-seam offsets, intelligent scrollbar, slim first-paint local[/-] [+]spike: viewport hydrate — latest-page F5 (#924), slim local, scrollbar (A5 rebase)[/+] on Sep 7, 2026
  3. btipling commented on Sep 7, 2026

    @btipling
    OwnerAuthor

    Rebase lock (2026-09-06, architecture-verified): this spike is scoped to what is still leftover on post-workflow main, and must close #924.

    Shipped (do not re-solve): durable POST /api/turns, attach cursor on the wire, ring window / idle latest-page, mid-turn prev chunks + terminal flatten, A1–A4 model seed. Original Q1 (HTTP Range over one messages[] Blob) is obsolete.

    Leftover:

    1. Live F5 cold-attaches at startIndex=0, prefixThroughLastUser wipes this-run suffix, reasoning_delta never skips → prompt → sped-up thinking → tools → text. That is bug: F5 / reload fast-forward replays entire durable stream (need latest-page hydrate, no thinking replay) #924. Idle F5 already lands on the latest ring.
    2. Full-snapshot localStorage stringify.
    3. Load-earlier is an in-memory button, not a Blob page / intelligent scrollbar.

    #924 stays open until a follow-up create-plan ships the three expected bullets. This issue stays a spike.

  4. btipling commented on Sep 7, 2026

    @btipling
    OwnerAuthor

    Lock (2026-09-07): follow-up plan requires end-to-end integration tests.

    Hard requirements on this spike, same class as “must close #924”:

    1. Close bug: F5 / reload fast-forward replays entire durable stream (need latest-page hydrate, no thinking replay) #924 (live F5 lands on the tail; no thinking replay; resume at C; Busy + Stop still restore).
    2. Green durable-turn int rows (npm run test:int / vitest run --project int) on a real Wasm ring that prove those three bullets. Unit tests of coldAttachFromSnapshot / turnApply are not enough. Existing int/f5-attach.int.test.ts currently locks startIndex: 0 — that expect is the bug and must be rewritten (or it.fails until the phase that flips it).

    Likely (not required) multi-phase parent: leftovers 1 (#924), 2 (slim local), and 3 (scrollbar) are separable. If the spike says they are not one cut, follow A4’s shape (parent create-plan + phase issues) so #924 is not blocked on scrollbar work. If they are one cut, a single plan is fine. Do not invent phases before the spike names the cut.

    This issue stays a spike. The e2e int gate lives on the follow-up plan’s DoD.

  5. btipling commented on Sep 7, 2026

    @btipling
    OwnerAuthor

    Reminder: if the follow-up is multi-phase, one GitHub issue per phase. Invincible is one issue per PR. N PRs → N phase issues plus a tracking-only parent (never implemented). A single issue that ships several phases in separate PRs is forbidden — Phase 1 IMPLEMENTED would close the issue and leave Phase 2 homeless. Same shape as A4 #947 → #948–#952.

  6. btipling commented on Sep 7, 2026

    @btipling
    OwnerAuthor

    A5 spike decision — verified against main (2026-09-07)

    Baseline: b31ca512e7ddee162fa8313cc52f549822e2d23a. Read-only investigation; no prototype, application edits, builds, or tests performed. This issue remains a spike, not an implementation plan.

    Findings that change the implementation, not the product requirements

    1. The three bug: F5 / reload fast-forward replays entire durable stream (need latest-page hydrate, no thinking replay) #924 requirements stand: latest tail on cold open, no historical thinking replay, live cursor/tail attach with Busy + Stop restored. int/f5-attach.int.test.ts still asserts the broken cold-at-zero policy.
    2. A bare envelope turnStreamCursor is not proof that the downloaded transcript contains that prefix. turnApply increments it on events, while assistant text is largely bridge-only until finalization; worker persistStep builds its own checkpoint and turnPersistSeam does not stamp an associated stream position into that object. overlayEnvelopeMeta can put a different tab's cursor onto the downloaded body. Taking max(local C, envelope C) would exchange replay for missing text.
    3. The pinned SDK is workflow / @workflow/core 4.8.4. Its public run.getReadable().getTailIndex() returns the last stored chunk index (or -1 for empty). Verified in the installed matching package's dist/runtime/run.d.ts, run.js, and packaged workflow-api/get-run.mdx; package-lock.json on main pins the same version. No SDK upgrade, guessed API, or new stream transport is needed. Use the actual durable stream position, not an attempted-write counter: turnSseWrite deliberately drops writes after an ambiguous failure.
    4. prev links are overlapping this-run checkpoints, not disjoint history pages. Terminal heads omit prev/depth. Walking prev and blindly prepending rows would duplicate history and would not page a completed flatten root. Preserve the logical transcript and reuse its reconstruction/merge semantics on the server; expose logical row windows over it. This reduces browser transfer/heap, not necessarily server Blob-read cost. Physical indexing/Range is not being smuggled into this plan.

    Decision table

    Concern Options Winner / reason
    Cold attach replay at 0; trust naked envelope C; use paired checkpoint + fixed stream fence Paired checkpoint + fixed fence. Capture the real next stream index with a persisted display checkpoint at an existing serial persist boundary. On cold open, restore that checkpoint, reduce only its uncovered suffix up to a captured tail fence server-side, omit reasoning, hydrate the resulting tail once, then subscribe at the fence. Never wipe to the prompt.
    Old/mismatched checkpoints blindly skip at C; fabricate a complete snapshot; compatibility recovery Read-only compatibility recovery. Keep the visible tail; reconstruct this-run display through a fixed stream fence on the server, suppress thinking, and publish only a viewport result. A legacy scan may start at 0 server-side, never as browser SSE replay. Failure leaves the current view and an honest retry state, not an invented cursor.
    Thinking hide all future reasoning; replay then collapse; omit historical data Omit historical reasoning entirely. Track segment boundary without retaining its body; a reasoning segment already open at the fence stays omitted until a real boundary. New post-attach thinking still paints.
    Slim local same full stringify; cache latest display rows with explicit partial marker Versioned partial cache, independently shippable after the F5 flip. Full history remains cloud-owned; a cache cannot become a full-record upload or model seed.
    Earlier history in-memory slices; raw prev prepend; logical windows backed by Blob Read-only logical windows over existing Blob objects, with generation-bound row coordinates. No new physical object layout.
    Scroll UX keep button as finished feature; second DOM transcript; Wasm document navigation Wasm window navigation: boundary-driven older/newer fetch, stable row anchor, document-position scrollbar (ordinal estimate, not a fake pixel-perfect size), jump-to-latest. Host fetches; canvas paints.

    Recommended cut — four phases, four PRs

    These are not one cut. #924 must not wait for cache or scrollbar work.

    1. Server checkpoint/cursor pairing + bounded tail handshake. One foundational PR; does not claim bug: F5 / reload fast-forward replays entire durable stream (need latest-page hydrate, no thinking replay) #924 fixed before the host uses it.
    2. Host cold-attach flip + real-Wasm e2e close gate. One PR, depending only on phase 1. This PR closes bug: F5 / reload fast-forward replays entire durable stream (need latest-page hydrate, no thinking replay) #924 only when the three required durable-turn int rows pass: tail retained through boot/attach; historical thinking absent and new live thinking present; verified resume C/fence used with running/cancelling Busy + Stop. Rewrite the old startIndex=0 expectations. Keep idle/completed, stream-drop, Stop, queue, malformed Blob, and race regressions.
    3. Slim local cache + round-trip/incomplete-write guards. One PR, depending on phase 2. Real-Wasm int rows prove large-session restore without full local JSON and that a partial cache cannot truncate the cloud transcript.
    4. Blob-backed history windows + intelligent Wasm scrolling. One PR, depending on phase 3. Real-Wasm int rows prove network-backed earlier pages with an initially tail-only host, bounded ring, stable range/anchor, and jump-to-latest; browser smoke proves visual scroll behavior.

    Published follow-up DRAFT tracking-only parent: #958, with one issue per phase: #959 → #960 → #961 → #962. Neither this spike nor the parent is implemented. Each phase owns its tests and living-doc changes in its own PR; no final docs-only phase to defer correctness. The final phase closes this spike after all three leftovers are delivered; phase 2 may close #924 earlier with its evidence. Planning does not close either issue.

    Locked constraints for the follow-up

    • A1–A4 seed/compaction untouched; display windows never enter the model fold.
    • Ring 2048, earlier step 512, transcript object 8 MiB, Function body 2 MiB under the real 4.5 MB wire, meta 1 MiB, prev walk 256, and turn-wall/step limits unchanged. New viewport/cache-only ceilings must have their own caps rows and may not silently redefine storage retention.
    • No historic thinking bodies in the cache, transcript, or hydration response; no DOM chat/scrollbar; no inference/tool execution during recovery.
    • Worker durability remains authoritative. A partial viewport is read-only; no host flatten from a cursor-only or tail-only snapshot, including Stop, terminal error, 409 adopt, or model/effort changes.
    • Existing int-durable CI runs npm run test:int on a real protocol-current Wasm artifact, failing closed when absent/stale. Unit tests alone cannot close bug: F5 / reload fast-forward replays entire durable stream (need latest-page hydrate, no thinking replay) #924 or this spike.
    • No Production migration/backfill/env cutover. Existing Git deployment, Actions test/artifact supply, and hosted browser smoke; no laptop ops.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions