@loicngr/kobo 1.11.12 → 1.11.13
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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ All notable changes to Kōbō are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
5
|
section — the in-app "What's new" dialog reads this file.
|
|
6
6
|
|
|
7
|
+
## 1.11.13
|
|
8
|
+
|
|
9
|
+
- fix(engines): unstick agent sessions that never emit session:ended
|
|
10
|
+
|
|
7
11
|
## 1.11.12
|
|
8
12
|
|
|
9
13
|
- ✨ develop - feat(engines): Garder les sessions ouvertes pour les subagents en arrière-plan
|
|
@@ -20,6 +20,12 @@ const RESULT_DRAIN_TIMEOUT_MS = 15_000;
|
|
|
20
20
|
// without producing a result message. Keep a separate, conservative guard for
|
|
21
21
|
// that shape; active tool/message traffic always cancels and re-arms it.
|
|
22
22
|
const TEXT_IDLE_TIMEOUT_MS = 120_000;
|
|
23
|
+
// Safety net for a subagent whose terminal `task_notification` carries a
|
|
24
|
+
// status outside the mapper's known-terminal set (SDK schema drift) — it
|
|
25
|
+
// never clears `activeSubagentToolCallIds`, which otherwise blocks
|
|
26
|
+
// `inputStream.close()`/the result-drain watchdog forever. Generous window:
|
|
27
|
+
// legitimate subagents can run for several minutes.
|
|
28
|
+
const SUBAGENT_STALL_TIMEOUT_MS = 10 * 60_000;
|
|
23
29
|
const MAX_PENDING_USER_MESSAGES = 20;
|
|
24
30
|
function toMcpServersMap(specs) {
|
|
25
31
|
if (!specs || specs.length === 0)
|
|
@@ -278,6 +284,41 @@ export function createClaudeCodeEngine() {
|
|
|
278
284
|
}, RESULT_DRAIN_TIMEOUT_MS);
|
|
279
285
|
resultDrainTimer.unref?.();
|
|
280
286
|
};
|
|
287
|
+
// Safety net for the "waiting on a background subagent" branch below:
|
|
288
|
+
// if `activeSubagentToolCallIds` never empties (a missed/unrecognised
|
|
289
|
+
// terminal notification), the workspace would otherwise stay `executing`
|
|
290
|
+
// forever. Force the drain through once this fires.
|
|
291
|
+
let subagentStallTimer;
|
|
292
|
+
const clearSubagentStallWatchdog = () => {
|
|
293
|
+
if (!subagentStallTimer)
|
|
294
|
+
return;
|
|
295
|
+
clearTimeout(subagentStallTimer);
|
|
296
|
+
subagentStallTimer = undefined;
|
|
297
|
+
};
|
|
298
|
+
// Re-armed (not merely armed-once) on every qualifying `result` so the
|
|
299
|
+
// deadline tracks the last observed activity, matching the warning
|
|
300
|
+
// message below — a long chain of legitimate subagent-backed turns
|
|
301
|
+
// must not be cut off just because 10 minutes have passed in total.
|
|
302
|
+
const armSubagentStallWatchdog = () => {
|
|
303
|
+
clearSubagentStallWatchdog();
|
|
304
|
+
subagentStallTimer = setTimeout(() => {
|
|
305
|
+
subagentStallTimer = undefined;
|
|
306
|
+
// A message queued between arming and firing means a new turn is
|
|
307
|
+
// about to start on this same stream — don't discard the subagent
|
|
308
|
+
// tracking or force-close under it; the next 'result' re-evaluates.
|
|
309
|
+
if (inputStream.hasUnansweredInput(completedResponses))
|
|
310
|
+
return;
|
|
311
|
+
console.warn(`[claude-engine] Subagent(s) still tracked active ${SUBAGENT_STALL_TIMEOUT_MS}ms after the last turn ended — forcing session drain.`);
|
|
312
|
+
activeSubagentToolCallIds.clear();
|
|
313
|
+
// This is a forced, unclean termination (the subagent may still be
|
|
314
|
+
// alive server-side) — never report it as a normal completion, so
|
|
315
|
+
// auto-loop doesn't treat an orphaned run as forward progress.
|
|
316
|
+
mapperState.sawErrorResult = true;
|
|
317
|
+
inputStream.close();
|
|
318
|
+
armResultDrainWatchdog();
|
|
319
|
+
}, SUBAGENT_STALL_TIMEOUT_MS);
|
|
320
|
+
subagentStallTimer.unref?.();
|
|
321
|
+
};
|
|
281
322
|
const iteratorPromise = (async () => {
|
|
282
323
|
iteratorRunning = true;
|
|
283
324
|
try {
|
|
@@ -291,6 +332,17 @@ export function createClaudeCodeEngine() {
|
|
|
291
332
|
else
|
|
292
333
|
activeSubagentToolCallIds.delete(ev.toolCallId);
|
|
293
334
|
}
|
|
335
|
+
// The stall watchdog is only armed while we're waiting purely on
|
|
336
|
+
// background subagents (no further SDK turn expected unless one
|
|
337
|
+
// reports done). React the moment the last one clears instead of
|
|
338
|
+
// waiting for a 'result' message that may never come.
|
|
339
|
+
if (subagentStallTimer &&
|
|
340
|
+
activeSubagentToolCallIds.size === 0 &&
|
|
341
|
+
!inputStream.hasUnansweredInput(completedResponses)) {
|
|
342
|
+
clearSubagentStallWatchdog();
|
|
343
|
+
inputStream.close();
|
|
344
|
+
armResultDrainWatchdog();
|
|
345
|
+
}
|
|
294
346
|
for (const ev of events) {
|
|
295
347
|
if (ev.kind === 'session:started')
|
|
296
348
|
discoveredSessionId = ev.engineSessionId;
|
|
@@ -307,9 +359,15 @@ export function createClaudeCodeEngine() {
|
|
|
307
359
|
clearTextIdleWatchdog();
|
|
308
360
|
completedResponses++;
|
|
309
361
|
// A queued forced message starts the next response on this same SDK stream.
|
|
310
|
-
if (!inputStream.hasUnansweredInput(completedResponses)
|
|
311
|
-
|
|
312
|
-
|
|
362
|
+
if (!inputStream.hasUnansweredInput(completedResponses)) {
|
|
363
|
+
if (activeSubagentToolCallIds.size === 0) {
|
|
364
|
+
clearSubagentStallWatchdog();
|
|
365
|
+
inputStream.close();
|
|
366
|
+
armResultDrainWatchdog();
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
armSubagentStallWatchdog();
|
|
370
|
+
}
|
|
313
371
|
}
|
|
314
372
|
}
|
|
315
373
|
}
|
|
@@ -354,6 +412,7 @@ export function createClaudeCodeEngine() {
|
|
|
354
412
|
resultDrainTimer = undefined;
|
|
355
413
|
}
|
|
356
414
|
clearTextIdleWatchdog();
|
|
415
|
+
clearSubagentStallWatchdog();
|
|
357
416
|
// Drain any callback still pending (SDK terminated while awaiting an
|
|
358
417
|
// answer). canUseTool's abort path covers signalled stops; this
|
|
359
418
|
// covers natural iterator completion.
|
|
@@ -391,6 +450,20 @@ export function createClaudeCodeEngine() {
|
|
|
391
450
|
// subtype `error_during_execution` through the normal iterator —
|
|
392
451
|
// the mapper needs this flag to treat it as a clean stop.
|
|
393
452
|
mapperState.userInterrupted = true;
|
|
453
|
+
// The soft interrupt below only ends the foreground turn — a
|
|
454
|
+
// background subagent task keeps running unless told to stop via
|
|
455
|
+
// its own API, which would otherwise leave the Stop button
|
|
456
|
+
// appearing to do nothing for up to SUBAGENT_STALL_TIMEOUT_MS.
|
|
457
|
+
for (const taskId of activeSubagentToolCallIds) {
|
|
458
|
+
try {
|
|
459
|
+
void q.stopTask(taskId).catch(() => {
|
|
460
|
+
/* best-effort */
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
/* best-effort */
|
|
465
|
+
}
|
|
466
|
+
}
|
|
394
467
|
const qq = q;
|
|
395
468
|
if (typeof qq.interrupt === 'function') {
|
|
396
469
|
try {
|
|
@@ -195,10 +195,12 @@ export function mapSdkMessage(msg, state) {
|
|
|
195
195
|
const toolCallId = typeof parsed.tool_use_id === 'string' ? parsed.tool_use_id : undefined;
|
|
196
196
|
if (toolCallId) {
|
|
197
197
|
const usage = parsed.usage;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
// Unlike task_started/task_progress (in-flight signals), task_notification
|
|
199
|
+
// is only ever emitted once a task settles — its `status` value is a
|
|
200
|
+
// reason, not a liveness signal. Treat every task_notification as
|
|
201
|
+
// terminal instead of matching against a status whitelist, so an SDK
|
|
202
|
+
// status value added later doesn't leave the subagent stuck 'running'.
|
|
203
|
+
const isDone = subtype === 'task_notification';
|
|
202
204
|
events.push({
|
|
203
205
|
kind: 'subagent:progress',
|
|
204
206
|
toolCallId,
|
|
@@ -11,6 +11,13 @@ import { createTurnLiveness } from './turn-liveness.js';
|
|
|
11
11
|
/** Long enough for normal tool work, short enough to recover a lost turn event. */
|
|
12
12
|
export const CODEX_TURN_IDLE_TIMEOUT_MS = 120_000;
|
|
13
13
|
export const CODEX_GRACEFUL_INTERRUPT_TIMEOUT_MS = 3_000;
|
|
14
|
+
// Safety net while `turnLiveness` is paused for background subagents: that
|
|
15
|
+
// pause is deliberately unbounded (a legitimate subagent can run long), but
|
|
16
|
+
// if a thread never reports a terminal status (dropped notification, or the
|
|
17
|
+
// sub-thread's own process hanging), nothing else would ever resume it or
|
|
18
|
+
// resolve `turnDonePromise` — the session would hang forever. Generous
|
|
19
|
+
// window, refreshed on every observed subagent-thread status change.
|
|
20
|
+
export const CODEX_SUBAGENT_STALL_TIMEOUT_MS = 10 * 60_000;
|
|
14
21
|
function wait(ms) {
|
|
15
22
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
23
|
}
|
|
@@ -108,6 +115,30 @@ export function createCodexEngine() {
|
|
|
108
115
|
resolveTurnDone = resolve;
|
|
109
116
|
rejectTurnDone = reject;
|
|
110
117
|
});
|
|
118
|
+
let subagentStallTimer;
|
|
119
|
+
const clearSubagentStallWatchdog = () => {
|
|
120
|
+
if (!subagentStallTimer)
|
|
121
|
+
return;
|
|
122
|
+
clearTimeout(subagentStallTimer);
|
|
123
|
+
subagentStallTimer = undefined;
|
|
124
|
+
};
|
|
125
|
+
// Re-armed on every observed subagent-thread status change so the
|
|
126
|
+
// deadline tracks the last activity, not the start of the wait.
|
|
127
|
+
const armSubagentStallWatchdog = () => {
|
|
128
|
+
clearSubagentStallWatchdog();
|
|
129
|
+
subagentStallTimer = setTimeout(() => {
|
|
130
|
+
subagentStallTimer = undefined;
|
|
131
|
+
console.warn(`[codex-engine] Background subagent thread(s) still tracked active ${CODEX_SUBAGENT_STALL_TIMEOUT_MS}ms after the turn completed — forcing session drain.`);
|
|
132
|
+
activeSubagentThreads.clear();
|
|
133
|
+
// Forced, unclean termination (a thread may still be alive
|
|
134
|
+
// server-side) — never report it as a normal completion, so
|
|
135
|
+
// auto-loop doesn't treat an orphaned run as forward progress.
|
|
136
|
+
mapperState.sawErrorResult = true;
|
|
137
|
+
if (waitingForBackgroundSubagents)
|
|
138
|
+
resolveTurnDone();
|
|
139
|
+
}, CODEX_SUBAGENT_STALL_TIMEOUT_MS);
|
|
140
|
+
subagentStallTimer.unref?.();
|
|
141
|
+
};
|
|
111
142
|
const finishBackgroundSubagent = (threadId, alreadyEmittedToolCallId) => {
|
|
112
143
|
const tracked = activeSubagentThreads.get(threadId);
|
|
113
144
|
if (!tracked)
|
|
@@ -122,8 +153,15 @@ export function createCodexEngine() {
|
|
|
122
153
|
taskType: tracked.taskType,
|
|
123
154
|
});
|
|
124
155
|
}
|
|
125
|
-
if (waitingForBackgroundSubagents
|
|
126
|
-
|
|
156
|
+
if (waitingForBackgroundSubagents) {
|
|
157
|
+
if (activeSubagentThreads.size === 0) {
|
|
158
|
+
clearSubagentStallWatchdog();
|
|
159
|
+
resolveTurnDone();
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
armSubagentStallWatchdog();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
127
165
|
};
|
|
128
166
|
const turnLiveness = createTurnLiveness({
|
|
129
167
|
timeoutMs: CODEX_TURN_IDLE_TIMEOUT_MS,
|
|
@@ -227,6 +265,7 @@ export function createCodexEngine() {
|
|
|
227
265
|
if (n.turn?.status === 'completed' && activeSubagentThreads.size > 0) {
|
|
228
266
|
waitingForBackgroundSubagents = true;
|
|
229
267
|
turnLiveness.pause();
|
|
268
|
+
armSubagentStallWatchdog();
|
|
230
269
|
}
|
|
231
270
|
else {
|
|
232
271
|
resolveTurnDone();
|
|
@@ -364,8 +403,21 @@ export function createCodexEngine() {
|
|
|
364
403
|
}
|
|
365
404
|
finally {
|
|
366
405
|
turnLiveness.stop();
|
|
406
|
+
clearSubagentStallWatchdog();
|
|
367
407
|
streamingBatcher.close();
|
|
368
408
|
iteratorRunning = false;
|
|
409
|
+
// Drain any outstanding approval/elicitation request (SDK terminated
|
|
410
|
+
// while awaiting a human decision) — without a response, Codex's own
|
|
411
|
+
// process would otherwise wait forever for a reply that never comes.
|
|
412
|
+
for (const pending of pendingByCallId.values()) {
|
|
413
|
+
try {
|
|
414
|
+
client.peer.respondError(pending.requestId, -32000, 'session ended');
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// best-effort
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
pendingByCallId.clear();
|
|
369
421
|
client.close();
|
|
370
422
|
try {
|
|
371
423
|
child.kill('SIGTERM');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.13",
|
|
4
4
|
"description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|