@loicngr/kobo 1.11.43 → 1.11.45

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 (166) hide show
  1. package/AGENTS.md +74 -1
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +13 -1
  4. package/dist/mcp-server/kobo-tasks-handlers.js +33 -75
  5. package/dist/mcp-server/kobo-tasks-server.js +82 -9
  6. package/dist/server/db/auto-loop-schema.js +44 -0
  7. package/dist/server/db/migrations.js +18 -0
  8. package/dist/server/db/review-return-schema.js +10 -0
  9. package/dist/server/db/schema.js +8 -1
  10. package/dist/server/db/session-handoff-schema.js +24 -0
  11. package/dist/server/index.js +12 -9
  12. package/dist/server/routes/auto-loop-messages.js +51 -0
  13. package/dist/server/routes/documents.js +1 -1
  14. package/dist/server/routes/workspaces.js +119 -145
  15. package/dist/server/services/agent/engines/claude-code/engine.js +16 -0
  16. package/dist/server/services/agent/engines/codex/engine.js +92 -31
  17. package/dist/server/services/agent/orchestrator.js +298 -96
  18. package/dist/server/services/agent/session-controller.js +42 -2
  19. package/dist/server/services/auto-loop-message-service.js +79 -0
  20. package/dist/server/services/auto-loop-service.js +225 -86
  21. package/dist/server/services/auto-loop-state-service.js +30 -0
  22. package/dist/server/services/cron-service.js +30 -16
  23. package/dist/server/services/engine-handoff-service.js +5 -5
  24. package/dist/server/services/quota-backoff-service.js +42 -10
  25. package/dist/server/services/review-return-service.js +64 -0
  26. package/dist/server/services/review-service.js +187 -0
  27. package/dist/server/services/session-activity-service.js +17 -0
  28. package/dist/server/services/session-handoff-runtime.js +26 -0
  29. package/dist/server/services/session-handoff-service.js +479 -0
  30. package/dist/server/services/settings-service.js +3 -3
  31. package/dist/server/services/task-mutations.js +186 -0
  32. package/dist/server/services/wakeup-service.js +24 -1
  33. package/dist/server/services/workspace-message-service.js +16 -2
  34. package/dist/server/services/workspace-service.js +28 -36
  35. package/dist/server/utils/workspace-lifecycle-guard.js +35 -20
  36. package/dist/shared/auto-loop-prompts.js +22 -11
  37. package/dist/shared/auto-loop-types.js +1 -0
  38. package/dist/shared/document-roots.js +2 -0
  39. package/dist/shared/review.js +6 -0
  40. package/dist/shared/session-handoff.js +4 -0
  41. package/dist/shared/task-verification.js +11 -0
  42. package/package.json +12 -12
  43. package/src/client/dist/pwa/assets/ActivityFeed-CLXjdqS8.js +8 -0
  44. package/src/client/dist/pwa/assets/{ChangelogPage-DX1WGFea.js → ChangelogPage-CnXTnFhe.js} +1 -1
  45. package/src/client/dist/pwa/assets/{ClosePopup-BqW5dy-O.js → ClosePopup-CWjIRHxu.js} +1 -1
  46. package/src/client/dist/pwa/assets/{ComparisonPanel-B4ne55Xj.js → ComparisonPanel-CKylN45q.js} +2 -2
  47. package/src/client/dist/pwa/assets/CreatePage-SoCcvxuD.js +2 -0
  48. package/src/client/dist/pwa/assets/{DashboardPage-Cv6HJEcO.js → DashboardPage-CZQLfoV1.js} +1 -1
  49. package/src/client/dist/pwa/assets/{DiffViewer-COwGxPt6.js → DiffViewer-Csf3WmLc.js} +4 -4
  50. package/src/client/dist/pwa/assets/{DocumentsPanel-DCXpLLb7.js → DocumentsPanel-BeCttB-x.js} +1 -1
  51. package/src/client/dist/pwa/assets/{DrawerToggleButton-DRL3RCkM.js → DrawerToggleButton-BJBBYeOc.js} +1 -1
  52. package/src/client/dist/pwa/assets/GitPanel-B-ZHgghQ.js +3 -0
  53. package/src/client/dist/pwa/assets/{HealthPage-XrOuMoDJ.js → HealthPage-l88TEk-k.js} +1 -1
  54. package/src/client/dist/pwa/assets/MainLayout-BAAQLq95.css +1 -0
  55. package/src/client/dist/pwa/assets/MainLayout-Cf4Q9x1u.js +2 -0
  56. package/src/client/dist/pwa/assets/{QBadge-DbkxGtht.js → QBadge-FWC1zDCq.js} +1 -1
  57. package/src/client/dist/pwa/assets/{QBanner-DhqH95GW.js → QBanner-CcRI_QIy.js} +1 -1
  58. package/src/client/dist/pwa/assets/QBtn-DEdx_Y1V.js +1 -0
  59. package/src/client/dist/pwa/assets/{QBtnDropdown-163oM6T_.js → QBtnDropdown-44Mj-y_1.js} +1 -1
  60. package/src/client/dist/pwa/assets/{QBtnGroup-DELKYo3u.js → QBtnGroup-CAoD6G0Q.js} +1 -1
  61. package/src/client/dist/pwa/assets/{QBtnToggle-BI3k3Ch2.js → QBtnToggle-C_hQ2UCQ.js} +1 -1
  62. package/src/client/dist/pwa/assets/{QChip-owciRuZu.js → QChip-DCPUhqXX.js} +1 -1
  63. package/src/client/dist/pwa/assets/QDrawer-B4qHw8wt.js +1 -0
  64. package/src/client/dist/pwa/assets/QExpansionItem-Bok1m8Wi.js +1 -0
  65. package/src/client/dist/pwa/assets/{QItemSection-D_5_0UrX.js → QItemSection-BhqphaH6.js} +1 -1
  66. package/src/client/dist/pwa/assets/{QList-qupzuEqO.js → QList-CHm4JNP0.js} +1 -1
  67. package/src/client/dist/pwa/assets/{QMarkupTable-B9QuKtK_.js → QMarkupTable-CNpiA9oq.js} +1 -1
  68. package/src/client/dist/pwa/assets/{QPage-DaX9sMQC.js → QPage-BXBw2Alx.js} +1 -1
  69. package/src/client/dist/pwa/assets/QSelect-BbtD5Iks.js +1 -0
  70. package/src/client/dist/pwa/assets/{QSpinnerDots-DnCV3VzI.js → QSpinnerDots-BkFiCsH3.js} +1 -1
  71. package/src/client/dist/pwa/assets/{QTooltip-BQWY7aeM.js → QTooltip-BDihn2aY.js} +1 -1
  72. package/src/client/dist/pwa/assets/SchedulePanel-DiWCDNZ8.js +1 -0
  73. package/src/client/dist/pwa/assets/{SearchPage-DOHkP5lj.js → SearchPage-BwZ2TX1G.js} +1 -1
  74. package/src/client/dist/pwa/assets/SettingsPage-BUh0WfNf.js +9 -0
  75. package/src/client/dist/pwa/assets/{SplitWorkspacePage-DL4kp9Ah.js → SplitWorkspacePage-Bg6nKBcA.js} +1 -1
  76. package/src/client/dist/pwa/assets/{TerminalPanel-DHDnyiYh.js → TerminalPanel-Cq-FR7A7.js} +9 -9
  77. package/src/client/dist/pwa/assets/{TouchPan-BnHXDR6F.js → TouchPan-BQ1cvzRC.js} +1 -1
  78. package/src/client/dist/pwa/assets/{TourReplayButton-ClVcB6mO.js → TourReplayButton-Dz1yEz_z.js} +1 -1
  79. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-Bxe6oIJI.js → WorkspaceAttentionLabels-BCVOP8vp.js} +1 -1
  80. package/src/client/dist/pwa/assets/WorkspacePage-BbUvR1Vf.css +1 -0
  81. package/src/client/dist/pwa/assets/WorkspacePage-Cl8gap4b.js +4 -0
  82. package/src/client/dist/pwa/assets/{browser-Bkx4fbxl.js → browser-BmWgBx9M.js} +1 -1
  83. package/src/client/dist/pwa/assets/build-path-tree-6EjBmYYY.js +1 -0
  84. package/src/client/dist/pwa/assets/de-COObokpu.js +17 -0
  85. package/src/client/dist/pwa/assets/documents-BZ8bwcjr.js +1 -0
  86. package/src/client/dist/pwa/assets/{editor.api-DreEG0If.js → editor.api-CK7--ckY.js} +1 -1
  87. package/src/client/dist/pwa/assets/editor.worker-iO5iN0DB.js +33 -0
  88. package/src/client/dist/pwa/assets/efforts-Bw0o2Bud.js +9 -0
  89. package/src/client/dist/pwa/assets/engineFeatures-B-9jHocY.css +1 -0
  90. package/src/client/dist/pwa/assets/engineFeatures-DQO9HHXI.js +1 -0
  91. package/src/client/dist/pwa/assets/es-By2Q9Tca.js +17 -0
  92. package/src/client/dist/pwa/assets/expand-template-C8VyjASo.js +1 -0
  93. package/src/client/dist/pwa/assets/{format-B7z3F-Mw.js → format-LtTLLitJ.js} +3 -3
  94. package/src/client/dist/pwa/assets/{formatters-BTBiU0v3.js → formatters-Bl9zA7Ai.js} +1 -1
  95. package/src/client/dist/pwa/assets/fr-D-sd0ddn.js +17 -0
  96. package/src/client/dist/pwa/assets/{freemarker2-BZNTVQ3K.js → freemarker2-DxezMaje.js} +1 -1
  97. package/src/client/dist/pwa/assets/{handlebars-k8tN0dI3.js → handlebars-DK_Bj6Rx.js} +1 -1
  98. package/src/client/dist/pwa/assets/{html-iZL3bbXq.js → html-DFfsfStD.js} +1 -1
  99. package/src/client/dist/pwa/assets/i18n-aDWy4KHt.js +1 -0
  100. package/src/client/dist/pwa/assets/index-0wtBh1Xt.css +1 -0
  101. package/src/client/dist/pwa/assets/index-BLd8DAzg.js +20 -0
  102. package/src/client/dist/pwa/assets/it-CeK5lyIA.js +17 -0
  103. package/src/client/dist/pwa/assets/{javascript-TV6gg0MO.js → javascript-DuNbcaL7.js} +1 -1
  104. package/src/client/dist/pwa/assets/{liquid-Cifa5ujF.js → liquid-CM3JKyhv.js} +1 -1
  105. package/src/client/dist/pwa/assets/{mdx-Bnj_8yGE.js → mdx-BG3zYwNF.js} +1 -1
  106. package/src/client/dist/pwa/assets/{monaco-contributions-pEh0n9Wk.js → monaco-contributions-DLscZGH-.js} +1 -1
  107. package/src/client/dist/pwa/assets/monaco.contribution-DnhZHwXT.js +2 -0
  108. package/src/client/dist/pwa/assets/{network-auth-3_5p4fdc.js → network-auth-BJiPbO5l.js} +1 -1
  109. package/src/client/dist/pwa/assets/{notify-theme-D_nf9AeF.js → notify-theme-D4p3qPkp.js} +1 -1
  110. package/src/client/dist/pwa/assets/{pending-diff-open-CUnxC1xw.js → pending-diff-open-dYIFdS4r.js} +1 -1
  111. package/src/client/dist/pwa/assets/permissionModes-B7fuUegU.js +36 -0
  112. package/src/client/dist/pwa/assets/{pinia-BKqmoVru.js → pinia-CTJ3OfP3.js} +1 -1
  113. package/src/client/dist/pwa/assets/{private.use-form-C7umbGO-.js → private.use-form-Bk8ti264.js} +1 -1
  114. package/src/client/dist/pwa/assets/{project-color-CihwpDAA.js → project-color-R_1je4vW.js} +1 -1
  115. package/src/client/dist/pwa/assets/{python-DOoNBSNL.js → python-COs6pHk2.js} +1 -1
  116. package/src/client/dist/pwa/assets/{razor-Bvq5vVpJ.js → razor-Jl1VqMsk.js} +1 -1
  117. package/src/client/dist/pwa/assets/reactivity.esm-bundler-Bo9anJjg.js +1 -0
  118. package/src/client/dist/pwa/assets/render-chat-markdown-AaOo0Tgq.js +67 -0
  119. package/src/client/dist/pwa/assets/{runtime-core.esm-bundler-CDnVUfeZ.js → runtime-core.esm-bundler-CNFKm4ij.js} +1 -1
  120. package/src/client/dist/pwa/assets/{typescript-VVzxoeyM.js → typescript-BU_TUuAM.js} +1 -1
  121. package/src/client/dist/pwa/assets/{use-checkbox-FnauAgTp.js → use-checkbox-CnvHBhSK.js} +1 -1
  122. package/src/client/dist/pwa/assets/{use-id-BIuaWKkB.js → use-id-wXd4c8FA.js} +1 -1
  123. package/src/client/dist/pwa/assets/use-is-mobile-DpKI3XMy.js +1 -0
  124. package/src/client/dist/pwa/assets/{use-search-index-status-DtLzygcy.js → use-search-index-status-Bluf9L99.js} +1 -1
  125. package/src/client/dist/pwa/assets/use-slide-transition-CiaDm-tR.js +1 -0
  126. package/src/client/dist/pwa/assets/use-tours-CVn874kD.js +2 -0
  127. package/src/client/dist/pwa/assets/{workspace-templates-1ui80Kqo.js → workspace-templates-BCKR8TXr.js} +1 -1
  128. package/src/client/dist/pwa/assets/{xml-CzGFIxQa.js → xml-gFFsobxE.js} +1 -1
  129. package/src/client/dist/pwa/assets/{yaml-Bl7nDJeC.js → yaml-DaXaqaGD.js} +1 -1
  130. package/src/client/dist/pwa/index.html +10 -10
  131. package/src/client/dist/pwa/sw.js +1 -1
  132. package/src/mcp-server/README.md +8 -1
  133. package/src/mcp-server/kobo-tasks-handlers.ts +72 -105
  134. package/src/mcp-server/kobo-tasks-server.ts +94 -9
  135. package/src/client/dist/pwa/assets/ActivityFeed-D1e-zV3O.js +0 -8
  136. package/src/client/dist/pwa/assets/CreatePage-Bx_GpUni.js +0 -2
  137. package/src/client/dist/pwa/assets/GitPanel-C9mz4GXF.js +0 -3
  138. package/src/client/dist/pwa/assets/MainLayout-BwV89R-l.css +0 -1
  139. package/src/client/dist/pwa/assets/MainLayout-CwWavQjV.js +0 -2
  140. package/src/client/dist/pwa/assets/QBtn-CZaBclNw.js +0 -1
  141. package/src/client/dist/pwa/assets/QDrawer-DNEj8wLZ.js +0 -1
  142. package/src/client/dist/pwa/assets/QSelect-BUlOQe5k.js +0 -1
  143. package/src/client/dist/pwa/assets/SchedulePanel-Bos94OyR.js +0 -1
  144. package/src/client/dist/pwa/assets/SettingsPage-DtGsd0XH.js +0 -9
  145. package/src/client/dist/pwa/assets/WorkspacePage-Clc4Fl3B.js +0 -4
  146. package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +0 -1
  147. package/src/client/dist/pwa/assets/build-path-tree-BpSY7N5O.js +0 -1
  148. package/src/client/dist/pwa/assets/de-BU0dILBs.js +0 -17
  149. package/src/client/dist/pwa/assets/documents-BIlWh3o-.js +0 -1
  150. package/src/client/dist/pwa/assets/editor.worker-CwQAd_xr.js +0 -30
  151. package/src/client/dist/pwa/assets/efforts-D_yTzymv.js +0 -9
  152. package/src/client/dist/pwa/assets/engineFeatures-C33LTVkK.js +0 -1
  153. package/src/client/dist/pwa/assets/es-CNp0s9na.js +0 -17
  154. package/src/client/dist/pwa/assets/expand-template-wRhAXT-1.js +0 -1
  155. package/src/client/dist/pwa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  156. package/src/client/dist/pwa/assets/fr-RqJyWSdm.js +0 -17
  157. package/src/client/dist/pwa/assets/i18n--PCet00a.js +0 -1
  158. package/src/client/dist/pwa/assets/index-CPfrY38K.js +0 -20
  159. package/src/client/dist/pwa/assets/index-CUoUy6tH.css +0 -1
  160. package/src/client/dist/pwa/assets/it-DSxNuCwL.js +0 -17
  161. package/src/client/dist/pwa/assets/monaco.contribution-D7NmJUJH.js +0 -2
  162. package/src/client/dist/pwa/assets/permissionModes-DC51Pz84.js +0 -36
  163. package/src/client/dist/pwa/assets/reactivity.esm-bundler-DEJj9cFv.js +0 -1
  164. package/src/client/dist/pwa/assets/render-chat-markdown-BQ_saiy5.js +0 -67
  165. package/src/client/dist/pwa/assets/use-is-mobile-CWjqe8gC.js +0 -1
  166. package/src/client/dist/pwa/assets/use-tours-BKeYrJaQ.js +0 -2
