@parall/claude-agent 1.57.2 → 1.58.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,17 @@
1
1
  import type { CleanupForkOpts, DispatchAdapter, DispatchInputLifecycle, DispatchOpts, ForkOpts, RuntimeEvent } from '@parall/agent-core';
2
2
  import type { ClaudeAgentConfig } from './config.js';
3
3
  import type { ClaudeSessionManager } from './session-manager.js';
4
+ import { buildSpawnEnv } from './spawn-env.js';
5
+ export { buildSpawnEnv };
4
6
  type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, 'additionalDirs' | 'allowApiKey' | 'allowedTools' | 'appendSystemPrompt' | 'claudeBin' | 'claudeHome' | 'disallowedTools' | 'model' | 'permissionMode' | 'workspaceDir'> & {
5
7
  sessionManager: ClaudeSessionManager;
6
8
  apiUrl: string;
7
9
  apiKey: string;
8
10
  orgId: string;
11
+ /** Catalog context size for the selected model. */
12
+ contextWindow?: number;
13
+ /** Catalog max output tokens for the selected model (input-budget subtrahend). */
14
+ maxTokens?: number;
9
15
  contextFilePathForSession?: (sessionKey: string) => string;
10
16
  /** @deprecated Use contextFilePathForSession. */
11
17
  stepIdFilePathForSession?: (sessionKey: string) => string;
@@ -27,12 +33,6 @@ type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, 'additionalDirs' | 'allo
27
33
  */
28
34
  capabilityBinDir?: string;
29
35
  };
30
- export declare function buildSpawnEnv(parentEnv: NodeJS.ProcessEnv, claudeHome: string, context: DispatchOpts['context'], opts: {
31
- allowApiKey: boolean;
32
- effortLevel?: string;
33
- wikiMountRoot?: string;
34
- capabilityBinDir?: string;
35
- }): NodeJS.ProcessEnv;
36
36
  export declare class ClaudeCodeAdapter implements DispatchAdapter {
37
37
  private readonly opts;
38
38
  readonly inputLifecycleMode: "explicit";
@@ -42,12 +42,16 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
42
42
  private shuttingDown;
43
43
  private _model;
44
44
  private _effortLevel;
45
+ private _contextWindow;
46
+ private _maxTokens;
45
47
  constructor(opts: ClaudeCodeAdapterOptions);
46
48
  get currentModel(): string | undefined;
47
49
  get currentEffort(): string | undefined;
48
50
  updateConfig(config: {
49
51
  model?: string | null;
50
52
  effort?: string | null;
53
+ contextWindow?: number | null;
54
+ maxTokens?: number | null;
51
55
  }): void;
52
56
  requestProcessRestart(): void;
53
57
  enqueueDuringDispatch(sessionKey: string, body: string, inputLifecycle?: DispatchInputLifecycle): boolean;
@@ -85,5 +89,4 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
85
89
  private buildArgs;
86
90
  private buildPlaceholderContext;
87
91
  }
88
- export {};
89
92
  //# sourceMappingURL=dispatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,YAAY,EACZ,QAAQ,EAER,YAAY,EACb,MAAM,oBAAoB,CAAC;AAK5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOrD,OAAO,KAAK,EAAuB,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,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,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yBAAyB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3D,iDAAiD;IACjD,wBAAwB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAqCF,qBAAa,iBAAkB,YAAW,eAAe;IAW3C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAVjC,QAAQ,CAAC,kBAAkB,EAAG,UAAU,CAAU;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D,OAAO,CAAC,eAAe,CAAC,CAAgB;IACxC,OAAO,CAAC,qBAAqB,CAAC,CAAsB;IACpD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,UAAU,CAAqB;gBAEV,IAAI,EAAE,wBAAwB;IAM3D,IAAI,YAAY,IAAI,MAAM,GAAG,SAAS,CAErC;IACD,IAAI,aAAa,IAAI,MAAM,GAAG,SAAS,CAEtC;IACD,YAAY,CAAC,MAAM,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,GAAG,IAAI;IAqBR,qBAAqB,IAAI,IAAI;IAM7B,qBAAqB,CACnB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,sBAAsB,GACtC,OAAO;IA2BV,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAcvC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAM1C,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,EACP,cAAc,EACd,YAAY,GACb,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAiE7C,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;IAOhB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAUhB,OAAO;IAuCtB,OAAO,CAAC,uBAAuB;IAa/B;;;;;OAKG;YACW,sBAAsB;IAwCpC;;;;;OAKG;YACY,eAAe;IA4L9B,OAAO,CAAC,aAAa;IAmCrB,OAAO,CAAC,YAAY;IA8DpB,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,uBAAuB;CAmBhC"}
package/dist/dispatch.js CHANGED
@@ -1,11 +1,13 @@
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
1
3
  import * as fs from 'node:fs';
2
4
  import * as path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
- import { execSync, spawn } from 'node:child_process';
5
5
  import { appendPreparedLocalAttachmentRefs, pinLocalAttachmentPaths, } from '@parall/agent-core/internal/attachment-input';
6
6
  import { ClaudeInputRegistry } from './input-lifecycle.js';
7
7
  import { parseClaudeStreamJson, } from './output-parser.js';
8
+ import { buildSpawnEnv } from './spawn-env.js';
8
9
  import { classifyClaudeTurn } from './turn-outcome.js';
10
+ export { buildSpawnEnv };
9
11
  const IS_WIN32 = process.platform === 'win32';
10
12
  const CAPABILITY_PROBE_TIMEOUT_MS = 15_000;
11
13
  class MissingClaudeLifecycleCapabilityError extends Error {
@@ -24,58 +26,6 @@ function killWin32Tree(pid) {
24
26
  return false;
25
27
  }
26
28
  }
27
- export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
28
- const env = { ...parentEnv };
29
- if (!opts.allowApiKey) {
30
- delete env.ANTHROPIC_API_KEY;
31
- delete env.ANTHROPIC_AUTH_TOKEN;
32
- }
33
- const result = {
34
- ...env,
35
- HOME: claudeHome,
36
- PRLL_API_URL: context.apiUrl,
37
- PRLL_API_KEY: context.apiKey,
38
- PRLL_ORG_ID: context.orgId,
39
- PRLL_SESSION_ID: context.sessionId ?? '',
40
- PRLL_CHAT_ID: context.chatId ?? '',
41
- PRLL_TRIGGER_MESSAGE_ID: context.triggerMessageId ?? '',
42
- PRLL_NO_REPLY: context.noReply ? '1' : '',
43
- PRLL_CONTEXT_FILE: context.contextFilePath ?? '',
44
- PRLL_STEP_ID_FILE: context.stepIdFilePath ?? '',
45
- // Per-lane dispatch context directory (stable for the bridge's lifetime,
46
- // so pinning it at spawn is safe even for long-lived subprocesses). The
47
- // CLI keys into it by send target to derive dispatch effect keys.
48
- PRLL_CONTEXT_DIR: context.contextDirPath ?? '',
49
- };
50
- // Pin the wiki workspace to the agent workspace (same convention as the
51
- // OpenClaw runtime). Without this, `parall wiki` resolves mounts relative
52
- // to the subprocess cwd — a `cd` into any subdirectory would silently
53
- // split the agent across two wiki workspaces. Operator-set env wins.
54
- if (!result.PRLL_WIKI_MOUNT_ROOT?.trim() && opts.wikiMountRoot) {
55
- result.PRLL_WIKI_MOUNT_ROOT = opts.wikiMountRoot;
56
- }
57
- if (opts.effortLevel) {
58
- result.CLAUDE_CODE_EFFORT_LEVEL = opts.effortLevel;
59
- }
60
- // Channel-capability shims resolve ahead of any globally-installed CLI of
61
- // the same name (the shim owns auth; the real binary is found further down
62
- // the PATH by the shim itself). On Windows env keys are case-insensitive and
63
- // the host key is usually `Path`; after the plain-object spread a literal
64
- // `PATH` write would SPLIT the key (undefined which wins in the child), so
65
- // reuse the parent's casing. On POSIX keys are case-SENSITIVE — `PATH` is
66
- // the real one and a stray `Path` is unrelated, so match exactly there or we
67
- // could prepend to the wrong variable and silently bypass the shim.
68
- if (opts.capabilityBinDir) {
69
- const pathKey = process.platform === 'win32'
70
- ? (Object.keys(result).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
71
- : 'PATH';
72
- const existing = result[pathKey];
73
- result[pathKey] = existing
74
- ? `${opts.capabilityBinDir}${path.delimiter}${existing}`
75
- : opts.capabilityBinDir;
76
- }
77
- return result;
78
- }
79
29
  export class ClaudeCodeAdapter {
80
30
  opts;
81
31
  inputLifecycleMode = 'explicit';
@@ -85,9 +35,13 @@ export class ClaudeCodeAdapter {
85
35
  shuttingDown = false;
86
36
  _model;
87
37
  _effortLevel;
38
+ _contextWindow;
39
+ _maxTokens;
88
40
  constructor(opts) {
89
41
  this.opts = opts;
90
42
  this._model = opts.model;
43
+ this._contextWindow = opts.contextWindow;
44
+ this._maxTokens = opts.maxTokens;
91
45
  }
92
46
  get currentModel() {
93
47
  return this._model;
@@ -98,11 +52,17 @@ export class ClaudeCodeAdapter {
98
52
  updateConfig(config) {
99
53
  const modelChanged = config.model !== undefined && config.model !== this._model;
100
54
  const effortChanged = config.effort !== undefined && config.effort !== this._effortLevel;
55
+ const contextWindowChanged = config.contextWindow !== undefined && config.contextWindow !== this._contextWindow;
56
+ const maxTokensChanged = config.maxTokens !== undefined && config.maxTokens !== this._maxTokens;
101
57
  if (modelChanged)
102
58
  this._model = config.model ?? undefined;
103
59
  if (effortChanged)
104
60
  this._effortLevel = config.effort ?? undefined;
105
- if (modelChanged || effortChanged) {
61
+ if (contextWindowChanged)
62
+ this._contextWindow = config.contextWindow ?? undefined;
63
+ if (maxTokensChanged)
64
+ this._maxTokens = config.maxTokens ?? undefined;
65
+ if (modelChanged || effortChanged || contextWindowChanged || maxTokensChanged) {
106
66
  this.requestProcessRestart();
107
67
  }
108
68
  }
@@ -557,11 +517,13 @@ export class ClaudeCodeAdapter {
557
517
  const args = this.buildArgs(sessionKey, resume);
558
518
  const env = buildSpawnEnv(process.env, this.opts.claudeHome, this.buildPlaceholderContext(sessionKey), {
559
519
  allowApiKey: this.opts.allowApiKey,
520
+ contextWindow: this._contextWindow,
521
+ maxTokens: this._maxTokens,
560
522
  effortLevel: this._effortLevel,
561
523
  wikiMountRoot: this.opts.workspaceDir,
562
524
  capabilityBinDir: this.opts.capabilityBinDir,
563
525
  });
564
- log?.info(`spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || 'default'}, effort=${this._effortLevel || 'default'}, mode=${this.opts.permissionMode})`);
526
+ log?.info(`spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || 'default'}, context=${this._contextWindow || 'cli-default'}, maxOut=${this._maxTokens || 'cli-default'}, effort=${this._effortLevel || 'default'}, mode=${this.opts.permissionMode})`);
565
527
  const proc = spawn(IS_WIN32 ? quoteWin32Arg(this.opts.claudeBin) : this.opts.claudeBin, IS_WIN32 ? args.map(quoteWin32Arg) : args, {
566
528
  cwd: this.opts.workspaceDir,
567
529
  env,
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import * as os from 'node:os';
3
- import { ParallAgentGateway, capabilityBinDir, configureHttpKeepAlive, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, identityFromMe, initAgentTelemetry, } from '@parall/agent-core';
3
+ import { capabilityBinDir, childLogger, clearAllProviderCreds, configureHttpKeepAlive, createLogger, createOtelLogger, createPlatformConfigManager, deriveModelIsPin, identityFromMe, initAgentTelemetry, llmSource, materializeChannelCapabilities, ParallAgentGateway, parseDispatchDeadlineMs, parseForkDeadlineMs, parseProviderConfig, parseShutdownDeadlineMs, resolveRuntimeContextWindow, resolveRuntimeMaxTokens, resolveRuntimeModel, } from '@parall/agent-core';
4
4
  import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
5
5
  import { buildClaudeRuntimeKey, contextFilePathForSession, dispatchContextDirPath, resolveClaudeAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from './config.js';
6
6
  import { ClaudeCodeAdapter } from './dispatch.js';
@@ -22,10 +22,10 @@ async function getAgentMeWithLegacyFallback(client, orgId) {
22
22
  }
23
23
  function resolveProviderEnv() {
24
24
  const pc = parseProviderConfig(process.env);
25
+ const source = llmSource(pc);
25
26
  if (!pc)
26
- return;
27
+ return source;
27
28
  clearAllProviderCreds(process.env);
28
- const source = llmSource(pc);
29
29
  if (source === 'parall') {
30
30
  process.env.ANTHROPIC_AUTH_TOKEN = process.env.PRLL_API_KEY;
31
31
  process.env.ANTHROPIC_BASE_URL = `${process.env.PRLL_API_URL}/api/llm`;
@@ -39,6 +39,7 @@ function resolveProviderEnv() {
39
39
  if (pc.anthropic_base_url)
40
40
  process.env.ANTHROPIC_BASE_URL = pc.anthropic_base_url;
41
41
  }
42
+ return source;
42
43
  }
43
44
  async function main() {
44
45
  // Before any fetch: long-lived HTTP connections for every bridge→api call.
@@ -46,7 +47,7 @@ async function main() {
46
47
  const telemetry = await initAgentTelemetry('parall-claude-agent', 'claude-code');
47
48
  activeLog = createOtelLogger('agent', 'claude-agent');
48
49
  try {
49
- resolveProviderEnv();
50
+ const activeLLMSource = resolveProviderEnv();
50
51
  const config = resolveClaudeAgentConfig(process.env);
51
52
  if (config.allowApiKey && (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN)) {
52
53
  // Surface the billing path: default is OAuth (Claude.ai subscription); this
@@ -111,17 +112,25 @@ async function main() {
111
112
  // server only when pinned, so we no longer gate it on a local flag.
112
113
  const platformModelOverride = deriveModelIsPin(platformDefaults, me.agent_profile);
113
114
  const resolvedModel = resolveRuntimeModel(platformModelOverride, platformDefaults.model, config.model);
115
+ const resolvedContextWindow = resolveRuntimeContextWindow(resolvedModel, platformDefaults, activeLLMSource);
116
+ const resolvedMaxTokens = resolveRuntimeMaxTokens(resolvedModel, platformDefaults, activeLLMSource);
114
117
  const resolvedEffort = platformDefaults.thinkingEffort ??
115
118
  (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined);
116
119
  if (platformDefaults.model)
117
120
  agentLog.info(`platform config: model=${platformDefaults.model} → resolved=${resolvedModel ?? 'cli-default'} (${platformModelOverride ? 'pin/override' : 'floor'})`);
118
121
  if (platformDefaults.thinkingEffort)
119
122
  agentLog.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort}`);
123
+ if (resolvedContextWindow)
124
+ agentLog.info(`platform config: context_window=${resolvedContextWindow}`);
125
+ if (resolvedMaxTokens)
126
+ agentLog.info(`platform config: max_tokens=${resolvedMaxTokens}`);
120
127
  const adapter = new ClaudeCodeAdapter({
121
128
  claudeBin: config.claudeBin,
122
129
  claudeHome: config.claudeHome,
123
130
  workspaceDir: config.workspaceDir,
124
131
  model: resolvedModel,
132
+ contextWindow: resolvedContextWindow,
133
+ maxTokens: resolvedMaxTokens,
125
134
  permissionMode: config.permissionMode,
126
135
  allowedTools: config.allowedTools,
127
136
  disallowedTools: config.disallowedTools,
@@ -170,8 +179,11 @@ async function main() {
170
179
  }
171
180
  const isPin = deriveModelIsPin(updated, lastKnownAgentProfile);
172
181
  const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
182
+ const model = resolveRuntimeModel(isPin, updated.model, config.model) ?? null;
173
183
  adapter.updateConfig({
174
- model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
184
+ model,
185
+ contextWindow: resolveRuntimeContextWindow(model, updated, activeLLMSource) ?? null,
186
+ maxTokens: resolveRuntimeMaxTokens(model, updated, activeLLMSource) ?? null,
175
187
  effort: updated.thinkingEffort ?? localEffort,
176
188
  });
177
189
  // Channel capabilities heat-update with the config: re-materialize the
@@ -0,0 +1,10 @@
1
+ import type { DispatchOpts } from '@parall/agent-core';
2
+ export declare function buildSpawnEnv(parentEnv: NodeJS.ProcessEnv, claudeHome: string, context: DispatchOpts['context'], opts: {
3
+ allowApiKey: boolean;
4
+ contextWindow?: number;
5
+ maxTokens?: number;
6
+ effortLevel?: string;
7
+ wikiMountRoot?: string;
8
+ capabilityBinDir?: string;
9
+ }): NodeJS.ProcessEnv;
10
+ //# sourceMappingURL=spawn-env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawn-env.d.ts","sourceRoot":"","sources":["../src/spawn-env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,CAAC,UAAU,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,EAChC,IAAI,EAAE;IACJ,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GACA,MAAM,CAAC,UAAU,CAiFnB"}
@@ -0,0 +1,78 @@
1
+ import * as path from 'node:path';
2
+ export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
3
+ const env = { ...parentEnv };
4
+ if (!opts.allowApiKey) {
5
+ delete env.ANTHROPIC_API_KEY;
6
+ delete env.ANTHROPIC_AUTH_TOKEN;
7
+ }
8
+ const result = {
9
+ ...env,
10
+ HOME: claudeHome,
11
+ PRLL_API_URL: context.apiUrl,
12
+ PRLL_API_KEY: context.apiKey,
13
+ PRLL_ORG_ID: context.orgId,
14
+ PRLL_SESSION_ID: context.sessionId ?? '',
15
+ PRLL_CHAT_ID: context.chatId ?? '',
16
+ PRLL_TRIGGER_MESSAGE_ID: context.triggerMessageId ?? '',
17
+ PRLL_NO_REPLY: context.noReply ? '1' : '',
18
+ PRLL_CONTEXT_FILE: context.contextFilePath ?? '',
19
+ PRLL_STEP_ID_FILE: context.stepIdFilePath ?? '',
20
+ // Per-lane dispatch context directory (stable for the bridge's lifetime,
21
+ // so pinning it at spawn is safe even for long-lived subprocesses). The
22
+ // CLI keys into it by send target to derive dispatch effect keys.
23
+ PRLL_CONTEXT_DIR: context.contextDirPath ?? '',
24
+ };
25
+ // Pin the wiki workspace to the agent workspace (same convention as the
26
+ // OpenClaw runtime). Without this, `parall wiki` resolves mounts relative
27
+ // to the subprocess cwd — a `cd` into any subdirectory would silently
28
+ // split the agent across two wiki workspaces. Operator-set env wins.
29
+ if (!result.PRLL_WIKI_MOUNT_ROOT?.trim() && opts.wikiMountRoot) {
30
+ result.PRLL_WIKI_MOUNT_ROOT = opts.wikiMountRoot;
31
+ }
32
+ if (opts.effortLevel) {
33
+ result.CLAUDE_CODE_EFFORT_LEVEL = opts.effortLevel;
34
+ }
35
+ if (typeof opts.contextWindow === 'number' &&
36
+ Number.isSafeInteger(opts.contextWindow) &&
37
+ opts.contextWindow > 0) {
38
+ // Claude Code otherwise assumes 200k for an unrecognized model ID.
39
+ //
40
+ // Declared as the model's INPUT budget (context_window - max_tokens; both
41
+ // raw catalog facts), not the full window. The upstream validates
42
+ // input + max_tokens <= context_window, while the CLI requests up to its
43
+ // own output default (32k) yet reserves at most 20k below this declared
44
+ // boundary before auto-compacting (2.1.251) — so a session declared the
45
+ // full window can grow into a band the provider already rejects but the
46
+ // CLI still considers legal: it retries forever, auto-compact never
47
+ // fires, and once past the boundary the compaction call itself is
48
+ // over-limit (the ParallSRE 08-31 lockup). Subtracting the catalog
49
+ // max_tokens keeps the compaction trigger strictly inside what the
50
+ // provider accepts.
51
+ let inputBudget = opts.contextWindow;
52
+ if (typeof opts.maxTokens === 'number' &&
53
+ Number.isSafeInteger(opts.maxTokens) &&
54
+ opts.maxTokens > 0 &&
55
+ opts.maxTokens < opts.contextWindow) {
56
+ inputBudget = opts.contextWindow - opts.maxTokens;
57
+ }
58
+ result.CLAUDE_CODE_MAX_CONTEXT_TOKENS = String(inputBudget);
59
+ }
60
+ // Channel-capability shims resolve ahead of any globally-installed CLI of
61
+ // the same name (the shim owns auth; the real binary is found further down
62
+ // the PATH by the shim itself). On Windows env keys are case-insensitive and
63
+ // the host key is usually `Path`; after the plain-object spread a literal
64
+ // `PATH` write would SPLIT the key (undefined which wins in the child), so
65
+ // reuse the parent's casing. On POSIX keys are case-SENSITIVE — `PATH` is
66
+ // the real one and a stray `Path` is unrelated, so match exactly there or we
67
+ // could prepend to the wrong variable and silently bypass the shim.
68
+ if (opts.capabilityBinDir) {
69
+ const pathKey = process.platform === 'win32'
70
+ ? (Object.keys(result).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
71
+ : 'PATH';
72
+ const existing = result[pathKey];
73
+ result[pathKey] = existing
74
+ ? `${opts.capabilityBinDir}${path.delimiter}${existing}`
75
+ : opts.capabilityBinDir;
76
+ }
77
+ return result;
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/claude-agent",
3
- "version": "1.57.2",
3
+ "version": "1.58.1",
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.57.2",
29
- "@parall/sdk": "1.57.2",
30
- "@parall/cli": "1.57.2"
28
+ "@parall/cli": "1.58.1",
29
+ "@parall/agent-core": "1.58.1",
30
+ "@parall/sdk": "1.58.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.0.0",
package/src/dispatch.ts CHANGED
@@ -1,11 +1,7 @@
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
1
3
  import * as fs from 'node:fs';
2
4
  import * as path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
- import { execSync, spawn } from 'node:child_process';
5
- import {
6
- appendPreparedLocalAttachmentRefs,
7
- pinLocalAttachmentPaths,
8
- } from '@parall/agent-core/internal/attachment-input';
9
5
  import type {
10
6
  CleanupForkOpts,
11
7
  DispatchAdapter,
@@ -15,16 +11,23 @@ import type {
15
11
  GatewayLogger,
16
12
  RuntimeEvent,
17
13
  } from '@parall/agent-core';
14
+ import {
15
+ appendPreparedLocalAttachmentRefs,
16
+ pinLocalAttachmentPaths,
17
+ } from '@parall/agent-core/internal/attachment-input';
18
18
  import type { ClaudeAgentConfig } from './config.js';
19
- import { ClaudeInputRegistry, type ClaudeInputDelivery } from './input-lifecycle.js';
19
+ import { type ClaudeInputDelivery, ClaudeInputRegistry } from './input-lifecycle.js';
20
20
  import {
21
- parseClaudeStreamJson,
22
21
  type ClaudeParsedEvent,
23
22
  type ClaudeResultMeta,
23
+ parseClaudeStreamJson,
24
24
  } from './output-parser.js';
25
25
  import type { ClaudeProcessHandle, ClaudeSessionManager } from './session-manager.js';
26
+ import { buildSpawnEnv } from './spawn-env.js';
26
27
  import { classifyClaudeTurn } from './turn-outcome.js';
27
28
 
29
+ export { buildSpawnEnv };
30
+
28
31
  type ClaudeCodeAdapterOptions = Pick<
29
32
  ClaudeAgentConfig,
30
33
  | 'additionalDirs'
@@ -42,6 +45,10 @@ type ClaudeCodeAdapterOptions = Pick<
42
45
  apiUrl: string;
43
46
  apiKey: string;
44
47
  orgId: string;
48
+ /** Catalog context size for the selected model. */
49
+ contextWindow?: number;
50
+ /** Catalog max output tokens for the selected model (input-budget subtrahend). */
51
+ maxTokens?: number;
45
52
  contextFilePathForSession?: (sessionKey: string) => string;
46
53
  /** @deprecated Use contextFilePathForSession. */
47
54
  stepIdFilePathForSession?: (sessionKey: string) => string;
@@ -83,70 +90,6 @@ function killWin32Tree(pid: number): boolean {
83
90
  }
84
91
  }
85
92
 
86
- export function buildSpawnEnv(
87
- parentEnv: NodeJS.ProcessEnv,
88
- claudeHome: string,
89
- context: DispatchOpts['context'],
90
- opts: {
91
- allowApiKey: boolean;
92
- effortLevel?: string;
93
- wikiMountRoot?: string;
94
- capabilityBinDir?: string;
95
- },
96
- ): NodeJS.ProcessEnv {
97
- const env: NodeJS.ProcessEnv = { ...parentEnv };
98
- if (!opts.allowApiKey) {
99
- delete env.ANTHROPIC_API_KEY;
100
- delete env.ANTHROPIC_AUTH_TOKEN;
101
- }
102
- const result: NodeJS.ProcessEnv = {
103
- ...env,
104
- HOME: claudeHome,
105
- PRLL_API_URL: context.apiUrl,
106
- PRLL_API_KEY: context.apiKey,
107
- PRLL_ORG_ID: context.orgId,
108
- PRLL_SESSION_ID: context.sessionId ?? '',
109
- PRLL_CHAT_ID: context.chatId ?? '',
110
- PRLL_TRIGGER_MESSAGE_ID: context.triggerMessageId ?? '',
111
- PRLL_NO_REPLY: context.noReply ? '1' : '',
112
- PRLL_CONTEXT_FILE: context.contextFilePath ?? '',
113
- PRLL_STEP_ID_FILE: context.stepIdFilePath ?? '',
114
- // Per-lane dispatch context directory (stable for the bridge's lifetime,
115
- // so pinning it at spawn is safe even for long-lived subprocesses). The
116
- // CLI keys into it by send target to derive dispatch effect keys.
117
- PRLL_CONTEXT_DIR: context.contextDirPath ?? '',
118
- };
119
- // Pin the wiki workspace to the agent workspace (same convention as the
120
- // OpenClaw runtime). Without this, `parall wiki` resolves mounts relative
121
- // to the subprocess cwd — a `cd` into any subdirectory would silently
122
- // split the agent across two wiki workspaces. Operator-set env wins.
123
- if (!result.PRLL_WIKI_MOUNT_ROOT?.trim() && opts.wikiMountRoot) {
124
- result.PRLL_WIKI_MOUNT_ROOT = opts.wikiMountRoot;
125
- }
126
- if (opts.effortLevel) {
127
- result.CLAUDE_CODE_EFFORT_LEVEL = opts.effortLevel;
128
- }
129
- // Channel-capability shims resolve ahead of any globally-installed CLI of
130
- // the same name (the shim owns auth; the real binary is found further down
131
- // the PATH by the shim itself). On Windows env keys are case-insensitive and
132
- // the host key is usually `Path`; after the plain-object spread a literal
133
- // `PATH` write would SPLIT the key (undefined which wins in the child), so
134
- // reuse the parent's casing. On POSIX keys are case-SENSITIVE — `PATH` is
135
- // the real one and a stray `Path` is unrelated, so match exactly there or we
136
- // could prepend to the wrong variable and silently bypass the shim.
137
- if (opts.capabilityBinDir) {
138
- const pathKey =
139
- process.platform === 'win32'
140
- ? (Object.keys(result).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
141
- : 'PATH';
142
- const existing = result[pathKey];
143
- result[pathKey] = existing
144
- ? `${opts.capabilityBinDir}${path.delimiter}${existing}`
145
- : opts.capabilityBinDir;
146
- }
147
- return result;
148
- }
149
-
150
93
  /**
151
94
  * Per-sessionKey long-lived process state. The process stays alive across
152
95
  * dispatches; each dispatch writes one NDJSON user message to stdin and
@@ -171,9 +114,13 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
171
114
  private shuttingDown = false;
172
115
  private _model: string | undefined;
173
116
  private _effortLevel: string | undefined;
117
+ private _contextWindow: number | undefined;
118
+ private _maxTokens: number | undefined;
174
119
 
175
120
  constructor(private readonly opts: ClaudeCodeAdapterOptions) {
176
121
  this._model = opts.model;
122
+ this._contextWindow = opts.contextWindow;
123
+ this._maxTokens = opts.maxTokens;
177
124
  }
178
125
 
179
126
  get currentModel(): string | undefined {
@@ -182,13 +129,22 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
182
129
  get currentEffort(): string | undefined {
183
130
  return this._effortLevel;
184
131
  }
185
-
186
- updateConfig(config: { model?: string | null; effort?: string | null }): void {
132
+ updateConfig(config: {
133
+ model?: string | null;
134
+ effort?: string | null;
135
+ contextWindow?: number | null;
136
+ maxTokens?: number | null;
137
+ }): void {
187
138
  const modelChanged = config.model !== undefined && config.model !== this._model;
188
139
  const effortChanged = config.effort !== undefined && config.effort !== this._effortLevel;
140
+ const contextWindowChanged =
141
+ config.contextWindow !== undefined && config.contextWindow !== this._contextWindow;
142
+ const maxTokensChanged = config.maxTokens !== undefined && config.maxTokens !== this._maxTokens;
189
143
  if (modelChanged) this._model = config.model ?? undefined;
190
144
  if (effortChanged) this._effortLevel = config.effort ?? undefined;
191
- if (modelChanged || effortChanged) {
145
+ if (contextWindowChanged) this._contextWindow = config.contextWindow ?? undefined;
146
+ if (maxTokensChanged) this._maxTokens = config.maxTokens ?? undefined;
147
+ if (modelChanged || effortChanged || contextWindowChanged || maxTokensChanged) {
192
148
  this.requestProcessRestart();
193
149
  }
194
150
  }
@@ -714,6 +670,8 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
714
670
  this.buildPlaceholderContext(sessionKey),
715
671
  {
716
672
  allowApiKey: this.opts.allowApiKey,
673
+ contextWindow: this._contextWindow,
674
+ maxTokens: this._maxTokens,
717
675
  effortLevel: this._effortLevel,
718
676
  wikiMountRoot: this.opts.workspaceDir,
719
677
  capabilityBinDir: this.opts.capabilityBinDir,
@@ -721,7 +679,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
721
679
  );
722
680
 
723
681
  log?.info(
724
- `spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || 'default'}, effort=${this._effortLevel || 'default'}, mode=${this.opts.permissionMode})`,
682
+ `spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || 'default'}, context=${this._contextWindow || 'cli-default'}, maxOut=${this._maxTokens || 'cli-default'}, effort=${this._effortLevel || 'default'}, mode=${this.opts.permissionMode})`,
725
683
  );
726
684
 
727
685
  const proc = spawn(
package/src/index.ts CHANGED
@@ -2,24 +2,26 @@
2
2
 
3
3
  import * as os from 'node:os';
4
4
  import {
5
- ParallAgentGateway,
6
5
  capabilityBinDir,
6
+ childLogger,
7
+ clearAllProviderCreds,
7
8
  configureHttpKeepAlive,
8
- createPlatformConfigManager,
9
9
  createLogger,
10
10
  createOtelLogger,
11
- childLogger,
11
+ createPlatformConfigManager,
12
12
  deriveModelIsPin,
13
+ identityFromMe,
14
+ initAgentTelemetry,
15
+ llmSource,
13
16
  materializeChannelCapabilities,
14
- resolveRuntimeModel,
15
- parseShutdownDeadlineMs,
16
- parseForkDeadlineMs,
17
+ ParallAgentGateway,
17
18
  parseDispatchDeadlineMs,
19
+ parseForkDeadlineMs,
18
20
  parseProviderConfig,
19
- clearAllProviderCreds,
20
- llmSource,
21
- identityFromMe,
22
- initAgentTelemetry,
21
+ parseShutdownDeadlineMs,
22
+ resolveRuntimeContextWindow,
23
+ resolveRuntimeMaxTokens,
24
+ resolveRuntimeModel,
23
25
  } from '@parall/agent-core';
24
26
  import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
25
27
  import {
@@ -52,11 +54,11 @@ async function getAgentMeWithLegacyFallback(client: ParallClient, orgId: string)
52
54
 
53
55
  type AgentMe = Awaited<ReturnType<typeof getAgentMeWithLegacyFallback>>;
54
56
 
55
- function resolveProviderEnv(): void {
57
+ function resolveProviderEnv(): string {
56
58
  const pc = parseProviderConfig(process.env);
57
- if (!pc) return;
58
- clearAllProviderCreds(process.env);
59
59
  const source = llmSource(pc);
60
+ if (!pc) return source;
61
+ clearAllProviderCreds(process.env);
60
62
  if (source === 'parall') {
61
63
  process.env.ANTHROPIC_AUTH_TOKEN = process.env.PRLL_API_KEY;
62
64
  process.env.ANTHROPIC_BASE_URL = `${process.env.PRLL_API_URL}/api/llm`;
@@ -68,6 +70,7 @@ function resolveProviderEnv(): void {
68
70
  }
69
71
  if (pc.anthropic_base_url) process.env.ANTHROPIC_BASE_URL = pc.anthropic_base_url;
70
72
  }
73
+ return source;
71
74
  }
72
75
 
73
76
  async function main() {
@@ -76,7 +79,7 @@ async function main() {
76
79
  const telemetry = await initAgentTelemetry('parall-claude-agent', 'claude-code');
77
80
  activeLog = createOtelLogger('agent', 'claude-agent');
78
81
  try {
79
- resolveProviderEnv();
82
+ const activeLLMSource = resolveProviderEnv();
80
83
  const config = resolveClaudeAgentConfig(process.env);
81
84
 
82
85
  if (config.allowApiKey && (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN)) {
@@ -152,6 +155,16 @@ async function main() {
152
155
  platformDefaults.model,
153
156
  config.model,
154
157
  );
158
+ const resolvedContextWindow = resolveRuntimeContextWindow(
159
+ resolvedModel,
160
+ platformDefaults,
161
+ activeLLMSource,
162
+ );
163
+ const resolvedMaxTokens = resolveRuntimeMaxTokens(
164
+ resolvedModel,
165
+ platformDefaults,
166
+ activeLLMSource,
167
+ );
155
168
  const resolvedEffort =
156
169
  platformDefaults.thinkingEffort ??
157
170
  (process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined);
@@ -161,12 +174,17 @@ async function main() {
161
174
  );
162
175
  if (platformDefaults.thinkingEffort)
163
176
  agentLog.info(`platform config: thinking_effort=${platformDefaults.thinkingEffort}`);
177
+ if (resolvedContextWindow)
178
+ agentLog.info(`platform config: context_window=${resolvedContextWindow}`);
179
+ if (resolvedMaxTokens) agentLog.info(`platform config: max_tokens=${resolvedMaxTokens}`);
164
180
 
165
181
  const adapter = new ClaudeCodeAdapter({
166
182
  claudeBin: config.claudeBin,
167
183
  claudeHome: config.claudeHome,
168
184
  workspaceDir: config.workspaceDir,
169
185
  model: resolvedModel,
186
+ contextWindow: resolvedContextWindow,
187
+ maxTokens: resolvedMaxTokens,
170
188
  permissionMode: config.permissionMode,
171
189
  allowedTools: config.allowedTools,
172
190
  disallowedTools: config.disallowedTools,
@@ -219,8 +237,11 @@ async function main() {
219
237
  }
220
238
  const isPin = deriveModelIsPin(updated, lastKnownAgentProfile);
221
239
  const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
240
+ const model = resolveRuntimeModel(isPin, updated.model, config.model) ?? null;
222
241
  adapter.updateConfig({
223
- model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
242
+ model,
243
+ contextWindow: resolveRuntimeContextWindow(model, updated, activeLLMSource) ?? null,
244
+ maxTokens: resolveRuntimeMaxTokens(model, updated, activeLLMSource) ?? null,
224
245
  effort: updated.thinkingEffort ?? localEffort,
225
246
  });
226
247
  // Channel capabilities heat-update with the config: re-materialize the
@@ -0,0 +1,97 @@
1
+ import * as path from 'node:path';
2
+ import type { DispatchOpts } from '@parall/agent-core';
3
+
4
+ export function buildSpawnEnv(
5
+ parentEnv: NodeJS.ProcessEnv,
6
+ claudeHome: string,
7
+ context: DispatchOpts['context'],
8
+ opts: {
9
+ allowApiKey: boolean;
10
+ contextWindow?: number;
11
+ maxTokens?: number;
12
+ effortLevel?: string;
13
+ wikiMountRoot?: string;
14
+ capabilityBinDir?: string;
15
+ },
16
+ ): NodeJS.ProcessEnv {
17
+ const env: NodeJS.ProcessEnv = { ...parentEnv };
18
+ if (!opts.allowApiKey) {
19
+ delete env.ANTHROPIC_API_KEY;
20
+ delete env.ANTHROPIC_AUTH_TOKEN;
21
+ }
22
+ const result: NodeJS.ProcessEnv = {
23
+ ...env,
24
+ HOME: claudeHome,
25
+ PRLL_API_URL: context.apiUrl,
26
+ PRLL_API_KEY: context.apiKey,
27
+ PRLL_ORG_ID: context.orgId,
28
+ PRLL_SESSION_ID: context.sessionId ?? '',
29
+ PRLL_CHAT_ID: context.chatId ?? '',
30
+ PRLL_TRIGGER_MESSAGE_ID: context.triggerMessageId ?? '',
31
+ PRLL_NO_REPLY: context.noReply ? '1' : '',
32
+ PRLL_CONTEXT_FILE: context.contextFilePath ?? '',
33
+ PRLL_STEP_ID_FILE: context.stepIdFilePath ?? '',
34
+ // Per-lane dispatch context directory (stable for the bridge's lifetime,
35
+ // so pinning it at spawn is safe even for long-lived subprocesses). The
36
+ // CLI keys into it by send target to derive dispatch effect keys.
37
+ PRLL_CONTEXT_DIR: context.contextDirPath ?? '',
38
+ };
39
+ // Pin the wiki workspace to the agent workspace (same convention as the
40
+ // OpenClaw runtime). Without this, `parall wiki` resolves mounts relative
41
+ // to the subprocess cwd — a `cd` into any subdirectory would silently
42
+ // split the agent across two wiki workspaces. Operator-set env wins.
43
+ if (!result.PRLL_WIKI_MOUNT_ROOT?.trim() && opts.wikiMountRoot) {
44
+ result.PRLL_WIKI_MOUNT_ROOT = opts.wikiMountRoot;
45
+ }
46
+ if (opts.effortLevel) {
47
+ result.CLAUDE_CODE_EFFORT_LEVEL = opts.effortLevel;
48
+ }
49
+ if (
50
+ typeof opts.contextWindow === 'number' &&
51
+ Number.isSafeInteger(opts.contextWindow) &&
52
+ opts.contextWindow > 0
53
+ ) {
54
+ // Claude Code otherwise assumes 200k for an unrecognized model ID.
55
+ //
56
+ // Declared as the model's INPUT budget (context_window - max_tokens; both
57
+ // raw catalog facts), not the full window. The upstream validates
58
+ // input + max_tokens <= context_window, while the CLI requests up to its
59
+ // own output default (32k) yet reserves at most 20k below this declared
60
+ // boundary before auto-compacting (2.1.251) — so a session declared the
61
+ // full window can grow into a band the provider already rejects but the
62
+ // CLI still considers legal: it retries forever, auto-compact never
63
+ // fires, and once past the boundary the compaction call itself is
64
+ // over-limit (the ParallSRE 08-31 lockup). Subtracting the catalog
65
+ // max_tokens keeps the compaction trigger strictly inside what the
66
+ // provider accepts.
67
+ let inputBudget = opts.contextWindow;
68
+ if (
69
+ typeof opts.maxTokens === 'number' &&
70
+ Number.isSafeInteger(opts.maxTokens) &&
71
+ opts.maxTokens > 0 &&
72
+ opts.maxTokens < opts.contextWindow
73
+ ) {
74
+ inputBudget = opts.contextWindow - opts.maxTokens;
75
+ }
76
+ result.CLAUDE_CODE_MAX_CONTEXT_TOKENS = String(inputBudget);
77
+ }
78
+ // Channel-capability shims resolve ahead of any globally-installed CLI of
79
+ // the same name (the shim owns auth; the real binary is found further down
80
+ // the PATH by the shim itself). On Windows env keys are case-insensitive and
81
+ // the host key is usually `Path`; after the plain-object spread a literal
82
+ // `PATH` write would SPLIT the key (undefined which wins in the child), so
83
+ // reuse the parent's casing. On POSIX keys are case-SENSITIVE — `PATH` is
84
+ // the real one and a stray `Path` is unrelated, so match exactly there or we
85
+ // could prepend to the wrong variable and silently bypass the shim.
86
+ if (opts.capabilityBinDir) {
87
+ const pathKey =
88
+ process.platform === 'win32'
89
+ ? (Object.keys(result).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
90
+ : 'PATH';
91
+ const existing = result[pathKey];
92
+ result[pathKey] = existing
93
+ ? `${opts.capabilityBinDir}${path.delimiter}${existing}`
94
+ : opts.capabilityBinDir;
95
+ }
96
+ return result;
97
+ }