Revert #4421 and narrow the cache restore fix (#4436)

* Revert "Restore cached conversations before reconnecting (#4421)"

This reverts commit 1c51b52d07.

* fix(app): restore provider loading on native

* fix(app): preserve native navigators across appearance changes

Reapply the isolated appearance boundaries and native menu transform origin from #4421 so appearance hydration preserves native navigator and gesture host lifetimes.

* fix(app): retain cached baselines during live updates

Read the timeline concurrently with agent preparation and reconcile display-only cache rows beneath the live head using the existing reducer. Merge cached directory entries beneath live state while preserving the existing checkpoint race guard.
This commit is contained in:
Mohamed Boudra
2026-09-07 19:18:54 +02:00
committed by GitHub
parent 9c8d775895
commit c46aee4e6a
52 changed files with 3422 additions and 8638 deletions
+15 -8
View File
@@ -113,14 +113,21 @@ Cross-platform React Native app that connects to one or more daemons.
- Timeline sync correctness is documented in [docs/timeline-sync.md](timeline-sync.md): live streams are for immediacy, `fetch_agent_timeline_request` is authoritative, and catch-up is paged but complete.
- Voice features: dictation (STT) and voice agent (realtime)
Consumers request data from host-lived owners without choosing memory, disk or network. Cached rows
satisfy the same readiness projection as network rows, including offline. Directory restoration
merges under live/local changes; network refresh waits for that baseline. Only completed catch-up
advances the directory checkpoint because a live sequence can follow an offline gap. The owner
retains surface demand across reconnects; React does not track connection generations. Storage
commit and scoped-read guarantees belong to [data-model.md](data-model.md#replica-row-store);
timeline display, resume and lifetime contracts belong to
[timeline-sync.md](timeline-sync.md#client-replica-lifetime).
Consumers request directory or timeline data without choosing memory, cache, or network. The owner
publishes an accepted cache hit and then reconciles it over the existing network path. A miss or an
invalid row uses that same path. Offline demand still publishes an accepted cache hit and defers
network reconciliation. Cache loading is demand-driven: opening a chat reads its agent row and
focused timeline row, plus the workspace and project rows needed by the route; opening a directory
reads directory rows for that host and establishes its live subscription when connected. Accepted
cached rows satisfy the same consumer-readiness projection as network rows. Host registry startup and
host connection do not create directory demand or install replicas. The directory owner retains
declared surface demand and re-establishes network reconciliation after reconnect; React does not
track connection generations. Late cache reads cannot replace state already advanced by live or
authoritative network data. Owners explicitly persist accepted commits; directory rows and their
checkpoint share one storage transaction. See
[data-model.md](data-model.md#replica-row-store)
for the storage shape and [timeline-sync.md](timeline-sync.md#client-replica-lifetime) for timeline
resume behavior.
The three directory entity types have independent monotonic sequences and share one daemon
generation. The daemon retains only the latest projection per entity and bounded tombstones, not an
+7 -11
View File
@@ -578,20 +578,16 @@ compound key `(serverId, kind, id)`; kinds are `agent`, `workspace`, `project`,
uses the singleton id.
The store is a typed persistence boundary. It returns values to directory and timeline owners and
accepts their explicit commits; it never reads or writes UI state.
One invalid row is deleted and returned as a miss without affecting other rows.
accepts their explicit commits; it never reads or writes UI state. Reads are scoped to the requested
host, kinds, and ids. Opening a cached workspace uses exact workspace and project keys rather than a
directory scan. One invalid row is deleted and returned as a miss without affecting other rows.
An invalid directory row and its affected checkpoint cursor are repaired in one transaction, so a
later launch cannot accept a checkpoint for a partial baseline. Directory changes and their
checkpoint are also applied in one transaction. Cold reads query only the requested host's directory
or agent timeline. Accepted commits overlay those reads, including changes accepted or persisted
while a read was in flight. Host removal invalidates old reads; reconciliation exposes moved rows
before the disk rename finishes. Disk is write-behind: a change stays pending until the disk
confirms it, so failed writes preserve accepted values. Timeline eligibility is checked without
serialization; the coalesced write encodes the latest accepted page once.
checkpoint are also applied in one transaction.
The cache is capped at 32 MiB and evicts whole hosts in least-recently-written order. The first
write loads the global row index and accounts for its bytes; requested reads do not await that
work. Later writes update the index. The row store is not encrypted. A cached timeline can contain
The cache is capped at 32 MiB and evicts whole hosts in least-recently-written order. Budget
bookkeeping may scan opaque row sizes during a deferred write, never during host registry startup or
before a requested cache row can paint. The row store is not encrypted. A cached timeline can contain
source code, prompts, and tool output; encrypted-at-rest storage is a separate security decision.
### Draft Store
+38 -72
View File
@@ -36,24 +36,11 @@ Large unbounded timeline responses can exceed relay frame limits, so catch-up us
Page limits are projected-item targets. A tool call lifecycle is one projected item even if it spans many source sequence numbers, and assistant/reasoning chunks are merged before counting. The response carries `seqStart`, `seqEnd`, `sourceSeqRanges`, and `collapsed` so clients can advance sequence cursors without rendering delta rows.
When live delivery detects a sequence gap, the app fetches `direction: "after"` from its current
cursor and remembers the sequence the live row reached. If the daemon responds with
`hasNewer: true`, the app immediately fetches the next page from `endCursor`. A catch-up is
complete only when the daemon reports `hasNewer: false` and the accepted coverage has reached
every sequence the live stream reported while the catch-up ran: a page the daemon built before
those rows existed cannot settle them, however it was requested, so the owner continues from the
cursor that page established. One catch-up runs per agent; a gap observed while it is running
raises its obligation instead of starting another request. A non-advancing reply cannot disprove
activity observed while the request was in flight. A newly raised obligation starts the next
page immediately, without an error. A reply that makes no progress against an obligation already
known when it started retains that obligation and uses bounded error backoff. Cancellation (the agent leaves view,
the connection drops) forgets the observed head; the next catch-up starts from the cursor and the
daemon returns everything after it.
When live delivery detects a sequence gap, the app fetches `direction: "after"`. If the daemon
responds with `hasNewer: true`, the app immediately fetches the next page from `endCursor`. Gap
recovery is complete only when `hasNewer: false`.
Initialization timeouts guard lack of catch-up progress, not the full multi-page sync. A successful page that queues the next `after` page refreshes the watchdog.
Response completion is independent of row admission: an obsolete tail can finish its initialization
without replacing newer accepted rows. Requests carry the existing init-promise identity, so an old
response cannot settle a replacement initialization.
Opening or resuming an agent fetches one bounded latest tail page. Older history remains
user-driven by scrolling upward.
@@ -95,9 +82,7 @@ authoritative range first.
that cursor are applied. Already-covered rows are not replayed.
- A true middle gap, epoch change, or rewind atomically replaces stale canonical history with the
latest tail. The replacement reconciles positioned live rows beyond its coverage and unresolved
local submissions; it never retains two discontiguous canonical ranges. A tail whose window ends
behind the local cursor is a rewind, so it supersedes every positioned row up to that cursor
rather than only the positions it carries; a gap replacement covers only what it carries.
local submissions; it never retains two discontiguous canonical ranges.
The installed tail carries `hasOlder`, so history skipped by a replacement remains reachable through
ordinary backward pagination. A backward page is accepted only when it is adjacent to the current
@@ -105,51 +90,39 @@ history start; a response requested from a pre-replacement range is stale and is
## Client replica lifetime
`timeline/replica.ts` is the host-lived timeline write authority. Cache restore, pages, live batches,
submissions and removal publish atomically into the existing Zustand read projection; unchanged
facts retain their references. `viewed-timeline-sync.ts` owns demand and fetch scheduling, available
before a network client so saved history can paint offline. Matching display and verified inputs
share one reduction. Fully covered identified text adopts canonical row identity and metadata;
partial coverage preserves live rows and newer text. Persistence uses the accepted transition,
never mutable UI state.
The session projection remains host-scoped for as long as the host is registered. The viewed-timeline
owner wraps cached preparation, network catch-up, accepted timeline application, and persistence
behind one interface. React supplies transport and projection operations without selecting a cache
path or issuing a separate persistence notification.
Unpositioned live updates do not revoke known in-session source coverage or sequencing: current
daemons also emit out-of-band timeline updates. They can make the restart page display-only, which
grants no resume coverage when reopened. Display pagination remains available, and older pages
cannot certify unpositioned content. Positioned activity ahead of incomplete catch-up preserves the
preceding certified baseline. A cold overflow awaiting its latest tail still accepts live observations;
only an intentionally detached, synchronized history window excludes them.
Epoch changes invalidate old display positions; authoritative resets, including empty windows,
invalidate the old durable baseline. A delayed old-epoch cache cannot replace newer live display.
Removing the host from the registry is the destructive boundary: it stops the runtime and clears the
session and host-scoped setup state together.
The restart window holds at most the latest40 complete entries in source-start order. Loading older
scrollback cannot populate or enlarge it, even when projection merging leaves a short page or its
certificate has been withdrawn. Retained messages can gain complete continuations; overlapping
older tool lifetimes do not pull earlier entries into the window. `startSeq` identifies the source-start
boundary for `before` pagination; `endSeq` is the independently established synchronization position
for `after`. A discarded older tool may finish last, so the latter can exceed every retained row's
completion position. `hasOlder` preserves access to omitted history.
The timeline owner asks durable replica storage for an agent when that agent becomes visible. An
accepted row paints immediately before subscription acknowledgement or timeline fetch. The stored
range describes those exact items: `startSeq` drives older pagination and `endSeq` drives forward
catch-up. The owner requests `after endSeq`, and requests `before startSeq` when the user loads older
history. Code outside the owner does not distinguish cached and network timelines.
Cache values without source provenance paint verbatim but cannot authorize resume, including after
new live activity. Canonical replacement restores certification through the ordinary bounded tail
bootstrap; no local sorting or text deduplication repairs saved rows. Disjoint retained ranges keep
the preceding certified baseline. The codec preserves complete text, source chunks and attachment
references; unreconciled local submissions and UI sync generations are not durable. Cache budgeting
and write-behind guarantees belong to [data-model.md](data-model.md#replica-row-store).
The first resume request is bounded. If it reports more newer history, fetch one latest bounded tail
instead of replaying every missed page. Live gap recovery still pages forward until current.
Preparation keeps one promise per agent lifetime. Host identity transfer immediately republishes
retained state; a pending read repeats only when identity changed during that read, because physical
rename may have retired its key. Settled transfers perform no storage read. Directory scope exclusion
ends the lifetime and rejects delayed work but retains the saved timeline; only explicit deletion
removes it. First admission preserves in-flight work; re-admission begins a fresh lifetime. Retained
timeline data alone does not provide offline archived-route metadata.
If the canonical window exceeds the cache item limit, contains a discontiguous retained range, has a
live head, or includes presentation data the cache cannot encode losslessly, persistence drops the
range and keeps a display-only tail. Never slice items while retaining the pre-slice range; that
falsely certifies discarded source rows. A display-only row paints without granting synchronization
authority, so the owner uses the ordinary bounded `tail` bootstrap.
DirectorySync coalesces fetch and acceptance into one operation. The first resume is bounded: a reply
with more history leads to one latest tail, while live gap recovery still pages to completion. A tail
passed by activity accepted during its request is obsolete; a tail behind the request's starting
cursor demonstrates rewind. Client window-replacement intent remains separate from daemon reset.
Covered canonical positions replace matching display history, while positioned rows beyond the page
survive as an unverified overlay. Submission continuity follows the rules below.
Live rows received between cache paint and catch-up stay in the separate live head and reconcile with
the authoritative range through the existing forward-page path. The cache does not persist sync
generation or unreconciled local submissions.
Every daemon-derived live item carries its timeline epoch and sequence position. Bootstrap
replacement keeps only positioned rows newer than the page it installs, while unresolved local
user presentations identify themselves by having client identity without a timeline position. This
prevents a page from duplicating rows it already covers without coupling display continuity to the
shorter-lived submission registry. Unreconciled local presentations are not persisted in the durable
replica cache.
## Selective and legacy delivery
@@ -171,18 +144,12 @@ version.
## Projected pages reconcile with live presentation
A projected page is canonical state, not live deltas. Positioned rows carry their source start and
text chunk offsets so page reconciliation replaces covered text while retaining newer chunks.
Tools keep their first-appearance order even when their completion arrives later. Both display lanes
share this source order; a missing canonical unit lands at its source start.
Message identity alone does not identify a segment: tools can divide same-ID text, and markdown can
split one segment into several rows. For unpositioned text, reconcile only a uniquely matching
contiguous segment using the same markdown boundaries as live rendering. A lagging canonical prefix
leaves newer text unpositioned. Known tool positions constrain ownership; identical unpositioned
segments without a distinguishing position remain ambiguous and are preserved. Text alone never
establishes message identity. Anonymous rows retain the conservative cursor-straddle fallback.
Insertion must not merge an identified segment whose ownership reconciliation already declined.
A projected page is canonical state, not a sequence of live deltas. One projected item can overlap
rows already received live—for example, a tool call retained at its original display position while
its completion advances `seqEnd`, followed by a merged assistant message. The app uses
`sourceSeqRanges` to replace overlapping assistant and reasoning projections before applying the
remaining page through the existing stream reducer. It must not append full projected text to a
live prefix.
Every path that sends a message to an agent — composer send, dictation accept-and-send, queued
send-now, and the host runtime's automatic queue drain — goes through
@@ -263,7 +230,6 @@ canonical assistant prefix, it stays in the head lane. No row may be returned in
- Server live stream forwarding: `packages/server/src/server/session.ts`
- App sync planning: `packages/app/src/timeline/timeline-sync-plan.ts`
- App timeline write authority: `packages/app/src/timeline/replica.ts`
- App viewed-agent synchronization: `packages/app/src/timeline/viewed-timeline-sync.ts`
- App stream/timeline reducer: `packages/app/src/timeline/session-stream-reducers.ts`
- Session wiring: `packages/app/src/contexts/session-context.tsx`