* feat(hub): guide starter setup (#3651) * feat(hub): guide setup with provider IDs * fix(hub): preserve login setup state * feat(hub): select starter agent runtime Use the connected daemon's provider snapshot so generated Hub bundles only contain complete provider, model, and mode combinations. * fix(hub): stage starter runtime choices Keep providers with valid modes selectable when the daemon has no default mode, so users explicitly choose the complete runtime. * fix(hub): scope runtime choices to workspace Provider capabilities can vary by repository, so request the snapshot for the bundle directory that guided setup will deploy. * fix(server): stop short start gates from undercutting provider startup budgets (#3621) * fix(server): stop short start gates from undercutting provider startup budgets Agent creation still failed on contended cold starts after #3578, because two shorter gates on the same path fire before the event-stream readiness budget can apply. A traced creation reached session.create twelve seconds in, past the 10s session.create gate, and the agent was then killed by a bare "timeout" at fifteen seconds from AGENT_RUN_START_TIMEOUT_MS while the SSE connect was still running. A run-start bound of 15s wraps a path that is deliberately allowed 30s, so the outer gate always wins and the inner budget is unreachable. Give session.create the shared OPENCODE_SERVER_STARTUP_TIMEOUT_MS budget, since creating the first session for a directory is part of OpenCode coming up, and derive its error message from the constant instead of hardcoding "10s". Raise AGENT_RUN_START_TIMEOUT_MS to 60s with a comment recording that it must stay above the slowest provider startup budget; it stays a literal because that module is provider-agnostic. Related to #3571. * test(server): drive the run start budget through a real AgentManager The run start boundary tests replaced AgentManager.waitForAgentRunStart with a bare timer, so they never exercised the production run-state transition or the agent_state subscription the real wait depends on. That path could regress while the tests stayed green. Build the scenario on a real AgentManager with a provider session that holds its turn start open for a configurable span, so pendingRun.started, the lifecycle change to "running", and the agent_state notification are what the wait actually observes. Budgets stay independent literals and the settled-before/after assertions are unchanged. * fix(server): describe agent run start timeouts --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> * fix(relay): validate handshake keys (#3669) * fix(files): keep large source previews responsive (#3665) Large read-only files bypassed the editor's virtualization and eagerly mounted every highlighted token, which could stall the renderer for seconds and consume gigabytes of memory. Route source previews through bounded platform renderers and reject reads above their display budgets before accumulation. * fix(hub): make guided setup choices actionable (#3677) Show a concise login success state, scope starter choices to available Hub connections, wait for daemon capabilities, and avoid partial starter bundles when setup cannot proceed. * Keep released agent SDK versions reproducible (#3678) Pin the validated provider and protocol SDK dependency pair so fresh installs cannot silently float after a Paseo release. Refresh the Nix npm dependency hash for the resulting lockfile. * feat(app): refine agent profile creation (#3680) Keep profiles as the richer primitive while restoring the familiar star cue and clearer guidance for agent selection. Account for borders in shared field geometry so controls preserve their declared heights across form consumers. * fix(app): restore native composer auto-growth (#3681) Native multiline inputs were always scrollable, which kept iOS content-size measurement pinned to one line. Keep native sizing intrinsic below the cap and enable internal scrolling only after the composer reaches it. * docs(hub): document zero-friction onboarding (#3657) Lead with the browser-to-CLI onboarding journey, document the generated starter bundle, and align guided setup with available Hub connections and daemon-reported runtime defaults. * Surface workspace changes from agent panes (#3682) * feat(app): refine workspace pane actions Prioritize Changes when available, keep Files useful before a selection, and reserve desktop diff clicks for selection so comments require the explicit affordance. Move the Changes shortcut to Shift+G to avoid consuming a likely copy variant. * feat(app): improve access to workspace changes * Expand workspace panel actions and keyboard navigation (#3685) * fix(app): keep file navigation in place Retargeting a File tab allocated a new tab identity, causing the tab track to unmount and remount during navigation. Preserve the identity for File-to-File changes while retaining new instances for cross-kind replacements. * feat(app): expand workspace panel actions Keep the Command Center aligned with workspace tab and pane primitives. Add explicit panel placement, focused open behavior, keyboard navigation for empty panes, and a Changes pill that toggles the visible Side panel without closing its tab. * Restore compact and stable chat timelines (#3716) Restore compact message and footer spacing, reserve independent composer-pill clearance for transcript content and floating controls, and keep visible web tool-call expansion from shifting the reading position. * perf(app): defer replica persistence until user inactivity Whole-replica capture and serialization blocked the JS thread during navigation and streaming. Defer that work until the user has been inactive while allowing long-running streams to persist without becoming the idle signal. * Give every pane a browser-style New tab (#3715) * feat(app): replace empty panes with New tabs Give every retained pane an independently identified New tab, replace it in place when a destination is chosen, and preserve split close and move invariants. Keep existing workspace startup and compact explorer routing explicit, with focused unit and browser regression coverage. * test(app): count content tabs in subagent integration * fix(app): preserve workspace shortcut ownership * fix(app): keep compact model options scrollable The compact model sheet had no sheet-aware scroll owner when profiles filled its viewport, leaving the controls below them unreachable. Keep the root provider view inside Gorhom's scroll container so native sheet gestures work on both platforms. * fix(app): order new tab launcher actions by workflow * fix(app): show the New agent shortcut The command center resolved New agent through the New tab binding, so it advertised Cmd+T despite creating an agent. Resolve the target-agent binding instead and cover the mapping. * fix(app): prioritize side panel launcher actions Lead with supporting workspace tools when the launcher is in the Side panel, while ordinary panes continue to lead with agent creation. * fix(app): align terminal profiles header * docs: add 0.5.0-beta.5 changelog * test(app): align new tab launcher expectations * chore(release): cut 0.5.0-beta.5 * fix: update lockfile signatures and Nix hash [skip ci] * fix(app): make compact git actions menu-only Keep compact toolbars narrow and require users to choose potentially consequential Git and workspace actions from the menu. * fix(app): keep pinned project actions reachable (#3722) * fix(app): keep pinned project actions reachable Preserve an empty project shell after its final workspace is hoisted into the pinned section so project-scoped workspace creation remains available. Co-authored-by: Agent Op <agentop718@gmail.com> * test(app): verify pinned project workspace context --------- Co-authored-by: Agent Op <agentop718@gmail.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> * fix(app): stop a string SVG transform from crashing the app (#3711) * fix(app): stop a string SVG transform from crashing the app A root <Svg> is a real React Native view, so react-native-svg rewrites a *string* `transform` into RN style syntax in `extractTransformSvgView`, using a PEG parser whose grammar only accepts SVG transform functions. Upstream lets that parser's SyntaxError escape, so any string it does not understand takes the whole app down during render: SyntaxError: Expected transform functions but "n" found. "found": "n", "location": { "start": { "offset": 0 } } `none` reproduces that message character for character; so does any other CSS value (`rotate(-90deg)`, `translate(10px, 0)`). Every sibling extractor in that file already catches the same error and degrades, so the asymmetry is the bug: a string that is merely ignored on a <Path> crashes on an <Svg>. We feed that path untrusted input. The daemon scans a project for favicon.svg / icon.svg / logo.svg (packages/server/src/utils/project-icon.ts) and ships it to the client, where ProjectIconImage hands it to SvgCss. Running the library's own XML parser over `<svg style="transform:none">` yields exactly `transform: "none"` on the root, which is why the crash starts once a host connects and the project icons render, with no terminal involved. Note this is *not* reachable from an RN-style array transform: the extractor returns arrays untouched and only parses strings, so `style={{ transform: [{ rotate: "-90deg" }] }}` (the context window meter and the checks ring) was never a factor. Patch the three builds a consumer can resolve - Metro takes the package's `react-native` field (src/*.ts), Node and Vitest take main/module - and register react-native-svg with the postinstall patch runner so the patch actually applies. The new test pins all three. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDwfsMWoSFfPm2cFXe5noQ * test(app): cover parsed root SVG transforms Exercise the dependency patch with root props produced by the library's XML and CSS parsers, including embedded style rules that can arrive through project icons. Keep dependency-patch documentation focused on the reusable install and resolution constraints. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> * fix(pairing): warn that pairing links grant daemon access (#3734) A pairing offer remains valid across daemon restarts, so remove the incorrect rotation guidance and warn users where the durable credential is displayed. * fix(app): preserve timeline retry backoff (#3736) Equivalent visibility publications were acting as implicit retry commands, so unrelated tab and layout changes could restart failed timeline work. Keep visibility identity stable and reserve immediate retries for explicit user action. * Open the tab chooser beside the pointer (#3735) * feat(app): open the tab chooser beside the pointer Keep keyboard-created and empty-pane New tabs as persistent launcher surfaces, while giving pointer users an anchored chooser at the pane-local plus button. Both presentations resolve the same launch catalogue so future ordering and customization cannot drift. * test(app): follow the anchored tab chooser The browser helpers still searched inside the pane for persistent-launcher controls after clicking the portalled menu trigger. Exercise the menu through its own roles and stable catalogue IDs. * fix(app): restore composer height lifecycle (#3740) Native Fabric can omit the initial content-size callback and retain stale size after deletion. Keep the common composer surface free of platform branches while native and web adapters own their measurement mechanisms. * fix(server): stop quota fetcher from writing provider credentials (#3442) * fix(server): recover already-settled interrupts (#3742) * fix(server): recover already-settled interrupts Provider state can drift from Paseo’s tracked run. Treat an authoritative already-idle interrupt as settled and retire the captured run token so retrying can start new work without weakening ambiguous-cancellation protection. * fix(codex): fence interruption during turn startup Reserve foreground-start ownership before asynchronous provider setup. An interrupt can now cancel preflight before turn/start or wait for an issued start to become interruptible, preventing acknowledged cancellation from leaving unmanaged work. * docs(changelog): prepare 0.5.0 stable release * chore(release): cut 0.5.0 * fix: update lockfile signatures and Nix hash [skip ci] * fix(app): stabilize native composer resizing JavaScript height state competed with native intrinsic layout across draft and agent ownership transitions. Let native inputs own sizing and scrolling while preserving DOM measurement on web. * docs(changelog): prepare 0.5.1 stable release * chore(release): cut 0.5.1 * fix: update lockfile signatures and Nix hash [skip ci] * perf(app): move replica cache validation to restore Add an observational live measurement harness for the row-store migration baseline. Trust already-shaped cache values on persist while retaining full schema validation at the restore boundary. * feat(app): add replica cache row store Ship the platform storage engines behind a dark interface so the cache migration can wire them separately. Both engines share a contract, including atomic writes and destructive schema-version resets. * feat(app): switch replica cache to row storage Persist only changed replica entities so steady-state writes scale with the update rather than the complete cached world. Restore remains an all-or-nothing validated boundary, with whole-host byte-budget eviction. * fix(app): harden replica row persistence Serialize pending batches at write execution time so retries cannot overwrite newer rows or resurrect removed hosts. Reject oversized hosts before storage writes and preserve unchanged directory identities so routine reconciliation stays row-grained. --------- Co-authored-by: BrianAguilarWasco <146237330+BrianAguilarWasco@users.noreply.github.com> Co-authored-by: paseo-ai[bot] <266920839+paseo-ai[bot]@users.noreply.github.com> Co-authored-by: Yuri Gui <yugui923@users.noreply.github.com> Co-authored-by: Agent Op <agentop718@gmail.com> Co-authored-by: Omer Cohen <639682+omercnet@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Dan Berindei <dan.berindei@gmail.com>
15 KiB
Timeline sync
Agent chat delivery has two paths:
- Live stream —
agent_streamWebSocket messages for immediacy. These may be delta-shaped lifecycle updates. - Authoritative history —
fetch_agent_timeline_requestfor correctness. This always returns full projected timeline items, never lifecycle deltas.
The daemon keeps canonical rows only for its runtime. Provider history is the durable transcript authority and repopulates those rows when an agent resumes.
The invariants are:
A continuously subscribed client applies every committed row in order. Opening or resuming an agent establishes the daemon's current tail in one bounded request, with older history reachable through backward pagination.
Tool output is bounded before it enters either delivery path. Canonical shell tool output is sliced to 64 KiB, and the same bounded item is used for runtime timeline rows and live stream events. Provider history hydration applies the same rule so reopening an agent cannot restore an oversized tool payload.
Presence is not delivery
Client heartbeat reports presence:
- device type
- app visibility
- focused agent
- last activity time
Heartbeat is used for notification routing. It must not be used as a correctness gate for agent_stream delivery. A stale mobile focus heartbeat may affect whether the user gets notified; it must not make timeline rows disappear from the live stream.
Gap recovery is paged but complete
Large unbounded timeline responses can exceed relay frame limits, so catch-up uses bounded pages. Bounded does not mean partial.
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". 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.
Opening or resuming an agent fetches one bounded latest tail page. Older history remains user-driven by scrolling upward.
A failed catch-up or subscription reconcile retries on its own, doubling from 1s to a 30s ceiling. A fixed 1s retry turned a persistent daemon-side refusal — a Codex thread that already has an active writer, say — into a request and a log line every second on an idle app. The delay resets on success, reconnect, delivery-mode change, and visibility change, so recovery is still immediate once the condition clears. Republishing the same visibility set is a no-op and never bypasses backoff.
Background retries are silent. The retry the user presses in the sync-error callout is a fallible
user action and owns its pending state: retrying is a status the sync model publishes, not a React
boolean, so the button reports in-flight and the callout returns to error when the attempt fails.
Reaching the history-start threshold loads one older page and preserves the visible content anchor. Cursor progress does not trigger another page. The user must leave and return to the threshold unless the anchored page still leaves the viewport at history start, as with short or compacted content; in that case pagination continues as one loading operation until the page fills the viewport or history is exhausted.
Durable item anchors
Provider message IDs are not guaranteed for every displayed item. Paseo-generated system errors are one example. Rendered item indices are not durable either because pagination and projection can merge source rows.
Actions that address a point in chat history, such as Fork, use the daemon timeline epoch plus the projected item's seqEnd. The app carries that position on the rendered assistant item for both live and fetched history. When adjacent projected chunks merge, the merged item retains the newer chunk's position.
The daemon validates that the epoch is current and the exact source sequence still exists before slicing rows. It slices before projection so later lifecycle updates cannot leak into the selected context.
Resume behavior
Opening, reconnecting, or revisiting after a selective-delivery coverage gap fetches the latest tail page. Focus alone does not mutate timeline state; the tail response is compared with the local authoritative range first.
- The same epoch and
window.maxSeqis an exact display no-op. The app advances synchronization bookkeeping without replacing timeline arrays, preserving an upward-scrolled viewport. - When the page overlaps or is adjacent to the local end cursor, only projected items newer than 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.
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
history start; a response requested from a pre-replacement range is stale and is discarded.
Client replica lifetime
The host runtime owns each session replica for as long as the host remains registered. React providers attach message handlers and UI integrations to that replica, but mounting or unmounting a provider must not create or clear it. A provider can remount during Fast Refresh or ordinary UI recomposition while the runtime still owns the same directory snapshot and timeline cursors.
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 durable replica cache persists synchronization authority only when it can store the complete
current canonical window losslessly. The stored range describes those exact items: startSeq drives
older pagination and endSeq drives forward catch-up. Restore paints the items immediately, requests
after endSeq, and requests before startSeq when the user loads older history.
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.
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. Restore then uses the ordinary bounded tail bootstrap. Never
slice items while retaining the pre-slice range; that falsely certifies discarded source rows.
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
The app chooses one delivery policy from server_info.features.selectiveAgentTimeline:
- Selective daemons receive every agent visible in any pane plus the most recently viewed hidden agents, up to five subscribed agents. Visible agents always win: if more than five are visible, they all remain subscribed and no hidden agent does. Switching and app backgrounding preserve this connection-scoped hot set, so returning to an agent still covered by it needs no catch-up. Losing window keyboard focus does not make a selected pane invisible. Disconnecting clears hidden hot agents; reconnect restores the currently visible set before authoritative catch-up. Revisiting an evicted retained timeline displays its cached state immediately while authoritative catch-up advances it to the current tail.
- Legacy daemons keep globally streaming agent timelines. Visibility still triggers the existing authoritative catch-up, but the app does not issue selective-subscription RPCs.
This policy is owned by viewed-timeline-sync.ts; downstream reducers do not branch on daemon
version.
Projected pages reconcile with live presentation
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 automatic queue drain in HostRuntime — goes through
dispatchComposerAgentMessage with a submission writer. There is no second transport for the same
product action: calling client.sendAgentMessage directly skips the submitted row and the pending
footer, and permanently drops attachments because the daemon does not echo them back.
A submitted prompt is one UserMessageItem row. That row is the authoritative local presentation:
its stable identity, text, timestamp, images, and attachments do not change when the provider
acknowledges it. Submission lifecycle is a separate record keyed by agent, not another row shape.
The transaction registry records two independent settlement facts: canonical acknowledgement and RPC
settlement. Canonical acknowledgement retires optimistic activity immediately. When both facts are
known, whichever arrives second deletes the record. A canonical acknowledgement that arrives first
prevents a later transport error from rolling back a prompt already observed.
The daemon's accepted response waits for the correlated run start and guarantees that the canonical submitted row has been recorded. It publishes the accepted turn's liveness before that row, so the client applies authoritative activity before canonical acknowledgement retires optimistic activity. Timeline render batching does not delay lifecycle application. Directory status never settles a submission. Overlapping sends settle independently rather than collapsing to one newest pending message.
Daemons advertising server_info.features.canonicalSubmittedPrompts guarantee that every accepted
prompt carrying a client message id is recorded and streamed as a canonical user_message with that
same id. This includes daemon-handled commands that do not allocate a foreground turn; their submitted
row is recorded before handler output. The app tracks submission transactions only for hosts with this
capability. Older hosts keep the shipped untracked optimistic-row behavior and roll that row back on RPC
rejection.
Turn activity has one client-side replica. Lifecycle events can attach turnId, and agent snapshots can
expose activeTurn: { turnId, startedAt } | null. An identified terminal cannot close a different
identified turn; an unnamed legacy terminal can close the current turn. Snapshots, stream events,
cancellation requests, and every visible surface update the same per-agent record. The snapshot covers
both user-started foreground turns and autonomous provider turns;
foreground control ownership remains a separate daemon concern. Cancellation request identity is stored
with that record rather than in a React component, so an old request cannot clear a newer one. Submissions
remain a separate pre-turn registry and retire on canonical acknowledgement.
Canonical turns and visible responses are different boundaries. System-injected prompts are absent from
the Paseo timeline, so one visible response can span several canonical turns without a user message
between them. Layout and copy group that response together; lifecycle, timing, tool sequences, and exact
fork positions retain the canonical turnId boundaries.
The compatibility boundary for older daemons is snapshot normalization: running/idle status becomes an anonymous active turn or idle state once, and downstream code consumes the same activity shape. The app does not combine anonymous lifecycle events, timestamps, timeline rows, and resume coverage to infer a second running state. Disconnect and replica removal remain destructive close boundaries. Elapsed time comes only from turn liveness, never from submission records or whichever timeline rows happen to be mounted.
The daemon records one canonical submitted user row at acceptance. Its wire messageId is the
submission's clientMessageId, so the row is born with its final identity and remains immutable on
the wire. A correlated provider echo records the provider's native identity internally without
dispatching another timeline event. Rewind resolves the wire identity to that provider identity at
the provider boundary. Daemon-handled prompts follow the same identity rule.
Content matching is limited to the dated compatibility path for daemon timelines created before that field existed. Canonical ingestion may match only an explicit unreconciled local candidate; the draft-create handoff is the one boundary that also permits the legacy canonical twin to have arrived first. Generic reducers and consumers do not reimplement message identity matching.
Ordinary bootstrap, same-epoch reset, and catch-up replacement preserve unmatched locally submitted rows because a provider may never echo them. A known epoch change or rewind replaces history and drops acknowledged local rows omitted by the new canonical epoch; every transaction not yet acknowledged by the provider, and no other local row, crosses that destructive boundary. A cold reset without an existing epoch is destructive because the client has no continuity anchor.
Tail rows are positioned history, so an unmatched local presentation is appended after the canonical replacement rather than ordered by timestamps from different machines. The head is a live overlay: cursorless items stay there during continuity replacement until canonical positions arrive, while a destructive replacement retains only active submission transactions.
Canonical replacement owns both timeline lanes. A matching local row keeps its presentation ID and payload while taking the canonical row's ordered position. If a live assistant head is the canonical assistant prefix, it stays in the head lane. No row may be returned in both lanes.
Relevant code
- Server live stream forwarding:
packages/server/src/server/session.ts - App sync planning:
packages/app/src/timeline/timeline-sync-plan.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