1# Entity IDs And Correlation
2
3## Principle
4
5Provider ids are evidence. App ids are identity.
6
7Every 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.
8
9## ID Families
10
11```text
12ThreadId app-visible conversation thread
13RunId counted app turn
14NodeId execution graph node
15ProviderSessionId live/resumable provider runtime session
16ProviderThreadId app handle for provider-native conversation
17ProviderTurnId app handle for provider-native turn
18RuntimeRequestId app handle for provider callback/request
19RawEventId optional raw provider diagnostic frame id
20CheckpointId app checkpoint id
21PlanId app plan/todo/question artifact id
22```
23
24Provider ids never replace these ids.
25
26## Provider Refs
27
28Provider refs are metadata on runtime entities and events.
29
30```ts
31type ProviderRefs = {
32 provider: ProviderKind;
33 nativeSessionRef?: string;
34 nativeThreadRef?: string;
35 nativeTurnRef?: string;
36 nativeItemRef?: string;
37 nativeRequestRef?: string;
38 rawEventId?: RawEventId;
39 method?: string;
40};
41```
42
43Codex can populate most of these fields. Other providers may populate only a subset.
44
45## Correlation Scope
46
47Correlation is always scoped. Never match a native id globally without a provider/session/thread scope.
48
49Preferred matching keys:
50
51```text
52ProviderThread:
53 provider + providerSessionId + nativeThreadRef
54
55ProviderTurn:
56 providerThreadId + nativeTurnRef
57 or providerThreadId + turnOrdinal
58
59Node / TurnItem:
60 providerTurnId + nativeItemRef
61 or providerTurnId + itemOrdinal
62
63RuntimeRequest:
64 providerSessionId + nativeRequestRef
65 or providerTurnId + requestOrdinal
66```
67
68If a provider recycles ids, the provider adapter must widen the scope until the mapping is unambiguous.
69
70## Mapping Store
71
72V2 needs a durable correlation table or equivalent event-sourced binding stream.
73
74```ts
75type IdentityBinding = {
76 id: IdentityBindingId;
77 appEntityKind: "provider_thread" | "provider_turn" | "node" | "item" | "request" | "message";
78 appEntityId: string;
79 provider: ProviderKind;
80 providerSessionId: ProviderSessionId;
81 nativeKind: string | null;
82 nativeRef: string | null;
83 scope: {
84 threadId: ThreadId | null;
85 runId: RunId | null;
86 parentNodeId: NodeId | null;
87 providerThreadId: ProviderThreadId | null;
88 providerTurnId: ProviderTurnId | null;
89 };
90 correlation: "native_exact" | "native_scoped" | "ordinal" | "fingerprint" | "synthetic";
91 firstRawEventId?: RawEventId;
92 lastRawEventId?: RawEventId;
93 createdAt: string;
94 updatedAt: string;
95};
96```
97
98This 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.
99
100## ID Allocation Rules
101
1021. If the provider gives a stable native id, reuse the existing binding or allocate a new app id and bind it.
1032. If the provider gives no stable id, allocate by scoped ordinal.
1043. If ordinals are insufficient, add a fingerprint inside the scope.
1054. Once allocated, never change the app id for that entity.
1065. Never infer parent/root completion by string matching ids.
107
108## Scoped Ordinals
109
110Weak providers need deterministic ordinals.
111
112```text
113runOrdinalWithinThread
114providerTurnOrdinalWithinProviderThread
115nodeOrdinalWithinParent
116itemOrdinalWithinProviderTurn
117requestOrdinalWithinProviderTurn
118messageOrdinalWithinNode
119```
120
121Ordinals are assigned by the normalizer when provider frames are processed. They must be replay-deterministic for a given provider transcript.
122
123## Fingerprints
124
125Fingerprints are only fallback correlation tools. They should include scope and stable structure, not large mutable text.
126
127Examples:
128
129```text
130ProviderTurn fingerprint:
131 providerThreadId + runId + providerTurnOrdinalWithinProviderThread
132
133Node / TurnItem fingerprint:
134 providerTurnId + itemKind + itemOrdinalWithinProviderTurn + toolName?
135
136RuntimeRequest fingerprint:
137 providerTurnId + requestKind + nativeItemRef? + requestOrdinalWithinProviderTurn
138```
139
140Do not use complete assistant text as a primary fingerprint. Text is mutable, can stream in chunks, and can be duplicated.
141
142## Runtime Events
143
144Normalized runtime events should carry both app ids and provider refs.
145
146```ts
147type RuntimeEvent = {
148 id: RuntimeEventId;
149 type: RuntimeEventType;
150 threadId: ThreadId;
151 runId: RunId | null;
152 nodeId: NodeId | null;
153 parentNodeId: NodeId | null;
154 providerSessionId: ProviderSessionId | null;
155 providerThreadId: ProviderThreadId | null;
156 providerTurnId: ProviderTurnId | null;
157 nativeItemRef: string | null;
158 requestId: RuntimeRequestId | null;
159 providerRefs: ProviderRefs;
160 payload: unknown;
161 createdAt: string;
162};
163```
164
165Downstream systems should use app ids. Provider refs are preserved for inspection and adapter routing.
166
167## Command Routing
168
169UI and orchestration commands target app ids.
170
171```text
172approval.respond(RuntimeRequestId)
173interrupt.run(RunId)
174interrupt.node(NodeId)
175fork.fromNode(NodeId)
176rollback.toRun(ThreadId, runOrdinal)
177```
178
179The 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.
180
181Examples:
182
183```text
184RuntimeRequestId -> nativeRequestRef + providerSessionId
185RunId -> root NodeId -> ProviderTurnId -> nativeTurnRef
186NodeId -> ProviderThreadId -> nativeThreadRef
187```
188
189## Pending Requests After Restart
190
191Pending request records may survive restart, but provider callback state usually does not.
192
193V2 should distinguish historical pending-looking requests from respondable requests:
194
195```ts
196type ResponseCapability =
197 | { type: "live"; providerSessionId: ProviderSessionId }
198 | { type: "not_resumable"; reason: string };
199```
200
201The UI can show that a request expired and the user must restart or rerun the turn.
202
203## Subagent Correlation
204
205Subagents are represented through parent-child execution nodes.
206
207For Codex:
208
209```text
210collabAgentToolCall item
211 -> ExecutionNode(kind="subagent" or "tool_call")
212receiverThreadIds[]
213 -> ProviderThread records
214child turn/started and turn/completed
215 -> child ProviderTurn and child ExecutionNode
216```
217
218The 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.
219
220For weak providers, the adapter may create a child node by ordinal under the active parent node.
221
222## Replay Determinism
223
224Given the same provider transcript and same initial app state, normalization must produce the same ids.
225
226Requirements:
227
228- Identity bindings are persisted as events or durable rows.
229- Generated ids can be random if the binding is persisted before downstream projection.
230- Fixture replay may use deterministic id generation to make assertions easier.
231- Reprocessing the same provider frame should find the existing binding, not allocate another entity.