t3-code-android-nightly/docs/orchestration-v2/entity-ids-and-correlation.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

6.9 KiB

Entity IDs And Correlation

Principle

Provider ids are evidence. App ids are identity.

Every durable entity uses an app-owned id as its primary key. Provider-native ids are stored as optional refs and used only for correlation, debugging, replay, and command routing.

ID Families

ThreadId              app-visible conversation thread
RunId                 counted app turn
NodeId                execution graph node
ProviderSessionId     live/resumable provider runtime session
ProviderThreadId      app handle for provider-native conversation
ProviderTurnId        app handle for provider-native turn
RuntimeRequestId      app handle for provider callback/request
RawEventId            optional raw provider diagnostic frame id
CheckpointId          app checkpoint id
PlanId                app plan/todo/question artifact id

Provider ids never replace these ids.

Provider Refs

Provider refs are metadata on runtime entities and events.

type ProviderRefs = {
  provider: ProviderKind;
  nativeSessionRef?: string;
  nativeThreadRef?: string;
  nativeTurnRef?: string;
  nativeItemRef?: string;
  nativeRequestRef?: string;
  rawEventId?: RawEventId;
  method?: string;
};

Codex can populate most of these fields. Other providers may populate only a subset.

Correlation Scope

Correlation is always scoped. Never match a native id globally without a provider/session/thread scope.

Preferred matching keys:

ProviderThread:
  provider + providerSessionId + nativeThreadRef

ProviderTurn:
  providerThreadId + nativeTurnRef
  or providerThreadId + turnOrdinal

Node / TurnItem:
  providerTurnId + nativeItemRef
  or providerTurnId + itemOrdinal

RuntimeRequest:
  providerSessionId + nativeRequestRef
  or providerTurnId + requestOrdinal

If a provider recycles ids, the provider adapter must widen the scope until the mapping is unambiguous.

Mapping Store

V2 needs a durable correlation table or equivalent event-sourced binding stream.

type IdentityBinding = {
  id: IdentityBindingId;
  appEntityKind: "provider_thread" | "provider_turn" | "node" | "item" | "request" | "message";
  appEntityId: string;
  provider: ProviderKind;
  providerSessionId: ProviderSessionId;
  nativeKind: string | null;
  nativeRef: string | null;
  scope: {
    threadId: ThreadId | null;
    runId: RunId | null;
    parentNodeId: NodeId | null;
    providerThreadId: ProviderThreadId | null;
    providerTurnId: ProviderTurnId | null;
  };
  correlation: "native_exact" | "native_scoped" | "ordinal" | "fingerprint" | "synthetic";
  firstRawEventId?: RawEventId;
  lastRawEventId?: RawEventId;
  createdAt: string;
  updatedAt: string;
};

This is not meant to be a large identity subsystem. It is the minimal durable place that says, "this native thing corresponds to this app thing." Raw event references are optional diagnostic pointers; the binding itself must not require production SQLite to retain raw provider frames forever.

ID Allocation Rules

  1. If the provider gives a stable native id, reuse the existing binding or allocate a new app id and bind it.
  2. If the provider gives no stable id, allocate by scoped ordinal.
  3. If ordinals are insufficient, add a fingerprint inside the scope.
  4. Once allocated, never change the app id for that entity.
  5. Never infer parent/root completion by string matching ids.

Scoped Ordinals

Weak providers need deterministic ordinals.

runOrdinalWithinThread
providerTurnOrdinalWithinProviderThread
nodeOrdinalWithinParent
itemOrdinalWithinProviderTurn
requestOrdinalWithinProviderTurn
messageOrdinalWithinNode

Ordinals are assigned by the normalizer when provider frames are processed. They must be replay-deterministic for a given provider transcript.

Fingerprints

Fingerprints are only fallback correlation tools. They should include scope and stable structure, not large mutable text.

Examples:

ProviderTurn fingerprint:
  providerThreadId + runId + providerTurnOrdinalWithinProviderThread

Node / TurnItem fingerprint:
  providerTurnId + itemKind + itemOrdinalWithinProviderTurn + toolName?

RuntimeRequest fingerprint:
  providerTurnId + requestKind + nativeItemRef? + requestOrdinalWithinProviderTurn

Do not use complete assistant text as a primary fingerprint. Text is mutable, can stream in chunks, and can be duplicated.

Runtime Events

Normalized runtime events should carry both app ids and provider refs.

type RuntimeEvent = {
  id: RuntimeEventId;
  type: RuntimeEventType;
  threadId: ThreadId;
  runId: RunId | null;
  nodeId: NodeId | null;
  parentNodeId: NodeId | null;
  providerSessionId: ProviderSessionId | null;
  providerThreadId: ProviderThreadId | null;
  providerTurnId: ProviderTurnId | null;
  nativeItemRef: string | null;
  requestId: RuntimeRequestId | null;
  providerRefs: ProviderRefs;
  payload: unknown;
  createdAt: string;
};

Downstream systems should use app ids. Provider refs are preserved for inspection and adapter routing.

Command Routing

UI and orchestration commands target app ids.

approval.respond(RuntimeRequestId)
interrupt.run(RunId)
interrupt.node(NodeId)
fork.fromNode(NodeId)
rollback.toRun(ThreadId, runOrdinal)

The provider command layer resolves app ids to provider refs at the edge. If the provider ref is missing or no longer live, the command fails with a typed capability/state error.

Examples:

RuntimeRequestId -> nativeRequestRef + providerSessionId
RunId -> root NodeId -> ProviderTurnId -> nativeTurnRef
NodeId -> ProviderThreadId -> nativeThreadRef

Pending Requests After Restart

Pending request records may survive restart, but provider callback state usually does not.

V2 should distinguish historical pending-looking requests from respondable requests:

type ResponseCapability =
  | { type: "live"; providerSessionId: ProviderSessionId }
  | { type: "not_resumable"; reason: string };

The UI can show that a request expired and the user must restart or rerun the turn.

Subagent Correlation

Subagents are represented through parent-child execution nodes.

For Codex:

collabAgentToolCall item
  -> ExecutionNode(kind="subagent" or "tool_call")
receiverThreadIds[]
  -> ProviderThread records
child turn/started and turn/completed
  -> child ProviderTurn and child ExecutionNode

The child provider turn keeps its own provider refs. It is linked to the parent through parentNodeId, not by replacing its turn id with the parent turn id.

For weak providers, the adapter may create a child node by ordinal under the active parent node.

Replay Determinism

Given the same provider transcript and same initial app state, normalization must produce the same ids.

Requirements:

  • Identity bindings are persisted as events or durable rows.
  • Generated ids can be random if the binding is persisted before downstream projection.
  • Fixture replay may use deterministic id generation to make assertions easier.
  • Reprocessing the same provider frame should find the existing binding, not allocate another entity.