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

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.
@@ -90,6 +90,8 @@ export type ClaudeCodeHarnessSettings = {
90
90
  /**
91
91
  * Anthropic model id the underlying `claude` CLI should use. Leaving this
92
92
  * unset defers to the CLI's default.
93
+ *
94
+ * @deprecated Use `model` on `HarnessAgent` instead.
93
95
  */
94
96
  readonly model?: string;
95
97
  /**
@@ -800,6 +802,17 @@ const claudeCodeBridgeCoordsSchema = z.object({
800
802
  const claudeCodeResumeStateSchema = z.looseObject({
801
803
  bridge: claudeCodeBridgeCoordsSchema.optional(),
802
804
  sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional(),
805
+ /**
806
+ * The exact Claude conversation to rehydrate on resume. Written by the
807
+ * adapter on `doStop()`/`doDetach()`/`doSuspendTurn()` from the session id
808
+ * the bridge observed, so a resume names the conversation instead of
809
+ * relying on the SDK's `continue` flag — "most recent thread in this
810
+ * workdir" — which silently picks the wrong one once a second thread
811
+ * exists there. Hosts that captured the id themselves (it is surfaced as
812
+ * `harnessMetadata['claude-code'].sessionId` on `finish` parts) may also
813
+ * set it explicitly.
814
+ */
815
+ claudeSessionId: z.string().optional(),
803
816
  });
804
817
 
805
818
  type ClaudeCodeBridgeCoords = z.infer<typeof claudeCodeBridgeCoordsSchema>;
