@jini-ai/daemon 0.6.8 → 0.6.9

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 (71) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
  3. package/dist/agent-executor/antigravity-permissions.js +64 -0
  4. package/dist/agent-executor/contracts.d.ts +31 -6
  5. package/dist/agent-executor/event-translation.d.ts +6 -1
  6. package/dist/agent-executor/event-translation.js +1 -1
  7. package/dist/agent-executor/events.d.ts +1 -0
  8. package/dist/agent-executor/events.js +85 -74
  9. package/dist/agent-executor/launch.d.ts +4 -2
  10. package/dist/agent-executor/launch.js +306 -217
  11. package/dist/agent-executor/mcp-bridge-probe.js +51 -35
  12. package/dist/agent-executor/mcp-bridge.d.ts +18 -8
  13. package/dist/agent-executor/mcp-bridge.js +105 -65
  14. package/dist/agent-executor/run-output.d.ts +20 -0
  15. package/dist/agent-executor/run-output.js +80 -0
  16. package/dist/attachment-content.js +18 -15
  17. package/dist/core/read-only-tools.d.ts +15 -0
  18. package/dist/core/read-only-tools.js +29 -1
  19. package/dist/core/tool-audit.d.ts +3 -1
  20. package/dist/core/tool-audit.js +35 -22
  21. package/dist/core/tool-gaps.d.ts +60 -0
  22. package/dist/core/tool-gaps.js +199 -0
  23. package/dist/delegated-tool-bridge.d.ts +1 -1
  24. package/dist/delegated-tool-bridge.js +161 -122
  25. package/dist/event-log.d.ts +45 -2
  26. package/dist/event-log.js +83 -4
  27. package/dist/http/attachments.d.ts +26 -1
  28. package/dist/http/attachments.js +129 -65
  29. package/dist/http/connectors.d.ts +1 -32
  30. package/dist/http/connectors.js +13 -68
  31. package/dist/http/delegated-tools.d.ts +5 -1
  32. package/dist/http/delegated-tools.js +29 -29
  33. package/dist/http/host-tools.d.ts +1 -0
  34. package/dist/http/host-tools.js +64 -59
  35. package/dist/http/index.d.ts +2 -2
  36. package/dist/http/index.js +1 -1
  37. package/dist/http/media.js +48 -69
  38. package/dist/http/memory.js +45 -36
  39. package/dist/http/model-proxy-fields.d.ts +23 -0
  40. package/dist/http/model-proxy-fields.js +29 -0
  41. package/dist/http/model-proxy.js +193 -395
  42. package/dist/http/research-tavily.adapter.js +72 -49
  43. package/dist/http/research.js +33 -23
  44. package/dist/http/resource-request.d.ts +37 -0
  45. package/dist/http/resource-request.js +59 -0
  46. package/dist/http/routines.js +49 -90
  47. package/dist/http/runs.d.ts +7 -5
  48. package/dist/http/runs.js +21 -7
  49. package/dist/http/terminals.js +23 -48
  50. package/dist/http/tool-catalog.d.ts +5 -0
  51. package/dist/http/xai.d.ts +11 -1
  52. package/dist/http/xai.js +125 -124
  53. package/dist/legacy-data-migration.js +41 -24
  54. package/dist/run/core/retry.js +51 -35
  55. package/dist/run/diagnostics/diagnostics.js +5 -17
  56. package/dist/run-credentials.js +49 -27
  57. package/dist/run-lifecycle.d.ts +12 -2
  58. package/dist/run-lifecycle.js +47 -26
  59. package/dist/store/agent-sessions/sqlite.d.ts +6 -0
  60. package/dist/store/agent-sessions/sqlite.js +6 -4
  61. package/dist/surface-exchanges.js +2 -1
  62. package/dist/terminal-session.d.ts +1 -1
  63. package/dist/terminal-session.js +26 -33
  64. package/dist/tool-audit.d.ts +1 -0
  65. package/dist/tool-audit.js +1 -0
  66. package/dist/tool-executor.d.ts +24 -3
  67. package/dist/tool-executor.js +276 -139
  68. package/dist/tool-recovery/recovery.js +64 -35
  69. package/dist/tool-recovery/stack.d.ts +2 -1
  70. package/dist/tool-recovery/stack.js +1 -1
  71. package/package.json +10 -10
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.6.9 — 2026-10-22
2
+
3
+ - 2026-10-22: Memory fixes (bounded event log and tool-executor audit), chromeIntegration option, provider-plugin registration, coverage.
4
+
1
5
  ## 0.6.8 — 2026-10-16
2
6
 
