@junctionpanel/server 0.1.41 → 0.1.42

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.
@@ -695,6 +695,60 @@ function formatProposedPlanChunk(text, options) {
695
695
  }
696
696
  return parts.join("");
697
697
  }
698
+ function buildCompletedCodexTimelineItem(item, options) {
699
+ const timelineItem = threadItemToTimeline(item, {
700
+ includeUserMessage: false,
701
+ cwd: options.cwd ?? null,
702
+ });
703
+ if (!timelineItem) {
704
+ return null;
705
+ }
706
+ const normalizedItemType = normalizeCodexThreadItemType(typeof item.type === "string" ? item.type : undefined);
707
+ const itemId = item.id;
708
+ if (timelineItem.type === "tool_call" && normalizedItemType === "commandExecution") {
709
+ const callId = timelineItem.callId || itemId;
710
+ if (callId && options.state.emittedExecCommandCompletedCallIds.has(callId)) {
711
+ return null;
712
+ }
713
+ }
714
+ if (itemId && options.state.emittedItemCompletedIds.has(itemId)) {
715
+ return null;
716
+ }
717
+ if (timelineItem.type === "assistant_message" && itemId) {
718
+ const buffered = options.state.pendingAgentMessages.get(itemId);
719
+ if (buffered && buffered.length > 0) {
720
+ timelineItem.text = buffered;
721
+ }
722
+ }
723
+ if (timelineItem.type === "assistant_message" && normalizedItemType === "plan" && itemId) {
724
+ const bufferedPlanText = options.state.pendingPlanTexts.get(itemId) ?? "";
725
+ const finalPlanText = typeof item.text === "string" ? item.text : "";
726
+ if (bufferedPlanText.length > 0) {
727
+ const trailingText = finalPlanText.startsWith(bufferedPlanText)
728
+ ? finalPlanText.slice(bufferedPlanText.length)
729
+ : "";
730
+ timelineItem.text = formatProposedPlanChunk(trailingText, {
731
+ close: true,
732
+ });
733
+ }
734
+ else if (finalPlanText.trim().length > 0) {
735
+ timelineItem.text = formatProposedPlanBlock(finalPlanText);
736
+ }
737
+ }
738
+ if (timelineItem.type === "reasoning" && itemId) {
739
+ const buffered = options.state.pendingReasoning.get(itemId);
740
+ if (buffered && buffered.length > 0) {
741
+ timelineItem.text = buffered.join("");
742
+ }
743
+ }
744
+ if (itemId) {
745
+ options.state.emittedItemCompletedIds.add(itemId);
746
+ options.state.pendingAgentMessages.delete(itemId);
747
+ options.state.pendingReasoning.delete(itemId);
748
+ options.state.pendingPlanTexts.delete(itemId);
749
+ }
750
+ return timelineItem;
751
+ }
698
752
  function planStepsToTodoItems(steps) {
699
753
  return steps.map((entry) => ({
700
754
  text: entry.step,
@@ -1492,6 +1546,8 @@ export const __codexAppServerInternals = {
1492
1546
  shouldRetryInitializeWithoutExperimentalApi,
1493
1547
  shouldRetryTurnStartWithoutCollaborationMode,
1494
1548
  formatProposedPlanBlock,
1549
+ formatProposedPlanChunk,
1550
+ buildCompletedCodexTimelineItem,
1495
1551
  normalizeCodexQuestionDescriptors,
1496
1552
  parseUpdatedQuestionAnswers,
1497
1553
  buildCodexPermissionsResponse,
@@ -1536,6 +1592,7 @@ class CodexAppServerAgentSession {
1536
1592
  this.nativePlanModeSupported = null;
1537
1593
  this.pendingPlanTexts = new Map();
1538
1594
  this.cachedSkills = [];
1595
+ this.turnCompletionInFlight = false;
1539
1596
  this.logger = logger.child({ module: "agent", provider: CODEX_PROVIDER });
1540
1597
  if (config.modeId === undefined) {
1541
1598
  throw new Error("Codex agent requires modeId to be specified");
@@ -2269,12 +2326,171 @@ class CodexAppServerAgentSession {
2269
2326
  return await codexAppServerTurnInputFromPrompt(blocks, this.logger);
2270
2327
  }
2271
2328
  emitEvent(event) {
2272
- if (event.type === "timeline") {
2273
- if (event.item.type === "assistant_message") {
2274
- this.pendingAgentMessages.clear();
2329
+ this.eventQueue?.push(event);
2330
+ }
2331
+ clearTurnState() {
2332
+ this.currentTurnId = null;
2333
+ this.emittedItemStartedIds.clear();
2334
+ this.emittedItemCompletedIds.clear();
2335
+ this.emittedExecCommandStartedCallIds.clear();
2336
+ this.emittedExecCommandCompletedCallIds.clear();
2337
+ this.pendingAgentMessages.clear();
2338
+ this.pendingReasoning.clear();
2339
+ this.pendingCommandOutputDeltas.clear();
2340
+ this.pendingFileChangeOutputDeltas.clear();
2341
+ this.pendingPlanTexts.clear();
2342
+ this.warnedIncompleteEditToolCallIds.clear();
2343
+ this.turnCompletionInFlight = false;
2344
+ }
2345
+ hasPendingBufferedCompletionContent() {
2346
+ return (this.pendingAgentMessages.size > 0 ||
2347
+ this.pendingReasoning.size > 0 ||
2348
+ this.pendingPlanTexts.size > 0);
2349
+ }
2350
+ emitCompletedThreadItem(item, source) {
2351
+ const timelineItem = buildCompletedCodexTimelineItem(item, {
2352
+ cwd: this.config.cwd ?? null,
2353
+ state: {
2354
+ pendingAgentMessages: this.pendingAgentMessages,
2355
+ pendingReasoning: this.pendingReasoning,
2356
+ pendingPlanTexts: this.pendingPlanTexts,
2357
+ emittedExecCommandCompletedCallIds: this.emittedExecCommandCompletedCallIds,
2358
+ emittedItemCompletedIds: this.emittedItemCompletedIds,
2359
+ },
2360
+ });
2361
+ if (!timelineItem) {
2362
+ return false;
2363
+ }
2364
+ const itemId = item.id;
2365
+ if (timelineItem.type === "tool_call") {
2366
+ this.warnOnIncompleteEditToolCall(timelineItem, source, item);
2367
+ }
2368
+ this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
2369
+ if (itemId) {
2370
+ this.emittedItemStartedIds.delete(itemId);
2371
+ this.pendingCommandOutputDeltas.delete(itemId);
2372
+ this.pendingFileChangeOutputDeltas.delete(itemId);
2373
+ }
2374
+ return true;
2375
+ }
2376
+ async reconcileTurnCompletionFromThreadRead() {
2377
+ if (!this.client || !this.currentThreadId) {
2378
+ return 0;
2379
+ }
2380
+ const response = (await this.client.request("thread/read", {
2381
+ threadId: this.currentThreadId,
2382
+ includeTurns: true,
2383
+ }));
2384
+ const turns = Array.isArray(response?.thread?.turns) ? response.thread.turns : [];
2385
+ const lastTurn = turns[turns.length - 1];
2386
+ const items = Array.isArray(lastTurn?.items) ? lastTurn.items : [];
2387
+ let emitted = 0;
2388
+ for (const item of items) {
2389
+ if (this.emitCompletedThreadItem(item, "thread_read_completion")) {
2390
+ emitted += 1;
2391
+ }
2392
+ }
2393
+ return emitted;
2394
+ }
2395
+ flushPendingCompletionFallback() {
2396
+ let emitted = 0;
2397
+ for (const [itemId, chunks] of Array.from(this.pendingReasoning.entries())) {
2398
+ const text = chunks.join("");
2399
+ if (text.length === 0) {
2400
+ this.pendingReasoning.delete(itemId);
2401
+ continue;
2275
2402
  }
2403
+ this.emitEvent({
2404
+ type: "timeline",
2405
+ provider: CODEX_PROVIDER,
2406
+ item: { type: "reasoning", text },
2407
+ });
2408
+ this.emittedItemCompletedIds.add(itemId);
2409
+ this.pendingReasoning.delete(itemId);
2410
+ emitted += 1;
2411
+ }
2412
+ for (const [itemId, text] of Array.from(this.pendingAgentMessages.entries())) {
2413
+ if (text.length === 0) {
2414
+ this.pendingAgentMessages.delete(itemId);
2415
+ continue;
2416
+ }
2417
+ this.emitEvent({
2418
+ type: "timeline",
2419
+ provider: CODEX_PROVIDER,
2420
+ item: { type: "assistant_message", text },
2421
+ });
2422
+ this.emittedItemCompletedIds.add(itemId);
2423
+ this.pendingAgentMessages.delete(itemId);
2424
+ emitted += 1;
2425
+ }
2426
+ for (const [itemId, text] of Array.from(this.pendingPlanTexts.entries())) {
2427
+ if (text.trim().length === 0) {
2428
+ this.pendingPlanTexts.delete(itemId);
2429
+ continue;
2430
+ }
2431
+ this.emitEvent({
2432
+ type: "timeline",
2433
+ provider: CODEX_PROVIDER,
2434
+ item: {
2435
+ type: "assistant_message",
2436
+ text: formatProposedPlanChunk("", { close: true }),
2437
+ },
2438
+ });
2439
+ this.emittedItemCompletedIds.add(itemId);
2440
+ this.pendingPlanTexts.delete(itemId);
2441
+ emitted += 1;
2442
+ }
2443
+ return emitted;
2444
+ }
2445
+ emitTerminalTurnEvent(parsed) {
2446
+ if (parsed.status === "failed") {
2447
+ this.emitEvent({
2448
+ type: "turn_failed",
2449
+ provider: CODEX_PROVIDER,
2450
+ error: parsed.errorMessage ?? "Codex turn failed",
2451
+ });
2452
+ return;
2453
+ }
2454
+ if (parsed.status === "interrupted") {
2455
+ this.emitEvent({ type: "turn_canceled", provider: CODEX_PROVIDER, reason: "interrupted" });
2456
+ return;
2457
+ }
2458
+ this.emitEvent({ type: "turn_completed", provider: CODEX_PROVIDER, usage: this.latestUsage });
2459
+ }
2460
+ async finalizeTurnCompletion(parsed) {
2461
+ let terminalEventEmitted = false;
2462
+ try {
2463
+ if (parsed.status === "completed" && this.hasPendingBufferedCompletionContent()) {
2464
+ this.logger.trace({
2465
+ pendingAgentMessages: this.pendingAgentMessages.size,
2466
+ pendingReasoning: this.pendingReasoning.size,
2467
+ pendingPlanTexts: this.pendingPlanTexts.size,
2468
+ threadId: this.currentThreadId,
2469
+ turnId: this.currentTurnId,
2470
+ }, "Codex turn completed with buffered items still pending; reconciling before closing stream");
2471
+ try {
2472
+ const emitted = await this.reconcileTurnCompletionFromThreadRead();
2473
+ this.logger.trace({ emitted, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Reconciled Codex turn completion from thread/read");
2474
+ }
2475
+ catch (error) {
2476
+ this.logger.warn({ error, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Failed to reconcile Codex turn completion from thread/read");
2477
+ }
2478
+ const fallbackEmitted = this.flushPendingCompletionFallback();
2479
+ if (fallbackEmitted > 0) {
2480
+ this.logger.warn({ fallbackEmitted, threadId: this.currentThreadId, turnId: this.currentTurnId }, "Flushed buffered Codex completion items without canonical item/completed notifications");
2481
+ }
2482
+ }
2483
+ this.emitTerminalTurnEvent(parsed);
2484
+ terminalEventEmitted = true;
2485
+ }
2486
+ finally {
2487
+ if (!terminalEventEmitted) {
2488
+ 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");
2489
+ this.emitTerminalTurnEvent(parsed);
2490
+ }
2491
+ this.clearTurnState();
2492
+ this.eventQueue?.end();
2276
2493
  }
2277
- this.eventQueue?.push(event);
2278
2494
  }
2279
2495
  handleNotification(method, params) {
2280
2496
  const parsed = CodexNotificationSchema.parse({ method, params });
@@ -2284,40 +2500,17 @@ class CodexAppServerAgentSession {
2284
2500
  return;
2285
2501
  }
2286
2502
  if (parsed.kind === "turn_started") {
2503
+ this.clearTurnState();
2287
2504
  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
2505
  this.emitEvent({ type: "turn_started", provider: CODEX_PROVIDER });
2296
2506
  return;
2297
2507
  }
2298
2508
  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
- });
2305
- }
2306
- else if (parsed.status === "interrupted") {
2307
- this.emitEvent({ type: "turn_canceled", provider: CODEX_PROVIDER, reason: "interrupted" });
2509
+ if (this.turnCompletionInFlight) {
2510
+ return;
2308
2511
  }
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();
2512
+ this.turnCompletionInFlight = true;
2513
+ void this.finalizeTurnCompletion(parsed);
2321
2514
  return;
2322
2515
  }
2323
2516
  if (parsed.kind === "plan_updated") {
@@ -2473,67 +2666,7 @@ class CodexAppServerAgentSession {
2473
2666
  if (parsed.source === "codex_event") {
2474
2667
  return;
2475
2668
  }
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
- }
2669
+ this.emitCompletedThreadItem(parsed.item, "item_completed");
2537
2670
  return;
2538
2671
  }
2539
2672
  if (parsed.kind === "item_started") {