@loicngr/kobo 1.11.16 → 1.11.18

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 (175) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/mcp-server/kobo-tasks-server.js +8 -7
  4. package/dist/server/db/migrations.js +57 -0
  5. package/dist/server/db/schema.js +10 -39
  6. package/dist/server/index.js +55 -7
  7. package/dist/server/routes/git.js +8 -3
  8. package/dist/server/routes/health.js +19 -2
  9. package/dist/server/routes/settings.js +24 -0
  10. package/dist/server/routes/workspaces.js +379 -72
  11. package/dist/server/services/agent/engines/claude-code/engine.js +39 -35
  12. package/dist/server/services/agent/engines/codex/client.js +16 -4
  13. package/dist/server/services/agent/engines/codex/engine.js +23 -2
  14. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +44 -5
  15. package/dist/server/services/agent/engines/types.js +13 -0
  16. package/dist/server/services/agent/orchestrator.js +354 -133
  17. package/dist/server/services/agent/session-controller.js +27 -0
  18. package/dist/server/services/agent/{engines/codex/turn-liveness.js → turn-liveness.js} +6 -2
  19. package/dist/server/services/change-source-branch-service.js +115 -11
  20. package/dist/server/services/db-backup-service.js +27 -16
  21. package/dist/server/services/pr-watcher-service.js +17 -2
  22. package/dist/server/services/settings-service.js +39 -0
  23. package/dist/server/services/websocket-service.js +114 -48
  24. package/dist/server/services/workspace-service.js +54 -2
  25. package/dist/server/services/worktree-purge-service.js +19 -5
  26. package/dist/server/services/worktree-service.js +38 -1
  27. package/dist/server/services/ws-events-retention-service.js +133 -0
  28. package/dist/server/utils/git-ops.js +209 -14
  29. package/dist/server/utils/git-repo-lock.js +45 -0
  30. package/dist/server/utils/logger.js +108 -0
  31. package/dist/server/utils/script-runner.js +11 -0
  32. package/package.json +1 -1
  33. package/src/client/dist/pwa/assets/ActivityFeed-BlDBh5jB.js +8 -0
  34. package/src/client/dist/pwa/assets/ActivityFeed-tC8sR0GV.css +1 -0
  35. package/src/client/dist/pwa/assets/ChangelogPage-DACtknly.js +1 -0
  36. package/src/client/dist/pwa/assets/ChangelogPage-D_WOVByV.css +1 -0
  37. package/src/client/dist/pwa/assets/ClosePopup-DEy0yFXE.js +1 -0
  38. package/src/client/dist/pwa/assets/CreatePage-Bb-Qn-Rd.js +2 -0
  39. package/src/client/dist/pwa/assets/CreatePage-RMBX-Zjn.css +1 -0
  40. package/src/client/dist/pwa/assets/DiffViewer-DKt6gFi4.js +8 -0
  41. package/src/client/dist/pwa/assets/DiffViewer-ahWtFrRy.css +1 -0
  42. package/src/client/dist/pwa/assets/DocumentsPanel-B8QY7FTj.css +1 -0
  43. package/src/client/dist/pwa/assets/DocumentsPanel-DhP1jk11.js +1 -0
  44. package/src/client/dist/pwa/assets/DrawerToggleButton-CBdOJzVZ.js +1 -0
  45. package/src/client/dist/pwa/assets/GitPanel-BKrtkPrV.css +1 -0
  46. package/src/client/dist/pwa/assets/GitPanel-DNCCB0G_.js +3 -0
  47. package/src/client/dist/pwa/assets/HealthPage-B1J4-dOH.js +1 -0
  48. package/src/client/dist/pwa/assets/HealthPage-BbqLoFQj.css +1 -0
  49. package/src/client/dist/pwa/assets/MainLayout-F8ShDnAy.css +1 -0
  50. package/src/client/dist/pwa/assets/MainLayout-qWwDy1B5.js +2 -0
  51. package/src/client/dist/pwa/assets/QBadge-BgS0-MXB.js +1 -0
  52. package/src/client/dist/pwa/assets/QBanner-BoAbhM1O.js +1 -0
  53. package/src/client/dist/pwa/assets/QBtn-B7WFdfmy.js +1 -0
  54. package/src/client/dist/pwa/assets/QBtnGroup-BMQlX2YS.js +1 -0
  55. package/src/client/dist/pwa/assets/QBtnToggle-eMu_yinY.js +1 -0
  56. package/src/client/dist/pwa/assets/QChip-BzB_GF4Z.js +1 -0
  57. package/src/client/dist/pwa/assets/QExpansionItem-C8CWo-aP.js +1 -0
  58. package/src/client/dist/pwa/assets/QItemSection-CDBweQnA.js +1 -0
  59. package/src/client/dist/pwa/assets/QList-Ccqhimcb.js +1 -0
  60. package/src/client/dist/pwa/assets/QPage-RpDxx5ah.js +1 -0
  61. package/src/client/dist/pwa/assets/QSelect-D9bHHxAA.js +1 -0
  62. package/src/client/dist/pwa/assets/QSlideTransition-6lWJvjLP.js +1 -0
  63. package/src/client/dist/pwa/assets/QSpace-D3PNLwa4.js +1 -0
  64. package/src/client/dist/pwa/assets/QSpinnerDots-DWOFEezR.js +1 -0
  65. package/src/client/dist/pwa/assets/SchedulePanel-Brtqtz1H.js +1 -0
  66. package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +1 -0
  67. package/src/client/dist/pwa/assets/SearchPage-CvZxJiJs.js +1 -0
  68. package/src/client/dist/pwa/assets/SearchPage-DdWM01rq.css +1 -0
  69. package/src/client/dist/pwa/assets/SettingsPage-JVje8OPs.js +16 -0
  70. package/src/client/dist/pwa/assets/SettingsPage-dMAg4JP8.css +1 -0
  71. package/src/client/dist/pwa/assets/TerminalPanel-B6xJJs8p.css +1 -0
  72. package/src/client/dist/pwa/assets/TerminalPanel-Dtc6B3US.js +18 -0
  73. package/src/client/dist/pwa/assets/TouchPan-DnEDaTFL.js +1 -0
  74. package/src/client/dist/pwa/assets/WorkspacePage-BVxtefbj.js +4 -0
  75. package/src/client/dist/pwa/assets/WorkspacePage-DN-A6XpI.css +1 -0
  76. package/src/client/dist/pwa/assets/build-path-tree-BU8Y8vRw.js +1 -0
  77. package/src/client/dist/pwa/assets/clipboard-t9qe6lK7.js +1 -0
  78. package/src/client/dist/pwa/assets/{cssMode-DNEi2cLH.js → cssMode-D_JDSbNP.js} +1 -1
  79. package/src/client/dist/pwa/assets/debounce-CDIE7CV6.js +1 -0
  80. package/src/client/dist/pwa/assets/documents-B6eOZeFK.js +1 -0
  81. package/src/client/dist/pwa/assets/{editor.api-_tFrtCR7.js → editor.api-Ccbr7NXr.js} +1 -1
  82. package/src/client/dist/pwa/assets/engineFeatures-DW9PydRL.js +1 -0
  83. package/src/client/dist/pwa/assets/expand-template-B4LAsXAF.css +1 -0
  84. package/src/client/dist/pwa/assets/expand-template-Xq6JwXsl.js +1 -0
  85. package/src/client/dist/pwa/assets/{freemarker2-kfVsFQjE.js → freemarker2-CUvQzJdx.js} +1 -1
  86. package/src/client/dist/pwa/assets/{handlebars-EwBi9SHt.js → handlebars-BXiAPlES.js} +1 -1
  87. package/src/client/dist/pwa/assets/{html-BOOCP_vn.js → html-CLmfyTMC.js} +1 -1
  88. package/src/client/dist/pwa/assets/{htmlMode-IN9oEIXW.js → htmlMode-DWYo6_e_.js} +1 -1
  89. package/src/client/dist/pwa/assets/i18n-CJclRByY.js +1 -0
  90. package/src/client/dist/pwa/assets/index-Bri8-39E.css +1 -0
  91. package/src/client/dist/pwa/assets/index-Dv1p-1sA.js +82 -0
  92. package/src/client/dist/pwa/assets/{javascript-CfV8qmeH.js → javascript-C9gWFLC_.js} +1 -1
  93. package/src/client/dist/pwa/assets/{jsonMode-C0b3m4VO.js → jsonMode-CbsjeFb8.js} +1 -1
  94. package/src/client/dist/pwa/assets/kobo-commands-DGygmOvc.js +9 -0
  95. package/src/client/dist/pwa/assets/layout-nWIyHeJf.js +1 -0
  96. package/src/client/dist/pwa/assets/{liquid-D3YK93J-.js → liquid-BqJ3SIha.js} +1 -1
  97. package/src/client/dist/pwa/assets/{lspLanguageFeatures-PklP2gEH.js → lspLanguageFeatures-BB0y1B51.js} +1 -1
  98. package/src/client/dist/pwa/assets/{mdx-DjZcevPS.js → mdx-rIxQLZDB.js} +1 -1
  99. package/src/client/dist/pwa/assets/network-auth-pzKTXRYP.js +1 -0
  100. package/src/client/dist/pwa/assets/notify-theme-D7RrwvJa.js +1 -0
  101. package/src/client/dist/pwa/assets/pending-diff-open-Cia8sndE.js +1 -0
  102. package/src/client/dist/pwa/assets/private.use-form-D8Yuc3aU.js +1 -0
  103. package/src/client/dist/pwa/assets/{project-color-Cc9yKGk3.js → project-color-Dur1z6Gn.js} +1 -1
  104. package/src/client/dist/pwa/assets/{python-BrgzhqrC.js → python-C6d5du9h.js} +1 -1
  105. package/src/client/dist/pwa/assets/{razor-g8NeeeQc.js → razor-Aek1yDc0.js} +1 -1
  106. package/src/client/dist/pwa/assets/{render-chat-markdown-CZyJL1-w.js → render-chat-markdown-B6sa44Xr.js} +2 -2
  107. package/src/client/dist/pwa/assets/skill-suite-prompts-DdoGRwhz.js +36 -0
  108. package/src/client/dist/pwa/assets/{tsMode-DAopv4hH.js → tsMode-rxNEyqyY.js} +1 -1
  109. package/src/client/dist/pwa/assets/{typescript-C2jZCVPF.js → typescript-DndxWYhC.js} +1 -1
  110. package/src/client/dist/pwa/assets/use-checkbox-z-Qs4ZHY.js +1 -0
  111. package/src/client/dist/pwa/assets/use-id-DlouqdB8.js +1 -0
  112. package/src/client/dist/pwa/assets/use-onboarding-BzSwN4xT.js +2 -0
  113. package/src/client/dist/pwa/assets/{vs-mk-rn9sP.js → vs-De8HKJ9r.js} +3 -3
  114. package/src/client/dist/pwa/assets/{workers-CP5iY9Kz.js → workers-DdNkyVm9.js} +1 -1
  115. package/src/client/dist/pwa/assets/{xml-D3pYNERH.js → xml-Cl1k1mQr.js} +1 -1
  116. package/src/client/dist/pwa/assets/{yaml-BTRPY5XP.js → yaml-C8G_k5TR.js} +1 -1
  117. package/src/client/dist/pwa/fonts/GEIST-LICENSE.txt +92 -0
  118. package/src/client/dist/pwa/fonts/Geist-Variable.woff2 +0 -0
  119. package/src/client/dist/pwa/fonts/GeistMono-Variable.woff2 +0 -0
  120. package/src/client/dist/pwa/index.html +7 -5
  121. package/src/client/dist/pwa/sw.js +1 -1
  122. package/src/mcp-server/kobo-tasks-server.ts +8 -7
  123. package/dist/server/utils/process-tracker.js +0 -49
  124. package/src/client/dist/pwa/assets/ActivityFeed-BrjeLlhi.css +0 -1
  125. package/src/client/dist/pwa/assets/ActivityFeed-WBBk7DgE.js +0 -8
  126. package/src/client/dist/pwa/assets/ChangelogPage-Cr5cW9Wx.js +0 -1
  127. package/src/client/dist/pwa/assets/ChangelogPage-DGhfnkax.css +0 -1
  128. package/src/client/dist/pwa/assets/ClosePopup-2KSSXX6P.js +0 -1
  129. package/src/client/dist/pwa/assets/CreatePage-Bl1Vjr27.css +0 -1
  130. package/src/client/dist/pwa/assets/CreatePage-DQk8TrUP.js +0 -2
  131. package/src/client/dist/pwa/assets/DiffViewer-C9xV8dh-.js +0 -8
  132. package/src/client/dist/pwa/assets/DiffViewer-CS3P9Ngi.css +0 -1
  133. package/src/client/dist/pwa/assets/DrawerToggleButton-CjM28eaw.js +0 -1
  134. package/src/client/dist/pwa/assets/HealthPage-BbLpLRjV.js +0 -1
  135. package/src/client/dist/pwa/assets/HealthPage-BqirO5le.css +0 -1
  136. package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BCOjigVR.woff +0 -0
  137. package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-Ocp2tNig.woff +0 -0
  138. package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-6jEn9xEl.woff +0 -0
  139. package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CCs8I9zJ.woff +0 -0
  140. package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-BLaboPi0.woff +0 -0
  141. package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-DHuK6FBl.woff +0 -0
  142. package/src/client/dist/pwa/assets/MainLayout-BUril7SK.js +0 -19
  143. package/src/client/dist/pwa/assets/MainLayout-u-AM87m3.css +0 -1
  144. package/src/client/dist/pwa/assets/QBadge-DS9cpnyx.js +0 -1
  145. package/src/client/dist/pwa/assets/QBanner-tANb95XA.js +0 -1
  146. package/src/client/dist/pwa/assets/QBtn-DJGkBel8.js +0 -1
  147. package/src/client/dist/pwa/assets/QBtnToggle-D-Hsqmcb.js +0 -1
  148. package/src/client/dist/pwa/assets/QChip-iLXQ5Khf.js +0 -1
  149. package/src/client/dist/pwa/assets/QItemSection-B-5iFRBZ.js +0 -1
  150. package/src/client/dist/pwa/assets/QList-D-yd1vEv.js +0 -1
  151. package/src/client/dist/pwa/assets/QSelect-TFRaHbzG.js +0 -36
  152. package/src/client/dist/pwa/assets/QSeparator-C_2BuEw9.js +0 -1
  153. package/src/client/dist/pwa/assets/QSpace-BuMMeXnK.js +0 -1
  154. package/src/client/dist/pwa/assets/QSpinnerDots-C-tv95pD.js +0 -1
  155. package/src/client/dist/pwa/assets/SearchPage-CPM7L9_M.js +0 -1
  156. package/src/client/dist/pwa/assets/SearchPage-CngtAqls.css +0 -1
  157. package/src/client/dist/pwa/assets/SettingsPage-BRYycuYY.js +0 -16
  158. package/src/client/dist/pwa/assets/SettingsPage-D5BacmAr.css +0 -1
  159. package/src/client/dist/pwa/assets/TouchPan-79_u9-Fd.js +0 -1
  160. package/src/client/dist/pwa/assets/WorkspacePage-DB1n1bmg.js +0 -4
  161. package/src/client/dist/pwa/assets/WorkspacePage-hxEpXqBA.css +0 -1
  162. package/src/client/dist/pwa/assets/documents-BMvE9Q_U.js +0 -1
  163. package/src/client/dist/pwa/assets/engineFeatures-JkVb6hry.js +0 -1
  164. package/src/client/dist/pwa/assets/expand-template-D2yUa54D.css +0 -1
  165. package/src/client/dist/pwa/assets/expand-template-Dl4wUjF8.js +0 -1
  166. package/src/client/dist/pwa/assets/i18n-Dezgz2hU.js +0 -1
  167. package/src/client/dist/pwa/assets/index-B12jWMl8.js +0 -82
  168. package/src/client/dist/pwa/assets/index-CK1nk7qr.css +0 -1
  169. package/src/client/dist/pwa/assets/kobo-commands-DznaKlq6.js +0 -9
  170. package/src/client/dist/pwa/assets/layout-me0gwmBV.js +0 -1
  171. package/src/client/dist/pwa/assets/network-auth-BMTuTKHx.js +0 -1
  172. package/src/client/dist/pwa/assets/pending-diff-open-BG3IOnWw.js +0 -1
  173. package/src/client/dist/pwa/assets/use-checkbox-D-9LXk-S.js +0 -1
  174. package/src/client/dist/pwa/assets/use-is-mobile-DX2Fiee-.js +0 -1
  175. package/src/client/dist/pwa/assets/use-onboarding-BNo8mb65.js +0 -2