3
7
  - An unknown tool on the read-only gateway points to discovery, never a mutation 409 (75d37c32).
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Persist one server's MCP grant for agy, which has no run-scoped settings override.
3
+ * Reuses the platform lock and atomic writer; keeps the first existing file as a backup.
4
+ * Invalid settings and I/O failures reject before spawn rather than widening permissions.
5
+ * @complexity O(settings bytes), including the allow-list scan; bounded by the settings file.
6
+ */
7
+ export declare function ensureAntigravityMcpPermission({ settingsPath, serverName }: {
8
+ settingsPath: string;
9
+ serverName: string;
10
+ }, _options?: Record<string, never>): Promise<void>;
11
+ //# sourceMappingURL=antigravity-permissions.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import { withFileLock, writeFileAtomicAsync } from '@jini-ai/platform/fs';
3
+ import { isRecord } from './values.js';
4
+ /** Read the current settings text; a missing file is undefined, every other failure rejects. */
5
+ async function readSettingsText(settingsPath) {
6
+ try {
7
+ return await fs.readFile(settingsPath, 'utf8');
8
+ }
9
+ catch (error) {
10
+ if (error.code !== 'ENOENT')
11
+ throw error;
12
+ return undefined;
13
+ }
14
+ }
15
+ /** Parse and validate the settings shape; anything unexpected rejects rather than widening permissions. */
16
+ function parseSettings(raw) {
17
+ const settings = raw === undefined ? {} : JSON.parse(raw);
18
+ if (!isRecord(settings))
19
+ throw new Error('Antigravity settings must be an object');
20
+ const permissions = settings.permissions === undefined ? {} : settings.permissions;
21
+ if (!isRecord(permissions) || (permissions.allow !== undefined && !Array.isArray(permissions.allow))) {
22
+ throw new Error('Antigravity permissions.allow must be an array');
23
+ }
24
+ return { settings, permissions, allow: permissions.allow ?? [] };
25
+ }
26
+ /** The allow list holding exactly one copy of rule (first occurrence kept, order otherwise
27
+ * unchanged), or null when it already holds exactly one and nothing needs writing. */
28
+ function grantedAllowList(allow, rule) {
29
+ const first = allow.indexOf(rule);
30
+ if (first < 0)
31
+ return [...allow, rule];
32
+ const deduped = allow.filter((entry, index) => entry !== rule || index === first);
33
+ return deduped.length === allow.length ? null : deduped;
34
+ }
35
+ async function backupOriginalOnce(settingsPath) {
36
+ // Never replace the first backup, including on subsequent launches after operator edits.
37
+ await fs.copyFile(settingsPath, `${settingsPath}.jini-backup`, constants.COPYFILE_EXCL)
38
+ .catch((error) => { if (error.code !== 'EEXIST')
39
+ throw error; });
40
+ }
41
+ /**
42
+ * Persist one server's MCP grant for agy, which has no run-scoped settings override.
43
+ * Reuses the platform lock and atomic writer; keeps the first existing file as a backup.
44
+ * Invalid settings and I/O failures reject before spawn rather than widening permissions.
45
+ * @complexity O(settings bytes), including the allow-list scan; bounded by the settings file.
46
+ */
47
+ export async function ensureAntigravityMcpPermission({ settingsPath, serverName }, _options = {}) {
48
+ if (!/^[\w.-]+$/.test(serverName))
49
+ throw new Error('Invalid MCP server name');
50
+ const rule = `mcp(${serverName}/*)`;
51
+ await withFileLock({ lockPath: `${settingsPath}.jini-lock`, run: async () => {
52
+ const raw = await readSettingsText(settingsPath);
53
+ const { settings, permissions, allow } = parseSettings(raw);
54
+ const nextAllow = grantedAllowList(allow, rule);
55
+ if (nextAllow === null)
56
+ return;
57
+ if (raw !== undefined)
58
+ await backupOriginalOnce(settingsPath);
59
+ await writeFileAtomicAsync({ filePath: settingsPath, content: `${JSON.stringify({
60
+ ...settings, permissions: { ...permissions, allow: nextAllow },
61
+ }, null, 2)}\n` }, { refuseSymlink: true });
62
+ } }, { createParent: true, timeoutMs: 5000 });
63
+ }
64
+ //# sourceMappingURL=antigravity-permissions.js.map
@@ -92,7 +92,7 @@ export type AgentRuntimeEventTranslation = {
92
92
  readonly kind: 'ignored';
93
93
  };
94
94
  /** Machine-readable failure reasons `run()` can reject with — every one is preceded by a `lifecycle.finish({status:'failed'})` call (see index.ts module doc's Invariant section). */
95
- export type AgentExecutorErrorCode = 'AGENT_MODEL_UNRESOLVED' | 'AGENT_PERMISSION_MODE_INVALID' | 'AGENT_NOT_FOUND' | 'AGENT_RUNTIME_UNSUPPORTED' | 'AGENT_BINARY_NOT_RESOLVED' | 'AGENT_SPAWN_FAILED' | 'AGENT_PROMPT_TOO_LARGE';
95
+ export type AgentExecutorErrorCode = 'NATIVE_TOOL_POLICY_UNSUPPORTED' | 'AGENT_MODEL_UNRESOLVED' | 'AGENT_PERMISSION_MODE_INVALID' | 'AGENT_NOT_FOUND' | 'AGENT_RUNTIME_UNSUPPORTED' | 'AGENT_BINARY_NOT_RESOLVED' | 'AGENT_SPAWN_FAILED' | 'AGENT_PROMPT_TOO_LARGE';
96
96
  /** Thrown by `AgentExecutor.run()` on every failure path — never a bare `Error`, so callers can branch on `.code` instead of parsing `.message`. */
