@jini-ai/daemon 0.5.4 → 0.6.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.
@@ -194,7 +194,27 @@ export interface AgentExecutor {
194
194
  * `lifecycle.finish()` before this rejects (see index.ts module doc's Invariant).
195
195
  */
196
196
  run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings">): Promise<void>;
197
+ /**
198
+ * Delivers a message the human sent while `runId` is going straight into its live agent process
199
+ * (see {@link UserMessageDelivery} for each outcome). A stream-json agent is interrupted first and
200
+ * gets the message once the CLI acknowledges the interrupt, so this settles in milliseconds (at
201
+ * most `INTERRUPT_ACK_TIMEOUT_MS`); it never waits on the agent's reply.
202
+ */
203
+ sendUserMessage(requiredArgs: {
204
+ readonly runId: string;
205
+ readonly text: string;
206
+ }): Promise<UserMessageDelivery>;
197
207
  }
208
+ /**
209
+ * What {@link AgentExecutor.sendUserMessage} did with a mid-run message:
210
+ * - `'delivered'` — the running turn was interrupted and the message written into the live
211
+ * session; the agent answers it next, in the same process and session.
212
+ * - `'unsupported'` — the run is live, but its agent has no way to take input mid-run (its prompt
213
+ * is plain text or an RPC request). The host should interrupt and send it as the next turn.
214
+ * - `'not-running'` — no live process will accept it: an unknown run, a finished one, or one whose
215
+ * last turn already ended and closed stdin. The run is ending; the host should send it next.
216
+ */
217
+ export type UserMessageDelivery = 'delivered' | 'unsupported' | 'not-running';
198
218
  /** Process discovery is a getter; process collection and stopping accept explicit host inputs. */
199
219
  export type CollectProcessTreePidsPort = (args: {
200
220
  readonly processes: ProcessSnapshot[];
@@ -212,11 +232,11 @@ export interface TerminateChildTreeDeps {
212
232
  * Which step reported a contained failure through `onCleanupFailure` (SEC-007).
213
233
  *
214
234
  * The first three come from {@link terminateChildTreeBestEffort} (process-tree teardown). The last
215
- * two are the two fallible steps that sit between a child's `'close'` and `finish()` — staged-file
216
- * removal and the host's own `classifyFailure` — neither of which may prevent the terminal
217
- * transition, and neither of which may fail silently either. See each close handler.
235
+ * three cover tool cancellation, staged-file removal and the host's own `classifyFailure`.
236
+ * These fallible steps between a child's `'close'` and `finish()` must never prevent the terminal
237
+ * transition, and none may fail silently either. See each close handler.
218
238
  */
219
- export type AgentCleanupFailurePhase = 'cancel' | 'acp-attach-failure' | 'pi-rpc-attach-failure' | 'staged-file-cleanup' | 'failure-classification';
239
+ export type AgentCleanupFailurePhase = 'cancel' | 'acp-attach-failure' | 'pi-rpc-attach-failure' | 'tool-cancellation' | 'staged-file-cleanup' | 'failure-classification';
220
240
  export interface AgentCleanupFailureContext {
221
241
  readonly runId: string;
222
242
  readonly phase: AgentCleanupFailurePhase;
@@ -237,6 +257,8 @@ export interface StdinCloseHandle {
237
257
  closeStdinOnce(): void;
238
258
  /** 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`). */
239
259
  recordSentBytes(content: string): void;
260
+ /** Interrupts this child's turn, then writes a mid-run user message into its open stdin — see {@link AgentExecutor.sendUserMessage}. */
261
+ sendUserMessage(text: string): Promise<UserMessageDelivery>;
240
262
  }
241
263
  /**
242
264
  * Gap 3 (capability-routed continuation transport) — host-owned config for the
@@ -374,6 +396,13 @@ export interface McpJsonInjectionOptions {
374
396
  readonly removeDir?: ({ path }: {
375
397
  readonly path: string;
376
398
  }) => Promise<void>;
399
+ /** `'codex-toml'` only. Links the run home's sessions/archived_sessions to the operator's
400
+ * persistent rollout store. Config and auth remain isolated and are removed after the run.
401
+ * @default Native mkdir (0700) and directory symlinks; errors fail staging before spawn. */
402
+ readonly linkSessionStore?: (required: {
403
+ runHome: string;
404
+ sourceHome: string;
405
+ }, optional?: {}) => Promise<void>;
377
406
  }
378
407
  /** One `.mcp.json` `mcpServers` entry — the shape Claude Code's own config schema expects. */
379
408
  export interface McpJsonServerEntry {
@@ -118,6 +118,21 @@ interface WireChildLifecycleContext extends TerminateChildTreeDeps {
118
118
  * @overallScore 100/100
119
119
  */
120
120
  export declare function wireChildLifecycle(ctx: WireChildLifecycleContext): StdinCloseHandle;
121
+ /**
122
+ * Whether a def's live process can take another user message mid-run: only a stream-json stdin
123
+ * transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
124
+ * Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
125
+ * reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
126
+ */
127
+ export declare function acceptsMidRunUserMessages({ def }: {
128
+ readonly def: RuntimeAgentDef;
129
+ }): boolean;
130
+ /**
131
+ * How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
132
+ * anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
133
+ * the ceiling only bounds an agent that ignores control requests.
134
+ */
135
+ export declare const INTERRUPT_ACK_TIMEOUT_MS = 5000;
121
136
  /**
122
137
  * Writes the initial user turn to the child's stdin per `def.promptInputFormat`
123
138
  * (both branches only run for `promptViaStdin: true` defs — the only shape
@@ -125,8 +140,8 @@ export declare function wireChildLifecycle(ctx: WireChildLifecycleContext): Stdi
125
140
  * - `'text'` (default): the raw prompt buffer, then stdin is closed —
126
141
  * matches `RuntimeAgentDef.promptInputFormat`'s own doc.
127
142
  * - `'stream-json'`: one JSONL line wrapping the prompt as an Anthropic
128
- * user message; stdin is deliberately left open (a real multi-turn
129
- * caller would inject further messages) — v1 has no such caller, so
143
+ * user message; stdin is deliberately left open so a message the human
144
+ * sends mid-run can follow it (`StdinCloseHandle.sendUserMessage`), and
130
145
  * {@link wireChildLifecycle}'s `turn_end` handling closes it once the
131
146
  * agent's own stream reports the turn ended.
132
147
  * @param def - The resolved agent def (only `.promptInputFormat` is read).
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { createModelReceiptTracker } from './model-receipts.js';
2
3
  import { messageContentWithImages } from '../attachment-content.js';
3
4
  import { redactSecrets, } from '@jini-ai/core';
@@ -8,7 +9,7 @@ import { resolveContinuationTransport, } from '../continuation/continuation-tran
8
9
  import { resultContent, } from '../delegated-tool-bridge.js';
9
10
  import { extractResultMedia, } from '../tool-result-media.js';
10
11
  import { translateAgentRuntimeEvent, translateAcpError, applyAgentTranslationSideEffects, } from './event-translation.js';
11
- import { errorMessage, } from './values.js';
12
+ import { errorMessage, isRecord, } from './values.js';
12
13
  import { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, bridgeUnavailableMessage, } from './mcp-bridge.js';
13
14
  /** Carry PID only on structured status so the host can record its OS start identity early.
14
15
  * A PID alone is never proof of death; hosts verify PID reuse before native continuation. */
@@ -68,7 +69,7 @@ export function defaultCleanupFailureSink(context) {
68
69
  /**
69
70
  * Reports a contained post-close failure through the host's sink, absorbing a throwing sink.
70
71
  *
71
- * A diagnostic sink is host code too, and the whole point of the two callers below is that nothing
72
+ * A diagnostic sink is host code too, and the whole point of these callers is that nothing
72
73
  * between `'close'` and `finish()` can strand the run — a sink that throws must not reintroduce
73
74
  * exactly that. Same reasoning `run-lifecycle.ts`'s `handleInactivityTimeout` already applies to its
74
75
  * own `onInternalError`.
@@ -273,6 +274,37 @@ export function wireChildLifecycle(ctx) {
273
274
  // consumed by a turn-end injection decision. See `ContinuationOptions`'s doc for why this is
274
275
  // only ever acted on when a host has explicitly allowlisted the tool's name.
275
276
  let pendingToolUse;
277
+ // Observe the canonical lifecycle, including delegated calls that bypass the CLI parser.
278
+ // A single latest tool_use cannot represent parallel tools at an interrupt boundary.
279
+ const unresolvedToolCalls = new Set();
280
+ const interruptedToolCalls = new Set();
281
+ const toolSubscription = lifecycle.stream({ runId, onEvent: (event) => {
282
+ if (event.kind !== 'agent')
283
+ return;
284
+ if (event.payload.type === 'tool_use')
285
+ unresolvedToolCalls.add(event.payload.id);
286
+ if (event.payload.type === 'tool_result')
287
+ unresolvedToolCalls.delete(event.payload.toolUseId);
288
+ } }).catch(error => {
289
+ // Observing tools is fallible I/O too: report it, but never strand close/finish on a replay
290
+ // failure or leave an unhandled rejection while the child is still running.
291
+ reportPostCloseFailure(ctx.onCleanupFailure, { runId, phase: 'tool-cancellation', pid: child.pid, error });
292
+ return null;
293
+ });
294
+ /** Run on the same FIFO as parsed results, after the aborted segment has drained.
295
+ * O(t) in unresolved calls; completed results are never replaced with cancellation. */
296
+ async function cancelUnresolvedToolCalls(ids) {
297
+ await toolSubscription;
298
+ for (const toolUseId of ids ?? [...unresolvedToolCalls]) {
299
+ if (!unresolvedToolCalls.has(toolUseId))
300
+ continue;
301
+ await lifecycle.emit({ runId, input: { event: 'agent', data: {
302
+ type: 'tool_result', toolUseId, content: 'Tool execution cancelled.', isError: true,
303
+ } } }).catch(error => {
304
+ reportPostCloseFailure(ctx.onCleanupFailure, { runId, phase: 'tool-cancellation', pid: child.pid, error });
305
+ });
306
+ }
307
+ }
276
308
  // Bridge guard (see `expectsJiniBridge`): checked on the first init frame only. Once the bridge is
277
309
  // found unavailable, the child is being stopped and nothing it still prints is forwarded.
278
310
  let bridgeChecked = !ctx.expectsJiniBridge;
@@ -311,6 +343,35 @@ export function wireChildLifecycle(ctx) {
311
343
  stdinClosed = true;
312
344
  child.stdin?.end();
313
345
  }
346
+ // Mid-run interrupts (see `sendUserMessage` below): each sent `interrupt` control request waits
347
+ // here, by request id, for the CLI's `control_response`. Settled with `true` on the ack, `false`
348
+ // when the process exits first. Bounded by the mid-run messages in flight on this one run.
349
+ const pendingInterruptAcks = new Map();
350
+ // Set once this run has interrupted its agent: from then on a turn the interrupt aborted
351
+ // (`aborted_*` stop reason) is not the run's last — the message that follows starts a new one.
352
+ let interruptSent = false;
353
+ // stdout carry-over for spotting acks; only filled while an interrupt waits for one.
354
+ let ackScanBuffer = '';
355
+ /** Resolves every waiting interrupt whose `control_response` is in this stdout chunk. @complexity O(chunk length). */
356
+ function scanForInterruptAcks(text) {
357
+ if (pendingInterruptAcks.size === 0)
358
+ return;
359
+ ackScanBuffer += text;
360
+ const lines = ackScanBuffer.split('\n');
361
+ ackScanBuffer = lines.pop() ?? '';
362
+ for (const line of lines) {
363
+ if (!line.includes('"control_response"'))
364
+ continue;
365
+ const requestId = controlResponseRequestId({ line: line });
366
+ const settle = requestId === undefined ? undefined : pendingInterruptAcks.get(requestId);
367
+ if (!settle)
368
+ continue;
369
+ pendingInterruptAcks.delete(requestId);
370
+ settle(true);
371
+ }
372
+ if (pendingInterruptAcks.size === 0)
373
+ ackScanBuffer = '';
374
+ }
314
375
  /**
315
376
  * Writes a structured (never string-concatenated — see `ContinuationOptions`'s doc on the
316
377
  * prompt-injection stakes here) tool_result JSONL line, mirroring the shape
@@ -330,6 +391,21 @@ export function wireChildLifecycle(ctx) {
330
391
  if (journal)
331
392
  enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
332
393
  }
394
+ /**
395
+ * Whether a `turn_end` leaves stdin open for a live agent that takes mid-run messages, because
396
+ * the run is not over: a `tool_use` stop (the CLI runs the tool itself and carries on, and a
397
+ * message sent while the tool runs must still reach it), any end while an interrupt still waits
398
+ * for its ack (the message is about to be written), and the end of a turn an interrupt aborted
399
+ * (the message written after it starts the next turn). Every other end closes stdin, so the CLI
400
+ * exits once it has answered everything written before EOF.
401
+ */
402
+ function keepsStdinOpenAfterTurnEnd(stopReason) {
403
+ if (!acceptsMidRunUserMessages({ def: def }))
404
+ return false;
405
+ if (stopReason === 'tool_use' || pendingInterruptAcks.size > 0)
406
+ return true;
407
+ return interruptSent && stopReason !== undefined && stopReason.startsWith('aborted');
408
+ }
333
409
  /**
334
410
  * Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
335
411
  * `ToolExecutor` and keep stdin open (gap 3), or close stdin exactly as every version of this
@@ -337,6 +413,11 @@ export function wireChildLifecycle(ctx) {
337
413
  * unconfigured or the pending tool isn't allowlisted).
338
414
  */
339
415
  function handleTurnEnd(stopReason) {
416
+ if (stopReason?.startsWith('aborted'))
417
+ enqueueEmit(async () => {
418
+ await cancelUnresolvedToolCalls([...interruptedToolCalls]);
419
+ interruptedToolCalls.clear();
420
+ });
340
421
  const toolUse = pendingToolUse;
341
422
  const shouldInject = stopReason === 'tool_use' &&
342
423
  toolUse !== undefined &&
@@ -344,7 +425,8 @@ export function wireChildLifecycle(ctx) {
344
425
  resolveContinuationTransport({ def: def }) === 'stdin-injection' &&
345
426
  continuation.autonomousToolNames.has(toolUse.name);
346
427
  if (!shouldInject) {
347
- closeStdinOnce();
428
+ if (!keepsStdinOpenAfterTurnEnd(stopReason))
429
+ closeStdinOnce();
348
430
  return;
349
431
  }
350
432
  pendingToolUse = undefined;
@@ -497,6 +579,9 @@ export function wireChildLifecycle(ctx) {
497
579
  return;
498
580
  }
499
581
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
582
+ // Before the parser: an ack and the aborted turn's end can share a chunk, and the turn end must
583
+ // see that no interrupt is still waiting (see `keepsStdinOpenAfterTurnEnd`).
584
+ scanForInterruptAcks(text);
500
585
  // Non-null: `streamHandler` is only ever null when `streamFormat === 'plain'` (see its
501
586
  // construction above), the branch this statement is provably unreachable from.
502
587
  streamHandler.feed({ chunk: text });
@@ -522,6 +607,9 @@ export function wireChildLifecycle(ctx) {
522
607
  void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
523
608
  } });
524
609
  child.on('close', (code, signal) => {
610
+ for (const settle of pendingInterruptAcks.values())
611
+ settle(false);
612
+ pendingInterruptAcks.clear();
525
613
  void (async () => {
526
614
  // Not wrapped in try/catch: all 4 supported parser factories'
527
615
  // flush() implementations already internally guard their own
@@ -536,6 +624,11 @@ export function wireChildLifecycle(ctx) {
536
624
  // `finish()`'s `'end'` event, exactly like every live-path emit already is.
537
625
  flushBufferedStdout();
538
626
  await emitQueue;
627
+ if (cancelRequested)
628
+ await cancelUnresolvedToolCalls();
629
+ const subscribedTools = await toolSubscription;
630
+ if (subscribedTools?.kind === 'ok')
631
+ subscribedTools.unsubscribe();
539
632
  unsubscribeCancel();
540
633
  // Both of the next two steps are guarded: neither a failed cleanup nor a rejecting host
541
634
  // classifier may prevent the terminal transition below — see each helper's own doc.
@@ -554,14 +647,92 @@ export function wireChildLifecycle(ctx) {
554
647
  await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
555
648
  })();
556
649
  });
650
+ /**
651
+ * A mid-run message: first an `interrupt` control request on the still-open stdin, so the agent
652
+ * stops generating (or abandons the tool it is running) at once instead of finishing its whole
653
+ * reply; then, once the CLI acknowledges, the message as the next user line, which the CLI
654
+ * answers straight away in the same session; then the run's own `user_message` event through the
655
+ * same FIFO as the agent's output, so the message sits in the stream where it took effect.
656
+ * An ack that never comes (an agent that ignores control requests) only delays the write by
657
+ * {@link INTERRUPT_ACK_TIMEOUT_MS}: the CLI then folds the line in at its next step. Once
658
+ * `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
659
+ * reach — `'not-running'`.
660
+ */
661
+ async function sendUserMessage(text) {
662
+ if (!acceptsMidRunUserMessages({ def: def }))
663
+ return 'unsupported';
664
+ const stdin = child.stdin;
665
+ if (stdinClosed || !stdin)
666
+ return 'not-running';
667
+ const requestId = `jini-interrupt-${randomUUID()}`;
668
+ interruptSent = true;
669
+ // Capture before requesting the interrupt. Its result can precede the acknowledgement, or
670
+ // arrive after the next segment starts: neither ordering may cancel that segment's new tools.
671
+ enqueueEmit(async () => {
672
+ await toolSubscription;
673
+ for (const id of interruptedToolCalls)
674
+ if (!unresolvedToolCalls.has(id))
675
+ interruptedToolCalls.delete(id);
676
+ for (const id of unresolvedToolCalls)
677
+ interruptedToolCalls.add(id);
678
+ });
679
+ const acked = new Promise((resolve) => pendingInterruptAcks.set(requestId, resolve));
680
+ stdin.write(`${JSON.stringify({ type: 'control_request', request_id: requestId, request: { subtype: 'interrupt' } })}\n`, 'utf8');
681
+ let timer;
682
+ const timedOut = new Promise((resolve) => {
683
+ timer = setTimeout(() => resolve(true), INTERRUPT_ACK_TIMEOUT_MS);
684
+ });
685
+ const processAlive = await Promise.race([acked, timedOut]);
686
+ clearTimeout(timer);
687
+ pendingInterruptAcks.delete(requestId);
688
+ if (!processAlive || stdinClosed)
689
+ return 'not-running';
690
+ stdin.write(`${JSON.stringify(streamJsonUserMessage({ content: [{ type: 'text', text: text }] }))}\n`, 'utf8');
691
+ if (journal)
692
+ enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(text) }));
693
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
694
+ return 'delivered';
695
+ }
557
696
  return {
558
697
  closeStdinOnce,
559
698
  recordSentBytes(content) {
560
699
  if (journal)
561
700
  enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
562
701
  },
702
+ sendUserMessage,
563
703
  };
564
704
  }
705
+ /**
706
+ * Whether a def's live process can take another user message mid-run: only a stream-json stdin
707
+ * transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
708
+ * Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
709
+ * reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
710
+ */
711
+ export function acceptsMidRunUserMessages({ def }) {
712
+ return def.promptViaStdin === true && def.promptInputFormat === 'stream-json';
713
+ }
714
+ /**
715
+ * How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
716
+ * anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
717
+ * the ceiling only bounds an agent that ignores control requests.
718
+ */
719
+ export const INTERRUPT_ACK_TIMEOUT_MS = 5_000;
720
+ /** The `request_id` of a stream-json `control_response` line, or `undefined` for anything else. */
721
+ function controlResponseRequestId({ line }) {
722
+ try {
723
+ const parsed = JSON.parse(line);
724
+ if (!isRecord(parsed) || parsed.type !== 'control_response' || !isRecord(parsed.response))
725
+ return undefined;
726
+ return typeof parsed.response.request_id === 'string' ? parsed.response.request_id : undefined;
727
+ }
728
+ catch {
729
+ return undefined;
730
+ }
731
+ }
732
+ /** The stream-json stdin envelope for one user message — the initial prompt and every mid-run one. */
733
+ function streamJsonUserMessage({ content }) {
734
+ return { type: 'user', message: { role: 'user', content: content } };
735
+ }
565
736
  /**
566
737
  * Wires an ACP child to a run. Unlike the JSON-stream path, ACP owns the
567
738
  * prompt protocol and reports its parsed events through `attachAcpSession`'s
@@ -773,8 +944,8 @@ function wirePiRpcLifecycle(ctx) {
773
944
  * - `'text'` (default): the raw prompt buffer, then stdin is closed —
774
945
  * matches `RuntimeAgentDef.promptInputFormat`'s own doc.
775
946
  * - `'stream-json'`: one JSONL line wrapping the prompt as an Anthropic
776
- * user message; stdin is deliberately left open (a real multi-turn
777
- * caller would inject further messages) — v1 has no such caller, so
947
+ * user message; stdin is deliberately left open so a message the human
948
+ * sends mid-run can follow it (`StdinCloseHandle.sendUserMessage`), and
778
949
  * {@link wireChildLifecycle}'s `turn_end` handling closes it once the
779
950
  * agent's own stream reports the turn ended.
780
951
  * @param def - The resolved agent def (only `.promptInputFormat` is read).
@@ -789,7 +960,7 @@ export function writePromptToStdin(def, child, prompt, handle) {
789
960
  if (!stdin)
790
961
  return;
791
962
  if (def.promptInputFormat === 'stream-json') {
792
- const line = JSON.stringify({ type: 'user', message: { role: 'user', content: messageContentWithImages({ prompt, images: handle.imageContents ?? [] }, {}) } });
963
+ const line = JSON.stringify(streamJsonUserMessage({ content: messageContentWithImages({ prompt, images: handle.imageContents ?? [] }, {}) }));
793
964
  stdin.write(`${line}\n`, 'utf8');
794
965
  handle.recordSentBytes(prompt);
795
966
  return;
@@ -89,7 +89,7 @@
89
89
  * a second, independently-built (and rejected) classifier that once lived in
90
90
  * this module.
91
91
  */
92
- export { type SupportedStreamFormat, type AgentExecutorCompatibility, type AgentRuntimeEventTranslation, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, type McpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, type FailureClassificationContext, type ClassifyFailure, type CreateAgentExecutorOptions, type SystemPromptOverlayDelivery, type PreparedSystemPromptOverlayFile, type SpawnAgentChildProcessResult, } from './contracts.js';
92
+ export { type SupportedStreamFormat, type AgentExecutorCompatibility, type AgentRuntimeEventTranslation, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, type McpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, type FailureClassificationContext, type ClassifyFailure, type CreateAgentExecutorOptions, type SystemPromptOverlayDelivery, type PreparedSystemPromptOverlayFile, type SpawnAgentChildProcessResult, } from './contracts.js';
93
93
  export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
94
94
  export { extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, applyAgentTranslationSideEffects, } from './event-translation.js';
95
95
  export { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, resolveMcpBridgeForRun, writeMcpJsonIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, } from './mcp-bridge.js';
@@ -640,6 +640,18 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
640
640
  await lifecycle.finish({ runId, status: 'failed', code: null, signal: null, resumable: false });
641
641
  throw new AgentExecutorError({ code: code, message: message });
642
642
  }
643
+ /**
644
+ * Every run with a live child, keyed by run id, mapped to how a mid-run message reaches it — the
645
+ * stdin handle's `sendUserMessage` for a stdin-driven run, a constant `'unsupported'` for an
646
+ * ACP/pi run (whose RPC takes one prompt per turn). Removed on the child's `'close'`, so a
647
+ * finished or unknown run reads as `'not-running'`. Bounded by the number of live children.
648
+ */
649
+ const liveRunInputs = new Map();
650
+ /** `AgentExecutor.sendUserMessage()` — see that interface method's own doc. @complexity O(1). */
651
+ async function sendUserMessage({ runId, text }) {
652
+ const deliver = liveRunInputs.get(runId);
653
+ return deliver ? deliver(text) : 'not-running';
654
+ }
643
655
  /**
644
656
  * `AgentExecutor.run()` — see that interface method's own doc for the
645
657
  * public contract. Implementation note on shape: every guard below
@@ -934,7 +946,18 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
934
946
  expectsJiniBridge: writtenMcpJsonPath !== undefined,
935
947
  })
936
948
  : null;
937
- await confirmChildSpawned({ input: { runId: input.runId, def, child }, deps: { releaseStagedResources, failBeforeSpawn } });
949
+ // Registered with the lifecycle wiring above, before the spawn-confirmation await, so a child
950
+ // that closes immediately never leaves a stale entry behind.
951
+ const runId = input.runId;
952
+ liveRunInputs.set(runId, stdinHandle ? stdinHandle.sendUserMessage : async () => 'unsupported');
953
+ child.once('close', () => liveRunInputs.delete(runId));
954
+ try {
955
+ await confirmChildSpawned({ input: { runId: input.runId, def, child }, deps: { releaseStagedResources, failBeforeSpawn } });
956
+ }
957
+ catch (error) {
958
+ liveRunInputs.delete(runId);
959
+ throw error;
960
+ }
938
961
  // Now — and only now — is there a live process that could consume the locked side effect, so
939
962
  // this is where a def's handoff watcher starts. Deliberately not awaited: the whole point is to
940
963
  // release the lock as soon as the child confirms the handoff, in parallel with this run
@@ -1019,6 +1042,6 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1019
1042
  stdinHandle.imageContents = input.imageContents;
1020
1043
  writePromptToStdin(def, child, stdinPrompt, stdinHandle);
1021
1044
  }
1022
- return { run };
1045
+ return { run, sendUserMessage };
1023
1046
  }
1024
1047
  //# sourceMappingURL=launch.js.map
@@ -1,6 +1,6 @@
1
1
  import { promises as fsPromises, } from 'node:fs';
2
2
  import { homedir, tmpdir, } from 'node:os';
3
- import { join, } from 'node:path';
3
+ import { join, resolve, } from 'node:path';
4
4
  import { isRecord, errorMessage, } from './values.js';
5
5
  const JINI_MCP_SERVER_KEY = 'jini';
6
6
  /** The run-error code a run gets when its CLI reports the injected `jini` bridge as anything but connected. */
@@ -552,12 +552,23 @@ function defaultMkdtempCodexHome({ prefix }) {
552
552
  function defaultRemoveCodexHomeDir({ path }) {
553
553
  return fsPromises.rm(path, { recursive: true, force: true });
554
554
  }
555
+ /** Config credentials stay run-scoped, but rollouts must survive both completion and cancellation.
556
+ * Native directory links keep Codex as the sole writer of its own persisted session format.
557
+ * Cleanup removes the links themselves, never their targets. Two fixed directories, O(1) I/O. */
558
+ async function defaultLinkCodexSessionStore({ runHome, sourceHome }, _optional = {}) {
559
+ for (const name of ['sessions', 'archived_sessions']) {
560
+ const target = resolve(sourceHome, name);
561
+ await fsPromises.mkdir(target, { recursive: true, mode: 0o700 });
562
+ await fsPromises.symlink(target, join(runHome, name), 'junction');
563
+ }
564
+ }
555
565
  function resolveCodexHomeSeams(options) {
556
566
  return {
557
567
  mkdtemp: options.mkdtemp ?? defaultMkdtempCodexHome,
558
568
  readFile: options.readFile ?? defaultReadMcpJsonFile,
559
569
  writeFile: options.writeFile ?? defaultWriteMcpJsonFile,
560
570
  removeDir: options.removeDir ?? defaultRemoveCodexHomeDir,
571
+ linkSessionStore: options.linkSessionStore ?? defaultLinkCodexSessionStore,
561
572
  };
562
573
  }
563
574
  /**
@@ -573,8 +584,9 @@ function resolveCodexHomeSeams(options) {
573
584
  * 0.151.0) to fail fast with a structured `401 Unauthorized` stream event, never an interactive
574
585
  * login prompt or a hang — see `defs/codex.ts`'s module doc for the full transcript summary.
575
586
  *
576
- * **Never touches the real `CODEX_HOME`.** `sourceCodexHomeDir` is read-only throughout; nothing is
577
- * ever written back to it.
587
+ * **Never changes the real config or credentials.** Those source files stay read-only. Only the
588
+ * native rollout directories are shared: deleting a run's scratch home must not erase the thread
589
+ * whose id the host already persisted. No copied login or run-bound MCP credential is retained.
578
590
  *
579
591
  * A failure after the directory is created (a rejecting `writeFile`, most plausibly) does not leak
580
592
  * it: the directory may already hold a partial `config.toml` or a copied credential, so the
@@ -584,9 +596,9 @@ function resolveCodexHomeSeams(options) {
584
596
  * `@jini-ai/agent-runtime`'s `prepareAgentLogFile`'s `label` is.
585
597
  * @param entry - The shared bridge entry.
586
598
  * @param sourceCodexHomeDir - Where to read the real install's `config.toml`/`auth.json` from — see {@link resolveSourceCodexHomeDir}.
587
- * @param seams - Injectable mkdtemp/readFile/writeFile/removeDir, real filesystem by default.
588
- * @throws Whatever `mkdtemp`/`writeFile` rejects with — the caller ({@link prepareCodexHomeIfNeeded}) turns that into a pre-spawn `AGENT_SPAWN_FAILED` failure, matching {@link writeMcpJsonForRun}'s own contract.
589
- * @complexity O(1) plus one directory creation and up to two best-effort file read/write round trips.
599
+ * @param seams - Injectable mkdtemp/readFile/writeFile/removeDir/linkSessionStore, real filesystem by default.
600
+ * @throws Whatever staging rejects with — the caller ({@link prepareCodexHomeIfNeeded}) turns that into a pre-spawn `AGENT_SPAWN_FAILED` failure, matching {@link writeMcpJsonForRun}'s own contract.
601
+ * @complexity O(1): fixed directory links and up to two best-effort file read/write round trips.
590
602
  * @overallScore 100/100
591
603
  */
592
604
  async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
@@ -616,6 +628,7 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
616
628
  // No stored login (or unreadable) — the spawned CLI runs unauthenticated. Confirmed above:
617
629
  // this fails the run fast and observably, never as a hang.
618
630
  }
631
+ await seams.linkSessionStore({ runHome: dir, sourceHome: sourceCodexHomeDir }, {});
619
632
  }
620
633
  catch (err) {
621
634
  await seams.removeDir({ path: dir }).catch(() => {
@@ -6,5 +6,5 @@
6
6
  * wrappers or a second implementation would let tested and shipped behavior drift.
7
7
  * See agent-executor/index.ts for the driver contracts and invariants.
8
8
  */
9
- export { type SupportedStreamFormat, isSupportedStreamFormat, type AgentExecutorCompatibility, assessAgentExecutorCompatibility, isAgentExecutorSupported, type AgentRuntimeEventTranslation, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, type McpBridgeDelivery, buildMcpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, resolveSourceClaudeConfigDir, type FailureClassificationContext, type ClassifyFailure, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, type CreateAgentExecutorOptions, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, type SystemPromptOverlayDelivery, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, type PreparedSystemPromptOverlayFile, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, type SpawnAgentChildProcessResult, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
9
+ export { type SupportedStreamFormat, isSupportedStreamFormat, type AgentExecutorCompatibility, assessAgentExecutorCompatibility, isAgentExecutorSupported, type AgentRuntimeEventTranslation, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, type McpBridgeDelivery, buildMcpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, resolveSourceClaudeConfigDir, type FailureClassificationContext, type ClassifyFailure, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, type CreateAgentExecutorOptions, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, type SystemPromptOverlayDelivery, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, type PreparedSystemPromptOverlayFile, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, type SpawnAgentChildProcessResult, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
10
10
  //# sourceMappingURL=agent-executor.d.ts.map
@@ -9,8 +9,8 @@ export type { CatalogueEntry, HostEditor, HostEditorsResponse, HostToolLaunchPla
9
9
  export { applicableForPlatform, CATALOGUE, currentPlatform, defaultProbeEnv, hostEditorsRoute, launchHostTool, listAvailableEditors, pathDirs, probeCommandOnPath, probeMacBundle, registerHostToolsRoutes, resolveEntry, resolveHostToolLaunchPlan, } from './host-tools.js';
10
10
  export type { DaemonShutdownResponse, DaemonStatusDeps, DaemonStatusResponse, } from './daemon-status.js';
11
11
  export { daemonShutdownRoute, daemonStatusRoute, registerDaemonStatusRoutes } from './daemon-status.js';
12
- export type { RunCancelResponse, RunCreateRequest, RunHttpDeps, RunInternalErrorContext, RunListResponse, RunStartContext, RunStartHandler, RunStartResponse, RunStatusResponse, } from './runs.js';
13
- export { registerRunEventStream, registerRunRoutes, RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
12
+ export type { RunCancelResponse, RunCreateRequest, RunHttpDeps, RunInternalErrorContext, RunListResponse, RunStartContext, RunMessageRequest, RunMessageResponse, RunStartHandler, RunStartResponse, RunStatusResponse, } from './runs.js';
13
+ export { registerRunEventStream, registerRunRoutes, RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runMessageRoute, RUN_MESSAGE_MAX_CHARS, runStartRoute, runStatusRoute, } from './runs.js';
14
14
  export { JINI_ROUTE_MANIFEST, manifestRoutesForFamilies, routeFamilyManifest } from './route-manifest.js';
15
15
  export type { AgentListResponse, AgentModelSummary, AgentsHttpDeps, AgentSummary } from './agents.js';
16
16
  export { agentListRoute, agentRescanRoute, registerAgentRoutes } from './agents.js';
@@ -3,7 +3,7 @@ export { cancelRunsOwnedBy } from './cancel-owned-runs.js';
3
3
  export { ACTIVE_CONTEXT_TTL_MS, getActiveRoute, registerActiveContextRoutes, setActiveRoute, } from './active-context.js';
4
4
  export { applicableForPlatform, CATALOGUE, currentPlatform, defaultProbeEnv, hostEditorsRoute, launchHostTool, listAvailableEditors, pathDirs, probeCommandOnPath, probeMacBundle, registerHostToolsRoutes, resolveEntry, resolveHostToolLaunchPlan, } from './host-tools.js';
5
5
  export { daemonShutdownRoute, daemonStatusRoute, registerDaemonStatusRoutes } from './daemon-status.js';
6
- export { registerRunEventStream, registerRunRoutes, RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
6
+ export { registerRunEventStream, registerRunRoutes, RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runMessageRoute, RUN_MESSAGE_MAX_CHARS, runStartRoute, runStatusRoute, } from './runs.js';
7
7
  export { JINI_ROUTE_MANIFEST, manifestRoutesForFamilies, routeFamilyManifest } from './route-manifest.js';
8
8
  export { agentListRoute, agentRescanRoute, registerAgentRoutes } from './agents.js';
9
9
  export { memoryClearExtractionsRoute, memoryClearVerificationsRoute, memoryCreateEntryRoute, memoryDeleteEntryRoute, memoryListExtractionsRoute, memoryListVerificationsRoute, memoryOverviewRoute, memoryReadEntryRoute, memoryRemoveExtractionRoute, memoryRemoveVerificationRoute, memoryTreeRoute, memoryUpdateEntryRoute, memoryUpdateTreeNodeRoute, memoryWriteConfigRoute, memoryWriteIndexRoute, registerMemoryEventStream, registerMemoryRoutes, } from './memory.js';
@@ -31,7 +31,7 @@
31
31
  import { agentListRoute, agentRescanRoute } from './agents.js';
32
32
  import { delegatedToolExecuteRoute } from './delegated-tools.js';
33
33
  import { apiHealthRoute, apiReadyRoute, apiVersionInfoRoute, healthRoute, readyRoute, versionInfoRoute, } from './health.js';
34
- import { RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
34
+ import { RUN_EVENTS_ROUTE_PATH, runCancelRoute, runMessageRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
35
35
  import { toolCatalogDescribeRoute, toolCatalogSearchRoute } from './tool-catalog.js';
36
36
  /**
37
37
  * Projects route specs onto `RouteRegistration`s, upper-casing the method so entries compare directly
@@ -52,7 +52,7 @@ function fromSpecs(...specs) {
52
52
  export const JINI_ROUTE_MANIFEST = {
53
53
  health: fromSpecs(healthRoute, apiHealthRoute, readyRoute, apiReadyRoute, versionInfoRoute, apiVersionInfoRoute),
54
54
  runs: [
55
- ...fromSpecs(runStartRoute, runListRoute, runStatusRoute, runCancelRoute),
55
+ ...fromSpecs(runStartRoute, runListRoute, runStatusRoute, runCancelRoute, runMessageRoute),
56
56
  // Registered with a bare `app.get` rather than a route spec (by `registerRunRoutes` itself), so
57
57
  // it contributes its exported path constant instead of a copied literal.
58
58
  { method: 'GET', path: RUN_EVENTS_ROUTE_PATH },
@@ -1,6 +1,6 @@
1
1
  import type { Express } from 'express';
2
2
  import { type RunStatus } from '@jini-ai/protocol';
3
- import type { RunLifecycle } from '../index.js';
3
+ import type { RunLifecycle, UserMessageDelivery } from '../index.js';
4
4
  import { type AdapterContext } from '@jini-ai/http-kit';
5
5
  import type { ServerResponse } from 'node:http';
6
6
  /**
@@ -34,6 +34,15 @@ export interface RunHttpDeps {
34
34
  readonly onStarted?: RunStartHandler;
35
35
  /** Host-owned sink for the real exception behind a generic `INTERNAL_ERROR` response (SEC-005). Defaults to `console.error`. */
36
36
  readonly onInternalError?: (context: RunInternalErrorContext) => void;
37
+ /**
38
+ * Delivers a message the human sent mid-run into that run's live agent — a host passes its
39
+ * `AgentExecutor.sendUserMessage`. Omitted, every such message reports `'unsupported'`, so the
40
+ * client falls back to interrupting the run and sending the text as the next turn.
41
+ */
42
+ readonly deliverUserMessage?: (input: {
43
+ readonly runId: string;
44
+ readonly text: string;
45
+ }) => UserMessageDelivery | Promise<UserMessageDelivery>;
37
46
  }
38
47
  export interface RunStartResponse {
39
48
  readonly run: RunStatus;
@@ -45,6 +54,15 @@ export interface RunStatusResponse {
45
54
  export interface RunCancelResponse {
46
55
  readonly run: RunStatus;
47
56
  }
57
+ export interface RunMessageRequest {
58
+ readonly runId: string;
59
+ readonly text: string;
60
+ }
61
+ export interface RunMessageResponse {
62
+ readonly delivery: 'delivered';
63
+ }
64
+ /** Ceiling on one mid-run message, matching what a person can reasonably type or paste into a composer. */
65
+ export declare const RUN_MESSAGE_MAX_CHARS = 100000;
48
66
  export interface RunListResponse {
49
67
  readonly runs: readonly RunStatus[];
50
68
  }
@@ -58,6 +76,13 @@ export declare const runCancelRoute: import("@jini-ai/http-kit").JsonRouteSpec<{
58
76
  runId: string;
59
77
  reason?: string;
60
78
  }, RunCancelResponse, RunHttpDeps, undefined>;
79
+ /**
80
+ * `POST /api/runs/:runId/messages` — a message the human sent while the run is going. `202` when it
81
+ * reached the live agent; `409 CONFLICT` with `details.delivery` (`'unsupported'` or
82
+ * `'not-running'`) when it did not, which tells the client whether to interrupt the run and send the
83
+ * text as the next turn, or simply send it next because the run is already ending.
84
+ */
85
+ export declare const runMessageRoute: import("@jini-ai/http-kit").JsonRouteSpec<RunMessageRequest, RunMessageResponse, RunHttpDeps, undefined>;
61
86
  /**
62
87
  * The core of `GET /api/runs/:runId/events` — canonical events as SSE, with Last-Event-ID
63
88
  * reconnect support. Takes `runId`/`afterCursor` already resolved by the caller. `res` is typed
@@ -82,7 +107,7 @@ export declare function registerRunEventStream({ app, deps }: {
82
107
  readonly app: Express;
83
108
  readonly deps: RunHttpDeps;
84
109
  }, _optional?: Record<string, never>): void;
85
- /** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
110
+ /** Mounts create/status/cancel/message JSON endpoints and the SSE event stream as one run transport. */
86
111
  export declare function registerRunRoutes({ app, deps, adapter }: {
87
112
  readonly app: Express;
88
113
  readonly deps: RunHttpDeps;
package/dist/http/runs.js CHANGED
@@ -57,6 +57,23 @@ function parseRunCreate(input) {
57
57
  }
58
58
  });
59
59
  }
60
+ /** Ceiling on one mid-run message, matching what a person can reasonably type or paste into a composer. */
61
+ export const RUN_MESSAGE_MAX_CHARS = 100_000;
62
+ function parseRunMessage(input) {
63
+ const parsedRunId = parseRunId(input);
64
+ if (!parsedRunId.ok)
65
+ return parsedRunId;
66
+ if (!isRecord(input.body))
67
+ return err({ error: validationError({ message: 'body must be a JSON object' }) });
68
+ const text = optionalString(input.body, 'text');
69
+ if (text === undefined || text === null) {
70
+ return err({ error: validationError({ message: 'text must be a non-empty string' }, { issues: [{ path: 'text', message: 'required non-empty string' }] }) });
71
+ }
72
+ if (text.length > RUN_MESSAGE_MAX_CHARS) {
73
+ return err({ error: validationError({ message: `text must be at most ${RUN_MESSAGE_MAX_CHARS} characters` }, { issues: [{ path: 'text', message: 'too long' }] }) });
74
+ }
75
+ return ok({ value: { runId: parsedRunId.value, text } });
76
+ }
60
77
  function parseRunList(input) {
61
78
  const value = input.query.contextRef;
62
79
  if (value === undefined)
@@ -121,6 +138,23 @@ export const runCancelRoute = defineJsonRoute({
121
138
  return ok({ value: { run: await deps.lifecycle.cancel(input) } });
122
139
  }
123
140
  }, { requireSameOrigin: true });
141
+ /**
142
+ * `POST /api/runs/:runId/messages` — a message the human sent while the run is going. `202` when it
143
+ * reached the live agent; `409 CONFLICT` with `details.delivery` (`'unsupported'` or
144
+ * `'not-running'`) when it did not, which tells the client whether to interrupt the run and send the
145
+ * text as the next turn, or simply send it next because the run is already ending.
146
+ */
147
+ export const runMessageRoute = defineJsonRoute({
148
+ method: 'post', path: '/api/runs/:runId/messages', parse: parseRunMessage, handle: async ({ input, deps }) => {
149
+ const run = await deps.lifecycle.get({ runId: input.runId });
150
+ if (run === undefined)
151
+ return err({ error: createApiError({ code: 'NOT_FOUND', message: `run "${input.runId}" was not found` }) });
152
+ const delivery = deps.deliverUserMessage ? await deps.deliverUserMessage({ runId: input.runId, text: input.text }) : 'unsupported';
153
+ if (delivery === 'delivered')
154
+ return ok({ value: { delivery } });
155
+ return err({ error: createApiError({ code: 'CONFLICT', message: `run "${input.runId}" did not take the message` }, { details: { delivery } }) });
156
+ }
157
+ }, { requireSameOrigin: true, successStatus: 202 });
124
158
  function sendStreamFailure(res, kind) {
125
159
  if (kind.kind === 'unknown-run') {
126
160
  sendRawApiError({ res, status: 404, error: createApiError({ code: 'NOT_FOUND', message: 'run was not found' }) });
@@ -210,12 +244,13 @@ export function registerRunEventStream({ app, deps }, _optional = {}) {
210
244
  await handleRunEventStreamRequest({ res, runId, afterCursor: requestedAfterCursor(req), deps });
211
245
  });
212
246
  }
213
- /** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
247
+ /** Mounts create/status/cancel/message JSON endpoints and the SSE event stream as one run transport. */
214
248
  export function registerRunRoutes({ app, deps, adapter }, _optional = {}) {
215
249
  mountJsonRoute({ app, spec: runStartRoute, deps, adapter });
216
250
  mountJsonRoute({ app, spec: runListRoute, deps, adapter });
217
251
  mountJsonRoute({ app, spec: runStatusRoute, deps, adapter });
218
252
  mountJsonRoute({ app, spec: runCancelRoute, deps, adapter });
253
+ mountJsonRoute({ app, spec: runMessageRoute, deps, adapter });
219
254
  registerRunEventStream({ app, deps });
220
255
  }
221
256
  //# sourceMappingURL=runs.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/daemon",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "The stateful agent daemon runtime: run lifecycle, durable event-log port, the tool-execution boundary, agent executor, terminal sessions, and routines.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -139,17 +139,17 @@
139
139
  "@types/pg": "8.23.1",
140
140
  "express": "^4.21.0",
141
141
  "@types/express": "^4.17.21",
142
- "@jini-ai/chat": "0.5.5",
143
- "@jini-ai/http-kit": "0.4.1",
144
142
  "@jini-ai/db": "0.3.1",
145
- "@jini-ai/agent-runtime": "^0.4.2",
143
+ "@jini-ai/http-kit": "0.4.1",
144
+ "@jini-ai/agent-runtime": "^0.4.4",
145
+ "@jini-ai/chat": "0.5.7",
146
146
  "@jini-ai/core": "^0.4.2",
147
- "@jini-ai/protocol": "^0.4.2",
148
- "@jini-ai/platform": "^0.5.2"
147
+ "@jini-ai/platform": "^0.5.2",
148
+ "@jini-ai/protocol": "^0.4.3"
149
149
  },
150
150
  "dependencies": {
151
151
  "@jini-ai/core": "^0.4.2",
152
- "@jini-ai/protocol": "^0.4.2"
152
+ "@jini-ai/protocol": "^0.4.3"
153
153
  },
154
154
  "peerDependencies": {
155
155
  "node-pty": "^1.1.0",
@@ -160,7 +160,7 @@
160
160
  "pg": "^8.23.0",
161
161
  "@jini-ai/http-kit": "^0.4.0",
162
162
  "express": "^4.21.0",
163
- "@jini-ai/agent-runtime": "^0.4.1",
163
+ "@jini-ai/agent-runtime": "^0.4.4",
164
164
  "@jini-ai/platform": "^0.5.1"
165
165
  },
166
166
  "peerDependenciesMeta": {