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

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.97",
3
+ "version": "1.0.99",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,8 +26,8 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.93",
30
- "@ai-sdk/provider-utils": "5.0.33",
29
+ "@ai-sdk/harness": "1.0.95",
30
+ "@ai-sdk/provider-utils": "5.0.34",
31
31
  "ws": "^8.21.0"
32
32
  },
33
33
  "peerDependencies": {
@@ -21,8 +21,17 @@ export type ClaudeMessage = {
21
21
  event?: {
22
22
  type?: string;
23
23
  index?: number;
24
- content_block?: { type?: string };
25
- delta?: { type?: string; text?: string; thinking?: string };
24
+ content_block?: {
25
+ type?: string;
26
+ id?: string;
27
+ name?: string;
28
+ };
29
+ delta?: {
30
+ type?: string;
31
+ text?: string;
32
+ thinking?: string;
33
+ partial_json?: string;
34
+ };
26
35
  };
27
36
  message?: {
28
37
  content?: ReadonlyArray<MessageBlock>;
@@ -56,7 +65,7 @@ export type ClaudeStreamEventState = {
56
65
  */
57
66
  nativeToolCallNames: Map<string, string>;
58
67
  approvalRequestedToolUseIds: Set<string>;
59
- partialBlocks: Map<number, { id: string; kind: 'text' | 'thinking' }>;
68
+ partialBlocks: Map<number, PartialBlock>;
60
69
  stepUsage: Record<string, unknown> | undefined;
61
70
  pendingStepToolUseIds: Set<string>;
62
71
  pendingStepUsage: Record<string, unknown> | undefined;
@@ -64,9 +73,8 @@ export type ClaudeStreamEventState = {
64
73
  /*
65
74
  * Tool-use ids that originated from the MCP server hosting user-supplied
66
75
  * tools. The MCP handler emits its own `tool-call`/`tool-result` pair with
67
- * the user-facing tool name and a synthetic id, so the duplicate
68
- * `tool_result` block Claude reports for the underlying native id must be
69
- * suppressed.
76
+ * the user-facing tool name, so the duplicate `tool_result` block Claude
77
+ * reports for the underlying native id must be suppressed.
70
78
  */
71
79
  mcpToolUseIds: Set<string>;
72
80
  externalMcpToolUseIds: Set<string>;
@@ -74,6 +82,10 @@ export type ClaudeStreamEventState = {
74
82
  observedTerminalError: string | undefined;
75
83
  };
76
84
 
85
+ type PartialBlock =
86
+ | { id: string; kind: 'text' | 'thinking' }
87
+ | { id: string; kind: 'tool-input' };
88
+
77
89
  export function createClaudeStreamEventState(): ClaudeStreamEventState {
78
90
  return {
79
91
  nativeToolCallNames: new Map(),
@@ -195,7 +207,12 @@ export function createEmitStreamEvent({
195
207
  }
196
208
 
197
209
  if (type === 'stream_event') {
198
- handleStreamEvent(msg.event, state.partialBlocks, emit);
210
+ handleStreamEvent({
211
+ event: msg.event,
212
+ state,
213
+ send: emit,
214
+ toCommonName,
215
+ });
199
216
  return;
200
217
  }
201
218
 
@@ -371,13 +388,22 @@ function formatApiRetryWarning(msg: ClaudeMessage): string {
371
388
  : 'Claude Code API retry';
372
389
  }
373
390
 
374
- function handleStreamEvent(
375
- event: ClaudeMessage['event'] | undefined,
376
- partialBlocks: Map<number, { id: string; kind: 'text' | 'thinking' }>,
377
- send: Emit,
378
- ): void {
391
+ const HOST_TOOL_PREFIX = 'mcp__harness-tools__';
392
+
393
+ function handleStreamEvent({
394
+ event,
395
+ state,
396
+ send,
397
+ toCommonName,
398
+ }: {
399
+ event: ClaudeMessage['event'] | undefined;
400
+ state: ClaudeStreamEventState;
401
+ send: Emit;
402
+ toCommonName: (nativeName: string) => string;
403
+ }): void {
379
404
  if (!event || typeof event.index !== 'number') return;
380
405
  const index = event.index;
406
+ const partialBlocks = state.partialBlocks;
381
407
 
382
408
  if (event.type === 'content_block_start') {
383
409
  const blockType = event.content_block?.type;
@@ -389,6 +415,29 @@ function handleStreamEvent(
389
415
  const id = randomUUID();
390
416
  partialBlocks.set(index, { id, kind: 'thinking' });
391
417
  send({ type: 'reasoning-start', id });
418
+ } else if (
419
+ blockType === 'tool_use' &&
420
+ typeof event.content_block?.id === 'string' &&
421
+ typeof event.content_block.name === 'string'
422
+ ) {
423
+ const id = event.content_block.id;
424
+ const nativeName = event.content_block.name;
425
+ if (nativeName === 'StructuredOutput') {
426
+ return;
427
+ }
428
+ const hostToolName = nativeName.startsWith(HOST_TOOL_PREFIX)
429
+ ? nativeName.slice(HOST_TOOL_PREFIX.length)
430
+ : undefined;
431
+ const dynamic =
432
+ hostToolName === undefined && nativeName.startsWith('mcp__');
433
+ partialBlocks.set(index, { id, kind: 'tool-input' });
434
+ send({
435
+ type: 'tool-input-start',
436
+ id,
437
+ toolName: hostToolName ?? toCommonName(nativeName),
438
+ providerExecuted: hostToolName === undefined,
439
+ ...(dynamic ? { dynamic: true } : {}),
440
+ });
392
441
  }
393
442
  return;
394
443
  }
@@ -412,6 +461,16 @@ function handleStreamEvent(
412
461
  id: block.id,
413
462
  delta: event.delta.thinking,
414
463
  });
464
+ } else if (
465
+ block.kind === 'tool-input' &&
466
+ event.delta?.type === 'input_json_delta' &&
467
+ typeof event.delta.partial_json === 'string'
468
+ ) {
469
+ send({
470
+ type: 'tool-input-delta',
471
+ id: block.id,
472
+ delta: event.delta.partial_json,
473
+ });
415
474
  }
416
475
  return;
417
476
  }
@@ -422,8 +481,10 @@ function handleStreamEvent(
422
481
  partialBlocks.delete(index);
423
482
  if (block.kind === 'text') {
424
483
  send({ type: 'text-end', id: block.id });
425
- } else {
484
+ } else if (block.kind === 'thinking') {
426
485
  send({ type: 'reasoning-end', id: block.id });
486
+ } else {
487
+ send({ type: 'tool-input-end', id: block.id });
427
488
  }
428
489
  }
429
490
  }
@@ -125,13 +125,23 @@ const claudeSdk = claudeAgentSdk as any;
125
125
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
126
  const mcpModule = mcpServerModule as any;
127
127
 
128
+ /**
129
+ * The Claude session id most recently reported by the SDK, captured from the
130
+ * message stream. Every turn in this bridge process may fork a new id (the
131
+ * SDK's `continue`/`resume` create a new session linked to the previous one),
132
+ * so the latest observation is the one a later resume must name.
133
+ */
134
+ let lastClaudeSessionId: string | undefined;
135
+
128
136
  await runBridge<StartMessage>({
129
137
  bridgeType: 'claude-code',
130
138
  bridgeStateDir,
131
139
  onStart: runTurn,
132
- // Claude Code's session state lives in the workdir on the sandbox filesystem
133
- // (captured by the sandbox snapshot on stop); the resume payload is empty.
134
- onStop: () => ({}),
140
+ // Claude Code's conversation state lives in the runtime's own store, keyed
141
+ // by working directory. The resume payload names the exact conversation so a
142
+ // later resume does not have to fall back to "most recent in this workdir".
143
+ onStop: () =>
144
+ lastClaudeSessionId == null ? {} : { claudeSessionId: lastClaudeSessionId },
135
145
  });
136
146
 
137
147
  type Emit = (msg: Record<string, unknown>) => void;
@@ -290,8 +300,18 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
290
300
  tool.name,
291
301
  tool.description ?? '',
292
302
  shape,
293
- async (input: Record<string, unknown>) => {
294
- const toolCallId = randomUUID();
303
+ async (
304
+ ...handlerArgs: [
305
+ Record<string, unknown>,
306
+ { requestId: string | number; _meta?: Record<string, unknown> },
307
+ ]
308
+ ) => {
309
+ const [input, extra] = handlerArgs;
310
+ const metadataToolCallId = extra._meta?.['claudecode/toolUseId'];
311
+ const toolCallId =
312
+ typeof metadataToolCallId === 'string'
313
+ ? metadataToolCallId
314
+ : randomUUID();
295
315
  emit({
296
316
  type: 'tool-call',
297
317
  toolCallId,
@@ -392,11 +412,24 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
392
412
  },
393
413
  ],
394
414
  },
395
- // Continuation rule: the host can force-continue (resume after a
396
- // cross-process detach) by setting `start.continue: true`; otherwise
397
- // we continue every subsequent turn after the first one in this
398
- // bridge process.
399
- ...(start.continue === true || !turn.firstTurn ? { continue: true } : {}),
415
+ // Continuation rule, most specific first.
416
+ //
417
+ // `resumeSessionId` names the exact conversation and is what a
418
+ // cross-process resume should use: `continue` means "most recent thread
419
+ // in this workdir", which silently picks the wrong one once anything
420
+ // else has run there. The bridge also retains the id observed during its
421
+ // previous query, so every later query stays pinned to that conversation
422
+ // even when the host detached and reattached between turns. `resume` and
423
+ // `continue` are mutually exclusive in the SDK.
424
+ //
425
+ // Otherwise the host can force-continue by setting `start.continue`,
426
+ // and turns after the first fall back to the legacy cwd-based behavior
427
+ // when no exact id was observed.
428
+ ...((start.resumeSessionId ?? lastClaudeSessionId)
429
+ ? { resume: start.resumeSessionId ?? lastClaudeSessionId }
430
+ : start.continue === true || !turn.firstTurn
431
+ ? { continue: true }
432
+ : {}),
400
433
  ...permissionOptions,
401
434
  mcpServers,
402
435
  cwd: workdir,
@@ -460,6 +493,14 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
460
493
  queryInput.handleLifecycle(msg);
461
494
  }
462
495
 
496
+ // Every SDK message carries the session id of the conversation it
497
+ // belongs to. Track the latest so the stop payload and the terminal
498
+ // finish metadata name the exact conversation.
499
+ const sessionId = (msg as { session_id?: unknown }).session_id;
500
+ if (typeof sessionId === 'string' && sessionId.length > 0) {
501
+ lastClaudeSessionId = sessionId;
502
+ }
503
+
463
504
  emitStreamEvent(msg);
464
505
 
465
506
  if (type === 'result') {
@@ -559,8 +600,20 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
559
600
  type: 'finish',
560
601
  finishReason: { unified: 'stop', raw: 'stop' },
561
602
  totalUsage: turnUsage ?? streamEventState.stepUsage ?? defaultUsage(),
562
- ...(totalCostUsd !== undefined
563
- ? { harnessMetadata: { 'claude-code': { costUsd: totalCostUsd } } }
603
+ ...(totalCostUsd !== undefined || lastClaudeSessionId !== undefined
604
+ ? {
605
+ harnessMetadata: {
606
+ 'claude-code': {
607
+ ...(totalCostUsd !== undefined ? { costUsd: totalCostUsd } : {}),
608
+ // The conversation this turn belongs to, resumable outside the
609
+ // SDK with `claude --resume <sessionId>` and captured by the
610
+ // adapter for exact cross-process resume.
611
+ ...(lastClaudeSessionId !== undefined
612
+ ? { sessionId: lastClaudeSessionId }
613
+ : {}),
614
+ },
615
+ },
616
+ }
564
617
  : {}),
565
618
  });
566
619
  }
@@ -51,19 +51,23 @@ export async function getClaudeCodeBootstrap(): Promise<HarnessV1Bootstrap> {
51
51
  }
52
52
 
53
53
  async function readBridgeAsset(name: string): Promise<string> {
54
- const candidates = [
55
- new URL(`./bridge/${name}`, import.meta.url),
56
- new URL(`../bridge/${name}`, import.meta.url),
57
- ];
58
- let lastErr: unknown;
59
- for (const url of candidates) {
60
- try {
61
- return await readFile(fileURLToPath(url), 'utf8');
62
- } catch (err) {
63
- const code = (err as NodeJS.ErrnoException).code;
64
- if (code !== 'ENOENT') throw err;
65
- lastErr = err;
66
- }
67
- }
68
- throw lastErr ?? new Error(`bridge asset not found: ${name}`);
54
+ return readFile(
55
+ fileURLToPath(
56
+ resolveBridgeAssetUrl({
57
+ name,
58
+ moduleUrl: import.meta.url,
59
+ }),
60
+ ),
61
+ 'utf8',
62
+ );
63
+ }
64
+
65
+ export function resolveBridgeAssetUrl({
66
+ name,
67
+ moduleUrl,
68
+ }: {
69
+ name: string;
70
+ moduleUrl: string | URL;
71
+ }): URL {
72
+ return new URL(`./bridge/${name}`, moduleUrl);
69
73
  }
@@ -42,6 +42,14 @@ export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
42
42
  // Claude SDK so the in-workdir thread state is rehydrated. The host sets this
43
43
  // on the first prompt after a cross-process resume.
44
44
  continue: z.boolean().optional(),
45
+ // Exact conversation to resume, as the Claude SDK's `resume` option.
46
+ //
47
+ // Preferred over `continue` whenever the host knows which conversation it
48
+ // means: `continue` resolves to the most recent thread in the workdir, which
49
+ // is the wrong one as soon as anything else has run there since — including
50
+ // a resume that itself started a new thread. Mutually exclusive with
51
+ // `continue` in the SDK, so the bridge sends one or the other.
52
+ resumeSessionId: z.string().optional(),
45
53
  });
46
54
 
47
55
  export type StartMessage = z.infer<typeof startMessageSchema>;
@@ -1,4 +1,3 @@
1
- import { randomBytes } from 'node:crypto';
2
1
  import { posix } from 'node:path';
3
2
  import {
4
3
  commonTool,
@@ -24,6 +23,7 @@ import {
24
23
  applyCredentialForwarding,
25
24
  classifyDiskLog,
26
25
  createSandboxCredentialEnvironment,
26
+ createBridgeToken,
27
27
  experimental_createBridgeUserMessageSubmitter,
28
28
  createBridgeErrorHandler,
29
29
  createBridgeStartupError,
@@ -37,6 +37,7 @@ import {
37
37
  shellQuote,
38
38
  warnCredentialBrokeringUnavailable,
39
39
  waitForBridgeReady,
40
+ withBridgeToken,
40
41
  writeSkills as writeHarnessSkills,
41
42
  } from '@ai-sdk/harness/utils';
42
43
  import {
@@ -802,6 +803,17 @@ const claudeCodeBridgeCoordsSchema = z.object({
802
803
  const claudeCodeResumeStateSchema = z.looseObject({
803
804
  bridge: claudeCodeBridgeCoordsSchema.optional(),
804
805
  sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional(),
806
+ /**
807
+ * The exact Claude conversation to rehydrate on resume. Written by the
808
+ * adapter on `doStop()`/`doDetach()`/`doSuspendTurn()` from the session id
809
+ * the bridge observed, so a resume names the conversation instead of
810
+ * relying on the SDK's `continue` flag — "most recent thread in this
811
+ * workdir" — which silently picks the wrong one once a second thread
812
+ * exists there. Hosts that captured the id themselves (it is surfaced as
813
+ * `harnessMetadata['claude-code'].sessionId` on `finish` parts) may also
814
+ * set it explicitly.
815
+ */
816
+ claudeSessionId: z.string().optional(),
805
817
  });
806
818
 
807
819
  type ClaudeCodeBridgeCoords = z.infer<typeof claudeCodeBridgeCoordsSchema>;
@@ -831,7 +843,6 @@ export function createClaudeCode(
831
843
  lifecycleStateSchema: claudeCodeResumeStateSchema,
832
844
  getBootstrap: getClaudeCodeBootstrap,
833
845
  doStart: async startOpts => {
834
- const model = startOpts.model ?? settings.model;
835
846
  const sandboxSession = startOpts.sandboxSession;
836
847
  const toolSafeSandboxSession =
837
848
  getRestrictedSandboxSession(sandboxSession);
@@ -860,6 +871,7 @@ export function createClaudeCode(
860
871
  ? (lifecycleState?.data as {
861
872
  bridge?: ClaudeCodeBridgeCoords;
862
873
  sandboxCredentialEnvironment?: Record<string, string>;
874
+ claudeSessionId?: string;
863
875
  })
864
876
  : undefined;
865
877
  const coords = resumeData?.bridge;
@@ -921,6 +933,10 @@ export function createClaudeCode(
921
933
  defaultWorkingDirectory,
922
934
  BOOTSTRAP_DIR,
923
935
  );
936
+ // The conversation the host wants back, when it is known. Absent on
937
+ // state written before this field existed; those resumes fall back to
938
+ // the `continue` flag as before.
939
+ const resumeSessionId = resumeData?.claudeSessionId;
924
940
 
925
941
  const workDir = startOpts.sessionWorkDir;
926
942
  const sandboxHomeDir = await resolveSandboxHomeDir({
@@ -994,11 +1010,12 @@ export function createClaudeCode(
994
1010
  return createSession({
995
1011
  sessionId: startOpts.sessionId,
996
1012
  channel: attachChannel,
1013
+ ...(resumeSessionId ? { resumeSessionId } : {}),
997
1014
  // The live bridge was spawned by another process; this one owns no
998
1015
  // process handle. The session lifecycle method decides whether the
999
1016
  // sandbox is left running, stopped, or destroyed.
1000
1017
  proc: undefined,
1001
- model,
1018
+ model: settings.model,
1002
1019
  maxTurns: settings.maxTurns,
1003
1020
  env: sandboxClaudeEnvironment,
1004
1021
  thinking,
@@ -1053,7 +1070,7 @@ export function createClaudeCode(
1053
1070
  });
1054
1071
  const token =
1055
1072
  settings.mintBridgeToken == null
1056
- ? randomBytes(32).toString('hex')
1073
+ ? createBridgeToken()
1057
1074
  : settings.mintBridgeToken(sandboxId!);
1058
1075
  const env = {
1059
1076
  BRIDGE_CHANNEL_TOKEN: token,
@@ -1149,13 +1166,14 @@ export function createClaudeCode(
1149
1166
  sessionId: startOpts.sessionId,
1150
1167
  channel,
1151
1168
  proc,
1152
- model,
1169
+ model: settings.model,
1153
1170
  maxTurns: settings.maxTurns,
1154
1171
  env: sandboxClaudeEnvironment,
1155
1172
  thinking,
1156
1173
  effort: settings.effort,
1157
1174
  isResume: respawnStrategy !== undefined,
1158
1175
  continueOnFirstPrompt: respawnStrategy !== undefined,
1176
+ ...(resumeSessionId ? { resumeSessionId } : {}),
1159
1177
  rerunContinue: respawnStrategy === 'rerun',
1160
1178
  bridgePort: boundPort,
1161
1179
  bridgeToken: token,
@@ -1451,18 +1469,6 @@ function webSocketMessageToString(raw: unknown): string {
1451
1469
  return String(raw);
1452
1470
  }
1453
1471
 
1454
- function withBridgeToken({
1455
- endpoint,
1456
- token,
1457
- }: {
1458
- endpoint: HarnessV1PortEndpoint;
1459
- token: string;
1460
- }): HarnessV1PortEndpoint {
1461
- const bridgeUrl = new URL(endpoint.url);
1462
- bridgeUrl.searchParams.set('agent_bridge_token', token);
1463
- return { ...endpoint, url: bridgeUrl.toString() };
1464
- }
1465
-
1466
1472
  function sleep(ms: number): Promise<void> {
1467
1473
  return new Promise(resolve => {
1468
1474
  const timer = setTimeout(resolve, ms);
@@ -1486,6 +1492,7 @@ function createSession({
1486
1492
  effort,
1487
1493
  isResume,
1488
1494
  continueOnFirstPrompt,
1495
+ resumeSessionId,
1489
1496
  rerunContinue,
1490
1497
  bridgePort,
1491
1498
  bridgeToken,
@@ -1510,6 +1517,12 @@ function createSession({
1510
1517
  effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined;
1511
1518
  isResume: boolean;
1512
1519
  continueOnFirstPrompt: boolean;
1520
+ /**
1521
+ * Exact conversation to rehydrate whenever a fresh SDK query starts. Takes
1522
+ * precedence over the `continue` flag, which can only mean "most recent in
1523
+ * this workdir".
1524
+ */
1525
+ resumeSessionId?: string;
1513
1526
  rerunContinue: boolean;
1514
1527
  bridgePort: number;
1515
1528
  bridgeToken: string;
@@ -1527,12 +1540,21 @@ function createSession({
1527
1540
  let stopPromise: Promise<void> | undefined;
1528
1541
  /*
1529
1542
  * Force the Claude SDK's `continue: true` on the first prompt only when the
1530
- * bridge was respawned (rerun/replay): a fresh bridge process treats its
1531
- * first turn as new, so it must be told to rehydrate the workdir thread. An
1532
- * `attach`ed bridge is already past its first turn and continues on its own.
1543
+ * bridge was respawned without an exact conversation id (rerun/replay): a
1544
+ * fresh bridge process treats its first turn as new, so it must be told to
1545
+ * rehydrate the workdir thread. Exact ids are sent independently of this
1546
+ * fallback on every fresh query.
1533
1547
  */
1534
1548
  let pendingResumeFlag = continueOnFirstPrompt;
1535
1549
 
1550
+ /*
1551
+ * The Claude conversation this session currently embodies. Seeded from the
1552
+ * resume state and updated from every `finish` part's metadata — each turn
1553
+ * may fork a new id, so the latest observation is the one a later
1554
+ * stop/detach must record for exact resume.
1555
+ */
1556
+ let lastClaudeSessionId = resumeSessionId;
1557
+
1536
1558
  /*
1537
1559
  * Wire the channel into one turn's worth of events and return the control
1538
1560
  * surface. Shared by `doPromptTurn` (which sends a `start` afterwards) and
@@ -1588,6 +1610,9 @@ function createSession({
1588
1610
  'reasoning-start',
1589
1611
  'reasoning-delta',
1590
1612
  'reasoning-end',
1613
+ 'tool-input-start',
1614
+ 'tool-input-delta',
1615
+ 'tool-input-end',
1591
1616
  'tool-call',
1592
1617
  'tool-approval-request',
1593
1618
  'tool-result',
@@ -1603,6 +1628,14 @@ function createSession({
1603
1628
  }
1604
1629
  unsubs.push(
1605
1630
  channel.on('finish', msg => {
1631
+ const metadata = (
1632
+ msg as {
1633
+ harnessMetadata?: { 'claude-code'?: { sessionId?: unknown } };
1634
+ }
1635
+ ).harnessMetadata?.['claude-code']?.sessionId;
1636
+ if (typeof metadata === 'string' && metadata.length > 0) {
1637
+ lastClaudeSessionId = metadata;
1638
+ }
1606
1639
  forward(msg);
1607
1640
  settleSuccess();
1608
1641
  }),
@@ -1683,7 +1716,6 @@ function createSession({
1683
1716
  return {
1684
1717
  sessionId,
1685
1718
  isResume,
1686
- modelId: model,
1687
1719
  doPromptTurn: async promptOpts => {
1688
1720
  if (
1689
1721
  promptOpts.responseFormat?.type === 'json' &&
@@ -1730,7 +1762,7 @@ function createSession({
1730
1762
  ...(promptOpts.instructions
1731
1763
  ? { instructions: promptOpts.instructions }
1732
1764
  : {}),
1733
- model,
1765
+ model: promptOpts.model ?? model,
1734
1766
  maxTurns,
1735
1767
  ...(env !== undefined ? { env } : {}),
1736
1768
  thinking,
@@ -1742,7 +1774,11 @@ function createSession({
1742
1774
  ...(permissionMode ? { permissionMode } : {}),
1743
1775
  ...(builtinToolFiltering ? { builtinToolFiltering } : {}),
1744
1776
  ...(debug ? { debug } : {}),
1745
- ...(pendingResumeFlag ? { continue: true } : {}),
1777
+ ...(lastClaudeSessionId
1778
+ ? { resumeSessionId: lastClaudeSessionId }
1779
+ : pendingResumeFlag
1780
+ ? { continue: true }
1781
+ : {}),
1746
1782
  };
1747
1783
  pendingResumeFlag = false;
1748
1784
  channel.send(startMessage);
@@ -1809,7 +1845,7 @@ function createSession({
1809
1845
  ...(continueOpts.instructions
1810
1846
  ? { instructions: continueOpts.instructions }
1811
1847
  : {}),
1812
- model,
1848
+ model: continueOpts.model ?? model,
1813
1849
  maxTurns,
1814
1850
  ...(env !== undefined ? { env } : {}),
1815
1851
  thinking,
@@ -1821,7 +1857,9 @@ function createSession({
1821
1857
  ...(permissionMode ? { permissionMode } : {}),
1822
1858
  ...(builtinToolFiltering ? { builtinToolFiltering } : {}),
1823
1859
  ...(debug ? { debug } : {}),
1824
- continue: true,
1860
+ ...(lastClaudeSessionId
1861
+ ? { resumeSessionId: lastClaudeSessionId }
1862
+ : { continue: true }),
1825
1863
  });
1826
1864
  }
1827
1865
 
@@ -1863,6 +1901,9 @@ function createSession({
1863
1901
  lastSeenEventId,
1864
1902
  ...(sandboxId == null ? {} : { sandboxId }),
1865
1903
  },
1904
+ ...(lastClaudeSessionId
1905
+ ? { claudeSessionId: lastClaudeSessionId }
1906
+ : {}),
1866
1907
  },
1867
1908
  };
1868
1909
  return payload;
@@ -1974,7 +2015,13 @@ function createSession({
1974
2015
  type: 'resume-session',
1975
2016
  harnessId: 'claude-code',
1976
2017
  specificationVersion: 'harness-v1',
2018
+ // The bridge's stop reply carries the session id it observed; the
2019
+ // adapter's own record backfills it when the reply predates the
2020
+ // field or the channel was already closed.
1977
2021
  data: {
2022
+ ...(lastClaudeSessionId
2023
+ ? { claudeSessionId: lastClaudeSessionId }
2024
+ : {}),
1978
2025
  ...lifecycleData,
1979
2026
  ...(sandboxCredentialEnvironment == null
1980
2027
  ? {}
@@ -2013,6 +2060,9 @@ function createSession({
2013
2060
  lastSeenEventId,
2014
2061
  ...(sandboxId == null ? {} : { sandboxId }),
2015
2062
  },
2063
+ ...(lastClaudeSessionId
2064
+ ? { claudeSessionId: lastClaudeSessionId }
2065
+ : {}),
2016
2066
  },
2017
2067
  };
2018
2068
  return payload;