97
97
  export declare class AgentExecutorError extends Error {
98
98
  readonly code: AgentExecutorErrorCode;
@@ -119,6 +119,12 @@ export interface AgentExecutorRunInput {
119
119
  * delegate auto-approval, and any unknown mode is rejected before launching a process.
120
120
  */
121
121
  readonly permissionMode?: 'bypass' | 'restricted';
122
+ /** Host-authorized native shell/edit grant; unsupported restricted providers fail before spawn. */
123
+ readonly nativeToolAccess?: boolean;
124
+ /** Independent host-verified permissions, snapshotted for this run. */
125
+ readonly nativeCapabilities?: Partial<import('@jini-ai/agent-runtime').NativeCapabilities>;
126
+ /** Trusted-host CLI parity; never derived from prompt/context fields. */
127
+ readonly cliParity?: boolean;
122
128
  /**
123
129
  * Host-validated image files. How each one actually reaches the model
124
130
  * depends on `def.imageDelivery` (see `@jini-ai/agent-runtime`'s `types.ts`):
@@ -184,6 +190,8 @@ export interface AgentExecutorRunInput {
184
190
  readonly settingSources?: readonly string[];
185
191
  /** Forwarded verbatim to `RuntimeBuildOptions.settings` (extra settings JSON or file path for this run). */
186
192
  readonly settings?: string;
193
+ /** Forwarded verbatim to `RuntimeBuildOptions.chromeIntegration`; `undefined` = no browser-integration flag. */
194
+ readonly chromeIntegration?: boolean;
187
195
  }
188
196
  export interface AgentExecutor {
189
197
  /**
@@ -195,16 +203,19 @@ export interface AgentExecutor {
195
203
  * underlying run is always already transitioned to `'failed'` via
196
204
  * `lifecycle.finish()` before this rejects (see index.ts module doc's Invariant).
197
205
  */
198
- 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>;
206
+ run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "nativeToolAccess" | "nativeCapabilities" | "cliParity" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings" | "chromeIntegration">): Promise<void>;
199
207
  /**
200
208
  * Delivers a message the human sent while `runId` is going straight into its live agent process
201
209
  * (see {@link UserMessageDelivery} for each outcome). A stream-json agent is interrupted first and
202
210
  * gets the message once the CLI acknowledges the interrupt, so this settles in milliseconds (at
203
211
  * most `INTERRUPT_ACK_TIMEOUT_MS`); it never waits on the agent's reply.
212
+ * Optional `imageContents` contains host-authorized pixels, encoded exactly as in an idle send.
204
213
  */
205
214
  sendUserMessage(requiredArgs: {
206
215
  readonly runId: string;
207
216
  readonly text: string;
217
+ }, optionalArgs?: {
218
+ readonly imageContents?: readonly MessageAttachmentImage[];
208
219
  }): Promise<UserMessageDelivery>;
209
220
  }
210
221
  /**
@@ -212,7 +223,8 @@ export interface AgentExecutor {
212
223
  * - `'delivered'` — the running turn was interrupted and the message written into the live
213
224
  * session; the agent answers it next, in the same process and session.
214
225
  * - `'unsupported'` — the run is live, but its agent has no way to take input mid-run (its prompt
215
- * is plain text or an RPC request). The host should interrupt and send it as the next turn.
226
+ * is plain text or an RPC request). Queue attachment turns intact after the reply; text-only
227
+ * turns may retain the interrupt-and-resume fallback.
216
228
  * - `'not-running'` — no live process will accept it: an unknown run, a finished one, or one whose
217
229
  * last turn already ended and closed stdin. The run is ending; the host should send it next.
218
230
  */
@@ -260,7 +272,7 @@ export interface StdinCloseHandle {
260
272
  /** 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`). */
261
273
  recordSentBytes(content: string): void;
262
274
  /** Interrupts this child's turn, then writes a mid-run user message into its open stdin — see {@link AgentExecutor.sendUserMessage}. */
263
- sendUserMessage(text: string): Promise<UserMessageDelivery>;
275
+ sendUserMessage(text: string, imageContents?: readonly MessageAttachmentImage[]): Promise<UserMessageDelivery>;
264
276
  }
265
277
  /**
266
278
  * Gap 3 (capability-routed continuation transport) — host-owned config for the
@@ -313,6 +325,17 @@ export interface ContinuationOptions {
313
325
  * interface that defaults to *nothing* rather than a real implementation).
314
326
  */
315
327
  export interface McpJsonInjectionOptions {
328
+ /** Read current host-registered tool IDs at launch, outside model-controlled input. */
329
+ readonly diagnosticToolIds?: () => readonly string[];
330
+ /**
331
+ * Visible registry IDs to resolve before the bridge's first tools/list, including for clients
332
+ * whose mid-run refresh is unverified. Independent of diagnostic/redaction IDs; this grants
333
+ * no execution permission. The bridge fetches authoritative descriptors using the run credential.
334
+ * @example startupToolIds: ({ runId }) => visibleToolsForRun(runId).map(tool => tool.id)
335
+ */
336
+ readonly startupToolIds?: (requiredArgs: {
337
+ readonly runId: string;
338
+ }, optionalArgs?: Record<string, never>) => readonly string[];
316
339
  /** Probe a reported bridge failure before emitting its error. @default true when injection is configured. */
317
340
  readonly probeOnFailure?: boolean;
318
341
  /** Absolute path (or PATH-resolvable name) to the `jini-mcp` bin entry (`packages/mcp/src/bin/serve.ts`) this driver tells the spawned CLI to launch as its own MCP server subprocess. */
@@ -401,12 +424,14 @@ export interface McpJsonInjectionOptions {
401
424
  readonly path: string;
402
425
  }) => Promise<void>;
403
426
  /** `'codex-toml'` only. Links the run home's sessions/archived_sessions to the operator's
404
- * persistent rollout store. Config and auth remain isolated and are removed after the run.
427
+ * persistent rollout store and authentication. Parity also links user home entries; config alone stays isolated.
405
428
  * @default Native mkdir (0700) and directory symlinks; errors fail staging before spawn. */
406
429
  readonly linkSessionStore?: (required: {
407
430
  runHome: string;
408
431
  sourceHome: string;
409
- }, optional?: {}) => Promise<void>;
432
+ }, optional?: {
433
+ inheritUserConfig?: boolean;
434
+ }) => Promise<void>;
410
435
  }
