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.
| 1 | ThreadId app-visible conversation thread |
| 2 | RunId counted app turn |
| 3 | NodeId execution graph node |
| 4 | ProviderSessionId live/resumable provider runtime session |
| 5 | ProviderThreadId app handle for provider-native conversation |
| 6 | ProviderTurnId app handle for provider-native turn |
| 7 | RuntimeRequestId app handle for provider callback/request |
| 8 | RawEventId optional raw provider diagnostic frame id |
| 9 | CheckpointId app checkpoint id |
| 10 | PlanId app plan/todo/question artifact id |
Provider ids never replace these ids.
Provider refs are metadata on runtime entities and events.
| 1 | type ProviderRefs = { |
| 2 | provider: ProviderKind; |
| 3 | nativeSessionRef?: string; |
| 4 | nativeThreadRef?: string; |
| 5 | nativeTurnRef?: string; |
| 6 | nativeItemRef?: string; |
| 7 | nativeRequestRef?: string; |
| 8 | rawEventId?: RawEventId; |
| 9 | method?: string; |
| 10 | }; |
Codex can populate most of these fields. Other providers may populate only a subset.
Correlation is always scoped. Never match a native id globally without a provider/session/thread scope.
Preferred matching keys:
| 1 | ProviderThread: |
| 2 | provider + providerSessionId + nativeThreadRef |
| 3 | |
| 4 | ProviderTurn: |
| 5 | providerThreadId + nativeTurnRef |
| 6 | or providerThreadId + turnOrdinal |
| 7 | |
| 8 | Node / TurnItem: |
| 9 | providerTurnId + nativeItemRef |
| 10 | or providerTurnId + itemOrdinal |
| 11 | |
| 12 | RuntimeRequest: |
| 13 | providerSessionId + nativeRequestRef |
| 14 | or providerTurnId + requestOrdinal |
If a provider recycles ids, the provider adapter must widen the scope until the mapping is unambiguous.
V2 needs a durable correlation table or equivalent event-sourced binding stream.
| 1 | type IdentityBinding = { |
| 2 | id: IdentityBindingId; |
| 3 | appEntityKind: "provider_thread" | "provider_turn" | "node" | "item" | "request" | "message"; |
| 4 | appEntityId: string; |
| 5 | provider: ProviderKind; |
| 6 | providerSessionId: ProviderSessionId; |
| 7 | nativeKind: string | null; |
| 8 | nativeRef: string | null; |
| 9 | scope: { |
| 10 | threadId: ThreadId | null; |
| 11 | runId: RunId | null; |
| 12 | parentNodeId: NodeId | null; |
| 13 | providerThreadId: ProviderThreadId | null; |
| 14 | providerTurnId: ProviderTurnId | null; |
| 15 | }; |
| 16 | correlation: "native_exact" | "native_scoped" | "ordinal" | "fingerprint" | "synthetic"; |
| 17 | firstRawEventId?: RawEventId; |
| 18 | lastRawEventId?: RawEventId; |
| 19 | createdAt: string; |
| 20 | updatedAt: string; |
| 21 | }; |
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.
Weak providers need deterministic ordinals.
| 1 | runOrdinalWithinThread |
| 2 | providerTurnOrdinalWithinProviderThread |
| 3 | nodeOrdinalWithinParent |
| 4 | itemOrdinalWithinProviderTurn |
| 5 | requestOrdinalWithinProviderTurn |
| 6 | messageOrdinalWithinNode |
Ordinals are assigned by the normalizer when provider frames are processed. They must be replay-deterministic for a given provider transcript.
Fingerprints are only fallback correlation tools. They should include scope and stable structure, not large mutable text.
Examples:
| 1 | ProviderTurn fingerprint: |
| 2 | providerThreadId + runId + providerTurnOrdinalWithinProviderThread |
| 3 | |
| 4 | Node / TurnItem fingerprint: |
| 5 | providerTurnId + itemKind + itemOrdinalWithinProviderTurn + toolName? |
| 6 | |
| 7 | RuntimeRequest fingerprint: |
| 8 | 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.
Normalized runtime events should carry both app ids and provider refs.
| 1 | type RuntimeEvent = { |
| 2 | id: RuntimeEventId; |
| 3 | type: RuntimeEventType; |
| 4 | threadId: ThreadId; |
| 5 | runId: RunId | null; |
| 6 | nodeId: NodeId | null; |
| 7 | parentNodeId: NodeId | null; |
| 8 | providerSessionId: ProviderSessionId | null; |
| 9 | providerThreadId: ProviderThreadId | null; |
| 10 | providerTurnId: ProviderTurnId | null; |
| 11 | nativeItemRef: string | null; |
| 12 | requestId: RuntimeRequestId | null; |
| 13 | providerRefs: ProviderRefs; |
| 14 | payload: unknown; |
| 15 | createdAt: string; |
| 16 | }; |
Downstream systems should use app ids. Provider refs are preserved for inspection and adapter routing.
UI and orchestration commands target app ids.
| 1 | approval.respond(RuntimeRequestId) |
| 2 | interrupt.run(RunId) |
| 3 | interrupt.node(NodeId) |
| 4 | fork.fromNode(NodeId) |
| 5 | 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:
| 1 | RuntimeRequestId -> nativeRequestRef + providerSessionId |
| 2 | RunId -> root NodeId -> ProviderTurnId -> nativeTurnRef |
| 3 | NodeId -> ProviderThreadId -> nativeThreadRef |
Pending request records may survive restart, but provider callback state usually does not.
V2 should distinguish historical pending-looking requests from respondable requests:
| 1 | type ResponseCapability = |
| 2 | | { type: "live"; providerSessionId: ProviderSessionId } |
| 3 | | { type: "not_resumable"; reason: string }; |
The UI can show that a request expired and the user must restart or rerun the turn.
Subagents are represented through parent-child execution nodes.
For Codex:
| 1 | collabAgentToolCall item |
| 2 | -> ExecutionNode(kind="subagent" or "tool_call") |
| 3 | receiverThreadIds[] |
| 4 | -> ProviderThread records |
| 5 | child turn/started and turn/completed |
| 6 | -> 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.
Given the same provider transcript and same initial app state, normalization must produce the same ids.
Requirements: