@junctionpanel/server 0.1.28 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/server/client/daemon-client.d.ts +42 -5
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +85 -3
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-sdk-types.d.ts +7 -0
  6. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  8. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +8 -0
  9. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  10. package/dist/server/server/agent/providers/codex-app-server-agent.js +244 -135
  11. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  12. package/dist/server/server/agent/providers/gemini-agent.d.ts +4 -1
  13. package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
  14. package/dist/server/server/agent/providers/gemini-agent.js +36 -8
  15. package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
  16. package/dist/server/server/agent/providers/image-attachments.d.ts +8 -0
  17. package/dist/server/server/agent/providers/image-attachments.d.ts.map +1 -0
  18. package/dist/server/server/agent/providers/image-attachments.js +47 -0
  19. package/dist/server/server/agent/providers/image-attachments.js.map +1 -0
  20. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +3 -0
  21. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  22. package/dist/server/server/daemon-doctor.d.ts +39 -0
  23. package/dist/server/server/daemon-doctor.d.ts.map +1 -0
  24. package/dist/server/server/daemon-doctor.js +260 -0
  25. package/dist/server/server/daemon-doctor.js.map +1 -0
  26. package/dist/server/server/daemon-provider-settings.d.ts +42 -0
  27. package/dist/server/server/daemon-provider-settings.d.ts.map +1 -0
  28. package/dist/server/server/daemon-provider-settings.js +207 -0
  29. package/dist/server/server/daemon-provider-settings.js.map +1 -0
  30. package/dist/server/server/file-explorer/service.d.ts +4 -2
  31. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  32. package/dist/server/server/file-explorer/service.js +104 -2
  33. package/dist/server/server/file-explorer/service.js.map +1 -1
  34. package/dist/server/server/persisted-config.d.ts +24 -24
  35. package/dist/server/server/session.d.ts +10 -1
  36. package/dist/server/server/session.d.ts.map +1 -1
  37. package/dist/server/server/session.js +439 -62
  38. package/dist/server/server/session.js.map +1 -1
  39. package/dist/server/server/worktree-bootstrap.d.ts +1 -0
  40. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
  41. package/dist/server/server/worktree-bootstrap.js +4 -0
  42. package/dist/server/server/worktree-bootstrap.js.map +1 -1
  43. package/dist/server/shared/messages.d.ts +4245 -34
  44. package/dist/server/shared/messages.d.ts.map +1 -1
  45. package/dist/server/shared/messages.js +167 -0
  46. package/dist/server/shared/messages.js.map +1 -1
  47. package/dist/server/utils/checkout-git.d.ts +23 -4
  48. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  49. package/dist/server/utils/checkout-git.js +298 -79
  50. package/dist/server/utils/checkout-git.js.map +1 -1
  51. package/dist/server/utils/directory-suggestions.d.ts +4 -0
  52. package/dist/server/utils/directory-suggestions.d.ts.map +1 -1
  53. package/dist/server/utils/directory-suggestions.js +83 -5
  54. package/dist/server/utils/directory-suggestions.js.map +1 -1
  55. package/dist/server/utils/workspace-ref-files.d.ts +31 -0
  56. package/dist/server/utils/workspace-ref-files.d.ts.map +1 -0
  57. package/dist/server/utils/workspace-ref-files.js +207 -0
  58. package/dist/server/utils/workspace-ref-files.js.map +1 -0
  59. package/dist/server/utils/worktree.d.ts +6 -3
  60. package/dist/server/utils/worktree.d.ts.map +1 -1
  61. package/dist/server/utils/worktree.js +46 -45
  62. package/dist/server/utils/worktree.js.map +1 -1
  63. package/package.json +2 -2
@@ -1,5 +1,4 @@
1
1
  import { execSync, spawn } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
2
  import fs from "node:fs/promises";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
@@ -8,10 +7,10 @@ import { z } from "zod";
8
7
  import { loadCodexPersistedTimeline } from "./codex-rollout-timeline.js";
9
8
  import { mapCodexRolloutToolCall, mapCodexToolCallFromThreadItem, } from "./codex/tool-call-mapper.js";
10
9
  import { applyProviderEnv, isProviderCommandAvailable, resolveProviderCommandPrefix, } from "../provider-launch-config.js";
10
+ import { writeImageAttachment } from "./image-attachments.js";
11
11
  const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
12
12
  const TURN_START_TIMEOUT_MS = 90 * 1000;
