@parall/claude-agent 1.25.0 → 1.26.1

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/config.d.ts CHANGED
@@ -21,5 +21,7 @@ export declare function resolveClaudeAgentConfig(env?: NodeJS.ProcessEnv): Claud
21
21
  export declare function resolveWsUrl(apiUrl: string, explicitWsUrl?: string, swimlaneName?: string): string;
22
22
  export declare function buildClaudeRuntimeKey(agentUserId: string): string;
23
23
  export declare function sessionStateFilePathForRuntime(stateDir: string, runtimeKey: string): string;
24
+ export declare function contextFilePathForSession(stateDir: string, sessionKey: string): string;
25
+ /** @deprecated Use contextFilePathForSession. */
24
26
  export declare function stepIdFilePathForSession(stateDir: string, sessionKey: string): string;
25
27
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAsBF,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,iBAAiB,CA4BhG;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAMlG;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAG3F;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGrF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAsBF,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,iBAAiB,CA4BhG;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAMlG;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAG3F;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGtF;AAED,iDAAiD;AACjD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGrF"}
package/dist/config.js CHANGED
@@ -61,6 +61,11 @@ export function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
61
61
  const fileName = Buffer.from(runtimeKey).toString("base64url");
62
62
  return path.join(stateDir, "sessions", `${fileName}.json`);
63
63
  }
