@jini-ai/daemon 0.6.10 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.6.11
2
+
3
+ ### Patch Changes
4
+
5
+ - Mid-run message relay for MCP-bridged engines (Codex, ACP, Antigravity): `registerMessageRelayRoutes` on `@jini-ai/daemon/http`; mid-run messages queue instead of interrupting, and recovery waits on an uncertain probe.
6
+ - Site agents are confined to the site folder: Claude file tools are bounded to the site root and private temp dir, and attachments are staged inside the site (`stageAttachmentClaim`).
7
+ - Tool inputs apply a sole near-miss and declared input aliases; plugin-load outcomes are reported; `web_fetch` reads SVG/JS as text; `content_read` suggests its resource first; the browser falls back to system Chrome.
8
+ - Updated dependencies
9
+ - Updated dependencies
10
+ - Updated dependencies
11
+ - @jini-ai/core@0.6.11
12
+ - @jini-ai/protocol@0.6.11
13
+
1
14
  ## 0.6.10
2
15
 
3
16
  ### Patch Changes
@@ -4,8 +4,8 @@ import type { MessageAttachmentImage } from '../attachment-content.js';
4
4
  import type { probeMcpBridge } from './mcp-bridge-probe.js';
5
5
  import type { spawn as nodeSpawn, ChildProcess } from 'node:child_process';
6
6
  import type { Principal } from '@jini-ai/core';
7
- import type { RunAgentPayload, RunErrorPayload } from '@jini-ai/protocol';
8
- import type { AgentRuntimeState, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, prepareAgentLogFile, preparePromptFileForAgent, AcpMcpServerInput, AcpPermissionHandler, PromptAugmenter } from '@jini-ai/agent-runtime';
7
+ import type { RunAgentPayload, RunErrorPayload, MessageRelayVia, RelayedUserMessage } from '@jini-ai/protocol';
8
+ import type { AgentRuntimeState, applyAgentLaunchEnv, ensureAgentCapabilities, assertClaudeFilesystemSupport, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, prepareAgentLogFile, preparePromptFileForAgent, AcpMcpServerInput, AcpPermissionHandler, PromptAugmenter } from '@jini-ai/agent-runtime';
9
9
  import type { createCommandInvocation } from '@jini-ai/platform/command';
10
10
  import type { listProcessSnapshots, ProcessSnapshot, StopProcessesResult } from '@jini-ai/platform/process';
11
11
  import type { RunByteJournal } from '../continuation/journal.js';
@@ -94,7 +94,7 @@ export type AgentRuntimeEventTranslation = {
94
94
  readonly kind: 'ignored';
95
95
  };
96
96
  /** Machine-readable failure reasons `run()` can reject with — every one is preceded by a `lifecycle.finish({status:'failed'})` call (see index.ts module doc's Invariant section). */
97
- export type AgentExecutorErrorCode = 'NATIVE_TOOL_POLICY_UNSUPPORTED' | 'AGENT_MODEL_UNRESOLVED' | 'AGENT_PERMISSION_MODE_INVALID' | 'AGENT_NOT_FOUND' | 'AGENT_RUNTIME_UNSUPPORTED' | 'AGENT_BINARY_NOT_RESOLVED' | 'AGENT_SPAWN_FAILED' | 'AGENT_PROMPT_TOO_LARGE';
97
+ export type AgentExecutorErrorCode = 'FILESYSTEM_POLICY_UNSUPPORTED' | 'FILESYSTEM_POLICY_INVALID' | 'NATIVE_TOOL_POLICY_UNSUPPORTED' | 'AGENT_MODEL_UNRESOLVED' | 'AGENT_PERMISSION_MODE_INVALID' | 'AGENT_NOT_FOUND' | 'AGENT_RUNTIME_UNSUPPORTED' | 'AGENT_BINARY_NOT_RESOLVED' | 'AGENT_SPAWN_FAILED' | 'AGENT_PROMPT_TOO_LARGE';
98
98
  /** Thrown by `AgentExecutor.run()` on every failure path — never a bare `Error`, so callers can branch on `.code` instead of parsing `.message`. */
99
99
  export declare class AgentExecutorError extends Error {
100
100
  readonly code: AgentExecutorErrorCode;
@@ -104,6 +104,8 @@ export declare class AgentExecutorError extends Error {
104
104
  });
105
105
  }
