@junctionpanel/server 0.1.62 → 0.1.64
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.
- package/dist/server/client/daemon-client-transport-utils.d.ts.map +1 -1
- package/dist/server/client/daemon-client-transport-utils.js +8 -6
- package/dist/server/client/daemon-client-transport-utils.js.map +1 -1
- package/dist/server/client/daemon-client.d.ts +24 -0
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +232 -2
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +5 -1
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +18 -2
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
- package/dist/server/server/agent/agent-projections.js +3 -0
- package/dist/server/server/agent/agent-projections.js.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +13 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +3 -0
- package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.js +1 -0
- package/dist/server/server/agent/agent-storage.js.map +1 -1
- package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
- package/dist/server/server/agent/mcp-server.js +2 -0
- package/dist/server/server/agent/mcp-server.js.map +1 -1
- package/dist/server/server/agent/provider-registry.d.ts.map +1 -1
- package/dist/server/server/agent/provider-registry.js +27 -11
- package/dist/server/server/agent/provider-registry.js.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.js +1 -0
- package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +59 -0
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +31 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js +1022 -50
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/gemini-agent.js +1 -0
- package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.js +95 -14
- package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
- package/dist/server/server/bootstrap.d.ts +22 -0
- package/dist/server/server/bootstrap.d.ts.map +1 -1
- package/dist/server/server/bootstrap.js +10 -0
- package/dist/server/server/bootstrap.js.map +1 -1
- package/dist/server/server/persisted-config.d.ts +4 -4
- package/dist/server/server/session.d.ts +26 -0
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +439 -5
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/voice-config.d.ts +10 -0
- package/dist/server/server/voice-config.d.ts.map +1 -0
- package/dist/server/server/voice-config.js +44 -0
- package/dist/server/server/voice-config.js.map +1 -0
- package/dist/server/server/websocket-server.d.ts +26 -2
- package/dist/server/server/websocket-server.d.ts.map +1 -1
- package/dist/server/server/websocket-server.js +133 -41
- package/dist/server/server/websocket-server.js.map +1 -1
- package/dist/server/shared/binary-mux.d.ts.map +1 -1
- package/dist/server/shared/binary-mux.js +3 -2
- package/dist/server/shared/binary-mux.js.map +1 -1
- package/dist/server/shared/messages.d.ts +8394 -2300
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +135 -0
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/terminal/terminal.d.ts.map +1 -1
- package/dist/server/terminal/terminal.js +58 -10
- package/dist/server/terminal/terminal.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pino from "pino";
|
|
2
2
|
import { execSync, spawn } from "node:child_process";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import { createReadStream } from "node:fs";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import readline from "node:readline";
|
|
@@ -14,7 +15,44 @@ import { writeImageAttachment } from "./image-attachments.js";
|
|
|
14
15
|
const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
|
|
15
16
|
const TURN_START_TIMEOUT_MS = 90 * 1000;
|
|
16
17
|
const TURN_COMPLETION_SETTLE_DELAY_MS = 75;
|
|
18
|
+
const CODEX_ROLLOUT_SPAWN_POLL_MS = 900;
|
|
17
19
|
const CODEX_PROVIDER = "codex";
|
|
20
|
+
const CODEX_SESSION_SCAN_MAX_DEPTH = 4;
|
|
21
|
+
const CODEX_THREAD_LIST_SOURCE_KINDS = [
|
|
22
|
+
"cli",
|
|
23
|
+
"vscode",
|
|
24
|
+
"exec",
|
|
25
|
+
"appServer",
|
|
26
|
+
"subAgent",
|
|
27
|
+
"subAgentReview",
|
|
28
|
+
"subAgentCompact",
|
|
29
|
+
"subAgentThreadSpawn",
|
|
30
|
+
"subAgentOther",
|
|
31
|
+
"unknown",
|
|
32
|
+
];
|
|
33
|
+
function parseCodexThreadListAllowedSourceKinds(errorMessage) {
|
|
34
|
+
const match = /expected one of (.+)$/i.exec(errorMessage);
|
|
35
|
+
if (!match) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const allowed = match[1]
|
|
39
|
+
.split(",")
|
|
40
|
+
.map((value) => value.trim().replace(/^`|`$/g, ""))
|
|
41
|
+
.filter((value) => value.length > 0);
|
|
42
|
+
return allowed.length > 0 ? allowed : null;
|
|
43
|
+
}
|
|
44
|
+
function resolveCodexThreadListRetrySourceKinds(preferredSourceKinds, errorMessage) {
|
|
45
|
+
const allowedSourceKinds = parseCodexThreadListAllowedSourceKinds(errorMessage);
|
|
46
|
+
if (!allowedSourceKinds) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const allowedSourceKindSet = new Set(allowedSourceKinds);
|
|
50
|
+
const retrySourceKinds = preferredSourceKinds.filter((value) => allowedSourceKindSet.has(value));
|
|
51
|
+
if (retrySourceKinds.length === 0 || retrySourceKinds.length === preferredSourceKinds.length) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return retrySourceKinds;
|
|
55
|
+
}
|
|
18
56
|
const CODEX_APP_SERVER_CAPABILITIES = {
|
|
19
57
|
supportsStreaming: true,
|
|
20
58
|
supportsSessionPersistence: true,
|
|
@@ -112,6 +150,41 @@ function resolveCodexLaunchPrefix(runtimeSettings) {
|
|
|
112
150
|
function resolveCodexHomeDir() {
|
|
113
151
|
return process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
114
152
|
}
|
|
153
|
+
function resolveCodexSessionRoot() {
|
|
154
|
+
return process.env.CODEX_SESSION_DIR ?? path.join(resolveCodexHomeDir(), "sessions");
|
|
155
|
+
}
|
|
156
|
+
function isCodexRolloutFileName(fileName) {
|
|
157
|
+
return (fileName.startsWith("rollout-") &&
|
|
158
|
+
(fileName.endsWith(".json") || fileName.endsWith(".jsonl")));
|
|
159
|
+
}
|
|
160
|
+
async function listCodexRolloutPaths(root) {
|
|
161
|
+
const rolloutPaths = [];
|
|
162
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
163
|
+
while (stack.length > 0) {
|
|
164
|
+
const current = stack.pop();
|
|
165
|
+
if (!current) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = await fs.readdir(current.dir, { withFileTypes: true });
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const entryPath = path.join(current.dir, entry.name);
|
|
177
|
+
if (entry.isFile() && isCodexRolloutFileName(entry.name)) {
|
|
178
|
+
rolloutPaths.push(entryPath);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (entry.isDirectory() && current.depth < CODEX_SESSION_SCAN_MAX_DEPTH) {
|
|
182
|
+
stack.push({ dir: entryPath, depth: current.depth + 1 });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return rolloutPaths;
|
|
187
|
+
}
|
|
115
188
|
function tokenizeCommandArgs(args) {
|
|
116
189
|
const tokens = [];
|
|
117
190
|
let current = "";
|
|
@@ -196,11 +269,368 @@ function toRecord(value) {
|
|
|
196
269
|
}
|
|
197
270
|
function toNonEmptyString(value) {
|
|
198
271
|
if (typeof value !== "string") {
|
|
272
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
273
|
+
return String(value);
|
|
274
|
+
}
|
|
199
275
|
return null;
|
|
200
276
|
}
|
|
201
277
|
const trimmed = value.trim();
|
|
202
278
|
return trimmed.length > 0 ? trimmed : null;
|
|
203
279
|
}
|
|
280
|
+
function readStringField(record, keys) {
|
|
281
|
+
if (!record) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
for (const key of keys) {
|
|
285
|
+
const value = toNonEmptyString(record[key]);
|
|
286
|
+
if (value) {
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
function readNestedRecord(record, keys) {
|
|
293
|
+
if (!record) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
for (const key of keys) {
|
|
297
|
+
const value = toRecord(record[key]);
|
|
298
|
+
if (value) {
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
function readCodexNotificationProvenance(input) {
|
|
305
|
+
const root = toRecord(input);
|
|
306
|
+
const turnRecord = readNestedRecord(root, ["turn"]);
|
|
307
|
+
const messageRecord = readNestedRecord(root, ["msg"]);
|
|
308
|
+
return {
|
|
309
|
+
threadId: readStringField(root, ["threadId", "thread_id"]) ??
|
|
310
|
+
readStringField(turnRecord, ["threadId", "thread_id"]) ??
|
|
311
|
+
readStringField(messageRecord, ["threadId", "thread_id"]),
|
|
312
|
+
turnId: readStringField(root, ["turnId", "turn_id"]) ??
|
|
313
|
+
readStringField(turnRecord, ["turnId", "turn_id"]) ??
|
|
314
|
+
readStringField(messageRecord, ["turnId", "turn_id"]),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function toTimestampDate(value, fallback) {
|
|
318
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
319
|
+
return new Date(value * 1000);
|
|
320
|
+
}
|
|
321
|
+
if (typeof value === "string") {
|
|
322
|
+
const numeric = Number(value);
|
|
323
|
+
if (Number.isFinite(numeric)) {
|
|
324
|
+
return new Date(numeric * 1000);
|
|
325
|
+
}
|
|
326
|
+
const parsed = Date.parse(value);
|
|
327
|
+
if (!Number.isNaN(parsed)) {
|
|
328
|
+
return new Date(parsed);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return fallback;
|
|
332
|
+
}
|
|
333
|
+
const codexPersistedThreadMetadataCache = new Map();
|
|
334
|
+
const CODEX_PERSISTED_THREAD_METADATA_CACHE_LIMIT = 512;
|
|
335
|
+
function setCodexPersistedThreadMetadataCache(rolloutPath, entry) {
|
|
336
|
+
if (codexPersistedThreadMetadataCache.has(rolloutPath)) {
|
|
337
|
+
codexPersistedThreadMetadataCache.delete(rolloutPath);
|
|
338
|
+
}
|
|
339
|
+
codexPersistedThreadMetadataCache.set(rolloutPath, entry);
|
|
340
|
+
while (codexPersistedThreadMetadataCache.size >
|
|
341
|
+
CODEX_PERSISTED_THREAD_METADATA_CACHE_LIMIT) {
|
|
342
|
+
const oldestKey = codexPersistedThreadMetadataCache.keys().next().value;
|
|
343
|
+
if (typeof oldestKey !== "string") {
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
codexPersistedThreadMetadataCache.delete(oldestKey);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function unwrapCodexPersistedThreadRecord(thread) {
|
|
350
|
+
const record = toRecord(thread);
|
|
351
|
+
if (!record) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
if (record.type === "session_meta") {
|
|
355
|
+
const payloadRecord = readNestedRecord(record, ["payload"]);
|
|
356
|
+
if (payloadRecord) {
|
|
357
|
+
return payloadRecord;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const nestedThreadRecord = readNestedRecord(record, ["thread"]);
|
|
361
|
+
if (nestedThreadRecord) {
|
|
362
|
+
return nestedThreadRecord;
|
|
363
|
+
}
|
|
364
|
+
return record;
|
|
365
|
+
}
|
|
366
|
+
function parseCodexPersistedThreadMetadata(thread, fallbackDate = new Date()) {
|
|
367
|
+
const threadRecord = unwrapCodexPersistedThreadRecord(thread);
|
|
368
|
+
const sourceRecord = readNestedRecord(threadRecord, ["source"]);
|
|
369
|
+
const subAgentRecord = readNestedRecord(sourceRecord, ["subAgent", "sub_agent", "subagent"]) ??
|
|
370
|
+
readNestedRecord(threadRecord, ["subAgent", "sub_agent", "subagent"]);
|
|
371
|
+
const threadSpawnRecord = readNestedRecord(subAgentRecord, ["thread_spawn", "threadSpawn"]) ??
|
|
372
|
+
readNestedRecord(sourceRecord, ["thread_spawn", "threadSpawn"]);
|
|
373
|
+
const createdAt = toTimestampDate(threadRecord?.createdAt, fallbackDate);
|
|
374
|
+
const updatedAt = toTimestampDate(threadRecord?.updatedAt ?? threadRecord?.createdAt, createdAt);
|
|
375
|
+
return {
|
|
376
|
+
threadId: toNonEmptyString(threadRecord?.id),
|
|
377
|
+
cwd: toNonEmptyString(threadRecord?.cwd),
|
|
378
|
+
title: toNonEmptyString(threadRecord?.preview) ?? null,
|
|
379
|
+
createdAt,
|
|
380
|
+
updatedAt,
|
|
381
|
+
parentSessionId: readStringField(threadRecord, [
|
|
382
|
+
"parentThreadId",
|
|
383
|
+
"parent_thread_id",
|
|
384
|
+
"forkedFromId",
|
|
385
|
+
"forked_from_id",
|
|
386
|
+
]) ??
|
|
387
|
+
readStringField(threadSpawnRecord, [
|
|
388
|
+
"parentThreadId",
|
|
389
|
+
"parent_thread_id",
|
|
390
|
+
"forkedFromId",
|
|
391
|
+
"forked_from_id",
|
|
392
|
+
]) ??
|
|
393
|
+
readStringField(subAgentRecord, [
|
|
394
|
+
"parentThreadId",
|
|
395
|
+
"parent_thread_id",
|
|
396
|
+
"forkedFromId",
|
|
397
|
+
"forked_from_id",
|
|
398
|
+
]),
|
|
399
|
+
rootSessionId: readStringField(threadRecord, ["rootThreadId", "root_thread_id"]) ??
|
|
400
|
+
readStringField(threadSpawnRecord, ["rootThreadId", "root_thread_id"]) ??
|
|
401
|
+
readStringField(subAgentRecord, ["rootThreadId", "root_thread_id"]),
|
|
402
|
+
agentId: readStringField(threadRecord, ["agentId", "agent_id"]) ??
|
|
403
|
+
readStringField(threadSpawnRecord, ["agentId", "agent_id", "id"]) ??
|
|
404
|
+
readStringField(subAgentRecord, ["agentId", "agent_id", "id"]),
|
|
405
|
+
agentNickname: readStringField(threadRecord, ["agentNickname", "agent_nickname"]) ??
|
|
406
|
+
readStringField(threadSpawnRecord, [
|
|
407
|
+
"agentNickname",
|
|
408
|
+
"agent_nickname",
|
|
409
|
+
"nickname",
|
|
410
|
+
"name",
|
|
411
|
+
]) ??
|
|
412
|
+
readStringField(subAgentRecord, [
|
|
413
|
+
"agentNickname",
|
|
414
|
+
"agent_nickname",
|
|
415
|
+
"nickname",
|
|
416
|
+
"name",
|
|
417
|
+
]),
|
|
418
|
+
agentRole: readStringField(threadRecord, [
|
|
419
|
+
"agentRole",
|
|
420
|
+
"agent_role",
|
|
421
|
+
"agentType",
|
|
422
|
+
"agent_type",
|
|
423
|
+
]) ??
|
|
424
|
+
readStringField(threadSpawnRecord, [
|
|
425
|
+
"agentRole",
|
|
426
|
+
"agent_role",
|
|
427
|
+
"agentType",
|
|
428
|
+
"agent_type",
|
|
429
|
+
]) ??
|
|
430
|
+
readStringField(subAgentRecord, [
|
|
431
|
+
"agentRole",
|
|
432
|
+
"agent_role",
|
|
433
|
+
"agentType",
|
|
434
|
+
"agent_type",
|
|
435
|
+
]),
|
|
436
|
+
model: readStringField(threadRecord, ["model", "modelProvider", "model_provider"]) ??
|
|
437
|
+
readStringField(threadSpawnRecord, ["model", "modelProvider", "model_provider"]) ??
|
|
438
|
+
readStringField(subAgentRecord, ["model", "modelProvider", "model_provider"]),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function mergeCodexPersistedThreadMetadata(base, incoming) {
|
|
442
|
+
const baseHasFallbackCwd = !base.cwd || base.cwd === process.cwd();
|
|
443
|
+
return {
|
|
444
|
+
threadId: base.threadId ?? incoming.threadId,
|
|
445
|
+
cwd: baseHasFallbackCwd ? incoming.cwd ?? base.cwd : base.cwd ?? incoming.cwd,
|
|
446
|
+
title: base.title ?? incoming.title,
|
|
447
|
+
createdAt: base.createdAt.getTime() <= incoming.createdAt.getTime()
|
|
448
|
+
? base.createdAt
|
|
449
|
+
: incoming.createdAt,
|
|
450
|
+
updatedAt: base.updatedAt.getTime() >= incoming.updatedAt.getTime()
|
|
451
|
+
? base.updatedAt
|
|
452
|
+
: incoming.updatedAt,
|
|
453
|
+
parentSessionId: base.parentSessionId ?? incoming.parentSessionId,
|
|
454
|
+
rootSessionId: base.rootSessionId ?? incoming.rootSessionId,
|
|
455
|
+
agentId: base.agentId ?? incoming.agentId,
|
|
456
|
+
agentNickname: base.agentNickname ?? incoming.agentNickname,
|
|
457
|
+
agentRole: base.agentRole ?? incoming.agentRole,
|
|
458
|
+
model: base.model ?? incoming.model,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
async function readCodexPersistedThreadMetadataFromRolloutPath(rolloutPath, logger) {
|
|
462
|
+
if (rolloutPath.endsWith(".json")) {
|
|
463
|
+
try {
|
|
464
|
+
const raw = await fs.readFile(rolloutPath, "utf8");
|
|
465
|
+
return parseCodexPersistedThreadMetadata(JSON.parse(raw));
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
logger?.debug({ err: error, rolloutPath }, "Failed to read Codex persisted thread metadata from JSON rollout path");
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const input = createReadStream(rolloutPath, { encoding: "utf8" });
|
|
473
|
+
const lines = readline.createInterface({
|
|
474
|
+
input,
|
|
475
|
+
crlfDelay: Infinity,
|
|
476
|
+
});
|
|
477
|
+
try {
|
|
478
|
+
for await (const line of lines) {
|
|
479
|
+
const trimmed = line.trim();
|
|
480
|
+
if (!trimmed) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const parsed = JSON.parse(trimmed);
|
|
484
|
+
return parseCodexPersistedThreadMetadata(parsed);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
logger?.debug({ err: error, rolloutPath }, "Failed to read Codex persisted thread metadata from rollout path");
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
lines.close();
|
|
492
|
+
input.destroy();
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
async function buildCodexPersistedDescriptorFromRolloutPath(input) {
|
|
497
|
+
let stat;
|
|
498
|
+
try {
|
|
499
|
+
stat = await fs.stat(input.rolloutPath);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const cached = codexPersistedThreadMetadataCache.get(input.rolloutPath);
|
|
505
|
+
let metadata = cached && cached.mtimeMs === stat.mtimeMs ? cached.metadata : undefined;
|
|
506
|
+
if (metadata === undefined) {
|
|
507
|
+
metadata = await readCodexPersistedThreadMetadataFromRolloutPath(input.rolloutPath, input.logger);
|
|
508
|
+
setCodexPersistedThreadMetadataCache(input.rolloutPath, {
|
|
509
|
+
mtimeMs: stat.mtimeMs,
|
|
510
|
+
metadata,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const threadId = metadata?.threadId;
|
|
514
|
+
if (!metadata || !threadId) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
const lastActivityAt = stat.mtime;
|
|
518
|
+
const timeline = input.includeTimeline
|
|
519
|
+
? await loadCodexPersistedTimeline(threadId, { rolloutPath: input.rolloutPath, sessionRoot: resolveCodexSessionRoot() }, input.logger)
|
|
520
|
+
: [];
|
|
521
|
+
return {
|
|
522
|
+
provider: CODEX_PROVIDER,
|
|
523
|
+
sessionId: threadId,
|
|
524
|
+
cwd: metadata.cwd ?? process.cwd(),
|
|
525
|
+
title: metadata.title,
|
|
526
|
+
createdAt: metadata.createdAt,
|
|
527
|
+
lastActivityAt,
|
|
528
|
+
persistence: {
|
|
529
|
+
provider: CODEX_PROVIDER,
|
|
530
|
+
sessionId: threadId,
|
|
531
|
+
nativeHandle: threadId,
|
|
532
|
+
metadata: {
|
|
533
|
+
provider: CODEX_PROVIDER,
|
|
534
|
+
cwd: metadata.cwd ?? process.cwd(),
|
|
535
|
+
title: metadata.title,
|
|
536
|
+
threadId,
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
timeline,
|
|
540
|
+
model: metadata.model,
|
|
541
|
+
parentSessionId: metadata.parentSessionId,
|
|
542
|
+
rootSessionId: metadata.rootSessionId,
|
|
543
|
+
agentId: metadata.agentId,
|
|
544
|
+
agentNickname: metadata.agentNickname,
|
|
545
|
+
agentRole: metadata.agentRole,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
async function requestCodexThreadList(input) {
|
|
549
|
+
try {
|
|
550
|
+
return (await input.client.request("thread/list", {
|
|
551
|
+
limit: input.limit,
|
|
552
|
+
...(input.sourceKinds ? { sourceKinds: [...input.sourceKinds] } : {}),
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
const retrySourceKinds = error instanceof CodexAppServerRequestError && error.method === "thread/list"
|
|
557
|
+
? resolveCodexThreadListRetrySourceKinds(input.sourceKinds ?? [], error.message)
|
|
558
|
+
: null;
|
|
559
|
+
if (!retrySourceKinds) {
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
input.logger.warn({
|
|
563
|
+
rejectedSourceKinds: input.sourceKinds,
|
|
564
|
+
retrySourceKinds,
|
|
565
|
+
error: error instanceof Error ? error.message : String(error),
|
|
566
|
+
}, "Codex thread/list rejected sourceKinds; retrying with supported variants");
|
|
567
|
+
return (await input.client.request("thread/list", {
|
|
568
|
+
limit: input.limit,
|
|
569
|
+
sourceKinds: retrySourceKinds,
|
|
570
|
+
}));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function readCodexPersistedDescriptorBySessionId(input) {
|
|
574
|
+
const response = (await input.client.request("thread/read", {
|
|
575
|
+
threadId: input.sessionId,
|
|
576
|
+
includeTurns: input.includeTimeline,
|
|
577
|
+
}));
|
|
578
|
+
const threadRecord = response?.thread ?? null;
|
|
579
|
+
if (!threadRecord) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
let metadata = parseCodexPersistedThreadMetadata(threadRecord);
|
|
583
|
+
const rolloutPath = readStringField(toRecord(threadRecord), ["path"]);
|
|
584
|
+
if (rolloutPath) {
|
|
585
|
+
const rolloutMetadata = await readCodexPersistedThreadMetadataFromRolloutPath(rolloutPath, input.logger);
|
|
586
|
+
if (rolloutMetadata) {
|
|
587
|
+
metadata = mergeCodexPersistedThreadMetadata(metadata, rolloutMetadata);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const threadId = metadata.threadId ?? input.sessionId.trim();
|
|
591
|
+
if (!threadId) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
const cwd = metadata.cwd ?? process.cwd();
|
|
595
|
+
let timeline = [];
|
|
596
|
+
if (input.includeTimeline) {
|
|
597
|
+
const rolloutTimeline = await loadCodexPersistedTimeline(threadId, undefined, input.logger).catch(() => []);
|
|
598
|
+
const turns = threadRecord.turns ?? [];
|
|
599
|
+
const threadTimeline = readCodexThreadTimelineFromTurns(turns, {
|
|
600
|
+
cwd,
|
|
601
|
+
});
|
|
602
|
+
timeline = mergeCodexThreadReadTimelineWithRolloutSpawnCalls({
|
|
603
|
+
threadTimeline,
|
|
604
|
+
rolloutTimeline,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
provider: CODEX_PROVIDER,
|
|
609
|
+
sessionId: threadId,
|
|
610
|
+
cwd,
|
|
611
|
+
title: metadata.title,
|
|
612
|
+
createdAt: metadata.createdAt,
|
|
613
|
+
lastActivityAt: metadata.updatedAt,
|
|
614
|
+
persistence: {
|
|
615
|
+
provider: CODEX_PROVIDER,
|
|
616
|
+
sessionId: threadId,
|
|
617
|
+
nativeHandle: threadId,
|
|
618
|
+
metadata: {
|
|
619
|
+
provider: CODEX_PROVIDER,
|
|
620
|
+
cwd,
|
|
621
|
+
title: metadata.title,
|
|
622
|
+
threadId,
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
timeline,
|
|
626
|
+
model: metadata.model,
|
|
627
|
+
parentSessionId: metadata.parentSessionId,
|
|
628
|
+
rootSessionId: metadata.rootSessionId,
|
|
629
|
+
agentId: metadata.agentId,
|
|
630
|
+
agentNickname: metadata.agentNickname,
|
|
631
|
+
agentRole: metadata.agentRole,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
204
634
|
function parseUpdatedQuestionAnswers(updatedInput) {
|
|
205
635
|
const parsed = {};
|
|
206
636
|
const root = toRecord(updatedInput);
|
|
@@ -586,6 +1016,12 @@ class CodexAppServerClient {
|
|
|
586
1016
|
setNotificationHandler(handler) {
|
|
587
1017
|
this.notificationHandler = handler;
|
|
588
1018
|
}
|
|
1019
|
+
dispatchNotification(method, params) {
|
|
1020
|
+
if (this.disposed) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
this.notificationHandler?.(method, params);
|
|
1024
|
+
}
|
|
589
1025
|
setRequestHandler(method, handler) {
|
|
590
1026
|
this.requestHandlers.set(method, handler);
|
|
591
1027
|
}
|
|
@@ -903,6 +1339,25 @@ function normalizeCodexThreadItemType(rawType) {
|
|
|
903
1339
|
if (!rawType) {
|
|
904
1340
|
return rawType;
|
|
905
1341
|
}
|
|
1342
|
+
const normalized = rawType.replace(/[._-]/g, "").toLowerCase();
|
|
1343
|
+
switch (normalized) {
|
|
1344
|
+
case "usermessage":
|
|
1345
|
+
return "userMessage";
|
|
1346
|
+
case "agentmessage":
|
|
1347
|
+
return "agentMessage";
|
|
1348
|
+
case "reasoning":
|
|
1349
|
+
return "reasoning";
|
|
1350
|
+
case "plan":
|
|
1351
|
+
return "plan";
|
|
1352
|
+
case "commandexecution":
|
|
1353
|
+
return "commandExecution";
|
|
1354
|
+
case "filechange":
|
|
1355
|
+
return "fileChange";
|
|
1356
|
+
case "mcptoolcall":
|
|
1357
|
+
return "mcpToolCall";
|
|
1358
|
+
case "websearch":
|
|
1359
|
+
return "webSearch";
|
|
1360
|
+
}
|
|
906
1361
|
switch (rawType) {
|
|
907
1362
|
case "UserMessage":
|
|
908
1363
|
return "userMessage";
|
|
@@ -1213,6 +1668,92 @@ function threadItemToTimeline(item, options) {
|
|
|
1213
1668
|
return null;
|
|
1214
1669
|
}
|
|
1215
1670
|
}
|
|
1671
|
+
function readCodexThreadTimelineFromTurns(turns, options) {
|
|
1672
|
+
const timeline = [];
|
|
1673
|
+
for (const turn of turns) {
|
|
1674
|
+
for (const item of turn.items ?? []) {
|
|
1675
|
+
const timelineItem = threadItemToTimeline(item, {
|
|
1676
|
+
cwd: options?.cwd ?? null,
|
|
1677
|
+
});
|
|
1678
|
+
if (timelineItem) {
|
|
1679
|
+
timeline.push(timelineItem);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return timeline;
|
|
1684
|
+
}
|
|
1685
|
+
function areCodexTimelineItemsEquivalent(left, right) {
|
|
1686
|
+
if (left.type !== right.type) {
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
switch (left.type) {
|
|
1690
|
+
case "user_message":
|
|
1691
|
+
case "assistant_message":
|
|
1692
|
+
case "reasoning":
|
|
1693
|
+
return left.text === right.text;
|
|
1694
|
+
case "todo":
|
|
1695
|
+
return JSON.stringify(left.items) === JSON.stringify(right.items);
|
|
1696
|
+
case "tool_call":
|
|
1697
|
+
if (left.callId && right.type === "tool_call" && right.callId) {
|
|
1698
|
+
return left.callId === right.callId;
|
|
1699
|
+
}
|
|
1700
|
+
return false;
|
|
1701
|
+
default:
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
function isCodexRolloutSpawnAgentToolCall(item) {
|
|
1706
|
+
return item.type === "tool_call" && item.name === "spawn_agent";
|
|
1707
|
+
}
|
|
1708
|
+
function mergeCodexThreadReadTimelineWithRolloutSpawnCalls(input) {
|
|
1709
|
+
if (input.rolloutTimeline.length === 0) {
|
|
1710
|
+
return input.threadTimeline;
|
|
1711
|
+
}
|
|
1712
|
+
const existingSpawnCallIds = new Set(input.threadTimeline.flatMap((item) => isCodexRolloutSpawnAgentToolCall(item) && item.callId ? [item.callId] : []));
|
|
1713
|
+
const queuedSpawnCallIds = new Set();
|
|
1714
|
+
const insertionsByThreadIndex = new Map();
|
|
1715
|
+
let searchIndex = 0;
|
|
1716
|
+
let lastMatchedThreadIndex = -1;
|
|
1717
|
+
for (const rolloutItem of input.rolloutTimeline) {
|
|
1718
|
+
const matchedThreadIndex = input.threadTimeline.findIndex((threadItem, index) => index >= searchIndex && areCodexTimelineItemsEquivalent(threadItem, rolloutItem));
|
|
1719
|
+
if (matchedThreadIndex >= 0) {
|
|
1720
|
+
searchIndex = matchedThreadIndex + 1;
|
|
1721
|
+
lastMatchedThreadIndex = matchedThreadIndex;
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
if (!isCodexRolloutSpawnAgentToolCall(rolloutItem)) {
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
if (rolloutItem.callId && existingSpawnCallIds.has(rolloutItem.callId)) {
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
if (rolloutItem.callId && queuedSpawnCallIds.has(rolloutItem.callId)) {
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
if (rolloutItem.callId) {
|
|
1734
|
+
queuedSpawnCallIds.add(rolloutItem.callId);
|
|
1735
|
+
}
|
|
1736
|
+
const queue = insertionsByThreadIndex.get(lastMatchedThreadIndex) ?? [];
|
|
1737
|
+
queue.push(rolloutItem);
|
|
1738
|
+
insertionsByThreadIndex.set(lastMatchedThreadIndex, queue);
|
|
1739
|
+
}
|
|
1740
|
+
if (insertionsByThreadIndex.size === 0) {
|
|
1741
|
+
return input.threadTimeline;
|
|
1742
|
+
}
|
|
1743
|
+
const merged = [];
|
|
1744
|
+
const leadingInsertions = insertionsByThreadIndex.get(-1);
|
|
1745
|
+
if (leadingInsertions) {
|
|
1746
|
+
merged.push(...leadingInsertions);
|
|
1747
|
+
}
|
|
1748
|
+
for (const [index, item] of input.threadTimeline.entries()) {
|
|
1749
|
+
merged.push(item);
|
|
1750
|
+
const insertedAfter = insertionsByThreadIndex.get(index);
|
|
1751
|
+
if (insertedAfter) {
|
|
1752
|
+
merged.push(...insertedAfter);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return merged;
|
|
1756
|
+
}
|
|
1216
1757
|
function toSandboxPolicy(type, networkAccess) {
|
|
1217
1758
|
switch (type) {
|
|
1218
1759
|
case "read-only":
|
|
@@ -1234,6 +1775,7 @@ const TurnStartedNotificationSchema = z.object({
|
|
|
1234
1775
|
const TurnCompletedNotificationSchema = z.object({
|
|
1235
1776
|
turn: z
|
|
1236
1777
|
.object({
|
|
1778
|
+
id: z.string().optional(),
|
|
1237
1779
|
status: z.string(),
|
|
1238
1780
|
error: z
|
|
1239
1781
|
.object({
|
|
@@ -1282,6 +1824,10 @@ const CodexEventTurnAbortedNotificationSchema = z.object({
|
|
|
1282
1824
|
msg: z
|
|
1283
1825
|
.object({
|
|
1284
1826
|
type: z.literal("turn_aborted"),
|
|
1827
|
+
turn_id: z.string().optional(),
|
|
1828
|
+
turnId: z.string().optional(),
|
|
1829
|
+
thread_id: z.string().optional(),
|
|
1830
|
+
threadId: z.string().optional(),
|
|
1285
1831
|
reason: z.string().optional(),
|
|
1286
1832
|
})
|
|
1287
1833
|
.passthrough(),
|
|
@@ -1290,6 +1836,12 @@ const CodexEventTaskCompleteNotificationSchema = z.object({
|
|
|
1290
1836
|
msg: z
|
|
1291
1837
|
.object({
|
|
1292
1838
|
type: z.literal("task_complete"),
|
|
1839
|
+
turn_id: z.string().optional(),
|
|
1840
|
+
turnId: z.string().optional(),
|
|
1841
|
+
thread_id: z.string().optional(),
|
|
1842
|
+
threadId: z.string().optional(),
|
|
1843
|
+
last_agent_message: z.string().nullable().optional(),
|
|
1844
|
+
lastAgentMessage: z.string().nullable().optional(),
|
|
1293
1845
|
})
|
|
1294
1846
|
.passthrough(),
|
|
1295
1847
|
}).passthrough();
|
|
@@ -1383,12 +1935,19 @@ const CodexEventTurnDiffNotificationSchema = z.object({
|
|
|
1383
1935
|
const CodexNotificationSchema = z.union([
|
|
1384
1936
|
z.object({ method: z.literal("thread/started"), params: ThreadStartedNotificationSchema }).transform(({ params }) => ({ kind: "thread_started", threadId: params.thread.id })),
|
|
1385
1937
|
z.object({ method: z.literal("thread/started"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1386
|
-
z.object({ method: z.literal("turn/started"), params: TurnStartedNotificationSchema }).transform(({ params }) => ({
|
|
1938
|
+
z.object({ method: z.literal("turn/started"), params: TurnStartedNotificationSchema }).transform(({ params }) => ({
|
|
1939
|
+
kind: "turn_started",
|
|
1940
|
+
...readCodexNotificationProvenance(params),
|
|
1941
|
+
turnId: params.turn.id,
|
|
1942
|
+
})),
|
|
1387
1943
|
z.object({ method: z.literal("turn/started"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1388
1944
|
z.object({ method: z.literal("turn/completed"), params: TurnCompletedNotificationSchema }).transform(({ params }) => ({
|
|
1389
1945
|
kind: "turn_completed",
|
|
1390
1946
|
status: params.turn.status,
|
|
1391
1947
|
errorMessage: params.turn.error?.message ?? null,
|
|
1948
|
+
turnId: params.turn.id ?? null,
|
|
1949
|
+
threadId: readCodexNotificationProvenance(params).threadId,
|
|
1950
|
+
lastAgentMessage: null,
|
|
1392
1951
|
})),
|
|
1393
1952
|
z.object({ method: z.literal("turn/completed"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1394
1953
|
z.object({ method: z.literal("turn/plan/updated"), params: TurnPlanUpdatedNotificationSchema }).transform(({ params }) => ({
|
|
@@ -1403,6 +1962,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1403
1962
|
kind: "plan_delta",
|
|
1404
1963
|
itemId: params.itemId,
|
|
1405
1964
|
delta: params.delta,
|
|
1965
|
+
...readCodexNotificationProvenance(params),
|
|
1406
1966
|
})),
|
|
1407
1967
|
z.object({ method: z.literal("item/plan/delta"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1408
1968
|
z.object({ method: z.literal("turn/diff/updated"), params: TurnDiffUpdatedNotificationSchema }).transform(({ params }) => ({ kind: "diff_updated", diff: params.diff })),
|
|
@@ -1424,6 +1984,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1424
1984
|
kind: "agent_message_delta",
|
|
1425
1985
|
itemId: params.itemId,
|
|
1426
1986
|
delta: params.delta,
|
|
1987
|
+
...readCodexNotificationProvenance(params),
|
|
1427
1988
|
})),
|
|
1428
1989
|
z.object({ method: z.literal("item/agentMessage/delta"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1429
1990
|
z.object({
|
|
@@ -1433,11 +1994,22 @@ const CodexNotificationSchema = z.union([
|
|
|
1433
1994
|
kind: "reasoning_delta",
|
|
1434
1995
|
itemId: params.itemId,
|
|
1435
1996
|
delta: params.delta,
|
|
1997
|
+
...readCodexNotificationProvenance(params),
|
|
1436
1998
|
})),
|
|
1437
1999
|
z.object({ method: z.literal("item/reasoning/summaryTextDelta"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1438
|
-
z.object({ method: z.literal("item/completed"), params: ItemLifecycleNotificationSchema }).transform(({ params }) => ({
|
|
2000
|
+
z.object({ method: z.literal("item/completed"), params: ItemLifecycleNotificationSchema }).transform(({ params }) => ({
|
|
2001
|
+
kind: "item_completed",
|
|
2002
|
+
source: "item",
|
|
2003
|
+
item: params.item,
|
|
2004
|
+
...readCodexNotificationProvenance(params),
|
|
2005
|
+
})),
|
|
1439
2006
|
z.object({ method: z.literal("item/completed"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1440
|
-
z.object({ method: z.literal("item/started"), params: ItemLifecycleNotificationSchema }).transform(({ params }) => ({
|
|
2007
|
+
z.object({ method: z.literal("item/started"), params: ItemLifecycleNotificationSchema }).transform(({ params }) => ({
|
|
2008
|
+
kind: "item_started",
|
|
2009
|
+
source: "item",
|
|
2010
|
+
item: params.item,
|
|
2011
|
+
...readCodexNotificationProvenance(params),
|
|
2012
|
+
})),
|
|
1441
2013
|
z.object({ method: z.literal("item/started"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1442
2014
|
z.object({
|
|
1443
2015
|
method: z.literal("codex/event/item_started"),
|
|
@@ -1446,6 +2018,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1446
2018
|
kind: "item_started",
|
|
1447
2019
|
source: "codex_event",
|
|
1448
2020
|
item: params.msg.item,
|
|
2021
|
+
...readCodexNotificationProvenance(params.msg),
|
|
1449
2022
|
})),
|
|
1450
2023
|
z.object({ method: z.literal("codex/event/item_started"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1451
2024
|
z.object({
|
|
@@ -1455,6 +2028,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1455
2028
|
kind: "item_completed",
|
|
1456
2029
|
source: "codex_event",
|
|
1457
2030
|
item: params.msg.item,
|
|
2031
|
+
...readCodexNotificationProvenance(params.msg),
|
|
1458
2032
|
})),
|
|
1459
2033
|
z.object({ method: z.literal("codex/event/item_completed"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1460
2034
|
z.object({
|
|
@@ -1465,6 +2039,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1465
2039
|
callId: params.msg.call_id ?? null,
|
|
1466
2040
|
command: params.msg.command ?? null,
|
|
1467
2041
|
cwd: params.msg.cwd ?? null,
|
|
2042
|
+
...readCodexNotificationProvenance(params.msg),
|
|
1468
2043
|
})),
|
|
1469
2044
|
z.object({ method: z.literal("codex/event/exec_command_begin"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1470
2045
|
z.object({
|
|
@@ -1483,6 +2058,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1483
2058
|
exitCode: params.msg.exit_code ?? params.msg.exitCode ?? null,
|
|
1484
2059
|
success: params.msg.success ?? null,
|
|
1485
2060
|
stderr: params.msg.stderr ?? null,
|
|
2061
|
+
...readCodexNotificationProvenance(params.msg),
|
|
1486
2062
|
})),
|
|
1487
2063
|
z.object({ method: z.literal("codex/event/exec_command_end"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1488
2064
|
z.object({
|
|
@@ -1493,6 +2069,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1493
2069
|
callId: params.msg.call_id ?? null,
|
|
1494
2070
|
stream: params.msg.stream ?? null,
|
|
1495
2071
|
chunk: params.msg.chunk ?? params.msg.delta ?? null,
|
|
2072
|
+
...readCodexNotificationProvenance(params.msg),
|
|
1496
2073
|
})),
|
|
1497
2074
|
z.object({
|
|
1498
2075
|
method: z.literal("codex/event/exec_command_output_delta"),
|
|
@@ -1505,6 +2082,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1505
2082
|
kind: "patch_apply_started",
|
|
1506
2083
|
callId: params.msg.call_id ?? null,
|
|
1507
2084
|
changes: params.msg.changes ?? null,
|
|
2085
|
+
...readCodexNotificationProvenance(params.msg),
|
|
1508
2086
|
})),
|
|
1509
2087
|
z.object({ method: z.literal("codex/event/patch_apply_begin"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1510
2088
|
z.object({
|
|
@@ -1517,6 +2095,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1517
2095
|
stdout: params.msg.stdout ?? null,
|
|
1518
2096
|
stderr: params.msg.stderr ?? null,
|
|
1519
2097
|
success: params.msg.success ?? null,
|
|
2098
|
+
...readCodexNotificationProvenance(params.msg),
|
|
1520
2099
|
})),
|
|
1521
2100
|
z.object({ method: z.literal("codex/event/patch_apply_end"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1522
2101
|
z.object({
|
|
@@ -1526,6 +2105,7 @@ const CodexNotificationSchema = z.union([
|
|
|
1526
2105
|
kind: "file_change_output_delta",
|
|
1527
2106
|
itemId: params.itemId,
|
|
1528
2107
|
delta: params.delta ?? params.chunk ?? null,
|
|
2108
|
+
...readCodexNotificationProvenance(params),
|
|
1529
2109
|
})),
|
|
1530
2110
|
z.object({ method: z.literal("item/fileChange/outputDelta"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1531
2111
|
z.object({
|
|
@@ -1539,19 +2119,25 @@ const CodexNotificationSchema = z.union([
|
|
|
1539
2119
|
z.object({
|
|
1540
2120
|
method: z.literal("codex/event/turn_aborted"),
|
|
1541
2121
|
params: CodexEventTurnAbortedNotificationSchema,
|
|
1542
|
-
}).transform(() => ({
|
|
2122
|
+
}).transform(({ params }) => ({
|
|
1543
2123
|
kind: "turn_completed",
|
|
1544
2124
|
status: "interrupted",
|
|
1545
2125
|
errorMessage: null,
|
|
2126
|
+
turnId: params.msg.turn_id ?? params.msg.turnId ?? null,
|
|
2127
|
+
threadId: params.msg.thread_id ?? params.msg.threadId ?? null,
|
|
2128
|
+
lastAgentMessage: null,
|
|
1546
2129
|
})),
|
|
1547
2130
|
z.object({ method: z.literal("codex/event/turn_aborted"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1548
2131
|
z.object({
|
|
1549
2132
|
method: z.literal("codex/event/task_complete"),
|
|
1550
2133
|
params: CodexEventTaskCompleteNotificationSchema,
|
|
1551
|
-
}).transform(() => ({
|
|
2134
|
+
}).transform(({ params }) => ({
|
|
1552
2135
|
kind: "turn_completed",
|
|
1553
2136
|
status: "completed",
|
|
1554
2137
|
errorMessage: null,
|
|
2138
|
+
turnId: params.msg.turn_id ?? params.msg.turnId ?? null,
|
|
2139
|
+
threadId: params.msg.thread_id ?? params.msg.threadId ?? null,
|
|
2140
|
+
lastAgentMessage: params.msg.last_agent_message ?? params.msg.lastAgentMessage ?? null,
|
|
1555
2141
|
})),
|
|
1556
2142
|
z.object({ method: z.literal("codex/event/task_complete"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
|
|
1557
2143
|
z.object({ method: z.string(), params: z.unknown() }).transform(({ method, params }) => ({ kind: "unknown_method", method, params })),
|
|
@@ -1620,10 +2206,15 @@ export const __codexAppServerInternals = {
|
|
|
1620
2206
|
shouldRetryInitializeWithoutExperimentalApi,
|
|
1621
2207
|
shouldRetryTurnStartWithoutCollaborationMode,
|
|
1622
2208
|
isCodexAppServerUnsupportedMethodError,
|
|
2209
|
+
parseCodexThreadListAllowedSourceKinds,
|
|
2210
|
+
resolveCodexThreadListRetrySourceKinds,
|
|
1623
2211
|
readCodexConfiguredDefaults,
|
|
2212
|
+
parseCodexPersistedThreadMetadata,
|
|
2213
|
+
readCodexPersistedThreadMetadataFromRolloutPath,
|
|
1624
2214
|
formatProposedPlanBlock,
|
|
1625
2215
|
formatProposedPlanChunk,
|
|
1626
2216
|
buildCompletedCodexTimelineItem,
|
|
2217
|
+
mergeCodexThreadReadTimelineWithRolloutSpawnCalls,
|
|
1627
2218
|
normalizeCodexQuestionDescriptors,
|
|
1628
2219
|
parseUpdatedQuestionAnswers,
|
|
1629
2220
|
buildCodexPermissionsResponse,
|
|
@@ -1637,6 +2228,20 @@ export const __codexAppServerInternals = {
|
|
|
1637
2228
|
"mcpServer/elicitation/request",
|
|
1638
2229
|
],
|
|
1639
2230
|
};
|
|
2231
|
+
function normalizeToolCallNameForFallback(name) {
|
|
2232
|
+
return name.trim().replace(/[.\s-]+/g, "_").toLowerCase();
|
|
2233
|
+
}
|
|
2234
|
+
function isShellLikeToolCall(item) {
|
|
2235
|
+
const normalizedName = normalizeToolCallNameForFallback(item.name);
|
|
2236
|
+
return (item.detail.type === "shell" ||
|
|
2237
|
+
normalizedName === "shell" ||
|
|
2238
|
+
normalizedName === "bash");
|
|
2239
|
+
}
|
|
2240
|
+
function isSpawnAgentToolCall(item) {
|
|
2241
|
+
return normalizeToolCallNameForFallback(item.name).endsWith("spawn_agent");
|
|
2242
|
+
}
|
|
2243
|
+
const COMPATIBILITY_FAILED_TOOL_STATUSES = new Set(["failed", "error"]);
|
|
2244
|
+
const COMPATIBILITY_CANCELED_TOOL_STATUSES = new Set(["canceled", "cancelled", "interrupted"]);
|
|
1640
2245
|
class CodexAppServerAgentSession {
|
|
1641
2246
|
constructor(config, resumeHandle, logger, spawnAppServer) {
|
|
1642
2247
|
this.resumeHandle = resumeHandle;
|
|
@@ -1659,6 +2264,7 @@ class CodexAppServerAgentSession {
|
|
|
1659
2264
|
this.pendingFileChangeOutputDeltas = new Map();
|
|
1660
2265
|
this.emittedExecCommandStartedCallIds = new Set();
|
|
1661
2266
|
this.emittedExecCommandCompletedCallIds = new Set();
|
|
2267
|
+
this.emittedRolloutSpawnAgentCallIds = new Set();
|
|
1662
2268
|
this.emittedItemStartedIds = new Set();
|
|
1663
2269
|
this.emittedItemCompletedIds = new Set();
|
|
1664
2270
|
this.warnedUnknownNotificationMethods = new Set();
|
|
@@ -1670,6 +2276,7 @@ class CodexAppServerAgentSession {
|
|
|
1670
2276
|
this.cachedSkills = [];
|
|
1671
2277
|
this.turnCompletionInFlight = false;
|
|
1672
2278
|
this.deferredTurnCompletion = null;
|
|
2279
|
+
this.rolloutSpawnPollTimer = null;
|
|
1673
2280
|
this.logger = logger.child({ module: "agent", provider: CODEX_PROVIDER });
|
|
1674
2281
|
if (config.modeId === undefined) {
|
|
1675
2282
|
throw new Error("Codex agent requires modeId to be specified");
|
|
@@ -1802,24 +2409,26 @@ class CodexAppServerAgentSession {
|
|
|
1802
2409
|
includeTurns: true,
|
|
1803
2410
|
}));
|
|
1804
2411
|
const thread = response?.thread;
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
if (timelineItem.type === "tool_call") {
|
|
1815
|
-
this.warnOnIncompleteEditToolCall(timelineItem, "thread_read", item);
|
|
1816
|
-
}
|
|
1817
|
-
threadTimeline.push(timelineItem);
|
|
1818
|
-
}
|
|
2412
|
+
const rawTurns = thread?.turns ?? [];
|
|
2413
|
+
for (const turn of rawTurns) {
|
|
2414
|
+
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
2415
|
+
for (const item of items) {
|
|
2416
|
+
const timelineItem = threadItemToTimeline(item, {
|
|
2417
|
+
cwd: this.config.cwd ?? null,
|
|
2418
|
+
});
|
|
2419
|
+
if (timelineItem?.type === "tool_call") {
|
|
2420
|
+
this.warnOnIncompleteEditToolCall(timelineItem, "thread_read", item);
|
|
1819
2421
|
}
|
|
1820
2422
|
}
|
|
1821
2423
|
}
|
|
1822
|
-
const
|
|
2424
|
+
const threadTimeline = readCodexThreadTimelineFromTurns(rawTurns, {
|
|
2425
|
+
cwd: this.config.cwd ?? null,
|
|
2426
|
+
});
|
|
2427
|
+
const timeline = mergeCodexThreadReadTimelineWithRolloutSpawnCalls({
|
|
2428
|
+
threadTimeline,
|
|
2429
|
+
rolloutTimeline,
|
|
2430
|
+
});
|
|
2431
|
+
this.seedRolloutSpawnAgentCallIds(timeline);
|
|
1823
2432
|
if (timeline.length > 0) {
|
|
1824
2433
|
this.persistedHistory = timeline;
|
|
1825
2434
|
this.historyPending = true;
|
|
@@ -2489,6 +3098,10 @@ class CodexAppServerAgentSession {
|
|
|
2489
3098
|
}
|
|
2490
3099
|
clearTurnState() {
|
|
2491
3100
|
this.clearDeferredTurnCompletion();
|
|
3101
|
+
if (this.rolloutSpawnPollTimer) {
|
|
3102
|
+
clearTimeout(this.rolloutSpawnPollTimer);
|
|
3103
|
+
this.rolloutSpawnPollTimer = null;
|
|
3104
|
+
}
|
|
2492
3105
|
this.currentTurnId = null;
|
|
2493
3106
|
this.emittedItemStartedIds.clear();
|
|
2494
3107
|
this.emittedItemCompletedIds.clear();
|
|
@@ -2507,6 +3120,128 @@ class CodexAppServerAgentSession {
|
|
|
2507
3120
|
this.pendingReasoning.size > 0 ||
|
|
2508
3121
|
this.pendingPlanTexts.size > 0);
|
|
2509
3122
|
}
|
|
3123
|
+
emitRolloutSpawnAgentCallsFromTimeline(rolloutTimeline) {
|
|
3124
|
+
let emitted = 0;
|
|
3125
|
+
for (const item of rolloutTimeline) {
|
|
3126
|
+
if (item.type !== "tool_call" || !item.callId) {
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
if (isSpawnAgentToolCall(item)) {
|
|
3130
|
+
if (this.emittedRolloutSpawnAgentCallIds.has(item.callId)) {
|
|
3131
|
+
continue;
|
|
3132
|
+
}
|
|
3133
|
+
this.emittedRolloutSpawnAgentCallIds.add(item.callId);
|
|
3134
|
+
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item });
|
|
3135
|
+
emitted += 1;
|
|
3136
|
+
continue;
|
|
3137
|
+
}
|
|
3138
|
+
if (this.hasAlreadyEmittedFallbackToolCall(item)) {
|
|
3139
|
+
continue;
|
|
3140
|
+
}
|
|
3141
|
+
this.markFallbackToolCallEmitted(item);
|
|
3142
|
+
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item });
|
|
3143
|
+
emitted += 1;
|
|
3144
|
+
}
|
|
3145
|
+
return emitted;
|
|
3146
|
+
}
|
|
3147
|
+
seedRolloutSpawnAgentCallIds(timeline) {
|
|
3148
|
+
for (const item of timeline) {
|
|
3149
|
+
if (item.type === "tool_call" && isSpawnAgentToolCall(item) && item.callId) {
|
|
3150
|
+
this.emittedRolloutSpawnAgentCallIds.add(item.callId);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
hasAlreadyEmittedFallbackToolCall(item) {
|
|
3155
|
+
if (!item.callId) {
|
|
3156
|
+
return false;
|
|
3157
|
+
}
|
|
3158
|
+
if (isShellLikeToolCall(item)) {
|
|
3159
|
+
if (item.status === "running") {
|
|
3160
|
+
return (this.emittedExecCommandStartedCallIds.has(item.callId) ||
|
|
3161
|
+
this.emittedExecCommandCompletedCallIds.has(item.callId));
|
|
3162
|
+
}
|
|
3163
|
+
return this.emittedExecCommandCompletedCallIds.has(item.callId);
|
|
3164
|
+
}
|
|
3165
|
+
if (item.status === "running") {
|
|
3166
|
+
return (this.emittedItemStartedIds.has(item.callId) ||
|
|
3167
|
+
this.emittedItemCompletedIds.has(item.callId));
|
|
3168
|
+
}
|
|
3169
|
+
return this.emittedItemCompletedIds.has(item.callId);
|
|
3170
|
+
}
|
|
3171
|
+
markFallbackToolCallEmitted(item) {
|
|
3172
|
+
if (!item.callId) {
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
if (isShellLikeToolCall(item)) {
|
|
3176
|
+
if (item.status === "running") {
|
|
3177
|
+
this.emittedExecCommandStartedCallIds.add(item.callId);
|
|
3178
|
+
}
|
|
3179
|
+
else {
|
|
3180
|
+
this.emittedExecCommandCompletedCallIds.add(item.callId);
|
|
3181
|
+
this.emittedExecCommandStartedCallIds.delete(item.callId);
|
|
3182
|
+
}
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
if (item.status === "running") {
|
|
3186
|
+
this.emittedItemStartedIds.add(item.callId);
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
this.emittedItemCompletedIds.add(item.callId);
|
|
3190
|
+
this.emittedItemStartedIds.delete(item.callId);
|
|
3191
|
+
}
|
|
3192
|
+
hasForeignNotificationProvenance(input) {
|
|
3193
|
+
return ((input.threadId !== null &&
|
|
3194
|
+
this.currentThreadId !== null &&
|
|
3195
|
+
input.threadId !== this.currentThreadId) ||
|
|
3196
|
+
(input.turnId !== null &&
|
|
3197
|
+
this.currentTurnId !== null &&
|
|
3198
|
+
input.turnId !== this.currentTurnId));
|
|
3199
|
+
}
|
|
3200
|
+
async emitNewRolloutSpawnAgentCalls() {
|
|
3201
|
+
if (!this.currentThreadId) {
|
|
3202
|
+
return 0;
|
|
3203
|
+
}
|
|
3204
|
+
let rolloutTimeline;
|
|
3205
|
+
try {
|
|
3206
|
+
rolloutTimeline = await loadCodexPersistedTimeline(this.currentThreadId, undefined, this.logger);
|
|
3207
|
+
}
|
|
3208
|
+
catch (error) {
|
|
3209
|
+
this.logger.trace({ error, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Failed to load Codex rollout timeline while reconciling spawn_agent calls");
|
|
3210
|
+
return 0;
|
|
3211
|
+
}
|
|
3212
|
+
return this.emitRolloutSpawnAgentCallsFromTimeline(rolloutTimeline);
|
|
3213
|
+
}
|
|
3214
|
+
scheduleRolloutSpawnAgentPoll() {
|
|
3215
|
+
if (!this.currentThreadId || !this.currentTurnId || this.rolloutSpawnPollTimer) {
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
this.rolloutSpawnPollTimer = setTimeout(() => {
|
|
3219
|
+
this.rolloutSpawnPollTimer = null;
|
|
3220
|
+
void this.pollRolloutSpawnAgentCalls();
|
|
3221
|
+
}, CODEX_ROLLOUT_SPAWN_POLL_MS);
|
|
3222
|
+
}
|
|
3223
|
+
async pollRolloutSpawnAgentCalls() {
|
|
3224
|
+
if (!this.currentThreadId || !this.currentTurnId) {
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
await this.emitNewRolloutSpawnAgentCalls();
|
|
3228
|
+
if (this.currentThreadId && this.currentTurnId) {
|
|
3229
|
+
this.scheduleRolloutSpawnAgentPoll();
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
async primeRolloutSpawnAgentPolling() {
|
|
3233
|
+
if (!this.currentThreadId || !this.currentTurnId) {
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
try {
|
|
3237
|
+
const rolloutTimeline = await loadCodexPersistedTimeline(this.currentThreadId, undefined, this.logger);
|
|
3238
|
+
this.emitRolloutSpawnAgentCallsFromTimeline(rolloutTimeline);
|
|
3239
|
+
}
|
|
3240
|
+
catch (error) {
|
|
3241
|
+
this.logger.trace({ error, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Failed to prime Codex rollout spawn_agent polling");
|
|
3242
|
+
}
|
|
3243
|
+
this.scheduleRolloutSpawnAgentPoll();
|
|
3244
|
+
}
|
|
2510
3245
|
emitCompletedThreadItem(item, source) {
|
|
2511
3246
|
const timelineItem = buildCompletedCodexTimelineItem(item, {
|
|
2512
3247
|
cwd: this.config.cwd ?? null,
|
|
@@ -2533,6 +3268,67 @@ class CodexAppServerAgentSession {
|
|
|
2533
3268
|
}
|
|
2534
3269
|
return true;
|
|
2535
3270
|
}
|
|
3271
|
+
emitCompatibilityLifecycleNotifications(parsed) {
|
|
3272
|
+
if (parsed.source !== "item" || !this.client) {
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
const itemType = normalizeCodexThreadItemType(typeof parsed.item.type === "string" ? parsed.item.type : undefined);
|
|
3276
|
+
const lifecycleMethod = parsed.kind === "item_started"
|
|
3277
|
+
? "codex/event/item_started"
|
|
3278
|
+
: "codex/event/item_completed";
|
|
3279
|
+
if (itemType === "agentMessage" ||
|
|
3280
|
+
itemType === "reasoning" ||
|
|
3281
|
+
itemType === "plan") {
|
|
3282
|
+
this.client.dispatchNotification(lifecycleMethod, {
|
|
3283
|
+
msg: {
|
|
3284
|
+
type: parsed.kind,
|
|
3285
|
+
item: parsed.item,
|
|
3286
|
+
...(parsed.threadId ? { threadId: parsed.threadId } : {}),
|
|
3287
|
+
...(parsed.turnId ? { turnId: parsed.turnId } : {}),
|
|
3288
|
+
},
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
if (itemType !== "commandExecution") {
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
if (parsed.kind === "item_started") {
|
|
3295
|
+
this.client.dispatchNotification("codex/event/exec_command_begin", {
|
|
3296
|
+
msg: {
|
|
3297
|
+
type: "exec_command_begin",
|
|
3298
|
+
call_id: parsed.item.id ?? null,
|
|
3299
|
+
command: parsed.item.command ?? null,
|
|
3300
|
+
cwd: parsed.item.cwd ?? null,
|
|
3301
|
+
...(parsed.threadId ? { threadId: parsed.threadId } : {}),
|
|
3302
|
+
...(parsed.turnId ? { turnId: parsed.turnId } : {}),
|
|
3303
|
+
},
|
|
3304
|
+
});
|
|
3305
|
+
return;
|
|
3306
|
+
}
|
|
3307
|
+
const exitCode = typeof parsed.item.exitCode === "number" ? parsed.item.exitCode : null;
|
|
3308
|
+
const status = typeof parsed.item.status === "string" ? parsed.item.status.trim().toLowerCase() : "";
|
|
3309
|
+
const success = typeof parsed.item.success === "boolean"
|
|
3310
|
+
? parsed.item.success
|
|
3311
|
+
: status.length > 0
|
|
3312
|
+
? !COMPATIBILITY_FAILED_TOOL_STATUSES.has(status) &&
|
|
3313
|
+
!COMPATIBILITY_CANCELED_TOOL_STATUSES.has(status)
|
|
3314
|
+
: exitCode === null || exitCode === 0;
|
|
3315
|
+
this.client.dispatchNotification("codex/event/exec_command_end", {
|
|
3316
|
+
msg: {
|
|
3317
|
+
type: "exec_command_end",
|
|
3318
|
+
call_id: parsed.item.id ?? null,
|
|
3319
|
+
command: parsed.item.command ?? null,
|
|
3320
|
+
cwd: parsed.item.cwd ?? null,
|
|
3321
|
+
aggregated_output: typeof parsed.item.aggregatedOutput === "string"
|
|
3322
|
+
? parsed.item.aggregatedOutput
|
|
3323
|
+
: null,
|
|
3324
|
+
exit_code: exitCode,
|
|
3325
|
+
success,
|
|
3326
|
+
stderr: typeof parsed.item.stderr === "string" ? parsed.item.stderr : null,
|
|
3327
|
+
...(parsed.threadId ? { threadId: parsed.threadId } : {}),
|
|
3328
|
+
...(parsed.turnId ? { turnId: parsed.turnId } : {}),
|
|
3329
|
+
},
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
2536
3332
|
async reconcileTurnCompletionFromThreadRead() {
|
|
2537
3333
|
if (!this.client || !this.currentThreadId) {
|
|
2538
3334
|
return 0;
|
|
@@ -2620,6 +3416,15 @@ class CodexAppServerAgentSession {
|
|
|
2620
3416
|
async finalizeTurnCompletion(parsed) {
|
|
2621
3417
|
let terminalEventEmitted = false;
|
|
2622
3418
|
try {
|
|
3419
|
+
if (parsed.status === "completed") {
|
|
3420
|
+
try {
|
|
3421
|
+
const emittedSpawnCalls = await this.emitNewRolloutSpawnAgentCalls();
|
|
3422
|
+
this.logger.trace({ emittedSpawnCalls, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Reconciled Codex spawn_agent rollout calls before turn completion");
|
|
3423
|
+
}
|
|
3424
|
+
catch (error) {
|
|
3425
|
+
this.logger.warn({ error, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Failed to reconcile Codex spawn_agent rollout calls before turn completion");
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
2623
3428
|
if (parsed.status === "completed" && this.hasPendingBufferedCompletionContent()) {
|
|
2624
3429
|
this.logger.trace({
|
|
2625
3430
|
pendingAgentMessages: this.pendingAgentMessages.size,
|
|
@@ -2655,17 +3460,57 @@ class CodexAppServerAgentSession {
|
|
|
2655
3460
|
handleNotification(method, params) {
|
|
2656
3461
|
const parsed = CodexNotificationSchema.parse({ method, params });
|
|
2657
3462
|
if (parsed.kind === "thread_started") {
|
|
3463
|
+
if (this.currentThreadId && this.currentThreadId !== parsed.threadId) {
|
|
3464
|
+
this.logger.trace({
|
|
3465
|
+
currentThreadId: this.currentThreadId,
|
|
3466
|
+
ignoredThreadId: parsed.threadId,
|
|
3467
|
+
turnId: this.currentTurnId,
|
|
3468
|
+
}, "Ignoring Codex thread_started for a different thread while session is already bound");
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
2658
3471
|
this.currentThreadId = parsed.threadId;
|
|
2659
3472
|
this.emitEvent({ type: "thread_started", provider: CODEX_PROVIDER, sessionId: parsed.threadId });
|
|
2660
3473
|
return;
|
|
2661
3474
|
}
|
|
2662
3475
|
if (parsed.kind === "turn_started") {
|
|
3476
|
+
if (parsed.threadId && this.currentThreadId && parsed.threadId !== this.currentThreadId) {
|
|
3477
|
+
this.logger.trace({
|
|
3478
|
+
currentThreadId: this.currentThreadId,
|
|
3479
|
+
ignoredThreadId: parsed.threadId,
|
|
3480
|
+
currentTurnId: this.currentTurnId,
|
|
3481
|
+
ignoredTurnId: parsed.turnId,
|
|
3482
|
+
}, "Ignoring Codex turn_started for a different thread");
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
if (this.currentTurnId && this.currentTurnId !== parsed.turnId) {
|
|
3486
|
+
if (this.deferredTurnCompletion) {
|
|
3487
|
+
this.discardDeferredTurnCompletion("turn_started");
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
2663
3490
|
this.clearTurnState();
|
|
2664
3491
|
this.currentTurnId = parsed.turnId;
|
|
2665
3492
|
this.emitEvent({ type: "turn_started", provider: CODEX_PROVIDER });
|
|
3493
|
+
void this.primeRolloutSpawnAgentPolling();
|
|
2666
3494
|
return;
|
|
2667
3495
|
}
|
|
2668
3496
|
if (parsed.kind === "turn_completed") {
|
|
3497
|
+
if (parsed.threadId && this.currentThreadId && parsed.threadId !== this.currentThreadId) {
|
|
3498
|
+
this.logger.trace({
|
|
3499
|
+
currentThreadId: this.currentThreadId,
|
|
3500
|
+
ignoredThreadId: parsed.threadId,
|
|
3501
|
+
turnId: parsed.turnId,
|
|
3502
|
+
currentTurnId: this.currentTurnId,
|
|
3503
|
+
}, "Ignoring Codex terminal event for a different thread");
|
|
3504
|
+
return;
|
|
3505
|
+
}
|
|
3506
|
+
if (parsed.turnId && this.currentTurnId && parsed.turnId !== this.currentTurnId) {
|
|
3507
|
+
this.logger.trace({
|
|
3508
|
+
currentTurnId: this.currentTurnId,
|
|
3509
|
+
ignoredTurnId: parsed.turnId,
|
|
3510
|
+
threadId: this.currentThreadId,
|
|
3511
|
+
}, "Ignoring Codex terminal event for a different turn");
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
2669
3514
|
// A later failed/interrupted terminal status must replace a deferred
|
|
2670
3515
|
// "completed" status so we do not hide a real terminal error behind
|
|
2671
3516
|
// the settle window.
|
|
@@ -2704,6 +3549,9 @@ class CodexAppServerAgentSession {
|
|
|
2704
3549
|
return;
|
|
2705
3550
|
}
|
|
2706
3551
|
if (parsed.kind === "plan_delta") {
|
|
3552
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
2707
3555
|
this.noteTurnActivityAfterPermission("plan_delta");
|
|
2708
3556
|
const previous = this.pendingPlanTexts.get(parsed.itemId) ?? "";
|
|
2709
3557
|
const next = previous + parsed.delta;
|
|
@@ -2746,12 +3594,18 @@ class CodexAppServerAgentSession {
|
|
|
2746
3594
|
return;
|
|
2747
3595
|
}
|
|
2748
3596
|
if (parsed.kind === "agent_message_delta") {
|
|
3597
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
2749
3600
|
this.noteTurnActivityAfterPermission("agent_message_delta");
|
|
2750
3601
|
const prev = this.pendingAgentMessages.get(parsed.itemId) ?? "";
|
|
2751
3602
|
this.pendingAgentMessages.set(parsed.itemId, prev + parsed.delta);
|
|
2752
3603
|
return;
|
|
2753
3604
|
}
|
|
2754
3605
|
if (parsed.kind === "reasoning_delta") {
|
|
3606
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
2755
3609
|
this.noteTurnActivityAfterPermission("reasoning_delta");
|
|
2756
3610
|
const prev = this.pendingReasoning.get(parsed.itemId) ?? [];
|
|
2757
3611
|
prev.push(parsed.delta);
|
|
@@ -2759,17 +3613,29 @@ class CodexAppServerAgentSession {
|
|
|
2759
3613
|
return;
|
|
2760
3614
|
}
|
|
2761
3615
|
if (parsed.kind === "exec_command_output_delta") {
|
|
3616
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
2762
3619
|
this.noteTurnActivityAfterPermission("exec_command_output_delta");
|
|
2763
3620
|
this.appendOutputDeltaChunk(this.pendingCommandOutputDeltas, parsed.callId, parsed.chunk, { decodeBase64: true });
|
|
2764
3621
|
return;
|
|
2765
3622
|
}
|
|
2766
3623
|
if (parsed.kind === "file_change_output_delta") {
|
|
3624
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
2767
3627
|
this.noteTurnActivityAfterPermission("file_change_output_delta");
|
|
2768
3628
|
this.appendOutputDeltaChunk(this.pendingFileChangeOutputDeltas, parsed.itemId, parsed.delta);
|
|
2769
3629
|
return;
|
|
2770
3630
|
}
|
|
2771
3631
|
if (parsed.kind === "exec_command_started") {
|
|
3632
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
2772
3635
|
this.noteTurnActivityAfterPermission("exec_command_started");
|
|
3636
|
+
if (parsed.callId && this.emittedExecCommandStartedCallIds.has(parsed.callId)) {
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
2773
3639
|
if (parsed.callId) {
|
|
2774
3640
|
this.emittedExecCommandStartedCallIds.add(parsed.callId);
|
|
2775
3641
|
this.pendingCommandOutputDeltas.delete(parsed.callId);
|
|
@@ -2786,7 +3652,13 @@ class CodexAppServerAgentSession {
|
|
|
2786
3652
|
return;
|
|
2787
3653
|
}
|
|
2788
3654
|
if (parsed.kind === "exec_command_completed") {
|
|
3655
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
2789
3658
|
this.noteTurnActivityAfterPermission("exec_command_completed");
|
|
3659
|
+
if (parsed.callId && this.emittedExecCommandCompletedCallIds.has(parsed.callId)) {
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
2790
3662
|
const bufferedOutput = this.consumeOutputDelta(this.pendingCommandOutputDeltas, parsed.callId);
|
|
2791
3663
|
const timelineItem = mapCodexExecNotificationToToolCall({
|
|
2792
3664
|
callId: parsed.callId,
|
|
@@ -2805,6 +3677,9 @@ class CodexAppServerAgentSession {
|
|
|
2805
3677
|
return;
|
|
2806
3678
|
}
|
|
2807
3679
|
if (parsed.kind === "patch_apply_started") {
|
|
3680
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
2808
3683
|
this.noteTurnActivityAfterPermission("patch_apply_started");
|
|
2809
3684
|
if (parsed.callId) {
|
|
2810
3685
|
this.pendingFileChangeOutputDeltas.delete(parsed.callId);
|
|
@@ -2825,6 +3700,9 @@ class CodexAppServerAgentSession {
|
|
|
2825
3700
|
return;
|
|
2826
3701
|
}
|
|
2827
3702
|
if (parsed.kind === "patch_apply_completed") {
|
|
3703
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
2828
3706
|
this.noteTurnActivityAfterPermission("patch_apply_completed");
|
|
2829
3707
|
const bufferedOutput = this.consumeOutputDelta(this.pendingFileChangeOutputDeltas, parsed.callId);
|
|
2830
3708
|
const timelineItem = mapCodexPatchNotificationToToolCall({
|
|
@@ -2847,6 +3725,10 @@ class CodexAppServerAgentSession {
|
|
|
2847
3725
|
return;
|
|
2848
3726
|
}
|
|
2849
3727
|
if (parsed.kind === "item_completed") {
|
|
3728
|
+
this.emitCompatibilityLifecycleNotifications(parsed);
|
|
3729
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
2850
3732
|
this.noteTurnActivityAfterPermission("item_completed");
|
|
2851
3733
|
// Codex emits mirrored lifecycle notifications via both `codex/event/item_*`
|
|
2852
3734
|
// and canonical `item/*`. We render only the canonical channel to avoid
|
|
@@ -2858,6 +3740,11 @@ class CodexAppServerAgentSession {
|
|
|
2858
3740
|
return;
|
|
2859
3741
|
}
|
|
2860
3742
|
if (parsed.kind === "item_started") {
|
|
3743
|
+
this.emitCompatibilityLifecycleNotifications(parsed);
|
|
3744
|
+
const normalizedItemType = normalizeCodexThreadItemType(typeof parsed.item.type === "string" ? parsed.item.type : undefined);
|
|
3745
|
+
if (this.hasForeignNotificationProvenance(parsed)) {
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
2861
3748
|
this.noteTurnActivityAfterPermission("item_started");
|
|
2862
3749
|
if (parsed.source === "codex_event") {
|
|
2863
3750
|
return;
|
|
@@ -2867,7 +3754,6 @@ class CodexAppServerAgentSession {
|
|
|
2867
3754
|
cwd: this.config.cwd ?? null,
|
|
2868
3755
|
});
|
|
2869
3756
|
if (timelineItem && timelineItem.type === "tool_call") {
|
|
2870
|
-
const normalizedItemType = normalizeCodexThreadItemType(typeof parsed.item.type === "string" ? parsed.item.type : undefined);
|
|
2871
3757
|
const itemId = parsed.item.id;
|
|
2872
3758
|
if (normalizedItemType === "commandExecution") {
|
|
2873
3759
|
const callId = timelineItem.callId || itemId;
|
|
@@ -3226,43 +4112,61 @@ export class CodexAppServerAgentClient {
|
|
|
3226
4112
|
});
|
|
3227
4113
|
client.notify("initialized", {});
|
|
3228
4114
|
const limit = options?.limit ?? 20;
|
|
3229
|
-
const
|
|
4115
|
+
const includeTimeline = options?.includeTimeline !== false;
|
|
4116
|
+
const response = await requestCodexThreadList({
|
|
4117
|
+
client,
|
|
4118
|
+
logger: this.logger,
|
|
4119
|
+
limit,
|
|
4120
|
+
sourceKinds: CODEX_THREAD_LIST_SOURCE_KINDS,
|
|
4121
|
+
});
|
|
3230
4122
|
const threads = Array.isArray(response?.data) ? response.data : [];
|
|
3231
|
-
const
|
|
4123
|
+
const descriptorsBySessionId = new Map();
|
|
4124
|
+
const threadListSessionIds = new Set();
|
|
3232
4125
|
for (const thread of threads.slice(0, limit)) {
|
|
3233
|
-
const
|
|
3234
|
-
const
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
threadId,
|
|
3241
|
-
includeTurns: true,
|
|
3242
|
-
}));
|
|
3243
|
-
const turns = read.thread?.turns ?? [];
|
|
3244
|
-
const itemsFromThreadRead = [];
|
|
3245
|
-
for (const turn of turns) {
|
|
3246
|
-
for (const item of turn.items ?? []) {
|
|
3247
|
-
const timelineItem = threadItemToTimeline(item, { cwd });
|
|
3248
|
-
if (timelineItem)
|
|
3249
|
-
itemsFromThreadRead.push(timelineItem);
|
|
3250
|
-
}
|
|
4126
|
+
const threadRecord = toRecord(thread);
|
|
4127
|
+
const rolloutPath = readStringField(threadRecord, ["path"]);
|
|
4128
|
+
let metadata = parseCodexPersistedThreadMetadata(thread);
|
|
4129
|
+
if (rolloutPath) {
|
|
4130
|
+
const rolloutMetadata = await readCodexPersistedThreadMetadataFromRolloutPath(rolloutPath, this.logger);
|
|
4131
|
+
if (rolloutMetadata) {
|
|
4132
|
+
metadata = mergeCodexPersistedThreadMetadata(metadata, rolloutMetadata);
|
|
3251
4133
|
}
|
|
3252
|
-
timeline =
|
|
3253
|
-
rolloutTimeline.length > 0
|
|
3254
|
-
? rolloutTimeline
|
|
3255
|
-
: itemsFromThreadRead;
|
|
3256
4134
|
}
|
|
3257
|
-
|
|
3258
|
-
|
|
4135
|
+
const threadId = metadata.threadId;
|
|
4136
|
+
if (!threadId) {
|
|
4137
|
+
continue;
|
|
3259
4138
|
}
|
|
3260
|
-
|
|
4139
|
+
threadListSessionIds.add(threadId);
|
|
4140
|
+
const cwd = metadata.cwd ?? process.cwd();
|
|
4141
|
+
const title = metadata.title;
|
|
4142
|
+
const createdAt = metadata.createdAt;
|
|
4143
|
+
const updatedAt = metadata.updatedAt;
|
|
4144
|
+
let timeline = [];
|
|
4145
|
+
if (includeTimeline) {
|
|
4146
|
+
try {
|
|
4147
|
+
const rolloutTimeline = await loadCodexPersistedTimeline(threadId, undefined, this.logger);
|
|
4148
|
+
const read = (await client.request("thread/read", {
|
|
4149
|
+
threadId,
|
|
4150
|
+
includeTurns: true,
|
|
4151
|
+
}));
|
|
4152
|
+
const turns = read.thread?.turns ?? [];
|
|
4153
|
+
const itemsFromThreadRead = readCodexThreadTimelineFromTurns(turns, { cwd });
|
|
4154
|
+
timeline = mergeCodexThreadReadTimelineWithRolloutSpawnCalls({
|
|
4155
|
+
threadTimeline: itemsFromThreadRead,
|
|
4156
|
+
rolloutTimeline,
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
catch {
|
|
4160
|
+
timeline = [];
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
descriptorsBySessionId.set(threadId, {
|
|
3261
4164
|
provider: CODEX_PROVIDER,
|
|
3262
4165
|
sessionId: threadId,
|
|
3263
4166
|
cwd,
|
|
3264
4167
|
title,
|
|
3265
|
-
|
|
4168
|
+
createdAt,
|
|
4169
|
+
lastActivityAt: updatedAt,
|
|
3266
4170
|
persistence: {
|
|
3267
4171
|
provider: CODEX_PROVIDER,
|
|
3268
4172
|
sessionId: threadId,
|
|
@@ -3275,9 +4179,77 @@ export class CodexAppServerAgentClient {
|
|
|
3275
4179
|
},
|
|
3276
4180
|
},
|
|
3277
4181
|
timeline,
|
|
4182
|
+
model: metadata.model,
|
|
4183
|
+
parentSessionId: metadata.parentSessionId,
|
|
4184
|
+
rootSessionId: metadata.rootSessionId,
|
|
4185
|
+
agentId: metadata.agentId,
|
|
4186
|
+
agentNickname: metadata.agentNickname,
|
|
4187
|
+
agentRole: metadata.agentRole,
|
|
3278
4188
|
});
|
|
3279
4189
|
}
|
|
3280
|
-
|
|
4190
|
+
const sessionRoot = resolveCodexSessionRoot();
|
|
4191
|
+
const rolloutPaths = await listCodexRolloutPaths(sessionRoot);
|
|
4192
|
+
const rolloutPathBySessionId = new Map();
|
|
4193
|
+
for (const rolloutPath of rolloutPaths) {
|
|
4194
|
+
const descriptor = await buildCodexPersistedDescriptorFromRolloutPath({
|
|
4195
|
+
rolloutPath,
|
|
4196
|
+
includeTimeline: false,
|
|
4197
|
+
logger: this.logger,
|
|
4198
|
+
});
|
|
4199
|
+
if (!descriptor) {
|
|
4200
|
+
continue;
|
|
4201
|
+
}
|
|
4202
|
+
rolloutPathBySessionId.set(descriptor.sessionId, rolloutPath);
|
|
4203
|
+
if (!descriptorsBySessionId.has(descriptor.sessionId)) {
|
|
4204
|
+
if (!includeTimeline && !descriptor.parentSessionId) {
|
|
4205
|
+
continue;
|
|
4206
|
+
}
|
|
4207
|
+
descriptorsBySessionId.set(descriptor.sessionId, descriptor);
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
const descriptors = Array.from(descriptorsBySessionId.values())
|
|
4211
|
+
.sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
|
|
4212
|
+
.slice(0, limit);
|
|
4213
|
+
if (!includeTimeline) {
|
|
4214
|
+
return descriptors;
|
|
4215
|
+
}
|
|
4216
|
+
return await Promise.all(descriptors.map(async (descriptor) => {
|
|
4217
|
+
if (threadListSessionIds.has(descriptor.sessionId)) {
|
|
4218
|
+
return descriptor;
|
|
4219
|
+
}
|
|
4220
|
+
const rolloutPath = rolloutPathBySessionId.get(descriptor.sessionId);
|
|
4221
|
+
if (!rolloutPath) {
|
|
4222
|
+
return descriptor;
|
|
4223
|
+
}
|
|
4224
|
+
return ((await buildCodexPersistedDescriptorFromRolloutPath({
|
|
4225
|
+
rolloutPath,
|
|
4226
|
+
includeTimeline: true,
|
|
4227
|
+
logger: this.logger,
|
|
4228
|
+
})) ?? descriptor);
|
|
4229
|
+
}));
|
|
4230
|
+
}
|
|
4231
|
+
finally {
|
|
4232
|
+
await client.dispose();
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
async readPersistedAgent(options) {
|
|
4236
|
+
const sessionId = options.sessionId.trim();
|
|
4237
|
+
if (!sessionId) {
|
|
4238
|
+
return null;
|
|
4239
|
+
}
|
|
4240
|
+
const child = this.spawnAppServer();
|
|
4241
|
+
const client = new CodexAppServerClient(child, this.logger);
|
|
4242
|
+
try {
|
|
4243
|
+
await client.request("initialize", {
|
|
4244
|
+
clientInfo: { name: "junction", title: "Junction", version: "0.0.0" },
|
|
4245
|
+
});
|
|
4246
|
+
client.notify("initialized", {});
|
|
4247
|
+
return await readCodexPersistedDescriptorBySessionId({
|
|
4248
|
+
client,
|
|
4249
|
+
sessionId,
|
|
4250
|
+
includeTimeline: options.includeTimeline !== false,
|
|
4251
|
+
logger: this.logger,
|
|
4252
|
+
});
|
|
3281
4253
|
}
|
|
3282
4254
|
finally {
|
|
3283
4255
|
await client.dispose();
|