13
13
  const CODEX_PROVIDER = "codex";
14
- const CODEX_IMAGE_ATTACHMENT_DIR = "junction-attachments";
15
14
  const CODEX_APP_SERVER_CAPABILITIES = {
16
15
  supportsStreaming: true,
17
16
  supportsSessionPersistence: true,
@@ -219,6 +218,9 @@ function parseUpdatedQuestionAnswers(updatedInput) {
219
218
  }
220
219
  return parsed;
221
220
  }
221
+ function toPendingPermissionId(requestId) {
222
+ return `permission-${String(requestId)}`;
223
+ }
222
224
  async function listCodexCustomPrompts() {
223
225
  const codexHome = resolveCodexHomeDir();
224
226
  const promptsDir = path.join(codexHome, "prompts");
@@ -525,7 +527,7 @@ class CodexAppServerClient {
525
527
  const request = msg;
526
528
  const handler = this.requestHandlers.get(request.method);
527
529
  try {
528
- const result = handler ? await handler(request.params) : {};
530
+ const result = handler ? await handler(request.params, request.id) : {};
529
531
  const response = { id: request.id, result };
530
532
  this.child.stdin.write(`${JSON.stringify(response)}\n`);
531
533
  }
@@ -582,12 +584,73 @@ function parsePlanTextToTodoItems(text) {
582
584
  completed: false,
583
585
  }));
584
586
  }
587
+ function formatProposedPlanBlock(text) {
588
+ return `<proposed_plan>\n${text}\n</proposed_plan>`;
589
+ }
590
+ function formatProposedPlanChunk(text, options) {
591
+ const parts = [];
592
+ if (options?.open) {
593
+ parts.push("<proposed_plan>\n");
594
+ }
595
+ parts.push(text);
596
+ if (options?.close) {
597
+ parts.push("\n</proposed_plan>");
598
+ }
599
+ return parts.join("");
600
+ }
585
601
  function planStepsToTodoItems(steps) {
586
602
  return steps.map((entry) => ({
587
603
  text: entry.step,
588
604
  completed: entry.status === "completed",
589
605
  }));
590
606
  }
607
+ function supportsPlanCollaborationMode(response) {
608
+ const candidateArrays = [
609
+ Array.isArray(response) ? response : null,
610
+ Array.isArray(response?.data)
611
+ ? (response.data)
612
+ : null,
613
+ Array.isArray(response?.modes)
614
+ ? (response.modes)
615
+ : null,
616
+ Array.isArray(response?.collaborationModes)
617
+ ? (response.collaborationModes)
618
+ : null,
619
+ Array.isArray(response?.items)
620
+ ? (response.items)
621
+ : null,
622
+ ];
623
+ for (const entries of candidateArrays) {
624
+ if (!entries)
625
+ continue;
626
+ for (const entry of entries) {
627
+ const record = toRecord(entry);
628
+ const modeName = (typeof record?.mode === "string" ? record.mode : null) ??
629
+ (typeof record?.name === "string" ? record.name : null) ??
630
+ (typeof record?.id === "string" ? record.id : null) ??
631
+ (typeof entry === "string" ? entry : null);
632
+ if (modeName?.trim().toLowerCase() === "plan") {
633
+ return true;
634
+ }
635
+ }
636
+ }
637
+ return false;
638
+ }
639
+ function shouldRetryInitializeWithoutExperimentalApi(error) {
640
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
641
+ return (message.includes("experimentalapi") ||
642
+ message.includes("experimental api") ||
643
+ message.includes("capabilities") ||
644
+ message.includes("unknown field") ||
645
+ message.includes("invalid params"));
646
+ }
647
+ function shouldRetryTurnStartWithoutCollaborationMode(error) {
648
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
649
+ return (message.includes("collaborationmode") ||
650
+ message.includes("collaboration mode") ||
651
+ message.includes("experimentalapi") ||
652
+ message.includes("experimental api"));
653
+ }
591
654
  function extractPatchLikeText(value) {
592
655
  if (!value || typeof value !== "object") {
593
656
  return undefined;
@@ -897,8 +960,11 @@ function threadItemToTimeline(item, options) {
897
960
  }
898
961
  case "plan": {
899
962
  const text = normalizedItem.text ?? "";
963
+ if (typeof text === "string" && text.trim().length > 0) {
964
+ return { type: "assistant_message", text: formatProposedPlanBlock(text) };
965
+ }
900
966
  const items = parsePlanTextToTodoItems(text);
901
- return { type: "todo", items };
967
+ return items.length > 0 ? { type: "todo", items } : null;
902
968
  }
903
969
  case "reasoning": {
904
970
  const summary = Array.isArray(normalizedItem.summary)
@@ -931,33 +997,6 @@ function toSandboxPolicy(type, networkAccess) {
931
997
  return { type: "workspaceWrite", networkAccess: networkAccess ?? false };
932
998
  }
933
999
  }
934
- function getImageExtension(mimeType) {
935
- switch (mimeType) {
936
- case "image/jpeg":
937
- return "jpg";
938
- case "image/png":
939
- return "png";
940
- case "image/webp":
941
- return "webp";
942
- case "image/gif":
943
- return "gif";
944
- case "image/bmp":
945
- return "bmp";
946
- case "image/tiff":
947
- return "tiff";
948
- default:
949
- return "bin";
950
- }
951
- }
952
- function normalizeImageData(mimeType, data) {
953
- if (data.startsWith("data:")) {
954
- const match = data.match(/^data:([^;]+);base64,(.*)$/);
955
- if (match) {
956
- return { mimeType: match[1], data: match[2] };
957
- }
958
- }
959
- return { mimeType, data };
960
- }
961
1000
  const ThreadStartedNotificationSchema = z.object({
962
1001
  thread: z.object({ id: z.string() }).passthrough(),
963
1002
  }).passthrough();
@@ -986,6 +1025,13 @@ const TurnPlanUpdatedNotificationSchema = z.object({
986
1025
  })
987
1026
  .passthrough()),
988
1027
  }).passthrough();
