factory-factory 0.3.4 → 0.3.5

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 (150) hide show
  1. package/dist/client/assets/{_basePickBy-kS1IrKFQ.js → _basePickBy-DOa44aLB.js} +1 -1
  2. package/dist/client/assets/{_baseUniq-DisTPj-I.js → _baseUniq-DuVP6l01.js} +1 -1
  3. package/dist/client/assets/{arc-CWpVTRns.js → arc-BRTVE1a5.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-VXUJARFQ-Dp1AYPMt.js → architectureDiagram-VXUJARFQ-DphOng2K.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-VD42YOAC-DXpmez0y.js → blockDiagram-VD42YOAC-DvVRHTty.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-YG6GDRKO-C6sDxR49.js → c4Diagram-YG6GDRKO-BZaLUCEP.js} +1 -1
  7. package/dist/client/assets/channel-v55vPUhK.js +1 -0
  8. package/dist/client/assets/{chunk-4BX2VUAB-CQxlRbYu.js → chunk-4BX2VUAB-n69YHsoN.js} +1 -1
  9. package/dist/client/assets/{chunk-55IACEB6-DxubFDep.js → chunk-55IACEB6-C67Ak1Qy.js} +1 -1
  10. package/dist/client/assets/{chunk-B4BG7PRW-BtTQ0im9.js → chunk-B4BG7PRW-_Ja9GXA4.js} +1 -1
  11. package/dist/client/assets/{chunk-DI55MBZ5-C2tNYDZj.js → chunk-DI55MBZ5-C1TAOocX.js} +1 -1
  12. package/dist/client/assets/{chunk-FMBD7UC4-kRQRN6vP.js → chunk-FMBD7UC4-Co8OmD7c.js} +1 -1
  13. package/dist/client/assets/{chunk-QN33PNHL-DP72lCp6.js → chunk-QN33PNHL-ESdvapXF.js} +1 -1
  14. package/dist/client/assets/{chunk-QZHKN3VN-BfxMFZDN.js → chunk-QZHKN3VN-DQTGFT4I.js} +1 -1
  15. package/dist/client/assets/{chunk-TZMSLE5B-G3wPktxK.js → chunk-TZMSLE5B-C7a0BTNr.js} +1 -1
  16. package/dist/client/assets/classDiagram-2ON5EDUG-DV-E9odE.js +1 -0
  17. package/dist/client/assets/classDiagram-v2-WZHVMYZB-DV-E9odE.js +1 -0
  18. package/dist/client/assets/clone-DQgzPMba.js +1 -0
  19. package/dist/client/assets/{cose-bilkent-S5V4N54A-QPsUxyAe.js → cose-bilkent-S5V4N54A-Dg_Elpr2.js} +1 -1
  20. package/dist/client/assets/{dagre-6UL2VRFP-BAmsmcnI.js → dagre-6UL2VRFP-DC_BntL_.js} +1 -1
  21. package/dist/client/assets/{diagram-PSM6KHXK-QA-3O0wr.js → diagram-PSM6KHXK-D4EnEadG.js} +1 -1
  22. package/dist/client/assets/{diagram-QEK2KX5R-VZkRJLXU.js → diagram-QEK2KX5R-DeDgxkkd.js} +1 -1
  23. package/dist/client/assets/{diagram-S2PKOQOG-DwLmogjW.js → diagram-S2PKOQOG-DqCrW2Mz.js} +1 -1
  24. package/dist/client/assets/{erDiagram-Q2GNP2WA-DMKIVzp4.js → erDiagram-Q2GNP2WA-CoIzOJMx.js} +1 -1
  25. package/dist/client/assets/{flowDiagram-NV44I4VS-op3xA_nS.js → flowDiagram-NV44I4VS-H-hzyVxN.js} +1 -1
  26. package/dist/client/assets/{ganttDiagram-JELNMOA3-nrnzVCJK.js → ganttDiagram-JELNMOA3-BZbu1Bvf.js} +1 -1
  27. package/dist/client/assets/{gitGraphDiagram-NY62KEGX-CK2ytvyZ.js → gitGraphDiagram-NY62KEGX-yHNzOwte.js} +1 -1
  28. package/dist/client/assets/{graph-D_lEcPMk.js → graph-0fFVLhuM.js} +1 -1
  29. package/dist/client/assets/index-CzQJ6qnR.css +1 -0
  30. package/dist/client/assets/{index-C2qHRaOH.js → index-D2gG7VAV.js} +249 -251
  31. package/dist/client/assets/{infoDiagram-WHAUD3N6-DO5X8HkW.js → infoDiagram-WHAUD3N6-BdYcSIzA.js} +1 -1
  32. package/dist/client/assets/{journeyDiagram-XKPGCS4Q-Bnhe4U1x.js → journeyDiagram-XKPGCS4Q-BgD0iboh.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-3W4ZIXB7-DpTaTXko.js → kanban-definition-3W4ZIXB7-2cCK2J0j.js} +1 -1
  34. package/dist/client/assets/{layout-HT5qr--Q.js → layout-UQByNmge.js} +1 -1
  35. package/dist/client/assets/{linear-CDmQ8HG8.js → linear-COsW-oY4.js} +1 -1
  36. package/dist/client/assets/{mindmap-definition-VGOIOE7T-zDr7s4o2.js → mindmap-definition-VGOIOE7T-CnhVGcBr.js} +1 -1
  37. package/dist/client/assets/{pieDiagram-ADFJNKIX-BVBJCvYL.js → pieDiagram-ADFJNKIX-DX0wt7fJ.js} +1 -1
  38. package/dist/client/assets/{quadrantDiagram-AYHSOK5B-DzdJzkAJ.js → quadrantDiagram-AYHSOK5B-nfJepDyE.js} +1 -1
  39. package/dist/client/assets/{requirementDiagram-UZGBJVZJ-ChrLzx8S.js → requirementDiagram-UZGBJVZJ-I_tnp42L.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-TZEHDZUN-GIzxIQQB.js → sankeyDiagram-TZEHDZUN-4r_mPLW_.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-WL72ISMW-BHxEpSCU.js → sequenceDiagram-WL72ISMW-DWymCFZa.js} +1 -1
  42. package/dist/client/assets/{stateDiagram-FKZM4ZOC-ByvdYZKl.js → stateDiagram-FKZM4ZOC-CCHlcmzO.js} +1 -1
  43. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-CuSoER-j.js +1 -0
  44. package/dist/client/assets/{terminal-instance-xdECz0dx.js → terminal-instance-DKydBe-u.js} +1 -1
  45. package/dist/client/assets/{timeline-definition-IT6M3QCI-BcCmIxix.js → timeline-definition-IT6M3QCI-EkYjHzU_.js} +1 -1
  46. package/dist/client/assets/{treemap-KMMF4GRG-CVpLLzhu.js → treemap-KMMF4GRG-BH9wTzrS.js} +1 -1
  47. package/dist/client/assets/{xychartDiagram-PRI3JC2R-DrgKBPUM.js → xychartDiagram-PRI3JC2R-DwOnTNwE.js} +1 -1
  48. package/dist/client/index.html +2 -2
  49. package/dist/prompts/quick-actions/screenshots.md +12 -6
  50. package/dist/prompts/ratchet/dispatch.md +4 -0
  51. package/dist/src/backend/domains/ratchet/bridges.d.ts +5 -1
  52. package/dist/src/backend/domains/ratchet/bridges.d.ts.map +1 -1
  53. package/dist/src/backend/domains/ratchet/ratchet.service.d.ts.map +1 -1
  54. package/dist/src/backend/domains/ratchet/ratchet.service.js +27 -51
  55. package/dist/src/backend/domains/ratchet/ratchet.service.js.map +1 -1
  56. package/dist/src/backend/domains/ratchet/ratchet.service.test.js +18 -18
  57. package/dist/src/backend/domains/ratchet/ratchet.service.test.js.map +1 -1
  58. package/dist/src/backend/domains/session/acp/acp-runtime-manager.d.ts.map +1 -1
  59. package/dist/src/backend/domains/session/acp/acp-runtime-manager.js +5 -1
  60. package/dist/src/backend/domains/session/acp/acp-runtime-manager.js.map +1 -1
  61. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.d.ts +13 -2
  62. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.d.ts.map +1 -1
  63. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.js +808 -106
  64. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.js.map +1 -1
  65. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.test.js +627 -8
  66. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.test.js.map +1 -1
  67. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.d.ts +20 -0
  68. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.d.ts.map +1 -1
  69. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.js +16 -0
  70. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.js.map +1 -1
  71. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.test.js +15 -0
  72. package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.test.js.map +1 -1
  73. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.d.ts +2 -2
  74. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.d.ts.map +1 -1
  75. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.js +32 -1
  76. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.js.map +1 -1
  77. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.test.js +79 -9
  78. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.test.js.map +1 -1
  79. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.d.ts +2 -2
  80. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.d.ts.map +1 -1
  81. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.js +2 -1
  82. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.js.map +1 -1
  83. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.test.js +7 -1
  84. package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.test.js.map +1 -1
  85. package/dist/src/backend/domains/session/chat/chat-message-handlers/registry.js +2 -2
  86. package/dist/src/backend/domains/session/chat/chat-message-handlers/registry.js.map +1 -1
  87. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.d.ts +1 -1
  88. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.d.ts.map +1 -1
  89. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.js +19 -10
  90. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.js.map +1 -1
  91. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.test.js +43 -0
  92. package/dist/src/backend/domains/session/chat/chat-message-handlers.service.test.js.map +1 -1
  93. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.d.ts.map +1 -1
  94. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.js +23 -59
  95. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.js.map +1 -1
  96. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.test.js +8 -21
  97. package/dist/src/backend/domains/session/data/codex-session-history-loader.service.test.js.map +1 -1
  98. package/dist/src/backend/domains/session/lifecycle/session.service.d.ts +7 -0
  99. package/dist/src/backend/domains/session/lifecycle/session.service.d.ts.map +1 -1
  100. package/dist/src/backend/domains/session/lifecycle/session.service.js +105 -6
  101. package/dist/src/backend/domains/session/lifecycle/session.service.js.map +1 -1
  102. package/dist/src/backend/domains/session/lifecycle/session.service.test.js +84 -0
  103. package/dist/src/backend/domains/session/lifecycle/session.service.test.js.map +1 -1
  104. package/dist/src/backend/domains/session/session-domain.service.d.ts +6 -0
  105. package/dist/src/backend/domains/session/session-domain.service.d.ts.map +1 -1
  106. package/dist/src/backend/domains/session/session-domain.service.js +26 -1
  107. package/dist/src/backend/domains/session/session-domain.service.js.map +1 -1
  108. package/dist/src/backend/domains/session/session-domain.service.test.js +21 -0
  109. package/dist/src/backend/domains/session/session-domain.service.test.js.map +1 -1
  110. package/dist/src/backend/domains/session/store/session-store.types.d.ts +1 -1
  111. package/dist/src/backend/domains/session/store/session-store.types.d.ts.map +1 -1
  112. package/dist/src/backend/domains/session/store/session-transcript.d.ts +1 -0
  113. package/dist/src/backend/domains/session/store/session-transcript.d.ts.map +1 -1
  114. package/dist/src/backend/domains/session/store/session-transcript.js +8 -0
  115. package/dist/src/backend/domains/session/store/session-transcript.js.map +1 -1
  116. package/dist/src/backend/interceptors/pr-detection.interceptor.d.ts.map +1 -1
  117. package/dist/src/backend/interceptors/pr-detection.interceptor.js +5 -1
  118. package/dist/src/backend/interceptors/pr-detection.interceptor.js.map +1 -1
  119. package/dist/src/backend/interceptors/pr-detection.interceptor.test.js +14 -0
  120. package/dist/src/backend/interceptors/pr-detection.interceptor.test.js.map +1 -1
  121. package/dist/src/backend/orchestration/cli-health.service.d.ts +1 -0
  122. package/dist/src/backend/orchestration/cli-health.service.d.ts.map +1 -1
  123. package/dist/src/backend/orchestration/cli-health.service.js +16 -1
  124. package/dist/src/backend/orchestration/cli-health.service.js.map +1 -1
  125. package/dist/src/backend/orchestration/cli-health.service.test.js +23 -0
  126. package/dist/src/backend/orchestration/cli-health.service.test.js.map +1 -1
  127. package/dist/src/backend/prompts/ratchet-dispatch.d.ts +8 -1
  128. package/dist/src/backend/prompts/ratchet-dispatch.d.ts.map +1 -1
  129. package/dist/src/backend/prompts/ratchet-dispatch.js +20 -3
  130. package/dist/src/backend/prompts/ratchet-dispatch.js.map +1 -1
  131. package/dist/src/backend/services/constants.d.ts +1 -0
  132. package/dist/src/backend/services/constants.d.ts.map +1 -1
  133. package/dist/src/backend/services/constants.js +1 -0
  134. package/dist/src/backend/services/constants.js.map +1 -1
  135. package/dist/src/backend/trpc/index.d.ts +1 -0
  136. package/dist/src/backend/trpc/index.d.ts.map +1 -1
  137. package/dist/src/backend/trpc/session.trpc.d.ts +1 -0
  138. package/dist/src/backend/trpc/session.trpc.d.ts.map +1 -1
  139. package/dist/src/backend/trpc/session.trpc.js +5 -1
  140. package/dist/src/backend/trpc/session.trpc.js.map +1 -1
  141. package/dist/tsconfig.backend.tsbuildinfo +1 -1
  142. package/package.json +1 -1
  143. package/prompts/quick-actions/screenshots.md +12 -6
  144. package/prompts/ratchet/dispatch.md +4 -0
  145. package/dist/client/assets/channel-D5BCeybW.js +0 -1
  146. package/dist/client/assets/classDiagram-2ON5EDUG-BUS1JDwr.js +0 -1
  147. package/dist/client/assets/classDiagram-v2-WZHVMYZB-BUS1JDwr.js +0 -1
  148. package/dist/client/assets/clone-C5xhPZw8.js +0 -1
  149. package/dist/client/assets/index-D5ywoSOH.css +0 -1
  150. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-ynQtAOKH.js +0 -1
