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.

plan: A5 viewport — phase 4 — bounded on-demand history and Wasm navigation #962

Description

@btipling

Plan header

Field Value
Status HANDOFF-READY — implement after #961 lands
Date / type 2026-09-07 /phase4, one issue per PR
Parent / source / dependency #958 /#553 /#961
Baseline / branch main b31ca512e7ddee162fa8313cc52f549822e2d23a, then dependencies; feat/a5-bounded-history-navigation
Layers / reusability Backend scoped one-object reads, host async controller, Wasm UI; existing Blob provider seam
Administrative mutate / cloud path None; existing Git deploy + build-harness→artifact→Vercel, int-durable
Living docs Session/stream/limits/feature-divide, AGENTS, README, native protocol guide, SECURITY

Review notes (2026-09-07)

Operator chose bounded cost and useful best-effort history over exact preservation. This replaces the global logical-history/overlap engine and queue-command transaction from the old draft. Queue/preferences are already handled by #960. This phase reads one existing Blob object per page request and builds useful Wasm navigation over those pages, without promising exact global history order/count/height.

Finding Resolved decision
Exact global merge consumes too much CPU/memory Per-object raw-row coordinates and explicit earlier-snapshot boundary; no full-chain reconstruction
Existing Load earlier only slices resident array Actual authenticated object-page fetch; fresh host starts tail-only
Global scrollbar implies an exact document Scrollbar position is approximate within loaded source snapshot, older/newer snapshot controls explicit; latest jump always direct
Streaming updateLast corrupts viewed old page Separate latest bounded buffer and visible history ring; no live bridge mutations to old rows
Ambiguous duplicate old snapshots Bounded exact (role,text) repeat suppression; omissions/repeats acceptable, progress always based on source indices

Scores: correctness4, performance5, architecture4, testing5, security4, reusability5, parent adherence5, layers5, docs5, cap governance5, UI/layout4. Administrative cloud ops N/A. HANDOFF-READY after dependency. No code/tests run in review. Existing sessionWindow, host hydrate/apply, Blob scope/prev semantics, ui/state/scroll, bridge/build/CI and palette baseline read.

Goals / scope / ownership

Concern Files
Page service/API #959 lib/sessions/viewportRead.ts, new pure lib/sessionHistoryPage.ts; app/api/sessions/[id]/viewport/route.ts
Host controller/cache new lib/viewportNavigation.ts, lib/sessionRepository.ts, lib/sessionWindow.ts, HarnessHost.tsx, turnApply.ts
Canvas geometry/navigation new native/harness/src/ui/history_navigation.zig, ui/{state,scroll}.zig, ui.zig, bridge.zig
Bridge supply lib/harnessBridge.ts, native/harness/build.zig, protocol tests
Int new int/viewport-pages.int.test.ts, shared driver/loadBridge

No worker storage/inference changes, physical layout/index, source-span rope, byte-Range JSON, global exact total, queue mutation endpoint, full-history rehydration or new DOM UI. Do not resurrect old parent precision gates.

1. Per-object page protocol

Extend existing latest JSON route:

GET /api/sessions/:id/viewport?objectId=<id>&beforeRow=B or ...&atRow=R.

  • Auth owned session every request. Validate object id scope with existing isObjectIdBoundTo; never client URL, SDK historical run or forged cross-session pointer. Current/previous owned transcript object ids permitted, like existing signed-object read seam; possession of a foreign id never grants access.
  • One Blob read per request, max existing8 MiB object; no prev read in the same request. Optional read work uses #9595s budget; timeout/error leaves current view, no automatic retries.
  • Coordinates are raw source array boundaries, not global canonical ordinal, SSE cursor, or number of coalesced rows. beforeRow=B examines up to512 raw rows immediately before B; default B=messages.length. atRow=R examines up to512 starting at R, clamped to valid range. Unknown/conflicting selectors400.
  • Scan candidates newest-first where choosing a tail; validate roles/text, project/excerpt to existing bridge bound and snapshot byte budget. If some candidates are invalid/duplicate/overbudget, return fewer rows and the next raw boundary reflecting examined/consumed candidates. Never return the same cursor repeatedly. A maximum-size single row gets a labeled excerpt; no silent zero-progress omission loop.
  • Return {version:1,objectId,rows,sourceRange:{start,end,totalRows},nextBeforeRow,prevObjectId?,historyComplete:false,incomplete:true} plus source-index key on each row/group. totalRows is this object's array length only. prevObjectId is syntax+scope-checked before returning, not fetched. A true flattened root has no prev.
  • At boundary0, next explicit older action may fetch prevObjectId. Never automatically walk the chain until unique rows fill a screen. Show a neutral “Earlier stored snapshot; some rows may repeat” boundary label when moving objects. Empty/all-duplicate page advances cursor but doesn't automatically chase another page without new user scroll/action.
  • Host records visited ids up to existing256 walk guard and refuses loop/repeat traversal while retaining current paint. Missing/unreadable/foreign source uses typed error/stale state, not empty successful complete history. Standard401/404/400/503, no payload logging, private/no-store.
  • No persisted generation manifest or body digest. Append-only source assumption follows production Blob; if a BYO object changes, range clamp/restart keeps UI usable, exact stale-version detection not guaranteed.

