@loicngr/kobo 2.1.11 → 2.1.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/AGENTS.md +12 -0
- package/CHANGELOG.md +8 -0
- package/CONFIGURATION.md +12 -2
- package/dist/server/services/agent/engines/claude-code/engine.js +79 -9
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -269,6 +269,18 @@ 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
|
+
Liveness must also inspect non-empty SDK content deltas (text, thinking,
|
|
280
|
+
input JSON, signature), not just mapped UI events: long reasoning/tool argument
|
|
281
|
+
generation can otherwise be killed as idle. Do not count empty deltas or neutral
|
|
282
|
+
metadata, or let child deltas cancel a settled parent's drain/grace.
|
|
283
|
+
|
|
272
284
|
**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
285
|
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
|
|
274
286
|
- `client.ts`: typed `AppServerClient` wrapping the peer (initialize / thread.start / thread.resume / turn.start / turn.interrupt)
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ 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.13
|
|
8
|
+
|
|
9
|
+
- fix(claude): count streaming generation as watchdog activity
|
|
10
|
+
|
|
11
|
+
## 2.1.12
|
|
12
|
+
|
|
13
|
+
- fix(claude): close settled SDK sessions without false watchdogs
|
|
14
|
+
|
|
7
15
|
## 2.1.11
|
|
8
16
|
|
|
9
17
|
- fix: harden agent lifecycle and workspace workflows
|
package/CONFIGURATION.md
CHANGED
|
@@ -1273,14 +1273,24 @@ 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
|
|
1281
1285
|
jobs). A compaction has a separate absolute 10-minute limit. Waiting for your
|
|
1282
1286
|
answer or permission pauses the inactivity watchdog.
|
|
1283
1287
|
|
|
1288
|
+
Non-empty streaming text, reasoning, tool-argument JSON and signature fragments
|
|
1289
|
+
count as activity even when they are not displayed in chat. Empty deltas and
|
|
1290
|
+
neutral metadata do not reset inactivity, and child output cannot postpone a
|
|
1291
|
+
settled parent's closure. The inactivity limit measures silence, not total time
|
|
1292
|
+
spent generating a large plan or file.
|
|
1293
|
+
|
|
1284
1294
|
Watchdog notices remain in conversation history, but do not display a red error
|
|
1285
1295
|
banner. Other failures retain their banners. Watchdog shutdown requests both
|
|
1286
1296
|
cancellation and closure of the SDK transport. Kōbō still requires confirmed
|
|
@@ -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';
|
|
@@ -144,6 +145,24 @@ class ClaudeInputStream {
|
|
|
144
145
|
return { type: 'user', uuid: randomUUID(), message: { role: 'user', content: text }, parent_tool_use_id: null };
|
|
145
146
|
}
|
|
146
147
|
}
|
|
148
|
+
/** Generation can be active without any user-visible mapped event. */
|
|
149
|
+
function hasStreamingContent(msg) {
|
|
150
|
+
if (msg.type !== 'stream_event' || msg.event.type !== 'content_block_delta')
|
|
151
|
+
return false;
|
|
152
|
+
const delta = msg.event.delta;
|
|
153
|
+
switch (delta.type) {
|
|
154
|
+
case 'text_delta':
|
|
155
|
+
return delta.text.length > 0;
|
|
156
|
+
case 'thinking_delta':
|
|
157
|
+
return delta.thinking.length > 0;
|
|
158
|
+
case 'input_json_delta':
|
|
159
|
+
return delta.partial_json.length > 0;
|
|
160
|
+
case 'signature_delta':
|
|
161
|
+
return delta.signature.length > 0;
|
|
162
|
+
default:
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
147
166
|
export function createClaudeCodeEngine() {
|
|
148
167
|
return {
|
|
149
168
|
id: 'claude-code',
|
|
@@ -327,6 +346,31 @@ export function createClaudeCodeEngine() {
|
|
|
327
346
|
env: options.env,
|
|
328
347
|
});
|
|
329
348
|
sdkOptions.abortController = abortController;
|
|
349
|
+
// SDK cleanup has its own bounded wait. Iterator completion alone does
|
|
350
|
+
// not prove that its runtime exited: retain writer ownership until both.
|
|
351
|
+
let runtimeRunning = false;
|
|
352
|
+
let runtimeClosed = Promise.resolve();
|
|
353
|
+
sdkOptions.spawnClaudeCodeProcess = ({ command, args, cwd, env, signal }) => {
|
|
354
|
+
const child = spawn(command, args, { cwd, env, signal, windowsHide: true, stdio: 'pipe' });
|
|
355
|
+
runtimeRunning = true;
|
|
356
|
+
runtimeClosed = new Promise((resolve) => {
|
|
357
|
+
const exited = () => {
|
|
358
|
+
runtimeRunning = false;
|
|
359
|
+
resolve();
|
|
360
|
+
};
|
|
361
|
+
child.once('exit', exited);
|
|
362
|
+
child.once('error', () => {
|
|
363
|
+
// A failed spawn has no process; an abort/kill error does not
|
|
364
|
+
// establish that an already spawned runtime has actually exited.
|
|
365
|
+
if (child.pid === undefined)
|
|
366
|
+
exited();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
child.stderr.setEncoding('utf8');
|
|
370
|
+
child.stderr.on('data', (data) => sdkOptions.stderr?.(data));
|
|
371
|
+
child.stderr.on('error', () => { });
|
|
372
|
+
return child;
|
|
373
|
+
};
|
|
330
374
|
// Override the SDK's libc-blind binary resolution on Linux glibc — see
|
|
331
375
|
// resolve-binary.ts for the full rationale. No-op on macOS/Windows/musl.
|
|
332
376
|
const explicitBinary = resolveClaudeBinaryPath();
|
|
@@ -395,6 +439,9 @@ export function createClaudeCodeEngine() {
|
|
|
395
439
|
const safeEmit = (ev) => streamingBatcher.push(ev);
|
|
396
440
|
let iteratorRunning = false;
|
|
397
441
|
let userInterrupted = false;
|
|
442
|
+
// Normal post-result disposal is distinct from a watchdog/user abort.
|
|
443
|
+
// The SDK can wait for an `idle` state even after stdin's iterable ends.
|
|
444
|
+
let settledCloseRequested = false;
|
|
398
445
|
let completedResponses = 0;
|
|
399
446
|
let waitingForBackground = false;
|
|
400
447
|
let turnCompletedEmittedForResponse = 0;
|
|
@@ -556,7 +603,9 @@ export function createClaudeCodeEngine() {
|
|
|
556
603
|
clearContinuationGrace();
|
|
557
604
|
continuationGraceTimer = setTimeout(() => {
|
|
558
605
|
continuationGraceTimer = undefined;
|
|
559
|
-
if (
|
|
606
|
+
if (completedResponses === 0 ||
|
|
607
|
+
!settlementPending ||
|
|
608
|
+
activeSubagentTaskIds.size > 0 ||
|
|
560
609
|
pendingResolvers.size > 0 ||
|
|
561
610
|
pendingToolCallIds.size > 0 ||
|
|
562
611
|
isCompacting ||
|
|
@@ -567,6 +616,20 @@ export function createClaudeCodeEngine() {
|
|
|
567
616
|
emitTurnCompletedIfSettled();
|
|
568
617
|
inputStream.close();
|
|
569
618
|
armResultDrainWatchdog();
|
|
619
|
+
// Closing the input iterable alone leaves the SDK in waitForRunEnd
|
|
620
|
+
// when its last session state is still running. Dispose explicitly,
|
|
621
|
+
// but let the iterator confirm actual closure before session:ended.
|
|
622
|
+
// The drain watchdog now guards a FAILED close, not normal cleanup.
|
|
623
|
+
if (!settledCloseRequested && typeof q.close === 'function') {
|
|
624
|
+
settledCloseRequested = true;
|
|
625
|
+
try {
|
|
626
|
+
q.close();
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
settledCloseRequested = false;
|
|
630
|
+
console.warn('[claude-engine] Post-result SDK close failed:', error);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
570
633
|
}, delayMs);
|
|
571
634
|
continuationGraceTimer.unref?.();
|
|
572
635
|
};
|
|
@@ -616,11 +679,13 @@ export function createClaudeCodeEngine() {
|
|
|
616
679
|
for await (const msg of q) {
|
|
617
680
|
const events = mapSdkMessage(msg, mapperState);
|
|
618
681
|
const isForeground = !('parent_tool_use_id' in msg && msg.parent_tool_use_id != null);
|
|
682
|
+
const streamingContent = hasStreamingContent(msg);
|
|
619
683
|
const foregroundProgress = isForeground &&
|
|
620
|
-
|
|
621
|
-
ev.kind === 'message:
|
|
622
|
-
|
|
623
|
-
|
|
684
|
+
(streamingContent ||
|
|
685
|
+
events.some((ev) => ev.kind === 'message:text' ||
|
|
686
|
+
ev.kind === 'message:thinking' ||
|
|
687
|
+
ev.kind === 'tool:call' ||
|
|
688
|
+
ev.kind === 'tool:result'));
|
|
624
689
|
// A parent continuation owns the foreground again. Background
|
|
625
690
|
// progress notifications alone do not end the between-turn wait.
|
|
626
691
|
if (foregroundProgress) {
|
|
@@ -757,7 +822,12 @@ export function createClaudeCodeEngine() {
|
|
|
757
822
|
abortController.signal.aborted ||
|
|
758
823
|
/aborted by user|process aborted|abortError|ede_diagnostic/i.test(error.message ?? '');
|
|
759
824
|
if (isAbort) {
|
|
760
|
-
|
|
825
|
+
if (settledCloseRequested && !userInterrupted && !abortController.signal.aborted) {
|
|
826
|
+
const reason = mapperState.sawErrorResult ? 'error' : 'completed';
|
|
827
|
+
emitSessionEnded(reason, reason === 'completed' ? 0 : null);
|
|
828
|
+
}
|
|
829
|
+
else
|
|
830
|
+
emitSessionEnded('killed', null);
|
|
761
831
|
}
|
|
762
832
|
else {
|
|
763
833
|
safeEmit({
|
|
@@ -797,7 +867,7 @@ export function createClaudeCodeEngine() {
|
|
|
797
867
|
})();
|
|
798
868
|
const engineProcess = {
|
|
799
869
|
ready,
|
|
800
|
-
closed: iteratorPromise.then(() =>
|
|
870
|
+
closed: iteratorPromise.then(() => runtimeClosed),
|
|
801
871
|
get pid() {
|
|
802
872
|
return undefined;
|
|
803
873
|
},
|
|
@@ -805,10 +875,10 @@ export function createClaudeCodeEngine() {
|
|
|
805
875
|
return discoveredSessionId;
|
|
806
876
|
},
|
|
807
877
|
isAlive() {
|
|
808
|
-
return iteratorRunning;
|
|
878
|
+
return iteratorRunning || runtimeRunning;
|
|
809
879
|
},
|
|
810
880
|
sendMessage(text) {
|
|
811
|
-
if (!iteratorRunning || queryCloseRequested || sessionEndedEmitted)
|
|
881
|
+
if (!iteratorRunning || queryCloseRequested || settledCloseRequested || sessionEndedEmitted)
|
|
812
882
|
throw new Error(`Claude ${AGENT_NO_LONGER_RUNNING_TEXT}`);
|
|
813
883
|
inputStream.send(text);
|
|
814
884
|
armSubagentStallWatchdog();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.13",
|
|
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",
|