1# Core Graph And Data Model
2
3## Overview
4
5V2 models orchestration as a graph with a small number of durable entity types:
6
7```text
8Project
9 AppThread
10 Run
11 ExecutionNode tree
12 ProviderThread handles
13 Context transfers and handoffs
14 Checkpoints
15```
16
17The central separation is:
18
19- `AppThread`: the user-visible conversation in T3 Code.
20- `Run`: a counted user-visible turn on an app thread.
21- `ExecutionNode`: a unit of provider/runtime work inside a run.
22- `ProviderThread`: a provider-native conversation handle.
23- `ProviderSession`: a live or resumable provider process/runtime.
24- `ContextTransfer`: a provider-neutral relationship/source-point record used by forks, provider switches, merge-back, and subagents.
25- `ContextHandoff`: a materialized portable context artifact used when native transfer is unavailable or insufficient.
26
27Provider-specific lifecycle is preserved in provider refs and diagnostic raw-provider logs. App behavior is driven by app-owned ids, V2-native orchestration events, and graph relationships.
28
29V2 events are durable because they are the app state transition log. Raw provider frames are not durable app state. They may be attached by optional correlation ids such as `rawEventId`, but the canonical durable row is the normalized V2 event.
30
31Every committed V2 event has a store-assigned monotonically increasing `sequence`. Projection snapshots and websocket streams use that sequence as the cursor boundary:
32
33```text
34read snapshot at sequence N
35 -> stream committed V2 events where sequence > N
36```
37
38The sequence belongs to the stored event envelope, not the provider event and not the domain payload. Provider-native ordering is preserved separately through provider refs and diagnostic logs. Clients should treat the snapshot's `snapshotSequence` as the authoritative cursor and apply only subsequent events with a greater sequence.
39
40## Entity Summary
41
42```text
43AppThread
44 id: ThreadId
45 projectId: ProjectId
46 title
47 providerBinding
48 activeProviderThreadId?
49 lineage?
50 status projection
51
52Run
53 id: RunId
54 threadId: ThreadId
55 ordinal: number
56 status
57 rootNodeId
58 attempts[]
59 providerThreadId
60 contextHandoffId?
61 userMessageId
62 checkpoint?
63
64ExecutionNode
65 id: NodeId
66 threadId: ThreadId
67 runId: RunId | null
68 parentNodeId: NodeId | null
69 rootNodeId: NodeId
70 kind
71 status
72 providerThreadId?
73 providerTurnId?
74 itemId?
75 countsForRun
76 checkpointScopeId?
77
78CheckpointScope
79 id: CheckpointScopeId
80 threadId
81 runId?
82 nodeId
83 parentScopeId?
84 kind
85 advancesAppRunCount
86
87ProviderSession
88 id: ProviderSessionId
89 provider
90 status
91 cwd
92 capabilities
93
94ProviderThread
95 id: ProviderThreadId
96 providerSessionId?
97 appThreadId?
98 nativeThreadRef?
99 nativeConversationHeadRef?
100 coveredRunRange?
101 contextHandoffIds[]
102 forkSource?
103
104ContextTransfer
105 id: ContextTransferId
106 type
107 sourceThreadId
108 targetThreadId
109 sourcePoint
110 basePoint?
111 sourceProvider?
112 targetProvider?
113 status
114 resolution?
115
116ContextHandoff
117 id: ContextHandoffId
118 transferId
119 kind
120 payload
121 status
122
123ProviderTurn
124 id: ProviderTurnId
125 providerThreadId
126 nativeTurnRef?
127 nodeId
128 runAttemptId?
129 status
130
131RuntimeRequest
132 id: RuntimeRequestId
133 nodeId
134 providerRequestRef?
135 kind
136 status
137```
138
139## AppThread
140
141An `AppThread` is the stable user-facing conversation. It owns messages, runs, checkpoints, and UI state. It does not have to map one-to-one with a provider-native thread forever.
142
143```ts
144type AppThread = {
145 id: ThreadId;
146 projectId: ProjectId;
147 title: string;
148 defaultProvider: ProviderKind;
149 modelSelection: ModelSelection;
150 runtimeMode: RuntimeMode;
151 interactionMode: InteractionMode;
152 branch: string | null;
153 worktreePath: string | null;
154 activeProviderThreadId: ProviderThreadId | null;
155 lineage: AppThreadLineage | null;
156 createdAt: string;
157 updatedAt: string;
158 archivedAt: string | null;
159 deletedAt: string | null;
160};
161```
162
163`activeProviderThreadId` points to the provider-native conversation currently backing the app thread. Forking, provider migration, or recovery may create new provider threads while preserving the same app thread, depending on the operation.
164
165`defaultProvider` is only the currently selected default for future runs. Historical runs retain their own provider and provider thread bindings.
166
167`lineage` is intentionally lightweight browsing metadata. Operational source points, lazy native fork resolution, portable context handoffs, merge-back deltas, and subagent result transfers live in `ContextTransfer` rows because an app thread can participate in more than one transfer over time.
168
169```ts
170type AppThreadLineage = {
171 parentThreadId: ThreadId | null;
172 relationshipToParent: "fork" | "subagent" | null;
173 rootThreadId: ThreadId;
174};
175```
176
177## Run
178
179A `Run` is the counted user-visible turn. This replaces using provider turn ids as the app-level lifecycle boundary.
180
181```ts
182type Run = {
183 id: RunId;
184 threadId: ThreadId;
185 ordinal: number;
186 provider: ProviderKind;
187 providerThreadId: ProviderThreadId | null;
188 userMessageId: MessageId;
189 rootNodeId: NodeId | null;
190 activeAttemptId: RunAttemptId | null;
191 status:
192 | "queued"
193 | "starting"
194 | "running"
195 | "waiting"
196 | "completed"
197 | "interrupted"
198 | "failed"
199 | "cancelled"
200 | "rolled_back";
201 requestedAt: string;
202 startedAt: string | null;
203 completedAt: string | null;
204 checkpointId: CheckpointId | null;
205 contextHandoffId: ContextHandoffId | null;
206 sourcePlanRef?: {
207 threadId: ThreadId;
208 planId: ProposedPlanId;
209 };
210};
211```
212
213Only a run with `countsForConversation = true` contributes to the user-visible turn count and checkpoint count.
214
215`providerThreadId` is the provider-native conversation used for this run. This makes mixed-provider app threads explicit: run 1 may be Codex, run 2 may be Claude, and run 3 may return to the original Codex provider thread.
216
217`contextHandoffId` points to a materialized handoff artifact consumed by this run. A run may also be associated with a broader `ContextTransfer` through the transfer's target/source fields. The handoff is the payload; the transfer is the durable relationship and policy record.
218
219## RunAttempt
220
221A `RunAttempt` represents one provider execution attempt for an app run. Most runs have exactly one attempt. Steering, retries, provider recovery, or provider-switch recovery may create more than one attempt.
222
223```ts
224type RunAttempt = {
225 id: RunAttemptId;
226 runId: RunId;
227 attemptOrdinal: number;
228 rootNodeId: NodeId;
229 provider: ProviderKind;
230 providerThreadId: ProviderThreadId;
231 providerTurnId: ProviderTurnId | null;
232 reason: "initial" | "steering_restart" | "retry" | "provider_recovery";
233 status:
234 "pending" | "running" | "completed" | "interrupted" | "failed" | "cancelled" | "superseded";
235 startedAt: string | null;
236 completedAt: string | null;
237};
238```
239
240Run attempts let app-level steering work even for providers that cannot steer an active native turn. The app can interrupt the active provider turn and create a replacement attempt under the same `RunId`.
241
242Only one attempt is the final selected attempt for run completion and checkpointing. Superseded/interrupted attempts remain in the execution graph for audit/debugging.
243
244## ExecutionNode
245
246An `ExecutionNode` is the generic unit of runtime work. It is the bridge between provider events and app behavior.
247
248```ts
249type ExecutionNode = {
250 id: NodeId;
251 threadId: ThreadId;
252 runId: RunId | null;
253 parentNodeId: NodeId | null;
254 rootNodeId: NodeId;
255 kind:
256 | "root_turn"
257 | "assistant_message"
258 | "reasoning"
259 | "plan"
260 | "todo_list"
261 | "tool_call"
262 | "approval_request"
263 | "user_input_request"
264 | "subagent"
265 | "hook"
266 | "system";
267 status:
268 | "pending"
269 | "running"
270 | "waiting"
271 | "completed"
272 | "interrupted"
273 | "failed"
274 | "cancelled"
275 | "rolled_back";
276 countsForRun: boolean;
277 providerThreadId: ProviderThreadId | null;
278 providerTurnId: ProviderTurnId | null;
279 nativeItemRef: ProviderRef | null;
280 runtimeRequestId: RuntimeRequestId | null;
281 checkpointScopeId: CheckpointScopeId | null;
282 startedAt: string | null;
283 completedAt: string | null;
284};
285```
286
287The root node of a run is the only node allowed to complete the run. Subagent nodes, tool nodes, approval nodes, and plan nodes may complete independently.
288
289## CheckpointScope
290
291A `CheckpointScope` describes a unit of filesystem state that can be checkpointed. Root runs and child execution nodes can both have checkpoint scopes.
292
293```ts
294type CheckpointScope = {
295 id: CheckpointScopeId;
296 threadId: ThreadId;
297 runId: RunId | null;
298 nodeId: NodeId;
299 parentScopeId: CheckpointScopeId | null;
300 providerThreadId: ProviderThreadId | null;
301 kind: "root_run" | "subagent" | "tool" | "provider_thread" | "manual";
302 ordinalWithinParent: number;
303 advancesAppRunCount: boolean;
304 cwd: string;
305 createdAt: string;
306};
307```
308
309Root run scopes have `advancesAppRunCount = true`. Child scopes, such as subagents, have `advancesAppRunCount = false` and are nested under the parent run's scope.
310
311## ProviderSession
312
313A `ProviderSession` is a live provider runtime process/session, such as a Codex app-server process or a Claude SDK session.
314
315```ts
316type ProviderSession = {
317 id: ProviderSessionId;
318 provider: ProviderKind;
319 status: "starting" | "ready" | "running" | "waiting" | "stopped" | "error";
320 cwd: string;
321 model: string | null;
322 capabilities: ProviderCapabilities;
323 createdAt: string;
324 updatedAt: string;
325 lastError: string | null;
326};
327```
328
329A provider session may host one or more provider threads if the provider supports it. If a provider only supports one active conversation per process, V2 still models that as one provider thread attached to the session.
330
331The session entity is durable metadata for a live-or-recoverable runtime, not the runtime handle itself. The in-memory process/client handle may disappear because the server restarted, an idle reaper released it, or the provider process crashed. In all cases, the app keeps the `ProviderSession`, `ProviderThread`, and native resume refs in durable state, then recreates the live runtime through the normal provider session manager path.
332
333Provider runtime ids and ordinals must not depend on adapter process memory. If a value is persisted or must survive recovery, it is allocated by the orchestrator/correlation/id services or derived from durable provider refs.
334
335## ProviderThread
336
337A `ProviderThread` is a provider-native conversation handle. It is addressable even when it is nested under a subagent execution node.
338
339```ts
340type ProviderThread = {
341 id: ProviderThreadId;
342 provider: ProviderKind;
343 providerSessionId: ProviderSessionId | null;
344 appThreadId: ThreadId | null;
345 ownerNodeId: NodeId | null;
346 nativeThreadRef: string | null;
347 nativeConversationHeadRef: ProviderRef | null;
348 status: "not_loaded" | "idle" | "active" | "archived" | "closed" | "error";
349 firstRunOrdinal: number | null;
350 lastRunOrdinal: number | null;
351 handoffIds: ContextHandoffId[];
352 forkedFrom: ProviderThreadForkSource | null;
353 createdAt: string;
354 updatedAt: string;
355};
356```
357
358`appThreadId` is set when the provider thread backs a first-class app thread. `ownerNodeId` is set when the provider thread is nested under an execution node, such as a subagent. A provider thread can later be forked or promoted into a first-class app thread.
359
360A provider thread may have gaps in its native run coverage. Example: runs 1-5 use Codex provider thread A, runs 6-8 use Claude provider thread B, and run 9 returns to Codex thread A with a handoff summary covering runs 6-8. In that case, Codex thread A remains the same provider thread, but it has an explicit `ContextHandoff` before run 9.
361
362## ContextTransfer
363
364A `ContextTransfer` records a source/target relationship and how context should move between them. It is the shared primitive for user forks, provider switching, merge-back, and subagents.
365
366```ts
367type ContextTransfer = {
368 id: ContextTransferId;
369 type: "fork" | "provider_handoff" | "merge_back" | "subagent_spawn" | "subagent_result";
370 sourceThreadId: ThreadId;
371 targetThreadId: ThreadId;
372 sourcePoint: ContextSourcePoint;
373 basePoint: ContextSourcePoint | null;
374 sourceProvider: ProviderKind | null;
375 targetProvider: ProviderKind | null;
376 status:
377 "pending" | "resolved_native" | "resolved_portable" | "failed" | "consumed" | "superseded";
378 resolution: ContextTransferResolution | null;
379 createdBy: "user" | "agent" | "system";
380 createdAt: string;
381 consumedAt: string | null;
382};
383```
384
385```ts
386type ContextSourcePoint = {
387 threadId: ThreadId;
388 runId: RunId | null;
389 checkpointId: CheckpointId | null;
390 turnItemId: TurnItemId | null;
391 providerThreadRef: NativeThreadRef | null;
392 providerTurnRef: NativeTurnRef | null;
393};
394
395type ContextTransferResolution =
396 | { strategy: "native_fork"; providerThreadRef: NativeThreadRef }
397 | { strategy: "portable_context"; contextHandoffId: ContextHandoffId }
398 | { strategy: "delta_context"; contextHandoffId: ContextHandoffId }
399 | { strategy: "checkpoint_context"; contextHandoffId: ContextHandoffId };
400```
401
402Creating a transfer should be cheap. Expensive context handoffs are materialized lazily when the target run starts and the selected provider is known.
403
404## ContextHandoff
405
406A `ContextHandoff` is a first-class artifact created when portable provider context must be bridged. It is most common when changing providers between runs, but it also applies when provider resume fails and a replacement provider thread must be seeded from app history.
407
408In the broader transfer model, `ContextTransfer` records source, target, and lifecycle. `ContextHandoff` is the materialized payload consumed by a run when native transfer cannot satisfy the relationship.
409
410```ts
411type ContextHandoff = {
412 id: ContextHandoffId;
413 transferId: ContextTransferId;
414 threadId: ThreadId;
415 targetRunId: RunId;
416 fromProviderThreadIds: ProviderThreadId[];
417 toProviderThreadId: ProviderThreadId;
418 coveredRunOrdinals: {
419 from: number;
420 to: number;
421 };
422 strategy:
423 | "delta_since_target_last_seen"
424 | "full_thread_summary"
425 | "checkpoint_summary"
426 | "manual_context";
427 status: "pending" | "ready" | "failed" | "superseded";
428 summaryMessageId: MessageId | null;
429 summaryText: string;
430 createdByProvider: ProviderKind | null;
431 createdAt: string;
432 updatedAt: string;
433};
434```
435
436The handoff is not just prompt text. It is part of the graph and can be inspected, regenerated, superseded, or audited.
437
438The preferred return-to-provider strategy is `delta_since_target_last_seen`: resume the previous provider thread and summarize only the runs that happened while that provider was inactive. Use `full_thread_summary` when resume fails, provider settings are incompatible, or the accumulated handoffs would create poor context quality.
439
440## ProviderTurn
441
442A `ProviderTurn` is the normalized handle for a provider-native turn.
443
444```ts
445type ProviderTurn = {
446 id: ProviderTurnId;
447 providerThreadId: ProviderThreadId;
448 nodeId: NodeId;
449 runAttemptId: RunAttemptId | null;
450 nativeTurnRef: string | null;
451 ordinal: number;
452 status: "pending" | "running" | "completed" | "interrupted" | "failed" | "cancelled";
453 startedAt: string | null;
454 completedAt: string | null;
455};
456```
457
458Codex has strong native turn ids. Weaker providers may only have ordinals. Both map into `ProviderTurnId`.
459
460## RuntimeRequest
461
462Requests represent provider-originated callbacks that require app/user response.
463
464```ts
465type RuntimeRequest = {
466 id: RuntimeRequestId;
467 nodeId: NodeId;
468 providerTurnId: ProviderTurnId | null;
469 nativeRequestRef: string | null;
470 kind:
471 "command" | "file-read" | "file-change" | "dynamic_tool_call" | "user_input" | "auth_refresh";
472 status: "pending" | "resolved" | "expired" | "cancelled";
473 responseCapability:
474 | { type: "live"; providerSessionId: ProviderSessionId }
475 | { type: "not_resumable"; reason: string };
476 createdAt: string;
477 resolvedAt: string | null;
478};
479```
480
481The permission kinds are the same canonical domain values used by V1 `ProviderRequestKind`.
482Adapters map provider-native callback names such as Codex `item/commandExecution/requestApproval`
483and Claude tool permission names into these app-level values.
484
485Requests may remain visible after restart, but they are only respondable if their `responseCapability` is live.
486
487## Messages
488
489Messages are part of the conversation projection, not the raw provider graph. They link back to runs and nodes when possible.
490
491```ts
492type ConversationMessage = {
493 id: MessageId;
494 threadId: ThreadId;
495 runId: RunId | null;
496 nodeId: NodeId | null;
497 role: "user" | "assistant" | "system";
498 text: string;
499 attachments: Attachment[];
500 streaming: boolean;
501 createdAt: string;
502 updatedAt: string;
503};
504```
505
506Provider message chunks are collected through items/content events and projected into messages. The projection may hide child/subagent messages by default while preserving them in the graph.
507
508## Plans, Questions, And Todo Lists
509
510V2 treats these as structured turn items or execution nodes.
511
512```ts
513type PlanArtifact = {
514 id: PlanId;
515 threadId: ThreadId;
516 runId: RunId | null;
517 nodeId: NodeId;
518 kind: "proposed_plan" | "todo_list" | "questions";
519 status: "draft" | "active" | "completed" | "superseded";
520 markdown?: string;
521 steps?: Array<{ id: string; text: string; status: "pending" | "running" | "completed" }>;
522 questions?: UserInputQuestion[];
523};
524```
525
526Codex `turn/plan/updated` maps to a `todo_list` artifact. Plan-mode final plan items map to `proposed_plan`. User-input question requests map to `questions` plus a `RuntimeRequest`.
527
528## Checkpoint
529
530Checkpoints attach to checkpoint scopes. A root run checkpoint is the user-visible conversation checkpoint. A child checkpoint records nested filesystem state for a subagent, tool, or provider thread without advancing the parent run count.
531
532```ts
533type Checkpoint = {
534 id: CheckpointId;
535 threadId: ThreadId;
536 scopeId: CheckpointScopeId;
537 runId: RunId | null;
538 nodeId: NodeId;
539 parentCheckpointId: CheckpointId | null;
540 ordinalWithinScope: number;
541 appRunOrdinal: number | null;
542 ref: string;
543 status: "ready" | "missing" | "error" | "stale";
544 files: CheckpointFileSummary[];
545 capturedAt: string;
546};
547```
548
549A child/subagent provider turn can create a child checkpoint. It does not create an app-run checkpoint unless it is running as a first-class app run in a forked/promoted thread.
550
551## Raw Provider Diagnostics
552
553Raw provider diagnostics are append-only evidence for debugging, support, and replay fixture generation. They should be written to bounded rotating log files, not treated as canonical durable SQLite state.
554
555```ts
556type RawProviderEvent = {
557 id: RawEventId;
558 provider: ProviderKind;
559 providerSessionId: ProviderSessionId;
560 sequence: number;
561 direction: "incoming" | "outgoing";
562 messageKind: "request" | "response" | "notification" | "error";
563 method: string | null;
564 jsonRpcId: string | number | null;
565 payload: unknown;
566 observedAt: string;
567};
568```
569
570No domain behavior should depend on parsing historic UI events if raw provider diagnostics are available. Production behavior should depend on normalized orchestration events/entities and provider refs. Raw provider frames are evidence and replay input, not the source of truth for normal app state.
571
572Normalized V2 entities should preserve enough correlation metadata to explain and route behavior:
573
574- provider kind.
575- provider session/thread/turn refs.
576- provider item/request refs when available.
577- native id strength.
578- diagnostic log location or raw frame reference when useful.
579
580The replay framework can use raw provider transcripts as input without requiring production SQLite to store every raw frame.
581
582## Projections
583
584V2 should expose separate projections:
585
586- Thread shell: fast sidebar list.
587- Thread detail: messages, runs, activities, checkpoints, plans.
588- Execution graph: debug/developer view of nodes and provider refs.
589- Provider sessions: live runtime/process state.
590- Pending requests: actionable approvals/user input.
591
592The UI can remain simple while the graph remains precise.
593
594Projection streaming should use the existing app snapshot-plus-cursor contract. A thread-detail subscription should return a projection snapshot at sequence `N`, then stream only events after `N`. The frontend should not receive events already reflected in the snapshot. Reconnects may reset from a fresh snapshot instead of replaying local client state; the fresh snapshot and its `snapshotSequence` become the new cursor boundary.
595
596## Turn Item Projection
597
598The frontend should not reconstruct display order by merging `messages`, `plans`, checkpoints, approvals, and activities itself. V2 exposes an ordered `turnItems` projection for thread detail rendering.
599
600`turnItems` are projection records, not the canonical source of truth. The canonical graph remains normalized in runs, attempts, nodes, requests, messages, plans, and checkpoints. Provider-native item refs live directly on nodes and turn items where correlation is needed.
601
602Forked threads also expose a derived `visibleTurnItems` projection. `turnItems` remains the canonical
603items emitted on that app thread. `visibleTurnItems` is the user-visible ordered read model: it may
604reference inherited source items through the fork source point, include synthetic lifecycle markers
605such as "forked from conversation", and then continue with local target-thread items. This keeps
606storage normalized while giving thread detail views and tests one backend-owned surface to assert.
607
608Known common tools should have structured item variants so the frontend can render stable custom components without parsing provider text:
609
610```ts
611type TurnItem =
612 | UserMessage
613 | AssistantMessage
614 | Reasoning
615 | Plan
616 | FileChange
617 | CommandExecution
618 | FileSearch
619 | WebSearch
620 | ApprovalRequest
621 | Checkpoint
622 | Compaction
623 | Handoff
624 | Fork
625 | DynamicTool;
626```
627
628Examples:
629
630```ts
631type FileChange = {
632 type: "file_change";
633 fileName: string;
634 additions?: number;
635 deletions?: number;
636 diffStr?: string;
637 oldStr?: string;
638 newStr?: string;
639};
640
641type CommandExecution = {
642 type: "command_execution";
643 input: string;
644 status: TurnItemStatus;
645 output?: string;
646 exitCode?: number;
647};
648
649type DynamicTool = {
650 type: "dynamic_tool";
651 toolName: string | null;
652 input: unknown;
653 output?: unknown;
654};
655```
656
657Compaction, handoff, and fork are orchestration lifecycle items, not dynamic tools:
658
659- `compaction`: one item can transition from `running` with title like "Compacting context..." to `completed` with title like "Compacted context".
660- `handoff`: records the context bridge from one or more source provider threads/providers to a target provider thread/provider.
661- `fork`: records that the user or system created a new app thread from a run, node, or provider thread.
662
663The UI can render known variants with deterministic components and render `dynamic_tool` as expandable JSON input/output. Each turn item keeps refs back to `runId`, `nodeId`, `providerTurnId`, and `nativeItemRef` so debug views can jump from the display stream back into the graph and provider logs.