@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.
@@ -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
- if (event.type === "timeline") {
2273
- if (event.item.type === "assistant_message") {
2274
- this.pendingAgentMessages.clear();
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
- if (parsed.status === "failed") {
2300
- this.emitEvent({
2301
- type: "turn_failed",
2302
- provider: CODEX_PROVIDER,
2303
- error: parsed.errorMessage ?? "Codex turn failed",
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
- else if (parsed.status === "interrupted") {
2307
- this.emitEvent({ type: "turn_canceled", provider: CODEX_PROVIDER, reason: "interrupted" });
2610
+ if (parsed.status === "completed") {
2611
+ this.scheduleDeferredTurnCompletion(parsed);
2612
+ return;
2308
2613
  }
2309
- else {
2310
- this.emitEvent({ type: "turn_completed", provider: CODEX_PROVIDER, usage: this.latestUsage });
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
- const timelineItem = threadItemToTimeline(parsed.item, {
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;