Small duplicate/group handling

Use bounded currently-held latest/viewed + arriving candidates for exact same (role,text) display suppression; preserve raw source cursor even if all dropped. Same-text legitimate messages may collapse and some encoded tool groups may repeat; accepted. Do not invoke prefix/suffix legacy matcher. Hash/index only bounded candidate strings, no global dedup set.

Coalesce only within a returned page using existing tool encoding rails and retain source-span anchors; don't merge across page/object boundaries and invent global ids. Stable display key {objectId,rawStart,rawEnd}; if byte fitting excerpts text, keep key and flag. No exact full document row count required.

2. Host window controller

State: {sessionEpoch,viewedObject/range,latestWindow,viewedWindow,followLatest,pendingTarget,loading,error,visitedIds}. One active page/bootstrap request per epoch; coalesce desired seek, abort/ignore obsolete response. Keep at most two bounded2048-row windows with total4 MiB serialized accounting. Incoming temporary page released after bounded merge, no all-pages cache or closure referencing prior full Blob arrays.

  • Following latest: plan: A5 viewport — phase 2 — usable tail-first F5 and real-Wasm #924 close gate #960 live events paint normally and maintain bounded latest state.
  • Reading history: live reducer updates latest buffer without calling bridge updateLast/push on the historical ring. Use a narrow display sink/suppress-paint adapter, not a second entire HarnessBridge implementation. Parser finalText/assistant/tool/thinking buffers also bounded to their existing display limits; do not retain whole turn through a hidden accumulator.
  • History rows are never submitted as promptHistory or full PUT. Queue/preferences keep plan: A5 viewport — phase 2 — usable tail-first F5 and real-Wasm #924 close gate #960 metadata-only path; no new persistence API here.
  • Before paging/window shift capture first visible source key + intra-row pixel offset. Preserve overlapping anchor; on explicit seek anchor target. If anchor absent due approximation/dedup, nearest remaining row/edge is acceptable and marked page boundary; no empty/frozen UI trying to prove exactness.
  • Jump latest invalidates pending page fetch and swaps a valid latest buffer or makes one plan: A5 viewport — phase 1 — bounded best-effort tail read and snapshot-first attach #959 tail acquisition; no repeated forward walk. Sending actual prompt follows latest as current product behavior. Browsing never cancels inference or loses Stop/pause/FIFO.
  • Local v2 saves latest buffer, not viewed old page. New/Clear/switch reset visited state, ignore late response, preserve current selected-session queue semantics.

3. Wasm navigation / bridge

Use next unused protocol version (baseline24; choose next free at implementation) and paired required exports/TS wrappers/build whitelist. Add scalar state/request/anchor exports:

  • setHistoryWindow(start,end,totalRows,loading,hasEarlierSnapshot,followLatest) for current source object's raw coordinates.
  • pending request kind older/newer/seek/latest + target raw ordinal; host polls/acks. Existing Load earlier export becomes alias to this controller, not separate in-memory implementation.
  • first-visible row/source anchor readback and pending anchor restore fields. Host supplies row source-index metadata as bridge write-time scalar entries; no arbitrary object id parsing in frame. Apply anchor after row geometry is measured, then clamp.

Boundary wheel/touch/keyboard issues a single request while more is available; loading/ack prevents repeated frame requests. Explicit earlier-snapshot/retry/latest controls keep navigation possible if empty/duplicate content has no scroll range. Newer can return within current object or the most recent bounded visited source reference; no full forward history reconstruction. Preserve existing browser-reserved keyboard keys.

Use a scrollbar/position control for current snapshot's raw range, clearly not a global archive total. Drag maps to raw row seek, local scroll stays pixels. Loaded-page/source boundary cues communicate approximation. Variable-height rows/images/math/expansion/resize remeasure and restore anchor; exact offscreen height not promised.

Palette from existing TS/Zig modules: TEAL chrome, WARM only intentional emphasis, EMBER actual errors. Incomplete history/boundaries neutral System/TEAL. ≥existing44px touch target metrics, ~390px completion, composer/Send/Stop/status fixed across loading/error. Preserve draft/focus, FIFO/pause/promote. No app GPA allocation or network in ui.frame; metadata decoding/work at bridge write time, fixed buffers/per-slot geometry. Small source-index arrays sized to existing ring, not history size.

Caps

New performance-first defaults authorized by operator; no existing cap is changed.

