cli-jaw 2.2.9 → 2.2.11

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 (225) hide show
  1. package/README.ja.md +15 -0
  2. package/README.ko.md +15 -0
  3. package/README.md +19 -4
  4. package/dist/bin/cli-jaw.js +5 -1
  5. package/dist/bin/cli-jaw.js.map +1 -1
  6. package/dist/bin/commands/doctor.js +66 -1
  7. package/dist/bin/commands/doctor.js.map +1 -1
  8. package/dist/bin/commands/init.js +107 -8
  9. package/dist/bin/commands/init.js.map +1 -1
  10. package/dist/bin/commands/slack.js +190 -0
  11. package/dist/bin/commands/slack.js.map +1 -0
  12. package/dist/lib/upload.js +5 -1
  13. package/dist/lib/upload.js.map +1 -1
  14. package/dist/server.js +1 -0
  15. package/dist/server.js.map +1 -1
  16. package/dist/src/agent/alert-escalation.js +1 -1
  17. package/dist/src/agent/alert-escalation.js.map +1 -1
  18. package/dist/src/agent/codex-app-catalog.js +57 -0
  19. package/dist/src/agent/codex-app-catalog.js.map +1 -0
  20. package/dist/src/agent/codex-app-client.js +89 -7
  21. package/dist/src/agent/codex-app-client.js.map +1 -1
  22. package/dist/src/agent/pi-rpc-verdict.js +22 -0
  23. package/dist/src/agent/pi-rpc-verdict.js.map +1 -0
  24. package/dist/src/agent/pi-runtime.js +240 -0
  25. package/dist/src/agent/pi-runtime.js.map +1 -1
  26. package/dist/src/agent/runtime-pool.js +445 -0
  27. package/dist/src/agent/runtime-pool.js.map +1 -0
  28. package/dist/src/agent/spawn.js +395 -240
  29. package/dist/src/agent/spawn.js.map +1 -1
  30. package/dist/src/cli/command-context.js +5 -2
  31. package/dist/src/cli/command-context.js.map +1 -1
  32. package/dist/src/cli/commands.js +43 -39
  33. package/dist/src/cli/commands.js.map +1 -1
  34. package/dist/src/cli/handlers-runtime.js +15 -7
  35. package/dist/src/cli/handlers-runtime.js.map +1 -1
  36. package/dist/src/cli/handlers-search.js +1 -1
  37. package/dist/src/cli/handlers-search.js.map +1 -1
  38. package/dist/src/cli/handlers-skill-invoke.js +1 -1
  39. package/dist/src/cli/handlers-skill-invoke.js.map +1 -1
  40. package/dist/src/cli/handlers-workflows.js +1 -1
  41. package/dist/src/cli/handlers-workflows.js.map +1 -1
  42. package/dist/src/cli/handlers.js +1 -1
  43. package/dist/src/cli/handlers.js.map +1 -1
  44. package/dist/src/cli/opencodex-models.js +40 -7
  45. package/dist/src/cli/opencodex-models.js.map +1 -1
  46. package/dist/src/cli/registry-live.js +6 -5
  47. package/dist/src/cli/registry-live.js.map +1 -1
  48. package/dist/src/command-contract/catalog.js +4 -0
  49. package/dist/src/command-contract/catalog.js.map +1 -1
  50. package/dist/src/core/config.js +69 -4
  51. package/dist/src/core/config.js.map +1 -1
  52. package/dist/src/core/runtime-settings.js +17 -0
  53. package/dist/src/core/runtime-settings.js.map +1 -1
  54. package/dist/src/core/settings-merge.js +1 -1
  55. package/dist/src/core/settings-merge.js.map +1 -1
  56. package/dist/src/discord/bot.js +29 -14
  57. package/dist/src/discord/bot.js.map +1 -1
  58. package/dist/src/discord/commands.js +5 -4
  59. package/dist/src/discord/commands.js.map +1 -1
  60. package/dist/src/discord/discord-file.js +8 -2
  61. package/dist/src/discord/discord-file.js.map +1 -1
  62. package/dist/src/discord/forwarder.js +17 -19
  63. package/dist/src/discord/forwarder.js.map +1 -1
  64. package/dist/src/discord/send-only-client.js +9 -1
  65. package/dist/src/discord/send-only-client.js.map +1 -1
  66. package/dist/src/manager/dashboard-service.js +38 -1
  67. package/dist/src/manager/dashboard-service.js.map +1 -1
  68. package/dist/src/manager/telegram-hub/hub-bot.js +47 -29
  69. package/dist/src/manager/telegram-hub/hub-bot.js.map +1 -1
  70. package/dist/src/messaging/channel-health.js +32 -0
  71. package/dist/src/messaging/channel-health.js.map +1 -1
  72. package/dist/src/messaging/chunk.js +376 -0
  73. package/dist/src/messaging/chunk.js.map +1 -0
  74. package/dist/src/messaging/dedupe.js +104 -0
  75. package/dist/src/messaging/dedupe.js.map +1 -0
  76. package/dist/src/messaging/fold.js +210 -0
  77. package/dist/src/messaging/fold.js.map +1 -0
  78. package/dist/src/messaging/redact.js +577 -0
  79. package/dist/src/messaging/redact.js.map +1 -0
  80. package/dist/src/messaging/retry.js +76 -0
  81. package/dist/src/messaging/retry.js.map +1 -0
  82. package/dist/src/messaging/runtime.js +6 -3
  83. package/dist/src/messaging/runtime.js.map +1 -1
  84. package/dist/src/messaging/send.js +45 -15
  85. package/dist/src/messaging/send.js.map +1 -1
  86. package/dist/src/messaging/slack-target.js +50 -0
  87. package/dist/src/messaging/slack-target.js.map +1 -0
  88. package/dist/src/messaging/types.js.map +1 -1
  89. package/dist/src/orchestrator/pipeline.js +4 -4
  90. package/dist/src/orchestrator/pipeline.js.map +1 -1
  91. package/dist/src/routes/command.js +11 -7
  92. package/dist/src/routes/command.js.map +1 -1
  93. package/dist/src/routes/messaging.js +28 -12
  94. package/dist/src/routes/messaging.js.map +1 -1
  95. package/dist/src/routes/settings.js +22 -6
  96. package/dist/src/routes/settings.js.map +1 -1
  97. package/dist/src/slack/api.js +116 -0
  98. package/dist/src/slack/api.js.map +1 -0
  99. package/dist/src/slack/bot.js +262 -0
  100. package/dist/src/slack/bot.js.map +1 -0
  101. package/dist/src/slack/commands.js +98 -0
  102. package/dist/src/slack/commands.js.map +1 -0
  103. package/dist/src/slack/events.js +116 -0
  104. package/dist/src/slack/events.js.map +1 -0
  105. package/dist/src/slack/format.js +58 -0
  106. package/dist/src/slack/format.js.map +1 -0
  107. package/dist/src/slack/forwarder.js +57 -0
  108. package/dist/src/slack/forwarder.js.map +1 -0
  109. package/dist/src/slack/manifest.js +84 -0
  110. package/dist/src/slack/manifest.js.map +1 -0
  111. package/dist/src/slack/register.js +13 -0
  112. package/dist/src/slack/register.js.map +1 -0
  113. package/dist/src/slack/send-handler.js +41 -0
  114. package/dist/src/slack/send-handler.js.map +1 -0
  115. package/dist/src/slack/send-only-client.js +46 -0
  116. package/dist/src/slack/send-only-client.js.map +1 -0
  117. package/dist/src/slack/slack-file.js +72 -0
  118. package/dist/src/slack/slack-file.js.map +1 -0
  119. package/dist/src/slack/socket.js +341 -0
  120. package/dist/src/slack/socket.js.map +1 -0
  121. package/dist/src/telegram/bot.js +117 -37
  122. package/dist/src/telegram/bot.js.map +1 -1
  123. package/dist/src/telegram/elicitation-buttons.js +7 -2
  124. package/dist/src/telegram/elicitation-buttons.js.map +1 -1
  125. package/dist/src/telegram/forwarder.js +15 -7
  126. package/dist/src/telegram/forwarder.js.map +1 -1
  127. package/dist/src/telegram/rich-message.js +209 -24
  128. package/dist/src/telegram/rich-message.js.map +1 -1
  129. package/dist/src/telegram/telegram-file.js +35 -4
  130. package/dist/src/telegram/telegram-file.js.map +1 -1
  131. package/dist/src/telegram/voice.js +4 -3
  132. package/dist/src/telegram/voice.js.map +1 -1
  133. package/package.json +7 -1
  134. package/public/assets/providers/slack.svg +1 -0
  135. package/public/dist/assets/ChannelsDiscord-D9FLnZso.js +1 -0
  136. package/public/dist/assets/ChannelsSlack-ybvupgdK.js +1 -0
  137. package/public/dist/assets/ChannelsTelegram-BOCOPrBc.js +1 -0
  138. package/public/dist/assets/HealthBadge-megTlF4n.js +1 -0
  139. package/public/dist/assets/TransportStatusChips-CjEySMis.js +1 -0
  140. package/public/dist/assets/app-BGIAy4he.js +55 -0
  141. package/public/dist/assets/{bgtask-badge-CdBqBy1L.js → bgtask-badge-DXca8E7c.js} +1 -1
  142. package/public/dist/assets/bgtask-badge-GRSiNILQ.js +1 -0
  143. package/public/dist/assets/{employees-C8qufeJl.js → employees-BL4qMxJj.js} +1 -1
  144. package/public/dist/assets/iframe-renderer-BUSk0Rw9.js +1 -0
  145. package/public/dist/assets/{iframe-renderer-CZ67aeTO.js → iframe-renderer-DUI0LYBy.js} +2 -2
  146. package/public/dist/assets/{manager-CLXT60M5.js → manager-B-gpf-3j.js} +3 -3
  147. package/public/dist/assets/{memory-lWramLVw.js → memory-B_Y0L0KD.js} +1 -1
  148. package/public/dist/assets/memory-CX6YahTc.js +1 -0
  149. package/public/dist/assets/message-history-Det7u76S.js +1 -0
  150. package/public/dist/assets/{message-history-2NUN3UxM.js → message-history-Dl8dJzez.js} +1 -1
  151. package/public/dist/assets/{provider-icons-stsNmQKx.js → provider-icons-VDfahiUp.js} +3 -2
  152. package/public/dist/assets/{render-CsF-ojLe.js → render-BI1aqKoc.js} +1 -1
  153. package/public/dist/assets/{settings-BMLowRCh.js → settings-B0Sj4sUH.js} +1 -1
  154. package/public/dist/assets/settings-C7NutP6X.js +1 -0
  155. package/public/dist/assets/settings-core-CyX-px6_.js +100 -0
  156. package/public/dist/assets/settings-core-DVpFmrhY.js +1 -0
  157. package/public/dist/assets/{sidebar-gKjYLv49.js → sidebar-DOR9fTgN.js} +3 -3
  158. package/public/dist/assets/skills-BWhKreQ2.js +1 -0
  159. package/public/dist/assets/{skills-CKTbomdm.js → skills-wt0fkV9Y.js} +1 -1
  160. package/public/dist/assets/slash-commands-BZbqTg3v.js +20 -0
  161. package/public/dist/assets/slash-commands-Di4NDA4w.js +1 -0
  162. package/public/dist/assets/{trace-drawer-C3G4mmB1.js → trace-drawer-kMYzaVFW.js} +1 -1
  163. package/public/dist/assets/ui-BghexWrB.js +1 -0
  164. package/public/dist/assets/{ui-CVVC_flX.js → ui-D4D5HUfo.js} +1 -1
  165. package/public/dist/index.html +62 -1
  166. package/public/dist/manager/index.html +1 -1
  167. package/public/index.html +61 -0
  168. package/public/js/features/command-info.ts +1 -0
  169. package/public/js/features/help-content.ts +8 -0
  170. package/public/js/features/settings-channel.ts +5 -1
  171. package/public/js/features/settings-core.ts +2 -0
  172. package/public/js/features/settings-slack.ts +74 -0
  173. package/public/js/features/settings-types.ts +5 -1
  174. package/public/js/features/settings.ts +1 -0
  175. package/public/js/features/transport-status-row.ts +15 -3
  176. package/public/js/main.ts +19 -0
  177. package/public/js/provider-icons.ts +4 -1
  178. package/public/js/ws.ts +1 -1
  179. package/public/locales/en.json +21 -0
  180. package/public/locales/ja.json +21 -0
  181. package/public/locales/ko.json +21 -0
  182. package/public/locales/zh.json +21 -0
  183. package/public/manager/src/settings/SettingsShell.tsx +1 -0
  184. package/public/manager/src/settings/SettingsSidebar.tsx +1 -0
  185. package/public/manager/src/settings/pages/ChannelsSlack.tsx +323 -0
  186. package/public/manager/src/settings/pages/components/ActiveChannelToggle.tsx +2 -1
  187. package/public/manager/src/settings/pages/components/TransportStatusChips.tsx +13 -4
  188. package/public/manager/src/settings/types.ts +1 -0
  189. package/scripts/bundle-sidecar.sh +13 -4
  190. package/scripts/check-redaction-sinks.mjs +276 -0
  191. package/scripts/check-sidecar-prune-safety.mjs +167 -0
  192. package/scripts/pi-rpc-probe.mts +361 -0
  193. package/scripts/pick-gyp-python.sh +61 -0
  194. package/scripts/release-gates.mjs +97 -0
  195. package/scripts/release-preview.sh +8 -1
  196. package/scripts/release.sh +13 -2
  197. package/scripts/sync-electron-version.cjs +93 -0
  198. package/public/dist/assets/ChannelsDiscord-Bt1_rtNC.js +0 -1
  199. package/public/dist/assets/ChannelsTelegram-BLV0m1oq.js +0 -1
  200. package/public/dist/assets/TransportStatusChips-Bld0yXi7.js +0 -1
  201. package/public/dist/assets/app-iSJhsP20.js +0 -55
  202. package/public/dist/assets/bgtask-badge-CSkSsoB6.js +0 -1
  203. package/public/dist/assets/iframe-renderer-DXdo0YXN.js +0 -1
  204. package/public/dist/assets/memory-DeKUlD2M.js +0 -1
  205. package/public/dist/assets/message-history-CWTQ2woA.js +0 -1
  206. package/public/dist/assets/settings-CDjs7hTT.js +0 -1
  207. package/public/dist/assets/settings-core-Cs_oq-b3.js +0 -99
  208. package/public/dist/assets/settings-core-D-TOh1a6.js +0 -1
  209. package/public/dist/assets/skills-GuIfNASs.js +0 -1
  210. package/public/dist/assets/slash-commands-BtZa093n.js +0 -20
  211. package/public/dist/assets/slash-commands-DGJKfpMT.js +0 -1
  212. package/public/dist/assets/ui-DJg5Q_NJ.js +0 -1
  213. /package/public/dist/assets/{DashboardMeta-B7CVqXcU.js → DashboardMeta-DennaaA_.js} +0 -0
  214. /package/public/dist/assets/{Display-BF-rACK6.js → Display-hGuSn_W5.js} +0 -0
  215. /package/public/dist/assets/{Employees-D3hnciP0.js → Employees-B5YXk5ma.js} +0 -0
  216. /package/public/dist/assets/{Heartbeat-D4Pn5O3O.js → Heartbeat-vDGhPfct.js} +0 -0
  217. /package/public/dist/assets/{Mcp-SfytKWGX.js → Mcp-BHWnJ6SP.js} +0 -0
  218. /package/public/dist/assets/{Memory-DNxBOQ7N.js → Memory-B_UZ-iCA.js} +0 -0
  219. /package/public/dist/assets/{ModelProvider-CXwtlG7l.js → ModelProvider-CgUvs6pE.js} +0 -0
  220. /package/public/dist/assets/{Network-DISUhige.js → Network-AWpSkALD.js} +0 -0
  221. /package/public/dist/assets/{Profile-B8yBO3Ju.js → Profile-DnIE2ios.js} +0 -0
  222. /package/public/dist/assets/{Prompts-5lNQ5qOm.js → Prompts-B9aKLall.js} +0 -0
  223. /package/public/dist/assets/{SpeechKeys-BepaIHBa.js → SpeechKeys-K4Be_o7z.js} +0 -0
  224. /package/public/dist/assets/{TelegramHub--DNjRAwW.js → TelegramHub-CFwSkzHP.js} +0 -0
  225. /package/public/dist/assets/{TerminalPanel-DQovYfwz.js → TerminalPanel-CdlFZHAK.js} +0 -0