@@ -2,6 +2,8 @@ import { query, } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { nanoid } from 'nanoid';
3
3
  import { isWorkspacePermissionAllowed } from '../../../workspace-permission-policy-service.js';
4
4
  import { createStreamingBatcher } from '../../streaming-batcher.js';
5
+ import { createTurnLiveness } from '../../turn-liveness.js';
6
+ import { AGENT_NO_LONGER_RUNNING_TEXT, } from '../types.js';
5
7
  import { CLAUDE_CODE_CAPABILITIES } from './capabilities.js';
6
8
  import { createMapperState, mapSdkMessage, QUOTA_PATTERN, tryEmitQuota } from './event-mapper.js';
7
9
  import { buildClaudeOptions } from './options-builder.js';
@@ -16,10 +18,14 @@ import { buildStopHookOutput } from './stop-hook.js';
16
18
  * and auto-loop are not frozen forever.
17
19
  */
18
20
  const RESULT_DRAIN_TIMEOUT_MS = 15_000;
19
- // Claude may occasionally leave its async iterator open after its final text
20
- // without producing a result message. Keep a separate, conservative guard for
21
- // that shape; active tool/message traffic always cancels and re-arms it.
22
- const TEXT_IDLE_TIMEOUT_MS = 120_000;
21
+ /**
22
+ * Deadline for a stream that stops reporting ANY activity. The former
23
+ * text-only guard was re-armed solely on a message with text and no tool call
24
+ * — the minority shape — so a run frozen mid-tool-call had no timer at all,
25
+ * `isAlive()` kept answering true, and the workspace stayed `executing`
26
+ * forever with zero trace. Exported so tests never hard-code the value.
27
+ */
28
+ export const CLAUDE_STREAM_IDLE_TIMEOUT_MS = 120_000;
23
29
  // Safety net for a subagent whose terminal `task_notification` carries a
