@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,10 +1,12 @@
1
1
  import { buildAgentEnv } from './agent-env.js';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { supportsMessageRelay } from './message-relay.js';
2
4
  import { probeMcpBridge } from './mcp-bridge-probe.js';
3
5
  import { spawn as nodeSpawn, } from 'node:child_process';
4
6
  import { promises as fsPromises, } from 'node:fs';
5
7
  import { tmpdir, } from 'node:os';
6
8
  import { join, } from 'node:path';
7
- import { agentCapabilities, bindRuntimeAgentDef, defaultRuntimeState, getRuntimeModelDiscoveryDeps, userToolchainBinDirs, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
9
+ import { agentCapabilities, assertClaudeFilesystemSupport, claudeFilesystemRuntimePaths, claudeFilesystemSettings, validateFilesystemLaunch, bindRuntimeAgentDef, defaultRuntimeState, getRuntimeModelDiscoveryDeps, userToolchainBinDirs, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, prepareAgentLogFile, preparePromptFileForAgent, } from '@jini-ai/agent-runtime';
8
10
  import { createCommandInvocation } from '@jini-ai/platform/command';
9
11
  import { collectProcessTreePids, listProcessSnapshots, stopProcesses, } from '@jini-ai/platform/process';
10
12
  import { applyImagePromptDelivery, } from '../image-prompt-delivery.js';
@@ -337,6 +339,10 @@ function nativeBuildOptions({ input, permissionMode }, _options = {}) {
337
339
  /** Preserve explicit empty restrictions and settings: they have meaning to the native CLI. */
338
340
  function settingsBuildOptions({ input }, _options = {}) {
339
341
  return {
342
+ ...(input.filesystemRoot !== undefined ? { filesystemRoot: input.filesystemRoot } : {}),
343
+ ...(input.filesystemSharedTempRoots ? { filesystemSharedTempRoots: input.filesystemSharedTempRoots } : {}),
344
+ ...(input.filesystemTempDir ? { filesystemTempDir: input.filesystemTempDir } : {}),
345
+ ...(input.filesystemRuntimeReadPaths ? { filesystemRuntimeReadPaths: input.filesystemRuntimeReadPaths } : {}),
340
346
  ...(input.disallowedTools !== undefined ? { disallowedTools: input.disallowedTools } : {}),
341
347
  ...(input.allowedTools !== undefined ? { allowedTools: input.allowedTools } : {}),
342
348
  ...(input.settingSources !== undefined ? { settingSources: input.settingSources } : {}),
@@ -616,6 +622,12 @@ export async function confirmChildSpawned({ input, deps }) {
616
622
  function nativeCapabilityInput({ input }, _options = {}) {
617
623
  return { ...(input.nativeCapabilities !== undefined ? { nativeCapabilities: input.nativeCapabilities } : {}) };
618
624
  }
625
+ /** Correlation projects retained stdout, so exact-byte journals need a separate blob writer. */
626
+ function validateStdoutJournalOptions(options, _options = {}) {
627
+ if (options.delegatedResultCorrelation && options.journal && !options.stdoutJournalBlobs) {
628
+ throw new Error('delegated result correlation with a byte journal requires stdoutJournalBlobs');
629
+ }
630
+ }
619
631
  /** Model discovery consumes the exact spawn environment and caller-supplied settings. */
620
632
  function launchModelContext({ input, launch, spawnEnv }, _options = {}) {
621
633
  return {
@@ -664,6 +676,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
664
676
  const { getAgentDef: getAgentDefFn, resolveAgentLaunch: resolveAgentLaunchFn, ensureAgentCapabilities: ensureAgentCapabilitiesFn, applyAgentLaunchEnv: applyAgentLaunchEnvFn, attachAcpSession: attachAcpSessionFn, attachPiRpcSession: attachPiRpcSessionFn, preparePromptFileForAgent: preparePromptFileForAgentFn, prepareAgentLogFile: prepareAgentLogFileFn, } = resolveAgentRuntimeDeps({ ...options, hostEnv });
665
677
  const { createCommandInvocation: createCommandInvocationFn, spawn: spawnFn, listProcessSnapshots: listProcessSnapshotsFn, collectProcessTreePids: collectProcessTreePidsFn, stopProcesses: stopProcessesFn, } = resolveProcessDeps(options);
666
678
  const { onCleanupFailure: onCleanupFailureFn, bufferedStdoutMaxBytes } = resolveMiscExecutorDeps(options);
679
+ validateStdoutJournalOptions(options);
667
680
  const journal = options.journal;
668
681
  const continuation = options.continuation;
669
682
  const classifyFailure = options.classifyFailure;
@@ -671,6 +684,15 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
671
684
  const claudeConfigDirIsolation = options.claudeConfigDirIsolation;
672
685
  const claudeConfigDirIsolationEnabled = options.claudeConfigDirIsolationEnabled ?? false;
673
686
  const promptAugmenter = options.promptAugmenter;
687
+ const filesystemTemps = new Map();
688
+ /** Remove only this run's private temp directory, on startup failure or child cleanup. */
689
+ async function cleanupFilesystemTemp({ runId }, _optional = {}) {
690
+ const path = filesystemTemps.get(runId);
691
+ if (!path)
692
+ return;
693
+ await fsPromises.rm(path, { recursive: true, force: true }).catch(() => console.warn('[agent-executor] private temp cleanup failed'));
694
+ filesystemTemps.delete(runId);
695
+ }
674
696
  /**
675
697
  * Transitions `runId` to `'failed'` (idempotent, never resumable — no
676
698
  * classifier exists, see index.ts module doc) then rejects with a typed
@@ -691,6 +713,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
691
713
  await lifecycle
692
714
  .emit({ runId: runId, input: { event: 'error', data: { message: `The assistant could not start: ${message.replace(/^AgentExecutor:\s*/, '')}` } } })
693
715
  .catch(() => undefined);
716
+ await cleanupFilesystemTemp({ runId }, {});
694
717
  await lifecycle.finish({ runId, status: 'failed', code: null, signal: null, resumable: false });
695
718
  throw new AgentExecutorError({ code: code, message: message });
696
719
  }
@@ -701,10 +724,12 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
701
724
  * finished or unknown run reads as `'not-running'`. Bounded by the number of live children.
702
725
  */
703
726
  const liveRunInputs = new Map();
727
+ const liveRunRelays = new Map();
728
+ const claimUserMessages = ({ runId, via }) => liveRunRelays.get(runId)?.claim({ via }) ?? [];
704
729
  /** `AgentExecutor.sendUserMessage()` — see that interface method's own doc. @complexity O(1). */
705
- async function sendUserMessage({ runId, text }, { imageContents } = {}) {
730
+ async function sendUserMessage({ runId, text, id = randomUUID(), attachmentIds }, { imageContents } = {}) {
706
731
  const deliver = liveRunInputs.get(runId);
707
- return deliver ? deliver(text, imageContents) : 'not-running';
732
+ return deliver ? deliver({ text, id, ...(attachmentIds ? { attachmentIds } : {}) }, imageContents ? { imageContents } : {}) : 'not-running';
708
733
  }
709
734
  /** Validates launch input and selects the definition synchronously before yielding. */
710
735
  async function prepareRunContext({ requiredArgs, optionalArgs }, _options = {}) {
@@ -724,6 +749,66 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
724
749
  return failBeforeSpawn({ runId: input.runId, code: 'NATIVE_TOOL_POLICY_UNSUPPORTED', message: 'This CLI has no native-tool policy and audit adapter. Choose a CLI with native-tool policy support.' });
725
750
  }
726
751
  }
752
+ /** Validate every directory after image augmentation, before model probes or resource staging.
753
+ * Unknown containment fails this run; O(n) filesystem checks for n attachment directories.
754
+ */
755
+ async function enforceFilesystemPaths({ input, imageDelivery }, _options = {}) {
756
+ if (input.filesystemRoot === undefined)
757
+ return;
758
+ try {
759
+ input.filesystemRoot = validateFilesystemLaunch({ filesystemRoot: input.filesystemRoot, cwd: input.cwd,
760
+ ...(imageDelivery.extraAllowedDirs ? { extraAllowedDirs: imageDelivery.extraAllowedDirs } : {}),
761
+ ...(input.imagePaths ? { imagePaths: input.imagePaths } : {}),
762
+ }, {});
763
+ input.cwd = input.filesystemRoot;
764
+ }
765
+ catch {
766
+ return failBeforeSpawn({ runId: input.runId, code: 'FILESYSTEM_POLICY_INVALID', message: 'Folder-limited launch refused: invalid filesystem root, working directory or attachment path.' });
767
+ }
768
+ }
769
+ /** Apply supported confinement without removing providers; unknown versions keep the settings.
770
+ * Runtime and git config reads are explicit infrastructure exceptions; network is unchanged.
771
+ * O(1) metadata/staging I/O plus the cached, bounded version probe.
772
+ */
773
+ async function enforceFilesystemRuntime({ input, def, launch, spawnEnv }, _options = {}) {
774
+ if (input.filesystemRoot === undefined)
775
+ return;
776
+ try {
777
+ const verify = options.assertClaudeFilesystemSupport ?? assertClaudeFilesystemSupport;
778
+ const supported = await verify({ agentId: def.id, launchPath: launch.launchPath, env: spawnEnv }, {});
779
+ const runtimeDir = join(input.filesystemRoot, '.agent-runtime');
780
+ await fsPromises.mkdir(runtimeDir, { recursive: true, mode: 0o700 });
781
+ // Refuse a runtime-folder symlink escaping the site before creating anything beneath it.
782
+ validateFilesystemLaunch({ filesystemRoot: input.filesystemRoot, cwd: input.cwd, extraAllowedDirs: [runtimeDir] }, {});
783
+ input.filesystemTempDir = await fsPromises.realpath(await fsPromises.mkdtemp(join(runtimeDir, 'tmp-')));
784
+ filesystemTemps.set(input.runId, input.filesystemTempDir);
785
+ // Recheck after creation too: swapping the runtime symlink must never grant outside temp access.
786
+ validateFilesystemLaunch({ filesystemRoot: input.filesystemRoot, cwd: input.cwd, extraAllowedDirs: [input.filesystemTempDir] }, {});
787
+ input.filesystemSharedTempRoots = [spawnEnv.TMPDIR, spawnEnv.TMP, spawnEnv.TEMP].filter((path) => Boolean(path));
788
+ spawnEnv.TMPDIR = input.filesystemTempDir;
789
+ spawnEnv.TMP = input.filesystemTempDir;
790
+ spawnEnv.TEMP = input.filesystemTempDir;
791
+ if (supported === false) {
792
+ input.filesystemNotice = 'files and shell are not folder-limited for this agent';
793
+ console.warn('[agent-executor] files and shell are not folder-limited for this agent');
794
+ // Personal Claude hooks/permissions stay excluded even on an unconfined CLI fallback.
795
+ if (def.id === 'claude')
796
+ input.settingSources = [];
797
+ delete input.filesystemRoot;
798
+ return;
799
+ }
800
+ if (supported === undefined)
801
+ console.warn('[agent-executor] filesystem capability unknown; applying read block and sandbox settings');
802
+ input.filesystemRuntimeReadPaths = claudeFilesystemRuntimePaths({ launchPath: launch.launchPath }, {});
803
+ // Validates host settings here so a malformed value fails cleanly; buildArgs merges again (idempotent).
804
+ input.settings = claudeFilesystemSettings({ options: input }, {});
805
+ // A login environment can otherwise force auto-memory back on despite the host setting.
806
+ spawnEnv.CLAUDE_CODE_DISABLE_AUTO_MEMORY = '1';
807
+ }
808
+ catch {
809
+ return failBeforeSpawn({ runId: input.runId, code: 'FILESYSTEM_POLICY_UNSUPPORTED', message: 'Folder-limited launch refused: invalid host settings or private runtime directory.' });
810
+ }
811
+ }
727
812
  function resolveRunOverlay({ input, def, imageDelivery }, _options = {}) {
728
813
  // Phase 8, hoisted deliberately above EVERY step that writes or sends the prompt. Four
729
814
  // different channels carry a prompt in this driver, and they do not all run at the same point:
@@ -744,7 +829,8 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
744
829
  // same overlay on every turn, not a first-turn-only one; a caller that needs finer-grained turn
745
830
  // numbering can track it itself and ignore this arg.
746
831
  const preStagingRuntimeContext = computeRuntimeContext({ preparedPromptFile: null, preparedLogFile: null, mcpBridge: null }, sessionRuntimeContext(input, {}));
747
- const systemPromptOverlay = computeSystemPromptOverlay(promptAugmenter, def.id);
832
+ const hostOverlay = computeSystemPromptOverlay(promptAugmenter, def.id);
833
+ const systemPromptOverlay = input.filesystemNotice ? `${hostOverlay ?? ''}\n${input.filesystemNotice}\n` : hostOverlay;
748
834
  const overlayDelivery = resolveSystemPromptOverlayDelivery({
749
835
  defId: def.id,
750
836
  systemPromptDelivery: def.systemPromptDelivery,
@@ -798,6 +884,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
798
884
  const formatForStdin = { streamFormat };
799
885
  const stdinHandle = isStdinDrivenFormat(formatForStdin)
800
886
  ? wireChildLifecycle({
887
+ ...(mcpBridge && supportsMessageRelay({ def }) ? { registerMessageRelay: (relay) => liveRunRelays.set(input.runId, relay) } : {}),
801
888
  ...(input.model ? { startingModel: input.model } : {}),
802
889
  ...(modelCatalog ? { modelCatalog } : {}),
803
890
  runId: input.runId,
@@ -811,6 +898,8 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
811
898
  onCleanupFailure: onCleanupFailureFn,
812
899
  cleanupStagedFiles,
813
900
  journal,
901
+ ...(options.delegatedResultCorrelation ? { delegatedResultCorrelation: options.delegatedResultCorrelation } : {}),
902
+ ...(options.stdoutJournalBlobs ? { stdoutJournalBlobs: options.stdoutJournalBlobs } : {}),
814
903
  continuation,
815
904
  classifyFailure,
816
905
  bufferedStdoutMaxBytes,
@@ -822,8 +911,10 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
822
911
  // Registered with the lifecycle wiring above, before the spawn-confirmation await, so a child
823
912
  // that closes immediately never leaves a stale entry behind.
824
913
  const runId = input.runId;
825
- liveRunInputs.set(runId, stdinHandle ? stdinHandle.sendUserMessage : async () => 'unsupported');
826
- child.once('close', () => liveRunInputs.delete(runId));
914
+ liveRunInputs.set(runId, stdinHandle ? stdinHandle.sendUserMessage : async ({ text, id = randomUUID(), attachmentIds }, { imageContents } = {}) => {
915
+ return liveRunRelays.get(runId)?.enqueue({ id, text, hasAttachments: (attachmentIds?.length ?? 0) > 0 || (imageContents?.length ?? 0) > 0 }) ?? 'unsupported';
916
+ });
917
+ child.once('close', () => { liveRunInputs.delete(runId); liveRunRelays.delete(runId); });
827
918
  return stdinHandle;
828
919
  }
829
920
  async function dispatchAcpPrompt({ input, def, child, modelCatalog, cleanupStagedFiles, releaseStagedResources, mcpBridge, overlayDelivery }, _options = {}) {
@@ -850,6 +941,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
850
941
  }, deps: {
851
942
  lifecycle,
852
943
  attachAcpSession: attachAcpSessionFn,
944
+ ...(mcpBridge && supportsMessageRelay({ def }) ? { registerMessageRelay: (relay) => liveRunRelays.set(input.runId, relay) } : {}),
853
945
  onPermissionRequest: options.acpPermissionHandler,
854
946
  listProcessSnapshots: listProcessSnapshotsFn,
855
947
  collectProcessTreePids: collectProcessTreePidsFn,
@@ -930,8 +1022,10 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
930
1022
  // treatment (the double-delivery hazard this mechanism exists to avoid).
931
1023
  await enforceNativeToolPolicy({ input, def }, {});
932
1024
  const imageDelivery = await resolveImageDeliveryAndArgvBudget({ input: { runId: input.runId, def, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs }, deps: { failBeforeSpawn } });
933
- const { systemPromptOverlay, overlayDelivery } = resolveRunOverlay({ input, def, imageDelivery }, {});
1025
+ await enforceFilesystemPaths({ input, imageDelivery }, {});
934
1026
  const { launch, spawnEnv } = await resolveRunLaunch({ input, def }, {});
1027
+ await enforceFilesystemRuntime({ input, def, launch, spawnEnv }, {});
1028
+ const { systemPromptOverlay, overlayDelivery } = resolveRunOverlay({ input, def, imageDelivery }, {});
935
1029
  const modelCatalog = await selectRunModel({ input, def, launch, spawnEnv }, {});
936
1030
  // Fill the def's `--help` capability gate before `buildArgs` reads it. Without this, only a
937
1031
  // host that happened to call `detectAgents` in THIS process ever had the gate filled, so e.g.
@@ -989,6 +1083,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
989
1083
  */
990
1084
  let preparedSystemPromptOverlayFile = null;
991
1085
  const cleanupStagedFiles = async () => {
1086
+ await cleanupFilesystemTemp({ runId: input.runId }, {});
992
1087
  if (preparedPromptFile)
993
1088
  await preparedPromptFile.cleanup();
994
1089
  if (preparedLogFile)
@@ -1121,6 +1216,7 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1121
1216
  }
1122
1217
  catch (error) {
1123
1218
  liveRunInputs.delete(runId);
1219
+ liveRunRelays.delete(runId);
1124
1220
  throw error;
1125
1221
  }
1126
1222
  // Now — and only now — is there a live process that could consume the locked side effect, so
@@ -1137,6 +1233,6 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
1137
1233
  }
1138
1234
  dispatchStdinPrompt({ input, def, child, overlayDelivery, imageDelivery, stdinHandle }, {});
1139
1235
  }
1140
- return { run, sendUserMessage };
1236
+ return { run, sendUserMessage, claimUserMessages };
1141
1237
  }
1142
1238
  //# sourceMappingURL=launch.js.map
@@ -0,0 +1,23 @@
1
+ import type { RunAgentPayload } from '@jini-ai/protocol';
2
+ /** Per-process receipts and task projection for the existing stdin lifecycle. O(p + t) retained
3
+ * state for pending inputs and observed tasks; each frame is O(frame size + affected tasks).
4
+ * The injected emitter uses the lifecycle's existing FIFO, preserving ordering with tool output.
5
+ */
6
+ export declare function createLiveSessionState({ emit }: {
7
+ emit: (payload: RunAgentPayload) => void;
8
+ }, _options?: {}): {
9
+ queue({ id, text }: {
10
+ id: string;
11
+ text: string;
12
+ }): void;
13
+ observe: ({ line }: {
14
+ line: string;
15
+ }) => {
16
+ result: boolean;
17
+ startedInput: boolean;
18
+ };
19
+ hasPendingInput: () => boolean;
20
+ backgroundTaskCount: () => number;
21
+ endSession(): void;
22
+ };
23
+ //# sourceMappingURL=live-session-state.d.ts.map
@@ -0,0 +1,155 @@
1
+ import { isRecord } from './values.js';
2
+ /** Parse only records; malformed stdout remains the stream parser's responsibility. */
3
+ function frameRecord(line) {
4
+ try {
5
+ const value = JSON.parse(line);
6
+ return isRecord(value) ? value : undefined;
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ }
12
+ /** UUID attribution is a consumption receipt; an echoed user line or HTTP 202 is not. */
13
+ function consumedIds(frame) {
14
+ if (!['assistant', 'stream_event', 'result'].includes(String(frame.type)))
15
+ return [];
16
+ const ids = Array.isArray(frame.user_message_uuids) ? frame.user_message_uuids : [frame.user_message_uuid];
17
+ return ids.filter((id) => typeof id === 'string');
18
+ }
19
+ /** Preserve unknown task outcomes rather than guessing success or who stopped them. */
20
+ function taskStatus(value) {
21
+ if (value === 'killed')
22
+ return 'stopped';
23
+ if (value === 'running' || value === 'completed' || value === 'failed' || value === 'stopped')
24
+ return value;
25
+ return 'unknown';
26
+ }
27
+ /** Per-process receipts and task projection for the existing stdin lifecycle. O(p + t) retained
28
+ * state for pending inputs and observed tasks; each frame is O(frame size + affected tasks).
29
+ * The injected emitter uses the lifecycle's existing FIFO, preserving ordering with tool output.
30
+ */
31
+ export function createLiveSessionState({ emit }, _options = {}) {
32
+ const pending = new Map();
33
+ // Ids written since the last result. Only these and the CLI's own queued_turn_count hold stdin:
34
+ // a CLI that never attributes a receipt must not keep the run open forever. Closing after a
35
+ // result is safe for a line still in the pipe: the CLI answers everything written before EOF.
36
+ const unanswered = new Set();
37
+ const tasks = new Map();
38
+ let queuedTurns = 0;
39
+ let backgroundTasks = 0;
40
+ function publishTask(task) {
41
+ tasks.set(task.id, task);
42
+ const { local: _local, ...payload } = task;
43
+ emit(payload);
44
+ }
45
+ function taskDetails(frame, previous) {
46
+ const patch = isRecord(frame.patch) ? frame.patch : frame;
47
+ const description = typeof patch.description === 'string' ? patch.description : previous?.description;
48
+ const detail = typeof frame.summary === 'string' ? frame.summary : previous?.detail;
49
+ const local = frame.task_type === 'local_agent' || typeof frame.subagent_type === 'string' || previous?.local === true;
50
+ return { local, ...(description === undefined ? {} : { description }), ...(detail === undefined ? {} : { detail }) };
51
+ }
52
+ function nextTaskStatus(frame, previous) {
53
+ if (frame.subtype === 'task_started')
54
+ return 'running';
55
+ const patch = isRecord(frame.patch) ? frame.patch : frame;
56
+ if (patch.status === undefined)
57
+ return previous?.status ?? 'unknown';
58
+ return taskStatus(patch.status);
59
+ }
60
+ /** Older task runs and duplicate starts cannot overwrite a confirmed terminal receipt. */
61
+ function staleTaskUpdate(previous, taskRunId, status) {
62
+ if (!previous)
63
+ return false;
64
+ // The CLI mints run ids as zero-padded base36 time plus random hex, so they sort by start.
65
+ if (previous.taskRunId && taskRunId && taskRunId < previous.taskRunId)
66
+ return true;
67
+ const terminal = ['completed', 'failed', 'stopped'].includes(previous.status);
68
+ return terminal && previous.taskRunId === taskRunId && status === 'running';
69
+ }
70
+ function updateTask(frame) {
71
+ if (typeof frame.task_id !== 'string')
72
+ return;
73
+ const previous = tasks.get(frame.task_id);
74
+ const taskRunId = typeof frame.run_id === 'string' ? frame.run_id : previous?.taskRunId;
75
+ const status = nextTaskStatus(frame, previous);
76
+ // Resumed tasks reuse task_id. A late frame from an older run cannot stop its replacement.
77
+ if (staleTaskUpdate(previous, taskRunId, status))
78
+ return;
79
+ publishTask({ type: 'background_task', id: frame.task_id, status, ...taskDetails(frame, previous),
80
+ ...(taskRunId === undefined ? {} : { taskRunId }) });
81
+ }
82
+ function updateMembership(frame) {
83
+ if (!Array.isArray(frame.tasks))
84
+ return;
85
+ backgroundTasks = frame.tasks.length;
86
+ const listed = new Set();
87
+ for (const task of frame.tasks) {
88
+ if (!isRecord(task))
89
+ continue;
90
+ if (typeof task.task_id === 'string')
91
+ listed.add(task.task_id);
92
+ updateTask({ ...task, subtype: 'task_started' });
93
+ }
94
+ // Level and edge notifications have unspecified ordering. Disappearance proves neither
95
+ // completion nor cancellation; a later task_notification supplies the actual outcome.
96
+ for (const task of tasks.values()) {
97
+ if (task.status === 'running' && !listed.has(task.id))
98
+ publishTask({ ...task, status: 'unknown', detail: 'status unknown' });
99
+ }
100
+ }
101
+ function consume(frame) {
102
+ let consumed = false;
103
+ for (const id of consumedIds(frame)) {
104
+ const text = pending.get(id);
105
+ if (text === undefined)
106
+ continue;
107
+ pending.delete(id);
108
+ unanswered.delete(id);
109
+ emit({ type: 'user_message', id, text, deliveryState: 'consumed' });
110
+ consumed = true;
111
+ }
112
+ return consumed;
113
+ }
114
+ function observe({ line }) {
115
+ const frame = frameRecord(line);
116
+ if (!frame)
117
+ return { result: false, startedInput: false };
118
+ const consumed = consume(frame);
119
+ if (frame.type === 'result') {
120
+ queuedTurns = typeof frame.queued_turn_count === 'number' ? Math.max(0, frame.queued_turn_count) : 0;
121
+ unanswered.clear();
122
+ }
123
+ if (frame.type === 'system')
124
+ observeSystem(frame);
125
+ return { result: frame.type === 'result', startedInput: consumed && frame.type !== 'result' };
126
+ }
127
+ function observeSystem(frame) {
128
+ if (frame.subtype === 'background_tasks_changed')
129
+ updateMembership(frame);
130
+ if (['task_started', 'task_notification', 'task_updated'].includes(String(frame.subtype)))
131
+ updateTask(frame);
132
+ }
133
+ return {
134
+ queue({ id, text }) {
135
+ pending.set(id, text);
136
+ unanswered.add(id);
137
+ emit({ type: 'user_message', id, text, deliveryState: 'queued' });
138
+ },
139
+ observe,
140
+ hasPendingInput: () => unanswered.size > 0 || queuedTurns > 0,
141
+ backgroundTaskCount: () => backgroundTasks,
142
+ endSession() {
143
+ for (const task of tasks.values()) {
144
+ if (task.status !== 'running' && task.status !== 'unknown')
145
+ continue;
146
+ // A task already missing from membership may have completed without a receipt. Only
147
+ // a still-running, process-local subagent is known to stop with this session.
148
+ const stopped = task.local && task.status === 'running';
149
+ publishTask({ ...task, status: stopped ? 'stopped' : 'unknown',
150
+ detail: stopped ? 'Stopped because the agent session ended' : 'status unknown' });
151
+ }
152
+ },
153
+ };
154
+ }
155
+ //# sourceMappingURL=live-session-state.js.map
@@ -34,8 +34,9 @@ export declare function bridgeUnavailableMessage(status: string, probe?: McpBrid
34
34
  export declare function buildMcpJsonServerEntry({ runId, options }: {
35
35
  readonly runId: string;
36
36
  readonly options: Pick<McpJsonInjectionOptions, 'command' | 'args' | 'daemonUrl' | 'env' | 'diagnosticToolIds' | 'startupToolIds'>;
37
- }, { credential }?: {
37
+ }, { credential, messageRelayNonce }?: {
38
38
  readonly credential?: string;
39
+ readonly messageRelayNonce?: string;
39
40
  }): McpJsonServerEntry;
40
41
  /**
41
42
  * Merges {@link JINI_MCP_SERVER_KEY} into an existing `.mcp.json`'s `mcpServers` map, preserving
@@ -235,6 +236,7 @@ export declare function buildMcpBridgeDelivery(input: {
235
236
  readonly strategy: RuntimeAgentDef['externalMcpInjection'];
236
237
  readonly options: McpJsonInjectionOptions | undefined;
237
238
  readonly credential: string | undefined;
239
+ readonly messageRelayNonce?: string;
238
240
  }): McpBridgeDelivery | null;
239
241
  export declare function defaultRemoveMcpJsonFile({ path }: {
240
242
  readonly path: string;
@@ -1,4 +1,6 @@
1
1
  import { promises as fsPromises, } from 'node:fs';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { supportsMessageRelay } from './message-relay.js';
2
4
  import { ensureAntigravityMcpPermission } from './antigravity-permissions.js';
3
5
  import { homedir, tmpdir, } from 'node:os';
4
6
  import { join, resolve, } from 'node:path';
@@ -46,7 +48,7 @@ export function bridgeUnavailableMessage(status, probe) {
46
48
  * entirely. Omitting produces byte-identical output to before this parameter existed.
47
49
  * @complexity O(t) output space/time for t launch environment and registered ID bytes.
48
50
  */
49
- export function buildMcpJsonServerEntry({ runId, options }, { credential } = {}) {
51
+ export function buildMcpJsonServerEntry({ runId, options }, { credential, messageRelayNonce } = {}) {
50
52
  const hostEnv = Object.entries(options.env ?? {}).filter(([key]) => !key.startsWith('JINI_'));
51
53
  return {
52
54
  command: options.command,
@@ -58,6 +60,7 @@ export function buildMcpJsonServerEntry({ runId, options }, { credential } = {})
58
60
  ...(options.diagnosticToolIds ? { JINI_DIAGNOSTIC_TOOL_IDS: JSON.stringify(options.diagnosticToolIds()) } : {}),
59
61
  ...(options.startupToolIds ? { JINI_STARTUP_TOOL_IDS: JSON.stringify(options.startupToolIds({ runId })) } : {}),
60
62
  ...(credential !== undefined ? { JINI_DAEMON_TOKEN: credential } : {}),
63
+ ...(messageRelayNonce ? { JINI_MESSAGE_RELAY: '1', JINI_MESSAGE_RELAY_NONCE: messageRelayNonce } : {}),
61
64
  },
62
65
  };
63
66
  }
@@ -479,7 +482,10 @@ export function buildMcpBridgeDelivery(input) {
479
482
  const { cwd, runId, strategy, options, credential } = input;
480
483
  if (options === undefined || strategy === undefined)
481
484
  return null;
482
- const serverEntry = buildMcpJsonServerEntry({ runId: runId, options: options }, credential === undefined ? {} : { credential });
485
+ const serverEntry = buildMcpJsonServerEntry({ runId: runId, options: options }, {
486
+ ...(credential === undefined ? {} : { credential }),
487
+ ...(input.messageRelayNonce ? { messageRelayNonce: input.messageRelayNonce } : {}),
488
+ });
483
489
  switch (strategy) {
484
490
  case 'claude-mcp-json':
485
491
  return { kind: 'claude-mcp-json', mcpJsonPath: mcpJsonPathForRun(cwd, runId), serverEntry };
@@ -789,6 +795,7 @@ export async function resolveMcpBridgeForRun({ input, deps }) {
789
795
  strategy: input.def.externalMcpInjection,
790
796
  options: deps.mcpJsonInjection,
791
797
  credential,
798
+ ...(supportsMessageRelay({ def: input.def }) ? { messageRelayNonce: randomBytes(24).toString('hex') } : {}),
792
799
  });
793
800
  if (input.def.id === 'antigravity' && delivery?.kind === 'env-passthrough') {
794
801
  await (deps.ensureAntigravityMcpPermission ?? ensureAntigravityMcpPermission)({
@@ -0,0 +1,39 @@
1
+ import type { RuntimeAgentDef } from '@jini-ai/agent-runtime';
2
+ import type { RunAgentPayload, RelayedUserMessage, MessageRelayVia } from '@jini-ai/protocol';
3
+ export interface RunMessageRelay {
4
+ enqueue(input: {
5
+ id: string;
6
+ text: string;
7
+ hasAttachments: boolean;
8
+ }): 'delivered' | 'not-running';
9
+ claim(input: {
10
+ via: MessageRelayVia;
11
+ }): readonly RelayedUserMessage[];
12
+ close(): void;
13
+ }
14
+ /**
15
+ * Whether a def's live process can take another user message mid-run: only a stream-json stdin
16
+ * transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
17
+ * Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
18
+ * reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
19
+ * Native stream-json input wins even when the engine also has MCP.
20
+ */
21
+ export declare function acceptsMidRunUserMessages({ def }: {
22
+ def: RuntimeAgentDef;
23
+ }, _options?: {}): boolean;
24
+ /** A definition without an injected MCP bridge must retain next-turn fallback. */
25
+ export declare function supportsMessageRelay({ def }: {
26
+ def: RuntimeAgentDef;
27
+ }, _options?: {}): boolean;
28
+ /** One synchronous FIFO claim per process: parallel tool results get disjoint prefixes.
29
+ * The browser retains the fallback copy until the emitted consumption receipt arrives.
30
+ * @param emit - Enqueues receipts on the driver's existing lifecycle FIFO; never waits on I/O.
31
+ * @returns A text-only inbox, closed synchronously before the driver's terminal event.
32
+ * @complexity O(n) retained messages; each claim scans its prefix and removes it in one splice.
33
+ */
34
+ export declare function createMessageRelay({ emit }: {
35
+ emit: (payload: RunAgentPayload) => void;
36
+ }, { maxChars }?: {
37
+ maxChars?: number;
38
+ }): RunMessageRelay;
39
+ //# sourceMappingURL=message-relay.d.ts.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Whether a def's live process can take another user message mid-run: only a stream-json stdin
3
+ * transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
4
+ * Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
5
+ * reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
6
+ * Native stream-json input wins even when the engine also has MCP.
7
+ */
8
+ export function acceptsMidRunUserMessages({ def }, _options = {}) {
9
+ return def.promptViaStdin === true && def.promptInputFormat === 'stream-json';
10
+ }
11
+ /** A definition without an injected MCP bridge must retain next-turn fallback. */
12
+ export function supportsMessageRelay({ def }, _options = {}) {
13
+ return def.externalMcpInjection !== undefined && !acceptsMidRunUserMessages({ def });
14
+ }
15
+ /** One synchronous FIFO claim per process: parallel tool results get disjoint prefixes.
16
+ * The browser retains the fallback copy until the emitted consumption receipt arrives.
17
+ * @param emit - Enqueues receipts on the driver's existing lifecycle FIFO; never waits on I/O.
18
+ * @returns A text-only inbox, closed synchronously before the driver's terminal event.
19
+ * @complexity O(n) retained messages; each claim scans its prefix and removes it in one splice.
20
+ */
21
+ export function createMessageRelay({ emit }, { maxChars = 32_000 } = {}) {
22
+ const pending = [];
23
+ let seq = 0;
24
+ let closed = false;
25
+ return {
26
+ enqueue({ id, text, hasAttachments }) {
27
+ if (closed)
28
+ return 'not-running';
29
+ pending.push({ id, text, hasAttachments, seq: ++seq });
30
+ emit({ type: 'user_message', id, text, deliveryState: 'queued' });
31
+ return 'delivered';
32
+ },
33
+ claim({ via }) {
34
+ if (closed)
35
+ return [];
36
+ let count = 0;
37
+ let chars = 0;
38
+ for (const message of pending) {
39
+ if (message.hasAttachments)
40
+ break;
41
+ const size = JSON.stringify({ id: message.id, seq: message.seq, text: message.text }).length;
42
+ // Whole messages only; a single oversized message is delivered alone.
43
+ if (count > 0 && chars + size > maxChars)
44
+ break;
45
+ chars += size;
46
+ count++;
47
+ }
48
+ return pending.splice(0, count).map(({ id, seq, text }) => {
49
+ emit({ type: 'user_message', id, text, deliveryState: 'consumed', via });
50
+ return { id, seq, text };
51
+ });
52
+ },
53
+ close() { closed = true; pending.length = 0; },
54
+ };
55
+ }
56
+ //# sourceMappingURL=message-relay.js.map
@@ -55,6 +55,8 @@ export interface ReadOnlyToolMessages {
55
55
  }
56
56
  export interface ReadOnlyToolCheckRequired {
57
57
  readonly toolId: string;
58
+ /** Diagnostic selector only; never changes which registration is checked or executed. */
59
+ readonly input?: unknown;
58
60
  readonly registry: Pick<ToolRegistry, 'list'> | undefined;
59
61
  readonly messages: ReadOnlyToolMessages;
60
62
  }
@@ -94,8 +96,9 @@ export declare function readOnlyToolRefusalMessage({ toolId, messages }: {
94
96
  /**
95
97
  * Single read-only registration decision, shared by routes and inner dispatch.
96
98
  * @returns Null for an explicitly read-only registration, otherwise host refusal text.
99
+ * @complexity O(n) for known tools; unknown-tool diagnostics add bounded name scoring and O(n log n) sorting.
97
100
  */
98
- export declare function checkReadOnlyTool({ toolId, registry, messages }: ReadOnlyToolCheckRequired, _optional?: Record<string, never>): string | null;
101
+ export declare function checkReadOnlyTool({ toolId, input, registry, messages }: ReadOnlyToolCheckRequired, _optional?: Record<string, never>): string | null;
99
102
  /**
100
103
  * THE read-only dispatch decision for any tool id a consumer's own `ToolExecutor` composition is
101
104
  * about to invoke — the outer call the route already checked, or a nested one a decorator issues
@@ -110,8 +113,9 @@ export declare function checkReadOnlyTool({ toolId, registry, messages }: ReadOn
110
113
  * @param required.registry - Descriptors to resolve `toolId` against; `undefined` ⇒ refuse (an
111
114
  * unverifiable constrained dispatch is refused, never waived, matching `checkReadOnlyTool`).
112
115
  * @returns `null` when the dispatch may proceed, otherwise the refusal text to report.
113
- * @complexity O(1) for an unconstrained principal; O(n) in registered tool count for a constrained
114
- * one, since `ToolRegistry` exposes enumeration rather than lookup by id.
116
+ * @complexity O(1) for an unconstrained principal; O(n) in registered tool count for a known tool,
117
+ * since `ToolRegistry` exposes enumeration rather than lookup by id. Unknown ids also incur
118
+ * the bounded similarity scoring and sorting cost documented by `toolCandidates`.
115
119
  */
116
120
  export declare function refuseNonReadOnlyDispatch(required: ReadOnlyToolCheckRequired & {
117
121
  readonly principal: Principal;
@@ -147,7 +151,8 @@ export declare function readOnlyRemedyRefusalMessage({ refusal, formatMessage }:
147
151
  *
148
152
  * @returns A drop-in `ToolExecutor`; `resumeConfirmation`/`cancel`/`getAuditRecord` delegate
149
153
  * straight through.
150
- * @complexity One extra property read per unconstrained call; one registry scan per constrained one.
154
+ * @complexity One extra property read per unconstrained call; one registry scan per constrained one,
155
+ * plus bounded similarity scoring and sorting for an unknown id.
151
156
  */
152
157
  export declare function withReadOnlyToolConstraint({ inner, registry, idGenerator, messages }: ReadOnlyToolConstraintRequired, _optional?: Record<string, never>): ToolExecutor;
153
158
  //# sourceMappingURL=read-only-tools.d.ts.map