@jini-ai/daemon 0.6.9 → 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.
Files changed (41) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/agent-executor/contracts.d.ts +34 -11
  3. package/dist/agent-executor/event-translation.js +2 -0
  4. package/dist/agent-executor/events.d.ts +7 -15
  5. package/dist/agent-executor/events.js +142 -194
  6. package/dist/agent-executor/launch.d.ts +9 -3
  7. package/dist/agent-executor/launch.js +104 -8
  8. package/dist/agent-executor/live-session-state.d.ts +23 -0
  9. package/dist/agent-executor/live-session-state.js +155 -0
  10. package/dist/agent-executor/mcp-bridge.d.ts +3 -1
  11. package/dist/agent-executor/mcp-bridge.js +9 -2
  12. package/dist/agent-executor/message-relay.d.ts +39 -0
  13. package/dist/agent-executor/message-relay.js +56 -0
  14. package/dist/core/read-only-tools.d.ts +9 -4
  15. package/dist/core/read-only-tools.js +10 -7
  16. package/dist/core/tool-candidates.d.ts +8 -3
  17. package/dist/core/tool-candidates.js +39 -4
  18. package/dist/delegated-result-correlation.d.ts +26 -0
  19. package/dist/delegated-result-correlation.js +68 -0
  20. package/dist/delegated-tool-bridge.d.ts +5 -13
  21. package/dist/delegated-tool-bridge.js +19 -58
  22. package/dist/http/attachments.d.ts +13 -0
  23. package/dist/http/attachments.js +27 -1
  24. package/dist/http/delegated-tools.d.ts +3 -0
  25. package/dist/http/delegated-tools.js +2 -2
  26. package/dist/http/index.d.ts +2 -1
  27. package/dist/http/index.js +3 -1
  28. package/dist/http/message-relay.d.ts +21 -0
  29. package/dist/http/message-relay.js +44 -0
  30. package/dist/http/runs.d.ts +5 -2
  31. package/dist/http/runs.js +11 -5
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +1 -0
  34. package/dist/run-lifecycle.d.ts +20 -1
  35. package/dist/run-lifecycle.js +2 -16
  36. package/dist/tool-executor.js +5 -4
  37. package/dist/tool-result-diagnostics.d.ts +27 -0
  38. package/dist/tool-result-diagnostics.js +77 -0
  39. package/dist/tool-result-media.d.ts +27 -14
  40. package/dist/tool-result-media.js +51 -38
  41. package/package.json +14 -14
@@ -1,4 +1,7 @@
1
+ import { createToolResultDiagnosticProjection, projectDiagnosticFrame } from '../tool-result-diagnostics.js';
1
2
  import { appendSerialTask } from "@jini-ai/core/primitives";
3
+ import { createLiveSessionState } from './live-session-state.js';
4
+ import { acceptsMidRunUserMessages, createMessageRelay } from './message-relay.js';
2
5
  import { randomUUID } from 'node:crypto';
3
6
  import { createModelReceiptTracker } from './model-receipts.js';
4
7
  import { observeRunOutput } from './run-output.js';
@@ -9,7 +12,7 @@ import { collectProcessTreePids, listProcessSnapshots, stopProcesses, } from '@j
9
12
  import { classifyRunCloseStatus, } from '../close-status.js';
10
13
  import { resolveContinuationTransport, } from '../continuation/continuation-transport.js';
11
14
  import { resultContent, } from '../delegated-tool-bridge.js';
12
- import { extractResultMedia, } from '../tool-result-media.js';
15
+ import { projectToolResultEvent, } from '../tool-result-media.js';
13
16
  import { translateAgentRuntimeEvent, translateAcpError, applyAgentTranslationSideEffects, } from './event-translation.js';
14
17
  import { errorMessage, isRecord, } from './values.js';
15
18
  import { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, bridgeUnavailableMessage, } from './mcp-bridge.js';
