@mono-agent/agent-runtime 0.20.4 → 0.20.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -343,6 +343,8 @@ grepToolImpl
343
343
  inspectCodexSubscriptionSearch
344
344
  isPathAllowed
345
345
  isWorkdirAllowed
346
+ normalizeBackgroundBashTimeoutMs
347
+ normalizeBackgroundTimeoutMs
346
348
  normalizeBashTimeoutMs
347
349
  normalizeProcessTimeoutMs
348
350
  performWebFetch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.20.4",
3
+ "version": "0.20.5",
4
4
  "description": "Agent runtime supporting Claude SDK/CLI, Codex, OpenCode, Pi SDK, and ACP v1 bridges out of the box",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-only",
@@ -54,6 +54,31 @@ export function normalizeProcessTimeoutMs(value, fallback = DEFAULT_BASH_TIMEOUT
54
54
  return Math.max(1, Math.min(Math.floor(n), cap));
55
55
  }
56
56
 
57
+ /**
58
+ * Background process-job timeout: positive-integer milliseconds with no
59
+ * foreground ceiling. A background budget belongs to the host's `processJobs`
60
+ * settings (`maxRuntimeMs`), which clamp it on their own side; reusing the
61
+ * foreground default as a cap here silently discarded the long runtime a caller
62
+ * deliberately asked for. `undefined` means "no explicit request", leaving the
63
+ * host default in force.
64
+ */
65
+ export function normalizeBackgroundTimeoutMs(value) {
66
+ const n = Number(value);
67
+ if (!Number.isFinite(n) || n <= 0) return undefined;
68
+ return Math.max(1, Math.floor(n));
69
+ }
70
+
71
+ /**
72
+ * Legacy Bash `timeout` for a background job: the same seconds-vs-milliseconds
73
+ * heuristic as {@link normalizeBashTimeoutMs}, minus the foreground ceiling.
74
+ */
75
+ export function normalizeBackgroundBashTimeoutMs(value) {
76
+ const n = Number(value);
77
+ if (!Number.isFinite(n) || n <= 0) return undefined;
78
+ const floored = Math.floor(n);
79
+ return Math.max(1_000, floored <= 600 ? floored * 1_000 : floored);
80
+ }
81
+
57
82
  /**
58
83
  * Compatibility wrapper retained for direct callers and tests.
59
84
  *
@@ -131,9 +156,11 @@ export async function bashToolRun(
131
156
  }
132
157
 
133
158
  if (background === true && processJobsController) {
159
+ // Deliberately re-derived from the raw params: `timeoutMs` above carries the
160
+ // foreground ceiling, which is not this job's budget.
134
161
  const requestedTimeoutMs = timeout_ms !== undefined
135
- ? timeoutMs
136
- : (timeout === undefined ? undefined : timeoutMs);
162
+ ? normalizeBackgroundTimeoutMs(timeout_ms)
163
+ : normalizeBackgroundBashTimeoutMs(timeout);
137
164
  const handedOff = await handOffProcessJob({
138
165
  controller: processJobsController,
139
166
  tool: "Bash",
@@ -3,7 +3,7 @@
3
3
  import { existsSync } from "node:fs";
4
4
  import { passthroughSandbox } from "../sandbox-seam.js";
5
5
  import { DEFAULT_MAX_BASH_OUTPUT_CHARS } from "./shared/constants.js";
6
- import { normalizeProcessTimeoutMs } from "./bash.js";
6
+ import { normalizeBackgroundTimeoutMs, normalizeProcessTimeoutMs } from "./bash.js";
7
7
  import { capChars } from "./shared/output-truncation.js";
8
8
  import {
9
9
  isPathAllowed,
@@ -101,7 +101,9 @@ export async function execToolRun(
101
101
  tool: "Exec",
102
102
  prepared,
103
103
  summary: `Exec command (${args.length} argument${args.length === 1 ? "" : "s"}; values redacted)`,
104
- timeoutMs: timeout_ms === undefined ? undefined : timeoutMs,
104
+ // Re-derived from the raw param: `timeoutMs` carries the foreground
105
+ // ceiling, and a background job is bounded by processJobs instead.
106
+ timeoutMs: timeout_ms === undefined ? undefined : normalizeBackgroundTimeoutMs(timeout_ms),
105
107
  maxOutputChars: max_output_chars === undefined ? undefined : maxChars,
106
108
  startedAt,
107
109
  failed,
@@ -12,6 +12,8 @@ export { grepToolImpl } from "./grep.js";
12
12
  export {
13
13
  bashToolImpl,
14
14
  bashToolRun,
15
+ normalizeBackgroundBashTimeoutMs,
16
+ normalizeBackgroundTimeoutMs,
15
17
  normalizeBashTimeoutMs,
16
18
  normalizeProcessTimeoutMs,
17
19
  } from "./bash.js";
@@ -4,6 +4,7 @@ import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
4
4
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
5
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
6
6
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
7
8
  import { passthroughSandbox } from "../sandbox-seam.js";
8
9
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
9
10
  import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
@@ -185,6 +186,20 @@ function writeFileChangeDetails(path, before, after) {
185
186
  };
186
187
  }
187
188
 
189
+ /**
190
+ * Render a budget for a tool description: the exact milliseconds the host
191
+ * enforces, plus a human unit, so the model can both reason about the scale and
192
+ * pass an exact value back.
193
+ */
194
+ function formatDurationForModel(ms) {
195
+ const hours = ms / 3_600_000;
196
+ const minutes = ms / 60_000;
197
+ const human = hours >= 1
198
+ ? `${Number.isInteger(hours) ? String(hours) : hours.toFixed(1)}h`
199
+ : (minutes >= 1 ? `${Number.isInteger(minutes) ? String(minutes) : minutes.toFixed(1)}m` : `${String(Math.round(ms / 1_000))}s`);
200
+ return `${human} (${String(ms)} ms)`;
201
+ }
202
+
188
203
  function limitedNumber(value, fallback) {
189
204
  const n = Number(value);
190
205
  if (!Number.isFinite(n) || n <= 0) return fallback;
@@ -208,10 +223,19 @@ function withToolLimits(name, params, limits = {}) {
208
223
  if (name === "Bash" || name === "Exec") {
209
224
  const timeoutLimit = limits.bashTimeoutMs || DEFAULT_BASH_TIMEOUT_MS;
210
225
  next.max_output_chars = limitedNumber(next.max_output_chars, limits.bashOutputLimitChars || limits.toolTextLimitChars || 20000);
211
- if (name === "Bash" && next.timeout_ms === undefined && next.timeout !== undefined) {
212
- next.timeout = normalizeBashTimeoutMs(next.timeout, timeoutLimit);
213
- } else {
214
- next.timeout_ms = normalizeProcessTimeoutMs(next.timeout_ms, timeoutLimit);
226
+ // A background hand-off is bounded by the host's processJobs budget, so the
227
+ // FOREGROUND DEFAULT must never bound it: falling back to that default is what
228
+ // silently capped multi-hour background jobs at two minutes, and the `else`
229
+ // branch injects it even when no timeout was requested at all. An explicitly
230
+ // configured bashTimeoutMs still narrows them — a host that sets one means it
231
+ // for background work too.
232
+ const effectiveTimeoutLimit = next.background === true ? limits.bashTimeoutMs : timeoutLimit;
233
+ if (effectiveTimeoutLimit !== undefined) {
234
+ if (name === "Bash" && next.timeout_ms === undefined && next.timeout !== undefined) {
235
+ next.timeout = normalizeBashTimeoutMs(next.timeout, effectiveTimeoutLimit);
236
+ } else {
237
+ next.timeout_ms = normalizeProcessTimeoutMs(next.timeout_ms, effectiveTimeoutLimit);
238
+ }
215
239
  }
216
240
  }
217
241
  return next;
@@ -468,10 +492,16 @@ export function getPiBuiltinTools(allowedTools, {
468
492
  type: "integer",
469
493
  description: "Deprecated compatibility timeout. Values up to 600 mean seconds and larger values mean milliseconds; use timeout_ms instead.",
470
494
  };
495
+ const foregroundTimeoutLimitMs = toolLimits?.bashTimeoutMs || DEFAULT_BASH_TIMEOUT_MS;
496
+ const backgroundLimitMs = processJobsController?.limits?.maxRuntimeMs;
471
497
  const processTimeoutSchema = {
472
498
  type: "integer",
473
499
  minimum: 1,
474
- description: "Exact timeout in milliseconds.",
500
+ description: `Exact timeout in milliseconds. A foreground run is capped at ${formatDurationForModel(foregroundTimeoutLimitMs)} and is killed at that point, so anything longer belongs in the background${
501
+ backgroundLimitMs === undefined
502
+ ? ""
503
+ : `, where this host allows up to ${formatDurationForModel(backgroundLimitMs)}`
504
+ }.`,
475
505
  };
476
506
  // Shared by Exec and Bash, and injected only when the host supplies a
477
507
  // process-job controller. House style for a tool description is
@@ -479,7 +509,11 @@ export function getPiBuiltinTools(allowedTools, {
479
509
  // the model which commands belong here rather than in the foreground.
480
510
  const backgroundSchema = {
481
511
  type: "boolean",
482
- description: "Run as a durable background process job and notify this conversation when it finishes. Prefer this for work that outlives a reply — builds, full test suites, long installs, migrations, long-running watchers — and leave it off whenever you need the output to answer right now. Do not use for commands that daemonize into another POSIX process group or session.",
512
+ description: `Run as a durable background process job and notify this conversation when it finishes. Prefer this for work that outlives a reply — builds, full test suites, long installs, migrations, long-running watchers — and leave it off whenever you need the output to answer right now. Do not use for commands that daemonize into another POSIX process group or session.${
513
+ backgroundLimitMs === undefined
514
+ ? ""
515
+ : ` This host runs a background job for up to ${formatDurationForModel(backgroundLimitMs)}; \`timeout_ms\` may lower that but never raise it, and the start receipt reports \`max_runtime_ms\`, the budget actually granted — check it, because a job is killed at that limit.`
516
+ }`,
483
517
  };
484
518
  // Per-tool closure config (cwd/event sink/limits/policy) plus the per-instance
485
519
  // ToolContext `ctx` that the tool impls and shared helpers read from.
@@ -697,6 +731,9 @@ async function connectMcpClient(name, cfg, { cwd, sandboxPolicy, sandboxEngine,
697
731
  retainedByMcpApps: false,
698
732
  privateCapabilityUrl,
699
733
  closed: false,
734
+ // AbortControllers for calls currently on the wire. Teardown aborts these
735
+ // BEFORE closing so the server is told to stop — see closeConnectedMcpClient.
736
+ inFlight: new Set(),
700
737
  };
701
738
  } catch (error) {
702
739
  try { await transport?.close?.(); } catch { /* best-effort */ }
@@ -872,6 +909,25 @@ export async function initPiMcpTools(mcpConfig, reservedNames = new Set(), {
872
909
  parameters: sourceTool.inputSchema || /** @type {any} */ (sourceTool).input_schema || objectSchema({}),
873
910
  async execute(toolCallId, params, signal) {
874
911
  if (signal?.aborted) throw new Error("tool execution aborted");
912
+ // OWN this request's abort signal rather than forwarding the caller's
913
+ // directly, so run teardown can cancel the call ON THE WIRE. The SDK
914
+ // turns an abort into `notifications/cancelled`; closing the client
915
+ // only rejects the caller locally (Protocol._onclose) and then
916
+ // SIGTERM/SIGKILLs a stdio child with its request still outstanding —
917
+ // which is how a proxy's upstream work was left dangling in #664.
918
+ const callAbort = new AbortController();
919
+ const forwardAbort = () => callAbort.abort(signal?.reason);
920
+ signal?.addEventListener("abort", forwardAbort, { once: true });
921
+ const inFlight = { controller: callAbort, label: `${serverName}:${sourceTool.name}` };
922
+ connected.inFlight.add(inFlight);
923
+ try {
924
+ return await callMcpTool();
925
+ } finally {
926
+ connected.inFlight.delete(inFlight);
927
+ signal?.removeEventListener("abort", forwardAbort);
928
+ }
929
+
930
+ async function callMcpTool() {
875
931
  const textLimit = limits.mcpTextLimitChars || MCP_TEXT_RESULT_LIMIT;
876
932
  const imageInlineMaxBytes = limits.imageInlineMaxBytes ?? MCP_IMAGE_INLINE_MAX_BYTES;
877
933
  const normalizedParams = normalizeMcpToolParams(serverName, sourceTool.name, params || {}, { qaOutputDir, ctx });
@@ -917,7 +973,7 @@ export async function initPiMcpTools(mcpConfig, reservedNames = new Set(), {
917
973
  timeout: mcpCallTimeoutMs,
918
974
  resetTimeoutOnProgress: true,
919
975
  maxTotalTimeout: mcpCallMaxTotalTimeoutMs,
920
- signal,
976
+ signal: callAbort.signal,
921
977
  onprogress,
922
978
  },
923
979
  ).catch((error) => {
@@ -929,7 +985,7 @@ export async function initPiMcpTools(mcpConfig, reservedNames = new Set(), {
929
985
  const out = await withTimeout(
930
986
  request,
931
987
  mcpCallTimeoutMs,
932
- signal,
988
+ callAbort.signal,
933
989
  `${serverName}:${sourceTool.name}`,
934
990
  (reset) => {
935
991
  resetInactivityTimeout = reset;
@@ -980,6 +1036,7 @@ export async function initPiMcpTools(mcpConfig, reservedNames = new Set(), {
980
1036
  } : {}),
981
1037
  },
982
1038
  };
1039
+ }
983
1040
  },
984
1041
  });
985
1042
  }
@@ -1071,15 +1128,30 @@ async function registerMcpAppForToolResult({
1071
1128
  const connection = {
1072
1129
  connectionId: connected.connectionId,
1073
1130
  readResource: async (uri) => await connected.client.readResource({ uri }),
1074
- callTool: async (name, args, signal) => await connected.client.callTool(
1075
- { name, arguments: args && typeof args === "object" && !Array.isArray(args) ? args : {} },
1076
- undefined,
1077
- {
1078
- timeout: 120_000,
1079
- maxTotalTimeout: 120_000,
1080
- ...(signal ? { signal } : {}),
1081
- },
1082
- ),
1131
+ // Same in-flight registration as the tool path: a retained MCP App
1132
+ // connection torn down mid-call must cancel on the wire, not be closed out
1133
+ // from under the request. See closeConnectedMcpClient.
1134
+ callTool: async (name, args, signal) => {
1135
+ const callAbort = new AbortController();
1136
+ const forwardAbort = () => callAbort.abort(signal?.reason);
1137
+ signal?.addEventListener("abort", forwardAbort, { once: true });
1138
+ const inFlight = { controller: callAbort, label: `${serverName}:${name}` };
1139
+ connected.inFlight.add(inFlight);
1140
+ try {
1141
+ return await connected.client.callTool(
1142
+ { name, arguments: args && typeof args === "object" && !Array.isArray(args) ? args : {} },
1143
+ undefined,
1144
+ {
1145
+ timeout: 120_000,
1146
+ maxTotalTimeout: 120_000,
1147
+ signal: callAbort.signal,
1148
+ },
1149
+ );
1150
+ } finally {
1151
+ connected.inFlight.delete(inFlight);
1152
+ signal?.removeEventListener("abort", forwardAbort);
1153
+ }
1154
+ },
1083
1155
  close: async () => {
1084
1156
  connected.retainedByMcpApps = false;
1085
1157
  await closeConnectedMcpClient(connected, 5_000);
@@ -1143,9 +1215,36 @@ function selectMcpAppResource(response, resourceUri) {
1143
1215
  };
1144
1216
  }
1145
1217
 
1218
+ /**
1219
+ * Cancel every call still on the wire, then close.
1220
+ *
1221
+ * Order matters. `Protocol._onclose` rejects pending requests LOCALLY with
1222
+ * ConnectionClosed and never notifies the server, and StdioClientTransport then
1223
+ * escalates stdin.end -> SIGTERM -> SIGKILL — so closing first kills a stdio
1224
+ * proxy mid-request with its upstream work still outstanding, and the caller
1225
+ * gets a bare "Connection closed" that cannot be told apart from a server that
1226
+ * died on its own. Aborting first makes the SDK emit `notifications/cancelled`
1227
+ * so the server can unwind, and rejects the caller with a sentence naming the
1228
+ * server and tool. See mono-agent#664.
1229
+ */
1230
+ function cancelInFlightMcpCalls(connected) {
1231
+ const pending = connected?.inFlight;
1232
+ if (!pending || pending.size === 0) return;
1233
+ for (const entry of [...pending]) {
1234
+ try {
1235
+ entry.controller.abort(new McpError(
1236
+ ErrorCode.ConnectionClosed,
1237
+ `${entry.label} was cancelled because its MCP connection was torn down.`,
1238
+ ));
1239
+ } catch { /* best-effort */ }
1240
+ }
1241
+ pending.clear();
1242
+ }
1243
+
1146
1244
  async function closeConnectedMcpClient(connected, timeoutMs) {
1147
1245
  if (!connected || connected.closed === true) return;
1148
1246
  connected.closed = true;
1247
+ cancelInFlightMcpCalls(connected);
1149
1248
  const { client, transport } = connected;
1150
1249
  try { await closeWithTimeout(client?.close?.bind(client), timeoutMs); } catch { /* best-effort */ }
1151
1250
  try { await closeWithTimeout(transport?.close?.bind(transport), timeoutMs); } catch { /* best-effort */ }
@@ -16,7 +16,7 @@ import { startPreparedProcess } from "./process-runner.js";
16
16
  * timeoutMs?: number,
17
17
  * maxOutputChars?: number,
18
18
  * launch: (options?: {timeoutMs?: number, signal?: AbortSignal, maxBufferBytes?: number, onStdout?: (chunk: Buffer) => void, onStderr?: (chunk: Buffer) => void}) => ReturnType<typeof startPreparedProcess>,
19
- * }) => Promise<{jobId: string, state: "queued"|"starting"|"running", startedAt: string|null}>} start
19
+ * }) => Promise<{jobId: string, state: "queued"|"starting"|"running", startedAt: string|null, maxRuntimeMs?: number}>} start
20
20
  */
21
21
 
22
22
  /**
@@ -82,6 +82,7 @@ export async function handOffProcessJob({
82
82
  job_id: result.jobId,
83
83
  state: result.state,
84
84
  started_at: result.startedAt,
85
+ ...(result.maxRuntimeMs === undefined ? {} : { max_runtime_ms: result.maxRuntimeMs }),
85
86
  };
86
87
  return {
87
88
  text: `${BACKGROUND_START_GUIDANCE}\n${JSON.stringify(payload)}`,
@@ -119,7 +120,7 @@ export async function handOffProcessJob({
119
120
  * status command invites exactly the polling this forbids.
120
121
  */
121
122
  const BACKGROUND_START_GUIDANCE =
122
- "Background process job started (tool-authored guidance): this conversation is woken with a new turn when the job reaches a terminal state, and its output arrives with that turn. Do not poll, sleep, wait on it, or re-run the command to check progress, and do not report the work as finished yet.";
123
+ "Background process job started (tool-authored guidance): this conversation is woken with a new turn when the job reaches a terminal state, and its output arrives with that turn. Do not poll, sleep, wait on it, or re-run the command to check progress, and do not report the work as finished yet. `max_runtime_ms` is the budget the host granted; if it is below what you requested, the host capped it and the job will be killed at that limit, so plan the work around the granted budget rather than re-running the same command.";
123
124
 
124
125
  const PUBLIC_BACKGROUND_START_FAILURES = Object.freeze({
125
126
  background_unsupported: "Background process jobs are unsupported for this tool call.",
@@ -191,6 +192,8 @@ function validProcessJobStartResult(value) {
191
192
  if (!value || typeof value !== "object") return false;
192
193
  if (typeof value.jobId !== "string" || value.jobId.trim().length === 0 || value.jobId.length > 256) return false;
193
194
  if (value.state !== "queued" && value.state !== "starting" && value.state !== "running") return false;
195
+ if (value.maxRuntimeMs !== undefined
196
+ && (!Number.isSafeInteger(value.maxRuntimeMs) || value.maxRuntimeMs <= 0)) return false;
194
197
  if (value.startedAt === null) return true;
195
198
  if (typeof value.startedAt !== "string") return false;
196
199
  const timestamp = Date.parse(value.startedAt);
package/src/ai/types.js CHANGED
@@ -214,9 +214,10 @@
214
214
  * @property {number} [toolPayloadMaxBytes] Hard cap on a single tool_result payload.
215
215
  * @property {number} [mcpCallTimeoutMs] Per-MCP-call inactivity timeout.
216
216
  * @property {number} [mcpCallMaxTotalTimeoutMs] Hard wall-clock cap for one MCP call.
217
- * @property {number} [bashTimeoutMs] Documented for forward-compat; NOT wired to
218
- * any tool today (no `agent_bash_*_timeout` mechanism exists bash reads its per-call
219
- * `timeout` argument), so setting it has no effect until a run-level default is introduced.
217
+ * @property {number} [bashTimeoutMs] Foreground ceiling and default for
218
+ * Bash/Exec timeouts on the Pi bridge, applied by `normalizePiBuiltinToolParams`
219
+ * (defaults to 120_000). Background hand-offs deliberately ignore it: a process job is
220
+ * bounded by the host's `processJobs.maxRuntimeMs` instead.
220
221
  */
221
222
 
222
223
  /**
@@ -7,6 +7,20 @@ export function normalizeBashTimeoutMs(value: any, fallback?: number): any;
7
7
  * Exact millisecond timeout used by Bash.timeout_ms and Exec.timeout_ms.
8
8
  */
9
9
  export function normalizeProcessTimeoutMs(value: any, fallback?: number): any;
10
+ /**
11
+ * Background process-job timeout: positive-integer milliseconds with no
12
+ * foreground ceiling. A background budget belongs to the host's `processJobs`
13
+ * settings (`maxRuntimeMs`), which clamp it on their own side; reusing the
14
+ * foreground default as a cap here silently discarded the long runtime a caller
15
+ * deliberately asked for. `undefined` means "no explicit request", leaving the
16
+ * host default in force.
17
+ */
18
+ export function normalizeBackgroundTimeoutMs(value: any): number;
19
+ /**
20
+ * Legacy Bash `timeout` for a background job: the same seconds-vs-milliseconds
21
+ * heuristic as {@link normalizeBashTimeoutMs}, minus the foreground ceiling.
22
+ */
23
+ export function normalizeBackgroundBashTimeoutMs(value: any): number;
10
24
  /**
11
25
  * Compatibility wrapper retained for direct callers and tests.
12
26
  *
@@ -5,7 +5,7 @@ export { globToolImpl } from "./glob.js";
5
5
  export { grepToolImpl } from "./grep.js";
6
6
  export { createWebToolController } from "./web-controller.js";
7
7
  export { resolveRgPath } from "./shared/ripgrep.js";
8
- export { bashToolImpl, bashToolRun, normalizeBashTimeoutMs, normalizeProcessTimeoutMs } from "./bash.js";
8
+ export { bashToolImpl, bashToolRun, normalizeBackgroundBashTimeoutMs, normalizeBackgroundTimeoutMs, normalizeBashTimeoutMs, normalizeProcessTimeoutMs } from "./bash.js";
9
9
  export { execToolImpl, execToolRun } from "./exec.js";
10
10
  export { webFetchToolImpl, performWebFetch } from "./web-fetch.js";
11
11
  export { webSearchToolImpl, performWebSearch } from "./web-search.js";
@@ -138,6 +138,7 @@ export function initPiMcpTools(mcpConfig: any, reservedNames?: Set<any>, { limit
138
138
  retainedByMcpApps: boolean;
139
139
  privateCapabilityUrl: boolean;
140
140
  closed: boolean;
141
+ inFlight: Set<any>;
141
142
  }[];
142
143
  tools: any[];
143
144
  warnings: {
@@ -10,7 +10,7 @@
10
10
  * timeoutMs?: number,
11
11
  * maxOutputChars?: number,
12
12
  * launch: (options?: {timeoutMs?: number, signal?: AbortSignal, maxBufferBytes?: number, onStdout?: (chunk: Buffer) => void, onStderr?: (chunk: Buffer) => void}) => ReturnType<typeof startPreparedProcess>,
13
- * }) => Promise<{jobId: string, state: "queued"|"starting"|"running", startedAt: string|null}>} start
13
+ * }) => Promise<{jobId: string, state: "queued"|"starting"|"running", startedAt: string|null, maxRuntimeMs?: number}>} start
14
14
  */
15
15
  /**
16
16
  * Transfer one prepared command to the injected host controller. From the
@@ -59,6 +59,7 @@ export type ProcessJobsController = {
59
59
  jobId: string;
60
60
  state: "queued" | "starting" | "running";
61
61
  startedAt: string | null;
62
+ maxRuntimeMs?: number;
62
63
  }>;
63
64
  };
64
65
  import { startPreparedProcess } from "./process-runner.js";
@@ -177,9 +177,10 @@
177
177
  * @property {number} [toolPayloadMaxBytes] Hard cap on a single tool_result payload.
178
178
  * @property {number} [mcpCallTimeoutMs] Per-MCP-call inactivity timeout.
179
179
  * @property {number} [mcpCallMaxTotalTimeoutMs] Hard wall-clock cap for one MCP call.
180
- * @property {number} [bashTimeoutMs] Documented for forward-compat; NOT wired to
181
- * any tool today (no `agent_bash_*_timeout` mechanism exists bash reads its per-call
182
- * `timeout` argument), so setting it has no effect until a run-level default is introduced.
180
+ * @property {number} [bashTimeoutMs] Foreground ceiling and default for
181
+ * Bash/Exec timeouts on the Pi bridge, applied by `normalizePiBuiltinToolParams`
182
+ * (defaults to 120_000). Background hand-offs deliberately ignore it: a process job is
183
+ * bounded by the host's `processJobs.maxRuntimeMs` instead.
183
184
  */
184
185
  /**
185
186
  * @typedef {Object} RuntimeCompactionPolicy
@@ -754,9 +755,10 @@ export type RuntimeToolLimits = {
754
755
  */
755
756
  mcpCallMaxTotalTimeoutMs?: number;
756
757
  /**
757
- * Documented for forward-compat; NOT wired to
758
- * any tool today (no `agent_bash_*_timeout` mechanism exists bash reads its per-call
759
- * `timeout` argument), so setting it has no effect until a run-level default is introduced.
758
+ * Foreground ceiling and default for
759
+ * Bash/Exec timeouts on the Pi bridge, applied by `normalizePiBuiltinToolParams`
760
+ * (defaults to 120_000). Background hand-offs deliberately ignore it: a process job is
761
+ * bounded by the host's `processJobs.maxRuntimeMs` instead.
760
762
  */
761
763
  bashTimeoutMs?: number;
762
764
  };