@ai-sdk/harness-claude-code 1.0.126 → 1.0.127

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.126",
3
+ "version": "1.0.127",
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.122",
30
- "@ai-sdk/provider-utils": "5.0.46",
29
+ "@ai-sdk/harness": "1.0.123",
30
+ "@ai-sdk/provider-utils": "5.0.47",
31
31
  "ws": "^8.21.0"
32
32
  },
33
33
  "peerDependencies": {
@@ -390,7 +390,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
390
390
 
391
391
  const streamEventState = createClaudeStreamEventState();
392
392
 
393
- const mcpServers: Record<string, unknown> = { ...(start.mcpServers ?? {}) };
393
+ const mcpServers: Record<string, unknown> = { ...start.mcpServers };
394
394
  if (start.tools && start.tools.length > 0) {
395
395
  const server = new mcpModule.McpServer({
396
396
  name: 'harness-tools',
@@ -37,7 +37,7 @@ function toZodObject(schema: JsonSchemaObject) {
37
37
 
38
38
  function toZodShape(schema: JsonSchemaObject | undefined): ZodShape {
39
39
  if (!schema?.properties) return {};
40
- const required = new Set(schema.required ?? []);
40
+ const required = new Set(schema.required);
41
41
  const shape: ZodShape = {};
42
42
  for (const [key, propSchema] of Object.entries(schema.properties)) {
43
43
  const propType = toZodType(propSchema);
@@ -16,16 +16,14 @@ const readBridgeAsset = createReadBridgeAsset({
16
16
  });
17
17
 
18
18
  /*
19
- * Bootstrap is derived state stored under the sandbox's default working
20
- * directory so snapshot-capable providers can preserve the installed CLI,
21
- * bridge, and recipe marker without requiring root filesystem access.
19
+ * Bootstrap is derived state stored under `$HOME/.ai-sdk-harness`, outside
20
+ * the agent's working directory. Snapshot-capable providers preserve the
21
+ * installed CLI, bridge, and recipe marker there.
22
22
  *
23
- * The session work dir (`startOpts.sessionWorkDir`) and the bridge-state dir
24
- * derived from `sandboxSession.defaultWorkingDirectory` both live under the sandbox's
25
- * default working directory — the provider's persistent mount — so the
26
- * workdir's CLI state (Claude's `~/.claude/projects/<dir>/*.jsonl` thread
27
- * history is keyed by working directory) and the bridge state files survive
28
- * both detach -> attach/replay and stop -> snapshot -> resume cycles.
23
+ * The session work dir (`startOpts.sessionWorkDir`) lives under the sandbox's
24
+ * default working directory, while the bridge-state dir lives under
25
+ * `$HOME/.ai-sdk-harness/.agent-runs`. Claude's project history is keyed by
26
+ * the working directory, so the same work dir is needed across resumes.
29
27
  */
30
28
  export const CLAUDE_CODE_BOOTSTRAP_DIR = '.harness-bootstrap/claude-code';
31
29
 
@@ -21,6 +21,8 @@ import {
21
21
  type HarnessV1Session,
22
22
  type HarnessV1Skill,
23
23
  type HarnessV1StreamPart,
24
+ harnessStateDirectoryPath,
25
+ harnessSessionDataDirectoryPath,
24
26
  } from '@ai-sdk/harness';
25
27
  import {
26
28
  applyCredentialForwarding,
@@ -34,7 +36,6 @@ import {
34
36
  forwardBridgeProcessStream,
35
37
  getRestrictedSandboxSession,
36
38
  markBridgeStarting,
37
- resolveSandboxDefaultWorkingDirectory,
38
39
  resolveSandboxHomeDir,
39
40
  SandboxChannel,
40
41
  shellQuote,
@@ -842,11 +843,6 @@ export function createClaudeCode(
842
843
  'The Claude Code harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id.',
843
844
  });
844
845
  }
845
- const defaultWorkingDirectory =
846
- await resolveSandboxDefaultWorkingDirectory({
847
- sandboxSession,
848
- abortSignal: startOpts.abortSignal,
849
- });
850
846
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
851
847
  const isResume = lifecycleState != null;
852
848
  const isContinue = startOpts.continueFrom != null;
@@ -932,21 +928,25 @@ export function createClaudeCode(
932
928
  CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
933
929
  });
934
930
  }
935
- const bootstrapDir = posix.resolve(
936
- defaultWorkingDirectory,
937
- BOOTSTRAP_DIR,
938
- );
931
+ // Harness SDK state (bootstrap, per-session runs) always lives under
932
+ // the sandbox's own HOME, never the working directory, so it stays
933
+ // out of a user-owned workspace.
934
+ const sandboxHomeDir = await resolveSandboxHomeDir({
935
+ sandbox: toolSafeSandboxSession,
936
+ abortSignal: startOpts.abortSignal,
937
+ });
938
+ const stateDir = harnessStateDirectoryPath({ sandboxHomeDir });
939
+ const bootstrapDir = posix.resolve(stateDir, BOOTSTRAP_DIR);
939
940
  // The conversation the host wants back, when it is known. Absent on
940
941
  // state written before this field existed; those resumes fall back to
941
942
  // the `continue` flag as before.
942
943
  const resumeSessionId = resumeData?.claudeSessionId;
943
944
 
944
945
  const workDir = startOpts.sessionWorkDir;
945
- const sandboxHomeDir = await resolveSandboxHomeDir({
946
- sandbox: toolSafeSandboxSession,
947
- abortSignal: startOpts.abortSignal,
946
+ const sessionDataDir = harnessSessionDataDirectoryPath({
947
+ stateDirectory: stateDir,
948
+ sessionId: startOpts.sessionId,
948
949
  });
949
- const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
950
950
  const bridgeStateDir = `${sessionDataDir}/bridge`;
951
951
  const timeoutMs = settings.startupTimeoutMs ?? 120_000;
952
952
 
@@ -1016,10 +1016,19 @@ export function createClaudeCode(
1016
1016
  onBridgeError,
1017
1017
  reconnect: settings.reconnect,
1018
1018
  });
1019
- await attachChannel.open(isContinue ? { resume: true } : undefined);
1019
+ const finishListenerAttachment = isContinue
1020
+ ? attachChannel.beginListenerAttachment()
1021
+ : undefined;
1022
+ try {
1023
+ await attachChannel.open(isContinue ? { resume: true } : undefined);
1024
+ } catch (error) {
1025
+ finishListenerAttachment?.();
1026
+ throw error;
1027
+ }
1020
1028
  return createSession({
1021
1029
  sessionId: startOpts.sessionId,
1022
1030
  channel: attachChannel,
1031
+ finishListenerAttachment,
1023
1032
  ...(resumeSessionId ? { resumeSessionId } : {}),
1024
1033
  // The live bridge was spawned by another process; this one owns no
1025
1034
  // process handle. The session lifecycle method decides whether the
@@ -1168,13 +1177,23 @@ export function createClaudeCode(
1168
1177
  ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 }
1169
1178
  : {}),
1170
1179
  });
1171
- await channel.open(
1172
- respawnStrategy === 'replay' ? { resume: true } : undefined,
1173
- );
1180
+ const finishListenerAttachment =
1181
+ respawnStrategy === 'replay'
1182
+ ? channel.beginListenerAttachment()
1183
+ : undefined;
1184
+ try {
1185
+ await channel.open(
1186
+ respawnStrategy === 'replay' ? { resume: true } : undefined,
1187
+ );
1188
+ } catch (error) {
1189
+ finishListenerAttachment?.();
1190
+ throw error;
1191
+ }
1174
1192
 
1175
1193
  return createSession({
1176
1194
  sessionId: startOpts.sessionId,
1177
1195
  channel,
1196
+ finishListenerAttachment,
1178
1197
  proc,
1179
1198
  maxTurns: settings.maxTurns,
1180
1199
  env: sandboxClaudeEnvironment,
@@ -1490,6 +1509,7 @@ function formatUnknownError(error: unknown): string {
1490
1509
  function createSession({
1491
1510
  sessionId,
1492
1511
  channel,
1512
+ finishListenerAttachment,
1493
1513
  proc,
1494
1514
  maxTurns,
1495
1515
  env,
@@ -1513,6 +1533,7 @@ function createSession({
1513
1533
  }: {
1514
1534
  sessionId: string;
1515
1535
  channel: ClaudeCodeChannel;
1536
+ finishListenerAttachment: (() => void) | undefined;
1516
1537
  /** Undefined on `attach` — the live bridge was spawned by another process. */
1517
1538
  proc: Experimental_SandboxProcess | undefined;
1518
1539
  maxTurns: number | undefined;
@@ -1621,6 +1642,7 @@ function createSession({
1621
1642
  'tool-approval-request',
1622
1643
  'tool-result',
1623
1644
  'finish-step',
1645
+ 'compaction',
1624
1646
  'raw',
1625
1647
  ] as const;
1626
1648
  for (const type of eventTypes) {
@@ -1650,6 +1672,8 @@ function createSession({
1650
1672
  settleError(msg.error);
1651
1673
  }),
1652
1674
  );
1675
+ finishListenerAttachment?.();
1676
+ finishListenerAttachment = undefined;
1653
1677
 
1654
1678
  /*
1655
1679
  * A `'suspended'` close is a graceful slice-boundary freeze the host