@@ -202,6 +205,45 @@ export const DEFAULT_BUFFERED_STDOUT_MAX_BYTES = 8 * 1024 * 1024;
202
205
  function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
203
206
  return `\n[jini] agent stdout truncated: ${droppedBytes} byte(s) dropped after the ${maxBytes}-byte buffer limit was reached.\n`;
204
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
+ }
218
+ /** Keeps exact-byte auditing separate from retained diagnostics on the run's existing FIFO.
219
+ * Projection retains at most one bounded frame; ordinary stdout keeps its legacy byte journal. */
220
+ function createStdoutDiagnostics({ ctx, enqueueEmit }, _options = {}) {
221
+ const { runId, lifecycle, journal, stdoutJournalBlobs } = ctx;
222
+ const projection = ctx.delegatedResultCorrelation && ctx.streamFormat === 'claude-stream-json'
223
+ ? createToolResultDiagnosticProjection({}) : undefined;
224
+ function emit({ text }) {
225
+ if (text)
226
+ enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'stdout', data: { chunk: text } } }));
227
+ }
228
+ return {
229
+ record({ chunk, text }) {
230
+ if (!journal)
231
+ return;
232
+ if (projection && stdoutJournalBlobs) {
233
+ enqueueEmit(() => stdoutJournalBlobs.record({ runId, chunk }));
234
+ }
235
+ else {
236
+ enqueueEmit(() => journal.record({ runId, entry: receivedJournalEntry('stdout', text) }));
237
+ }
238
+ },
239
+ feed({ text }) {
240
+ emit({ text: projection ? projection.feed({ chunk: text }) : text });
241
+ },
242
+ flush() {
243
+ emit({ text: projection?.flush({}) ?? '' });
244
+ },
245
+ };
246
+ }
205
247
  /**
206
248
  * Wires one spawned child's full observable lifecycle: raw stdout/stderr
207
249
  * forwarding, structured stream-parser dispatch (translated via
@@ -264,6 +306,7 @@ function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
264
306
  */
