1# Performance regression checks
2
3The v2 transport has a focused regression command:
4
5```bash
6vp run test:perf:v2-wire
7```
8
9It exercises the real v2 projection and client reducers. This matters because the older
10built-app benchmark fixtures seed v1 orchestration events; running those fixtures against a v2
11client can produce reassuring timings while missing the active transport path.
12
13The v2 checks pin these invariants:
14
15- Cold opens and history pages stop at about 1 MiB of encoded rows. A window that holds a user
16 turn also stops at 10 user turns (20 on older pages) or 500 rows, so ordinary turns stay whole.
17 Any other window, including the inside of one turn longer than 500 rows, stops at 75 rows. The
18 snapshot query applies the same rule before it decodes payloads. Clients can opt into deferring
19 large work groups until expansion. Messages and items outside those groups remain in normal
20 history paging. Required control records and
21 the at-least-one-row rule can exceed the byte target; it is not a hard cap.
22- The bounded snapshot does not retain the full duplicate conversation-message table. Messages of
23 a run that is still in progress are the exception: all of them accompany the window.
24- Older activity is fetched in bounded HTTP pages and merged without disturbing the live scroll
25 window.
26- Clients with confirmed history paging opt into socket snapshot fallbacks that use the same
27 bounded recent-history window and carry the cursor needed to fetch older history. Older clients
28 and WebSocket-only starts retain the compatible full-snapshot fallback.
29- Snapshot queries bound timeline reads per fork ancestor, not total rows: dependency and
30 control records still accompany the window. Smaller socket frames alone do not establish a
31 whole-server memory reduction.
32- Resume catch-up checks the original 1 MiB persisted-payload budget before decoding, then
33 replays at most 128 thread events and 1 MiB of projected event JSON before falling back to a
34 snapshot. Raw JSON bytes are not a heap-memory cap. Raising this budget merely because output
35 projects small can increase server allocations on repeated large tool updates.
36- Raw command output, dynamic-tool result bodies, and inline file-change bodies are omitted at
37 the wire boundary, including small outputs. Explicit failure flags and bounded result IDs
38 preserve status and grouped action counts without shipping those bodies. Reasoning text,
39 command lines, and search results ship when their work group loads. Subagent prompts and results
40 and checkpoint control records still accompany the initial window.
41 Persisted events remain complete; the existing diff endpoints still provide file content when
42 requested.
43 Explicit cross-environment transcript exports preserve full tool bodies so the destination
44 agent can read the context. The export rejects timeline JSON above the file attachment limit
45 before sending it, and before decoding any row when the tool output text alone is over that
46 limit; the client still applies its attachment limit to the complete file.
47- Live command output reaches a client only through `orchestration.subscribeCommandOutput`, which
48 an expanded command row opens and closes with itself. Raw output deltas are never orchestration
49 events: adapters send them to the
50 [command output hub](../../apps/server/src/orchestration-v2/CommandOutputHub.ts) ahead of
51 ingestion, which keeps one bounded tail per running command and throttles each subscriber. Do not
52 route output deltas through `turn_item.updated`: a busy command would flood the event store and
53 block ingestion for every other thread.
54- The initial shell contains active navigation rows only. Archived rows use the dedicated archive
55 query, and transcript message bodies stay in thread detail regardless of message size.
56- Shell resume sends deltas plus compact repository-enrichment metadata, not another full project
57 and thread snapshot. A cursor at the head sequence always resumes, even below the pruned
58 boundary, so a client that just loaded the HTTP snapshot never downloads it again over the
59 socket. That client also names the snapshot's project identities, and a server whose
60 identities did not change since resends only the roots that resolved to no identity.
61- Auto, steer, and restart sends resolve delivery from authoritative state inside the
62 server's per-thread dispatch lock. Model selection and identified-checkpoint rollback also
63 dispatch without first fetching a full thread projection when the server advertises support.
64 Older servers retain projection-based validation. Explicit start sends already skipped that
65 read, so this saving does not apply to every client send path.
66
67When changing projection schemas, paging, shell synchronization, or thread state, run this command
68alongside the focused package typechecks and a real-client pass on every affected surface. The
69transport performance fixtures validate contract encoding and compare pre-compression RPC JSON
70for the same synthetic workload, preserving historical application-object counts separately.
71These are neither compressed WebSocket captures nor measurements of server RSS, and command
72fixtures exclude unrelated settings calls and subscription events. Payload budgets belong in
73these tests so regressions fail locally. The transfer budget integration test
74(`apps/server/integration/transferBudgetV2.integration.test.ts`) sends the opt-ins current clients
75send. Its opt-in objects are typed `Required` against the contracts, so an opt-in added to a
76contract fails typecheck until the test sends it; give a fixture the data the opt-in saves. The
77test caps decoded bytes and rows for a 900-row agent turn in both states: while the turn runs (the
78bounded snapshot and one older page) and after it finishes (the deferred cold open over HTTP and
79over the socket alone, and the first page of its deferred work). It caps the socket bytes of three
80resumes that must not send a snapshot: a thread after one ordinary turn, an idle thread, and a
81shell cursor too old to replay. It caps the cold-connect shell for a sidebar of settled threads and
82idle subagents, and the subscription a client opens from that snapshot with its project identities.
83It caps both answers of the HTTP shell route to a client that holds rows and names their cursor: the
84few bytes that keep them, and the snapshot that replaces rows too old to replay. It also caps the
85bytes and
86the node, subagent, plan, provider turn and checkpoint counts of the bounded snapshot for a thread
87of 40 finished runs behind one working run, so finished runs cannot ride along with a window. The
88working run has a ready context handoff, so its history cannot ride along either.
89
90CI prints every row in the job summary. The pull request comment compares the budgeted rows with
91`main`, lists the rows that changed or failed, and names each ceiling a pull request raises. A row
92added to `scenarioRows` in `TransferBudgetReport.integration.ts` reaches the pass/fail check, the
93summary and the comment.
94
95The server config has its own budget in `apps/server/src/ws.test.ts`. A client that asks for
96catalog digests and status deltas gets the provider list of its first config in under 1 KiB, for a
97provider with 250 slash commands and 200 skills. The lists load with the first cwd snapshot, and a
98machine refresh sends back an empty provider list.
99
100## Event store and startup
101
102Sequence cursors must seek an index rather than scan retained history. The application sequence
103index contains project events and v2 thread events; the per-thread sequence index contains only v2
104thread events. Legacy-only and unknown threads must return zero without scanning other threads.
105Catch-up queries keep sequence ranges and optional thread or command predicates indexable.
106
107Startup selects recovery candidates from current projection state before reading full thread
108projections. Candidate selection includes queued runs, pending runtime requests, live provider
109sessions, background work, and unfinished delegated deliveries. Archived threads still participate
110where recovery requires them. Completed thread history is not loaded merely to discover that no
111work remains.
112
113Projection verification decodes canonical rows in bounded pages, including shared provider records
114and fork ancestry. Rebuild replays event pages through a fixed sequence within its transaction;
115it must not collect the entire event log before projecting it.
116
117The focused regression coverage lives in `OrchestrationEventStore.sequence.test.ts`,
118`ProjectionRecovery.test.ts`, and the provider runtime recovery tests under `apps/server/src`.
119
120## Background work and navigation
121
122The settlement worker reads active, unpinned threads without explicit settlement overrides from
123`ProjectionStore.getSettlementCandidates`. It reads the latest run and user-message timestamps,
124rejects active runs and pending requests, and checks background work using the same derivation as
125the shell. It does not load archived histories, fork ancestry, transcript counts, or provider
126sessions. The orchestrator still validates the current thread before applying settlement.
127
128Read receipts use `ProjectionStore.getThread` to load only canonical thread metadata. A visit
129advances `lastVisitedAt` monotonically without changing `updatedAt` or decoding history. Shell
130queries read item counts from a per-thread, per-run table that turn-item triggers keep exact, and
131use a partial index for nonterminal background items, including idle items. Latest user-message
132reads use an index ordered by update time.
133
134Command palette results keep live VCS and linked pull-request queries leased to the visible
135viewport, including a small overscan region. Cached badges remain available while leases are
136released. Local Git status reads skip divergence calculations; callers that need ahead/behind
137counts keep the full status path. Cached fetch failures share the existing backoff and log only
138the actual failed attempt. Unchanged desktop environment bootstrap reads retain their array
139identity so polling does not invalidate subscribers.
140
141Local trace records and provider event logs use asynchronous, serialized file writes and rotation.
142Batching synchronous appends still stalls the server event loop on slow storage, especially on
143Windows. While a trace write is slow, producers keep up to one trace file of records, so the spans
144that time a stall survive it; records drop only while the disk stays stuck. Provider logging
145retains backpressure. Shutdown drains accepted writes before closing their scope.
146
147Relay awareness queues one pending publication per thread and retains a follow-up when the thread
148changes during a send. Transcript deltas and tool output do not enqueue awareness work. Run,
149request, and relevant thread metadata events do. Configuration is checked before shell reads, and
150connection changes invalidate published identities so relinking can publish unchanged state.
151Failed publications retry the latest state up to five times with exponential delays. A new
152relevant event resets the budget; successful publication, disabling, or unlinking cancels retries.
153After the budget is exhausted, publication waits for another relevant event instead of generating
154continuous traffic during an outage.
155
156Focused coverage: `ProjectionSettlement.test.ts`, `ThreadSettlementService.test.ts`,
157`runtimeLayer.test.ts`, `AgentAwarenessRelay.test.ts`, `GitVcsDriverCore.test.ts`, and
158`desktopLocal.test.ts`.
159
160## Mobile looping animations
161
162`apps/mobile/package.json` sets Reanimated's `DISABLE_COMMIT_PAUSING_MECHANISM` so keyboard avoiding
163animates on iOS. Reanimated documents that flag as safe only alongside React Native's
164`preventShadowTreeCommitExhaustion`, which defaults to off and needs React Native built from source,
165so our builds run without it. Reanimated commits each animation frame to the shadow tree, and
166without commit pausing those commits can starve React's: the JS thread keeps retrying its commit,
167the screen freezes, and back does nothing. Release builds retry forever; dev builds abort with
168`ShadowTree.cpp: assertion failed (attempts < 1024)`. Looping animations that stay on screen while
169heavy content renders should use React Native's `Animated` with `useNativeDriver: true`, which
170updates views without commits, not Reanimated's `withRepeat`. The running-thread shimmer in
171`thread-work-log.tsx` is the example.