1028
+ const ItemPlanDeltaNotificationSchema = z.object({
1029
+ itemId: z.string(),
1030
+ delta: z.string(),
1031
+ }).passthrough();
1032
+ const ServerRequestResolvedNotificationSchema = z.object({
1033
+ requestId: z.union([z.string(), z.number()]),
1034
+ }).passthrough();
989
1035
  const TurnDiffUpdatedNotificationSchema = z.object({
990
1036
  diff: z.string(),
991
1037
  }).passthrough();
@@ -1125,6 +1171,12 @@ const CodexNotificationSchema = z.union([
1125
1171
  })),
1126
1172
  })),
1127
1173
  z.object({ method: z.literal("turn/plan/updated"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1174
+ z.object({ method: z.literal("item/plan/delta"), params: ItemPlanDeltaNotificationSchema }).transform(({ params }) => ({
1175
+ kind: "plan_delta",
1176
+ itemId: params.itemId,
1177
+ delta: params.delta,
1178
+ })),
1179
+ z.object({ method: z.literal("item/plan/delta"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1128
1180
  z.object({ method: z.literal("turn/diff/updated"), params: TurnDiffUpdatedNotificationSchema }).transform(({ params }) => ({ kind: "diff_updated", diff: params.diff })),
1129
1181
  z.object({ method: z.literal("turn/diff/updated"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1130
1182
  z.object({
@@ -1132,6 +1184,14 @@ const CodexNotificationSchema = z.union([
1132
1184
  params: ThreadTokenUsageUpdatedNotificationSchema,
1133
1185
  }).transform(({ params }) => ({ kind: "token_usage_updated", tokenUsage: params.tokenUsage })),
1134
1186
  z.object({ method: z.literal("thread/tokenUsage/updated"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1187
+ z.object({
1188
+ method: z.literal("serverRequest/resolved"),
1189
+ params: ServerRequestResolvedNotificationSchema,
1190
+ }).transform(({ params }) => ({
1191
+ kind: "server_request_resolved",
1192
+ requestId: String(params.requestId),
1193
+ })),
1194
+ z.object({ method: z.literal("serverRequest/resolved"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1135
1195
  z.object({ method: z.literal("item/agentMessage/delta"), params: ItemTextDeltaNotificationSchema }).transform(({ params }) => ({
1136
1196
  kind: "agent_message_delta",
1137
1197
  itemId: params.itemId,
@@ -1268,16 +1328,6 @@ const CodexNotificationSchema = z.union([
1268
1328
  z.object({ method: z.literal("codex/event/task_complete"), params: z.unknown() }).transform(({ method, params }) => ({ kind: "invalid_payload", method, params })),
1269
1329
  z.object({ method: z.string(), params: z.unknown() }).transform(({ method, params }) => ({ kind: "unknown_method", method, params })),
1270
1330
  ]);
1271
- async function writeImageAttachment(mimeType, data) {
1272
- const attachmentsDir = path.join(os.tmpdir(), CODEX_IMAGE_ATTACHMENT_DIR);
1273
- await fs.mkdir(attachmentsDir, { recursive: true });
1274
- const normalized = normalizeImageData(mimeType, data);
1275
- const extension = getImageExtension(normalized.mimeType);
1276
- const filename = `${randomUUID()}.${extension}`;
1277
- const filePath = path.join(attachmentsDir, filename);
1278
- await fs.writeFile(filePath, Buffer.from(normalized.data, "base64"));
1279
- return filePath;
1280
- }
1281
1331
  async function readCodexConfiguredDefaults(client, logger) {
1282
1332
  let savedConfigDefaults = {};
1283
1333
  try {
@@ -1341,6 +1391,10 @@ export async function codexAppServerTurnInputFromPrompt(prompt, logger) {
1341
1391
  }
1342
1392
  export const __codexAppServerInternals = {
1343
1393
  mapCodexPatchNotificationToToolCall,
1394
+ supportsPlanCollaborationMode,
1395
+ shouldRetryInitializeWithoutExperimentalApi,
1396
+ shouldRetryTurnStartWithoutCollaborationMode,
1397
+ formatProposedPlanBlock,
1344
1398
  };
1345
1399
  class CodexAppServerAgentSession {
1346
1400
  constructor(config, resumeHandle, logger, spawnAppServer) {
@@ -1370,8 +1424,8 @@ class CodexAppServerAgentSession {
1370
1424
  this.warnedInvalidNotificationPayloads = new Set();
1371
1425
  this.warnedIncompleteEditToolCallIds = new Set();
1372
1426
  this.connected = false;
1373
- this.collaborationModes = [];
1374
- this.resolvedCollaborationMode = null;
1427
+ this.nativePlanModeSupported = null;
1428
+ this.pendingPlanTexts = new Map();
1375
1429
  this.cachedSkills = [];
1376
1430
  this.logger = logger.child({ module: "agent", provider: CODEX_PROVIDER });
1377
1431
  if (config.modeId === undefined) {
@@ -1396,13 +1450,26 @@ class CodexAppServerAgentSession {
1396
1450
  this.client = new CodexAppServerClient(child, this.logger);
1397
1451
  this.client.setNotificationHandler((method, params) => this.handleNotification(method, params));
1398
1452
  this.registerRequestHandlers();
1399
- await this.client.request("initialize", {
1400
- clientInfo: {
1401
- name: "junction",
1402
- title: "Junction",
1403
- version: "0.0.0",
1404
- },
1405
- });
1453
+ const clientInfo = {
1454
+ name: "junction",
1455
+ title: "Junction",
1456
+ version: "0.0.0",
1457
+ };
1458
+ try {
1459
+ await this.client.request("initialize", {
1460
+ clientInfo,
1461
+ capabilities: {
1462
+ experimentalApi: true,
1463
+ },
1464
+ });
1465
+ }
1466
+ catch (error) {
1467
+ if (!shouldRetryInitializeWithoutExperimentalApi(error)) {
1468
+ throw error;
1469
+ }
1470
+ await this.client.request("initialize", { clientInfo });
1471
+ this.nativePlanModeSupported = false;
1472
+ }
1406
1473
  this.client.notify("initialized", {});
1407
1474
  await this.loadCollaborationModes();
1408
1475
  await this.loadSkills();
@@ -1415,22 +1482,17 @@ class CodexAppServerAgentSession {
1415
1482
  async loadCollaborationModes() {
1416
1483
  if (!this.client)
1417
1484
  return;
1485
+ if (this.nativePlanModeSupported === false) {
1486
+ return;
1487
+ }
1418
1488
  try {
1419
- const response = (await this.client.request("collaborationMode/list", {}));
1420
- const data = Array.isArray(response?.data) ? response.data : [];
1421
- this.collaborationModes = data.map((entry) => ({
1422
- name: String(entry.name ?? ""),
1423
- mode: entry.mode ?? null,
1424
- model: entry.model ?? null,
1425
- reasoning_effort: entry.reasoning_effort ?? null,
1426
- developer_instructions: entry.developer_instructions ?? null,
1427
- }));
1489
+ const response = await this.client.request("collaborationMode/list", {});
1490
+ this.nativePlanModeSupported = supportsPlanCollaborationMode(response);
1428
1491
  }
1429
1492
  catch (error) {
1430
1493
  this.logger.trace({ error }, "Failed to load collaboration modes");
1431
- this.collaborationModes = [];
1494
+ this.nativePlanModeSupported = false;
1432
1495
  }
1433
- this.resolvedCollaborationMode = this.resolveCollaborationMode(this.currentMode);
1434
1496
  }
1435
1497
  async loadSkills() {
1436
1498
  if (!this.client)
@@ -1460,59 +1522,12 @@ class CodexAppServerAgentSession {
1460
1522
  this.cachedSkills = [];
1461
1523
  }
1462
1524
  }
1463
- resolveCollaborationMode(modeId) {
1464
- if (this.collaborationModes.length === 0)
1465
- return null;
1466
- const normalized = modeId.toLowerCase();
1467
- const findByName = (predicate) => this.collaborationModes.find((entry) => predicate(entry.name.toLowerCase()));
1468
- let match;
1469
- if (normalized === "read-only") {
1470
- // Prefer explicit plan collaboration modes over generic read-only modes.
1471
- match =
1472
- findByName((name) => name.includes("plan")) ??
1473
- findByName((name) => name.includes("read"));
1474
- }
1475
- else if (normalized === "full-access") {
1476
- match = findByName((name) => name.includes("full") || name.includes("exec"));
1477
- }
1478
- else {
1479
- match = findByName((name) => name.includes("auto") || name.includes("code"));
1480
- }
1481
- if (!match) {
1482
- match = this.collaborationModes[0] ?? null;
1483
- }
1484
- if (!match)
1485
- return null;
1486
- const settings = {};
1487
- if (match.model)
1488
- settings.model = match.model;
1489
- if (match.reasoning_effort)
1490
- settings.reasoning_effort = match.reasoning_effort;
1491
- const modeSpecificInstruction = normalized === "read-only"
1492
- ? "Plan mode is enabled. Do not execute commands, edit files, or perform write operations. Provide analysis and a step-by-step plan only."
1493
- : "";
1494
- const developerInstructions = [
1495
- modeSpecificInstruction,
1496
- match.developer_instructions?.trim(),
1497
- this.config.systemPrompt?.trim(),
1498
- ]
1499
- .filter((entry) => typeof entry === "string" && entry.length > 0)
1500
- .join("\n\n");
1501
- if (developerInstructions)
1502
- settings.developer_instructions = developerInstructions;
1503
- if (this.config.model)
1504
- settings.model = this.config.model;
1505
- const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
1506
- if (thinkingOptionId)
1507
- settings.reasoning_effort = thinkingOptionId;
1508
- return { mode: match.mode ?? "code", settings, name: match.name };
1509
- }
1510
1525
  registerRequestHandlers() {
1511
1526
  if (!this.client)
1512
1527
  return;
1513
- this.client.setRequestHandler("item/commandExecution/requestApproval", (params) => this.handleCommandApprovalRequest(params));
1514
- this.client.setRequestHandler("item/fileChange/requestApproval", (params) => this.handleFileChangeApprovalRequest(params));
1515
- this.client.setRequestHandler("tool/requestUserInput", (params) => this.handleToolApprovalRequest(params));
1528
+ this.client.setRequestHandler("item/commandExecution/requestApproval", (params, requestId) => this.handleCommandApprovalRequest(params, requestId));
1529
+ this.client.setRequestHandler("item/fileChange/requestApproval", (params, requestId) => this.handleFileChangeApprovalRequest(params, requestId));
1530
+ this.client.setRequestHandler("tool/requestUserInput", (params, requestId) => this.handleToolApprovalRequest(params, requestId));
1516
1531
  }
1517
1532
  async loadPersistedHistory() {
1518
1533
  if (!this.client || !this.currentThreadId)
@@ -1710,6 +1725,8 @@ class CodexAppServerAgentSession {
1710
1725
  const preset = MODE_PRESETS[this.currentMode] ?? MODE_PRESETS[DEFAULT_CODEX_MODE_ID];
1711
1726
  const approvalPolicy = this.config.approvalPolicy ?? preset.approvalPolicy;
1712
1727
  const sandboxPolicyType = this.config.sandboxMode ?? preset.sandbox;
1728
+ const planModeRequested = options?.extra?.codex?.planMode === true;
1729
+ const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
1713
1730
  const params = {
1714
1731
  threadId: this.currentThreadId,
1715
1732
  input,
@@ -1721,16 +1738,9 @@ class CodexAppServerAgentSession {
1721
1738
  if (this.config.model) {
1722
1739
  params.model = this.config.model;
1723
1740
  }
1724
- const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
1725
1741
  if (thinkingOptionId) {
1726
1742
  params.effort = thinkingOptionId;
1727
1743
  }
1728
- if (this.resolvedCollaborationMode) {
1729
- params.collaborationMode = {
1730
- mode: this.resolvedCollaborationMode.mode,
1731
- settings: this.resolvedCollaborationMode.settings,
1732
- };
1733
- }
1734
1744
  if (this.config.cwd) {
1735
1745
  params.cwd = this.config.cwd;
1736
1746
  }
@@ -1744,7 +1754,38 @@ class CodexAppServerAgentSession {
1744
1754
  if (codexConfig) {
1745
1755
  params.config = codexConfig;
1746
1756
  }
1747
- await this.client.request("turn/start", params, TURN_START_TIMEOUT_MS);
1757
+ let downgradedFromPlanMode = false;
1758
+ if (this.nativePlanModeSupported !== false) {
1759
+ const collaborationMode = this.buildCollaborationModePayload(planModeRequested ? "plan" : "default");
1760
+ if (collaborationMode) {
1761
+ params.collaborationMode = collaborationMode;
1762
+ }
1763
+ }
1764
+ try {
1765
+ await this.client.request("turn/start", params, TURN_START_TIMEOUT_MS);
1766
+ }
1767
+ catch (error) {
1768
+ const canRetryWithoutPlanMode = Object.prototype.hasOwnProperty.call(params, "collaborationMode") &&
1769
+ shouldRetryTurnStartWithoutCollaborationMode(error);
1770
+ if (!canRetryWithoutPlanMode) {
1771
+ throw error;
1772
+ }
1773
+ delete params.collaborationMode;
1774
+ this.nativePlanModeSupported = false;
1775
+ this.cachedRuntimeInfo = null;
1776
+ downgradedFromPlanMode = planModeRequested;
1777
+ await this.client.request("turn/start", params, TURN_START_TIMEOUT_MS);
1778
+ }
1779
+ if (downgradedFromPlanMode) {
1780
+ this.emitEvent({
1781
+ type: "timeline",
1782
+ provider: CODEX_PROVIDER,
1783
+ item: {
1784
+ type: "assistant_message",
1785
+ text: "Plan mode is not supported by this Codex runtime. Sent as a normal turn instead.",
1786
+ },
1787
+ });
1788
+ }
1748
1789
  let sawTurnStarted = false;
1749
1790
  for await (const event of queue) {
1750
1791
  // Drop pre-start timeline noise that can leak from the previous turn.
@@ -1801,9 +1842,9 @@ class CodexAppServerAgentSession {
1801
1842
  model: this.config.model ?? null,
1802
1843
  thinkingOptionId: normalizeCodexThinkingOptionId(this.config.thinkingOptionId) ?? null,
1803
1844
  modeId: this.currentMode ?? null,
1804
- extra: this.resolvedCollaborationMode
1805
- ? { collaborationMode: this.resolvedCollaborationMode.name }
1806
- : undefined,
1845
+ extra: this.nativePlanModeSupported === null
1846
+ ? undefined
1847
+ : { planModeSupported: this.nativePlanModeSupported },
1807
1848
  };
1808
1849
  this.cachedRuntimeInfo = info;
1809
1850
  return { ...info };
@@ -1817,17 +1858,14 @@ class CodexAppServerAgentSession {
1817
1858
  async setMode(modeId) {
1818
1859
  validateCodexMode(modeId);
1819
1860
  this.currentMode = modeId;
1820
- this.resolvedCollaborationMode = this.resolveCollaborationMode(modeId);
1821
1861
  this.cachedRuntimeInfo = null;
1822
1862
  }
1823
1863
  async setModel(modelId) {
1824
1864
  this.config.model = modelId ?? undefined;
1825
- this.resolvedCollaborationMode = this.resolveCollaborationMode(this.currentMode);
1826
1865
  this.cachedRuntimeInfo = null;
1827
1866
  }
1828
1867
  async setThinkingOption(thinkingOptionId) {
1829
1868
  this.config.thinkingOptionId = normalizeCodexThinkingOptionId(thinkingOptionId);
1830
- this.resolvedCollaborationMode = this.resolveCollaborationMode(this.currentMode);
1831
1869
  this.cachedRuntimeInfo = null;
1832
1870
  }
1833
1871
  getPendingPermissions() {
@@ -2057,6 +2095,24 @@ class CodexAppServerAgentSession {
2057
2095
  }
2058
2096
  return Object.keys(innerConfig).length > 0 ? innerConfig : null;
2059
2097
  }
2098
+ buildCollaborationModePayload(mode) {
2099
+ if (this.nativePlanModeSupported !== true) {
2100
+ return null;
2101
+ }
2102
+ const model = normalizeCodexModelId(this.config.model);
2103
+ if (!model) {
2104
+ return null;
2105
+ }
2106
+ const thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId) ?? null;
2107
+ return {
2108
+ mode,
2109
+ settings: {
2110
+ model,
2111
+ reasoning_effort: thinkingOptionId,
2112
+ developer_instructions: null,
2113
+ },
2114
+ };
2115
+ }
2060
2116
  async buildUserInput(prompt) {
2061
2117
  if (typeof prompt === "string") {
2062
2118
  return [{ type: "text", text: prompt }];
@@ -2111,6 +2167,7 @@ class CodexAppServerAgentSession {
2111
2167
  this.emittedExecCommandCompletedCallIds.clear();
2112
2168
  this.pendingCommandOutputDeltas.clear();
2113
2169
  this.pendingFileChangeOutputDeltas.clear();
2170
+ this.pendingPlanTexts.clear();
2114
2171
  this.warnedIncompleteEditToolCallIds.clear();
2115
2172
  this.eventQueue?.end();
2116
2173
  return;
@@ -2127,6 +2184,22 @@ class CodexAppServerAgentSession {
2127
2184
  });
2128
2185
  return;
2129
2186
  }
2187
+ if (parsed.kind === "plan_delta") {
2188
+ const previous = this.pendingPlanTexts.get(parsed.itemId) ?? "";
2189
+ const next = previous + parsed.delta;
2190
+ this.pendingPlanTexts.set(parsed.itemId, next);
2191
+ this.emitEvent({
2192
+ type: "timeline",
2193
+ provider: CODEX_PROVIDER,
2194
+ item: {
2195
+ type: "assistant_message",
2196
+ text: formatProposedPlanChunk(parsed.delta, {
2197
+ open: previous.length === 0,
2198
+ }),
2199
+ },
2200
+ });
2201
+ return;
2202
+ }
2130
2203
  if (parsed.kind === "diff_updated") {
2131
2204
  // NOTE: Codex app-server emits frequent `turn/diff/updated` notifications
2132
2205
  // containing a full accumulated unified diff for the *entire turn*.
@@ -2134,6 +2207,20 @@ class CodexAppServerAgentSession {
2134
2207
  // We intentionally do NOT store every diff update in the timeline.
2135
2208
  return;
2136
2209
  }
2210
+ if (parsed.kind === "server_request_resolved") {
2211
+ const pendingRequestId = toPendingPermissionId(parsed.requestId);
2212
+ if (this.resolvedPermissionRequests.has(pendingRequestId)) {
2213
+ return;
2214
+ }
2215
+ this.pendingPermissions.delete(pendingRequestId);
2216
+ this.emitEvent({
2217
+ type: "permission_resolved",
2218
+ provider: CODEX_PROVIDER,
2219
+ requestId: pendingRequestId,
2220
+ resolution: { behavior: "allow" },
2221
+ });
2222
+ return;
2223
+ }
2137
2224
  if (parsed.kind === "token_usage_updated") {
2138
2225
  this.latestUsage = toAgentUsage(parsed.tokenUsage);
2139
2226
  return;
@@ -2263,6 +2350,24 @@ class CodexAppServerAgentSession {
2263
2350
  timelineItem.text = buffered;
2264
2351
  }
2265
2352
  }
2353
+ if (timelineItem.type === "assistant_message" &&
2354
+ normalizedItemType === "plan" &&
2355
+ itemId) {
2356
+ const bufferedPlanText = this.pendingPlanTexts.get(itemId) ?? "";
2357
+ const finalPlanText = typeof parsed.item.text === "string" ? parsed.item.text : "";
2358
+ if (bufferedPlanText.length > 0) {
2359
+ const trailingText = finalPlanText.startsWith(bufferedPlanText)
2360
+ ? finalPlanText.slice(bufferedPlanText.length)
2361
+ : "";
2362
+ timelineItem.text = formatProposedPlanChunk(trailingText, {
2363
+ close: true,
2364
+ });
2365
+ this.pendingPlanTexts.delete(itemId);
2366
+ }
2367
+ else if (finalPlanText.trim().length > 0) {
2368
+ timelineItem.text = formatProposedPlanBlock(finalPlanText);
2369
+ }
2370
+ }
2266
2371
  if (timelineItem.type === "reasoning" && itemId) {
2267
2372
  const buffered = this.pendingReasoning.get(itemId);
2268
2373
  if (buffered && buffered.length > 0) {
@@ -2278,6 +2383,7 @@ class CodexAppServerAgentSession {
2278
2383
  this.emittedItemStartedIds.delete(itemId);
2279
2384
  this.pendingCommandOutputDeltas.delete(itemId);
2280
2385
  this.pendingFileChangeOutputDeltas.delete(itemId);
2386
+ this.pendingPlanTexts.delete(itemId);
2281
2387
  }
2282
2388
  }
2283
2389
  return;
@@ -2374,7 +2480,7 @@ class CodexAppServerAgentSession {
2374
2480
  payload,
2375
2481
  }, "Codex edit tool call is missing diff/content fields");
2376
2482
  }
2377
- handleCommandApprovalRequest(params) {
2483
+ handleCommandApprovalRequest(params, rawRequestId) {
2378
2484
  const parsed = params;
2379
2485
  const commandPreview = mapCodexExecNotificationToToolCall({
2380
2486
  callId: parsed.itemId,
@@ -2382,7 +2488,7 @@ class CodexAppServerAgentSession {
2382
2488
  cwd: parsed.cwd ?? this.config.cwd ?? null,
2383
2489
  running: true,
2384
2490
  });
2385
- const requestId = `permission-${parsed.itemId}`;
2491
+ const requestId = toPendingPermissionId(rawRequestId ?? parsed.itemId);
2386
2492
  const title = parsed.command ? `Run command: ${parsed.command}` : "Run command";
2387
2493
  const request = {
2388
2494
  id: requestId,
@@ -2415,9 +2521,9 @@ class CodexAppServerAgentSession {
2415
2521
  this.pendingPermissionHandlers.set(requestId, { resolve, kind: "command" });
2416
2522
  });
2417
2523
  }
2418
- handleFileChangeApprovalRequest(params) {
2524
+ handleFileChangeApprovalRequest(params, rawRequestId) {
2419
2525
  const parsed = params;
2420
- const requestId = `permission-${parsed.itemId}`;
2526
+ const requestId = toPendingPermissionId(rawRequestId ?? parsed.itemId);
2421
2527
  const request = {
2422
2528
  id: requestId,
2423
2529
  provider: CODEX_PROVIDER,
@@ -2444,16 +2550,19 @@ class CodexAppServerAgentSession {
2444
2550
  this.pendingPermissionHandlers.set(requestId, { resolve, kind: "file" });
2445
2551
  });
2446
2552
  }
2447
- handleToolApprovalRequest(params) {
2553
+ handleToolApprovalRequest(params, rawRequestId) {
2448
2554
  const parsed = params;
2449
- const requestId = `permission-${parsed.itemId}`;
2555
+ const requestId = toPendingPermissionId(rawRequestId ?? parsed.itemId);
2450
2556
  const request = {
2451
2557
  id: requestId,
2452
2558
  provider: CODEX_PROVIDER,
2453
- name: "CodexTool",
2454
- kind: "tool",
2455
- title: "Tool action requires approval",
2559
+ name: "CodexQuestion",
2560
+ kind: "question",
2561
+ title: "Answer question",
2456
2562
  description: undefined,
2563
+ input: {
2564
+ questions: Array.isArray(parsed.questions) ? parsed.questions : [],
2565
+ },
2457
2566
  detail: {
2458
2567
  type: "unknown",
2459
2568
  input: {
@@ -2473,7 +2582,7 @@ class CodexAppServerAgentSession {
2473
2582
  return new Promise((resolve) => {
2474
2583
  this.pendingPermissionHandlers.set(requestId, {
2475
2584
  resolve,
2476
- kind: "tool",
2585
+ kind: "question",
2477
2586
  questions: Array.isArray(parsed.questions) ? parsed.questions : [],
2478
2587
  });
2479
2588
  });