@parall/claude-agent 1.30.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,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"}
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,CAgChG;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,EACtB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAMR;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
@@ -1,5 +1,5 @@
1
- import * as os from "node:os";
2
- import * as path from "node:path";
1
+ import * as os from 'node:os';
2
+ import * as path from 'node:path';
3
3
  function requireEnv(env, name) {
4
4
  const value = env[name]?.trim();
5
5
  if (!value) {
@@ -19,12 +19,12 @@ function resolvePath(value) {
19
19
  return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
20
20
  }
21
21
  export function resolveClaudeAgentConfig(env = process.env) {
22
- const apiUrl = requireEnv(env, "PRLL_API_URL");
23
- const apiKey = requireEnv(env, "PRLL_API_KEY");
24
- const orgId = requireEnv(env, "PRLL_ORG_ID");
22
+ const apiUrl = requireEnv(env, 'PRLL_API_URL');
23
+ const apiKey = requireEnv(env, 'PRLL_API_KEY');
24
+ const orgId = requireEnv(env, 'PRLL_ORG_ID');
25
25
  const claudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os.homedir());
26
- const stateDir = resolvePath(env.PRLL_CLAUDE_STATE_DIR?.trim() || path.join(claudeHome, ".parall-agent"));
27
- const workspaceDir = resolvePath(env.PRLL_CLAUDE_WORKSPACE_DIR?.trim() || path.join(stateDir, "workspace"));
26
+ const stateDir = resolvePath(env.PRLL_STATE_DIR?.trim() || path.join(claudeHome, '.parall-agent'));
27
+ const workspaceDir = resolvePath(env.PRLL_WORKSPACE_DIR?.trim() || path.join(stateDir, 'workspace'));
28
28
  const additionalDirs = parseList(env.PRLL_CLAUDE_ADD_DIRS).map(resolvePath);
29
29
  return {
30
30
  apiUrl,
@@ -32,41 +32,41 @@ export function resolveClaudeAgentConfig(env = process.env) {
32
32
  orgId,
33
33
  wsUrl: env.PRLL_WS_URL?.trim() || undefined,
34
34
  swimlaneName: env.PRLL_SWIMLANE_NAME?.trim() || undefined,
35
- claudeBin: env.PRLL_CLAUDE_BIN?.trim() || "claude",
35
+ claudeBin: env.PRLL_CLAUDE_BIN?.trim() || 'claude',
36
36
  claudeHome,
37
37
  stateDir,
38
38
  workspaceDir,
39
39
  model: env.PRLL_CLAUDE_MODEL?.trim() || undefined,
40
- permissionMode: env.PRLL_CLAUDE_PERMISSION_MODE?.trim() || "bypassPermissions",
40
+ permissionMode: env.PRLL_CLAUDE_PERMISSION_MODE?.trim() || 'bypassPermissions',
41
41
  allowedTools: parseList(env.PRLL_CLAUDE_ALLOWED_TOOLS),
42
42
  disallowedTools: parseList(env.PRLL_CLAUDE_DISALLOWED_TOOLS),
43
43
  additionalDirs,
44
44
  appendSystemPrompt: env.PRLL_CLAUDE_APPEND_SYSTEM_PROMPT?.trim() || undefined,
45
45
  runtimeKey: env.PRLL_CLAUDE_RUNTIME_KEY?.trim() || undefined,
46
- allowApiKey: env.PRLL_CLAUDE_ALLOW_API_KEY?.trim() === "1",
46
+ allowApiKey: env.PRLL_CLAUDE_ALLOW_API_KEY?.trim() === '1',
47
47
  };
48
48
  }
49
49
  export function resolveWsUrl(apiUrl, explicitWsUrl, swimlaneName) {
50
- const base = explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
50
+ const base = explicitWsUrl || `${apiUrl.replace(/\/$/, '').replace(/^http/, 'ws')}/ws`;
51
51
  if (!swimlaneName)
52
52
  return base;
53
53
  const url = new URL(base);
54
- url.searchParams.set("swimlane", swimlaneName);
54
+ url.searchParams.set('swimlane', swimlaneName);
55
55
  return url.toString();
56
56
  }
57
57
  export function buildClaudeRuntimeKey(agentUserId) {
58
58
  return `agent:main:claude:${agentUserId}:orchestrator`;
59
59
  }
60
60
  export function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
61
- const fileName = Buffer.from(runtimeKey).toString("base64url");
62
- return path.join(stateDir, "sessions", `${fileName}.json`);
61
+ const fileName = Buffer.from(runtimeKey).toString('base64url');
62
+ return path.join(stateDir, 'sessions', `${fileName}.json`);
63
63
  }
64
64
  export function contextFilePathForSession(stateDir, sessionKey) {
65
- const fileName = Buffer.from(sessionKey).toString("base64url");
66
- return path.join(stateDir, "dispatch-context", `${fileName}.json`);
65
+ const fileName = Buffer.from(sessionKey).toString('base64url');
66
+ return path.join(stateDir, 'dispatch-context', `${fileName}.json`);
67
67
  }
68
68
  /** @deprecated Use contextFilePathForSession. */
69
69
  export function stepIdFilePathForSession(stateDir, sessionKey) {
70
- const fileName = Buffer.from(sessionKey).toString("base64url");
71
- return path.join(stateDir, "step-ids", `${fileName}.txt`);
70
+ const fileName = Buffer.from(sessionKey).toString('base64url');
71
+ return path.join(stateDir, 'step-ids', `${fileName}.txt`);
72
72
  }
@@ -1,7 +1,7 @@
1
- import type { CleanupForkOpts, DispatchAdapter, DispatchOpts, ForkOpts, RuntimeEvent } from "@parall/agent-core";
2
- import type { ClaudeAgentConfig } from "./config.js";
3
- import { ClaudeSessionManager } from "./session-manager.js";
4
- type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, "additionalDirs" | "allowApiKey" | "allowedTools" | "appendSystemPrompt" | "claudeBin" | "claudeHome" | "disallowedTools" | "model" | "permissionMode" | "workspaceDir"> & {
1
+ import type { CleanupForkOpts, DispatchAdapter, DispatchOpts, ForkOpts, RuntimeEvent } from '@parall/agent-core';
2
+ import type { ClaudeAgentConfig } from './config.js';
3
+ import { ClaudeSessionManager } from './session-manager.js';
4
+ type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, 'additionalDirs' | 'allowApiKey' | 'allowedTools' | 'appendSystemPrompt' | 'claudeBin' | 'claudeHome' | 'disallowedTools' | 'model' | 'permissionMode' | 'workspaceDir'> & {
5
5
  sessionManager: ClaudeSessionManager;
6
6
  apiUrl: string;
7
7
  apiKey: string;
@@ -10,7 +10,7 @@ type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, "additionalDirs" | "allo
10
10
  /** @deprecated Use contextFilePathForSession. */
11
11
  stepIdFilePathForSession?: (sessionKey: string) => string;
12
12
  };
13
- export declare function buildSpawnEnv(parentEnv: NodeJS.ProcessEnv, claudeHome: string, context: DispatchOpts["context"], opts: {
13
+ export declare function buildSpawnEnv(parentEnv: NodeJS.ProcessEnv, claudeHome: string, context: DispatchOpts['context'], opts: {
14
14
  allowApiKey: boolean;
15
15
  effortLevel?: string;
16
16
  }): NodeJS.ProcessEnv;
@@ -29,14 +29,16 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
29
29
  effort?: string | null;
30
30
  }): void;
31
31
  enqueueDuringDispatch(sessionKey: string, body: string): boolean;
32
+ abortDispatch(sessionKey: string): void;
32
33
  hasPendingInjections(sessionKey: string): boolean;
33
- dispatch({ event, bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent>;
34
+ dispatch({ event, bodyForAgent, sessionKey, context, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
34
35
  getBranchPoint(_sessionKey: string): string | undefined;
35
36
  getSessionHistoryPath(sessionKey: string): string | undefined;
36
37
  forkSession({ sessionKey }: ForkOpts): import("@parall/agent-core").ForkSessionHandle | null;
37
38
  cleanupFork({ fork }: CleanupForkOpts): void;
39
+ resetProcesses(): void;
38
40
  shutdown(): Promise<void>;
39
- private static readonly STEER_TURN_TIMEOUT_MS;
41
+ private readonly steerTurnTimeoutMs;
40
42
  /**
41
43
  * Consume a steer turn whose message was already written to stdin via
42
44
  * enqueueDuringDispatch. Skip the stdin write — only read parser output.
@@ -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;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"}
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,EAAE,oBAAoB,EAA4B,MAAM,sBAAsB,CAAC;AAEtF,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;AAkBF,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,CAErC;IACD,IAAI,aAAa,IAAI,MAAM,GAAG,SAAS,CAEtC;IAED,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,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAYvC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,GACR,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA+B7C,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIvD,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAc7D,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE,QAAQ;IAIpC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAQrC,cAAc,IAAI,IAAI;IAQhB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ/B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA4D;IAE/F;;;;;;;;;OASG;YACY,gBAAgB;YAqEhB,OAAO;IAkHtB,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,YAAY;IAmDpB,OAAO,CAAC,WAAW;IAsBnB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,uBAAuB;CAkBhC"}
package/dist/dispatch.js CHANGED
@@ -1,10 +1,10 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { randomUUID } from "node:crypto";
4
- import { execSync, spawn } from "node:child_process";
5
- import { appendPreparedLocalAttachmentRefs, pinLocalAttachmentPaths, } from "@parall/agent-core/internal/attachment-input";
6
- import { parseClaudeStreamJson } from "./output-parser.js";
7
- const IS_WIN32 = process.platform === "win32";
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { execSync, spawn } from 'node:child_process';
5
+ import { appendPreparedLocalAttachmentRefs, pinLocalAttachmentPaths, } from '@parall/agent-core/internal/attachment-input';
6
+ import { parseClaudeStreamJson } from './output-parser.js';
7
+ const IS_WIN32 = process.platform === 'win32';
8
8
  function quoteWin32Arg(arg) {
9
9
  if (!/[\s"&|^<>()]/.test(arg))
10
10
  return arg;
@@ -12,7 +12,7 @@ function quoteWin32Arg(arg) {
12
12
  }
13
13
  function killWin32Tree(pid) {
14
14
  try {
15
- execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: "ignore" });
15
+ execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: 'ignore' });
16
16
  return true;
17
17
  }
18
18
  catch {
@@ -31,12 +31,12 @@ export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
31
31
  PRLL_API_URL: context.apiUrl,
32
32
  PRLL_API_KEY: context.apiKey,
33
33
  PRLL_ORG_ID: context.orgId,
34
- PRLL_SESSION_ID: context.sessionId ?? "",
35
- PRLL_CHAT_ID: context.chatId ?? "",
36
- PRLL_TRIGGER_MESSAGE_ID: context.triggerMessageId ?? "",
37
- PRLL_NO_REPLY: context.noReply ? "1" : "",
38
- PRLL_CONTEXT_FILE: context.contextFilePath ?? "",
39
- PRLL_STEP_ID_FILE: context.stepIdFilePath ?? "",
34
+ PRLL_SESSION_ID: context.sessionId ?? '',
35
+ PRLL_CHAT_ID: context.chatId ?? '',
36
+ PRLL_TRIGGER_MESSAGE_ID: context.triggerMessageId ?? '',
37
+ PRLL_NO_REPLY: context.noReply ? '1' : '',
38
+ PRLL_CONTEXT_FILE: context.contextFilePath ?? '',
39
+ PRLL_STEP_ID_FILE: context.stepIdFilePath ?? '',
40
40
  };
41
41
  if (opts.effortLevel) {
42
42
  result.CLAUDE_CODE_EFFORT_LEVEL = opts.effortLevel;
@@ -54,8 +54,12 @@ export class ClaudeCodeAdapter {
54
54
  this.opts = opts;
55
55
  this._model = opts.model;
56
56
  }
57
- get currentModel() { return this._model; }
58
- get currentEffort() { return this._effortLevel; }
57
+ get currentModel() {
58
+ return this._model;
59
+ }
60
+ get currentEffort() {
61
+ return this._effortLevel;
62
+ }
59
63
  updateConfig(config) {
60
64
  const modelChanged = config.model !== undefined && config.model !== this._model;
61
65
  const effortChanged = config.effort !== undefined && config.effort !== this._effortLevel;
@@ -85,10 +89,23 @@ export class ClaudeCodeAdapter {
85
89
  return false;
86
90
  }
87
91
  }
92
+ abortDispatch(sessionKey) {
93
+ this.pendingInjections.delete(sessionKey);
94
+ const state = this.processes.get(sessionKey);
95
+ if (!state || state.done)
96
+ return;
97
+ state.done = true;
98
+ try {
99
+ state.handle.proc.stdin.end();
100
+ }
101
+ catch {
102
+ /* best-effort */
103
+ }
104
+ }
88
105
  hasPendingInjections(sessionKey) {
89
106
  return (this.pendingInjections.get(sessionKey) ?? 0) > 0;
90
107
  }
91
- async *dispatch({ event, bodyForAgent, sessionKey, context }) {
108
+ async *dispatch({ event, bodyForAgent, sessionKey, context, }) {
92
109
  const pending = this.pendingInjections.get(sessionKey) ?? 0;
93
110
  if (pending > 0) {
94
111
  this.pendingInjections.delete(sessionKey);
@@ -125,8 +142,8 @@ export class ClaudeCodeAdapter {
125
142
  const sessionId = this.opts.sessionManager.getSessionId(sessionKey);
126
143
  if (!sessionId)
127
144
  return undefined;
128
- const projectSlug = this.opts.workspaceDir.replace(/[/.]/g, "-");
129
- const filePath = path.join(this.opts.claudeHome, ".claude", "projects", projectSlug, `${sessionId}.jsonl`);
145
+ const projectSlug = this.opts.workspaceDir.replace(/[/.]/g, '-');
146
+ const filePath = path.join(this.opts.claudeHome, '.claude', 'projects', projectSlug, `${sessionId}.jsonl`);
130
147
  return fs.existsSync(filePath) ? filePath : undefined;
131
148
  }
132
149
  forkSession({ sessionKey }) {
@@ -139,15 +156,21 @@ export class ClaudeCodeAdapter {
139
156
  }
140
157
  this.opts.sessionManager.cleanupFork(fork.sessionKey);
141
158
  }
142
- async shutdown() {
143
- this.shuttingDown = true;
159
+ resetProcesses() {
144
160
  for (const [sessionKey, state] of this.processes) {
145
161
  this.killProcess(sessionKey, state);
146
162
  }
147
163
  this.processes.clear();
164
+ this.pendingInjections.clear();
165
+ }
166
+ async shutdown() {
167
+ this.shuttingDown = true;
168
+ this.resetProcesses();
148
169
  await this.opts.sessionManager.shutdownAll();
149
170
  }
150
- static STEER_TURN_TIMEOUT_MS = 10_000;
171
+ // Steer-turn read timeout. Overridable via PRLL_STEER_TURN_TIMEOUT_MS (ms),
172
+ // chiefly so tests can exercise the timeout path without a 10s wait.
173
+ steerTurnTimeoutMs = Number(process.env.PRLL_STEER_TURN_TIMEOUT_MS) || 10_000;
151
174
  /**
152
175
  * Consume a steer turn whose message was already written to stdin via
153
176
  * enqueueDuringDispatch. Skip the stdin write — only read parser output.
@@ -161,15 +184,26 @@ export class ClaudeCodeAdapter {
161
184
  async *consumeSteerTurn(sessionKey, log) {
162
185
  const state = this.processes.get(sessionKey);
163
186
  if (!state || state.done) {
164
- throw new Error("process dead during steer consumption");
187
+ throw new Error('process dead during steer consumption');
165
188
  }
166
189
  const groupKey = randomUUID();
167
- const parserNext = state.parser.next();
190
+ // Reuse a pull already issued by a prior steer timeout instead of issuing a
191
+ // fresh one. A burst of N steers all takes the pending-skip path, which calls
192
+ // this method N times in a loop. Issuing a new state.parser.next() on each
193
+ // call while a previous pull is still in flight queues multiple reads on the
194
+ // same async generator, but only one can be stashed in the single-slot
195
+ // state.steerReadPending — every earlier pull is then consumed-and-dropped,
196
+ // silently losing parser events. When a dropped event is a tool_call, its
197
+ // tool_result later orphans into "call_id does not match any tool_call".
198
+ // Threading the single in-flight pull through every call keeps at most one
199
+ // outstanding read and loses nothing.
200
+ const parserNext = state.steerReadPending ?? state.parser.next();
201
+ state.steerReadPending = undefined;
168
202
  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)),
203
+ parserNext.then((r) => ({ kind: 'value', result: r })),
204
+ new Promise((resolve) => setTimeout(() => resolve({ kind: 'timeout' }), this.steerTurnTimeoutMs)),
171
205
  ]);
172
- if (firstRead.kind === "timeout") {
206
+ if (firstRead.kind === 'timeout') {
173
207
  state.steerReadPending = parserNext;
174
208
  log?.info?.(`steer turn timeout — steer was incorporated into previous turn`);
175
209
  return;
@@ -180,25 +214,29 @@ export class ClaudeCodeAdapter {
180
214
  if (next.done) {
181
215
  state.done = true;
182
216
  this.processes.delete(sessionKey);
183
- throw new Error("process exited while consuming steer turn");
217
+ throw new Error('process exited while consuming steer turn');
184
218
  }
185
219
  const parsed = next.value;
186
- if (parsed.type === "session_id") {
220
+ if (parsed.type === 'session_id') {
187
221
  this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
188
- yield { type: "runtime_session", runtimeSessionId: parsed.sessionId, runtimeLaneKey: sessionKey };
222
+ yield {
223
+ type: 'runtime_session',
224
+ runtimeSessionId: parsed.sessionId,
225
+ runtimeLaneKey: sessionKey,
226
+ };
189
227
  }
190
- else if (parsed.type === "turn_end") {
228
+ else if (parsed.type === 'turn_end') {
191
229
  if (state.needsRestart)
192
230
  this.killProcess(sessionKey, state);
193
231
  return;
194
232
  }
195
- else if (parsed.type === "error") {
233
+ else if (parsed.type === 'error') {
196
234
  yield parsed;
197
235
  }
198
- else if (parsed.type === "text") {
236
+ else if (parsed.type === 'text') {
199
237
  yield { ...parsed, project: false, groupKey };
200
238
  }
201
- else if (parsed.type === "runtime_session") {
239
+ else if (parsed.type === 'runtime_session') {
202
240
  yield parsed;
203
241
  }
204
242
  else {
@@ -213,7 +251,7 @@ export class ClaudeCodeAdapter {
213
251
  state = this.ensureProcess(sessionKey, log);
214
252
  }
215
253
  catch (err) {
216
- yield { type: "error", message: `Claude spawn failed: ${String(err)}` };
254
+ yield { type: 'error', message: `Claude spawn failed: ${String(err)}` };
217
255
  return;
218
256
  }
219
257
  const groupKey = randomUUID();
@@ -222,7 +260,7 @@ export class ClaudeCodeAdapter {
222
260
  this.writeUserMessage(state.handle, promptBody);
223
261
  }
224
262
  catch (err) {
225
- yield { type: "error", message: `Claude stdin write failed: ${String(err)}` };
263
+ yield { type: 'error', message: `Claude stdin write failed: ${String(err)}` };
226
264
  this.killProcess(sessionKey, state);
227
265
  return;
228
266
  }
@@ -231,23 +269,27 @@ export class ClaudeCodeAdapter {
231
269
  state.steerReadPending = undefined;
232
270
  if (!orphaned.done) {
233
271
  const parsed = orphaned.value;
234
- if (parsed.type === "session_id") {
272
+ if (parsed.type === 'session_id') {
235
273
  this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
236
- yield { type: "runtime_session", runtimeSessionId: parsed.sessionId, runtimeLaneKey: sessionKey };
274
+ yield {
275
+ type: 'runtime_session',
276
+ runtimeSessionId: parsed.sessionId,
277
+ runtimeLaneKey: sessionKey,
278
+ };
237
279
  }
238
- else if (parsed.type === "turn_end") {
280
+ else if (parsed.type === 'turn_end') {
239
281
  if (state.needsRestart)
240
282
  this.killProcess(sessionKey, state);
241
283
  return;
242
284
  }
243
- else if (parsed.type === "error") {
285
+ else if (parsed.type === 'error') {
244
286
  sawError = true;
245
287
  yield parsed;
246
288
  }
247
- else if (parsed.type === "text") {
289
+ else if (parsed.type === 'text') {
248
290
  yield { ...parsed, project: false, groupKey };
249
291
  }
250
- else if (parsed.type === "runtime_session") {
292
+ else if (parsed.type === 'runtime_session') {
251
293
  yield parsed;
252
294
  }
253
295
  else {
@@ -260,46 +302,46 @@ export class ClaudeCodeAdapter {
260
302
  if (next.done) {
261
303
  state.done = true;
262
304
  this.processes.delete(sessionKey);
263
- const detail = state.handle.stderrChunks.join("").trim();
305
+ const detail = state.handle.stderrChunks.join('').trim();
264
306
  const exit = await state.handle.exitPromise.catch(() => ({ code: null, signal: null }));
265
307
  if (detail) {
266
308
  log?.warn?.(`subprocess stderr: ${detail}`);
267
309
  }
268
310
  if (!sawError) {
269
311
  yield {
270
- type: "error",
271
- message: detail
272
- || `Claude exited with code ${exit.code ?? "unknown"}${exit.signal ? ` (${exit.signal})` : ""}`,
312
+ type: 'error',
313
+ message: detail ||
314
+ `Claude exited with code ${exit.code ?? 'unknown'}${exit.signal ? ` (${exit.signal})` : ''}`,
273
315
  };
274
316
  }
275
317
  return;
276
318
  }
277
319
  const parsed = next.value;
278
- if (parsed.type === "session_id") {
320
+ if (parsed.type === 'session_id') {
279
321
  this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
280
322
  yield {
281
- type: "runtime_session",
323
+ type: 'runtime_session',
282
324
  runtimeSessionId: parsed.sessionId,
283
325
  runtimeLaneKey: sessionKey,
284
326
  };
285
327
  continue;
286
328
  }
287
- if (parsed.type === "turn_end") {
329
+ if (parsed.type === 'turn_end') {
288
330
  if (state.needsRestart) {
289
331
  this.killProcess(sessionKey, state);
290
332
  }
291
333
  return;
292
334
  }
293
- if (parsed.type === "error") {
335
+ if (parsed.type === 'error') {
294
336
  sawError = true;
295
337
  yield parsed;
296
338
  continue;
297
339
  }
298
- if (parsed.type === "text") {
340
+ if (parsed.type === 'text') {
299
341
  yield { ...parsed, project: false, groupKey };
300
342
  continue;
301
343
  }
302
- if (parsed.type === "runtime_session") {
344
+ if (parsed.type === 'runtime_session') {
303
345
  yield parsed;
304
346
  continue;
305
347
  }
@@ -308,7 +350,7 @@ export class ClaudeCodeAdapter {
308
350
  }
309
351
  ensureProcess(sessionKey, log) {
310
352
  if (this.shuttingDown) {
311
- throw new Error("adapter shutting down, refusing new process");
353
+ throw new Error('adapter shutting down, refusing new process');
312
354
  }
313
355
  const existing = this.processes.get(sessionKey);
314
356
  if (existing && !existing.done) {
@@ -333,31 +375,31 @@ export class ClaudeCodeAdapter {
333
375
  spawnProcess(sessionKey, log) {
334
376
  const args = this.buildArgs(sessionKey);
335
377
  const env = buildSpawnEnv(process.env, this.opts.claudeHome, this.buildPlaceholderContext(sessionKey), { allowApiKey: this.opts.allowApiKey, effortLevel: this._effortLevel });
336
- log?.info(`spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || "default"}, effort=${this._effortLevel || "default"}, mode=${this.opts.permissionMode})`);
378
+ log?.info(`spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || 'default'}, effort=${this._effortLevel || 'default'}, mode=${this.opts.permissionMode})`);
337
379
  const proc = spawn(IS_WIN32 ? quoteWin32Arg(this.opts.claudeBin) : this.opts.claudeBin, IS_WIN32 ? args.map(quoteWin32Arg) : args, {
338
380
  cwd: this.opts.workspaceDir,
339
381
  env,
340
- stdio: ["pipe", "pipe", "pipe"],
382
+ stdio: ['pipe', 'pipe', 'pipe'],
341
383
  shell: IS_WIN32,
342
384
  });
343
385
  if (!proc.stdout || !proc.stderr || !proc.stdin) {
344
- throw new Error("Claude subprocess did not provide stdio pipes");
386
+ throw new Error('Claude subprocess did not provide stdio pipes');
345
387
  }
346
388
  const stderrChunks = [];
347
- proc.stderr.on("data", (chunk) => {
389
+ proc.stderr.on('data', (chunk) => {
348
390
  stderrChunks.push(chunk.toString());
349
391
  });
350
- proc.stdin.on("error", () => {
392
+ proc.stdin.on('error', () => {
351
393
  proc.stdin.destroy();
352
394
  if (proc.exitCode === null && proc.signalCode === null) {
353
395
  if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
354
- proc.kill("SIGTERM");
396
+ proc.kill('SIGTERM');
355
397
  }
356
398
  }
357
399
  });
358
400
  const exitPromise = new Promise((resolve) => {
359
- proc.once("close", (code, signal) => resolve({ code, signal }));
360
- proc.once("error", () => resolve({ code: null, signal: null }));
401
+ proc.once('close', (code, signal) => resolve({ code, signal }));
402
+ proc.once('error', () => resolve({ code: null, signal: null }));
361
403
  });
362
404
  return { proc, exitPromise, stderrChunks };
363
405
  }
@@ -370,70 +412,74 @@ export class ClaudeCodeAdapter {
370
412
  try {
371
413
  state.handle.proc.stdin.end();
372
414
  }
373
- catch { /* best-effort */ }
415
+ catch {
416
+ /* best-effort */
417
+ }
374
418
  if (state.handle.proc.exitCode === null && state.handle.proc.signalCode === null) {
375
419
  try {
376
420
  if (!IS_WIN32 || !state.handle.proc.pid || !killWin32Tree(state.handle.proc.pid)) {
377
- state.handle.proc.kill("SIGTERM");
421
+ state.handle.proc.kill('SIGTERM');
378
422
  }
379
423
  }
380
- catch { /* best-effort */ }
424
+ catch {
425
+ /* best-effort */
426
+ }
381
427
  }
382
428
  }
383
429
  writeUserMessage(handle, text) {
384
430
  const payload = JSON.stringify({
385
- type: "user",
431
+ type: 'user',
386
432
  message: {
387
- role: "user",
388
- content: [{ type: "text", text }],
433
+ role: 'user',
434
+ content: [{ type: 'text', text }],
389
435
  },
390
436
  });
391
437
  handle.proc.stdin.write(`${payload}\n`);
392
438
  }
393
439
  buildArgs(sessionKey) {
394
440
  const args = [
395
- "--verbose",
396
- "--input-format",
397
- "stream-json",
398
- "--output-format",
399
- "stream-json",
400
- "--permission-mode",
441
+ '--verbose',
442
+ '--input-format',
443
+ 'stream-json',
444
+ '--output-format',
445
+ 'stream-json',
446
+ '--permission-mode',
401
447
  this.opts.permissionMode,
402
448
  ];
403
449
  if (this._model) {
404
- args.push("--model", this._model);
450
+ args.push('--model', this._model);
405
451
  }
406
452
  if (this.opts.allowedTools.length > 0) {
407
- args.push("--allowedTools", this.opts.allowedTools.join(","));
453
+ args.push('--allowedTools', this.opts.allowedTools.join(','));
408
454
  }
409
455
  if (this.opts.disallowedTools.length > 0) {
410
- args.push("--disallowedTools", this.opts.disallowedTools.join(","));
456
+ args.push('--disallowedTools', this.opts.disallowedTools.join(','));
411
457
  }
412
- args.push("--append-system-prompt-file", path.join(this.opts.workspaceDir, ".parall", "system-prompt.md"));
458
+ args.push('--append-system-prompt-file', path.join(this.opts.workspaceDir, '.parall', 'system-prompt.md'));
413
459
  if (this.opts.appendSystemPrompt) {
414
- args.push("--append-system-prompt", this.opts.appendSystemPrompt);
460
+ args.push('--append-system-prompt', this.opts.appendSystemPrompt);
415
461
  }
416
462
  if (this.opts.additionalDirs.length > 0) {
417
- args.push("--add-dir", ...this.opts.additionalDirs);
463
+ args.push('--add-dir', ...this.opts.additionalDirs);
418
464
  }
419
465
  args.push(...this.opts.sessionManager.getResumeArgs(sessionKey));
420
466
  return args;
421
467
  }
422
468
  buildPlaceholderContext(sessionKey) {
423
469
  return {
424
- accountId: "",
470
+ accountId: '',
425
471
  apiUrl: this.opts.apiUrl,
426
472
  apiKey: this.opts.apiKey,
427
473
  orgId: this.opts.orgId,
428
- agentUserId: "",
429
- runtimeType: "",
430
- runtimeKey: "",
431
- sessionId: "",
432
- chatId: "",
433
- triggerMessageId: "",
474
+ agentUserId: '',
475
+ runtimeType: '',
476
+ runtimeKey: '',
477
+ sessionId: '',
478
+ chatId: '',
479
+ triggerMessageId: '',
434
480
  noReply: false,
435
- contextFilePath: this.opts.contextFilePathForSession?.(sessionKey) ?? "",
436
- stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey) ?? "",
481
+ contextFilePath: this.opts.contextFilePathForSession?.(sessionKey) ?? '',
482
+ stepIdFilePath: this.opts.stepIdFilePathForSession?.(sessionKey) ?? '',
437
483
  client: undefined,
438
484
  };
439
485
  }