@oai404iao/pi-codex-minimal-tools 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,182 +1,596 @@
1
- //! Codex wire identity: UUID v7 session/thread/window identifiers and the
2
- //! sticky-routing turn-state token, matching the Codex CLI's request identity.
3
- //!
4
- //! Pi's own session ids are short opaque strings (often 8 hex chars). The
5
- //! Codex ChatGPT backend expects real UUID-shaped ids in `session-id`,
6
- //! `thread-id`, `x-codex-window-id`, `prompt_cache_key`, and
7
- //! `client_metadata`; the CLI generates UUID v7 values. This module derives a
8
- //! stable UUID v7 identity per (pi session, thread) pair and remembers the
9
- //! `x-codex-turn-state` sticky-routing token emitted by the backend so later
10
- //! requests in the same session can replay it.
11
-
12
- const WIRE_IDENTITY_CACHE_LIMIT = 1024;
1
+ import { uuidv7 } from "@earendil-works/pi-ai";
2
+ import {
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
13
10
 
14
- export interface CodexWireIdentity {
15
- /** Stable UUID v7 for the pi session. */
11
+ /**
12
+ * Persistent Codex identity written by pi-codex-minimal-tools.
13
+ *
14
+ * `piSessionId` is only the lookup/binding key for Pi's session file. It is
15
+ * never emitted on the Codex wire.
16
+ */
17
+ export interface CodexThreadIdentity {
18
+ version: 1;
19
+ piSessionId: string;
16
20
  sessionId: string;
17
- /** Stable UUID v7 for the thread (defaults to a per-thread value). */
18
21
  threadId: string;
19
- /** UUID v7 for the current auto-compact window; rotated on compaction. */
20
22
  windowId: string;
23
+ firstWindowId: string;
24
+ previousWindowId?: string;
25
+ windowNumber: number;
26
+ parentThreadId?: string;
27
+ forkedFromThreadId?: string;
28
+ agentName?: string;
29
+ subagentKind?: string;
30
+ lastCompactionEntryId?: string;
21
31
  }
22
32
 
23
- interface CachedThread {
33
+ export interface CodexWireIdentity {
34
+ sessionId: string;
24
35
  threadId: string;
25
36
  windowId: string;
26
37
  }
27
38
 
28
- interface CachedSession {
29
- sessionId: string;
30
- threads: Map<string, CachedThread>;
39
+ export interface CodexTurnIdentity {
40
+ turnId: string;
41
+ startedAtMs: number;
42
+ parentTurnId?: string;
43
+ rootTurnId?: string;
44
+ turnState?: string;
31
45
  }
32
46
 
33
- const sessionCache = new Map<string, CachedSession>();
34
- const turnStateCache = new Map<string, string>();
35
- const installationIdCache = new Map<string, string>();
47
+ export interface CodexRequestIdentity extends CodexWireIdentity {
48
+ installationId: string;
49
+ turnId: string;
50
+ turnStartedAtMs?: number;
51
+ requestKind: "turn" | "compaction" | "prewarm";
52
+ parentThreadId?: string;
53
+ forkedFromThreadId?: string;
54
+ parentTurnId?: string;
55
+ rootTurnId?: string;
56
+ agentName?: string;
57
+ subagentKind?: string;
58
+ turnState?: string;
59
+ }
36
60
 
37
- function randomBytes(count: number): Uint8Array {
38
- if (typeof globalThis.crypto?.getRandomValues === "function") {
39
- const bytes = new Uint8Array(count);
40
- globalThis.crypto.getRandomValues(bytes);
41
- return bytes;
42
- }
43
- const bytes = new Uint8Array(count);
44
- for (let index = 0; index < count; index++) {
45
- bytes[index] = Math.floor(Math.random() * 256);
46
- }
47
- return bytes;
61
+ interface RuntimeSession {
62
+ identity: CodexThreadIdentity;
63
+ activeTurn?: CodexTurnIdentity;
64
+ pendingTurnState?: string;
65
+ extraThreads: Map<string, CodexThreadIdentity>;
48
66
  }
49
67
 
50
- function uuidBytesToHex(bytes: Uint8Array): string {
51
- const hex: string[] = [];
52
- for (const byte of bytes) {
53
- hex.push(byte.toString(16).padStart(2, "0"));
68
+ interface CodexIdentityRuntime {
69
+ sessions: Map<string, RuntimeSession>;
70
+ installationId?: string;
71
+ installationPath?: string;
72
+ }
73
+
74
+ const RUNTIME_SYMBOL = Symbol.for("@oai404iao/pi-codex/identity-runtime/v1");
75
+ const UUID_PATTERN =
76
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
77
+ const UUID_V7_PATTERN =
78
+ /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
79
+ const INSTALLATION_UUID_V4_PATTERN =
80
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
81
+
82
+ function runtime(): CodexIdentityRuntime {
83
+ const globals = globalThis as typeof globalThis & {
84
+ [RUNTIME_SYMBOL]?: CodexIdentityRuntime;
85
+ };
86
+ if (!globals[RUNTIME_SYMBOL]) {
87
+ globals[RUNTIME_SYMBOL] = {
88
+ sessions: new Map(),
89
+ };
54
90
  }
55
- return [
56
- hex.slice(0, 4).join(""),
57
- hex.slice(4, 6).join(""),
58
- hex.slice(6, 8).join(""),
59
- hex.slice(8, 10).join(""),
60
- hex.slice(10, 16).join(""),
61
- ].join("-");
91
+ return globals[RUNTIME_SYMBOL];
62
92
  }
63
93
 
64
- /** RFC 9562 UUID v7: 48-bit millisecond timestamp + random, version 7, variant RFC 4122. */
94
+ /** Pi-owned identities are generated as RFC 9562 UUIDv7 values. */
65
95
  export function uuidV7(): string {
66
- const bytes = randomBytes(16);
67
- const timestamp = Date.now();
68
- bytes[0] = (timestamp / 0x10000000000) & 0xff;
69
- bytes[1] = (timestamp / 0x100000000) & 0xff;
70
- bytes[2] = (timestamp / 0x1000000) & 0xff;
71
- bytes[3] = (timestamp / 0x10000) & 0xff;
72
- bytes[4] = (timestamp / 0x100) & 0xff;
73
- bytes[5] = timestamp & 0xff;
74
- bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x70;
75
- bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
76
- return uuidBytesToHex(bytes);
96
+ return uuidv7();
77
97
  }
78
98
 
79
99
  function uuidV4(): string {
80
- const bytes = randomBytes(16);
100
+ if (typeof globalThis.crypto?.randomUUID === "function") {
101
+ return globalThis.crypto.randomUUID();
102
+ }
103
+ const bytes = new Uint8Array(16);
104
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
105
+ globalThis.crypto.getRandomValues(bytes);
106
+ } else {
107
+ for (let index = 0; index < bytes.length; index++) {
108
+ bytes[index] = Math.floor(Math.random() * 256);
109
+ }
110
+ }
81
111
  bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
82
112
  bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
83
- return uuidBytesToHex(bytes);
113
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
114
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
115
+ }
116
+
117
+ export function isUuid(value: unknown): value is string {
118
+ return typeof value === "string" && UUID_PATTERN.test(value);
119
+ }
120
+
121
+ export function isUuidV7(value: unknown): value is string {
122
+ return typeof value === "string" && UUID_V7_PATTERN.test(value);
84
123
  }
85
124
 
86
125
  /**
87
- * Stable per-session installation id (UUID v4), mirroring the CLI's
88
- * `installation_id` file: one opaque id per installation that never changes
89
- * for the lifetime of the session.
126
+ * Codex has one installation id for the installation, not one per Pi session.
127
+ *
128
+ * It is loaded and repaired synchronously because request payload assembly is
129
+ * synchronous at this boundary.
90
130
  */
91
- export function codexInstallationIdFor(sessionKey: string): string {
92
- let id = installationIdCache.get(sessionKey);
93
- if (!id) {
94
- id = typeof globalThis.crypto?.randomUUID === "function"
95
- ? globalThis.crypto.randomUUID()
96
- : uuidV4();
97
- installationIdCache.set(sessionKey, id);
131
+ function installationIdPath(): string {
132
+ return process.env.PI_CODEX_INSTALLATION_ID_PATH
133
+ ?? join(
134
+ getAgentDir(),
135
+ "pi-codex-minimal-tools",
136
+ "installation_id",
137
+ );
138
+ }
139
+
140
+ export function codexInstallationIdFor(_sessionKey?: string): string {
141
+ const state = runtime();
142
+ const path = installationIdPath();
143
+ if (state.installationId && state.installationPath === path) {
144
+ return state.installationId;
145
+ }
146
+ try {
147
+ const persisted = readFileSync(path, "utf8").trim();
148
+ if (INSTALLATION_UUID_V4_PATTERN.test(persisted)) {
149
+ state.installationId = persisted;
150
+ state.installationPath = path;
151
+ return persisted;
152
+ }
153
+ } catch {
154
+ // Missing/unreadable state is repaired below.
155
+ }
156
+
157
+ const generated = uuidV4();
158
+ try {
159
+ mkdirSync(dirname(path), { recursive: true });
160
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
161
+ writeFileSync(temporary, `${generated}\n`, {
162
+ encoding: "utf8",
163
+ mode: 0o600,
164
+ });
165
+ renameSync(temporary, path);
166
+ } catch {
167
+ // Read-only environments retain the installation id for this process.
168
+ }
169
+ state.installationId = generated;
170
+ state.installationPath = path;
171
+ return state.installationId;
172
+ }
173
+
174
+ export function setCodexInstallationId(value: string): void {
175
+ if (!INSTALLATION_UUID_V4_PATTERN.test(value)) {
176
+ throw new Error("Codex installation id must be a UUIDv4");
98
177
  }
99
- return id;
178
+ runtime().installationId = value;
179
+ runtime().installationPath = installationIdPath();
100
180
  }
101
181
 
102
- function newCachedSession(): CachedSession {
182
+ function newRootIdentity(piSessionId: string, forkedFromThreadId?: string): CodexThreadIdentity {
183
+ const threadId = uuidV7();
184
+ const windowId = uuidV7();
103
185
  return {
104
- sessionId: uuidV7(),
105
- threads: new Map(),
186
+ version: 1,
187
+ piSessionId,
188
+ sessionId: threadId,
189
+ threadId,
190
+ windowId,
191
+ firstWindowId: windowId,
192
+ windowNumber: 0,
193
+ ...(forkedFromThreadId ? { forkedFromThreadId } : {}),
194
+ agentName: "root",
106
195
  };
107
196
  }
108
197
 
109
- function ensureSession(sessionKey: string): CachedSession {
110
- let session = sessionCache.get(sessionKey);
111
- if (!session) {
112
- if (sessionCache.size >= WIRE_IDENTITY_CACHE_LIMIT) {
113
- sessionCache.clear();
114
- turnStateCache.clear();
115
- }
116
- session = newCachedSession();
117
- sessionCache.set(sessionKey, session);
118
- }
119
- return session;
198
+ export function createCodexRootIdentity(
199
+ piSessionId: string,
200
+ options: { forkedFromThreadId?: string } = {},
201
+ ): CodexThreadIdentity {
202
+ return newRootIdentity(piSessionId, options.forkedFromThreadId);
120
203
  }
121
204
 
122
- function ensureThread(session: CachedSession, threadKey: string): CachedThread {
123
- let thread = session.threads.get(threadKey);
124
- if (!thread) {
125
- thread = { threadId: uuidV7(), windowId: uuidV7() };
126
- session.threads.set(threadKey, thread);
205
+ export function createCodexChildIdentity(
206
+ piSessionId: string,
207
+ parent: CodexThreadIdentity,
208
+ options: {
209
+ relation: "spawn" | "fork";
210
+ agentName?: string;
211
+ subagentKind?: string;
212
+ },
213
+ ): CodexThreadIdentity {
214
+ const threadId = uuidV7();
215
+ const windowId = uuidV7();
216
+ return {
217
+ version: 1,
218
+ piSessionId,
219
+ sessionId: parent.sessionId,
220
+ threadId,
221
+ windowId,
222
+ firstWindowId: windowId,
223
+ windowNumber: 0,
224
+ parentThreadId: parent.threadId,
225
+ ...(options.relation === "fork"
226
+ ? { forkedFromThreadId: parent.threadId }
227
+ : {}),
228
+ ...(options.agentName ? { agentName: options.agentName } : {}),
229
+ subagentKind: options.subagentKind ?? "collab_spawn",
230
+ };
231
+ }
232
+
233
+ function cloneIdentity(identity: CodexThreadIdentity): CodexThreadIdentity {
234
+ return structuredClone(identity);
235
+ }
236
+
237
+ export function registerCodexThreadIdentity(identity: CodexThreadIdentity): CodexThreadIdentity {
238
+ const parsed = parseCodexThreadIdentity(identity);
239
+ const existing = runtime().sessions.get(parsed.piSessionId);
240
+ runtime().sessions.set(parsed.piSessionId, {
241
+ identity: cloneIdentity(parsed),
242
+ activeTurn: existing?.activeTurn,
243
+ pendingTurnState: existing?.pendingTurnState,
244
+ extraThreads: existing?.extraThreads ?? new Map(),
245
+ });
246
+ return cloneIdentity(parsed);
247
+ }
248
+
249
+ export function codexThreadIdentityFor(piSessionId: string | undefined): CodexThreadIdentity | undefined {
250
+ if (!piSessionId) return undefined;
251
+ const value = runtime().sessions.get(piSessionId)?.identity;
252
+ return value ? cloneIdentity(value) : undefined;
253
+ }
254
+
255
+ function ensureFallbackSession(piSessionId: string): RuntimeSession {
256
+ let state = runtime().sessions.get(piSessionId);
257
+ if (!state) {
258
+ state = {
259
+ identity: newRootIdentity(piSessionId),
260
+ extraThreads: new Map(),
261
+ };
262
+ runtime().sessions.set(piSessionId, state);
127
263
  }
128
- return thread;
264
+ return state;
129
265
  }
130
266
 
131
267
  /**
132
- * Resolve the stable UUID v7 wire identity for a (session, thread) pair.
133
- * Values are generated once and reused for the lifetime of the pair so the
134
- * backend sees consistent `session-id`/`thread-id`/`x-codex-window-id` values,
135
- * exactly like the Codex CLI.
268
+ * Compatibility resolver used by tests and legacy call sites.
269
+ *
270
+ * A root Codex thread has `sessionId === threadId`. Additional explicit
271
+ * thread keys share that root session while receiving their own thread/window.
136
272
  */
137
273
  export function resolveCodexWireIdentity(
138
- sessionKey: string,
274
+ piSessionId: string,
139
275
  threadKey?: string,
140
276
  ): CodexWireIdentity {
141
- const session = ensureSession(sessionKey);
142
- const thread = ensureThread(session, threadKey ?? sessionKey);
277
+ const session = ensureFallbackSession(piSessionId);
278
+ if (
279
+ !threadKey
280
+ || threadKey === piSessionId
281
+ || threadKey === session.identity.threadId
282
+ ) {
283
+ return {
284
+ sessionId: session.identity.sessionId,
285
+ threadId: session.identity.threadId,
286
+ windowId: session.identity.windowId,
287
+ };
288
+ }
289
+ let thread = session.extraThreads.get(threadKey);
290
+ if (!thread) {
291
+ const threadId = uuidV7();
292
+ const windowId = uuidV7();
293
+ thread = {
294
+ version: 1,
295
+ piSessionId,
296
+ sessionId: session.identity.sessionId,
297
+ threadId,
298
+ windowId,
299
+ firstWindowId: windowId,
300
+ windowNumber: 0,
301
+ };
302
+ session.extraThreads.set(threadKey, thread);
303
+ }
143
304
  return {
144
- sessionId: session.sessionId,
305
+ sessionId: thread.sessionId,
145
306
  threadId: thread.threadId,
146
307
  windowId: thread.windowId,
147
308
  };
148
309
  }
149
310
 
311
+ export function rotateCodexWindowId(piSessionId: string, threadKey?: string): void {
312
+ const session = ensureFallbackSession(piSessionId);
313
+ const identity =
314
+ threadKey && threadKey !== piSessionId && threadKey !== session.identity.threadId
315
+ ? session.extraThreads.get(threadKey)
316
+ : session.identity;
317
+ if (!identity) {
318
+ resolveCodexWireIdentity(piSessionId, threadKey);
319
+ return rotateCodexWindowId(piSessionId, threadKey);
320
+ }
321
+ identity.previousWindowId = identity.windowId;
322
+ identity.windowId = uuidV7();
323
+ identity.windowNumber += 1;
324
+ }
325
+
326
+ export function advanceCodexWindow(
327
+ piSessionId: string,
328
+ compactionEntryId?: string,
329
+ ): CodexThreadIdentity {
330
+ const state = ensureFallbackSession(piSessionId);
331
+ if (
332
+ compactionEntryId
333
+ && state.identity.lastCompactionEntryId === compactionEntryId
334
+ ) {
335
+ return cloneIdentity(state.identity);
336
+ }
337
+ state.identity.previousWindowId = state.identity.windowId;
338
+ state.identity.windowId = uuidV7();
339
+ state.identity.windowNumber += 1;
340
+ if (compactionEntryId) {
341
+ state.identity.lastCompactionEntryId = compactionEntryId;
342
+ }
343
+ return cloneIdentity(state.identity);
344
+ }
345
+
346
+ export function beginCodexTurn(
347
+ piSessionId: string,
348
+ options: {
349
+ parentPiSessionId?: string;
350
+ turnId?: string;
351
+ startedAtMs?: number;
352
+ } = {},
353
+ ): CodexTurnIdentity {
354
+ const state = ensureFallbackSession(piSessionId);
355
+ if (state.activeTurn) return structuredClone(state.activeTurn);
356
+
357
+ const parentTurn = options.parentPiSessionId
358
+ ? runtime().sessions.get(options.parentPiSessionId)?.activeTurn
359
+ : undefined;
360
+ const turnId = isUuidV7(options.turnId) ? options.turnId : uuidV7();
361
+ const turn: CodexTurnIdentity = {
362
+ turnId,
363
+ startedAtMs: options.startedAtMs ?? Date.now(),
364
+ ...(parentTurn ? { parentTurnId: parentTurn.turnId } : {}),
365
+ rootTurnId: parentTurn?.rootTurnId ?? parentTurn?.turnId ?? turnId,
366
+ ...(state.pendingTurnState
367
+ ? { turnState: state.pendingTurnState }
368
+ : {}),
369
+ };
370
+ delete state.pendingTurnState;
371
+ state.activeTurn = turn;
372
+ return structuredClone(turn);
373
+ }
374
+
375
+ export function currentCodexTurn(piSessionId: string | undefined): CodexTurnIdentity | undefined {
376
+ if (!piSessionId) return undefined;
377
+ const turn = runtime().sessions.get(piSessionId)?.activeTurn;
378
+ return turn ? structuredClone(turn) : undefined;
379
+ }
380
+
381
+ export function endCodexTurn(piSessionId: string, turnId?: string): void {
382
+ const state = runtime().sessions.get(piSessionId);
383
+ if (!state?.activeTurn) return;
384
+ if (turnId && state.activeTurn.turnId !== turnId) return;
385
+ delete state.activeTurn;
386
+ }
387
+
388
+ export function codexTurnStateFor(piSessionId: string): string | undefined {
389
+ return runtime().sessions.get(piSessionId)?.activeTurn?.turnState;
390
+ }
391
+
392
+ /** Store the first sticky-routing token for the active logical turn only. */
393
+ export function captureCodexTurnState(
394
+ piSessionId: string,
395
+ token: string | undefined,
396
+ ): void {
397
+ if (typeof token !== "string" || !token.trim()) return;
398
+ const state = ensureFallbackSession(piSessionId);
399
+ if (!state.activeTurn) {
400
+ if (!state.pendingTurnState) state.pendingTurnState = token.trim();
401
+ return;
402
+ }
403
+ if (state.activeTurn.turnState) return;
404
+ state.activeTurn.turnState = token.trim();
405
+ }
406
+
407
+ function metadataString(
408
+ metadata: Record<string, unknown> | undefined,
409
+ key: string,
410
+ ): string | undefined {
411
+ const value = metadata?.[key];
412
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
413
+ }
414
+
150
415
  /**
151
- * Rotate the auto-compact window id for a (session, thread) pair. Session and
152
- * thread ids stay stable; only `x-codex-window-id` changes, mirroring the
153
- * CLI's auto-compact window lifecycle.
416
+ * Resolve one immutable identity snapshot for an actual provider request.
417
+ * Valid explicit UUID values are consumed directly; they are never used as
418
+ * derivation keys.
154
419
  */
155
- export function rotateCodexWindowId(sessionKey: string, threadKey?: string): void {
156
- const session = ensureSession(sessionKey);
157
- const thread = ensureThread(session, threadKey ?? sessionKey);
158
- thread.windowId = uuidV7();
420
+ export function resolveCodexRequestIdentity(
421
+ piSessionId: string | undefined,
422
+ metadata: Record<string, unknown> | undefined,
423
+ requestKind: CodexRequestIdentity["requestKind"] = "turn",
424
+ ): CodexRequestIdentity | undefined {
425
+ const explicitSessionId = metadataString(metadata, "session_id");
426
+ const explicitThreadId = metadataString(metadata, "thread_id");
427
+ const explicitWindowId = metadataString(metadata, "window_id")
428
+ ?? metadataString(metadata, "x-codex-window-id");
429
+ const state = piSessionId ? ensureFallbackSession(piSessionId) : undefined;
430
+ if (
431
+ !state
432
+ && (!isUuidV7(explicitSessionId) || !isUuidV7(explicitThreadId))
433
+ ) {
434
+ return undefined;
435
+ }
436
+ const identity = state?.identity;
437
+ const sessionId = isUuidV7(explicitSessionId)
438
+ ? explicitSessionId
439
+ : identity?.sessionId;
440
+ const threadId = isUuidV7(explicitThreadId)
441
+ ? explicitThreadId
442
+ : identity?.threadId;
443
+ const windowId = isUuidV7(explicitWindowId)
444
+ ? explicitWindowId
445
+ : identity?.windowId;
446
+ if (!sessionId || !threadId || !windowId) return undefined;
447
+
448
+ let turn = state?.activeTurn;
449
+ const explicitTurnId = metadataString(metadata, "turn_id");
450
+ if (isUuidV7(explicitTurnId)) {
451
+ if (!turn || turn.turnId !== explicitTurnId) {
452
+ turn = {
453
+ turnId: explicitTurnId,
454
+ startedAtMs: Date.now(),
455
+ rootTurnId: explicitTurnId,
456
+ ...(state?.pendingTurnState
457
+ ? { turnState: state.pendingTurnState }
458
+ : {}),
459
+ };
460
+ if (state) {
461
+ state.activeTurn = turn;
462
+ delete state.pendingTurnState;
463
+ }
464
+ }
465
+ } else if (!turn && requestKind !== "prewarm" && piSessionId) {
466
+ turn = beginCodexTurn(piSessionId);
467
+ }
468
+
469
+ const explicitParentThreadId = metadataString(metadata, "parent_thread_id");
470
+ const explicitForkedFromThreadId = metadataString(metadata, "forked_from_thread_id");
471
+ const explicitParentTurnId = metadataString(metadata, "parent_turn_id");
472
+ const explicitRootTurnId = metadataString(metadata, "root_turn_id");
473
+ return {
474
+ installationId: codexInstallationIdFor(),
475
+ sessionId,
476
+ threadId,
477
+ windowId,
478
+ turnId: turn?.turnId ?? "",
479
+ ...(turn ? { turnStartedAtMs: turn.startedAtMs } : {}),
480
+ requestKind,
481
+ ...(isUuidV7(explicitParentThreadId)
482
+ ? { parentThreadId: explicitParentThreadId }
483
+ : identity?.parentThreadId
484
+ ? { parentThreadId: identity.parentThreadId }
485
+ : {}),
486
+ ...(isUuidV7(explicitForkedFromThreadId)
487
+ ? { forkedFromThreadId: explicitForkedFromThreadId }
488
+ : identity?.forkedFromThreadId
489
+ ? { forkedFromThreadId: identity.forkedFromThreadId }
490
+ : {}),
491
+ ...(isUuidV7(explicitParentTurnId)
492
+ ? { parentTurnId: explicitParentTurnId }
493
+ : turn?.parentTurnId
494
+ ? { parentTurnId: turn.parentTurnId }
495
+ : {}),
496
+ ...(isUuidV7(explicitRootTurnId)
497
+ ? { rootTurnId: explicitRootTurnId }
498
+ : turn?.rootTurnId
499
+ ? { rootTurnId: turn.rootTurnId }
500
+ : {}),
501
+ ...(identity?.agentName ? { agentName: identity.agentName } : {}),
502
+ ...(identity?.subagentKind ? { subagentKind: identity.subagentKind } : {}),
503
+ ...(turn?.turnState ? { turnState: turn.turnState } : {}),
504
+ };
159
505
  }
160
506
 
161
- /** Current sticky-routing turn-state token for a session, if the backend issued one. */
162
- export function codexTurnStateFor(sessionKey: string): string | undefined {
163
- return turnStateCache.get(sessionKey);
507
+ type UnknownRecord = Record<string, unknown>;
508
+
509
+ function asRecord(value: unknown): UnknownRecord | undefined {
510
+ return value && typeof value === "object" && !Array.isArray(value)
511
+ ? value as UnknownRecord
512
+ : undefined;
164
513
  }
165
514
 
166
- /** Remember the sticky-routing token emitted by the backend. */
167
- export function captureCodexTurnState(sessionKey: string, token: string | undefined): void {
168
- if (!sessionKey || typeof token !== "string" || !token.trim()) return;
169
- turnStateCache.set(sessionKey, token.trim());
515
+ export function parseCodexThreadIdentity(value: unknown): CodexThreadIdentity {
516
+ const input = asRecord(value);
517
+ if (!input || input.version !== 1) {
518
+ throw new Error("unsupported Codex thread identity version");
519
+ }
520
+ for (const field of [
521
+ "piSessionId",
522
+ "sessionId",
523
+ "threadId",
524
+ "windowId",
525
+ "firstWindowId",
526
+ ] as const) {
527
+ if (typeof input[field] !== "string" || !input[field]) {
528
+ throw new Error(`Codex thread identity ${field} must be a non-empty string`);
529
+ }
530
+ }
531
+ for (const field of [
532
+ "sessionId",
533
+ "threadId",
534
+ "windowId",
535
+ "firstWindowId",
536
+ "previousWindowId",
537
+ "parentThreadId",
538
+ "forkedFromThreadId",
539
+ ] as const) {
540
+ if (input[field] !== undefined && !isUuidV7(input[field])) {
541
+ throw new Error(`Codex thread identity ${field} must be a UUIDv7`);
542
+ }
543
+ }
544
+ if (
545
+ typeof input.windowNumber !== "number"
546
+ || !Number.isSafeInteger(input.windowNumber)
547
+ || input.windowNumber < 0
548
+ ) {
549
+ throw new Error("Codex thread identity windowNumber must be a non-negative integer");
550
+ }
551
+ for (const field of [
552
+ "agentName",
553
+ "subagentKind",
554
+ "lastCompactionEntryId",
555
+ ] as const) {
556
+ if (input[field] !== undefined && typeof input[field] !== "string") {
557
+ throw new Error(`Codex thread identity ${field} must be a string`);
558
+ }
559
+ }
560
+ return {
561
+ version: 1,
562
+ piSessionId: input.piSessionId as string,
563
+ sessionId: input.sessionId as string,
564
+ threadId: input.threadId as string,
565
+ windowId: input.windowId as string,
566
+ firstWindowId: input.firstWindowId as string,
567
+ windowNumber: input.windowNumber,
568
+ ...(input.previousWindowId
569
+ ? { previousWindowId: input.previousWindowId as string }
570
+ : {}),
571
+ ...(input.parentThreadId
572
+ ? { parentThreadId: input.parentThreadId as string }
573
+ : {}),
574
+ ...(input.forkedFromThreadId
575
+ ? { forkedFromThreadId: input.forkedFromThreadId as string }
576
+ : {}),
577
+ ...(input.agentName ? { agentName: input.agentName as string } : {}),
578
+ ...(input.subagentKind
579
+ ? { subagentKind: input.subagentKind as string }
580
+ : {}),
581
+ ...(input.lastCompactionEntryId
582
+ ? { lastCompactionEntryId: input.lastCompactionEntryId as string }
583
+ : {}),
584
+ };
170
585
  }
171
586
 
172
- /** Forget all derived identity and turn-state state (tests, session teardown). */
173
587
  export function resetCodexWireState(): void {
174
- sessionCache.clear();
175
- turnStateCache.clear();
176
- installationIdCache.clear();
588
+ const state = runtime();
589
+ state.sessions.clear();
590
+ delete state.installationId;
591
+ delete state.installationPath;
177
592
  }
178
593
 
179
- /** Current number of cached identities (diagnostics/tests). */
180
594
  export function codexWireIdentityCount(): number {
181
- return sessionCache.size;
595
+ return runtime().sessions.size;
182
596
  }