| 1 | # Entity IDs And Correlation |
| 2 | |
| 3 | ## Principle |
| 4 | |
| 5 | Provider ids are evidence. App ids are identity. |
| 6 | |
| 7 | 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. |
| 8 | |
| 9 | ## ID Families |
| 10 | |
| 11 | ```text |
| 12 | ThreadId app-visible conversation thread |
| 13 | RunId counted app turn |
| 14 | NodeId execution graph node |
| 15 | ProviderSessionId live/resumable provider runtime session |
| 16 | ProviderThreadId app handle for provider-native conversation |
| 17 | ProviderTurnId app handle for provider-native turn |
| 18 | RuntimeRequestId app handle for provider callback/request |
| 19 | RawEventId optional raw provider diagnostic frame id |
| 20 | CheckpointId app checkpoint id |
| 21 | PlanId app plan/todo/question artifact id |
| 22 | ``` |
| 23 | |
| 24 | Provider ids never replace these ids. |
| 25 | |
| 26 | ## Provider Refs |
| 27 | |
| 28 | Provider refs are metadata on runtime entities and events. |
| 29 | |
| 30 | ```ts |
| 31 | type 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 | |
| 43 | Codex can populate most of these fields. Other providers may populate only a subset. |
| 44 | |
| 45 | ## Correlation Scope |
| 46 | |
| 47 | Correlation is always scoped. Never match a native id globally without a provider/session/thread scope. |
| 48 | |
| 49 | Preferred matching keys: |
| 50 | |
| 51 | ```text |
| 52 | ProviderThread: |
| 53 | provider + providerSessionId + nativeThreadRef |
| 54 | |
| 55 | ProviderTurn: |
| 56 | providerThreadId + nativeTurnRef |
| 57 | or providerThreadId + turnOrdinal |
| 58 | |
| 59 | Node / TurnItem: |
| 60 | providerTurnId + nativeItemRef |
| 61 | or providerTurnId + itemOrdinal |
| 62 | |
| 63 | RuntimeRequest: |
| 64 | providerSessionId + nativeRequestRef |
| 65 | or providerTurnId + requestOrdinal |
| 66 | ``` |
| 67 | |
| 68 | If a provider recycles ids, the provider adapter must widen the scope until the mapping is unambiguous. |
| 69 | |
| 70 | ## Mapping Store |
| 71 | |
| 72 | V2 needs a durable correlation table or equivalent event-sourced binding stream. |
| 73 | |
| 74 | ```ts |
| 75 | type 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 | |
| 98 | 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. |
| 99 | |
| 100 | ## ID Allocation Rules |
| 101 | |
| 102 | 1. If the provider gives a stable native id, reuse the existing binding or allocate a new app id and bind it. |
| 103 | 2. If the provider gives no stable id, allocate by scoped ordinal. |
| 104 | 3. If ordinals are insufficient, add a fingerprint inside the scope. |
| 105 | 4. Once allocated, never change the app id for that entity. |
| 106 | 5. Never infer parent/root completion by string matching ids. |
| 107 | |
| 108 | ## Scoped Ordinals |
| 109 | |
| 110 | Weak providers need deterministic ordinals. |
| 111 | |
| 112 | ```text |
| 113 | runOrdinalWithinThread |
| 114 | providerTurnOrdinalWithinProviderThread |
| 115 | nodeOrdinalWithinParent |
| 116 | itemOrdinalWithinProviderTurn |
| 117 | requestOrdinalWithinProviderTurn |
| 118 | messageOrdinalWithinNode |
| 119 | ``` |
| 120 | |
| 121 | Ordinals are assigned by the normalizer when provider frames are processed. They must be replay-deterministic for a given provider transcript. |
| 122 | |
| 123 | ## Fingerprints |
| 124 | |
| 125 | Fingerprints are only fallback correlation tools. They should include scope and stable structure, not large mutable text. |
| 126 | |
| 127 | Examples: |
| 128 | |
| 129 | ```text |
| 130 | ProviderTurn fingerprint: |
| 131 | providerThreadId + runId + providerTurnOrdinalWithinProviderThread |
| 132 | |
| 133 | Node / TurnItem fingerprint: |
| 134 | providerTurnId + itemKind + itemOrdinalWithinProviderTurn + toolName? |
| 135 | |
| 136 | RuntimeRequest fingerprint: |
| 137 | providerTurnId + requestKind + nativeItemRef? + requestOrdinalWithinProviderTurn |
| 138 | ``` |
| 139 | |
| 140 | Do 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 | |
| 144 | Normalized runtime events should carry both app ids and provider refs. |
| 145 | |
| 146 | ```ts |
| 147 | type 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 | |
| 165 | Downstream systems should use app ids. Provider refs are preserved for inspection and adapter routing. |
| 166 | |
| 167 | ## Command Routing |
| 168 | |
| 169 | UI and orchestration commands target app ids. |
| 170 | |
| 171 | ```text |
| 172 | approval.respond(RuntimeRequestId) |
| 173 | interrupt.run(RunId) |
| 174 | interrupt.node(NodeId) |
| 175 | fork.fromNode(NodeId) |
| 176 | rollback.toRun(ThreadId, runOrdinal) |
| 177 | ``` |
| 178 | |
| 179 | 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. |
| 180 | |
| 181 | Examples: |
| 182 | |
| 183 | ```text |
| 184 | RuntimeRequestId -> nativeRequestRef + providerSessionId |
| 185 | RunId -> root NodeId -> ProviderTurnId -> nativeTurnRef |
| 186 | NodeId -> ProviderThreadId -> nativeThreadRef |
| 187 | ``` |
| 188 | |
| 189 | ## Pending Requests After Restart |
| 190 | |
| 191 | Pending request records may survive restart, but provider callback state usually does not. |
| 192 | |
| 193 | V2 should distinguish historical pending-looking requests from respondable requests: |
| 194 | |
| 195 | ```ts |
| 196 | type ResponseCapability = |
| 197 | | { type: "live"; providerSessionId: ProviderSessionId } |
| 198 | | { type: "not_resumable"; reason: string }; |
| 199 | ``` |
| 200 | |
| 201 | The UI can show that a request expired and the user must restart or rerun the turn. |
| 202 | |
| 203 | ## Subagent Correlation |
| 204 | |
| 205 | Subagents are represented through parent-child execution nodes. |
| 206 | |
| 207 | For Codex: |
| 208 | |
| 209 | ```text |
| 210 | collabAgentToolCall item |
| 211 | -> ExecutionNode(kind="subagent" or "tool_call") |
| 212 | receiverThreadIds[] |
| 213 | -> ProviderThread records |
| 214 | child turn/started and turn/completed |
| 215 | -> child ProviderTurn and child ExecutionNode |
| 216 | ``` |
| 217 | |
| 218 | 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. |
| 219 | |
| 220 | For weak providers, the adapter may create a child node by ordinal under the active parent node. |
| 221 | |
| 222 | ## Replay Determinism |
| 223 | |
| 224 | Given the same provider transcript and same initial app state, normalization must produce the same ids. |
| 225 | |
| 226 | Requirements: |
| 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. |