106
106
  export interface AgentExecutorRunInput {
107
+ /** Host-selected mandatory filesystem boundary; no user grant may widen it. */
108
+ readonly filesystemRoot?: string;
107
109
  readonly runId: string;
108
110
  readonly agentId: string;
109
111
  readonly prompt: string;
@@ -205,28 +207,35 @@ export interface AgentExecutor {
205
207
  * underlying run is always already transitioned to `'failed'` via
206
208
  * `lifecycle.finish()` before this rejects (see index.ts module doc's Invariant).
207
209
  */
208
- run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "nativeToolAccess" | "nativeCapabilities" | "cliParity" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings" | "chromeIntegration">): Promise<void>;
210
+ run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "filesystemRoot" | "model" | "reasoning" | "nativeToolAccess" | "nativeCapabilities" | "cliParity" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings" | "chromeIntegration">): Promise<void>;
209
211
  /**
210
212
  * Delivers a message the human sent while `runId` is going straight into its live agent process
211
- * (see {@link UserMessageDelivery} for each outcome). A stream-json agent is interrupted first and
212
- * gets the message once the CLI acknowledges the interrupt, so this settles in milliseconds (at
213
- * most `INTERRUPT_ACK_TIMEOUT_MS`); it never waits on the agent's reply.
213
+ * (see {@link UserMessageDelivery} for each outcome). A stream-json agent queues the message
214
+ * directly without interrupting tools. This settles on write acceptance; consumption is reported
215
+ * separately by UUID receipts. It never waits on the agent's reply.
214
216
  * Optional `imageContents` contains host-authorized pixels, encoded exactly as in an idle send.
215
217
  */
216
218
  sendUserMessage(requiredArgs: {
217
219
  readonly runId: string;
218
220
  readonly text: string;
221
+ readonly id?: string;
222
+ readonly attachmentIds?: readonly string[];
219
223
  }, optionalArgs?: {
220
224
  readonly imageContents?: readonly MessageAttachmentImage[];
221
225
  }): Promise<UserMessageDelivery>;
226
+ /** Trusted transport supplies the credential-bound run, never model arguments. No I/O or waiting. */
227
+ claimUserMessages(input: {
228
+ readonly runId: string;
229
+ readonly via: MessageRelayVia;
230
+ }, options?: Record<string, never>): readonly RelayedUserMessage[];
222
231
  }
223
232
  /**
224
233
  * What {@link AgentExecutor.sendUserMessage} did with a mid-run message:
225
- * - `'delivered'` — the running turn was interrupted and the message written into the live
234
+ * - `'delivered'` — the message was queued without interruption into the live
226
235
  * session; the agent answers it next, in the same process and session.
227
236
  * - `'unsupported'` — the run is live, but its agent has no way to take input mid-run (its prompt
228
237
  * is plain text or an RPC request). Queue attachment turns intact after the reply; text-only
229
- * turns may retain the interrupt-and-resume fallback.
238
+ * turns retain the next-turn queue fallback.
230
239
  * - `'not-running'` — no live process will accept it: an unknown run, a finished one, or one whose
231
240
  * last turn already ended and closed stdin. The run is ending; the host should send it next.
232
241
  */
@@ -273,8 +282,14 @@ export interface StdinCloseHandle {
273
282
  closeStdinOnce(): void;
274
283
  /** Journals a sent-to-stdin byte chunk, queued through the same FIFO {@link wireChildLifecycle} already uses for emitted events. No-op when no journal was configured (see `CreateAgentExecutorOptions.journal`). */
275
284
  recordSentBytes(content: string): void;
276
- /** Interrupts this child's turn, then writes a mid-run user message into its open stdin — see {@link AgentExecutor.sendUserMessage}. */
277
- sendUserMessage(text: string, imageContents?: readonly MessageAttachmentImage[]): Promise<UserMessageDelivery>;
285
+ /** Queues a mid-run user message into this child's open stdin — see {@link AgentExecutor.sendUserMessage}. */
286
+ sendUserMessage(input: {
287
+ text: string;
288
+ id?: string;
289
+ attachmentIds?: readonly string[];
290
+ }, options?: {
291
+ imageContents?: readonly MessageAttachmentImage[];
292
+ }): Promise<UserMessageDelivery>;
278
293
  }
279
294
  /**
280
295
  * Gap 3 (capability-routed continuation transport) — host-owned config for the
@@ -622,6 +637,8 @@ export interface CreateAgentExecutorOptions {
622
637
  readonly resolveAgentLaunch?: typeof resolveAgentLaunch;
623
638
  /** @default the real `@jini-ai/agent-runtime` once-per-binary `--help` capability probe */
624
639
  readonly ensureAgentCapabilities?: typeof ensureAgentCapabilities;
640
+ /** Bounded version/enforcement probe; replaceable for offline host tests. */
641
+ readonly assertClaudeFilesystemSupport?: typeof assertClaudeFilesystemSupport;
625
642
  /** @default the real `@jini-ai/agent-runtime` PATH-env composer */
626
643
  readonly applyAgentLaunchEnv?: typeof applyAgentLaunchEnv;
627
644
  /** @default the real `@jini-ai/platform` cross-platform invocation builder */
@@ -1,5 +1,6 @@
1
1
  import type { DelegatedResultCorrelation } from '../delegated-result-correlation.js';
2
2
  import type { StdoutJournalBlobPort } from '../tool-result-diagnostics.js';
3
+ import { type RunMessageRelay } from './message-relay.js';
3
4
  import type { probeMcpBridge } from './mcp-bridge-probe.js';
4
5
  import type { ModelCatalogSnapshot } from '@jini-ai/agent-runtime';
5
6
  import type { ChildProcess } from 'node:child_process';
@@ -26,6 +27,7 @@ export declare function defaultCleanupFailureSink(context: AgentCleanupFailureCo
26
27
  */
27
28
  export declare const DEFAULT_BUFFERED_STDOUT_MAX_BYTES: number;
28
29
  interface WireChildLifecycleContext extends TerminateChildTreeDeps {
30
+ readonly registerMessageRelay?: (relay: RunMessageRelay) => void;
29
31
  readonly delegatedResultCorrelation?: DelegatedResultCorrelation;
30
32
  readonly stdoutJournalBlobs?: StdoutJournalBlobPort;
31
33
  readonly startingModel?: string;
@@ -125,21 +127,6 @@ interface WireChildLifecycleContext extends TerminateChildTreeDeps {
125
127
  * @overallScore 100/100
126
128
  */
127
129
  export declare function wireChildLifecycle(ctx: WireChildLifecycleContext): StdinCloseHandle;
128
- /**
129
- * Whether a def's live process can take another user message mid-run: only a stream-json stdin
130
- * transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
131
- * Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
132
- * reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
133
- */
134
- export declare function acceptsMidRunUserMessages({ def }: {
135
- readonly def: RuntimeAgentDef;
136
- }): boolean;
137
- /**
138
- * How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
139
- * anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
140
- * the ceiling only bounds an agent that ignores control requests.
141
- */
142
- export declare const INTERRUPT_ACK_TIMEOUT_MS = 5000;
143
130
  /**
144
131
  * Writes the initial user turn to the child's stdin per `def.promptInputFormat`
145
132
  * (both branches only run for `promptViaStdin: true` defs — the only shape
@@ -174,6 +161,7 @@ interface RunAcpDispatchInput {
174
161
  readonly mcpBridge: McpBridgeDelivery | null;
175
162
  }
176
163
  interface RunAcpDispatchDeps extends TerminateChildTreeDeps {
164
+ readonly registerMessageRelay?: (relay: RunMessageRelay) => void;
177
165
  readonly lifecycle: RunLifecycle;
178
166
  readonly attachAcpSession: typeof attachAcpSession;
179
167
  readonly onPermissionRequest: AcpPermissionHandler | undefined;
@@ -1,5 +1,7 @@
1
1
  import { createToolResultDiagnosticProjection, projectDiagnosticFrame } from '../tool-result-diagnostics.js';
2
2
  import { appendSerialTask } from "@jini-ai/core/primitives";
3
+ import { createLiveSessionState } from './live-session-state.js';
4
+ import { acceptsMidRunUserMessages, createMessageRelay } from './message-relay.js';
3
5
  import { randomUUID } from 'node:crypto';
4
6
  import { createModelReceiptTracker } from './model-receipts.js';
5
7
  import { observeRunOutput } from './run-output.js';
@@ -203,6 +205,16 @@ export const DEFAULT_BUFFERED_STDOUT_MAX_BYTES = 8 * 1024 * 1024;
203
205
  function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
204
206
  return `\n[jini] agent stdout truncated: ${droppedBytes} byte(s) dropped after the ${maxBytes}-byte buffer limit was reached.\n`;
205
207
  }
208
+ /** Install once on a driver's FIFO; close synchronously before any terminal handler yields.
209
+ * Native runs omit registration, so they allocate no relay and emit only CLI receipts. */
210
+ function attachMessageRelay({ register, emit, child }) {
211
+ if (!register)
212
+ return undefined;
213
+ const relay = createMessageRelay({ emit });
214
+ register(relay);
215
+ child.once('close', () => relay.close());
216
+ return relay;
217
+ }
206
218
  /** Keeps exact-byte auditing separate from retained diagnostics on the run's existing FIFO.
207
219
  * Projection retains at most one bounded frame; ordinary stdout keeps its legacy byte journal. */
208
220
  function createStdoutDiagnostics({ ctx, enqueueEmit }, _options = {}) {
@@ -314,9 +326,8 @@ export function wireChildLifecycle(ctx) {
314
326
  // only ever acted on when a host has explicitly allowlisted the tool's name.
315
327
  let pendingToolUse;
316
328
  // Observe the canonical lifecycle, including delegated calls that bypass the CLI parser.
317
- // A single latest tool_use cannot represent parallel tools at an interrupt boundary.
329
+ // A single latest tool_use cannot represent all parallel calls at explicit cancellation.
318
330
  const unresolvedToolCalls = new Set();
319
- const interruptedToolCalls = new Set();
320
331
  const toolSubscription = lifecycle.stream({ runId, onEvent: (event) => {
321
332
  if (event.kind !== 'agent')
322
333
  return;
@@ -332,9 +343,9 @@ export function wireChildLifecycle(ctx) {
332
343
  });
333
344
  /** Run on the same FIFO as parsed results, after the aborted segment has drained.
334
345
  * O(t) in unresolved calls; completed results are never replaced with cancellation. */
335
- async function cancelUnresolvedToolCalls(ids) {
346
+ async function cancelUnresolvedToolCalls() {
336
347
  await toolSubscription;
337
- for (const toolUseId of ids ?? [...unresolvedToolCalls]) {
348
+ for (const toolUseId of [...unresolvedToolCalls]) {
338
349
  if (!unresolvedToolCalls.has(toolUseId))
339
350
  continue;
340
351
  await lifecycle.emit({ runId, input: { event: 'agent', data: {
@@ -373,71 +384,46 @@ export function wireChildLifecycle(ctx) {
373
384
  stdinClosed = true;
374
385
  child.stdin?.end();
375
386
  }
376
- // Mid-run interrupts (see `sendUserMessage` below): each sent `interrupt` control request waits
377
- // here, by request id, for the CLI's `control_response`. Settled with `true` on the ack, `false`
378
- // when the process exits first. Bounded by the mid-run messages in flight on this one run.
379
- const pendingInterruptAcks = new Map();
380
- // Set once this run has interrupted its agent: from then on a turn the interrupt aborted
381
- // (`aborted_*` stop reason) is not the run's last — the message that follows starts a new one.
382
- let interruptSent = false;
383
- // Claude Code's background tasks still running (a subagent or shell it started in the
384
- // background), from its latest `background_tasks_changed` line, which lists them all. While any
385
- // runs, an `end_turn` is not the run's end: the CLI keeps going until they finish and reports on
386
- // them, so stdin must stay open for mid-run messages. Closing it there (2026-10-10, plannie chat)
387
- // made every message sent while two subagents worked come back 'not-running' and wait for the run
388
- // to end — and at EOF the CLI stops background tasks at its own wait ceiling (10 minutes).
389
- let backgroundTaskCount = 0;
390
- // Set when a turn ended and stdin stayed open only for background tasks: no turn is running, so a
391
- // mid-run message has nothing to interrupt, and the last task finishing closes stdin (below).
387
+ // Unanswered UUIDs and the CLI's queued_turn_count keep stdin open across intermediate turn ends.
388
+ // Consumption is confirmed by CLI reply/result attribution, never by HTTP delivery or an echo.
389
+ const emitInput = (data) => enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data } }));
390
+ const session = createLiveSessionState({ emit: emitInput });
391
+ const relay = attachMessageRelay({ register: ctx.registerMessageRelay, emit: emitInput, child });
392
+ // Claude's background tasks can outlive a parent turn. Keep the existing hold until the last
393
+ // task finishes; closing stdin early stops them at the CLI's background-wait ceiling.
394
+ // 2026-10-10 (plannie chat): closing at the parent's end_turn made messages return
395
+ // 'not-running' for ten minutes, then EOF killed the still-working subagents at that ceiling.
392
396
  let heldForBackgroundTasks = false;
393
- // stdout carry-over for the control lines below; holds at most the current partial line.
397
+ let heldForQueuedInput = false;
394
398
  let controlScanBuffer = '';
395
- /** Resolves the waiting interrupt this `control_response` line acknowledges, if any. @complexity O(line length). */
396
- function settleInterruptAck(line) {
397
- const requestId = controlResponseRequestId({ line: line });
398
- const settle = requestId === undefined ? undefined : pendingInterruptAcks.get(requestId);
399
- if (!settle)
400
- return;
401
- pendingInterruptAcks.delete(requestId);
402
- settle(true);
403
- }
404
- /**
405
- * Tracks a `background_tasks_changed` line. The last background task finishing while stdin is
406
- * held only for them closes it, exactly as the turn end would have: the CLI then answers what it
407
- * still owes (the tasks' report) and exits. @complexity O(line length).
408
- */
409
- function updateBackgroundTasks(line) {
410
- const count = backgroundTaskListLength({ line: line });
411
- if (count === undefined)
412
- return;
413
- backgroundTaskCount = count;
414
- if (count === 0 && heldForBackgroundTasks) {
415
- heldForBackgroundTasks = false;
416
- closeStdinOnce();
417
- }
418
- }
419
- /**
420
- * Reads the stdout lines a mid-run message depends on: interrupt acks and background-task
421
- * changes. Only agents that take mid-run messages are scanned. A chunk with no newline is only
422
- * appended, so a long line split across many chunks is split once, not once per chunk.
423
- * @complexity O(chunk length) amortized.
424
- */
399
+ /** Scan and parse in wire order: a receipt later in a chunk cannot affect an earlier turn end.
400
+ * O(chunk length) amortized; only the partial line remains buffered. */
425
401
  function scanControlLines(text) {
426
- if (!acceptsMidRunUserMessages({ def: def }))
427
- return;
428
402
  controlScanBuffer += text;
429
403
  if (!text.includes('\n'))
430
404
  return;
431
- // `text` holds a newline, so the buffer does: split off the complete lines, keep the partial tail.
432
405
  const lastNewline = controlScanBuffer.lastIndexOf('\n');
433
406
  const lines = controlScanBuffer.slice(0, lastNewline).split('\n');
434
407
  controlScanBuffer = controlScanBuffer.slice(lastNewline + 1);
435
- for (const line of lines) {
436
- // Both checked: either word can also appear inside the other kind of line's text.
437
- if (line.includes('"control_response"'))
438
- settleInterruptAck(line);
439
- if (line.includes('"background_tasks_changed"'))
440
- updateBackgroundTasks(line);
408
+ for (const line of lines)
409
+ feedSessionLine(line);
410
+ }
411
+ function feedSessionLine(line) {
412
+ const observed = session.observe({ line });
413
+ if (observed.startedInput)
414
+ heldForQueuedInput = false;
415
+ streamHandler.feed({ chunk: `${line}\n` });
416
+ if (session.hasPendingInput())
417
+ return;
418
+ if (heldForBackgroundTasks && session.backgroundTaskCount() === 0) {
419
+ heldForBackgroundTasks = false;
420
+ closeStdinOnce();
421
+ }
422
+ // A result can lower queued_turn_count after the assistant end already held stdin open.
423
+ // Re-evaluate that held end after parsing the result (the parser deduplicates turn ends).
424
+ if (observed.result && heldForQueuedInput && session.backgroundTaskCount() === 0) {
425
+ heldForQueuedInput = false;
426
+ closeStdinOnce();
441
427
  }
442
428
  }
443
429
  /**
@@ -459,25 +445,15 @@ export function wireChildLifecycle(ctx) {
459
445
  if (journal)
460
446
  enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
461
447
  }
462
- /**
463
- * Whether a `turn_end` leaves stdin open for a live agent that takes mid-run messages — and why:
464
- * `'background-tasks'` when only background tasks keep the run going (no turn runs until the next
465
- * message or task report), `'turn-continues'` for the reasons below, `'close'` otherwise. Open
466
- * because the run is not over: a `tool_use` stop (the CLI runs the tool itself and carries on, and a
467
- * message sent while the tool runs must still reach it), any end while an interrupt still waits
468
- * for its ack (the message is about to be written), and the end of a turn an interrupt aborted
469
- * (the message written after it starts the next turn), and any end while background tasks still
470
- * run (see `backgroundTaskCount`). Every other end closes stdin, so the CLI exits once it has
471
- * answered everything written before EOF.
472
- */
448
+ /** A tool_use end is intermediate: the CLI executes that batch itself and carries on.
449
+ * Pending input and background tasks also owe work. Every other end closes stdin so the CLI
450
+ * exits once it has answered everything written before EOF. */
473
451
  function stdinHoldAfterTurnEnd(stopReason) {
474
- if (!acceptsMidRunUserMessages({ def: def }))
452
+ if (!acceptsMidRunUserMessages({ def }))
475
453
  return 'close';
476
- if (stopReason === 'tool_use' || pendingInterruptAcks.size > 0)
477
- return 'turn-continues';
478
- if (interruptSent && stopReason !== undefined && stopReason.startsWith('aborted'))
454
+ if (stopReason === 'tool_use' || session.hasPendingInput())
479
455
  return 'turn-continues';
480
- return backgroundTaskCount > 0 ? 'background-tasks' : 'close';
456
+ return session.backgroundTaskCount() > 0 ? 'background-tasks' : 'close';
481
457
  }
482
458
  /**
483
459
  * Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
@@ -486,11 +462,6 @@ export function wireChildLifecycle(ctx) {
486
462
  * unconfigured or the pending tool isn't allowlisted).
487
463
  */
488
464
  function handleTurnEnd(stopReason) {
489
- if (stopReason?.startsWith('aborted'))
490
- enqueueEmit(async () => {
491
- await cancelUnresolvedToolCalls([...interruptedToolCalls]);
492
- interruptedToolCalls.clear();
493
- });
494
465
  const toolUse = pendingToolUse;
495
466
  const shouldInject = stopReason === 'tool_use' &&
496
467
  toolUse !== undefined &&
@@ -500,6 +471,7 @@ export function wireChildLifecycle(ctx) {
500
471
  if (!shouldInject) {
501
472
  const hold = stdinHoldAfterTurnEnd(stopReason);
502
473
  heldForBackgroundTasks = hold === 'background-tasks';
474
+ heldForQueuedInput = hold === 'turn-continues' && stopReason !== 'tool_use';
503
475
  if (hold === 'close')
504
476
  closeStdinOnce();
505
477
  return;
@@ -688,13 +660,12 @@ export function wireChildLifecycle(ctx) {
688
660
  return;
689
661
  }
690
662
  stdoutDiagnostics.feed({ text });
691
- // Before the parser: an ack and the aborted turn's end can share a chunk, and the turn end must
692
- // see that no interrupt is still waiting (see `stdinHoldAfterTurnEnd`); a background task
693
- // starting and the turn's end can share one too.
694
- scanControlLines(text);
695
- // Non-null: `streamHandler` is only ever null when `streamFormat === 'plain'` (see its
696
- // construction above), the branch this statement is provably unreachable from.
697
- streamHandler.feed({ chunk: text });
663
+ // Receipts, background membership and turn ends must be processed in the same wire order.
664
+ // streamHandler is non-null here: the plain-format branch returned above.
665
+ if (acceptsMidRunUserMessages({ def }))
666
+ scanControlLines(text);
667
+ else
668
+ streamHandler.feed({ chunk: text });
698
669
  });
699
670
  child.stderr?.on('data', (chunk) => {
700
671
  const text = chunk.toString('utf8');
@@ -717,9 +688,7 @@ export function wireChildLifecycle(ctx) {
717
688
  void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
718
689
  } });
719
690
  child.on('close', (code, signal) => {
720
- for (const settle of pendingInterruptAcks.values())
721
- settle(false);
722
- pendingInterruptAcks.clear();
691
+ stdinClosed = true;
723
692
  void (async () => {
724
693
  // Not wrapped in try/catch: all 4 supported parser factories'
725
694
  // flush() implementations already internally guard their own
@@ -729,7 +698,12 @@ export function wireChildLifecycle(ctx) {
729
698
  // here would be dead code for the fixed, closed set of parsers this
730
699
  // driver dispatches to. `streamHandler` is null for `'plain'` (no
731
700
  // parser, hence nothing to flush) — `?.` skips it cleanly.
701
+ if (controlScanBuffer) {
702
+ feedSessionLine(controlScanBuffer);
703
+ controlScanBuffer = '';
704
+ }
732
705
  streamHandler?.flush();
706
+ session.endSession();
733
707
  stdoutDiagnostics.flush();
734
708
  // Queued before `await emitQueue` so the flushed text is durably appended ahead of
735
709
  // `finish()`'s `'end'` event, exactly like every live-path emit already is.
@@ -761,63 +735,27 @@ export function wireChildLifecycle(ctx) {
761
735
  })
762
736
  : false;
763
737
  }
764
- /**
765
- * A mid-run message: first an `interrupt` control request on the still-open stdin, so the agent
766
- * stops generating (or abandons the tool it is running) at once instead of finishing its whole
767
- * reply; then, once the CLI acknowledges, the message as the next user line, which the CLI
768
- * answers straight away in the same session; then the run's own `user_message` event through the
769
- * same FIFO as the agent's output, so the message sits in the stream where it took effect.
770
- * An ack that never comes (an agent that ignores control requests) only delays the write by
771
- * {@link INTERRUPT_ACK_TIMEOUT_MS}: the CLI then folds the line in at its next step. Once
772
- * `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
773
- * reach — `'not-running'`. Between turns, while stdin is held only for background tasks, there is
774
- * no generation to stop: the line is written at once and starts the next turn.
775
- * Host-prepared images use the same content blocks as the initial stdin turn; no file tools
776
- * or new directory grants are needed to see these pixels.
777
- */
778
- async function sendUserMessage(text, images = []) {
779
- if (!acceptsMidRunUserMessages({ def: def }))
780
- return 'unsupported';
738
+ /** Queue into the existing Claude session without interrupting parallel tools or subagents.
739
+ * Images use the idle-send encoder without file tools or new directory grants; journals keep
740
+ * text without base64. HTTP acceptance only
741
+ * confirms the write; UUID-attributed CLI receipts separately confirm consumption. */
742
+ async function sendUserMessage({ text, id = randomUUID(), attachmentIds }, { imageContents: images = [] } = {}) {
743
+ if (!acceptsMidRunUserMessages({ def }))
744
+ return relay?.enqueue({ id, text, hasAttachments: images.length > 0 || (attachmentIds?.length ?? 0) > 0 }) ?? 'unsupported';
781
745
  const stdin = child.stdin;
782
746
  if (stdinClosed || !stdin)
783
747
  return 'not-running';
784
- if (heldForBackgroundTasks) {
785
- heldForBackgroundTasks = false;
786
- writeUserMessage(stdin, text, images);
787
- return 'delivered';
788
- }
789
- const requestId = `jini-interrupt-${randomUUID()}`;
790
- interruptSent = true;
791
- // Capture before requesting the interrupt. Its result can precede the acknowledgement, or
792
- // arrive after the next segment starts: neither ordering may cancel that segment's new tools.
793
- enqueueEmit(async () => {
794
- await toolSubscription;
795
- for (const id of interruptedToolCalls)
796
- if (!unresolvedToolCalls.has(id))
797
- interruptedToolCalls.delete(id);
798
- for (const id of unresolvedToolCalls)
799
- interruptedToolCalls.add(id);
800
- });
801
- const acked = new Promise((resolve) => pendingInterruptAcks.set(requestId, resolve));
802
- stdin.write(`${JSON.stringify({ type: 'control_request', request_id: requestId, request: { subtype: 'interrupt' } })}\n`, 'utf8');
803
- let timer;
804
- const timedOut = new Promise((resolve) => {
805
- timer = setTimeout(() => resolve(true), INTERRUPT_ACK_TIMEOUT_MS);
806
- });
807
- const processAlive = await Promise.race([acked, timedOut]);
808
- clearTimeout(timer);
809
- pendingInterruptAcks.delete(requestId);
810
- if (!processAlive || stdinClosed)
811
- return 'not-running';
812
- writeUserMessage(stdin, text, images);
748
+ heldForBackgroundTasks = false;
749
+ writeUserMessage(stdin, text, images, id);
813
750
  return 'delivered';
814
751
  }
815
- /** Writes a complete mid-run turn using the idle image encoder; journals text without base64. */
816
- function writeUserMessage(stdin, text, images) {
817
- stdin.write(`${JSON.stringify(streamJsonUserMessage({ content: messageContentWithImages({ prompt: text, images }, {}) }))}\n`, 'utf8');
752
+ /** Writes a complete mid-run turn using the idle image encoder; journals text without base64.
753
+ * The same UUID identifies stdin, queue acceptance and subsequent consumption receipts. */
754
+ function writeUserMessage(stdin, text, images, id) {
755
+ session.queue({ id, text });
756
+ stdin.write(`${JSON.stringify({ ...streamJsonUserMessage({ content: messageContentWithImages({ prompt: text, images }, {}) }), uuid: id })}\n`, 'utf8');
818
757
  if (journal)
819
- enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(text) }));
820
- enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
758
+ enqueueEmit(() => journal.record({ runId, entry: sentJournalEntry(text) }));
821
759
  }
822
760
  return {
823
761
  closeStdinOnce,
@@ -828,45 +766,6 @@ export function wireChildLifecycle(ctx) {
828
766
  sendUserMessage,
829
767
  };
830
768
  }
831
- /**
832
- * Whether a def's live process can take another user message mid-run: only a stream-json stdin
833
- * transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
834
- * Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
835
- * reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
836
- */
837
- export function acceptsMidRunUserMessages({ def }) {
838
- return def.promptViaStdin === true && def.promptInputFormat === 'stream-json';
839
- }
840
- /**
841
- * How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
842
- * anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
843
- * the ceiling only bounds an agent that ignores control requests.
844
- */
845
- export const INTERRUPT_ACK_TIMEOUT_MS = 5_000;
846
- /** The `request_id` of a stream-json `control_response` line, or `undefined` for anything else. */
847
- function controlResponseRequestId({ line }) {
848
- try {
849
- const parsed = JSON.parse(line);
850
- if (!isRecord(parsed) || parsed.type !== 'control_response' || !isRecord(parsed.response))
851
- return undefined;
852
- return typeof parsed.response.request_id === 'string' ? parsed.response.request_id : undefined;
853
- }
854
- catch {
855
- return undefined;
856
- }
857
- }
858
- /** How many tasks a `background_tasks_changed` line lists, or `undefined` for any other line. */
859
- function backgroundTaskListLength({ line }) {
860
- try {
861
- const parsed = JSON.parse(line);
862
- if (!isRecord(parsed) || parsed.type !== 'system' || parsed.subtype !== 'background_tasks_changed')
863
- return undefined;
864
- return Array.isArray(parsed.tasks) ? parsed.tasks.length : undefined;
865
- }
866
- catch {
867
- return undefined;
868
- }
869
- }
870
769
  /** The stream-json stdin envelope for one user message — the initial prompt and every mid-run one. */
871
770
  function streamJsonUserMessage({ content }) {
872
771
  return { type: 'user', message: { role: 'user', content: content } };
@@ -910,6 +809,9 @@ function wireAcpLifecycle(ctx) {
910
809
  function enqueueEmit(task) {
911
810
  emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
912
811
  }
812
+ attachMessageRelay({ register: ctx.registerMessageRelay, child,
813
+ emit: data => enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data } })),
814
+ });
913
815
  if (ctx.model)
914
816
  enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.model } } }));
915
817
  child.stdout?.on('data', (chunk) => {
@@ -1109,6 +1011,7 @@ export function writePromptToStdin(def, child, prompt, handle) {
1109
1011
  export async function runAcpDispatch({ input, deps }) {
1110
1012
  try {
1111
1013
  wireAcpLifecycle({
1014
+ ...(deps.registerMessageRelay ? { registerMessageRelay: deps.registerMessageRelay } : {}),
1112
1015
  runId: input.runId,
1113
1016
  agentId: input.agentId,
1114
1017
  child: input.child,
@@ -186,10 +186,16 @@ export declare function computeRuntimeContext({ preparedPromptFile, preparedLogF
186
186
  readonly resumeSessionId?: string | null;
187
187
  readonly newSessionId?: string;
188
188
  }): RuntimeContext | undefined;
189
+ type AgentBuildInput = Parameters<typeof buildAgentBuildArgsOptions>[0]['input'];
189
190
  /** Phase 9a: the def's `buildArgs` 4th argument. Historically omitted when every override was absent;
190
191
  * that let defs silently enable auto-approval. Always supply the validated restricted default now. Pure. */
191
192
  export declare function buildAgentBuildArgsOptions({ input, systemPromptOverlay }: {
192
- readonly input: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode' | 'nativeToolAccess' | 'nativeCapabilities' | 'cliParity' | 'disallowedTools' | 'allowedTools' | 'settingSources' | 'settings' | 'chromeIntegration'>;
193
+ readonly input: Pick<AgentExecutorRunInput, 'filesystemRoot' | 'model' | 'reasoning' | 'permissionMode' | 'nativeToolAccess' | 'nativeCapabilities' | 'cliParity' | 'disallowedTools' | 'allowedTools' | 'settingSources' | 'settings' | 'chromeIntegration'> & {
194
+ filesystemRuntimeReadPaths?: readonly string[];
195
+ filesystemTempDir?: string;
196
+ filesystemSharedTempRoots?: readonly string[];
197
+ filesystemNotice?: string;
198
+ };
193
199
  readonly systemPromptOverlay: string | null | undefined;
194
200
  }): RuntimeBuildOptions;
195
201
  /**
@@ -262,7 +268,7 @@ export declare function buildRunArgs({ input, deps }: {
262
268
  readonly def: RuntimeAgentDef;
263
269
  readonly imageDelivery: ImagePromptDelivery;
264
270
  readonly imagePaths: readonly string[] | undefined;
265
- readonly runInput: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode'>;
271
+ readonly runInput: AgentBuildInput;
266
272
  readonly systemPromptOverlay: string | null | undefined;
267
273
  readonly overlayDelivery: SystemPromptOverlayDelivery;
268
274
  readonly runtimeContext: RuntimeContext | undefined;
@@ -381,6 +387,6 @@ export declare function confirmChildSpawned({ input, deps }: {
381
387
  * @complexity `run()`'s own setup is O(1); steady-state cost is the chosen stream parser's.
382
388
  * @overallScore 100/100
383
389
  */
384
- export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "runtimeState" | "codexDisablePlugins" | "piGracefulShutdownMs" | "hostEnv" | "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "probeMcpBridge" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "delegatedResultCorrelation" | "stdoutJournalBlobs" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
390
+ export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "runtimeState" | "codexDisablePlugins" | "piGracefulShutdownMs" | "hostEnv" | "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "assertClaudeFilesystemSupport" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "probeMcpBridge" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "delegatedResultCorrelation" | "stdoutJournalBlobs" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
385
391
  export {};
386
392
  //# sourceMappingURL=launch.d.ts.map