24
30
  // status outside the mapper's known-terminal set (SDK schema drift) — it
25
31
  // never clears `activeSubagentToolCallIds`, which otherwise blocks
@@ -119,9 +125,9 @@ export function createClaudeCodeEngine() {
119
125
  return new Promise((resolve, reject) => {
120
126
  const resolver = { resolve, input, requestKind };
121
127
  pendingResolvers.set(toolCallId, resolver);
122
- // A user decision is intentional inactivity. The text-only watchdog
123
- // must never end this session while the permission card is visible.
124
- clearTextIdleWatchdog();
128
+ // A user decision is intentional inactivity — suspend the deadline
129
+ // rather than cancel it, so it resumes the moment the card is answered.
130
+ turnLiveness.pause();
125
131
  const onAbort = () => {
126
132
  if (pendingResolvers.get(toolCallId) === resolver) {
127
133
  pendingResolvers.delete(toolCallId);
@@ -270,21 +276,13 @@ export function createClaudeCodeEngine() {
270
276
  // background subagent is active is not terminal: the SDK can still emit
271
277
  // its task notification and an automatic continuation turn.
272
278
  let resultDrainTimer;
273
- let textIdleTimer;
274
- const clearTextIdleWatchdog = () => {
275
- if (!textIdleTimer)
276
- return;
277
- clearTimeout(textIdleTimer);
278
- textIdleTimer = undefined;
279
- };
280
- const armTextIdleWatchdog = () => {
281
- clearTextIdleWatchdog();
282
- textIdleTimer = setTimeout(() => {
283
- if (pendingResolvers.size > 0) {
284
- textIdleTimer = undefined;
285
- return;
286
- }
287
- console.warn(`[claude-engine] SDK stream inactive ${TEXT_IDLE_TIMEOUT_MS}ms after a text-only response — forcing session:ended`);
279
+ // D2 — one shared liveness module for both engines. Armed before the
280
+ // first SDK message, re-armed on EVERY message, suspended while a human
281
+ // decision or a background subagent is outstanding, stopped in `finally`.
282
+ const turnLiveness = createTurnLiveness({
283
+ timeoutMs: CLAUDE_STREAM_IDLE_TIMEOUT_MS,
284
+ onTimeout() {
285
+ console.warn(`[claude-engine] SDK stream reported no activity for ${CLAUDE_STREAM_IDLE_TIMEOUT_MS}ms — forcing session:ended`);
288
286
  if (userInterrupted)
289
287
  emitSessionEnded('killed', null);
290
288
  else if (mapperState.sawErrorResult)
@@ -292,9 +290,8 @@ export function createClaudeCodeEngine() {
292
290
  else
293
291
  emitSessionEnded('watchdog', null);
294
292
  abortController.abort();
295
- }, TEXT_IDLE_TIMEOUT_MS);
296
- textIdleTimer.unref?.();
297
- };
293
+ },
294
+ });
298
295
  const armResultDrainWatchdog = () => {
299
296
  if (resultDrainTimer)
300
297
  return;
@@ -347,6 +344,7 @@ export function createClaudeCodeEngine() {
347
344
  };
348
345
  const iteratorPromise = (async () => {
349
346
  iteratorRunning = true;
347
+ turnLiveness.start();
350
348
  try {
351
349
  for await (const msg of q) {
352
350
  const events = mapSdkMessage(msg, mapperState);
@@ -374,15 +372,17 @@ export function createClaudeCodeEngine() {
374
372
  discoveredSessionId = ev.engineSessionId;
375
373
  safeEmit(ev);
376
374
  }
377
- // A plain final response is normally followed by `result`. If the
378
- // SDK instead leaves the iterator parked, release the workspace
379
- // after a conservative quiet period. Tool activity never arms it.
380
- clearTextIdleWatchdog();
381
- if (events.some((ev) => ev.kind === 'message:text') && !events.some((ev) => ev.kind === 'tool:call')) {
382
- armTextIdleWatchdog();
383
- }
375
+ // Any SDK message is proof of life — re-arm on EVERY message, not
376
+ // only on a text-without-tool-call one.
377
+ turnLiveness.activity();
378
+ // A background subagent or a pending permission card can stay
379
+ // legitimately quiet for minutes; their own dedicated watchdogs
380
+ // own those windows, so suspend the turn deadline meanwhile.
381
+ if (activeSubagentTaskIds.size > 0 || pendingResolvers.size > 0)
382
+ turnLiveness.pause();
383
+ else
384
+ turnLiveness.resume();
384
385
  if (msg.type === 'result') {
385
- clearTextIdleWatchdog();
386
386
  completedResponses++;
387
387
  emitTurnCompletedIfSettled();
388
388
  // A queued forced message starts the next response on this same SDK stream.
@@ -438,7 +438,7 @@ export function createClaudeCodeEngine() {
438
438
  clearTimeout(resultDrainTimer);
439
439
  resultDrainTimer = undefined;
440
440
  }
441
- clearTextIdleWatchdog();
441
+ turnLiveness.stop();
442
442
  clearSubagentStallWatchdog();
443
443
  // Drain any callback still pending (SDK terminated while awaiting an
444
444
  // answer). canUseTool's abort path covers signalled stops; this
@@ -468,7 +468,7 @@ export function createClaudeCodeEngine() {
468
468
  },
469
469
  sendMessage(text) {
470
470
  if (!iteratorRunning)
471
- throw new Error('Claude agent is no longer running');
471
+ throw new Error(`Claude ${AGENT_NO_LONGER_RUNNING_TEXT}`);
472
472
  inputStream.send(text);
473
473
  },
474
474
  interrupt() {
@@ -524,6 +524,10 @@ export function createClaudeCodeEngine() {
524
524
  if (!resolver)
525
525
  return false;
526
526
  pendingResolvers.delete(toolCallId);
527
+ // Only resume the deadline once every outstanding card is answered —
528
+ // a sibling request may still be waiting on a human.
529
+ if (pendingResolvers.size === 0)
530
+ turnLiveness.resume();
527
531
  if (response.kind === 'question') {
528
532
  // Echo the original questions array + answers so the SDK
529
533
  // reconstructs the AskUserQuestion tool input.
@@ -1,4 +1,13 @@
1
1
  import { createJsonRpcPeer } from './jsonrpc/peer.js';
2
+ /**
3
+ * A handshake that never answers is a broken install (missing binary, expired
4
+ * token, protocol drift), not a model thinking — fail fast and loudly.
5
+ */
6
+ export const CODEX_HANDSHAKE_TIMEOUT_MS = 20_000;
7
+ /** Thread creation/resume can touch disk and the network; give it more room. */
8
+ export const CODEX_THREAD_TIMEOUT_MS = 30_000;
9
+ /** An interrupt that is not acknowledged must never block the escalation path. */
10
+ export const CODEX_INTERRUPT_TIMEOUT_MS = 10_000;
2
11
  export function createAppServerClient(opts) {
3
12
  const peer = createJsonRpcPeer({
4
13
  stdin: opts.stdin,
@@ -15,22 +24,25 @@ export function createAppServerClient(opts) {
15
24
  clientInfo: opts.clientInfo,
16
25
  capabilities: { experimentalApi: true },
17
26
  };
18
- return peer.request('initialize', params);
27
+ return peer.request('initialize', params, CODEX_HANDSHAKE_TIMEOUT_MS);
19
28
  },
20
29
  startThread(params) {
21
- return peer.request('thread/start', params);
30
+ return peer.request('thread/start', params, CODEX_THREAD_TIMEOUT_MS);
22
31
  },
23
32
  resumeThread(params) {
24
- return peer.request('thread/resume', params);
33
+ return peer.request('thread/resume', params, CODEX_THREAD_TIMEOUT_MS);
25
34
  },
26
35
  startTurn(params) {
36
+ // Default deadline: starting a turn is the model's own latency budget.
27
37
  return peer.request('turn/start', params);
28
38
  },
29
39
  steerTurn(params) {
40
+ // Same budget. Steering requests are chained, so a request left pending
41
+ // here used to block every later message of the workspace for good.
30
42
  return peer.request('turn/steer', params);
31
43
  },
32
44
  async interruptTurn(params) {
33
- await peer.request('turn/interrupt', params);
45
+ await peer.request('turn/interrupt', params, CODEX_INTERRUPT_TIMEOUT_MS);
34
46
  },
35
47
  close() {
36
48
  peer.close();
@@ -1,13 +1,14 @@
1
1
  import { getPackageVersion } from '../../../../utils/paths.js';
2
2
  import { isWorkspacePermissionAllowed } from '../../../workspace-permission-policy-service.js';
3
3
  import { createStreamingBatcher } from '../../streaming-batcher.js';
4
+ import { createTurnLiveness } from '../../turn-liveness.js';
5
+ import { AGENT_NO_LONGER_RUNNING_TEXT, } from '../types.js';
4
6
  import { CODEX_CAPABILITIES } from './capabilities.js';
5
7
  import { createAppServerClient } from './client.js';
6
8
  import { createMapperState, emitSessionStarted, handleAgentMessageDelta, handleItemCompleted, handleItemStarted, handleRateLimitsUpdated, handleTurnCompleted, QUOTA_PATTERN, tryEmitQuota, } from './event-mapper.js';
7
9
  import { buildCodexOptions } from './options-builder.js';
8
10
  import { buildResponseForResolve, handleServerRequest } from './server-requests.js';
9
11
  import { spawnAppServer } from './spawn.js';
10
- import { createTurnLiveness } from './turn-liveness.js';
11
12
  /** Long enough for normal tool work, short enough to recover a lost turn event. */
12
13
  export const CODEX_TURN_IDLE_TIMEOUT_MS = 120_000;
13
14
  export const CODEX_GRACEFUL_INTERRUPT_TIMEOUT_MS = 3_000;
@@ -329,6 +330,11 @@ export function createCodexEngine() {
329
330
  const iteratorPromise = (async () => {
330
331
  iteratorRunning = true;
331
332
  try {
333
+ // Armed BEFORE the handshake (D2). Previously the probe only started
334
+ // once `turn/start` had answered, leaving initialize / thread.start /
335
+ // turn.start entirely uncovered — the exact window where a broken
336
+ // install hangs.
337
+ turnLiveness.start();
332
338
  await waitForChild(client.connect());
333
339
  if (isResume && options.resumeFromEngineSessionId) {
334
340
  await waitForChild(client.resumeThread({
@@ -358,7 +364,7 @@ export function createCodexEngine() {
358
364
  collaborationMode,
359
365
  }));
360
366
  activeTurnId = initialTurn.turnId;
361
- turnLiveness.start();
367
+ turnLiveness.activity();
362
368
  readySettled = true;
363
369
  resolveReady();
364
370
  await waitForChild(turnDonePromise);
@@ -442,6 +448,21 @@ export function createCodexEngine() {
442
448
  sendMessage(text) {
443
449
  const steer = async () => {
444
450
  await readyPromise;
451
+ // `readyPromise` only ever resolves once, on the FIRST turn
452
+ // reaching `turn/start` — it says nothing about whether the
453
+ // session has since ended. A one-prompt-one-turn session that
454
+ // completed naturally (or was stopped) already ran the `finally`
455
+ // block below: `client.close()` tore down the JSON-RPC peer and
456
+ // `child.kill('SIGTERM')` killed the process. Without this check
457
+ // a message typed after that point would either write to an
458
+ // already-closed peer or sit for the full 120s JSON-RPC timeout
459
+ // before failing — and neither shape was recognised as "resume
460
+ // instead of rejecting". Fail fast instead, reusing the same
461
+ // substring Claude's engine throws once its stdin is closed, so
462
+ // the orchestrator's existing pattern covers this case too.
463
+ if (!iteratorRunning) {
464
+ throw new Error(`Codex ${AGENT_NO_LONGER_RUNNING_TEXT}`);
465
+ }
445
466
  if (!discoveredSessionId || !activeTurnId) {
446
467
  throw new Error('Codex session is not ready to receive a message');
447
468
  }
@@ -1,7 +1,35 @@
1
1
  import { createJsonRpcTransport } from './transport.js';
2
+ /**
3
+ * Default per-request deadline. A live-but-mute app-server used to park a
4
+ * request forever: the child-exit guard only watches for an exit, the turn
5
+ * liveness probe was armed after the handshake, and the cleanup block that
6
+ * would close the connection is never reached while a `try` awaits. The
7
+ * session hung for good, with no trace and no event.
8
+ */
9
+ export const DEFAULT_JSONRPC_REQUEST_TIMEOUT_MS = 120_000;
10
+ export class JsonRpcTimeoutError extends Error {
11
+ method;
12
+ timeoutMs;
13
+ constructor(method, timeoutMs) {
14
+ super(`JSON-RPC request '${method}' timed out after ${timeoutMs}ms`);
15
+ this.method = method;
16
+ this.timeoutMs = timeoutMs;
17
+ this.name = 'JsonRpcTimeoutError';
18
+ }
19
+ }
2
20
  export function createJsonRpcPeer(opts) {
3
21
  let nextId = 1;
4
22
  const pending = new Map();
23
+ /** Remove a pending slot and clear its deadline in one place. */
24
+ const takePending = (id) => {
25
+ const slot = pending.get(id);
26
+ if (!slot)
27
+ return undefined;
28
+ if (slot.timer)
29
+ clearTimeout(slot.timer);
30
+ pending.delete(id);
31
+ return slot;
32
+ };
5
33
  const transport = createJsonRpcTransport({
6
34
  stdin: opts.stdin,
7
35
  stdout: opts.stdout,
@@ -9,10 +37,9 @@ export function createJsonRpcPeer(opts) {
9
37
  onMessage(msg) {
10
38
  // Response to one of our requests
11
39
  if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
12
- const slot = pending.get(msg.id);
40
+ const slot = takePending(msg.id);
13
41
  if (!slot)
14
42
  return;
15
- pending.delete(msg.id);
16
43
  if (msg.error) {
17
44
  slot.reject(new Error(`JSON-RPC error ${msg.error.code}: ${msg.error.message}`));
18
45
  }
@@ -33,10 +60,19 @@ export function createJsonRpcPeer(opts) {
33
60
  },
34
61
  });
35
62
  return {
36
- request(method, params) {
63
+ request(method, params, timeoutMs) {
37
64
  const id = nextId++;
65
+ const deadlineMs = timeoutMs ?? opts.defaultRequestTimeoutMs ?? DEFAULT_JSONRPC_REQUEST_TIMEOUT_MS;
38
66
  return new Promise((resolve, reject) => {
39
- pending.set(id, { resolve: (v) => resolve(v), reject });
67
+ const slot = { resolve: (v) => resolve(v), reject };
68
+ if (deadlineMs > 0) {
69
+ slot.timer = setTimeout(() => {
70
+ pending.delete(id);
71
+ reject(new JsonRpcTimeoutError(method, deadlineMs));
72
+ }, deadlineMs);
73
+ slot.timer.unref?.();
74
+ }
75
+ pending.set(id, slot);
40
76
  transport.send({ jsonrpc: '2.0', id, method, params });
41
77
  });
42
78
  },
@@ -52,8 +88,11 @@ export function createJsonRpcPeer(opts) {
52
88
  close() {
53
89
  transport.close();
54
90
  const err = new Error('peer closed');
55
- for (const slot of pending.values())
91
+ for (const slot of pending.values()) {
92
+ if (slot.timer)
93
+ clearTimeout(slot.timer);
56
94
  slot.reject(err);
95
+ }
57
96
  pending.clear();
58
97
  },
59
98
  };
@@ -1,3 +1,16 @@
1
+ // ── Engine contract ───────────────────────────────────────────────────────────
2
+ /**
3
+ * Substring an engine's error message must contain once its session has
4
+ * fully ended and it can no longer receive a message. The Claude engine
5
+ * throws `Claude agent is no longer running`; the Codex engine's
6
+ * post-session-end guard in `codex/engine.ts` reuses this same substring so a
7
+ * single already-recognized `isAgentUnavailableError` pattern in the
8
+ * orchestrator covers both engines, instead of growing that list with a
9
+ * fourth, engine-specific string. Kept here — a module neither engine's
10
+ * business logic depends on — purely to give the two call sites a shared
11
+ * anchor; it is not itself part of the `AgentEngine` interface.
12
+ */
13
+ export const AGENT_NO_LONGER_RUNNING_TEXT = 'agent is no longer running';
1
14
  /** Every AgentEvent kind, as a const for exhaustive iteration in tests. */
2
15
  export const ALL_AGENT_EVENT_KINDS = [
3
16
  'session:started',