411
436
  /** One `.mcp.json` `mcpServers` entry — the shape Claude Code's own config schema expects. */
412
437
  export interface McpJsonServerEntry {
@@ -63,7 +63,12 @@ interface AgentTranslationSideEffectSink {
63
63
  readonly onSessionId: (args: {
64
64
  readonly sessionId: string;
65
65
  }) => void;
66
- readonly onToolCall: () => void;
66
+ /** Receives the already-narrowed `tool_use` payload, so a caller never re-checks its variant. */
67
+ readonly onToolCall: (args: {
68
+ readonly toolUse: Extract<RunAgentPayload, {
69
+ type: 'tool_use';
70
+ }>;
71
+ }) => void;
67
72
  readonly onUserVisibleOutput: () => void;
68
73
  }
69
74
  /**
@@ -224,7 +224,7 @@ export function applyAgentTranslationSideEffects({ payload, sessionId, sink }) {
224
224
  if (sessionId !== undefined)
225
225
  sink.onSessionId({ sessionId });
226
226
  if (payload.type === 'tool_use') {
227
- sink.onToolCall();
227
+ sink.onToolCall({ toolUse: payload });
228
228
  }
229
229
  else if ((payload.type === 'text_delta' || payload.type === 'thinking_delta') && payload.delta.length > 0) {
230
230
  sink.onUserVisibleOutput();
@@ -162,6 +162,7 @@ interface RunAcpDispatchInput {
162
162
  readonly prompt: string;
163
163
  readonly cwd: string;
164
164
  readonly permissionMode: 'bypass' | 'restricted' | undefined;
165
+ readonly cliParity?: boolean | undefined;
165
166
  readonly model: string | undefined;
166
167
  readonly modelCatalog?: ModelCatalogSnapshot;
167
168
  readonly imagePaths: readonly string[];
@@ -1,5 +1,7 @@
1
+ import { appendSerialTask } from "@jini-ai/core/primitives";
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { createModelReceiptTracker } from './model-receipts.js';
4
+ import { observeRunOutput } from './run-output.js';
3
5
  import { messageContentWithImages } from '../attachment-content.js';
4
6
  import { redactSecrets, } from '@jini-ai/core';
5
7
  import { createClaudeStreamHandler, createCopilotStreamHandler, createJsonEventStreamHandler, createQoderStreamHandler, attachAcpSession, attachPiRpcSession, } from '@jini-ai/agent-runtime';
@@ -61,6 +63,11 @@ async function terminateChildTree(deps, child) {
61
63
  const pids = deps.collectProcessTreePids({ processes, rootPids: [child.pid] });
62
64
  await deps.stopProcesses({ pids });
63
65
  }
66
+ // A late emit can race a terminal run. Keep later events/cleanup ordered even after failure;
67
+ // lifecycle exceptions can contain provider text, so report only this fixed diagnostic.
68
+ function reportDroppedEmit() {
69
+ console.warn('[daemon] Queued lifecycle event could not be delivered.');
70
+ }
64
71
  /** Default sink when a host does not supply `onCleanupFailure`: still observable, never silent. Redacted per SEC-007 (a spawn/permission error can embed paths/host detail). */
65
72
  export function defaultCleanupFailureSink(context) {
66
73
  // eslint-disable-next-line no-console
@@ -257,6 +264,7 @@ function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
257
264
  */
258
265
  export function wireChildLifecycle(ctx) {
259
266
  const { runId, def, streamFormat, child, lifecycle, journal, continuation, classifyFailure } = ctx;
267
+ const output = observeRunOutput({ lifecycle, runId, agentName: def.name }, { plainStdout: streamFormat === 'plain' });
260
268
  const modelReceipt = createModelReceiptTracker(ctx.startingModel, ctx.modelCatalog?.models);
261
269
  let stdinClosed = false;
262
270
  let cancelRequested = false;
@@ -324,16 +332,7 @@ export function wireChildLifecycle(ctx) {
324
332
  /** Bytes the ceiling refused, reported verbatim on flush so truncation is never silent. */
325
333
  let droppedStdoutBytes = 0;
326
334
  function enqueueEmit(task) {
327
- emitQueue = emitQueue.then(async () => {
328
- try {
329
- await task();
330
- }
331
- catch {
332
- // A single emit failing (e.g. a race against an already-terminal
333
- // run) must not block delivery of subsequently queued events —
334
- // see this function's own doc.
335
- }
336
- });
335
+ emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
337
336
  }
338
337
  if (ctx.startingModel)
339
338
  enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.startingModel } } }));
