mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 17:52:49 +00:00
* 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:
+15
-8
@@ -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
@@ -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
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user