@@ -209,6 +209,9 @@ export const { messageQueue, enqueueMessage, removeQueuedMessage, processQueue,
209
209
  let mainSpawnStarting = false;
210
210
  let cancelPendingMainSpawn = null;
211
211
  let steerInProgress = false;
212
+ let cancelActiveCodexAppTurn = null;
213
+ let cancelActivePiTurn = null;
214
+ const piProfileFingerprintKey = crypto.randomBytes(32);
212
215
  export function setSteerInProgress(v) {
213
216
  const was = steerInProgress;
214
217
  steerInProgress = v;
@@ -230,6 +233,12 @@ const DEFAULT_STEER_WAIT_MS = 3_000;
230
233
  const DEFAULT_KILL_ESCALATION_MS = 2_000;
231
234
  const CLAUDE_E_STEER_WAIT_MS = 30_000;
232
235
  const CLAUDE_E_STEER_KILL_ESCALATION_MS = 8_000;
236
+ const DEFAULT_CODEX_APP_TURN_IDLE_MS = 300_000;
237
+ const DEFAULT_CODEX_APP_TURN_ABS_MS = 2 * 60 * 60_000;
238
+ function configuredPositiveMs(value, fallback) {
239
+ const parsed = Number(value);
240
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
241
+ }
233
242
  function getActiveMainCli() {
234
243
  return typeof currentMainMeta?.cli === 'string' ? currentMainMeta.cli : null;
235
244
  }
@@ -310,6 +319,24 @@ export function killActiveAgent(reason = 'user') {
310
319
  queueCtrl.purgeQueueOnStop(reason);
311
320
  clearWorkerSlotsOnStop(reason);
312
321
  }
322
+ if (cancelActiveCodexAppTurn && getActiveMainCli() === 'codex-app') {
323
+ if (activeProcess?.pid)
324
+ killReasons.set(activeProcess.pid, reason);
325
+ console.log(`[jaw:kill] reason=${reason} cli=codex-app action=lease.cancel`);
326
+ cancelActiveCodexAppTurn(reason);
327
+ if (reason === 'api' || reason === 'user' || reason === 'steer')
328
+ activeProcess = null;
329
+ return true;
330
+ }
331
+ if (cancelActivePiTurn && getActiveMainCli() === 'pi') {
332
+ if (activeProcess?.pid)
333
+ killReasons.set(activeProcess.pid, reason);
334
+ console.log(`[jaw:kill] reason=${reason} cli=pi action=lease.cancel`);
335
+ cancelActivePiTurn(reason);
336
+ if (reason === 'api' || reason === 'user' || reason === 'steer')
337
+ activeProcess = null;
338
+ return true;
339
+ }
313
340
  if (!activeProcess)
314
341
  return hadTimer || cancelledPendingMain || abortedInProcess; // timer/gated spawn/jwc abort 취소도 "killed" 취급
315
342
  const policy = getKillPolicy(reason);
@@ -560,7 +587,9 @@ export const saveUpload = (buffer, originalName, options) => _saveUpload(UPLOADS
560
587
  export { buildMediaPrompt, buildMediaPromptMany };
561
588
  // ─── Spawn Agent ─────────────────────────────────────
562
589
  import { AcpClient } from '../cli/acp-client.js';
563
- import { CodexAppClient } from './codex-app-client.js';
590
+ import { CodexAppClient, isRecoverableResumeError } from './codex-app-client.js';
591
+ import { acquireCodexAppRuntime, acquirePiRuntime } from './runtime-pool.js';
592
+ import { loadCatalogEfforts, resolveCatalogPath, validateModelEffort } from './codex-app-catalog.js';
564
593
  import { extractFromCodexAppEvent } from './codex-app-events.js';
565
594
  import { canGuardedAgyResume, resolveAgyNativeResume, shouldEmitHeartbeat, shouldResumeBucketSession } from './spawn/resume.js';
566
595
  export { canGuardedAgyResume, resolveAgyNativeResume, shouldEmitHeartbeat, shouldResumeBucketSession };
@@ -1079,6 +1108,8 @@ export function spawnAgent(prompt, opts = {}) {
1079
1108
  }
1080
1109
  if (!opts.internal)
1081
1110
  broadcast('agent_status', { status: 'running', cli, agentId: agentLabel, ...empTag }, traceAudience);
1111
+ if (mainManaged && !opts.internal)
1112
+ beginLiveRun(liveScope, cli);
1082
1113
  const traceRunId = startTraceRun({ cli, model, workingDir: settings["workingDir"] || null, agentLabel, audience: traceAudience });
1083
1114
  if (mainManaged && !opts.internal)
1084
1115
  setLiveRunTraceId(liveScope, traceRunId);
@@ -1383,174 +1414,207 @@ export function spawnAgent(prompt, opts = {}) {
1383
1414
  'Capitalized variants (Read, Bash, Edit, Write, Grep, Find, Ls) do NOT exist and will fail.',
1384
1415
  ].join('\n');
1385
1416
  const piSysPrompt = sysPrompt ? `${sysPrompt}\n\n${piToolDiscipline}` : piToolDiscipline;
1386
- const { child, done } = spawnPiRpc(profile, pi, {
1387
- prompt: piPrompt,
1388
- model: runtimeModel,
1389
- ...(piSessionId ? { sessionId: piSessionId } : {}),
1390
- effort,
1391
- cwd: spawnCwd,
1392
- sysPrompt: piSysPrompt,
1393
- onEvent: (event) => {
1394
- opts.lifecycle?.onActivity?.('pi-rpc');
1395
- if (event.kind === 'thinking') {
1396
- ctx.thinkingBuf = (ctx.thinkingBuf || '') + event.text;
1397
- return;
1398
- }
1399
- if (event.kind === 'text') {
1400
- flushPiThinking();
1401
- const delta = String(event.text || '');
1402
- if (!delta)
1403
- return;
1404
- ctx.fullText += delta;
1405
- const displayDelta = normalizeAssistantDisplayText(delta);
1406
- if (ctx.liveOutputText !== undefined)
1407
- ctx.liveOutputText += displayDelta;
1408
- if (!ctx.outputTextStarted)
1409
- ctx.outputTextStarted = true;
1410
- broadcastAgentOutput(ctx, agentLabel, cli, displayDelta, empTag, traceAudience);
1411
- return;
1412
- }
1413
- if (event.kind === 'tool') {
1414
- flushPiThinking();
1415
- const tool = stripUndefined({ icon: '🔧', label: event.label, status: event.status, detail: event.detail, toolType: 'tool' });
1416
- stampTraceTool(tool, ctx, 'tool');
1417
- ctx.toolLog.push(tool);
1418
- if (ctx.liveScope)
1419
- replaceLiveRunTools(ctx.liveScope, ctx.toolLog);
1420
- appendParentLiveRunTool(ctx, tool);
1421
- emitAgentTool(ctx, agentLabel, tool, empTag);
1417
+ const onPiEvent = (event) => {
1418
+ opts.lifecycle?.onActivity?.('pi-rpc');
1419
+ if (event.kind === 'thinking') {
1420
+ ctx.thinkingBuf = (ctx.thinkingBuf || '') + event.text;
1421
+ return;
1422
+ }
1423
+ if (event.kind === 'text') {
1424
+ flushPiThinking();
1425
+ const delta = String(event.text || '');
1426
+ if (!delta)
1422
1427
  return;
1423
- }
1424
- if (event.kind === 'session') {
1425
- ctx.sessionId = event.sessionId;
1426
- }
1427
- },
1428
- });
1429
- const piWatchdog = attachWatchdog(child, agentLabel, (reason) => {
1430
- console.log(`[jaw:watchdog] killing ${agentLabel} (pi) — ${reason}`);
1431
- ctx.stallReason = reason;
1432
- if (child.pid) {
1433
- killProcessTree(child.pid, 'SIGTERM');
1434
- const pid = child.pid;
1435
- setTimeout(() => {
1436
- try {
1437
- killProcessTree(pid, 'SIGKILL');
1438
- }
1439
- catch { /* already dead */ }
1440
- }, 5_000);
1428
+ ctx.fullText += delta;
1429
+ const displayDelta = normalizeAssistantDisplayText(delta);
1430
+ if (ctx.liveOutputText !== undefined)
1431
+ ctx.liveOutputText += displayDelta;
1432
+ if (!ctx.outputTextStarted)
1433
+ ctx.outputTextStarted = true;
1434
+ broadcastAgentOutput(ctx, agentLabel, cli, displayDelta, empTag, traceAudience);
1435
+ return;
1441
1436
  }
1442
- });
1443
- ctx.stallWatchdog = piWatchdog;
1444
- if (mainManaged)
1445
- activeProcess = child;
1446
- registerActiveProcess(agentLabel, child);
1447
- if (!opts.internal)
1448
- broadcast('agent_status', { running: true, agentId: agentLabel, cli, provider: profile.id, ...empTag });
1449
- if (mainManaged && !opts.internal) {
1450
- beginLiveRun(liveScope, cli);
1451
- setLiveRunTraceId(liveScope, traceRunId);
1452
- }
1453
- if (mainManaged && !opts.internal && !opts._skipInsert) {
1454
- insertMessage.run('user', prompt, cli, runtimeModel, settings["workingDir"] || null, getActiveChatSession());
1455
- }
1456
- if (!opts.internal)
1457
- broadcast('agent_status', { status: 'running', cli, agentId: agentLabel, provider: profile.id, ...empTag }, traceAudience);
1458
- done.then((result) => {
1459
- piWatchdog.stop();
1460
- flushPiThinking();
1461
- if (ctx.stderrBuf.length < 4000)
1462
- ctx.stderrBuf += result.stderr || '';
1463
- if (result.sessionId)
1464
- ctx.sessionId = result.sessionId;
1465
- if (!ctx.fullText && result.text)
1466
- ctx.fullText = result.text;
1467
- cleanupEmployeeTmpDir(spawnCwd, settings["workingDir"], agentLabel);
1468
- opts.lifecycle?.onExit?.(result.code);
1469
- const killReason = consumeKillReason(child.pid);
1470
- const wasKilled = !!killReason;
1471
- const wasSteer = killReason === 'steer';
1472
- const smokeResult = detectSmokeResponse(ctx.fullText, ctx.toolLog, result.code, cli);
1473
- return handleAgentExit({
1474
- ctx, code: result.code, cli, model: runtimeModel, effectiveProvider: profile.id, agentLabel, mainManaged, origin,
1475
- resumeKey,
1476
- prompt, opts, cfg, ownerGeneration, forceNew, empSid,
1477
- isResume: false, wasKilled, wasSteer, smokeResult,
1478
- effortDefault: 'medium', costLine: '',
1479
- resolve: resolve,
1480
- activeProcesses,
1481
- setActiveProcess: (v) => { activeProcess = v; },
1482
- retryState: queueCtrl.retryState,
1483
- fallbackState: queueCtrl.fallbackState,
1484
- fallbackMaxRetries: FALLBACK_MAX_RETRIES,
1485
- processQueue,
1437
+ if (event.kind === 'tool') {
1438
+ flushPiThinking();
1439
+ const tool = stripUndefined({ icon: '🔧', label: event.label, status: event.status, detail: event.detail, toolType: 'tool' });
1440
+ stampTraceTool(tool, ctx, 'tool');
1441
+ ctx.toolLog.push(tool);
1442
+ if (ctx.liveScope)
1443
+ replaceLiveRunTools(ctx.liveScope, ctx.toolLog);
1444
+ appendParentLiveRunTool(ctx, tool);
1445
+ emitAgentTool(ctx, agentLabel, tool, empTag);
1446
+ return;
1447
+ }
1448
+ if (event.kind === 'session')
1449
+ ctx.sessionId = event.sessionId;
1450
+ };
1451
+ const runPiTurn = (child, done, lease) => {
1452
+ let leaseCancel = null;
1453
+ const requestCancel = () => {
1454
+ if (!lease) {
1455
+ if (child.pid) {
1456
+ const pid = child.pid;
1457
+ killProcessTree(pid, 'SIGTERM');
1458
+ setTimeout(() => {
1459
+ try {
1460
+ killProcessTree(pid, 'SIGKILL');
1461
+ }
1462
+ catch { /* already dead */ }
1463
+ }, 5_000);
1464
+ }
1465
+ else
1466
+ child.kill('SIGTERM');
1467
+ return Promise.resolve();
1468
+ }
1469
+ leaseCancel ??= lease.cancel();
1470
+ return leaseCancel;
1471
+ };
1472
+ const cancelHook = (_reason) => { void requestCancel(); };
1473
+ if (lease)
1474
+ cancelActivePiTurn = cancelHook;
1475
+ const piWatchdog = attachWatchdog(child, agentLabel, (reason) => {
1476
+ console.log(`[jaw:watchdog] cancelling ${agentLabel} (pi) ${reason}`);
1477
+ ctx.stallReason = reason;
1478
+ void requestCancel();
1486
1479
  });
1487
- }).catch((err) => {
1488
- piWatchdog.stop();
1489
- if (ctx.stderrBuf.length < 4000)
1490
- ctx.stderrBuf += err.message;
1491
- console.error('[jaw:pi] runtime failed:', err.message);
1492
- handleAgentExit({
1493
- ctx, code: 1, cli, model: runtimeModel, effectiveProvider: profile.id, agentLabel, mainManaged, origin,
1494
- resumeKey,
1495
- prompt, opts, cfg, ownerGeneration, forceNew, empSid,
1496
- isResume: false, wasKilled: false, wasSteer: false, smokeResult: detectSmokeResponse('', [], 1, cli),
1497
- effortDefault: 'medium', costLine: '',
1498
- resolve: resolve,
1499
- activeProcesses,
1500
- setActiveProcess: (v) => { activeProcess = v; },
1501
- retryState: queueCtrl.retryState,
1502
- fallbackState: queueCtrl.fallbackState,
1503
- fallbackMaxRetries: FALLBACK_MAX_RETRIES,
1504
- processQueue,
1505
- }).catch((handleErr) => {
1506
- console.error('[jaw:lifecycle] handleAgentExit failed (Pi):', handleErr.message);
1480
+ ctx.stallWatchdog = piWatchdog;
1481
+ if (mainManaged)
1482
+ activeProcess = child;
1483
+ registerActiveProcess(agentLabel, child);
1484
+ if (!opts.internal)
1485
+ broadcast('agent_status', { running: true, agentId: agentLabel, cli, provider: profile.id, ...empTag });
1486
+ if (mainManaged && !opts.internal) {
1487
+ beginLiveRun(liveScope, cli);
1488
+ setLiveRunTraceId(liveScope, traceRunId);
1489
+ }
1490
+ if (mainManaged && !opts.internal && !opts._skipInsert) {
1491
+ insertMessage.run('user', prompt, cli, runtimeModel, settings["workingDir"] || null, getActiveChatSession());
1492
+ }
1493
+ if (!opts.internal)
1494
+ broadcast('agent_status', { status: 'running', cli, agentId: agentLabel, provider: profile.id, ...empTag }, traceAudience);
1495
+ const releaseLease = async () => {
1496
+ if (leaseCancel)
1497
+ await leaseCancel;
1498
+ if (cancelActivePiTurn === cancelHook)
1499
+ cancelActivePiTurn = null;
1500
+ if (lease)
1501
+ lease.release();
1502
+ else
1503
+ cleanupEmployeeTmpDir(spawnCwd, settings["workingDir"], agentLabel);
1504
+ };
1505
+ done.then(async (result) => {
1506
+ piWatchdog.stop();
1507
+ await releaseLease();
1508
+ flushPiThinking();
1509
+ if (ctx.stderrBuf.length < 4000)
1510
+ ctx.stderrBuf += result.stderr || '';
1511
+ if (result.sessionId)
1512
+ ctx.sessionId = result.sessionId;
1513
+ if (!ctx.fullText && result.text)
1514
+ ctx.fullText = result.text;
1515
+ opts.lifecycle?.onExit?.(result.code);
1516
+ const killReason = consumeKillReason(child.pid);
1517
+ const wasKilled = !!killReason;
1518
+ const wasSteer = killReason === 'steer';
1519
+ const smokeResult = detectSmokeResponse(ctx.fullText, ctx.toolLog, result.code, cli);
1520
+ return handleAgentExit({
1521
+ ctx, code: result.code, cli, model: runtimeModel, effectiveProvider: profile.id, agentLabel, mainManaged, origin,
1522
+ resumeKey,
1523
+ prompt, opts, cfg, ownerGeneration, forceNew, empSid,
1524
+ isResume: false, wasKilled, wasSteer, smokeResult,
1525
+ effortDefault: 'medium', costLine: '',
1526
+ resolve: resolve,
1527
+ activeProcesses,
1528
+ setActiveProcess: (v) => { activeProcess = v; },
1529
+ retryState: queueCtrl.retryState,
1530
+ fallbackState: queueCtrl.fallbackState,
1531
+ fallbackMaxRetries: FALLBACK_MAX_RETRIES,
1532
+ processQueue,
1533
+ });
1534
+ }).catch(async (err) => {
1535
+ piWatchdog.stop();
1536
+ await releaseLease().catch(() => { });
1537
+ if (ctx.stderrBuf.length < 4000)
1538
+ ctx.stderrBuf += err.message;
1539
+ console.error('[jaw:pi] runtime failed:', err.message);
1540
+ handleAgentExit({
1541
+ ctx, code: 1, cli, model: runtimeModel, effectiveProvider: profile.id, agentLabel, mainManaged, origin,
1542
+ resumeKey,
1543
+ prompt, opts, cfg, ownerGeneration, forceNew, empSid,
1544
+ isResume: false, wasKilled: false, wasSteer: false, smokeResult: detectSmokeResponse('', [], 1, cli),
1545
+ effortDefault: 'medium', costLine: '',
1546
+ resolve: resolve,
1547
+ activeProcesses,
1548
+ setActiveProcess: (v) => { activeProcess = v; },
1549
+ retryState: queueCtrl.retryState,
1550
+ fallbackState: queueCtrl.fallbackState,
1551
+ fallbackMaxRetries: FALLBACK_MAX_RETRIES,
1552
+ processQueue,
1553
+ }).catch((handleErr) => {
1554
+ console.error('[jaw:lifecycle] handleAgentExit failed (Pi):', handleErr.message);
1555
+ });
1507
1556
  });
1557
+ };
1558
+ if (opts.agentId) {
1559
+ const { child, done } = spawnPiRpc(profile, pi, {
1560
+ prompt: piPrompt,
1561
+ model: runtimeModel,
1562
+ ...(piSessionId ? { sessionId: piSessionId } : {}),
1563
+ effort,
1564
+ cwd: spawnCwd,
1565
+ sysPrompt: piSysPrompt,
1566
+ onEvent: onPiEvent,
1567
+ });
1568
+ runPiTurn(child, done, null);
1569
+ return { child, promise: resultPromise };
1570
+ }
1571
+ const profileFp = crypto.createHmac('sha256', piProfileFingerprintKey)
1572
+ .update(profile.apiKey || '')
1573
+ .digest('hex')
1574
+ .slice(0, 12);
1575
+ mainSpawnStarting = true;
1576
+ void acquirePiRuntime({
1577
+ key: {
1578
+ scopeKey: `chat:${getActiveChatSession()}`,
1579
+ cwd: spawnCwd,
1580
+ profileId: profile.id,
1581
+ fullEndpoint: profile.endpoint,
1582
+ apiKind: profile.apiKind,
1583
+ model: runtimeModel,
1584
+ effort,
1585
+ profileFp,
1586
+ },
1587
+ piSettings: pi,
1588
+ storedSessionId: piSessionId || null,
1589
+ instructions: piSysPrompt,
1590
+ forceNew,
1591
+ }).then((lease) => {
1592
+ mainSpawnStarting = false;
1593
+ ctx.sessionId = lease.session.sessionId;
1594
+ console.log(`[jaw:pi:pool] reused=${lease.reused} sessionId=${lease.session.sessionId || 'new'}`);
1595
+ const done = lease.session.sendPrompt(piPrompt, { effort, onEvent: onPiEvent })
1596
+ .then((result) => ({ ...result, code: 0, sessionId: lease.session.sessionId }));
1597
+ runPiTurn(lease.session.child, done, lease);
1598
+ }).catch((err) => {
1599
+ mainSpawnStarting = false;
1600
+ console.error(`[jaw:pi:pool] acquire failed: ${err.message}`);
1601
+ clearLiveRun(liveScope);
1602
+ broadcast('agent_status', { running: false, agentId: agentLabel });
1603
+ broadcast('agent_done', { text: `❌ Pi RPC acquire failed: ${err.message}`, error: true, origin }, 'public');
1604
+ resolve({ text: '', code: 1 });
1605
+ processQueue();
1508
1606
  });
1509
- return { child, promise: resultPromise };
1607
+ return { child: null, promise: resultPromise };
1510
1608
  }
1511
1609
  // ─── Codex AppServer branch ────────────────────
1512
1610
  if (cli === 'codex-app') {
1513
- const appClient = new CodexAppClient({
1514
- binary: detected.path || 'codex',
1515
- workDir: spawnCwd,
1516
- env: spawnEnv,
1517
- model,
1518
- effort,
1519
- fastMode: cfg.fastMode ?? settings["perCli"]?.["codex"]?.fastMode,
1520
- });
1521
- appClient.spawn();
1522
- const child = appClient.proc;
1523
- if (!child) {
1524
- throw new Error('Codex AppServer process was not created');
1525
- }
1526
- if (mainManaged)
1527
- activeProcess = child;
1528
- registerActiveProcess(agentLabel, child);
1529
- if (!opts.internal)
1530
- broadcast('agent_status', { running: true, agentId: agentLabel, cli, ...empTag });
1531
- if (mainManaged && !opts.internal)
1532
- beginLiveRun(liveScope, cli);
1533
- let codexAppSettled = false;
1534
- appClient.on('error', (err) => {
1535
- if (codexAppSettled)
1536
- return;
1537
- codexAppSettled = true;
1538
- appClient.cleanup();
1539
- cleanupEmployeeTmpDir(spawnCwd, settings["workingDir"], agentLabel);
1540
- opts.lifecycle?.onExit?.(null);
1541
- const msg = `Codex AppServer spawn failed: ${err.message}`;
1542
- console.error(`[codex-app:error] ${msg}`);
1543
- activeProcesses.delete(agentLabel);
1544
- if (mainManaged) {
1545
- activeProcess = null;
1546
- clearLiveRun(liveScope);
1547
- broadcast('agent_status', { running: false, agentId: agentLabel });
1611
+ const catalogPath = resolveCatalogPath();
1612
+ if (catalogPath) {
1613
+ const verdict = validateModelEffort(model, effort, loadCatalogEfforts(catalogPath));
1614
+ if (!verdict.ok) {
1615
+ throw new Error(`[codex-app] ${verdict.error}`);
1548
1616
  }
1549
- broadcast('agent_done', { text: `❌ ${msg}`, error: true, origin, ...empTag }, isEmployee ? 'internal' : 'public');
1550
- resolve({ text: '', code: 1 });
1551
- if (mainManaged)
1552
- processQueue();
1553
- });
1617
+ }
1554
1618
  if (mainManaged && !opts.internal && !opts._skipInsert) {
1555
1619
  insertMessage.run('user', prompt, cli, model, settings["workingDir"] || null, getActiveChatSession());
1556
1620
  }
@@ -1590,14 +1654,27 @@ export function spawnAgent(prompt, opts = {}) {
1590
1654
  }
1591
1655
  let lastVisibleBroadcastTs = Date.now();
1592
1656
  let heartbeatSent = false;
1593
- appClient.on('notification', (method, params) => {
1657
+ let turnCompleted = false;
1658
+ let turnReportedFailure = false;
1659
+ let markCodexProgress = () => { };
1660
+ let settleTurn;
1661
+ let rejectTurn;
1662
+ const turnDone = new Promise((resolveTurn, rejectTurnPromise) => {
1663
+ settleTurn = resolveTurn;
1664
+ rejectTurn = rejectTurnPromise;
1665
+ });
1666
+ const handleNotification = (method, params) => {
1667
+ markCodexProgress();
1594
1668
  if (method === 'turn/completed' || method === 'turn/started' || method === 'error') {
1595
1669
  console.log(`[codex-app:notify] ${method}`);
1596
1670
  }
1597
1671
  appendTraceEvent({ runId: ctx.traceRunId, source: 'codex_app_raw', eventType: method, raw: params });
1598
1672
  const parsed = extractFromCodexAppEvent(method, params, ctx);
1599
- if (!parsed)
1673
+ if (!parsed) {
1674
+ if (method === 'turn/completed')
1675
+ settleTurn();
1600
1676
  return;
1677
+ }
1601
1678
  if (parsed.flushThinking) {
1602
1679
  flushCodexAppThinking();
1603
1680
  }
@@ -1638,11 +1715,13 @@ export function spawnAgent(prompt, opts = {}) {
1638
1715
  }
1639
1716
  if (parsed.turnStatus && parsed.turnStatus !== 'completed') {
1640
1717
  console.warn(`[codex-app:turn] final status: ${parsed.turnStatus}`);
1641
- turnCompleted = false;
1718
+ turnReportedFailure = true;
1642
1719
  }
1643
1720
  opts.lifecycle?.onActivity?.('codex-app');
1644
- });
1645
- appClient.on('stderr', (text) => {
1721
+ if (method === 'turn/completed')
1722
+ settleTurn();
1723
+ };
1724
+ const handleStderr = (text) => {
1646
1725
  appendTraceEvent({ runId: ctx.traceRunId, source: 'stderr', eventType: 'stderr', raw: text });
1647
1726
  if (ctx.stderrBuf.length < 4000) {
1648
1727
  ctx.stderrBuf += text + '\n';
@@ -1657,51 +1736,103 @@ export function spawnAgent(prompt, opts = {}) {
1657
1736
  label: 'working... (no visible progress)',
1658
1737
  }, empTag);
1659
1738
  }
1660
- });
1661
- let turnCompleted = false;
1662
- (async () => {
1739
+ };
1740
+ const effectiveFastMode = cfg.fastMode ?? settings["perCli"]?.["codex"]?.fastMode ?? false;
1741
+ const runCodexAppTurn = async (appClient, lease) => {
1742
+ const child = appClient.proc;
1743
+ if (!child)
1744
+ throw new Error('Codex AppServer process was not created');
1745
+ if (mainManaged)
1746
+ activeProcess = child;
1747
+ registerActiveProcess(agentLabel, child);
1748
+ if (!opts.internal)
1749
+ broadcast('agent_status', { running: true, agentId: agentLabel, cli, ...empTag });
1750
+ const processExit = { value: null };
1751
+ const idleMs = configuredPositiveMs(process.env["CODEX_APP_TURN_IDLE_MS"], DEFAULT_CODEX_APP_TURN_IDLE_MS);
1752
+ const absoluteMs = configuredPositiveMs(process.env["CODEX_APP_TURN_ABS_MS"], DEFAULT_CODEX_APP_TURN_ABS_MS);
1753
+ let idleTimer;
1754
+ let absoluteTimer;
1755
+ let watchdogCancel = null;
1756
+ let leaseCancel = null;
1757
+ const requestLeaseCancel = () => {
1758
+ if (!lease) {
1759
+ appClient.kill();
1760
+ return Promise.resolve();
1761
+ }
1762
+ leaseCancel ??= lease.cancel().catch((err) => {
1763
+ console.warn('[codex-app:turn] cancel failed:', err.message);
1764
+ });
1765
+ return leaseCancel;
1766
+ };
1767
+ const cancelHook = (_reason) => { void requestLeaseCancel(); };
1768
+ if (lease)
1769
+ cancelActiveCodexAppTurn = cancelHook;
1770
+ const watchdogTimeout = (kind) => {
1771
+ if (watchdogCancel)
1772
+ return;
1773
+ console.warn(`[codex-app:turn] watchdog stall (${kind}, idleMs=${idleMs}, absoluteMs=${absoluteMs})`);
1774
+ watchdogCancel = requestLeaseCancel();
1775
+ rejectTurn(new Error(`Codex AppServer turn ${kind} watchdog timeout`));
1776
+ };
1777
+ const resetIdleTimer = () => {
1778
+ clearTimeout(idleTimer);
1779
+ idleTimer = setTimeout(() => { watchdogTimeout('idle'); }, idleMs);
1780
+ };
1781
+ idleTimer = setTimeout(() => { watchdogTimeout('idle'); }, idleMs);
1782
+ absoluteTimer = setTimeout(() => { watchdogTimeout('absolute'); }, absoluteMs);
1783
+ markCodexProgress = resetIdleTimer;
1784
+ const scope = appClient.listenTurn({
1785
+ onNotification: handleNotification,
1786
+ onStderr: handleStderr,
1787
+ onExit: (code, signal) => {
1788
+ processExit.value = { code, signal };
1789
+ rejectTurn(new Error(`Codex AppServer exited (code=${code}, signal=${signal})`));
1790
+ },
1791
+ onError: rejectTurn,
1792
+ onInterruptFailed: (err) => {
1793
+ console.warn(`[codex-app:interrupt] ${err.message}`);
1794
+ },
1795
+ });
1663
1796
  try {
1664
- const initResult = await appClient.initialize();
1665
- if (process.env["DEBUG"])
1666
- console.log('[codex-app:init]', JSON.stringify(initResult).slice(0, 200));
1667
- if (isResume && resumeSessionId) {
1668
- try {
1669
- await appClient.resumeThread(resumeSessionId);
1670
- console.log(`[codex-app:session] resumeThread OK: ${resumeSessionId.slice(0, 12)}...`);
1671
- }
1672
- catch (resumeErr) {
1673
- console.warn(`[codex-app:session] resumeThread FAILED: ${resumeErr.message} — starting new thread`);
1674
- if (empSid && opts.agentId) {
1675
- clearEmployeeSession.run(opts.agentId);
1797
+ if (lease) {
1798
+ ctx.sessionId = lease.threadId;
1799
+ console.log(`[codex-app:pool] thread=${lease.threadId.slice(0, 12)}... reused=${lease.reused} resumed=${lease.resumedThread}`);
1800
+ }
1801
+ else {
1802
+ const initResult = await appClient.initialize();
1803
+ if (process.env["DEBUG"])
1804
+ console.log('[codex-app:init]', JSON.stringify(initResult).slice(0, 200));
1805
+ if (isResume && resumeSessionId) {
1806
+ try {
1807
+ await appClient.resumeThread(resumeSessionId, { instructions: sysPrompt });
1808
+ console.log(`[codex-app:session] resumeThread OK: ${resumeSessionId.slice(0, 12)}...`);
1809
+ }
1810
+ catch (resumeErr) {
1811
+ const message = resumeErr.message || '';
1812
+ if (!isRecoverableResumeError(message))
1813
+ throw resumeErr;
1814
+ console.warn(`[codex-app:session] resumeThread FAILED (recoverable): ${message} — starting new thread`);
1815
+ if (empSid && opts.agentId)
1816
+ clearEmployeeSession.run(opts.agentId);
1817
+ await appClient.startThread({ instructions: sysPrompt, cwd: spawnCwd });
1676
1818
  }
1819
+ }
1820
+ else {
1677
1821
  await appClient.startThread({ instructions: sysPrompt, cwd: spawnCwd });
1678
1822
  }
1823
+ ctx.sessionId = appClient.threadId;
1679
1824
  }
1680
- else {
1681
- await appClient.startThread({ instructions: sysPrompt, cwd: spawnCwd });
1682
- }
1683
- ctx.sessionId = appClient.threadId;
1684
- const useNativeResume = isResume && Boolean(resumeSessionId);
1685
- const codexAppPrompt = (!useNativeResume && historyBlock)
1825
+ const shouldPrependHistory = lease
1826
+ ? !lease.resumedThread
1827
+ : !(isResume && Boolean(resumeSessionId));
1828
+ const codexAppPrompt = (shouldPrependHistory && historyBlock)
1686
1829
  ? `${historyBlock}\n\n[User Message]\n${prompt}`
1687
1830
  : prompt;
1688
- const turnDone = new Promise((resolveTurn, rejectTurn) => {
1689
- appClient.once('turn/completed', () => {
1690
- appClient.removeListener('error', rejectTurn);
1691
- resolveTurn();
1692
- });
1693
- appClient.once('error', rejectTurn);
1694
- });
1695
- await appClient.startTurn(codexAppPrompt);
1696
- try {
1697
- await turnDone;
1698
- turnCompleted = true;
1699
- }
1700
- catch (turnErr) {
1701
- console.warn(`[codex-app:turn] error during turn: ${turnErr.message}`);
1702
- }
1831
+ await Promise.race([appClient.startTurn(codexAppPrompt), turnDone]);
1832
+ await turnDone;
1833
+ turnCompleted = !turnReportedFailure;
1703
1834
  flushCodexAppThinking();
1704
- const persistedThreadId = appClient.threadId;
1835
+ const persistedThreadId = lease?.threadId ?? appClient.threadId;
1705
1836
  if (persistedThreadId && persistMainSession(stripUndefined({
1706
1837
  ownerGeneration,
1707
1838
  forceNew,
@@ -1716,53 +1847,43 @@ export function spawnAgent(prompt, opts = {}) {
1716
1847
  }))) {
1717
1848
  console.log(`[jaw:session] saved ${cli} session=${persistedThreadId.slice(0, 12)}... (pre-shutdown)`);
1718
1849
  }
1719
- if (!codexAppSettled) {
1720
- codexAppSettled = true;
1721
- const smokeResult = detectSmokeResponse(ctx.fullText, ctx.toolLog, 0, cli);
1722
- handleAgentExit({
1723
- ctx, code: turnCompleted ? 0 : 1, cli, model, agentLabel, mainManaged, origin,
1724
- resumeKey,
1725
- prompt, opts, cfg, ownerGeneration, forceNew, empSid,
1726
- isResume, wasKilled: false, wasSteer: false, smokeResult,
1727
- effortDefault: '', costLine: '',
1728
- resolve: resolve,
1729
- activeProcesses,
1730
- setActiveProcess: (v) => { activeProcess = v; },
1731
- retryState: queueCtrl.retryState,
1732
- fallbackState: queueCtrl.fallbackState,
1733
- fallbackMaxRetries: FALLBACK_MAX_RETRIES,
1734
- processQueue,
1735
- }).catch((err) => {
1736
- console.error(`[codex-app:handleAgentExit] ${err.message}`);
1737
- });
1738
- }
1739
- await appClient.closeGracefully();
1740
1850
  }
1741
1851
  catch (err) {
1742
1852
  console.error(`[codex-app:error] ${err.message}`);
1743
1853
  if (ctx.stderrBuf.length < 4000)
1744
1854
  ctx.stderrBuf += err.message;
1745
- appClient.kill();
1855
+ if (!lease)
1856
+ appClient.kill();
1746
1857
  }
1747
- })();
1748
- appClient.on('exit', (code, signal) => {
1749
- if (codexAppSettled)
1750
- return;
1751
- codexAppSettled = true;
1752
- appClient.cleanup();
1753
- cleanupEmployeeTmpDir(spawnCwd, settings["workingDir"], agentLabel);
1754
- opts.lifecycle?.onExit?.(code ?? null);
1755
- const killReason = consumeKillReason(appClient.proc?.pid);
1756
- if (code !== 0 && !killReason) {
1757
- console.warn(`[codex-app:unexpected-exit] code=${code} signal=${signal} threadId=${ctx.sessionId || 'none'}`);
1858
+ finally {
1859
+ clearTimeout(idleTimer);
1860
+ clearTimeout(absoluteTimer);
1861
+ markCodexProgress = () => { };
1862
+ if (watchdogCancel)
1863
+ await watchdogCancel;
1864
+ if (cancelActiveCodexAppTurn === cancelHook)
1865
+ cancelActiveCodexAppTurn = null;
1866
+ scope.dispose();
1867
+ if (lease)
1868
+ lease.release();
1869
+ else {
1870
+ await appClient.closeGracefully();
1871
+ appClient.cleanup();
1872
+ cleanupEmployeeTmpDir(spawnCwd, settings["workingDir"], agentLabel);
1873
+ }
1874
+ }
1875
+ const exitCode = turnCompleted ? 0 : (processExit.value?.code ?? 1);
1876
+ opts.lifecycle?.onExit?.(exitCode);
1877
+ const killReason = consumeKillReason(child.pid);
1878
+ if (processExit.value && processExit.value.code !== 0 && !killReason) {
1879
+ console.warn(`[codex-app:unexpected-exit] code=${processExit.value.code} signal=${processExit.value.signal} threadId=${ctx.sessionId || 'none'}`);
1758
1880
  }
1759
1881
  const wasKilled = !!killReason;
1760
1882
  const wasSteer = killReason === 'steer';
1761
1883
  flushCodexAppThinking();
1762
- const smokeResult = detectSmokeResponse(ctx.fullText, ctx.toolLog, code, cli);
1763
- const codexAppCode = turnCompleted ? 0 : (code ?? 1);
1764
- handleAgentExit({
1765
- ctx, code: codexAppCode, cli, model, agentLabel, mainManaged, origin,
1884
+ const smokeResult = detectSmokeResponse(ctx.fullText, ctx.toolLog, exitCode, cli);
1885
+ await handleAgentExit({
1886
+ ctx, code: exitCode, cli, model, agentLabel, mainManaged, origin,
1766
1887
  resumeKey,
1767
1888
  prompt, opts, cfg, ownerGeneration, forceNew, empSid,
1768
1889
  isResume, wasKilled, wasSteer, smokeResult,
@@ -1777,8 +1898,42 @@ export function spawnAgent(prompt, opts = {}) {
1777
1898
  }).catch((err) => {
1778
1899
  console.error('[jaw:lifecycle] handleAgentExit failed (codex-app):', err.message);
1779
1900
  });
1901
+ };
1902
+ if (opts.agentId) {
1903
+ const appClient = new CodexAppClient({
1904
+ binary: detected.path || 'codex', workDir: spawnCwd, env: spawnEnv,
1905
+ model, effort, fastMode: effectiveFastMode,
1906
+ });
1907
+ appClient.spawn();
1908
+ const child = appClient.proc;
1909
+ if (!child)
1910
+ throw new Error('Codex AppServer process was not created');
1911
+ void runCodexAppTurn(appClient, null);
1912
+ return { child, promise: resultPromise };
1913
+ }
1914
+ mainSpawnStarting = true;
1915
+ void acquireCodexAppRuntime({
1916
+ binary: detected.path || 'codex', env: spawnEnv,
1917
+ key: {
1918
+ scopeKey: `chat:${getActiveChatSession()}`,
1919
+ cwd: spawnCwd, model, effort, fastMode: effectiveFastMode,
1920
+ },
1921
+ storedThreadId: resumeSessionId || null,
1922
+ instructions: sysPrompt,
1923
+ forceNew,
1924
+ }).then(async (lease) => {
1925
+ mainSpawnStarting = false;
1926
+ await runCodexAppTurn(lease.client, lease);
1927
+ }).catch((err) => {
1928
+ mainSpawnStarting = false;
1929
+ console.error(`[codex-app:pool] acquire failed: ${err.message}`);
1930
+ clearLiveRun(liveScope);
1931
+ broadcast('agent_status', { running: false, agentId: agentLabel });
1932
+ broadcast('agent_done', { text: `❌ Codex AppServer acquire failed: ${err.message}`, error: true, origin }, 'public');
1933
+ resolve({ text: '', code: 1 });
1934
+ processQueue();
1780
1935
  });
1781
- return { child, promise: resultPromise };
1936
+ return { child: null, promise: resultPromise };
1782
1937
  }
1783
1938
  // ─── Standard CLI branch (claude/codex/opencode) ──────
1784
1939
  // DIFF-B: Windows needs shell:true only when falling back to .cmd shims.