t3-code-android-nightly/docs/orchestration-v2/core-graph-and-data-model.md
Julius Marminge de34391427
feat(orchestrator): introduce new orchestrator (#2829)
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Bilal Bakr <62337003+Bil0000@users.noreply.github.com>
Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Vitalii Yehorov <vitalyiegorov@gmail.com>
Co-authored-by: Jake Leventhal <jakeleventhal@me.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Alex Southwell <saphid@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nicholas Wasmiller <derped@mineperial.com>
Co-authored-by: PB <poilmb@gmail.com>
Co-authored-by: Exotic <118054752+extoci@users.noreply.github.com>
Co-authored-by: Yash Singh <saiansh2525@gmail.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Theo Browne <me@t3.gg>
Co-authored-by: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com>
Co-authored-by: Dara Adedeji <76637177+SunkenInTime@users.noreply.github.com>
Co-authored-by: scratchyone <scratchywon@gmail.com>
Co-authored-by: Dominic Roy <dominic@sdko.org>
Co-authored-by: chukfinley <chuk@chuk.dev>
Co-authored-by: Primož Ajdišek <bigpod@bigpod.si>
Co-authored-by: benthecarman <benthecarman@live.com>
Co-authored-by: NaveDanan <nave0712@gmail.com>
Co-authored-by: aaditagrawal <103925638+aaditagrawal@users.noreply.github.com>
Co-authored-by: Aditya Garud <153842990+yashranaway@users.noreply.github.com>
Co-authored-by: Nick Anisimov <n.anisimov.23@gmail.com>
Co-authored-by: MacKinley Smith <smithmackinley@gmail.com>
Co-authored-by: Yordis Prieto <yordis.prieto@gmail.com>
Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com>
Co-authored-by: AKolenda <akole779@mtroyal.ca>
Co-authored-by: Guillermo Casanova <75276669+Gigioxx@users.noreply.github.com>
Co-authored-by: Otavio Salvador <otavio@ossystems.com.br>
Co-authored-by: Shirish Pothi <183252392+shirishpothi@users.noreply.github.com>
Co-authored-by: Ishaan Kothari <ishaanko.mail@gmail.com>
Co-authored-by: Bob Fowler <bob@rjf.ca>
Co-authored-by: Anton Bezdenezhnykh <gamer392@yandex.ru>
Co-authored-by: ValeraZSD <48602572+ValeraZSD@users.noreply.github.com>
Co-authored-by: Ephraim <ephraim39hr14m@gmail.com>
Co-authored-by: Ryan Ilano <ryanilano@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
Co-authored-by: maco <gosarmarcel7@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Tristan Knight <admin@snappeh.com>
Co-authored-by: PR Batch Tester <agent@local.test>
Co-authored-by: oliver <97427849+flamboh@users.noreply.github.com>
Co-authored-by: kamkm <99585688+Kamkmgamer@users.noreply.github.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2026-10-02 12:22:22 -07:00

23 KiB

Core Graph And Data Model

Overview

V2 models orchestration as a graph with a small number of durable entity types:

Project
  AppThread
    Run
      ExecutionNode tree
    ProviderThread handles
    Context transfers and handoffs
    Checkpoints

The central separation is:

  • AppThread: the user-visible conversation in T3 Code.
  • Run: a counted user-visible turn on an app thread.
  • ExecutionNode: a unit of provider/runtime work inside a run.
  • ProviderThread: a provider-native conversation handle.
  • ProviderSession: a live or resumable provider process/runtime.
  • ContextTransfer: a provider-neutral relationship/source-point record used by forks, provider switches, merge-back, and subagents.
  • ContextHandoff: a materialized portable context artifact used when native transfer is unavailable or insufficient.

Provider-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.

V2 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.

Every committed V2 event has a store-assigned monotonically increasing sequence. Projection snapshots and websocket streams use that sequence as the cursor boundary:

read snapshot at sequence N
  -> stream committed V2 events where sequence > N

The 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.

Entity Summary

AppThread
  id: ThreadId
  projectId: ProjectId
  title
  providerBinding
  activeProviderThreadId?
  lineage?
  status projection

Run
  id: RunId
  threadId: ThreadId
  ordinal: number
  status
  rootNodeId
  attempts[]
  providerThreadId
  contextHandoffId?
  userMessageId
  checkpoint?

ExecutionNode
  id: NodeId
  threadId: ThreadId
  runId: RunId | null
  parentNodeId: NodeId | null
  rootNodeId: NodeId
  kind
  status
  providerThreadId?
  providerTurnId?
  itemId?
  countsForRun
  checkpointScopeId?

CheckpointScope
  id: CheckpointScopeId
  threadId
  runId?
  nodeId
  parentScopeId?
  kind
  advancesAppRunCount

ProviderSession
  id: ProviderSessionId
  provider
  status
  cwd
  capabilities

ProviderThread
  id: ProviderThreadId
  providerSessionId?
  appThreadId?
  nativeThreadRef?
  nativeConversationHeadRef?
  coveredRunRange?
  contextHandoffIds[]
  forkSource?

ContextTransfer
  id: ContextTransferId
  type
  sourceThreadId
  targetThreadId
  sourcePoint
  basePoint?
  sourceProvider?
  targetProvider?
  status
  resolution?

ContextHandoff
  id: ContextHandoffId
  transferId
  kind
  payload
  status

ProviderTurn
  id: ProviderTurnId
  providerThreadId
  nativeTurnRef?
  nodeId
  runAttemptId?
  status

RuntimeRequest
  id: RuntimeRequestId
  nodeId
  providerRequestRef?
  kind
  status

AppThread

An 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.

type AppThread = {
  id: ThreadId;
  projectId: ProjectId;
  title: string;
  defaultProvider: ProviderKind;
  modelSelection: ModelSelection;
  runtimeMode: RuntimeMode;
  interactionMode: InteractionMode;
  branch: string | null;
  worktreePath: string | null;
  activeProviderThreadId: ProviderThreadId | null;
  lineage: AppThreadLineage | null;
  createdAt: string;
  updatedAt: string;
  archivedAt: string | null;
  deletedAt: string | null;
};

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.

defaultProvider is only the currently selected default for future runs. Historical runs retain their own provider and provider thread bindings.

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.

type AppThreadLineage = {
  parentThreadId: ThreadId | null;
  relationshipToParent: "fork" | "subagent" | null;
  rootThreadId: ThreadId;
};

Run

A Run is the counted user-visible turn. This replaces using provider turn ids as the app-level lifecycle boundary.

type Run = {
  id: RunId;
  threadId: ThreadId;
  ordinal: number;
  provider: ProviderKind;
  providerThreadId: ProviderThreadId | null;
  userMessageId: MessageId;
  rootNodeId: NodeId | null;
  activeAttemptId: RunAttemptId | null;
  status:
    | "queued"
    | "starting"
    | "running"
    | "waiting"
    | "completed"
    | "interrupted"
    | "failed"
    | "cancelled"
    | "rolled_back";
  requestedAt: string;
  startedAt: string | null;
  completedAt: string | null;
  checkpointId: CheckpointId | null;
  contextHandoffId: ContextHandoffId | null;
  sourcePlanRef?: {
    threadId: ThreadId;
    planId: ProposedPlanId;
  };
};

Only a run with countsForConversation = true contributes to the user-visible turn count and checkpoint count.

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.

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.

RunAttempt

A 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.

type RunAttempt = {
  id: RunAttemptId;
  runId: RunId;
  attemptOrdinal: number;
  rootNodeId: NodeId;
  provider: ProviderKind;
  providerThreadId: ProviderThreadId;
  providerTurnId: ProviderTurnId | null;
  reason: "initial" | "steering_restart" | "retry" | "provider_recovery";
  status:
    "pending" | "running" | "completed" | "interrupted" | "failed" | "cancelled" | "superseded";
  startedAt: string | null;
  completedAt: string | null;
};

Run 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.

Only one attempt is the final selected attempt for run completion and checkpointing. Superseded/interrupted attempts remain in the execution graph for audit/debugging.

ExecutionNode

An ExecutionNode is the generic unit of runtime work. It is the bridge between provider events and app behavior.

type ExecutionNode = {
  id: NodeId;
  threadId: ThreadId;
  runId: RunId | null;
  parentNodeId: NodeId | null;
  rootNodeId: NodeId;
  kind:
    | "root_turn"
    | "assistant_message"
    | "reasoning"
    | "plan"
    | "todo_list"
    | "tool_call"
    | "approval_request"
    | "user_input_request"
    | "subagent"
    | "hook"
    | "system";
  status:
    | "pending"
    | "running"
    | "waiting"
    | "completed"
    | "interrupted"
    | "failed"
    | "cancelled"
    | "rolled_back";
  countsForRun: boolean;
  providerThreadId: ProviderThreadId | null;
  providerTurnId: ProviderTurnId | null;
  nativeItemRef: ProviderRef | null;
  runtimeRequestId: RuntimeRequestId | null;
  checkpointScopeId: CheckpointScopeId | null;
  startedAt: string | null;
  completedAt: string | null;
};

The 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.

CheckpointScope

A CheckpointScope describes a unit of filesystem state that can be checkpointed. Root runs and child execution nodes can both have checkpoint scopes.

type CheckpointScope = {
  id: CheckpointScopeId;
  threadId: ThreadId;
  runId: RunId | null;
  nodeId: NodeId;
  parentScopeId: CheckpointScopeId | null;
  providerThreadId: ProviderThreadId | null;
  kind: "root_run" | "subagent" | "tool" | "provider_thread" | "manual";
  ordinalWithinParent: number;
  advancesAppRunCount: boolean;
  cwd: string;
  createdAt: string;
};

Root run scopes have advancesAppRunCount = true. Child scopes, such as subagents, have advancesAppRunCount = false and are nested under the parent run's scope.

ProviderSession

A ProviderSession is a live provider runtime process/session, such as a Codex app-server process or a Claude SDK session.

type ProviderSession = {
  id: ProviderSessionId;
  provider: ProviderKind;
  status: "starting" | "ready" | "running" | "waiting" | "stopped" | "error";
  cwd: string;
  model: string | null;
  capabilities: ProviderCapabilities;
  createdAt: string;
  updatedAt: string;
  lastError: string | null;
};

A 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.

The 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.

Provider 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.

ProviderThread

A ProviderThread is a provider-native conversation handle. It is addressable even when it is nested under a subagent execution node.

type ProviderThread = {
  id: ProviderThreadId;
  provider: ProviderKind;
  providerSessionId: ProviderSessionId | null;
  appThreadId: ThreadId | null;
  ownerNodeId: NodeId | null;
  nativeThreadRef: string | null;
  nativeConversationHeadRef: ProviderRef | null;
  status: "not_loaded" | "idle" | "active" | "archived" | "closed" | "error";
  firstRunOrdinal: number | null;
  lastRunOrdinal: number | null;
  handoffIds: ContextHandoffId[];
  forkedFrom: ProviderThreadForkSource | null;
  createdAt: string;
  updatedAt: string;
};

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.

A 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.

ContextTransfer

A 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.

type ContextTransfer = {
  id: ContextTransferId;
  type: "fork" | "provider_handoff" | "merge_back" | "subagent_spawn" | "subagent_result";
  sourceThreadId: ThreadId;
  targetThreadId: ThreadId;
  sourcePoint: ContextSourcePoint;
  basePoint: ContextSourcePoint | null;
  sourceProvider: ProviderKind | null;
  targetProvider: ProviderKind | null;
  status:
    "pending" | "resolved_native" | "resolved_portable" | "failed" | "consumed" | "superseded";
  resolution: ContextTransferResolution | null;
  createdBy: "user" | "agent" | "system";
  createdAt: string;
  consumedAt: string | null;
};
type ContextSourcePoint = {
  threadId: ThreadId;
  runId: RunId | null;
  checkpointId: CheckpointId | null;
  turnItemId: TurnItemId | null;
  providerThreadRef: NativeThreadRef | null;
  providerTurnRef: NativeTurnRef | null;
};

type ContextTransferResolution =
  | { strategy: "native_fork"; providerThreadRef: NativeThreadRef }
  | { strategy: "portable_context"; contextHandoffId: ContextHandoffId }
  | { strategy: "delta_context"; contextHandoffId: ContextHandoffId }
  | { strategy: "checkpoint_context"; contextHandoffId: ContextHandoffId };

Creating a transfer should be cheap. Expensive context handoffs are materialized lazily when the target run starts and the selected provider is known.

ContextHandoff

A 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.

In 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.

type ContextHandoff = {
  id: ContextHandoffId;
  transferId: ContextTransferId;
  threadId: ThreadId;
  targetRunId: RunId;
  fromProviderThreadIds: ProviderThreadId[];
  toProviderThreadId: ProviderThreadId;
  coveredRunOrdinals: {
    from: number;
    to: number;
  };
  strategy:
    | "delta_since_target_last_seen"
    | "full_thread_summary"
    | "checkpoint_summary"
    | "manual_context";
  status: "pending" | "ready" | "failed" | "superseded";
  summaryMessageId: MessageId | null;
  summaryText: string;
  createdByProvider: ProviderKind | null;
  createdAt: string;
  updatedAt: string;
};

The handoff is not just prompt text. It is part of the graph and can be inspected, regenerated, superseded, or audited.

The 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.

ProviderTurn

A ProviderTurn is the normalized handle for a provider-native turn.

type ProviderTurn = {
  id: ProviderTurnId;
  providerThreadId: ProviderThreadId;
  nodeId: NodeId;
  runAttemptId: RunAttemptId | null;
  nativeTurnRef: string | null;
  ordinal: number;
  status: "pending" | "running" | "completed" | "interrupted" | "failed" | "cancelled";
  startedAt: string | null;
  completedAt: string | null;
};

Codex has strong native turn ids. Weaker providers may only have ordinals. Both map into ProviderTurnId.

RuntimeRequest

Requests represent provider-originated callbacks that require app/user response.

type RuntimeRequest = {
  id: RuntimeRequestId;
  nodeId: NodeId;
  providerTurnId: ProviderTurnId | null;
  nativeRequestRef: string | null;
  kind:
    "command" | "file-read" | "file-change" | "dynamic_tool_call" | "user_input" | "auth_refresh";
  status: "pending" | "resolved" | "expired" | "cancelled";
  responseCapability:
    | { type: "live"; providerSessionId: ProviderSessionId }
    | { type: "not_resumable"; reason: string };
  createdAt: string;
  resolvedAt: string | null;
};

The permission kinds are the same canonical domain values used by V1 ProviderRequestKind. Adapters map provider-native callback names such as Codex item/commandExecution/requestApproval and Claude tool permission names into these app-level values.

Requests may remain visible after restart, but they are only respondable if their responseCapability is live.

Messages

Messages are part of the conversation projection, not the raw provider graph. They link back to runs and nodes when possible.

type ConversationMessage = {
  id: MessageId;
  threadId: ThreadId;
  runId: RunId | null;
  nodeId: NodeId | null;
  role: "user" | "assistant" | "system";
  text: string;
  attachments: Attachment[];
  streaming: boolean;
  createdAt: string;
  updatedAt: string;
};

Provider 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.

Plans, Questions, And Todo Lists

V2 treats these as structured turn items or execution nodes.

type PlanArtifact = {
  id: PlanId;
  threadId: ThreadId;
  runId: RunId | null;
  nodeId: NodeId;
  kind: "proposed_plan" | "todo_list" | "questions";
  status: "draft" | "active" | "completed" | "superseded";
  markdown?: string;
  steps?: Array<{ id: string; text: string; status: "pending" | "running" | "completed" }>;
  questions?: UserInputQuestion[];
};

Codex 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.

Checkpoint

Checkpoints 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.

type Checkpoint = {
  id: CheckpointId;
  threadId: ThreadId;
  scopeId: CheckpointScopeId;
  runId: RunId | null;
  nodeId: NodeId;
  parentCheckpointId: CheckpointId | null;
  ordinalWithinScope: number;
  appRunOrdinal: number | null;
  ref: string;
  status: "ready" | "missing" | "error" | "stale";
  files: CheckpointFileSummary[];
  capturedAt: string;
};

A 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.

Raw Provider Diagnostics

Raw 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.

type RawProviderEvent = {
  id: RawEventId;
  provider: ProviderKind;
  providerSessionId: ProviderSessionId;
  sequence: number;
  direction: "incoming" | "outgoing";
  messageKind: "request" | "response" | "notification" | "error";
  method: string | null;
  jsonRpcId: string | number | null;
  payload: unknown;
  observedAt: string;
};

No 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.

Normalized V2 entities should preserve enough correlation metadata to explain and route behavior:

  • provider kind.
  • provider session/thread/turn refs.
  • provider item/request refs when available.
  • native id strength.
  • diagnostic log location or raw frame reference when useful.

The replay framework can use raw provider transcripts as input without requiring production SQLite to store every raw frame.

Projections

V2 should expose separate projections:

  • Thread shell: fast sidebar list.
  • Thread detail: messages, runs, activities, checkpoints, plans.
  • Execution graph: debug/developer view of nodes and provider refs.
  • Provider sessions: live runtime/process state.
  • Pending requests: actionable approvals/user input.

The UI can remain simple while the graph remains precise.

Projection 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.

Turn Item Projection

The 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.

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.

Forked threads also expose a derived visibleTurnItems projection. turnItems remains the canonical items emitted on that app thread. visibleTurnItems is the user-visible ordered read model: it may reference inherited source items through the fork source point, include synthetic lifecycle markers such as "forked from conversation", and then continue with local target-thread items. This keeps storage normalized while giving thread detail views and tests one backend-owned surface to assert.

Known common tools should have structured item variants so the frontend can render stable custom components without parsing provider text:

type TurnItem =
  | UserMessage
  | AssistantMessage
  | Reasoning
  | Plan
  | FileChange
  | CommandExecution
  | FileSearch
  | WebSearch
  | ApprovalRequest
  | Checkpoint
  | Compaction
  | Handoff
  | Fork
  | DynamicTool;

Examples:

type FileChange = {
  type: "file_change";
  fileName: string;
  additions?: number;
  deletions?: number;
  diffStr?: string;
  oldStr?: string;
  newStr?: string;
};

type CommandExecution = {
  type: "command_execution";
  input: string;
  status: TurnItemStatus;
  output?: string;
  exitCode?: number;
};

type DynamicTool = {
  type: "dynamic_tool";
  toolName: string | null;
  input: unknown;
  output?: unknown;
};

Compaction, handoff, and fork are orchestration lifecycle items, not dynamic tools:

  • compaction: one item can transition from running with title like "Compacting context..." to completed with title like "Compacted context".
  • handoff: records the context bridge from one or more source provider threads/providers to a target provider thread/provider.
  • fork: records that the user or system created a new app thread from a run, node, or provider thread.

The 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.