@ai-sdk/harness-claude-code 1.0.96 → 1.0.97

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-claude-code",
3
- "version": "1.0.96",
3
+ "version": "1.0.97",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,16 +26,16 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.92",
30
- "@ai-sdk/provider-utils": "5.0.32",
29
+ "@ai-sdk/harness": "1.0.93",
30
+ "@ai-sdk/provider-utils": "5.0.33",
31
31
  "ws": "^8.21.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "zod": "^3.25.76 || ^4.1.8"
35
35
  },
36
36
  "devDependencies": {
37
- "@anthropic-ai/claude-agent-sdk": "0.3.213",
38
- "@modelcontextprotocol/sdk": "1.29.0",
37
+ "@anthropic-ai/claude-agent-sdk": "0.3.245",
38
+ "@modelcontextprotocol/sdk": "1.30.0",
39
39
  "@types/node": "22.19.19",
40
40
  "@types/ws": "^8.5.13",
41
41
  "@vercel/ai-tsconfig": "0.0.0",
@@ -253,12 +253,27 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
253
253
  // Local controller for the Claude query. Aborted either by the host (via the
254
254
  // shared runtime's `turn.abortSignal`) or by us on a terminal error.
255
255
  const abortCtl = new AbortController();
256
+ // A host abort prefers the SDK's graceful `interrupt()` — Esc semantics: the
257
+ // in-flight turn is persisted to the session transcript and settles with an
258
+ // interrupted result, so a later resume (including the user's own
259
+ // `claude --resume`) still sees the work done before the interrupt. The hard
260
+ // abort kills the CLI process and loses that turn's records, so it is only
261
+ // the fallback — armed unconditionally, because aborting an already-settled
262
+ // query is a no-op — and the immediate path when the abort arrives before
263
+ // the query exists.
264
+ let gracefulAbort: (() => void) | undefined;
265
+ let hardAbortTimer: ReturnType<typeof setTimeout> | undefined;
266
+ const onHostAbort = (): void => {
267
+ if (gracefulAbort) {
268
+ gracefulAbort();
269
+ } else {
270
+ abortCtl.abort();
271
+ }
272
+ };
256
273
  if (turn.abortSignal.aborted) {
257
274
  abortCtl.abort();
258
275
  } else {
259
- turn.abortSignal.addEventListener('abort', () => abortCtl.abort(), {
260
- once: true,
261
- });
276
+ turn.abortSignal.addEventListener('abort', onHostAbort, { once: true });
262
277
  }
263
278
 
264
279
  const streamEventState = createClaudeStreamEventState();
@@ -388,6 +403,19 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
388
403
  abortSignal: abortCtl.signal,
389
404
  },
390
405
  });
406
+
407
+ gracefulAbort = () => {
408
+ // Backstop for the whole teardown, not just the interrupt call: if the
409
+ // stream has not settled five seconds after a graceful interrupt was
410
+ // requested, fall back to the hard abort. Aborting an already-settled
411
+ // query is a no-op, and `unref` keeps the timer from pinning the bridge
412
+ // process open on its own.
413
+ hardAbortTimer = setTimeout(() => abortCtl.abort(), 5000);
414
+ hardAbortTimer.unref?.();
415
+ void Promise.resolve()
416
+ .then(() => q.interrupt())
417
+ .catch(() => abortCtl.abort());
418
+ };
391
419
  let turnUsage: Record<string, unknown> | undefined;
392
420
  let totalCostUsd: number | undefined;
393
421
  let emittedTerminalError = false;
@@ -398,10 +426,17 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
398
426
  if (!normalized || emittedTerminalError || emittedTerminalFinish) return;
399
427
  streamEventState.observedTerminalError = normalized;
400
428
  emittedTerminalError = true;
401
- turn.emitError({
402
- error: normalized,
403
- message: 'claude-code terminal error',
404
- });
429
+ // A turn the host itself stopped ends with an error-shaped result by
430
+ // construction (an interrupted query reports a diagnostic, not success);
431
+ // reporting the host's own stop as a terminal error makes every clean
432
+ // interrupt look like a malfunction. The host has already settled the
433
+ // turn on its side.
434
+ if (!turn.abortSignal.aborted) {
435
+ turn.emitError({
436
+ error: normalized,
437
+ message: 'claude-code terminal error',
438
+ });
439
+ }
405
440
  queryInput.close();
406
441
  abortCtl.abort();
407
442
  };
@@ -483,12 +518,38 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
483
518
  }
484
519
  }
485
520
  } catch (err) {
486
- if (!(abortCtl.signal.aborted && emittedTerminalError)) {
521
+ // Same reasoning as `emitTerminalError`: a throw after the host's own
522
+ // abort (e.g. the hard-abort fallback killing the CLI mid-iteration, or
523
+ // a rejected `interrupt()`) is the stop the host asked for, not a
524
+ // malfunction. The host has already settled the turn on its side.
525
+ if (
526
+ !turn.abortSignal.aborted &&
527
+ !(abortCtl.signal.aborted && emittedTerminalError)
528
+ ) {
487
529
  turn.emitError({ error: err, message: 'claude-code turn failed' });
488
530
  }
489
531
  return;
490
532
  } finally {
533
+ // The turn is over; disarm the host-abort path first. An abort of this
534
+ // turn's signal arriving after this point (e.g. an `abort` message racing
535
+ // the next `start`) must not interrupt the disposed query or arm the
536
+ // hard-abort fallback timer for it.
537
+ gracefulAbort = undefined;
538
+ if (hardAbortTimer != null) clearTimeout(hardAbortTimer);
539
+ turn.abortSignal.removeEventListener('abort', onHostAbort);
491
540
  queryInput.close();
541
+ // Dispose the query explicitly: with streaming input the SDK keeps its
542
+ // CLI subprocess alive for more user messages, and a turn that ended
543
+ // through an interrupt or error path can otherwise leak that process —
544
+ // observed as orphaned `claude` processes holding the very conversation
545
+ // the next turn continues.
546
+ try {
547
+ await (q as { return?: (value?: unknown) => Promise<unknown> }).return?.(
548
+ undefined,
549
+ );
550
+ } catch {
551
+ // Best effort; the abort controller tears the process down otherwise.
552
+ }
492
553
  }
493
554
 
494
555
  if (emittedTerminalError) return;
@@ -4,9 +4,9 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@anthropic-ai/claude-agent-sdk": "0.3.213",
8
- "@anthropic-ai/claude-code": "2.1.213",
9
- "@modelcontextprotocol/sdk": "1.29.0",
7
+ "@anthropic-ai/claude-agent-sdk": "0.3.245",
8
+ "@anthropic-ai/claude-code": "2.1.245",
9
+ "@modelcontextprotocol/sdk": "1.30.0",
10
10
  "ws": "8.21.0",
11
11
  "zod": "4.4.3"
12
12
  }