@@ -124,14 +124,41 @@ export function createCodexEngine() {
124
124
  });
125
125
  void childFailurePromise.catch(() => { });
126
126
  const child = spawnAppServer({ cwd: options.workingDir, env: options.env, signal: abortController.signal });
127
+ let childExited = false;
128
+ let resolveChildExited;
129
+ const childExitedPromise = new Promise((resolve) => {
130
+ resolveChildExited = resolve;
131
+ });
132
+ const confirmChildExited = () => {
133
+ childExited = true;
134
+ resolveChildExited();
135
+ };
136
+ let shutdownPromise;
137
+ const terminateChild = () => {
138
+ if (childExited)
139
+ return Promise.resolve();
140
+ shutdownPromise ??= (async () => {
141
+ if (!(await signalAndWaitForExit(child, 'SIGTERM', CODEX_FORCE_KILL_TIMEOUT_MS))) {
142
+ console.warn('[codex] app-server ignored SIGTERM — sending SIGKILL');
143
+ if (!(await signalAndWaitForExit(child, 'SIGKILL', CODEX_FORCE_KILL_TIMEOUT_MS))) {
144
+ throw new Error('Codex app-server did not exit after SIGKILL');
145
+ }
146
+ }
147
+ confirmChildExited();
148
+ })();
149
+ return shutdownPromise;
150
+ };
127
151
  child.on('error', (error) => {
128
152
  const isExpectedAbort = abortController.signal.aborted && (error.code === 'ABORT_ERR' || error.name === 'AbortError');
129
153
  if (isExpectedAbort)
130
154
  return;
155
+ if (child.pid === undefined)
156
+ confirmChildExited();
131
157
  console.error('[codex] child process error:', error);
132
158
  rejectChildFailure(error);
133
159
  });