Cap / carrier Value / rationale Location
Page candidates Existing512 raw source rows/request HISTORY_PAGE, page service; output may coalesce/drop
Source reads #959 one object/request,5s optional read budget No auto ancestor loop; doesn't change existing256 legacy reconstruct rail
JSON snapshot/control #9592 MiB full serialized Below4.5 MB Function ceiling; all metadata/escaping counted
Object/row Existing8 MiB /262144B display Source untouched; labeled excerpt
NEW VIEWPORT_CACHE_MAX_BYTES 4 MiB serialized, at most2×2048 windows latest+viewed; JS/temporary decode overhead separate
Visited source ids Reuse existing256 chain guard IDs only, no retained bodies; stop cycles
NEW bridge row scalar u32 max4294967295 Per-object coordinate representation; unsigned JS normalization; never truncate source to fit scalar
Local cache/tool/queue/turn rails Existing/#959–#961 values unchanged No new retry/turn/model/queue limits
Demand One in-flight per epoch Latest target coalesces; late results discarded

Server cost per page linear in one object plus at most512 candidates, no exact overlap CPU. Browser may reread same8 MiB object for multiple pages; acceptable initial cost, no cache/index subsystem prerequisite. Measure calls/bytes to detect accidental full-chain work, not demand perfect latency.

Test matrix

# Required case / evidence
P1 Flatten root and overlapping prev-bearing heads: correct per-object raw ranges, no hidden chain merge; user-driven prev only
P2 before/seek bounds, malformed/duplicate/overbudget rows, Unicode/oversized single row: always source-cursor progress or explicit boundary/error
P3 Auth foreign object/session, missing/deleted source, loop/max visited, read timeout: no leak or auto chase, current ring retained
P4 Tail-only host older request→actual scoped page route/Blob read→real Wasm ring; spy full-history repo.get never called
P5 Repeated page/seek/latest with bounded windows, exact-repeat suppression, empty duplicate page explicit action; no unbounded request loop
P6 Live text/tools/thinking while old history viewed, Stop/cancelling/queue; viewed ring unchanged, latest advances, raw cursor unaffected
P7 Anchor/source-index bridge, geometry reset/resize/expansion, pending request ack; pure Zig module test-rich + real bridge readback
P8 Source switch/New/Clear/late response and local cache latest-not-viewed; partial view cannot full-PUT or seed promptHistory
P9 #924 F5 real-Wasm and cache int rows remain green
P10 npm test, npm run test:int, npm run typecheck, npm run build; build-harness test-rich/build+current required exports
P11 Matched PR/main artifact supply via int-durable/Vercel; mismatched/missing Wasm fails closed
P12 Hosted desktop/~390px touch/wheel/keyboard seek/retry/latest, draft/focus, composer/status/Stop stable; no DOM transcript

All delivered tests ordinary green, no skipped/expected-failure mask. Browser visual smoke is additive; unit page slices alone don't prove network→controller→real ring. No tests run during review.

Docs / ops / order / DoD

Surface Required current-behavior update
Session guide Per-object approximate history and duplicate/gap behavior, live/latest separation and failures
Stream guide Live buffer while viewing history, raw cursor independent of page indices
Feature-divide Wasm owns controls/scrollbar, host fetches scoped pages
Limits One-object cost,512 candidates, two-window bytes, no global count/height claim
AGENTS New controller/bridge/page ownership and bounded best-effort rule
README Concise on-demand history feature/link
SECURITY Per-request owned-session/scoped-object checks, no writes or keys
native/harness/README.md Actual next-version exports, page source indices/anchors/navigation
.env.example N/A no secret/env/feature flag

No admin migrate/backfill/index/cleanup job. Use existing configured runner build-harness for Zig, same-repo PR artifact→int-durable and main artifact→Vercel. Paired host/Wasm rollback; no transcript deletion/key purge/laptop steps. Docs timeless, not phase diary.

Implement page protocol/service/tests → bounded host controller/display sink → paired bridge/Wasm requests/anchors/control → real-Wasm page/live integration → docs/full gates/hosted smoke. One PR only.

  • G1–G4/P1–P11 green and P12 recorded; actual cloud-page→real ring, finite cost.
  • No global exact matcher/index, no auto full-chain chase, no head-only source falsely called full history.
  • Live cursor/Stop/FIFO/draft safe while history read; partial rows never uploaded/seeded.
  • Approximate seek/anchor and boundary/retry/latest usable on desktop/mobile; palette/geometry/frame budget intact.
  • New paired protocol and current artifact gates, docs complete.
  • Prior children shipped; PR Fixes #962 and Fixes #553 after all three leftovers delivered; Refs #958 #924. Parent closes separately as tracker.

Open questions: none. Accepted tradeoffs: same object read on multiple page requests, overlapping snapshots may repeat/drop some rows, approximate per-snapshot rather than whole-archive positioning, source unavailable stops earlier navigation. These are the approved sensible defaults, not grounds to block for a perfect archive.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions