@jini-ai/daemon 0.3.8 → 0.5.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.
Files changed (173) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +50 -8
  3. package/dist/agent-executor/contracts.d.ts +736 -0
  4. package/dist/agent-executor/contracts.js +28 -0
  5. package/dist/agent-executor/event-translation.d.ts +87 -0
  6. package/dist/agent-executor/event-translation.js +230 -0
  7. package/dist/agent-executor/events.d.ts +190 -0
  8. package/dist/agent-executor/events.js +848 -0
  9. package/dist/agent-executor/handoff.d.ts +18 -0
  10. package/dist/agent-executor/handoff.js +16 -0
  11. package/dist/agent-executor/index.d.ts +98 -0
  12. package/dist/agent-executor/index.js +98 -0
  13. package/dist/agent-executor/launch.d.ts +375 -0
  14. package/dist/agent-executor/launch.js +1001 -0
  15. package/dist/agent-executor/mcp-bridge.d.ts +338 -0
  16. package/dist/agent-executor/mcp-bridge.js +835 -0
  17. package/dist/agent-executor/values.d.ts +5 -0
  18. package/dist/agent-executor/values.js +13 -0
  19. package/dist/agent-executor.d.ts +375 -176
  20. package/dist/agent-executor.js +388 -335
  21. package/dist/artifacts/index.d.ts +14 -0
  22. package/dist/artifacts/index.js +14 -0
  23. package/dist/artifacts/manifest.d.ts +90 -0
  24. package/dist/artifacts/manifest.js +229 -0
  25. package/dist/artifacts/publication-guard.d.ts +28 -0
  26. package/dist/artifacts/publication-guard.js +77 -0
  27. package/dist/artifacts/runtime-compat.d.ts +37 -0
  28. package/dist/artifacts/runtime-compat.js +33 -0
  29. package/dist/artifacts/store.d.ts +85 -0
  30. package/dist/artifacts/store.js +92 -0
  31. package/dist/artifacts/stub-guard.d.ts +73 -0
  32. package/dist/artifacts/stub-guard.js +198 -0
  33. package/dist/artifacts/text-suppression.d.ts +68 -0
  34. package/dist/artifacts/text-suppression.js +184 -0
  35. package/dist/close-status.d.ts +11 -8
  36. package/dist/close-status.js +31 -19
  37. package/dist/continuation/continuation-transport.d.ts +3 -1
  38. package/dist/continuation/continuation-transport.js +1 -1
  39. package/dist/continuation/journal.d.ts +12 -5
  40. package/dist/continuation/journal.js +4 -4
  41. package/dist/continuation/run-scoped-context-store.d.ts +13 -4
  42. package/dist/continuation/run-scoped-context-store.js +6 -6
  43. package/dist/continuation/run-start-handler.d.ts +1 -1
  44. package/dist/continuation/run-start-handler.js +3 -16
  45. package/dist/delegated-tool-bridge.d.ts +7 -3
  46. package/dist/delegated-tool-bridge.js +32 -31
  47. package/dist/event-log.d.ts +7 -5
  48. package/dist/event-log.js +12 -11
  49. package/dist/frontend-capability-tools.d.ts +1 -1
  50. package/dist/frontend-capability-tools.js +3 -2
  51. package/dist/frontend-session-registry.d.ts +32 -9
  52. package/dist/frontend-session-registry.js +12 -11
  53. package/dist/http/active-context.d.ts +105 -0
  54. package/dist/http/active-context.js +67 -0
  55. package/dist/http/agents.d.ts +68 -0
  56. package/dist/http/agents.js +12 -0
  57. package/dist/http/attachments.d.ts +618 -0
  58. package/dist/http/attachments.js +1034 -0
  59. package/dist/http/cancel-owned-runs.d.ts +49 -0
  60. package/dist/http/cancel-owned-runs.js +35 -0
  61. package/dist/http/component-catalog.d.ts +84 -0
  62. package/dist/http/component-catalog.js +64 -0
  63. package/dist/http/connectors.d.ts +209 -0
  64. package/dist/http/connectors.js +409 -0
  65. package/dist/http/daemon-status.d.ts +76 -0
  66. package/dist/http/daemon-status.js +55 -0
  67. package/dist/http/db-ops.d.ts +123 -0
  68. package/dist/http/db-ops.js +189 -0
  69. package/dist/http/delegated-tools.d.ts +113 -0
  70. package/dist/http/delegated-tools.js +230 -0
  71. package/dist/http/frontend-control.d.ts +136 -0
  72. package/dist/http/frontend-control.js +79 -0
  73. package/dist/http/frontend-sessions.d.ts +110 -0
  74. package/dist/http/frontend-sessions.js +180 -0
  75. package/dist/http/health.d.ts +95 -0
  76. package/dist/http/health.js +40 -0
  77. package/dist/http/host-tools.d.ts +214 -0
  78. package/dist/http/host-tools.js +329 -0
  79. package/dist/http/index.d.ts +52 -0
  80. package/dist/http/index.js +27 -0
  81. package/dist/http/media.d.ts +146 -0
  82. package/dist/http/media.js +346 -0
  83. package/dist/http/memory.d.ts +265 -0
  84. package/dist/http/memory.js +328 -0
  85. package/dist/http/model-proxy.d.ts +49 -0
  86. package/dist/http/model-proxy.js +642 -0
  87. package/dist/http/remote-run-events.d.ts +70 -0
  88. package/dist/http/remote-run-events.js +224 -0
  89. package/dist/http/research.d.ts +48 -0
  90. package/dist/http/research.js +315 -0
  91. package/dist/http/route-manifest.d.ts +35 -0
  92. package/dist/http/route-manifest.js +103 -0
  93. package/dist/http/routines.d.ts +120 -0
  94. package/dist/http/routines.js +298 -0
  95. package/dist/http/run-credentials.d.ts +73 -0
  96. package/dist/http/run-credentials.js +71 -0
  97. package/dist/http/runs.d.ts +91 -0
  98. package/dist/http/runs.js +221 -0
  99. package/dist/http/terminals.d.ts +138 -0
  100. package/dist/http/terminals.js +325 -0
  101. package/dist/http/tool-catalog.d.ts +78 -0
  102. package/dist/http/tool-catalog.js +66 -0
  103. package/dist/http/workspace-root.d.ts +77 -0
  104. package/dist/http/workspace-root.js +63 -0
  105. package/dist/http/xai.d.ts +90 -0
  106. package/dist/http/xai.js +655 -0
  107. package/dist/image-prompt-delivery.d.ts +13 -3
  108. package/dist/image-prompt-delivery.js +5 -5
  109. package/dist/index.d.ts +556 -4
  110. package/dist/index.js +556 -4
  111. package/dist/legacy-data-migration.d.ts +59 -8
  112. package/dist/legacy-data-migration.js +38 -33
  113. package/dist/read-only-tools.d.ts +130 -0
  114. package/dist/read-only-tools.js +115 -0
  115. package/dist/remote-tool-bridge.d.ts +11 -5
  116. package/dist/remote-tool-bridge.js +15 -15
  117. package/dist/routines/index.d.ts +1 -1
  118. package/dist/routines/index.js +1 -1
  119. package/dist/routines/routine-store.d.ts +34 -12
  120. package/dist/routines/routine-store.js +21 -20
  121. package/dist/routines/schedule.d.ts +48 -10
  122. package/dist/routines/schedule.js +21 -21
  123. package/dist/routines/scheduler.d.ts +32 -11
  124. package/dist/routines/scheduler.js +61 -57
  125. package/dist/routines/types.d.ts +10 -3
  126. package/dist/run/core/result.d.ts +6 -2
  127. package/dist/run/core/result.js +3 -3
  128. package/dist/run/core/retry.d.ts +17 -4
  129. package/dist/run/core/retry.js +7 -11
  130. package/dist/run/diagnostics/diagnostics.d.ts +14 -4
  131. package/dist/run/diagnostics/diagnostics.js +7 -7
  132. package/dist/run/index.d.ts +1 -1
  133. package/dist/run/index.js +1 -1
  134. package/dist/run-credentials.d.ts +138 -0
  135. package/dist/run-credentials.js +152 -0
  136. package/dist/run-lifecycle.d.ts +92 -33
  137. package/dist/run-lifecycle.js +81 -74
  138. package/dist/scheduler.d.ts +8 -0
  139. package/dist/scheduler.js +2 -0
  140. package/dist/session-coordination.d.ts +115 -0
  141. package/dist/session-coordination.js +111 -0
  142. package/dist/store/agent-sessions/errors.d.ts +8 -0
  143. package/dist/store/agent-sessions/errors.js +10 -0
  144. package/dist/store/agent-sessions/index.d.ts +12 -0
  145. package/dist/store/agent-sessions/index.js +19 -0
  146. package/dist/store/agent-sessions/pglite.d.ts +12 -0
  147. package/dist/store/agent-sessions/pglite.js +7 -0
  148. package/dist/store/agent-sessions/ports.d.ts +24 -0
  149. package/dist/store/agent-sessions/ports.js +2 -0
  150. package/dist/store/agent-sessions/postgres.d.ts +12 -0
  151. package/dist/store/agent-sessions/postgres.js +7 -0
  152. package/dist/store/agent-sessions/schema.d.ts +3 -0
  153. package/dist/store/agent-sessions/schema.js +23 -0
  154. package/dist/store/agent-sessions/sql.d.ts +35 -0
  155. package/dist/store/agent-sessions/sql.js +59 -0
  156. package/dist/store/agent-sessions/sqlite.d.ts +117 -0
  157. package/dist/store/agent-sessions/sqlite.js +128 -0
  158. package/dist/store/event-log/sqlite.d.ts +75 -0
  159. package/dist/store/event-log/sqlite.js +230 -0
  160. package/dist/surface-exchanges.d.ts +97 -0
  161. package/dist/surface-exchanges.js +153 -0
  162. package/dist/terminal-session.d.ts +43 -11
  163. package/dist/terminal-session.js +22 -20
  164. package/dist/tokens.js +4 -4
  165. package/dist/tool-audit.d.ts +114 -0
  166. package/dist/tool-audit.js +95 -0
  167. package/dist/tool-executor.d.ts +22 -7
  168. package/dist/tool-executor.js +16 -16
  169. package/dist/tool-result-media.d.ts +3 -1
  170. package/dist/tool-result-media.js +1 -1
  171. package/dist/tool-result-surfaces.d.ts +3 -1
  172. package/dist/tool-result-surfaces.js +1 -1
  173. package/package.json +127 -9
@@ -81,10 +81,10 @@
81
81
  * byte-identical to pre-gap-4 behavior — every `'failed'` outcome
82
82
  * resumable:false). OD's ~20-vendor-CLI text-matching failure classifier was
83
83
  * deliberately never ported (see `run/core/failure-taxonomy.ts`'s own doc and
84
- * `source-map.md`). The real zero-config classifier lives in `@jini-ai/daemon`'s
84
+ * `archived provenance ledger`). The real zero-config classifier lives in `@jini-ai/daemon`'s
85
85
  * `run/core/retry.ts` (`resumableFromProcessExit`/`classifyProcessExitFailure`)
86
86
  * and is wired in by `@jini-ai/server`'s `createLocalNodeDaemon` — see that
87
- * package's own source-map.md, and `run/core/retry.ts`'s own doc for the
87
+ * package's own archived provenance ledger, and `run/core/retry.ts`'s own doc for the
88
88
  * classification policy and its 2026-07-22 merge-time reconciliation against
89
89
  * a second, independently-built (and rejected) classifier that once lived in
90
90
  * this module.
@@ -118,7 +118,8 @@ const SUPPORTED_STREAM_FORMATS = [
118
118
  * @complexity O(1) — fixed membership check.
119
119
  * @overallScore 100/100
120
120
  */
121
- export function isSupportedStreamFormat(value) {
121
+ export function isSupportedStreamFormat(args) {
122
+ const { value } = args;
122
123
  return SUPPORTED_STREAM_FORMATS.includes(value);
123
124
  }
124
125
  /**
@@ -142,14 +143,15 @@ export function isSupportedStreamFormat(value) {
142
143
  * @complexity O(1) — fixed field checks.
143
144
  * @overallScore 100/100
144
145
  */
145
- export function assessAgentExecutorCompatibility(def) {
146
- const streamFormat = def.streamFormat;
147
- if (!isSupportedStreamFormat(streamFormat)) {
146
+ export function assessAgentExecutorCompatibility({ def }) {
147
+ const format = { value: def.streamFormat };
148
+ if (!isSupportedStreamFormat(format)) {
148
149
  return {
149
150
  supported: false,
150
- reason: `AgentExecutor: agent "${def.id}" has streamFormat "${streamFormat}", which is not implemented in v1 — only ${SUPPORTED_STREAM_FORMATS.join(', ')} are supported`,
151
+ reason: `AgentExecutor: agent "${def.id}" has streamFormat "${format.value}", which is not implemented in v1 — only ${SUPPORTED_STREAM_FORMATS.join(', ')} are supported`,
151
152
  };
152
153
  }
154
+ const streamFormat = format.value;
153
155
  if (streamFormat !== 'acp-json-rpc' &&
154
156
  def.promptViaStdin !== true &&
155
157
  def.promptViaFile !== true &&
@@ -171,8 +173,8 @@ export function assessAgentExecutorCompatibility(def) {
171
173
  * @complexity O(1).
172
174
  * @overallScore 100/100
173
175
  */
174
- export function isAgentExecutorSupported(def) {
175
- return assessAgentExecutorCompatibility(def).supported;
176
+ export function isAgentExecutorSupported({ def }) {
177
+ return assessAgentExecutorCompatibility({ def: def }).supported;
176
178
  }
177
179
  /**
178
180
  * Selects and constructs the real stream-parser handler for a supported
@@ -194,13 +196,13 @@ export function isAgentExecutorSupported(def) {
194
196
  function createStreamHandlerForDef(def, streamFormat, onEvent) {
195
197
  switch (streamFormat) {
196
198
  case 'claude-stream-json':
197
- return createClaudeStreamHandler(onEvent);
199
+ return createClaudeStreamHandler({ onEvent: onEvent });
198
200
  case 'copilot-stream-json':
199
- return createCopilotStreamHandler(onEvent);
201
+ return createCopilotStreamHandler({ onEvent: onEvent });
200
202
  case 'qoder-stream-json':
201
- return createQoderStreamHandler(onEvent);
203
+ return createQoderStreamHandler({ onEvent: onEvent });
202
204
  case 'json-event-stream':
203
- return createJsonEventStreamHandler(def.eventParser ?? '', onEvent);
205
+ return createJsonEventStreamHandler({ kind: def.eventParser ?? '', onEvent: onEvent });
204
206
  }
205
207
  }
206
208
  function isRecord(value) {
@@ -224,7 +226,7 @@ function asOptionalNumber(value) {
224
226
  * original "omit the whole `usage` field rather than emit an empty object" behavior.
225
227
  * @complexity O(1).
226
228
  */
227
- export function extractUsageTokens(rawUsage) {
229
+ export function extractUsageTokens({ rawUsage }) {
228
230
  if (!rawUsage)
229
231
  return undefined;
230
232
  const inputTokens = asOptionalNumber(rawUsage.input_tokens);
@@ -250,7 +252,7 @@ export function extractUsageTokens(rawUsage) {
250
252
  */
251
253
  function translateUsagePayload(rawEvent) {
252
254
  const rawUsage = isRecord(rawEvent.usage) ? rawEvent.usage : undefined;
253
- const usage = extractUsageTokens(rawUsage);
255
+ const usage = extractUsageTokens({ rawUsage: rawUsage });
254
256
  const costUsd = asOptionalNumber(rawEvent.costUsd);
255
257
  const durationMs = asOptionalNumber(rawEvent.durationMs);
256
258
  return {
@@ -260,7 +262,7 @@ function translateUsagePayload(rawEvent) {
260
262
  ...(durationMs !== undefined ? { durationMs } : {}),
261
263
  };
262
264
  }
263
- export function translateStatusEvent(rawEvent) {
265
+ export function translateStatusEvent({ rawEvent }) {
264
266
  const model = asOptionalString(rawEvent.model);
265
267
  const ttftMs = asOptionalNumber(rawEvent.ttftMs);
266
268
  const detail = asOptionalString(rawEvent.detail);
@@ -308,7 +310,7 @@ function translateToolInputDeltaEvent(rawEvent) {
308
310
  },
309
311
  };
310
312
  }
311
- export function translateToolResultEvent(rawEvent) {
313
+ export function translateToolResultEvent({ rawEvent }) {
312
314
  const isError = typeof rawEvent.isError === 'boolean' ? rawEvent.isError : undefined;
313
315
  return {
314
316
  kind: 'agent',
@@ -326,7 +328,7 @@ function translateUsageEvent(rawEvent) {
326
328
  function translateRawEvent(rawEvent) {
327
329
  return { kind: 'agent', payload: { type: 'raw', line: asString(rawEvent.line) } };
328
330
  }
329
- export function translateErrorEvent(rawEvent) {
331
+ export function translateErrorEvent({ rawEvent }) {
330
332
  const code = asOptionalString(rawEvent.code);
331
333
  const message = asString(rawEvent.message, 'Unknown agent error');
332
334
  return {
@@ -334,7 +336,7 @@ export function translateErrorEvent(rawEvent) {
334
336
  payload: { message, ...(code !== undefined ? { error: { code, message } } : {}) },
335
337
  };
336
338
  }
337
- export function translateTurnEndEvent(rawEvent) {
339
+ export function translateTurnEndEvent({ rawEvent }) {
338
340
  // Claude-specific per-turn boundary. Not forwarded as an 'agent'
339
341
  // event (no RunAgentPayload variant represents it) — run() reacts to
340
342
  // it directly to close stdin (or, for gap 3, decide whether to inject
@@ -350,17 +352,17 @@ export function translateTurnEndEvent(rawEvent) {
350
352
  * `translateAgentRuntimeEvent` itself is just a lookup plus the two upfront guards.
351
353
  */
352
354
  const EVENT_TYPE_TRANSLATORS = {
353
- status: translateStatusEvent,
355
+ status: rawEvent => translateStatusEvent({ rawEvent }),
354
356
  text_delta: translateTextDeltaEvent,
355
357
  thinking_start: translateThinkingStartEvent,
356
358
  thinking_delta: translateThinkingDeltaEvent,
357
359
  tool_use: translateToolUseEvent,
358
360
  tool_input_delta: translateToolInputDeltaEvent,
359
- tool_result: translateToolResultEvent,
361
+ tool_result: rawEvent => translateToolResultEvent({ rawEvent }),
360
362
  usage: translateUsageEvent,
361
363
  raw: translateRawEvent,
362
- error: translateErrorEvent,
363
- turn_end: translateTurnEndEvent,
364
+ error: rawEvent => translateErrorEvent({ rawEvent }),
365
+ turn_end: rawEvent => translateTurnEndEvent({ rawEvent }),
364
366
  };
365
367
  /**
366
368
  * Narrows one parser-emitted `{type, ...}` record into this engine's
@@ -384,7 +386,7 @@ const EVENT_TYPE_TRANSLATORS = {
384
386
  * @complexity O(1) — one table lookup, no iteration.
385
387
  * @overallScore 100/100
386
388
  */
387
- export function translateAgentRuntimeEvent(rawEvent) {
389
+ export function translateAgentRuntimeEvent({ rawEvent }) {
388
390
  if (!isRecord(rawEvent) || typeof rawEvent.type !== 'string') {
389
391
  return { kind: 'ignored' };
390
392
  }
@@ -394,7 +396,7 @@ export function translateAgentRuntimeEvent(rawEvent) {
394
396
  /** Thrown by `AgentExecutor.run()` on every failure path — never a bare `Error`, so callers can branch on `.code` instead of parsing `.message`. */
395
397
  export class AgentExecutorError extends Error {
396
398
  code;
397
- constructor(code, message) {
399
+ constructor({ code, message }) {
398
400
  super(message);
399
401
  this.name = 'AgentExecutorError';
400
402
  this.code = code;
@@ -435,7 +437,8 @@ const BASELINE_AGENT_ENV_KEYS = [
435
437
  // run /login" despite real credentials existing on disk/keychain. Confirmed by bisection against
436
438
  // a real authenticated `claude` install: `BASELINE_AGENT_ENV_KEYS` alone fails, adding back every
437
439
  // `CLAUDE_CODE_*`/`CLAUDECODE` var still fails, `LOGNAME`/`SSH_AUTH_SOCK` alone still fail, but
438
- // `USER` alone flips it to success. See tovu-learnings.md §9 for the full investigation trail.
440
+ // `USER` alone flips it to success. Login lookup requires the user identity
441
+ // as well as the home directory.
439
442
  'USER',
440
443
  'SystemRoot', 'windir', 'ComSpec', 'PATHEXT', // Windows-only; harmless no-ops elsewhere
441
444
  ];
@@ -485,13 +488,13 @@ async function terminateChildTree(deps, child) {
485
488
  if (child.pid == null)
486
489
  return;
487
490
  const processes = await deps.listProcessSnapshots();
488
- const pids = deps.collectProcessTreePids(processes, [child.pid]);
489
- await deps.stopProcesses(pids);
491
+ const pids = deps.collectProcessTreePids({ processes, rootPids: [child.pid] });
492
+ await deps.stopProcesses({ pids });
490
493
  }
491
494
  /** Default sink when a host does not supply `onCleanupFailure`: still observable, never silent. Redacted per SEC-007 (a spawn/permission error can embed paths/host detail). */
492
495
  function defaultCleanupFailureSink(context) {
493
496
  // eslint-disable-next-line no-console
494
- console.error(`[@jini-ai/daemon] agent-executor: process-tree cleanup failed for run "${context.runId}" (${context.phase}, pid=${context.pid})`, redactSecrets(errorMessage(context.error)));
497
+ console.error(`[@jini-ai/daemon] agent-executor: process-tree cleanup failed for run "${context.runId}" (${context.phase}, pid=${context.pid})`, redactSecrets({ input: errorMessage(context.error) }));
495
498
  }
496
499
  /**
497
500
  * Reports a contained post-close failure through the host's sink, absorbing a throwing sink.
@@ -599,6 +602,31 @@ function receivedJournalEntry(channel, content) {
599
602
  return { content, provenance: { source: 'agent', channel }, trust: 'untrusted' };
600
603
  }
601
604
  const JINI_MCP_SERVER_KEY = 'jini';
605
+ /** The run-error code a run gets when its CLI reports the injected `jini` bridge as anything but connected. */
606
+ export const MCP_BRIDGE_UNAVAILABLE = 'MCP_BRIDGE_UNAVAILABLE';
607
+ /**
608
+ * Judges a CLI's startup report of the injected bridge. Returns the bridge's status (`'missing'`
609
+ * when the list omits it) when it is not `'connected'`, and `undefined` when it is connected or
610
+ * when `rawEvent` is not an init status carrying an MCP server list (nothing to judge).
611
+ *
612
+ * `'pending'` counts as a failure on purpose: the CLI decides which MCP tools a session has at
613
+ * startup, so a bridge that is not connected by then never joins this run.
614
+ * @param rawEvent - One parsed stream event, as `@jini-ai/agent-runtime`'s parser emits it.
615
+ * @complexity O(n) in the number of reported servers.
616
+ */
617
+ export function unavailableJiniBridgeStatus({ rawEvent }) {
618
+ if (!isRecord(rawEvent) || rawEvent.type !== 'status' || rawEvent.label !== 'initializing')
619
+ return undefined;
620
+ if (!Array.isArray(rawEvent.mcpServers))
621
+ return undefined;
622
+ const bridge = rawEvent.mcpServers.find((server) => isRecord(server) && server.name === JINI_MCP_SERVER_KEY);
623
+ const status = isRecord(bridge) ? String(bridge.status) : 'missing';
624
+ return status === 'connected' ? undefined : status;
625
+ }
626
+ /** The user-readable reason a run stopped because its bridge did not connect. */
627
+ function bridgeUnavailableMessage(status) {
628
+ return `The assistant's tools did not load (MCP bridge "${JINI_MCP_SERVER_KEY}" status: ${status}). The run was stopped instead of continuing without them.`;
629
+ }
602
630
  /**
603
631
  * Builds this run's `mcpServers.jini` entry — pure and synchronous, so every field mapping is
604
632
  * directly assertable without touching the filesystem. The credential arrives already resolved:
@@ -606,17 +634,19 @@ const JINI_MCP_SERVER_KEY = 'jini';
606
634
  * `writeMcpJsonForRun`'s job, which keeps the effect out of this function.
607
635
  *
608
636
  * @param runId - The run this entry scopes its child to.
609
- * @param options - `command`/`args`/`daemonUrl` from the host's injection options.
637
+ * @param options - `command`/`args`/`daemonUrl`/`env` from the host's injection options.
610
638
  * @param credential - The already-resolved bearer token, or `undefined` to omit `JINI_DAEMON_TOKEN`
611
639
  * entirely. Omitting produces byte-identical output to before this parameter existed.
612
640
  * @complexity O(1).
613
641
  * @overallScore 100/100
614
642
  */
615
- export function buildMcpJsonServerEntry(runId, options, credential) {
643
+ export function buildMcpJsonServerEntry({ runId, options }, { credential } = {}) {
644
+ const hostEnv = Object.entries(options.env ?? {}).filter(([key]) => !key.startsWith('JINI_'));
616
645
  return {
617
646
  command: options.command,
618
647
  args: options.args !== undefined ? [...options.args] : [],
619
648
  env: {
649
+ ...Object.fromEntries(hostEnv),
620
650
  JINI_RUN_ID: runId,
621
651
  JINI_DAEMON_URL: options.daemonUrl,
622
652
  ...(credential !== undefined ? { JINI_DAEMON_TOKEN: credential } : {}),
@@ -634,7 +664,7 @@ export function buildMcpJsonServerEntry(runId, options, credential) {
634
664
  * @complexity O(1) plus `JSON.parse`/`JSON.stringify`'s own cost on a small config file.
635
665
  * @overallScore 100/100
636
666
  */
637
- export function mergeMcpJsonContent(existingRaw, serverEntry) {
667
+ export function mergeMcpJsonContent({ existingRaw, serverEntry }) {
638
668
  let doc = {};
639
669
  if (existingRaw !== undefined) {
640
670
  try {
@@ -671,7 +701,7 @@ export function mergeMcpJsonContent(existingRaw, serverEntry) {
671
701
  * @complexity O(1).
672
702
  * @overallScore 100/100
673
703
  */
674
- export function buildAcpMcpBridgeServers(entry) {
704
+ export function buildAcpMcpBridgeServers({ entry }) {
675
705
  return [
676
706
  {
677
707
  type: 'stdio',
@@ -719,7 +749,7 @@ const ENV_CONTENT_VAR_BY_STRATEGY = {
719
749
  * @complexity O(1) plus `JSON.parse`/`JSON.stringify` over a small config document.
720
750
  * @overallScore 100/100
721
751
  */
722
- export function mergeEnvContentMcpConfig(existingRaw, entry) {
752
+ export function mergeEnvContentMcpConfig({ existingRaw, entry }) {
723
753
  let doc = {};
724
754
  if (existingRaw !== undefined && existingRaw.length > 0) {
725
755
  try {
@@ -780,7 +810,7 @@ export function mergeEnvContentMcpConfig(existingRaw, entry) {
780
810
  * @complexity O(1) plus `JSON.parse`/`JSON.stringify` over a small config document.
781
811
  * @overallScore 100/100
782
812
  */
783
- export function mergeEnvContentInstructions(existingRaw, instructionsFilePath) {
813
+ export function mergeEnvContentInstructions({ existingRaw, instructionsFilePath }) {
784
814
  let doc = {};
785
815
  if (existingRaw !== undefined && existingRaw.length > 0) {
786
816
  try {
@@ -825,6 +855,10 @@ function escapeTomlBasicString(value) {
825
855
  function tomlString(value) {
826
856
  return `"${escapeTomlBasicString(value)}"`;
827
857
  }
858
+ /** A TOML key: bare when TOML allows it, quoted otherwise — host-supplied bridge env names are not guaranteed bare-safe. */
859
+ function tomlKey(key) {
860
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key);
861
+ }
828
862
  /**
829
863
  * Codex's per-tool deadline for the Jini bridge, in seconds: `@jini-ai/mcp`'s
830
864
  * `DEFAULT_DELEGATED_TOOL_TIMEOUT_MS` (6 min, mirrored because this package does not depend on
@@ -843,19 +877,19 @@ const CODEX_TOOL_TIMEOUT_SEC = 6 * 60 + 40;
843
877
  * Confirmed against a real installed Codex CLI (0.151.0), not assumed from docs: round-tripping
844
878
  * `codex mcp add <name> --env K=V -- <cmd> <args>` against a scratch `CODEX_HOME` and reading back
845
879
  * `config.toml` produced exactly this shape (`command`/`args` as TOML strings/array in the main
846
- * table, env vars in a nested `.env` table) — see `source-map.md` for the transcript.
880
+ * table, env vars in a nested `.env` table) — see `archived provenance ledger` for the transcript.
847
881
  * @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
848
882
  * @returns A TOML fragment with no leading/trailing blank-line padding — {@link buildCodexHomeConfigToml} owns spacing when combining it with existing content.
849
883
  * @complexity O(n) in the number of argv/env entries.
850
884
  * @overallScore 100/100
851
885
  */
852
- export function buildCodexMcpServerToml(entry) {
886
+ export function buildCodexMcpServerToml({ entry }) {
853
887
  const argsLiteral = entry.args.map(tomlString).join(', ');
854
888
  const serverTable = `[mcp_servers.${JINI_MCP_SERVER_KEY}]\ncommand = ${tomlString(entry.command)}\nargs = [${argsLiteral}]\n` +
855
889
  `tool_timeout_sec = ${CODEX_TOOL_TIMEOUT_SEC}\n`;
856
890
  const envLines = Object.entries(entry.env)
857
891
  .filter((pair) => typeof pair[1] === 'string')
858
- .map(([key, value]) => `${key} = ${tomlString(value)}`);
892
+ .map(([key, value]) => `${tomlKey(key)} = ${tomlString(value)}`);
859
893
  if (envLines.length === 0)
860
894
  return serverTable;
861
895
  return `${serverTable}\n[mcp_servers.${JINI_MCP_SERVER_KEY}.env]\n${envLines.join('\n')}\n`;
@@ -975,10 +1009,10 @@ function stripExistingJiniMcpServerTable(existingRaw) {
975
1009
  * @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
976
1010
  * @complexity O(n) in the existing config's length.
977
1011
  */
978
- export function buildCodexHomeConfigToml(existingRaw, entry) {
1012
+ export function buildCodexHomeConfigToml({ existingRaw, entry }) {
979
1013
  const base = stripExistingJiniMcpServerTable(existingRaw ?? '');
980
1014
  const separator = base.length === 0 ? '' : base.endsWith('\n') ? '\n' : '\n\n';
981
- return `${base}${separator}${buildCodexMcpServerToml(entry)}`;
1015
+ return `${base}${separator}${buildCodexMcpServerToml({ entry: entry })}`;
982
1016
  }
983
1017
  /**
984
1018
  * Where `'codex-toml'` reads the operator's REAL Codex config from, to seed a run's scratch copy —
@@ -992,7 +1026,7 @@ export function buildCodexHomeConfigToml(existingRaw, entry) {
992
1026
  * @complexity O(1).
993
1027
  * @overallScore 100/100
994
1028
  */
995
- export function resolveSourceCodexHomeDir(hostEnv) {
1029
+ export function resolveSourceCodexHomeDir({ hostEnv }) {
996
1030
  const override = hostEnv.CODEX_HOME;
997
1031
  return override !== undefined && override.trim().length > 0 ? override : join(homedir(), '.codex');
998
1032
  }
@@ -1021,12 +1055,12 @@ export function buildMcpBridgeDelivery(input) {
1021
1055
  const { cwd, runId, strategy, options, credential } = input;
1022
1056
  if (options === undefined || strategy === undefined)
1023
1057
  return null;
1024
- const serverEntry = buildMcpJsonServerEntry(runId, options, credential);
1058
+ const serverEntry = buildMcpJsonServerEntry({ runId: runId, options: options }, credential === undefined ? {} : { credential });
1025
1059
  switch (strategy) {
1026
1060
  case 'claude-mcp-json':
1027
1061
  return { kind: 'claude-mcp-json', mcpJsonPath: mcpJsonPathForRun(cwd, runId), serverEntry };
1028
1062
  case 'acp-merge':
1029
- return { kind: 'acp-merge', mcpServers: buildAcpMcpBridgeServers(serverEntry) };
1063
+ return { kind: 'acp-merge', mcpServers: buildAcpMcpBridgeServers({ entry: serverEntry }) };
1030
1064
  case 'opencode-env-content':
1031
1065
  case 'mimo-env-content':
1032
1066
  return { kind: 'env-content', envVarName: ENV_CONTENT_VAR_BY_STRATEGY[strategy], serverEntry };
@@ -1036,13 +1070,13 @@ export function buildMcpBridgeDelivery(input) {
1036
1070
  return { kind: 'env-passthrough', serverEntry };
1037
1071
  }
1038
1072
  }
1039
- function defaultReadMcpJsonFile(path) {
1073
+ function defaultReadMcpJsonFile({ path }) {
1040
1074
  return fsPromises.readFile(path, 'utf8');
1041
1075
  }
1042
- function defaultWriteMcpJsonFile(path, content) {
1076
+ function defaultWriteMcpJsonFile({ path, content }) {
1043
1077
  return fsPromises.writeFile(path, content, 'utf8');
1044
1078
  }
1045
- function defaultRemoveMcpJsonFile(path) {
1079
+ function defaultRemoveMcpJsonFile({ path }) {
1046
1080
  return fsPromises.rm(path, { force: true });
1047
1081
  }
1048
1082
  /**
@@ -1102,19 +1136,19 @@ async function writeMcpJsonForRun(cwd, delivery, options) {
1102
1136
  const writeFileFn = options.writeFile ?? defaultWriteMcpJsonFile;
1103
1137
  let existingRaw;
1104
1138
  try {
1105
- existingRaw = await readFileFn(join(cwd, '.mcp.json'));
1139
+ existingRaw = await readFileFn({ path: join(cwd, '.mcp.json') });
1106
1140
  }
1107
1141
  catch {
1108
1142
  // No existing file (ENOENT — the common case) or unreadable for any other reason: both
1109
1143
  // degrade to "start fresh", matching mergeMcpJsonContent's own doc.
1110
1144
  existingRaw = undefined;
1111
1145
  }
1112
- await writeFileFn(delivery.mcpJsonPath, mergeMcpJsonContent(existingRaw, delivery.serverEntry));
1146
+ await writeFileFn({ path: delivery.mcpJsonPath, content: mergeMcpJsonContent({ existingRaw: existingRaw, serverEntry: delivery.serverEntry }) });
1113
1147
  }
1114
- function defaultMkdtempCodexHome(prefix) {
1148
+ function defaultMkdtempCodexHome({ prefix }) {
1115
1149
  return fsPromises.mkdtemp(join(tmpdir(), prefix));
1116
1150
  }
1117
- function defaultRemoveCodexHomeDir(path) {
1151
+ function defaultRemoveCodexHomeDir({ path }) {
1118
1152
  return fsPromises.rm(path, { recursive: true, force: true });
1119
1153
  }
1120
1154
  function resolveCodexHomeSeams(options) {
@@ -1161,21 +1195,21 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
1161
1195
  // too, not just path separators, so a run id like `../../etc/evil` cannot leave even a cosmetic
1162
1196
  // `..` substring in the mkdtemp prefix.
1163
1197
  const safeRunId = runId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 80) || 'run';
1164
- const dir = await seams.mkdtemp(`jini-codex-home-${safeRunId}-`);
1198
+ const dir = await seams.mkdtemp({ prefix: `jini-codex-home-${safeRunId}-` });
1165
1199
  try {
1166
1200
  let existingConfigRaw;
1167
1201
  try {
1168
- existingConfigRaw = await seams.readFile(join(sourceCodexHomeDir, 'config.toml'));
1202
+ existingConfigRaw = await seams.readFile({ path: join(sourceCodexHomeDir, 'config.toml') });
1169
1203
  }
1170
1204
  catch {
1171
1205
  // No config yet (fresh Codex install) or unreadable — start from just this run's block,
1172
1206
  // matching writeMcpJsonForRun's identical "missing file" handling.
1173
1207
  existingConfigRaw = undefined;
1174
1208
  }
1175
- await seams.writeFile(join(dir, 'config.toml'), buildCodexHomeConfigToml(existingConfigRaw, entry));
1209
+ await seams.writeFile({ path: join(dir, 'config.toml'), content: buildCodexHomeConfigToml({ existingRaw: existingConfigRaw, entry: entry }) });
1176
1210
  try {
1177
- const authRaw = await seams.readFile(join(sourceCodexHomeDir, 'auth.json'));
1178
- await seams.writeFile(join(dir, 'auth.json'), authRaw);
1211
+ const authRaw = await seams.readFile({ path: join(sourceCodexHomeDir, 'auth.json') });
1212
+ await seams.writeFile({ path: join(dir, 'auth.json'), content: authRaw });
1179
1213
  }
1180
1214
  catch {
1181
1215
  // No stored login (or unreadable) — the spawned CLI runs unauthenticated. Confirmed above:
@@ -1183,7 +1217,7 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
1183
1217
  }
1184
1218
  }
1185
1219
  catch (err) {
1186
- await seams.removeDir(dir).catch(() => {
1220
+ await seams.removeDir({ path: dir }).catch(() => {
1187
1221
  // Best-effort only — the original error below is what the caller must see either way.
1188
1222
  });
1189
1223
  throw err;
@@ -1191,14 +1225,14 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
1191
1225
  return {
1192
1226
  path: dir,
1193
1227
  cleanup: async () => {
1194
- await seams.removeDir(dir);
1228
+ await seams.removeDir({ path: dir });
1195
1229
  },
1196
1230
  };
1197
1231
  }
1198
- function defaultMkdtempClaudeConfigDir(prefix) {
1232
+ function defaultMkdtempClaudeConfigDir({ prefix }) {
1199
1233
  return fsPromises.mkdtemp(join(tmpdir(), prefix));
1200
1234
  }
1201
- function defaultRemoveClaudeConfigDir(path) {
1235
+ function defaultRemoveClaudeConfigDir({ path }) {
1202
1236
  return fsPromises.rm(path, { recursive: true, force: true });
1203
1237
  }
1204
1238
  function resolveClaudeConfigDirSeams(options) {
@@ -1221,7 +1255,7 @@ function resolveClaudeConfigDirSeams(options) {
1221
1255
  * default, `~/.claude`.
1222
1256
  * @complexity O(1).
1223
1257
  */
1224
- export function resolveSourceClaudeConfigDir(hostEnv) {
1258
+ export function resolveSourceClaudeConfigDir({ hostEnv }) {
1225
1259
  const override = hostEnv.CLAUDE_CONFIG_DIR;
1226
1260
  return override !== undefined && override.trim().length > 0 ? override : join(homedir(), '.claude');
1227
1261
  }
@@ -1274,12 +1308,12 @@ export function resolveSourceClaudeConfigDir(hostEnv) {
1274
1308
  */
1275
1309
  async function prepareClaudeConfigDirForRun(runId, sourceConfigDir, seams) {
1276
1310
  const safeRunId = runId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 80) || 'run';
1277
- const dir = await seams.mkdtemp(`jini-claude-config-${safeRunId}-`);
1311
+ const dir = await seams.mkdtemp({ prefix: `jini-claude-config-${safeRunId}-` });
1278
1312
  try {
1279
1313
  const credentialsPath = join(sourceConfigDir, '.credentials.json');
1280
1314
  let credentialsRaw;
1281
1315
  try {
1282
- credentialsRaw = await seams.readFile(credentialsPath);
1316
+ credentialsRaw = await seams.readFile({ path: credentialsPath });
1283
1317
  }
1284
1318
  catch {
1285
1319
  // No file-based credential to copy (the common macOS-Keychain-only case) — see this
@@ -1287,11 +1321,11 @@ async function prepareClaudeConfigDirForRun(runId, sourceConfigDir, seams) {
1287
1321
  credentialsRaw = undefined;
1288
1322
  }
1289
1323
  if (credentialsRaw !== undefined) {
1290
- await seams.writeFile(join(dir, '.credentials.json'), credentialsRaw);
1324
+ await seams.writeFile({ path: join(dir, '.credentials.json'), content: credentialsRaw });
1291
1325
  }
1292
1326
  }
1293
1327
  catch (err) {
1294
- await seams.removeDir(dir).catch(() => {
1328
+ await seams.removeDir({ path: dir }).catch(() => {
1295
1329
  // Best-effort only — the original error below is what the caller must see either way.
1296
1330
  });
1297
1331
  throw err;
@@ -1299,7 +1333,7 @@ async function prepareClaudeConfigDirForRun(runId, sourceConfigDir, seams) {
1299
1333
  return {
1300
1334
  path: dir,
1301
1335
  cleanup: async () => {
1302
- await seams.removeDir(dir);
1336
+ await seams.removeDir({ path: dir });
1303
1337
  },
1304
1338
  };
1305
1339
  }
@@ -1357,7 +1391,7 @@ function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
1357
1391
  * **Deliberately un-hygiened for v1**: no ANSI/terminal-control-sequence
1358
1392
  * stripping is applied (there is no Jini equivalent of OD's
1359
1393
  * `TerminalControlSequenceStripper` yet) — a documented decision, not an
1360
- * oversight; see `packages/daemon/source-map.md`'s 2026-07-21 addition for
1394
+ * oversight; see `packages/daemon/archived provenance ledger`'s 2026-07-21 addition for
1361
1395
  * the reasoning.
1362
1396
  *
1363
1397
  * *When* those chunks leave is the def's call, via `def.stdoutPolicy`:
@@ -1405,6 +1439,10 @@ function wireChildLifecycle(ctx) {
1405
1439
  // consumed by a turn-end injection decision. See `ContinuationOptions`'s doc for why this is
1406
1440
  // only ever acted on when a host has explicitly allowlisted the tool's name.
1407
1441
  let pendingToolUse;
1442
+ // Bridge guard (see `expectsJiniBridge`): checked on the first init frame only. Once the bridge is
1443
+ // found unavailable, the child is being stopped and nothing it still prints is forwarded.
1444
+ let bridgeChecked = !ctx.expectsJiniBridge;
1445
+ let bridgeUnavailable = false;
1408
1446
  // `def.stdoutPolicy` read once, up front, so the per-chunk handler below is a single boolean
1409
1447
  // test rather than a repeated union narrowing. `undefined` (every def but antigravity) means
1410
1448
  // live — see this function's own doc.
@@ -1454,7 +1492,7 @@ function wireChildLifecycle(ctx) {
1454
1492
  });
1455
1493
  stdin.write(`${line}\n`, 'utf8');
1456
1494
  if (journal)
1457
- enqueueEmit(() => journal.record(runId, sentJournalEntry(content)));
1495
+ enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
1458
1496
  }
1459
1497
  /**
1460
1498
  * Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
@@ -1467,7 +1505,7 @@ function wireChildLifecycle(ctx) {
1467
1505
  const shouldInject = stopReason === 'tool_use' &&
1468
1506
  toolUse !== undefined &&
1469
1507
  continuation !== undefined &&
1470
- resolveContinuationTransport(def) === 'stdin-injection' &&
1508
+ resolveContinuationTransport({ def: def }) === 'stdin-injection' &&
1471
1509
  continuation.autonomousToolNames.has(toolUse.name);
1472
1510
  if (!shouldInject) {
1473
1511
  closeStdinOnce();
@@ -1490,40 +1528,50 @@ function wireChildLifecycle(ctx) {
1490
1528
  // the CPU-starved-and-silent condition the watchdog exists to catch. Resumed in `finally` so a
1491
1529
  // genuinely stalled stretch *after* this tool settles is still caught — see
1492
1530
  // `RunLifecycle.suspendSlowRunNotice`'s own doc.
1493
- lifecycle.suspendSlowRunNotice(runId);
1531
+ lifecycle.suspendSlowRunNotice({ runId: runId });
1494
1532
  try {
1495
- const result = await continuation.toolExecutor.execute(continuation.principal, run, toolUse.name, toolUse.input);
1496
- content = resultContent(result);
1533
+ const result = await continuation.toolExecutor.execute({ principal: continuation.principal, run: run, toolId: toolUse.name, input: toolUse.input });
1534
+ content = resultContent({ result: result });
1497
1535
  isError = result.status !== 'completed';
1498
- media = extractResultMedia(result.output).media;
1536
+ media = extractResultMedia({ output: result.output }).media;
1499
1537
  }
1500
1538
  catch (error) {
1501
1539
  content = errorMessage(error);
1502
1540
  isError = true;
1503
1541
  }
1504
1542
  finally {
1505
- lifecycle.resumeSlowRunNotice(runId);
1543
+ lifecycle.resumeSlowRunNotice({ runId: runId });
1506
1544
  }
1507
- await lifecycle.emit(runId, {
1508
- event: 'agent',
1509
- data: {
1510
- type: 'tool_result',
1511
- toolUseId: toolUse.id,
1512
- content,
1513
- ...(isError ? { isError: true } : {}),
1514
- ...(media.length > 0 ? { media } : {}),
1515
- },
1516
- });
1545
+ await lifecycle.emit({ runId: runId, input: {
1546
+ event: 'agent',
1547
+ data: {
1548
+ type: 'tool_result',
1549
+ toolUseId: toolUse.id,
1550
+ content,
1551
+ ...(isError ? { isError: true } : {}),
1552
+ ...(media.length > 0 ? { media } : {}),
1553
+ },
1554
+ } });
1517
1555
  injectToolResultLine(toolUse.id, content, isError);
1518
1556
  });
1519
1557
  }
1520
1558
  const streamHandler = streamFormat === 'plain'
1521
1559
  ? null
1522
1560
  : createStreamHandlerForDef(def, streamFormat, (rawEvent) => {
1523
- const translation = translateAgentRuntimeEvent(rawEvent);
1561
+ if (bridgeUnavailable)
1562
+ return;
1563
+ const translation = translateAgentRuntimeEvent({ rawEvent: rawEvent });
1524
1564
  if (translation.kind === 'agent') {
1525
1565
  if (translation.sessionId !== undefined)
1526
1566
  capturedSessionId = translation.sessionId;
1567
+ if (!bridgeChecked && translation.payload.type === 'status' && translation.payload.label === 'initializing') {
1568
+ bridgeChecked = true;
1569
+ const bridgeStatus = unavailableJiniBridgeStatus({ rawEvent: rawEvent });
1570
+ if (bridgeStatus !== undefined) {
1571
+ stopForUnavailableBridge(translation.payload, bridgeStatus);
1572
+ return;
1573
+ }
1574
+ }
1527
1575
  if (translation.payload.type === 'tool_use') {
1528
1576
  pendingToolUse = { id: translation.payload.id, name: translation.payload.name, input: translation.payload.input };
1529
1577
  toolCallSeen = true;
@@ -1532,15 +1580,28 @@ function wireChildLifecycle(ctx) {
1532
1580
  translation.payload.delta.length > 0) {
1533
1581
  userVisibleOutputSeen = true;
1534
1582
  }
1535
- enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: translation.payload }));
1583
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: translation.payload } }));
1536
1584
  }
1537
1585
  else if (translation.kind === 'error') {
1538
- enqueueEmit(() => lifecycle.emit(runId, { event: 'error', data: translation.payload }));
1586
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
1539
1587
  }
1540
1588
  else if (translation.kind === 'turn-end') {
1541
1589
  handleTurnEnd(translation.stopReason);
1542
1590
  }
1543
1591
  });
1592
+ /**
1593
+ * Ends a run whose CLI started without the `jini` bridge: emits the init status and a
1594
+ * {@link MCP_BRIDGE_UNAVAILABLE} error, then stops the child's process tree before the model can
1595
+ * answer with no host tools. The close handler then finishes the run `'failed'` whatever exit code
1596
+ * the child reports.
1597
+ */
1598
+ function stopForUnavailableBridge(initStatus, bridgeStatus) {
1599
+ bridgeUnavailable = true;
1600
+ const message = bridgeUnavailableMessage(bridgeStatus);
1601
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: initStatus } }));
1602
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: { message, error: { code: MCP_BRIDGE_UNAVAILABLE, message } } } }));
1603
+ void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
1604
+ }
1544
1605
  /**
1545
1606
  * Emits the accumulated `'until-close'` stdout — sanitized — as exactly one raw `'stdout'` echo
1546
1607
  * plus one `text_delta`, through the same `enqueueEmit` FIFO queue every other emit uses, so the
@@ -1555,7 +1616,7 @@ function wireChildLifecycle(ctx) {
1555
1616
  function flushBufferedStdout() {
1556
1617
  if (bufferedStdout.length === 0 && droppedStdoutBytes === 0)
1557
1618
  return;
1558
- const safe = sanitizeBufferedStdout ? sanitizeBufferedStdout(bufferedStdout) : bufferedStdout;
1619
+ const safe = sanitizeBufferedStdout ? sanitizeBufferedStdout({ fullText: bufferedStdout }) : bufferedStdout;
1559
1620
  bufferedStdout = '';
1560
1621
  bufferedStdoutBytes = 0;
1561
1622
  const text = droppedStdoutBytes > 0
@@ -1564,13 +1625,13 @@ function wireChildLifecycle(ctx) {
1564
1625
  droppedStdoutBytes = 0;
1565
1626
  if (text.length === 0)
1566
1627
  return;
1567
- enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
1568
- enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: { type: 'text_delta', delta: text } }));
1628
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
1629
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'text_delta', delta: text } } }));
1569
1630
  }
1570
1631
  child.stdout?.on('data', (chunk) => {
1571
1632
  const text = chunk.toString('utf8');
1572
1633
  if (journal)
1573
- enqueueEmit(() => journal.record(runId, receivedJournalEntry('stdout', text)));
1634
+ enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stdout', text) }));
1574
1635
  if (streamFormat === 'plain') {
1575
1636
  if (text.length > 0)
1576
1637
  userVisibleOutputSeen = true;
@@ -1592,20 +1653,20 @@ function wireChildLifecycle(ctx) {
1592
1653
  bufferedStdoutBytes += chunkBytes;
1593
1654
  return;
1594
1655
  }
1595
- enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
1596
- enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: { type: 'text_delta', delta: text } }));
1656
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
1657
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'text_delta', delta: text } } }));
1597
1658
  return;
1598
1659
  }
1599
- enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
1660
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
1600
1661
  // Non-null: `streamHandler` is only ever null when `streamFormat === 'plain'` (see its
1601
1662
  // construction above), the branch this statement is provably unreachable from.
1602
- streamHandler.feed(text);
1663
+ streamHandler.feed({ chunk: text });
1603
1664
  });
1604
1665
  child.stderr?.on('data', (chunk) => {
1605
1666
  const text = chunk.toString('utf8');
1606
1667
  if (journal)
1607
- enqueueEmit(() => journal.record(runId, receivedJournalEntry('stderr', text)));
1608
- enqueueEmit(() => lifecycle.emit(runId, { event: 'stderr', data: { chunk: text } }));
1668
+ enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stderr', text) }));
1669
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stderr', data: { chunk: text } } }));
1609
1670
  });
1610
1671
  // EPIPE-tolerant: a fast-exiting child that closes its stdin read end
1611
1672
  // before every queued write lands must not crash the host process with
@@ -1617,10 +1678,10 @@ function wireChildLifecycle(ctx) {
1617
1678
  // EventEmitter throws on an unheard 'error' otherwise. The real outcome
1618
1679
  // is still decided by 'close' below.
1619
1680
  child.on('error', () => { });
1620
- const unsubscribeCancel = lifecycle.onCancelRequested(runId, () => {
1621
- cancelRequested = true;
1622
- void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
1623
- });
1681
+ const unsubscribeCancel = lifecycle.onCancelRequested({ runId: runId, listener: () => {
1682
+ cancelRequested = true;
1683
+ void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
1684
+ } });
1624
1685
  child.on('close', (code, signal) => {
1625
1686
  void (async () => {
1626
1687
  // Not wrapped in try/catch: all 4 supported parser factories'
@@ -1640,8 +1701,9 @@ function wireChildLifecycle(ctx) {
1640
1701
  // Both of the next two steps are guarded: neither a failed cleanup nor a rejecting host
1641
1702
  // classifier may prevent the terminal transition below — see each helper's own doc.
1642
1703
  await cleanupStagedFilesSafely(ctx);
1643
- const status = classifyRunCloseStatus({ cancelRequested, code, signal });
1644
- const resumable = status === 'failed' && classifyFailure !== undefined
1704
+ const status = bridgeUnavailable ? 'failed' : classifyRunCloseStatus({ cancelRequested, code }, { signal });
1705
+ // A bridge failure is not resumable: resuming would start the same CLI with the same bridge.
1706
+ const resumable = status === 'failed' && !bridgeUnavailable && classifyFailure !== undefined
1645
1707
  ? await classifyFailureSafely(ctx, classifyFailure, {
1646
1708
  runId,
1647
1709
  agentId: def.id,
@@ -1650,21 +1712,14 @@ function wireChildLifecycle(ctx) {
1650
1712
  sideEffects: { userVisibleOutputSeen, toolCallSeen },
1651
1713
  })
1652
1714
  : false;
1653
- await lifecycle.finish({
1654
- runId,
1655
- status,
1656
- code,
1657
- signal: signal ?? null,
1658
- resumable,
1659
- ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}),
1660
- });
1715
+ await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
1661
1716
  })();
1662
1717
  });
1663
1718
  return {
1664
1719
  closeStdinOnce,
1665
1720
  recordSentBytes(content) {
1666
1721
  if (journal)
1667
- enqueueEmit(() => journal.record(runId, sentJournalEntry(content)));
1722
+ enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
1668
1723
  },
1669
1724
  };
1670
1725
  }
@@ -1705,9 +1760,9 @@ function translateAcpError(payload) {
1705
1760
  * @param sink - The driver-specific effects to apply.
1706
1761
  * @complexity O(1).
1707
1762
  */
1708
- export function applyAgentTranslationSideEffects(payload, sessionId, sink) {
1763
+ export function applyAgentTranslationSideEffects({ payload, sessionId, sink }) {
1709
1764
  if (sessionId !== undefined)
1710
- sink.onSessionId(sessionId);
1765
+ sink.onSessionId({ sessionId });
1711
1766
  if (payload.type === 'tool_use') {
1712
1767
  sink.onToolCall();
1713
1768
  }
@@ -1747,23 +1802,23 @@ function wireAcpLifecycle(ctx) {
1747
1802
  child.stdout?.on('data', (chunk) => {
1748
1803
  const text = chunk.toString('utf8');
1749
1804
  if (journal)
1750
- enqueueEmit(() => journal.record(runId, receivedJournalEntry('stdout', text)));
1751
- enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
1805
+ enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stdout', text) }));
1806
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
1752
1807
  });
1753
1808
  child.stderr?.on('data', (chunk) => {
1754
1809
  const text = chunk.toString('utf8');
1755
1810
  if (journal)
1756
- enqueueEmit(() => journal.record(runId, receivedJournalEntry('stderr', text)));
1757
- enqueueEmit(() => lifecycle.emit(runId, { event: 'stderr', data: { chunk: text } }));
1811
+ enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stderr', text) }));
1812
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stderr', data: { chunk: text } } }));
1758
1813
  });
1759
1814
  child.stdin?.on('error', () => { });
1760
1815
  child.on('error', () => { });
1761
1816
  let controller = null;
1762
- const unsubscribeCancel = lifecycle.onCancelRequested(runId, () => {
1763
- cancelRequested = true;
1764
- controller?.abort();
1765
- void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
1766
- });
1817
+ const unsubscribeCancel = lifecycle.onCancelRequested({ runId: runId, listener: () => {
1818
+ cancelRequested = true;
1819
+ controller?.abort();
1820
+ void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
1821
+ } });
1767
1822
  child.on('close', (code, signal) => {
1768
1823
  void (async () => {
1769
1824
  await emitQueue;
@@ -1780,49 +1835,33 @@ function wireAcpLifecycle(ctx) {
1780
1835
  sideEffects: { userVisibleOutputSeen, toolCallSeen },
1781
1836
  })
1782
1837
  : false;
1783
- await lifecycle.finish({
1784
- runId,
1785
- status,
1786
- code,
1787
- signal: signal ?? null,
1788
- resumable,
1789
- ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}),
1790
- });
1838
+ await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
1791
1839
  })();
1792
1840
  });
1793
- controller = ctx.attachAcpSession({
1794
- child,
1795
- prompt: ctx.prompt,
1796
- cwd: ctx.cwd,
1797
- ...(ctx.model !== undefined ? { model: ctx.model } : {}),
1798
- ...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}),
1799
- ...(ctx.envFormat !== undefined ? { envFormat: ctx.envFormat } : {}),
1800
- // Spread-when-present rather than always: passing `mcpServers: []` is not the same as passing
1801
- // nothing for every downstream ACP agent, and "no bridge configured" must stay byte-identical
1802
- // to before this field existed.
1803
- ...(ctx.mcpServers !== undefined && ctx.mcpServers.length > 0 ? { mcpServers: [...ctx.mcpServers] } : {}),
1804
- ...(ctx.onPermissionRequest !== undefined ? { onPermissionRequest: ctx.onPermissionRequest } : {}),
1805
- send(event, payload) {
1841
+ controller = ctx.attachAcpSession({ child, prompt: ctx.prompt, send({ event, payload }) {
1806
1842
  if (event === 'agent') {
1807
- const translation = translateAgentRuntimeEvent(payload);
1843
+ const translation = translateAgentRuntimeEvent({ rawEvent: payload });
1808
1844
  if (translation.kind === 'agent') {
1809
- applyAgentTranslationSideEffects(translation.payload, translation.sessionId, {
1810
- onSessionId: (sessionId) => { capturedSessionId = sessionId; },
1811
- onToolCall: () => { toolCallSeen = true; },
1812
- onUserVisibleOutput: () => { userVisibleOutputSeen = true; },
1813
- });
1814
- enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: translation.payload }));
1845
+ applyAgentTranslationSideEffects({ payload: translation.payload, sessionId: translation.sessionId, sink: {
1846
+ onSessionId: ({ sessionId }) => { capturedSessionId = sessionId; },
1847
+ onToolCall: () => { toolCallSeen = true; },
1848
+ onUserVisibleOutput: () => { userVisibleOutputSeen = true; },
1849
+ } });
1850
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: translation.payload } }));
1815
1851
  }
1816
1852
  else if (translation.kind === 'error') {
1817
- enqueueEmit(() => lifecycle.emit(runId, { event: 'error', data: translation.payload }));
1853
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
1818
1854
  }
1819
1855
  return;
1820
1856
  }
1821
1857
  if (event === 'error') {
1822
- enqueueEmit(() => lifecycle.emit(runId, { event: 'error', data: translateAcpError(payload) }));
1858
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translateAcpError(payload) } }));
1823
1859
  }
1824
- },
1825
- });
1860
+ } }, { cwd: ctx.cwd, ...(ctx.model !== undefined ? { model: ctx.model } : {}), ...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}), ...(ctx.envFormat !== undefined ? { envFormat: ctx.envFormat } : {}),
1861
+ // Spread-when-present rather than always: passing `mcpServers: []` is not the same as passing
1862
+ // nothing for every downstream ACP agent, and "no bridge configured" must stay byte-identical
1863
+ // to before this field existed.
1864
+ ...(ctx.mcpServers !== undefined && ctx.mcpServers.length > 0 ? { mcpServers: [...ctx.mcpServers] } : {}), ...(ctx.onPermissionRequest !== undefined ? { onPermissionRequest: ctx.onPermissionRequest } : {}) });
1826
1865
  return controller;
1827
1866
  }
1828
1867
  /**
@@ -1867,23 +1906,23 @@ function wirePiRpcLifecycle(ctx) {
1867
1906
  child.stdout?.on('data', (chunk) => {
1868
1907
  const text = chunk.toString('utf8');
1869
1908
  if (journal)
1870
- enqueueEmit(() => journal.record(runId, receivedJournalEntry('stdout', text)));
1871
- enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
1909
+ enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stdout', text) }));
1910
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
1872
1911
  });
1873
1912
  child.stderr?.on('data', (chunk) => {
1874
1913
  const text = chunk.toString('utf8');
1875
1914
  if (journal)
1876
- enqueueEmit(() => journal.record(runId, receivedJournalEntry('stderr', text)));
1877
- enqueueEmit(() => lifecycle.emit(runId, { event: 'stderr', data: { chunk: text } }));
1915
+ enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stderr', text) }));
1916
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stderr', data: { chunk: text } } }));
1878
1917
  });
1879
1918
  child.stdin?.on('error', () => { });
1880
1919
  child.on('error', () => { });
1881
1920
  let session = null;
1882
- const unsubscribeCancel = lifecycle.onCancelRequested(runId, () => {
1883
- cancelRequested = true;
1884
- session?.abort();
1885
- void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
1886
- });
1921
+ const unsubscribeCancel = lifecycle.onCancelRequested({ runId: runId, listener: () => {
1922
+ cancelRequested = true;
1923
+ session?.abort();
1924
+ void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
1925
+ } });
1887
1926
  child.on('close', (code, signal) => {
1888
1927
  void (async () => {
1889
1928
  await emitQueue;
@@ -1900,25 +1939,11 @@ function wirePiRpcLifecycle(ctx) {
1900
1939
  sideEffects: { userVisibleOutputSeen, toolCallSeen },
1901
1940
  })
1902
1941
  : false;
1903
- await lifecycle.finish({
1904
- runId,
1905
- status,
1906
- code,
1907
- signal: signal ?? null,
1908
- resumable,
1909
- ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}),
1910
- });
1942
+ await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
1911
1943
  })();
1912
1944
  });
1913
- session = ctx.attachPiRpcSession({
1914
- child: ctx.child,
1915
- prompt: ctx.prompt,
1916
- cwd: ctx.cwd,
1917
- ...(ctx.model !== undefined ? { model: ctx.model } : {}),
1918
- ...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}),
1919
- ...(ctx.uploadRoot !== undefined ? { uploadRoot: ctx.uploadRoot } : {}),
1920
- send(_channel, payload) {
1921
- const translation = translateAgentRuntimeEvent(payload);
1945
+ session = ctx.attachPiRpcSession({ child: ctx.child, prompt: ctx.prompt, send({ payload }) {
1946
+ const translation = translateAgentRuntimeEvent({ rawEvent: payload });
1922
1947
  if (translation.kind === 'agent') {
1923
1948
  if (translation.sessionId !== undefined)
1924
1949
  capturedSessionId = translation.sessionId;
@@ -1929,13 +1954,12 @@ function wirePiRpcLifecycle(ctx) {
1929
1954
  translation.payload.delta.length > 0) {
1930
1955
  userVisibleOutputSeen = true;
1931
1956
  }
1932
- enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: translation.payload }));
1957
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: translation.payload } }));
1933
1958
  }
1934
1959
  else if (translation.kind === 'error') {
1935
- enqueueEmit(() => lifecycle.emit(runId, { event: 'error', data: translation.payload }));
1960
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
1936
1961
  }
1937
- },
1938
- });
1962
+ } }, { cwd: ctx.cwd, ...(ctx.model !== undefined ? { model: ctx.model } : {}), ...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}), ...(ctx.uploadRoot !== undefined ? { uploadRoot: ctx.uploadRoot } : {}) });
1939
1963
  return session;
1940
1964
  }
1941
1965
  /**
@@ -1997,8 +2021,8 @@ function resolveProcessDeps(options) {
1997
2021
  createCommandInvocation: options.createCommandInvocation ?? createCommandInvocation,
1998
2022
  spawn: options.spawn ?? nodeSpawn,
1999
2023
  listProcessSnapshots: options.listProcessSnapshots ?? listProcessSnapshots,
2000
- collectProcessTreePids: options.collectProcessTreePids ?? collectProcessTreePids,
2001
- stopProcesses: options.stopProcesses ?? stopProcesses,
2024
+ collectProcessTreePids: options.collectProcessTreePids ?? (({ processes, rootPids }) => collectProcessTreePids(processes, rootPids)),
2025
+ stopProcesses: options.stopProcesses ?? (({ pids }) => stopProcesses(pids)),
2002
2026
  };
2003
2027
  }
2004
2028
  /** Resolves the two remaining defaultable options — see {@link resolveAgentRuntimeDeps}'s doc. Pure. */
@@ -2009,23 +2033,23 @@ function resolveMiscExecutorDeps(options) {
2009
2033
  };
2010
2034
  }
2011
2035
  /** Phase 1: registry lookup + `assessAgentExecutorCompatibility` guard. */
2012
- export async function resolveDefAndStreamFormat(input, deps) {
2013
- const def = deps.getAgentDef(input.agentId);
2036
+ export async function resolveDefAndStreamFormat({ input, deps }) {
2037
+ const def = deps.getAgentDef({ id: input.agentId });
2014
2038
  if (!def) {
2015
- return deps.failBeforeSpawn(input.runId, 'AGENT_NOT_FOUND', `AgentExecutor: unknown agentId "${input.agentId}"`);
2039
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_NOT_FOUND', message: `AgentExecutor: unknown agentId "${input.agentId}"` });
2016
2040
  }
2017
- const compatibility = assessAgentExecutorCompatibility(def);
2041
+ const compatibility = assessAgentExecutorCompatibility({ def: def });
2018
2042
  if (!compatibility.supported) {
2019
- return deps.failBeforeSpawn(input.runId, 'AGENT_RUNTIME_UNSUPPORTED', compatibility.reason);
2043
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_RUNTIME_UNSUPPORTED', message: compatibility.reason });
2020
2044
  }
2021
2045
  return { def, streamFormat: compatibility.streamFormat };
2022
2046
  }
2023
2047
  /** Phase 2: image-prompt-delivery augmentation + argv-budget guard for argv-bound defs. */
2024
- export async function resolveImageDeliveryAndArgvBudget(input, deps) {
2025
- const imageDelivery = applyImagePromptDelivery(input.def.imageDelivery, input.prompt, input.imagePaths, input.extraAllowedDirs);
2026
- const argvBudgetError = checkPromptArgvBudget(input.def, imageDelivery.prompt);
2048
+ export async function resolveImageDeliveryAndArgvBudget({ input, deps }) {
2049
+ const imageDelivery = applyImagePromptDelivery({ imageDelivery: input.def.imageDelivery, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs });
2050
+ const argvBudgetError = checkPromptArgvBudget({ def: input.def, composed: imageDelivery.prompt });
2027
2051
  if (argvBudgetError) {
2028
- return deps.failBeforeSpawn(input.runId, 'AGENT_PROMPT_TOO_LARGE', argvBudgetError.message);
2052
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_PROMPT_TOO_LARGE', message: argvBudgetError.message });
2029
2053
  }
2030
2054
  return imageDelivery;
2031
2055
  }
@@ -2034,44 +2058,44 @@ export async function resolveImageDeliveryAndArgvBudget(input, deps) {
2034
2058
  * caller-supplied escape hatch verbatim, or the deny-by-default `BASELINE_AGENT_ENV_KEYS` allowlist.
2035
2059
  * Pure.
2036
2060
  */
2037
- export function resolveRunEnv(input, hostEnv) {
2061
+ export function resolveRunEnv({ input, hostEnv }) {
2038
2062
  return input.env !== undefined ? toStringEnvRecord(input.env) : buildAgentEnv(hostEnv, input.credentialEnv);
2039
2063
  }
2040
2064
  /** Phase 3b: launch-path resolution + binary-not-resolved guard. */
2041
- export async function resolveLaunch(input, deps) {
2042
- const launch = deps.resolveAgentLaunch(input.def, input.resolvedEnv);
2065
+ export async function resolveLaunch({ input, deps }) {
2066
+ const launch = deps.resolveAgentLaunch({ def: input.def }, { configuredEnv: input.resolvedEnv });
2043
2067
  if (!launch.launchPath) {
2044
- return deps.failBeforeSpawn(input.runId, 'AGENT_BINARY_NOT_RESOLVED', `AgentExecutor: could not resolve an executable for agent "${input.def.id}" (bin "${input.def.bin}")`);
2068
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_BINARY_NOT_RESOLVED', message: `AgentExecutor: could not resolve an executable for agent "${input.def.id}" (bin "${input.def.bin}")` });
2045
2069
  }
2046
2070
  // Narrowed by the guard above; `resolveAgentLaunch`'s own return type still declares
2047
2071
  // `launchPath: string | null` since it can't know this call site already checked.
2048
2072
  return launch;
2049
2073
  }
2050
2074
  /** Phase 4a: stage a `promptViaFile` def's prompt to a temp file (a no-op for every other def). */
2051
- export async function stagePromptFile(input, deps) {
2075
+ export async function stagePromptFile({ input, deps }) {
2052
2076
  try {
2053
- return await deps.preparePromptFileForAgent(input.def, input.prompt, input.runId);
2077
+ return await deps.preparePromptFileForAgent({ def: input.def, prompt: input.prompt, label: input.runId });
2054
2078
  }
2055
2079
  catch (err) {
2056
- return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a prompt file for agent "${input.def.id}": ${errorMessage(err)}`);
2080
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a prompt file for agent "${input.def.id}": ${errorMessage(err)}` });
2057
2081
  }
2058
2082
  }
2059
2083
  /** Phase 4b: stage a `needsAgentLogFile` def's diagnostic-log path (a no-op for every other def). */
2060
- export async function stageLogFile(input, deps) {
2084
+ export async function stageLogFile({ input, deps }) {
2061
2085
  try {
2062
- return await deps.prepareAgentLogFile(input.def, input.runId);
2086
+ return await deps.prepareAgentLogFile({ def: input.def, label: input.runId });
2063
2087
  }
2064
2088
  catch (err) {
2065
2089
  await (input.preparedPromptFile ? input.preparedPromptFile.cleanup() : Promise.resolve());
2066
- return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a log file for agent "${input.def.id}": ${errorMessage(err)}`);
2090
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a log file for agent "${input.def.id}": ${errorMessage(err)}` });
2067
2091
  }
2068
2092
  }
2069
2093
  /** Phase 5: resolves this run's MCP bridge delivery (credential resolution + {@link buildMcpBridgeDelivery}). */
2070
- export async function resolveMcpBridgeForRun(input, deps) {
2094
+ export async function resolveMcpBridgeForRun({ input, deps }) {
2071
2095
  try {
2072
2096
  // Awaited here rather than inside `buildMcpBridgeDelivery` so that function stays pure and
2073
2097
  // synchronous. `undefined` when the host supplied no resolver, which omits the token entirely.
2074
- const credential = deps.mcpJsonInjection !== undefined ? await deps.mcpJsonInjection.credential?.(input.runId) : undefined;
2098
+ const credential = deps.mcpJsonInjection !== undefined ? await deps.mcpJsonInjection.credential?.({ runId: input.runId }) : undefined;
2075
2099
  return buildMcpBridgeDelivery({
2076
2100
  cwd: input.cwd,
2077
2101
  runId: input.runId,
@@ -2084,7 +2108,7 @@ export async function resolveMcpBridgeForRun(input, deps) {
2084
2108
  // Spawning a child that cannot authenticate would produce a run whose every bridged tool call
2085
2109
  // 401s, so a rejecting credential resolver fails the run before spawn instead.
2086
2110
  await deps.cleanupStagedFiles();
2087
- return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not resolve the MCP bridge credential for agent "${input.def.id}": ${errorMessage(err)}`);
2111
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not resolve the MCP bridge credential for agent "${input.def.id}": ${errorMessage(err)}` });
2088
2112
  }
2089
2113
  }
2090
2114
  /**
@@ -2108,11 +2132,11 @@ export async function resolveMcpBridgeForRun(input, deps) {
2108
2132
  * @complexity O(1) plus `mergeEnvContentMcpConfig`'s and `mergeEnvContentInstructions`'s own `JSON.parse`/`JSON.stringify` cost.
2109
2133
  * @overallScore 100/100
2110
2134
  */
2111
- export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptEnvOverrides, stagedInstructionsFile, claudeConfigDir) {
2135
+ export function computeChildEnv({ spawnEnv, mcpBridge }, { codexHomeDir, systemPromptEnvOverrides, stagedInstructionsFile, claudeConfigDir } = {}) {
2112
2136
  const envContentApplied = mcpBridge?.kind === 'env-content'
2113
2137
  ? {
2114
2138
  ...spawnEnv,
2115
- [mcpBridge.envVarName]: mergeEnvContentMcpConfig(spawnEnv[mcpBridge.envVarName], mcpBridge.serverEntry),
2139
+ [mcpBridge.envVarName]: mergeEnvContentMcpConfig({ existingRaw: spawnEnv[mcpBridge.envVarName], entry: mcpBridge.serverEntry }),
2116
2140
  }
2117
2141
  : spawnEnv;
2118
2142
  // `'env-passthrough'` (antigravity): no document, no named carrier variable — the bridge
@@ -2125,7 +2149,7 @@ export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptE
2125
2149
  ? envPassthroughApplied
2126
2150
  : {
2127
2151
  ...envPassthroughApplied,
2128
- [stagedInstructionsFile.varName]: mergeEnvContentInstructions(envPassthroughApplied[stagedInstructionsFile.varName], stagedInstructionsFile.path),
2152
+ [stagedInstructionsFile.varName]: mergeEnvContentInstructions({ existingRaw: envPassthroughApplied[stagedInstructionsFile.varName], instructionsFilePath: stagedInstructionsFile.path }),
2129
2153
  };
2130
2154
  const codexHomeApplied = codexHomeDir === undefined ? instructionsApplied : { ...instructionsApplied, CODEX_HOME: codexHomeDir };
2131
2155
  // Finding 1 of SEC-assistant-env-isolation-2026-09-07: same "only set when staged" shape as
@@ -2146,7 +2170,7 @@ export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptE
2146
2170
  * session id, with no prompt/log file staged and no claude-mcp-json bridge, is exactly the common
2147
2171
  * case for a resumed turn.
2148
2172
  */
2149
- export function computeRuntimeContext(preparedPromptFile, preparedLogFile, mcpBridge, resumeSessionId, newSessionId) {
2173
+ export function computeRuntimeContext({ preparedPromptFile, preparedLogFile, mcpBridge }, { resumeSessionId, newSessionId } = {}) {
2150
2174
  // Matches claude.ts buildArgs' own `typeof x === 'string' && x` truthiness check, so an empty
2151
2175
  // string or explicit `null` (no resume target yet) is treated as absent here too, rather than
2152
2176
  // manufacturing a context that carries a session field the def would ignore anyway.
@@ -2187,13 +2211,18 @@ function computeSystemPromptOverlay(promptAugmenter, agentId, runtimeContext) {
2187
2211
  turnIndex: runtimeContext?.hasPriorAssistantTurn ? 1 : 0,
2188
2212
  });
2189
2213
  }
2190
- /** Phase 9a: the def's `buildArgs` 4th argument — `undefined` when the run selects no model/reasoning/permissionMode/overlay/tool-restriction at all (byte-identical to omitting the argument). Pure. */
2191
- export function buildAgentBuildArgsOptions(input, systemPromptOverlay) {
2214
+ /** Phase 9a: the def's `buildArgs` 4th argument. Historically omitted when every override was absent;
2215
+ * that let defs silently enable auto-approval. Always supply the validated restricted default now. Pure. */
2216
+ export function buildAgentBuildArgsOptions({ input, systemPromptOverlay }) {
2217
+ const permissionMode = input.permissionMode ?? 'restricted';
2218
+ if (permissionMode !== 'restricted' && permissionMode !== 'bypass') {
2219
+ throw new AgentExecutorError({ code: 'AGENT_PERMISSION_MODE_INVALID', message: 'AgentExecutor: permissionMode must be restricted or bypass' });
2220
+ }
2192
2221
  const hasOverlay = systemPromptOverlay !== undefined && systemPromptOverlay !== null;
2193
2222
  const options = {
2194
2223
  ...(input.model !== undefined ? { model: input.model } : {}),
2195
2224
  ...(input.reasoning !== undefined ? { reasoning: input.reasoning } : {}),
2196
- ...(input.permissionMode !== undefined ? { permissionMode: input.permissionMode } : {}),
2225
+ permissionMode,
2197
2226
  ...(input.disallowedTools !== undefined ? { disallowedTools: input.disallowedTools } : {}),
2198
2227
  ...(input.allowedTools !== undefined ? { allowedTools: input.allowedTools } : {}),
2199
2228
  ...(input.settingSources !== undefined ? { settingSources: input.settingSources } : {}),
@@ -2314,10 +2343,15 @@ export function resolveSystemPromptOverlayDelivery(input) {
2314
2343
  * therefore only ever reach the 7 defs whose `buildArgs` reads its first argument at all — the
2315
2344
  * other 17 declare it `_prompt` and discard it, which is exactly how the overlay used to go missing.
2316
2345
  */
2317
- export async function buildRunArgs(input, deps) {
2346
+ export async function buildRunArgs({ input, deps }) {
2318
2347
  try {
2319
2348
  const delivery = input.overlayDelivery;
2320
- const args = input.def.buildArgs(delivery.prompt, [...(input.imagePaths ?? [])], input.imageDelivery.extraAllowedDirs === undefined ? undefined : [...input.imageDelivery.extraAllowedDirs], buildAgentBuildArgsOptions(input.runInput, input.systemPromptOverlay), input.runtimeContext);
2349
+ const options = buildAgentBuildArgsOptions({ input: input.runInput, systemPromptOverlay: input.systemPromptOverlay });
2350
+ const args = input.def.buildArgs({ prompt: delivery.prompt, imagePaths: [...(input.imagePaths ?? [])] }, {
2351
+ ...(input.imageDelivery.extraAllowedDirs === undefined ? {} : { extraAllowedDirs: [...input.imageDelivery.extraAllowedDirs] }),
2352
+ ...(options === undefined ? {} : { options }),
2353
+ ...(input.runtimeContext === undefined ? {} : { runtimeContext: input.runtimeContext }),
2354
+ });
2321
2355
  // `'append-flag'` delivery's extra argv (empty for every other def/strategy) is appended after
2322
2356
  // whatever the def's own `buildArgs` returned — safe because it is only ever non-empty for a
2323
2357
  // `promptViaStdin` def with no trailing positional argv (`claude`/`pi` today; see
@@ -2329,11 +2363,11 @@ export async function buildRunArgs(input, deps) {
2329
2363
  }
2330
2364
  catch (err) {
2331
2365
  await deps.releaseStagedResources();
2332
- return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not build launch arguments for agent "${input.def.id}": ${errorMessage(err)}`);
2366
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not build launch arguments for agent "${input.def.id}": ${errorMessage(err)}` });
2333
2367
  }
2334
2368
  }
2335
2369
  /** Phase 10: mechanism 1 of 5's one effect — stages this run's own `.mcp.json`, returning the path `cleanupStagedFiles` should later remove (`undefined` for every other mechanism / unconfigured host). */
2336
- export async function writeMcpJsonIfNeeded(input, deps) {
2370
+ export async function writeMcpJsonIfNeeded({ input, deps }) {
2337
2371
  if (input.mcpBridge?.kind !== 'claude-mcp-json' || deps.mcpJsonInjection === undefined) {
2338
2372
  return undefined;
2339
2373
  }
@@ -2343,7 +2377,7 @@ export async function writeMcpJsonIfNeeded(input, deps) {
2343
2377
  }
2344
2378
  catch (err) {
2345
2379
  await deps.releaseStagedResources();
2346
- return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not write .mcp.json for agent "${input.def.id}": ${errorMessage(err)}`);
2380
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not write .mcp.json for agent "${input.def.id}": ${errorMessage(err)}` });
2347
2381
  }
2348
2382
  }
2349
2383
  /**
@@ -2364,7 +2398,7 @@ export async function writeMcpJsonIfNeeded(input, deps) {
2364
2398
  * @complexity O(1) plus one directory creation and one file write.
2365
2399
  * @overallScore 100/100
2366
2400
  */
2367
- export async function prepareSystemPromptOverlayFileIfNeeded(input, deps) {
2401
+ export async function prepareSystemPromptOverlayFileIfNeeded({ input, deps }) {
2368
2402
  if (input.def.systemPromptDelivery?.strategy !== 'config-instructions-file' ||
2369
2403
  typeof input.overlay !== 'string' ||
2370
2404
  input.overlay.length === 0) {
@@ -2384,7 +2418,7 @@ export async function prepareSystemPromptOverlayFileIfNeeded(input, deps) {
2384
2418
  }
2385
2419
  catch (err) {
2386
2420
  await deps.releaseStagedResources();
2387
- return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a system-prompt overlay file for agent "${input.def.id}": ${errorMessage(err)}`);
2421
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a system-prompt overlay file for agent "${input.def.id}": ${errorMessage(err)}` });
2388
2422
  }
2389
2423
  }
2390
2424
  /**
@@ -2397,16 +2431,16 @@ export async function prepareSystemPromptOverlayFileIfNeeded(input, deps) {
2397
2431
  * @complexity O(1) plus {@link prepareCodexHomeForRun}'s own cost.
2398
2432
  * @overallScore 100/100
2399
2433
  */
2400
- export async function prepareCodexHomeIfNeeded(input, deps) {
2434
+ export async function prepareCodexHomeIfNeeded({ input, deps }) {
2401
2435
  if (input.mcpBridge?.kind !== 'codex-toml' || deps.mcpJsonInjection === undefined) {
2402
2436
  return null;
2403
2437
  }
2404
2438
  try {
2405
- return await prepareCodexHomeForRun(input.runId, input.mcpBridge.serverEntry, resolveSourceCodexHomeDir(deps.hostEnv), resolveCodexHomeSeams(deps.mcpJsonInjection));
2439
+ return await prepareCodexHomeForRun(input.runId, input.mcpBridge.serverEntry, resolveSourceCodexHomeDir({ hostEnv: deps.hostEnv }), resolveCodexHomeSeams(deps.mcpJsonInjection));
2406
2440
  }
2407
2441
  catch (err) {
2408
2442
  await deps.releaseStagedResources();
2409
- return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a CODEX_HOME for agent "${input.def.id}": ${errorMessage(err)}`);
2443
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a CODEX_HOME for agent "${input.def.id}": ${errorMessage(err)}` });
2410
2444
  }
2411
2445
  }
2412
2446
  /**
@@ -2432,25 +2466,25 @@ export async function prepareCodexHomeIfNeeded(input, deps) {
2432
2466
  * @param deps.hostEnv - The daemon's own environment, threaded through to {@link resolveSourceClaudeConfigDir} rather than read from a module-level `process.env`, matching {@link prepareCodexHomeIfNeeded}'s identical testability reasoning.
2433
2467
  * @complexity O(1) plus {@link prepareClaudeConfigDirForRun}'s own cost.
2434
2468
  */
2435
- export async function prepareClaudeConfigDirIfNeeded(input, deps) {
2469
+ export async function prepareClaudeConfigDirIfNeeded({ input, deps }) {
2436
2470
  if (input.def.id !== 'claude' || !deps.enabled) {
2437
2471
  return null;
2438
2472
  }
2439
2473
  try {
2440
- return await prepareClaudeConfigDirForRun(input.runId, resolveSourceClaudeConfigDir(deps.hostEnv), resolveClaudeConfigDirSeams(deps.claudeConfigDirIsolation));
2474
+ return await prepareClaudeConfigDirForRun(input.runId, resolveSourceClaudeConfigDir({ hostEnv: deps.hostEnv }), resolveClaudeConfigDirSeams(deps.claudeConfigDirIsolation));
2441
2475
  }
2442
2476
  catch (err) {
2443
2477
  await deps.releaseStagedResources();
2444
- return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a CLAUDE_CONFIG_DIR for agent "${input.def.id}": ${errorMessage(err)}`);
2478
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a CLAUDE_CONFIG_DIR for agent "${input.def.id}": ${errorMessage(err)}` });
2445
2479
  }
2446
2480
  }
2447
2481
  /** Phase 11: post-`buildArgs` guard for argv-bound defs whose resolved binary is a Windows shim/.exe — a no-op off-Windows and for non-argv-bound defs. */
2448
- export async function guardWindowsCommandLineBudget(input, deps) {
2449
- const windowsBudgetError = checkWindowsCmdShimCommandLineBudget(input.def, input.launchPath, input.args) ??
2450
- checkWindowsDirectExeCommandLineBudget(input.def, input.launchPath, input.args);
2482
+ export async function guardWindowsCommandLineBudget({ input, deps }) {
2483
+ const windowsBudgetError = checkWindowsCmdShimCommandLineBudget({ def: input.def, resolvedBin: input.launchPath, args: input.args }) ??
2484
+ checkWindowsDirectExeCommandLineBudget({ def: input.def, resolvedBin: input.launchPath, args: input.args });
2451
2485
  if (windowsBudgetError) {
2452
2486
  await deps.releaseStagedResources();
2453
- await deps.failBeforeSpawn(input.runId, 'AGENT_PROMPT_TOO_LARGE', windowsBudgetError.message);
2487
+ await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_PROMPT_TOO_LARGE', message: windowsBudgetError.message });
2454
2488
  }
2455
2489
  }
2456
2490
  /**
@@ -2472,7 +2506,7 @@ export async function guardWindowsCommandLineBudget(input, deps) {
2472
2506
  * which are safe to make asynchronous since no child (and hence no listener race) exists yet.
2473
2507
  * @complexity O(1) plus `spawn`'s own cost.
2474
2508
  */
2475
- export function spawnAgentChildProcess(input, deps) {
2509
+ export function spawnAgentChildProcess({ input, deps }) {
2476
2510
  try {
2477
2511
  return {
2478
2512
  kind: 'ok',
@@ -2489,27 +2523,28 @@ export function spawnAgentChildProcess(input, deps) {
2489
2523
  }
2490
2524
  }
2491
2525
  /** Named predicate replacing an inline `streamFormat === 'acp-json-rpc' || streamFormat === 'pi-rpc'` check — the two formats that own their own prompt/event protocol and skip `wireChildLifecycle`. */
2492
- export function isStdinDrivenFormat(streamFormat) {
2526
+ export function isStdinDrivenFormat(args) {
2527
+ const { streamFormat } = args;
2493
2528
  return streamFormat !== 'acp-json-rpc' && streamFormat !== 'pi-rpc';
2494
2529
  }
2495
2530
  /** Phase 13: awaits spawn confirmation, routing a failure through the same `failBeforeSpawn` shape every earlier guard uses. */
2496
- export async function confirmChildSpawned(input, deps) {
2531
+ export async function confirmChildSpawned({ input, deps }) {
2497
2532
  try {
2498
2533
  await waitForSpawnOrError(input.child);
2499
2534
  }
2500
2535
  catch (err) {
2501
2536
  await deps.releaseStagedResources();
2502
- await deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: failed to spawn agent "${input.def.id}": ${errorMessage(err)}`);
2537
+ await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: failed to spawn agent "${input.def.id}": ${errorMessage(err)}` });
2503
2538
  }
2504
2539
  }
2505
2540
  /** Phase 14: starts a `runtimeLock` def's handoff watcher once a live process exists to consume the locked side effect — a no-op when the def declared no `waitForHandoff`. Deliberately not awaited; see `RuntimeLockHold.waitForHandoff`'s own doc. */
2506
- export function armHandoffWatcher(runtimeLockHold, handoffInput, release) {
2541
+ export function armHandoffWatcher({ runtimeLockHold, handoffInput, release }) {
2507
2542
  if (!runtimeLockHold?.waitForHandoff)
2508
2543
  return;
2509
2544
  void runtimeLockHold.waitForHandoff(handoffInput).then(release, release);
2510
2545
  }
2511
2546
  /** Phase 15 (ACP branch): attaches the ACP session, escalating process-tree teardown and failing the run through `failBeforeSpawn` on an attach-time throw. */
2512
- export async function runAcpDispatch(input, deps) {
2547
+ export async function runAcpDispatch({ input, deps }) {
2513
2548
  try {
2514
2549
  wireAcpLifecycle({
2515
2550
  runId: input.runId,
@@ -2541,11 +2576,11 @@ export async function runAcpDispatch(input, deps) {
2541
2576
  // fired-and-forgotten (SEC-007: "await where lifecycle ordering allows it").
2542
2577
  await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'acp-attach-failure', deps.onCleanupFailure);
2543
2578
  await deps.releaseStagedResources();
2544
- await deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not attach ACP session for agent "${input.agentId}": ${errorMessage(err)}`);
2579
+ await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach ACP session for agent "${input.agentId}": ${errorMessage(err)}` });
2545
2580
  }
2546
2581
  }
2547
2582
  /** Phase 15 (pi-rpc branch): same discipline as {@link runAcpDispatch}, for the one `'pi-rpc'` def. */
2548
- export async function runPiRpcDispatch(input, deps) {
2583
+ export async function runPiRpcDispatch({ input, deps }) {
2549
2584
  try {
2550
2585
  wirePiRpcLifecycle({
2551
2586
  runId: input.runId,
@@ -2572,10 +2607,11 @@ export async function runPiRpcDispatch(input, deps) {
2572
2607
  // fire-and-forget (SEC-007).
2573
2608
  await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'pi-rpc-attach-failure', deps.onCleanupFailure);
2574
2609
  await deps.releaseStagedResources();
2575
- await deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not attach pi-rpc session for agent "${input.agentId}": ${errorMessage(err)}`);
2610
+ await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach pi-rpc session for agent "${input.agentId}": ${errorMessage(err)}` });
2576
2611
  }
2577
2612
  }
2578
- export function createAgentExecutor(options) {
2613
+ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
2614
+ const options = { ...requiredArgs, ...optionalArgs };
2579
2615
  const lifecycle = options.lifecycle;
2580
2616
  const { getAgentDef: getAgentDefFn, resolveAgentLaunch: resolveAgentLaunchFn, ensureAgentCapabilities: ensureAgentCapabilitiesFn, applyAgentLaunchEnv: applyAgentLaunchEnvFn, attachAcpSession: attachAcpSessionFn, attachPiRpcSession: attachPiRpcSessionFn, preparePromptFileForAgent: preparePromptFileForAgentFn, prepareAgentLogFile: prepareAgentLogFileFn, } = resolveAgentRuntimeDeps(options);
2581
2617
  const { createCommandInvocation: createCommandInvocationFn, spawn: spawnFn, listProcessSnapshots: listProcessSnapshotsFn, collectProcessTreePids: collectProcessTreePidsFn, stopProcesses: stopProcessesFn, } = resolveProcessDeps(options);
@@ -2599,16 +2635,16 @@ export function createAgentExecutor(options) {
2599
2635
  * @complexity O(1) plus `lifecycle.finish()`'s own cost.
2600
2636
  * @overallScore 100/100
2601
2637
  */
2602
- async function failBeforeSpawn(runId, code, message) {
2638
+ async function failBeforeSpawn({ runId, code, message }) {
2603
2639
  // The reason goes on the run's own stream before `end`, not only into the thrown error: the
2604
2640
  // thrown error reaches the server log, while a chat client sees only this stream. Without it
2605
2641
  // the user got a bare "Run failed". Best-effort: a stream that will not take the event must not
2606
2642
  // stop the run from finishing.
2607
2643
  await lifecycle
2608
- .emit(runId, { event: 'error', data: { message: `The assistant could not start: ${message.replace(/^AgentExecutor:\s*/, '')}` } })
2644
+ .emit({ runId: runId, input: { event: 'error', data: { message: `The assistant could not start: ${message.replace(/^AgentExecutor:\s*/, '')}` } } })
2609
2645
  .catch(() => undefined);
2610
2646
  await lifecycle.finish({ runId, status: 'failed', code: null, signal: null, resumable: false });
2611
- throw new AgentExecutorError(code, message);
2647
+ throw new AgentExecutorError({ code: code, message: message });
2612
2648
  }
2613
2649
  /**
2614
2650
  * `AgentExecutor.run()` — see that interface method's own doc for the
@@ -2621,8 +2657,12 @@ export function createAgentExecutor(options) {
2621
2657
  * @complexity O(1) setup (registry lookup, launch resolution, one spawn call); steady-state cost thereafter belongs to {@link wireChildLifecycle}.
2622
2658
  * @overallScore 100/100
2623
2659
  */
2624
- async function run(input) {
2625
- const { def, streamFormat } = await resolveDefAndStreamFormat({ runId: input.runId, agentId: input.agentId }, { getAgentDef: getAgentDefFn, failBeforeSpawn });
2660
+ async function run(requiredArgs, optionalArgs = {}) {
2661
+ const input = { ...requiredArgs, ...optionalArgs, permissionMode: optionalArgs.permissionMode ?? 'restricted' };
2662
+ if (input.permissionMode !== 'restricted' && input.permissionMode !== 'bypass') {
2663
+ return failBeforeSpawn({ runId: input.runId, code: 'AGENT_PERMISSION_MODE_INVALID', message: 'AgentExecutor: permissionMode must be restricted or bypass' });
2664
+ }
2665
+ const { def, streamFormat } = await resolveDefAndStreamFormat({ input: { runId: input.runId, agentId: input.agentId }, deps: { getAgentDef: getAgentDefFn, failBeforeSpawn } });
2626
2666
  // Computed once, before anything downstream ever looks at "the prompt" or "the allowed
2627
2667
  // dirs" — a no-op (`{prompt: input.prompt, extraAllowedDirs: input.extraAllowedDirs}`,
2628
2668
  // literally unchanged) unless `def.imageDelivery === 'prompt-path'` AND `input.imagePaths`
@@ -2633,7 +2673,7 @@ export function createAgentExecutor(options) {
2633
2673
  // calls further down deliberately keep reading `input.prompt` verbatim, since those two
2634
2674
  // defs' own native protocol already delivers the image and must never also get this
2635
2675
  // treatment (the double-delivery hazard this mechanism exists to avoid).
2636
- const imageDelivery = await resolveImageDeliveryAndArgvBudget({ runId: input.runId, def, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs }, { failBeforeSpawn });
2676
+ const imageDelivery = await resolveImageDeliveryAndArgvBudget({ input: { runId: input.runId, def, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs }, deps: { failBeforeSpawn } });
2637
2677
  // Phase 8, hoisted deliberately above EVERY step that writes or sends the prompt. Four
2638
2678
  // different channels carry a prompt in this driver, and they do not all run at the same point:
2639
2679
  // the `promptViaFile` staging below writes its file BEFORE `buildArgs`, `buildArgs` itself runs
@@ -2652,7 +2692,10 @@ export function createAgentExecutor(options) {
2652
2692
  // because every `PromptAugmenter.systemOverlay()` implementation this seam has today wants the
2653
2693
  // same overlay on every turn, not a first-turn-only one; a caller that needs finer-grained turn
2654
2694
  // numbering can track it itself and ignore this arg.
2655
- const preStagingRuntimeContext = computeRuntimeContext(null, null, null, input.resumeSessionId, input.newSessionId);
2695
+ const preStagingRuntimeContext = computeRuntimeContext({ preparedPromptFile: null, preparedLogFile: null, mcpBridge: null }, {
2696
+ ...(input.resumeSessionId === undefined ? {} : { resumeSessionId: input.resumeSessionId }),
2697
+ ...(input.newSessionId === undefined ? {} : { newSessionId: input.newSessionId }),
2698
+ });
2656
2699
  const systemPromptOverlay = computeSystemPromptOverlay(promptAugmenter, def.id, preStagingRuntimeContext);
2657
2700
  const overlayDelivery = resolveSystemPromptOverlayDelivery({
2658
2701
  defId: def.id,
@@ -2666,14 +2709,14 @@ export function createAgentExecutor(options) {
2666
2709
  // The def's own declared env (e.g. claude's `ENABLE_TOOL_SEARCH=false`) is a default the run's
2667
2710
  // env can override. Detection already spawns with it (`detection.ts`); without this the run
2668
2711
  // spawn silently dropped it. Declared by the def itself, so it never widens SEC-001's allowlist.
2669
- const resolvedEnv = { ...(def.env ?? {}), ...resolveRunEnv(input, process.env) };
2670
- const launch = await resolveLaunch({ runId: input.runId, def, resolvedEnv }, { resolveAgentLaunch: resolveAgentLaunchFn, failBeforeSpawn });
2671
- const spawnEnv = applyAgentLaunchEnvFn({ ...resolvedEnv }, launch);
2712
+ const resolvedEnv = { ...(def.env ?? {}), ...resolveRunEnv({ input: input, hostEnv: process.env }) };
2713
+ const launch = await resolveLaunch({ input: { runId: input.runId, def, resolvedEnv }, deps: { resolveAgentLaunch: resolveAgentLaunchFn, failBeforeSpawn } });
2714
+ const spawnEnv = applyAgentLaunchEnvFn({ env: { ...resolvedEnv }, launch });
2672
2715
  // Fill the def's `--help` capability gate before `buildArgs` reads it. Without this, only a
2673
2716
  // host that happened to call `detectAgents` in THIS process ever had the gate filled, so e.g.
2674
2717
  // `claude` never got `--include-partial-messages` (no streamed text). Probes once per binary;
2675
2718
  // never rejects (a failed probe keeps the safe no-optional-flags baseline).
2676
- await ensureAgentCapabilitiesFn(def, launch.launchPath, spawnEnv);
2719
+ await ensureAgentCapabilitiesFn({ def, launchPath: launch.launchPath, env: spawnEnv });
2677
2720
  // Stage a promptViaFile def's (grok-build) prompt to a temp file before buildArgs runs — its
2678
2721
  // buildArgs throws without runtimeContext.promptFilePath. A no-op (returns null) for every
2679
2722
  // def without promptViaFile: true (preparePromptFileForAgent's own guard).
@@ -2681,13 +2724,13 @@ export function createAgentExecutor(options) {
2681
2724
  // `overlayDelivery.prompt`, not `imageDelivery.prompt`: for a `promptViaFile` def this file IS
2682
2725
  // the prompt transport — its `buildArgs` declares `_prompt` and passes only the path — so the
2683
2726
  // overlay has to be in the bytes written here or the CLI never sees it at all.
2684
- const preparedPromptFile = await stagePromptFile({ runId: input.runId, def, prompt: overlayDelivery.prompt }, { preparePromptFileForAgent: preparePromptFileForAgentFn, failBeforeSpawn });
2727
+ const preparedPromptFile = await stagePromptFile({ input: { runId: input.runId, def, prompt: overlayDelivery.prompt }, deps: { preparePromptFileForAgent: preparePromptFileForAgentFn, failBeforeSpawn } });
2685
2728
  // Stage a needsAgentLogFile def's (antigravity) diagnostic-log path, on the same terms and at
2686
2729
  // the same point as the prompt file above: before buildArgs, since buildArgs is what turns the
2687
2730
  // path into a `--log-file <path>` argument. A no-op (returns null) for every def without
2688
2731
  // `needsAgentLogFile: true` (prepareAgentLogFile's own guard). Sequenced after the prompt file
2689
2732
  // rather than concurrently so the failure path above has exactly one thing to clean up.
2690
- const preparedLogFile = await stageLogFile({ runId: input.runId, def, preparedPromptFile }, { prepareAgentLogFile: prepareAgentLogFileFn, failBeforeSpawn });
2733
+ const preparedLogFile = await stageLogFile({ input: { runId: input.runId, def, preparedPromptFile }, deps: { prepareAgentLogFile: prepareAgentLogFileFn, failBeforeSpawn } });
2691
2734
  // Cleaned up after the child exits (wireChildLifecycle/wireAcpLifecycle/wirePiRpcLifecycle's
2692
2735
  // close handlers) and on every pre-spawn/spawn-failure path below — a leaked temp file
2693
2736
  // containing the full prompt, or whatever the CLI chose to write into its log, is a
@@ -2732,7 +2775,7 @@ export function createAgentExecutor(options) {
2732
2775
  if (writtenMcpJsonPath !== undefined) {
2733
2776
  const mcpJsonFileToRemove = writtenMcpJsonPath;
2734
2777
  writtenMcpJsonPath = undefined;
2735
- await removeMcpJsonFileFn(mcpJsonFileToRemove);
2778
+ await removeMcpJsonFileFn({ path: mcpJsonFileToRemove });
2736
2779
  }
2737
2780
  if (preparedCodexHome) {
2738
2781
  const codexHomeToRemove = preparedCodexHome;
@@ -2755,8 +2798,11 @@ export function createAgentExecutor(options) {
2755
2798
  // resolving here means the per-run bearer credential is minted exactly once no matter which of
2756
2799
  // the five mechanisms ends up carrying it. `null` for an unconfigured host or a def declaring
2757
2800
  // no strategy — see `buildMcpBridgeDelivery`'s doc.
2758
- const mcpBridge = await resolveMcpBridgeForRun({ runId: input.runId, cwd: input.cwd, def }, { mcpJsonInjection, cleanupStagedFiles, failBeforeSpawn });
2759
- const runtimeContext = computeRuntimeContext(preparedPromptFile, preparedLogFile, mcpBridge, input.resumeSessionId, input.newSessionId);
2801
+ const mcpBridge = await resolveMcpBridgeForRun({ input: { runId: input.runId, cwd: input.cwd, def }, deps: { mcpJsonInjection, cleanupStagedFiles, failBeforeSpawn } });
2802
+ const runtimeContext = computeRuntimeContext({ preparedPromptFile: preparedPromptFile, preparedLogFile: preparedLogFile, mcpBridge: mcpBridge }, {
2803
+ ...(input.resumeSessionId === undefined ? {} : { resumeSessionId: input.resumeSessionId }),
2804
+ ...(input.newSessionId === undefined ? {} : { newSessionId: input.newSessionId }),
2805
+ });
2760
2806
  // A `runtimeLock` def's buildArgs mutates process-global state its own CLI reads back at
2761
2807
  // startup, so the mutex must be held from before buildArgs until the spawned child has
2762
2808
  // demonstrably consumed it — see `RuntimeLock`'s own doc for the concrete race. Undefined for
@@ -2787,60 +2833,65 @@ export function createAgentExecutor(options) {
2787
2833
  // `Error` — breaking this driver's "never a bare throw, always an `AgentExecutorError`" contract
2788
2834
  // — and left the run `'running'` forever while still holding the process-global mutex and both
2789
2835
  // staged files, so no later run of that def could ever acquire the lock either.
2790
- const { args, envOverrides: systemPromptEnvOverrides } = await buildRunArgs({ runId: input.runId, def, imageDelivery, imagePaths: input.imagePaths, runInput: input, systemPromptOverlay, overlayDelivery, runtimeContext }, { releaseStagedResources, failBeforeSpawn });
2836
+ const { args, envOverrides: systemPromptEnvOverrides } = await buildRunArgs({ input: { runId: input.runId, def, imageDelivery, imagePaths: input.imagePaths, runInput: input, systemPromptOverlay, overlayDelivery, runtimeContext }, deps: { releaseStagedResources, failBeforeSpawn } });
2791
2837
  // Mechanism 1 of 5's one effect — stage this run's own MCP config file (run-scoped, see
2792
2838
  // `mcpJsonPathForRun`) before spawn so the `--mcp-config <path>` argv buildArgs just produced
2793
2839
  // points at a real file. Skipped entirely for the other four mechanisms and whenever no bridge
2794
2840
  // was resolved at all. `writtenMcpJsonPath` is set only once the write actually happens, so
2795
2841
  // `cleanupStagedFiles` knows there is a live-token file to remove afterward.
2796
- writtenMcpJsonPath = await writeMcpJsonIfNeeded({ runId: input.runId, cwd: input.cwd, def, mcpBridge }, { mcpJsonInjection, releaseStagedResources, failBeforeSpawn });
2842
+ writtenMcpJsonPath = await writeMcpJsonIfNeeded({ input: { runId: input.runId, cwd: input.cwd, def, mcpBridge }, deps: { mcpJsonInjection, releaseStagedResources, failBeforeSpawn } });
2797
2843
  // Mechanism 5 of 5's one effect — stage this run's scratch `CODEX_HOME` directory. Skipped
2798
2844
  // entirely for the other four mechanisms and whenever no bridge was resolved at all.
2799
2845
  // `codex.ts`'s `buildArgs` needs no argv change for this (CODEX_HOME is an env var, not a flag),
2800
2846
  // so — unlike the `.mcp.json` staging above — this can run after `buildArgs` with no ordering
2801
2847
  // constraint of its own; it is placed here only to keep the two staging steps adjacent.
2802
- preparedCodexHome = await prepareCodexHomeIfNeeded({ runId: input.runId, def, mcpBridge }, { mcpJsonInjection, hostEnv: process.env, releaseStagedResources, failBeforeSpawn });
2848
+ preparedCodexHome = await prepareCodexHomeIfNeeded({ input: { runId: input.runId, def, mcpBridge }, deps: { mcpJsonInjection, hostEnv: process.env, releaseStagedResources, failBeforeSpawn } });
2803
2849
  // Finding 1 of SEC-assistant-env-isolation-2026-09-07's one effect — stage this run's scratch
2804
2850
  // `CLAUDE_CONFIG_DIR` directory. Unconditional for a `claude`-id run (unlike CODEX_HOME above,
2805
2851
  // this does not depend on `mcpJsonInjection` being configured at all — see
2806
2852
  // `prepareClaudeConfigDirIfNeeded`'s own doc for why). Placed here only to stay adjacent to the
2807
2853
  // other pre-`computeChildEnv` staging steps; `claude.ts`'s `buildArgs` needs no argv change for
2808
2854
  // this (CLAUDE_CONFIG_DIR is an env var, not a flag), same as CODEX_HOME.
2809
- preparedClaudeConfigDir = await prepareClaudeConfigDirIfNeeded({ runId: input.runId, def }, {
2810
- enabled: claudeConfigDirIsolationEnabled,
2811
- claudeConfigDirIsolation,
2812
- hostEnv: process.env,
2813
- releaseStagedResources,
2814
- failBeforeSpawn,
2815
- });
2855
+ preparedClaudeConfigDir = await prepareClaudeConfigDirIfNeeded({ input: { runId: input.runId, def }, deps: {
2856
+ enabled: claudeConfigDirIsolationEnabled,
2857
+ claudeConfigDirIsolation,
2858
+ hostEnv: process.env,
2859
+ releaseStagedResources,
2860
+ failBeforeSpawn,
2861
+ } });
2816
2862
  // `'config-instructions-file'`'s one effect — stage the overlay to a temp file so
2817
2863
  // `computeChildEnv` below has a real path to merge into that def's `instructions` array. A
2818
2864
  // no-op (`null`) for every other def/strategy or a run with no overlay at all. Independent of
2819
2865
  // `mcpBridge`/`preparedCodexHome` above (a different strategy field entirely), so placed here
2820
2866
  // only to stay adjacent to the other pre-`computeChildEnv` staging steps, not for any ordering
2821
2867
  // requirement between them.
2822
- preparedSystemPromptOverlayFile = await prepareSystemPromptOverlayFileIfNeeded({ runId: input.runId, def, overlay: systemPromptOverlay }, { releaseStagedResources, failBeforeSpawn });
2868
+ preparedSystemPromptOverlayFile = await prepareSystemPromptOverlayFileIfNeeded({ input: { runId: input.runId, def, overlay: systemPromptOverlay }, deps: { releaseStagedResources, failBeforeSpawn } });
2823
2869
  // Computed only now, not right after `mcpBridge` resolution: mechanism 5's directory path is
2824
2870
  // not known until the staging step directly above actually runs `mkdtemp` (see
2825
2871
  // `McpBridgeDelivery`'s `'codex-toml'` variant doc for why it cannot be pre-computed the way
2826
2872
  // `'claude-mcp-json'`'s deterministic path is). Nothing between the old, earlier call site and
2827
2873
  // here ever read `childEnv`, so moving the call cost nothing.
2828
- const childEnv = computeChildEnv(spawnEnv, mcpBridge, preparedCodexHome?.path, systemPromptEnvOverrides, preparedSystemPromptOverlayFile && def.systemPromptDelivery?.strategy === 'config-instructions-file'
2829
- ? { varName: def.systemPromptDelivery.varName, path: preparedSystemPromptOverlayFile.path }
2830
- : undefined, preparedClaudeConfigDir?.path);
2874
+ const childEnv = computeChildEnv({ spawnEnv: spawnEnv, mcpBridge: mcpBridge }, {
2875
+ ...(preparedCodexHome === null ? {} : { codexHomeDir: preparedCodexHome.path }),
2876
+ systemPromptEnvOverrides,
2877
+ ...(preparedSystemPromptOverlayFile && def.systemPromptDelivery?.strategy === 'config-instructions-file'
2878
+ ? { stagedInstructionsFile: { varName: def.systemPromptDelivery.varName, path: preparedSystemPromptOverlayFile.path } }
2879
+ : {}),
2880
+ ...(preparedClaudeConfigDir === null ? {} : { claudeConfigDir: preparedClaudeConfigDir.path }),
2881
+ });
2831
2882
  // Post-buildArgs guard for argv-bound defs whose resolved binary is a
2832
2883
  // Windows .cmd/.bat shim or a direct .exe: a prompt under the raw byte
2833
2884
  // budget can still expand past CreateProcess's command-line cap once
2834
2885
  // quote-escaped. Both are no-ops off-Windows / for non-argv-bound defs.
2835
- await guardWindowsCommandLineBudget({ runId: input.runId, def, launchPath: launch.launchPath, args }, { releaseStagedResources, failBeforeSpawn });
2886
+ await guardWindowsCommandLineBudget({ input: { runId: input.runId, def, launchPath: launch.launchPath, args }, deps: { releaseStagedResources, failBeforeSpawn } });
2836
2887
  const invocation = createCommandInvocationFn({ command: launch.launchPath, args, env: childEnv });
2837
2888
  // Kept a synchronous call (no `await`) on purpose — see `spawnAgentChildProcess`'s own doc for
2838
2889
  // the microtask-timing race this avoids. The error branch's own cleanup/failBeforeSpawn calls are
2839
2890
  // async, which is fine: no child exists yet on that path, so nothing is racing a listener.
2840
- const spawnResult = spawnAgentChildProcess({ cwd: input.cwd, childEnv, invocation }, { spawn: spawnFn });
2891
+ const spawnResult = spawnAgentChildProcess({ input: { cwd: input.cwd, childEnv, invocation }, deps: { spawn: spawnFn } });
2841
2892
  if (spawnResult.kind === 'error') {
2842
2893
  await releaseStagedResources();
2843
- return failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: spawn threw synchronously for agent "${def.id}": ${errorMessage(spawnResult.error)}`);
2894
+ return failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: spawn threw synchronously for agent "${def.id}": ${errorMessage(spawnResult.error)}` });
2844
2895
  }
2845
2896
  const child = spawnResult.child;
2846
2897
  // Registered before the spawn-confirmation await below, for the same reason
@@ -2850,11 +2901,12 @@ export function createAgentExecutor(options) {
2850
2901
  // grandchild inheriting them can delay arbitrarily). A spawn that never produced a process at
2851
2902
  // all emits no `'exit'`, and is covered instead by `releaseStagedResources` on the reject path.
2852
2903
  child.once('exit', releaseRuntimeLock);
2853
- const stdinHandle = isStdinDrivenFormat(streamFormat)
2904
+ const formatForStdin = { streamFormat };
2905
+ const stdinHandle = isStdinDrivenFormat(formatForStdin)
2854
2906
  ? wireChildLifecycle({
2855
2907
  runId: input.runId,
2856
2908
  def,
2857
- streamFormat,
2909
+ streamFormat: formatForStdin.streamFormat,
2858
2910
  child,
2859
2911
  lifecycle,
2860
2912
  listProcessSnapshots: listProcessSnapshotsFn,
@@ -2866,75 +2918,76 @@ export function createAgentExecutor(options) {
2866
2918
  continuation,
2867
2919
  classifyFailure,
2868
2920
  bufferedStdoutMaxBytes,
2921
+ expectsJiniBridge: writtenMcpJsonPath !== undefined,
2869
2922
  })
2870
2923
  : null;
2871
- await confirmChildSpawned({ runId: input.runId, def, child }, { releaseStagedResources, failBeforeSpawn });
2924
+ await confirmChildSpawned({ input: { runId: input.runId, def, child }, deps: { releaseStagedResources, failBeforeSpawn } });
2872
2925
  // Now — and only now — is there a live process that could consume the locked side effect, so
2873
2926
  // this is where a def's handoff watcher starts. Deliberately not awaited: the whole point is to
2874
2927
  // release the lock as soon as the child confirms the handoff, in parallel with this run
2875
2928
  // continuing. Rejection releases too — a lock stuck open because a watcher threw is strictly
2876
2929
  // worse than releasing early (see `RuntimeLockHold.waitForHandoff`'s own doc).
2877
- armHandoffWatcher(runtimeLockHold, { logFilePath: preparedLogFile?.path, model: selectedModel, processExited: processExitedController.signal }, releaseRuntimeLock);
2930
+ armHandoffWatcher({ runtimeLockHold: runtimeLockHold, handoffInput: { logFilePath: preparedLogFile?.path, model: selectedModel, processExited: processExitedController.signal }, release: releaseRuntimeLock });
2878
2931
  if (streamFormat === 'acp-json-rpc') {
2879
- await runAcpDispatch({
2880
- runId: input.runId,
2881
- agentId: def.id,
2882
- child,
2883
- // `overlayDelivery.promptPrefix` applied to `input.prompt`, NOT `overlayDelivery.prompt`:
2884
- // this call site deliberately sends the raw input prompt rather than the image-delivery
2885
- // rewrite (see `resolveImageDeliveryAndArgvBudget`'s call site above — an ACP def delivers
2886
- // images natively and must never also get `'prompt-path'`'s appended paths), and the prefix
2887
- // is the part of the overlay decision that applies to whatever prompt text a transport was
2888
- // already sending. `''` for `reasonix` (env-var) and any resumed ACP session, so those keep
2889
- // sending byte-identical text and never receive the overlay twice.
2890
- prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
2891
- cwd: input.cwd,
2892
- model: input.model,
2893
- imagePaths: input.imagePaths ?? [],
2894
- envFormat: def.acpMcpEnvFormat,
2895
- mcpBridge,
2896
- }, {
2897
- lifecycle,
2898
- attachAcpSession: attachAcpSessionFn,
2899
- onPermissionRequest: options.acpPermissionHandler,
2900
- listProcessSnapshots: listProcessSnapshotsFn,
2901
- collectProcessTreePids: collectProcessTreePidsFn,
2902
- stopProcesses: stopProcessesFn,
2903
- onCleanupFailure: onCleanupFailureFn,
2904
- cleanupStagedFiles,
2905
- journal,
2906
- classifyFailure,
2907
- releaseStagedResources,
2908
- failBeforeSpawn,
2909
- });
2932
+ await runAcpDispatch({ input: {
2933
+ runId: input.runId,
2934
+ agentId: def.id,
2935
+ child,
2936
+ // `overlayDelivery.promptPrefix` applied to `input.prompt`, NOT `overlayDelivery.prompt`:
2937
+ // this call site deliberately sends the raw input prompt rather than the image-delivery
2938
+ // rewrite (see `resolveImageDeliveryAndArgvBudget`'s call site above — an ACP def delivers
2939
+ // images natively and must never also get `'prompt-path'`'s appended paths), and the prefix
2940
+ // is the part of the overlay decision that applies to whatever prompt text a transport was
2941
+ // already sending. `''` for `reasonix` (env-var) and any resumed ACP session, so those keep
2942
+ // sending byte-identical text and never receive the overlay twice.
2943
+ prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
2944
+ cwd: input.cwd,
2945
+ model: input.model,
2946
+ imagePaths: input.imagePaths ?? [],
2947
+ envFormat: def.acpMcpEnvFormat,
2948
+ mcpBridge,
2949
+ }, deps: {
2950
+ lifecycle,
2951
+ attachAcpSession: attachAcpSessionFn,
2952
+ onPermissionRequest: options.acpPermissionHandler,
2953
+ listProcessSnapshots: listProcessSnapshotsFn,
2954
+ collectProcessTreePids: collectProcessTreePidsFn,
2955
+ stopProcesses: stopProcessesFn,
2956
+ onCleanupFailure: onCleanupFailureFn,
2957
+ cleanupStagedFiles,
2958
+ journal,
2959
+ classifyFailure,
2960
+ releaseStagedResources,
2961
+ failBeforeSpawn,
2962
+ } });
2910
2963
  return;
2911
2964
  }
2912
2965
  if (streamFormat === 'pi-rpc') {
2913
- await runPiRpcDispatch({
2914
- runId: input.runId,
2915
- agentId: def.id,
2916
- child,
2917
- // Same reasoning as the ACP call site above. `pi` declares an `'append-flag'` strategy,
2918
- // so its prefix is `''` and this stays byte-identical to the raw prompt — the overlay
2919
- // rides `--append-system-prompt` instead, exactly once.
2920
- prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
2921
- cwd: input.cwd,
2922
- model: input.model,
2923
- imagePaths: input.imagePaths ?? [],
2924
- uploadRoot: input.uploadRoot,
2925
- }, {
2926
- lifecycle,
2927
- attachPiRpcSession: attachPiRpcSessionFn,
2928
- listProcessSnapshots: listProcessSnapshotsFn,
2929
- collectProcessTreePids: collectProcessTreePidsFn,
2930
- stopProcesses: stopProcessesFn,
2931
- onCleanupFailure: onCleanupFailureFn,
2932
- cleanupStagedFiles,
2933
- journal,
2934
- classifyFailure,
2935
- releaseStagedResources,
2936
- failBeforeSpawn,
2937
- });
2966
+ await runPiRpcDispatch({ input: {
2967
+ runId: input.runId,
2968
+ agentId: def.id,
2969
+ child,
2970
+ // Same reasoning as the ACP call site above. `pi` declares an `'append-flag'` strategy,
2971
+ // so its prefix is `''` and this stays byte-identical to the raw prompt — the overlay
2972
+ // rides `--append-system-prompt` instead, exactly once.
2973
+ prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
2974
+ cwd: input.cwd,
2975
+ model: input.model,
2976
+ imagePaths: input.imagePaths ?? [],
2977
+ uploadRoot: input.uploadRoot,
2978
+ }, deps: {
2979
+ lifecycle,
2980
+ attachPiRpcSession: attachPiRpcSessionFn,
2981
+ listProcessSnapshots: listProcessSnapshotsFn,
2982
+ collectProcessTreePids: collectProcessTreePidsFn,
2983
+ stopProcesses: stopProcessesFn,
2984
+ onCleanupFailure: onCleanupFailureFn,
2985
+ cleanupStagedFiles,
2986
+ journal,
2987
+ classifyFailure,
2988
+ releaseStagedResources,
2989
+ failBeforeSpawn,
2990
+ } });
2938
2991
  return;
2939
2992
  }
2940
2993
  // The overlay reaches stdin only for a def that declares stdin as its prompt transport. The