@@ -857,6 +870,7 @@ export function createClaudeCode(
857
870
  ? (lifecycleState?.data as {
858
871
  bridge?: ClaudeCodeBridgeCoords;
859
872
  sandboxCredentialEnvironment?: Record<string, string>;
873
+ claudeSessionId?: string;
860
874
  })
861
875
  : undefined;
862
876
  const coords = resumeData?.bridge;
@@ -918,8 +932,16 @@ export function createClaudeCode(
918
932
  defaultWorkingDirectory,
919
933
  BOOTSTRAP_DIR,
920
934
  );
935
+ // The conversation the host wants back, when it is known. Absent on
936
+ // state written before this field existed; those resumes fall back to
937
+ // the `continue` flag as before.
938
+ const resumeSessionId = resumeData?.claudeSessionId;
921
939
 
922
940
  const workDir = startOpts.sessionWorkDir;
941
+ const sandboxHomeDir = await resolveSandboxHomeDir({
942
+ sandbox: toolSafeSandboxSession,
943
+ abortSignal: startOpts.abortSignal,
944
+ });
923
945
  const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
924
946
  const bridgeStateDir = `${sessionDataDir}/bridge`;
925
947
  const timeoutMs = settings.startupTimeoutMs ?? 120_000;
@@ -987,6 +1009,7 @@ export function createClaudeCode(
987
1009
  return createSession({
988
1010
  sessionId: startOpts.sessionId,
989
1011
  channel: attachChannel,
1012
+ ...(resumeSessionId ? { resumeSessionId } : {}),
990
1013
  // The live bridge was spawned by another process; this one owns no
991
1014
  // process handle. The session lifecycle method decides whether the
992
1015
  // sandbox is left running, stopped, or destroyed.
@@ -1006,7 +1029,8 @@ export function createClaudeCode(
1006
1029
  debug: startOpts.observability?.debug,
1007
1030
  permissionMode: startOpts.permissionMode,
1008
1031
  builtinToolFiltering: startOpts.builtinToolFiltering,
1009
- skills: startOpts.skills ?? [],
1032
+ sandbox: toolSafeSandboxSession,
1033
+ sandboxHomeDir,
1010
1034
  mcpServers: settings.mcpServers,
1011
1035
  supportsUserMessageResponses: () => supportsUserMessageResponses,
1012
1036
  });
@@ -1039,13 +1063,6 @@ export function createClaudeCode(
1039
1063
  }
1040
1064
  }
1041
1065
 
1042
- const sandboxHomeDir =
1043
- startOpts.skills && startOpts.skills.length > 0
1044
- ? await resolveSandboxHomeDir({
1045
- sandbox: toolSafeSandboxSession,
1046
- abortSignal: startOpts.abortSignal,
1047
- })
1048
- : undefined;
1049
1066
  const port = resolveBridgePort({
1050
1067
  sandboxSession,
1051
1068
  override: settings.port,
@@ -1057,35 +1074,21 @@ export function createClaudeCode(
1057
1074
  const env = {
1058
1075
  BRIDGE_CHANNEL_TOKEN: token,
1059
1076
  BRIDGE_WS_PORT: String(port),
1060
- ...(sandboxHomeDir ? { HOME: sandboxHomeDir } : {}),
1061
1077
  ...(respawnStrategy === 'replay'
1062
1078
  ? { BRIDGE_REPLAY_FROM_DISK: '1' }
1063
1079
  : {}),
1064
1080
  };
1065
1081
 
1066
1082
  /*
1067
- * On a fresh start the workdir, skill files, and bridge-state directory
1068
- * must be created. On any resume they already exist in the
1069
- * sandbox snapshot, so skip the rewrite. The env is sent fresh on every
1070
- * spawn — `BRIDGE_CHANNEL_TOKEN` rotates per start.
1083
+ * On a fresh start the workdir and bridge-state directory must be
1084
+ * created. The env is sent fresh on every spawn —
1085
+ * `BRIDGE_CHANNEL_TOKEN` rotates per start.
1071
1086
  */
1072
1087
  if (respawnStrategy === undefined) {
1073
1088
  await toolSafeSandboxSession.run({
1074
1089
  command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
1075
1090
  abortSignal: startOpts.abortSignal,
1076
1091
  });
1077
-
1078
- if (startOpts.skills && startOpts.skills.length > 0) {
1079
- if (!sandboxHomeDir) {
1080
- throw new Error('Unable to resolve sandbox HOME directory.');
1081
- }
1082
- await writeClaudeCodeSkills({
1083
- sandbox: toolSafeSandboxSession,
1084
- homeDir: sandboxHomeDir,
1085
- skills: startOpts.skills,
1086
- abortSignal: startOpts.abortSignal,
1087
- });
1088
- }
1089
1092
  }
1090
1093
 
1091
1094
  await markBridgeStarting({
@@ -1169,6 +1172,7 @@ export function createClaudeCode(
1169
1172
  effort: settings.effort,
1170
1173
  isResume: respawnStrategy !== undefined,
1171
1174
  continueOnFirstPrompt: respawnStrategy !== undefined,
1175
+ ...(resumeSessionId ? { resumeSessionId } : {}),
1172
1176
  rerunContinue: respawnStrategy === 'rerun',
1173
1177
  bridgePort: boundPort,
1174
1178
  bridgeToken: token,
@@ -1177,7 +1181,8 @@ export function createClaudeCode(
1177
1181
  debug: startOpts.observability?.debug,
1178
1182
  permissionMode: startOpts.permissionMode,
1179
1183
  builtinToolFiltering: startOpts.builtinToolFiltering,
1180
- skills: startOpts.skills ?? [],
1184
+ sandbox: toolSafeSandboxSession,
1185
+ sandboxHomeDir,
1181
1186
  mcpServers: settings.mcpServers,
1182
1187
  supportsUserMessageResponses: () => supportsUserMessageResponses,
1183
1188
  });
@@ -1498,6 +1503,7 @@ function createSession({
1498
1503
  effort,
1499
1504
  isResume,
1500
1505
  continueOnFirstPrompt,
1506
+ resumeSessionId,
1501
1507
  rerunContinue,
1502
1508
  bridgePort,
1503
1509
  bridgeToken,
@@ -1506,7 +1512,8 @@ function createSession({
1506
1512
  debug,
1507
1513
  permissionMode,
1508
1514
  builtinToolFiltering,
1509
- skills,
1515
+ sandbox,
1516
+ sandboxHomeDir,
1510
1517
  mcpServers,
1511
1518
  supportsUserMessageResponses,
1512
1519
  }: {
@@ -1521,6 +1528,12 @@ function createSession({
1521
1528
  effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined;
1522
1529
  isResume: boolean;
1523
1530
  continueOnFirstPrompt: boolean;
1531
+ /**
1532
+ * Exact conversation to rehydrate whenever a fresh SDK query starts. Takes
1533
+ * precedence over the `continue` flag, which can only mean "most recent in
1534
+ * this workdir".
1535
+ */
1536
+ resumeSessionId?: string;
1524
1537
  rerunContinue: boolean;
1525
1538
  bridgePort: number;
1526
1539
  bridgeToken: string;
@@ -1529,7 +1542,8 @@ function createSession({
1529
1542
  debug: HarnessV1DebugConfig | undefined;
1530
1543
  permissionMode: HarnessV1PermissionMode | undefined;
1531
1544
  builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
1532
- skills: ReadonlyArray<HarnessV1Skill>;
1545
+ sandbox: SandboxSession;
1546
+ sandboxHomeDir: string;
1533
1547
  mcpServers: Record<string, unknown> | undefined;
1534
1548
  supportsUserMessageResponses: () => boolean;
1535
1549
  }): HarnessV1Session {
@@ -1537,12 +1551,21 @@ function createSession({
1537
1551
  let stopPromise: Promise<void> | undefined;
1538
1552
  /*
1539
1553
  * Force the Claude SDK's `continue: true` on the first prompt only when the
1540
- * bridge was respawned (rerun/replay): a fresh bridge process treats its
1541
- * first turn as new, so it must be told to rehydrate the workdir thread. An
1542
- * `attach`ed bridge is already past its first turn and continues on its own.
1554
+ * bridge was respawned without an exact conversation id (rerun/replay): a
1555
+ * fresh bridge process treats its first turn as new, so it must be told to
1556
+ * rehydrate the workdir thread. Exact ids are sent independently of this
1557
+ * fallback on every fresh query.
1543
1558
  */
1544
1559
  let pendingResumeFlag = continueOnFirstPrompt;
1545
1560
 
1561
+ /*
1562
+ * The Claude conversation this session currently embodies. Seeded from the
1563
+ * resume state and updated from every `finish` part's metadata — each turn
1564
+ * may fork a new id, so the latest observation is the one a later
1565
+ * stop/detach must record for exact resume.
1566
+ */
1567
+ let lastClaudeSessionId = resumeSessionId;
1568
+
1546
1569
  /*
1547
1570
  * Wire the channel into one turn's worth of events and return the control
1548
1571
  * surface. Shared by `doPromptTurn` (which sends a `start` afterwards) and
@@ -1598,6 +1621,9 @@ function createSession({
1598
1621
  'reasoning-start',
1599
1622
  'reasoning-delta',
1600
1623
  'reasoning-end',
1624
+ 'tool-input-start',
1625
+ 'tool-input-delta',
1626
+ 'tool-input-end',
1601
1627
  'tool-call',
1602
1628
  'tool-approval-request',
1603
1629
  'tool-result',
@@ -1613,6 +1639,14 @@ function createSession({
1613
1639
  }
1614
1640
  unsubs.push(
1615
1641
  channel.on('finish', msg => {
1642
+ const metadata = (
1643
+ msg as {
1644
+ harnessMetadata?: { 'claude-code'?: { sessionId?: unknown } };
1645
+ }
1646
+ ).harnessMetadata?.['claude-code']?.sessionId;
1647
+ if (typeof metadata === 'string' && metadata.length > 0) {
1648
+ lastClaudeSessionId = metadata;
1649
+ }
1616
1650
  forward(msg);
1617
1651
  settleSuccess();
1618
1652
  }),
@@ -1693,7 +1727,6 @@ function createSession({
1693
1727
  return {
1694
1728
  sessionId,
1695
1729
  isResume,
1696
- modelId: model,
1697
1730
  doPromptTurn: async promptOpts => {
1698
1731
  if (
1699
1732
  promptOpts.responseFormat?.type === 'json' &&
@@ -1705,11 +1738,27 @@ function createSession({
1705
1738
  harnessId: 'claude-code',
1706
1739
  });
1707
1740
  }
1741
+ await writeClaudeCodeSkills({
1742
+ sandbox,
1743
+ homeDir: sandboxHomeDir,
1744
+ skills: promptOpts.skills,
1745
+ abortSignal: promptOpts.abortSignal,
1746
+ });
1708
1747
  const control = wireTurn({
1709
1748
  emit: promptOpts.emit,
1710
1749
  abortSignal: promptOpts.abortSignal,
1711
1750
  });
1712
1751
 
1752
+ /*
1753
+ * A signal that was already aborted has settled the turn inside
1754
+ * `wireTurn`. Sending `start` anyway would run a full unattended turn
1755
+ * in the bridge — consuming tokens and possibly acting on the
1756
+ * workspace — after the caller has already observed the abort.
1757
+ */
1758
+ if (promptOpts.abortSignal?.aborted) {
1759
+ return control;
1760
+ }
1761
+
1713
1762
  const startMessage = {
1714
1763
  type: 'start' as const,
1715
1764
  prompt: extractUserText(promptOpts.prompt),
@@ -1724,19 +1773,23 @@ function createSession({
1724
1773
  ...(promptOpts.instructions
1725
1774
  ? { instructions: promptOpts.instructions }
1726
1775
  : {}),
1727
- model,
1776
+ model: promptOpts.model ?? model,
1728
1777
  maxTurns,
1729
1778
  ...(env !== undefined ? { env } : {}),
1730
1779
  thinking,
1731
1780
  ...(effort !== undefined ? { effort } : {}),
1732
- ...(skills.length > 0
1733
- ? { skills: skills.map(skill => skill.name) }
1781
+ ...(promptOpts.skills.length > 0
1782
+ ? { skills: promptOpts.skills.map(skill => skill.name) }
1734
1783
  : {}),
1735
1784
  ...(mcpServers == null ? {} : { mcpServers }),
1736
1785
  ...(permissionMode ? { permissionMode } : {}),
1737
1786
  ...(builtinToolFiltering ? { builtinToolFiltering } : {}),
1738
1787
  ...(debug ? { debug } : {}),
1739
- ...(pendingResumeFlag ? { continue: true } : {}),
1788
+ ...(lastClaudeSessionId
1789
+ ? { resumeSessionId: lastClaudeSessionId }
1790
+ : pendingResumeFlag
1791
+ ? { continue: true }
1792
+ : {}),
1740
1793
  };
1741
1794
  pendingResumeFlag = false;
1742
1795
  channel.send(startMessage);
@@ -1754,6 +1807,12 @@ function createSession({
1754
1807
  harnessId: 'claude-code',
1755
1808
  });
1756
1809
  }
1810
+ await writeClaudeCodeSkills({
1811
+ sandbox,
1812
+ homeDir: sandboxHomeDir,
1813
+ skills: continueOpts.skills,
1814
+ abortSignal: continueOpts.abortSignal,
1815
+ });
1757
1816
  const control = wireTurn({
1758
1817
  emit: continueOpts.emit,
1759
1818
  abortSignal: continueOpts.abortSignal,
@@ -1772,7 +1831,9 @@ function createSession({
1772
1831
  * recomputed. This is the rare bridge-died fallback; the common slice path
1773
1832
  * is `attach`.
1774
1833
  */
1775
- if (rerunContinue) {
1834
+ // Same as `doPromptTurn`: an already-aborted signal settled the turn in
1835
+ // `wireTurn`, so don't re-drive the thread nobody is waiting on.
1836
+ if (rerunContinue && !continueOpts.abortSignal?.aborted) {
1776
1837
  pendingResumeFlag = false;
1777
1838
  channel.send({
1778
1839
  type: 'start' as const,
@@ -1795,19 +1856,21 @@ function createSession({
1795
1856
  ...(continueOpts.instructions
1796
1857
  ? { instructions: continueOpts.instructions }
1797
1858
  : {}),
1798
- model,
1859
+ model: continueOpts.model ?? model,
1799
1860
  maxTurns,
1800
1861
  ...(env !== undefined ? { env } : {}),
1801
1862
  thinking,
1802
1863
  ...(effort !== undefined ? { effort } : {}),
1803
- ...(skills.length > 0
1804
- ? { skills: skills.map(skill => skill.name) }
1864
+ ...(continueOpts.skills.length > 0
1865
+ ? { skills: continueOpts.skills.map(skill => skill.name) }
1805
1866
  : {}),
1806
1867
  ...(mcpServers == null ? {} : { mcpServers }),
1807
1868
  ...(permissionMode ? { permissionMode } : {}),
1808
1869
  ...(builtinToolFiltering ? { builtinToolFiltering } : {}),
1809
1870
  ...(debug ? { debug } : {}),
1810
- continue: true,
1871
+ ...(lastClaudeSessionId
1872
+ ? { resumeSessionId: lastClaudeSessionId }
1873
+ : { continue: true }),
1811
1874
  });
1812
1875
  }
1813
1876
 
@@ -1849,6 +1912,9 @@ function createSession({
1849
1912
  lastSeenEventId,
1850
1913
  ...(sandboxId == null ? {} : { sandboxId }),
1851
1914
  },
1915
+ ...(lastClaudeSessionId
1916
+ ? { claudeSessionId: lastClaudeSessionId }
1917
+ : {}),
1852
1918
  },
1853
1919
  };
1854
1920
  return payload;
@@ -1960,7 +2026,13 @@ function createSession({
1960
2026
  type: 'resume-session',
1961
2027
  harnessId: 'claude-code',
1962
2028
  specificationVersion: 'harness-v1',
2029
+ // The bridge's stop reply carries the session id it observed; the
2030
+ // adapter's own record backfills it when the reply predates the
2031
+ // field or the channel was already closed.
1963
2032
  data: {
2033
+ ...(lastClaudeSessionId
2034
+ ? { claudeSessionId: lastClaudeSessionId }
2035
+ : {}),
1964
2036
  ...lifecycleData,
1965
2037
  ...(sandboxCredentialEnvironment == null
1966
2038
  ? {}
@@ -1999,6 +2071,9 @@ function createSession({
1999
2071
  lastSeenEventId,
2000
2072
  ...(sandboxId == null ? {} : { sandboxId }),
2001
2073
  },
2074
+ ...(lastClaudeSessionId
2075
+ ? { claudeSessionId: lastClaudeSessionId }
2076
+ : {}),
2002
2077
  },
2003
2078
  };
2004
2079
  return payload;