@loicngr/kobo 1.11.35 → 1.11.37
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 +15 -0
- package/CHANGELOG.md +9 -0
- package/dist/server/index.js +11 -24
- package/dist/server/routes/dev-server.js +2 -1
- package/dist/server/routes/pull-requests.js +5 -4
- package/dist/server/routes/workspaces.js +122 -91
- package/dist/server/services/agent/engines/claude-code/engine.js +26 -0
- package/dist/server/services/agent/engines/claude-code/event-mapper.js +10 -10
- package/dist/server/services/agent/engines/codex/client.js +1 -0
- package/dist/server/services/agent/engines/codex/engine.js +47 -10
- package/dist/server/services/agent/engines/codex/event-mapper.js +5 -1
- package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +21 -8
- package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +30 -3
- package/dist/server/services/agent/orchestrator.js +175 -68
- package/dist/server/services/agent/session-controller.js +12 -2
- package/dist/server/services/auto-loop-service.js +71 -6
- package/dist/server/services/change-source-branch-service.js +36 -4
- package/dist/server/services/cron-service.js +19 -0
- package/dist/server/services/db-backup-service.js +30 -0
- package/dist/server/services/dev-server-service.js +167 -38
- package/dist/server/services/image-service.js +25 -28
- package/dist/server/services/pr-checkout-service.js +34 -5
- package/dist/server/services/quota-backoff-service.js +25 -8
- package/dist/server/services/wakeup-service.js +37 -3
- package/dist/server/services/worktree-purge-service.js +3 -7
- package/dist/server/utils/agent-stop-result.js +12 -0
- package/dist/server/utils/git-ops.js +8 -1
- package/dist/server/utils/git-worktree-fingerprint.js +86 -0
- package/dist/server/utils/safe-path.js +21 -0
- package/dist/server/utils/script-runner.js +32 -9
- package/dist/server/utils/workspace-lifecycle-guard.js +27 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-Bakgvay6.css → ActivityFeed-BDyFBoxd.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-C_-hxIo8.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-DVLWWvvz.js → ChangelogPage-CBlGJ4zT.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-LZrvk6a6.js → ComparisonPanel-DZAE2X9j.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-ArmtcF4d.js +2 -0
- package/src/client/dist/pwa/assets/{DashboardPage-0odIhseb.js → DashboardPage-eQTCXcdz.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-NWB2uQkz.js → DiffViewer-CaT7vuSr.js} +3 -3
- package/src/client/dist/pwa/assets/{DocumentsPanel-3yApHgbl.js → DocumentsPanel-A0isCTaI.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-C_bNtZsE.js → DrawerToggleButton-T70fh8dp.js} +1 -1
- package/src/client/dist/pwa/assets/{GitPanel-BMjWwiAX.js → GitPanel-CAvk0G7h.js} +3 -3
- package/src/client/dist/pwa/assets/{HealthPage-Vyr1YG2o.js → HealthPage-D8Mr0V6w.js} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-s8_9FPE2.js → MainLayout-x6Es4Da-.js} +2 -2
- package/src/client/dist/pwa/assets/{QBtnDropdown-BxuXEBp5.js → QBtnDropdown-CQqzzlFI.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-BdlDiU6a.js → QDrawer-Cwj2qh0r.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-BrSemS3o.js → QSelect-BSPtuXJg.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-CLCFHW2D.js → QTooltip-DC_kDKku.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-F9bdAq6K.js → SchedulePanel-9aJSoz6Y.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-CBCefYqc.js → SearchPage-BjNvtE_U.js} +1 -1
- package/src/client/dist/pwa/assets/{SettingsPage-BPgTHmKj.js → SettingsPage-CvLnT73V.js} +1 -1
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-DDs2nuoT.js → SplitWorkspacePage-DTao0DHr.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-CulE0wTU.js → TerminalPanel-BrKIW-bJ.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-DUgqs2cW.js → TouchPan-_n1AQgoo.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-CdHpGBdr.js → TourReplayButton-CJF3nVvn.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-W5SvqzXO.js → WorkspaceAttentionLabels-CxesUiIm.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-DYQrfeYw.css → WorkspacePage-DoXtwpjV.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-MxBE0mr3.js +4 -0
- package/src/client/dist/pwa/assets/{browser-CnuqDTyc.js → browser-DGWpkvN3.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-JodIO7-I.js → build-path-tree-Ba1dSLgi.js} +1 -1
- package/src/client/dist/pwa/assets/{de-Dt_m-H83.js → de-DVK21RB6.js} +1 -1
- package/src/client/dist/pwa/assets/{documents-3ZBK1pXE.js → documents-BMHzjW_g.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-DZ9WO_J7.js → editor.api-CYTuNwqu.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-LrtJPx-j.js → efforts-BPlRSHXp.js} +1 -1
- package/src/client/dist/pwa/assets/{engineFeatures-DmnHj_uD.js → engineFeatures-DVB2rklO.js} +1 -1
- package/src/client/dist/pwa/assets/{es-DFg25DCS.js → es-C8cQwDZM.js} +1 -1
- package/src/client/dist/pwa/assets/{expand-template-Dv4JC2zU.js → expand-template-Cj82YE-O.js} +1 -1
- package/src/client/dist/pwa/assets/{format-CHjfjuhr.js → format-Dk7O3LhA.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-BsgH8CoH.js → formatters-TGjvs7Ca.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-CQ1slpol.js → fr-BVOMZ2Bl.js} +1 -1
- package/src/client/dist/pwa/assets/{freemarker2-Dla55SIR.js → freemarker2-39lO_dWH.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-DtuuZy7D.js → handlebars-M_0V82cD.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CKYOCR8W.js → html-yILqYDsl.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-2UeMfd5m.js +1 -0
- package/src/client/dist/pwa/assets/{index-CIP08MpU.js → index-BJU5dnkF.js} +2 -2
- package/src/client/dist/pwa/assets/{it-w09rfaP7.js → it-CxkAoOj4.js} +1 -1
- package/src/client/dist/pwa/assets/{javascript-BxzTnsIQ.js → javascript-DrW-amDc.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-C8BfPFUy.js → liquid-D_mXmUoL.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DZm_YXkC.js → mdx-E8O3lvR5.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-HDbRHiNR.js → monaco-contributions-C2K3sl_V.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-DRW-851R.js → monaco.contribution-DLhHQIJn.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-Ce9hsryo.js → network-auth-BUwWtnui.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-OBpMEBvP.js → notify-theme-1mOpY2vN.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-CVQxWyzP.js → pending-diff-open-BL7dKRqF.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-CVFHS2ec.js → permissionModes-BtC3_P-P.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-mcpQp2o6.js → project-color-BFBFZWeG.js} +1 -1
- package/src/client/dist/pwa/assets/{python-CO5iQN74.js → python-DoZAYim6.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-BC7D8LMf.js → razor-9bZ30g2y.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-Cb15kvox.js → render-chat-markdown-7EtWNjG0.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-BCHCSNpD.js → typescript-C1CBfL3L.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-DhLhglwr.js → use-tours-BJ81clv6.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-CoWrOEac.js → workspace-templates-r1-bipjM.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-y9M3NAIZ.js → xml-COuu5isK.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-CthlToPo.js → yaml-ChZ7WqaJ.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-YRbIRutK.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-DxOyY65u.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-gFrn4xl9.js +0 -2
- package/src/client/dist/pwa/assets/WorkspacePage-B6O7j57u.js +0 -4
- package/src/client/dist/pwa/assets/i18n-DTfKQUMf.js +0 -1
|
@@ -255,6 +255,7 @@ export function createClaudeCodeEngine() {
|
|
|
255
255
|
let iteratorRunning = false;
|
|
256
256
|
let userInterrupted = false;
|
|
257
257
|
let completedResponses = 0;
|
|
258
|
+
let waitingForBackground = false;
|
|
258
259
|
let turnCompletedEmittedForResponse = 0;
|
|
259
260
|
// `result` is the Claude Agent SDK's per-turn completion signal. Keep
|
|
260
261
|
// it distinct from iterator completion: the SDK may still drain
|
|
@@ -433,6 +434,11 @@ export function createClaudeCodeEngine() {
|
|
|
433
434
|
turnLiveness.start();
|
|
434
435
|
try {
|
|
435
436
|
for await (const msg of q) {
|
|
437
|
+
// A parent continuation owns the foreground again. Background
|
|
438
|
+
// progress notifications alone do not end the between-turn wait.
|
|
439
|
+
if (msg.type === 'assistant' || msg.type === 'user' || msg.type === 'stream_event') {
|
|
440
|
+
waitingForBackground = false;
|
|
441
|
+
}
|
|
436
442
|
// This SDK message proves that any drain condition armed by a
|
|
437
443
|
// *previous* message is no longer current. A drain armed while
|
|
438
444
|
// processing this message remains active if the generator then
|
|
@@ -495,6 +501,7 @@ export function createClaudeCodeEngine() {
|
|
|
495
501
|
turnLiveness.setTimeoutMs(pendingToolCallIds.size > 0 ? CLAUDE_TOOL_IDLE_TIMEOUT_MS : CLAUDE_STREAM_IDLE_TIMEOUT_MS);
|
|
496
502
|
reevaluateLivenessPause();
|
|
497
503
|
if (msg.type === 'result') {
|
|
504
|
+
waitingForBackground = false;
|
|
498
505
|
pendingToolCallIds.clear();
|
|
499
506
|
completedResponses++;
|
|
500
507
|
emitTurnCompletedIfSettled();
|
|
@@ -506,6 +513,7 @@ export function createClaudeCodeEngine() {
|
|
|
506
513
|
armResultDrainWatchdog();
|
|
507
514
|
}
|
|
508
515
|
else {
|
|
516
|
+
waitingForBackground = true;
|
|
509
517
|
armSubagentStallWatchdog();
|
|
510
518
|
}
|
|
511
519
|
}
|
|
@@ -583,6 +591,24 @@ export function createClaudeCodeEngine() {
|
|
|
583
591
|
throw new Error(`Claude ${AGENT_NO_LONGER_RUNNING_TEXT}`);
|
|
584
592
|
inputStream.send(text);
|
|
585
593
|
},
|
|
594
|
+
sendWakeupIfWaiting(text) {
|
|
595
|
+
if (!iteratorRunning ||
|
|
596
|
+
abortController.signal.aborted ||
|
|
597
|
+
mapperState.sawErrorResult ||
|
|
598
|
+
!waitingForBackground ||
|
|
599
|
+
activeSubagentTaskIds.size === 0 ||
|
|
600
|
+
pendingToolCallIds.size > 0 ||
|
|
601
|
+
pendingResolvers.size > 0 ||
|
|
602
|
+
isCompacting ||
|
|
603
|
+
inputStream.hasUnansweredInput(completedResponses))
|
|
604
|
+
return false;
|
|
605
|
+
inputStream.send(text);
|
|
606
|
+
waitingForBackground = false;
|
|
607
|
+
// The wakeup starts another turn on this stream; its result will
|
|
608
|
+
// re-arm the stall deadline if background work is still outstanding.
|
|
609
|
+
clearSubagentStallWatchdog();
|
|
610
|
+
return true;
|
|
611
|
+
},
|
|
586
612
|
interrupt() {
|
|
587
613
|
userInterrupted = true;
|
|
588
614
|
// The SDK ends an interrupted run by emitting a `result` with
|
|
@@ -63,20 +63,20 @@ function normalizeRateLimitInfo(info) {
|
|
|
63
63
|
}
|
|
64
64
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* - assistant `message:text` blocks (the SDK occasionally streams the user-
|
|
70
|
-
* visible quota notice as plain assistant text instead of a structured
|
|
71
|
-
* error result; see workspace `-GyiAYM7X4xTWyZbcHGiR` session #25 for the
|
|
72
|
-
* repro that motivated this path)
|
|
73
|
-
* - CLI stderr in `engine.ts`
|
|
66
|
+
* Quota keywords for error results and CLI stderr. Assistant prose uses
|
|
67
|
+
* the narrower ASSISTANT_QUOTA_NOTICE below, because summaries may mention
|
|
68
|
+
* a previous quota failure while the current run is working normally.
|
|
74
69
|
*
|
|
75
70
|
* Patterns are kept loose on purpose to absorb minor wording drift between
|
|
76
71
|
* Anthropic's surfaces (`rate_limit_exceeded`, `Claude AI usage limit
|
|
77
72
|
* reached`, `You're out of extra usage`, `quota exceeded`).
|
|
78
73
|
*/
|
|
79
74
|
export const QUOTA_PATTERN = /\b429\b|out of extra usage|rate[_ ]limit|usage limit|quota exceeded/i;
|
|
75
|
+
/** Only canonical, standalone notices qualify on the assistant prose surface.
|
|
76
|
+
* Broad quota keywords remain appropriate for structured errors, not summaries
|
|
77
|
+
* of a previous attempt, a sub-agent failure, or application HTTP handling.
|
|
78
|
+
*/
|
|
79
|
+
const ASSISTANT_QUOTA_NOTICE = /^(?:you(?:'|’)re out of extra usage|you(?:'|’)ve hit your (?:session|weekly|usage) limit|claude ai usage limit reached|usage limit reached|quota exceeded|rate limit exceeded)(?:[.!]?(?:[ \t]*[·—-]?[ \t]*resets?\b[^\r\n]*)?)?[.!]?$/i;
|
|
80
80
|
export function createMapperState() {
|
|
81
81
|
return {
|
|
82
82
|
sessionStartedEmitted: false,
|
|
@@ -274,8 +274,8 @@ export function mapSdkMessage(msg, state) {
|
|
|
274
274
|
// Last-resort fallback: some SDK runs surface the quota notice as
|
|
275
275
|
// plain assistant text without setting `assistant.error` or a
|
|
276
276
|
// `result.error`. The structured signals above cover modern SDK
|
|
277
|
-
// versions;
|
|
278
|
-
if (
|
|
277
|
+
// versions; only a standalone notice qualifies on this prose surface.
|
|
278
|
+
if (ASSISTANT_QUOTA_NOTICE.test(text.trim())) {
|
|
279
279
|
tryEmitQuotaError(state, events, text);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
@@ -11,6 +11,7 @@ import { buildResponseForResolve, handleServerRequest } from './server-requests.
|
|
|
11
11
|
import { spawnAppServer } from './spawn.js';
|
|
12
12
|
/** Long enough for normal tool work, short enough to recover a lost turn event. */
|
|
13
13
|
export const CODEX_TURN_IDLE_TIMEOUT_MS = 120_000;
|
|
14
|
+
export const CODEX_TOOL_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
14
15
|
export const CODEX_GRACEFUL_INTERRUPT_TIMEOUT_MS = 3_000;
|
|
15
16
|
/** Grace given to a SIGTERM before the child is killed outright. */
|
|
16
17
|
const CODEX_FORCE_KILL_TIMEOUT_MS = 3_000;
|
|
@@ -24,6 +25,35 @@ export const CODEX_SUBAGENT_STALL_TIMEOUT_MS = 10 * 60_000;
|
|
|
24
25
|
function wait(ms) {
|
|
25
26
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
27
|
}
|
|
28
|
+
/** A sent signal is only a request; resolve true exclusively on confirmed exit. */
|
|
29
|
+
function signalAndWaitForExit(child, signal, timeoutMs) {
|
|
30
|
+
if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined)
|
|
31
|
+
return Promise.resolve(true);
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const cleanup = () => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
child.removeListener('exit', onExit);
|
|
36
|
+
};
|
|
37
|
+
const onExit = () => {
|
|
38
|
+
cleanup();
|
|
39
|
+
resolve(true);
|
|
40
|
+
};
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
cleanup();
|
|
43
|
+
resolve(false);
|
|
44
|
+
}, timeoutMs);
|
|
45
|
+
timer.unref?.();
|
|
46
|
+
// Install before signalling so an immediate exit cannot be missed.
|
|
47
|
+
child.once('exit', onExit);
|
|
48
|
+
try {
|
|
49
|
+
child.kill(signal);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
cleanup();
|
|
53
|
+
reject(err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
27
57
|
class CodexTurnTimeoutError extends Error {
|
|
28
58
|
constructor() {
|
|
29
59
|
super('Codex stopped reporting activity for this turn');
|
|
@@ -77,7 +107,17 @@ export function createCodexEngine() {
|
|
|
77
107
|
}
|
|
78
108
|
};
|
|
79
109
|
const streamingBatcher = createStreamingBatcher(emitDirect);
|
|
80
|
-
const
|
|
110
|
+
const pendingToolCalls = new Set();
|
|
111
|
+
const safeEmit = (ev) => {
|
|
112
|
+
if (ev.kind === 'tool:call')
|
|
113
|
+
pendingToolCalls.add(ev.toolCallId);
|
|
114
|
+
if (ev.kind === 'tool:result')
|
|
115
|
+
pendingToolCalls.delete(ev.toolCallId);
|
|
116
|
+
if (ev.kind === 'tool:call' || ev.kind === 'tool:result') {
|
|
117
|
+
turnLiveness.setTimeoutMs(pendingToolCalls.size ? CODEX_TOOL_IDLE_TIMEOUT_MS : CODEX_TURN_IDLE_TIMEOUT_MS);
|
|
118
|
+
}
|
|
119
|
+
streamingBatcher.push(ev);
|
|
120
|
+
};
|
|
81
121
|
let rejectChildFailure;
|
|
82
122
|
const childFailurePromise = new Promise((_resolve, reject) => {
|
|
83
123
|
rejectChildFailure = reject;
|
|
@@ -324,6 +364,7 @@ export function createCodexEngine() {
|
|
|
324
364
|
autoApprove: (toolName, payload) => isWorkspacePermissionAllowed(options.workspaceId, { engine: 'codex', toolName, payload }),
|
|
325
365
|
});
|
|
326
366
|
},
|
|
367
|
+
onDisconnect: rejectChildFailure,
|
|
327
368
|
onError(err) {
|
|
328
369
|
console.error('[codex] JSON-RPC transport error:', err);
|
|
329
370
|
rejectTurnDone(err);
|
|
@@ -428,6 +469,7 @@ export function createCodexEngine() {
|
|
|
428
469
|
}
|
|
429
470
|
}
|
|
430
471
|
pendingByCallId.clear();
|
|
472
|
+
pendingToolCalls.clear();
|
|
431
473
|
client.close();
|
|
432
474
|
try {
|
|
433
475
|
child.kill('SIGTERM');
|
|
@@ -515,15 +557,10 @@ export function createCodexEngine() {
|
|
|
515
557
|
// stop, the archive, the delete and Kōbō's own shutdown, holding the
|
|
516
558
|
// worktree and its share of the model quota, with nothing left in
|
|
517
559
|
// this process tracking it.
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
new
|
|
522
|
-
wait(CODEX_FORCE_KILL_TIMEOUT_MS).then(() => false),
|
|
523
|
-
]);
|
|
524
|
-
if (!exited) {
|
|
525
|
-
console.warn('[codex] app-server ignored SIGTERM — sending SIGKILL');
|
|
526
|
-
child.kill('SIGKILL');
|
|
560
|
+
if (!(await signalAndWaitForExit(child, 'SIGTERM', CODEX_FORCE_KILL_TIMEOUT_MS))) {
|
|
561
|
+
console.warn('[codex] app-server ignored SIGTERM — sending SIGKILL');
|
|
562
|
+
if (!(await signalAndWaitForExit(child, 'SIGKILL', CODEX_FORCE_KILL_TIMEOUT_MS))) {
|
|
563
|
+
throw new Error('Codex app-server did not exit after SIGKILL');
|
|
527
564
|
}
|
|
528
565
|
}
|
|
529
566
|
},
|
|
@@ -302,13 +302,17 @@ export function handleItemCompleted(item, state) {
|
|
|
302
302
|
}
|
|
303
303
|
if (item.type === 'plan') {
|
|
304
304
|
// Codex's `plan` is a markdown blob, not a structured list — parse bullets.
|
|
305
|
+
const toolCallId = scopedId(state, item.id);
|
|
305
306
|
events.push({
|
|
306
307
|
kind: 'tool:call',
|
|
307
308
|
messageId: '',
|
|
308
|
-
toolCallId
|
|
309
|
+
toolCallId,
|
|
309
310
|
name: 'TodoWrite',
|
|
310
311
|
input: { todos: parseCodexPlanText(item.text) },
|
|
311
312
|
});
|
|
313
|
+
// The plan has already completed. Close the synthetic update so both the
|
|
314
|
+
// tool card and the engine's idle watchdog see no outstanding operation.
|
|
315
|
+
events.push({ kind: 'tool:result', toolCallId, output: null, isError: false });
|
|
312
316
|
return events;
|
|
313
317
|
}
|
|
314
318
|
if (item.type === 'error') {
|
|
@@ -19,6 +19,7 @@ export class JsonRpcTimeoutError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
export function createJsonRpcPeer(opts) {
|
|
21
21
|
let nextId = 1;
|
|
22
|
+
let closedError;
|
|
22
23
|
const pending = new Map();
|
|
23
24
|
/** Remove a pending slot and clear its deadline in one place. */
|
|
24
25
|
const takePending = (id) => {
|
|
@@ -30,10 +31,21 @@ export function createJsonRpcPeer(opts) {
|
|
|
30
31
|
pending.delete(id);
|
|
31
32
|
return slot;
|
|
32
33
|
};
|
|
34
|
+
const rejectPending = (err) => {
|
|
35
|
+
if (closedError)
|
|
36
|
+
return;
|
|
37
|
+
closedError = err;
|
|
38
|
+
for (const id of pending.keys())
|
|
39
|
+
takePending(id)?.reject(err);
|
|
40
|
+
};
|
|
33
41
|
const transport = createJsonRpcTransport({
|
|
34
42
|
stdin: opts.stdin,
|
|
35
43
|
stdout: opts.stdout,
|
|
36
44
|
onError: opts.onError ?? (() => { }),
|
|
45
|
+
onDisconnect(err) {
|
|
46
|
+
rejectPending(err);
|
|
47
|
+
opts.onDisconnect?.(err);
|
|
48
|
+
},
|
|
37
49
|
onMessage(msg) {
|
|
38
50
|
// Response to one of our requests
|
|
39
51
|
if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
|
|
@@ -61,6 +73,8 @@ export function createJsonRpcPeer(opts) {
|
|
|
61
73
|
});
|
|
62
74
|
return {
|
|
63
75
|
request(method, params, timeoutMs) {
|
|
76
|
+
if (closedError)
|
|
77
|
+
return Promise.reject(closedError);
|
|
64
78
|
const id = nextId++;
|
|
65
79
|
const deadlineMs = timeoutMs ?? opts.defaultRequestTimeoutMs ?? DEFAULT_JSONRPC_REQUEST_TIMEOUT_MS;
|
|
66
80
|
return new Promise((resolve, reject) => {
|
|
@@ -73,7 +87,12 @@ export function createJsonRpcPeer(opts) {
|
|
|
73
87
|
slot.timer.unref?.();
|
|
74
88
|
}
|
|
75
89
|
pending.set(id, slot);
|
|
76
|
-
|
|
90
|
+
try {
|
|
91
|
+
transport.send({ jsonrpc: '2.0', id, method, params });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
takePending(id)?.reject(err instanceof Error ? err : new Error(String(err)));
|
|
95
|
+
}
|
|
77
96
|
});
|
|
78
97
|
},
|
|
79
98
|
notify(method, params) {
|
|
@@ -86,14 +105,8 @@ export function createJsonRpcPeer(opts) {
|
|
|
86
105
|
transport.send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
87
106
|
},
|
|
88
107
|
close() {
|
|
108
|
+
rejectPending(new Error('peer closed'));
|
|
89
109
|
transport.close();
|
|
90
|
-
const err = new Error('peer closed');
|
|
91
|
-
for (const slot of pending.values()) {
|
|
92
|
-
if (slot.timer)
|
|
93
|
-
clearTimeout(slot.timer);
|
|
94
|
-
slot.reject(err);
|
|
95
|
-
}
|
|
96
|
-
pending.clear();
|
|
97
110
|
},
|
|
98
111
|
};
|
|
99
112
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
export function createJsonRpcTransport(opts) {
|
|
2
2
|
let buffer = '';
|
|
3
|
+
let closed = false;
|
|
4
|
+
const fail = (err) => {
|
|
5
|
+
if (closed)
|
|
6
|
+
return;
|
|
7
|
+
closed = true;
|
|
8
|
+
opts.onDisconnect?.(err);
|
|
9
|
+
opts.onError(err);
|
|
10
|
+
};
|
|
11
|
+
// Keep listeners installed through teardown: late pipe errors must stay handled.
|
|
12
|
+
opts.stdin.on('error', fail);
|
|
13
|
+
opts.stdout.on('error', fail);
|
|
14
|
+
opts.stdout.on('end', () => fail(new Error('JSON-RPC transport closed')));
|
|
3
15
|
opts.stdout.setEncoding?.('utf-8');
|
|
4
16
|
opts.stdout.on('data', (chunk) => {
|
|
5
17
|
buffer += chunk;
|
|
@@ -19,13 +31,28 @@ export function createJsonRpcTransport(opts) {
|
|
|
19
31
|
}
|
|
20
32
|
}
|
|
21
33
|
});
|
|
22
|
-
opts.stdout.on('error', opts.onError);
|
|
23
34
|
return {
|
|
24
35
|
send(msg) {
|
|
25
|
-
|
|
36
|
+
if (closed)
|
|
37
|
+
throw new Error('JSON-RPC transport closed');
|
|
38
|
+
try {
|
|
39
|
+
opts.stdin.write(`${JSON.stringify(msg)}\n`);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
26
45
|
},
|
|
27
46
|
close() {
|
|
28
|
-
|
|
47
|
+
if (closed)
|
|
48
|
+
return;
|
|
49
|
+
closed = true;
|
|
50
|
+
try {
|
|
51
|
+
opts.stdin.end();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* Already disconnected. */
|
|
55
|
+
}
|
|
29
56
|
},
|
|
30
57
|
};
|
|
31
58
|
}
|