@loicngr/kobo 1.11.16 → 1.11.17
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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +9 -0
- package/dist/mcp-server/kobo-tasks-server.js +8 -7
- package/dist/server/db/migrations.js +57 -0
- package/dist/server/db/schema.js +10 -39
- package/dist/server/index.js +55 -7
- package/dist/server/routes/git.js +8 -3
- package/dist/server/routes/health.js +19 -2
- package/dist/server/routes/settings.js +24 -0
- package/dist/server/routes/workspaces.js +371 -72
- package/dist/server/services/agent/engines/claude-code/engine.js +39 -35
- package/dist/server/services/agent/engines/codex/client.js +16 -4
- package/dist/server/services/agent/engines/codex/engine.js +23 -2
- package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +44 -5
- package/dist/server/services/agent/engines/types.js +13 -0
- package/dist/server/services/agent/orchestrator.js +354 -133
- package/dist/server/services/agent/session-controller.js +27 -0
- package/dist/server/services/agent/{engines/codex/turn-liveness.js → turn-liveness.js} +6 -2
- package/dist/server/services/change-source-branch-service.js +115 -11
- package/dist/server/services/db-backup-service.js +27 -16
- package/dist/server/services/pr-watcher-service.js +17 -2
- package/dist/server/services/settings-service.js +39 -0
- package/dist/server/services/websocket-service.js +114 -48
- package/dist/server/services/workspace-service.js +54 -2
- package/dist/server/services/worktree-purge-service.js +19 -5
- package/dist/server/services/worktree-service.js +38 -1
- package/dist/server/services/ws-events-retention-service.js +133 -0
- package/dist/server/utils/git-ops.js +209 -14
- package/dist/server/utils/git-repo-lock.js +45 -0
- package/dist/server/utils/logger.js +108 -0
- package/dist/server/utils/script-runner.js +11 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BlDBh5jB.js +8 -0
- package/src/client/dist/pwa/assets/ActivityFeed-tC8sR0GV.css +1 -0
- package/src/client/dist/pwa/assets/ChangelogPage-DACtknly.js +1 -0
- package/src/client/dist/pwa/assets/ChangelogPage-D_WOVByV.css +1 -0
- package/src/client/dist/pwa/assets/ClosePopup-DEy0yFXE.js +1 -0
- package/src/client/dist/pwa/assets/CreatePage-Bb-Qn-Rd.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-RMBX-Zjn.css +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-DKt6gFi4.js +8 -0
- package/src/client/dist/pwa/assets/DiffViewer-ahWtFrRy.css +1 -0
- package/src/client/dist/pwa/assets/DocumentsPanel-B8QY7FTj.css +1 -0
- package/src/client/dist/pwa/assets/DocumentsPanel-DhP1jk11.js +1 -0
- package/src/client/dist/pwa/assets/DrawerToggleButton-CBdOJzVZ.js +1 -0
- package/src/client/dist/pwa/assets/GitPanel-BKrtkPrV.css +1 -0
- package/src/client/dist/pwa/assets/GitPanel-DNCCB0G_.js +3 -0
- package/src/client/dist/pwa/assets/HealthPage-B1J4-dOH.js +1 -0
- package/src/client/dist/pwa/assets/HealthPage-BbqLoFQj.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-F8ShDnAy.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-qWwDy1B5.js +2 -0
- package/src/client/dist/pwa/assets/QBadge-BgS0-MXB.js +1 -0
- package/src/client/dist/pwa/assets/QBanner-BoAbhM1O.js +1 -0
- package/src/client/dist/pwa/assets/QBtn-B7WFdfmy.js +1 -0
- package/src/client/dist/pwa/assets/QBtnGroup-BMQlX2YS.js +1 -0
- package/src/client/dist/pwa/assets/QBtnToggle-eMu_yinY.js +1 -0
- package/src/client/dist/pwa/assets/QChip-BzB_GF4Z.js +1 -0
- package/src/client/dist/pwa/assets/QExpansionItem-C8CWo-aP.js +1 -0
- package/src/client/dist/pwa/assets/QItemSection-CDBweQnA.js +1 -0
- package/src/client/dist/pwa/assets/QList-Ccqhimcb.js +1 -0
- package/src/client/dist/pwa/assets/QPage-RpDxx5ah.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-D9bHHxAA.js +1 -0
- package/src/client/dist/pwa/assets/QSlideTransition-6lWJvjLP.js +1 -0
- package/src/client/dist/pwa/assets/QSpace-D3PNLwa4.js +1 -0
- package/src/client/dist/pwa/assets/QSpinnerDots-DWOFEezR.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-Brtqtz1H.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-CvZxJiJs.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-DdWM01rq.css +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-JVje8OPs.js +16 -0
- package/src/client/dist/pwa/assets/SettingsPage-dMAg4JP8.css +1 -0
- package/src/client/dist/pwa/assets/TerminalPanel-B6xJJs8p.css +1 -0
- package/src/client/dist/pwa/assets/TerminalPanel-Dtc6B3US.js +18 -0
- package/src/client/dist/pwa/assets/TouchPan-DnEDaTFL.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-BVxtefbj.js +4 -0
- package/src/client/dist/pwa/assets/WorkspacePage-DN-A6XpI.css +1 -0
- package/src/client/dist/pwa/assets/build-path-tree-BU8Y8vRw.js +1 -0
- package/src/client/dist/pwa/assets/clipboard-t9qe6lK7.js +1 -0
- package/src/client/dist/pwa/assets/{cssMode-DNEi2cLH.js → cssMode-D_JDSbNP.js} +1 -1
- package/src/client/dist/pwa/assets/debounce-CDIE7CV6.js +1 -0
- package/src/client/dist/pwa/assets/documents-B6eOZeFK.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api-_tFrtCR7.js → editor.api-Ccbr7NXr.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-DW9PydRL.js +1 -0
- package/src/client/dist/pwa/assets/expand-template-B4LAsXAF.css +1 -0
- package/src/client/dist/pwa/assets/expand-template-Xq6JwXsl.js +1 -0
- package/src/client/dist/pwa/assets/{freemarker2-kfVsFQjE.js → freemarker2-CUvQzJdx.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-EwBi9SHt.js → handlebars-BXiAPlES.js} +1 -1
- package/src/client/dist/pwa/assets/{html-BOOCP_vn.js → html-CLmfyTMC.js} +1 -1
- package/src/client/dist/pwa/assets/{htmlMode-IN9oEIXW.js → htmlMode-DWYo6_e_.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CJclRByY.js +1 -0
- package/src/client/dist/pwa/assets/index-Bri8-39E.css +1 -0
- package/src/client/dist/pwa/assets/index-Dv1p-1sA.js +82 -0
- package/src/client/dist/pwa/assets/{javascript-CfV8qmeH.js → javascript-C9gWFLC_.js} +1 -1
- package/src/client/dist/pwa/assets/{jsonMode-C0b3m4VO.js → jsonMode-CbsjeFb8.js} +1 -1
- package/src/client/dist/pwa/assets/kobo-commands-DGygmOvc.js +9 -0
- package/src/client/dist/pwa/assets/layout-nWIyHeJf.js +1 -0
- package/src/client/dist/pwa/assets/{liquid-D3YK93J-.js → liquid-BqJ3SIha.js} +1 -1
- package/src/client/dist/pwa/assets/{lspLanguageFeatures-PklP2gEH.js → lspLanguageFeatures-BB0y1B51.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DjZcevPS.js → mdx-rIxQLZDB.js} +1 -1
- package/src/client/dist/pwa/assets/network-auth-pzKTXRYP.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-D7RrwvJa.js +1 -0
- package/src/client/dist/pwa/assets/pending-diff-open-Cia8sndE.js +1 -0
- package/src/client/dist/pwa/assets/private.use-form-D8Yuc3aU.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-Cc9yKGk3.js → project-color-Dur1z6Gn.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BrgzhqrC.js → python-C6d5du9h.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-g8NeeeQc.js → razor-Aek1yDc0.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-CZyJL1-w.js → render-chat-markdown-B6sa44Xr.js} +2 -2
- package/src/client/dist/pwa/assets/skill-suite-prompts-DdoGRwhz.js +36 -0
- package/src/client/dist/pwa/assets/{tsMode-DAopv4hH.js → tsMode-rxNEyqyY.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-C2jZCVPF.js → typescript-DndxWYhC.js} +1 -1
- package/src/client/dist/pwa/assets/use-checkbox-z-Qs4ZHY.js +1 -0
- package/src/client/dist/pwa/assets/use-id-DlouqdB8.js +1 -0
- package/src/client/dist/pwa/assets/use-onboarding-BzSwN4xT.js +2 -0
- package/src/client/dist/pwa/assets/{vs-mk-rn9sP.js → vs-De8HKJ9r.js} +3 -3
- package/src/client/dist/pwa/assets/{workers-CP5iY9Kz.js → workers-DdNkyVm9.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-D3pYNERH.js → xml-Cl1k1mQr.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BTRPY5XP.js → yaml-C8G_k5TR.js} +1 -1
- package/src/client/dist/pwa/fonts/GEIST-LICENSE.txt +92 -0
- package/src/client/dist/pwa/fonts/Geist-Variable.woff2 +0 -0
- package/src/client/dist/pwa/fonts/GeistMono-Variable.woff2 +0 -0
- package/src/client/dist/pwa/index.html +7 -5
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/kobo-tasks-server.ts +8 -7
- package/dist/server/utils/process-tracker.js +0 -49
- package/src/client/dist/pwa/assets/ActivityFeed-BrjeLlhi.css +0 -1
- package/src/client/dist/pwa/assets/ActivityFeed-WBBk7DgE.js +0 -8
- package/src/client/dist/pwa/assets/ChangelogPage-Cr5cW9Wx.js +0 -1
- package/src/client/dist/pwa/assets/ChangelogPage-DGhfnkax.css +0 -1
- package/src/client/dist/pwa/assets/ClosePopup-2KSSXX6P.js +0 -1
- package/src/client/dist/pwa/assets/CreatePage-Bl1Vjr27.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-DQk8TrUP.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-C9xV8dh-.js +0 -8
- package/src/client/dist/pwa/assets/DiffViewer-CS3P9Ngi.css +0 -1
- package/src/client/dist/pwa/assets/DrawerToggleButton-CjM28eaw.js +0 -1
- package/src/client/dist/pwa/assets/HealthPage-BbLpLRjV.js +0 -1
- package/src/client/dist/pwa/assets/HealthPage-BqirO5le.css +0 -1
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BCOjigVR.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-Ocp2tNig.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-6jEn9xEl.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CCs8I9zJ.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-BLaboPi0.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-DHuK6FBl.woff +0 -0
- package/src/client/dist/pwa/assets/MainLayout-BUril7SK.js +0 -19
- package/src/client/dist/pwa/assets/MainLayout-u-AM87m3.css +0 -1
- package/src/client/dist/pwa/assets/QBadge-DS9cpnyx.js +0 -1
- package/src/client/dist/pwa/assets/QBanner-tANb95XA.js +0 -1
- package/src/client/dist/pwa/assets/QBtn-DJGkBel8.js +0 -1
- package/src/client/dist/pwa/assets/QBtnToggle-D-Hsqmcb.js +0 -1
- package/src/client/dist/pwa/assets/QChip-iLXQ5Khf.js +0 -1
- package/src/client/dist/pwa/assets/QItemSection-B-5iFRBZ.js +0 -1
- package/src/client/dist/pwa/assets/QList-D-yd1vEv.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-TFRaHbzG.js +0 -36
- package/src/client/dist/pwa/assets/QSeparator-C_2BuEw9.js +0 -1
- package/src/client/dist/pwa/assets/QSpace-BuMMeXnK.js +0 -1
- package/src/client/dist/pwa/assets/QSpinnerDots-C-tv95pD.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-CPM7L9_M.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-CngtAqls.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-BRYycuYY.js +0 -16
- package/src/client/dist/pwa/assets/SettingsPage-D5BacmAr.css +0 -1
- package/src/client/dist/pwa/assets/TouchPan-79_u9-Fd.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DB1n1bmg.js +0 -4
- package/src/client/dist/pwa/assets/WorkspacePage-hxEpXqBA.css +0 -1
- package/src/client/dist/pwa/assets/documents-BMvE9Q_U.js +0 -1
- package/src/client/dist/pwa/assets/engineFeatures-JkVb6hry.js +0 -1
- package/src/client/dist/pwa/assets/expand-template-D2yUa54D.css +0 -1
- package/src/client/dist/pwa/assets/expand-template-Dl4wUjF8.js +0 -1
- package/src/client/dist/pwa/assets/i18n-Dezgz2hU.js +0 -1
- package/src/client/dist/pwa/assets/index-B12jWMl8.js +0 -82
- package/src/client/dist/pwa/assets/index-CK1nk7qr.css +0 -1
- package/src/client/dist/pwa/assets/kobo-commands-DznaKlq6.js +0 -9
- package/src/client/dist/pwa/assets/layout-me0gwmBV.js +0 -1
- package/src/client/dist/pwa/assets/network-auth-BMTuTKHx.js +0 -1
- package/src/client/dist/pwa/assets/pending-diff-open-BG3IOnWw.js +0 -1
- package/src/client/dist/pwa/assets/use-checkbox-D-9LXk-S.js +0 -1
- package/src/client/dist/pwa/assets/use-is-mobile-DX2Fiee-.js +0 -1
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
123
|
-
//
|
|
124
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
},
|
|
296
|
-
|
|
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
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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',
|