@parall/claude-agent 1.28.0 → 1.29.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/dispatch.d.ts +15 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +147 -11
- package/dist/index.js +3 -10
- package/package.json +4 -4
- package/src/dispatch.ts +150 -11
- package/src/index.ts +3 -10
package/dist/dispatch.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare function buildSpawnEnv(parentEnv: NodeJS.ProcessEnv, claudeHome:
|
|
|
17
17
|
export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
18
18
|
private readonly opts;
|
|
19
19
|
private readonly processes;
|
|
20
|
+
private readonly pendingInjections;
|
|
20
21
|
private shuttingDown;
|
|
21
22
|
private _model;
|
|
22
23
|
private _effortLevel;
|
|
@@ -27,12 +28,26 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
27
28
|
model?: string | null;
|
|
28
29
|
effort?: string | null;
|
|
29
30
|
}): void;
|
|
31
|
+
enqueueDuringDispatch(sessionKey: string, body: string): boolean;
|
|
32
|
+
hasPendingInjections(sessionKey: string): boolean;
|
|
30
33
|
dispatch({ event, bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent>;
|
|
31
34
|
getBranchPoint(_sessionKey: string): string | undefined;
|
|
32
35
|
getSessionHistoryPath(sessionKey: string): string | undefined;
|
|
33
36
|
forkSession({ sessionKey }: ForkOpts): import("@parall/agent-core").ForkSessionHandle | null;
|
|
34
37
|
cleanupFork({ fork }: CleanupForkOpts): void;
|
|
35
38
|
shutdown(): Promise<void>;
|
|
39
|
+
private static readonly STEER_TURN_TIMEOUT_MS;
|
|
40
|
+
/**
|
|
41
|
+
* Consume a steer turn whose message was already written to stdin via
|
|
42
|
+
* enqueueDuringDispatch. Skip the stdin write — only read parser output.
|
|
43
|
+
* If no output arrives within STEER_TURN_TIMEOUT_MS, the steer was
|
|
44
|
+
* incorporated into the previous turn and there is nothing to consume.
|
|
45
|
+
*
|
|
46
|
+
* On timeout, the losing parser.next() promise is saved to
|
|
47
|
+
* state.steerReadPending so the next runTurn can drain it instead of
|
|
48
|
+
* silently losing the first event of the subsequent turn.
|
|
49
|
+
*/
|
|
50
|
+
private consumeSteerTurn;
|
|
36
51
|
private runTurn;
|
|
37
52
|
private ensureProcess;
|
|
38
53
|
private spawnProcess;
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAQA,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;
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAQA,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;AAgBF,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;AAiBD,qBAAa,iBAAkB,YAAW,eAAe;IAO3C,OAAO,CAAC,QAAQ,CAAC,IAAI;IANjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,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;IAY7E,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAchE,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C,QAAQ,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAiCxG,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIvD,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAQ7D,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE,QAAQ;IAIpC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAQ/B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAS/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAU;IAEvD;;;;;;;;;OASG;YACY,gBAAgB;YAsDhB,OAAO;IA2GtB,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,YAAY;IA+CpB,OAAO,CAAC,WAAW;IAkBnB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,uBAAuB;CAkBhC"}
|
package/dist/dispatch.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import { appendPreparedLocalAttachmentRefs, pinLocalAttachmentPaths, } from "@parall/agent-core/internal/attachment-input";
|
|
6
6
|
import { parseClaudeStreamJson } from "./output-parser.js";
|
|
7
|
+
const IS_WIN32 = process.platform === "win32";
|
|
8
|
+
function quoteWin32Arg(arg) {
|
|
9
|
+
if (!/[\s"&|^<>()]/.test(arg))
|
|
10
|
+
return arg;
|
|
11
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
12
|
+
}
|
|
13
|
+
function killWin32Tree(pid) {
|
|
14
|
+
try {
|
|
15
|
+
execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: "ignore" });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
7
22
|
export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
|
|
8
23
|
const env = { ...parentEnv };
|
|
9
24
|
if (!opts.allowApiKey) {
|
|
@@ -31,6 +46,7 @@ export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
|
|
|
31
46
|
export class ClaudeCodeAdapter {
|
|
32
47
|
opts;
|
|
33
48
|
processes = new Map();
|
|
49
|
+
pendingInjections = new Map();
|
|
34
50
|
shuttingDown = false;
|
|
35
51
|
_model;
|
|
36
52
|
_effortLevel;
|
|
@@ -53,7 +69,35 @@ export class ClaudeCodeAdapter {
|
|
|
53
69
|
}
|
|
54
70
|
}
|
|
55
71
|
}
|
|
72
|
+
enqueueDuringDispatch(sessionKey, body) {
|
|
73
|
+
const state = this.processes.get(sessionKey);
|
|
74
|
+
if (!state || state.done)
|
|
75
|
+
return false;
|
|
76
|
+
const { proc } = state.handle;
|
|
77
|
+
if (proc.exitCode !== null || proc.signalCode !== null || proc.stdin.destroyed)
|
|
78
|
+
return false;
|
|
79
|
+
try {
|
|
80
|
+
this.writeUserMessage(state.handle, body);
|
|
81
|
+
this.pendingInjections.set(sessionKey, (this.pendingInjections.get(sessionKey) ?? 0) + 1);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
hasPendingInjections(sessionKey) {
|
|
89
|
+
return (this.pendingInjections.get(sessionKey) ?? 0) > 0;
|
|
90
|
+
}
|
|
56
91
|
async *dispatch({ event, bodyForAgent, sessionKey, context }) {
|
|
92
|
+
const pending = this.pendingInjections.get(sessionKey) ?? 0;
|
|
93
|
+
if (pending > 0) {
|
|
94
|
+
this.pendingInjections.delete(sessionKey);
|
|
95
|
+
context.log?.info?.(`claude-agent[${context.accountId}]: consuming ${pending} steer turn(s)`);
|
|
96
|
+
for (let i = 0; i < pending; i++) {
|
|
97
|
+
yield* this.consumeSteerTurn(sessionKey, context.log);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
57
101
|
let promptBody = bodyForAgent;
|
|
58
102
|
let releasePreparedAttachments = () => { };
|
|
59
103
|
try {
|
|
@@ -103,6 +147,66 @@ export class ClaudeCodeAdapter {
|
|
|
103
147
|
this.processes.clear();
|
|
104
148
|
await this.opts.sessionManager.shutdownAll();
|
|
105
149
|
}
|
|
150
|
+
static STEER_TURN_TIMEOUT_MS = 10_000;
|
|
151
|
+
/**
|
|
152
|
+
* Consume a steer turn whose message was already written to stdin via
|
|
153
|
+
* enqueueDuringDispatch. Skip the stdin write — only read parser output.
|
|
154
|
+
* If no output arrives within STEER_TURN_TIMEOUT_MS, the steer was
|
|
155
|
+
* incorporated into the previous turn and there is nothing to consume.
|
|
156
|
+
*
|
|
157
|
+
* On timeout, the losing parser.next() promise is saved to
|
|
158
|
+
* state.steerReadPending so the next runTurn can drain it instead of
|
|
159
|
+
* silently losing the first event of the subsequent turn.
|
|
160
|
+
*/
|
|
161
|
+
async *consumeSteerTurn(sessionKey, log) {
|
|
162
|
+
const state = this.processes.get(sessionKey);
|
|
163
|
+
if (!state || state.done) {
|
|
164
|
+
throw new Error("claude-agent: process dead during steer consumption");
|
|
165
|
+
}
|
|
166
|
+
const groupKey = randomUUID();
|
|
167
|
+
const parserNext = state.parser.next();
|
|
168
|
+
const firstRead = await Promise.race([
|
|
169
|
+
parserNext.then((r) => ({ kind: "value", result: r })),
|
|
170
|
+
new Promise((resolve) => setTimeout(() => resolve({ kind: "timeout" }), ClaudeCodeAdapter.STEER_TURN_TIMEOUT_MS)),
|
|
171
|
+
]);
|
|
172
|
+
if (firstRead.kind === "timeout") {
|
|
173
|
+
state.steerReadPending = parserNext;
|
|
174
|
+
log?.info?.(`claude-agent: steer turn timeout — steer was incorporated into previous turn`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let next = firstRead.result;
|
|
178
|
+
// eslint-disable-next-line no-constant-condition
|
|
179
|
+
while (true) {
|
|
180
|
+
if (next.done) {
|
|
181
|
+
state.done = true;
|
|
182
|
+
this.processes.delete(sessionKey);
|
|
183
|
+
throw new Error("claude-agent: process exited while consuming steer turn");
|
|
184
|
+
}
|
|
185
|
+
const parsed = next.value;
|
|
186
|
+
if (parsed.type === "session_id") {
|
|
187
|
+
this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
|
|
188
|
+
yield { type: "runtime_session", runtimeSessionId: parsed.sessionId, runtimeLaneKey: sessionKey };
|
|
189
|
+
}
|
|
190
|
+
else if (parsed.type === "turn_end") {
|
|
191
|
+
if (state.needsRestart)
|
|
192
|
+
this.killProcess(sessionKey, state);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
else if (parsed.type === "error") {
|
|
196
|
+
yield parsed;
|
|
197
|
+
}
|
|
198
|
+
else if (parsed.type === "text") {
|
|
199
|
+
yield { ...parsed, project: false, groupKey };
|
|
200
|
+
}
|
|
201
|
+
else if (parsed.type === "runtime_session") {
|
|
202
|
+
yield parsed;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
yield { ...parsed, groupKey };
|
|
206
|
+
}
|
|
207
|
+
next = await state.parser.next();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
106
210
|
async *runTurn(sessionKey, promptBody, log) {
|
|
107
211
|
let state;
|
|
108
212
|
try {
|
|
@@ -122,14 +226,46 @@ export class ClaudeCodeAdapter {
|
|
|
122
226
|
this.killProcess(sessionKey, state);
|
|
123
227
|
return;
|
|
124
228
|
}
|
|
229
|
+
if (state.steerReadPending) {
|
|
230
|
+
const orphaned = await state.steerReadPending;
|
|
231
|
+
state.steerReadPending = undefined;
|
|
232
|
+
if (!orphaned.done) {
|
|
233
|
+
const parsed = orphaned.value;
|
|
234
|
+
if (parsed.type === "session_id") {
|
|
235
|
+
this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
|
|
236
|
+
yield { type: "runtime_session", runtimeSessionId: parsed.sessionId, runtimeLaneKey: sessionKey };
|
|
237
|
+
}
|
|
238
|
+
else if (parsed.type === "turn_end") {
|
|
239
|
+
if (state.needsRestart)
|
|
240
|
+
this.killProcess(sessionKey, state);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
else if (parsed.type === "error") {
|
|
244
|
+
sawError = true;
|
|
245
|
+
yield parsed;
|
|
246
|
+
}
|
|
247
|
+
else if (parsed.type === "text") {
|
|
248
|
+
yield { ...parsed, project: false, groupKey };
|
|
249
|
+
}
|
|
250
|
+
else if (parsed.type === "runtime_session") {
|
|
251
|
+
yield parsed;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
yield { ...parsed, groupKey };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
125
258
|
while (true) {
|
|
126
259
|
const next = await state.parser.next();
|
|
127
260
|
if (next.done) {
|
|
128
261
|
state.done = true;
|
|
129
262
|
this.processes.delete(sessionKey);
|
|
263
|
+
const detail = state.handle.stderrChunks.join("").trim();
|
|
264
|
+
const exit = await state.handle.exitPromise.catch(() => ({ code: null, signal: null }));
|
|
265
|
+
if (detail) {
|
|
266
|
+
log?.warn?.(`claude-agent: subprocess stderr: ${detail}`);
|
|
267
|
+
}
|
|
130
268
|
if (!sawError) {
|
|
131
|
-
const detail = state.handle.stderrChunks.join("").trim();
|
|
132
|
-
const exit = await state.handle.exitPromise.catch(() => ({ code: null, signal: null }));
|
|
133
269
|
yield {
|
|
134
270
|
type: "error",
|
|
135
271
|
message: detail
|
|
@@ -198,10 +334,11 @@ export class ClaudeCodeAdapter {
|
|
|
198
334
|
const args = this.buildArgs(sessionKey);
|
|
199
335
|
const env = buildSpawnEnv(process.env, this.opts.claudeHome, this.buildPlaceholderContext(sessionKey), { allowApiKey: this.opts.allowApiKey, effortLevel: this._effortLevel });
|
|
200
336
|
log?.info(`claude-agent: spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || "default"}, effort=${this._effortLevel || "default"}, mode=${this.opts.permissionMode})`);
|
|
201
|
-
const proc = spawn(this.opts.claudeBin, args, {
|
|
337
|
+
const proc = spawn(IS_WIN32 ? quoteWin32Arg(this.opts.claudeBin) : this.opts.claudeBin, IS_WIN32 ? args.map(quoteWin32Arg) : args, {
|
|
202
338
|
cwd: this.opts.workspaceDir,
|
|
203
339
|
env,
|
|
204
340
|
stdio: ["pipe", "pipe", "pipe"],
|
|
341
|
+
shell: IS_WIN32,
|
|
205
342
|
});
|
|
206
343
|
if (!proc.stdout || !proc.stderr || !proc.stdin) {
|
|
207
344
|
throw new Error("Claude subprocess did not provide stdio pipes");
|
|
@@ -211,13 +348,11 @@ export class ClaudeCodeAdapter {
|
|
|
211
348
|
stderrChunks.push(chunk.toString());
|
|
212
349
|
});
|
|
213
350
|
proc.stdin.on("error", () => {
|
|
214
|
-
// stdin pipe broken: kill the process so parser.next() sees EOF
|
|
215
|
-
// instead of hanging forever on a process that's alive but can't
|
|
216
|
-
// accept input. If the process was already exiting (the common
|
|
217
|
-
// EPIPE-on-close race), kill is a harmless no-op.
|
|
218
351
|
proc.stdin.destroy();
|
|
219
352
|
if (proc.exitCode === null && proc.signalCode === null) {
|
|
220
|
-
proc.
|
|
353
|
+
if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
|
|
354
|
+
proc.kill("SIGTERM");
|
|
355
|
+
}
|
|
221
356
|
}
|
|
222
357
|
});
|
|
223
358
|
const exitPromise = new Promise((resolve) => {
|
|
@@ -238,7 +373,9 @@ export class ClaudeCodeAdapter {
|
|
|
238
373
|
catch { /* best-effort */ }
|
|
239
374
|
if (state.handle.proc.exitCode === null && state.handle.proc.signalCode === null) {
|
|
240
375
|
try {
|
|
241
|
-
state.handle.proc.
|
|
376
|
+
if (!IS_WIN32 || !state.handle.proc.pid || !killWin32Tree(state.handle.proc.pid)) {
|
|
377
|
+
state.handle.proc.kill("SIGTERM");
|
|
378
|
+
}
|
|
242
379
|
}
|
|
243
380
|
catch { /* best-effort */ }
|
|
244
381
|
}
|
|
@@ -255,7 +392,6 @@ export class ClaudeCodeAdapter {
|
|
|
255
392
|
}
|
|
256
393
|
buildArgs(sessionKey) {
|
|
257
394
|
const args = [
|
|
258
|
-
"-p",
|
|
259
395
|
"--verbose",
|
|
260
396
|
"--input-format",
|
|
261
397
|
"stream-json",
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as os from "node:os";
|
|
3
|
-
import { ParallAgentGateway, createPlatformConfigManager, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
|
|
3
|
+
import { ParallAgentGateway, createPlatformConfigManager, createLogger, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
|
|
4
4
|
import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
|
|
5
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";
|
|
9
|
-
|
|
10
|
-
return {
|
|
11
|
-
info: (msg) => console.log(`[${prefix}] ${msg}`),
|
|
12
|
-
warn: (msg) => console.warn(`[${prefix}] ${msg}`),
|
|
13
|
-
error: (msg) => console.error(`[${prefix}] ${msg}`),
|
|
14
|
-
};
|
|
15
|
-
}
|
|
9
|
+
const log = createLogger("claude-agent");
|
|
16
10
|
async function getAgentMeWithLegacyFallback(client, orgId) {
|
|
17
11
|
try {
|
|
18
12
|
return await client.getAgentMe(orgId);
|
|
@@ -27,7 +21,6 @@ async function getAgentMeWithLegacyFallback(client, orgId) {
|
|
|
27
21
|
}
|
|
28
22
|
async function main() {
|
|
29
23
|
const config = resolveClaudeAgentConfig(process.env);
|
|
30
|
-
const log = createLogger("claude-agent");
|
|
31
24
|
if (config.allowApiKey &&
|
|
32
25
|
(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN)) {
|
|
33
26
|
// Surface the billing path: default is OAuth (Claude.ai subscription); this
|
|
@@ -154,6 +147,6 @@ async function main() {
|
|
|
154
147
|
}
|
|
155
148
|
}
|
|
156
149
|
main().catch((err) => {
|
|
157
|
-
|
|
150
|
+
log.error(`fatal: ${String(err)}`);
|
|
158
151
|
process.exitCode = 1;
|
|
159
152
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/claude-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "Claude Code bridge runtime for self-hosted Parall agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@parall/agent-core": "1.
|
|
29
|
-
"@parall/cli": "1.
|
|
30
|
-
"@parall/sdk": "1.
|
|
28
|
+
"@parall/agent-core": "1.29.0",
|
|
29
|
+
"@parall/cli": "1.29.0",
|
|
30
|
+
"@parall/sdk": "1.29.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.0.0",
|
package/src/dispatch.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import {
|
|
6
6
|
appendPreparedLocalAttachmentRefs,
|
|
7
7
|
pinLocalAttachmentPaths,
|
|
@@ -43,6 +43,20 @@ type ClaudeCodeAdapterOptions = Pick<
|
|
|
43
43
|
stepIdFilePathForSession?: (sessionKey: string) => string;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
const IS_WIN32 = process.platform === "win32";
|
|
47
|
+
|
|
48
|
+
function quoteWin32Arg(arg: string): string {
|
|
49
|
+
if (!/[\s"&|^<>()]/.test(arg)) return arg;
|
|
50
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function killWin32Tree(pid: number): boolean {
|
|
54
|
+
try {
|
|
55
|
+
execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: "ignore" });
|
|
56
|
+
return true;
|
|
57
|
+
} catch { return false; }
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
export function buildSpawnEnv(
|
|
47
61
|
parentEnv: NodeJS.ProcessEnv,
|
|
48
62
|
claudeHome: string,
|
|
@@ -85,10 +99,12 @@ type ProcessState = {
|
|
|
85
99
|
parser: AsyncGenerator<ClaudeParsedEvent>;
|
|
86
100
|
done: boolean;
|
|
87
101
|
needsRestart: boolean;
|
|
102
|
+
steerReadPending?: Promise<IteratorResult<ClaudeParsedEvent>>;
|
|
88
103
|
};
|
|
89
104
|
|
|
90
105
|
export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
91
106
|
private readonly processes = new Map<string, ProcessState>();
|
|
107
|
+
private readonly pendingInjections = new Map<string, number>();
|
|
92
108
|
private shuttingDown = false;
|
|
93
109
|
private _model: string | undefined;
|
|
94
110
|
private _effortLevel: string | undefined;
|
|
@@ -112,7 +128,35 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
112
128
|
}
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
enqueueDuringDispatch(sessionKey: string, body: string): boolean {
|
|
132
|
+
const state = this.processes.get(sessionKey);
|
|
133
|
+
if (!state || state.done) return false;
|
|
134
|
+
const { proc } = state.handle;
|
|
135
|
+
if (proc.exitCode !== null || proc.signalCode !== null || proc.stdin.destroyed) return false;
|
|
136
|
+
try {
|
|
137
|
+
this.writeUserMessage(state.handle, body);
|
|
138
|
+
this.pendingInjections.set(sessionKey, (this.pendingInjections.get(sessionKey) ?? 0) + 1);
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
hasPendingInjections(sessionKey: string): boolean {
|
|
146
|
+
return (this.pendingInjections.get(sessionKey) ?? 0) > 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
115
149
|
async *dispatch({ event, bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent> {
|
|
150
|
+
const pending = this.pendingInjections.get(sessionKey) ?? 0;
|
|
151
|
+
if (pending > 0) {
|
|
152
|
+
this.pendingInjections.delete(sessionKey);
|
|
153
|
+
context.log?.info?.(`claude-agent[${context.accountId}]: consuming ${pending} steer turn(s)`);
|
|
154
|
+
for (let i = 0; i < pending; i++) {
|
|
155
|
+
yield* this.consumeSteerTurn(sessionKey, context.log);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
116
160
|
let promptBody = bodyForAgent;
|
|
117
161
|
let releasePreparedAttachments = () => {};
|
|
118
162
|
try {
|
|
@@ -168,6 +212,72 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
168
212
|
await this.opts.sessionManager.shutdownAll();
|
|
169
213
|
}
|
|
170
214
|
|
|
215
|
+
private static readonly STEER_TURN_TIMEOUT_MS = 10_000;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Consume a steer turn whose message was already written to stdin via
|
|
219
|
+
* enqueueDuringDispatch. Skip the stdin write — only read parser output.
|
|
220
|
+
* If no output arrives within STEER_TURN_TIMEOUT_MS, the steer was
|
|
221
|
+
* incorporated into the previous turn and there is nothing to consume.
|
|
222
|
+
*
|
|
223
|
+
* On timeout, the losing parser.next() promise is saved to
|
|
224
|
+
* state.steerReadPending so the next runTurn can drain it instead of
|
|
225
|
+
* silently losing the first event of the subsequent turn.
|
|
226
|
+
*/
|
|
227
|
+
private async *consumeSteerTurn(
|
|
228
|
+
sessionKey: string,
|
|
229
|
+
log: GatewayLogger | undefined,
|
|
230
|
+
): AsyncGenerator<RuntimeEvent> {
|
|
231
|
+
const state = this.processes.get(sessionKey);
|
|
232
|
+
if (!state || state.done) {
|
|
233
|
+
throw new Error("claude-agent: process dead during steer consumption");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const groupKey = randomUUID();
|
|
237
|
+
|
|
238
|
+
const parserNext = state.parser.next();
|
|
239
|
+
const firstRead = await Promise.race([
|
|
240
|
+
parserNext.then((r) => ({ kind: "value" as const, result: r })),
|
|
241
|
+
new Promise<{ kind: "timeout" }>((resolve) =>
|
|
242
|
+
setTimeout(() => resolve({ kind: "timeout" }), ClaudeCodeAdapter.STEER_TURN_TIMEOUT_MS),
|
|
243
|
+
),
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
if (firstRead.kind === "timeout") {
|
|
247
|
+
state.steerReadPending = parserNext;
|
|
248
|
+
log?.info?.(`claude-agent: steer turn timeout — steer was incorporated into previous turn`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let next = firstRead.result;
|
|
253
|
+
// eslint-disable-next-line no-constant-condition
|
|
254
|
+
while (true) {
|
|
255
|
+
if (next.done) {
|
|
256
|
+
state.done = true;
|
|
257
|
+
this.processes.delete(sessionKey);
|
|
258
|
+
throw new Error("claude-agent: process exited while consuming steer turn");
|
|
259
|
+
}
|
|
260
|
+
const parsed = next.value;
|
|
261
|
+
|
|
262
|
+
if (parsed.type === "session_id") {
|
|
263
|
+
this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
|
|
264
|
+
yield { type: "runtime_session", runtimeSessionId: parsed.sessionId, runtimeLaneKey: sessionKey };
|
|
265
|
+
} else if (parsed.type === "turn_end") {
|
|
266
|
+
if (state.needsRestart) this.killProcess(sessionKey, state);
|
|
267
|
+
return;
|
|
268
|
+
} else if (parsed.type === "error") {
|
|
269
|
+
yield parsed;
|
|
270
|
+
} else if (parsed.type === "text") {
|
|
271
|
+
yield { ...parsed, project: false, groupKey };
|
|
272
|
+
} else if (parsed.type === "runtime_session") {
|
|
273
|
+
yield parsed;
|
|
274
|
+
} else {
|
|
275
|
+
yield { ...parsed, groupKey };
|
|
276
|
+
}
|
|
277
|
+
next = await state.parser.next();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
171
281
|
private async *runTurn(
|
|
172
282
|
sessionKey: string,
|
|
173
283
|
promptBody: string,
|
|
@@ -191,15 +301,42 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
191
301
|
return;
|
|
192
302
|
}
|
|
193
303
|
|
|
304
|
+
if (state.steerReadPending) {
|
|
305
|
+
const orphaned = await state.steerReadPending;
|
|
306
|
+
state.steerReadPending = undefined;
|
|
307
|
+
if (!orphaned.done) {
|
|
308
|
+
const parsed = orphaned.value;
|
|
309
|
+
if (parsed.type === "session_id") {
|
|
310
|
+
this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
|
|
311
|
+
yield { type: "runtime_session", runtimeSessionId: parsed.sessionId, runtimeLaneKey: sessionKey };
|
|
312
|
+
} else if (parsed.type === "turn_end") {
|
|
313
|
+
if (state.needsRestart) this.killProcess(sessionKey, state);
|
|
314
|
+
return;
|
|
315
|
+
} else if (parsed.type === "error") {
|
|
316
|
+
sawError = true;
|
|
317
|
+
yield parsed;
|
|
318
|
+
} else if (parsed.type === "text") {
|
|
319
|
+
yield { ...parsed, project: false, groupKey };
|
|
320
|
+
} else if (parsed.type === "runtime_session") {
|
|
321
|
+
yield parsed;
|
|
322
|
+
} else {
|
|
323
|
+
yield { ...parsed, groupKey };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
194
328
|
while (true) {
|
|
195
329
|
const next = await state.parser.next();
|
|
196
330
|
|
|
197
331
|
if (next.done) {
|
|
198
332
|
state.done = true;
|
|
199
333
|
this.processes.delete(sessionKey);
|
|
334
|
+
const detail = state.handle.stderrChunks.join("").trim();
|
|
335
|
+
const exit = await state.handle.exitPromise.catch(() => ({ code: null, signal: null } as const));
|
|
336
|
+
if (detail) {
|
|
337
|
+
log?.warn?.(`claude-agent: subprocess stderr: ${detail}`);
|
|
338
|
+
}
|
|
200
339
|
if (!sawError) {
|
|
201
|
-
const detail = state.handle.stderrChunks.join("").trim();
|
|
202
|
-
const exit = await state.handle.exitPromise.catch(() => ({ code: null, signal: null } as const));
|
|
203
340
|
yield {
|
|
204
341
|
type: "error",
|
|
205
342
|
message: detail
|
|
@@ -286,10 +423,13 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
286
423
|
`claude-agent: spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || "default"}, effort=${this._effortLevel || "default"}, mode=${this.opts.permissionMode})`,
|
|
287
424
|
);
|
|
288
425
|
|
|
289
|
-
const proc = spawn(
|
|
426
|
+
const proc = spawn(
|
|
427
|
+
IS_WIN32 ? quoteWin32Arg(this.opts.claudeBin) : this.opts.claudeBin,
|
|
428
|
+
IS_WIN32 ? args.map(quoteWin32Arg) : args, {
|
|
290
429
|
cwd: this.opts.workspaceDir,
|
|
291
430
|
env,
|
|
292
431
|
stdio: ["pipe", "pipe", "pipe"],
|
|
432
|
+
shell: IS_WIN32,
|
|
293
433
|
});
|
|
294
434
|
|
|
295
435
|
if (!proc.stdout || !proc.stderr || !proc.stdin) {
|
|
@@ -301,13 +441,11 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
301
441
|
stderrChunks.push(chunk.toString());
|
|
302
442
|
});
|
|
303
443
|
proc.stdin.on("error", () => {
|
|
304
|
-
// stdin pipe broken: kill the process so parser.next() sees EOF
|
|
305
|
-
// instead of hanging forever on a process that's alive but can't
|
|
306
|
-
// accept input. If the process was already exiting (the common
|
|
307
|
-
// EPIPE-on-close race), kill is a harmless no-op.
|
|
308
444
|
proc.stdin.destroy();
|
|
309
445
|
if (proc.exitCode === null && proc.signalCode === null) {
|
|
310
|
-
proc.
|
|
446
|
+
if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
|
|
447
|
+
proc.kill("SIGTERM");
|
|
448
|
+
}
|
|
311
449
|
}
|
|
312
450
|
});
|
|
313
451
|
|
|
@@ -330,7 +468,9 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
330
468
|
} catch { /* best-effort */ }
|
|
331
469
|
if (state.handle.proc.exitCode === null && state.handle.proc.signalCode === null) {
|
|
332
470
|
try {
|
|
333
|
-
state.handle.proc.
|
|
471
|
+
if (!IS_WIN32 || !state.handle.proc.pid || !killWin32Tree(state.handle.proc.pid)) {
|
|
472
|
+
state.handle.proc.kill("SIGTERM");
|
|
473
|
+
}
|
|
334
474
|
} catch { /* best-effort */ }
|
|
335
475
|
}
|
|
336
476
|
}
|
|
@@ -348,7 +488,6 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
348
488
|
|
|
349
489
|
private buildArgs(sessionKey: string): string[] {
|
|
350
490
|
const args = [
|
|
351
|
-
"-p",
|
|
352
491
|
"--verbose",
|
|
353
492
|
"--input-format",
|
|
354
493
|
"stream-json",
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
|
-
import { ParallAgentGateway, createPlatformConfigManager, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
|
|
4
|
+
import { ParallAgentGateway, createPlatformConfigManager, createLogger, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
|
|
5
5
|
import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
|
|
6
6
|
import {
|
|
7
7
|
buildClaudeRuntimeKey,
|
|
@@ -15,13 +15,7 @@ import { ClaudeCodeAdapter } from "./dispatch.js";
|
|
|
15
15
|
import { ClaudeSessionManager } from "./session-manager.js";
|
|
16
16
|
import { ensureClaudeWorkspace } from "./workspace.js";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
info: (msg: string) => console.log(`[${prefix}] ${msg}`),
|
|
21
|
-
warn: (msg: string) => console.warn(`[${prefix}] ${msg}`),
|
|
22
|
-
error: (msg: string) => console.error(`[${prefix}] ${msg}`),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
18
|
+
const log = createLogger("claude-agent");
|
|
25
19
|
|
|
26
20
|
async function getAgentMeWithLegacyFallback(client: ParallClient, orgId: string) {
|
|
27
21
|
try {
|
|
@@ -37,7 +31,6 @@ async function getAgentMeWithLegacyFallback(client: ParallClient, orgId: string)
|
|
|
37
31
|
|
|
38
32
|
async function main() {
|
|
39
33
|
const config = resolveClaudeAgentConfig(process.env);
|
|
40
|
-
const log = createLogger("claude-agent");
|
|
41
34
|
|
|
42
35
|
if (
|
|
43
36
|
config.allowApiKey &&
|
|
@@ -179,6 +172,6 @@ async function main() {
|
|
|
179
172
|
}
|
|
180
173
|
|
|
181
174
|
main().catch((err) => {
|
|
182
|
-
|
|
175
|
+
log.error(`fatal: ${String(err)}`);
|
|
183
176
|
process.exitCode = 1;
|
|
184
177
|
});
|