Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,24 @@ Docs:

## Performance: State & Rendering Architecture

### Two-Phase Bootstrap (`__root.tsx`, `store.ts`, `ProjectionSnapshotQuery.ts`)

The initial data load uses a **two-phase bootstrap** to render the sidebar immediately:

1. **Phase 1 — Listing Snapshot** (`getListingSnapshot()`): Fetches projects + lightweight `OrchestrationThreadSummary` (thread metadata, sessions, latest turns, pre-computed `latestUserMessageAt` via SQL aggregate). Skips messages, activities, checkpoints, and proposed plans. Sets `bootstrapComplete = true` so the sidebar renders immediately. Threads in the store are "hollow" (empty `messages[]`, `activities[]`, etc.).

2. **Phase 2 — Lazy Thread Hydration** (`getThread(threadId)`): When the user navigates to a thread, `_chat.$threadId.tsx` checks `isThreadHydrated()` (messages exist or no turn has happened). If hollow, it calls `getThread()` to fetch full data for that single thread and calls `hydrateThread()` to replace the hollow thread in the store.

Sidebar summary fields (`hasPendingApprovals`, `hasPendingUserInput`, `hasActionableProposedPlan`) default to `false` in the listing snapshot — real-time domain events correct these for active threads within seconds.

Full `getSnapshot()` remains available as a fallback for `replay-failed` snapshot recovery.

### Incremental domain event application (`__root.tsx`, `store.ts`)

High-frequency events (`thread.message-sent`, `thread.activity-appended`, `thread.session-set`, `thread.turn-diff-completed`, `thread.proposed-plan-upserted`) are applied **incrementally** to the Zustand store from event payloads — no full snapshot fetch. This avoids blocking the main thread with JSON parsing and object reconstruction during active agent work.

Full snapshot sync (`getSnapshot()`) only runs for:

- Non-incremental events (thread created/deleted/archived, etc.) via `nonIncrementalThrottler` (500ms)
- Sequence gaps (missed events)
- Deferred reconciliation safety net (every 10 seconds after last incremental event)
- Welcome/reconnect
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,9 @@ describe("CheckpointDiffQueryLive", () => {
Layer.succeed(ProjectionSnapshotQuery, {
getSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
getListingSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the listing snapshot"),
getThread: () => Effect.die("CheckpointDiffQuery should not request a single thread"),
getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
Expand DownExpand Up@@ -136,6 +139,9 @@ describe("CheckpointDiffQueryLive", () => {
Layer.succeed(ProjectionSnapshotQuery, {
getSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
getListingSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the listing snapshot"),
getThread: () => Effect.die("CheckpointDiffQuery should not request a single thread"),
getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,10 @@ describe("OrchestrationEngine", () => {
Layer.provide(
Layer.succeed(ProjectionSnapshotQuery, {
getSnapshot: () => Effect.succeed(projectionSnapshot),
getListingSnapshot: () =>
Effect.die("OrchestrationEngine test should not request the listing snapshot"),
getThread: () =>
Effect.die("OrchestrationEngine test should not request a single thread"),
getCounts: () => Effect.succeed({ projectCount: 1, threadCount: 1 }),
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
Expand Down
Loading
Loading