1import { applyThreadPullRequestLinkUpdate } from "@t3tools/shared/threadPullRequests";
2import { projectTurnItemForWire } from "./WireProjection.ts";
3import * as Stream from "effect/Stream";
4import { makeThreadFind, findProjectedThreadItems } from "./ThreadFind.ts";
5import type {
6 OrchestrationV2SearchThreadInput,
7 OrchestrationV2SearchThreadResult,
8 OrchestrationV2ThreadHistoryPage,
9} from "@t3tools/contracts";
10import {
11 latestRootProviderFailure,
12 latestUnheldRun,
13 threadErrorSummary,
14 usageLimitRunPresentedAsLatest,
15} from "@t3tools/shared/orchestrationV2ThreadError";
16import { threadPullRequestsOf } from "@t3tools/shared/threadPullRequests";
17import type {
18 OrchestrationV2AppThread,
19 OrchestrationV2CheckpointScope,
20 OrchestrationV2ExecutionNode,
21 OrchestrationV2PlanArtifact,
22 PlanId,
23 OrchestrationV2ConversationMessage,
24 OrchestrationV2DomainEvent,
25 OrchestrationV2ProjectedTurnItem,
26 OrchestrationV2ProviderThread,
27 OrchestrationV2ProviderTurn,
28 OrchestrationV2Run,
29 OrchestrationV2Subagent,
30 OrchestrationV2ThreadShellSnapshot,
31 OrchestrationV2ShellThreadStatus,
32 OrchestrationV2ThreadShell,
33 OrchestrationV2ThreadProjection,
34 OrchestrationV2TurnItem,
35 ProjectId,
36 ProviderInstanceId,
37 ProviderSessionId,
38 ProviderThreadId,
39 ProviderTurnId,
40 RunAttemptId,
41} from "@t3tools/contracts";
42import {
43 OrchestrationV2AppThreadJson as OrchestrationV2AppThreadJsonSchema,
44 OrchestrationV2CheckpointJson as OrchestrationV2CheckpointJsonSchema,
45 OrchestrationV2CheckpointScopeJson as OrchestrationV2CheckpointScopeJsonSchema,
46 OrchestrationV2ContextHandoffJson as OrchestrationV2ContextHandoffJsonSchema,
47 OrchestrationV2ContextTransferJson as OrchestrationV2ContextTransferJsonSchema,
48 OrchestrationV2ConversationMessageJson as OrchestrationV2ConversationMessageJsonSchema,
49 OrchestrationV2ExecutionNodeJson as OrchestrationV2ExecutionNodeJsonSchema,
50 OrchestrationV2PlanArtifact as OrchestrationV2PlanArtifactSchema,
51 OrchestrationV2ProviderSessionJson as OrchestrationV2ProviderSessionJsonSchema,
52 OrchestrationV2ProviderThreadJson as OrchestrationV2ProviderThreadJsonSchema,
53 OrchestrationV2ProviderTurnJson as OrchestrationV2ProviderTurnJsonSchema,
54 OrchestrationV2RunAttemptJson as OrchestrationV2RunAttemptJsonSchema,
55 OrchestrationV2RunJson as OrchestrationV2RunJsonSchema,
56 OrchestrationV2RuntimeRequestJson as OrchestrationV2RuntimeRequestJsonSchema,
57 OrchestrationV2SubagentJson as OrchestrationV2SubagentJsonSchema,
58 OrchestrationV2TurnItemJson as OrchestrationV2TurnItemJsonSchema,
59 orchestrationV2RunWorkStartedAt,
60 RunId,
61 MessageId,
62 RuntimeRequestId,
63 CheckpointScopeId,
64 ThreadId,
65 TurnItemId,
66 NodeId,
67} from "@t3tools/contracts";
68import {
69 createOrchestrationV2TurnItemVisibility,
70 isOrchestrationV2SupersededInterrupt,
71} from "@t3tools/shared/orchestrationV2Timeline";
72import { derivePendingBackgroundWork } from "@t3tools/shared/orchestrationV2PendingBackgroundWork";
73import * as Context from "effect/Context";
74import * as DateTime from "effect/DateTime";
75import * as Effect from "effect/Effect";
76import * as Equal from "effect/Equal";
77import * as Layer from "effect/Layer";
78import * as Ref from "effect/Ref";
79import * as Schema from "effect/Schema";
80import * as SqlClient from "effect/sql/SqlClient";
81import type * as Statement from "effect/sql/Statement";
82
83import { MCP_APP_OUTPUT_KEY } from "@t3tools/shared/mcpApp";
84import { threadHtmlRenderAttachmentIds } from "../attachmentStore.ts";
85import {
86 isThreadHistoryUserTurn,
87 isConversationHistoryItem,
88 threadHistoryTurnStarts,
89 THREAD_HISTORY_MAX_RAW_TURNS,
90 selectHistoryPageFromCursor,
91 selectRecentTimelineWindow,
92 THREAD_HISTORY_PAGE_POLICY,
93 OLDER_THREAD_USER_TURN_LIMIT,
94} from "./threadHistoryPaging.ts";
95
96export class ProjectionStoreApplyEventError extends Schema.TaggedError<ProjectionStoreApplyEventError>()(
97 "ProjectionStoreApplyEventError",
98 {
99 eventType: Schema.String,
100 cause: Schema.optional(Schema.Defect()),
101 },
102) {
103 override get message(): string {
104 return `Failed to apply orchestration projection event ${this.eventType}.`;
105 }
106}
107
108export class ProjectionStoreSetupError extends Schema.TaggedError<ProjectionStoreSetupError>()(
109 "ProjectionStoreSetupError",
110 {
111 cause: Schema.optional(Schema.Defect()),
112 },
113) {
114 override get message(): string {
115 return "Failed to initialize orchestration projection store.";
116 }
117}
118
119export class ProjectionStoreThreadNotFoundError extends Schema.TaggedError<ProjectionStoreThreadNotFoundError>()(
120 "ProjectionStoreThreadNotFoundError",
121 {
122 threadId: ThreadId,
123 },
124) {
125 override get message(): string {
126 return `No orchestration projection exists for thread ${this.threadId}.`;
127 }
128}
129
130export class ProjectionStoreReadError extends Schema.TaggedError<ProjectionStoreReadError>()(
131 "ProjectionStoreReadError",
132 {
133 threadId: ThreadId,
134 cause: Schema.optional(Schema.Defect()),
135 },
136) {
137 override get message(): string {
138 return `Failed to read orchestration projection for thread ${this.threadId}.`;
139 }
140}
141
142export const ProjectionStoreV2Error = Schema.Union([
143 ProjectionStoreSetupError,
144 ProjectionStoreApplyEventError,
145 ProjectionStoreThreadNotFoundError,
146 ProjectionStoreReadError,
147]);
148export type ProjectionStoreV2Error = typeof ProjectionStoreV2Error.Type;
149
150export type ProjectionRecoveryKind =
151 | "queued-runs"
152 | "runtime"
153 | "subagent-results"
154 | "delegated-completions";
155
156/** Persisted state needed for limit recovery, without transcript or fork history. */
157export type ProjectionLimitRecoveryCandidate = Pick<
158 OrchestrationV2ThreadShell,
159 | "id"
160 | "status"
161 | "lastErrorClass"
162 | "latestRunId"
163 | "usageLimitResetAt"
164 | "archivedAt"
165 | "settledOverride"
166 | "pendingRuntimeRequest"
167 | "latestRunCompletedAt"
168 | "updatedAt"
169 | "limitRecovery"
170 | "snoozedUntil"
171>;
172
173/** The thread fields pull request sync reads, for a thread with at least one link. */
174export type ProjectionThreadPullRequests = Pick<
175 OrchestrationV2AppThread,
176 "id" | "projectId" | "lineage" | "settledOverride" | "settledAt" | "pullRequests"
177>;
178
179/**
180 * Thread activity needed by settlement, without transcript or fork history.
181 * Settlement always loads `latestUserAuthoredMessageAt`, so it is required here.
182 */
183export type ProjectionSettlementCandidate = Pick<
184 OrchestrationV2ThreadShell,
185 | "id"
186 | "projectId"
187 | "branch"
188 | "worktreePath"
189 | "pullRequests"
190 | "linkedPullRequest"
191 | "branchPullRequest"
192 | "createdAt"
193 | "updatedAt"
194 | "archivedAt"
195 | "settledOverride"
196 | "pinnedAt"
197 | "autoSettleDisabledAt"
198 | "snoozedUntil"
199 | "snoozedAt"
200 | "latestRunId"
201 | "latestRunRequestedAt"
202 | "latestRunStartedAt"
203 | "latestRunCompletedAt"
204 | "latestUserMessageAt"
205 | "status"
206 | "activityRunStartedAt"
207 | "activityRunStatus"
208 | "pendingRuntimeRequest"
209 | "pendingBackgroundTasks"
210> &
211 Pick<OrchestrationV2AppThread, "lastSnoozeWakeAt"> & {
212 readonly latestUserAuthoredMessageAt: DateTime.Utc | null;
213 };
214
215const ProjectionCheckpointContext = Schema.Struct({
216 runs: Schema.Array(
217 OrchestrationV2RunJsonSchema.mapFields(({ id, ordinal, status }) => ({ id, ordinal, status })),
218 ),
219 checkpointScopes: Schema.Array(
220 OrchestrationV2CheckpointScopeJsonSchema.mapFields(({ id, runId, kind, cwd }) => ({
221 id,
222 runId,
223 kind,
224 cwd,
225 })),
226 ),
227 checkpoints: Schema.Array(
228 OrchestrationV2CheckpointJsonSchema.mapFields(
229 ({ scopeId, runId, appRunOrdinal, status, ref }) => ({
230 scopeId,
231 runId,
232 appRunOrdinal,
233 status,
234 ref,
235 }),
236 ),
237 ),
238});
239export type ProjectionCheckpointContext = typeof ProjectionCheckpointContext.Type;
240const decodeCheckpointContext = Schema.decodeUnknownEffect(ProjectionCheckpointContext);
241
242/** Durable capture targets, without transcript or checkpoint file payloads. */
243export interface ProjectionCheckpointCaptureContext {
244 readonly run: OrchestrationV2Run | undefined;
245 readonly rootNode: OrchestrationV2ExecutionNode | undefined;
246 readonly scope: OrchestrationV2CheckpointScope | undefined;
247 readonly providerThread: OrchestrationV2ProviderThread | undefined;
248 readonly readyCheckpointOrdinals: ReadonlyArray<number>;
249}
250
251/** Exact durable targets used by interrupt, restart, and steering effects. */
252export interface ProjectionProviderControlContext {
253 readonly providerThread: OrchestrationV2ThreadProjection["providerThreads"][number] | undefined;
254 readonly providerTurn: OrchestrationV2ProviderTurn | undefined;
255 readonly attempt: OrchestrationV2ThreadProjection["attempts"][number] | undefined;
256 readonly message: OrchestrationV2ConversationMessage | undefined;
257 readonly run: OrchestrationV2Run | undefined;
258}
259
260export interface ProjectionProviderControlTarget {
261 readonly providerThreadId: ProviderThreadId;
262 readonly providerTurnId: ProviderTurnId;
263 readonly attemptId?: RunAttemptId;
264 readonly messageId?: MessageId;
265}
266
267export type ProjectionRuntimeRecoveryState = Pick<
268 OrchestrationV2ThreadProjection,
269 | "thread"
270 | "runs"
271 | "attempts"
272 | "nodes"
273 | "subagents"
274 | "providerSessions"
275 | "providerThreads"
276 | "providerTurns"
277 | "runtimeRequests"
278 | "messages"
279 | "turnItems"
280>;
281
282export type ProjectionPendingUserInputs = Pick<
283 OrchestrationV2ThreadProjection,
284 "runtimeRequests" | "nodes" | "turnItems"
285>;
286
287export type ProjectionThreadProviderContext = Pick<
288 OrchestrationV2ThreadProjection,
289 "thread" | "providerSessions" | "providerThreads"
290>;
291export interface ProjectionRuntimeResponseContext {
292 readonly request: OrchestrationV2ThreadProjection["runtimeRequests"][number] | undefined;
293 readonly node: OrchestrationV2ThreadProjection["nodes"][number] | undefined;
294 readonly item: OrchestrationV2ThreadProjection["turnItems"][number] | undefined;
295 readonly session: OrchestrationV2ThreadProjection["providerSessions"][number] | undefined;
296}
297
298export interface ProjectionRecordFilter {
299 readonly messageRoles?: ReadonlyArray<OrchestrationV2ConversationMessage["role"]>;
300 readonly turnItemRunId?: RunId;
301 readonly messageIds?: ReadonlyArray<MessageId>;
302 readonly messageRunIds?: ReadonlyArray<RunId>;
303 readonly turnItemRunIds?: ReadonlyArray<RunId | null>;
304 readonly runIds?: ReadonlyArray<RunId>;
305 readonly turnItemTypes?: ReadonlyArray<OrchestrationV2TurnItem["type"]>;
306 readonly turnItemStatuses?: ReadonlyArray<OrchestrationV2TurnItem["status"]>;
307 // Skips finished tool call, assistant message and reasoning nodes of finished
308 // runs. Command decisions only read root, subagent and active-run nodes.
309 readonly omitSettledLeafNodes?: boolean;
310}
311
312type ProjectionNode = OrchestrationV2ThreadProjection["nodes"][number];
313type ProjectionRun = OrchestrationV2ThreadProjection["runs"][number];
314const settledLeafNodeKinds: ReadonlySet<ProjectionNode["kind"]> = new Set([
315 "tool_call",
316 "assistant_message",
317 "reasoning",
318]);
319const settledNodeStatuses: ReadonlySet<ProjectionNode["status"]> = new Set([
320 "completed",
321 "interrupted",
322 "failed",
323 "cancelled",
324 "rolled_back",
325]);
326const activeRunStatuses: ReadonlySet<ProjectionRun["status"]> = new Set([
327 "preparing",
328 "queued",
329 "starting",
330 "running",
331 "waiting",
332]);
333
334// In-memory twin of the omitSettledLeafNodes SQL clause in readCanonicalProjection.
335const isSettledLeafNode = (node: ProjectionNode, runs: ReadonlyArray<ProjectionRun>) =>
336 settledLeafNodeKinds.has(node.kind) &&
337 settledNodeStatuses.has(node.status) &&
338 node.runId !== null &&
339 !runs.some((run) => run.id === node.runId && activeRunStatuses.has(run.status));
340export type ProjectionRecordField = Exclude<
341 keyof OrchestrationV2ThreadProjection,
342 "thread" | "updatedAt" | "visibleTurnItems"
343>;
344export type ProjectionRecords<K extends ProjectionRecordField> = Pick<
345 OrchestrationV2ThreadProjection,
346 "thread" | K
347>;
348
349export interface ProjectionTimelinePageOptions {
350 readonly afterPosition?: number;
351 readonly itemId?: TurnItemId;
352 readonly view?: "messages" | "activity";
353 readonly limit: number;
354}
355export interface ProjectionTimelinePage {
356 readonly items: ReadonlyArray<OrchestrationV2ProjectedTurnItem>;
357 readonly totalItems: number;
358 readonly hasMore: boolean;
359}
360
361export interface ShellSnapshotOptions {
362 readonly location?: "active" | "archive";
363 /**
364 * Reads only this project's threads. Fork sources in other projects still
365 * load, so visible item counts match the unscoped snapshot.
366 */
367 readonly projectId?: ProjectId;
368 /**
369 * For background sweeps, not clients: skips settled threads before any of
370 * their run, item or session rows are read.
371 */
372 readonly unsettledOnly?: boolean;
373 readonly settledOnly?: boolean;
374}
375
376export interface ProjectionStoreV2Shape {
377 readonly searchThreadStream: (
378 input: OrchestrationV2SearchThreadInput,
379 ) => Stream.Stream<OrchestrationV2SearchThreadResult, ProjectionStoreV2Error>;
380 readonly searchThread: (
381 input: OrchestrationV2SearchThreadInput,
382 ) => Effect.Effect<OrchestrationV2SearchThreadResult, ProjectionStoreV2Error>;
383 readonly getThreadAttachmentIds: (
384 threadId: ThreadId,
385 ) => Effect.Effect<ReadonlyArray<string>, ProjectionStoreV2Error>;
386 readonly getThreadHistoryPage: (
387 threadId: ThreadId,
388 cursor: string,
389 throughEntryId?: string,
390 conversationOnly?: boolean,
391 ) => Effect.Effect<OrchestrationV2ThreadHistoryPage, ProjectionStoreV2Error>;
392 readonly getTimelinePage: (
393 threadId: ThreadId,
394 options: ProjectionTimelinePageOptions,
395 ) => Effect.Effect<ProjectionTimelinePage, ProjectionStoreV2Error>;
396 readonly getMessageCount: (threadId: ThreadId) => Effect.Effect<number, ProjectionStoreV2Error>;
397 readonly getNextTurnItemOrdinal: (
398 threadId: ThreadId,
399 ) => Effect.Effect<number, ProjectionStoreV2Error>;
400 /** One persisted turn item, or null when the thread has no such item. */
401 readonly getTurnItem: (input: {
402 readonly threadId: ThreadId;
403 readonly itemId: TurnItemId;
404 }) => Effect.Effect<OrchestrationV2TurnItem | null, ProjectionStoreV2Error>;
405 readonly getThreadRecords: <K extends ProjectionRecordField>(
406 threadId: ThreadId,
407 fields: ReadonlyArray<K>,
408 filter?: ProjectionRecordFilter,
409 ) => Effect.Effect<ProjectionRecords<K>, ProjectionStoreV2Error>;
410
411 readonly apply: (
412 event: OrchestrationV2DomainEvent,
413 ) => Effect.Effect<void, ProjectionStoreV2Error>;
414 readonly getShellSnapshot: (
415 options?: ShellSnapshotOptions,
416 ) => Effect.Effect<OrchestrationV2ThreadShellSnapshot, ProjectionStoreV2Error>;
417 /**
418 * Runs the shell snapshot's SQL reads and returns the step that decodes them.
419 * Callers that combine the shell with other reads run this inside their own
420 * transaction and the returned decode after it commits, so the shared
421 * connection is not held while thousands of rows are converted.
422 */
423 readonly readShellSnapshot: (
424 options?: ShellSnapshotOptions,
425 ) => Effect.Effect<
426 Effect.Effect<OrchestrationV2ThreadShellSnapshot, ProjectionStoreV2Error>,
427 ProjectionStoreV2Error
428 >;
429 readonly getThreadShell: (
430 threadId: ThreadId,
431 ) => Effect.Effect<OrchestrationV2ThreadShell | null, ProjectionStoreV2Error>;
432 readonly getThread: (
433 threadId: ThreadId,
434 ) => Effect.Effect<OrchestrationV2AppThread, ProjectionStoreV2Error>;
435 readonly getLimitRecoveryCandidates: (options: {
436 readonly now: DateTime.Utc;
437 readonly autoResume: boolean;
438 readonly snooze: boolean;
439 }) => Effect.Effect<ReadonlyArray<ProjectionLimitRecoveryCandidate>, ProjectionStoreV2Error>;
440 /** Every candidate, or only `threadId` when a sweep checks one thread. */
441 readonly getSettlementCandidates: (
442 threadId?: ThreadId,
443 ) => Effect.Effect<ReadonlyArray<ProjectionSettlementCandidate>, ProjectionStoreV2Error>;
444 /**
445 * Active (not deleted, not archived) threads with at least one pull request
446 * link, in shell snapshot order, or only `threadId` when given. Skips run,
447 * message and item reads.
448 */
449 readonly getThreadsWithPullRequests: (
450 threadId?: ThreadId,
451 ) => Effect.Effect<ReadonlyArray<ProjectionThreadPullRequests>, ProjectionStoreV2Error>;
452 /**
453 * Active (not deleted, not archived) threads that have a worktree path, the
454 * same threads as the shell snapshot's `threads`. Reads only the thread rows,
455 * so callers can check who uses a path without a shell snapshot.
456 */
457 readonly getThreadWorktreePaths: () => Effect.Effect<
458 ReadonlyArray<{ readonly threadId: ThreadId; readonly worktreePath: string }>,
459 ProjectionStoreV2Error
460 >;
461 readonly getTurnStartContext: (
462 threadId: ThreadId,
463 runId: RunId,
464 ) => Effect.Effect<
465 Pick<
466 OrchestrationV2ThreadProjection,
467 | "thread"
468 | "runs"
469 | "attempts"
470 | "nodes"
471 | "subagents"
472 | "providerSessions"
473 | "providerThreads"
474 | "providerTurns"
475 | "messages"
476 | "checkpointScopes"
477 | "contextHandoffs"
478 | "contextTransfers"
479 | "turnItems"
480 > & { readonly hasConversation: boolean },
481 ProjectionStoreV2Error
482 >;
483 readonly getTurnStartHistory: (
484 threadId: ThreadId,
485 runIds?: ReadonlyArray<RunId>,
486 ) => Effect.Effect<ReadonlyArray<OrchestrationV2TurnItem>, ProjectionStoreV2Error>;
487 readonly getThreadProjection: (
488 threadId: ThreadId,
489 ) => Effect.Effect<OrchestrationV2ThreadProjection, ProjectionStoreV2Error>;
490 readonly getRuntimeRecoveryProjection: (
491 threadId: ThreadId,
492 ) => Effect.Effect<ProjectionRuntimeRecoveryState, ProjectionStoreV2Error>;
493 readonly getPendingNativeUserInputs: (
494 threadId: ThreadId,
495 providerTurnId: ProviderTurnId,
496 ) => Effect.Effect<ProjectionPendingUserInputs, ProjectionStoreV2Error>;
497 readonly getPlan: (
498 threadId: ThreadId,
499 planId: PlanId,
500 ) => Effect.Effect<OrchestrationV2PlanArtifact | undefined, ProjectionStoreV2Error>;
501 readonly hasUnpairedRunInterruptRequest: (
502 threadId: ThreadId,
503 requestId: TurnItemId,
504 resultId: TurnItemId,
505 ) => Effect.Effect<boolean, ProjectionStoreV2Error>;
506 /** True when the projection already stores this exact node, so a write would change nothing. */
507 readonly isNodeStored: (
508 node: OrchestrationV2ExecutionNode,
509 ) => Effect.Effect<boolean, ProjectionStoreV2Error>;
510 readonly getRuntimeRequest: (
511 threadId: ThreadId,
512 requestId: RuntimeRequestId,
513 ) => Effect.Effect<
514 OrchestrationV2ThreadProjection["runtimeRequests"][number] | undefined,
515 ProjectionStoreV2Error
516 >;
517 readonly getProviderControlContext: (
518 threadId: ThreadId,
519 target: ProjectionProviderControlTarget,
520 ) => Effect.Effect<ProjectionProviderControlContext, ProjectionStoreV2Error>;
521 readonly getRunningTurnContext: (
522 threadId: ThreadId,
523 ) => Effect.Effect<
524 Pick<ProjectionProviderControlContext, "run" | "providerThread" | "providerTurn">,
525 ProjectionStoreV2Error
526 >;
527 readonly getThreadProviderContext: (
528 threadId: ThreadId,
529 targetInstanceId?: ProviderInstanceId,
530 ) => Effect.Effect<ProjectionThreadProviderContext, ProjectionStoreV2Error>;
531 readonly getRuntimeResponseContext: (
532 threadId: ThreadId,
533 requestId: RuntimeRequestId,
534 ) => Effect.Effect<ProjectionRuntimeResponseContext, ProjectionStoreV2Error>;
535 readonly getCheckpointContext: (
536 threadId: ThreadId,
537 ) => Effect.Effect<ProjectionCheckpointContext, ProjectionStoreV2Error>;
538 readonly getCheckpointCaptureContext: (
539 threadId: ThreadId,
540 target: { readonly runId: RunId; readonly scopeId: CheckpointScopeId },
541 ) => Effect.Effect<ProjectionCheckpointCaptureContext, ProjectionStoreV2Error>;
542 readonly getRunMessage: (
543 threadId: ThreadId,
544 runId: RunId,
545 ) => Effect.Effect<OrchestrationV2ConversationMessage | undefined, ProjectionStoreV2Error>;
546 readonly canStartQueuedRun: (
547 threadId: ThreadId,
548 ) => Effect.Effect<boolean, ProjectionStoreV2Error>;
549 readonly getRecoveryThreadIds: (
550 kind: ProjectionRecoveryKind,
551 ) => Effect.Effect<ReadonlyArray<ThreadId>, ProjectionStoreV2Error>;
552 readonly getUnreadableThreadIds: () => Effect.Effect<
553 ReadonlyArray<ThreadId>,
554 ProjectionStoreV2Error
555 >;
556 readonly getThreadSnapshot: (threadId: ThreadId) => Effect.Effect<
557 {
558 readonly schemaVersion: number;
559 readonly snapshotSequence: number;
560 readonly projection: OrchestrationV2ThreadProjection;
561 },
562 ProjectionStoreV2Error
563 >;
564 readonly getThreadSnapshotWindow: (
565 threadId: ThreadId,
566 options: {
567 /** Row fallback for histories without turn starts. */
568 readonly rowLimit: number;
569 /** User-turn window, with extra anchors for the inclusive cursor and has-more check. */
570 readonly userTurnLimit?: number | undefined;
571 readonly anchorItemId?: TurnItemId | undefined;
572 readonly anchorThreadId?: ThreadId | undefined;
573 readonly requiredRunId?: RunId | undefined;
574 },
575 ) => Effect.Effect<
576 {
577 readonly schemaVersion: number;
578 readonly snapshotSequence: number;
579 readonly projection: OrchestrationV2ThreadProjection;
580 },
581 ProjectionStoreV2Error
582 >;
583}
584
585export class ProjectionStoreV2 extends Context.Service<ProjectionStoreV2, ProjectionStoreV2Shape>()(
586 "t3/orchestration-v2/ProjectionStore/ProjectionStoreV2",
587) {}
588
589export const ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION = 2;
590
591function needsRecovery(
592 projection: OrchestrationV2ThreadProjection,
593 kind: ProjectionRecoveryKind,
594 parent?: Pick<OrchestrationV2ThreadProjection, "subagents" | "contextTransfers">,
595): boolean {
596 if (projection.thread.deletedAt !== null) return false;
597 switch (kind) {
598 case "queued-runs":
599 return (
600 projection.thread.archivedAt === null &&
601 projection.runs.some((run) => run.status === "queued") &&
602 !projection.runs.some((run) =>
603 ["preparing", "starting", "running", "waiting"].includes(run.status),
604 )
605 );
606 case "delegated-completions":
607 return projection.runs.some((run) => run.delegatedCompletion?.delivery != null);
608 case "subagent-results": {
609 const parentThreadId = projection.thread.lineage.parentThreadId;
610 return (
611 projection.thread.lineage.relationshipToParent === "subagent" &&
612 parentThreadId !== null &&
613 projection.thread.forkedFrom?.type === "node" &&
614 ((parent?.subagents ?? []).some(
615 (task) =>
616 task.origin === "app_owned" &&
617 task.result === null &&
618 task.childThreadId === projection.thread.id &&
619 projection.runs.some(
620 (run) =>
621 run.delegatedTaskId === task.id &&
622 ["completed", "interrupted", "failed", "cancelled", "rolled_back"].includes(
623 run.status,
624 ),
625 ),
626 ) ||
627 (["completed", "interrupted", "failed", "cancelled", "rolled_back"].includes(
628 latestUnheldRun(projection.runs)?.status ?? "idle",
629 ) &&
630 ![...projection.contextTransfers, ...(parent?.contextTransfers ?? [])].some(
631 (transfer) =>
632 transfer.type === "subagent_result" &&
633 transfer.sourceThreadId === projection.thread.id &&
634 transfer.targetThreadId === parentThreadId,
635 )))
636 );
637 }
638 case "runtime":
639 return (
640 projection.runs.some(
641 (run) =>
642 ["preparing", "starting", "running", "waiting"].includes(run.status) ||
643 (run.status === "queued" && run.queueHeld !== true),
644 ) ||
645 projection.runtimeRequests.some((request) => request.status === "pending") ||
646 projection.subagents.some(
647 (subagent) =>
648 subagent.origin === "provider_native" &&
649 ["pending", "running", "waiting"].includes(subagent.status),
650 ) ||
651 projection.providerSessions.some(
652 (session) => session.status !== "stopped" && session.status !== "error",
653 ) ||
654 projection.providerThreads.some(
655 (thread) =>
656 thread.status === "active" || (thread.pendingBackgroundTasks?.length ?? 0) > 0,
657 ) ||
658 projection.turnItems.some(
659 (item) =>
660 ["command_execution", "dynamic_tool", "subagent"].includes(item.type) &&
661 ["pending", "running", "waiting"].includes(item.status) &&
662 !projection.runs.some((run) => run.id === item.runId && run.status === "rolled_back"),
663 )
664 );
665 }
666}
667
668function upsertById<T extends { readonly id: string }>(items: ReadonlyArray<T>, next: T): Array<T> {
669 const index = items.findIndex((item) => item.id === next.id);
670 if (index === -1) {
671 return [...items, next];
672 }
673
674 const updated = [...items];
675 updated[index] = next;
676 return updated;
677}
678
679export function upsertProviderTurn(
680 turns: ReadonlyArray<OrchestrationV2ProviderTurn>,
681 next: OrchestrationV2ProviderTurn,
682): Array<OrchestrationV2ProviderTurn> {
683 const current = turns.find((turn) => turn.id === next.id);
684 return upsertById(turns, {
685 ...next,
686 ...((next.tokenUsage ?? current?.tokenUsage) === undefined
687 ? {}
688 : { tokenUsage: next.tokenUsage ?? current?.tokenUsage }),
689 });
690}
691
692/** A stale run snapshot must not erase fields that other events recorded on the run. */
693function preserveRunRecordedFields(
694 current: OrchestrationV2Run | undefined,
695 next: OrchestrationV2Run,
696): OrchestrationV2Run {
697 if (current === undefined) return next;
698 return {
699 ...next,
700 ...(next.delegatedCompletion === undefined && current.delegatedCompletion !== undefined
701 ? { delegatedCompletion: current.delegatedCompletion }
702 : {}),
703 ...(next.restartCancelledBackgroundWork === undefined &&
704 current.restartCancelledBackgroundWork !== undefined
705 ? { restartCancelledBackgroundWork: current.restartCancelledBackgroundWork }
706 : {}),
707 };
708}
709
710function preserveCompletionDelivery(
711 current: OrchestrationV2Subagent | undefined,
712 next: OrchestrationV2Subagent,
713): OrchestrationV2Subagent {
714 if (next.completionDelivery !== undefined || current?.completionDelivery === undefined) {
715 return next;
716 }
717 return { ...next, completionDelivery: current.completionDelivery };
718}
719
720export function emptyProjection(
721 event: Extract<OrchestrationV2DomainEvent, { readonly type: "thread.created" }>,
722): OrchestrationV2ThreadProjection {
723 return {
724 thread: event.payload,
725 runs: [],
726 attempts: [],
727 nodes: [],
728 subagents: [],
729 providerSessions: [],
730 providerThreads: [],
731 providerTurns: [],
732 runtimeRequests: [],
733 messages: [],
734 plans: [],
735 turnItems: [],
736 checkpointScopes: [],
737 checkpoints: [],
738 contextHandoffs: [],
739 contextTransfers: [],
740 visibleTurnItems: [],
741 updatedAt: event.occurredAt,
742 };
743}
744
745// A future queued provider has a reserved thread record but is not active until delivery.
746function isQueuedProviderThreadPlaceholder(providerThread: OrchestrationV2ProviderThread): boolean {
747 return (
748 providerThread.status === "not_loaded" &&
749 providerThread.firstRunOrdinal === null &&
750 providerThread.nativeThreadRef === null &&
751 providerThread.providerSessionId === null
752 );
753}
754
755export function applyToProjection(
756 projection: OrchestrationV2ThreadProjection,
757 event: OrchestrationV2DomainEvent,
758): OrchestrationV2ThreadProjection {
759 const base = {
760 ...projection,
761 thread: {
762 ...projection.thread,
763 updatedAt: event.occurredAt,
764 },
765 updatedAt: event.occurredAt,
766 };
767
768 switch (event.type) {
769 case "thread.pull-request-link-updated":
770 return {
771 ...base,
772 thread: applyThreadPullRequestLinkUpdate(projection.thread, event.payload),
773 };
774 case "thread.created":
775 case "thread.archived":
776 case "thread.unarchived":
777 case "thread.deleted":
778 case "thread.settled":
779 case "thread.unsettled":
780 case "thread.snoozed":
781 case "thread.unsnoozed":
782 case "thread.auto-settle-set":
783 case "thread.pinned":
784 case "thread.unpinned":
785 case "thread.pin-reordered":
786 case "thread.active-reordered":
787 case "thread.metadata-updated":
788 case "thread.pull-request-synced":
789 case "thread.runtime-mode-updated":
790 case "thread.interaction-mode-updated":
791 case "thread.model-selection-updated":
792 case "thread.provider-switched":
793 return {
794 ...base,
795 thread: event.payload,
796 };
797 // Visited tracking is read state, not activity: skip the updatedAt bump so
798 // viewing a thread does not surface it as recently active.
799 case "thread.visited":
800 case "thread.marked-unread":
801 return {
802 ...projection,
803 thread: event.payload,
804 };
805 case "run.created":
806 case "run.updated":
807 return withLocalVisibleTurnItems({
808 ...base,
809 runs: upsertById(
810 base.runs,
811 preserveRunRecordedFields(
812 base.runs.find((run) => run.id === event.payload.id),
813 event.payload,
814 ),
815 ),
816 });
817 case "run.background-work-cancelled":
818 return {
819 ...base,
820 runs: base.runs.map((run) =>
821 run.id === event.payload.runId
822 ? {
823 ...run,
824 restartCancelledBackgroundWork: event.payload.restartCancelledBackgroundWork,
825 }
826 : run,
827 ),
828 };
829 case "run-attempt.created":
830 case "run-attempt.updated":
831 return withLocalVisibleTurnItems({
832 ...base,
833 attempts: upsertById(base.attempts, event.payload),
834 });
835 case "node.updated":
836 return {
837 ...base,
838 nodes: upsertById(base.nodes, event.payload),
839 };
840 case "subagent.updated":
841 return {
842 ...base,
843 subagents: upsertById(
844 base.subagents,
845 preserveCompletionDelivery(
846 base.subagents.find((task) => task.id === event.payload.id),
847 event.payload,
848 ),
849 ),
850 };
851 case "provider-session.attached":
852 case "provider-session.updated":
853 return {
854 ...base,
855 providerSessions: upsertById(base.providerSessions, event.payload),
856 };
857 case "provider-session.detached":
858 return {
859 ...base,
860 providerSessions: base.providerSessions.filter(
861 (session) => session.id !== event.payload.providerSessionId,
862 ),
863 };
864 case "provider-thread.updated":
865 return {
866 ...base,
867 thread:
868 event.payload.appThreadId === base.thread.id &&
869 !isQueuedProviderThreadPlaceholder(event.payload)
870 ? {
871 ...base.thread,
872 activeProviderThreadId: event.payload.id,
873 }
874 : base.thread,
875 providerThreads: upsertById(base.providerThreads, event.payload),
876 };
877 case "provider-turn.updated":
878 return {
879 ...base,
880 providerTurns: upsertProviderTurn(base.providerTurns, event.payload),
881 };
882 case "runtime-request.updated":
883 return {
884 ...base,
885 runtimeRequests: upsertById(base.runtimeRequests, event.payload),
886 };
887 case "message.updated":
888 return {
889 ...base,
890 messages: upsertById(base.messages, event.payload),
891 };
892 case "turn-item.updated":
893 return withLocalVisibleTurnItems({
894 ...base,
895 turnItems: upsertById(base.turnItems, event.payload),
896 });
897 case "plan.updated":
898 return {
899 ...base,
900 plans: upsertById(base.plans, event.payload),
901 };
902 case "checkpoint-scope.created":
903 return {
904 ...base,
905 checkpointScopes: upsertById(base.checkpointScopes, event.payload),
906 };
907 case "checkpoint.captured":
908 return {
909 ...base,
910 checkpoints: upsertById(base.checkpoints, event.payload),
911 };
912 case "checkpoint.rollback-requested":
913 return base;
914 case "context-handoff.updated":
915 return {
916 ...base,
917 contextHandoffs: upsertById(base.contextHandoffs, event.payload),
918 };
919 case "context-transfer.created":
920 case "context-transfer.updated":
921 return {
922 ...base,
923 contextTransfers: upsertById(base.contextTransfers, event.payload),
924 };
925 }
926}
927
928/**
929 * Replay state for entities whose persisted projection is shared across thread bindings.
930 *
931 * Provider sessions are process-scoped: one session row can be bound to several app
932 * threads. Updating that row changes what every bound thread reads, even though the
933 * application event itself belongs to one thread stream. Keeping the binding index here
934 * makes in-memory replay match the normalized SQL projection without scanning every
935 * thread for every session event.
936 */
937export interface ProjectionReplayState {
938 readonly projections: Map<ThreadId, OrchestrationV2ThreadProjection>;
939 readonly providerSessionThreadIds: Map<ProviderSessionId, ReadonlySet<ThreadId>>;
940}
941
942function makeProjectionReplayState(): ProjectionReplayState {
943 return {
944 projections: new Map(),
945 providerSessionThreadIds: new Map(),
946 };
947}
948
949function applyToProjectionReplayState(
950 state: ProjectionReplayState,
951 event: OrchestrationV2DomainEvent,
952): boolean {
953 if (event.type === "thread.created" && !state.projections.has(event.threadId)) {
954 state.projections.set(event.threadId, emptyProjection(event));
955 return true;
956 }
957
958 const current = state.projections.get(event.threadId);
959 if (current === undefined) {
960 return false;
961 }
962
963 let next = applyToProjection(current, event);
964 if (event.type === "provider-session.updated") {
965 const boundThreadIds = state.providerSessionThreadIds.get(event.payload.id);
966 if (boundThreadIds?.has(event.threadId) !== true) {
967 // The SQL projection updates the global session row but does not implicitly
968 // create a thread binding for an update event.
969 next = { ...next, providerSessions: current.providerSessions };
970 }
971 }
972 state.projections.set(event.threadId, next);
973
974 switch (event.type) {
975 case "provider-session.attached": {
976 const boundThreadIds = new Set(state.providerSessionThreadIds.get(event.payload.id) ?? []);
977 boundThreadIds.add(event.threadId);
978 state.providerSessionThreadIds.set(event.payload.id, boundThreadIds);
979 for (const threadId of boundThreadIds) {
980 if (threadId === event.threadId) continue;
981 const projection = state.projections.get(threadId);
982 if (projection === undefined) continue;
983 state.projections.set(threadId, {
984 ...projection,
985 providerSessions: upsertById(projection.providerSessions, event.payload),
986 });
987 }
988 break;
989 }
990 case "provider-session.updated": {
991 const boundThreadIds = state.providerSessionThreadIds.get(event.payload.id) ?? [];
992 for (const threadId of boundThreadIds) {
993 if (threadId === event.threadId) continue;
994 const projection = state.projections.get(threadId);
995 if (projection === undefined) continue;
996 state.projections.set(threadId, {
997 ...projection,
998 providerSessions: upsertById(projection.providerSessions, event.payload),
999 });
1000 }
1001 break;
1002 }
1003 case "provider-session.detached": {
1004 const boundThreadIds = new Set(
1005 state.providerSessionThreadIds.get(event.payload.providerSessionId) ?? [],
1006 );
1007 boundThreadIds.delete(event.threadId);
1008 if (boundThreadIds.size === 0) {
1009 state.providerSessionThreadIds.delete(event.payload.providerSessionId);
1010 } else {
1011 state.providerSessionThreadIds.set(event.payload.providerSessionId, boundThreadIds);
1012 }
1013 break;
1014 }
1015 }
1016
1017 return true;
1018}
1019
1020type PayloadRow = {
1021 readonly payload_json: string;
1022};
1023
1024type ShellThreadRow = {
1025 readonly thread_id: string;
1026 readonly payload_json: string;
1027 readonly forked_from_run_source_thread_id: string | null;
1028 readonly latest_run_id: string | null;
1029 readonly latest_run_status: string | null;
1030 readonly latest_run_requested_at: string | null;
1031 readonly latest_run_started_at: string | null;
1032 readonly latest_run_completed_at: string | null;
1033 readonly active_run_id: string | null;
1034 readonly activity_run_status: string | null;
1035 readonly activity_run_started_at: string | null;
1036 readonly last_error: string | null;
1037 readonly terminal_failure_payload_json: string | null;
1038 readonly blocking_run_id: string | null;
1039 readonly blocking_run_requested_at: string | null;
1040 readonly blocking_run_started_at: string | null;
1041 readonly blocking_run_completed_at: string | null;
1042 readonly blocking_failure_payload_json: string | null;
1043 readonly pending_request_payload_json: string | null;
1044 readonly pending_secret_request_payload_json: string | null;
1045 readonly latest_user_message_at: string | null;
1046 readonly latest_user_authored_message_at: string | null;
1047 readonly has_actionable_proposed_plan: number;
1048 readonly item_count: number;
1049 readonly runless_item_count: number;
1050};
1051
1052type ShellRunRow = {
1053 readonly thread_id: string;
1054 readonly run_id: string;
1055 readonly ordinal: number;
1056};
1057
1058type SettlementThreadRow = Pick<
1059 ShellThreadRow,
1060 | "thread_id"
1061 | "payload_json"
1062 | "latest_run_id"
1063 | "latest_run_status"
1064 | "latest_run_requested_at"
1065 | "latest_run_started_at"
1066 | "latest_run_completed_at"
1067 | "latest_user_message_at"
1068 | "latest_user_authored_message_at"
1069>;
1070
1071type ShellRunItemCountRow = {
1072 readonly thread_id: string;
1073 readonly run_id: string;
1074 readonly item_count: number;
1075};
1076
1077const encodeTimelineSources = Schema.encodeEffect(
1078 Schema.fromJsonString(
1079 Schema.Array(Schema.Struct({ threadId: Schema.String, id: Schema.String })),
1080 ),
1081);
1082const encodeIdList = Schema.encodeSync(Schema.fromJsonString(Schema.Array(Schema.String)));
1083
1084const encodeThreadPayload = Schema.encodeEffect(
1085 Schema.fromJsonString(OrchestrationV2AppThreadJsonSchema),
1086);
1087const encodeThreadUpdatedAt = Schema.encodeEffect(
1088 OrchestrationV2AppThreadJsonSchema.fields.updatedAt,
1089);
1090const encodeRunPayload = Schema.encodeEffect(Schema.fromJsonString(OrchestrationV2RunJsonSchema));
1091const encodeRestartCancelledBackgroundWork = Schema.encodeEffect(
1092 Schema.fromJsonString(OrchestrationV2RunJsonSchema.fields.restartCancelledBackgroundWork),
1093);
1094const encodeRunAttemptPayload = Schema.encodeEffect(
1095 Schema.fromJsonString(OrchestrationV2RunAttemptJsonSchema),
1096);
1097const encodeNodePayload = Schema.encodeEffect(
1098 Schema.fromJsonString(OrchestrationV2ExecutionNodeJsonSchema),
1099);
1100const encodeSubagentPayload = Schema.encodeEffect(
1101 Schema.fromJsonString(OrchestrationV2SubagentJsonSchema),
1102);
1103const encodeProviderSessionPayload = Schema.encodeEffect(
1104 Schema.fromJsonString(OrchestrationV2ProviderSessionJsonSchema),
1105);
1106/** Encodes a provider thread exactly as its projection row stores `payload_json`. */
1107export const encodeProviderThreadPayload = Schema.encodeEffect(
1108 Schema.fromJsonString(OrchestrationV2ProviderThreadJsonSchema),
1109);
1110const encodeProviderTurnPayload = Schema.encodeEffect(
1111 Schema.fromJsonString(OrchestrationV2ProviderTurnJsonSchema),
1112);
1113const encodeRuntimeRequestPayload = Schema.encodeEffect(
1114 Schema.fromJsonString(OrchestrationV2RuntimeRequestJsonSchema),
1115);
1116const encodeMessagePayload = Schema.encodeEffect(
1117 Schema.fromJsonString(OrchestrationV2ConversationMessageJsonSchema),
1118);
1119const encodePlanPayload = Schema.encodeEffect(
1120 Schema.fromJsonString(OrchestrationV2PlanArtifactSchema),
1121);
1122const encodeTurnItemPayload = Schema.encodeEffect(
1123 Schema.fromJsonString(OrchestrationV2TurnItemJsonSchema),
1124);
1125const encodeCheckpointScopePayload = Schema.encodeEffect(
1126 Schema.fromJsonString(OrchestrationV2CheckpointScopeJsonSchema),
1127);
1128const encodeCheckpointPayload = Schema.encodeEffect(
1129 Schema.fromJsonString(OrchestrationV2CheckpointJsonSchema),
1130);
1131const encodeContextHandoffPayload = Schema.encodeEffect(
1132 Schema.fromJsonString(OrchestrationV2ContextHandoffJsonSchema),
1133);
1134const encodeContextTransferPayload = Schema.encodeEffect(
1135 Schema.fromJsonString(OrchestrationV2ContextTransferJsonSchema),
1136);
1137
1138const decodeThreadPayload = Schema.decodeUnknownEffect(
1139 Schema.fromJsonString(OrchestrationV2AppThreadJsonSchema),
1140);
1141const decodeRunPayload = Schema.decodeUnknownEffect(
1142 Schema.fromJsonString(OrchestrationV2RunJsonSchema),
1143);
1144const decodeRunAttemptPayload = Schema.decodeUnknownEffect(
1145 Schema.fromJsonString(OrchestrationV2RunAttemptJsonSchema),
1146);
1147const decodeNodePayload = Schema.decodeUnknownEffect(
1148 Schema.fromJsonString(OrchestrationV2ExecutionNodeJsonSchema),
1149);
1150const decodeSubagentPayload = Schema.decodeUnknownEffect(
1151 Schema.fromJsonString(OrchestrationV2SubagentJsonSchema),
1152);
1153const decodeProviderSessionPayload = Schema.decodeUnknownEffect(
1154 Schema.fromJsonString(OrchestrationV2ProviderSessionJsonSchema),
1155);
1156const decodeProviderThreadPayload = Schema.decodeUnknownEffect(
1157 Schema.fromJsonString(OrchestrationV2ProviderThreadJsonSchema),
1158);
1159const decodeProviderTurnPayload = Schema.decodeUnknownEffect(
1160 Schema.fromJsonString(OrchestrationV2ProviderTurnJsonSchema),
1161);
1162const decodeRuntimeRequestPayload = Schema.decodeUnknownEffect(
1163 Schema.fromJsonString(OrchestrationV2RuntimeRequestJsonSchema),
1164);
1165const decodeMessagePayload = Schema.decodeUnknownEffect(
1166 Schema.fromJsonString(OrchestrationV2ConversationMessageJsonSchema),
1167);
1168const decodePlanArtifact = Schema.decodeUnknownEffect(OrchestrationV2PlanArtifactSchema);
1169const decodePlanPayload = (json: string) => decodePlanArtifact(parseEncodedPayload(json));
1170const decodeTurnItemPayload = Schema.decodeUnknownEffect(
1171 Schema.fromJsonString(OrchestrationV2TurnItemJsonSchema),
1172);
1173const decodeCheckpointScopePayload = Schema.decodeUnknownEffect(
1174 Schema.fromJsonString(OrchestrationV2CheckpointScopeJsonSchema),
1175);
1176const decodeCheckpointPayload = Schema.decodeUnknownEffect(
1177 Schema.fromJsonString(OrchestrationV2CheckpointJsonSchema),
1178);
1179const decodeContextHandoffPayload = Schema.decodeUnknownEffect(
1180 Schema.fromJsonString(OrchestrationV2ContextHandoffJsonSchema),
1181);
1182const decodeContextTransferPayload = Schema.decodeUnknownEffect(
1183 Schema.fromJsonString(OrchestrationV2ContextTransferJsonSchema),
1184);
1185
1186const isProjectionStoreThreadNotFoundError = Schema.is(ProjectionStoreThreadNotFoundError);
1187const isProjectionStoreReadError = Schema.is(ProjectionStoreReadError);
1188
1189function parseEncodedPayload(json: string): Record<string, unknown> {
1190 return JSON.parse(json) as Record<string, unknown>;
1191}
1192
1193function stringField(payload: Record<string, unknown>, field: string): string {
1194 const value = payload[field];
1195 return typeof value === "string" ? value : String(value);
1196}
1197
1198function nullableStringField(payload: Record<string, unknown>, field: string): string | null {
1199 const value = payload[field];
1200 if (value === null || value === undefined) {
1201 return null;
1202 }
1203 return typeof value === "string" ? value : String(value);
1204}
1205
1206function booleanInt(value: boolean): 0 | 1 {
1207 return value ? 1 : 0;
1208}
1209
1210const decodeRows =
1211 <A, E>(decode: (json: string) => Effect.Effect<A, E>, threadId: ThreadId) =>
1212 (rows: ReadonlyArray<PayloadRow>): Effect.Effect<Array<A>, ProjectionStoreReadError> =>
1213 Effect.forEach(rows, (row) => decode(row.payload_json)).pipe(
1214 Effect.mapError(
1215 (cause) =>
1216 new ProjectionStoreReadError({
1217 threadId,
1218 cause,
1219 }),
1220 ),
1221 );
1222
1223function messageIdForTurnItem(item: OrchestrationV2TurnItem): string | null {
1224 switch (item.type) {
1225 case "user_message":
1226 case "assistant_message":
1227 return item.messageId;
1228 default:
1229 return null;
1230 }
1231}
1232
1233function sortMessagesByTurnItemOrder(
1234 messages: ReadonlyArray<OrchestrationV2ConversationMessage>,
1235 turnItems: ReadonlyArray<OrchestrationV2TurnItem>,
1236 messageOrdinals = new Map<string, number>(),
1237): Array<OrchestrationV2ConversationMessage> {
1238 for (const turnItem of turnItems) {
1239 const messageId = messageIdForTurnItem(turnItem);
1240 if (messageId === null) {
1241 continue;
1242 }
1243 const existing = messageOrdinals.get(messageId);
1244 if (existing === undefined || turnItem.ordinal < existing) {
1245 messageOrdinals.set(messageId, turnItem.ordinal);
1246 }
1247 }
1248
1249 return messages.toSorted((left, right) => {
1250 const leftOrdinal = messageOrdinals.get(left.id) ?? Number.MAX_SAFE_INTEGER;
1251 const rightOrdinal = messageOrdinals.get(right.id) ?? Number.MAX_SAFE_INTEGER;
1252 if (leftOrdinal !== rightOrdinal) {
1253 return leftOrdinal - rightOrdinal;
1254 }
1255
1256 const leftCreatedAt = DateTime.toEpochMillis(left.createdAt);
1257 const rightCreatedAt = DateTime.toEpochMillis(right.createdAt);
1258 if (leftCreatedAt !== rightCreatedAt) {
1259 return leftCreatedAt - rightCreatedAt;
1260 }
1261
1262 return left.id.localeCompare(right.id);
1263 });
1264}
1265
1266function activeLocalTurnItems(
1267 projection: OrchestrationV2ThreadProjection,
1268): Array<OrchestrationV2ProjectedTurnItem> {
1269 const isVisible = createOrchestrationV2TurnItemVisibility({
1270 runs: projection.runs,
1271 attempts: projection.attempts,
1272 items: projection.turnItems,
1273 });
1274 return projection.turnItems.filter(isVisible).map((item, position) => ({
1275 position,
1276 visibility: "local" as const,
1277 sourceThreadId: item.threadId,
1278 sourceItemId: item.id,
1279 item,
1280 }));
1281}
1282
1283function localVisibleTurnItems(
1284 projection: OrchestrationV2ThreadProjection,
1285): Array<OrchestrationV2ProjectedTurnItem> {
1286 return activeLocalTurnItems(projection);
1287}
1288
1289function inheritedVisibleTurnItemsFromLocalItems(
1290 items: ReadonlyArray<OrchestrationV2TurnItem>,
1291): Array<Omit<OrchestrationV2ProjectedTurnItem, "position">> {
1292 return items.map((item) => ({
1293 visibility: "inherited" as const,
1294 sourceThreadId: item.threadId,
1295 sourceItemId: item.id,
1296 item,
1297 }));
1298}
1299
1300function withLocalVisibleTurnItems(
1301 projection: OrchestrationV2ThreadProjection,
1302): OrchestrationV2ThreadProjection {
1303 return {
1304 ...projection,
1305 visibleTurnItems: localVisibleTurnItems(projection),
1306 };
1307}
1308
1309function renumberVisibleTurnItems(
1310 rows: ReadonlyArray<Omit<OrchestrationV2ProjectedTurnItem, "position">>,
1311): Array<OrchestrationV2ProjectedTurnItem> {
1312 return rows.map((row, position) => ({ ...row, position }));
1313}
1314
1315function makeForkMarkerTurnItem(input: {
1316 readonly targetProjection: Pick<OrchestrationV2ThreadProjection, "thread">;
1317 readonly sourceThreadId: ThreadId;
1318 readonly sourceRunId: NonNullable<OrchestrationV2TurnItem["runId"]>;
1319}): Extract<OrchestrationV2TurnItem, { type: "fork" }> {
1320 const createdAt = input.targetProjection.thread.createdAt;
1321 return {
1322 id: TurnItemId.make(`turn-item:fork:${input.targetProjection.thread.id}`),
1323 threadId: input.targetProjection.thread.id,
1324 runId: null,
1325 nodeId: null,
1326 providerTurnId: null,
1327 nativeItemRef: null,
1328 parentItemId: null,
1329 ordinal: 0,
1330 status: "completed",
1331 title: "Forked from conversation",
1332 startedAt: null,
1333 completedAt: createdAt,
1334 updatedAt: createdAt,
1335 type: "fork",
1336 source: { type: "run", threadId: input.sourceThreadId, runId: input.sourceRunId },
1337 targetThreadId: input.targetProjection.thread.id,
1338 };
1339}
1340
1341export function isTurnItemAtOrBeforeRun(input: {
1342 readonly historyOrigin: OrchestrationV2ThreadProjection["thread"]["historyOrigin"];
1343 readonly itemRunId: OrchestrationV2TurnItem["runId"];
1344 readonly runOrdinalById: ReadonlyMap<NonNullable<OrchestrationV2TurnItem["runId"]>, number>;
1345 readonly sourceRunOrdinal: number;
1346}): boolean {
1347 if (input.itemRunId === null) {
1348 return input.historyOrigin === "v1_import";
1349 }
1350 const ordinal = input.runOrdinalById.get(input.itemRunId);
1351 return ordinal !== undefined && ordinal <= input.sourceRunOrdinal;
1352}
1353
1354function visibleTurnItemsThroughRun(input: {
1355 readonly sourceProjection: OrchestrationV2ThreadProjection;
1356 readonly sourceRunId: NonNullable<OrchestrationV2TurnItem["runId"]>;
1357}): Array<Omit<OrchestrationV2ProjectedTurnItem, "position">> {
1358 const sourceRun = input.sourceProjection.runs.find((run) => run.id === input.sourceRunId);
1359 if (sourceRun === undefined) {
1360 return [];
1361 }
1362
1363 const runOrdinalById = new Map(input.sourceProjection.runs.map((run) => [run.id, run.ordinal]));
1364 const inheritedPrefix = input.sourceProjection.visibleTurnItems
1365 .filter(
1366 (row) => row.item.threadId !== input.sourceProjection.thread.id || row.item.type === "fork",
1367 )
1368 .map((row) => ({
1369 visibility: "inherited" as const,
1370 sourceThreadId: row.sourceThreadId,
1371 sourceItemId: row.sourceItemId,
1372 item: row.item,
1373 }));
1374 const localPrefix = inheritedVisibleTurnItemsFromLocalItems(
1375 input.sourceProjection.turnItems.filter((item) => {
1376 if (
1377 isOrchestrationV2SupersededInterrupt({
1378 item,
1379 attempts: input.sourceProjection.attempts,
1380 items: input.sourceProjection.turnItems,
1381 })
1382 ) {
1383 return false;
1384 }
1385 return isTurnItemAtOrBeforeRun({
1386 historyOrigin: input.sourceProjection.thread.historyOrigin,
1387 itemRunId: item.runId,
1388 runOrdinalById,
1389 sourceRunOrdinal: sourceRun.ordinal,
1390 });
1391 }),
1392 );
1393
1394 return [...inheritedPrefix, ...localPrefix];
1395}
1396
1397function buildVisibleTurnItems(input: {
1398 readonly projection: OrchestrationV2ThreadProjection;
1399 readonly sourceProjection: OrchestrationV2ThreadProjection | null;
1400}): Array<OrchestrationV2ProjectedTurnItem> {
1401 const forkedFrom = input.projection.thread.forkedFrom;
1402 if (forkedFrom?.type !== "run" || input.sourceProjection === null) {
1403 return localVisibleTurnItems(input.projection);
1404 }
1405
1406 const inherited = visibleTurnItemsThroughRun({
1407 sourceProjection: input.sourceProjection,
1408 sourceRunId: forkedFrom.runId,
1409 });
1410 const markerItem = makeForkMarkerTurnItem({
1411 targetProjection: input.projection,
1412 sourceThreadId: forkedFrom.threadId,
1413 sourceRunId: forkedFrom.runId,
1414 });
1415 const local = activeLocalTurnItems(input.projection).map((row) => ({
1416 visibility: "local" as const,
1417 sourceThreadId: row.sourceThreadId,
1418 sourceItemId: row.sourceItemId,
1419 item: row.item,
1420 }));
1421
1422 return renumberVisibleTurnItems([
1423 ...inherited,
1424 {
1425 visibility: "synthetic",
1426 sourceThreadId: forkedFrom.threadId,
1427 sourceItemId: markerItem.id,
1428 item: markerItem,
1429 },
1430 ...local,
1431 ]);
1432}
1433
1434/**
1435 * An agent waiting on a secret is waiting on the user just like a question,
1436 * so the shell reports it as pending user input. Secret requests have no
1437 * runtime request of their own; this stands one in for the shell summary
1438 * only, keyed by the card's turn item.
1439 */
1440function secretRequestAsPendingInput(
1441 item: OrchestrationV2TurnItem | null,
1442): OrchestrationV2ThreadProjection["runtimeRequests"][number] | null {
1443 if (item?.type !== "secret_request" || item.nodeId === null) return null;
1444 return {
1445 id: RuntimeRequestId.make(item.id),
1446 nodeId: item.nodeId,
1447 providerTurnId: item.providerTurnId,
1448 nativeRequestRef: null,
1449 kind: "user_input",
1450 status: "pending",
1451 responseCapability: { type: "message" },
1452 createdAt: item.startedAt ?? item.updatedAt,
1453 resolvedAt: null,
1454 };
1455}
1456
1457export function threadShellFromProjection(
1458 projection: OrchestrationV2ThreadProjection,
1459): OrchestrationV2ThreadShell {
1460 const providerSession =
1461 projection.providerSessions
1462 .filter((session) => session.providerInstanceId === projection.thread.providerInstanceId)
1463 .toSorted(
1464 (left, right) =>
1465 DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt),
1466 )[0] ?? null;
1467 const latestRun =
1468 usageLimitRunPresentedAsLatest(
1469 projection.runs,
1470 projection.turnItems,
1471 providerSession?.lastError ?? null,
1472 ) ?? latestUnheldRun(projection.runs);
1473 const activeRun =
1474 projection.runs
1475 .filter(isInterruptibleRunForShell)
1476 .toSorted((left, right) => right.ordinal - left.ordinal)[0] ?? null;
1477 const activityRun =
1478 projection.runs
1479 .filter(isActivityRunForShell)
1480 .toSorted((left, right) => right.ordinal - left.ordinal)[0] ?? null;
1481 const liveRunIds = new Set(projection.runs.filter(isActivityRunForShell).map((run) => run.id));
1482 const pendingRuntimeRequest =
1483 projection.runtimeRequests
1484 .filter((request) => request.status === "pending")
1485 .toSorted(
1486 (left, right) =>
1487 DateTime.toEpochMillis(right.createdAt) - DateTime.toEpochMillis(left.createdAt),
1488 )[0] ??
1489 secretRequestAsPendingInput(
1490 projection.turnItems
1491 .filter(
1492 (item) =>
1493 item.type === "secret_request" &&
1494 item.status === "waiting" &&
1495 item.runId !== null &&
1496 liveRunIds.has(item.runId),
1497 )
1498 .toSorted(
1499 (left, right) =>
1500 DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt),
1501 )[0] ?? null,
1502 );
1503 const userMessages = projection.messages
1504 .filter((message) => message.role === "user")
1505 .toSorted(
1506 (left, right) =>
1507 DateTime.toEpochMillis(right.updatedAt) - DateTime.toEpochMillis(left.updatedAt),
1508 );
1509 const latestUserMessage = userMessages[0] ?? null;
1510 const pendingBackgroundTasks = derivePendingBackgroundWork({
1511 latestRun,
1512 providerThreads: projection.providerThreads,
1513 turnItems: projection.turnItems,
1514 activeProviderThreadId: projection.thread.activeProviderThreadId,
1515 runs: projection.runs,
1516 pullRequests: projection.thread.pullRequests,
1517 });
1518 return {
1519 createdBy: projection.thread.createdBy,
1520 creationSource: projection.thread.creationSource,
1521 id: projection.thread.id,
1522 projectId: projection.thread.projectId,
1523 title: projection.thread.title,
1524 providerInstanceId: projection.thread.providerInstanceId,
1525 modelSelection: projection.thread.modelSelection,
1526 runtimeMode: projection.thread.runtimeMode,
1527 interactionMode: projection.thread.interactionMode,
1528 branch: projection.thread.branch,
1529 worktreePath: projection.thread.worktreePath,
1530 pullRequests: threadPullRequestsOf(projection.thread),
1531 ...(projection.thread.linkedPullRequest === undefined
1532 ? {}
1533 : { linkedPullRequest: projection.thread.linkedPullRequest }),
1534 ...(projection.thread.branchPullRequest === undefined
1535 ? {}
1536 : { branchPullRequest: projection.thread.branchPullRequest }),
1537 ...(projection.thread.activeOrderKey === undefined
1538 ? {}
1539 : { activeOrderKey: projection.thread.activeOrderKey }),
1540 lineage: projection.thread.lineage,
1541 forkedFrom: projection.thread.forkedFrom,
1542 activeProviderThreadId: projection.thread.activeProviderThreadId,
1543 ...(projection.thread.historyOrigin === undefined
1544 ? {}
1545 : { historyOrigin: projection.thread.historyOrigin }),
1546 latestRunId: latestRun?.id ?? null,
1547 latestRunRequestedAt: latestRun?.requestedAt ?? null,
1548 latestRunStartedAt: latestRun?.startedAt ?? null,
1549 latestRunCompletedAt: latestRun?.completedAt ?? null,
1550 activeRunId: activeRun?.id ?? null,
1551 activityRunStatus: activityRun?.status ?? null,
1552 activityRunStartedAt:
1553 activityRun === null ? null : orchestrationV2RunWorkStartedAt(activityRun),
1554 status: latestRun?.status ?? "idle",
1555 ...threadErrorSummary(
1556 latestRootProviderFailure(latestRun, projection.turnItems),
1557 providerSession?.lastError ?? null,
1558 ),
1559 pendingRuntimeRequest:
1560 pendingRuntimeRequest === null
1561 ? null
1562 : {
1563 id: pendingRuntimeRequest.id,
1564 kind: pendingRuntimeRequest.kind,
1565 createdAt: pendingRuntimeRequest.createdAt,
1566 },
1567 // Thread detail owns message bodies. Keeping them out of shell rows makes
1568 // initial hydration and streaming updates independent of transcript size.
1569 latestVisibleMessage: null,
1570 latestUserMessageAt: latestUserMessage?.updatedAt ?? null,
1571 latestUserAuthoredMessageAt:
1572 userMessages.find((message) => message.createdBy === "user")?.updatedAt ?? null,
1573 hasActionableProposedPlan: projection.plans.some(
1574 (plan) => plan.kind === "proposed_plan" && plan.status === "active",
1575 ),
1576 pendingBackgroundTasks: [...pendingBackgroundTasks],
1577 providerInstanceHistory: providerInstanceHistoryForShell({
1578 threadId: projection.thread.id,
1579 providerThreads: projection.providerThreads,
1580 }),
1581 goal: activeProviderGoalForShell(
1582 projection.providerThreads,
1583 projection.thread.activeProviderThreadId,
1584 ),
1585 itemCount: activeLocalTurnItems(projection).length,
1586 visibleItemCount: projection.visibleTurnItems.length,
1587 createdAt: projection.thread.createdAt,
1588 updatedAt: projection.updatedAt,
1589 archivedAt: projection.thread.archivedAt,
1590 settledOverride: projection.thread.settledOverride,
1591 settledAt: projection.thread.settledAt,
1592 unsettledAt: projection.thread.unsettledAt ?? null,
1593 snoozedUntil: projection.thread.snoozedUntil ?? null,
1594 snoozedAt: projection.thread.snoozedAt ?? null,
1595 pinnedAt: projection.thread.pinnedAt ?? null,
1596
1597 autoSettleDisabledAt: projection.thread.autoSettleDisabledAt ?? null,
1598 pinOrderKey: projection.thread.pinOrderKey ?? null,
1599 lastVisitedAt: projection.thread.lastVisitedAt,
1600 titleRegeneration: projection.thread.titleRegeneration ?? null,
1601 limitRecovery: projection.thread.limitRecovery ?? null,
1602 deletedAt: projection.thread.deletedAt,
1603 };
1604}
1605
1606/**
1607 * Provider instances that have owned this thread's root conversation, oldest
1608 * first. Subagent provider threads carry an owner node and are excluded so a
1609 * delegated Codex child does not make a Claude thread look handed off.
1610 */
1611function providerInstanceHistoryForShell(input: {
1612 readonly threadId: ThreadId;
1613 readonly providerThreads: ReadonlyArray<
1614 OrchestrationV2ThreadProjection["providerThreads"][number]
1615 >;
1616}): ReadonlyArray<ProviderInstanceId> {
1617 const history: Array<ProviderInstanceId> = [];
1618 for (const providerThread of input.providerThreads
1619 .filter((thread) => thread.appThreadId === input.threadId && thread.ownerNodeId === null)
1620 .toSorted(
1621 (left, right) =>
1622 DateTime.toEpochMillis(left.createdAt) - DateTime.toEpochMillis(right.createdAt) ||
1623 left.id.localeCompare(right.id),
1624 )) {
1625 if (!history.includes(providerThread.providerInstanceId)) {
1626 history.push(providerThread.providerInstanceId);
1627 }
1628 }
1629 return history;
1630}
1631
1632/** The native goal lives on the provider thread that currently owns the conversation. */
1633function activeProviderGoalForShell(
1634 providerThreads: ReadonlyArray<OrchestrationV2ThreadProjection["providerThreads"][number]>,
1635 activeProviderThreadId: OrchestrationV2ThreadProjection["thread"]["activeProviderThreadId"],
1636): OrchestrationV2ThreadShell["goal"] {
1637 return providerThreads.find((thread) => thread.id === activeProviderThreadId)?.goal ?? null;
1638}
1639
1640function isInterruptibleRunForShell(run: OrchestrationV2ThreadProjection["runs"][number]): boolean {
1641 return run.status === "preparing" || run.status === "starting" || run.status === "running";
1642}
1643
1644type ShellActivityRunStatus = "preparing" | "running" | "starting" | "waiting";
1645
1646function isActivityRunForShell(
1647 run: OrchestrationV2ThreadProjection["runs"][number],
1648): run is OrchestrationV2ThreadProjection["runs"][number] & {
1649 readonly status: ShellActivityRunStatus;
1650} {
1651 return isInterruptibleRunForShell(run) || run.status === "waiting";
1652}
1653
1654type ShellThreadState = {
1655 readonly thread: OrchestrationV2ThreadProjection["thread"];
1656 readonly latestRunId: RunId | null;
1657 readonly latestRunStatus: OrchestrationV2ShellThreadStatus;
1658 readonly latestRunRequestedAt: DateTime.Utc | null;
1659 readonly latestRunStartedAt: DateTime.Utc | null;
1660 readonly latestRunCompletedAt: DateTime.Utc | null;
1661 readonly activeRunId: RunId | null;
1662 readonly activityRunStatus: ShellActivityRunStatus | null;
1663 readonly activityRunStartedAt: DateTime.Utc | null;
1664 readonly lastError: string | null;
1665 readonly lastErrorClass: OrchestrationV2ThreadShell["lastErrorClass"];
1666 readonly usageLimitResetAt: OrchestrationV2ThreadShell["usageLimitResetAt"];
1667 readonly pendingRuntimeRequest: OrchestrationV2ThreadProjection["runtimeRequests"][number] | null;
1668 readonly latestUserMessageAt: DateTime.Utc | null;
1669 readonly latestUserAuthoredMessageAt: DateTime.Utc | null;
1670 readonly hasActionableProposedPlan: boolean;
1671 readonly pendingBackgroundTasks: OrchestrationV2ThreadShell["pendingBackgroundTasks"];
1672 readonly providerInstanceHistory: OrchestrationV2ThreadShell["providerInstanceHistory"];
1673 readonly goal: OrchestrationV2ThreadShell["goal"];
1674 readonly itemCount: number;
1675 readonly runlessItemCount: number;
1676 readonly updatedAt: OrchestrationV2ThreadProjection["updatedAt"];
1677 readonly runOrdinalById: ReadonlyMap<RunId, number>;
1678 readonly itemCountByRunId: ReadonlyMap<RunId, number>;
1679};
1680
1681function shellStatusFromStoredRunStatus(status: string | null): OrchestrationV2ShellThreadStatus {
1682 switch (status) {
1683 case null:
1684 return "idle";
1685 case "preparing":
1686 case "queued":
1687 case "starting":
1688 case "running":
1689 case "waiting":
1690 case "completed":
1691 case "interrupted":
1692 case "failed":
1693 case "cancelled":
1694 case "rolled_back":
1695 return status;
1696 default:
1697 return "failed";
1698 }
1699}
1700
1701function itemCountThroughRun(input: {
1702 readonly state: ShellThreadState;
1703 readonly runId: RunId;
1704}): number {
1705 const runOrdinal = input.state.runOrdinalById.get(input.runId);
1706 if (runOrdinal === undefined) {
1707 return 0;
1708 }
1709
1710 let count = input.state.thread.historyOrigin === "v1_import" ? input.state.runlessItemCount : 0;
1711 for (const [runId, itemCount] of input.state.itemCountByRunId) {
1712 const itemRunOrdinal = input.state.runOrdinalById.get(runId);
1713 if (itemRunOrdinal !== undefined && itemRunOrdinal <= runOrdinal) {
1714 count += itemCount;
1715 }
1716 }
1717 return count;
1718}
1719
1720/**
1721 * Threads that some shell row forks from at a run. Only these need run
1722 * ordinals and per-run item counts, to count the inherited prefix, so a thread
1723 * nobody forked from never pays for a scan of its run history.
1724 */
1725function shellForkSourceIds(
1726 rows: ReadonlyArray<{ readonly forked_from_run_source_thread_id: string | null }>,
1727): ReadonlyArray<ThreadId> {
1728 return [
1729 ...new Set(
1730 rows.flatMap((row) =>
1731 row.forked_from_run_source_thread_id === null
1732 ? []
1733 : [ThreadId.make(row.forked_from_run_source_thread_id)],
1734 ),
1735 ),
1736 ];
1737}
1738
1739function visibleItemCountForShell(input: {
1740 readonly threadId: ThreadId;
1741 readonly statesByThreadId: ReadonlyMap<ThreadId, ShellThreadState>;
1742 readonly seenThreadIds?: ReadonlySet<ThreadId>;
1743}): number {
1744 const state = input.statesByThreadId.get(input.threadId);
1745 if (state === undefined) {
1746 return 0;
1747 }
1748
1749 const forkedFrom = state.thread.forkedFrom;
1750 if (forkedFrom?.type !== "run") {
1751 return state.itemCount;
1752 }
1753
1754 const seenThreadIds = input.seenThreadIds ?? new Set<ThreadId>();
1755 if (seenThreadIds.has(state.thread.id)) {
1756 return state.itemCount;
1757 }
1758
1759 const sourceState = input.statesByThreadId.get(forkedFrom.threadId);
1760 if (sourceState === undefined) {
1761 return state.itemCount;
1762 }
1763
1764 const sourceForkedFrom = sourceState.thread.forkedFrom;
1765 const inheritedPrefixCount =
1766 sourceForkedFrom?.type === "run"
1767 ? visibleItemCountForShell({
1768 threadId: sourceState.thread.id,
1769 statesByThreadId: input.statesByThreadId,
1770 seenThreadIds: new Set([...seenThreadIds, state.thread.id]),
1771 }) - sourceState.itemCount
1772 : 0;
1773
1774 return (
1775 inheritedPrefixCount +
1776 itemCountThroughRun({ state: sourceState, runId: forkedFrom.runId }) +
1777 1 +
1778 state.itemCount
1779 );
1780}
1781
1782function shellFromState(input: {
1783 readonly state: ShellThreadState;
1784 readonly visibleItemCount: number;
1785}): OrchestrationV2ThreadShell {
1786 return {
1787 createdBy: input.state.thread.createdBy,
1788 creationSource: input.state.thread.creationSource,
1789 id: input.state.thread.id,
1790 projectId: input.state.thread.projectId,
1791 title: input.state.thread.title,
1792 providerInstanceId: input.state.thread.providerInstanceId,
1793 modelSelection: input.state.thread.modelSelection,
1794 runtimeMode: input.state.thread.runtimeMode,
1795 interactionMode: input.state.thread.interactionMode,
1796 branch: input.state.thread.branch,
1797 worktreePath: input.state.thread.worktreePath,
1798 pullRequests: threadPullRequestsOf(input.state.thread),
1799 ...(input.state.thread.linkedPullRequest === undefined
1800 ? {}
1801 : { linkedPullRequest: input.state.thread.linkedPullRequest }),
1802 ...(input.state.thread.branchPullRequest === undefined
1803 ? {}
1804 : { branchPullRequest: input.state.thread.branchPullRequest }),
1805 ...(input.state.thread.activeOrderKey === undefined
1806 ? {}
1807 : { activeOrderKey: input.state.thread.activeOrderKey }),
1808 lineage: input.state.thread.lineage,
1809 forkedFrom: input.state.thread.forkedFrom,
1810 activeProviderThreadId: input.state.thread.activeProviderThreadId,
1811 ...(input.state.thread.historyOrigin === undefined
1812 ? {}
1813 : { historyOrigin: input.state.thread.historyOrigin }),
1814 latestRunId: input.state.latestRunId,
1815 latestRunRequestedAt: input.state.latestRunRequestedAt,
1816 latestRunStartedAt: input.state.latestRunStartedAt,
1817 latestRunCompletedAt: input.state.latestRunCompletedAt,
1818 activeRunId: input.state.activeRunId,
1819 activityRunStatus: input.state.activityRunStatus,
1820 activityRunStartedAt: input.state.activityRunStartedAt,
1821 status: input.state.latestRunStatus,
1822 lastError: input.state.lastError,
1823 lastErrorClass: input.state.lastErrorClass,
1824 usageLimitResetAt: input.state.usageLimitResetAt,
1825 pendingRuntimeRequest:
1826 input.state.pendingRuntimeRequest === null
1827 ? null
1828 : {
1829 id: input.state.pendingRuntimeRequest.id,
1830 kind: input.state.pendingRuntimeRequest.kind,
1831 createdAt: input.state.pendingRuntimeRequest.createdAt,
1832 },
1833 latestVisibleMessage: null,
1834 latestUserMessageAt: input.state.latestUserMessageAt,
1835 latestUserAuthoredMessageAt: input.state.latestUserAuthoredMessageAt,
1836 hasActionableProposedPlan: input.state.hasActionableProposedPlan,
1837 pendingBackgroundTasks: input.state.pendingBackgroundTasks,
1838 providerInstanceHistory: input.state.providerInstanceHistory,
1839 goal: input.state.goal,
1840 itemCount: input.state.itemCount,
1841 visibleItemCount: input.visibleItemCount,
1842 createdAt: input.state.thread.createdAt,
1843 updatedAt: input.state.updatedAt,
1844 archivedAt: input.state.thread.archivedAt,
1845 settledOverride: input.state.thread.settledOverride,
1846 settledAt: input.state.thread.settledAt,
1847 unsettledAt: input.state.thread.unsettledAt ?? null,
1848 snoozedUntil: input.state.thread.snoozedUntil ?? null,
1849 snoozedAt: input.state.thread.snoozedAt ?? null,
1850 pinnedAt: input.state.thread.pinnedAt ?? null,
1851
1852 autoSettleDisabledAt: input.state.thread.autoSettleDisabledAt ?? null,
1853 pinOrderKey: input.state.thread.pinOrderKey ?? null,
1854 lastVisitedAt: input.state.thread.lastVisitedAt,
1855 titleRegeneration: input.state.thread.titleRegeneration ?? null,
1856 limitRecovery: input.state.thread.limitRecovery ?? null,
1857 deletedAt: input.state.thread.deletedAt,
1858 };
1859}
1860
1861export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> = Layer.effect(
1862 ProjectionStoreV2,
1863 Effect.gen(function* () {
1864 const sql = yield* SqlClient.SqlClient;
1865
1866 // For run upserts: a snapshot without `path` keeps the value another event recorded there.
1867 const keepRecordedRunField = (payload: Statement.Fragment, path: string) => sql`
1868 CASE
1869 WHEN json_type(excluded.payload_json, ${path}) IS NULL
1870 AND json_type(orchestration_v2_projection_runs.payload_json, ${path}) IS NOT NULL
1871 THEN json_set(
1872 ${payload},
1873 ${path},
1874 json_extract(orchestration_v2_projection_runs.payload_json, ${path})
1875 )
1876 ELSE ${payload}
1877 END
1878 `;
1879
1880 const apply: ProjectionStoreV2Shape["apply"] = (event) =>
1881 Effect.gen(function* () {
1882 switch (event.type) {
1883 case "thread.pull-request-link-updated": {
1884 const thread = yield* getThread(event.threadId);
1885 yield* apply({
1886 ...event,
1887 type: "thread.pull-request-synced",
1888 payload: applyThreadPullRequestLinkUpdate(thread, event.payload),
1889 });
1890 break;
1891 }
1892 case "thread.created":
1893 case "thread.archived":
1894 case "thread.unarchived":
1895 case "thread.deleted":
1896 case "thread.settled":
1897 case "thread.unsettled":
1898 case "thread.snoozed":
1899 case "thread.unsnoozed":
1900 case "thread.auto-settle-set":
1901 case "thread.pinned":
1902 case "thread.unpinned":
1903 case "thread.pin-reordered":
1904 case "thread.active-reordered":
1905 case "thread.visited":
1906 case "thread.marked-unread":
1907 case "thread.metadata-updated":
1908 case "thread.pull-request-synced":
1909 case "thread.runtime-mode-updated":
1910 case "thread.interaction-mode-updated":
1911 case "thread.model-selection-updated":
1912 case "thread.provider-switched": {
1913 const payloadJson = yield* encodeThreadPayload(event.payload);
1914 const payload = parseEncodedPayload(payloadJson);
1915 yield* sql`
1916 INSERT INTO orchestration_v2_projection_threads (
1917 thread_id,
1918 project_id,
1919 title,
1920 default_provider,
1921 provider_instance_id,
1922 runtime_mode,
1923 interaction_mode,
1924 active_provider_thread_id,
1925 created_at,
1926 updated_at,
1927 archived_at,
1928 deleted_at,
1929 payload_json
1930 )
1931 VALUES (
1932 ${event.payload.id},
1933 ${event.payload.projectId},
1934 ${event.payload.title},
1935 ${event.payload.providerInstanceId},
1936 ${event.payload.providerInstanceId},
1937 ${event.payload.runtimeMode},
1938 ${event.payload.interactionMode},
1939 ${event.payload.activeProviderThreadId},
1940 ${stringField(payload, "createdAt")},
1941 ${stringField(payload, "updatedAt")},
1942 ${nullableStringField(payload, "archivedAt")},
1943 ${nullableStringField(payload, "deletedAt")},
1944 ${payloadJson}
1945 )
1946 ON CONFLICT(thread_id)
1947 DO UPDATE SET
1948 project_id = excluded.project_id,
1949 title = excluded.title,
1950 default_provider = excluded.default_provider,
1951 provider_instance_id = excluded.provider_instance_id,
1952 runtime_mode = excluded.runtime_mode,
1953 interaction_mode = excluded.interaction_mode,
1954 active_provider_thread_id = excluded.active_provider_thread_id,
1955 created_at = excluded.created_at,
1956 updated_at = excluded.updated_at,
1957 archived_at = excluded.archived_at,
1958 deleted_at = excluded.deleted_at,
1959 payload_json = excluded.payload_json
1960 `;
1961 break;
1962 }
1963 case "run.created":
1964 case "run.updated": {
1965 const payloadJson = yield* encodeRunPayload(event.payload);
1966 const payload = parseEncodedPayload(payloadJson);
1967 yield* sql`
1968 INSERT INTO orchestration_v2_projection_runs (
1969 run_id,
1970 thread_id,
1971 ordinal,
1972 provider,
1973 provider_instance_id,
1974 provider_thread_id,
1975 status,
1976 requested_at,
1977 completed_at,
1978 payload_json
1979 )
1980 VALUES (
1981 ${event.payload.id},
1982 ${event.payload.threadId},
1983 ${event.payload.ordinal},
1984 ${event.payload.providerInstanceId},
1985 ${event.payload.providerInstanceId},
1986 ${event.payload.providerThreadId},
1987 ${event.payload.status},
1988 ${stringField(payload, "requestedAt")},
1989 ${nullableStringField(payload, "completedAt")},
1990 ${payloadJson}
1991 )
1992 ON CONFLICT(run_id)
1993 DO UPDATE SET
1994 thread_id = excluded.thread_id,
1995 ordinal = excluded.ordinal,
1996 provider = excluded.provider,
1997 provider_instance_id = excluded.provider_instance_id,
1998 provider_thread_id = excluded.provider_thread_id,
1999 status = excluded.status,
2000 requested_at = excluded.requested_at,
2001 completed_at = excluded.completed_at,
2002 payload_json = ${keepRecordedRunField(
2003 keepRecordedRunField(sql`excluded.payload_json`, "$.delegatedCompletion"),
2004 "$.restartCancelledBackgroundWork",
2005 )}
2006 `;
2007 break;
2008 }
2009 case "run.background-work-cancelled": {
2010 // Only this field changes, so a concurrent lifecycle write is never regressed.
2011 const workJson = yield* encodeRestartCancelledBackgroundWork(
2012 event.payload.restartCancelledBackgroundWork,
2013 );
2014 yield* sql`
2015 UPDATE orchestration_v2_projection_runs
2016 SET payload_json = json_set(
2017 payload_json,
2018 '$.restartCancelledBackgroundWork',
2019 json(${workJson})
2020 )
2021 WHERE run_id = ${event.payload.runId} AND thread_id = ${event.threadId}
2022 `;
2023 break;
2024 }
2025 case "run-attempt.created":
2026 case "run-attempt.updated": {
2027 const payloadJson = yield* encodeRunAttemptPayload(event.payload);
2028 yield* sql`
2029 INSERT INTO orchestration_v2_projection_run_attempts (
2030 attempt_id,
2031 thread_id,
2032 run_id,
2033 attempt_ordinal,
2034 root_node_id,
2035 provider,
2036 provider_instance_id,
2037 provider_thread_id,
2038 provider_turn_id,
2039 status,
2040 payload_json
2041 )
2042 VALUES (
2043 ${event.payload.id},
2044 ${event.threadId},
2045 ${event.payload.runId},
2046 ${event.payload.attemptOrdinal},
2047 ${event.payload.rootNodeId},
2048 ${event.payload.providerInstanceId},
2049 ${event.payload.providerInstanceId},
2050 ${event.payload.providerThreadId},
2051 ${event.payload.providerTurnId},
2052 ${event.payload.status},
2053 ${payloadJson}
2054 )
2055 ON CONFLICT(attempt_id)
2056 DO UPDATE SET
2057 thread_id = excluded.thread_id,
2058 run_id = excluded.run_id,
2059 attempt_ordinal = excluded.attempt_ordinal,
2060 root_node_id = excluded.root_node_id,
2061 provider = excluded.provider,
2062 provider_instance_id = excluded.provider_instance_id,
2063 provider_thread_id = excluded.provider_thread_id,
2064 provider_turn_id = excluded.provider_turn_id,
2065 status = excluded.status,
2066 payload_json = excluded.payload_json
2067 `;
2068 break;
2069 }
2070 case "node.updated": {
2071 const payloadJson = yield* encodeNodePayload(event.payload);
2072 const payload = parseEncodedPayload(payloadJson);
2073 yield* sql`
2074 INSERT INTO orchestration_v2_projection_nodes (
2075 node_id,
2076 thread_id,
2077 run_id,
2078 parent_node_id,
2079 root_node_id,
2080 kind,
2081 status,
2082 provider_thread_id,
2083 provider_turn_id,
2084 runtime_request_id,
2085 checkpoint_scope_id,
2086 started_at,
2087 completed_at,
2088 payload_json
2089 )
2090 VALUES (
2091 ${event.payload.id},
2092 ${event.payload.threadId},
2093 ${event.payload.runId},
2094 ${event.payload.parentNodeId},
2095 ${event.payload.rootNodeId},
2096 ${event.payload.kind},
2097 ${event.payload.status},
2098 ${event.payload.providerThreadId},
2099 ${event.payload.providerTurnId},
2100 ${event.payload.runtimeRequestId},
2101 ${event.payload.checkpointScopeId},
2102 ${nullableStringField(payload, "startedAt")},
2103 ${nullableStringField(payload, "completedAt")},
2104 ${payloadJson}
2105 )
2106 ON CONFLICT(node_id)
2107 DO UPDATE SET
2108 thread_id = excluded.thread_id,
2109 run_id = excluded.run_id,
2110 parent_node_id = excluded.parent_node_id,
2111 root_node_id = excluded.root_node_id,
2112 kind = excluded.kind,
2113 status = excluded.status,
2114 provider_thread_id = excluded.provider_thread_id,
2115 provider_turn_id = excluded.provider_turn_id,
2116 runtime_request_id = excluded.runtime_request_id,
2117 checkpoint_scope_id = excluded.checkpoint_scope_id,
2118 started_at = excluded.started_at,
2119 completed_at = excluded.completed_at,
2120 payload_json = excluded.payload_json
2121 `;
2122 break;
2123 }
2124 case "subagent.updated": {
2125 const payloadJson = yield* encodeSubagentPayload(event.payload);
2126 const payload = parseEncodedPayload(payloadJson);
2127 yield* sql`
2128 INSERT INTO orchestration_v2_projection_subagents (
2129 subagent_id,
2130 thread_id,
2131 run_id,
2132 parent_node_id,
2133 provider,
2134 driver,
2135 provider_instance_id,
2136 provider_thread_id,
2137 child_thread_id,
2138 origin,
2139 status,
2140 started_at,
2141 completed_at,
2142 updated_at,
2143 payload_json
2144 )
2145 VALUES (
2146 ${event.payload.id},
2147 ${event.payload.threadId},
2148 ${event.payload.runId},
2149 ${event.payload.parentNodeId},
2150 ${event.payload.providerInstanceId},
2151 ${event.payload.driver},
2152 ${event.payload.providerInstanceId},
2153 ${event.payload.providerThreadId},
2154 ${event.payload.childThreadId},
2155 ${event.payload.origin},
2156 ${event.payload.status},
2157 ${nullableStringField(payload, "startedAt")},
2158 ${nullableStringField(payload, "completedAt")},
2159 ${stringField(payload, "updatedAt")},
2160 ${payloadJson}
2161 )
2162 ON CONFLICT(subagent_id)
2163 DO UPDATE SET
2164 thread_id = excluded.thread_id,
2165 run_id = excluded.run_id,
2166 parent_node_id = excluded.parent_node_id,
2167 provider = excluded.provider,
2168 driver = excluded.driver,
2169 provider_instance_id = excluded.provider_instance_id,
2170 provider_thread_id = excluded.provider_thread_id,
2171 child_thread_id = excluded.child_thread_id,
2172 origin = excluded.origin,
2173 status = excluded.status,
2174 started_at = excluded.started_at,
2175 completed_at = excluded.completed_at,
2176 updated_at = excluded.updated_at,
2177 payload_json = CASE
2178 WHEN json_type(excluded.payload_json, '$.completionDelivery') IS NULL
2179 AND json_type(orchestration_v2_projection_subagents.payload_json, '$.completionDelivery') IS NOT NULL
2180 THEN json_set(
2181 excluded.payload_json,
2182 '$.completionDelivery',
2183 json_extract(
2184 orchestration_v2_projection_subagents.payload_json,
2185 '$.completionDelivery'
2186 )
2187 )
2188 ELSE excluded.payload_json
2189 END
2190 `;
2191 break;
2192 }
2193 case "provider-session.attached":
2194 case "provider-session.updated": {
2195 const payloadJson = yield* encodeProviderSessionPayload(event.payload);
2196 const payload = parseEncodedPayload(payloadJson);
2197 yield* sql`
2198 INSERT INTO orchestration_v2_projection_provider_sessions (
2199 provider_session_id,
2200 thread_id,
2201 provider,
2202 driver,
2203 provider_instance_id,
2204 status,
2205 model,
2206 updated_at,
2207 payload_json
2208 )
2209 VALUES (
2210 ${event.payload.id},
2211 ${event.threadId},
2212 ${event.payload.providerInstanceId},
2213 ${event.payload.driver},
2214 ${event.payload.providerInstanceId},
2215 ${event.payload.status},
2216 ${event.payload.model},
2217 ${stringField(payload, "updatedAt")},
2218 ${payloadJson}
2219 )
2220 ON CONFLICT(provider_session_id)
2221 DO UPDATE SET
2222 thread_id = excluded.thread_id,
2223 provider = excluded.provider,
2224 driver = excluded.driver,
2225 provider_instance_id = excluded.provider_instance_id,
2226 status = excluded.status,
2227 model = excluded.model,
2228 updated_at = excluded.updated_at,
2229 payload_json = excluded.payload_json
2230 `;
2231 if (event.type === "provider-session.attached") {
2232 yield* sql`
2233 INSERT OR IGNORE INTO orchestration_v2_projection_provider_session_bindings (
2234 provider_session_id,
2235 thread_id
2236 )
2237 VALUES (${event.payload.id}, ${event.threadId})
2238 `;
2239 }
2240 break;
2241 }
2242 case "provider-session.detached": {
2243 yield* sql`
2244 DELETE FROM orchestration_v2_projection_provider_session_bindings
2245 WHERE provider_session_id = ${event.payload.providerSessionId}
2246 AND thread_id = ${event.threadId}
2247 `;
2248 break;
2249 }
2250 case "provider-thread.updated": {
2251 const payloadJson = yield* encodeProviderThreadPayload(event.payload);
2252 const payload = parseEncodedPayload(payloadJson);
2253 yield* sql`
2254 INSERT INTO orchestration_v2_projection_provider_threads (
2255 provider_thread_id,
2256 thread_id,
2257 owner_node_id,
2258 provider,
2259 driver,
2260 provider_instance_id,
2261 provider_session_id,
2262 status,
2263 first_run_ordinal,
2264 last_run_ordinal,
2265 updated_at,
2266 payload_json
2267 )
2268 VALUES (
2269 ${event.payload.id},
2270 ${event.payload.appThreadId},
2271 ${event.payload.ownerNodeId},
2272 ${event.payload.providerInstanceId},
2273 ${event.payload.driver},
2274 ${event.payload.providerInstanceId},
2275 ${event.payload.providerSessionId},
2276 ${event.payload.status},
2277 ${event.payload.firstRunOrdinal},
2278 ${event.payload.lastRunOrdinal},
2279 ${stringField(payload, "updatedAt")},
2280 ${payloadJson}
2281 )
2282 ON CONFLICT(provider_thread_id)
2283 DO UPDATE SET
2284 thread_id = excluded.thread_id,
2285 owner_node_id = excluded.owner_node_id,
2286 provider = excluded.provider,
2287 driver = excluded.driver,
2288 provider_instance_id = excluded.provider_instance_id,
2289 provider_session_id = excluded.provider_session_id,
2290 status = excluded.status,
2291 first_run_ordinal = excluded.first_run_ordinal,
2292 last_run_ordinal = excluded.last_run_ordinal,
2293 updated_at = excluded.updated_at,
2294 payload_json = excluded.payload_json
2295 `;
2296 if (
2297 event.payload.appThreadId !== null &&
2298 !isQueuedProviderThreadPlaceholder(event.payload)
2299 ) {
2300 const threadRows = yield* sql<PayloadRow>`
2301 SELECT payload_json
2302 FROM orchestration_v2_projection_threads
2303 WHERE thread_id = ${event.payload.appThreadId}
2304 LIMIT 1
2305 `;
2306 const threadRow = threadRows[0];
2307 if (threadRow !== undefined) {
2308 const thread = yield* decodeThreadPayload(threadRow.payload_json);
2309 const updatedThread = {
2310 ...thread,
2311 activeProviderThreadId: event.payload.id,
2312 updatedAt: event.payload.updatedAt,
2313 };
2314 const updatedThreadPayloadJson = yield* encodeThreadPayload(updatedThread);
2315 yield* sql`
2316 UPDATE orchestration_v2_projection_threads
2317 SET
2318 active_provider_thread_id = ${event.payload.id},
2319 updated_at = ${stringField(parseEncodedPayload(updatedThreadPayloadJson), "updatedAt")},
2320 payload_json = ${updatedThreadPayloadJson}
2321 WHERE thread_id = ${event.payload.appThreadId}
2322 `;
2323 }
2324 }
2325 break;
2326 }
2327 case "provider-turn.updated": {
2328 const existingRows =
2329 event.payload.tokenUsage === undefined
2330 ? yield* sql<PayloadRow>`
2331 SELECT payload_json
2332 FROM orchestration_v2_projection_provider_turns
2333 WHERE provider_turn_id = ${event.payload.id}
2334 LIMIT 1
2335 `
2336 : [];
2337 const existing = existingRows[0];
2338 const providerTurn =
2339 existing === undefined
2340 ? event.payload
2341 : upsertProviderTurn(
2342 [yield* decodeProviderTurnPayload(existing.payload_json)],
2343 event.payload,
2344 )[0]!;
2345 const payloadJson = yield* encodeProviderTurnPayload(providerTurn);
2346 const payload = parseEncodedPayload(payloadJson);
2347 yield* sql`
2348 INSERT INTO orchestration_v2_projection_provider_turns (
2349 provider_turn_id,
2350 thread_id,
2351 provider_thread_id,
2352 node_id,
2353 run_attempt_id,
2354 ordinal,
2355 status,
2356 started_at,
2357 completed_at,
2358 payload_json
2359 )
2360 VALUES (
2361 ${event.payload.id},
2362 ${event.threadId},
2363 ${providerTurn.providerThreadId},
2364 ${providerTurn.nodeId},
2365 ${providerTurn.runAttemptId},
2366 ${providerTurn.ordinal},
2367 ${providerTurn.status},
2368 ${nullableStringField(payload, "startedAt")},
2369 ${nullableStringField(payload, "completedAt")},
2370 ${payloadJson}
2371 )
2372 ON CONFLICT(provider_turn_id)
2373 DO UPDATE SET
2374 thread_id = excluded.thread_id,
2375 provider_thread_id = excluded.provider_thread_id,
2376 node_id = excluded.node_id,
2377 run_attempt_id = excluded.run_attempt_id,
2378 ordinal = excluded.ordinal,
2379 status = excluded.status,
2380 started_at = excluded.started_at,
2381 completed_at = excluded.completed_at,
2382 payload_json = excluded.payload_json
2383 `;
2384 break;
2385 }
2386 case "runtime-request.updated": {
2387 const payloadJson = yield* encodeRuntimeRequestPayload(event.payload);
2388 const payload = parseEncodedPayload(payloadJson);
2389 yield* sql`
2390 INSERT INTO orchestration_v2_projection_runtime_requests (
2391 runtime_request_id,
2392 thread_id,
2393 node_id,
2394 provider_turn_id,
2395 kind,
2396 status,
2397 created_at,
2398 resolved_at,
2399 payload_json
2400 )
2401 VALUES (
2402 ${event.payload.id},
2403 ${event.threadId},
2404 ${event.payload.nodeId},
2405 ${event.payload.providerTurnId},
2406 ${event.payload.kind},
2407 ${event.payload.status},
2408 ${stringField(payload, "createdAt")},
2409 ${nullableStringField(payload, "resolvedAt")},
2410 ${payloadJson}
2411 )
2412 ON CONFLICT(runtime_request_id)
2413 DO UPDATE SET
2414 thread_id = excluded.thread_id,
2415 node_id = excluded.node_id,
2416 provider_turn_id = excluded.provider_turn_id,
2417 kind = excluded.kind,
2418 status = excluded.status,
2419 created_at = excluded.created_at,
2420 resolved_at = excluded.resolved_at,
2421 payload_json = excluded.payload_json
2422 `;
2423 break;
2424 }
2425 case "message.updated": {
2426 const payloadJson = yield* encodeMessagePayload(event.payload);
2427 const payload = parseEncodedPayload(payloadJson);
2428 yield* sql`
2429 INSERT INTO orchestration_v2_projection_messages (
2430 message_id,
2431 thread_id,
2432 run_id,
2433 node_id,
2434 role,
2435 streaming,
2436 created_at,
2437 updated_at,
2438 payload_json
2439 )
2440 VALUES (
2441 ${event.payload.id},
2442 ${event.payload.threadId},
2443 ${event.payload.runId},
2444 ${event.payload.nodeId},
2445 ${event.payload.role},
2446 ${booleanInt(event.payload.streaming)},
2447 ${stringField(payload, "createdAt")},
2448 ${stringField(payload, "updatedAt")},
2449 ${payloadJson}
2450 )
2451 ON CONFLICT(message_id)
2452 DO UPDATE SET
2453 thread_id = excluded.thread_id,
2454 run_id = excluded.run_id,
2455 node_id = excluded.node_id,
2456 role = excluded.role,
2457 streaming = excluded.streaming,
2458 created_at = excluded.created_at,
2459 updated_at = excluded.updated_at,
2460 payload_json = excluded.payload_json
2461 `;
2462 break;
2463 }
2464 case "plan.updated": {
2465 const payloadJson = yield* encodePlanPayload(event.payload);
2466 yield* sql`
2467 INSERT INTO orchestration_v2_projection_plans (
2468 plan_id,
2469 thread_id,
2470 run_id,
2471 node_id,
2472 kind,
2473 status,
2474 payload_json
2475 )
2476 VALUES (
2477 ${event.payload.id},
2478 ${event.payload.threadId},
2479 ${event.payload.runId},
2480 ${event.payload.nodeId},
2481 ${event.payload.kind},
2482 ${event.payload.status},
2483 ${payloadJson}
2484 )
2485 ON CONFLICT(plan_id)
2486 DO UPDATE SET
2487 thread_id = excluded.thread_id,
2488 run_id = excluded.run_id,
2489 node_id = excluded.node_id,
2490 kind = excluded.kind,
2491 status = excluded.status,
2492 payload_json = excluded.payload_json
2493 `;
2494 break;
2495 }
2496 case "turn-item.updated": {
2497 const payloadJson = yield* encodeTurnItemPayload(event.payload);
2498 const payload = parseEncodedPayload(payloadJson);
2499 yield* sql`
2500 INSERT INTO orchestration_v2_projection_turn_items (
2501 turn_item_id,
2502 thread_id,
2503 run_id,
2504 node_id,
2505 provider_thread_id,
2506 provider_turn_id,
2507 parent_item_id,
2508 ordinal,
2509 type,
2510 status,
2511 updated_at,
2512 payload_json
2513 )
2514 VALUES (
2515 ${event.payload.id},
2516 ${event.payload.threadId},
2517 ${event.payload.runId},
2518 ${event.payload.nodeId},
2519 ${event.payload.providerThreadId},
2520 ${event.payload.providerTurnId},
2521 ${event.payload.parentItemId},
2522 ${event.payload.ordinal},
2523 ${event.payload.type},
2524 ${event.payload.status},
2525 ${stringField(payload, "updatedAt")},
2526 ${payloadJson}
2527 )
2528 ON CONFLICT(turn_item_id)
2529 DO UPDATE SET
2530 thread_id = excluded.thread_id,
2531 run_id = excluded.run_id,
2532 node_id = excluded.node_id,
2533 provider_thread_id = excluded.provider_thread_id,
2534 provider_turn_id = excluded.provider_turn_id,
2535 parent_item_id = excluded.parent_item_id,
2536 ordinal = excluded.ordinal,
2537 type = excluded.type,
2538 status = excluded.status,
2539 updated_at = excluded.updated_at,
2540 payload_json = excluded.payload_json
2541 `;
2542 break;
2543 }
2544 case "checkpoint-scope.created": {
2545 const payloadJson = yield* encodeCheckpointScopePayload(event.payload);
2546 const payload = parseEncodedPayload(payloadJson);
2547 yield* sql`
2548 INSERT INTO orchestration_v2_projection_checkpoint_scopes (
2549 scope_id,
2550 thread_id,
2551 run_id,
2552 node_id,
2553 parent_scope_id,
2554 provider_thread_id,
2555 kind,
2556 ordinal_within_parent,
2557 advances_app_run_count,
2558 created_at,
2559 payload_json
2560 )
2561 VALUES (
2562 ${event.payload.id},
2563 ${event.payload.threadId},
2564 ${event.payload.runId},
2565 ${event.payload.nodeId},
2566 ${event.payload.parentScopeId},
2567 ${event.payload.providerThreadId},
2568 ${event.payload.kind},
2569 ${event.payload.ordinalWithinParent},
2570 ${booleanInt(event.payload.advancesAppRunCount)},
2571 ${stringField(payload, "createdAt")},
2572 ${payloadJson}
2573 )
2574 ON CONFLICT(scope_id)
2575 DO UPDATE SET
2576 thread_id = excluded.thread_id,
2577 run_id = excluded.run_id,
2578 node_id = excluded.node_id,
2579 parent_scope_id = excluded.parent_scope_id,
2580 provider_thread_id = excluded.provider_thread_id,
2581 kind = excluded.kind,
2582 ordinal_within_parent = excluded.ordinal_within_parent,
2583 advances_app_run_count = excluded.advances_app_run_count,
2584 created_at = excluded.created_at,
2585 payload_json = excluded.payload_json
2586 `;
2587 break;
2588 }
2589 case "checkpoint.captured": {
2590 const payloadJson = yield* encodeCheckpointPayload(event.payload);
2591 const payload = parseEncodedPayload(payloadJson);
2592 yield* sql`
2593 INSERT INTO orchestration_v2_projection_checkpoints (
2594 checkpoint_id,
2595 thread_id,
2596 scope_id,
2597 run_id,
2598 node_id,
2599 parent_checkpoint_id,
2600 ordinal_within_scope,
2601 app_run_ordinal,
2602 status,
2603 captured_at,
2604 payload_json
2605 )
2606 VALUES (
2607 ${event.payload.id},
2608 ${event.payload.threadId},
2609 ${event.payload.scopeId},
2610 ${event.payload.runId},
2611 ${event.payload.nodeId},
2612 ${event.payload.parentCheckpointId},
2613 ${event.payload.ordinalWithinScope},
2614 ${event.payload.appRunOrdinal},
2615 ${event.payload.status},
2616 ${stringField(payload, "capturedAt")},
2617 ${payloadJson}
2618 )
2619 ON CONFLICT(checkpoint_id)
2620 DO UPDATE SET
2621 thread_id = excluded.thread_id,
2622 scope_id = excluded.scope_id,
2623 run_id = excluded.run_id,
2624 node_id = excluded.node_id,
2625 parent_checkpoint_id = excluded.parent_checkpoint_id,
2626 ordinal_within_scope = excluded.ordinal_within_scope,
2627 app_run_ordinal = excluded.app_run_ordinal,
2628 status = excluded.status,
2629 captured_at = excluded.captured_at,
2630 payload_json = excluded.payload_json
2631 `;
2632 break;
2633 }
2634 case "checkpoint.rollback-requested":
2635 break;
2636 case "context-handoff.updated": {
2637 const payloadJson = yield* encodeContextHandoffPayload(event.payload);
2638 const payload = parseEncodedPayload(payloadJson);
2639 yield* sql`
2640 INSERT INTO orchestration_v2_projection_context_handoffs (
2641 context_handoff_id,
2642 thread_id,
2643 target_run_id,
2644 to_provider_thread_id,
2645 strategy,
2646 status,
2647 updated_at,
2648 payload_json
2649 )
2650 VALUES (
2651 ${event.payload.id},
2652 ${event.payload.threadId},
2653 ${event.payload.targetRunId},
2654 ${event.payload.toProviderThreadId},
2655 ${event.payload.strategy},
2656 ${event.payload.status},
2657 ${stringField(payload, "updatedAt")},
2658 ${payloadJson}
2659 )
2660 ON CONFLICT(context_handoff_id)
2661 DO UPDATE SET
2662 thread_id = excluded.thread_id,
2663 target_run_id = excluded.target_run_id,
2664 to_provider_thread_id = excluded.to_provider_thread_id,
2665 strategy = excluded.strategy,
2666 status = excluded.status,
2667 updated_at = excluded.updated_at,
2668 payload_json = excluded.payload_json
2669 `;
2670 break;
2671 }
2672 case "context-transfer.created":
2673 case "context-transfer.updated": {
2674 const payloadJson = yield* encodeContextTransferPayload(event.payload);
2675 const payload = parseEncodedPayload(payloadJson);
2676 yield* sql`
2677 INSERT INTO orchestration_v2_projection_context_transfers (
2678 context_transfer_id,
2679 source_thread_id,
2680 target_thread_id,
2681 target_run_id,
2682 type,
2683 status,
2684 source_provider,
2685 target_provider,
2686 source_provider_instance_id,
2687 target_provider_instance_id,
2688 updated_at,
2689 payload_json
2690 )
2691 VALUES (
2692 ${event.payload.id},
2693 ${event.payload.sourceThreadId},
2694 ${event.payload.targetThreadId},
2695 ${event.payload.targetRunId},
2696 ${event.payload.type},
2697 ${event.payload.status},
2698 ${event.payload.sourceProviderInstanceId},
2699 ${event.payload.targetProviderInstanceId},
2700 ${event.payload.sourceProviderInstanceId},
2701 ${event.payload.targetProviderInstanceId},
2702 ${stringField(payload, "updatedAt")},
2703 ${payloadJson}
2704 )
2705 ON CONFLICT(context_transfer_id)
2706 DO UPDATE SET
2707 source_thread_id = excluded.source_thread_id,
2708 target_thread_id = excluded.target_thread_id,
2709 target_run_id = excluded.target_run_id,
2710 type = excluded.type,
2711 status = excluded.status,
2712 source_provider = excluded.source_provider,
2713 target_provider = excluded.target_provider,
2714 source_provider_instance_id = excluded.source_provider_instance_id,
2715 target_provider_instance_id = excluded.target_provider_instance_id,
2716 updated_at = excluded.updated_at,
2717 payload_json = excluded.payload_json
2718 `;
2719 break;
2720 }
2721 }
2722
2723 if (
2724 event.type !== "thread.created" &&
2725 event.type !== "thread.archived" &&
2726 event.type !== "thread.unarchived" &&
2727 event.type !== "thread.deleted" &&
2728 event.type !== "thread.settled" &&
2729 event.type !== "thread.unsettled" &&
2730 event.type !== "thread.snoozed" &&
2731 event.type !== "thread.unsnoozed" &&
2732 event.type !== "thread.auto-settle-set" &&
2733 event.type !== "thread.pinned" &&
2734 event.type !== "thread.unpinned" &&
2735 event.type !== "thread.pin-reordered" &&
2736 event.type !== "thread.visited" &&
2737 event.type !== "thread.marked-unread" &&
2738 event.type !== "thread.metadata-updated" &&
2739 event.type !== "thread.runtime-mode-updated" &&
2740 event.type !== "thread.interaction-mode-updated" &&
2741 event.type !== "thread.model-selection-updated" &&
2742 event.type !== "thread.provider-switched"
2743 ) {
2744 // Only the activity timestamp changes. This runs for every streamed
2745 // run, node and turn-item event, so the thread row is patched in place
2746 // instead of being read, decoded and re-encoded each time.
2747 const updatedAt = yield* encodeThreadUpdatedAt(event.occurredAt);
2748 yield* sql`
2749 UPDATE orchestration_v2_projection_threads
2750 SET
2751 updated_at = ${updatedAt},
2752 payload_json = json_set(payload_json, '$.updatedAt', ${updatedAt})
2753 WHERE thread_id = ${event.threadId}
2754 `;
2755 }
2756 }).pipe(
2757 Effect.mapError(
2758 (cause) =>
2759 new ProjectionStoreApplyEventError({
2760 eventType: event.type,
2761 cause,
2762 }),
2763 ),
2764 );
2765
2766 const readCanonicalProjection = (
2767 threadId: ThreadId,
2768 window?: {
2769 readonly rowLimit: number;
2770 readonly userTurnLimit?: number | undefined;
2771 readonly anchorItemId?: TurnItemId | undefined;
2772 readonly requiredRunId?: RunId | undefined;
2773 readonly suppressLocal?: boolean | undefined;
2774 },
2775 fields?: ReadonlyArray<ProjectionRecordField>,
2776 filter?: ProjectionRecordFilter,
2777 ) =>
2778 Effect.gen(function* () {
2779 const threadRows = yield* sql<PayloadRow>`
2780 SELECT payload_json
2781 FROM orchestration_v2_projection_threads
2782 WHERE thread_id = ${threadId}
2783 LIMIT 1
2784 `;
2785 const threadRow = threadRows[0];
2786 if (!threadRow) {
2787 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
2788 }
2789
2790 const boundedTurnItemRows =
2791 fields !== undefined && !fields.includes("turnItems")
2792 ? []
2793 : window === undefined
2794 ? yield* sql<PayloadRow>`
2795 SELECT payload_json
2796 FROM orchestration_v2_projection_turn_items
2797 WHERE thread_id = ${threadId}
2798 ${filter?.turnItemTypes === undefined ? sql`` : sql`AND type IN (SELECT value FROM json_each(${encodeIdList(filter.turnItemTypes)}))`}
2799 ${filter?.turnItemStatuses === undefined ? sql`` : sql`AND status IN (SELECT value FROM json_each(${encodeIdList(filter.turnItemStatuses)}))`}
2800 ${filter?.turnItemRunId === undefined ? sql`` : sql`AND run_id = ${filter.turnItemRunId}`}
2801 ${filter?.turnItemRunIds === undefined ? sql`` : sql`AND (run_id IN (SELECT value FROM json_each(${encodeIdList(filter.turnItemRunIds.filter((id): id is RunId => id !== null))})) OR (${filter.turnItemRunIds.includes(null) ? 1 : 0} = 1 AND run_id IS NULL))`}
2802 ORDER BY ordinal ASC, turn_item_id ASC
2803 `
2804 : yield* sql<PayloadRow>`
2805 WITH eligible AS NOT MATERIALIZED (
2806 SELECT item.rowid AS item_rowid, item.payload_json, item.ordinal, item.turn_item_id,
2807 item.run_id, item.node_id, item.type
2808 FROM orchestration_v2_projection_turn_items AS item
2809 LEFT JOIN orchestration_v2_projection_runs AS run
2810 ON run.run_id = item.run_id
2811 WHERE item.thread_id = ${threadId}
2812 AND item.ordinal <= COALESCE(
2813 (
2814 SELECT ordinal
2815 FROM orchestration_v2_projection_turn_items
2816 WHERE thread_id = ${threadId}
2817 AND turn_item_id = ${window.anchorItemId ?? null}
2818 LIMIT 1
2819 ),
2820 9223372036854775807
2821 )
2822 AND (
2823 ${window.requiredRunId ?? null} IS NOT NULL
2824 OR (
2825 (run.status IS NULL OR run.status <> 'rolled_back')
2826 AND NOT (
2827 item.type = 'user_message'
2828 AND json_extract(item.payload_json, '$.inputIntent') = 'queued_turn'
2829 AND run.status IS 'cancelled'
2830 )
2831 )
2832 )
2833 AND (
2834 ${window.requiredRunId ?? null} IS NULL
2835 OR (
2836 item.run_id IS NULL
2837 AND EXISTS (
2838 SELECT 1
2839 FROM orchestration_v2_projection_threads AS source_thread
2840 WHERE source_thread.thread_id = item.thread_id
2841 AND json_extract(source_thread.payload_json, '$.historyOrigin') = 'v1_import'
2842 )
2843 )
2844 OR run.ordinal <= (
2845 SELECT required_run.ordinal
2846 FROM orchestration_v2_projection_runs AS required_run
2847 WHERE required_run.thread_id = item.thread_id
2848 AND required_run.run_id = ${window.requiredRunId ?? null}
2849 LIMIT 1
2850 )
2851 )
2852 AND NOT (
2853 item.type = 'run_interrupt_result'
2854 AND EXISTS (
2855 SELECT 1
2856 FROM orchestration_v2_projection_run_attempts AS attempt
2857 WHERE attempt.run_id = item.run_id
2858 AND attempt.root_node_id = item.node_id
2859 AND attempt.status = 'superseded'
2860 )
2861 AND NOT EXISTS (
2862 SELECT 1
2863 FROM orchestration_v2_projection_turn_items AS request
2864 WHERE request.thread_id = item.thread_id
2865 AND request.run_id = item.run_id
2866 AND request.type = 'run_interrupt_request'
2867 )
2868 )
2869 ), run_starts AS (
2870 -- A wake (PR watch, task completion) starts its run with a
2871 -- notification instead of a user message. Read the first
2872 -- input of each run, newest run first, and add the
2873 -- notifications to the user-message turn starts. A run
2874 -- without an eligible first input (queued, rolled back) is
2875 -- skipped, so it cannot hide older turns. CROSS JOIN keeps
2876 -- the runs as the outer loop, and each probe reads only the
2877 -- first rows of a run.
2878 SELECT start.ordinal, start.type
2879 FROM orchestration_v2_projection_runs AS recent_run
2880 CROSS JOIN eligible AS start ON start.item_rowid = (
2881 SELECT first_input.rowid
2882 FROM orchestration_v2_projection_turn_items AS first_input
2883 WHERE first_input.run_id = recent_run.run_id
2884 AND first_input.type IN ('user_message', 'notification')
2885 ORDER BY first_input.ordinal
2886 LIMIT 1
2887 )
2888 WHERE recent_run.thread_id = ${threadId}
2889 AND recent_run.ordinal <= COALESCE(
2890 (
2891 SELECT anchor_run.ordinal
2892 FROM orchestration_v2_projection_turn_items AS anchor_item
2893 JOIN orchestration_v2_projection_runs AS anchor_run
2894 ON anchor_run.run_id = anchor_item.run_id
2895 WHERE anchor_item.thread_id = ${threadId}
2896 AND anchor_item.turn_item_id = ${window.anchorItemId ?? null}
2897 LIMIT 1
2898 ),
2899 9223372036854775807
2900 )
2901 AND ${window.userTurnLimit ?? null} IS NOT NULL
2902 ORDER BY recent_run.ordinal DESC
2903 LIMIT ${THREAD_HISTORY_MAX_RAW_TURNS + 2}
2904 ), turn_anchors AS (
2905 SELECT ordinal, payload_json
2906 FROM (
2907 SELECT ordinal, payload_json
2908 FROM eligible
2909 WHERE type = 'user_message'
2910 AND json_extract(payload_json, '$.inputIntent') IN ('turn_start', 'queued_turn')
2911 AND ${window.userTurnLimit ?? null} IS NOT NULL
2912 ORDER BY ordinal DESC
2913 LIMIT ${THREAD_HISTORY_MAX_RAW_TURNS + 2}
2914 )
2915 UNION ALL
2916 SELECT ordinal, NULL FROM run_starts WHERE type = 'notification'
2917 ORDER BY ordinal DESC
2918 LIMIT ${THREAD_HISTORY_MAX_RAW_TURNS + 2}
2919 ), user_anchors AS (
2920 SELECT ordinal
2921 FROM turn_anchors
2922 WHERE json_extract(payload_json, '$.createdBy') = 'user'
2923 ORDER BY ordinal DESC
2924 LIMIT ${(window.userTurnLimit ?? 0) + 2}
2925 ), boundary AS (
2926 SELECT CASE
2927 WHEN COUNT(*) >= ${(window.userTurnLimit ?? 0) + 2} THEN MIN(ordinal)
2928 WHEN (SELECT COUNT(*) FROM turn_anchors) >= ${THREAD_HISTORY_MAX_RAW_TURNS + 2}
2929 THEN (SELECT MIN(ordinal) FROM turn_anchors)
2930 ELSE 0
2931 END AS ordinal
2932 FROM user_anchors
2933 ), selected AS (
2934 -- Choose and sort rows by ID, then fetch payloads in that
2935 -- order. A turn window can hold megabytes of tool output, and
2936 -- carrying it through the union and sort cost about a second.
2937 SELECT item_rowid, ordinal, turn_item_id, run_id, type
2938 FROM eligible
2939 WHERE ordinal >= (SELECT ordinal FROM boundary)
2940 ORDER BY ordinal DESC, turn_item_id DESC
2941 LIMIT CASE
2942 WHEN ${window.rowLimit} = 0 THEN 0
2943 WHEN (SELECT COUNT(*) FROM user_anchors) > 0 THEN -1
2944 ELSE ${window.rowLimit}
2945 END
2946 ), retained AS MATERIALIZED (
2947 SELECT item_rowid, ordinal, turn_item_id FROM selected
2948 UNION
2949 SELECT request.rowid AS item_rowid, request.ordinal, request.turn_item_id
2950 FROM orchestration_v2_projection_turn_items AS request
2951 WHERE request.run_id IN (
2952 SELECT run_id FROM selected
2953 WHERE type = 'run_interrupt_result' AND run_id IS NOT NULL
2954 )
2955 AND request.type = 'run_interrupt_request'
2956 UNION
2957 SELECT latest.item_rowid, latest.ordinal, latest.turn_item_id
2958 FROM (
2959 SELECT rowid AS item_rowid, ordinal, turn_item_id
2960 FROM orchestration_v2_projection_turn_items
2961 WHERE thread_id = ${threadId}
2962 AND ${window.anchorItemId ?? null} IS NULL
2963 AND ${window.requiredRunId ?? null} IS NULL
2964 ORDER BY ordinal DESC, turn_item_id DESC
2965 LIMIT 1
2966 ) AS latest
2967 ORDER BY ordinal ASC, turn_item_id ASC
2968 )
2969 -- CROSS JOIN keeps the sorted IDs as the outer loop, so SQLite
2970 -- skips sorting again once the payloads are attached. Carry the
2971 -- rowid through selection to avoid a second string-key index lookup.
2972 SELECT item.payload_json
2973 FROM retained
2974 CROSS JOIN orchestration_v2_projection_turn_items AS item
2975 ON item.rowid = retained.item_rowid
2976 ORDER BY retained.ordinal ASC, retained.turn_item_id ASC
2977 `;
2978 // Reuse the decoded items for cohort IDs and the resulting projection.
2979 // Parsing these rows separately duplicates every retained tool output.
2980 const turnItems = yield* decodeRows(decodeTurnItemPayload, threadId)(boundedTurnItemRows);
2981 const cohortJson = (field: string) =>
2982 JSON.stringify(
2983 turnItems.flatMap((payload) => {
2984 const value = nullableStringField(payload, field);
2985 return value === null ? [] : [value];
2986 }),
2987 );
2988 const cohortRunIds = cohortJson("runId");
2989 // A run can contain thousands of completed nodes. Load the visible
2990 // nodes and live control dependencies, then walk only their ancestors.
2991 const cohortNodeIds =
2992 window === undefined ||
2993 (fields !== undefined &&
2994 fields.every(
2995 (field) => field === "runs" || field === "attempts" || field === "turnItems",
2996 ))
2997 ? cohortJson("nodeId")
2998 : encodeIdList(
2999 (yield* sql<{ readonly node_id: string }>`
3000 WITH RECURSIVE retained(node_id) AS (
3001 SELECT value FROM json_each(${cohortJson("nodeId")})
3002 UNION
3003 SELECT node_id FROM orchestration_v2_projection_nodes
3004 WHERE thread_id = ${threadId} AND status IN ('pending','starting','running','waiting')
3005 UNION
3006 SELECT root_node_id FROM orchestration_v2_projection_run_attempts
3007 WHERE thread_id = ${threadId} AND (
3008 status IN ('pending','starting','running','waiting')
3009 OR run_id IN (SELECT value FROM json_each(${cohortRunIds}))
3010 OR run_id = ${window.requiredRunId ?? null})
3011 UNION
3012 SELECT json_extract(payload_json, '$.rootNodeId') FROM orchestration_v2_projection_runs
3013 WHERE thread_id = ${threadId} AND (
3014 status IN ('queued','preparing','starting','running','waiting')
3015 OR run_id IN (SELECT value FROM json_each(${cohortRunIds}))
3016 OR run_id = ${window.requiredRunId ?? null})
3017 UNION
3018 SELECT node_id FROM orchestration_v2_projection_runtime_requests
3019 WHERE thread_id = ${threadId} AND status IN ('pending','waiting')
3020 UNION
3021 SELECT parent_node_id FROM orchestration_v2_projection_subagents
3022 WHERE thread_id = ${threadId} AND status IN ('pending','starting','running','waiting')
3023 UNION
3024 SELECT node_id FROM orchestration_v2_projection_provider_turns
3025 WHERE thread_id = ${threadId} AND status IN ('starting','running','waiting')
3026 UNION
3027 SELECT parent.parent_node_id FROM orchestration_v2_projection_nodes AS parent
3028 INNER JOIN retained ON parent.node_id = retained.node_id
3029 WHERE parent.thread_id = ${threadId} AND parent.parent_node_id IS NOT NULL
3030 )
3031 -- Sort IDs before fetching bodies, so large payloads never enter
3032 -- SQLite's temporary sort. The node read restores this order by key.
3033 SELECT retained.node_id
3034 FROM retained
3035 LEFT JOIN orchestration_v2_projection_nodes AS node
3036 ON node.node_id = retained.node_id AND node.thread_id = ${threadId}
3037 WHERE retained.node_id IS NOT NULL
3038 ORDER BY COALESCE(node.started_at, ''), retained.node_id ASC
3039 `).map((row) => row.node_id),
3040 );
3041 const cohortProviderThreadIds = cohortJson("providerThreadId");
3042 const cohortProviderTurnIds = cohortJson("providerTurnId");
3043 const cohortMessageIds = cohortJson("messageId");
3044 const cohortPlanIds = cohortJson("planId");
3045 const cohortCheckpointIds = cohortJson("checkpointId");
3046 const cohortHandoffIds = cohortJson("contextHandoffId");
3047
3048 const [
3049 thread,
3050 runRows,
3051 attemptRows,
3052 nodeRows,
3053 subagentRows,
3054 providerSessionRows,
3055 providerThreadRows,
3056 providerTurnRows,
3057 runtimeRequestRows,
3058 messageRows,
3059 planRows,
3060 checkpointScopeRows,
3061 checkpointRows,
3062 contextHandoffRows,
3063 contextTransferRows,
3064 ] = yield* Effect.all([
3065 decodeThreadPayload(threadRow.payload_json),
3066 fields !== undefined && !fields.includes("runs")
3067 ? Effect.succeed([])
3068 : window === undefined
3069 ? sql<PayloadRow>`
3070 SELECT payload_json
3071 FROM orchestration_v2_projection_runs
3072 WHERE thread_id = ${threadId}
3073 ${filter?.runIds === undefined ? sql`` : sql`AND run_id IN (SELECT value FROM json_each(${encodeIdList(filter.runIds)}))`}
3074 ORDER BY ordinal ASC
3075 `
3076 : sql<PayloadRow>`
3077 SELECT payload_json FROM orchestration_v2_projection_runs
3078 WHERE thread_id = ${threadId}
3079 AND (status IN ('queued','preparing','starting','running','waiting')
3080 OR run_id IN (SELECT value FROM json_each(${cohortRunIds}))
3081 OR run_id = ${window.requiredRunId ?? null})
3082 ORDER BY ordinal ASC
3083 `,
3084 fields !== undefined && !fields.includes("attempts")
3085 ? Effect.succeed([])
3086 : window === undefined
3087 ? sql<PayloadRow>`
3088 SELECT payload_json
3089 FROM orchestration_v2_projection_run_attempts
3090 WHERE thread_id = ${threadId}
3091 ORDER BY run_id ASC, attempt_ordinal ASC
3092 `
3093 : sql<PayloadRow>`
3094 SELECT payload_json FROM orchestration_v2_projection_run_attempts
3095 WHERE thread_id = ${threadId}
3096 AND (status IN ('pending','starting','running','waiting')
3097 OR run_id IN (SELECT value FROM json_each(${cohortRunIds})))
3098 ORDER BY run_id ASC, attempt_ordinal ASC
3099 `,
3100 fields !== undefined && !fields.includes("nodes")
3101 ? Effect.succeed([])
3102 : window === undefined
3103 ? sql<PayloadRow>`
3104 SELECT payload_json
3105 FROM orchestration_v2_projection_nodes AS node
3106 WHERE node.thread_id = ${threadId}
3107 ${
3108 filter?.omitSettledLeafNodes === true
3109 ? sql`AND NOT (
3110 node.kind IN ('tool_call', 'assistant_message', 'reasoning')
3111 AND node.status IN ('completed', 'interrupted', 'failed', 'cancelled', 'rolled_back')
3112 AND node.run_id IS NOT NULL
3113 AND NOT EXISTS (
3114 SELECT 1 FROM orchestration_v2_projection_runs AS run
3115 WHERE run.run_id = node.run_id
3116 AND run.status IN ('preparing', 'queued', 'starting', 'running', 'waiting')
3117 )
3118 )`
3119 : sql``
3120 }
3121 ORDER BY COALESCE(node.started_at, ''), node.node_id ASC
3122 `
3123 : sql<PayloadRow & { readonly position: number }>`
3124 -- Keep retained IDs outermost so SQLite looks up each node by its
3125 -- primary key instead of scanning every historical node in the thread.
3126 SELECT node.payload_json, retained.key AS position
3127 FROM json_each(${cohortNodeIds}) AS retained
3128 CROSS JOIN orchestration_v2_projection_nodes AS node
3129 ON node.node_id = retained.value
3130 WHERE node.thread_id = ${threadId}
3131 `.pipe(
3132 Effect.map((rows) =>
3133 rows.toSorted((left, right) => left.position - right.position),
3134 ),
3135 ),
3136 fields !== undefined && !fields.includes("subagents")
3137 ? Effect.succeed([])
3138 : window === undefined
3139 ? sql<PayloadRow>`
3140 SELECT payload_json
3141 FROM orchestration_v2_projection_subagents
3142 WHERE thread_id = ${threadId}
3143 ORDER BY COALESCE(started_at, ''), subagent_id ASC
3144 `
3145 : sql<PayloadRow>`
3146 SELECT payload_json FROM orchestration_v2_projection_subagents
3147 WHERE thread_id = ${threadId}
3148 AND (status IN ('pending','starting','running','waiting')
3149 OR run_id IN (SELECT value FROM json_each(${cohortRunIds}))
3150 OR parent_node_id IN (SELECT value FROM json_each(${cohortNodeIds})))
3151 ORDER BY COALESCE(started_at, ''), subagent_id ASC
3152 `,
3153 fields !== undefined && !fields.includes("providerSessions")
3154 ? Effect.succeed([])
3155 : window === undefined
3156 ? sql<PayloadRow>`
3157 SELECT sessions.payload_json
3158 FROM orchestration_v2_projection_provider_sessions AS sessions
3159 INNER JOIN orchestration_v2_projection_provider_session_bindings AS bindings
3160 ON bindings.provider_session_id = sessions.provider_session_id
3161 WHERE bindings.thread_id = ${threadId}
3162 ORDER BY sessions.updated_at ASC, sessions.provider_session_id ASC
3163 `
3164 : sql<PayloadRow>`
3165 SELECT DISTINCT sessions.payload_json
3166 FROM orchestration_v2_projection_provider_sessions AS sessions
3167 INNER JOIN orchestration_v2_projection_provider_session_bindings AS bindings
3168 ON bindings.provider_session_id = sessions.provider_session_id
3169 LEFT JOIN orchestration_v2_projection_provider_threads AS threads
3170 ON threads.provider_session_id = sessions.provider_session_id
3171 WHERE bindings.thread_id = ${threadId}
3172 AND (sessions.status IN ('starting','running','waiting')
3173 OR threads.provider_thread_id IN (SELECT value FROM json_each(${cohortProviderThreadIds})))
3174 ORDER BY sessions.updated_at ASC, sessions.provider_session_id ASC
3175 `,
3176 fields !== undefined && !fields.includes("providerThreads")
3177 ? Effect.succeed([])
3178 : window === undefined
3179 ? sql<PayloadRow>`
3180 SELECT payload_json
3181 FROM orchestration_v2_projection_provider_threads
3182 WHERE thread_id = ${threadId}
3183 OR owner_node_id IN (
3184 SELECT node_id
3185 FROM orchestration_v2_projection_nodes
3186 WHERE thread_id = ${threadId}
3187 )
3188 OR provider_thread_id IN (
3189 SELECT provider_thread_id
3190 FROM orchestration_v2_projection_subagents
3191 WHERE thread_id = ${threadId}
3192 AND provider_thread_id IS NOT NULL
3193 )
3194 ORDER BY COALESCE(first_run_ordinal, 0), provider_thread_id ASC
3195 `
3196 : sql<PayloadRow>`
3197 SELECT payload_json FROM orchestration_v2_projection_provider_threads
3198 WHERE (thread_id = ${threadId} AND status = 'active')
3199 OR provider_thread_id IN (SELECT value FROM json_each(${cohortProviderThreadIds}))
3200 OR owner_node_id IN (SELECT value FROM json_each(${cohortNodeIds}))
3201 ORDER BY COALESCE(first_run_ordinal, 0), provider_thread_id ASC
3202 `,
3203 fields !== undefined && !fields.includes("providerTurns")
3204 ? Effect.succeed([])
3205 : window === undefined
3206 ? sql<PayloadRow>`
3207 SELECT payload_json
3208 FROM orchestration_v2_projection_provider_turns
3209 WHERE thread_id = ${threadId}
3210 ORDER BY provider_thread_id ASC, ordinal ASC
3211 `
3212 : sql<PayloadRow>`
3213 SELECT payload_json FROM orchestration_v2_projection_provider_turns
3214 WHERE thread_id = ${threadId}
3215 AND (status IN ('starting','running','waiting')
3216 OR provider_turn_id IN (SELECT value FROM json_each(${cohortProviderTurnIds}))
3217 OR node_id IN (SELECT value FROM json_each(${cohortNodeIds})))
3218 ORDER BY provider_thread_id ASC, ordinal ASC
3219 `,
3220 fields !== undefined && !fields.includes("runtimeRequests")
3221 ? Effect.succeed([])
3222 : window === undefined
3223 ? sql<PayloadRow>`
3224 SELECT payload_json
3225 FROM orchestration_v2_projection_runtime_requests
3226 WHERE thread_id = ${threadId}
3227 ORDER BY created_at ASC, runtime_request_id ASC
3228 `
3229 : sql<PayloadRow>`
3230 SELECT payload_json FROM orchestration_v2_projection_runtime_requests
3231 WHERE thread_id = ${threadId}
3232 AND (status IN ('pending','waiting')
3233 OR node_id IN (SELECT value FROM json_each(${cohortNodeIds}))
3234 OR provider_turn_id IN (SELECT value FROM json_each(${cohortProviderTurnIds})))
3235 ORDER BY created_at ASC, runtime_request_id ASC
3236 `,
3237 fields !== undefined && !fields.includes("messages")
3238 ? Effect.succeed([])
3239 : window === undefined
3240 ? sql<PayloadRow>`
3241 SELECT payload_json FROM orchestration_v2_projection_messages
3242 WHERE thread_id = ${threadId}
3243 ${filter?.messageIds === undefined ? sql`` : sql`AND message_id IN (SELECT value FROM json_each(${encodeIdList(filter.messageIds)}))`}
3244 ${filter?.messageRoles === undefined ? sql`` : sql`AND role IN (SELECT value FROM json_each(${encodeIdList(filter.messageRoles)}))`}
3245 ${filter?.messageRunIds === undefined ? sql`` : sql`AND run_id IN (SELECT value FROM json_each(${encodeIdList(filter.messageRunIds)}))`}
3246 ORDER BY created_at ASC, message_id ASC
3247 `
3248 : sql<PayloadRow>`
3249 SELECT payload_json FROM orchestration_v2_projection_messages AS message
3250 WHERE message.thread_id = ${threadId}
3251 AND (
3252 message.message_id IN (
3253 SELECT value FROM json_each(${cohortMessageIds})
3254 )
3255 OR message.run_id IN (
3256 SELECT run_id FROM orchestration_v2_projection_runs
3257 WHERE thread_id = ${threadId}
3258 AND status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
3259 )
3260 )
3261 ORDER BY created_at ASC, message_id ASC
3262 `,
3263 fields !== undefined && !fields.includes("plans")
3264 ? Effect.succeed([])
3265 : window === undefined
3266 ? sql<PayloadRow>`
3267 SELECT payload_json FROM orchestration_v2_projection_plans
3268 WHERE thread_id = ${threadId} ORDER BY plan_id ASC
3269 `
3270 : sql<PayloadRow>`
3271 SELECT payload_json FROM orchestration_v2_projection_plans AS plan
3272 WHERE plan.thread_id = ${threadId}
3273 AND (
3274 plan.status = 'active'
3275 OR plan.plan_id IN (
3276 SELECT value FROM json_each(${cohortPlanIds})
3277 )
3278 )
3279 ORDER BY plan_id ASC
3280 `,
3281 fields !== undefined && !fields.includes("checkpointScopes")
3282 ? Effect.succeed([])
3283 : window === undefined
3284 ? sql<PayloadRow>`
3285 SELECT payload_json
3286 FROM orchestration_v2_projection_checkpoint_scopes
3287 WHERE thread_id = ${threadId}
3288 ORDER BY ordinal_within_parent ASC, scope_id ASC
3289 `
3290 : sql<PayloadRow>`
3291 SELECT payload_json FROM orchestration_v2_projection_checkpoint_scopes
3292 WHERE thread_id = ${threadId}
3293 AND (run_id IN (SELECT value FROM json_each(${cohortRunIds}))
3294 OR node_id IN (SELECT value FROM json_each(${cohortNodeIds})))
3295 ORDER BY ordinal_within_parent ASC, scope_id ASC
3296 `,
3297 fields !== undefined && !fields.includes("checkpoints")
3298 ? Effect.succeed([])
3299 : window === undefined
3300 ? sql<PayloadRow>`
3301 SELECT payload_json
3302 FROM orchestration_v2_projection_checkpoints
3303 WHERE thread_id = ${threadId}
3304 ORDER BY scope_id ASC, ordinal_within_scope ASC
3305 `
3306 : sql<PayloadRow>`
3307 SELECT payload_json FROM orchestration_v2_projection_checkpoints
3308 WHERE thread_id = ${threadId}
3309 AND (status IN ('pending','capturing')
3310 OR checkpoint_id IN (SELECT value FROM json_each(${cohortCheckpointIds}))
3311 OR run_id IN (SELECT value FROM json_each(${cohortRunIds}))
3312 OR node_id IN (SELECT value FROM json_each(${cohortNodeIds})))
3313 ORDER BY scope_id ASC, ordinal_within_scope ASC
3314 `,
3315 fields !== undefined && !fields.includes("contextHandoffs")
3316 ? Effect.succeed([])
3317 : window === undefined
3318 ? sql<PayloadRow>`
3319 SELECT payload_json FROM orchestration_v2_projection_context_handoffs
3320 WHERE thread_id = ${threadId} ORDER BY rowid ASC
3321 `
3322 : sql<PayloadRow>`
3323 SELECT payload_json FROM orchestration_v2_projection_context_handoffs AS handoff
3324 WHERE handoff.thread_id = ${threadId}
3325 AND (
3326 handoff.status IN ('pending', 'ready')
3327 OR handoff.context_handoff_id IN (
3328 SELECT value FROM json_each(${cohortHandoffIds})
3329 )
3330 )
3331 ORDER BY rowid ASC
3332 `,
3333 fields !== undefined && !fields.includes("contextTransfers")
3334 ? Effect.succeed([])
3335 : window === undefined
3336 ? sql<PayloadRow>`
3337 SELECT payload_json
3338 FROM orchestration_v2_projection_context_transfers
3339 WHERE source_thread_id = ${threadId} OR target_thread_id = ${threadId}
3340 ORDER BY rowid ASC
3341 `
3342 : sql<PayloadRow>`
3343 SELECT payload_json FROM orchestration_v2_projection_context_transfers
3344 WHERE (source_thread_id = ${threadId} OR target_thread_id = ${threadId})
3345 AND (status IN ('pending','running','waiting')
3346 OR target_run_id IN (SELECT value FROM json_each(${cohortRunIds}))
3347 OR json_extract(payload_json, '$.contextHandoffId') IN
3348 (SELECT value FROM json_each(${cohortHandoffIds})))
3349 ORDER BY rowid ASC
3350 `,
3351 ]);
3352
3353 const [
3354 runs,
3355 attempts,
3356 nodes,
3357 subagents,
3358 providerSessions,
3359 providerThreads,
3360 providerTurns,
3361 runtimeRequests,
3362 messages,
3363 plans,
3364 checkpointScopes,
3365 checkpoints,
3366 contextHandoffs,
3367 contextTransfers,
3368 ] = yield* Effect.all([
3369 decodeRows(decodeRunPayload, threadId)(runRows),
3370 decodeRows(decodeRunAttemptPayload, threadId)(attemptRows),
3371 decodeRows(decodeNodePayload, threadId)(nodeRows),
3372 decodeRows(decodeSubagentPayload, threadId)(subagentRows),
3373 decodeRows(decodeProviderSessionPayload, threadId)(providerSessionRows),
3374 decodeRows(decodeProviderThreadPayload, threadId)(providerThreadRows),
3375 decodeRows(decodeProviderTurnPayload, threadId)(providerTurnRows),
3376 decodeRows(decodeRuntimeRequestPayload, threadId)(runtimeRequestRows),
3377 decodeRows(decodeMessagePayload, threadId)(messageRows),
3378 decodeRows(decodePlanPayload, threadId)(planRows),
3379 decodeRows(decodeCheckpointScopePayload, threadId)(checkpointScopeRows),
3380 decodeRows(decodeCheckpointPayload, threadId)(checkpointRows),
3381 decodeRows(decodeContextHandoffPayload, threadId)(contextHandoffRows),
3382 decodeRows(decodeContextTransferPayload, threadId)(contextTransferRows),
3383 ]);
3384 // A metadata read must preserve message ordering without decoding tool output.
3385 const messageOrdinals = new Map<string, number>();
3386 if (fields !== undefined && fields.includes("messages") && messages.length > 1) {
3387 const rows = yield* sql<{ message_id: string; ordinal: number }>`
3388 SELECT json_extract(payload_json, '$.messageId') AS message_id, MIN(ordinal) AS ordinal
3389 FROM orchestration_v2_projection_turn_items
3390 WHERE thread_id = ${threadId} AND type IN ('user_message', 'assistant_message')
3391 GROUP BY json_extract(payload_json, '$.messageId')
3392 `;
3393 for (const row of rows) messageOrdinals.set(row.message_id, row.ordinal);
3394 }
3395 const orderedMessages = sortMessagesByTurnItemOrder(messages, turnItems, messageOrdinals);
3396 const projection = {
3397 thread,
3398 runs,
3399 attempts,
3400 nodes,
3401 subagents,
3402 providerSessions,
3403 providerThreads,
3404 providerTurns,
3405 runtimeRequests,
3406 messages: orderedMessages,
3407 plans,
3408 turnItems,
3409 checkpointScopes,
3410 checkpoints,
3411 contextHandoffs,
3412 contextTransfers,
3413 visibleTurnItems: [],
3414 updatedAt: thread.updatedAt,
3415 } satisfies OrchestrationV2ThreadProjection;
3416 return projection;
3417 }).pipe(
3418 Effect.mapError((cause) =>
3419 isProjectionStoreThreadNotFoundError(cause)
3420 ? cause
3421 : new ProjectionStoreReadError({
3422 threadId,
3423 cause,
3424 }),
3425 ),
3426 );
3427
3428 const readProjection = (
3429 threadId: ThreadId,
3430 seenThreadIds: ReadonlySet<ThreadId>,
3431 window?: {
3432 readonly rowLimit: number;
3433 readonly userTurnLimit?: number | undefined;
3434 readonly anchorItemId?: TurnItemId | undefined;
3435 readonly requiredRunId?: RunId | undefined;
3436 readonly suppressLocal?: boolean | undefined;
3437 readonly historyAnchor?:
3438 | { readonly threadId: ThreadId; readonly itemId: TurnItemId }
3439 | undefined;
3440 },
3441 ): Effect.Effect<OrchestrationV2ThreadProjection, ProjectionStoreV2Error> =>
3442 Effect.gen(function* () {
3443 const localWindow =
3444 window?.suppressLocal === true ||
3445 (window?.historyAnchor !== undefined && window.historyAnchor.threadId !== threadId)
3446 ? { ...window, rowLimit: 0, anchorItemId: undefined }
3447 : window;
3448 // Ancestors contribute timeline items, not their provider/control state.
3449 // Keep their runs and attempts for fork boundaries and visibility.
3450 const projection = yield* readCanonicalProjection(
3451 threadId,
3452 localWindow,
3453 window !== undefined && seenThreadIds.size > 0
3454 ? ["runs", "attempts", "turnItems"]
3455 : undefined,
3456 );
3457 const forkedFrom = projection.thread.forkedFrom;
3458 if (forkedFrom?.type !== "run" || seenThreadIds.has(forkedFrom.threadId)) {
3459 return withLocalVisibleTurnItems(projection);
3460 }
3461
3462 // A row-limited segment without turn anchors must finish paging locally
3463 // before inherited user turns can influence the page boundary.
3464 if (
3465 window?.userTurnLimit !== undefined &&
3466 localWindow !== undefined &&
3467 localWindow.rowLimit > 0 &&
3468 projection.turnItems.length >= localWindow.rowLimit &&
3469 !projection.turnItems.some(isThreadHistoryUserTurn)
3470 ) {
3471 return withLocalVisibleTurnItems(projection);
3472 }
3473
3474 const sourceWindow =
3475 window === undefined
3476 ? undefined
3477 : yield* Effect.gen(function* () {
3478 const historyAnchor = window.historyAnchor;
3479 const anchorBelongsToSource = historyAnchor?.threadId === forkedFrom.threadId;
3480 const rows = yield* sql<{ readonly turn_item_id: string }>`
3481 WITH fork_run AS (
3482 SELECT ordinal
3483 FROM orchestration_v2_projection_runs
3484 WHERE thread_id = ${forkedFrom.threadId}
3485 AND run_id = ${forkedFrom.runId}
3486 LIMIT 1
3487 ), fork_boundary AS (
3488 SELECT (
3489 SELECT item.ordinal
3490 FROM orchestration_v2_projection_turn_items AS item
3491 WHERE item.run_id = run.run_id
3492 ORDER BY item.ordinal DESC
3493 LIMIT 1
3494 ) AS ordinal
3495 FROM orchestration_v2_projection_runs AS run
3496 WHERE run.thread_id = ${forkedFrom.threadId}
3497 AND run.ordinal <= (SELECT ordinal FROM fork_run)
3498 AND EXISTS (
3499 SELECT 1
3500 FROM orchestration_v2_projection_turn_items AS item
3501 WHERE item.run_id = run.run_id
3502 LIMIT 1
3503 )
3504 ORDER BY run.ordinal DESC
3505 LIMIT 1
3506 ), effective_boundary AS (
3507 SELECT COALESCE(
3508 (SELECT ordinal FROM fork_boundary),
3509 (
3510 SELECT ordinal
3511 FROM orchestration_v2_projection_turn_items
3512 WHERE thread_id = ${forkedFrom.threadId}
3513 AND run_id IS NULL
3514 AND json_extract(payload_json, '$.historyOrigin') = 'v1_import'
3515 ORDER BY ordinal DESC, turn_item_id DESC
3516 LIMIT 1
3517 ),
3518 -1
3519 ) AS ordinal
3520 )
3521 SELECT turn_item_id
3522 FROM orchestration_v2_projection_turn_items
3523 WHERE thread_id = ${forkedFrom.threadId}
3524 AND ordinal <= COALESCE(
3525 (SELECT ordinal FROM effective_boundary),
3526 -1
3527 )
3528 AND (
3529 ${anchorBelongsToSource ? 1 : 0} = 0
3530 OR ordinal <= COALESCE(
3531 (SELECT ordinal FROM orchestration_v2_projection_turn_items
3532 WHERE thread_id = ${forkedFrom.threadId}
3533 AND turn_item_id = ${historyAnchor?.itemId ?? null}
3534 LIMIT 1),
3535 (SELECT ordinal FROM effective_boundary),
3536 -1
3537 )
3538 )
3539 ORDER BY ordinal DESC, turn_item_id DESC
3540 LIMIT 1
3541 `;
3542 const anchor = rows[0]?.turn_item_id;
3543 const anchorIsInDescendant =
3544 historyAnchor !== undefined &&
3545 historyAnchor.threadId !== threadId &&
3546 historyAnchor.threadId !== forkedFrom.threadId;
3547 return {
3548 rowLimit: window.rowLimit,
3549 userTurnLimit: window.userTurnLimit,
3550 requiredRunId: forkedFrom.runId,
3551 suppressLocal: anchor === undefined || anchorIsInDescendant,
3552 ...(anchor === undefined || anchorIsInDescendant
3553 ? {}
3554 : { anchorItemId: TurnItemId.make(anchor) }),
3555 ...(historyAnchor === undefined || historyAnchor.threadId === threadId
3556 ? {}
3557 : { historyAnchor }),
3558 };
3559 });
3560
3561 const sourceProjection = yield* readProjection(
3562 forkedFrom.threadId,
3563 new Set([...seenThreadIds, threadId]),
3564 sourceWindow,
3565 );
3566 return {
3567 ...projection,
3568 visibleTurnItems: buildVisibleTurnItems({
3569 projection,
3570 sourceProjection,
3571 }),
3572 };
3573 }).pipe(
3574 Effect.mapError((cause) =>
3575 isProjectionStoreThreadNotFoundError(cause) || isProjectionStoreReadError(cause)
3576 ? cause
3577 : new ProjectionStoreReadError({ threadId, cause }),
3578 ),
3579 );
3580
3581 const getLimitRecoveryCandidates = Effect.fn("ProjectionStore.getLimitRecoveryCandidates")(
3582 function* (options: Parameters<ProjectionStoreV2Shape["getLimitRecoveryCandidates"]>[0]) {
3583 // Indexed latest-run and root-error lookups avoid reading run histories,
3584 // counting transcript items, or walking fork ancestors on scheduler ticks.
3585 const rows = yield* sql<{
3586 readonly payload_json: string;
3587 readonly run_id: string;
3588 readonly completed_at: string | null;
3589 readonly failure_payload_json: string;
3590 readonly last_error: string | null;
3591 }>`
3592 SELECT t.payload_json, r.run_id, r.completed_at,
3593 item.payload_json AS failure_payload_json,
3594 (
3595 SELECT json_extract(session.payload_json, '$.lastError')
3596 -- Keep bindings outermost: one provider instance can hold every session.
3597 FROM orchestration_v2_projection_provider_session_bindings binding
3598 CROSS JOIN orchestration_v2_projection_provider_sessions session
3599 ON session.provider_session_id = binding.provider_session_id
3600 WHERE binding.thread_id = t.thread_id
3601 AND session.provider_instance_id = t.provider_instance_id
3602 ORDER BY session.updated_at DESC, session.provider_session_id DESC
3603 LIMIT 1
3604 ) AS last_error
3605 FROM orchestration_v2_projection_threads t
3606 INNER JOIN orchestration_v2_projection_runs r ON r.run_id = (
3607 SELECT latest.run_id FROM orchestration_v2_projection_runs latest
3608 WHERE latest.thread_id = t.thread_id
3609 AND latest.status <> 'queued'
3610 AND NOT (
3611 latest.status = 'cancelled'
3612 AND json_extract(latest.payload_json, '$.startedAt') IS NULL
3613 )
3614 -- latestExecutedRun: the run that ended last (runRanAfter).
3615 ORDER BY latest.completed_at IS NULL DESC, latest.completed_at DESC,
3616 latest.ordinal DESC, latest.run_id DESC
3617 LIMIT 1
3618 ) AND r.status = 'failed'
3619 INNER JOIN orchestration_v2_projection_turn_items item ON item.turn_item_id = (
3620 SELECT error.turn_item_id FROM orchestration_v2_projection_turn_items error
3621 WHERE error.thread_id = t.thread_id AND error.run_id = r.run_id
3622 AND error.type = 'error' AND error.status = 'failed'
3623 AND error.node_id IS json_extract(r.payload_json, '$.rootNodeId')
3624 ORDER BY error.updated_at DESC, error.ordinal DESC, error.turn_item_id DESC
3625 LIMIT 1
3626 )
3627 WHERE t.deleted_at IS NULL
3628 AND json_extract(t.payload_json, '$.archivedAt') IS NULL
3629 AND json_extract(t.payload_json, '$.settledOverride') IS NOT 'settled'
3630 AND json_extract(item.payload_json, '$.failure.class') = 'usage_limit'
3631 AND json_extract(item.payload_json, '$.failure.resetAt') IS NOT NULL
3632 AND julianday(json_extract(item.payload_json, '$.failure.resetAt')) > julianday(COALESCE(r.completed_at, json_extract(t.payload_json, '$.updatedAt')))
3633 AND (
3634 (
3635 json_extract(t.payload_json, '$.limitRecovery.runId') IS r.run_id
3636 AND json_extract(t.payload_json, '$.limitRecovery.resetAt') IS json_extract(item.payload_json, '$.failure.resetAt')
3637 AND json_extract(t.payload_json, '$.limitRecovery.autoResume') = 1
3638 AND julianday(json_extract(item.payload_json, '$.failure.resetAt')) <= julianday(${DateTime.formatIso(options.now)})
3639 AND (
3640 json_extract(t.payload_json, '$.snoozedUntil') IS NULL
3641 OR julianday(json_extract(t.payload_json, '$.snoozedUntil')) <= julianday(${DateTime.formatIso(options.now)})
3642 )
3643 )
3644 OR (
3645 (
3646 json_extract(t.payload_json, '$.limitRecovery.runId') IS NOT r.run_id
3647 OR json_extract(t.payload_json, '$.limitRecovery.resetAt') IS NOT json_extract(item.payload_json, '$.failure.resetAt')
3648 )
3649 AND (
3650 ${booleanInt(options.autoResume)}
3651 OR (${booleanInt(options.snooze)} AND julianday(json_extract(item.payload_json, '$.failure.resetAt')) > julianday(${DateTime.formatIso(options.now)}))
3652 )
3653 )
3654 )
3655 AND NOT EXISTS (
3656 SELECT 1 FROM orchestration_v2_projection_runtime_requests request
3657 WHERE request.thread_id = t.thread_id AND request.status = 'pending'
3658 )
3659 ORDER BY t.thread_id
3660 `;
3661 const candidates: Array<ProjectionLimitRecoveryCandidate> = [];
3662 for (const row of rows) {
3663 const thread = yield* decodeThreadPayload(row.payload_json);
3664 const item = yield* decodeTurnItemPayload(row.failure_payload_json);
3665 const summary = threadErrorSummary(
3666 item.type === "error" ? item.failure : null,
3667 row.last_error,
3668 );
3669 if (summary.lastErrorClass !== "usage_limit") continue;
3670 candidates.push({
3671 id: thread.id,
3672 status: "failed",
3673 lastErrorClass: summary.lastErrorClass,
3674 usageLimitResetAt: summary.usageLimitResetAt,
3675 latestRunId: RunId.make(row.run_id),
3676 latestRunCompletedAt:
3677 row.completed_at === null ? null : DateTime.makeUnsafe(row.completed_at),
3678 updatedAt: thread.updatedAt,
3679 archivedAt: thread.archivedAt,
3680 settledOverride: thread.settledOverride,
3681 pendingRuntimeRequest: null,
3682 limitRecovery: thread.limitRecovery ?? null,
3683 snoozedUntil: thread.snoozedUntil ?? null,
3684 });
3685 }
3686 return candidates;
3687 },
3688 Effect.mapError((cause) => new ProjectionStoreSetupError({ cause })),
3689 );
3690
3691 const getRecoveryThreadIds = Effect.fn("ProjectionStore.getRecoveryThreadIds")(
3692 function* (kind: ProjectionRecoveryKind) {
3693 const candidates = (() => {
3694 switch (kind) {
3695 case "queued-runs":
3696 return sql`
3697 SELECT thread_id FROM orchestration_v2_projection_runs
3698 WHERE status = 'queued'
3699 AND NOT EXISTS (
3700 SELECT 1 FROM orchestration_v2_projection_runs AS active
3701 WHERE active.thread_id = orchestration_v2_projection_runs.thread_id
3702 AND active.status IN ('preparing', 'starting', 'running', 'waiting')
3703 )
3704 `;
3705 case "delegated-completions":
3706 return sql`
3707 SELECT thread_id FROM orchestration_v2_projection_runs
3708 WHERE CASE WHEN json_valid(payload_json)
3709 THEN json_type(payload_json, '$.delegatedCompletion.delivery') = 'object'
3710 ELSE 0 END
3711 `;
3712 case "subagent-results":
3713 return sql`
3714 SELECT child.thread_id FROM orchestration_v2_projection_threads AS child
3715 WHERE CASE WHEN json_valid(child.payload_json) THEN
3716 json_extract(child.payload_json, '$.lineage.relationshipToParent') = 'subagent'
3717 AND json_extract(child.payload_json, '$.lineage.parentThreadId') IS NOT NULL
3718 AND json_extract(child.payload_json, '$.forkedFrom.type') = 'node'
3719 AND (EXISTS (
3720 SELECT 1 FROM orchestration_v2_projection_runs AS requested
3721 JOIN orchestration_v2_projection_subagents AS task
3722 ON task.subagent_id = json_extract(requested.payload_json, '$.delegatedTaskId')
3723 WHERE requested.thread_id = child.thread_id
3724 AND requested.status IN ('completed', 'interrupted', 'failed', 'cancelled', 'rolled_back')
3725 AND task.child_thread_id = child.thread_id
3726 AND json_extract(task.payload_json, '$.origin') = 'app_owned'
3727 AND json_extract(task.payload_json, '$.result') IS NULL
3728 ) OR (
3729 -- The original task still waits for the latest unheld work.
3730 (SELECT status FROM orchestration_v2_projection_runs
3731 WHERE thread_id = child.thread_id
3732 AND NOT (status = 'queued' AND json_extract(payload_json, '$.queueHeld') IS 1)
3733 ORDER BY ordinal DESC LIMIT 1
3734 ) IN ('completed', 'interrupted', 'failed', 'cancelled', 'rolled_back')
3735 AND NOT EXISTS (
3736 SELECT 1 FROM orchestration_v2_projection_context_transfers
3737 WHERE source_thread_id = child.thread_id
3738 AND target_thread_id = json_extract(child.payload_json, '$.lineage.parentThreadId')
3739 AND type = 'subagent_result'
3740 )))
3741 ELSE 0 END
3742 `;
3743 case "runtime":
3744 return sql`
3745 WITH pending_provider_threads AS MATERIALIZED (
3746 SELECT provider_thread_id, thread_id, owner_node_id
3747 FROM orchestration_v2_projection_provider_threads
3748 WHERE status = 'active'
3749 OR CASE WHEN json_valid(payload_json)
3750 THEN json_array_length(payload_json, '$.pendingBackgroundTasks') > 0
3751 ELSE 0 END
3752 )
3753 SELECT thread_id FROM orchestration_v2_projection_runs
3754 WHERE status IN ('preparing', 'starting', 'running', 'waiting')
3755 UNION
3756 -- A held queue already went through recovery; rereading it on
3757 -- every boot costs a projection read per held thread.
3758 SELECT thread_id FROM orchestration_v2_projection_runs
3759 WHERE status = 'queued'
3760 AND CASE WHEN json_valid(payload_json)
3761 THEN json_extract(payload_json, '$.queueHeld') IS NOT 1
3762 ELSE 1 END
3763 UNION
3764 SELECT thread_id FROM orchestration_v2_projection_runtime_requests
3765 WHERE status = 'pending'
3766 UNION
3767 SELECT bindings.thread_id
3768 FROM orchestration_v2_projection_provider_sessions AS sessions
3769 CROSS JOIN orchestration_v2_projection_provider_session_bindings AS bindings
3770 ON bindings.provider_session_id = sessions.provider_session_id
3771 WHERE sessions.status NOT IN ('stopped', 'error')
3772 UNION
3773 -- A detach drops the binding but keeps the session's status.
3774 SELECT sessions.thread_id
3775 FROM orchestration_v2_projection_provider_sessions AS sessions
3776 WHERE sessions.status NOT IN ('stopped', 'error')
3777 AND sessions.thread_id IS NOT NULL
3778 AND NOT EXISTS (
3779 SELECT 1 FROM orchestration_v2_projection_provider_session_bindings AS bindings
3780 WHERE bindings.provider_session_id = sessions.provider_session_id
3781 )
3782 UNION
3783 SELECT thread_id FROM pending_provider_threads
3784 UNION
3785 SELECT nodes.thread_id FROM pending_provider_threads
3786 CROSS JOIN orchestration_v2_projection_nodes AS nodes
3787 ON nodes.node_id = pending_provider_threads.owner_node_id
3788 UNION
3789 SELECT subagents.thread_id FROM pending_provider_threads
3790 CROSS JOIN orchestration_v2_projection_subagents AS subagents
3791 ON subagents.provider_thread_id = pending_provider_threads.provider_thread_id
3792 UNION
3793 SELECT subagents.child_thread_id FROM orchestration_v2_projection_subagents AS subagents
3794 WHERE subagents.child_thread_id IS NOT NULL
3795 AND EXISTS (
3796 SELECT 1 FROM orchestration_v2_projection_nodes AS node
3797 WHERE node.thread_id = subagents.child_thread_id
3798 AND node.run_id IS NULL
3799 AND node.kind = 'root_turn'
3800 AND node.status IN ('pending', 'running', 'waiting')
3801 )
3802 UNION
3803 -- No native subagent outlives the provider process that ran it.
3804 SELECT thread_id FROM orchestration_v2_projection_subagents
3805 WHERE origin = 'provider_native'
3806 AND status IN ('pending', 'running', 'waiting')
3807 UNION
3808 SELECT item.thread_id FROM orchestration_v2_projection_turn_items AS item
3809 WHERE NOT EXISTS (
3810 SELECT 1 FROM orchestration_v2_projection_runs AS run
3811 WHERE run.run_id = item.run_id AND run.status = 'rolled_back'
3812 )
3813 AND type IN ('command_execution', 'dynamic_tool', 'subagent')
3814 AND status IN ('pending', 'running', 'waiting')
3815 UNION
3816 SELECT thread_id FROM orchestration_v2_effect_outbox
3817 WHERE status IN ('pending', 'running')
3818 `;
3819 }
3820 })();
3821 const rows = yield* sql<{ readonly thread_id: string }>`
3822 SELECT thread_id FROM orchestration_v2_projection_threads
3823 WHERE deleted_at IS NULL
3824 AND thread_id IN (${candidates})
3825 ${kind === "queued-runs" ? sql`AND archived_at IS NULL` : sql``}
3826 ORDER BY updated_at ASC, thread_id ASC
3827 `;
3828 return rows.map((row) => ThreadId.make(row.thread_id));
3829 },
3830 Effect.mapError((cause) => new ProjectionStoreSetupError({ cause })),
3831 );
3832
3833 // Decode every canonical row once. Full thread reads repeat shared sessions,
3834 // provider threads, transfers, and inherited fork histories for each owner.
3835 const getUnreadableThreadIds = Effect.fn("ProjectionStore.getUnreadableThreadIds")(
3836 function* () {
3837 const actualThreadIds = new Set<ThreadId>();
3838 const unreadable = new Set<ThreadId>();
3839 const forkChildren = new Map<ThreadId, Array<ThreadId>>();
3840 const tables: ReadonlyArray<{
3841 readonly name: string;
3842 readonly id: string;
3843 readonly decode: (payload: string, entityId: string) => Effect.Effect<unknown, unknown>;
3844 }> = [
3845 {
3846 name: "threads",
3847 id: "thread_id",
3848 decode: (payload: string, entityId: string) =>
3849 decodeThreadPayload(payload).pipe(
3850 Effect.tap((thread) =>
3851 Effect.sync(() => {
3852 if (thread.forkedFrom?.type !== "run") return;
3853 const sourceId = thread.forkedFrom.threadId;
3854 const children = forkChildren.get(sourceId) ?? [];
3855 children.push(ThreadId.make(entityId));
3856 forkChildren.set(sourceId, children);
3857 }),
3858 ),
3859 ),
3860 },
3861 { name: "runs", id: "run_id", decode: (payload) => decodeRunPayload(payload) },
3862 {
3863 name: "run_attempts",
3864 id: "attempt_id",
3865 decode: (payload) => decodeRunAttemptPayload(payload),
3866 },
3867 { name: "nodes", id: "node_id", decode: (payload) => decodeNodePayload(payload) },
3868 {
3869 name: "subagents",
3870 id: "subagent_id",
3871 decode: (payload) => decodeSubagentPayload(payload),
3872 },
3873 {
3874 name: "provider_sessions",
3875 id: "provider_session_id",
3876 decode: (payload) => decodeProviderSessionPayload(payload),
3877 },
3878 {
3879 name: "provider_threads",
3880 id: "provider_thread_id",
3881 decode: (payload) => decodeProviderThreadPayload(payload),
3882 },
3883 {
3884 name: "provider_turns",
3885 id: "provider_turn_id",
3886 decode: (payload) => decodeProviderTurnPayload(payload),
3887 },
3888 {
3889 name: "runtime_requests",
3890 id: "runtime_request_id",
3891 decode: (payload) => decodeRuntimeRequestPayload(payload),
3892 },
3893 {
3894 name: "messages",
3895 id: "message_id",
3896 decode: (payload) => decodeMessagePayload(payload),
3897 },
3898 { name: "plans", id: "plan_id", decode: decodePlanPayload },
3899 {
3900 name: "turn_items",
3901 id: "turn_item_id",
3902 decode: (payload) => decodeTurnItemPayload(payload),
3903 },
3904 {
3905 name: "checkpoint_scopes",
3906 id: "scope_id",
3907 decode: (payload) => decodeCheckpointScopePayload(payload),
3908 },
3909 {
3910 name: "checkpoints",
3911 id: "checkpoint_id",
3912 decode: (payload) => decodeCheckpointPayload(payload),
3913 },
3914 {
3915 name: "context_handoffs",
3916 id: "context_handoff_id",
3917 decode: (payload) => decodeContextHandoffPayload(payload),
3918 },
3919 {
3920 name: "context_transfers",
3921 id: "context_transfer_id",
3922 decode: (payload) => decodeContextTransferPayload(payload),
3923 },
3924 ];
3925 const pageSize = 500;
3926 for (const table of tables) {
3927 let afterRowId = 0;
3928 while (true) {
3929 const rows = yield* sql<{
3930 readonly row_id: number;
3931 readonly entity_id: string;
3932 readonly thread_id: string | null;
3933 readonly target_thread_id: string | null;
3934 readonly payload_json: string;
3935 }>`
3936 SELECT rowid AS row_id, ${sql(table.id)} AS entity_id,
3937 ${sql(table.name === "context_transfers" ? "source_thread_id" : "thread_id")} AS thread_id,
3938 ${table.name === "context_transfers" ? sql`target_thread_id` : sql`NULL`} AS target_thread_id,
3939 payload_json
3940 FROM ${sql(`orchestration_v2_projection_${table.name}`)}
3941 WHERE rowid > ${afterRowId}
3942 ${
3943 ["messages", "turn_items", "subagents"].includes(table.name)
3944 ? sql`AND NOT (
3945 json_extract(payload_json, '$.archivedPayload') = 1
3946 AND EXISTS (SELECT 1 FROM transcript_archives a WHERE a.thread_id = ${sql(`orchestration_v2_projection_${table.name}`)}.thread_id))`
3947 : sql``
3948 }
3949 ORDER BY rowid ASC LIMIT ${pageSize}
3950 `;
3951 for (const row of rows) {
3952 if (table.name === "threads") actualThreadIds.add(ThreadId.make(row.entity_id));
3953 const decoded = yield* Effect.exit(
3954 Effect.suspend(() => table.decode(row.payload_json, row.entity_id)),
3955 );
3956 if (decoded._tag === "Success") continue;
3957 if (table.name === "provider_sessions") {
3958 const bindings = yield* sql<{ readonly thread_id: string }>`
3959 SELECT thread_id FROM orchestration_v2_projection_provider_session_bindings
3960 WHERE provider_session_id = ${row.entity_id}
3961 `;
3962 for (const binding of bindings) unreadable.add(ThreadId.make(binding.thread_id));
3963 } else {
3964 if (row.thread_id !== null) unreadable.add(ThreadId.make(row.thread_id));
3965 if (row.target_thread_id !== null)
3966 unreadable.add(ThreadId.make(row.target_thread_id));
3967 }
3968 if (table.name === "provider_threads") {
3969 const owners = yield* sql<{ readonly thread_id: string }>`
3970 SELECT thread_id FROM orchestration_v2_projection_nodes
3971 WHERE node_id = (
3972 SELECT owner_node_id FROM orchestration_v2_projection_provider_threads
3973 WHERE provider_thread_id = ${row.entity_id}
3974 )
3975 UNION
3976 SELECT thread_id FROM orchestration_v2_projection_subagents
3977 WHERE provider_thread_id = ${row.entity_id}
3978 `;
3979 for (const owner of owners) unreadable.add(ThreadId.make(owner.thread_id));
3980 }
3981 }
3982 if (rows.length < pageSize) break;
3983 afterRowId = rows[rows.length - 1]!.row_id;
3984 yield* Effect.yieldNow;
3985 }
3986 }
3987 // A full fork projection also reads its source. Propagate decode failures
3988 // and missing sources through the same ancestry without loading history.
3989 const pending = [
3990 ...unreadable,
3991 ...[...forkChildren.keys()].filter((sourceId) => !actualThreadIds.has(sourceId)),
3992 ];
3993 for (let index = 0; index < pending.length; index += 1) {
3994 for (const childId of forkChildren.get(pending[index]!) ?? []) {
3995 if (unreadable.has(childId)) continue;
3996 unreadable.add(childId);
3997 pending.push(childId);
3998 }
3999 }
4000 return [...unreadable].filter((threadId) => actualThreadIds.has(threadId)).sort();
4001 },
4002 Effect.mapError((cause) => new ProjectionStoreSetupError({ cause })),
4003 );
4004
4005 // SQLite trim defaults to ASCII spaces; compact recognition uses JavaScript trim.
4006 const javascriptTrimWhitespace =
4007 " \t\n\r\v\f\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff";
4008
4009 // Startup needs execution metadata, not the transcript or inherited fork history.
4010 const getTurnStartContext: ProjectionStoreV2Shape["getTurnStartContext"] = (threadId, runId) =>
4011 sql
4012 .withTransaction(
4013 Effect.gen(function* () {
4014 const thread = yield* getThread(threadId);
4015 const runs = yield* sql<PayloadRow>`
4016 SELECT payload_json FROM orchestration_v2_projection_runs
4017 WHERE thread_id = ${threadId} ORDER BY ordinal ASC
4018 `.pipe(Effect.flatMap(decodeRows(decodeRunPayload, threadId)));
4019 const attempts = yield* sql<PayloadRow>`
4020 SELECT payload_json FROM orchestration_v2_projection_run_attempts
4021 WHERE thread_id = ${threadId} ORDER BY run_id ASC, attempt_ordinal ASC
4022 `.pipe(Effect.flatMap(decodeRows(decodeRunAttemptPayload, threadId)));
4023 const nodes = yield* sql<PayloadRow>`
4024 SELECT payload_json FROM orchestration_v2_projection_nodes
4025 WHERE thread_id = ${threadId} AND node_id IN (SELECT json_extract(payload_json, '$.rootNodeId') FROM orchestration_v2_projection_runs WHERE thread_id = ${threadId} AND run_id = ${runId}) ORDER BY node_id ASC
4026 `.pipe(Effect.flatMap(decodeRows(decodeNodePayload, threadId)));
4027 const subagents = yield* sql<PayloadRow>`
4028 SELECT payload_json FROM orchestration_v2_projection_subagents
4029 WHERE thread_id = ${threadId} AND status NOT IN ('interrupted','failed','cancelled') ORDER BY subagent_id ASC
4030 `.pipe(Effect.flatMap(decodeRows(decodeSubagentPayload, threadId)));
4031 const providerThreads = yield* sql<PayloadRow>`
4032 SELECT payload_json FROM orchestration_v2_projection_provider_threads
4033 WHERE thread_id = ${threadId} ORDER BY COALESCE(first_run_ordinal, 0), provider_thread_id ASC
4034 `.pipe(Effect.flatMap(decodeRows(decodeProviderThreadPayload, threadId)));
4035 const providerTurns = yield* sql<PayloadRow>`
4036 SELECT payload_json FROM orchestration_v2_projection_provider_turns
4037 WHERE thread_id = ${threadId} ORDER BY provider_thread_id ASC, ordinal ASC
4038 `.pipe(Effect.flatMap(decodeRows(decodeProviderTurnPayload, threadId)));
4039 const checkpointScopes = yield* sql<PayloadRow>`
4040 SELECT payload_json FROM orchestration_v2_projection_checkpoint_scopes
4041 WHERE thread_id = ${threadId} AND scope_id IN ${sql.in(nodes.flatMap((node) => (node.checkpointScopeId === null ? [] : [node.checkpointScopeId])))} ORDER BY ordinal_within_parent ASC, scope_id ASC
4042 `.pipe(Effect.flatMap(decodeRows(decodeCheckpointScopePayload, threadId)));
4043 const contextHandoffs = yield* sql<PayloadRow>`
4044 SELECT payload_json FROM orchestration_v2_projection_context_handoffs
4045 WHERE thread_id = ${threadId} ORDER BY rowid ASC
4046 `.pipe(Effect.flatMap(decodeRows(decodeContextHandoffPayload, threadId)));
4047 const contextTransfers = yield* sql<PayloadRow>`
4048 SELECT payload_json FROM orchestration_v2_projection_context_transfers
4049 WHERE source_thread_id = ${threadId} OR target_thread_id = ${threadId} ORDER BY rowid ASC
4050 `.pipe(Effect.flatMap(decodeRows(decodeContextTransferPayload, threadId)));
4051 const turnItems = yield* sql<PayloadRow>`
4052 SELECT payload_json FROM orchestration_v2_projection_turn_items
4053 WHERE thread_id = ${threadId} AND run_id = ${runId} ORDER BY ordinal ASC, turn_item_id ASC
4054 `.pipe(Effect.flatMap(decodeRows(decodeTurnItemPayload, threadId)));
4055 const providerSessions = yield* sql<PayloadRow>`
4056 SELECT sessions.payload_json FROM orchestration_v2_projection_provider_sessions AS sessions
4057 INNER JOIN orchestration_v2_projection_provider_session_bindings AS bindings
4058 ON bindings.provider_session_id = sessions.provider_session_id
4059 WHERE bindings.thread_id = ${threadId}
4060 ORDER BY sessions.updated_at ASC, sessions.provider_session_id ASC
4061 `.pipe(Effect.flatMap(decodeRows(decodeProviderSessionPayload, threadId)));
4062 // Compact retries need their original markers; emptiness is an existence check.
4063 const messages = yield* sql<PayloadRow>`
4064 SELECT payload_json FROM orchestration_v2_projection_messages
4065 WHERE thread_id = ${threadId} AND (
4066 message_id IN (SELECT json_extract(payload_json, '$.userMessageId') FROM orchestration_v2_projection_runs
4067 WHERE thread_id = ${threadId} AND run_id = ${runId})
4068 OR (role = 'user' AND lower(trim(json_extract(payload_json, '$.text'), ${javascriptTrimWhitespace})) = '/compact')
4069 ) ORDER BY created_at ASC, message_id ASC
4070 `.pipe(Effect.flatMap(decodeRows(decodeMessagePayload, threadId)));
4071 const conversation = yield* sql<{ present: number }>`
4072 SELECT EXISTS(
4073 SELECT 1 FROM orchestration_v2_projection_messages
4074 WHERE thread_id = ${threadId} AND role = 'user'
4075 AND (lower(trim(json_extract(payload_json, '$.text'), ${javascriptTrimWhitespace})) <> '/compact'
4076 OR json_array_length(payload_json, '$.attachments') > 0)
4077 ) AS present
4078 `;
4079 return {
4080 hasConversation: conversation[0]?.present === 1,
4081 thread,
4082 runs,
4083 attempts,
4084 nodes,
4085 subagents,
4086 providerSessions,
4087 providerThreads,
4088 providerTurns,
4089 messages,
4090 checkpointScopes,
4091 contextHandoffs,
4092 contextTransfers,
4093 turnItems,
4094 };
4095 }),
4096 )
4097 .pipe(
4098 Effect.mapError((cause) =>
4099 isProjectionStoreThreadNotFoundError(cause)
4100 ? cause
4101 : new ProjectionStoreReadError({ threadId, cause }),
4102 ),
4103 );
4104
4105 const getTurnStartHistory: ProjectionStoreV2Shape["getTurnStartHistory"] = (threadId, runIds) =>
4106 Effect.gen(function* () {
4107 const rows = yield* sql<PayloadRow>`
4108 SELECT payload_json FROM orchestration_v2_projection_turn_items
4109 WHERE thread_id = ${threadId}
4110 AND type IN ('user_message','assistant_message','command_execution','error',
4111 'run_interrupt_result','file_change','proposed_plan')
4112 AND ${runIds === undefined ? sql`1` : sql`run_id IN ${sql.in(runIds)}`}
4113 ORDER BY ordinal ASC, turn_item_id ASC
4114 `;
4115 return yield* decodeRows(decodeTurnItemPayload, threadId)(rows);
4116 }).pipe(Effect.mapError((cause) => new ProjectionStoreReadError({ threadId, cause })));
4117
4118 const getThreadProjection: ProjectionStoreV2Shape["getThreadProjection"] = (threadId) =>
4119 readProjection(threadId, new Set());
4120
4121 const getRuntimeRecoveryProjection: ProjectionStoreV2Shape["getRuntimeRecoveryProjection"] = (
4122 threadId,
4123 ) =>
4124 Effect.gen(function* () {
4125 const threadRows = yield* sql<PayloadRow>`
4126 SELECT payload_json FROM orchestration_v2_projection_threads
4127 WHERE thread_id = ${threadId}
4128 `;
4129 if (threadRows[0] === undefined) {
4130 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
4131 }
4132 const [
4133 thread,
4134 runRows,
4135 attemptRows,
4136 nodeRows,
4137 subagentRows,
4138 providerSessionRows,
4139 providerThreadRows,
4140 providerTurnRows,
4141 runtimeRequestRows,
4142 messageRows,
4143 turnItemRows,
4144 ] = yield* Effect.all([
4145 decodeThreadPayload(threadRows[0].payload_json),
4146 sql<PayloadRow>`
4147 SELECT payload_json FROM orchestration_v2_projection_runs AS run
4148 WHERE run.thread_id = ${threadId}
4149 AND (
4150 run.status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
4151 OR run.run_id = (
4152 SELECT latest.run_id FROM orchestration_v2_projection_runs AS latest
4153 WHERE latest.thread_id = ${threadId}
4154 ORDER BY latest.ordinal DESC LIMIT 1
4155 )
4156 -- Retain the latest run for each provider thread with lost
4157 -- background work, including owners used before a handoff.
4158 OR run.run_id IN (
4159 SELECT (
4160 SELECT ended.run_id FROM orchestration_v2_projection_runs AS ended
4161 WHERE ended.thread_id = ${threadId}
4162 AND ended.provider_thread_id = roster.provider_thread_id
4163 AND ended.status NOT IN ('queued', 'rolled_back')
4164 ORDER BY ended.completed_at IS NULL DESC, ended.completed_at DESC,
4165 ended.ordinal DESC
4166 LIMIT 1
4167 )
4168 FROM orchestration_v2_projection_provider_threads AS roster
4169 WHERE roster.thread_id = ${threadId}
4170 AND CASE WHEN json_valid(roster.payload_json)
4171 THEN json_array_length(roster.payload_json, '$.pendingBackgroundTasks') > 0
4172 ELSE 0 END
4173 )
4174 OR run.run_id IN (
4175 SELECT item.run_id FROM orchestration_v2_projection_turn_items AS item
4176 WHERE item.thread_id = ${threadId}
4177 AND item.type IN ('command_execution', 'dynamic_tool', 'subagent')
4178 AND item.status IN ('pending', 'running', 'waiting')
4179 AND item.run_id IS NOT NULL
4180 )
4181 )
4182 ORDER BY run.ordinal ASC
4183 `,
4184 sql<PayloadRow>`
4185 SELECT attempt.payload_json
4186 FROM orchestration_v2_projection_run_attempts AS attempt
4187 WHERE attempt.thread_id = ${threadId}
4188 AND attempt.run_id IN (
4189 SELECT run_id FROM orchestration_v2_projection_runs
4190 WHERE thread_id = ${threadId}
4191 AND status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
4192 )
4193 ORDER BY attempt.run_id ASC, attempt.attempt_ordinal ASC
4194 `,
4195 sql<PayloadRow>`
4196 SELECT node.payload_json FROM orchestration_v2_projection_nodes AS node
4197 WHERE node.thread_id = ${threadId}
4198 AND node.status IN ('pending', 'starting', 'running', 'waiting')
4199 AND (
4200 (node.run_id IS NULL AND node.kind = 'root_turn')
4201 OR node.run_id IN (
4202 SELECT run_id FROM orchestration_v2_projection_runs
4203 WHERE thread_id = ${threadId}
4204 AND status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
4205 )
4206 OR node.node_id IN (
4207 SELECT item.node_id FROM orchestration_v2_projection_turn_items AS item
4208 WHERE item.thread_id = ${threadId}
4209 AND item.type IN ('command_execution', 'dynamic_tool', 'subagent')
4210 AND item.status IN ('pending', 'running', 'waiting')
4211 )
4212 OR node.node_id IN (
4213 SELECT json_extract(item.payload_json, '$.subagentId')
4214 FROM orchestration_v2_projection_turn_items AS item
4215 WHERE item.thread_id = ${threadId} AND item.type = 'subagent'
4216 AND item.status IN ('pending', 'running', 'waiting')
4217 )
4218 OR node.node_id IN (
4219 SELECT subagent_id FROM orchestration_v2_projection_subagents
4220 WHERE thread_id = ${threadId} AND origin = 'provider_native'
4221 AND status IN ('pending', 'running', 'waiting')
4222 )
4223 )
4224 ORDER BY COALESCE(node.started_at, ''), node.node_id ASC
4225 `,
4226 sql<PayloadRow>`
4227 SELECT subagent.payload_json FROM orchestration_v2_projection_subagents AS subagent
4228 WHERE subagent.thread_id = ${threadId}
4229 AND subagent.status IN ('pending', 'starting', 'running', 'waiting')
4230 AND (
4231 subagent.origin = 'provider_native'
4232 OR subagent.run_id IN (
4233 SELECT run_id FROM orchestration_v2_projection_runs
4234 WHERE thread_id = ${threadId}
4235 AND status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
4236 )
4237 OR subagent.subagent_id IN (
4238 SELECT json_extract(item.payload_json, '$.subagentId')
4239 FROM orchestration_v2_projection_turn_items AS item
4240 WHERE item.thread_id = ${threadId} AND item.type = 'subagent'
4241 AND item.status IN ('pending', 'running', 'waiting')
4242 )
4243 )
4244 ORDER BY COALESCE(subagent.started_at, ''), subagent.subagent_id ASC
4245 `,
4246 sql<PayloadRow>`
4247 SELECT session.payload_json
4248 FROM orchestration_v2_projection_provider_sessions AS session
4249 WHERE (
4250 session.provider_session_id IN (
4251 SELECT binding.provider_session_id
4252 FROM orchestration_v2_projection_provider_session_bindings AS binding
4253 WHERE binding.thread_id = ${threadId}
4254 )
4255 AND (
4256 session.status NOT IN ('stopped', 'error')
4257 OR session.provider_session_id IN (
4258 SELECT provider_thread.provider_session_id
4259 FROM orchestration_v2_projection_provider_threads AS provider_thread
4260 WHERE provider_thread.provider_thread_id IN (
4261 SELECT run.provider_thread_id
4262 FROM orchestration_v2_projection_runs AS run
4263 WHERE run.thread_id = ${threadId}
4264 AND run.status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
4265 )
4266 )
4267 )
4268 )
4269 -- This thread detached it, which keeps its status. Its process
4270 -- did not outlive the server either, so recovery stops it.
4271 OR (
4272 session.thread_id = ${threadId}
4273 AND session.status NOT IN ('stopped', 'error')
4274 AND NOT EXISTS (
4275 SELECT 1 FROM orchestration_v2_projection_provider_session_bindings AS binding
4276 WHERE binding.provider_session_id = session.provider_session_id
4277 )
4278 )
4279 ORDER BY session.updated_at ASC, session.provider_session_id ASC
4280 `,
4281 sql<PayloadRow>`
4282 SELECT provider_thread.payload_json
4283 FROM orchestration_v2_projection_provider_threads AS provider_thread
4284 WHERE (
4285 provider_thread.thread_id = ${threadId}
4286 OR EXISTS (
4287 SELECT 1 FROM orchestration_v2_projection_nodes AS owner
4288 WHERE owner.node_id = provider_thread.owner_node_id
4289 AND owner.thread_id = ${threadId}
4290 )
4291 OR EXISTS (
4292 SELECT 1 FROM orchestration_v2_projection_subagents AS subagent
4293 WHERE subagent.provider_thread_id = provider_thread.provider_thread_id
4294 AND subagent.thread_id = ${threadId}
4295 )
4296 )
4297 AND (
4298 provider_thread.status = 'active'
4299 OR CASE WHEN json_valid(provider_thread.payload_json)
4300 THEN json_array_length(provider_thread.payload_json, '$.pendingBackgroundTasks') > 0
4301 ELSE 0 END
4302 OR provider_thread.provider_thread_id IN (
4303 SELECT run.provider_thread_id FROM orchestration_v2_projection_runs AS run
4304 WHERE run.thread_id = ${threadId}
4305 AND run.status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
4306 )
4307 OR provider_thread.provider_thread_id IN (
4308 SELECT item.provider_thread_id
4309 FROM orchestration_v2_projection_turn_items AS item
4310 WHERE item.thread_id = ${threadId}
4311 AND item.status IN ('pending', 'running', 'waiting')
4312 )
4313 )
4314 ORDER BY COALESCE(provider_thread.first_run_ordinal, 0), provider_thread.provider_thread_id ASC
4315 `,
4316 sql<PayloadRow>`
4317 SELECT provider_turn.payload_json
4318 FROM orchestration_v2_projection_provider_turns AS provider_turn
4319 WHERE provider_turn.thread_id = ${threadId}
4320 AND provider_turn.status IN ('pending', 'starting', 'running', 'waiting')
4321 AND provider_turn.run_attempt_id IN (
4322 SELECT attempt_id FROM orchestration_v2_projection_run_attempts
4323 WHERE thread_id = ${threadId}
4324 AND run_id IN (
4325 SELECT run_id FROM orchestration_v2_projection_runs
4326 WHERE thread_id = ${threadId}
4327 AND status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
4328 )
4329 )
4330 ORDER BY provider_turn.provider_thread_id ASC, provider_turn.ordinal ASC
4331 `,
4332 sql<PayloadRow>`
4333 SELECT payload_json FROM orchestration_v2_projection_runtime_requests
4334 WHERE thread_id = ${threadId} AND status = 'pending'
4335 ORDER BY created_at ASC, runtime_request_id ASC
4336 `,
4337 sql<PayloadRow>`
4338 SELECT message.payload_json FROM orchestration_v2_projection_messages AS message
4339 WHERE message.thread_id = ${threadId} AND message.streaming = 1
4340 AND message.run_id IN (
4341 SELECT run_id FROM orchestration_v2_projection_runs
4342 WHERE thread_id = ${threadId}
4343 AND status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
4344 )
4345 ORDER BY message.created_at ASC, message.message_id ASC
4346 `,
4347 sql<PayloadRow>`
4348 SELECT item.payload_json FROM orchestration_v2_projection_turn_items AS item
4349 WHERE item.thread_id = ${threadId}
4350 AND item.status IN ('pending', 'running', 'waiting')
4351 AND (
4352 item.run_id IN (
4353 SELECT run_id FROM orchestration_v2_projection_runs
4354 WHERE thread_id = ${threadId}
4355 AND status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
4356 )
4357 OR item.type IN ('command_execution', 'dynamic_tool', 'subagent')
4358 OR (
4359 item.run_id IS NULL
4360 AND item.node_id IN (
4361 SELECT node_id FROM orchestration_v2_projection_nodes
4362 WHERE thread_id = ${threadId}
4363 AND run_id IS NULL
4364 AND kind = 'root_turn'
4365 AND status IN ('pending', 'running', 'waiting')
4366 )
4367 )
4368 )
4369 ORDER BY item.ordinal ASC, item.turn_item_id ASC
4370 `,
4371 ]);
4372 const [
4373 runs,
4374 attempts,
4375 nodes,
4376 subagents,
4377 providerSessions,
4378 providerThreads,
4379 providerTurns,
4380 runtimeRequests,
4381 messages,
4382 turnItems,
4383 ] = yield* Effect.all([
4384 decodeRows(decodeRunPayload, threadId)(runRows),
4385 decodeRows(decodeRunAttemptPayload, threadId)(attemptRows),
4386 decodeRows(decodeNodePayload, threadId)(nodeRows),
4387 decodeRows(decodeSubagentPayload, threadId)(subagentRows),
4388 decodeRows(decodeProviderSessionPayload, threadId)(providerSessionRows),
4389 decodeRows(decodeProviderThreadPayload, threadId)(providerThreadRows),
4390 decodeRows(decodeProviderTurnPayload, threadId)(providerTurnRows),
4391 decodeRows(decodeRuntimeRequestPayload, threadId)(runtimeRequestRows),
4392 decodeRows(decodeMessagePayload, threadId)(messageRows),
4393 decodeRows(decodeTurnItemPayload, threadId)(turnItemRows),
4394 ]);
4395 return {
4396 thread,
4397 runs,
4398 attempts,
4399 nodes,
4400 subagents,
4401 providerSessions,
4402 providerThreads,
4403 providerTurns,
4404 runtimeRequests,
4405 messages,
4406 turnItems,
4407 } satisfies ProjectionRuntimeRecoveryState;
4408 }).pipe(
4409 Effect.mapError((cause) =>
4410 isProjectionStoreThreadNotFoundError(cause)
4411 ? cause
4412 : new ProjectionStoreReadError({ threadId, cause }),
4413 ),
4414 );
4415
4416 const getThread: ProjectionStoreV2Shape["getThread"] = (threadId) =>
4417 Effect.gen(function* () {
4418 const rows = yield* sql<PayloadRow>`
4419 SELECT payload_json FROM orchestration_v2_projection_threads
4420 WHERE thread_id = ${threadId}
4421 `;
4422 if (rows[0] === undefined) {
4423 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
4424 }
4425 return yield* decodeThreadPayload(rows[0].payload_json);
4426 }).pipe(
4427 Effect.mapError((cause) =>
4428 isProjectionStoreThreadNotFoundError(cause)
4429 ? cause
4430 : new ProjectionStoreReadError({ threadId, cause }),
4431 ),
4432 );
4433
4434 const requireThread = (threadId: ThreadId) =>
4435 Effect.gen(function* () {
4436 const rows = yield* sql<{ readonly thread_id: string }>`
4437 SELECT thread_id FROM orchestration_v2_projection_threads WHERE thread_id = ${threadId} LIMIT 1
4438 `;
4439 if (rows.length === 0) return yield* new ProjectionStoreThreadNotFoundError({ threadId });
4440 });
4441 const controlReadError = (threadId: ThreadId) => (cause: unknown) =>
4442 isProjectionStoreThreadNotFoundError(cause)
4443 ? cause
4444 : new ProjectionStoreReadError({ threadId, cause });
4445
4446 const getPendingNativeUserInputs: ProjectionStoreV2Shape["getPendingNativeUserInputs"] = (
4447 threadId,
4448 providerTurnId,
4449 ) =>
4450 sql
4451 .withTransaction(
4452 Effect.gen(function* () {
4453 const rows = yield* sql<PayloadRow>`
4454 SELECT payload_json FROM orchestration_v2_projection_runtime_requests
4455 WHERE thread_id = ${threadId} AND provider_turn_id = ${providerTurnId}
4456 AND kind = 'user_input' AND status = 'pending'
4457 `;
4458 const runtimeRequests = (yield* decodeRows(
4459 decodeRuntimeRequestPayload,
4460 threadId,
4461 )(rows)).filter((request) => request.responseCapability.type !== "message");
4462 if (runtimeRequests.length === 0) return { runtimeRequests, nodes: [], turnItems: [] };
4463 const nodeRows = yield* sql<PayloadRow>`
4464 SELECT payload_json FROM orchestration_v2_projection_nodes
4465 WHERE thread_id = ${threadId} AND ${sql.in(
4466 "node_id",
4467 runtimeRequests.map((request) => request.nodeId),
4468 )}
4469 `;
4470 const itemRows = yield* sql<PayloadRow>`
4471 SELECT payload_json FROM orchestration_v2_projection_turn_items
4472 WHERE thread_id = ${threadId} AND provider_turn_id = ${providerTurnId}
4473 AND type = 'user_input_request'
4474 `;
4475 const requestIds = new Set(runtimeRequests.map((request) => request.id));
4476 return {
4477 runtimeRequests,
4478 nodes: yield* decodeRows(decodeNodePayload, threadId)(nodeRows),
4479 turnItems: (yield* decodeRows(decodeTurnItemPayload, threadId)(itemRows)).filter(
4480 (item) => item.type === "user_input_request" && requestIds.has(item.requestId),
4481 ),
4482 };
4483 }),
4484 )
4485 .pipe(Effect.mapError(controlReadError(threadId)));
4486
4487 const getPlan: ProjectionStoreV2Shape["getPlan"] = (threadId, planId) =>
4488 sql
4489 .withTransaction(
4490 Effect.gen(function* () {
4491 yield* requireThread(threadId);
4492 const rows =
4493 yield* sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_plans
4494 WHERE thread_id = ${threadId} AND plan_id = ${planId}`;
4495 return rows[0] === undefined
4496 ? undefined
4497 : yield* decodePlanPayload(rows[0].payload_json);
4498 }),
4499 )
4500 .pipe(Effect.mapError(controlReadError(threadId)));
4501
4502 const hasUnpairedRunInterruptRequest: ProjectionStoreV2Shape["hasUnpairedRunInterruptRequest"] =
4503 (threadId, requestId, resultId) =>
4504 Effect.gen(function* () {
4505 const rows = yield* sql<{ pending: number }>`
4506 SELECT EXISTS (
4507 SELECT 1 FROM orchestration_v2_projection_turn_items
4508 WHERE thread_id = ${threadId} AND turn_item_id = ${requestId}
4509 ) AND NOT EXISTS (
4510 SELECT 1 FROM orchestration_v2_projection_turn_items
4511 WHERE thread_id = ${threadId} AND turn_item_id = ${resultId}
4512 ) AS pending
4513 `;
4514 return rows[0]?.pending === 1;
4515 }).pipe(Effect.mapError(controlReadError(threadId)));
4516
4517 const isNodeStored: ProjectionStoreV2Shape["isNodeStored"] = (node) =>
4518 Effect.gen(function* () {
4519 const payloadJson = yield* encodeNodePayload(node);
4520 const rows = yield* sql<PayloadRow>`
4521 SELECT payload_json FROM orchestration_v2_projection_nodes WHERE node_id = ${node.id}
4522 `;
4523 return rows[0]?.payload_json === payloadJson;
4524 }).pipe(Effect.mapError(controlReadError(node.threadId)));
4525
4526 const getRuntimeRequest: ProjectionStoreV2Shape["getRuntimeRequest"] = (threadId, requestId) =>
4527 sql
4528 .withTransaction(
4529 Effect.gen(function* () {
4530 yield* requireThread(threadId);
4531 const rows = yield* sql<PayloadRow>`
4532 SELECT payload_json FROM orchestration_v2_projection_runtime_requests
4533 WHERE thread_id = ${threadId} AND runtime_request_id = ${requestId}
4534 `;
4535 return rows[0] === undefined
4536 ? undefined
4537 : yield* decodeRuntimeRequestPayload(rows[0].payload_json);
4538 }),
4539 )
4540 .pipe(Effect.mapError(controlReadError(threadId)));
4541
4542 const getProviderControlContext: ProjectionStoreV2Shape["getProviderControlContext"] = (
4543 threadId,
4544 target,
4545 ) =>
4546 sql
4547 .withTransaction(
4548 Effect.gen(function* () {
4549 yield* requireThread(threadId);
4550 // Match the full projection's ownership scope, but fetch only the target
4551 // records. Transcript items and fork ancestors are never read here.
4552 const [threadRows, turnRows, attemptRows, messageRows] = yield* Effect.all([
4553 sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_provider_threads
4554 WHERE provider_thread_id = ${target.providerThreadId} AND (
4555 thread_id = ${threadId}
4556 OR owner_node_id IN (SELECT node_id FROM orchestration_v2_projection_nodes WHERE thread_id = ${threadId})
4557 OR provider_thread_id IN (SELECT provider_thread_id FROM orchestration_v2_projection_subagents WHERE thread_id = ${threadId})
4558 )`,
4559 sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_provider_turns
4560 WHERE thread_id = ${threadId} AND provider_turn_id = ${target.providerTurnId}`,
4561 target.attemptId === undefined
4562 ? Effect.succeed([] as ReadonlyArray<PayloadRow>)
4563 : sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_run_attempts
4564 WHERE thread_id = ${threadId} AND attempt_id = ${target.attemptId}`,
4565 target.messageId === undefined
4566 ? Effect.succeed([] as ReadonlyArray<PayloadRow>)
4567 : sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_messages
4568 WHERE thread_id = ${threadId} AND message_id = ${target.messageId}`,
4569 ]);
4570 const providerThread =
4571 threadRows[0] === undefined
4572 ? undefined
4573 : yield* decodeProviderThreadPayload(threadRows[0].payload_json);
4574 const providerTurn =
4575 turnRows[0] === undefined
4576 ? undefined
4577 : yield* decodeProviderTurnPayload(turnRows[0].payload_json);
4578 const attempt =
4579 attemptRows[0] === undefined
4580 ? undefined
4581 : yield* decodeRunAttemptPayload(attemptRows[0].payload_json);
4582 const message =
4583 messageRows[0] === undefined
4584 ? undefined
4585 : yield* decodeMessagePayload(messageRows[0].payload_json);
4586 let run: OrchestrationV2Run | undefined;
4587 if (target.messageId !== undefined && providerTurn !== undefined) {
4588 const rows =
4589 yield* sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_runs
4590 WHERE thread_id = ${threadId} AND json_extract(payload_json, '$.activeAttemptId') IS ${providerTurn.runAttemptId}
4591 ORDER BY ordinal ASC LIMIT 1`;
4592 if (rows[0] !== undefined) run = yield* decodeRunPayload(rows[0].payload_json);
4593 }
4594 return { providerThread, providerTurn, attempt, message, run };
4595 }),
4596 )
4597 .pipe(Effect.mapError(controlReadError(threadId)));
4598
4599 const getRunningTurnContext: ProjectionStoreV2Shape["getRunningTurnContext"] = (threadId) =>
4600 sql
4601 .withTransaction(
4602 Effect.gen(function* () {
4603 yield* requireThread(threadId);
4604 const rows =
4605 yield* sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_runs
4606 WHERE thread_id = ${threadId} AND status = 'running' ORDER BY ordinal ASC LIMIT 1`;
4607 const run =
4608 rows[0] === undefined ? undefined : yield* decodeRunPayload(rows[0].payload_json);
4609 if (run === undefined)
4610 return { run, providerThread: undefined, providerTurn: undefined };
4611 const threadRows =
4612 yield* sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_provider_threads WHERE provider_thread_id = ${run.providerThreadId}`;
4613 const turnRows =
4614 yield* sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_provider_turns
4615 WHERE provider_thread_id = ${run.providerThreadId}
4616 AND run_attempt_id = ${run.activeAttemptId}
4617 AND node_id = ${run.rootNodeId}
4618 AND status = 'running'`;
4619 return {
4620 run,
4621 providerThread:
4622 threadRows[0] === undefined
4623 ? undefined
4624 : yield* decodeProviderThreadPayload(threadRows[0].payload_json),
4625 providerTurn:
4626 turnRows[0] === undefined
4627 ? undefined
4628 : yield* decodeProviderTurnPayload(turnRows[0].payload_json),
4629 };
4630 }),
4631 )
4632 .pipe(Effect.mapError(controlReadError(threadId)));
4633
4634 const getThreadProviderContext: ProjectionStoreV2Shape["getThreadProviderContext"] = (
4635 threadId,
4636 targetInstanceId,
4637 ) =>
4638 sql
4639 .withTransaction(
4640 Effect.gen(function* () {
4641 const thread = yield* getThread(threadId);
4642 const sessionRows = yield* sql<PayloadRow>`
4643 SELECT session.payload_json FROM orchestration_v2_projection_provider_sessions AS session
4644 JOIN orchestration_v2_projection_provider_session_bindings AS binding
4645 ON binding.provider_session_id = session.provider_session_id
4646 WHERE binding.thread_id = ${threadId}
4647 ORDER BY session.updated_at ASC, session.provider_session_id ASC
4648 `;
4649 const threadRows =
4650 targetInstanceId === undefined
4651 ? []
4652 : yield* sql<PayloadRow>`
4653 SELECT payload_json FROM orchestration_v2_projection_provider_threads
4654 WHERE provider_thread_id = ${thread.activeProviderThreadId}
4655 OR provider_thread_id = (
4656 SELECT provider_thread_id FROM orchestration_v2_projection_provider_threads
4657 WHERE thread_id = ${threadId} AND owner_node_id IS NULL
4658 AND provider_instance_id = ${targetInstanceId}
4659 ORDER BY updated_at DESC, provider_thread_id ASC LIMIT 1
4660 )
4661 ORDER BY updated_at ASC, provider_thread_id ASC
4662 `;
4663 return {
4664 thread,
4665 providerSessions: yield* decodeRows(
4666 decodeProviderSessionPayload,
4667 threadId,
4668 )(sessionRows),
4669 providerThreads: yield* decodeRows(decodeProviderThreadPayload, threadId)(threadRows),
4670 };
4671 }),
4672 )
4673 .pipe(Effect.mapError(controlReadError(threadId)));
4674
4675 const getRuntimeResponseContext: ProjectionStoreV2Shape["getRuntimeResponseContext"] = (
4676 threadId,
4677 requestId,
4678 ) =>
4679 sql
4680 .withTransaction(
4681 Effect.gen(function* () {
4682 yield* requireThread(threadId);
4683 const request = yield* getRuntimeRequest(threadId, requestId);
4684 if (request === undefined)
4685 return { request, node: undefined, item: undefined, session: undefined };
4686 const nodeRows =
4687 yield* sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_nodes
4688 WHERE thread_id = ${threadId} AND node_id = ${request.nodeId}`;
4689 const itemRows =
4690 yield* sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_turn_items
4691 WHERE thread_id = ${threadId} AND node_id = ${request.nodeId} AND type IN ('approval_request', 'user_input_request')
4692 AND json_extract(payload_json, '$.requestId') = ${requestId}
4693 ORDER BY ordinal ASC LIMIT 1`;
4694 const sessionRows =
4695 request.responseCapability.type !== "live"
4696 ? []
4697 : yield* sql<PayloadRow>`
4698 SELECT session.payload_json FROM orchestration_v2_projection_provider_sessions AS session
4699 JOIN orchestration_v2_projection_provider_session_bindings AS binding
4700 ON binding.provider_session_id = session.provider_session_id
4701 WHERE binding.thread_id = ${threadId} AND session.provider_session_id = ${request.responseCapability.providerSessionId}`;
4702 return {
4703 request,
4704 node:
4705 nodeRows[0] === undefined
4706 ? undefined
4707 : yield* decodeNodePayload(nodeRows[0].payload_json),
4708 item:
4709 itemRows[0] === undefined
4710 ? undefined
4711 : yield* decodeTurnItemPayload(itemRows[0].payload_json),
4712 session:
4713 sessionRows[0] === undefined
4714 ? undefined
4715 : yield* decodeProviderSessionPayload(sessionRows[0].payload_json),
4716 };
4717 }),
4718 )
4719 .pipe(Effect.mapError(controlReadError(threadId)));
4720
4721 const getCheckpointContext: ProjectionStoreV2Shape["getCheckpointContext"] = (threadId) =>
4722 sql
4723 .withTransaction(
4724 Effect.gen(function* () {
4725 const threads = yield* sql<{ readonly thread_id: string }>`
4726 SELECT thread_id FROM orchestration_v2_projection_threads
4727 WHERE thread_id = ${threadId} LIMIT 1
4728 `;
4729 if (threads.length === 0) {
4730 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
4731 }
4732 // Checkpoint diffs need no transcript or fork ancestry. Select just the
4733 // metadata columns so large run/checkpoint JSON payloads stay in SQLite.
4734 const [runs, checkpointScopes, checkpoints] = yield* Effect.all([
4735 sql`
4736 SELECT run_id AS id, ordinal, status
4737 FROM orchestration_v2_projection_runs
4738 WHERE thread_id = ${threadId}
4739 ORDER BY ordinal ASC
4740 `,
4741 sql`
4742 SELECT scope_id AS id, run_id AS "runId", kind,
4743 json_extract(payload_json, '$.cwd') AS cwd
4744 FROM orchestration_v2_projection_checkpoint_scopes
4745 WHERE thread_id = ${threadId}
4746 ORDER BY ordinal_within_parent ASC, scope_id ASC
4747 `,
4748 sql`
4749 SELECT scope_id AS "scopeId", run_id AS "runId",
4750 app_run_ordinal AS "appRunOrdinal", status,
4751 json_extract(payload_json, '$.ref') AS ref
4752 FROM orchestration_v2_projection_checkpoints
4753 WHERE thread_id = ${threadId}
4754 ORDER BY scope_id ASC, ordinal_within_scope ASC
4755 `,
4756 ]);
4757 return yield* decodeCheckpointContext({ runs, checkpointScopes, checkpoints });
4758 }),
4759 )
4760 .pipe(
4761 Effect.mapError((cause) =>
4762 isProjectionStoreThreadNotFoundError(cause)
4763 ? cause
4764 : new ProjectionStoreReadError({ threadId, cause }),
4765 ),
4766 );
4767
4768 const getCheckpointCaptureContext: ProjectionStoreV2Shape["getCheckpointCaptureContext"] = (
4769 threadId,
4770 target,
4771 ) =>
4772 sql
4773 .withTransaction(
4774 Effect.gen(function* () {
4775 yield* requireThread(threadId);
4776 const runRows = yield* sql<PayloadRow>`
4777 SELECT payload_json FROM orchestration_v2_projection_runs
4778 WHERE thread_id = ${threadId} AND run_id = ${target.runId}`;
4779 const run =
4780 runRows[0] === undefined
4781 ? undefined
4782 : yield* decodeRunPayload(runRows[0].payload_json);
4783 const [nodeRows, scopeRows, providerRows, readyRows] = yield* Effect.all([
4784 sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_nodes
4785 WHERE thread_id = ${threadId} AND node_id = ${run?.rootNodeId ?? null}`,
4786 sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_checkpoint_scopes
4787 WHERE thread_id = ${threadId} AND scope_id = ${target.scopeId}`,
4788 sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_provider_threads
4789 WHERE provider_thread_id = ${run?.providerThreadId ?? null}
4790 AND (thread_id = ${threadId}
4791 OR owner_node_id IN (SELECT node_id FROM orchestration_v2_projection_nodes WHERE thread_id = ${threadId})
4792 OR provider_thread_id IN (SELECT provider_thread_id FROM orchestration_v2_projection_subagents WHERE thread_id = ${threadId}))`,
4793 sql<{ readonly ordinal: number }>`SELECT ordinal_within_scope AS ordinal
4794 FROM orchestration_v2_projection_checkpoints
4795 WHERE thread_id = ${threadId} AND scope_id = ${target.scopeId} AND status = 'ready'`,
4796 ]);
4797 return {
4798 run,
4799 rootNode:
4800 nodeRows[0] === undefined
4801 ? undefined
4802 : yield* decodeNodePayload(nodeRows[0].payload_json),
4803 scope:
4804 scopeRows[0] === undefined
4805 ? undefined
4806 : yield* decodeCheckpointScopePayload(scopeRows[0].payload_json),
4807 providerThread:
4808 providerRows[0] === undefined
4809 ? undefined
4810 : yield* decodeProviderThreadPayload(providerRows[0].payload_json),
4811 readyCheckpointOrdinals: readyRows.map(({ ordinal }) => ordinal),
4812 };
4813 }),
4814 )
4815 .pipe(Effect.mapError(controlReadError(threadId)));
4816
4817 const getRunMessage: ProjectionStoreV2Shape["getRunMessage"] = (threadId, runId) =>
4818 sql
4819 .withTransaction(
4820 Effect.gen(function* () {
4821 yield* requireThread(threadId);
4822 const rows = yield* sql<PayloadRow>`
4823 SELECT message.payload_json FROM orchestration_v2_projection_messages AS message
4824 JOIN orchestration_v2_projection_runs AS run
4825 ON message.message_id = json_extract(run.payload_json, '$.userMessageId')
4826 WHERE run.thread_id = ${threadId} AND run.run_id = ${runId}
4827 AND message.thread_id = ${threadId}`;
4828 return rows[0] === undefined
4829 ? undefined
4830 : yield* decodeMessagePayload(rows[0].payload_json);
4831 }),
4832 )
4833 .pipe(Effect.mapError(controlReadError(threadId)));
4834
4835 const canStartQueuedRun: ProjectionStoreV2Shape["canStartQueuedRun"] = (threadId) =>
4836 sql
4837 .withTransaction(
4838 Effect.gen(function* () {
4839 yield* requireThread(threadId);
4840 const rows = yield* sql<{ readonly ready: number }>`
4841 SELECT 1 AS ready FROM orchestration_v2_projection_threads AS thread
4842 WHERE thread.thread_id = ${threadId} AND thread.archived_at IS NULL AND thread.deleted_at IS NULL
4843 AND EXISTS (SELECT 1 FROM orchestration_v2_projection_runs
4844 WHERE thread_id = ${threadId} AND status = 'queued')
4845 AND NOT EXISTS (SELECT 1 FROM orchestration_v2_projection_runs
4846 WHERE thread_id = ${threadId}
4847 AND (status IN ('preparing', 'starting', 'running', 'waiting')
4848 OR (status = 'queued' AND json_extract(payload_json, '$.queueHeld') = 1)))`;
4849 return rows.length > 0;
4850 }),
4851 )
4852 .pipe(Effect.mapError(controlReadError(threadId)));
4853
4854 const getMessageCount: ProjectionStoreV2Shape["getMessageCount"] = (threadId) =>
4855 sql<{
4856 count: number;
4857 }>`SELECT COUNT(*) AS count FROM orchestration_v2_projection_messages WHERE thread_id = ${threadId}`.pipe(
4858 Effect.map((rows) => rows[0]?.count ?? 0),
4859 Effect.mapError(controlReadError(threadId)),
4860 );
4861 const getNextTurnItemOrdinal: ProjectionStoreV2Shape["getNextTurnItemOrdinal"] = (threadId) =>
4862 sql<{ ordinal: number | null }>`SELECT MAX(ordinal) AS ordinal
4863 FROM orchestration_v2_projection_turn_items WHERE thread_id = ${threadId}`.pipe(
4864 Effect.map((rows) => (rows[0]?.ordinal ?? 0) + 1),
4865 Effect.mapError(controlReadError(threadId)),
4866 );
4867
4868 const getTurnItem: ProjectionStoreV2Shape["getTurnItem"] = ({ threadId, itemId }) =>
4869 sql<{ payload_json: string }>`SELECT payload_json
4870 FROM orchestration_v2_projection_turn_items
4871 WHERE turn_item_id = ${itemId} AND thread_id = ${threadId}`.pipe(
4872 Effect.flatMap((rows) =>
4873 rows[0] === undefined
4874 ? Effect.succeed(null)
4875 : decodeTurnItemPayload(rows[0].payload_json),
4876 ),
4877 Effect.mapError(controlReadError(threadId)),
4878 );
4879
4880 const getThreadAttachmentIds: ProjectionStoreV2Shape["getThreadAttachmentIds"] = (threadId) =>
4881 Effect.all([
4882 sql<{ id: string }>`
4883 SELECT DISTINCT json_extract(attachment.value, '$.id') AS id
4884 FROM orchestration_v2_projection_messages AS message,
4885 json_each(message.payload_json, '$.attachments') AS attachment
4886 WHERE message.thread_id = ${threadId}
4887 `,
4888 // Pages published by html_render, and captured MCP App documents,
4889 // live in the attachment store too.
4890 sql<{ payload_json: string }>`
4891 SELECT payload_json
4892 FROM orchestration_v2_projection_turn_items
4893 WHERE thread_id = ${threadId}
4894 AND type = 'dynamic_tool'
4895 AND (payload_json LIKE '%htmlRender%' OR payload_json LIKE ${`%${MCP_APP_OUTPUT_KEY}%`})
4896 `,
4897 ]).pipe(
4898 Effect.map(([messages, renders]) => [
4899 ...new Set([
4900 ...messages.map((row) => row.id),
4901 ...threadHtmlRenderAttachmentIds(
4902 threadId,
4903 renders.map((row) => {
4904 const item = parseEncodedPayload(row.payload_json);
4905 return {
4906 toolName: typeof item.toolName === "string" ? item.toolName : null,
4907 output: item.output,
4908 };
4909 }),
4910 ),
4911 ]),
4912 ]),
4913 Effect.mapError((cause) => new ProjectionStoreReadError({ threadId, cause })),
4914 );
4915
4916 const getThreadRecords: ProjectionStoreV2Shape["getThreadRecords"] = (
4917 threadId,
4918 fields,
4919 filter,
4920 ) =>
4921 sql.withTransaction(readCanonicalProjection(threadId, undefined, fields, filter)).pipe(
4922 Effect.map(
4923 (projection) =>
4924 Object.fromEntries([
4925 ["thread", projection.thread],
4926 ...fields.map((field) => [field, projection[field]]),
4927 ]) as ProjectionRecords<(typeof fields)[number]>,
4928 ),
4929 Effect.mapError(controlReadError(threadId)),
4930 );
4931
4932 const getThreadSnapshot: ProjectionStoreV2Shape["getThreadSnapshot"] = (threadId) =>
4933 sql
4934 .withTransaction(
4935 Effect.gen(function* () {
4936 const projection = yield* getThreadProjection(threadId);
4937 const rows = yield* sql<{ readonly snapshot_sequence: number | null }>`
4938 SELECT MAX(sequence) AS snapshot_sequence
4939 FROM orchestration_events
4940 WHERE application_event_version = 2
4941 AND aggregate_kind = 'thread'
4942 AND stream_id = ${threadId}
4943 `;
4944 return {
4945 schemaVersion: ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION,
4946 snapshotSequence: rows[0]?.snapshot_sequence ?? 0,
4947 projection,
4948 };
4949 }),
4950 )
4951 .pipe(
4952 Effect.mapError((cause) =>
4953 isProjectionStoreThreadNotFoundError(cause) || isProjectionStoreReadError(cause)
4954 ? cause
4955 : new ProjectionStoreReadError({ threadId, cause }),
4956 ),
4957 );
4958
4959 // Only the timeline index is assembled across fork ancestry. Payloads are read
4960 // after page selection, so history and MCP pages hydrate only the requested rows.
4961 type TimelineIndexItem = Pick<
4962 OrchestrationV2TurnItem,
4963 "id" | "threadId" | "runId" | "nodeId" | "type"
4964 > & {
4965 readonly inputIntent?: Extract<
4966 OrchestrationV2TurnItem,
4967 { type: "user_message" }
4968 >["inputIntent"];
4969 readonly createdBy?: "user" | "agent" | "system";
4970 readonly messageId?: MessageId;
4971 };
4972 type TimelineIndexRow = {
4973 readonly sourceThreadId: ThreadId;
4974 readonly sourceItemId: TurnItemId;
4975 readonly visibility: OrchestrationV2ProjectedTurnItem["visibility"];
4976 readonly item: TimelineIndexItem;
4977 readonly synthetic?: OrchestrationV2TurnItem;
4978 };
4979 const readTimelineIndex = (
4980 threadId: ThreadId,
4981 seen: ReadonlySet<ThreadId>,
4982 ): Effect.Effect<
4983 {
4984 readonly records: ProjectionRecords<"runs" | "attempts">;
4985 readonly local: ReadonlyArray<TimelineIndexRow>;
4986 readonly visible: ReadonlyArray<TimelineIndexRow>;
4987 },
4988 ProjectionStoreV2Error
4989 > =>
4990 Effect.gen(function* () {
4991 const records = yield* getThreadRecords(threadId, ["runs", "attempts"]);
4992 const rows = yield* sql<{
4993 turn_item_id: string;
4994 run_id: string | null;
4995 node_id: string | null;
4996 type: OrchestrationV2TurnItem["type"];
4997 input_intent: NonNullable<TimelineIndexItem["inputIntent"]> | null;
4998 created_by: "user" | "agent" | "system" | null;
4999 message_id: string | null;
5000 }>`SELECT turn_item_id, run_id, node_id, type,
5001 CASE WHEN type = 'user_message' THEN json_extract(payload_json, '$.inputIntent') END AS input_intent,
5002 CASE WHEN type = 'user_message' THEN json_extract(payload_json, '$.createdBy') END AS created_by,
5003 CASE WHEN type IN ('user_message', 'assistant_message') THEN json_extract(payload_json, '$.messageId') END AS message_id
5004 FROM orchestration_v2_projection_turn_items WHERE thread_id = ${threadId}
5005 ORDER BY ordinal ASC, turn_item_id ASC`;
5006 const items: Array<TimelineIndexItem> = rows.map((row) => ({
5007 id: TurnItemId.make(row.turn_item_id),
5008 threadId,
5009 runId: row.run_id === null ? null : RunId.make(row.run_id),
5010 nodeId: row.node_id === null ? null : NodeId.make(row.node_id),
5011 type: row.type,
5012 ...(row.input_intent === null ? {} : { inputIntent: row.input_intent }),
5013 ...(row.created_by === null ? {} : { createdBy: row.created_by }),
5014 ...(row.message_id === null ? {} : { messageId: MessageId.make(row.message_id) }),
5015 }));
5016 const local: Array<TimelineIndexRow> = items.map((item) => ({
5017 sourceThreadId: threadId,
5018 sourceItemId: item.id,
5019 visibility: "local",
5020 item,
5021 }));
5022 const isVisible = createOrchestrationV2TurnItemVisibility({
5023 runs: records.runs,
5024 attempts: records.attempts,
5025 items,
5026 });
5027 const visible = local.filter((row) => isVisible(row.item));
5028 const fork = records.thread.forkedFrom;
5029 if (fork?.type !== "run" || seen.has(fork.threadId)) return { records, local, visible };
5030 const source = yield* readTimelineIndex(fork.threadId, new Set([...seen, threadId]));
5031 const sourceRun = source.records.runs.find((run) => run.id === fork.runId);
5032 let inherited: Array<TimelineIndexRow> = [];
5033 if (sourceRun !== undefined) {
5034 const ordinals = new Map(source.records.runs.map((run) => [run.id, run.ordinal]));
5035 const sourceItems = source.local.map((row) => row.item);
5036 inherited = [
5037 ...source.visible.filter(
5038 (row) => row.item.threadId !== fork.threadId || row.item.type === "fork",
5039 ),
5040 ...source.local.filter(
5041 (row) =>
5042 !isOrchestrationV2SupersededInterrupt({
5043 item: row.item,
5044 attempts: source.records.attempts,
5045 items: sourceItems,
5046 }) &&
5047 isTurnItemAtOrBeforeRun({
5048 historyOrigin: source.records.thread.historyOrigin,
5049 itemRunId: row.item.runId,
5050 runOrdinalById: ordinals,
5051 sourceRunOrdinal: sourceRun.ordinal,
5052 }),
5053 ),
5054 ].map((row) => ({ ...row, visibility: "inherited" }));
5055 }
5056 const marker = makeForkMarkerTurnItem({
5057 targetProjection: records,
5058 sourceThreadId: fork.threadId,
5059 sourceRunId: fork.runId,
5060 });
5061 return {
5062 records,
5063 local,
5064 visible: [
5065 ...inherited,
5066 {
5067 sourceThreadId: fork.threadId,
5068 sourceItemId: marker.id,
5069 visibility: "synthetic" as const,
5070 item: marker,
5071 synthetic: marker,
5072 },
5073 ...visible,
5074 ],
5075 };
5076 }).pipe(
5077 Effect.mapError((cause) =>
5078 isProjectionStoreThreadNotFoundError(cause) || isProjectionStoreReadError(cause)
5079 ? cause
5080 : new ProjectionStoreReadError({ threadId, cause }),
5081 ),
5082 );
5083
5084 const { searchThread, searchThreadStream } = yield* makeThreadFind((threadId) =>
5085 readTimelineIndex(threadId, new Set()).pipe(Effect.map((index) => index.visible)),
5086 );
5087
5088 const getThreadHistoryPage: ProjectionStoreV2Shape["getThreadHistoryPage"] = (
5089 threadId,
5090 cursor,
5091 throughEntryId,
5092 conversationOnly,
5093 ) =>
5094 sql
5095 .withTransaction(
5096 Effect.gen(function* () {
5097 const index = yield* readTimelineIndex(threadId, new Set());
5098 if (index.records.thread.deletedAt !== null)
5099 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
5100 const revisions = yield* sql<{
5101 sequence: number;
5102 }>`SELECT COALESCE(MAX(sequence), 0) AS sequence
5103 FROM orchestration_events WHERE application_event_version = 2
5104 AND aggregate_kind = 'thread' AND stream_id = ${threadId}`;
5105 const snapshotSequence = revisions[0]?.sequence ?? 0;
5106 // Choose complete turns from the light index before loading any bodies.
5107 // A history page does not need nodes, provider turns, or other control state.
5108 const page = yield* Effect.try({
5109 try: () =>
5110 selectHistoryPageFromCursor({
5111 items: index.visible.map((row, position) => ({
5112 ...row,
5113 position,
5114 canonicalPosition: position,
5115 })),
5116 cursor,
5117 snapshotSequence,
5118 rowEncodedBytes: () => 0,
5119 throughEntryId,
5120 }),
5121 catch: (cause) => new ProjectionStoreReadError({ threadId, cause }),
5122 });
5123 const end = (page.items.at(-1)?.canonicalPosition ?? -1) + 1;
5124 const hasUserTurns = index.visible
5125 .slice(0, end)
5126 .some((row) => isThreadHistoryUserTurn(row.item));
5127 const materialized =
5128 conversationOnly && hasUserTurns
5129 ? page.items.filter((row) => isConversationHistoryItem(row.item))
5130 : page.items;
5131 const sources = yield* encodeTimelineSources(
5132 materialized
5133 .filter((row) => !row.synthetic)
5134 .map((row) => ({ threadId: row.sourceThreadId, id: row.sourceItemId })),
5135 );
5136 const payloads = yield* sql<{
5137 thread_id: string;
5138 turn_item_id: string;
5139 payload_json: string;
5140 }>`
5141 SELECT item.thread_id, item.turn_item_id, item.payload_json
5142 FROM orchestration_v2_projection_turn_items item JOIN json_each(${sources}) wanted
5143 ON item.thread_id = json_extract(wanted.value, '$.threadId')
5144 AND item.turn_item_id = json_extract(wanted.value, '$.id')`;
5145 const byIdentity = new Map(
5146 payloads.map((row) => [`${row.thread_id}:${row.turn_item_id}`, row.payload_json]),
5147 );
5148 const items = yield* Effect.forEach(materialized, (row) =>
5149 Effect.gen(function* () {
5150 const payload = byIdentity.get(`${row.sourceThreadId}:${row.sourceItemId}`);
5151 const item =
5152 row.synthetic ??
5153 (payload === undefined
5154 ? yield* new ProjectionStoreReadError({ threadId })
5155 : yield* decodeTurnItemPayload(payload));
5156 return {
5157 position: row.canonicalPosition,
5158 visibility: row.visibility,
5159 sourceThreadId: row.sourceThreadId,
5160 sourceItemId: row.sourceItemId,
5161 item: projectTurnItemForWire(item),
5162 };
5163 }),
5164 );
5165 if (hasUserTurns)
5166 return {
5167 ...page,
5168 snapshotSequence,
5169 items: items.map((row, position) => ({ ...row, position })),
5170 };
5171 // The no-turn fallback also respects the encoded byte budget after projection.
5172 const bounded = selectRecentTimelineWindow({
5173 items,
5174 snapshotSequence,
5175 policy: {
5176 ...THREAD_HISTORY_PAGE_POLICY,
5177 maxUserTurns: OLDER_THREAD_USER_TURN_LIMIT,
5178 maxItems:
5179 THREAD_HISTORY_PAGE_POLICY.maxItems * (throughEntryId === undefined ? 1 : 2),
5180 },
5181 });
5182 return {
5183 snapshotSequence,
5184 items: conversationOnly
5185 ? bounded.items.filter((row) => isConversationHistoryItem(row.item))
5186 : bounded.items,
5187 nextCursor: bounded.hasMoreHistory ? bounded.nextCursor : page.nextCursor,
5188 hasMoreHistory: bounded.hasMoreHistory || page.hasMoreHistory,
5189 };
5190 }),
5191 )
5192 .pipe(Effect.mapError(controlReadError(threadId)));
5193
5194 const getTimelinePage: ProjectionStoreV2Shape["getTimelinePage"] = (threadId, options) =>
5195 sql
5196 .withTransaction(
5197 Effect.gen(function* () {
5198 const index = yield* readTimelineIndex(threadId, new Set());
5199 const matching = index.visible
5200 .map((row, position) => ({ ...row, position }))
5201 .filter((row) =>
5202 options.itemId === undefined
5203 ? row.position > (options.afterPosition ?? -1) &&
5204 (options.view === "activity" ||
5205 ["user_message", "assistant_message", "proposed_plan"].includes(
5206 row.item.type,
5207 ))
5208 : row.sourceItemId === options.itemId,
5209 );
5210 const page = matching.slice(0, options.limit);
5211 const items = yield* Effect.forEach(page, (row) =>
5212 Effect.gen(function* () {
5213 if (row.synthetic !== undefined)
5214 return {
5215 position: row.position,
5216 sourceThreadId: row.sourceThreadId,
5217 sourceItemId: row.sourceItemId,
5218 visibility: row.visibility,
5219 item: row.synthetic,
5220 };
5221 const payloads =
5222 yield* sql<PayloadRow>`SELECT payload_json FROM orchestration_v2_projection_turn_items
5223 WHERE thread_id = ${row.sourceThreadId} AND turn_item_id = ${row.sourceItemId}`;
5224 const decoded = yield* decodeRows(
5225 decodeTurnItemPayload,
5226 row.sourceThreadId,
5227 )(payloads);
5228 const item = decoded[0];
5229 if (item === undefined)
5230 return yield* new ProjectionStoreReadError({
5231 threadId,
5232 cause: "Timeline item disappeared during snapshot",
5233 });
5234 return {
5235 position: row.position,
5236 sourceThreadId: row.sourceThreadId,
5237 sourceItemId: row.sourceItemId,
5238 visibility: row.visibility,
5239 item,
5240 };
5241 }),
5242 );
5243 return {
5244 items,
5245 totalItems: index.visible.length,
5246 hasMore: matching.length > page.length,
5247 };
5248 }),
5249 )
5250 .pipe(
5251 Effect.mapError((cause) =>
5252 isProjectionStoreThreadNotFoundError(cause) || isProjectionStoreReadError(cause)
5253 ? cause
5254 : new ProjectionStoreReadError({ threadId, cause }),
5255 ),
5256 );
5257
5258 const getThreadSnapshotWindow: ProjectionStoreV2Shape["getThreadSnapshotWindow"] = (
5259 threadId,
5260 options,
5261 ) =>
5262 sql
5263 .withTransaction(
5264 Effect.gen(function* () {
5265 const historyAnchor =
5266 options.anchorItemId === undefined
5267 ? undefined
5268 : {
5269 itemId: options.anchorItemId,
5270 threadId:
5271 options.anchorThreadId ??
5272 (yield* sql<{ readonly thread_id: string }>`
5273 SELECT thread_id
5274 FROM orchestration_v2_projection_turn_items
5275 WHERE turn_item_id = ${options.anchorItemId}
5276 LIMIT 1
5277 `).map((row) => ThreadId.make(row.thread_id))[0] ??
5278 threadId,
5279 };
5280 const projection = yield* readProjection(threadId, new Set(), {
5281 rowLimit: options.rowLimit,
5282 userTurnLimit: options.userTurnLimit,
5283 ...(historyAnchor?.threadId === threadId
5284 ? { anchorItemId: historyAnchor.itemId }
5285 : {}),
5286 ...(historyAnchor === undefined ? {} : { historyAnchor }),
5287 });
5288 const rows = yield* sql<{ readonly snapshot_sequence: number | null }>`
5289 SELECT MAX(sequence) AS snapshot_sequence
5290 FROM orchestration_events
5291 WHERE application_event_version = 2
5292 AND aggregate_kind = 'thread'
5293 AND stream_id = ${threadId}
5294 `;
5295 return {
5296 schemaVersion: ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION,
5297 snapshotSequence: rows[0]?.snapshot_sequence ?? 0,
5298 projection,
5299 };
5300 }),
5301 )
5302 .pipe(
5303 Effect.mapError((cause) =>
5304 isProjectionStoreThreadNotFoundError(cause) || isProjectionStoreReadError(cause)
5305 ? cause
5306 : new ProjectionStoreReadError({ threadId, cause }),
5307 ),
5308 );
5309
5310 const selectShellThreadRows = (
5311 threadId?: ThreadId,
5312 {
5313 location,
5314 projectId,
5315 unsettledOnly = false,
5316 settledOnly = false,
5317 }: ShellSnapshotOptions = {},
5318 ) =>
5319 sql<ShellThreadRow>`
5320 SELECT
5321 t.thread_id,
5322 t.payload_json,
5323 CASE
5324 WHEN json_extract(t.payload_json, '$.forkedFrom.type') = 'run'
5325 THEN json_extract(t.payload_json, '$.forkedFrom.threadId')
5326 ELSE NULL
5327 END AS forked_from_run_source_thread_id,
5328 presented.run_id AS latest_run_id,
5329 presented.status AS latest_run_status,
5330 presented.requested_at AS latest_run_requested_at,
5331 json_extract(presented.payload_json, '$.startedAt') AS latest_run_started_at,
5332 presented.completed_at AS latest_run_completed_at,
5333 (
5334 SELECT r.run_id
5335 FROM orchestration_v2_projection_runs r
5336 WHERE r.thread_id = t.thread_id
5337 AND r.status IN ('preparing', 'starting', 'running')
5338 ORDER BY r.ordinal DESC, r.run_id DESC
5339 LIMIT 1
5340 ) AS active_run_id,
5341 (
5342 SELECT r.status
5343 FROM orchestration_v2_projection_runs r
5344 WHERE r.thread_id = t.thread_id
5345 AND r.status IN ('preparing', 'starting', 'running', 'waiting')
5346 ORDER BY r.ordinal DESC, r.run_id DESC
5347 LIMIT 1
5348 ) AS activity_run_status,
5349 (
5350 -- Mirrors orchestrationV2RunWorkStartedAt.
5351 SELECT COALESCE(
5352 json_extract(r.payload_json, '$.workStartedAt'),
5353 json_extract(r.payload_json, '$.startedAt'),
5354 r.requested_at
5355 )
5356 FROM orchestration_v2_projection_runs r
5357 WHERE r.thread_id = t.thread_id
5358 AND r.status IN ('preparing', 'starting', 'running', 'waiting')
5359 ORDER BY r.ordinal DESC, r.run_id DESC
5360 LIMIT 1
5361 ) AS activity_run_started_at,
5362 (
5363 SELECT json_extract(session.payload_json, '$.lastError')
5364 -- Keep bindings outermost: one provider instance can hold every session.
5365 FROM orchestration_v2_projection_provider_session_bindings binding
5366 CROSS JOIN orchestration_v2_projection_provider_sessions session
5367 ON session.provider_session_id = binding.provider_session_id
5368 WHERE binding.thread_id = t.thread_id
5369 AND session.provider_instance_id = t.provider_instance_id
5370 ORDER BY session.updated_at DESC, session.provider_session_id DESC
5371 LIMIT 1
5372 ) AS last_error,
5373 (
5374 SELECT item.payload_json
5375 FROM orchestration_v2_projection_turn_items item
5376 INNER JOIN orchestration_v2_projection_runs r ON r.run_id = item.run_id
5377 WHERE r.run_id = presented.run_id
5378 AND r.status = 'failed'
5379 AND item.thread_id = t.thread_id
5380 AND item.type = 'error' AND item.status = 'failed'
5381 AND item.node_id IS json_extract(r.payload_json, '$.rootNodeId')
5382 ORDER BY item.updated_at DESC, item.ordinal DESC, item.turn_item_id DESC
5383 LIMIT 1
5384 ) AS terminal_failure_payload_json,
5385 blocked.run_id AS blocking_run_id,
5386 blocked.requested_at AS blocking_run_requested_at,
5387 json_extract(blocked.payload_json, '$.startedAt') AS blocking_run_started_at,
5388 blocked.completed_at AS blocking_run_completed_at,
5389 (
5390 SELECT item.payload_json
5391 FROM orchestration_v2_projection_turn_items item
5392 WHERE item.thread_id = t.thread_id
5393 AND item.run_id = blocked.run_id
5394 AND item.type = 'error' AND item.status = 'failed'
5395 AND item.node_id IS json_extract(blocked.payload_json, '$.rootNodeId')
5396 ORDER BY item.updated_at DESC, item.ordinal DESC, item.turn_item_id DESC
5397 LIMIT 1
5398 ) AS blocking_failure_payload_json,
5399 (
5400 SELECT request.payload_json
5401 FROM orchestration_v2_projection_runtime_requests request
5402 WHERE request.thread_id = t.thread_id
5403 AND request.status = 'pending'
5404 ORDER BY request.created_at DESC, request.runtime_request_id DESC
5405 LIMIT 1
5406 ) AS pending_request_payload_json,
5407 (
5408 SELECT secret.payload_json
5409 -- Keep runs outermost so completed history is never scanned for a pending secret.
5410 FROM orchestration_v2_projection_runs r
5411 CROSS JOIN orchestration_v2_projection_turn_items secret
5412 INDEXED BY orchestration_v2_projection_turn_items_thread_run_idx
5413 WHERE r.thread_id = t.thread_id
5414 AND r.status IN ('preparing', 'starting', 'running', 'waiting')
5415 AND secret.thread_id = t.thread_id AND secret.run_id = r.run_id
5416 AND secret.type = 'secret_request' AND secret.status = 'waiting'
5417 ORDER BY secret.updated_at DESC, secret.turn_item_id DESC
5418 LIMIT 1
5419 ) AS pending_secret_request_payload_json,
5420 (
5421 SELECT message.updated_at
5422 FROM orchestration_v2_projection_messages message
5423 WHERE message.thread_id = t.thread_id
5424 AND message.role = 'user'
5425 ORDER BY message.updated_at DESC, message.message_id DESC
5426 LIMIT 1
5427 ) AS latest_user_message_at,
5428 (
5429 SELECT message.updated_at
5430 FROM orchestration_v2_projection_messages message
5431 WHERE message.thread_id = t.thread_id
5432 AND message.role = 'user'
5433 AND json_extract(message.payload_json, '$.createdBy') = 'user'
5434 ORDER BY message.updated_at DESC, message.message_id DESC
5435 LIMIT 1
5436 ) AS latest_user_authored_message_at,
5437 EXISTS (
5438 SELECT 1
5439 FROM orchestration_v2_projection_plans plan
5440 WHERE plan.thread_id = t.thread_id
5441 AND plan.kind = 'proposed_plan'
5442 AND plan.status = 'active'
5443 ) AS has_actionable_proposed_plan,
5444 -- Count per run on the covering (thread_id, run_id) index, then
5445 -- look up each run once, instead of one run lookup per item.
5446 (
5447 SELECT COALESCE(SUM(per_run.item_count), 0)
5448 FROM (
5449 SELECT i.run_id, COUNT(*) AS item_count
5450 FROM orchestration_v2_projection_turn_items i
5451 WHERE i.thread_id = t.thread_id
5452 GROUP BY i.run_id
5453 ) per_run
5454 LEFT JOIN orchestration_v2_projection_runs r
5455 ON r.run_id = per_run.run_id
5456 WHERE per_run.run_id IS NULL OR r.status <> 'rolled_back'
5457 ) AS item_count,
5458 (
5459 SELECT COUNT(*)
5460 FROM orchestration_v2_projection_turn_items i
5461 WHERE i.thread_id = t.thread_id
5462 AND i.run_id IS NULL
5463 ) AS runless_item_count
5464 FROM orchestration_v2_projection_threads t
5465 -- The newest run not waiting in a held queue, matching latestUnheldRun.
5466 LEFT JOIN orchestration_v2_projection_runs presented ON presented.run_id = (
5467 SELECT candidate.run_id
5468 FROM orchestration_v2_projection_runs candidate
5469 WHERE candidate.thread_id = t.thread_id
5470 AND NOT (
5471 candidate.status = 'queued'
5472 AND json_extract(candidate.payload_json, '$.queueHeld') IS 1
5473 )
5474 ORDER BY candidate.ordinal DESC, candidate.run_id DESC
5475 LIMIT 1
5476 )
5477 LEFT JOIN orchestration_v2_projection_runs blocked ON blocked.run_id = (
5478 SELECT candidate.run_id
5479 FROM orchestration_v2_projection_runs candidate
5480 WHERE candidate.thread_id = t.thread_id
5481 AND candidate.status <> 'queued'
5482 AND NOT (
5483 candidate.status = 'cancelled'
5484 AND json_extract(candidate.payload_json, '$.startedAt') IS NULL
5485 )
5486 -- latestExecutedRun: the run that ended last (runRanAfter).
5487 ORDER BY candidate.completed_at IS NULL DESC, candidate.completed_at DESC,
5488 candidate.ordinal DESC, candidate.run_id DESC
5489 LIMIT 1
5490 ) AND blocked.status = 'failed'
5491 WHERE t.deleted_at IS NULL${threadId === undefined ? sql`` : sql` AND t.thread_id = ${threadId}`}${
5492 projectId === undefined ? sql`` : sql` AND t.project_id = ${projectId}`
5493 }${
5494 location === "active"
5495 ? sql` AND json_extract(t.payload_json, '$.archivedAt') IS NULL`
5496 : location === "archive"
5497 ? sql` AND json_extract(t.payload_json, '$.archivedAt') IS NOT NULL`
5498 : sql``
5499 }${
5500 unsettledOnly
5501 ? sql` AND json_extract(t.payload_json, '$.settledAt') IS NULL AND json_extract(t.payload_json, '$.settledOverride') IS NOT 'settled'`
5502 : sql``
5503 }${
5504 settledOnly
5505 ? sql` AND json_extract(t.payload_json, '$.settledOverride') = 'settled' AND json_extract(t.payload_json, '$.settledAt') IS NOT NULL`
5506 : sql``
5507 }
5508 ORDER BY t.updated_at ASC, t.thread_id ASC
5509 `;
5510
5511 const selectShellRunRows = (threadIds?: ReadonlyArray<ThreadId>) =>
5512 threadIds === undefined
5513 ? sql<ShellRunRow>`
5514 SELECT thread_id, run_id, ordinal
5515 FROM orchestration_v2_projection_runs
5516 `
5517 : sql<ShellRunRow>`
5518 SELECT thread_id, run_id, ordinal
5519 FROM orchestration_v2_projection_runs
5520 WHERE thread_id IN ${sql.in(threadIds)}
5521 `;
5522
5523 const selectShellRunItemCounts = (threadIds?: ReadonlyArray<ThreadId>) =>
5524 threadIds === undefined
5525 ? sql<ShellRunItemCountRow>`
5526 SELECT thread_id, run_id, COUNT(*) AS item_count
5527 FROM orchestration_v2_projection_turn_items
5528 WHERE run_id IS NOT NULL
5529 GROUP BY thread_id, run_id
5530 `
5531 : sql<ShellRunItemCountRow>`
5532 SELECT thread_id, run_id, COUNT(*) AS item_count
5533 FROM orchestration_v2_projection_turn_items
5534 WHERE run_id IS NOT NULL
5535 AND thread_id IN ${sql.in(threadIds)}
5536 GROUP BY thread_id, run_id
5537 `;
5538
5539 const selectShellProviderThreadRows = (threadIds?: ReadonlyArray<ThreadId>) =>
5540 threadIds === undefined
5541 ? sql<PayloadRow & { readonly thread_id: string }>`
5542 SELECT thread_id, payload_json
5543 FROM orchestration_v2_projection_provider_threads
5544 WHERE thread_id IS NOT NULL
5545 `
5546 : sql<PayloadRow & { readonly thread_id: string }>`
5547 SELECT thread_id, payload_json
5548 FROM orchestration_v2_projection_provider_threads
5549 WHERE thread_id IN ${sql.in(threadIds)}
5550 `;
5551
5552 const selectShellPendingTurnItemRows = (threadIds?: ReadonlyArray<ThreadId>) =>
5553 threadIds === undefined
5554 ? sql<PayloadRow & { readonly thread_id: string }>`
5555 SELECT i.thread_id, i.payload_json
5556 FROM orchestration_v2_projection_turn_items i
5557 LEFT JOIN orchestration_v2_projection_runs r
5558 ON r.run_id = i.run_id
5559 WHERE i.type IN ('command_execution', 'dynamic_tool', 'subagent')
5560 AND i.status NOT IN ('completed', 'interrupted', 'failed', 'cancelled')
5561 -- A rolled-back run's items are abandoned, not pending. Without
5562 -- this the shell reports Waiting for work no one will finish,
5563 -- matching the item_count query's exclusion above.
5564 AND (i.run_id IS NULL OR r.status <> 'rolled_back')
5565 `
5566 : sql<PayloadRow & { readonly thread_id: string }>`
5567 SELECT i.thread_id, i.payload_json
5568 FROM orchestration_v2_projection_turn_items i
5569 LEFT JOIN orchestration_v2_projection_runs r
5570 ON r.run_id = i.run_id
5571 WHERE i.type IN ('command_execution', 'dynamic_tool', 'subagent')
5572 AND i.status NOT IN ('completed', 'interrupted', 'failed', 'cancelled')
5573 AND (i.run_id IS NULL OR r.status <> 'rolled_back')
5574 AND i.thread_id IN ${sql.in(threadIds)}
5575 `;
5576
5577 const runMapsByThreadId = (input: {
5578 readonly runRows: ReadonlyArray<ShellRunRow>;
5579 readonly itemCountRows: ReadonlyArray<ShellRunItemCountRow>;
5580 }) => {
5581 const runOrdinalsByThreadId = new Map<ThreadId, Map<RunId, number>>();
5582 for (const row of input.runRows) {
5583 const threadId = ThreadId.make(row.thread_id);
5584 const runId = RunId.make(row.run_id);
5585 const existing = runOrdinalsByThreadId.get(threadId) ?? new Map<RunId, number>();
5586 existing.set(runId, row.ordinal);
5587 runOrdinalsByThreadId.set(threadId, existing);
5588 }
5589 const itemCountsByThreadId = new Map<ThreadId, Map<RunId, number>>();
5590 for (const row of input.itemCountRows) {
5591 const threadId = ThreadId.make(row.thread_id);
5592 const runId = RunId.make(row.run_id);
5593 const existing = itemCountsByThreadId.get(threadId) ?? new Map<RunId, number>();
5594 existing.set(runId, row.item_count);
5595 itemCountsByThreadId.set(threadId, existing);
5596 }
5597 return { runOrdinalsByThreadId, itemCountsByThreadId };
5598 };
5599
5600 const pendingBackgroundDataByThreadId = (input: {
5601 readonly providerThreadRows: ReadonlyArray<PayloadRow & { readonly thread_id: string }>;
5602 readonly pendingTurnItemRows: ReadonlyArray<PayloadRow & { readonly thread_id: string }>;
5603 }) =>
5604 Effect.gen(function* () {
5605 const providerThreadsByThreadId = new Map<
5606 ThreadId,
5607 Array<OrchestrationV2ThreadProjection["providerThreads"][number]>
5608 >();
5609 for (const row of input.providerThreadRows) {
5610 const providerThread = yield* decodeProviderThreadPayload(row.payload_json);
5611 const threadId =
5612 row.thread_id.length > 0 ? ThreadId.make(row.thread_id) : providerThread.appThreadId;
5613 if (threadId === null) {
5614 continue;
5615 }
5616 const existing = providerThreadsByThreadId.get(threadId) ?? [];
5617 existing.push(providerThread);
5618 providerThreadsByThreadId.set(threadId, existing);
5619 }
5620
5621 const pendingTurnItemsByThreadId = new Map<ThreadId, Array<OrchestrationV2TurnItem>>();
5622 for (const row of input.pendingTurnItemRows) {
5623 const turnItem = yield* decodeTurnItemPayload(row.payload_json);
5624 const threadId = ThreadId.make(row.thread_id);
5625 const existing = pendingTurnItemsByThreadId.get(threadId) ?? [];
5626 existing.push(turnItem);
5627 pendingTurnItemsByThreadId.set(threadId, existing);
5628 }
5629
5630 return { providerThreadsByThreadId, pendingTurnItemsByThreadId };
5631 });
5632
5633 const getSettlementCandidates: ProjectionStoreV2Shape["getSettlementCandidates"] = (threadId) =>
5634 sql
5635 .withTransaction(
5636 Effect.gen(function* () {
5637 // Settlement needs activity timestamps, not item counts, run history,
5638 // provider sessions or inherited fork rows. Reject parked and busy
5639 // threads before loading the remaining candidates' background work.
5640 const rows = yield* sql<SettlementThreadRow>`
5641 SELECT t.thread_id, t.payload_json,
5642 r.run_id AS latest_run_id,
5643 r.status AS latest_run_status,
5644 r.requested_at AS latest_run_requested_at,
5645 json_extract(r.payload_json, '$.startedAt') AS latest_run_started_at,
5646 r.completed_at AS latest_run_completed_at,
5647 (
5648 SELECT message.updated_at
5649 FROM orchestration_v2_projection_messages message
5650 WHERE message.thread_id = t.thread_id AND message.role = 'user'
5651 ORDER BY message.updated_at DESC, message.message_id DESC
5652 LIMIT 1
5653 ) AS latest_user_message_at,
5654 (
5655 SELECT message.updated_at
5656 FROM orchestration_v2_projection_messages message
5657 WHERE message.thread_id = t.thread_id AND message.role = 'user'
5658 AND json_extract(message.payload_json, '$.createdBy') = 'user'
5659 ORDER BY message.updated_at DESC, message.message_id DESC
5660 LIMIT 1
5661 ) AS latest_user_authored_message_at
5662 FROM orchestration_v2_projection_threads t
5663 LEFT JOIN orchestration_v2_projection_runs r ON r.run_id = (
5664 SELECT latest.run_id FROM orchestration_v2_projection_runs latest
5665 WHERE latest.thread_id = t.thread_id
5666 ORDER BY latest.ordinal DESC, latest.run_id DESC
5667 LIMIT 1
5668 )
5669 WHERE t.deleted_at IS NULL${threadId === undefined ? sql`` : sql` AND t.thread_id = ${threadId}`}
5670 AND json_extract(t.payload_json, '$.archivedAt') IS NULL
5671 AND json_extract(t.payload_json, '$.settledOverride') IS NULL
5672 AND json_extract(t.payload_json, '$.pinnedAt') IS NULL
5673 AND json_extract(t.payload_json, '$.autoSettleDisabledAt') IS NULL
5674 AND NOT EXISTS (
5675 SELECT 1 FROM orchestration_v2_projection_runs active
5676 WHERE active.thread_id = t.thread_id
5677 AND active.status IN ('preparing', 'starting', 'running', 'waiting')
5678 )
5679 AND NOT EXISTS (
5680 SELECT 1 FROM orchestration_v2_projection_runtime_requests request
5681 WHERE request.thread_id = t.thread_id AND request.status = 'pending'
5682 )
5683 ORDER BY t.updated_at ASC, t.thread_id ASC
5684 `;
5685 if (rows.length === 0) return [];
5686 const threadIds = rows.map((row) => ThreadId.make(row.thread_id));
5687 const { providerThreadsByThreadId, pendingTurnItemsByThreadId } =
5688 yield* pendingBackgroundDataByThreadId({
5689 providerThreadRows: yield* selectShellProviderThreadRows(threadIds),
5690 pendingTurnItemRows: yield* selectShellPendingTurnItemRows(threadIds),
5691 });
5692 return yield* Effect.forEach(rows, (row) =>
5693 Effect.gen(function* () {
5694 const thread = yield* decodeThreadPayload(row.payload_json);
5695 const status = shellStatusFromStoredRunStatus(row.latest_run_status);
5696 const latestRunId =
5697 row.latest_run_id === null ? null : RunId.make(row.latest_run_id);
5698 return {
5699 ...thread,
5700 pinnedAt: thread.pinnedAt ?? null,
5701
5702 autoSettleDisabledAt: thread.autoSettleDisabledAt ?? null,
5703 snoozedUntil: thread.snoozedUntil ?? null,
5704 snoozedAt: thread.snoozedAt ?? null,
5705 status,
5706 latestRunId,
5707 latestRunRequestedAt:
5708 row.latest_run_requested_at === null
5709 ? null
5710 : DateTime.makeUnsafe(row.latest_run_requested_at),
5711 latestRunStartedAt:
5712 row.latest_run_started_at === null
5713 ? null
5714 : DateTime.makeUnsafe(row.latest_run_started_at),
5715 latestRunCompletedAt:
5716 row.latest_run_completed_at === null
5717 ? null
5718 : DateTime.makeUnsafe(row.latest_run_completed_at),
5719 latestUserMessageAt:
5720 row.latest_user_message_at === null
5721 ? null
5722 : DateTime.makeUnsafe(row.latest_user_message_at),
5723 latestUserAuthoredMessageAt:
5724 row.latest_user_authored_message_at === null
5725 ? null
5726 : DateTime.makeUnsafe(row.latest_user_authored_message_at),
5727 activityRunStatus: null,
5728 activityRunStartedAt: null,
5729 pendingRuntimeRequest: null,
5730 pendingBackgroundTasks: derivePendingBackgroundWork({
5731 latestRun:
5732 latestRunId === null || status === "idle"
5733 ? null
5734 : { id: latestRunId, ordinal: 0, status },
5735 providerThreads: providerThreadsByThreadId.get(thread.id) ?? [],
5736 turnItems: pendingTurnItemsByThreadId.get(thread.id) ?? [],
5737 activeProviderThreadId: thread.activeProviderThreadId,
5738 hasActiveRun: false,
5739 pullRequests: thread.pullRequests,
5740 }),
5741 } satisfies ProjectionSettlementCandidate;
5742 }),
5743 );
5744 }),
5745 )
5746 .pipe(Effect.mapError((cause) => new ProjectionStoreSetupError({ cause })));
5747
5748 const getThreadsWithPullRequests: ProjectionStoreV2Shape["getThreadsWithPullRequests"] = (
5749 threadId,
5750 ) =>
5751 Effect.gen(function* () {
5752 const rows = yield* sql<PayloadRow>`
5753 SELECT payload_json
5754 FROM orchestration_v2_projection_threads
5755 WHERE deleted_at IS NULL${threadId === undefined ? sql`` : sql` AND thread_id = ${threadId}`}
5756 AND json_extract(payload_json, '$.archivedAt') IS NULL
5757 AND json_array_length(payload_json, '$.pullRequests') > 0
5758 ORDER BY updated_at ASC, thread_id ASC
5759 `;
5760 return yield* Effect.forEach(rows, (row) =>
5761 decodeThreadPayload(row.payload_json).pipe(
5762 Effect.map((thread): ProjectionThreadPullRequests => ({
5763 id: thread.id,
5764 projectId: thread.projectId,
5765 lineage: thread.lineage,
5766 settledOverride: thread.settledOverride,
5767 settledAt: thread.settledAt,
5768 pullRequests: thread.pullRequests ?? [],
5769 })),
5770 ),
5771 );
5772 }).pipe(Effect.mapError((cause) => new ProjectionStoreSetupError({ cause })));
5773
5774 const getThreadWorktreePaths: ProjectionStoreV2Shape["getThreadWorktreePaths"] = () =>
5775 sql<{ readonly thread_id: string; readonly worktree_path: string }>`
5776 SELECT thread_id, json_extract(payload_json, '$.worktreePath') AS worktree_path
5777 FROM orchestration_v2_projection_threads
5778 WHERE deleted_at IS NULL
5779 AND json_extract(payload_json, '$.archivedAt') IS NULL
5780 AND json_extract(payload_json, '$.worktreePath') IS NOT NULL
5781 `.pipe(
5782 Effect.map((rows) =>
5783 rows.map((row) => ({
5784 threadId: ThreadId.make(row.thread_id),
5785 worktreePath: row.worktree_path,
5786 })),
5787 ),
5788 Effect.mapError((cause) => new ProjectionStoreSetupError({ cause })),
5789 );
5790
5791 const shellThreadStateFromRow = (input: {
5792 readonly row: ShellThreadRow;
5793 readonly runOrdinalsByThreadId: ReadonlyMap<ThreadId, Map<RunId, number>>;
5794 readonly itemCountsByThreadId: ReadonlyMap<ThreadId, Map<RunId, number>>;
5795 readonly providerThreadsByThreadId: ReadonlyMap<
5796 ThreadId,
5797 ReadonlyArray<OrchestrationV2ThreadProjection["providerThreads"][number]>
5798 >;
5799 readonly pendingTurnItemsByThreadId: ReadonlyMap<
5800 ThreadId,
5801 ReadonlyArray<OrchestrationV2TurnItem>
5802 >;
5803 }) =>
5804 Effect.gen(function* () {
5805 const {
5806 row,
5807 runOrdinalsByThreadId,
5808 itemCountsByThreadId,
5809 providerThreadsByThreadId,
5810 pendingTurnItemsByThreadId,
5811 } = input;
5812 const thread = yield* decodeThreadPayload(row.payload_json);
5813 const pendingRuntimeRequest =
5814 row.pending_request_payload_json !== null
5815 ? yield* decodeRuntimeRequestPayload(row.pending_request_payload_json)
5816 : row.pending_secret_request_payload_json !== null
5817 ? secretRequestAsPendingInput(
5818 yield* decodeTurnItemPayload(row.pending_secret_request_payload_json),
5819 )
5820 : null;
5821 let terminalFailureItem =
5822 row.terminal_failure_payload_json === null
5823 ? null
5824 : yield* decodeTurnItemPayload(row.terminal_failure_payload_json);
5825 let latestRunId = row.latest_run_id === null ? null : RunId.make(row.latest_run_id);
5826 let latestRunStatus = shellStatusFromStoredRunStatus(row.latest_run_status);
5827 let latestRunRequestedAt =
5828 row.latest_run_requested_at === null
5829 ? null
5830 : DateTime.makeUnsafe(row.latest_run_requested_at);
5831 let latestRunStartedAt =
5832 row.latest_run_started_at === null
5833 ? null
5834 : DateTime.makeUnsafe(row.latest_run_started_at);
5835 let latestRunCompletedAt =
5836 row.latest_run_completed_at === null
5837 ? null
5838 : DateTime.makeUnsafe(row.latest_run_completed_at);
5839 if (row.blocking_run_id !== null && row.blocking_failure_payload_json !== null) {
5840 const blockingFailure = yield* decodeTurnItemPayload(
5841 row.blocking_failure_payload_json,
5842 ).pipe(Effect.orElseSucceed(() => null));
5843 const blocksQueue =
5844 threadErrorSummary(
5845 blockingFailure?.type === "error" ? blockingFailure.failure : null,
5846 row.last_error,
5847 ).lastErrorClass === "usage_limit";
5848 if (blocksQueue && blockingFailure !== null) {
5849 terminalFailureItem = blockingFailure;
5850 latestRunId = RunId.make(row.blocking_run_id);
5851 latestRunStatus = "failed";
5852 latestRunRequestedAt =
5853 row.blocking_run_requested_at === null
5854 ? null
5855 : DateTime.makeUnsafe(row.blocking_run_requested_at);
5856 latestRunStartedAt =
5857 row.blocking_run_started_at === null
5858 ? null
5859 : DateTime.makeUnsafe(row.blocking_run_started_at);
5860 latestRunCompletedAt =
5861 row.blocking_run_completed_at === null
5862 ? null
5863 : DateTime.makeUnsafe(row.blocking_run_completed_at);
5864 }
5865 }
5866 const pendingBackgroundTasks = [
5867 ...derivePendingBackgroundWork({
5868 latestRun:
5869 latestRunId === null || latestRunStatus === "idle"
5870 ? null
5871 : {
5872 id: latestRunId,
5873 ordinal: 0,
5874 status: latestRunStatus,
5875 },
5876 providerThreads: providerThreadsByThreadId.get(thread.id) ?? [],
5877 turnItems: pendingTurnItemsByThreadId.get(thread.id) ?? [],
5878 activeProviderThreadId: thread.activeProviderThreadId,
5879 hasActiveRun: row.active_run_id !== null,
5880 pullRequests: thread.pullRequests,
5881 }),
5882 ];
5883 return {
5884 thread,
5885 latestRunId,
5886 latestRunStatus,
5887 latestRunRequestedAt,
5888 latestRunStartedAt,
5889 latestRunCompletedAt,
5890 activeRunId: row.active_run_id === null ? null : RunId.make(row.active_run_id),
5891 activityRunStartedAt:
5892 row.activity_run_started_at === null
5893 ? null
5894 : DateTime.makeUnsafe(row.activity_run_started_at),
5895 activityRunStatus:
5896 row.activity_run_status === "preparing" ||
5897 row.activity_run_status === "starting" ||
5898 row.activity_run_status === "running" ||
5899 row.activity_run_status === "waiting"
5900 ? row.activity_run_status
5901 : null,
5902 ...threadErrorSummary(
5903 terminalFailureItem?.type === "error" ? terminalFailureItem.failure : null,
5904 row.last_error,
5905 ),
5906 pendingRuntimeRequest,
5907 latestUserMessageAt:
5908 row.latest_user_message_at === null
5909 ? null
5910 : DateTime.makeUnsafe(row.latest_user_message_at),
5911 latestUserAuthoredMessageAt:
5912 row.latest_user_authored_message_at === null
5913 ? null
5914 : DateTime.makeUnsafe(row.latest_user_authored_message_at),
5915 hasActionableProposedPlan: row.has_actionable_proposed_plan === 1,
5916 pendingBackgroundTasks,
5917 providerInstanceHistory: providerInstanceHistoryForShell({
5918 threadId: thread.id,
5919 providerThreads: providerThreadsByThreadId.get(thread.id) ?? [],
5920 }),
5921 goal: activeProviderGoalForShell(
5922 providerThreadsByThreadId.get(thread.id) ?? [],
5923 thread.activeProviderThreadId,
5924 ),
5925 itemCount: row.item_count,
5926 runlessItemCount: row.runless_item_count,
5927 updatedAt: thread.updatedAt,
5928 runOrdinalById: runOrdinalsByThreadId.get(ThreadId.make(row.thread_id)) ?? new Map(),
5929 itemCountByRunId: itemCountsByThreadId.get(ThreadId.make(row.thread_id)) ?? new Map(),
5930 } satisfies ShellThreadState;
5931 });
5932
5933 const shellSnapshotReadError = (cause: unknown) =>
5934 new ProjectionStoreReadError({ threadId: ThreadId.make("thread:shell"), cause });
5935
5936 const readShellSnapshotRows = (options: ShellSnapshotOptions | undefined) =>
5937 Effect.gen(function* () {
5938 const targetThreadRows = yield* selectShellThreadRows(undefined, options);
5939 const targetThreadIds = new Set(
5940 targetThreadRows.map((row) => ThreadId.make(row.thread_id)),
5941 );
5942 const rowsByThreadId = new Map(
5943 targetThreadRows.map((row) => [ThreadId.make(row.thread_id), row] as const),
5944 );
5945 const pendingSourceIds = targetThreadRows.flatMap((row) =>
5946 row.forked_from_run_source_thread_id === null
5947 ? []
5948 : [ThreadId.make(row.forked_from_run_source_thread_id)],
5949 );
5950 while (pendingSourceIds.length > 0) {
5951 const sourceId = pendingSourceIds.pop();
5952 if (sourceId === undefined || rowsByThreadId.has(sourceId)) continue;
5953 const source = (yield* selectShellThreadRows(sourceId))[0];
5954 if (source === undefined) continue;
5955 rowsByThreadId.set(sourceId, source);
5956 if (source.forked_from_run_source_thread_id !== null) {
5957 pendingSourceIds.push(ThreadId.make(source.forked_from_run_source_thread_id));
5958 }
5959 }
5960 const threadRows = [...rowsByThreadId.values()];
5961 const threadIds = [...rowsByThreadId.keys()];
5962 const forkSourceIds = shellForkSourceIds(threadRows);
5963 const readForThreadIds = <A>(
5964 read: (ids: ReadonlyArray<ThreadId>) => Effect.Effect<ReadonlyArray<A>, unknown>,
5965 ids: ReadonlyArray<ThreadId> = threadIds,
5966 ) => (ids.length === 0 ? Effect.succeed([] as ReadonlyArray<A>) : read(ids));
5967 const [runRows, itemCountRows, sequenceRows, providerThreadRows, pendingTurnItemRows] =
5968 yield* Effect.all([
5969 readForThreadIds(selectShellRunRows, forkSourceIds),
5970 readForThreadIds(selectShellRunItemCounts, forkSourceIds),
5971 sql<{ readonly snapshot_sequence: number | null }>`
5972 SELECT MAX(sequence) AS snapshot_sequence
5973 FROM orchestration_events
5974 WHERE application_event_version = 2
5975 AND aggregate_kind = 'thread'
5976 `,
5977 readForThreadIds(selectShellProviderThreadRows),
5978 readForThreadIds(selectShellPendingTurnItemRows),
5979 ]);
5980 return {
5981 targetThreadIds,
5982 threadRows,
5983 runRows,
5984 itemCountRows,
5985 sequenceRows,
5986 providerThreadRows,
5987 pendingTurnItemRows,
5988 };
5989 });
5990
5991 const decodeShellSnapshot = ({
5992 targetThreadIds,
5993 threadRows,
5994 runRows,
5995 itemCountRows,
5996 sequenceRows,
5997 providerThreadRows,
5998 pendingTurnItemRows,
5999 }: Effect.Success<ReturnType<typeof readShellSnapshotRows>>) =>
6000 Effect.gen(function* () {
6001 const { runOrdinalsByThreadId, itemCountsByThreadId } = runMapsByThreadId({
6002 runRows,
6003 itemCountRows,
6004 });
6005 const { providerThreadsByThreadId, pendingTurnItemsByThreadId } =
6006 yield* pendingBackgroundDataByThreadId({ providerThreadRows, pendingTurnItemRows });
6007 const states = yield* Effect.forEach(threadRows, (row) =>
6008 shellThreadStateFromRow({
6009 row,
6010 runOrdinalsByThreadId,
6011 itemCountsByThreadId,
6012 providerThreadsByThreadId,
6013 pendingTurnItemsByThreadId,
6014 }),
6015 );
6016 const statesByThreadId = new Map(states.map((state) => [state.thread.id, state]));
6017
6018 const shells = states
6019 .filter((state) => targetThreadIds.has(state.thread.id))
6020 .map((state) =>
6021 shellFromState({
6022 state,
6023 visibleItemCount: visibleItemCountForShell({
6024 threadId: state.thread.id,
6025 statesByThreadId,
6026 }),
6027 }),
6028 );
6029
6030 return {
6031 schemaVersion: ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION,
6032 snapshotSequence: sequenceRows[0]?.snapshot_sequence ?? 0,
6033 threads: shells.filter((thread) => thread.archivedAt === null),
6034 archivedThreads: shells.filter((thread) => thread.archivedAt !== null),
6035 };
6036 }).pipe(Effect.mapError(shellSnapshotReadError));
6037
6038 const readShellSnapshot: ProjectionStoreV2Shape["readShellSnapshot"] = (options) =>
6039 readShellSnapshotRows(options).pipe(
6040 Effect.mapError(shellSnapshotReadError),
6041 Effect.map(decodeShellSnapshot),
6042 );
6043
6044 const getShellSnapshot: ProjectionStoreV2Shape["getShellSnapshot"] = (options) =>
6045 sql
6046 .withTransaction(readShellSnapshotRows(options))
6047 .pipe(Effect.mapError(shellSnapshotReadError), Effect.flatMap(decodeShellSnapshot));
6048
6049 // Per-thread shell for the live shell streams: reads only the target thread
6050 // plus its fork-source chain instead of materializing every thread. Returns
6051 // null when the thread is deleted or unknown.
6052 const getThreadShell: ProjectionStoreV2Shape["getThreadShell"] = (threadId) =>
6053 sql
6054 .withTransaction(
6055 Effect.gen(function* () {
6056 const rowsByThreadId = new Map<ThreadId, ShellThreadRow>();
6057 const pending: Array<ThreadId> = [threadId];
6058 while (pending.length > 0) {
6059 const nextId = pending.pop();
6060 if (nextId === undefined || rowsByThreadId.has(nextId)) {
6061 continue;
6062 }
6063 const rows = yield* selectShellThreadRows(nextId);
6064 const row = rows[0];
6065 if (row === undefined) {
6066 continue;
6067 }
6068 rowsByThreadId.set(nextId, row);
6069 if (row.forked_from_run_source_thread_id !== null) {
6070 pending.push(ThreadId.make(row.forked_from_run_source_thread_id));
6071 }
6072 }
6073 if (!rowsByThreadId.has(threadId)) {
6074 return null;
6075 }
6076
6077 const threadIds = [...rowsByThreadId.keys()];
6078 const forkSourceIds = shellForkSourceIds([...rowsByThreadId.values()]);
6079 const [runRows, itemCountRows, providerThreadRows, pendingTurnItemRows] =
6080 yield* Effect.all([
6081 forkSourceIds.length === 0
6082 ? Effect.succeed([] as ReadonlyArray<ShellRunRow>)
6083 : selectShellRunRows(forkSourceIds),
6084 forkSourceIds.length === 0
6085 ? Effect.succeed([] as ReadonlyArray<ShellRunItemCountRow>)
6086 : selectShellRunItemCounts(forkSourceIds),
6087 selectShellProviderThreadRows(threadIds),
6088 selectShellPendingTurnItemRows(threadIds),
6089 ]);
6090 const { runOrdinalsByThreadId, itemCountsByThreadId } = runMapsByThreadId({
6091 runRows,
6092 itemCountRows,
6093 });
6094 const { providerThreadsByThreadId, pendingTurnItemsByThreadId } =
6095 yield* pendingBackgroundDataByThreadId({ providerThreadRows, pendingTurnItemRows });
6096
6097 const states = yield* Effect.forEach([...rowsByThreadId.values()], (row) =>
6098 shellThreadStateFromRow({
6099 row,
6100 runOrdinalsByThreadId,
6101 itemCountsByThreadId,
6102 providerThreadsByThreadId,
6103 pendingTurnItemsByThreadId,
6104 }),
6105 );
6106 const statesByThreadId = new Map(states.map((state) => [state.thread.id, state]));
6107 const state = statesByThreadId.get(threadId);
6108 if (state === undefined) {
6109 return null;
6110 }
6111 return shellFromState({
6112 state,
6113 visibleItemCount: visibleItemCountForShell({ threadId, statesByThreadId }),
6114 });
6115 }),
6116 )
6117 .pipe(Effect.mapError((cause) => new ProjectionStoreReadError({ threadId, cause })));
6118
6119 return {
6120 apply,
6121 getShellSnapshot,
6122 readShellSnapshot,
6123 getThreadShell,
6124 getThread,
6125 getSettlementCandidates,
6126 getThreadsWithPullRequests,
6127 getThreadWorktreePaths,
6128 getThreadProjection,
6129 getTurnStartContext,
6130 getTurnStartHistory,
6131 getRuntimeRecoveryProjection,
6132 getRunningTurnContext,
6133 getThreadProviderContext,
6134 getRuntimeResponseContext,
6135 getCheckpointContext,
6136 getCheckpointCaptureContext,
6137 getRunMessage,
6138 canStartQueuedRun,
6139 getPendingNativeUserInputs,
6140 hasUnpairedRunInterruptRequest,
6141 isNodeStored,
6142 getMessageCount,
6143 getNextTurnItemOrdinal,
6144 getTurnItem,
6145 getThreadRecords,
6146 getRuntimeRequest,
6147 getPlan,
6148 getProviderControlContext,
6149 getLimitRecoveryCandidates,
6150 getRecoveryThreadIds,
6151 getUnreadableThreadIds,
6152 getThreadSnapshot,
6153 getThreadSnapshotWindow,
6154 getTimelinePage,
6155 getThreadHistoryPage,
6156 searchThread,
6157 searchThreadStream,
6158 getThreadAttachmentIds,
6159 } satisfies ProjectionStoreV2Shape;
6160 }),
6161);
6162
6163export const layerMemory: Layer.Layer<ProjectionStoreV2> = Layer.effect(
6164 ProjectionStoreV2,
6165 Effect.gen(function* () {
6166 const replayState = yield* Ref.make(makeProjectionReplayState());
6167 const sequence = yield* Ref.make(0);
6168
6169 const service: ProjectionStoreV2Shape = {
6170 apply: (event) =>
6171 Effect.gen(function* () {
6172 const result = yield* Ref.modify(replayState, (existing) => {
6173 const next: ProjectionReplayState = {
6174 projections: new Map(existing.projections),
6175 providerSessionThreadIds: new Map(existing.providerSessionThreadIds),
6176 };
6177 if (!applyToProjectionReplayState(next, event)) {
6178 return [
6179 new ProjectionStoreThreadNotFoundError({ threadId: event.threadId }),
6180 existing,
6181 ] as const;
6182 }
6183 return [undefined, next] as const;
6184 });
6185
6186 if (result) {
6187 return yield* result;
6188 }
6189 yield* Ref.update(sequence, (current) => current + 1);
6190 }),
6191 getShellSnapshot: (options) =>
6192 Effect.gen(function* () {
6193 const existing = (yield* Ref.get(replayState)).projections;
6194 const selectedThreadIds = [...existing.entries()]
6195 .filter(([, projection]) => {
6196 if (
6197 options?.projectId !== undefined &&
6198 projection.thread.projectId !== options.projectId
6199 ) {
6200 return false;
6201 }
6202 if (
6203 options?.unsettledOnly &&
6204 (projection.thread.settledAt !== null ||
6205 projection.thread.settledOverride === "settled")
6206 ) {
6207 return false;
6208 }
6209 if (
6210 options?.settledOnly &&
6211 (projection.thread.settledOverride !== "settled" ||
6212 projection.thread.settledAt === null)
6213 )
6214 return false;
6215 if (options?.location === "active") return projection.thread.archivedAt === null;
6216 if (options?.location === "archive") return projection.thread.archivedAt !== null;
6217 return true;
6218 })
6219 .map(([threadId]) => threadId);
6220 const shells = yield* Effect.forEach(
6221 selectedThreadIds.toSorted((left, right) => String(left).localeCompare(String(right))),
6222 (threadId) =>
6223 service.getThreadProjection(threadId).pipe(Effect.map(threadShellFromProjection)),
6224 );
6225 const visible = shells.filter((thread) => thread.deletedAt === null);
6226 return {
6227 schemaVersion: ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION,
6228 snapshotSequence: yield* Ref.get(sequence),
6229 threads: visible.filter((thread) => thread.archivedAt === null),
6230 archivedThreads: visible.filter((thread) => thread.archivedAt !== null),
6231 };
6232 }),
6233 readShellSnapshot: (options) =>
6234 service.getShellSnapshot(options).pipe(Effect.map(Effect.succeed)),
6235 getThreadShell: (threadId) =>
6236 Effect.gen(function* () {
6237 const existing = (yield* Ref.get(replayState)).projections;
6238 if (!existing.has(threadId)) {
6239 return null;
6240 }
6241 const shell = yield* service
6242 .getThreadProjection(threadId)
6243 .pipe(Effect.map(threadShellFromProjection));
6244 return shell.deletedAt === null ? shell : null;
6245 }),
6246 getThread: (threadId) =>
6247 Effect.gen(function* () {
6248 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6249 if (projection === undefined) {
6250 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6251 }
6252 return projection.thread;
6253 }),
6254 getSettlementCandidates: (threadId) =>
6255 Effect.gen(function* () {
6256 const projections = (yield* Ref.get(replayState)).projections;
6257 return [...projections.values()]
6258 .filter(
6259 ({ thread, runs, runtimeRequests }) =>
6260 (threadId === undefined || thread.id === threadId) &&
6261 thread.deletedAt === null &&
6262 thread.archivedAt === null &&
6263 thread.settledOverride === null &&
6264 thread.pinnedAt == null &&
6265 thread.autoSettleDisabledAt == null &&
6266 !runs.some(isActivityRunForShell) &&
6267 !runtimeRequests.some((request) => request.status === "pending"),
6268 )
6269 .map((projection) => {
6270 const shell = threadShellFromProjection(projection);
6271 return {
6272 ...shell,
6273 lastSnoozeWakeAt: projection.thread.lastSnoozeWakeAt ?? null,
6274 latestUserAuthoredMessageAt: shell.latestUserAuthoredMessageAt ?? null,
6275 };
6276 })
6277 .toSorted(
6278 (left, right) =>
6279 DateTime.toEpochMillis(left.updatedAt) - DateTime.toEpochMillis(right.updatedAt) ||
6280 left.id.localeCompare(right.id),
6281 );
6282 }),
6283 getThreadWorktreePaths: () =>
6284 Ref.get(replayState).pipe(
6285 Effect.map((state) =>
6286 [...state.projections.values()].flatMap(({ thread }) =>
6287 thread.deletedAt === null &&
6288 thread.archivedAt === null &&
6289 thread.worktreePath !== null
6290 ? [{ threadId: thread.id, worktreePath: thread.worktreePath }]
6291 : [],
6292 ),
6293 ),
6294 ),
6295 getThreadsWithPullRequests: (threadId) =>
6296 Ref.get(replayState).pipe(
6297 Effect.map((state) =>
6298 [...state.projections.values()]
6299 .map(({ thread }) => thread)
6300 .filter(
6301 (thread) =>
6302 (threadId === undefined || thread.id === threadId) &&
6303 thread.deletedAt === null &&
6304 thread.archivedAt === null &&
6305 (thread.pullRequests ?? []).length > 0,
6306 )
6307 .toSorted(
6308 (left, right) =>
6309 DateTime.toEpochMillis(left.updatedAt) -
6310 DateTime.toEpochMillis(right.updatedAt) || left.id.localeCompare(right.id),
6311 )
6312 .map((thread): ProjectionThreadPullRequests => ({
6313 id: thread.id,
6314 projectId: thread.projectId,
6315 lineage: thread.lineage,
6316 settledOverride: thread.settledOverride,
6317 settledAt: thread.settledAt,
6318 pullRequests: thread.pullRequests ?? [],
6319 })),
6320 ),
6321 ),
6322 getLimitRecoveryCandidates: (options) =>
6323 Ref.get(replayState).pipe(
6324 Effect.map((state) =>
6325 [...state.projections.values()]
6326 .filter(
6327 ({ thread }) =>
6328 thread.deletedAt === null &&
6329 thread.archivedAt === null &&
6330 thread.settledOverride !== "settled",
6331 )
6332 .map(threadShellFromProjection)
6333 .filter(
6334 (thread) =>
6335 thread.status === "failed" &&
6336 thread.lastErrorClass === "usage_limit" &&
6337 thread.usageLimitResetAt !== null &&
6338 thread.pendingRuntimeRequest === null,
6339 )
6340 .filter((thread) => {
6341 const resetMs = Date.parse(thread.usageLimitResetAt!);
6342 const nowMs = DateTime.toEpochMillis(options.now);
6343 if (
6344 !Number.isFinite(resetMs) ||
6345 resetMs <= DateTime.toEpochMillis(thread.latestRunCompletedAt ?? thread.updatedAt)
6346 )
6347 return false;
6348 if (
6349 thread.limitRecovery?.runId !== thread.latestRunId ||
6350 thread.limitRecovery.resetAt !== thread.usageLimitResetAt
6351 ) {
6352 return options.autoResume || (options.snooze && resetMs > nowMs);
6353 }
6354 return (
6355 thread.limitRecovery.autoResume &&
6356 resetMs <= nowMs &&
6357 (thread.snoozedUntil == null ||
6358 DateTime.toEpochMillis(thread.snoozedUntil) <= nowMs)
6359 );
6360 })
6361 .toSorted((left, right) => left.id.localeCompare(right.id)),
6362 ),
6363 ),
6364 getRecoveryThreadIds: (kind) =>
6365 Effect.gen(function* () {
6366 const projections = (yield* Ref.get(replayState)).projections;
6367 return [...projections.values()]
6368 .filter((projection) =>
6369 needsRecovery(
6370 projection,
6371 kind,
6372 projection.thread.lineage.parentThreadId === null
6373 ? undefined
6374 : projections.get(projection.thread.lineage.parentThreadId),
6375 ),
6376 )
6377 .toSorted(
6378 (left, right) =>
6379 DateTime.toEpochMillis(left.thread.updatedAt) -
6380 DateTime.toEpochMillis(right.thread.updatedAt) ||
6381 left.thread.id.localeCompare(right.thread.id),
6382 )
6383 .map((projection) => projection.thread.id);
6384 }),
6385 getUnreadableThreadIds: () => Effect.succeed([]),
6386 getPendingNativeUserInputs: (threadId, providerTurnId) =>
6387 Effect.gen(function* () {
6388 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6389 if (projection === undefined) return { runtimeRequests: [], nodes: [], turnItems: [] };
6390 const runtimeRequests = projection.runtimeRequests.filter(
6391 (request) =>
6392 request.providerTurnId === providerTurnId &&
6393 request.kind === "user_input" &&
6394 request.status === "pending" &&
6395 request.responseCapability.type !== "message",
6396 );
6397 const requestIds = new Set(runtimeRequests.map((request) => request.id));
6398 const nodeIds = new Set(runtimeRequests.map((request) => request.nodeId));
6399 return {
6400 runtimeRequests,
6401 nodes: projection.nodes.filter((node) => nodeIds.has(node.id)),
6402 turnItems: projection.turnItems.filter(
6403 (item) => item.type === "user_input_request" && requestIds.has(item.requestId),
6404 ),
6405 };
6406 }),
6407 getPlan: (threadId, planId) =>
6408 Effect.gen(function* () {
6409 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6410 if (projection === undefined)
6411 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6412 return projection.plans.find((plan) => plan.id === planId);
6413 }),
6414 hasUnpairedRunInterruptRequest: (threadId, requestId, resultId) =>
6415 Effect.gen(function* () {
6416 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6417 return (
6418 projection !== undefined &&
6419 projection.turnItems.some((item) => item.id === requestId) &&
6420 !projection.turnItems.some((item) => item.id === resultId)
6421 );
6422 }),
6423 isNodeStored: (node) =>
6424 Ref.get(replayState).pipe(
6425 Effect.map((state) => {
6426 const stored = state.projections
6427 .get(node.threadId)
6428 ?.nodes.find((candidate) => candidate.id === node.id);
6429 return stored !== undefined && Equal.equals(stored, node);
6430 }),
6431 ),
6432 getMessageCount: (threadId) =>
6433 Ref.get(replayState).pipe(
6434 Effect.map((state) => state.projections.get(threadId)?.messages.length ?? 0),
6435 ),
6436 getNextTurnItemOrdinal: (threadId) =>
6437 Ref.get(replayState).pipe(
6438 Effect.map(
6439 (state) =>
6440 (state.projections
6441 .get(threadId)
6442 ?.turnItems.reduce((max, item) => Math.max(max, item.ordinal), 0) ?? 0) + 1,
6443 ),
6444 ),
6445 getTurnItem: ({ threadId, itemId }) =>
6446 Ref.get(replayState).pipe(
6447 Effect.map(
6448 (state) =>
6449 state.projections.get(threadId)?.turnItems.find((item) => item.id === itemId) ?? null,
6450 ),
6451 ),
6452 getThreadAttachmentIds: (threadId) =>
6453 service.getThreadProjection(threadId).pipe(
6454 Effect.map((projection) => [
6455 ...new Set([
6456 ...projection.messages.flatMap((message) =>
6457 message.attachments.map((attachment) => attachment.id),
6458 ),
6459 ...threadHtmlRenderAttachmentIds(
6460 threadId,
6461 projection.turnItems.flatMap((item) =>
6462 item.type === "dynamic_tool" ? [item] : [],
6463 ),
6464 ),
6465 ]),
6466 ]),
6467 ),
6468 getThreadRecords: (threadId, fields, filter) =>
6469 Effect.gen(function* () {
6470 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6471 if (projection === undefined)
6472 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6473 const selected = {
6474 ...projection,
6475 messages: projection.messages.filter(
6476 (row) =>
6477 (filter?.messageRunIds === undefined ||
6478 (row.runId !== null && filter.messageRunIds.includes(row.runId))) &&
6479 (filter?.messageIds === undefined || filter.messageIds.includes(row.id)) &&
6480 (filter?.messageRoles === undefined || filter.messageRoles.includes(row.role)),
6481 ),
6482 runs:
6483 filter?.runIds === undefined
6484 ? projection.runs
6485 : projection.runs.filter((row) => filter.runIds!.includes(row.id)),
6486 turnItems: projection.turnItems.filter(
6487 (row) =>
6488 (filter?.turnItemRunIds === undefined ||
6489 filter.turnItemRunIds.includes(row.runId)) &&
6490 (filter?.turnItemTypes === undefined || filter.turnItemTypes.includes(row.type)) &&
6491 (filter?.turnItemStatuses === undefined ||
6492 filter.turnItemStatuses.includes(row.status)) &&
6493 (filter?.turnItemRunId === undefined || filter.turnItemRunId === row.runId),
6494 ),
6495 nodes:
6496 filter?.omitSettledLeafNodes === true
6497 ? projection.nodes.filter((node) => !isSettledLeafNode(node, projection.runs))
6498 : projection.nodes,
6499 };
6500 return Object.fromEntries([
6501 ["thread", projection.thread],
6502 ...fields.map((field) => [field, selected[field]]),
6503 ]) as ProjectionRecords<(typeof fields)[number]>;
6504 }),
6505 getRuntimeRequest: (threadId, requestId) =>
6506 Effect.gen(function* () {
6507 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6508 if (projection === undefined)
6509 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6510 return projection.runtimeRequests.find((request) => request.id === requestId);
6511 }),
6512 getProviderControlContext: (threadId, target) =>
6513 Effect.gen(function* () {
6514 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6515 if (projection === undefined)
6516 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6517 const providerTurn = projection.providerTurns.find(
6518 (turn) => turn.id === target.providerTurnId,
6519 );
6520 return {
6521 providerThread: projection.providerThreads.find(
6522 (thread) => thread.id === target.providerThreadId,
6523 ),
6524 providerTurn,
6525 attempt:
6526 target.attemptId === undefined
6527 ? undefined
6528 : projection.attempts.find((attempt) => attempt.id === target.attemptId),
6529 message:
6530 target.messageId === undefined
6531 ? undefined
6532 : projection.messages.find((message) => message.id === target.messageId),
6533 run:
6534 target.messageId === undefined || providerTurn === undefined
6535 ? undefined
6536 : projection.runs.find((run) => run.activeAttemptId === providerTurn.runAttemptId),
6537 };
6538 }),
6539 getRunningTurnContext: (threadId) =>
6540 Effect.gen(function* () {
6541 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6542 if (projection === undefined)
6543 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6544 const run = projection.runs.find((run) => run.status === "running");
6545 return {
6546 run,
6547 providerThread: projection.providerThreads.find(
6548 (thread) => thread.id === run?.providerThreadId,
6549 ),
6550 providerTurn: projection.providerTurns.find(
6551 (turn) =>
6552 turn.providerThreadId === run?.providerThreadId &&
6553 turn.runAttemptId === run?.activeAttemptId &&
6554 turn.nodeId === run?.rootNodeId &&
6555 turn.status === "running",
6556 ),
6557 };
6558 }),
6559 getThreadProviderContext: (threadId, targetInstanceId) =>
6560 Effect.gen(function* () {
6561 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6562 if (projection === undefined)
6563 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6564 const target = projection.providerThreads
6565 .filter(
6566 (thread) =>
6567 thread.appThreadId === threadId &&
6568 thread.ownerNodeId === null &&
6569 thread.providerInstanceId === targetInstanceId,
6570 )
6571 .toSorted(
6572 (a, b) =>
6573 DateTime.toEpochMillis(b.updatedAt) - DateTime.toEpochMillis(a.updatedAt) ||
6574 a.id.localeCompare(b.id),
6575 )[0];
6576 return {
6577 thread: projection.thread,
6578 providerSessions: projection.providerSessions,
6579 providerThreads:
6580 targetInstanceId === undefined
6581 ? []
6582 : projection.providerThreads.filter(
6583 (thread) =>
6584 thread.id === projection.thread.activeProviderThreadId ||
6585 thread.id === target?.id,
6586 ),
6587 };
6588 }),
6589 getRuntimeResponseContext: (threadId, requestId) =>
6590 Effect.gen(function* () {
6591 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6592 if (projection === undefined)
6593 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6594 const request = projection.runtimeRequests.find((request) => request.id === requestId);
6595 return {
6596 request,
6597 node: projection.nodes.find((node) => node.id === request?.nodeId),
6598 item: projection.turnItems.find(
6599 (item) =>
6600 item.nodeId === request?.nodeId &&
6601 (item.type === "approval_request" || item.type === "user_input_request") &&
6602 item.requestId === requestId,
6603 ),
6604 session: projection.providerSessions.find(
6605 (session) =>
6606 request?.responseCapability.type === "live" &&
6607 session.id === request.responseCapability.providerSessionId,
6608 ),
6609 };
6610 }),
6611 getCheckpointContext: (threadId) =>
6612 Effect.gen(function* () {
6613 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6614 if (projection === undefined) {
6615 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6616 }
6617 return {
6618 runs: projection.runs.map(({ id, ordinal, status }) => ({ id, ordinal, status })),
6619 checkpointScopes: projection.checkpointScopes.map(({ id, runId, kind, cwd }) => ({
6620 id,
6621 runId,
6622 kind,
6623 cwd,
6624 })),
6625 checkpoints: projection.checkpoints.map(
6626 ({ scopeId, runId, appRunOrdinal, status, ref }) => ({
6627 scopeId,
6628 runId,
6629 appRunOrdinal,
6630 status,
6631 ref,
6632 }),
6633 ),
6634 };
6635 }),
6636 getCheckpointCaptureContext: (threadId, target) =>
6637 Effect.gen(function* () {
6638 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6639 if (projection === undefined)
6640 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6641 const run = projection.runs.find((candidate) => candidate.id === target.runId);
6642 return {
6643 run,
6644 rootNode: projection.nodes.find((candidate) => candidate.id === run?.rootNodeId),
6645 scope: projection.checkpointScopes.find((candidate) => candidate.id === target.scopeId),
6646 providerThread: projection.providerThreads.find(
6647 (candidate) => candidate.id === run?.providerThreadId,
6648 ),
6649 readyCheckpointOrdinals: projection.checkpoints
6650 .filter(
6651 (candidate) => candidate.scopeId === target.scopeId && candidate.status === "ready",
6652 )
6653 .map((candidate) => candidate.ordinalWithinScope),
6654 };
6655 }),
6656 getRunMessage: (threadId, runId) =>
6657 Effect.gen(function* () {
6658 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6659 if (projection === undefined)
6660 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6661 const run = projection.runs.find((candidate) => candidate.id === runId);
6662 return projection.messages.find((message) => message.id === run?.userMessageId);
6663 }),
6664 canStartQueuedRun: (threadId) =>
6665 Effect.gen(function* () {
6666 const projection = (yield* Ref.get(replayState)).projections.get(threadId);
6667 if (projection === undefined)
6668 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6669 return (
6670 projection.thread.archivedAt === null &&
6671 projection.thread.deletedAt === null &&
6672 projection.runs.some((run) => run.status === "queued") &&
6673 !projection.runs.some(
6674 (run) =>
6675 run.status === "preparing" ||
6676 run.status === "starting" ||
6677 run.status === "running" ||
6678 run.status === "waiting" ||
6679 (run.status === "queued" && run.queueHeld === true),
6680 )
6681 );
6682 }),
6683 getThreadProjection: (threadId) =>
6684 Effect.gen(function* () {
6685 const existing = (yield* Ref.get(replayState)).projections;
6686 const readProjection = (
6687 targetThreadId: ThreadId,
6688 seenThreadIds: ReadonlySet<ThreadId>,
6689 ): OrchestrationV2ThreadProjection | null => {
6690 const projection = existing.get(targetThreadId);
6691 if (!projection) {
6692 return null;
6693 }
6694 const forkedFrom = projection.thread.forkedFrom;
6695 if (forkedFrom?.type !== "run" || seenThreadIds.has(forkedFrom.threadId)) {
6696 return withLocalVisibleTurnItems(projection);
6697 }
6698 const sourceProjection = readProjection(
6699 forkedFrom.threadId,
6700 new Set([...seenThreadIds, targetThreadId]),
6701 );
6702 return {
6703 ...projection,
6704 visibleTurnItems: buildVisibleTurnItems({
6705 projection,
6706 sourceProjection,
6707 }),
6708 };
6709 };
6710 const projection = readProjection(threadId, new Set());
6711 if (!projection) {
6712 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6713 }
6714 return projection;
6715 }),
6716 getTurnStartContext: (threadId, runId) =>
6717 service.getThreadProjection(threadId).pipe(
6718 Effect.map((projection) => ({
6719 ...projection,
6720 hasConversation: projection.messages.some(
6721 (message) =>
6722 message.role === "user" &&
6723 (message.text.trim().toLowerCase() !== "/compact" ||
6724 message.attachments.length > 0),
6725 ),
6726 turnItems: projection.turnItems.filter((item) => item.runId === runId),
6727 })),
6728 ),
6729 getTurnStartHistory: (threadId, runIds) =>
6730 service
6731 .getThreadProjection(threadId)
6732 .pipe(
6733 Effect.map((projection) =>
6734 projection.turnItems.filter(
6735 (item) =>
6736 [
6737 "user_message",
6738 "assistant_message",
6739 "command_execution",
6740 "error",
6741 "run_interrupt_result",
6742 "file_change",
6743 "proposed_plan",
6744 ].includes(item.type) &&
6745 (runIds === undefined || (item.runId !== null && runIds.includes(item.runId))),
6746 ),
6747 ),
6748 ),
6749 getRuntimeRecoveryProjection: (threadId) => service.getThreadProjection(threadId),
6750 getThreadSnapshot: (threadId) =>
6751 service.getThreadProjection(threadId).pipe(
6752 Effect.flatMap((projection) =>
6753 Ref.get(sequence).pipe(
6754 Effect.map((snapshotSequence) => ({
6755 schemaVersion: ORCHESTRATION_V2_PROJECTION_SCHEMA_VERSION,
6756 snapshotSequence,
6757 projection,
6758 })),
6759 ),
6760 ),
6761 ),
6762 searchThreadStream: (input) => Stream.fromEffect(service.searchThread(input)),
6763 searchThread: (input) =>
6764 service.getThreadSnapshot(input.threadId).pipe(
6765 Effect.flatMap(({ projection, snapshotSequence }) =>
6766 projection.thread.deletedAt !== null
6767 ? Effect.fail(new ProjectionStoreThreadNotFoundError({ threadId: input.threadId }))
6768 : Effect.succeed(
6769 // This store has no project records, so only worktree paths resolve labels.
6770 findProjectedThreadItems(
6771 projection.visibleTurnItems,
6772 input,
6773 snapshotSequence,
6774 projection.thread.worktreePath ?? undefined,
6775 ),
6776 ),
6777 ),
6778 ),
6779 getThreadHistoryPage: (threadId, cursor, throughEntryId, conversationOnly) =>
6780 service.getThreadSnapshot(threadId).pipe(
6781 Effect.flatMap(({ projection, snapshotSequence }) =>
6782 Effect.gen(function* () {
6783 if (projection.thread.deletedAt !== null)
6784 return yield* new ProjectionStoreThreadNotFoundError({ threadId });
6785 return yield* Effect.try({
6786 try: () => {
6787 const page = selectHistoryPageFromCursor({
6788 items: projection.visibleTurnItems.map((row) => ({
6789 ...row,
6790 item: projectTurnItemForWire(row.item),
6791 })),
6792 cursor,
6793 throughEntryId,
6794 snapshotSequence,
6795 });
6796 return {
6797 ...page,
6798 snapshotSequence,
6799 items: conversationOnly
6800 ? page.items.filter((row) => isConversationHistoryItem(row.item))
6801 : page.items,
6802 };
6803 },
6804 catch: (cause) => new ProjectionStoreReadError({ threadId, cause }),
6805 });
6806 }),
6807 ),
6808 ),
6809 getTimelinePage: (threadId, options) =>
6810 service.getThreadProjection(threadId).pipe(
6811 Effect.map((projection) => {
6812 const matching = projection.visibleTurnItems.filter((row) =>
6813 options.itemId === undefined
6814 ? row.position > (options.afterPosition ?? -1) &&
6815 (options.view === "activity" ||
6816 ["user_message", "assistant_message", "proposed_plan"].includes(row.item.type))
6817 : row.sourceItemId === options.itemId,
6818 );
6819 return {
6820 items: matching.slice(0, options.limit),
6821 totalItems: projection.visibleTurnItems.length,
6822 hasMore: matching.length > options.limit,
6823 };
6824 }),
6825 ),
6826 getThreadSnapshotWindow: (threadId, options) =>
6827 service.getThreadSnapshot(threadId).pipe(
6828 Effect.map((snapshot) => {
6829 const anchorIndex =
6830 options.anchorItemId === undefined
6831 ? snapshot.projection.visibleTurnItems.length
6832 : snapshot.projection.visibleTurnItems.findIndex(
6833 (row) => row.sourceItemId === options.anchorItemId,
6834 ) + 1;
6835 const candidates = snapshot.projection.visibleTurnItems.slice(0, anchorIndex);
6836 const turnStarts = threadHistoryTurnStarts(candidates.map((row) => row.item));
6837 const turnAnchors =
6838 options.userTurnLimit === undefined
6839 ? []
6840 : candidates.flatMap((_row, index) => (turnStarts[index] ? [index] : []));
6841 const rawStart = turnAnchors.at(-(THREAD_HISTORY_MAX_RAW_TURNS + 2)) ?? 0;
6842 const anchors = turnAnchors.filter(
6843 (index) => index >= rawStart && isThreadHistoryUserTurn(candidates[index]!.item),
6844 );
6845 const anchorLimit = (options.userTurnLimit ?? 0) + 2;
6846 const start =
6847 anchors.length > 0
6848 ? anchors.length < anchorLimit
6849 ? rawStart
6850 : anchors.at(-anchorLimit)!
6851 : Math.max(0, anchorIndex - options.rowLimit);
6852 const visibleTurnItems = candidates.slice(start);
6853 return {
6854 ...snapshot,
6855 projection: { ...snapshot.projection, visibleTurnItems },
6856 };
6857 }),
6858 ),
6859 };
6860
6861 return service;
6862 }),
6863);