@parall/claude-agent 1.24.0 → 1.26.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/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,41 @@ 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>;
14
31
  forkSession({ sessionKey }: ForkOpts): import("@parall/agent-core").ForkSessionHandle | null;
15
32
  cleanupFork({ fork }: CleanupForkOpts): void;
33
+ shutdown(): Promise<void>;
34
+ private runTurn;
35
+ private ensureProcess;
36
+ private spawnProcess;
37
+ private killProcess;
38
+ private writeUserMessage;
16
39
  private buildArgs;
40
+ private buildPlaceholderContext;
17
41
  }
18
42
  export {};
19
43
  //# 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,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,73 +67,7 @@ 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();
@@ -122,19 +77,170 @@ export class ClaudeCodeAdapter {
122
77
  return this.opts.sessionManager.createForkSession(sessionKey);
123
78
  }
124
79
  cleanupFork({ fork }) {
80
+ const state = this.processes.get(fork.sessionKey);
81
+ if (state) {
82
+ this.killProcess(fork.sessionKey, state);
83
+ }
125
84
  this.opts.sessionManager.cleanupFork(fork.sessionKey);
126
85
  }
127
- buildArgs(sessionKey, prompt) {
86
+ async shutdown() {
87
+ this.shuttingDown = true;
88
+ for (const [sessionKey, state] of this.processes) {
89
+ this.killProcess(sessionKey, state);
90
+ }
91
+ this.processes.clear();
92
+ await this.opts.sessionManager.shutdownAll();
93
+ }
94
+ async *runTurn(sessionKey, promptBody, log) {
95
+ let state;
96
+ try {
97
+ state = this.ensureProcess(sessionKey, log);
98
+ }
99
+ catch (err) {
100
+ yield { type: "error", message: `Claude spawn failed: ${String(err)}` };
101
+ return;
102
+ }
103
+ const groupKey = randomUUID();
104
+ let sawError = false;
105
+ try {
106
+ this.writeUserMessage(state.handle, promptBody);
107
+ }
108
+ catch (err) {
109
+ yield { type: "error", message: `Claude stdin write failed: ${String(err)}` };
110
+ this.killProcess(sessionKey, state);
111
+ return;
112
+ }
113
+ while (true) {
114
+ const next = await state.parser.next();
115
+ if (next.done) {
116
+ state.done = true;
117
+ this.processes.delete(sessionKey);
118
+ if (!sawError) {
119
+ const detail = state.handle.stderrChunks.join("").trim();
120
+ const exit = await state.handle.exitPromise.catch(() => ({ code: null, signal: null }));
121
+ yield {
122
+ type: "error",
123
+ message: detail
124
+ || `Claude exited with code ${exit.code ?? "unknown"}${exit.signal ? ` (${exit.signal})` : ""}`,
125
+ };
126
+ }
127
+ return;
128
+ }
129
+ const parsed = next.value;
130
+ if (parsed.type === "session_id") {
131
+ this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
132
+ continue;
133
+ }
134
+ if (parsed.type === "turn_end") {
135
+ if (state.needsRestart) {
136
+ this.killProcess(sessionKey, state);
137
+ }
138
+ return;
139
+ }
140
+ if (parsed.type === "error") {
141
+ sawError = true;
142
+ yield parsed;
143
+ continue;
144
+ }
145
+ if (parsed.type === "text") {
146
+ yield { ...parsed, project: false, groupKey };
147
+ continue;
148
+ }
149
+ yield { ...parsed, groupKey };
150
+ }
151
+ }
152
+ ensureProcess(sessionKey, log) {
153
+ if (this.shuttingDown) {
154
+ throw new Error("claude-agent: adapter shutting down, refusing new process");
155
+ }
156
+ const existing = this.processes.get(sessionKey);
157
+ if (existing && !existing.done) {
158
+ const { proc } = existing.handle;
159
+ if (existing.needsRestart) {
160
+ this.killProcess(sessionKey, existing);
161
+ }
162
+ else if (proc.exitCode === null && proc.signalCode === null && !proc.stdin.destroyed) {
163
+ return existing;
164
+ }
165
+ else {
166
+ this.processes.delete(sessionKey);
167
+ }
168
+ }
169
+ const handle = this.spawnProcess(sessionKey, log);
170
+ const parser = parseClaudeStreamJson(handle.proc.stdout);
171
+ const state = { handle, parser, done: false, needsRestart: false };
172
+ this.processes.set(sessionKey, state);
173
+ this.opts.sessionManager.registerProcess(sessionKey, handle);
174
+ return state;
175
+ }
176
+ spawnProcess(sessionKey, log) {
177
+ const args = this.buildArgs(sessionKey);
178
+ const env = buildSpawnEnv(process.env, this.opts.claudeHome, this.buildPlaceholderContext(sessionKey), { allowApiKey: this.opts.allowApiKey, effortLevel: this._effortLevel });
179
+ log?.info(`claude-agent: spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || "default"}, effort=${this._effortLevel || "default"}, mode=${this.opts.permissionMode})`);
180
+ const proc = spawn(this.opts.claudeBin, args, {
181
+ cwd: this.opts.workspaceDir,
182
+ env,
183
+ stdio: ["pipe", "pipe", "pipe"],
184
+ });
185
+ if (!proc.stdout || !proc.stderr || !proc.stdin) {
186
+ throw new Error("Claude subprocess did not provide stdio pipes");
187
+ }
188
+ const stderrChunks = [];
189
+ proc.stderr.on("data", (chunk) => {
190
+ stderrChunks.push(chunk.toString());
191
+ });
192
+ proc.stdin.on("error", () => {
193
+ // Absorb async EPIPE / ERR_STREAM_WRITE_AFTER_END when the child
194
+ // exits between our write check and the kernel delivering the data.
195
+ // The next parser.next() will see the stream end and surface an error
196
+ // RuntimeEvent through the normal turn-end path.
197
+ });
198
+ const exitPromise = new Promise((resolve) => {
199
+ proc.once("close", (code, signal) => resolve({ code, signal }));
200
+ proc.once("error", () => resolve({ code: null, signal: null }));
201
+ });
202
+ return { proc, exitPromise, stderrChunks };
203
+ }
204
+ killProcess(sessionKey, state) {
205
+ state.done = true;
206
+ const current = this.processes.get(sessionKey);
207
+ if (current === state) {
208
+ this.processes.delete(sessionKey);
209
+ }
210
+ try {
211
+ state.handle.proc.stdin.end();
212
+ }
213
+ catch { /* best-effort */ }
214
+ if (state.handle.proc.exitCode === null && state.handle.proc.signalCode === null) {
215
+ try {
216
+ state.handle.proc.kill("SIGTERM");
217
+ }
218
+ catch { /* best-effort */ }
219
+ }
220
+ }
221
+ writeUserMessage(handle, text) {
222
+ const payload = JSON.stringify({
223
+ type: "user",
224
+ message: {
225
+ role: "user",
226
+ content: [{ type: "text", text }],
227
+ },
228
+ });
229
+ handle.proc.stdin.write(`${payload}\n`);
230
+ }
231
+ buildArgs(sessionKey) {
128
232
  const args = [
129
- "--print",
233
+ "-p",
130
234
  "--verbose",
235
+ "--input-format",
236
+ "stream-json",
131
237
  "--output-format",
132
238
  "stream-json",
133
239
  "--permission-mode",
134
240
  this.opts.permissionMode,
135
241
  ];
136
- if (this.opts.model) {
137
- args.push("--model", this.opts.model);
242
+ if (this._model) {
243
+ args.push("--model", this._model);
138
244
  }
139
245
  if (this.opts.allowedTools.length > 0) {
140
246
  args.push("--allowedTools", this.opts.allowedTools.join(","));
@@ -142,6 +248,7 @@ export class ClaudeCodeAdapter {
142
248
  if (this.opts.disallowedTools.length > 0) {
143
249
  args.push("--disallowedTools", this.opts.disallowedTools.join(","));
144
250
  }
251
+ args.push("--append-system-prompt-file", path.join(this.opts.workspaceDir, ".parall", "system-prompt.md"));
145
252
  if (this.opts.appendSystemPrompt) {
146
253
  args.push("--append-system-prompt", this.opts.appendSystemPrompt);
147
254
  }
@@ -149,7 +256,24 @@ export class ClaudeCodeAdapter {
149
256
  args.push("--add-dir", ...this.opts.additionalDirs);
150
257
  }
151
258
  args.push(...this.opts.sessionManager.getResumeArgs(sessionKey));
152
- args.push(prompt);
153
259
  return args;
154
260
  }
261
+ buildPlaceholderContext(sessionKey) {
262
+ return {
263
+ accountId: "",
264
+ apiUrl: this.opts.apiUrl,
265
+ apiKey: this.opts.apiKey,
266
+ orgId: this.opts.orgId,
267
+ agentUserId: "",
268
+ runtimeType: "",
269
+ runtimeKey: "",
270
+ sessionId: "",
271
+ chatId: "",
272
+ triggerMessageId: "",
273
+ noReply: false,
274
+ contextFilePath: this.opts.contextFilePathForSession?.(sessionKey) ?? "",
275
+ stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey) ?? "",
276
+ client: undefined,
277
+ };
278
+ }
155
279
  }
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"}