@parall/claude-agent 1.30.0 → 1.32.0

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/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import * as os from "node:os";
3
- import { ParallAgentGateway, createPlatformConfigManager, createLogger, childLogger, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
4
- import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
5
- import { buildClaudeRuntimeKey, contextFilePathForSession, resolveClaudeAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
6
- import { ClaudeCodeAdapter } from "./dispatch.js";
7
- import { ClaudeSessionManager } from "./session-manager.js";
8
- import { ensureClaudeWorkspace } from "./workspace.js";
9
- const log = createLogger("claude-agent");
2
+ import * as os from 'node:os';
3
+ import { ParallAgentGateway, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, isPlatformManagedProfile, isPlatformModelOverride, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } from '@parall/agent-core';
4
+ import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
5
+ import { buildClaudeRuntimeKey, contextFilePathForSession, resolveClaudeAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from './config.js';
6
+ import { ClaudeCodeAdapter } from './dispatch.js';
7
+ import { ClaudeSessionManager } from './session-manager.js';
8
+ import { ensureClaudeWorkspace } from './workspace.js';
9
+ const log = createLogger('claude-agent');
10
10
  let activeLog = log;
11
11
  async function getAgentMeWithLegacyFallback(client, orgId) {
12
12
  try {
@@ -20,137 +20,188 @@ async function getAgentMeWithLegacyFallback(client, orgId) {
20
20
  return { ...user, agent_profile: null };
21
21
  }
22
22
  }
23
- async function main() {
24
- const config = resolveClaudeAgentConfig(process.env);
25
- if (config.allowApiKey &&
26
- (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN)) {
27
- // Surface the billing path: default is OAuth (Claude.ai subscription); this
28
- // branch means the operator opted into Anthropic API pay-per-use, and the
29
- // whole point of the strip default is that "silent" is the bad state.
30
- log.warn("PRLL_CLAUDE_ALLOW_API_KEY=1 — ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN will reach the Claude CLI; billing routes through Anthropic API pay-per-use instead of Claude.ai OAuth.");
23
+ function resolveProviderEnv() {
24
+ const pc = parseProviderConfig(process.env);
25
+ if (!pc)
26
+ return;
27
+ clearAllProviderCreds(process.env);
28
+ const source = llmSource(pc);
29
+ if (source === 'parall') {
30
+ process.env.ANTHROPIC_AUTH_TOKEN = process.env.PRLL_API_KEY;
31
+ process.env.ANTHROPIC_BASE_URL = `${process.env.PRLL_API_URL}/api/llm`;
32
+ process.env.PRLL_CLAUDE_ALLOW_API_KEY = '1';
31
33
  }
32
- const client = new ParallClient({
33
- baseUrl: config.apiUrl,
34
- token: config.apiKey,
35
- swimlaneName: config.swimlaneName,
36
- });
37
- const me = await getAgentMeWithLegacyFallback(client, config.orgId);
38
- const agentUserId = me.id;
39
- const agentLog = childLogger(log, agentUserId);
40
- activeLog = agentLog;
41
- ensureClaudeWorkspace(config.workspaceDir, agentLog, {
42
- userId: agentUserId,
43
- displayName: me.display_name,
44
- description: me.agent_profile?.description ?? undefined,
45
- });
46
- const runtimeKey = config.runtimeKey || buildClaudeRuntimeKey(agentUserId);
47
- const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
48
- const sessionManager = new ClaudeSessionManager(runtimeKey, sessionStateFilePath, agentLog);
49
- const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
50
- const ws = new ParallWs({
51
- getTicket: () => client.getWsTicket(),
52
- wsUrl: resolvedWsUrl,
53
- });
54
- const configMgr = createPlatformConfigManager({ client, stateDir: config.stateDir, runtimeType: "claude-code", log: agentLog });
55
- const platformDefaults = await configMgr.fetch();
56
- let platformManaged = isPlatformManagedProfile(me.agent_profile);
57
- const resolvedModel = platformManaged ? (platformDefaults.model ?? config.model) : config.model;
58
- const resolvedEffort = platformManaged
59
- ? (platformDefaults.thinkingEffort ?? (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined))
60
- : (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined);
61
- if (platformDefaults.model)
62
- agentLog.info(`platform config: model=${platformDefaults.model} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
63
- if (platformDefaults.thinkingEffort)
64
- agentLog.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
65
- const adapter = new ClaudeCodeAdapter({
66
- claudeBin: config.claudeBin,
67
- claudeHome: config.claudeHome,
68
- workspaceDir: config.workspaceDir,
69
- model: resolvedModel,
70
- permissionMode: config.permissionMode,
71
- allowedTools: config.allowedTools,
72
- disallowedTools: config.disallowedTools,
73
- additionalDirs: config.additionalDirs,
74
- appendSystemPrompt: config.appendSystemPrompt,
75
- allowApiKey: config.allowApiKey,
76
- sessionManager,
77
- // Static Parall credentials. The long-lived subprocess must authenticate
78
- // against Parall's API across many dispatches, so these cannot come from
79
- // a per-dispatch DispatchContext.
80
- apiUrl: config.apiUrl,
81
- apiKey: config.apiKey,
82
- orgId: config.orgId,
83
- contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
84
- stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
85
- });
86
- if (resolvedEffort) {
87
- adapter.updateConfig({ effort: resolvedEffort });
34
+ else if (source === 'custom') {
35
+ if (pc.anthropic_auth_token) {
36
+ process.env.ANTHROPIC_AUTH_TOKEN = pc.anthropic_auth_token;
37
+ process.env.PRLL_CLAUDE_ALLOW_API_KEY = '1';
38
+ }
39
+ if (pc.anthropic_base_url)
40
+ process.env.ANTHROPIC_BASE_URL = pc.anthropic_base_url;
88
41
  }
89
- const gateway = new ParallAgentGateway({
90
- accountId: agentUserId,
91
- client,
92
- ws,
93
- connectionLabel: resolvedWsUrl,
94
- config: {
95
- parall_url: config.apiUrl,
96
- api_key: config.apiKey,
97
- org_id: config.orgId,
98
- },
99
- agentUserId,
100
- runtimeType: "claude-code",
101
- runtimeKey,
102
- runtimeRef: {
103
- hostname: os.hostname(),
104
- pid: process.pid,
105
- workspace_dir: config.workspaceDir,
106
- claude_home: config.claudeHome,
107
- },
108
- dispatchAdapter: adapter,
109
- log: agentLog,
110
- shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
111
- contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
112
- stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
113
- onConfigUpdate: async () => {
114
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
115
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
116
- const updated = await configMgr.fetch();
117
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
118
- adapter.updateConfig({
119
- model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
120
- effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
121
- });
122
- },
123
- onSessionReady: async () => {
124
- const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
125
- platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
126
- const updated = await configMgr.fetch();
127
- const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
128
- adapter.updateConfig({
129
- model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
130
- effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
131
- });
132
- },
133
- // Long-lived `claude` subprocesses outlive a single dispatch, so the
134
- // gateway's shuttingDown flag alone does not tear them down. Piggyback on
135
- // onBeforeDisconnect to close stdin and SIGTERM any survivors after
136
- // in-flight drains finish.
137
- onBeforeDisconnect: () => adapter.shutdown(),
138
- onNewSession: async () => {
139
- sessionManager.clearMainSession();
140
- await adapter.shutdown();
141
- },
142
- });
143
- const abortController = new AbortController();
144
- const abort = () => abortController.abort();
145
- process.on("SIGINT", abort);
146
- process.on("SIGTERM", abort);
42
+ }
43
+ async function main() {
44
+ const telemetry = await initAgentTelemetry('parall-claude-agent', 'claude-code');
45
+ activeLog = createOtelLogger('agent', 'claude-agent');
147
46
  try {
148
- agentLog.info(`starting self-hosted Claude runtime for ${me.display_name} (${agentUserId})`);
149
- await gateway.run(abortController.signal);
47
+ resolveProviderEnv();
48
+ const config = resolveClaudeAgentConfig(process.env);
49
+ if (config.allowApiKey && (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN)) {
50
+ // Surface the billing path: default is OAuth (Claude.ai subscription); this
51
+ // branch means the operator opted into Anthropic API pay-per-use, and the
52
+ // whole point of the strip default is that "silent" is the bad state.
53
+ activeLog.warn('PRLL_CLAUDE_ALLOW_API_KEY=1 — ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN will reach the Claude CLI; billing routes through Anthropic API pay-per-use instead of Claude.ai OAuth.');
54
+ }
55
+ const client = new ParallClient({
56
+ baseUrl: config.apiUrl,
57
+ token: config.apiKey,
58
+ swimlaneName: config.swimlaneName,
59
+ });
60
+ const me = await getAgentMeWithLegacyFallback(client, config.orgId);
61
+ const agentUserId = me.id;
62
+ const agentLog = childLogger(activeLog, agentUserId);
63
+ activeLog = agentLog;
64
+ ensureClaudeWorkspace(config.workspaceDir, agentLog, {
65
+ userId: agentUserId,
66
+ displayName: me.display_name,
67
+ description: me.agent_profile?.description ?? undefined,
68
+ });
69
+ const runtimeKey = config.runtimeKey || buildClaudeRuntimeKey(agentUserId);
70
+ const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
71
+ const sessionManager = new ClaudeSessionManager(runtimeKey, sessionStateFilePath, agentLog);
72
+ const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
73
+ const ws = new ParallWs({
74
+ getTicket: () => client.getWsTicket(),
75
+ wsUrl: resolvedWsUrl,
76
+ });
77
+ const configMgr = createPlatformConfigManager({
78
+ client,
79
+ stateDir: config.stateDir,
80
+ runtimeType: 'claude-code',
81
+ log: agentLog,
82
+ });
83
+ const platformDefaults = await configMgr.fetch();
84
+ let platformManaged = isPlatformManagedProfile(me.agent_profile);
85
+ // Model precedence: operator override (platform-managed) > env > server floor.
86
+ // The server delivers a model floor for Parall-proxy routes even when
87
+ // model_management=self, so the CLI never falls back to a built-in default
88
+ // the catalog might reject — but that floor must lose to an explicit env
89
+ // override (env > floor). A profile is an operator-override only when it is
90
+ // platform-managed AND not explicitly self-managed: this treats legacy hosted
91
+ // agents (model_management=null, resolved to platform server-side) as
92
+ // overrides, while excluding local self agents whose delivered model is a
93
+ // floor. effort stays an operator override (platform).
94
+ let platformModelOverride = isPlatformModelOverride(me.agent_profile);
95
+ const resolvedModel = resolveRuntimeModel(platformModelOverride, platformDefaults.model, config.model);
96
+ const resolvedEffort = platformManaged
97
+ ? (platformDefaults.thinkingEffort ??
98
+ (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined))
99
+ : process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined;
100
+ if (platformDefaults.model)
101
+ agentLog.info(`platform config: model=${platformDefaults.model} → resolved=${resolvedModel ?? 'cli-default'} (${platformModelOverride ? 'platform-managed' : 'floor/self'})`);
102
+ if (platformDefaults.thinkingEffort)
103
+ agentLog.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort} (${platformModelOverride ? 'applied' : 'ignored, model_management=self'})`);
104
+ const adapter = new ClaudeCodeAdapter({
105
+ claudeBin: config.claudeBin,
106
+ claudeHome: config.claudeHome,
107
+ workspaceDir: config.workspaceDir,
108
+ model: resolvedModel,
109
+ permissionMode: config.permissionMode,
110
+ allowedTools: config.allowedTools,
111
+ disallowedTools: config.disallowedTools,
112
+ additionalDirs: config.additionalDirs,
113
+ appendSystemPrompt: config.appendSystemPrompt,
114
+ allowApiKey: config.allowApiKey,
115
+ sessionManager,
116
+ // Static Parall credentials. The long-lived subprocess must authenticate
117
+ // against Parall's API across many dispatches, so these cannot come from
118
+ // a per-dispatch DispatchContext.
119
+ apiUrl: config.apiUrl,
120
+ apiKey: config.apiKey,
121
+ orgId: config.orgId,
122
+ contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
123
+ stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
124
+ });
125
+ if (resolvedEffort) {
126
+ adapter.updateConfig({ effort: resolvedEffort });
127
+ }
128
+ const gateway = new ParallAgentGateway({
129
+ accountId: agentUserId,
130
+ client,
131
+ ws,
132
+ connectionLabel: resolvedWsUrl,
133
+ config: {
134
+ parall_url: config.apiUrl,
135
+ api_key: config.apiKey,
136
+ org_id: config.orgId,
137
+ },
138
+ agentUserId,
139
+ runtimeType: 'claude-code',
140
+ runtimeKey,
141
+ runtimeRef: {
142
+ hostname: os.hostname(),
143
+ pid: process.pid,
144
+ workspace_dir: config.workspaceDir,
145
+ claude_home: config.claudeHome,
146
+ },
147
+ dispatchAdapter: adapter,
148
+ log: agentLog,
149
+ shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
150
+ forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
151
+ dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
152
+ contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
153
+ stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
154
+ onConfigUpdate: async () => {
155
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
156
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
157
+ platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
158
+ const updated = await configMgr.fetch();
159
+ const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
160
+ adapter.updateConfig({
161
+ model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
162
+ effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
163
+ });
164
+ },
165
+ onSessionReady: async () => {
166
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
167
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
168
+ platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
169
+ const updated = await configMgr.fetch();
170
+ const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
171
+ adapter.updateConfig({
172
+ model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
173
+ effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
174
+ });
175
+ },
176
+ // Long-lived `claude` subprocesses outlive a single dispatch, so the
177
+ // gateway's shuttingDown flag alone does not tear them down. Piggyback on
178
+ // onBeforeDisconnect to close stdin and SIGTERM any survivors after
179
+ // in-flight drains finish.
180
+ onBeforeDisconnect: () => adapter.shutdown(),
181
+ onNewSession: async () => {
182
+ sessionManager.clearMainSession();
183
+ adapter.resetProcesses();
184
+ },
185
+ onSessionStale: () => {
186
+ sessionManager.clearMainSession();
187
+ adapter.resetProcesses();
188
+ },
189
+ });
190
+ const abortController = new AbortController();
191
+ const abort = () => abortController.abort();
192
+ process.on('SIGINT', abort);
193
+ process.on('SIGTERM', abort);
194
+ try {
195
+ agentLog.info(`starting self-hosted Claude runtime for ${me.display_name} (${agentUserId})`);
196
+ await gateway.run(abortController.signal);
197
+ }
198
+ finally {
199
+ process.off('SIGINT', abort);
200
+ process.off('SIGTERM', abort);
201
+ }
150
202
  }
151
203
  finally {
152
- process.off("SIGINT", abort);
153
- process.off("SIGTERM", abort);
204
+ await telemetry.shutdown();
154
205
  }
155
206
  }
156
207
  main().catch((err) => {
@@ -1,10 +1,10 @@
1
- import type { Readable } from "node:stream";
2
- import type { RuntimeEvent } from "@parall/agent-core";
1
+ import type { Readable } from 'node:stream';
2
+ import type { RuntimeEvent } from '@parall/agent-core';
3
3
  export type ClaudeParsedEvent = RuntimeEvent | {
4
- type: "session_id";
4
+ type: 'session_id';
5
5
  sessionId: string;
6
6
  } | {
7
- type: "turn_end";
7
+ type: 'turn_end';
8
8
  sessionId?: string;
9
9
  isError: boolean;
10
10
  };
@@ -1 +1 @@
1
- {"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAWzC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AA4E/D,wBAAuB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,GAAG,cAAc,CAAC,iBAAiB,CAAC,CA8HlG"}
1
+ {"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAWzC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AA4E/D,wBAAuB,qBAAqB,CAC1C,QAAQ,EAAE,QAAQ,GACjB,cAAc,CAAC,iBAAiB,CAAC,CAiInC"}
@@ -1,28 +1,28 @@
1
1
  function asTrimmedString(value) {
2
- if (typeof value !== "string")
2
+ if (typeof value !== 'string')
3
3
  return undefined;
4
4
  const trimmed = value.trim();
5
5
  return trimmed.length > 0 ? trimmed : undefined;
6
6
  }
7
7
  function parseEventTimestampMs(event) {
8
- if (!event || typeof event !== "object")
8
+ if (!event || typeof event !== 'object')
9
9
  return undefined;
10
10
  const timestamp = event.timestamp;
11
- if (typeof timestamp !== "string")
11
+ if (typeof timestamp !== 'string')
12
12
  return undefined;
13
13
  const ms = Date.parse(timestamp);
14
14
  return Number.isFinite(ms) ? ms : undefined;
15
15
  }
16
16
  function stringifyContent(value) {
17
- if (typeof value === "string")
17
+ if (typeof value === 'string')
18
18
  return value;
19
19
  if (Array.isArray(value)) {
20
20
  return value
21
21
  .map((item) => {
22
- if (typeof item === "string")
22
+ if (typeof item === 'string')
23
23
  return item;
24
- if (!item || typeof item !== "object")
25
- return "";
24
+ if (!item || typeof item !== 'object')
25
+ return '';
26
26
  const text = asTrimmedString(item.text);
27
27
  if (text)
28
28
  return text;
@@ -32,18 +32,18 @@ function stringifyContent(value) {
32
32
  return JSON.stringify(item);
33
33
  })
34
34
  .filter(Boolean)
35
- .join("\n");
35
+ .join('\n');
36
36
  }
37
- if (value && typeof value === "object") {
37
+ if (value && typeof value === 'object') {
38
38
  return JSON.stringify(value);
39
39
  }
40
- return "";
40
+ return '';
41
41
  }
42
42
  function normalizeToolResultContent(content, fallback) {
43
43
  const direct = stringifyContent(content).trim();
44
44
  if (direct)
45
45
  return direct;
46
- if (fallback && typeof fallback === "object") {
46
+ if (fallback && typeof fallback === 'object') {
47
47
  const stdout = asTrimmedString(fallback.stdout);
48
48
  const stderr = asTrimmedString(fallback.stderr);
49
49
  if (stdout && stderr)
@@ -53,20 +53,20 @@ function normalizeToolResultContent(content, fallback) {
53
53
  if (stderr)
54
54
  return stderr;
55
55
  }
56
- return "";
56
+ return '';
57
57
  }
58
58
  async function* readJsonLines(readable) {
59
- let buffer = "";
60
- readable.setEncoding("utf8");
59
+ let buffer = '';
60
+ readable.setEncoding('utf8');
61
61
  for await (const chunk of readable) {
62
62
  buffer += chunk;
63
- let newlineIndex = buffer.indexOf("\n");
63
+ let newlineIndex = buffer.indexOf('\n');
64
64
  while (newlineIndex >= 0) {
65
65
  const line = buffer.slice(0, newlineIndex).trim();
66
66
  buffer = buffer.slice(newlineIndex + 1);
67
67
  if (line)
68
68
  yield line;
69
- newlineIndex = buffer.indexOf("\n");
69
+ newlineIndex = buffer.indexOf('\n');
70
70
  }
71
71
  }
72
72
  const tail = buffer.trim();
@@ -83,45 +83,45 @@ export async function* parseClaudeStreamJson(readable) {
83
83
  catch {
84
84
  continue;
85
85
  }
86
- if (!event || typeof event !== "object")
86
+ if (!event || typeof event !== 'object')
87
87
  continue;
88
88
  const now = Date.now();
89
89
  const eventTimestampMs = parseEventTimestampMs(event) ?? now;
90
90
  const eventRecord = event;
91
- if (eventRecord.type === "system" && eventRecord.subtype === "init") {
91
+ if (eventRecord.type === 'system' && eventRecord.subtype === 'init') {
92
92
  const sessionId = asTrimmedString(eventRecord.session_id);
93
93
  if (sessionId) {
94
- yield { type: "session_id", sessionId };
94
+ yield { type: 'session_id', sessionId };
95
95
  }
96
96
  continue;
97
97
  }
98
- if (eventRecord.type === "assistant") {
98
+ if (eventRecord.type === 'assistant') {
99
99
  if (eventRecord.error)
100
100
  continue;
101
101
  const message = eventRecord.message;
102
- const content = message && typeof message === "object"
102
+ const content = message && typeof message === 'object'
103
103
  ? message.content
104
104
  : undefined;
105
105
  if (!Array.isArray(content))
106
106
  continue;
107
107
  for (const block of content) {
108
- if (!block || typeof block !== "object")
108
+ if (!block || typeof block !== 'object')
109
109
  continue;
110
110
  const blockRecord = block;
111
111
  switch (blockRecord.type) {
112
- case "thinking": {
112
+ case 'thinking': {
113
113
  const text = asTrimmedString(blockRecord.thinking);
114
114
  if (text)
115
- yield { type: "thinking", text };
115
+ yield { type: 'thinking', text };
116
116
  break;
117
117
  }
118
- case "text": {
118
+ case 'text': {
119
119
  const text = asTrimmedString(blockRecord.text);
120
120
  if (text)
121
- yield { type: "text", text, project: true };
121
+ yield { type: 'text', text, project: true };
122
122
  break;
123
123
  }
124
- case "tool_use": {
124
+ case 'tool_use': {
125
125
  const callId = asTrimmedString(blockRecord.id);
126
126
  const toolName = asTrimmedString(blockRecord.name);
127
127
  if (!callId || !toolName)
@@ -131,7 +131,7 @@ export async function* parseClaudeStreamJson(readable) {
131
131
  startedAtMs: eventTimestampMs,
132
132
  });
133
133
  yield {
134
- type: "tool_call",
134
+ type: 'tool_call',
135
135
  callId,
136
136
  toolName,
137
137
  input: blockRecord.input ?? {},
@@ -143,18 +143,18 @@ export async function* parseClaudeStreamJson(readable) {
143
143
  }
144
144
  continue;
145
145
  }
146
- if (eventRecord.type === "user") {
146
+ if (eventRecord.type === 'user') {
147
147
  const message = eventRecord.message;
148
- const content = message && typeof message === "object"
148
+ const content = message && typeof message === 'object'
149
149
  ? message.content
150
150
  : undefined;
151
151
  if (!Array.isArray(content))
152
152
  continue;
153
153
  for (const block of content) {
154
- if (!block || typeof block !== "object")
154
+ if (!block || typeof block !== 'object')
155
155
  continue;
156
156
  const blockRecord = block;
157
- if (blockRecord.type !== "tool_result")
157
+ if (blockRecord.type !== 'tool_result')
158
158
  continue;
159
159
  const callId = asTrimmedString(blockRecord.tool_use_id);
160
160
  if (!callId)
@@ -162,24 +162,24 @@ export async function* parseClaudeStreamJson(readable) {
162
162
  const meta = toolUses.get(callId);
163
163
  const output = normalizeToolResultContent(blockRecord.content, eventRecord.tool_use_result);
164
164
  yield {
165
- type: "tool_result",
165
+ type: 'tool_result',
166
166
  callId,
167
- toolName: meta?.toolName ?? "unknown",
167
+ toolName: meta?.toolName ?? 'unknown',
168
168
  output,
169
- ...(blockRecord.is_error === true ? { error: output || "Tool call failed" } : {}),
169
+ ...(blockRecord.is_error === true ? { error: output || 'Tool call failed' } : {}),
170
170
  ...(meta ? { durationMs: Math.max(0, eventTimestampMs - meta.startedAtMs) } : {}),
171
171
  };
172
172
  toolUses.delete(callId);
173
173
  }
174
174
  continue;
175
175
  }
176
- if (eventRecord.type === "result") {
176
+ if (eventRecord.type === 'result') {
177
177
  const isError = eventRecord.is_error === true;
178
178
  if (isError) {
179
- const message = asTrimmedString(eventRecord.result)
180
- || asTrimmedString(eventRecord.error)
181
- || "Claude dispatch failed";
182
- yield { type: "error", message };
179
+ const message = asTrimmedString(eventRecord.result) ||
180
+ asTrimmedString(eventRecord.error) ||
181
+ 'Claude dispatch failed';
182
+ yield { type: 'error', message };
183
183
  }
184
184
  // Turn boundary: the subprocess lives across many turns, so any
185
185
  // tool_use entries that never matched a tool_result this turn
@@ -188,7 +188,7 @@ export async function* parseClaudeStreamJson(readable) {
188
188
  // the map grow unbounded or mislabel durations on id collisions.
189
189
  toolUses.clear();
190
190
  yield {
191
- type: "turn_end",
191
+ type: 'turn_end',
192
192
  sessionId: asTrimmedString(eventRecord.session_id),
193
193
  isError,
194
194
  };
@@ -1,5 +1,5 @@
1
- import type { ChildProcessWithoutNullStreams } from "node:child_process";
2
- import type { ForkSessionHandle } from "@parall/agent-core";
1
+ import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import type { ForkSessionHandle } from '@parall/agent-core';
3
3
  type ClaudeSessionManagerLogger = {
4
4
  warn(message: string): void;
5
5
  };
@@ -1 +1 @@
1
- {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAO5D,KAAK,0BAA0B,GAAG;IAChC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,8BAA8B,CAAC;IACrC,WAAW,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7E,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,qBAAa,oBAAoB;IAM7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAP1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;gBAGjD,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,0BAA0B,YAAA;IAKtD,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpD,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAU3C,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAQrD,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAYrE,WAAW,CAAC,UAAU,EAAE,MAAM;IAU9B,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI/D,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB;IAsB/D,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,mBAAmB;IAO7D,qEAAqE;IACrE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE5C,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;YAQpB,WAAW;IAuCzB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,WAAW;IAqBnB,gBAAgB;IAShB,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,OAAO;CAahB"}
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAO5D,KAAK,0BAA0B,GAAG;IAChC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,8BAA8B,CAAC;IACrC,WAAW,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7E,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,qBAAa,oBAAoB;IAM7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAP1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;gBAGjD,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,0BAA0B,YAAA;IAKtD,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpD,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAU3C,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAQrD,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAYrE,WAAW,CAAC,UAAU,EAAE,MAAM;IAU9B,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI/D,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB;IAsB/D,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,mBAAmB;IAO7D,qEAAqE;IACrE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE5C,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;YAMpB,WAAW;IAmCzB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,WAAW;IAmBnB,gBAAgB;IAShB,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,OAAO;CAoBhB"}
@@ -1,6 +1,6 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { randomUUID } from "node:crypto";
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
4
  export class ClaudeSessionManager {
5
5
  mainSessionKey;
6
6
  stateFilePath;
@@ -20,10 +20,10 @@ export class ClaudeSessionManager {
20
20
  getResumeArgs(sessionKey) {
21
21
  const existing = this.sessionIds.get(sessionKey);
22
22
  if (existing)
23
- return ["--resume", existing];
23
+ return ['--resume', existing];
24
24
  const parent = this.pendingForkParents.get(sessionKey);
25
25
  if (parent)
26
- return ["--resume", parent, "--fork-session"];
26
+ return ['--resume', parent, '--fork-session'];
27
27
  return [];
28
28
  }
29
29
  recordSessionId(sessionKey, sessionId) {
@@ -102,7 +102,7 @@ export class ClaudeSessionManager {
102
102
  if (handle.proc.exitCode === null && handle.proc.signalCode === null) {
103
103
  this.logger?.warn(`claude-agent: SIGTERM timed out for ${sessionKey}, escalating to SIGKILL`);
104
104
  try {
105
- handle.proc.kill("SIGKILL");
105
+ handle.proc.kill('SIGKILL');
106
106
  }
107
107
  catch (error) {
108
108
  this.logger?.warn(`claude-agent: SIGKILL failed for ${sessionKey}: ${String(error)}`);
@@ -131,7 +131,7 @@ export class ClaudeSessionManager {
131
131
  resolve(true);
132
132
  }, ms);
133
133
  // Don't keep the event loop alive purely on the timeout.
134
- if (typeof timer.unref === "function")
134
+ if (typeof timer.unref === 'function')
135
135
  timer.unref();
136
136
  promise
137
137
  .catch(() => {
@@ -157,7 +157,7 @@ export class ClaudeSessionManager {
157
157
  }
158
158
  if (handle.proc.exitCode === null && handle.proc.signalCode === null) {
159
159
  try {
160
- handle.proc.kill("SIGTERM");
160
+ handle.proc.kill('SIGTERM');
161
161
  }
162
162
  catch (error) {
163
163
  this.logger?.warn(`claude-agent: failed to SIGTERM claude subprocess (${reason}): ${String(error)}`);
@@ -175,9 +175,11 @@ export class ClaudeSessionManager {
175
175
  }
176
176
  restore() {
177
177
  try {
178
- const raw = fs.readFileSync(this.stateFilePath, "utf8");
178
+ const raw = fs.readFileSync(this.stateFilePath, 'utf8');
179
179
  const parsed = JSON.parse(raw);
180
- if (parsed.runtimeKey === this.mainSessionKey && typeof parsed.sessionId === "string" && parsed.sessionId.trim()) {
180
+ if (parsed.runtimeKey === this.mainSessionKey &&
181
+ typeof parsed.sessionId === 'string' &&
182
+ parsed.sessionId.trim()) {
181
183
  this.sessionIds.set(this.mainSessionKey, parsed.sessionId.trim());
182
184
  }
183
185
  }