@@ -398,8 +397,10 @@ export function wireChildLifecycle(ctx) {
398
397
  controlScanBuffer += text;
399
398
  if (!text.includes('\n'))
400
399
  return;
401
- const lines = controlScanBuffer.split('\n');
402
- controlScanBuffer = lines.pop() ?? '';
400
+ // `text` holds a newline, so the buffer does: split off the complete lines, keep the partial tail.
401
+ const lastNewline = controlScanBuffer.lastIndexOf('\n');
402
+ const lines = controlScanBuffer.slice(0, lastNewline).split('\n');
403
+ controlScanBuffer = controlScanBuffer.slice(lastNewline + 1);
403
404
  for (const line of lines) {
404
405
  // Both checked: either word can also appear inside the other kind of line's text.
405
406
  if (line.includes('"control_response"'))
@@ -526,24 +527,18 @@ export function wireChildLifecycle(ctx) {
526
527
  const receipt = modelReceipt(translation.payload);
527
528
  if (receipt)
528
529
  enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: receipt } }));
529
- if (translation.sessionId !== undefined)
530
- capturedSessionId = translation.sessionId;
531
- if (!bridgeChecked && translation.payload.type === 'status' && translation.payload.label === 'initializing') {
532
- bridgeChecked = true;
533
- const bridgeStatus = unavailableJiniBridgeStatus({ rawEvent: rawEvent });
534
- if (bridgeStatus !== undefined) {
535
- stopForUnavailableBridge(translation.payload, bridgeStatus);
536
- return;
537
- }
538
- }
539
- if (translation.payload.type === 'tool_use') {
540
- pendingToolUse = { id: translation.payload.id, name: translation.payload.name, input: translation.payload.input };
541
- toolCallSeen = true;
542
- }
543
- else if ((translation.payload.type === 'text_delta' || translation.payload.type === 'thinking_delta') &&
544
- translation.payload.delta.length > 0) {
545
- userVisibleOutputSeen = true;
546
- }
530
+ applyAgentTranslationSideEffects({ payload: translation.payload, sessionId: translation.sessionId, sink: {
531
+ onSessionId: ({ sessionId }) => { capturedSessionId = sessionId; },
532
+ onToolCall: ({ toolUse }) => {
533
+ // A tool signal always carries the tool_use variant (the sink hands it over narrowed);
534
+ // retain its full input for continuation.
535
+ pendingToolUse = { id: toolUse.id, name: toolUse.name, input: toolUse.input };
536
+ toolCallSeen = true;
537
+ },
538
+ onUserVisibleOutput: () => { userVisibleOutputSeen = true; },
539
+ } });
540
+ if (rejectUnavailableBridge({ payload: translation.payload, rawEvent }, {}))
541
+ return;
547
542
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: statusWithChildIdentity({ payload: translation.payload, childPid: ctx.child.pid }, {}) } }));
548
543
  }
549
544
  else if (translation.kind === 'error') {
@@ -553,6 +548,17 @@ export function wireChildLifecycle(ctx) {
553
548
  handleTurnEnd(translation.stopReason);
554
549
  }
555
550
  });
551
+ function rejectUnavailableBridge({ payload, rawEvent }, _options = {}) {
552
+ if (!bridgeChecked && payload.type === 'status' && payload.label === 'initializing') {
553
+ bridgeChecked = true;
554
+ const bridgeStatus = unavailableJiniBridgeStatus({ rawEvent });
555
+ if (bridgeStatus !== undefined) {
556
+ stopForUnavailableBridge(payload, bridgeStatus);
557
+ return true;
558
+ }
559
+ }
560
+ return false;
561
+ }
556
562
  /**
557
563
  * Ends a run whose CLI started without the `jini` bridge: emits the init status and a
558
564
  * {@link MCP_BRIDGE_UNAVAILABLE} error, then stops the child's process tree before the model can
@@ -693,20 +699,23 @@ export function wireChildLifecycle(ctx) {
693
699
  // Both of the next two steps are guarded: neither a failed cleanup nor a rejecting host
694
700
  // classifier may prevent the terminal transition below — see each helper's own doc.
695
701
  await cleanupStagedFilesSafely(ctx);
696
- const status = bridgeUnavailable ? 'failed' : classifyRunCloseStatus({ cancelRequested, code }, { signal });
697
- // A bridge failure is not resumable: resuming would start the same CLI with the same bridge.
698
- const resumable = status === 'failed' && !bridgeUnavailable && classifyFailure !== undefined
699
- ? await classifyFailureSafely(ctx, classifyFailure, {
700
- runId,
701
- agentId: def.id,
702
- code,
703
- signal: signal ?? null,
704
- sideEffects: { userVisibleOutputSeen, toolCallSeen },
705
- })
706
- : false;
702
+ const status = await output.settle({ status: bridgeUnavailable ? 'failed' : classifyRunCloseStatus({ cancelRequested, code }, { signal }) });
703
+ const resumable = await resolveChildResumable({ status, code, signal }, {});
707
704
  await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
708
705
  })();
709
706
  });
707
+ async function resolveChildResumable({ status, code, signal }, _options = {}) {
708
+ // A bridge failure is not resumable: resuming would start the same CLI with the same bridge.
709
+ return status === 'failed' && !bridgeUnavailable && classifyFailure !== undefined
710
+ ? await classifyFailureSafely(ctx, classifyFailure, {
711
+ runId,
712
+ agentId: def.id,
713
+ code,
714
+ signal: signal ?? null,
715
+ sideEffects: { userVisibleOutputSeen, toolCallSeen },
716
+ })
717
+ : false;
718
+ }
710
719
  /**
711
720
  * A mid-run message: first an `interrupt` control request on the still-open stdin, so the agent
712
721
  * stops generating (or abandons the tool it is running) at once instead of finishing its whole
@@ -718,8 +727,10 @@ export function wireChildLifecycle(ctx) {
718
727
  * `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
719
728
  * reach — `'not-running'`. Between turns, while stdin is held only for background tasks, there is
720
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.
721
732
  */
