@pasko70/pibo 2.5.2 → 3.0.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.
Files changed (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +457 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -0,0 +1,294 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { PENDING_NATIVE_SESSION_METADATA_KEY, } from "../../sessions/runtime-binding.js";
4
+ export const CODEX_FIRST_USE_METADATA_KEY = "codexNativeFirstUse";
5
+ export const CODEX_FIRST_USE_METADATA_VERSION = 3;
6
+ const LEGACY_BYTE_EXACT_METADATA_VERSIONS = new Set([1, 2]);
7
+ const MAX_NATIVE_ID_LENGTH = 512;
8
+ const MAX_MESSAGE_ID_LENGTH = 512;
9
+ const MAX_PROCESS_START_ID_LENGTH = 64;
10
+ const MAX_PID = 0x7fff_ffff;
11
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
12
+ const UUID_V4_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
13
+ const PROCESS_START_ID_PATTERN = /^([1-9][0-9]{0,9}):([1-9][0-9]{0,30})$/;
14
+ const TERMINAL_TURN_STATUSES = new Set(["completed", "interrupted", "failed"]);
15
+ const PROCESS_INSTANCE_ID = randomUUID();
16
+ const activeAttemptIds = new Set();
17
+ function boundedNonEmptyString(value, maximumLength) {
18
+ return typeof value === "string"
19
+ && value.length > 0
20
+ && value.length <= maximumLength
21
+ && !/[\u0000-\u001f\u007f]/.test(value);
22
+ }
23
+ function validPid(value) {
24
+ return Number.isSafeInteger(value) && Number(value) > 0 && Number(value) <= MAX_PID;
25
+ }
26
+ function errorCode(error) {
27
+ return error && typeof error === "object" && "code" in error && typeof error.code === "string"
28
+ ? error.code
29
+ : undefined;
30
+ }
31
+ function probeLinuxProcess(pid, readProcStat) {
32
+ let stat;
33
+ try {
34
+ stat = readProcStat(pid);
35
+ }
36
+ catch (error) {
37
+ return ["ENOENT", "ESRCH"].includes(errorCode(error) ?? "")
38
+ ? { liveness: "dead" }
39
+ : { liveness: "ambiguous" };
40
+ }
41
+ const close = stat.lastIndexOf(")");
42
+ if (close < 2 || close + 2 >= stat.length)
43
+ return { liveness: "ambiguous" };
44
+ const fields = stat.slice(close + 2).trim().split(/\s+/);
45
+ if (fields.length <= 19)
46
+ return { liveness: "ambiguous" };
47
+ if (fields[0] === "Z")
48
+ return { liveness: "dead" };
49
+ const startTicks = fields[19];
50
+ if (!startTicks || !/^[1-9][0-9]{0,30}$/.test(startTicks))
51
+ return { liveness: "ambiguous" };
52
+ return { liveness: "active", startId: `${pid}:${startTicks}` };
53
+ }
54
+ function probePortableProcess(pid, probePid) {
55
+ try {
56
+ probePid(pid);
57
+ return "active";
58
+ }
59
+ catch (error) {
60
+ return errorCode(error) === "ESRCH" ? "dead" : "ambiguous";
61
+ }
62
+ }
63
+ function defaultReadProcStat(pid) {
64
+ return readFileSync(`/proc/${pid}/stat`, "utf8");
65
+ }
66
+ function defaultProbePid(pid) {
67
+ process.kill(pid, 0);
68
+ }
69
+ function hashUtf8(value) {
70
+ return createHash("sha256").update(value, "utf8").digest("hex");
71
+ }
72
+ /**
73
+ * Canonical first-use prompts use LF line endings and Unicode NFC. Pibo hashes
74
+ * this representation before native execution and when proving native history.
75
+ */
76
+ export function canonicalizeCodexNativeFirstUsePrompt(prompt) {
77
+ return prompt.replace(/\r\n?/g, "\n").normalize("NFC");
78
+ }
79
+ export function hashCanonicalCodexNativeFirstUsePrompt(prompt) {
80
+ return hashUtf8(canonicalizeCodexNativeFirstUsePrompt(prompt));
81
+ }
82
+ function hashPersistedPromptEvidence(identity, prompt) {
83
+ return LEGACY_BYTE_EXACT_METADATA_VERSIONS.has(identity.version)
84
+ ? hashUtf8(prompt)
85
+ : hashCanonicalCodexNativeFirstUsePrompt(prompt);
86
+ }
87
+ export function codexNativeFirstUseDeliveryReceipt(pending) {
88
+ return {
89
+ version: pending.version,
90
+ state: "delivered",
91
+ messageId: pending.messageId,
92
+ promptHash: pending.promptHash,
93
+ };
94
+ }
95
+ export function readCodexNativePendingFirstUse(binding) {
96
+ const value = binding.metadata?.[CODEX_FIRST_USE_METADATA_KEY];
97
+ if (value === undefined)
98
+ return undefined;
99
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
100
+ throw new Error("The pending native Codex first-use metadata is invalid.");
101
+ }
102
+ const record = value;
103
+ const allowedKeys = new Set([
104
+ "version",
105
+ "state",
106
+ "threadId",
107
+ "messageId",
108
+ "promptHash",
109
+ "attemptId",
110
+ "ownerPid",
111
+ "ownerProcessStartId",
112
+ "ownerProcessInstanceId",
113
+ ]);
114
+ const ownerProcessStartId = record.ownerProcessStartId;
115
+ const startMatch = typeof ownerProcessStartId === "string"
116
+ ? PROCESS_START_ID_PATTERN.exec(ownerProcessStartId)
117
+ : undefined;
118
+ if (Object.keys(record).some((key) => !allowedKeys.has(key))
119
+ || (record.version !== 1 && record.version !== 2 && record.version !== CODEX_FIRST_USE_METADATA_VERSION)
120
+ || record.state !== "pending"
121
+ || !boundedNonEmptyString(record.threadId, MAX_NATIVE_ID_LENGTH)
122
+ || !boundedNonEmptyString(record.messageId, MAX_MESSAGE_ID_LENGTH)
123
+ || typeof record.promptHash !== "string"
124
+ || !SHA256_PATTERN.test(record.promptHash)
125
+ || typeof record.attemptId !== "string"
126
+ || !UUID_V4_PATTERN.test(record.attemptId)
127
+ || !validPid(record.ownerPid)
128
+ || typeof record.ownerProcessInstanceId !== "string"
129
+ || !UUID_V4_PATTERN.test(record.ownerProcessInstanceId)
130
+ || (ownerProcessStartId !== undefined
131
+ && (typeof ownerProcessStartId !== "string"
132
+ || ownerProcessStartId.length > MAX_PROCESS_START_ID_LENGTH
133
+ || !startMatch
134
+ || Number(startMatch[1]) !== record.ownerPid))) {
135
+ throw new Error("The pending native Codex first-use metadata is invalid.");
136
+ }
137
+ return record;
138
+ }
139
+ export function readCodexNativeFirstUseDeliveryReceipt(binding) {
140
+ const value = binding.metadata?.[CODEX_FIRST_USE_METADATA_KEY];
141
+ if (value === undefined)
142
+ return undefined;
143
+ if (binding.state !== "bound"
144
+ || binding.metadata?.[PENDING_NATIVE_SESSION_METADATA_KEY] !== undefined
145
+ || !value
146
+ || typeof value !== "object"
147
+ || Array.isArray(value)) {
148
+ throw new Error("The delivered native Codex first-use receipt is invalid.");
149
+ }
150
+ const record = value;
151
+ const allowedKeys = new Set(["version", "state", "messageId", "promptHash"]);
152
+ if (Object.keys(record).some((key) => !allowedKeys.has(key))
153
+ || (record.version !== 1 && record.version !== 2 && record.version !== CODEX_FIRST_USE_METADATA_VERSION)
154
+ || record.state !== "delivered"
155
+ || !boundedNonEmptyString(record.messageId, MAX_MESSAGE_ID_LENGTH)
156
+ || typeof record.promptHash !== "string"
157
+ || !SHA256_PATTERN.test(record.promptHash)) {
158
+ throw new Error("The delivered native Codex first-use receipt is invalid.");
159
+ }
160
+ return record;
161
+ }
162
+ export function beginCodexNativeFirstUseAttempt() {
163
+ const attemptId = randomUUID();
164
+ let ownerProcessStartId;
165
+ if (process.platform === "linux") {
166
+ const probe = probeLinuxProcess(process.pid, defaultReadProcStat);
167
+ if (probe.liveness !== "active" || !probe.startId) {
168
+ throw new Error("Native Codex first use cannot establish the current process identity safely.");
169
+ }
170
+ ownerProcessStartId = probe.startId;
171
+ }
172
+ activeAttemptIds.add(attemptId);
173
+ return {
174
+ attemptId,
175
+ ownerPid: process.pid,
176
+ ...(ownerProcessStartId ? { ownerProcessStartId } : {}),
177
+ ownerProcessInstanceId: PROCESS_INSTANCE_ID,
178
+ };
179
+ }
180
+ export function endCodexNativeFirstUseAttempt(attemptId) {
181
+ activeAttemptIds.delete(attemptId);
182
+ }
183
+ export function codexNativePendingFirstUseOwnerLiveness(pending, dependencies = {}) {
184
+ const platform = dependencies.platform ?? process.platform;
185
+ const currentPid = dependencies.currentPid ?? process.pid;
186
+ const currentProcessInstanceId = dependencies.currentProcessInstanceId ?? PROCESS_INSTANCE_ID;
187
+ let processLiveness;
188
+ let observedStartId;
189
+ if (platform === "linux") {
190
+ const probe = probeLinuxProcess(pending.ownerPid, dependencies.readProcStat ?? defaultReadProcStat);
191
+ processLiveness = probe.liveness;
192
+ observedStartId = probe.startId;
193
+ if (processLiveness !== "active")
194
+ return processLiveness;
195
+ if (!pending.ownerProcessStartId || !observedStartId)
196
+ return "ambiguous";
197
+ if (pending.ownerProcessStartId !== observedStartId)
198
+ return "dead";
199
+ }
200
+ else {
201
+ processLiveness = probePortableProcess(pending.ownerPid, dependencies.probePid ?? defaultProbePid);
202
+ if (processLiveness !== "active")
203
+ return processLiveness;
204
+ }
205
+ if (pending.ownerPid !== currentPid)
206
+ return "active";
207
+ if (pending.ownerProcessInstanceId !== currentProcessInstanceId)
208
+ return "ambiguous";
209
+ return activeAttemptIds.has(pending.attemptId) ? "active" : "dead";
210
+ }
211
+ function exactTextUserItem(item) {
212
+ if (item.type !== "userMessage" || !Array.isArray(item.content) || item.content.length !== 1)
213
+ return undefined;
214
+ const part = item.content[0];
215
+ if (!part || typeof part !== "object" || Array.isArray(part))
216
+ return undefined;
217
+ const record = part;
218
+ if (record.type !== "text" || typeof record.text !== "string")
219
+ return undefined;
220
+ if (item.clientId !== undefined && item.clientId !== null && typeof item.clientId !== "string")
221
+ return undefined;
222
+ return { ...(item.clientId !== undefined ? { clientId: item.clientId } : {}), text: record.text };
223
+ }
224
+ function assertCodexNativeFirstUseTurnEvidence(identity, turn, state) {
225
+ if (!TERMINAL_TURN_STATUSES.has(turn.status)) {
226
+ throw new Error(`The ${state} native Codex first turn is not terminal and cannot be reconciled safely.`);
227
+ }
228
+ const userItems = turn.items.filter((item) => item.type === "userMessage");
229
+ const evidence = userItems.map(exactTextUserItem);
230
+ if (evidence.length === 0 || evidence.some((item) => !item)) {
231
+ throw new Error(`The ${state} native Codex first turn has ambiguous user input evidence.`);
232
+ }
233
+ const exactEvidence = evidence;
234
+ const evidenceWithClientIds = exactEvidence.filter((item) => item.clientId !== undefined && item.clientId !== null);
235
+ let firstRequestEvidence;
236
+ if (evidenceWithClientIds.length > 0) {
237
+ if (evidenceWithClientIds.length !== exactEvidence.length) {
238
+ throw new Error(`The ${state} native Codex first turn has ambiguous user input evidence.`);
239
+ }
240
+ const matches = evidenceWithClientIds.filter((item) => item.clientId === identity.messageId);
241
+ if (matches.length === 0) {
242
+ throw new Error(`The ${state} native Codex first turn does not match the persisted message id.`);
243
+ }
244
+ if (matches.length > 1) {
245
+ throw new Error(`The ${state} native Codex first turn has ambiguous user input evidence.`);
246
+ }
247
+ firstRequestEvidence = matches[0];
248
+ }
249
+ else {
250
+ if (exactEvidence.length !== 1) {
251
+ throw new Error(`The ${state} native Codex first turn has ambiguous user input evidence.`);
252
+ }
253
+ firstRequestEvidence = exactEvidence[0];
254
+ }
255
+ if (hashPersistedPromptEvidence(identity, firstRequestEvidence.text) !== identity.promptHash) {
256
+ throw new Error(`The ${state} native Codex first turn does not match the persisted prompt hash.`);
257
+ }
258
+ if (firstRequestEvidence.clientId !== undefined
259
+ && firstRequestEvidence.clientId !== null
260
+ && !boundedNonEmptyString(firstRequestEvidence.clientId, MAX_MESSAGE_ID_LENGTH)) {
261
+ throw new Error(`The ${state} native Codex first turn has invalid message identity evidence.`);
262
+ }
263
+ }
264
+ export function assertCodexNativePendingFirstUseTurn(pending, thread) {
265
+ if (thread.id !== pending.threadId || thread.turns.length !== 1) {
266
+ throw new Error("The pending native Codex first turn is missing, multiple, or belongs to another thread.");
267
+ }
268
+ assertCodexNativeFirstUseTurnEvidence(pending, thread.turns[0], "pending");
269
+ }
270
+ export function assertCodexNativeFirstUseDeliveryReceiptTurn(receipt, thread) {
271
+ const firstTurn = thread.turns[0];
272
+ if (!firstTurn)
273
+ throw new Error("The delivered native Codex first turn is missing.");
274
+ assertCodexNativeFirstUseTurnEvidence(receipt, firstTurn, "delivered");
275
+ return firstTurn;
276
+ }
277
+ export function isExactCodexNativeFirstUseDeliveryReplay(receipt, messageId, prompt) {
278
+ if (messageId !== receipt.messageId)
279
+ return false;
280
+ if (hashPersistedPromptEvidence(receipt, prompt) !== receipt.promptHash) {
281
+ throw new Error("Native Codex first-use replay reuses the delivered message id with a different prompt.");
282
+ }
283
+ return true;
284
+ }
285
+ export function assertCodexNativePendingFirstUseRequest(pending, messageId, prompt) {
286
+ if (messageId !== pending.messageId || hashPersistedPromptEvidence(pending, prompt) !== pending.promptHash) {
287
+ throw new Error("Native Codex first-use retry does not match the persisted message id and prompt.");
288
+ }
289
+ }
290
+ export function assertCodexNativeFirstUseMessageId(messageId) {
291
+ if (!boundedNonEmptyString(messageId, MAX_MESSAGE_ID_LENGTH)) {
292
+ throw new Error("Native Codex first-use message id is invalid.");
293
+ }
294
+ }
@@ -1,5 +1,7 @@
1
+ import { historyReconciliationDigest } from "../../agent-runtime/history.js";
1
2
  import { redactCodexNativeSensitiveText } from "./redaction.js";