134
160
  child.once('exit', (code, signal) => {
161
+ confirmChildExited();
135
162
  if (!iteratorRunning || abortController.signal.aborted)
136
163
  return;
137
164
  const detail = code === null ? `signal ${signal ?? 'unknown'}` : `code ${code}`;
@@ -154,6 +181,7 @@ export function createCodexEngine() {
154
181
  let rejectTurnDone;
155
182
  const activeSubagentThreads = new Map();
156
183
  let waitingForBackgroundSubagents = false;
184
+ let watchdogEnded = false;
157
185
  const turnDonePromise = new Promise((resolve, reject) => {
158
186
  resolveTurnDone = resolve;
159
187
  rejectTurnDone = reject;
@@ -169,6 +197,8 @@ export function createCodexEngine() {
169
197
  // deadline tracks the last activity, not the start of the wait.
170
198
  const armSubagentStallWatchdog = () => {
171
199
  clearSubagentStallWatchdog();
200
+ if (!waitingForBackgroundSubagents || pendingByCallId.size > 0 || activeSubagentThreads.size === 0)
201
+ return;
172
202
  subagentStallTimer = setTimeout(() => {
173
203
  subagentStallTimer = undefined;
174
204
  console.warn(`[codex-engine] Background subagent thread(s) still tracked active ${CODEX_SUBAGENT_STALL_TIMEOUT_MS}ms after the turn completed — forcing session drain.`);
@@ -176,7 +206,13 @@ export function createCodexEngine() {
176
206
  // Forced, unclean termination (a thread may still be alive
177
207
  // server-side) — never report it as a normal completion, so
178
208
  // auto-loop doesn't treat an orphaned run as forward progress.
179
- mapperState.sawErrorResult = true;
209
+ watchdogEnded = true;
210
+ safeEmit({
211
+ kind: 'error',
212
+ category: 'other',
213
+ code: 'subagent_idle_timeout',
214
+ message: 'Session force-ended: background subagents stopped reporting activity (watchdog).',
215
+ });
180
216
  if (waitingForBackgroundSubagents)
181
217
  resolveTurnDone();
182
218
  }, CODEX_SUBAGENT_STALL_TIMEOUT_MS);
@@ -209,8 +245,13 @@ export function createCodexEngine() {
209
245
  const turnLiveness = createTurnLiveness({
210
246
  timeoutMs: CODEX_TURN_IDLE_TIMEOUT_MS,
211
247
  onTimeout() {
212
- mapperState.sawErrorResult = true;
213
- safeEmit({ kind: 'error', category: 'other', message: 'Codex stopped reporting activity for this turn' });
248
+ watchdogEnded = true;
249
+ safeEmit({
250
+ kind: 'error',
251
+ category: 'other',
252
+ code: 'stream_idle_timeout',
253
+ message: 'Codex stopped reporting activity for this turn',
254
+ });
214
255
  rejectTurnDone(new CodexTurnTimeoutError());
215
256
  },
216
257
  });
@@ -221,6 +262,21 @@ export function createCodexEngine() {
221
262
  rejectTurnDone(err);
222
263
  });
223
264
  const fileChanges = new Map();
265
+ const isParentTurn = (threadId, turnId) => (!threadId || threadId === discoveredSessionId) && (!activeTurnId || !turnId || turnId === activeTurnId);
266
+ const otherTurnMappers = new Map();
267
+ const mapTurnEvents = (notification, map) => {
268
+ if (isParentTurn(notification.threadId, notification.turnId))
269
+ return map(mapperState);
270
+ const key = JSON.stringify([notification.threadId, notification.turnId]);
271
+ let state = otherTurnMappers.get(key);
272
+ if (!state) {
273
+ state = createMapperState();
274
+ otherTurnMappers.set(key, state);
275
+ }
276
+ // Child output remains visible, but its failures/compaction/markers
277
+ // cannot control the parent session or its auto-loop lifecycle.
278
+ return map(state).filter((event) => event.kind !== 'error' && !event.kind.startsWith('session:'));
279
+ };
224
280
  const itemKey = (threadId, turnId, itemId) => JSON.stringify([threadId, turnId, itemId]);
225
281
  const client = createAppServerClient({
226
282
  stdin: child.stdin,
@@ -228,6 +284,10 @@ export function createCodexEngine() {
228
284
  clientInfo: { name: 'kobo', version: getPackageVersion() },
229
285
  onNotification(method, params) {
230
286
  turnLiveness.activity();
287
+ const notification = params;
288
+ if (notification?.threadId && activeSubagentThreads.has(notification.threadId)) {
289
+ armSubagentStallWatchdog();
290
+ }
231
291
  // Ignored notifications — harmless bookkeeping by the server
232
292
  if (method === 'mcpServer/startupStatus/updated') {
233
293
  const status = (params ?? {});
@@ -257,7 +317,7 @@ export function createCodexEngine() {
257
317
  const n = params;
258
318
  if (n.item.type === 'fileChange')
259
319
  fileChanges.set(itemKey(n.threadId, n.turnId, n.item.id), n.item);
260
- const events = handleItemStarted(n.item, mapperState);
320
+ const events = mapTurnEvents(n, (state) => handleItemStarted(n.item, state));
261
321
  for (const ev of events)
262
322
  safeEmit(ev);
263
323
  if (n.item.type === 'collabAgentToolCall' && n.item.tool === 'spawnAgent') {
@@ -278,7 +338,7 @@ export function createCodexEngine() {
278
338
  const n = params;
279
339
  if (n.item.type === 'fileChange')
280
340
  fileChanges.set(itemKey(n.threadId, n.turnId, n.item.id), n.item);
281
- const events = handleItemCompleted(n.item, mapperState);
341
+ const events = mapTurnEvents(n, (state) => handleItemCompleted(n.item, state));
282
342
  for (const ev of events)
283
343
  safeEmit(ev);
284
344
  if (n.item.type === 'collabAgentToolCall') {
@@ -302,7 +362,7 @@ export function createCodexEngine() {
302
362
  }
303
363
  if (method === 'item/agentMessage/delta') {
304
364
  const n = params;
305
- for (const ev of handleAgentMessageDelta(n, mapperState))
365
+ for (const ev of mapTurnEvents(n, (state) => handleAgentMessageDelta(n, state)))
306
366
  safeEmit(ev);
307
367
  return;
308
368
  }
@@ -313,9 +373,9 @@ export function createCodexEngine() {
313
373
  if (threadId === n.threadId && turnId === n.turn.id)
314
374
  fileChanges.delete(key);
315
375
  }
316
- for (const ev of handleTurnCompleted(n, mapperState))
317
- safeEmit(ev);
318
- if (!activeTurnId || !n.turn?.id || n.turn.id === activeTurnId) {
376
+ if (isParentTurn(n.threadId, n.turn.id)) {
377
+ for (const ev of handleTurnCompleted(n, mapperState))
378
+ safeEmit(ev);
319
379
  if (n.turn?.status === 'completed' && activeSubagentThreads.size > 0) {
320
380
  waitingForBackgroundSubagents = true;
321
381
  turnLiveness.pause();
@@ -349,6 +409,8 @@ export function createCodexEngine() {
349
409
  }
350
410
  if (method === 'error') {
351
411
  const n = params;
412
+ if (!isParentTurn(n.threadId, n.turnId))
413
+ return;
352
414
  if (n.willRetry)
353
415
  return;
354
416
  const msg = n?.error?.message ?? 'unknown error';
@@ -371,6 +433,7 @@ export function createCodexEngine() {
371
433
  register(callId, pending) {
372
434
  pendingByCallId.set(callId, pending);
373
435
  turnLiveness.pause();
436
+ clearSubagentStallWatchdog();
374
437
  },
375
438
  respondError: (reqId, code, message) => client.peer.respondError(reqId, code, message),
376
439
  respond: (reqId, result) => client.peer.respond(reqId, result),
@@ -444,7 +507,9 @@ export function createCodexEngine() {
444
507
  ? 'error'
445
508
  : mapperState.sawTurnInterrupted
446
509
  ? 'killed'
447
- : 'completed';
510
+ : watchdogEnded
511
+ ? 'watchdog'
512
+ : 'completed';
448
513
  safeEmit({
449
514
  kind: 'session:ended',
450
515
  reason,
@@ -465,7 +530,11 @@ export function createCodexEngine() {
465
530
  safeEmit({ kind: 'session:ended', reason: 'killed', exitCode: null });
466
531
  }
467
532
  else if (error.name === 'CodexTurnTimeoutError') {
468
- safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
533
+ safeEmit({
534
+ kind: 'session:ended',
535
+ reason: mapperState.sawErrorResult ? 'error' : 'watchdog',
536
+ exitCode: null,
537
+ });
469
538
  }
470
539
  else if (QUOTA_PATTERN.test(message)) {
471
540
  tryEmitQuota(mapperState, safeEmit, message);
@@ -497,18 +566,16 @@ export function createCodexEngine() {
497
566
  }
498
567
  }
499
568
  fileChanges.clear();
569
+ otherTurnMappers.clear();
500
570
  pendingByCallId.clear();
501
571
  pendingToolCalls.clear();
502
572
  client.close();
503
- try {
504
- child.kill('SIGTERM');
505
- }
506
- catch {
507
- // best-effort
508
- }
573
+ await terminateChild().catch((error) => console.error('[codex] runtime shutdown unconfirmed:', error));
509
574
  }
510
575
  })();
511
576
  const engineProcess = {
577
+ ready: readyPromise,
578
+ closed: Promise.all([iteratorPromise, childExitedPromise]).then(() => { }),
512
579
  get pid() {
513
580
  return child.pid;
514
581
  },
@@ -516,7 +583,7 @@ export function createCodexEngine() {
516
583
  return discoveredSessionId;
517
584
  },
518
585
  isAlive() {
519
- return iteratorRunning;
586
+ return iteratorRunning || !childExited;
520
587
  },
521
588
  sendMessage(text) {
522
589
  const steer = async () => {
@@ -581,17 +648,7 @@ export function createCodexEngine() {
581
648
  catch {
582
649
  // swallow
583
650
  }
584
- // Escalate. The SIGTERM in the `finally` below is a request, not a
585
- // guarantee: a wedged `codex app-server` that ignores it outlives the
586
- // stop, the archive, the delete and Kōbō's own shutdown, holding the
587
- // worktree and its share of the model quota, with nothing left in
588
- // this process tracking it.
589
- if (!(await signalAndWaitForExit(child, 'SIGTERM', CODEX_FORCE_KILL_TIMEOUT_MS))) {
590
- console.warn('[codex] app-server ignored SIGTERM — sending SIGKILL');
591
- if (!(await signalAndWaitForExit(child, 'SIGKILL', CODEX_FORCE_KILL_TIMEOUT_MS))) {
592
- throw new Error('Codex app-server did not exit after SIGKILL');
593
- }
594
- }
651
+ await terminateChild();
595
652
  },
596
653
  resolvePendingUserInput(callId, response) {
597
654
  const pending = pendingByCallId.get(callId);
@@ -601,8 +658,12 @@ export function createCodexEngine() {
601
658
  // Only resume the idle-timeout clock once every outstanding
602
659
  // approval has been answered — a sibling request may still be
603
660
  // waiting on a human decision.
604
- if (pendingByCallId.size === 0)
605
- turnLiveness.resume();
661
+ if (pendingByCallId.size === 0) {
662
+ if (waitingForBackgroundSubagents)
663
+ armSubagentStallWatchdog();
664
+ else
665
+ turnLiveness.resume();
666
+ }
606
667
  const result = buildResponseForResolve(pending, response);
607
668
  client.peer.respond(pending.requestId, result);
608
669
  return true;