722
- async function sendUserMessage(text) {
733
+ async function sendUserMessage(text, images = []) {
723
734
  if (!acceptsMidRunUserMessages({ def: def }))
724
735
  return 'unsupported';
725
736
  const stdin = child.stdin;
@@ -727,7 +738,7 @@ export function wireChildLifecycle(ctx) {
727
738
  return 'not-running';
728
739
  if (heldForBackgroundTasks) {
729
740
  heldForBackgroundTasks = false;
730
- writeUserMessage(stdin, text);
741
+ writeUserMessage(stdin, text, images);
731
742
  return 'delivered';
732
743
  }
733
744
  const requestId = `jini-interrupt-${randomUUID()}`;
@@ -753,12 +764,12 @@ export function wireChildLifecycle(ctx) {
753
764
  pendingInterruptAcks.delete(requestId);
754
765
  if (!processAlive || stdinClosed)
755
766
  return 'not-running';
756
- writeUserMessage(stdin, text);
767
+ writeUserMessage(stdin, text, images);
757
768
  return 'delivered';
758
769
  }
759
- /** Writes a mid-run message as the next user line, journals it, and emits its `user_message` run event. */
760
- function writeUserMessage(stdin, text) {
761
- stdin.write(`${JSON.stringify(streamJsonUserMessage({ content: [{ type: 'text', text: text }] }))}\n`, 'utf8');
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');
762
773
  if (journal)
763
774
  enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(text) }));
764
775
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
@@ -815,6 +826,22 @@ function backgroundTaskListLength({ line }) {
815
826
  function streamJsonUserMessage({ content }) {
816
827
  return { type: 'user', message: { role: 'user', content: content } };
817
828
  }
829
+ /** Preserve ACP's distinction between absent fields and explicitly supplied empty values. */
830
+ function acpSessionOptions({ ctx }, _options = {}) {
831
+ return {
832
+ cwd: ctx.cwd,
833
+ ...(ctx.cliParity ? { cliParity: true, stageTimeoutMs: 0 } : {}),
834
+ ...(ctx.permissionMode !== undefined ? { permissionMode: ctx.permissionMode } : {}),
835
+ ...(ctx.model !== undefined ? { model: ctx.model } : {}),
836
+ ...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}),
837
+ ...(ctx.envFormat !== undefined ? { envFormat: ctx.envFormat } : {}),
838
+ // Spread-when-present rather than always: passing `mcpServers: []` is not the same as passing
839
+ // nothing for every downstream ACP agent, and "no bridge configured" must stay byte-identical
840
+ // to before this field existed.
841
+ ...(ctx.mcpServers !== undefined && ctx.mcpServers.length > 0 ? { mcpServers: [...ctx.mcpServers] } : {}),
842
+ ...(ctx.onPermissionRequest !== undefined ? { onPermissionRequest: ctx.onPermissionRequest } : {}),
843
+ };
844
+ }
818
845
  /**
819
846
  * Wires an ACP child to a run. Unlike the JSON-stream path, ACP owns the
820
847
  * prompt protocol and reports its parsed events through `attachAcpSession`'s
@@ -827,6 +854,7 @@ function streamJsonUserMessage({ content }) {
827
854
  function wireAcpLifecycle(ctx) {
828
855
  const modelReceipt = createModelReceiptTracker(ctx.model, ctx.modelCatalog?.models);
829
856
  const { runId, agentId, child, lifecycle, journal, classifyFailure } = ctx;
857
+ const output = observeRunOutput({ lifecycle, runId, agentName: agentId });
830
858
  let cancelRequested = false;
831
859
  let emitQueue = Promise.resolve();
832
860
  // Gap 5 (session resume) — see wireChildLifecycle's identical local for the full rationale.
@@ -835,15 +863,7 @@ function wireAcpLifecycle(ctx) {
835
863
  let userVisibleOutputSeen = false;
836
864
  let toolCallSeen = false;
837
865
  function enqueueEmit(task) {
838
- emitQueue = emitQueue.then(async () => {
839
- try {
840
- await task();
841
- }
842
- catch {
843
- // A late event racing a terminal lifecycle is intentionally dropped;
844
- // it must not prevent subsequent queued cleanup from running.
845
- }
846
- });
866
+ emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
847
867
  }
848
868
  if (ctx.model)
849
869
  enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.model } } }));
@@ -873,7 +893,7 @@ function wireAcpLifecycle(ctx) {
873
893
  unsubscribeCancel();
874
894
  // Guarded for the same reasons as the child-driven handler above.
875
895
  await cleanupStagedFilesSafely(ctx);
876
- const status = cancelRequested ? 'cancelled' : controller?.completedSuccessfully() ? 'succeeded' : 'failed';
896
+ const status = await output.settle({ status: cancelRequested ? 'cancelled' : controller?.completedSuccessfully() ? 'succeeded' : 'failed' });
877
897
  const resumable = status === 'failed' && classifyFailure !== undefined
878
898
  ? await classifyFailureSafely(ctx, classifyFailure, {
879
899
  runId,
@@ -908,11 +928,7 @@ function wireAcpLifecycle(ctx) {
908
928
  if (event === 'error') {
909
929
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translateAcpError(payload) } }));
910
930
  }
911
- } }, { cwd: ctx.cwd, ...(ctx.permissionMode !== undefined ? { permissionMode: ctx.permissionMode } : {}), ...(ctx.model !== undefined ? { model: ctx.model } : {}), ...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}), ...(ctx.envFormat !== undefined ? { envFormat: ctx.envFormat } : {}),
912
- // Spread-when-present rather than always: passing `mcpServers: []` is not the same as passing
913
- // nothing for every downstream ACP agent, and "no bridge configured" must stay byte-identical
914
- // to before this field existed.
915
- ...(ctx.mcpServers !== undefined && ctx.mcpServers.length > 0 ? { mcpServers: [...ctx.mcpServers] } : {}), ...(ctx.onPermissionRequest !== undefined ? { onPermissionRequest: ctx.onPermissionRequest } : {}) });
931
+ } }, acpSessionOptions({ ctx }, {}));
916
932
  return controller;
917
933
  }
918
934
  /**
@@ -937,6 +953,7 @@ function wireAcpLifecycle(ctx) {
937
953
  function wirePiRpcLifecycle(ctx) {
938
954
  const modelReceipt = createModelReceiptTracker(ctx.model, ctx.modelCatalog?.models);
939
955
  const { runId, agentId, child, lifecycle, journal, classifyFailure } = ctx;
956
+ const output = observeRunOutput({ lifecycle, runId, agentName: agentId });
940
957
  let cancelRequested = false;
941
958
  let emitQueue = Promise.resolve();
942
959
  // Gap 5 (session resume) — see wireChildLifecycle's identical local for the full rationale.
@@ -945,15 +962,7 @@ function wirePiRpcLifecycle(ctx) {
945
962
  let userVisibleOutputSeen = false;
946
963
  let toolCallSeen = false;
947
964
  function enqueueEmit(task) {
948
- emitQueue = emitQueue.then(async () => {
949
- try {
950
- await task();
951
- }
952
- catch {
953
- // A late event racing a terminal lifecycle is intentionally dropped;
954
- // it must not prevent subsequent queued cleanup from running.
955
- }
956
- });
965
+ emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
957
966
  }
958
967
  if (ctx.model)
959
968
  enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.model } } }));
@@ -983,7 +992,7 @@ function wirePiRpcLifecycle(ctx) {
983
992
  unsubscribeCancel();
984
993
  // Guarded for the same reasons as the child-driven handler above.
985
994
  await cleanupStagedFilesSafely(ctx);
986
- const status = cancelRequested ? 'cancelled' : session?.hasFatalError() ? 'failed' : 'succeeded';
995
+ const status = await output.settle({ status: cancelRequested ? 'cancelled' : session?.hasFatalError() ? 'failed' : 'succeeded' });
987
996
  const resumable = status === 'failed' && classifyFailure !== undefined
988
997
  ? await classifyFailureSafely(ctx, classifyFailure, {
989
998
  runId,
@@ -1062,6 +1071,7 @@ export async function runAcpDispatch({ input, deps }) {
1062
1071
  prompt: input.prompt,
1063
1072
  cwd: input.cwd,
1064
1073
  permissionMode: input.permissionMode,
1074
+ cliParity: input.cliParity,
1065
1075
  model: input.model,
1066
1076
  ...(input.modelCatalog ? { modelCatalog: input.modelCatalog } : {}),
1067
1077
  imagePaths: input.imagePaths,
@@ -1086,7 +1096,8 @@ export async function runAcpDispatch({ input, deps }) {
1086
1096
  // fired-and-forgotten (SEC-007: "await where lifecycle ordering allows it").
1087
1097
  await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'acp-attach-failure', deps.onCleanupFailure);
1088
1098
  await deps.releaseStagedResources();
1089
- await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach ACP session for agent "${input.agentId}": ${errorMessage(err)}` });
1099
+ // `failBeforeSpawn` always rejects (`Promise<never>`); returning it propagates that rejection.
1100
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach ACP session for agent "${input.agentId}": ${errorMessage(err)}` });
1090
1101
  }
1091
1102
  }
1092
1103
  /** Phase 15 (pi-rpc branch): same discipline as {@link runAcpDispatch}, for the one `'pi-rpc'` def. */
