@loicngr/kobo 2.1.11 → 2.1.12
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
CHANGED
|
@@ -269,6 +269,14 @@ Two engines live under `src/server/services/agent/engines/`, both implementing t
|
|
|
269
269
|
|
|
270
270
|
**Claude Code** (`claude-code/`): consumes the `@anthropic-ai/claude-agent-sdk` async iterator. The SDK manages the Claude runtime; Kōbō does not launch a separate `claude` CLI command. Authentication reuses the user's Claude login or `ANTHROPIC_API_KEY`. A settled result gets a **3 s continuation grace**, then a **15 s result-drain watchdog**. Trailing metadata and child output must not cancel the parent's drain/grace. Silence is bounded at **2 minutes** without tools, **30 minutes** for foreground tools and background shell/workflow/MCP jobs, and **10 minutes** for agents. Compaction has an absolute **10-minute** ceiling; repeated status messages cannot extend it. Human questions/permissions pause inactivity; resolution AND SDK cancellation re-evaluate that pause. Watchdogs request abort plus SDK `query.close()`; neither is proof of closure: controller ownership is released only when `EngineProcess.closed` confirms it. Never fake closure or allow a second writer if the SDK ignores termination. End emission is idempotent and timers are cleared on termination and in `finally`.
|
|
271
271
|
|
|
272
|
+
After the continuation grace, Claude explicitly calls `query.close()`; closing
|
|
273
|
+
the input iterator alone can leave the SDK waiting for an idle notification after
|
|
274
|
+
a successful result. The 15-second drain watchdog is a failed-close backstop,
|
|
275
|
+
not normal cleanup. The SDK still constructs its runtime command; Kōbō's
|
|
276
|
+
`spawnClaudeCodeProcess` hook tracks actual child exit and forwards stderr.
|
|
277
|
+
`EngineProcess.closed` waits for both that exit and iterator completion because
|
|
278
|
+
the SDK's internal cleanup wait is bounded and can return before child exit.
|
|
279
|
+
|
|
272
280
|
**OpenAI Codex** (`codex/`): uses the **`codex app-server` JSON-RPC protocol** (line-delimited JSON over stdio with a long-lived `codex` subprocess). The engine layers are:
|
|
273
281
|
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
|
|
274
282
|
- `client.ts`: typed `AppServerClient` wrapping the peer (initialize / thread.start / thread.resume / turn.start / turn.interrupt)
|
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
|
+
## 2.1.12
|
|
8
|
+
|
|
9
|
+
- fix(claude): close settled SDK sessions without false watchdogs
|
|
10
|
+
|
|
7
11
|
## 2.1.11
|
|
8
12
|
|
|
9
13
|
- fix: harden agent lifecycle and workspace workflows
|
package/CONFIGURATION.md
CHANGED
|
@@ -1273,8 +1273,12 @@ The model selector is populated from [`src/shared/models.ts`](./src/shared/model
|
|
|
1273
1273
|
#### Session watchdogs
|
|
1274
1274
|
|
|
1275
1275
|
Kōbō distinguishes a finished response from a fully closed SDK session. Once a
|
|
1276
|
-
response is settled, it allows 3 seconds for continuation, then
|
|
1277
|
-
SDK
|
|
1276
|
+
response is settled, it allows 3 seconds for continuation, then explicitly closes
|
|
1277
|
+
the SDK query. Normal cleanup is reported as completion, not a watchdog failure.
|
|
1278
|
+
A 15-second drain watchdog remains if that close fails to end the iterator.
|
|
1279
|
+
Metadata alone cannot extend this cleanup deadline. Writer ownership waits for
|
|
1280
|
+
both iterator completion and confirmed runtime exit, even if the SDK's own
|
|
1281
|
+
bounded cleanup returns earlier.
|
|
1278
1282
|
|
|
1279
1283
|
Silence limits are 2 minutes without a pending tool, 10 minutes for background
|
|
1280
1284
|
agents, and 30 minutes for tools (including background Bash, workflow and MCP
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
3
4
|
import { nanoid } from 'nanoid';
|
|
@@ -327,6 +328,31 @@ export function createClaudeCodeEngine() {
|
|
|
327
328
|
env: options.env,
|
|
328
329
|
});
|
|
329
330
|
sdkOptions.abortController = abortController;
|
|
331
|
+
// SDK cleanup has its own bounded wait. Iterator completion alone does
|
|
332
|
+
// not prove that its runtime exited: retain writer ownership until both.
|
|
333
|
+
let runtimeRunning = false;
|
|
334
|
+
let runtimeClosed = Promise.resolve();
|
|
335
|
+
sdkOptions.spawnClaudeCodeProcess = ({ command, args, cwd, env, signal }) => {
|
|
336
|
+
const child = spawn(command, args, { cwd, env, signal, windowsHide: true, stdio: 'pipe' });
|
|
337
|
+
runtimeRunning = true;
|
|
338
|
+
runtimeClosed = new Promise((resolve) => {
|
|
339
|
+
const exited = () => {
|
|
340
|
+
runtimeRunning = false;
|
|
341
|
+
resolve();
|
|
342
|
+
};
|
|
343
|
+
child.once('exit', exited);
|
|
344
|
+
child.once('error', () => {
|
|
345
|
+
// A failed spawn has no process; an abort/kill error does not
|
|
346
|
+
// establish that an already spawned runtime has actually exited.
|
|
347
|
+
if (child.pid === undefined)
|
|
348
|
+
exited();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
child.stderr.setEncoding('utf8');
|
|
352
|
+
child.stderr.on('data', (data) => sdkOptions.stderr?.(data));
|
|
353
|
+
child.stderr.on('error', () => { });
|
|
354
|
+
return child;
|
|
355
|
+
};
|
|
330
356
|
// Override the SDK's libc-blind binary resolution on Linux glibc — see
|
|
331
357
|
// resolve-binary.ts for the full rationale. No-op on macOS/Windows/musl.
|
|
332
358
|
const explicitBinary = resolveClaudeBinaryPath();
|
|
@@ -395,6 +421,9 @@ export function createClaudeCodeEngine() {
|
|
|
395
421
|
const safeEmit = (ev) => streamingBatcher.push(ev);
|
|
396
422
|
let iteratorRunning = false;
|
|
397
423
|
let userInterrupted = false;
|
|
424
|
+
// Normal post-result disposal is distinct from a watchdog/user abort.
|
|
425
|
+
// The SDK can wait for an `idle` state even after stdin's iterable ends.
|
|
426
|
+
let settledCloseRequested = false;
|
|
398
427
|
let completedResponses = 0;
|
|
399
428
|
let waitingForBackground = false;
|
|
400
429
|
let turnCompletedEmittedForResponse = 0;
|
|
@@ -556,7 +585,9 @@ export function createClaudeCodeEngine() {
|
|
|
556
585
|
clearContinuationGrace();
|
|
557
586
|
continuationGraceTimer = setTimeout(() => {
|
|
558
587
|
continuationGraceTimer = undefined;
|
|
559
|
-
if (
|
|
588
|
+
if (completedResponses === 0 ||
|
|
589
|
+
!settlementPending ||
|
|
590
|
+
activeSubagentTaskIds.size > 0 ||
|
|
560
591
|
pendingResolvers.size > 0 ||
|
|
561
592
|
pendingToolCallIds.size > 0 ||
|
|
562
593
|
isCompacting ||
|
|
@@ -567,6 +598,20 @@ export function createClaudeCodeEngine() {
|
|
|
567
598
|
emitTurnCompletedIfSettled();
|
|
568
599
|
inputStream.close();
|
|
569
600
|
armResultDrainWatchdog();
|
|
601
|
+
// Closing the input iterable alone leaves the SDK in waitForRunEnd
|
|
602
|
+
// when its last session state is still running. Dispose explicitly,
|
|
603
|
+
// but let the iterator confirm actual closure before session:ended.
|
|
604
|
+
// The drain watchdog now guards a FAILED close, not normal cleanup.
|
|
605
|
+
if (!settledCloseRequested && typeof q.close === 'function') {
|
|
606
|
+
settledCloseRequested = true;
|
|
607
|
+
try {
|
|
608
|
+
q.close();
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
settledCloseRequested = false;
|
|
612
|
+
console.warn('[claude-engine] Post-result SDK close failed:', error);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
570
615
|
}, delayMs);
|
|
571
616
|
continuationGraceTimer.unref?.();
|
|
572
617
|
};
|
|
@@ -757,7 +802,12 @@ export function createClaudeCodeEngine() {
|
|
|
757
802
|
abortController.signal.aborted ||
|
|
758
803
|
/aborted by user|process aborted|abortError|ede_diagnostic/i.test(error.message ?? '');
|
|
759
804
|
if (isAbort) {
|
|
760
|
-
|
|
805
|
+
if (settledCloseRequested && !userInterrupted && !abortController.signal.aborted) {
|
|
806
|
+
const reason = mapperState.sawErrorResult ? 'error' : 'completed';
|
|
807
|
+
emitSessionEnded(reason, reason === 'completed' ? 0 : null);
|
|
808
|
+
}
|
|
809
|
+
else
|
|
810
|
+
emitSessionEnded('killed', null);
|
|
761
811
|
}
|
|
762
812
|
else {
|
|
763
813
|
safeEmit({
|
|
@@ -797,7 +847,7 @@ export function createClaudeCodeEngine() {
|
|
|
797
847
|
})();
|
|
798
848
|
const engineProcess = {
|
|
799
849
|
ready,
|
|
800
|
-
closed: iteratorPromise.then(() =>
|
|
850
|
+
closed: iteratorPromise.then(() => runtimeClosed),
|
|
801
851
|
get pid() {
|
|
802
852
|
return undefined;
|
|
803
853
|
},
|
|
@@ -805,10 +855,10 @@ export function createClaudeCodeEngine() {
|
|
|
805
855
|
return discoveredSessionId;
|
|
806
856
|
},
|
|
807
857
|
isAlive() {
|
|
808
|
-
return iteratorRunning;
|
|
858
|
+
return iteratorRunning || runtimeRunning;
|
|
809
859
|
},
|
|
810
860
|
sendMessage(text) {
|
|
811
|
-
if (!iteratorRunning || queryCloseRequested || sessionEndedEmitted)
|
|
861
|
+
if (!iteratorRunning || queryCloseRequested || settledCloseRequested || sessionEndedEmitted)
|
|
812
862
|
throw new Error(`Claude ${AGENT_NO_LONGER_RUNNING_TEXT}`);
|
|
813
863
|
inputStream.send(text);
|
|
814
864
|
armSubagentStallWatchdog();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.12",
|
|
4
4
|
"description": "Kōbō — local multi-workspace orchestrator for Claude Code and OpenAI Codex, with isolated Git worktrees, live review and optional automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|