265
307
  export function wireChildLifecycle(ctx) {
266
308
  const { runId, def, streamFormat, child, lifecycle, journal, continuation, classifyFailure } = ctx;
309
+ const stdoutDiagnostics = createStdoutDiagnostics({ ctx, enqueueEmit });
267
310
  const output = observeRunOutput({ lifecycle, runId, agentName: def.name }, { plainStdout: streamFormat === 'plain' });
268
311
  const modelReceipt = createModelReceiptTracker(ctx.startingModel, ctx.modelCatalog?.models);
269
312
  let stdinClosed = false;
@@ -283,9 +326,8 @@ export function wireChildLifecycle(ctx) {
283
326
  // only ever acted on when a host has explicitly allowlisted the tool's name.
284
327
  let pendingToolUse;
285
328
  // Observe the canonical lifecycle, including delegated calls that bypass the CLI parser.
286
- // 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.
287
330
  const unresolvedToolCalls = new Set();
288
- const interruptedToolCalls = new Set();
289
331
  const toolSubscription = lifecycle.stream({ runId, onEvent: (event) => {
290
332
  if (event.kind !== 'agent')
291
333
  return;
@@ -301,9 +343,9 @@ export function wireChildLifecycle(ctx) {
301
343
  });
302
344
  /** Run on the same FIFO as parsed results, after the aborted segment has drained.
303
345
  * O(t) in unresolved calls; completed results are never replaced with cancellation. */
304
- async function cancelUnresolvedToolCalls(ids) {
346
+ async function cancelUnresolvedToolCalls() {
305
347
  await toolSubscription;
306
- for (const toolUseId of ids ?? [...unresolvedToolCalls]) {
348
+ for (const toolUseId of [...unresolvedToolCalls]) {
307
349
  if (!unresolvedToolCalls.has(toolUseId))
308
350
  continue;
309
351
  await lifecycle.emit({ runId, input: { event: 'agent', data: {
@@ -342,71 +384,46 @@ export function wireChildLifecycle(ctx) {
342
384
  stdinClosed = true;
343
385
  child.stdin?.end();
344
386
  }
345
- // Mid-run interrupts (see `sendUserMessage` below): each sent `interrupt` control request waits
346
- // here, by request id, for the CLI's `control_response`. Settled with `true` on the ack, `false`
347
- // when the process exits first. Bounded by the mid-run messages in flight on this one run.
348
- const pendingInterruptAcks = new Map();
349
- // Set once this run has interrupted its agent: from then on a turn the interrupt aborted
350
- // (`aborted_*` stop reason) is not the run's last — the message that follows starts a new one.
351
- let interruptSent = false;
352
- // Claude Code's background tasks still running (a subagent or shell it started in the
353
- // background), from its latest `background_tasks_changed` line, which lists them all. While any
354
- // runs, an `end_turn` is not the run's end: the CLI keeps going until they finish and reports on
355
- // them, so stdin must stay open for mid-run messages. Closing it there (2026-10-10, plannie chat)
356
- // made every message sent while two subagents worked come back 'not-running' and wait for the run
357
- // to end — and at EOF the CLI stops background tasks at its own wait ceiling (10 minutes).
358
- let backgroundTaskCount = 0;
359
- // Set when a turn ended and stdin stayed open only for background tasks: no turn is running, so a
360
- // 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.
361
396
  let heldForBackgroundTasks = false;
362
- // stdout carry-over for the control lines below; holds at most the current partial line.
397
+ let heldForQueuedInput = false;
363
398
  let controlScanBuffer = '';
364
- /** Resolves the waiting interrupt this `control_response` line acknowledges, if any. @complexity O(line length). */
365
- function settleInterruptAck(line) {
366
- const requestId = controlResponseRequestId({ line: line });
367
- const settle = requestId === undefined ? undefined : pendingInterruptAcks.get(requestId);
368
- if (!settle)
369
- return;
370
- pendingInterruptAcks.delete(requestId);
371
- settle(true);
372
- }
373
- /**
374
- * Tracks a `background_tasks_changed` line. The last background task finishing while stdin is
375
- * held only for them closes it, exactly as the turn end would have: the CLI then answers what it
376
- * still owes (the tasks' report) and exits. @complexity O(line length).
377
- */
378
- function updateBackgroundTasks(line) {
379
- const count = backgroundTaskListLength({ line: line });
380
- if (count === undefined)
381
- return;
382
- backgroundTaskCount = count;
383
- if (count === 0 && heldForBackgroundTasks) {
384
- heldForBackgroundTasks = false;
385
- closeStdinOnce();
386
- }
387
- }
388
- /**
389
- * Reads the stdout lines a mid-run message depends on: interrupt acks and background-task
390
- * changes. Only agents that take mid-run messages are scanned. A chunk with no newline is only
391
- * appended, so a long line split across many chunks is split once, not once per chunk.
392
- * @complexity O(chunk length) amortized.
393
- */
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. */
394
401
  function scanControlLines(text) {
395
- if (!acceptsMidRunUserMessages({ def: def }))
396
- return;
397
402
  controlScanBuffer += text;
398
403
  if (!text.includes('\n'))
399
404
  return;
400
- // `text` holds a newline, so the buffer does: split off the complete lines, keep the partial tail.
401
405
  const lastNewline = controlScanBuffer.lastIndexOf('\n');
402
406
  const lines = controlScanBuffer.slice(0, lastNewline).split('\n');
403
407
  controlScanBuffer = controlScanBuffer.slice(lastNewline + 1);
404
- for (const line of lines) {
405
- // Both checked: either word can also appear inside the other kind of line's text.
406
- if (line.includes('"control_response"'))
407
- settleInterruptAck(line);
408
- if (line.includes('"background_tasks_changed"'))
409
- 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();
410
427
  }
411
428
  }
412
429
  /**
@@ -428,25 +445,15 @@ export function wireChildLifecycle(ctx) {
428
445
  if (journal)
429
446
  enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
430
447
  }
431
- /**
432
- * Whether a `turn_end` leaves stdin open for a live agent that takes mid-run messages — and why:
433
- * `'background-tasks'` when only background tasks keep the run going (no turn runs until the next
434
- * message or task report), `'turn-continues'` for the reasons below, `'close'` otherwise. Open
435
- * because the run is not over: a `tool_use` stop (the CLI runs the tool itself and carries on, and a
436
- * message sent while the tool runs must still reach it), any end while an interrupt still waits
437
- * for its ack (the message is about to be written), and the end of a turn an interrupt aborted
438
- * (the message written after it starts the next turn), and any end while background tasks still
439
- * run (see `backgroundTaskCount`). Every other end closes stdin, so the CLI exits once it has
440
- * answered everything written before EOF.
441
- */
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. */
442
451
  function stdinHoldAfterTurnEnd(stopReason) {
443
- if (!acceptsMidRunUserMessages({ def: def }))
452
+ if (!acceptsMidRunUserMessages({ def }))
444
453
  return 'close';
445
- if (stopReason === 'tool_use' || pendingInterruptAcks.size > 0)
446
- return 'turn-continues';
447
- if (interruptSent && stopReason !== undefined && stopReason.startsWith('aborted'))
454
+ if (stopReason === 'tool_use' || session.hasPendingInput())
448
455
  return 'turn-continues';
449
- return backgroundTaskCount > 0 ? 'background-tasks' : 'close';
456
+ return session.backgroundTaskCount() > 0 ? 'background-tasks' : 'close';
450
457
  }
451
458
  /**
452
459
  * Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
@@ -455,11 +462,6 @@ export function wireChildLifecycle(ctx) {
455
462
  * unconfigured or the pending tool isn't allowlisted).
456
463
  */
457
464
  function handleTurnEnd(stopReason) {
458
- if (stopReason?.startsWith('aborted'))
459
- enqueueEmit(async () => {
460
- await cancelUnresolvedToolCalls([...interruptedToolCalls]);
461
- interruptedToolCalls.clear();
462
- });
463
465
  const toolUse = pendingToolUse;
464
466
  const shouldInject = stopReason === 'tool_use' &&
465
467
  toolUse !== undefined &&
@@ -469,6 +471,7 @@ export function wireChildLifecycle(ctx) {
469
471
  if (!shouldInject) {
470
472
  const hold = stdinHoldAfterTurnEnd(stopReason);
471
473
  heldForBackgroundTasks = hold === 'background-tasks';
474
+ heldForQueuedInput = hold === 'turn-continues' && stopReason !== 'tool_use';
472
475
  if (hold === 'close')
473
476
  closeStdinOnce();
474
477
  return;
@@ -478,11 +481,8 @@ export function wireChildLifecycle(ctx) {
478
481
  const run = { id: runId };
479
482
  let content;
480
483
  let isError;
481
- // Same extraction `delegated-tool-bridge.ts`'s `execute()` runs, kept consistent per
482
- // `resultContent`'s own doc ("both callers share one mapping"). This path never ran
483
- // `splitToolResultSurfaces` (it has no `mcp-ui` withhold-from-model concept — the flattened
484
- // `content` below already carries the whole raw output, a pre-existing, unrelated gap), so
485
- // there is no `remainder` to thread back in — only the extracted blocks are used here.
484
+ // Event projection removes pixels only from saved content; stdin keeps the original.
485
+ let eventContent;
486
486
  let media = [];
487
487
  // Suspend the slow-run watchdog for the duration of this daemon-awaited execution: the daemon
488
488
  // knows exactly why the run is quiet here (it dispatched the tool itself and is waiting on it),
@@ -494,11 +494,14 @@ export function wireChildLifecycle(ctx) {
494
494
  try {
495
495
  const result = await continuation.toolExecutor.execute({ principal: continuation.principal, run: run, toolId: toolUse.name, input: toolUse.input });
496
496
  content = resultContent({ result: result });
497
- isError = result.status !== 'completed';
498
- media = extractResultMedia({ output: result.output }).media;
497
+ const projection = projectToolResultEvent({ result });
498
+ eventContent = projection.content;
499
+ isError = projection.isError === true;
500
+ media = projection.media ?? [];
499
501
  }
500
502
  catch (error) {
501
503
  content = errorMessage(error);
504
+ eventContent = content;
502
505
  isError = true;
503
506
  }
504
507
  finally {
@@ -509,7 +512,7 @@ export function wireChildLifecycle(ctx) {
509
512
  data: {
510
513
  type: 'tool_result',
511
514
  toolUseId: toolUse.id,
512
- content,
515
+ content: eventContent,
513
516
  ...(isError ? { isError: true } : {}),
514
517
  ...(media.length > 0 ? { media } : {}),
515
518
  },
@@ -539,7 +542,10 @@ export function wireChildLifecycle(ctx) {
539
542
  } });
540
543
  if (rejectUnavailableBridge({ payload: translation.payload, rawEvent }, {}))
541
544
  return;
542
- enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: statusWithChildIdentity({ payload: translation.payload, childPid: ctx.child.pid }, {}) } }));
545
+ enqueueEmit(() => {
546
+ const payload = projectEcho({ payload: translation.payload, rawEvent }, {});
547
+ return lifecycle.emit({ runId, input: { event: 'agent', data: statusWithChildIdentity({ payload, childPid: ctx.child.pid }, {}) } });
548
+ });
543
549
  }
544
550
  else if (translation.kind === 'error') {
545
551
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
@@ -548,6 +554,17 @@ export function wireChildLifecycle(ctx) {
548
554
  handleTurnEnd(translation.stopReason);
549
555
  }
550
556
  });
557
+ /** Verify against an already-saved call in this run; unverified metadata preserves all output. */
558
+ function projectEcho({ payload, rawEvent }, _options = {}) {
559
+ if (!ctx.delegatedResultCorrelation)
560
+ return payload;
561
+ if (payload.type === 'raw')
562
+ return { ...payload, line: projectDiagnosticFrame({ line: payload.line }) };
563
+ if (payload.type !== 'tool_result' || !payload.delegatedToolUseId || !Array.isArray(rawEvent.delegatedResultContent))
564
+ return payload;
565
+ const matches = ctx.delegatedResultCorrelation.matches({ runId, toolUseId: payload.delegatedToolUseId, content: rawEvent.delegatedResultContent, isError: payload.isError === true });
566
+ return matches ? { ...payload, content: `Result shown by delegated call ${payload.delegatedToolUseId}.` } : payload;
567
+ }
551
568
  function rejectUnavailableBridge({ payload, rawEvent }, _options = {}) {
552
569
  if (!bridgeChecked && payload.type === 'status' && payload.label === 'initializing') {
553
570
  bridgeChecked = true;
@@ -616,8 +633,7 @@ export function wireChildLifecycle(ctx) {
616
633
  }
617
634
  child.stdout?.on('data', (chunk) => {
618
635
  const text = chunk.toString('utf8');
619
- if (journal)
620
- enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stdout', text) }));
636
+ stdoutDiagnostics.record({ chunk, text });
621
637
  if (streamFormat === 'plain') {
622
638
  if (text.length > 0)
623
639
  userVisibleOutputSeen = true;
@@ -643,14 +659,13 @@ export function wireChildLifecycle(ctx) {
643
659
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'text_delta', delta: text } } }));
644
660
  return;
645
661
  }
646
- enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
647
- // Before the parser: an ack and the aborted turn's end can share a chunk, and the turn end must
648
- // see that no interrupt is still waiting (see `stdinHoldAfterTurnEnd`); a background task
649
- // starting and the turn's end can share one too.
650
- scanControlLines(text);
651
- // Non-null: `streamHandler` is only ever null when `streamFormat === 'plain'` (see its
652
- // construction above), the branch this statement is provably unreachable from.
653
- streamHandler.feed({ chunk: text });
662
+ stdoutDiagnostics.feed({ 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 });
654
669
  });
655
670
  child.stderr?.on('data', (chunk) => {
656
671
  const text = chunk.toString('utf8');
@@ -673,9 +688,7 @@ export function wireChildLifecycle(ctx) {
673
688
  void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
674
689
  } });
675
690
  child.on('close', (code, signal) => {
676
- for (const settle of pendingInterruptAcks.values())
677
- settle(false);
678
- pendingInterruptAcks.clear();
691
+ stdinClosed = true;
679
692
  void (async () => {
680
693
  // Not wrapped in try/catch: all 4 supported parser factories'
681
694
  // flush() implementations already internally guard their own
@@ -685,7 +698,13 @@ export function wireChildLifecycle(ctx) {
685
698
  // here would be dead code for the fixed, closed set of parsers this
686
699
  // driver dispatches to. `streamHandler` is null for `'plain'` (no
687
700
  // parser, hence nothing to flush) — `?.` skips it cleanly.
701
+ if (controlScanBuffer) {
702
+ feedSessionLine(controlScanBuffer);
703
+ controlScanBuffer = '';
704
+ }
688
705
  streamHandler?.flush();
706
+ session.endSession();
707
+ stdoutDiagnostics.flush();
689
708
  // Queued before `await emitQueue` so the flushed text is durably appended ahead of
690
709
  // `finish()`'s `'end'` event, exactly like every live-path emit already is.
691
710
  flushBufferedStdout();
@@ -716,63 +735,27 @@ export function wireChildLifecycle(ctx) {
716
735
  })
717
736
  : false;
718
737
  }
719
- /**
720
- * A mid-run message: first an `interrupt` control request on the still-open stdin, so the agent
721
- * stops generating (or abandons the tool it is running) at once instead of finishing its whole
722
- * reply; then, once the CLI acknowledges, the message as the next user line, which the CLI
723
- * answers straight away in the same session; then the run's own `user_message` event through the
724
- * same FIFO as the agent's output, so the message sits in the stream where it took effect.
725
- * An ack that never comes (an agent that ignores control requests) only delays the write by
726
- * {@link INTERRUPT_ACK_TIMEOUT_MS}: the CLI then folds the line in at its next step. Once
727
- * `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
728
- * reach — `'not-running'`. Between turns, while stdin is held only for background tasks, there is
729
- * no generation to stop: the line is written at once and starts the next turn.
730
- * Host-prepared images use the same content blocks as the initial stdin turn; no file tools
731
- * or new directory grants are needed to see these pixels.
732
- */
733
- async function sendUserMessage(text, images = []) {
734
- if (!acceptsMidRunUserMessages({ def: def }))
735
- 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';
736
745
  const stdin = child.stdin;
737
746
  if (stdinClosed || !stdin)
738
747
  return 'not-running';
739
- if (heldForBackgroundTasks) {
740
- heldForBackgroundTasks = false;
741
- writeUserMessage(stdin, text, images);
742
- return 'delivered';
743
- }
744
- const requestId = `jini-interrupt-${randomUUID()}`;
745
- interruptSent = true;
746
- // Capture before requesting the interrupt. Its result can precede the acknowledgement, or
747
- // arrive after the next segment starts: neither ordering may cancel that segment's new tools.
748
- enqueueEmit(async () => {
749
- await toolSubscription;
750
- for (const id of interruptedToolCalls)
751
- if (!unresolvedToolCalls.has(id))
752
- interruptedToolCalls.delete(id);
753
- for (const id of unresolvedToolCalls)
754
- interruptedToolCalls.add(id);
755
- });
756
- const acked = new Promise((resolve) => pendingInterruptAcks.set(requestId, resolve));
757
- stdin.write(`${JSON.stringify({ type: 'control_request', request_id: requestId, request: { subtype: 'interrupt' } })}\n`, 'utf8');
758
- let timer;
759
- const timedOut = new Promise((resolve) => {
760
- timer = setTimeout(() => resolve(true), INTERRUPT_ACK_TIMEOUT_MS);
761
- });
762
- const processAlive = await Promise.race([acked, timedOut]);
763
- clearTimeout(timer);
764
- pendingInterruptAcks.delete(requestId);
765
- if (!processAlive || stdinClosed)
766
- return 'not-running';
767
- writeUserMessage(stdin, text, images);
748
+ heldForBackgroundTasks = false;
749
+ writeUserMessage(stdin, text, images, id);
768
750
  return 'delivered';
769
751
  }
770
- /** Writes a complete mid-run turn using the idle image encoder; journals text without base64. */
771
- function writeUserMessage(stdin, text, images) {
772
- 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');
773
757
  if (journal)
774
- enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(text) }));
775
- enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
758
+ enqueueEmit(() => journal.record({ runId, entry: sentJournalEntry(text) }));
776
759
  }
777
760
  return {
778
761
  closeStdinOnce,
@@ -783,45 +766,6 @@ export function wireChildLifecycle(ctx) {
783
766
  sendUserMessage,
784
767
  };
785
768
  }
786
- /**
787
- * Whether a def's live process can take another user message mid-run: only a stream-json stdin
788
- * transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
789
- * Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
790
- * reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
791
- */
792
- export function acceptsMidRunUserMessages({ def }) {
793
- return def.promptViaStdin === true && def.promptInputFormat === 'stream-json';
794
- }
795
- /**
796
- * How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
797
- * anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
798
- * the ceiling only bounds an agent that ignores control requests.
799
- */
800
- export const INTERRUPT_ACK_TIMEOUT_MS = 5_000;
801
- /** The `request_id` of a stream-json `control_response` line, or `undefined` for anything else. */
802
- function controlResponseRequestId({ line }) {
803
- try {
804
- const parsed = JSON.parse(line);
805
- if (!isRecord(parsed) || parsed.type !== 'control_response' || !isRecord(parsed.response))
806
- return undefined;
807
- return typeof parsed.response.request_id === 'string' ? parsed.response.request_id : undefined;
808
- }
809
- catch {
810
- return undefined;
811
- }
812
- }
813
- /** How many tasks a `background_tasks_changed` line lists, or `undefined` for any other line. */
814
- function backgroundTaskListLength({ line }) {
815
- try {
816
- const parsed = JSON.parse(line);
817
- if (!isRecord(parsed) || parsed.type !== 'system' || parsed.subtype !== 'background_tasks_changed')
818
- return undefined;
819
- return Array.isArray(parsed.tasks) ? parsed.tasks.length : undefined;
820
- }
821
- catch {
822
- return undefined;
823
- }
824
- }
825
769
  /** The stream-json stdin envelope for one user message — the initial prompt and every mid-run one. */
826
770
  function streamJsonUserMessage({ content }) {
827
771
  return { type: 'user', message: { role: 'user', content: content } };
@@ -865,6 +809,9 @@ function wireAcpLifecycle(ctx) {
865
809
  function enqueueEmit(task) {
866
810
  emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
867
811
  }
812
+ attachMessageRelay({ register: ctx.registerMessageRelay, child,
813
+ emit: data => enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data } })),
814
+ });
868
815
  if (ctx.model)
869
816
  enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.model } } }));
870
817
  child.stdout?.on('data', (chunk) => {
@@ -1064,6 +1011,7 @@ export function writePromptToStdin(def, child, prompt, handle) {
1064
1011
  export async function runAcpDispatch({ input, deps }) {
1065
1012
  try {
1066
1013
  wireAcpLifecycle({
1014
+ ...(deps.registerMessageRelay ? { registerMessageRelay: deps.registerMessageRelay } : {}),
1067
1015
  runId: input.runId,
1068
1016
  agentId: input.agentId,
1069
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" | "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