@@ -1118,7 +1129,7 @@ export async function runPiRpcDispatch({ input, deps }) {
1118
1129
  // fire-and-forget (SEC-007).
1119
1130
  await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'pi-rpc-attach-failure', deps.onCleanupFailure);
1120
1131
  await deps.releaseStagedResources();
1121
- await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach pi-rpc session for agent "${input.agentId}": ${errorMessage(err)}` });
1132
+ return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach pi-rpc session for agent "${input.agentId}": ${errorMessage(err)}` });
1122
1133
  }
1123
1134
  }
1124
1135
  //# sourceMappingURL=events.js.map
@@ -189,9 +189,9 @@ export declare function computeRuntimeContext({ preparedPromptFile, preparedLogF
189
189
  /** Phase 9a: the def's `buildArgs` 4th argument. Historically omitted when every override was absent;
190
190
  * that let defs silently enable auto-approval. Always supply the validated restricted default now. Pure. */
191
191
  export declare function buildAgentBuildArgsOptions({ input, systemPromptOverlay }: {
192
- readonly input: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode' | 'disallowedTools' | 'allowedTools' | 'settingSources' | 'settings'>;
192
+ readonly input: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode' | 'nativeToolAccess' | 'nativeCapabilities' | 'cliParity' | 'disallowedTools' | 'allowedTools' | 'settingSources' | 'settings' | 'chromeIntegration'>;
193
193
  readonly systemPromptOverlay: string | null | undefined;
194
- }): RuntimeBuildOptions | undefined;
194
+ }): RuntimeBuildOptions;
195
195
  /**
196
196
  * **The single dispatch point from a computed system-prompt overlay to its delivery mechanism** —
197
197
  * see `RuntimeAgentDef.systemPromptDelivery`'s own doc for the declared shape. Pure and
@@ -290,6 +290,7 @@ export declare function buildRunArgs({ input, deps }: {
290
290
  * a defensive extra.
291
291
  * @param input.def - Only used for its `id`, in the failure message, and its `systemPromptDelivery` declaration.
292
292
  * @param input.overlay - The computed `PromptAugmenter.systemOverlay()` result for this run.
293
+ * @param deps.mkdtemp - Creates the run-scoped directory from a prefix; default a fresh `mkdtemp` under `os.tmpdir()`.
293
294
  * @complexity O(1) plus one directory creation and one file write.
294
295
  * @overallScore 100/100
295
296
  */
@@ -302,6 +303,7 @@ export declare function prepareSystemPromptOverlayFileIfNeeded({ input, deps }:
302
303
  readonly deps: {
303
304
  readonly releaseStagedResources: () => Promise<void>;
304
305
  readonly failBeforeSpawn: FailBeforeSpawn;
306
+ readonly mkdtemp?: (prefix: string) => Promise<string>;
305
307
  };
306
308
  }): Promise<PreparedSystemPromptOverlayFile | null>;
307
309
  /** Phase 11: post-`buildArgs` guard for argv-bound defs whose resolved binary is a Windows shim/.exe — a no-op off-Windows and for non-argv-bound defs. */