64
+ export function contextFilePathForSession(stateDir, sessionKey) {
65
+ const fileName = Buffer.from(sessionKey).toString("base64url");
66
+ return path.join(stateDir, "dispatch-context", `${fileName}.json`);
67
+ }
68
+ /** @deprecated Use contextFilePathForSession. */
64
69
  export function stepIdFilePathForSession(stateDir, sessionKey) {
65
70
  const fileName = Buffer.from(sessionKey).toString("base64url");
66
71
  return path.join(stateDir, "step-ids", `${fileName}.txt`);
@@ -3,17 +3,42 @@ import type { ClaudeAgentConfig } from "./config.js";
3
3
  import { ClaudeSessionManager } from "./session-manager.js";
4
4
  type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, "additionalDirs" | "allowApiKey" | "allowedTools" | "appendSystemPrompt" | "claudeBin" | "claudeHome" | "disallowedTools" | "model" | "permissionMode" | "workspaceDir"> & {
5
5
  sessionManager: ClaudeSessionManager;
6
+ apiUrl: string;
7
+ apiKey: string;
8
+ orgId: string;
9
+ contextFilePathForSession?: (sessionKey: string) => string;
10
+ /** @deprecated Use contextFilePathForSession. */
11
+ stepIdFilePathForSession?: (sessionKey: string) => string;
6
12
  };
7
13
  export declare function buildSpawnEnv(parentEnv: NodeJS.ProcessEnv, claudeHome: string, context: DispatchOpts["context"], opts: {
8
14
  allowApiKey: boolean;
15
+ effortLevel?: string;
9
16
  }): NodeJS.ProcessEnv;
10
17
  export declare class ClaudeCodeAdapter implements DispatchAdapter {
11
18
  private readonly opts;
19
+ private readonly processes;
20
+ private shuttingDown;
21
+ private _model;
22
+ private _effortLevel;
12
23
  constructor(opts: ClaudeCodeAdapterOptions);
24
+ get currentModel(): string | undefined;
25
+ get currentEffort(): string | undefined;
26
+ updateConfig(config: {
27
+ model?: string | null;
28
+ effort?: string | null;
29
+ }): void;
13
30
  dispatch({ event, bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent>;
31
+ getBranchPoint(_sessionKey: string): string | undefined;
14
32
  forkSession({ sessionKey }: ForkOpts): import("@parall/agent-core").ForkSessionHandle | null;
15
33
  cleanupFork({ fork }: CleanupForkOpts): void;
34
+ shutdown(): Promise<void>;
35
+ private runTurn;
36
+ private ensureProcess;
37
+ private spawnProcess;
38
+ private killProcess;
39
+ private writeUserMessage;
16
40
  private buildArgs;
41
+ private buildPlaceholderContext;
17
42
  }
18
43
  export {};
19
44
  //# sourceMappingURL=dispatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,KAAK,wBAAwB,GAAG,IAAI,CAClC,iBAAiB,EACf,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,oBAAoB,GACpB,WAAW,GACX,YAAY,GACZ,iBAAiB,GACjB,OAAO,GACP,gBAAgB,GAChB,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,oBAAoB,CAAC;CACtC,CAAC;AAEF,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,CAAC,UAAU,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,EAChC,IAAI,EAAE;IAAE,WAAW,EAAE,OAAO,CAAA;CAAE,GAC7B,MAAM,CAAC,UAAU,CAuBnB;AAED,qBAAa,iBAAkB,YAAW,eAAe;IAC3C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,wBAAwB;IAEpD,QAAQ,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAkGxG,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE,QAAQ;IAIpC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,SAAS;CAkClB"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EAER,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EACL,oBAAoB,EAErB,MAAM,sBAAsB,CAAC;AAE9B,KAAK,wBAAwB,GAAG,IAAI,CAClC,iBAAiB,EACf,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,oBAAoB,GACpB,WAAW,GACX,YAAY,GACZ,iBAAiB,GACjB,OAAO,GACP,gBAAgB,GAChB,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,oBAAoB,CAAC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,yBAAyB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3D,iDAAiD;IACjD,wBAAwB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3D,CAAC;AAEF,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,CAAC,UAAU,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,EAChC,IAAI,EAAE;IAAE,WAAW,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,MAAM,CAAC,UAAU,CAuBnB;AAgBD,qBAAa,iBAAkB,YAAW,eAAe;IAM3C,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,YAAY,CAAqB;gBAEZ,IAAI,EAAE,wBAAwB;IAI3D,IAAI,YAAY,IAAI,MAAM,GAAG,SAAS,CAAwB;IAC9D,IAAI,aAAa,IAAI,MAAM,GAAG,SAAS,CAA8B;IAErE,YAAY,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAYtE,QAAQ,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAuBxG,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMvD,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE,QAAQ;IAIpC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAQ/B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAShB,OAAO;IAsEtB,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,YAAY;IA0CpB,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,SAAS;IAyCjB,OAAO,CAAC,uBAAuB;CAkBhC"}
package/dist/dispatch.js CHANGED
@@ -1,19 +1,15 @@
1
+ import * as path from "node:path";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { spawn } from "node:child_process";
3
4
  import { appendPreparedLocalAttachmentRefs, pinLocalAttachmentPaths, } from "@parall/agent-core/internal/attachment-input";
4
5
  import { parseClaudeStreamJson } from "./output-parser.js";
5
6
  export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
6
7
  const env = { ...parentEnv };
7
- // Default: behave like the hosted Claude container — OAuth via
8
- // ~/.claude/.credentials.json only. Inheriting the operator's
9
- // ANTHROPIC_API_KEY would silently divert billing to Anthropic API
10
- // pay-per-use instead of the connected Claude.ai subscription.
11
- // Opt back in with PRLL_CLAUDE_ALLOW_API_KEY=1.
12
8
  if (!opts.allowApiKey) {
13
9
  delete env.ANTHROPIC_API_KEY;
14
10
  delete env.ANTHROPIC_AUTH_TOKEN;
15
11
  }
16
- return {
12
+ const result = {
17
13
  ...env,
18
14
  HOME: claudeHome,
19
15
  PRLL_API_URL: context.apiUrl,
@@ -23,13 +19,38 @@ export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
23
19
  PRLL_CHAT_ID: context.chatId ?? "",
24
20
  PRLL_TRIGGER_MESSAGE_ID: context.triggerMessageId ?? "",
25
21
  PRLL_NO_REPLY: context.noReply ? "1" : "",
22
+ PRLL_CONTEXT_FILE: context.contextFilePath ?? "",
26
23
  PRLL_STEP_ID_FILE: context.stepIdFilePath ?? "",
27
24
  };
25
+ if (opts.effortLevel) {
26
+ result.CLAUDE_CODE_EFFORT_LEVEL = opts.effortLevel;
27
+ }
28
+ return result;
28
29
  }
29
30
  export class ClaudeCodeAdapter {
30
31
  opts;
32
+ processes = new Map();
33
+ shuttingDown = false;
34
+ _model;
35
+ _effortLevel;
31
36
  constructor(opts) {
32
37
  this.opts = opts;
38
+ this._model = opts.model;
39
+ }
40
+ get currentModel() { return this._model; }
41
+ get currentEffort() { return this._effortLevel; }
42
+ updateConfig(config) {
43
+ const modelChanged = config.model !== undefined && config.model !== this._model;
44
+ const effortChanged = config.effort !== undefined && config.effort !== this._effortLevel;
45
+ if (modelChanged)
46
+ this._model = config.model ?? undefined;
47
+ if (effortChanged)
48
+ this._effortLevel = config.effort ?? undefined;
49
+ if (modelChanged || effortChanged) {
50
+ for (const [, state] of this.processes) {
51
+ state.needsRestart = true;
52
+ }
53
+ }
33
54
  }
34
55
  async *dispatch({ event, bodyForAgent, sessionKey, context }) {
35
56
  let promptBody = bodyForAgent;
@@ -46,95 +67,185 @@ export class ClaudeCodeAdapter {
46
67
  context.log?.warn?.(`claude-agent[${context.accountId}]: failed to prepare local attachments: ${String(err)}`);
47
68
  }
48
69
  try {
49
- const args = this.buildArgs(sessionKey, promptBody);
50
- const env = buildSpawnEnv(process.env, this.opts.claudeHome, context, {
51
- allowApiKey: this.opts.allowApiKey,
52
- });
53
- context.log?.info(`claude-agent[${context.accountId}]: spawn ${this.opts.claudeBin} ${args.slice(0, -1).join(" ")}`);
54
- const proc = spawn(this.opts.claudeBin, args, {
55
- cwd: this.opts.workspaceDir,
56
- env,
57
- stdio: ["ignore", "pipe", "pipe"],
58
- });
59
- if (!proc.stdout || !proc.stderr) {
60
- throw new Error("Claude subprocess did not provide stdio pipes");
61
- }
62
- const stderr = [];
63
- proc.stderr.on("data", (chunk) => {
64
- stderr.push(chunk.toString());
65
- });
66
- const exitPromise = new Promise((resolve, reject) => {
67
- proc.once("error", reject);
68
- proc.once("close", (code, signal) => resolve({ code, signal }));
69
- });
70
- const groupKey = randomUUID();
71
- let sawError = false;
72
- let completed = false;
73
- try {
74
- for await (const streamEvent of parseClaudeStreamJson(proc.stdout)) {
75
- if (streamEvent.type === "session_id") {
76
- this.opts.sessionManager.recordSessionId(sessionKey, streamEvent.sessionId);
77
- continue;
78
- }
79
- if (streamEvent.type === "error") {
80
- sawError = true;
81
- yield streamEvent;
82
- continue;
83
- }
84
- if (streamEvent.type === "text") {
85
- // Runtime output contract (symmetric with OpenClaw channel): Claude's
86
- // plain text is never projected as a chat message. To reply, the agent
87
- // must explicitly run `parall messages send` / `dm` via Bash. Text
88
- // events are still recorded as suppressed session steps for audit.
89
- yield {
90
- ...streamEvent,
91
- project: false,
92
- groupKey,
93
- };
94
- continue;
95
- }
96
- yield {
97
- ...streamEvent,
98
- groupKey,
99
- };
100
- }
101
- completed = true;
102
- }
103
- finally {
104
- if (!completed && proc.exitCode === null && proc.signalCode === null) {
105
- proc.kill("SIGTERM");
106
- }
107
- const { code, signal } = await exitPromise;
108
- if ((code ?? 0) !== 0 && !sawError) {
109
- const detail = stderr.join("").trim();
110
- yield {
111
- type: "error",
112
- message: detail || `Claude exited with code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`,
113
- };
114
- }
115
- }
70
+ yield* this.runTurn(sessionKey, promptBody, context.log);
116
71
  }
117
72
  finally {
118
73
  releasePreparedAttachments();
119
74
  }
120
75
  }
76
+ getBranchPoint(_sessionKey) {
77
+ // Claude Code does not expose a branch-point API; fork scope prefix
78
+ // provides the behavioral fallback for this runtime.
79
+ return undefined;
80
+ }
121
81
  forkSession({ sessionKey }) {
122
82
  return this.opts.sessionManager.createForkSession(sessionKey);
123
83
  }
124
84
  cleanupFork({ fork }) {
85
+ const state = this.processes.get(fork.sessionKey);
86
+ if (state) {
87
+ this.killProcess(fork.sessionKey, state);
88
+ }
125
89
  this.opts.sessionManager.cleanupFork(fork.sessionKey);
126
90
  }
127
- buildArgs(sessionKey, prompt) {
91
+ async shutdown() {
92
+ this.shuttingDown = true;
93
+ for (const [sessionKey, state] of this.processes) {
94
+ this.killProcess(sessionKey, state);
95
+ }
96
+ this.processes.clear();
97
+ await this.opts.sessionManager.shutdownAll();
98
+ }
99
+ async *runTurn(sessionKey, promptBody, log) {
100
+ let state;
101
+ try {
102
+ state = this.ensureProcess(sessionKey, log);
103
+ }
104
+ catch (err) {
105
+ yield { type: "error", message: `Claude spawn failed: ${String(err)}` };
106
+ return;
107
+ }
108
+ const groupKey = randomUUID();
109
+ let sawError = false;
110
+ try {
111
+ this.writeUserMessage(state.handle, promptBody);
112
+ }
113
+ catch (err) {
114
+ yield { type: "error", message: `Claude stdin write failed: ${String(err)}` };
115
+ this.killProcess(sessionKey, state);
116
+ return;
117
+ }
118
+ while (true) {
119
+ const next = await state.parser.next();
120
+ if (next.done) {
121
+ state.done = true;
122
+ this.processes.delete(sessionKey);
123
+ if (!sawError) {
124
+ const detail = state.handle.stderrChunks.join("").trim();
125
+ const exit = await state.handle.exitPromise.catch(() => ({ code: null, signal: null }));
126
+ yield {
127
+ type: "error",
128
+ message: detail
129
+ || `Claude exited with code ${exit.code ?? "unknown"}${exit.signal ? ` (${exit.signal})` : ""}`,
130
+ };
131
+ }
132
+ return;
133
+ }
134
+ const parsed = next.value;
135
+ if (parsed.type === "session_id") {
136
+ this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
137
+ continue;
138
+ }
139
+ if (parsed.type === "turn_end") {
140
+ if (state.needsRestart) {
141
+ this.killProcess(sessionKey, state);
142
+ }
143
+ return;
144
+ }
145
+ if (parsed.type === "error") {
146
+ sawError = true;
147
+ yield parsed;
148
+ continue;
149
+ }
150
+ if (parsed.type === "text") {
151
+ yield { ...parsed, project: false, groupKey };
152
+ continue;
153
+ }
154
+ yield { ...parsed, groupKey };
155
+ }
156
+ }
157
+ ensureProcess(sessionKey, log) {
158
+ if (this.shuttingDown) {
159
+ throw new Error("claude-agent: adapter shutting down, refusing new process");
160
+ }
161
+ const existing = this.processes.get(sessionKey);
162
+ if (existing && !existing.done) {
163
+ const { proc } = existing.handle;
164
+ if (existing.needsRestart) {
165
+ this.killProcess(sessionKey, existing);
166
+ }
167
+ else if (proc.exitCode === null && proc.signalCode === null && !proc.stdin.destroyed) {
168
+ return existing;
169
+ }
170
+ else {
171
+ this.processes.delete(sessionKey);
172
+ }
173
+ }
174
+ const handle = this.spawnProcess(sessionKey, log);
175
+ const parser = parseClaudeStreamJson(handle.proc.stdout);
176
+ const state = { handle, parser, done: false, needsRestart: false };
177
+ this.processes.set(sessionKey, state);
178
+ this.opts.sessionManager.registerProcess(sessionKey, handle);
179
+ return state;
180
+ }
181
+ spawnProcess(sessionKey, log) {
182
+ const args = this.buildArgs(sessionKey);
183
+ const env = buildSpawnEnv(process.env, this.opts.claudeHome, this.buildPlaceholderContext(sessionKey), { allowApiKey: this.opts.allowApiKey, effortLevel: this._effortLevel });
184
+ log?.info(`claude-agent: spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || "default"}, effort=${this._effortLevel || "default"}, mode=${this.opts.permissionMode})`);
185
+ const proc = spawn(this.opts.claudeBin, args, {
186
+ cwd: this.opts.workspaceDir,
187
+ env,
188
+ stdio: ["pipe", "pipe", "pipe"],
189
+ });
190
+ if (!proc.stdout || !proc.stderr || !proc.stdin) {
191
+ throw new Error("Claude subprocess did not provide stdio pipes");
192
+ }
193
+ const stderrChunks = [];
194
+ proc.stderr.on("data", (chunk) => {
195
+ stderrChunks.push(chunk.toString());
196
+ });
197
+ proc.stdin.on("error", () => {
198
+ // Absorb async EPIPE / ERR_STREAM_WRITE_AFTER_END when the child
199
+ // exits between our write check and the kernel delivering the data.
200
+ // The next parser.next() will see the stream end and surface an error
201
+ // RuntimeEvent through the normal turn-end path.
202
+ });
203
+ const exitPromise = new Promise((resolve) => {
204
+ proc.once("close", (code, signal) => resolve({ code, signal }));
205
+ proc.once("error", () => resolve({ code: null, signal: null }));
206
+ });
207
+ return { proc, exitPromise, stderrChunks };
208
+ }
209
+ killProcess(sessionKey, state) {
210
+ state.done = true;
211
+ const current = this.processes.get(sessionKey);
212
+ if (current === state) {
213
+ this.processes.delete(sessionKey);
214
+ }
215
+ try {
216
+ state.handle.proc.stdin.end();
217
+ }
218
+ catch { /* best-effort */ }
219
+ if (state.handle.proc.exitCode === null && state.handle.proc.signalCode === null) {
220
+ try {
221
+ state.handle.proc.kill("SIGTERM");
222
+ }
223
+ catch { /* best-effort */ }
224
+ }
225
+ }
226
+ writeUserMessage(handle, text) {
227
+ const payload = JSON.stringify({
228
+ type: "user",
229
+ message: {
230
+ role: "user",
231
+ content: [{ type: "text", text }],
232
+ },
233
+ });
234
+ handle.proc.stdin.write(`${payload}\n`);
235
+ }
236
+ buildArgs(sessionKey) {
128
237
  const args = [
129
- "--print",
238
+ "-p",
130
239
  "--verbose",
240
+ "--input-format",
241
+ "stream-json",
131
242
  "--output-format",
132
243
  "stream-json",
133
244
  "--permission-mode",
134
245
  this.opts.permissionMode,
135
246
  ];
136
- if (this.opts.model) {
137
- args.push("--model", this.opts.model);
247
+ if (this._model) {
248
+ args.push("--model", this._model);
138
249
  }
139
250
  if (this.opts.allowedTools.length > 0) {
140
251
  args.push("--allowedTools", this.opts.allowedTools.join(","));
@@ -142,6 +253,7 @@ export class ClaudeCodeAdapter {
142
253
  if (this.opts.disallowedTools.length > 0) {
143
254
  args.push("--disallowedTools", this.opts.disallowedTools.join(","));
144
255
  }
256
+ args.push("--append-system-prompt-file", path.join(this.opts.workspaceDir, ".parall", "system-prompt.md"));
145
257
  if (this.opts.appendSystemPrompt) {
146
258
  args.push("--append-system-prompt", this.opts.appendSystemPrompt);
147
259
  }
@@ -149,7 +261,24 @@ export class ClaudeCodeAdapter {
149
261
  args.push("--add-dir", ...this.opts.additionalDirs);
150
262
  }
151
263
  args.push(...this.opts.sessionManager.getResumeArgs(sessionKey));
152
- args.push(prompt);
153
264
  return args;
154
265
  }
266
+ buildPlaceholderContext(sessionKey) {
267
+ return {
268
+ accountId: "",
269
+ apiUrl: this.opts.apiUrl,
270
+ apiKey: this.opts.apiKey,
271
+ orgId: this.opts.orgId,
272
+ agentUserId: "",
273
+ runtimeType: "",
274
+ runtimeKey: "",
275
+ sessionId: "",
276
+ chatId: "",
277
+ triggerMessageId: "",
278
+ noReply: false,
279
+ contextFilePath: this.opts.contextFilePathForSession?.(sessionKey) ?? "",
280
+ stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey) ?? "",
281
+ client: undefined,
282
+ };
283
+ }
155
284
  }
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import * as os from "node:os";
3
- import { ParallAgentGateway, parseShutdownDeadlineMs } from "@parall/agent-core";
3
+ import { ParallAgentGateway, createPlatformConfigManager, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
4
4
  import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
5
- import { buildClaudeRuntimeKey, resolveClaudeAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
5
+ import { buildClaudeRuntimeKey, contextFilePathForSession, resolveClaudeAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
6
6
  import { ClaudeCodeAdapter } from "./dispatch.js";
7
7
  import { ClaudeSessionManager } from "./session-manager.js";
8
8
  import { ensureClaudeWorkspace } from "./workspace.js";
@@ -55,11 +55,22 @@ async function main() {
55
55
  getTicket: () => client.getWsTicket(),
56
56
  wsUrl: resolvedWsUrl,
57
57
  });
58
+ const configMgr = createPlatformConfigManager({ client, stateDir: config.stateDir, runtimeType: "claude-code", log });
59
+ const platformDefaults = await configMgr.fetch();
60
+ let platformManaged = isPlatformManagedProfile(me.agent_profile);
61
+ const resolvedModel = platformManaged ? (platformDefaults.model ?? config.model) : config.model;
62
+ const resolvedEffort = platformManaged
63
+ ? (platformDefaults.thinkingEffort ?? (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined))
64
+ : (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined);
65
+ if (platformDefaults.model)
66
+ log.info(`platform config: model=${platformDefaults.model} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
67
+ if (platformDefaults.thinkingEffort)
68
+ log.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
58
69
  const adapter = new ClaudeCodeAdapter({
59
70
  claudeBin: config.claudeBin,
60
71
  claudeHome: config.claudeHome,
61
72
  workspaceDir: config.workspaceDir,
62
- model: config.model,
73
+ model: resolvedModel,
63
74
  permissionMode: config.permissionMode,
64
75
  allowedTools: config.allowedTools,
65
76
  disallowedTools: config.disallowedTools,
@@ -67,7 +78,18 @@ async function main() {
67
78
  appendSystemPrompt: config.appendSystemPrompt,
68
79
  allowApiKey: config.allowApiKey,
69
80
  sessionManager,
81
+ // Static Parall credentials. The long-lived subprocess must authenticate
82
+ // against Parall's API across many dispatches, so these cannot come from
83
+ // a per-dispatch DispatchContext.
84
+ apiUrl: config.apiUrl,
85
+ apiKey: config.apiKey,
86
+ orgId: config.orgId,
87
+ contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
88
+ stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
70
89
  });
90
+ if (resolvedEffort) {
91
+ adapter.updateConfig({ effort: resolvedEffort });
92
+ }
71
93
  const gateway = new ParallAgentGateway({
72
94
  accountId: agentUserId,
73
95
  client,
@@ -90,7 +112,33 @@ async function main() {
90
112
  dispatchAdapter: adapter,
91
113
  log,
92
114
  shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
115
+ contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
93
116
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
117
+ onConfigUpdate: async () => {
118
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
119
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
120
+ const updated = await configMgr.fetch();
121
+ const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
122
+ adapter.updateConfig({
123
+ model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
124
+ effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
125
+ });
126
+ },
127
+ onSessionReady: async () => {
128
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
129
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
130
+ const updated = await configMgr.fetch();
131
+ const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
132
+ adapter.updateConfig({
133
+ model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
134
+ effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
135
+ });
136
+ },
137
+ // Long-lived `claude` subprocesses outlive a single dispatch, so the
138
+ // gateway's shuttingDown flag alone does not tear them down. Piggyback on
139
+ // onBeforeDisconnect to close stdin and SIGTERM any survivors after
140
+ // in-flight drains finish.
141
+ onBeforeDisconnect: () => adapter.shutdown(),
94
142
  });
95
143
  const abortController = new AbortController();
96
144
  const abort = () => abortController.abort();
@@ -3,6 +3,10 @@ import type { RuntimeEvent } from "@parall/agent-core";
3
3
  export type ClaudeParsedEvent = RuntimeEvent | {
4
4
  type: "session_id";
5
5
  sessionId: string;
6
+ } | {
7
+ type: "turn_end";
8
+ sessionId?: string;
9
+ isError: boolean;
6
10
  };
7
11
  export declare function parseClaudeStreamJson(readable: Readable): AsyncGenerator<ClaudeParsedEvent>;
8
12
  //# sourceMappingURL=output-parser.d.ts.map
@@ -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,CAAC;AA4E9C,wBAAuB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAgHlG"}
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"}
@@ -173,11 +173,25 @@ export async function* parseClaudeStreamJson(readable) {
173
173
  }
174
174
  continue;
175
175
  }
176
- if (eventRecord.type === "result" && eventRecord.is_error === true) {
177
- const message = asTrimmedString(eventRecord.result)
178
- || asTrimmedString(eventRecord.error)
179
- || "Claude dispatch failed";
180
- yield { type: "error", message };
176
+ if (eventRecord.type === "result") {
177
+ const isError = eventRecord.is_error === true;
178
+ if (isError) {
179
+ const message = asTrimmedString(eventRecord.result)
180
+ || asTrimmedString(eventRecord.error)
181
+ || "Claude dispatch failed";
182
+ yield { type: "error", message };
183
+ }
184
+ // Turn boundary: the subprocess lives across many turns, so any
185
+ // tool_use entries that never matched a tool_result this turn
186
+ // (tool crashed mid-call, Claude aborted, etc.) must not leak into
187
+ // the next turn's accounting. Drop them here rather than letting
188
+ // the map grow unbounded or mislabel durations on id collisions.
189
+ toolUses.clear();
190
+ yield {
191
+ type: "turn_end",
192
+ sessionId: asTrimmedString(eventRecord.session_id),
193
+ isError,
194
+ };
181
195
  }
182
196
  }
183
197
  }
@@ -1,18 +1,41 @@
1
+ import type { ChildProcessWithoutNullStreams } from "node:child_process";
1
2
  import type { ForkSessionHandle } from "@parall/agent-core";
2
3
  type ClaudeSessionManagerLogger = {
3
4
  warn(message: string): void;
4
5
  };
6
+ export type ClaudeProcessHandle = {
7
+ proc: ChildProcessWithoutNullStreams;
8
+ exitPromise: Promise<{
9
+ code: number | null;
10
+ signal: NodeJS.Signals | null;
11
+ }>;
12
+ stderrChunks: string[];
13
+ };
5
14
  export declare class ClaudeSessionManager {
6
15
  private readonly mainSessionKey;
7
16
  private readonly stateFilePath;
8
17
  private readonly logger?;
9
18
  private readonly sessionIds;
10
19
  private readonly pendingForkParents;
20
+ private readonly processes;
11
21
  constructor(mainSessionKey: string, stateFilePath: string, logger?: ClaudeSessionManagerLogger | undefined);
12
22
  getResumeArgs(sessionKey: string): string[];
13
23
  recordSessionId(sessionKey: string, sessionId: string): void;
14
24
  createForkSession(parentSessionKey: string): ForkSessionHandle | null;
15
25
  cleanupFork(sessionKey: string): void;
26
+ getProcess(sessionKey: string): ClaudeProcessHandle | undefined;
27
+ registerProcess(sessionKey: string, handle: ClaudeProcessHandle): void;
28
+ clearProcess(sessionKey: string, handle?: ClaudeProcessHandle): void;
29
+ /** Grace period (ms) between SIGTERM and SIGKILL during shutdown. */
30
+ private static readonly SHUTDOWN_GRACE_MS;
31
+ shutdownAll(): Promise<void>;
32
+ private shutdownOne;
33
+ /**
34
+ * Wait up to `ms` for `promise`. Resolves `false` if the promise settled
35
+ * in time, `true` if the timeout fired first. Never rejects.
36
+ */
37
+ private raceWithTimeout;
38
+ private closeHandle;
16
39
  private restore;
17
40
  private persist;
18
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAO5D,KAAK,0BAA0B,GAAG;IAChC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,qBAAa,oBAAoB;IAK7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAN1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;gBAG7C,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,0BAA0B,YAAA;IAKtD,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;IAK9B,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,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,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,OAAO;CAahB"}