@junctionpanel/server 0.1.41 → 0.1.43
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.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +19 -8
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +1 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +73 -31
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +22 -1
- 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 +350 -92
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/package.json +2 -2
|
@@ -11,6 +11,7 @@ import { buildCodexRuntimeExtra, DEFAULT_CODEX_MODE_ID, isCodexPlanModeEnabled,
|
|
|
11
11
|
import { writeImageAttachment } from "./image-attachments.js";
|
|
12
12
|
const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
|
|
13
13
|
const TURN_START_TIMEOUT_MS = 90 * 1000;
|
|
14
|
+
const TURN_COMPLETION_SETTLE_DELAY_MS = 75;
|
|
14
15
|
const CODEX_PROVIDER = "codex";
|
|
15
16
|
const CODEX_APP_SERVER_CAPABILITIES = {
|
|
16
17
|
supportsStreaming: true,
|
|
@@ -695,6 +696,60 @@ function formatProposedPlanChunk(text, options) {
|
|
|
695
696
|
}
|
|
696
697
|
return parts.join("");
|
|
697
698
|
}
|
|
699
|
+
function buildCompletedCodexTimelineItem(item, options) {
|
|
700
|
+
const timelineItem = threadItemToTimeline(item, {
|
|
701
|
+
includeUserMessage: false,
|
|
702
|
+
cwd: options.cwd ?? null,
|
|
703
|
+
});
|
|
704
|
+
if (!timelineItem) {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
const normalizedItemType = normalizeCodexThreadItemType(typeof item.type === "string" ? item.type : undefined);
|
|
708
|
+
const itemId = item.id;
|
|
709
|
+
if (timelineItem.type === "tool_call" && normalizedItemType === "commandExecution") {
|
|
710
|
+
const callId = timelineItem.callId || itemId;
|
|
711
|
+
if (callId && options.state.emittedExecCommandCompletedCallIds.has(callId)) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (itemId && options.state.emittedItemCompletedIds.has(itemId)) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
if (timelineItem.type === "assistant_message" && itemId) {
|
|
719
|
+
const buffered = options.state.pendingAgentMessages.get(itemId);
|
|
720
|
+
if (buffered && buffered.length > 0) {
|
|
721
|
+
timelineItem.text = buffered;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (timelineItem.type === "assistant_message" && normalizedItemType === "plan" && itemId) {
|
|
725
|
+
const bufferedPlanText = options.state.pendingPlanTexts.get(itemId) ?? "";
|
|
726
|
+
const finalPlanText = typeof item.text === "string" ? item.text : "";
|
|
727
|
+
if (bufferedPlanText.length > 0) {
|
|
728
|
+
const trailingText = finalPlanText.startsWith(bufferedPlanText)
|
|
729
|
+
? finalPlanText.slice(bufferedPlanText.length)
|
|
730
|
+
: "";
|
|
731
|
+
timelineItem.text = formatProposedPlanChunk(trailingText, {
|
|
732
|
+
close: true,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
else if (finalPlanText.trim().length > 0) {
|
|
736
|
+
timelineItem.text = formatProposedPlanBlock(finalPlanText);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (timelineItem.type === "reasoning" && itemId) {
|
|
740
|
+
const buffered = options.state.pendingReasoning.get(itemId);
|
|
741
|
+
if (buffered && buffered.length > 0) {
|
|
742
|
+
timelineItem.text = buffered.join("");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (itemId) {
|
|
746
|
+
options.state.emittedItemCompletedIds.add(itemId);
|
|
747
|
+
options.state.pendingAgentMessages.delete(itemId);
|
|
748
|
+
options.state.pendingReasoning.delete(itemId);
|
|
749
|
+
options.state.pendingPlanTexts.delete(itemId);
|
|
750
|
+
}
|
|
751
|
+
return timelineItem;
|
|
752
|
+
}
|
|
698
753
|
function planStepsToTodoItems(steps) {
|
|
699
754
|
return steps.map((entry) => ({
|
|
700
755
|
text: entry.step,
|
|
@@ -1492,6 +1547,8 @@ export const __codexAppServerInternals = {
|
|
|
1492
1547
|
shouldRetryInitializeWithoutExperimentalApi,
|
|
1493
1548
|
shouldRetryTurnStartWithoutCollaborationMode,
|
|
1494
1549
|
formatProposedPlanBlock,
|
|
1550
|
+
formatProposedPlanChunk,
|
|
1551
|
+
buildCompletedCodexTimelineItem,
|
|
1495
1552
|
normalizeCodexQuestionDescriptors,
|
|
1496
1553
|
parseUpdatedQuestionAnswers,
|
|
1497
1554
|
buildCodexPermissionsResponse,
|
|
@@ -1536,6 +1593,8 @@ class CodexAppServerAgentSession {
|
|
|
1536
1593
|
this.nativePlanModeSupported = null;
|
|
1537
1594
|
this.pendingPlanTexts = new Map();
|
|
1538
1595
|
this.cachedSkills = [];
|
|
1596
|
+
this.turnCompletionInFlight = false;
|
|
1597
|
+
this.deferredTurnCompletion = null;
|
|
1539
1598
|
this.logger = logger.child({ module: "agent", provider: CODEX_PROVIDER });
|
|
1540
1599
|
if (config.modeId === undefined) {
|
|
1541
1600
|
throw new Error("Codex agent requires modeId to be specified");
|
|
@@ -2058,6 +2117,7 @@ class CodexAppServerAgentSession {
|
|
|
2058
2117
|
? "cancel"
|
|
2059
2118
|
: "decline");
|
|
2060
2119
|
pending.resolve({ decision });
|
|
2120
|
+
this.scheduleDeferredTurnCompletionAfterPermissionResponse();
|
|
2061
2121
|
return;
|
|
2062
2122
|
}
|
|
2063
2123
|
if (pending.kind === "file") {
|
|
@@ -2068,14 +2128,17 @@ class CodexAppServerAgentSession {
|
|
|
2068
2128
|
? "cancel"
|
|
2069
2129
|
: "decline");
|
|
2070
2130
|
pending.resolve({ decision });
|
|
2131
|
+
this.scheduleDeferredTurnCompletionAfterPermissionResponse();
|
|
2071
2132
|
return;
|
|
2072
2133
|
}
|
|
2073
2134
|
if (pending.kind === "permissions") {
|
|
2074
2135
|
pending.resolve(buildCodexPermissionsResponse(response, pending.permissions));
|
|
2136
|
+
this.scheduleDeferredTurnCompletionAfterPermissionResponse();
|
|
2075
2137
|
return;
|
|
2076
2138
|
}
|
|
2077
2139
|
if (pending.kind === "elicitation") {
|
|
2078
2140
|
pending.resolve(buildCodexElicitationResponse(response));
|
|
2141
|
+
this.scheduleDeferredTurnCompletionAfterPermissionResponse();
|
|
2079
2142
|
return;
|
|
2080
2143
|
}
|
|
2081
2144
|
// tool/requestUserInput
|
|
@@ -2104,6 +2167,7 @@ class CodexAppServerAgentSession {
|
|
|
2104
2167
|
answers["default"] = { answers: fallbackAnswers && fallbackAnswers.length > 0 ? fallbackAnswers : [decision] };
|
|
2105
2168
|
}
|
|
2106
2169
|
pending.resolve({ answers });
|
|
2170
|
+
this.scheduleDeferredTurnCompletionAfterPermissionResponse();
|
|
2107
2171
|
}
|
|
2108
2172
|
describePersistence() {
|
|
2109
2173
|
if (!this.currentThreadId)
|
|
@@ -2147,6 +2211,7 @@ class CodexAppServerAgentSession {
|
|
|
2147
2211
|
this.pendingPermissionHandlers.clear();
|
|
2148
2212
|
this.pendingPermissions.clear();
|
|
2149
2213
|
this.resolvedPermissionRequests.clear();
|
|
2214
|
+
this.clearDeferredTurnCompletion();
|
|
2150
2215
|
this.eventQueue?.end();
|
|
2151
2216
|
this.eventQueue = null;
|
|
2152
2217
|
if (this.client) {
|
|
@@ -2269,12 +2334,248 @@ class CodexAppServerAgentSession {
|
|
|
2269
2334
|
return await codexAppServerTurnInputFromPrompt(blocks, this.logger);
|
|
2270
2335
|
}
|
|
2271
2336
|
emitEvent(event) {
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2337
|
+
this.eventQueue?.push(event);
|
|
2338
|
+
}
|
|
2339
|
+
hasOutstandingPermissionRequests() {
|
|
2340
|
+
return this.pendingPermissionHandlers.size > 0;
|
|
2341
|
+
}
|
|
2342
|
+
clearDeferredTurnCompletion() {
|
|
2343
|
+
if (this.deferredTurnCompletion?.timer) {
|
|
2344
|
+
clearTimeout(this.deferredTurnCompletion.timer);
|
|
2345
|
+
}
|
|
2346
|
+
this.deferredTurnCompletion = null;
|
|
2347
|
+
}
|
|
2348
|
+
scheduleDeferredTurnCompletion(parsed, options) {
|
|
2349
|
+
const waitingOnPermission = options?.waitingOnPermission ??
|
|
2350
|
+
this.deferredTurnCompletion?.waitingOnPermission ??
|
|
2351
|
+
this.hasOutstandingPermissionRequests();
|
|
2352
|
+
this.clearDeferredTurnCompletion();
|
|
2353
|
+
this.turnCompletionInFlight = true;
|
|
2354
|
+
const deferred = {
|
|
2355
|
+
parsed,
|
|
2356
|
+
turnId: this.currentTurnId,
|
|
2357
|
+
waitingOnPermission,
|
|
2358
|
+
timer: null,
|
|
2359
|
+
};
|
|
2360
|
+
deferred.timer = setTimeout(() => {
|
|
2361
|
+
void this.flushDeferredTurnCompletion(deferred.turnId);
|
|
2362
|
+
}, TURN_COMPLETION_SETTLE_DELAY_MS);
|
|
2363
|
+
this.deferredTurnCompletion = deferred;
|
|
2364
|
+
}
|
|
2365
|
+
markDeferredTurnCompletionAwaitingPermissions() {
|
|
2366
|
+
if (!this.deferredTurnCompletion) {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
this.scheduleDeferredTurnCompletion(this.deferredTurnCompletion.parsed, {
|
|
2370
|
+
waitingOnPermission: true,
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
discardDeferredTurnCompletion(reason) {
|
|
2374
|
+
const deferred = this.deferredTurnCompletion;
|
|
2375
|
+
if (!deferred) {
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
this.logger.trace({
|
|
2379
|
+
reason,
|
|
2380
|
+
threadId: this.currentThreadId,
|
|
2381
|
+
turnId: deferred.turnId,
|
|
2382
|
+
}, "Discarding deferred Codex turn completion after resumed activity");
|
|
2383
|
+
this.clearDeferredTurnCompletion();
|
|
2384
|
+
this.turnCompletionInFlight = false;
|
|
2385
|
+
}
|
|
2386
|
+
noteTurnActivityAfterPermission(activity) {
|
|
2387
|
+
const deferred = this.deferredTurnCompletion;
|
|
2388
|
+
if (!deferred || !deferred.waitingOnPermission || this.hasOutstandingPermissionRequests()) {
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
this.discardDeferredTurnCompletion(activity);
|
|
2392
|
+
}
|
|
2393
|
+
async flushDeferredTurnCompletion(turnId) {
|
|
2394
|
+
const deferred = this.deferredTurnCompletion;
|
|
2395
|
+
if (!deferred || deferred.turnId !== turnId) {
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (this.hasOutstandingPermissionRequests()) {
|
|
2399
|
+
this.scheduleDeferredTurnCompletion(deferred.parsed, {
|
|
2400
|
+
waitingOnPermission: true,
|
|
2401
|
+
});
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
this.clearDeferredTurnCompletion();
|
|
2405
|
+
await this.finalizeTurnCompletion(deferred.parsed);
|
|
2406
|
+
}
|
|
2407
|
+
scheduleDeferredTurnCompletionAfterPermissionResponse() {
|
|
2408
|
+
if (!this.deferredTurnCompletion || this.hasOutstandingPermissionRequests()) {
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
this.scheduleDeferredTurnCompletion(this.deferredTurnCompletion.parsed, {
|
|
2412
|
+
waitingOnPermission: true,
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
clearTurnState() {
|
|
2416
|
+
this.clearDeferredTurnCompletion();
|
|
2417
|
+
this.currentTurnId = null;
|
|
2418
|
+
this.emittedItemStartedIds.clear();
|
|
2419
|
+
this.emittedItemCompletedIds.clear();
|
|
2420
|
+
this.emittedExecCommandStartedCallIds.clear();
|
|
2421
|
+
this.emittedExecCommandCompletedCallIds.clear();
|
|
2422
|
+
this.pendingAgentMessages.clear();
|
|
2423
|
+
this.pendingReasoning.clear();
|
|
2424
|
+
this.pendingCommandOutputDeltas.clear();
|
|
2425
|
+
this.pendingFileChangeOutputDeltas.clear();
|
|
2426
|
+
this.pendingPlanTexts.clear();
|
|
2427
|
+
this.warnedIncompleteEditToolCallIds.clear();
|
|
2428
|
+
this.turnCompletionInFlight = false;
|
|
2429
|
+
}
|
|
2430
|
+
hasPendingBufferedCompletionContent() {
|
|
2431
|
+
return (this.pendingAgentMessages.size > 0 ||
|
|
2432
|
+
this.pendingReasoning.size > 0 ||
|
|
2433
|
+
this.pendingPlanTexts.size > 0);
|
|
2434
|
+
}
|
|
2435
|
+
emitCompletedThreadItem(item, source) {
|
|
2436
|
+
const timelineItem = buildCompletedCodexTimelineItem(item, {
|
|
2437
|
+
cwd: this.config.cwd ?? null,
|
|
2438
|
+
state: {
|
|
2439
|
+
pendingAgentMessages: this.pendingAgentMessages,
|
|
2440
|
+
pendingReasoning: this.pendingReasoning,
|
|
2441
|
+
pendingPlanTexts: this.pendingPlanTexts,
|
|
2442
|
+
emittedExecCommandCompletedCallIds: this.emittedExecCommandCompletedCallIds,
|
|
2443
|
+
emittedItemCompletedIds: this.emittedItemCompletedIds,
|
|
2444
|
+
},
|
|
2445
|
+
});
|
|
2446
|
+
if (!timelineItem) {
|
|
2447
|
+
return false;
|
|
2448
|
+
}
|
|
2449
|
+
const itemId = item.id;
|
|
2450
|
+
if (timelineItem.type === "tool_call") {
|
|
2451
|
+
this.warnOnIncompleteEditToolCall(timelineItem, source, item);
|
|
2452
|
+
}
|
|
2453
|
+
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
|
2454
|
+
if (itemId) {
|
|
2455
|
+
this.emittedItemStartedIds.delete(itemId);
|
|
2456
|
+
this.pendingCommandOutputDeltas.delete(itemId);
|
|
2457
|
+
this.pendingFileChangeOutputDeltas.delete(itemId);
|
|
2458
|
+
}
|
|
2459
|
+
return true;
|
|
2460
|
+
}
|
|
2461
|
+
async reconcileTurnCompletionFromThreadRead() {
|
|
2462
|
+
if (!this.client || !this.currentThreadId) {
|
|
2463
|
+
return 0;
|
|
2464
|
+
}
|
|
2465
|
+
const response = (await this.client.request("thread/read", {
|
|
2466
|
+
threadId: this.currentThreadId,
|
|
2467
|
+
includeTurns: true,
|
|
2468
|
+
}));
|
|
2469
|
+
const turns = Array.isArray(response?.thread?.turns) ? response.thread.turns : [];
|
|
2470
|
+
const lastTurn = turns[turns.length - 1];
|
|
2471
|
+
const items = Array.isArray(lastTurn?.items) ? lastTurn.items : [];
|
|
2472
|
+
let emitted = 0;
|
|
2473
|
+
for (const item of items) {
|
|
2474
|
+
if (this.emitCompletedThreadItem(item, "thread_read_completion")) {
|
|
2475
|
+
emitted += 1;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
return emitted;
|
|
2479
|
+
}
|
|
2480
|
+
flushPendingCompletionFallback() {
|
|
2481
|
+
let emitted = 0;
|
|
2482
|
+
for (const [itemId, chunks] of Array.from(this.pendingReasoning.entries())) {
|
|
2483
|
+
const text = chunks.join("");
|
|
2484
|
+
if (text.length === 0) {
|
|
2485
|
+
this.pendingReasoning.delete(itemId);
|
|
2486
|
+
continue;
|
|
2275
2487
|
}
|
|
2488
|
+
this.emitEvent({
|
|
2489
|
+
type: "timeline",
|
|
2490
|
+
provider: CODEX_PROVIDER,
|
|
2491
|
+
item: { type: "reasoning", text },
|
|
2492
|
+
});
|
|
2493
|
+
this.emittedItemCompletedIds.add(itemId);
|
|
2494
|
+
this.pendingReasoning.delete(itemId);
|
|
2495
|
+
emitted += 1;
|
|
2496
|
+
}
|
|
2497
|
+
for (const [itemId, text] of Array.from(this.pendingAgentMessages.entries())) {
|
|
2498
|
+
if (text.length === 0) {
|
|
2499
|
+
this.pendingAgentMessages.delete(itemId);
|
|
2500
|
+
continue;
|
|
2501
|
+
}
|
|
2502
|
+
this.emitEvent({
|
|
2503
|
+
type: "timeline",
|
|
2504
|
+
provider: CODEX_PROVIDER,
|
|
2505
|
+
item: { type: "assistant_message", text },
|
|
2506
|
+
});
|
|
2507
|
+
this.emittedItemCompletedIds.add(itemId);
|
|
2508
|
+
this.pendingAgentMessages.delete(itemId);
|
|
2509
|
+
emitted += 1;
|
|
2510
|
+
}
|
|
2511
|
+
for (const [itemId, text] of Array.from(this.pendingPlanTexts.entries())) {
|
|
2512
|
+
if (text.trim().length === 0) {
|
|
2513
|
+
this.pendingPlanTexts.delete(itemId);
|
|
2514
|
+
continue;
|
|
2515
|
+
}
|
|
2516
|
+
this.emitEvent({
|
|
2517
|
+
type: "timeline",
|
|
2518
|
+
provider: CODEX_PROVIDER,
|
|
2519
|
+
item: {
|
|
2520
|
+
type: "assistant_message",
|
|
2521
|
+
text: formatProposedPlanChunk("", { close: true }),
|
|
2522
|
+
},
|
|
2523
|
+
});
|
|
2524
|
+
this.emittedItemCompletedIds.add(itemId);
|
|
2525
|
+
this.pendingPlanTexts.delete(itemId);
|
|
2526
|
+
emitted += 1;
|
|
2527
|
+
}
|
|
2528
|
+
return emitted;
|
|
2529
|
+
}
|
|
2530
|
+
emitTerminalTurnEvent(parsed) {
|
|
2531
|
+
if (parsed.status === "failed") {
|
|
2532
|
+
this.emitEvent({
|
|
2533
|
+
type: "turn_failed",
|
|
2534
|
+
provider: CODEX_PROVIDER,
|
|
2535
|
+
error: parsed.errorMessage ?? "Codex turn failed",
|
|
2536
|
+
});
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (parsed.status === "interrupted") {
|
|
2540
|
+
this.emitEvent({ type: "turn_canceled", provider: CODEX_PROVIDER, reason: "interrupted" });
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
this.emitEvent({ type: "turn_completed", provider: CODEX_PROVIDER, usage: this.latestUsage });
|
|
2544
|
+
}
|
|
2545
|
+
async finalizeTurnCompletion(parsed) {
|
|
2546
|
+
let terminalEventEmitted = false;
|
|
2547
|
+
try {
|
|
2548
|
+
if (parsed.status === "completed" && this.hasPendingBufferedCompletionContent()) {
|
|
2549
|
+
this.logger.trace({
|
|
2550
|
+
pendingAgentMessages: this.pendingAgentMessages.size,
|
|
2551
|
+
pendingReasoning: this.pendingReasoning.size,
|
|
2552
|
+
pendingPlanTexts: this.pendingPlanTexts.size,
|
|
2553
|
+
threadId: this.currentThreadId,
|
|
2554
|
+
turnId: this.currentTurnId,
|
|
2555
|
+
}, "Codex turn completed with buffered items still pending; reconciling before closing stream");
|
|
2556
|
+
try {
|
|
2557
|
+
const emitted = await this.reconcileTurnCompletionFromThreadRead();
|
|
2558
|
+
this.logger.trace({ emitted, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Reconciled Codex turn completion from thread/read");
|
|
2559
|
+
}
|
|
2560
|
+
catch (error) {
|
|
2561
|
+
this.logger.warn({ error, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Failed to reconcile Codex turn completion from thread/read");
|
|
2562
|
+
}
|
|
2563
|
+
const fallbackEmitted = this.flushPendingCompletionFallback();
|
|
2564
|
+
if (fallbackEmitted > 0) {
|
|
2565
|
+
this.logger.warn({ fallbackEmitted, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Flushed buffered Codex completion items without canonical item/completed notifications");
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
this.emitTerminalTurnEvent(parsed);
|
|
2569
|
+
terminalEventEmitted = true;
|
|
2570
|
+
}
|
|
2571
|
+
finally {
|
|
2572
|
+
if (!terminalEventEmitted) {
|
|
2573
|
+
this.logger.warn({ status: parsed.status, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Codex turn completion exited before emitting a terminal event; emitting fallback terminal event");
|
|
2574
|
+
this.emitTerminalTurnEvent(parsed);
|
|
2575
|
+
}
|
|
2576
|
+
this.clearTurnState();
|
|
2577
|
+
this.eventQueue?.end();
|
|
2276
2578
|
}
|
|
2277
|
-
this.eventQueue?.push(event);
|
|
2278
2579
|
}
|
|
2279
2580
|
handleNotification(method, params) {
|
|
2280
2581
|
const parsed = CodexNotificationSchema.parse({ method, params });
|
|
@@ -2284,43 +2585,38 @@ class CodexAppServerAgentSession {
|
|
|
2284
2585
|
return;
|
|
2285
2586
|
}
|
|
2286
2587
|
if (parsed.kind === "turn_started") {
|
|
2588
|
+
this.clearTurnState();
|
|
2287
2589
|
this.currentTurnId = parsed.turnId;
|
|
2288
|
-
this.emittedItemStartedIds.clear();
|
|
2289
|
-
this.emittedItemCompletedIds.clear();
|
|
2290
|
-
this.emittedExecCommandStartedCallIds.clear();
|
|
2291
|
-
this.emittedExecCommandCompletedCallIds.clear();
|
|
2292
|
-
this.pendingCommandOutputDeltas.clear();
|
|
2293
|
-
this.pendingFileChangeOutputDeltas.clear();
|
|
2294
|
-
this.warnedIncompleteEditToolCallIds.clear();
|
|
2295
2590
|
this.emitEvent({ type: "turn_started", provider: CODEX_PROVIDER });
|
|
2296
2591
|
return;
|
|
2297
2592
|
}
|
|
2298
2593
|
if (parsed.kind === "turn_completed") {
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2594
|
+
// A later failed/interrupted terminal status must replace a deferred
|
|
2595
|
+
// "completed" status so we do not hide a real terminal error behind
|
|
2596
|
+
// the settle window.
|
|
2597
|
+
if (this.turnCompletionInFlight) {
|
|
2598
|
+
if (parsed.status === "completed") {
|
|
2599
|
+
this.scheduleDeferredTurnCompletion(parsed);
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
if (this.deferredTurnCompletion) {
|
|
2603
|
+
this.clearDeferredTurnCompletion();
|
|
2604
|
+
this.turnCompletionInFlight = false;
|
|
2605
|
+
}
|
|
2606
|
+
else {
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2305
2609
|
}
|
|
2306
|
-
|
|
2307
|
-
this.
|
|
2610
|
+
if (parsed.status === "completed") {
|
|
2611
|
+
this.scheduleDeferredTurnCompletion(parsed);
|
|
2612
|
+
return;
|
|
2308
2613
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
}
|
|
2312
|
-
this.emittedItemStartedIds.clear();
|
|
2313
|
-
this.emittedItemCompletedIds.clear();
|
|
2314
|
-
this.emittedExecCommandStartedCallIds.clear();
|
|
2315
|
-
this.emittedExecCommandCompletedCallIds.clear();
|
|
2316
|
-
this.pendingCommandOutputDeltas.clear();
|
|
2317
|
-
this.pendingFileChangeOutputDeltas.clear();
|
|
2318
|
-
this.pendingPlanTexts.clear();
|
|
2319
|
-
this.warnedIncompleteEditToolCallIds.clear();
|
|
2320
|
-
this.eventQueue?.end();
|
|
2614
|
+
this.turnCompletionInFlight = true;
|
|
2615
|
+
void this.finalizeTurnCompletion(parsed);
|
|
2321
2616
|
return;
|
|
2322
2617
|
}
|
|
2323
2618
|
if (parsed.kind === "plan_updated") {
|
|
2619
|
+
this.noteTurnActivityAfterPermission("plan_updated");
|
|
2324
2620
|
const items = planStepsToTodoItems(parsed.plan.map((entry) => ({
|
|
2325
2621
|
step: entry.step ?? "",
|
|
2326
2622
|
status: entry.status ?? "pending",
|
|
@@ -2333,6 +2629,7 @@ class CodexAppServerAgentSession {
|
|
|
2333
2629
|
return;
|
|
2334
2630
|
}
|
|
2335
2631
|
if (parsed.kind === "plan_delta") {
|
|
2632
|
+
this.noteTurnActivityAfterPermission("plan_delta");
|
|
2336
2633
|
const previous = this.pendingPlanTexts.get(parsed.itemId) ?? "";
|
|
2337
2634
|
const next = previous + parsed.delta;
|
|
2338
2635
|
this.pendingPlanTexts.set(parsed.itemId, next);
|
|
@@ -2374,25 +2671,30 @@ class CodexAppServerAgentSession {
|
|
|
2374
2671
|
return;
|
|
2375
2672
|
}
|
|
2376
2673
|
if (parsed.kind === "agent_message_delta") {
|
|
2674
|
+
this.noteTurnActivityAfterPermission("agent_message_delta");
|
|
2377
2675
|
const prev = this.pendingAgentMessages.get(parsed.itemId) ?? "";
|
|
2378
2676
|
this.pendingAgentMessages.set(parsed.itemId, prev + parsed.delta);
|
|
2379
2677
|
return;
|
|
2380
2678
|
}
|
|
2381
2679
|
if (parsed.kind === "reasoning_delta") {
|
|
2680
|
+
this.noteTurnActivityAfterPermission("reasoning_delta");
|
|
2382
2681
|
const prev = this.pendingReasoning.get(parsed.itemId) ?? [];
|
|
2383
2682
|
prev.push(parsed.delta);
|
|
2384
2683
|
this.pendingReasoning.set(parsed.itemId, prev);
|
|
2385
2684
|
return;
|
|
2386
2685
|
}
|
|
2387
2686
|
if (parsed.kind === "exec_command_output_delta") {
|
|
2687
|
+
this.noteTurnActivityAfterPermission("exec_command_output_delta");
|
|
2388
2688
|
this.appendOutputDeltaChunk(this.pendingCommandOutputDeltas, parsed.callId, parsed.chunk, { decodeBase64: true });
|
|
2389
2689
|
return;
|
|
2390
2690
|
}
|
|
2391
2691
|
if (parsed.kind === "file_change_output_delta") {
|
|
2692
|
+
this.noteTurnActivityAfterPermission("file_change_output_delta");
|
|
2392
2693
|
this.appendOutputDeltaChunk(this.pendingFileChangeOutputDeltas, parsed.itemId, parsed.delta);
|
|
2393
2694
|
return;
|
|
2394
2695
|
}
|
|
2395
2696
|
if (parsed.kind === "exec_command_started") {
|
|
2697
|
+
this.noteTurnActivityAfterPermission("exec_command_started");
|
|
2396
2698
|
if (parsed.callId) {
|
|
2397
2699
|
this.emittedExecCommandStartedCallIds.add(parsed.callId);
|
|
2398
2700
|
this.pendingCommandOutputDeltas.delete(parsed.callId);
|
|
@@ -2409,6 +2711,7 @@ class CodexAppServerAgentSession {
|
|
|
2409
2711
|
return;
|
|
2410
2712
|
}
|
|
2411
2713
|
if (parsed.kind === "exec_command_completed") {
|
|
2714
|
+
this.noteTurnActivityAfterPermission("exec_command_completed");
|
|
2412
2715
|
const bufferedOutput = this.consumeOutputDelta(this.pendingCommandOutputDeltas, parsed.callId);
|
|
2413
2716
|
const timelineItem = mapCodexExecNotificationToToolCall({
|
|
2414
2717
|
callId: parsed.callId,
|
|
@@ -2427,6 +2730,7 @@ class CodexAppServerAgentSession {
|
|
|
2427
2730
|
return;
|
|
2428
2731
|
}
|
|
2429
2732
|
if (parsed.kind === "patch_apply_started") {
|
|
2733
|
+
this.noteTurnActivityAfterPermission("patch_apply_started");
|
|
2430
2734
|
if (parsed.callId) {
|
|
2431
2735
|
this.pendingFileChangeOutputDeltas.delete(parsed.callId);
|
|
2432
2736
|
}
|
|
@@ -2446,6 +2750,7 @@ class CodexAppServerAgentSession {
|
|
|
2446
2750
|
return;
|
|
2447
2751
|
}
|
|
2448
2752
|
if (parsed.kind === "patch_apply_completed") {
|
|
2753
|
+
this.noteTurnActivityAfterPermission("patch_apply_completed");
|
|
2449
2754
|
const bufferedOutput = this.consumeOutputDelta(this.pendingFileChangeOutputDeltas, parsed.callId);
|
|
2450
2755
|
const timelineItem = mapCodexPatchNotificationToToolCall({
|
|
2451
2756
|
callId: parsed.callId,
|
|
@@ -2467,76 +2772,18 @@ class CodexAppServerAgentSession {
|
|
|
2467
2772
|
return;
|
|
2468
2773
|
}
|
|
2469
2774
|
if (parsed.kind === "item_completed") {
|
|
2775
|
+
this.noteTurnActivityAfterPermission("item_completed");
|
|
2470
2776
|
// Codex emits mirrored lifecycle notifications via both `codex/event/item_*`
|
|
2471
2777
|
// and canonical `item/*`. We render only the canonical channel to avoid
|
|
2472
2778
|
// duplicated assistant/reasoning rows.
|
|
2473
2779
|
if (parsed.source === "codex_event") {
|
|
2474
2780
|
return;
|
|
2475
2781
|
}
|
|
2476
|
-
|
|
2477
|
-
includeUserMessage: false,
|
|
2478
|
-
cwd: this.config.cwd ?? null,
|
|
2479
|
-
});
|
|
2480
|
-
if (timelineItem) {
|
|
2481
|
-
const normalizedItemType = normalizeCodexThreadItemType(typeof parsed.item.type === "string" ? parsed.item.type : undefined);
|
|
2482
|
-
const itemId = parsed.item.id;
|
|
2483
|
-
// For commandExecution items, codex/event/exec_command_* is authoritative.
|
|
2484
|
-
// Keep item/completed as fallback only when no exec_command completion was seen.
|
|
2485
|
-
if (timelineItem.type === "tool_call" &&
|
|
2486
|
-
normalizedItemType === "commandExecution") {
|
|
2487
|
-
const callId = timelineItem.callId || itemId;
|
|
2488
|
-
if (callId && this.emittedExecCommandCompletedCallIds.has(callId)) {
|
|
2489
|
-
return;
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
|
-
if (itemId && this.emittedItemCompletedIds.has(itemId)) {
|
|
2493
|
-
return;
|
|
2494
|
-
}
|
|
2495
|
-
if (timelineItem.type === "assistant_message" && itemId) {
|
|
2496
|
-
const buffered = this.pendingAgentMessages.get(itemId);
|
|
2497
|
-
if (buffered && buffered.length > 0) {
|
|
2498
|
-
timelineItem.text = buffered;
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
if (timelineItem.type === "assistant_message" &&
|
|
2502
|
-
normalizedItemType === "plan" &&
|
|
2503
|
-
itemId) {
|
|
2504
|
-
const bufferedPlanText = this.pendingPlanTexts.get(itemId) ?? "";
|
|
2505
|
-
const finalPlanText = typeof parsed.item.text === "string" ? parsed.item.text : "";
|
|
2506
|
-
if (bufferedPlanText.length > 0) {
|
|
2507
|
-
const trailingText = finalPlanText.startsWith(bufferedPlanText)
|
|
2508
|
-
? finalPlanText.slice(bufferedPlanText.length)
|
|
2509
|
-
: "";
|
|
2510
|
-
timelineItem.text = formatProposedPlanChunk(trailingText, {
|
|
2511
|
-
close: true,
|
|
2512
|
-
});
|
|
2513
|
-
this.pendingPlanTexts.delete(itemId);
|
|
2514
|
-
}
|
|
2515
|
-
else if (finalPlanText.trim().length > 0) {
|
|
2516
|
-
timelineItem.text = formatProposedPlanBlock(finalPlanText);
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
if (timelineItem.type === "reasoning" && itemId) {
|
|
2520
|
-
const buffered = this.pendingReasoning.get(itemId);
|
|
2521
|
-
if (buffered && buffered.length > 0) {
|
|
2522
|
-
timelineItem.text = buffered.join("");
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
if (timelineItem.type === "tool_call") {
|
|
2526
|
-
this.warnOnIncompleteEditToolCall(timelineItem, "item_completed", parsed.item);
|
|
2527
|
-
}
|
|
2528
|
-
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
|
2529
|
-
if (itemId) {
|
|
2530
|
-
this.emittedItemCompletedIds.add(itemId);
|
|
2531
|
-
this.emittedItemStartedIds.delete(itemId);
|
|
2532
|
-
this.pendingCommandOutputDeltas.delete(itemId);
|
|
2533
|
-
this.pendingFileChangeOutputDeltas.delete(itemId);
|
|
2534
|
-
this.pendingPlanTexts.delete(itemId);
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2782
|
+
this.emitCompletedThreadItem(parsed.item, "item_completed");
|
|
2537
2783
|
return;
|
|
2538
2784
|
}
|
|
2539
2785
|
if (parsed.kind === "item_started") {
|
|
2786
|
+
this.noteTurnActivityAfterPermission("item_started");
|
|
2540
2787
|
if (parsed.source === "codex_event") {
|
|
2541
2788
|
return;
|
|
2542
2789
|
}
|
|
@@ -2642,6 +2889,7 @@ class CodexAppServerAgentSession {
|
|
|
2642
2889
|
: parsed.networkApprovalContext
|
|
2643
2890
|
? "Allow network access"
|
|
2644
2891
|
: "Run command";
|
|
2892
|
+
this.markDeferredTurnCompletionAwaitingPermissions();
|
|
2645
2893
|
const request = {
|
|
2646
2894
|
id: requestId,
|
|
2647
2895
|
provider: CODEX_PROVIDER,
|
|
@@ -2685,6 +2933,7 @@ class CodexAppServerAgentSession {
|
|
|
2685
2933
|
handleFileChangeApprovalRequest(requestMethod, params, rawRequestId) {
|
|
2686
2934
|
const parsed = params;
|
|
2687
2935
|
const requestId = toPendingPermissionId(rawRequestId ?? parsed.itemId);
|
|
2936
|
+
this.markDeferredTurnCompletionAwaitingPermissions();
|
|
2688
2937
|
const request = {
|
|
2689
2938
|
id: requestId,
|
|
2690
2939
|
provider: CODEX_PROVIDER,
|
|
@@ -2718,6 +2967,7 @@ class CodexAppServerAgentSession {
|
|
|
2718
2967
|
const parsed = params;
|
|
2719
2968
|
const requestId = toPendingPermissionId(rawRequestId ?? parsed.itemId);
|
|
2720
2969
|
const questions = normalizeCodexQuestionDescriptors(parsed.questions);
|
|
2970
|
+
this.markDeferredTurnCompletionAwaitingPermissions();
|
|
2721
2971
|
const request = {
|
|
2722
2972
|
id: requestId,
|
|
2723
2973
|
provider: CODEX_PROVIDER,
|
|
@@ -2756,6 +3006,7 @@ class CodexAppServerAgentSession {
|
|
|
2756
3006
|
handlePermissionsApprovalRequest(requestMethod, params, rawRequestId) {
|
|
2757
3007
|
const parsed = params;
|
|
2758
3008
|
const requestId = toPendingPermissionId(rawRequestId ?? parsed.itemId);
|
|
3009
|
+
this.markDeferredTurnCompletionAwaitingPermissions();
|
|
2759
3010
|
const request = {
|
|
2760
3011
|
id: requestId,
|
|
2761
3012
|
provider: CODEX_PROVIDER,
|
|
@@ -2803,6 +3054,7 @@ class CodexAppServerAgentSession {
|
|
|
2803
3054
|
: parsed.serverName
|
|
2804
3055
|
? `Respond to ${parsed.serverName}`
|
|
2805
3056
|
: "Respond to MCP server";
|
|
3057
|
+
this.markDeferredTurnCompletionAwaitingPermissions();
|
|
2806
3058
|
const request = {
|
|
2807
3059
|
id: requestId,
|
|
2808
3060
|
provider: CODEX_PROVIDER,
|
|
@@ -2852,6 +3104,12 @@ class CodexAppServerAgentSession {
|
|
|
2852
3104
|
});
|
|
2853
3105
|
}
|
|
2854
3106
|
}
|
|
3107
|
+
Object.assign(__codexAppServerInternals, {
|
|
3108
|
+
TURN_COMPLETION_SETTLE_DELAY_MS,
|
|
3109
|
+
createTestSession(config, resumeHandle, logger, spawnAppServer) {
|
|
3110
|
+
return new CodexAppServerAgentSession(config, resumeHandle, logger, spawnAppServer);
|
|
3111
|
+
},
|
|
3112
|
+
});
|
|
2855
3113
|
export class CodexAppServerAgentClient {
|
|
2856
3114
|
constructor(logger, runtimeSettings) {
|
|
2857
3115
|
this.logger = logger;
|