@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 15 seconds for
1277
- SDK cleanup. Metadata alone cannot extend this cleanup deadline.
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 (activeSubagentTaskIds.size > 0 ||
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
- emitSessionEnded('killed', null);
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.11",
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",