2
3
  import { CODEX_NATIVE_ADAPTER_ID } from "./thread.js";
4
+ import { TRACE_RECONCILIATION_ENTRY_CAP } from "../../shared/trace-limits.js";
3
5
  const CODEX_HISTORY_CURSOR_PREFIX = "codex-history:";
4
6
  const DEFAULT_HISTORY_LIMIT = 100;
5
7
  const MAX_HISTORY_LIMIT = 200;
@@ -67,7 +69,7 @@ function reasoningParts(item) {
67
69
  .map(redactCodexNativeSensitiveText);
68
70
  return values.length > 0 ? [{ type: "reasoning", text: values.join("\n") }] : [];
69
71
  }
70
- function itemEntry(thread, turn, item, sequence) {
72
+ function itemEntry(thread, turn, item, sequence, historyPosition, historyScopeId) {
71
73
  const createdAt = isoFromSeconds(turn.startedAt ?? turn.completedAt, thread.updatedAt);
72
74
  const base = {
73
75
  id: `codex:${thread.id}:${turn.id}:${item.id}`,
@@ -75,6 +77,8 @@ function itemEntry(thread, turn, item, sequence) {
75
77
  source: "native",
76
78
  createdAt,
77
79
  sequence,
80
+ historyPosition,
81
+ historyScopeId,
78
82
  turnId: turn.id,
79
83
  nativeTurnId: turn.id,
80
84
  nativeEntryId: item.id,
@@ -159,7 +163,7 @@ function itemEntry(thread, turn, item, sequence) {
159
163
  }
160
164
  return undefined;
161
165
  }
162
- export function codexThreadHistoryEntries(thread) {
166
+ export function codexThreadHistoryEntries(thread, historyScopeId = `codex:${thread.id}`) {
163
167
  const entries = [];
164
168
  let sequence = 0;
165
169
  if (thread.name) {
@@ -169,13 +173,15 @@ export function codexThreadHistoryEntries(thread) {
169
173
  source: "native",
170
174
  createdAt: isoFromSeconds(thread.createdAt, thread.createdAt),
171
175
  sequence: sequence++,
176
+ historyPosition: `codex:${thread.id}:session-info`,
177
+ historyScopeId,
172
178
  nativeEntryId: thread.id,
173
179
  name: redactCodexNativeSensitiveText(thread.name),
174
180
  });
175
181
  }
176
- for (const turn of thread.turns) {
177
- for (const item of turn.items) {
178
- const entry = itemEntry(thread, turn, item, sequence);
182
+ for (const [turnIndex, turn] of thread.turns.entries()) {
183
+ for (const [itemIndex, item] of turn.items.entries()) {
184
+ const entry = itemEntry(thread, turn, item, sequence, `codex:${thread.id}:turn:${turnIndex}:item:${itemIndex}:${item.type}`, historyScopeId);
179
185
  if (!entry)
180
186
  continue;
181
187
  entries.push(entry);
@@ -187,7 +193,9 @@ export function codexThreadHistoryEntries(thread) {
187
193
  type: "message",
188
194
  source: "native",
189
195
  createdAt: isoFromSeconds(turn.completedAt ?? turn.startedAt, thread.updatedAt),
190
- sequence: sequence++,
196
+ sequence,
197
+ historyPosition: `codex:${thread.id}:turn:${turnIndex}:error`,
198
+ historyScopeId,
191
199
  turnId: turn.id,
192
200
  nativeTurnId: turn.id,
193
201
  nativeEntryId: `${turn.id}:error`,
@@ -196,6 +204,7 @@ export function codexThreadHistoryEntries(thread) {
196
204
  status: "error",
197
205
  error: "Codex turn failed.",
198
206
  });
207
+ sequence += 1;
199
208
  }
200
209
  }
201
210
  return entries;
@@ -256,7 +265,8 @@ function selectedLimit(value) {
256
265
  return Math.min(value, MAX_HISTORY_LIMIT);
257
266
  }
258
267
  export function pageCodexThreadHistory(input) {
259
- const allEntries = codexThreadHistoryEntries(input.thread);
268
+ const historyScopeId = `codex:${input.runtimeInstanceId}:${input.thread.id}`;
269
+ const allEntries = codexThreadHistoryEntries(input.thread, historyScopeId);
260
270
  const beforeTimestampMs = input.beforeTimestamp === undefined ? undefined : Date.parse(input.beforeTimestamp);
261
271
  if (beforeTimestampMs !== undefined && Number.isNaN(beforeTimestampMs)) {
262
272
  throw new Error("Codex native history beforeTimestamp is invalid.");
@@ -270,11 +280,20 @@ export function pageCodexThreadHistory(input) {
270
280
  const start = Math.max(0, end - limit);
271
281
  const entries = filtered.slice(start, end);
272
282
  const hasMore = start > 0;
283
+ const proofComplete = allEntries.length <= TRACE_RECONCILIATION_ENTRY_CAP;
273
284
  return {
274
285
  runtimeInstanceId: input.runtimeInstanceId,
275
286
  adapterId: CODEX_NATIVE_ADAPTER_ID,
276
287
  source: "native",
277
288
  entries,
289
+ reconciliationProof: proofComplete
290
+ ? {
291
+ complete: true,
292
+ scopeId: historyScopeId,
293
+ fullScope: { entryCount: allEntries.length, digest: historyReconciliationDigest(allEntries) },
294
+ entries: allEntries,
295
+ }
296
+ : { complete: false, scopeId: historyScopeId, entries },
278
297
  orderOffset: start,
279
298
  ...(hasMore ? { nextCursor: encodeCursor({ v: 1, threadId: input.thread.id, beforeIndex: start }) } : {}),
280
299
  hasMore,
@@ -419,6 +419,12 @@ export async function startCodexNativeAppServer(input) {
419
419
  if (!input.clientVersion.trim()) {
420
420
  throw new CodexNativeProcessError("start_failed", "Codex App Server client version is required.");
421
421
  }
422
+ if (input.realtimeSidebandBaseUrl !== undefined && !input.realtimeSidebandBaseUrl.trim()) {
423
+ throw new CodexNativeProcessError("start_failed", "Codex realtime sideband base URL must not be empty.");
424
+ }
425
+ if (input.realtimeWebrtcCallBaseUrl !== undefined && !input.realtimeWebrtcCallBaseUrl.trim()) {
426
+ throw new CodexNativeProcessError("start_failed", "Codex realtime WebRTC call base URL must not be empty.");
427
+ }
422
428
  const paths = await prepareCodexNativeSessionPaths(input);
423
429
  let client;
424
430
  try {
@@ -431,6 +437,13 @@ export async function startCodexNativeAppServer(input) {
431
437
  `tools.experimental_request_user_input.enabled=${input.config.experimentalUserInput}`,
432
438
  "-c",
433
439
  `features.default_mode_request_user_input=${input.config.experimentalUserInput}`,
440
+ ...(input.realtimeConversation ? ["-c", "features.realtime_conversation=true"] : []),
441
+ ...(input.realtimeSidebandBaseUrl
442
+ ? ["-c", `experimental_realtime_ws_base_url=${JSON.stringify(input.realtimeSidebandBaseUrl)}`]
443
+ : []),
444
+ ...(input.realtimeWebrtcCallBaseUrl
445
+ ? ["-c", `experimental_realtime_webrtc_call_base_url=${JSON.stringify(input.realtimeWebrtcCallBaseUrl)}`]
446
+ : []),
434
447
  ]);
435
448
  client = await CodexAppServerClient.start({
436
449
  command: invocation.command,
@@ -450,6 +463,7 @@ export async function startCodexNativeAppServer(input) {
450
463
  shutdownTimeoutMs: input.config.shutdownTimeoutMs,
451
464
  killTimeoutMs: input.config.killTimeoutMs,
452
465
  onDiagnostic: input.onDiagnostic,
466
+ signal: input.signal,
453
467
  });
454
468
  const [reportedHome, expectedHome] = await Promise.all([
455
469
  realpath(client.initializeResponse.codexHome).catch(() => resolve(client.initializeResponse.codexHome)),
@@ -165,17 +165,14 @@ function secondsToIso(seconds) {
165
165
  const date = new Date(Number(seconds) * 1_000);
166
166
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
167
167
  }
168
- function itemText(item) {
169
- if (item.type === "agentMessage" && typeof item.text === "string")
170
- return redactCodexNativeSensitiveText(item.text);
171
- if (item.type === "userMessage" && Array.isArray(item.content)) {
172
- const text = item.content
173
- .filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
174
- .map((part) => redactCodexNativeSensitiveText(part.text))
175
- .join("\n");
176
- return text || undefined;
177
- }
178
- return undefined;
168
+ function userMessageText(item) {
169
+ if (item.type !== "userMessage" || !Array.isArray(item.content))
170
+ return undefined;
171
+ const text = item.content
172
+ .filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
173
+ .map((part) => redactCodexNativeSensitiveText(part.text))
174
+ .join("\n");
175
+ return text || undefined;
179
176
  }
180
177
  function threadLeafId(thread) {
181
178
  return thread.turns.at(-1)?.id ?? null;
@@ -204,12 +201,21 @@ export function codexThreadInfo(runtimeInstanceId, thread) {
204
201
  firstMessage: thread.preview ? redactCodexNativeSensitiveText(thread.preview) : undefined,
205
202
  };
206
203
  }
207
- export function codexThreadForkCandidates(thread) {
208
- return thread.turns.map((turn) => {
209
- const text = [...turn.items].reverse().map(itemText).find((candidate) => candidate?.trim()) ?? `Codex turn ${turn.id}`;
210
- return { entryId: turn.id, text };
204
+ function codexThreadForkTargets(thread) {
205
+ return thread.turns.flatMap((turn, index) => {
206
+ const userMessage = turn.items.find((item) => item.type === "userMessage");
207
+ if (!userMessage)
208
+ return [];
209
+ return [{
210
+ entryId: userMessage.id,
211
+ text: userMessageText(userMessage) ?? `Codex turn ${turn.id}`,
212
+ ...(index > 0 ? { previousTurnId: thread.turns[index - 1].id } : {}),
213
+ }];
211
214
  });
212
215
  }
216
+ export function codexThreadForkCandidates(thread) {
217
+ return codexThreadForkTargets(thread).map(({ entryId, text }) => ({ entryId, text }));
218
+ }
213
219
  export class CodexNativeThreadController {
214
220
  client;
215
221
  currentThread;
@@ -241,6 +247,7 @@ export class CodexNativeThreadController {
241
247
  ...(selection.sandbox ? { sandbox: selection.sandbox } : {}),
242
248
  ...(selection.config ? { config: structuredClone(selection.config) } : {}),
243
249
  ...(selection.developerInstructions ? { developerInstructions: selection.developerInstructions } : {}),
250
+ ...(selection.personality !== undefined ? { personality: selection.personality } : {}),
244
251
  });
245
252
  }
246
253
  static async resume(client, threadId, workspace, selection = {}) {
@@ -265,6 +272,7 @@ export class CodexNativeThreadController {
265
272
  ...(selection.sandbox ? { sandbox: selection.sandbox } : {}),
266
273
  ...(selection.config ? { config: structuredClone(selection.config) } : {}),
267
274
  ...(selection.developerInstructions ? { developerInstructions: selection.developerInstructions } : {}),
275
+ ...(selection.personality !== undefined ? { personality: selection.personality } : {}),
268
276
  });
269
277
  }
270
278
  catch (error) {
@@ -345,15 +353,29 @@ export class CodexNativeThreadController {
345
353
  .sort((left, right) => right.updatedAt - left.updatedAt || right.createdAt - left.createdAt)
346
354
  .map((thread) => codexThreadInfo(runtimeInstanceId, thread));
347
355
  }
348
- async fork(runtimeInstanceId, workspace, lastTurnId, validateThread) {
349
- if (!lastTurnId.trim())
350
- throw new Error("Codex thread fork requires a native turn id.");
351
- return await this.forkAt(runtimeInstanceId, workspace, lastTurnId, validateThread);
356
+ async fork(runtimeInstanceId, workspace, entryId, validateThread) {
357
+ if (!entryId.trim())
358
+ throw new Error("Codex thread fork requires a native user-message id.");
359
+ const target = codexThreadForkTargets(this.currentThread).find((candidate) => candidate.entryId === entryId);
360
+ if (target) {
361
+ if (!target.previousTurnId) {
362
+ return this.branchBeforeFirstTurn(runtimeInstanceId, workspace, target);
363
+ }
364
+ return await this.forkAt(runtimeInstanceId, workspace, target.previousTurnId, validateThread, target);
365
+ }
366
+ const legacyTurn = this.currentThread.turns.find((turn) => turn.id === entryId);
367
+ if (!legacyTurn)
368
+ throw new Error(`Codex fork target "${entryId}" was not found.`);
369
+ const legacyText = legacyTurn.items.map(userMessageText).find((text) => text?.trim());
370
+ return await this.forkAt(runtimeInstanceId, workspace, entryId, validateThread, {
371
+ entryId,
372
+ text: legacyText ?? `Codex turn ${entryId}`,
373
+ });
352
374
  }
353
375
  async clone(runtimeInstanceId, workspace, validateThread) {
354
376
  return await this.forkAt(runtimeInstanceId, workspace, undefined, validateThread);
355
377
  }
356
- async forkAt(runtimeInstanceId, workspace, lastTurnId, validateThread) {
378
+ async forkAt(runtimeInstanceId, workspace, lastTurnId, validateThread, selectedTarget) {
357
379
  const previousThread = this.currentThread;
358
380
  const previous = codexThreadSnapshot(runtimeInstanceId, previousThread);
359
381
  const params = {
@@ -376,9 +398,6 @@ export class CodexNativeThreadController {
376
398
  throw new CodexNativeThreadProtocolError("Codex thread/fork returned the source thread id.");
377
399
  }
378
400
  await validateThread?.(forked.id);
379
- const selectedText = lastTurnId
380
- ? codexThreadForkCandidates(previousThread).find((candidate) => candidate.entryId === lastTurnId)?.text
381
- : undefined;
382
401
  this.knownThreads.set(previousThread.id, structuredClone(previousThread));
383
402
  this.knownThreads.set(forked.id, structuredClone(forked));
384
403
  this.currentThread = forked;
@@ -387,12 +406,27 @@ export class CodexNativeThreadController {
387
406
  previous,
388
407
  current: codexThreadSnapshot(runtimeInstanceId, forked),
389
408
  cancelled: false,
390
- ...(selectedText ? { selectedText } : {}),
391
- ...(lastTurnId ? { summaryEntryId: lastTurnId } : {}),
409
+ ...(selectedTarget?.text ? { selectedText: selectedTarget.text } : {}),
410
+ ...(selectedTarget ? { summaryEntryId: selectedTarget.entryId } : {}),
392
411
  };
393
412
  }
394
413
  catch (error) {
395
414
  return normalizeThreadError(error, previousThread.id);
396
415
  }
397
416
  }
417
+ branchBeforeFirstTurn(runtimeInstanceId, workspace, target) {
418
+ const previousThread = this.currentThread;
419
+ return {
420
+ previous: codexThreadSnapshot(runtimeInstanceId, previousThread),
421
+ current: {
422
+ adapterId: CODEX_NATIVE_ADAPTER_ID,
423
+ runtimeInstanceId,
424
+ cwd: workspace,
425
+ leafId: null,
426
+ },
427
+ cancelled: false,
428
+ selectedText: target.text,
429
+ summaryEntryId: target.entryId,
430
+ };
431
+ }
398
432
  }
@@ -319,6 +319,33 @@ export class CodexNativeTurnController {
319
319
  throw error;
320
320
  }
321
321
  }
322
+ replayTerminalTurn(turn) {
323
+ if (this.disposed)
324
+ throw new Error("Codex native turn controller is disposed.");
325
+ if (this.pending)
326
+ throw new Error("Codex native session already has an active turn.");
327
+ if (turn.status === "inProgress") {
328
+ throw new CodexNativeTurnProtocolError("Codex cannot replay a non-terminal native turn.");
329
+ }
330
+ const replay = newPendingTurn();
331
+ this.assignTurnId(replay, turn.id);
332
+ replay.started = true;
333
+ for (const item of turn.items)
334
+ this.completeItem(replay, item);
335
+ this.finishIncompleteItems(replay, turn.status);
336
+ if (turn.status === "failed") {
337
+ const message = errorMessage(turn.error);
338
+ this.emit({
339
+ type: "turn_failed",
340
+ message,
341
+ details: providerErrorDetails(turn.error, message, false),
342
+ turnId: turn.id,
343
+ });
344
+ }
345
+ else {
346
+ this.emit({ type: "turn_completed", status: turn.status, turnId: turn.id });
347
+ }
348
+ }
322
349
  async compact() {
323
350
  if (this.disposed)
324
351
  throw new Error("Codex native turn controller is disposed.");
@@ -0,0 +1,8 @@
1
+ import { isCodexNativeBuiltInHistoryReconciliationProof } from "./codex-native/adapter.js";
2
+ import { isOmpBuiltInHistoryReconciliationProof } from "./omp/adapter.js";
3
+ import { isPiBuiltInHistoryReconciliationProof } from "./pi/adapter.js";
4
+ export function isBuiltInHistoryReconciliationProof(proof) {
5
+ return proof !== undefined && (isPiBuiltInHistoryReconciliationProof(proof)
6
+ || isOmpBuiltInHistoryReconciliationProof(proof)
7
+ || isCodexNativeBuiltInHistoryReconciliationProof(proof));
8
+ }