@@ -6,6 +6,24 @@ const PENDING_TURN_ID = '__pending_turn__';
6
6
  const MAX_CLOSE_WATCHER_ATTACH_RETRIES = 50;
7
7
  const DEFAULT_APPROVAL_POLICIES = ['on-failure', 'on-request', 'never'];
8
8
  const DEFAULT_SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'];
9
+ const SHAPE_DRIFT_DETAILS_LIMIT = 700;
10
+ function toTurnItemKey(turnId, itemId) {
11
+ return `${turnId}:${itemId}`;
12
+ }
13
+ function toShapeDriftDetails(value) {
14
+ try {
15
+ const text = JSON.stringify(value);
16
+ if (typeof text !== 'string') {
17
+ return '[unserializable]';
18
+ }
19
+ return text.length > SHAPE_DRIFT_DETAILS_LIMIT
20
+ ? `${text.slice(0, SHAPE_DRIFT_DETAILS_LIMIT)}...`
21
+ : text;
22
+ }
23
+ catch {
24
+ return '[unserializable]';
25
+ }
26
+ }
9
27
  function isNonEmptyString(value) {
10
28
  return typeof value === 'string' && value.trim().length > 0;
11
29
  }
@@ -107,8 +125,14 @@ function toSessionId(threadId) {
107
125
  function toThreadId(sessionId) {
108
126
  return sessionId.startsWith('sess_') ? sessionId.slice('sess_'.length) : sessionId;
109
127
  }
110
- function createToolCallId(threadId, turnId, itemId) {
111
- return `codex:${threadId}:${turnId}:${itemId}`;
128
+ function extractToolCallIdFromUnknown(value) {
129
+ if (!isRecord(value)) {
130
+ return null;
131
+ }
132
+ return asString(value.callId) ?? asString(value.call_id) ?? null;
133
+ }
134
+ function resolveToolCallId(params) {
135
+ return extractToolCallIdFromUnknown(params.source) ?? params.itemId;
112
136
  }
113
137
  function isPlanLikeMode(mode) {
114
138
  return /plan/i.test(mode);
@@ -123,6 +147,16 @@ const PLAN_TEXT_PREFERRED_KEYS = [
123
147
  'value',
124
148
  'message',
125
149
  ];
150
+ const REASONING_TEXT_MAX_DEPTH = 8;
151
+ const REASONING_TEXT_PREFERRED_KEYS = [
152
+ 'summary',
153
+ 'summaryText',
154
+ 'text',
155
+ 'delta',
156
+ 'message',
157
+ 'content',
158
+ 'reasoning',
159
+ ];
126
160
  function extractFirstPlanText(values, depth) {
127
161
  for (const entry of values) {
128
162
  const extracted = extractPlanTextLocal(entry, depth + 1);
@@ -155,6 +189,37 @@ function extractPlanTextLocal(value, depth = 0) {
155
189
  }
156
190
  return extractPlanTextFromRecord(value, depth);
157
191
  }
192
+ function collectReasoningText(values, value, depth = 0) {
193
+ if (depth > REASONING_TEXT_MAX_DEPTH) {
194
+ return;
195
+ }
196
+ if (typeof value === 'string') {
197
+ if (value.trim().length > 0) {
198
+ values.push(value);
199
+ }
200
+ return;
201
+ }
202
+ if (Array.isArray(value)) {
203
+ for (const entry of value) {
204
+ collectReasoningText(values, entry, depth + 1);
205
+ }
206
+ return;
207
+ }
208
+ if (!isRecord(value)) {
209
+ return;
210
+ }
211
+ for (const key of REASONING_TEXT_PREFERRED_KEYS) {
212
+ collectReasoningText(values, value[key], depth + 1);
213
+ }
214
+ }
215
+ function extractReasoningTextLocal(value) {
216
+ const collected = [];
217
+ collectReasoningText(collected, value);
218
+ if (collected.length === 0) {
219
+ return null;
220
+ }
221
+ return dedupeStrings(collected).join('\n\n');
222
+ }
158
223
  function toMcpEnvRecord(envVars) {
159
224
  const env = {};
160
225
  for (const entry of envVars) {
@@ -293,6 +358,307 @@ function parseToolUserInputAnswersFromPermissionMeta(params) {
293
358
  function asString(value) {
294
359
  return typeof value === 'string' ? value : null;
295
360
  }
361
+ function trimOptionalQuotes(token) {
362
+ if (token.length >= 2 && token.startsWith('"') && token.endsWith('"')) {
363
+ return { text: token.slice(1, -1), quote: '"' };
364
+ }
365
+ if (token.length >= 2 && token.startsWith("'") && token.endsWith("'")) {
366
+ return { text: token.slice(1, -1), quote: "'" };
367
+ }
368
+ return { text: token, quote: null };
369
+ }
370
+ function unescapeCommandToken(token, quote) {
371
+ if (quote === "'") {
372
+ return token.replace(/\\'/g, "'").replace(/\\\\/g, '\\');
373
+ }
374
+ return token.replace(/\\(["'\\\s|&;])/g, '$1');
375
+ }
376
+ function sanitizeCommandToken(token) {
377
+ const trimmed = token.trim();
378
+ const { text, quote } = trimOptionalQuotes(trimmed);
379
+ return unescapeCommandToken(text, quote).trim();
380
+ }
381
+ function tokenizeCommand(command) {
382
+ return (command.match(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+/g) ?? [])
383
+ .map((token) => sanitizeCommandToken(token))
384
+ .filter((token) => token.length > 0);
385
+ }
386
+ function commandName(token) {
387
+ const unix = token.split('/').at(-1) ?? token;
388
+ return (unix.split('\\').at(-1) ?? unix).toLowerCase();
389
+ }
390
+ function isChainSeparatorChar(char) {
391
+ return char === ';' || char === '\n' || char === '|' || char === '&';
392
+ }
393
+ function readChainSeparatorLength(command, index) {
394
+ const char = command[index];
395
+ if (!(char && isChainSeparatorChar(char))) {
396
+ return 0;
397
+ }
398
+ const next = command[index + 1];
399
+ if ((char === '|' || char === '&') && next === char) {
400
+ return 2;
401
+ }
402
+ return 1;
403
+ }
404
+ function isLikelyPathToken(token) {
405
+ if (!token || token.startsWith('-')) {
406
+ return false;
407
+ }
408
+ if (token.startsWith('./') ||
409
+ token.startsWith('../') ||
410
+ token.startsWith('/') ||
411
+ token.startsWith('~')) {
412
+ return true;
413
+ }
414
+ return (token.includes('/') ||
415
+ token.includes('\\') ||
416
+ token.endsWith('.') ||
417
+ /\.[a-z0-9]+$/i.test(token));
418
+ }
419
+ function normalizeLocationPath(pathToken, cwd) {
420
+ if (pathToken.startsWith('/')) {
421
+ return pathToken;
422
+ }
423
+ if (pathToken.startsWith('~')) {
424
+ return pathToken;
425
+ }
426
+ const base = cwd.endsWith('/') ? cwd.slice(0, -1) : cwd;
427
+ return `${base}/${pathToken}`;
428
+ }
429
+ const READ_COMMANDS = new Set(['cat', 'head', 'tail', 'less', 'more']);
430
+ const LIST_OR_FIND_COMMANDS = new Set(['ls', 'tree', 'find', 'fd']);
431
+ const GREP_LIKE_COMMANDS = new Set(['rg', 'ripgrep', 'grep', 'ag']);
432
+ const SHELL_META_COMMANDS = new Set(['cd', 'export', 'set', 'unset', 'alias', 'unalias', 'source']);
433
+ function pushCommandChainPart(state) {
434
+ const trimmed = state.current.trim();
435
+ if (trimmed.length > 0) {
436
+ state.parts.push(trimmed);
437
+ }
438
+ state.current = '';
439
+ }
440
+ function consumeEscapedCommandChar(state, char) {
441
+ if (!state.escaped) {
442
+ return false;
443
+ }
444
+ state.current += char;
445
+ state.escaped = false;
446
+ return true;
447
+ }
448
+ function consumeEscapeInitiator(state, char) {
449
+ if (!(char === '\\' && state.quote !== "'")) {
450
+ return false;
451
+ }
452
+ state.current += char;
453
+ state.escaped = true;
454
+ return true;
455
+ }
456
+ function hasOddTrailingBackslashes(value) {
457
+ let count = 0;
458
+ for (let index = value.length - 1; index >= 0; index -= 1) {
459
+ if (value[index] !== '\\') {
460
+ break;
461
+ }
462
+ count += 1;
463
+ }
464
+ return count % 2 === 1;
465
+ }
466
+ function consumeQuotedCommandChar(state, char) {
467
+ if (!state.quote) {
468
+ return false;
469
+ }
470
+ const escapedSingleQuote = state.quote === "'" && char === "'" && hasOddTrailingBackslashes(state.current);
471
+ state.current += char;
472
+ if (char === state.quote && !escapedSingleQuote) {
473
+ state.quote = null;
474
+ }
475
+ return true;
476
+ }
477
+ function consumeQuoteStart(state, char) {
478
+ if (!(char === '"' || char === "'")) {
479
+ return false;
480
+ }
481
+ state.quote = char;
482
+ state.current += char;
483
+ return true;
484
+ }
485
+ function consumeCommandChainSeparator(state, command, index) {
486
+ const separatorLength = readChainSeparatorLength(command, index);
487
+ if (separatorLength <= 0) {
488
+ return 0;
489
+ }
490
+ pushCommandChainPart(state);
491
+ return separatorLength;
492
+ }
493
+ function splitCommandChain(command) {
494
+ const state = {
495
+ parts: [],
496
+ current: '',
497
+ quote: null,
498
+ escaped: false,
499
+ };
500
+ for (let index = 0; index < command.length; index += 1) {
501
+ const char = command[index];
502
+ if (!char) {
503
+ continue;
504
+ }
505
+ if (consumeEscapedCommandChar(state, char)) {
506
+ continue;
507
+ }
508
+ if (consumeEscapeInitiator(state, char)) {
509
+ continue;
510
+ }
511
+ if (consumeQuotedCommandChar(state, char)) {
512
+ continue;
513
+ }
514
+ if (consumeQuoteStart(state, char)) {
515
+ continue;
516
+ }
517
+ const consumedSeparatorLength = consumeCommandChainSeparator(state, command, index);
518
+ if (consumedSeparatorLength > 0) {
519
+ index += consumedSeparatorLength - 1;
520
+ continue;
521
+ }
522
+ state.current += char;
523
+ }
524
+ pushCommandChainPart(state);
525
+ return state.parts;
526
+ }
527
+ function buildCommandDisplayContexts(rawCommand, cwd) {
528
+ const command = rawCommand?.trim();
529
+ if (!command) {
530
+ return [];
531
+ }
532
+ const contexts = [];
533
+ const segments = splitCommandChain(command);
534
+ for (const segment of segments) {
535
+ const tokens = tokenizeCommand(segment);
536
+ if (tokens.length === 0) {
537
+ continue;
538
+ }
539
+ const [firstToken] = tokens;
540
+ if (!firstToken) {
541
+ continue;
542
+ }
543
+ const firstCommand = commandName(firstToken);
544
+ const nonFlagArgs = tokens.slice(1).filter((token) => !token.startsWith('-'));
545
+ const pathArg = nonFlagArgs.find(isLikelyPathToken) ?? null;
546
+ const commandPath = pathArg ? normalizeLocationPath(pathArg, cwd) : null;
547
+ const locations = commandPath ? [{ path: commandPath }] : [];
548
+ contexts.push({
549
+ command: segment,
550
+ firstCommand,
551
+ nonFlagArgs,
552
+ pathArg,
553
+ cwd,
554
+ locations,
555
+ isShellMeta: SHELL_META_COMMANDS.has(firstCommand),
556
+ });
557
+ }
558
+ return contexts;
559
+ }
560
+ function chooseActionableCommandContexts(contexts) {
561
+ const actionable = contexts.filter((context) => !context.isShellMeta);
562
+ return actionable.length > 0 ? actionable : contexts;
563
+ }
564
+ function selectCombinedCommandKind(displays) {
565
+ const firstNonExecute = displays.find((display) => display.kind !== 'execute');
566
+ return firstNonExecute?.kind ?? 'execute';
567
+ }
568
+ function resolveCommandDisplay(params) {
569
+ const contexts = chooseActionableCommandContexts(buildCommandDisplayContexts(params.command, params.cwd));
570
+ if (contexts.length === 0) {
571
+ return { title: 'commandExecution', kind: 'execute', locations: [] };
572
+ }
573
+ const [singleContext] = contexts;
574
+ if (singleContext && contexts.length === 1) {
575
+ return resolveCommandDisplayFromContext(singleContext);
576
+ }
577
+ const displays = contexts.map((context) => resolveCommandDisplayFromContext(context));
578
+ const title = dedupeStrings(displays.map((display) => display.title)).join(', ');
579
+ const kind = selectCombinedCommandKind(displays);
580
+ const locations = dedupeLocations(displays.flatMap((display) => display.locations)).map((location) => ({ path: location.path }));
581
+ return { title, kind, locations };
582
+ }
583
+ function resolveCommandDisplayFromContext(context) {
584
+ if (READ_COMMANDS.has(context.firstCommand)) {
585
+ const label = context.pathArg ?? context.command;
586
+ return { title: `Read ${label}`, kind: 'read', locations: context.locations };
587
+ }
588
+ if (LIST_OR_FIND_COMMANDS.has(context.firstCommand)) {
589
+ const target = context.pathArg
590
+ ? normalizeLocationPath(context.pathArg, context.cwd)
591
+ : context.cwd;
592
+ const title = context.firstCommand === 'find' || context.firstCommand === 'fd'
593
+ ? `Search ${target}`
594
+ : `List ${target}`;
595
+ return { title, kind: 'search', locations: context.locations };
596
+ }
597
+ if (GREP_LIKE_COMMANDS.has(context.firstCommand)) {
598
+ const query = context.nonFlagArgs.find((token) => !isLikelyPathToken(token));
599
+ const target = context.pathArg ? ` in ${context.pathArg}` : '';
600
+ const title = query ? `Search ${query}${target}` : `Search ${context.command}`;
601
+ return { title, kind: 'search', locations: context.locations };
602
+ }
603
+ return { title: context.command, kind: 'execute', locations: context.locations };
604
+ }
605
+ function normalizeCwdForScope(cwd) {
606
+ if (cwd.length > 1 && cwd.endsWith('/')) {
607
+ return cwd.slice(0, -1);
608
+ }
609
+ return cwd;
610
+ }
611
+ function normalizeScopeToken(token) {
612
+ return token.replace(/\s+/g, ' ').trim();
613
+ }
614
+ function buildCommandApprovalScopeKey(params) {
615
+ const contexts = buildCommandDisplayContexts(params.command, params.cwd);
616
+ if (contexts.length === 0) {
617
+ return null;
618
+ }
619
+ let scopeCwd = normalizeCwdForScope(params.cwd);
620
+ const segments = [];
621
+ for (const context of contexts) {
622
+ if (context.firstCommand === 'cd') {
623
+ const rawTarget = context.nonFlagArgs[0];
624
+ const target = rawTarget ? normalizeScopeToken(rawTarget) : '';
625
+ if (target.length > 0) {
626
+ scopeCwd = normalizeCwdForScope(normalizeLocationPath(target, scopeCwd));
627
+ segments.push(`cd ${target} -> ${scopeCwd}`);
628
+ }
629
+ else {
630
+ segments.push('cd');
631
+ }
632
+ continue;
633
+ }
634
+ const args = context.nonFlagArgs.map(normalizeScopeToken).filter((arg) => arg.length > 0);
635
+ const normalized = normalizeScopeToken([context.firstCommand, ...args].join(' '));
636
+ if (normalized.length === 0) {
637
+ continue;
638
+ }
639
+ segments.push(`[cwd=${scopeCwd}] ${normalized}`);
640
+ }
641
+ const normalizedSegments = segments
642
+ .map(normalizeScopeToken)
643
+ .filter((segment) => segment.length > 0);
644
+ if (normalizedSegments.length === 0) {
645
+ return null;
646
+ }
647
+ return `cwd=${normalizeCwdForScope(params.cwd)}|${normalizedSegments.join(' && ')}`;
648
+ }
649
+ function dedupeLocations(locations) {
650
+ const seen = new Set();
651
+ const result = [];
652
+ for (const location of locations) {
653
+ const key = `${location.path}:${location.line ?? ''}`;
654
+ if (seen.has(key)) {
655
+ continue;
656
+ }
657
+ seen.add(key);
658
+ result.push(location);
659
+ }
660
+ return result;
661
+ }
296
662
  function parseTextFromPromptBlock(block) {
297
663
  if (block.type === 'text') {
298
664
  return block.text;
@@ -430,6 +796,7 @@ export class CodexAppServerAcpAdapter {
430
796
  collaborationModes = [];
431
797
  mcpServersByThreadId = new Map();
432
798
  appliedMcpServerConfigJson = '{}';
799
+ shapeDriftCounts = new Map();
433
800
  constructor(connection, codexClient) {
434
801
  this.connection = connection;
435
802
  this.codex =
@@ -542,10 +909,13 @@ export class CodexAppServerAcpAdapter {
542
909
  },
543
910
  activeTurn: null,
544
911
  toolCallsByItemId: new Map(),
912
+ reasoningDeltaItemIds: new Set(),
545
913
  planTextByItemId: new Map(),
546
914
  planApprovalRequestedByTurnId: new Set(),
547
915
  pendingPlanApprovalsByTurnId: new Map(),
548
916
  pendingTurnCompletionsByTurnId: new Map(),
917
+ commandApprovalScopes: new Set(),
918
+ replayedTurnItemKeys: new Set(),
549
919
  };
550
920
  this.sessions.set(sessionId, session);
551
921
  this.sessionIdByThreadId.set(session.threadId, sessionId);
@@ -587,10 +957,13 @@ export class CodexAppServerAcpAdapter {
587
957
  },
588
958
  activeTurn: null,
589
959
  toolCallsByItemId: new Map(),
960
+ reasoningDeltaItemIds: new Set(),
590
961
  planTextByItemId: new Map(),
591
962
  planApprovalRequestedByTurnId: new Set(),
592
963
  pendingPlanApprovalsByTurnId: new Map(),
593
964
  pendingTurnCompletionsByTurnId: new Map(),
965
+ commandApprovalScopes: new Set(),
966
+ replayedTurnItemKeys: new Set(),
594
967
  };
595
968
  this.sessions.set(session.sessionId, session);
596
969
  this.sessionIdByThreadId.set(session.threadId, session.sessionId);
@@ -1202,8 +1575,27 @@ export class CodexAppServerAcpAdapter {
1202
1575
  });
1203
1576
  }
1204
1577
  async replayToolLikeHistoryItem(session, sessionId, turnId, item) {
1578
+ if (item.type === 'reasoning') {
1579
+ await this.emitSessionUpdate(sessionId, {
1580
+ sessionUpdate: 'tool_call',
1581
+ toolCallId: resolveToolCallId({ itemId: item.id, source: item }),
1582
+ title: 'reasoning',
1583
+ kind: 'think',
1584
+ status: 'completed',
1585
+ rawInput: item,
1586
+ rawOutput: item,
1587
+ });
1588
+ await this.emitReasoningThoughtChunkFromItem(sessionId, item);
1589
+ session.replayedTurnItemKeys.add(toTurnItemKey(turnId, item.id));
1590
+ return;
1591
+ }
1205
1592
  const toolInfo = this.buildToolCallState(session, item, turnId);
1206
1593
  if (!toolInfo) {
1594
+ this.reportShapeDrift('unhandled_replay_item', {
1595
+ turnId,
1596
+ itemType: item.type,
1597
+ itemId: item.id,
1598
+ });
1207
1599
  return;
1208
1600
  }
1209
1601
  await this.emitSessionUpdate(sessionId, {
@@ -1215,11 +1607,15 @@ export class CodexAppServerAcpAdapter {
1215
1607
  rawInput: item,
1216
1608
  rawOutput: item,
1217
1609
  });
1610
+ session.replayedTurnItemKeys.add(toTurnItemKey(turnId, item.id));
1218
1611
  }
1219
1612
  async handleCodexNotification(method, params) {
1220
1613
  const parsed = knownCodexNotificationSchema.safeParse({ method, params });
1221
1614
  if (!parsed.success) {
1222
- process.stderr.write(`[codex-app-server-acp] dropped malformed notification: ${method}\n`);
1615
+ this.reportShapeDrift('malformed_notification', {
1616
+ method,
1617
+ issues: parsed.error.issues.slice(0, 3).map((issue) => issue.message),
1618
+ });
1223
1619
  return;
1224
1620
  }
1225
1621
  const notification = parsed.data;
@@ -1248,16 +1644,20 @@ export class CodexAppServerAcpAdapter {
1248
1644
  await this.handlePlanDelta(sessionId, session, notification.params.itemId, notification.params.delta);
1249
1645
  return;
1250
1646
  }
1647
+ if (notification.method === 'item/reasoning/summaryTextDelta') {
1648
+ await this.emitReasoningThoughtDelta(sessionId, session, notification.params.turnId, notification.params.itemId, notification.params.delta);
1649
+ return;
1650
+ }
1251
1651
  if (notification.method === 'item/commandExecution/outputDelta') {
1252
- await this.emitToolCallProgress(sessionId, session, notification.params.itemId, notification.params.delta);
1652
+ await this.emitToolCallProgress(sessionId, session, notification.params.turnId, notification.params.itemId, notification.params.delta);
1253
1653
  return;
1254
1654
  }
1255
1655
  if (notification.method === 'item/fileChange/outputDelta') {
1256
- await this.emitToolCallProgress(sessionId, session, notification.params.itemId, notification.params.delta);
1656
+ await this.emitToolCallProgress(sessionId, session, notification.params.turnId, notification.params.itemId, notification.params.delta);
1257
1657
  return;
1258
1658
  }
1259
1659
  if (notification.method === 'item/mcpToolCall/progress') {
1260
- await this.emitToolCallProgress(sessionId, session, notification.params.itemId, notification.params.message);
1660
+ await this.emitToolCallProgress(sessionId, session, notification.params.turnId, notification.params.itemId, notification.params.message);
1261
1661
  return;
1262
1662
  }
1263
1663
  if (notification.method === 'item/started') {
@@ -1287,15 +1687,76 @@ export class CodexAppServerAcpAdapter {
1287
1687
  ],
1288
1688
  });
1289
1689
  }
1290
- async emitToolCallProgress(sessionId, session, itemId, output) {
1690
+ async emitReasoningThoughtChunkFromItem(sessionId, item) {
1691
+ const text = extractReasoningTextLocal(item);
1692
+ if (!text) {
1693
+ return;
1694
+ }
1695
+ await this.emitSessionUpdate(sessionId, {
1696
+ sessionUpdate: 'agent_thought_chunk',
1697
+ content: { type: 'text', text },
1698
+ });
1699
+ }
1700
+ isReplayedTurnItem(session, turnId, itemId) {
1701
+ return session.replayedTurnItemKeys.has(toTurnItemKey(turnId, itemId));
1702
+ }
1703
+ async emitReasoningThoughtDelta(sessionId, session, turnId, itemId, delta) {
1704
+ if (this.isReplayedTurnItem(session, turnId, itemId)) {
1705
+ return;
1706
+ }
1707
+ if (delta.length === 0) {
1708
+ return;
1709
+ }
1710
+ const existing = session.toolCallsByItemId.get(itemId);
1711
+ if (!existing) {
1712
+ const toolCallId = resolveToolCallId({
1713
+ itemId,
1714
+ });
1715
+ session.toolCallsByItemId.set(itemId, {
1716
+ toolCallId,
1717
+ title: 'reasoning',
1718
+ kind: 'think',
1719
+ locations: [],
1720
+ });
1721
+ await this.emitSessionUpdate(sessionId, {
1722
+ sessionUpdate: 'tool_call',
1723
+ toolCallId,
1724
+ title: 'reasoning',
1725
+ kind: 'think',
1726
+ status: 'pending',
1727
+ rawInput: {
1728
+ type: 'reasoning',
1729
+ id: itemId,
1730
+ },
1731
+ });
1732
+ await this.emitSessionUpdate(sessionId, {
1733
+ sessionUpdate: 'tool_call_update',
1734
+ toolCallId,
1735
+ title: 'reasoning',
1736
+ kind: 'think',
1737
+ status: 'in_progress',
1738
+ });
1739
+ }
1740
+ session.reasoningDeltaItemIds.add(itemId);
1741
+ await this.emitSessionUpdate(sessionId, {
1742
+ sessionUpdate: 'agent_thought_chunk',
1743
+ content: { type: 'text', text: delta },
1744
+ });
1745
+ }
1746
+ async emitToolCallProgress(sessionId, session, turnId, itemId, output) {
1747
+ if (this.isReplayedTurnItem(session, turnId, itemId)) {
1748
+ return;
1749
+ }
1291
1750
  const toolCall = session.toolCallsByItemId.get(itemId);
1292
1751
  if (!toolCall) {
1752
+ this.reportShapeDrift('tool_progress_without_tool_call', { turnId, itemId });
1293
1753
  return;
1294
1754
  }
1295
1755
  await this.emitSessionUpdate(sessionId, {
1296
1756
  sessionUpdate: 'tool_call_update',
1297
1757
  toolCallId: toolCall.toolCallId,
1298
1758
  status: 'in_progress',
1759
+ ...(toolCall.locations.length > 0 ? { locations: toolCall.locations } : {}),
1299
1760
  rawOutput: output,
1300
1761
  });
1301
1762
  }
@@ -1338,8 +1799,48 @@ export class CodexAppServerAcpAdapter {
1338
1799
  this.settleTurn(session, session.activeTurn?.cancelRequested ? 'cancelled' : 'end_turn');
1339
1800
  }
1340
1801
  async handleItemStarted(session, item, turnId) {
1802
+ if (this.isReplayedTurnItem(session, turnId, item.id)) {
1803
+ return;
1804
+ }
1805
+ if (item.type === 'reasoning') {
1806
+ const existing = session.toolCallsByItemId.get(item.id);
1807
+ const toolInfo = existing ??
1808
+ {
1809
+ toolCallId: resolveToolCallId({
1810
+ itemId: item.id,
1811
+ source: item,
1812
+ }),
1813
+ title: 'reasoning',
1814
+ kind: 'think',
1815
+ locations: [],
1816
+ };
1817
+ if (!existing) {
1818
+ session.toolCallsByItemId.set(item.id, toolInfo);
1819
+ await this.emitSessionUpdate(session.sessionId, {
1820
+ sessionUpdate: 'tool_call',
1821
+ toolCallId: toolInfo.toolCallId,
1822
+ title: toolInfo.title,
1823
+ kind: toolInfo.kind,
1824
+ status: 'pending',
1825
+ rawInput: item,
1826
+ });
1827
+ }
1828
+ await this.emitSessionUpdate(session.sessionId, {
1829
+ sessionUpdate: 'tool_call_update',
1830
+ toolCallId: toolInfo.toolCallId,
1831
+ title: toolInfo.title,
1832
+ kind: toolInfo.kind,
1833
+ status: 'in_progress',
1834
+ });
1835
+ return;
1836
+ }
1341
1837
  const toolInfo = this.buildToolCallState(session, item, turnId);
1342
1838
  if (!toolInfo) {
1839
+ this.reportShapeDrift('unhandled_item_started_type', {
1840
+ turnId,
1841
+ itemType: item.type,
1842
+ itemId: item.id,
1843
+ });
1343
1844
  return;
1344
1845
  }
1345
1846
  session.toolCallsByItemId.set(item.id, toolInfo);
@@ -1349,6 +1850,7 @@ export class CodexAppServerAcpAdapter {
1349
1850
  title: toolInfo.title,
1350
1851
  kind: toolInfo.kind,
1351
1852
  status: 'pending',
1853
+ ...(toolInfo.locations.length > 0 ? { locations: toolInfo.locations } : {}),
1352
1854
  rawInput: item,
1353
1855
  });
1354
1856
  const itemStatus = toToolStatus(item.status);
@@ -1361,8 +1863,52 @@ export class CodexAppServerAcpAdapter {
1361
1863
  }
1362
1864
  }
1363
1865
  async handleItemCompleted(session, item, turnId) {
1866
+ if (this.isReplayedTurnItem(session, turnId, item.id)) {
1867
+ return;
1868
+ }
1869
+ if (item.type === 'reasoning') {
1870
+ const existing = session.toolCallsByItemId.get(item.id);
1871
+ const reasoningToolCallId = existing?.toolCallId ??
1872
+ resolveToolCallId({
1873
+ itemId: item.id,
1874
+ source: item,
1875
+ });
1876
+ if (existing) {
1877
+ await this.emitSessionUpdate(session.sessionId, {
1878
+ sessionUpdate: 'tool_call_update',
1879
+ toolCallId: reasoningToolCallId,
1880
+ title: existing.title,
1881
+ kind: existing.kind,
1882
+ status: toToolStatus(item.status) ?? 'completed',
1883
+ rawOutput: item,
1884
+ });
1885
+ }
1886
+ else {
1887
+ await this.emitSessionUpdate(session.sessionId, {
1888
+ sessionUpdate: 'tool_call',
1889
+ toolCallId: reasoningToolCallId,
1890
+ title: 'reasoning',
1891
+ kind: 'think',
1892
+ status: toToolStatus(item.status) ?? 'completed',
1893
+ rawInput: item,
1894
+ rawOutput: item,
1895
+ });
1896
+ }
1897
+ session.toolCallsByItemId.delete(item.id);
1898
+ const sawDelta = session.reasoningDeltaItemIds.has(item.id);
1899
+ session.reasoningDeltaItemIds.delete(item.id);
1900
+ if (!sawDelta) {
1901
+ await this.emitReasoningThoughtChunkFromItem(session.sessionId, item);
1902
+ }
1903
+ return;
1904
+ }
1364
1905
  const existing = session.toolCallsByItemId.get(item.id);
1365
1906
  if (!existing) {
1907
+ this.reportShapeDrift('item_completed_without_started_state', {
1908
+ turnId,
1909
+ itemType: item.type,
1910
+ itemId: item.id,
1911
+ });
1366
1912
  return;
1367
1913
  }
1368
1914
  const shouldHoldTurnForPlanApproval = item.type === 'plan' &&
@@ -1374,7 +1920,8 @@ export class CodexAppServerAcpAdapter {
1374
1920
  }
1375
1921
  const statusFromItem = toToolStatus(item.status);
1376
1922
  const status = statusFromItem ?? 'completed';
1377
- const locations = this.extractLocations(item);
1923
+ const completionLocations = this.extractLocations(item);
1924
+ const locations = dedupeLocations([...existing.locations, ...completionLocations]);
1378
1925
  await this.emitSessionUpdate(session.sessionId, {
1379
1926
  sessionUpdate: 'tool_call_update',
1380
1927
  toolCallId: existing.toolCallId,
@@ -1411,28 +1958,43 @@ export class CodexAppServerAcpAdapter {
1411
1958
  }
1412
1959
  return null;
1413
1960
  }
1414
- isPlanApprovalSelected(optionId) {
1415
- if (!optionId) {
1416
- return false;
1961
+ getPlanExitModePriority(modeId) {
1962
+ const normalized = modeId.toLowerCase();
1963
+ const index = PLAN_EXIT_MODE_PREFERENCE.findIndex((preferred) => preferred.toLowerCase() === normalized);
1964
+ return index >= 0 ? index : PLAN_EXIT_MODE_PREFERENCE.length;
1965
+ }
1966
+ comparePlanExitModePreference(left, right) {
1967
+ const priorityDiff = this.getPlanExitModePriority(left) - this.getPlanExitModePriority(right);
1968
+ if (priorityDiff !== 0) {
1969
+ return priorityDiff;
1417
1970
  }
1418
- return optionId.startsWith('allow') || optionId === 'default';
1971
+ return left.localeCompare(right);
1419
1972
  }
1420
- resolvePostPlanApprovalMode(currentMode) {
1421
- if (!isPlanLikeMode(currentMode)) {
1422
- return null;
1973
+ resolveCollaborationModeLabel(modeId) {
1974
+ const entry = this.collaborationModes.find((candidate) => candidate.mode === modeId);
1975
+ if (entry?.name) {
1976
+ return entry.name;
1423
1977
  }
1978
+ return sanitizeModeName(modeId);
1979
+ }
1980
+ buildPlanApprovalOptions(session) {
1981
+ const currentMode = session.defaults.collaborationMode;
1424
1982
  const availableModes = this.getCollaborationModeValues(currentMode);
1425
- for (const preferred of PLAN_EXIT_MODE_PREFERENCE) {
1426
- const match = availableModes.find((mode) => mode.toLowerCase() === preferred.toLowerCase());
1427
- if (match) {
1428
- return match;
1429
- }
1430
- }
1431
- const firstNonPlan = availableModes.find((mode) => !isPlanLikeMode(mode));
1432
- if (firstNonPlan) {
1433
- return firstNonPlan;
1434
- }
1435
- return currentMode.toLowerCase() === 'default' ? null : 'default';
1983
+ const nonPlanModes = availableModes
1984
+ .filter((modeId) => !isPlanLikeMode(modeId))
1985
+ .sort((left, right) => this.comparePlanExitModePreference(left, right));
1986
+ const approvableModeIds = new Set(nonPlanModes);
1987
+ const options = nonPlanModes.map((modeId) => ({
1988
+ optionId: modeId,
1989
+ name: `Approve and switch to ${this.resolveCollaborationModeLabel(modeId)}`,
1990
+ kind: 'allow_once',
1991
+ }));
1992
+ options.push({
1993
+ optionId: currentMode,
1994
+ name: 'Keep planning',
1995
+ kind: 'reject_once',
1996
+ });
1997
+ return { options, approvableModeIds };
1436
1998
  }
1437
1999
  holdTurnUntilPlanApprovalResolves(session, turnId) {
1438
2000
  if (this.hasPendingPlanApprovals(session, turnId)) {
@@ -1494,6 +2056,18 @@ export class CodexAppServerAcpAdapter {
1494
2056
  rawInput: approvalInput,
1495
2057
  });
1496
2058
  try {
2059
+ const { options: planApprovalOptions, approvableModeIds } = this.buildPlanApprovalOptions(session);
2060
+ if (approvableModeIds.size === 0) {
2061
+ await this.emitSessionUpdate(session.sessionId, {
2062
+ sessionUpdate: 'tool_call_update',
2063
+ toolCallId: approvalToolCallId,
2064
+ kind: 'switch_mode',
2065
+ title: 'ExitPlanMode',
2066
+ status: 'failed',
2067
+ rawOutput: 'Plan proposed, but no non-plan collaboration mode is available.',
2068
+ });
2069
+ return;
2070
+ }
1497
2071
  const permissionResult = await this.connection.requestPermission({
1498
2072
  sessionId: session.sessionId,
1499
2073
  toolCall: {
@@ -1503,30 +2077,19 @@ export class CodexAppServerAcpAdapter {
1503
2077
  status: 'pending',
1504
2078
  rawInput: approvalInput,
1505
2079
  },
1506
- options: [
1507
- {
1508
- optionId: 'allow_once',
1509
- name: 'Approve plan',
1510
- kind: 'allow_once',
1511
- },
1512
- {
1513
- optionId: 'reject_once',
1514
- name: 'Request changes',
1515
- kind: 'reject_once',
1516
- },
1517
- ],
2080
+ options: planApprovalOptions,
1518
2081
  });
1519
- const selectedOptionId = permissionResult.outcome.outcome === 'selected' ? permissionResult.outcome.optionId : null;
1520
- const approved = this.isPlanApprovalSelected(selectedOptionId);
1521
- if (approved) {
1522
- const postApprovalMode = this.resolvePostPlanApprovalMode(session.defaults.collaborationMode);
1523
- if (postApprovalMode) {
1524
- session.defaults.collaborationMode = postApprovalMode;
1525
- await this.emitSessionUpdate(session.sessionId, {
1526
- sessionUpdate: 'config_option_update',
1527
- configOptions: this.buildConfigOptions(session),
1528
- });
1529
- }
2082
+ const selectedMode = permissionResult.outcome.outcome === 'selected' &&
2083
+ approvableModeIds.has(permissionResult.outcome.optionId)
2084
+ ? permissionResult.outcome.optionId
2085
+ : null;
2086
+ const approved = selectedMode !== null;
2087
+ if (selectedMode && session.defaults.collaborationMode !== selectedMode) {
2088
+ session.defaults.collaborationMode = selectedMode;
2089
+ await this.emitSessionUpdate(session.sessionId, {
2090
+ sessionUpdate: 'config_option_update',
2091
+ configOptions: this.buildConfigOptions(session),
2092
+ });
1530
2093
  }
1531
2094
  await this.emitSessionUpdate(session.sessionId, {
1532
2095
  sessionUpdate: 'tool_call_update',
@@ -1573,26 +2136,32 @@ export class CodexAppServerAcpAdapter {
1573
2136
  }
1574
2137
  return locations;
1575
2138
  }
1576
- buildToolCallState(session, item, turnId) {
2139
+ buildToolCallState(session, item, _turnId) {
1577
2140
  const kindByType = {
1578
2141
  commandExecution: 'execute',
1579
2142
  fileChange: 'edit',
1580
2143
  mcpToolCall: 'fetch',
1581
2144
  webSearch: 'search',
1582
2145
  plan: 'think',
1583
- reasoning: 'think',
1584
2146
  };
1585
2147
  const kind = kindByType[item.type];
1586
2148
  if (!kind) {
1587
2149
  return null;
1588
2150
  }
1589
2151
  let title = item.type;
2152
+ let kindToEmit = kind;
2153
+ let locations = [];
1590
2154
  if (item.type === 'commandExecution') {
1591
2155
  const command = asString(item.command);
1592
- title = command ?? 'commandExecution';
2156
+ const cwd = asString(item.cwd) ?? session.cwd;
2157
+ const parsed = resolveCommandDisplay({ command, cwd });
2158
+ title = parsed.title;
2159
+ kindToEmit = parsed.kind;
2160
+ locations = parsed.locations;
1593
2161
  }
1594
2162
  else if (item.type === 'fileChange') {
1595
2163
  title = 'fileChange';
2164
+ locations = this.extractLocations(item);
1596
2165
  }
1597
2166
  else if (item.type === 'mcpToolCall') {
1598
2167
  const server = asString(item.server) ?? 'mcp';
@@ -1604,15 +2173,81 @@ export class CodexAppServerAcpAdapter {
1604
2173
  title = query ? `webSearch:${query}` : 'webSearch';
1605
2174
  }
1606
2175
  return {
1607
- toolCallId: createToolCallId(session.threadId, turnId, item.id),
1608
- kind,
2176
+ toolCallId: resolveToolCallId({
2177
+ itemId: item.id,
2178
+ source: item,
2179
+ }),
2180
+ kind: kindToEmit,
1609
2181
  title,
2182
+ locations,
1610
2183
  };
1611
2184
  }
2185
+ buildPermissionOptions(params) {
2186
+ if (params.method === 'item/tool/requestUserInput') {
2187
+ return buildToolUserInputPermissionOptions(params.questions);
2188
+ }
2189
+ if (params.method === 'item/commandExecution/requestApproval') {
2190
+ return [
2191
+ {
2192
+ optionId: 'allow_always',
2193
+ name: 'Allow for session',
2194
+ kind: 'allow_always',
2195
+ },
2196
+ {
2197
+ optionId: 'allow_once',
2198
+ name: 'Allow once',
2199
+ kind: 'allow_once',
2200
+ },
2201
+ {
2202
+ optionId: 'reject_once',
2203
+ name: 'Reject',
2204
+ kind: 'reject_once',
2205
+ },
2206
+ ];
2207
+ }
2208
+ return [
2209
+ {
2210
+ optionId: 'allow_once',
2211
+ name: 'Allow once',
2212
+ kind: 'allow_once',
2213
+ },
2214
+ {
2215
+ optionId: 'reject_once',
2216
+ name: 'Reject',
2217
+ kind: 'reject_once',
2218
+ },
2219
+ ];
2220
+ }
2221
+ async maybeAutoApproveCommandRequest(params) {
2222
+ if (params.request.method !== 'item/commandExecution/requestApproval') {
2223
+ return false;
2224
+ }
2225
+ const scopeKey = buildCommandApprovalScopeKey({
2226
+ command: asString(params.request.params.command),
2227
+ cwd: asString(params.request.params.cwd) ?? params.session.cwd,
2228
+ });
2229
+ if (!(scopeKey && params.session.commandApprovalScopes.has(scopeKey))) {
2230
+ return false;
2231
+ }
2232
+ await this.respondToCodexPermissionRequest({
2233
+ request: params.request,
2234
+ permission: {
2235
+ outcome: { outcome: 'selected', optionId: 'allow_always' },
2236
+ },
2237
+ session: params.session,
2238
+ toolCallId: params.toolCallId,
2239
+ questions: [],
2240
+ });
2241
+ return true;
2242
+ }
1612
2243
  async handleCodexServerRequest(request) {
1613
2244
  try {
1614
2245
  const parsed = knownCodexServerRequestSchema.safeParse(request);
1615
2246
  if (!parsed.success) {
2247
+ this.reportShapeDrift('malformed_server_request', {
2248
+ method: request.method,
2249
+ issues: parsed.error.issues.slice(0, 3).map((issue) => issue.message),
2250
+ });
1616
2251
  this.codex.respondError(request.id, {
1617
2252
  code: -32_602,
1618
2253
  message: 'Unsupported codex server request payload',
@@ -1622,6 +2257,11 @@ export class CodexAppServerAcpAdapter {
1622
2257
  const typed = parsed.data;
1623
2258
  const sessionId = this.sessionIdByThreadId.get(typed.params.threadId);
1624
2259
  if (!sessionId) {
2260
+ this.reportShapeDrift('request_for_unknown_thread', {
2261
+ method: typed.method,
2262
+ threadId: typed.params.threadId,
2263
+ itemId: typed.params.itemId,
2264
+ });
1625
2265
  this.codex.respondError(typed.id, {
1626
2266
  code: -32_603,
1627
2267
  message: 'No ACP session mapped for this thread',
@@ -1630,6 +2270,11 @@ export class CodexAppServerAcpAdapter {
1630
2270
  }
1631
2271
  const session = this.sessions.get(sessionId);
1632
2272
  if (!session) {
2273
+ this.reportShapeDrift('request_for_missing_session', {
2274
+ method: typed.method,
2275
+ sessionId,
2276
+ itemId: typed.params.itemId,
2277
+ });
1633
2278
  this.codex.respondError(typed.id, {
1634
2279
  code: -32_603,
1635
2280
  message: 'No ACP session mapped for this thread',
@@ -1647,24 +2292,30 @@ export class CodexAppServerAcpAdapter {
1647
2292
  title: toolCall.title,
1648
2293
  kind: toolCall.kind,
1649
2294
  status: 'pending',
2295
+ ...(toolCall.locations.length > 0 ? { locations: toolCall.locations } : {}),
1650
2296
  rawInput: typed.params,
1651
2297
  });
1652
2298
  }
2299
+ if (await this.maybeAutoApproveCommandRequest({
2300
+ request: typed,
2301
+ session,
2302
+ toolCallId: toolCall.toolCallId,
2303
+ })) {
2304
+ return;
2305
+ }
1653
2306
  const questions = typed.method === 'item/tool/requestUserInput' ? typed.params.questions : [];
1654
- const permissionOptions = typed.method === 'item/tool/requestUserInput'
1655
- ? buildToolUserInputPermissionOptions(questions)
1656
- : [
1657
- {
1658
- optionId: 'allow_once',
1659
- name: 'Allow once',
1660
- kind: 'allow_once',
1661
- },
1662
- {
1663
- optionId: 'reject_once',
1664
- name: 'Reject',
1665
- kind: 'reject_once',
1666
- },
1667
- ];
2307
+ const permissionOptions = this.buildPermissionOptions({
2308
+ method: typed.method,
2309
+ questions,
2310
+ });
2311
+ await this.emitSessionUpdate(sessionId, {
2312
+ sessionUpdate: 'tool_call_update',
2313
+ toolCallId: toolCall.toolCallId,
2314
+ title: toolCall.title,
2315
+ kind: toolCall.kind,
2316
+ status: 'pending',
2317
+ ...(toolCall.locations.length > 0 ? { locations: toolCall.locations } : {}),
2318
+ });
1668
2319
  const permissionResult = await this.connection.requestPermission({
1669
2320
  sessionId,
1670
2321
  toolCall: {
@@ -1693,67 +2344,110 @@ export class CodexAppServerAcpAdapter {
1693
2344
  });
1694
2345
  }
1695
2346
  }
1696
- buildApprovalToolCallState(session, method, itemId, turnId, params) {
2347
+ buildApprovalToolCallState(session, method, itemId, _turnId, params) {
1697
2348
  if (method === 'item/commandExecution/requestApproval') {
1698
2349
  const command = asString(params.command);
2350
+ const cwd = asString(params.cwd) ?? session.cwd;
2351
+ const parsed = resolveCommandDisplay({ command, cwd });
1699
2352
  return {
1700
- toolCallId: createToolCallId(session.threadId, turnId, itemId),
1701
- title: command ? command : 'commandExecution',
1702
- kind: 'execute',
2353
+ toolCallId: resolveToolCallId({
2354
+ itemId,
2355
+ source: params,
2356
+ }),
2357
+ title: parsed.title,
2358
+ kind: parsed.kind,
2359
+ locations: parsed.locations,
1703
2360
  };
1704
2361
  }
1705
2362
  if (method === 'item/fileChange/requestApproval') {
2363
+ const grantRoot = asString(params.grantRoot);
1706
2364
  return {
1707
- toolCallId: createToolCallId(session.threadId, turnId, itemId),
2365
+ toolCallId: resolveToolCallId({
2366
+ itemId,
2367
+ source: params,
2368
+ }),
1708
2369
  title: 'fileChange',
1709
2370
  kind: 'edit',
2371
+ locations: grantRoot ? [{ path: grantRoot }] : [],
1710
2372
  };
1711
2373
  }
1712
2374
  return {
1713
- toolCallId: createToolCallId(session.threadId, turnId, itemId),
2375
+ toolCallId: resolveToolCallId({
2376
+ itemId,
2377
+ source: params,
2378
+ }),
1714
2379
  title: 'item/tool/requestUserInput',
1715
2380
  kind: 'other',
2381
+ locations: [],
1716
2382
  };
1717
2383
  }
1718
- async respondToCodexPermissionRequest(params) {
1719
- const selected = params.permission.outcome.outcome === 'selected' ? params.permission.outcome.optionId : null;
1720
- const allow = selected === 'allow_once';
1721
- if (params.request.method === 'item/tool/requestUserInput') {
1722
- const rejected = selected === null || selected === 'reject_once';
1723
- let answers;
1724
- try {
1725
- answers = buildToolUserInputAnswers({
1726
- questions: params.questions,
1727
- permission: params.permission,
1728
- selectedOptionId: selected,
1729
- });
1730
- }
1731
- catch (error) {
1732
- const message = error instanceof Error ? error.message : String(error);
1733
- this.codex.respondError(params.request.id, {
1734
- code: -32_602,
1735
- message: 'Failed to map requestUserInput answers',
1736
- data: { error: message },
1737
- });
1738
- await this.emitSessionUpdate(params.session.sessionId, {
1739
- sessionUpdate: 'tool_call_update',
1740
- toolCallId: params.toolCallId,
1741
- status: 'failed',
1742
- rawOutput: { error: message },
1743
- });
1744
- return;
1745
- }
1746
- this.codex.respondSuccess(params.request.id, {
1747
- answers,
2384
+ isAllowedPermissionSelection(selectedOptionId) {
2385
+ return selectedOptionId === 'allow_once' || selectedOptionId === 'allow_always';
2386
+ }
2387
+ async handleToolUserInputPermissionResponse(params) {
2388
+ if (params.request.method !== 'item/tool/requestUserInput') {
2389
+ return false;
2390
+ }
2391
+ const rejected = params.selectedOptionId === null || params.selectedOptionId === 'reject_once';
2392
+ let answers;
2393
+ try {
2394
+ answers = buildToolUserInputAnswers({
2395
+ questions: params.questions,
2396
+ permission: params.permission,
2397
+ selectedOptionId: params.selectedOptionId,
2398
+ });
2399
+ }
2400
+ catch (error) {
2401
+ const message = error instanceof Error ? error.message : String(error);
2402
+ this.codex.respondError(params.request.id, {
2403
+ code: -32_602,
2404
+ message: 'Failed to map requestUserInput answers',
2405
+ data: { error: message },
1748
2406
  });
1749
2407
  await this.emitSessionUpdate(params.session.sessionId, {
1750
2408
  sessionUpdate: 'tool_call_update',
1751
2409
  toolCallId: params.toolCallId,
1752
- status: rejected ? 'failed' : 'completed',
1753
- rawOutput: rejected ? { outcome: 'rejected' } : { answers },
2410
+ status: 'failed',
2411
+ rawOutput: { error: message },
1754
2412
  });
2413
+ return true;
2414
+ }
2415
+ this.codex.respondSuccess(params.request.id, {
2416
+ answers,
2417
+ });
2418
+ await this.emitSessionUpdate(params.session.sessionId, {
2419
+ sessionUpdate: 'tool_call_update',
2420
+ toolCallId: params.toolCallId,
2421
+ status: rejected ? 'failed' : 'completed',
2422
+ rawOutput: rejected ? { outcome: 'rejected' } : { answers },
2423
+ });
2424
+ return true;
2425
+ }
2426
+ async respondToCodexPermissionRequest(params) {
2427
+ const selected = params.permission.outcome.outcome === 'selected' ? params.permission.outcome.optionId : null;
2428
+ if (await this.handleToolUserInputPermissionResponse({
2429
+ ...params,
2430
+ selectedOptionId: selected,
2431
+ })) {
1755
2432
  return;
1756
2433
  }
2434
+ if (params.request.method === 'item/commandExecution/requestApproval' &&
2435
+ selected === 'allow_always') {
2436
+ const scopeKey = buildCommandApprovalScopeKey({
2437
+ command: asString(params.request.params.command),
2438
+ cwd: asString(params.request.params.cwd) ?? params.session.cwd,
2439
+ });
2440
+ if (scopeKey) {
2441
+ params.session.commandApprovalScopes.add(scopeKey);
2442
+ }
2443
+ else {
2444
+ this.reportShapeDrift('allow_always_without_scope_key', {
2445
+ itemId: params.request.params.itemId,
2446
+ command: params.request.params.command,
2447
+ });
2448
+ }
2449
+ }
2450
+ const allow = this.isAllowedPermissionSelection(selected);
1757
2451
  this.codex.respondSuccess(params.request.id, {
1758
2452
  decision: allow ? 'accept' : 'decline',
1759
2453
  });
@@ -1775,8 +2469,16 @@ export class CodexAppServerAcpAdapter {
1775
2469
  session.planApprovalRequestedByTurnId.clear();
1776
2470
  session.pendingPlanApprovalsByTurnId.clear();
1777
2471
  session.toolCallsByItemId.clear();
2472
+ session.reasoningDeltaItemIds.clear();
1778
2473
  session.pendingTurnCompletionsByTurnId.clear();
1779
2474
  }
2475
+ reportShapeDrift(event, details) {
2476
+ const count = (this.shapeDriftCounts.get(event) ?? 0) + 1;
2477
+ this.shapeDriftCounts.set(event, count);
2478
+ const includeDetails = details !== undefined && (count <= 5 || count % 50 === 0);
2479
+ const detailsSuffix = includeDetails ? ` details=${toShapeDriftDetails(details)}` : '';
2480
+ process.stderr.write(`[codex-app-server-acp] shape-drift event=${event} count=${count}${detailsSuffix}\n`);
2481
+ }
1780
2482
  async emitSessionUpdate(sessionId, update) {
1781
2483
  try {
1782
2484
  await this.connection.sessionUpdate({ sessionId, update });