@agentproto/runtime 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,10 @@ import { createReadStream, promises } from 'fs';
2
2
  import 'crypto';
3
3
  import { homedir } from 'os';
4
4
  import { join, resolve } from 'path';
5
+ import '@agentproto/model-catalog/llm';
5
6
  import { createInterface } from 'readline';
6
7
  import 'fs/promises';
7
8
  import * as providers_store_star from '@agentproto/providers-store';
8
- import '@agentproto/model-catalog/llm';
9
9
  import '@agentproto/model-catalog/route-identity';
10
10
  import '@agentproto/provider-presets';
11
11
  import '@agentproto/secrets/provision/recipe';
@@ -184,6 +184,10 @@ var init_tool_call_record = __esm({
184
184
  "src/tool-call-record.ts"() {
185
185
  }
186
186
  });
187
+ var init_usage = __esm({
188
+ "src/usage.ts"() {
189
+ }
190
+ });
187
191
  function defaultTranscriptBaseDir() {
188
192
  return join(homedir(), ".agentproto", "sessions");
189
193
  }
@@ -197,6 +201,7 @@ var init_transcript_writer = __esm({
197
201
  "src/transcript-writer.ts"() {
198
202
  init_tool_call_record();
199
203
  init_pr_provenance();
204
+ init_usage();
200
205
  }
201
206
  });
202
207
 
@@ -593,11 +598,11 @@ async function discoverHermesSessions(cwd, since, expectedId) {
593
598
  }
594
599
  }
595
600
  async function defaultHermesRunner(adapterSessionId) {
596
- const { spawn: spawn4 } = await import('child_process');
601
+ const { spawn: spawn5 } = await import('child_process');
597
602
  return new Promise((resolve16, reject) => {
598
603
  const chunks = [];
599
604
  const errChunks = [];
600
- const proc = spawn4(
605
+ const proc = spawn5(
601
606
  "hermes",
602
607
  ["sessions", "export", "--session-id", adapterSessionId, "-"],
603
608
  { stdio: ["ignore", "pipe", "pipe"] }
@@ -2204,7 +2209,7 @@ var init_sandbox_spec_schema = __esm({
2204
2209
  }).strict();
2205
2210
  }
2206
2211
  });
2207
- var commandSandboxSchema, attachFieldSchema, contextContinuityInputSchema;
2212
+ var commandSandboxSchema, attachFieldSchema, contentBlockSchema, promptInputSchema, contextContinuityInputSchema;
2208
2213
  var init_spawn_field_schemas = __esm({
2209
2214
  "src/spawn-field-schemas.ts"() {
2210
2215
  commandSandboxSchema = z.enum(["off", "workspace", "strict"]);
@@ -2212,6 +2217,12 @@ var init_spawn_field_schemas = __esm({
2212
2217
  z.boolean(),
2213
2218
  z.object({ parent: z.string().min(1).optional() })
2214
2219
  ]);
2220
+ contentBlockSchema = z.record(z.string(), z.unknown());
2221
+ promptInputSchema = z.union([
2222
+ z.string().min(1),
2223
+ contentBlockSchema,
2224
+ z.array(contentBlockSchema).min(1)
2225
+ ]);
2215
2226
  contextContinuityInputSchema = z.object({
2216
2227
  mode: z.enum(["manual", "ask", "auto"]).optional(),
2217
2228
  warnAtPct: z.number().int().min(0).max(100).optional(),
@@ -2232,7 +2243,7 @@ var init_spawn_field_schemas = __esm({
2232
2243
  });
2233
2244
  }
2234
2245
  });
2235
- var mcpBool, mcpPositiveNumber, agentStartInputShape, agentStartInputSchema;
2246
+ var mcpBool, mcpPositiveNumber, help, agentStartInputShape, agentStartInputSchema;
2236
2247
  var init_agent_start_schema = __esm({
2237
2248
  "src/agent-start-schema.ts"() {
2238
2249
  init_json_tolerant();
@@ -2246,78 +2257,73 @@ var init_agent_start_schema = __esm({
2246
2257
  (v) => typeof v === "string" && v.trim() !== "" ? Number(v) : v,
2247
2258
  z.number().positive()
2248
2259
  );
2260
+ help = (field) => `Details: tool_help {name:"agent_start", topic:"${field}"}`;
2249
2261
  agentStartInputShape = {
2250
2262
  adapter: z.string().min(1).optional().describe(
2251
- "Adapter slug \u2014 one of the installed `@agentproto/adapter-*` packages (e.g. 'claude-code', 'hermes', 'aider'). Omit only when `presetId` names a saved preset with an adapter."
2252
- ),
2253
- harness: z.string().min(1).optional().describe(
2254
- "Canonical harness slug \u2014 alias for `adapter`. Accepts the same values; use whichever field your caller produces."
2263
+ `Adapter slug (e.g. 'claude-code', 'hermes', 'aider'). Omit only when \`presetId\` supplies one. ${help("adapter")}`
2255
2264
  ),
2265
+ harness: z.string().min(1).optional().describe(`Alias for \`adapter\` \u2014 same values, whichever your caller produces.`),
2256
2266
  presetId: z.string().min(1).optional().describe(
2257
- "Saved user spawn preset id from `agentproto preset list`. Its adapter and decomposed axes are applied first; explicit fields on this call override it."
2267
+ "Saved spawn preset id from `agentproto preset list`. Explicit fields on this call override its values."
2258
2268
  ),
2259
2269
  workspaceSlug: z.string().optional().describe(
2260
- "Workspace slug from `agentproto workspace list`. The daemon resolves it to an absolute path. Omit to use the `cwd` field or the active workspace."
2270
+ "Workspace slug from `agentproto workspace list`, resolved to a path. Omit to use `cwd` or the active workspace."
2261
2271
  ),
2262
- cwd: z.string().optional().describe(
2263
- "Absolute path to spawn the agent in. Wins over `workspaceSlug` when both are set."
2264
- ),
2265
- prompt: z.string().optional().describe(
2266
- "Optional initial prompt. The session is spawned and the prompt dispatched in one shot \u2014 equivalent to `start` then `prompt` back-to-back. Skip to spawn idle."
2267
- ),
2268
- label: z.string().optional().describe(
2269
- "Free-text label that surfaces in `agent_sessions_list` and the UI \u2014 useful for tagging sessions with a conversation id or operator name."
2272
+ cwd: z.string().optional().describe("Absolute path to spawn the agent in. Wins over `workspaceSlug` when both are set."),
2273
+ prompt: promptInputSchema.optional().describe(
2274
+ 'Optional initial prompt \u2014 spawns and dispatches it in one call (spawn + `agent_prompt` combined). Omit to spawn idle. A plain string is composed with the role/AGENTS.md disposition preamble as usual; a content block or block array (e.g. a pasted image, `{type:"image", data, mimeType}`) SKIPS that composition and is sent to the adapter verbatim \u2014 the adapter negotiates its own multimodal support. ' + help("prompt")
2270
2275
  ),
2276
+ label: z.string().optional().describe("Free-text label shown in `agent_sessions_list` and the UI."),
2271
2277
  mode: z.string().optional().describe(
2272
- "Manifest-declared mode id (AIP-45 `modes`) applied at spawn time, BEFORE the child process starts \u2014 e.g. claude-code's 'plan' (read-only: reasons and proposes but does not edit or run commands), 'accept-edits', 'bypass-permissions'; codex's 'read-only' / 'full-access'; mastracode/opencode's 'plan' / 'build'. Adapters that don't declare `modes` (e.g. hermes) reject ANY value here \u2014 only pass this for adapters known to support it. Omit for the adapter's normal interactive mode."
2278
+ `Manifest-declared mode id (AIP-45 \`modes\`), applied before the child starts (e.g. claude-code's 'plan'/'accept-edits'/'bypass-permissions'). Adapters with no declared \`modes\` REJECT any value here. ${help("mode")}`
2273
2279
  ),
2274
2280
  origin: z.string().min(1).optional().describe(
2275
- "Source label for this spawn \u2014 the calling channel/harness (codex, cowork, vscode, cron, \u2026). Descriptor-only: groups the session under a source node in the tree. In-repo callers set it; the mcp-bridge can auto-stamp it from the host clientInfo."
2281
+ "Source label for this spawn (codex, cowork, vscode, cron, \u2026) \u2014 descriptor-only, groups the session under a source node in the tree."
2276
2282
  ),
2277
2283
  parentSessionId: z.string().min(1).optional().describe(
2278
- "Parent-lineage hint: attribute this spawn to a logical parent session so it nests under that node in the sessions tree instead of appearing as a depth-0 root. Pass the `id` of the session doing the spawning (e.g. an agent-to-agent `agent_start`). The child's `depth` is derived from the parent (parent depth + 1); you don't set it. Ignored when this call arrives through the scoped orchestrator gateway \u2014 that path derives the parent from its own token, which always wins over this hint."
2284
+ `Parent-lineage hint: nests this spawn under that session in the tree instead of a depth-0 root. Ignored through the scoped orchestrator gateway (its own token wins). ${help("parentSessionId")}`
2279
2285
  ),
2280
2286
  attach: jsonTolerant(attachFieldSchema).optional().describe(
2281
- 'Parent-attach control, mirroring `worktree`. By DEFAULT (omitted) a spawn attaches under the session that made it \u2014 the daemon derives that parent from the trusted caller id, so a supervisor\'s executors nest instead of appearing as depth-0 roots, no `parentSessionId` needed. Pass `false` to launch an INDEPENDENT root (no parent) \u2014 the deliberate detached spawn. `true` forces attach even under an `on-request` daemon policy; `{ parent: "<id>" }` pins an explicit parent. Ignored when this call arrives through the scoped orchestrator gateway (the scope token wins). Descriptor-only lineage: never relaxes a depth-gated worktree/role guard.'
2287
+ `Parent-attach override, mirrors \`worktree\`. Default attaches under the calling session; \`false\` = independent root; \`true\` forces attach; \`{ parent: "<id>" }\` pins an explicit parent. ${help("attach")}`
2282
2288
  ),
2283
2289
  boardId: z.string().min(1).optional().describe(
2284
- "Task-board pin for the spawned child, stamped onto its descriptor as `meta.boardId`. The Task ledger resolves the child's default board from this BEFORE walking `parentSessionId` lineage \u2014 so a client spawning several depth-0 root sessions (no shared lineage) can join them all onto ONE shared board. An explicit `boardId` passed on a task verb still wins over this pin. Omit for the lineage-derived `tree:<root>` default."
2290
+ `Task-board pin for the child (\`meta.boardId\`), resolved before parent-lineage. An explicit board on a task verb still wins. ${help("boardId")}`
2285
2291
  ),
2286
2292
  idempotencyKey: z.string().min(1).optional().describe(
2287
- "Caller-declared 'this is the same logical spawn' token \u2014 a PROMISE, not a guess. A retried agent_start call (e.g. after a slow/lost response) that repeats the same `idempotencyKey` for the same `adapter`+`cwd` within ~10min of a successful spawn gets that SAME session's descriptor back instead of forking a second process \u2014 the response carries `deduped: true` and `dedupeSource: \"explicit\"` so you can tell. Always wins over the daemon's own derived key (see `dedupe` below) when both would apply. Omitting this does NOT mean 'spawn unconditionally' \u2014 see `dedupe`."
2293
+ `Caller-declared dedup token: a retried call with the SAME key for the same \`adapter\`+\`cwd\` within ~10min of a successful spawn returns that SAME session (\`deduped: true\`) instead of forking a new one. Always wins over the implicit key \`dedupe\` derives. ${help("idempotencyKey")}`
2288
2294
  ),
2289
2295
  dedupe: mcpBool.optional().describe(
2290
- 'Per-call override for the daemon\'s `spawn.dedupe` policy \u2014 what happens when NO `idempotencyKey` is supplied. By DEFAULT (`spawn.dedupe: "always"`) a spawn that carries a `label` gets an IMPLICIT key derived from that label plus a hash of `prompt`, and dedupes against it exactly like an explicit key \u2014 set `dedupeSource: "implicit"` on the response (alongside `deduped: true`) so you can tell it wasn\'t your own promise that matched. A spawn with no `label` is never touched by this \u2014 deliberate parallel fan-out into one cwd (a real, exercised pattern here) needs no label and stays exactly as many sessions as you asked for. Pass `dedupe: false` to opt this ONE spawn out of implicit derivation regardless of policy \u2014 the escape hatch, mirroring `attach: false` / `worktree: false`. `dedupe: true` forces derivation even under an `"on-request"` daemon policy, mirroring `attach: true`. Unrelated and NOT covered by this flag: a `worktree` spawn that lands in a worktree another LIVE session already occupies under the same `label` is always refused (`dedupeSource: "worktree-cwd"`) \u2014 a shared worktree, unlike a shared plain cwd, is never a legitimate fan-out.'
2296
+ `Override the implicit dedup-by-\`label\` default (on when no \`idempotencyKey\` is given): \`false\` opts this spawn out, \`true\` forces it even under an "on-request" daemon policy. ${help("dedupe")}`
2291
2297
  ),
2292
2298
  permissionHold: mcpBool.optional().describe(
2293
- "Start the session in permission-hold mode: every ACP permission request the agent raises (Write, Bash, \u2026) is SURFACED and HELD in the cross-session inbox (`permissions_list` / `permissions_respond`) instead of auto-answered, and the agent blocks until a human/orchestrator approves or denies it. Default false = today's auto-answer behaviour. ACP adapters only; others ignore it."
2299
+ "Hold every ACP permission request in the cross-session inbox (`permissions_list`/`permissions_respond`) instead of auto-answering. ACP adapters only; default false."
2294
2300
  ),
2295
2301
  notifyParentOnCrash: mcpBool.optional().describe(
2296
- "Opt this spawn into a direct in-band crash notice to its parent: if THIS session later crashes (adapter process gone between turns), the parent (`parentSessionId`, direct or inherited) is told via `[child-crashed] <label/id>: <reason> \u2014 <lastError>` \u2014 enqueued immediately if the parent is alive and idle, or queued for its next turn (never interrupting an in-flight one) if it's busy. Default false. The free external webhook (`notifyUrl`) already fires on any crash regardless of this flag \u2014 this only adds the direct signal into the parent's OWN session, for a delegating supervisor that wants to react to a child's death without polling."
2302
+ `Notify this session's parent directly (in-band) if it later crashes. Independent of the global \`notifyUrl\` webhook, which fires either way. ${help("notifyParentOnCrash")}`
2297
2303
  ),
2298
2304
  allowSharedCwd: mcpBool.optional().describe(
2299
- "Acknowledge that this nested spawn WILL run in place inside its parent's working tree even when that tree has uncommitted changes and isn't an isolated worktree \u2014 silencing the shared-dirty-cwd warning agent_start otherwise returns in `warnings`. Only relevant for a delegated (depth > 0) spawn with no `worktree` and no `sandbox`; ignored otherwise. Default false = warn."
2305
+ "Acknowledge running a nested spawn in the parent's dirty working tree, silencing the shared-dirty-cwd warning. Only relevant with no `worktree`/`sandbox`; default false = warn."
2300
2306
  ),
2301
2307
  keepAlive: mcpBool.optional().describe(
2302
- "Exempt this session from the idle-reaper: it is never auto-retired for sitting idle, no matter how long. For a supervisor that legitimately parks \u2014 waiting on a child, waiting on a scheduled wake \u2014 idle looks identical to finished, and the reaper would otherwise pull it out from under you. Default false = today's behaviour. Toggle later with `session_set_keepalive`."
2308
+ "Exempt this session from the idle-reaper \u2014 never auto-retired for sitting idle. Default false. Toggle later with `session_set_keepalive`."
2303
2309
  ),
2304
2310
  options: jsonTolerant(
2305
2311
  z.record(z.string(), z.union([z.boolean(), z.number(), z.string()]))
2306
2312
  ).optional().describe(
2307
- "Manifest-declared option id \u2192 value map (AIP-45 `options`), applied at spawn time alongside `mode` \u2014 e.g. hermes' `skills` (string, prepended before the subcommand) or a boolean flag appended when true. Each value is validated against the option's declared `type`/`enum`/`min`/`max`; unknown ids reject. Adapters that don't declare a given option id reject it."
2313
+ "Manifest-declared option id \u2192 value map (AIP-45 `options`), validated against each option's declared type/enum/min/max. Unknown ids reject."
2308
2314
  ),
2309
2315
  skills: jsonTolerant(z.array(z.string())).optional().describe(
2310
- "Normalized, adapter-agnostic skill ids for this session (e.g. ['agentproto']). Merges with `~/.agentproto/config.json`'s `defaults.skills` / `defaults.adapters.<slug>.skills` (global < per-adapter < this field, which REPLACES rather than unions the config defaults when provided \u2014 a deliberate exact set). Folded into `options.skills` using the resolved adapter's declared shape (e.g. hermes' comma-joined `--skills a,b`); adapters with no declared `skills` option (e.g. claude-code, which auto-discovers from `~/.claude/skills`) ignore this \u2014 no-op."
2316
+ `Normalized skill ids for this session (e.g. ['agentproto']). REPLACES (not unions) config defaults when set. Adapters with no \`skills\` option (e.g. claude-code) ignore it. ${help("skills")}`
2311
2317
  ),
2312
2318
  model: z.string().optional().describe(
2313
- "Model identifier to pass to the adapter (e.g. 'claude-opus-4-8'). For ACP adapters (claude-code) applied via session/set_config_option after newSession \u2014 NOT via a CLI flag. Others may ignore it."
2319
+ "Model identifier to pass to the adapter. For claude-code, applied via session/set_config_option \u2014 NOT a CLI flag."
2314
2320
  ),
2315
2321
  effort: z.string().optional().describe(
2316
- "Reasoning effort level (e.g. 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode'). IMPORTANT: effort is calibrated per model \u2014 the same label maps to different compute budgets across models, and defaults differ by model (Sonnet 4.6 / Opus 4.8 default 'high'; Opus 4.7 default 'xhigh'). 'max' and 'ultracode' are session-only. Omit to keep the model's own default."
2322
+ "Reasoning effort label ('low'/'medium'/'high'/'xhigh'/'max'/'ultracode'). Calibrated PER MODEL \u2014 the same label means different compute budgets across models. Omit to keep the model's own default."
2317
2323
  ),
2318
2324
  route: jsonTolerant(
2319
2325
  z.object({ gateway: z.string().min(1), baseUrl: z.string().url().optional() })
2320
- ).optional().describe("Billing route/gateway. This is independent of model and named access profile."),
2326
+ ).optional().describe("Billing route/gateway. Independent of `model` and `access`."),
2321
2327
  access: jsonTolerant(z.object({ profileRef: z.string().min(1).optional() })).optional().describe("Named auth profile to bill at initial spawn; resolved from the local keychain."),
2322
2328
  posture: z.string().optional().describe("Canonical agent posture (plan, bypass, accept-edits, read-only) or native harness mode."),
2323
2329
  contextProfile: z.string().optional().describe("Context intake profile (for example full or lean)."),
@@ -2329,7 +2335,7 @@ var init_agent_start_schema = __esm({
2329
2335
  apiKey: z.string().optional()
2330
2336
  })
2331
2337
  ).optional().describe(
2332
- "Deterministic billing-auth mode + EXPLICIT credential for adapters that declare it (today: claude-code). EXPLICIT credential selection, not scrub-by-absence: `mode` picks 'subscription' (default) or 'api-key'; `token`/`apiKey` (matching the resolved mode) is the secret VALUE, merged against `~/.agentproto/config.json`'s `defaults.adapters.claude-code.auth` (this field's `mode` wins; the credential for the resolved mode wins over the matching config field). For claude-code, 'subscription' SETS CLAUDE_CODE_OAUTH_TOKEN to `token` (a bearer token minted via `claude setup-token` \u2014 bills the Max/Pro subscription, not API credits) and DELETES ANTHROPIC_API_KEY + the cloud-provider redirect toggles + ANTHROPIC_BASE_URL. 'api-key' SETS ANTHROPIC_API_KEY to `apiKey` and DELETES ANTHROPIC_AUTH_TOKEN \u2014 the deliberate 'bill the API' choice. FAILS FAST (refuses the spawn, no fallback) when the resolved mode has no credential configured anywhere. The secret is never logged or echoed back \u2014 only a fingerprint appears on the session descriptor / `agent_sessions_list`. Adapters that don't declare this vocabulary ignore this field entirely. `source: \"claude-code-oauth\"` (subscription mode, opt-in) instead reads the bearer FRESH on every spawn from the local Claude Code login (Keychain / ~/.claude/.credentials.json) \u2014 effectively self-refreshing; an explicit `token` still wins over it."
2338
+ `Explicit billing-auth mode + credential for adapters that declare it (today: claude-code) \u2014 'subscription' (default) bills the Max/Pro plan, 'api-key' bills API credits. FAILS FAST with no fallback if the resolved mode has no credential configured anywhere. ${help("auth")}`
2333
2339
  ),
2334
2340
  mcpServers: jsonTolerant(
2335
2341
  z.array(
@@ -2337,42 +2343,34 @@ var init_agent_start_schema = __esm({
2337
2343
  name: z.string(),
2338
2344
  transport: z.enum(["stdio", "http", "sse"]),
2339
2345
  ref: z.string().optional(),
2340
- headers: z.record(z.string(), z.string()).optional().describe(
2341
- "Static HTTP headers sent with every request to an `http` or `sse` MCP server (e.g. a fixed auth token). Ignored for `stdio` transports."
2342
- ),
2346
+ headers: z.record(z.string(), z.string()).optional().describe("Static HTTP headers for `http`/`sse` transports. Ignored for `stdio`."),
2343
2347
  credentialRef: z.string().optional().describe(
2344
- "Brokered credential path resolved at spawn time into additional `headers` (typically `Authorization`). The actual secret never lives in env or config; brokered headers win on collision with `headers`."
2348
+ "Brokered credential resolved into `headers` at spawn time (wins on collision with `headers`); the secret never lives in env/config."
2345
2349
  ),
2346
- args: z.array(z.string()).optional().describe("`stdio` only: argv passed to the `ref` command. Ignored for `http`/`sse`."),
2347
- env: z.record(z.string(), z.string()).optional().describe("`stdio` only: extra environment for the launched server. Ignored for `http`/`sse`.")
2350
+ args: z.array(z.string()).optional().describe("`stdio` only: argv passed to the `ref` command."),
2351
+ env: z.record(z.string(), z.string()).optional().describe("`stdio` only: extra environment for the launched server.")
2348
2352
  })
2349
2353
  )
2350
2354
  ).optional().describe(
2351
- "MCP servers to mount into the spawned agent's session at spawn time. Forwarded verbatim to `session/new.mcpServers` on the ACP arm \u2014 gives the child agent a host-chosen scoped toolset (e.g. the daemon's own orchestration gateway so it can spawn + supervise sub-agents). Adapters that don't model MCP mounting ignore it."
2355
+ "MCP servers to mount into the spawned agent's session at spawn time. Adapters that don't model MCP mounting ignore it."
2352
2356
  ),
2353
2357
  orchestrator: jsonTolerant(
2354
2358
  z.union([
2355
2359
  z.boolean(),
2356
2360
  z.object({
2357
- tools: z.array(z.string()).optional().describe(
2358
- "Explicit allowlist \u2014 narrows the orchestration toolset to \u2286 the default subset. Names outside the default are dropped (a child can never widen its own scope). Omit for the full default subset."
2359
- ),
2360
- maxDepth: z.number().int().min(1).max(8).optional().describe(
2361
- "Max recursion depth reachable through this child (default 3, hard ceiling 8). A spawn that would exceed it is rejected. For a recursive spawn it can only LOWER the inherited cap, never raise it."
2362
- ),
2363
- maxChildren: z.number().int().min(1).optional().describe(
2364
- "Max concurrently-alive sub-agents this child may spawn (default 8). For a recursive spawn it can only lower the inherited quota."
2365
- )
2361
+ tools: z.array(z.string()).optional().describe("Explicit allowlist \u2286 the default subset \u2014 a child can never widen its own scope."),
2362
+ maxDepth: z.number().int().min(1).max(8).optional().describe("Max recursion depth reachable through this child (default 3, ceiling 8, lower-only on recursion)."),
2363
+ maxChildren: z.number().int().min(1).optional().describe("Max concurrently-alive sub-agents this child may spawn (default 8, lower-only on recursion).")
2366
2364
  })
2367
2365
  ])
2368
2366
  ).optional().describe(
2369
- "Make this child a SCOPED orchestrator \u2014 auto-mount the daemon's own orchestration MCP tools (start/prompt/wait/poll/output + subtree list/kill) so it can spawn and supervise its OWN sub-agents. `true` = the default curated subset; `{ tools: [...] }` narrows it. The daemon mints a per-child scope-token, injects the scoped sub-gateway URL into the child's session (alongside any `mcpServers` you pass), and revokes the token when the session exits. Shell/fs/remote/import/terminal tools are NEVER exposed this way."
2367
+ `Make this child a SCOPED orchestrator \u2014 auto-mounts the daemon's own spawn/supervise tools, scoped to its own sub-agents. \`true\` = default subset; \`{ tools }\` narrows it. Shell/fs/remote tools are NEVER exposed. ${help("orchestrator")}`
2370
2368
  ),
2371
2369
  notifyUrl: z.string().url().optional().describe(
2372
2370
  "Optional per-session webhook URL. POSTed (fire-and-forget) on this session's turn-end / awaiting-input / exited events, in addition to any global notify URL."
2373
2371
  ),
2374
2372
  wait: mcpBool.optional().describe(
2375
- "Block until the spawned session's first turn completes and include the cleaned output in the response. Default false = return the descriptor immediately. Note this blocks for the child's ENTIRE first turn (~40-90s+), not just the spawn. Batching several `wait: true` calls in one turn does NOT run them in parallel: harnesses that execute tool calls sequentially serialize them, each wait blocking its slot until its child's turn ends. For parallel fan-out spawn with `wait: false` (all spawns return in seconds), then wait on completion separately via `agentproto sessions wait <id> --until turn-end` (detached/background) or a completion policy via `policy_attach`."
2373
+ `Block until the spawned session's first turn completes (~40-90s+) and include its output. Default false = return immediately. Batched \`wait:true\` calls do NOT run in parallel \u2014 see \`tool_help {name:"agent_start", topic:"wait"}\` for the fan-out pattern.`
2376
2374
  ),
2377
2375
  maxCostUsd: mcpPositiveNumber.optional().describe(
2378
2376
  "Hard ceiling on cumulative session cost (USD). The session is stopped at a turn-end once exceeded."
@@ -2384,45 +2382,39 @@ var init_agent_start_schema = __esm({
2384
2382
  scope: z.enum(["session", "profile"]).describe("Spend surface: this session, or every session on its auth profile.")
2385
2383
  })
2386
2384
  ).optional().describe(
2387
- "Windowed cost-budget cap (DISTINCT from `maxCostUsd`). Auto-attaches a governance policy that trips `policy:failed` when the rolling windowed spend for `scope` crosses `maxCostUsd`. Never kills the session \u2014 it trips a policy for a supervisor to act on."
2385
+ "Windowed cost-budget cap (DISTINCT from `maxCostUsd`) \u2014 trips `policy:failed` on overage, never kills the session."
2388
2386
  ),
2389
2387
  restartPolicy: jsonTolerant(
2390
2388
  z.object({
2391
- on: z.array(z.enum(["crashed", "error"])).min(1).describe(
2392
- 'Which automatic death reasons trigger a restart: "crashed" (the crash-detect sweep found the adapter process gone) and/or "error" (an unexpected turn error with no other intentional reason). Never restarts a clean exit, an operator kill, an idle-reap, a daemon-restart death, or a cost-budget kill, regardless of this list.'
2393
- ),
2394
- maxRetries: z.number().int().positive().describe(
2395
- "Rolling-window crash-loop cap: give up (leave the session dead, no further auto-restart) once this many restarts have fired within `windowMs`."
2396
- ),
2389
+ on: z.array(z.enum(["crashed", "error"])).min(1).describe("Which automatic death reasons trigger a restart. Never a clean exit, kill, idle-reap, or cost-budget kill."),
2390
+ maxRetries: z.number().int().positive().describe("Rolling-window crash-loop cap: give up after this many restarts within `windowMs`."),
2397
2391
  windowMs: z.number().int().positive().describe("Rolling window (ms) `maxRetries` counts restarts over."),
2398
2392
  baseDelayMs: z.number().int().positive().describe("First restart's backoff delay (ms), before `factor` compounds it."),
2399
2393
  factor: z.number().positive().describe("Exponential backoff multiplier applied per consecutive restart."),
2400
2394
  maxDelayMs: z.number().int().positive().describe("Backoff ceiling (ms) \u2014 the computed delay never grows past this."),
2401
- resume: z.boolean().optional().describe(
2402
- "Reserved for a future explicit resume-vs-fresh-spawn toggle; this PR always revives in place (same session id, same conversation)."
2403
- )
2395
+ resume: z.boolean().optional().describe("Reserved for a future resume-vs-fresh-spawn toggle; this always revives in place today.")
2404
2396
  })
2405
2397
  ).optional().describe(
2406
- "Opt-in auto-restart policy (restart-scheduler PR-2). When set, an unexpected death (`crashed` and/or `error`, per `on`) is automatically revived IN PLACE (reusing the resume machinery \u2014 same session id, same conversation) after an exponential backoff, up to a rolling-window crash-loop cap. Omit for today's behaviour: a dead session stays dead until a human/orchestrator prompts or restarts it."
2398
+ `Opt-in auto-restart policy: an unexpected death (\`crashed\`/\`error\`, per \`on\`) is revived IN PLACE after exponential backoff, up to a rolling-window crash-loop cap. Omit for today's stay-dead behaviour. ${help("restartPolicy")}`
2407
2399
  ),
2408
2400
  contextContinuity: jsonTolerant(contextContinuityInputSchema).optional().describe(
2409
2401
  "Context-continuity policy for this session \u2014 controls warning, opportunistic compaction, fresh-continuation, and hard-stop thresholds. Resolved from global \u2192 per-adapter \u2192 explicit override."
2410
2402
  ),
2411
2403
  role: z.string().optional().describe(
2412
- "Spawn-time role gating whether this child may itself delegate (spawn/drive further children) and, if it can, which roles IT may in turn spawn. Built-ins: 'executor' = leaf, cannot delegate \u2014 `orchestrator` is ignored and `agent_start`/`agent_prompt` are stripped from its default toolset, regardless of `promptAppend`. 'supervisor' = may delegate (today's default behaviour). Custom roles installed as role packs (see `role_list`) resolve the same way. A spawn made THROUGH an orchestrator is additionally gated by the privilege lattice: the calling role may only spawn a role allowlisted in its `spawnableRoles`, or \u2014 open mode, the default \u2014 at or below its own `level` (never something MORE privileged than itself). Omit `role` to derive from spawn depth (root spawns default to supervisor; spawns made through an orchestrator default to executor \u2014 see `defaultRoleDepthCutoff` in config.json's `defaults` block)."
2404
+ `Spawn-time delegation gate. 'executor' = leaf, cannot delegate (\`agent_start\`/\`agent_prompt\` stripped). 'supervisor' = may delegate (default). Custom role packs resolve the same way. Omit to derive from spawn depth. ${help("role")}`
2413
2405
  ),
2414
2406
  promptAppend: z.string().optional().describe(
2415
- "One-off runtime text layered ON TOP of the resolved role's disposition and prepended to `prompt` \u2014 it specializes the disposition, it cannot replace it, and it cannot re-open the tool gate (an executor asked to 'delegate anyway' via this field still has no delegation tools)."
2407
+ "One-off runtime text layered onto the resolved role's disposition and prepended to `prompt` \u2014 specializes it, cannot reopen a denied tool gate."
2416
2408
  ),
2417
2409
  deferredTools: z.boolean().optional().describe(
2418
- "Override deferred/lazy MCP tool loading for this spawn's daemon self-mount: `true` hides every tool outside a small always-on set from `tools/list` (still fully callable \u2014 use `tool_search` to look up a hidden tool's schema by keyword before calling it), `false` keeps the full eager surface. Omit to use the resolved role's own default ('executor' defaults ON, since it can't delegate anyway and rarely needs the full ~190-tool surface); omit AND spawn a role with no opinion to fall through to the daemon's own boot-time `defaults.mcp.deferredTools` config."
2410
+ `Override lazy MCP tool loading for this spawn: \`true\` hides most tools from \`tools/list\` (still callable via \`tool_search\`), \`false\` keeps the full eager surface. Omit to use the role/daemon default. ${help("deferredTools")}`
2419
2411
  ),
2420
2412
  browser: z.preprocess(
2421
2413
  // `true` is sugar for the only mode; stringified booleans tolerated.
2422
2414
  (v) => v === true || v === "true" ? "headless" : v === "false" ? false : v,
2423
2415
  z.union([z.literal("headless"), z.literal(false)])
2424
2416
  ).optional().describe(
2425
- '`"headless"` gives the spawned agent its own isolated headless Chrome (1440x900, temporary profile) as a per-session `browser` MCP server (chrome-devtools-mcp: navigate_page, take_screenshot, evaluate_script, click, list_console_messages, \u2026), torn down with the session (`true` = `"headless"`). Works for any adapter that mounts stdio MCP servers; runs inside the session\'s `commandSandbox` (`strict` \u21D2 file:// only). `false` = none. Omit to use the role / preset / `defaults.spawn.browser` default (off). Not supported with `sandbox`.'
2417
+ `\`"headless"\` gives the spawned agent its own isolated headless Chrome as a per-session \`browser\` MCP server, torn down with the session. \`false\` = none (default). Not supported with \`sandbox\`. ${help("browser")}`
2426
2418
  ),
2427
2419
  trace: z.boolean().optional().describe(
2428
2420
  "Emit Langfuse observability traces for this session (prompt/completion + tool spans + tokens/cost). Off by default; requires langfuse eval-reporter creds configured."
@@ -2431,22 +2423,22 @@ var init_agent_start_schema = __esm({
2431
2423
  z.union([
2432
2424
  z.string().min(1).describe("Sandbox provider slug from `list_sandbox_providers` (e.g. 'local', 'e2b')."),
2433
2425
  sandboxSpecWithReuseSchema.describe(
2434
- "Inline AIP-36 SandboxDefinition \u2014 boots this exact spec instead of a catalog slug. Set `reuse` (sandboxId, ledger label, or unique sandboxId prefix \u2014 see `agentproto sandbox list`) to reconnect to an existing sandbox instead of booting fresh."
2426
+ "Inline AIP-36 SandboxDefinition \u2014 boots this exact spec instead of a catalog slug. Set `reuse` to reconnect to an existing sandbox instead of booting fresh."
2435
2427
  )
2436
2428
  ])
2437
2429
  ).optional().describe(
2438
- "Run this session inside a sandbox instead of on the host \u2014 pass a provider slug (see `list_sandbox_providers`) or an inline AIP-36 SandboxDefinition object. The daemon boots the sandbox, spawns `adapter` on the box's OWN agentproto daemon, and proxies the conversation back onto this session \u2014 `agent_prompt`/`agent_output`/`agent_kill` behave exactly as they do for a local spawn, and the transcript stays readable here even after the box is torn down. Omit to run locally (default). Pass an inline spec with `reuse: \"<sandboxId>\"` (from a prior session's `sandboxId`) to reconnect to an existing box instead \u2014 by default such a box is PAUSED (not killed) on session close so it stays reusable; set `lifecycle.destroy_on` to always kill it. DO NOT CONFUSE with `commandSandbox` below \u2014 this field boots a WHOLE SEPARATE machine/box; `commandSandbox` confines THIS host's own spawn argv in place. The two are independent and combine (or not) freely; `commandSandbox` is ignored for a `sandbox` spawn (the box's own daemon would need to apply it)."
2430
+ `Run this session inside a sandbox instead of on the host (provider slug or inline AIP-36 spec) \u2014 the daemon boots the box, spawns \`adapter\` on its own agentproto daemon, and proxies the conversation back here. DISTINCT from \`commandSandbox\` (whole separate machine vs. confining this host's own process). Omit to run locally. ${help("sandbox")}`
2439
2431
  ),
2440
2432
  appServe: jsonTolerant(
2441
2433
  z.object({
2442
2434
  dir: z.string().min(1).describe("Absolute path to the app's directory INSIDE the sandbox box (e.g. '/home/user/apps/<slug>')."),
2443
- port: z.number().int().min(1).max(65535).optional().describe("Port the UI binds inside the box (default 3210). The port is exposed by the sandbox provider and its public URL returned.")
2435
+ port: z.number().int().min(1).max(65535).optional().describe("Port the UI binds inside the box (default 3210). Exposed by the sandbox provider as a public URL.")
2444
2436
  }).strict()
2445
2437
  ).optional().describe(
2446
- "WP3 \u2014 serve an agentproto app's UI from INSIDE the sandbox box and return its public URL. Requires `sandbox` (rejected otherwise). The box daemon installs the app (`app_install` on the in-box `dir`), launches `agentproto app serve --host 0.0.0.0 --port <port>` detached through the box's `command_execute`, and the spawn result + descriptor carry `appServe: { appId, dir, port, url, ready }` \u2014 `url` is the provider-resolved public URL for the served UI (the port is also added to the spec's `extraPorts` and echoed in `sandboxPorts`)."
2438
+ `Serve an agentproto app's UI from INSIDE the sandbox box and return its public URL. Requires \`sandbox\` (rejected otherwise). ${help("appServe")}`
2447
2439
  ),
2448
2440
  commandSandbox: commandSandboxSchema.optional().describe(
2449
- 'OS-level process confinement (macOS Seatbelt / Linux bubblewrap) for the adapter\'s OWN spawned process on THIS host \u2014 NOT the `sandbox` field above, which boots an entirely separate remote box. This wraps the exact argv `adapter` spawns as (e.g. `claude`, `npx @agentclientprotocol/claude-agent-acp`) so its process tree is denied filesystem access outside the session\'s `cwd` \u2014 confinement an ACP permission seam can never provide, since it only sees tool calls the adapter chooses to report, not what an in-process Bash actually touches. `"off"` (default when omitted AND no `.agentproto/command-sandbox.json` sets an `adapterSpawn.mode`) = unconfined, unchanged behaviour. `"workspace"` = deny reads/writes to $HOME outside the workspace (protects ~/.ssh, ~/.aws, credentials, \u2026); network stays allowed. `"strict"` = `"workspace"` + deny all network. A workspace can set this same axis persistently via the `adapterSpawn` key of `.agentproto/command-sandbox.json` (a DISTINCT key from the top-level `mode` that key file also carries for `command_execute` \u2014 the two are never shared; misconfiguring the whole-session adapter jail is a bigger blast radius than misconfiguring one shell command) \u2014 this param, when set, overrides that file. Ignored for a `sandbox` spawn. `"workspace"`/`"strict"` with no backend installed for this platform (macOS needs `sandbox-exec`, Linux needs `bwrap`) FAILS the spawn rather than silently running unconfined.'
2441
+ `OS-level confinement (macOS Seatbelt / Linux bubblewrap) of the adapter's OWN process on THIS host \u2014 NOT \`sandbox\` above, which boots a separate machine. \`"off"\` (default) = unconfined; \`"workspace"\` = deny reads/writes outside cwd; \`"strict"\` = + deny network. Fails the spawn if the OS backend isn't installed. ${help("commandSandbox")}`
2450
2442
  ),
2451
2443
  worktree: jsonTolerant(
2452
2444
  z.union([
@@ -2455,17 +2447,15 @@ var init_agent_start_schema = __esm({
2455
2447
  slug: z.string().regex(
2456
2448
  /^[a-z0-9][a-z0-9-]*$/,
2457
2449
  "slug must be lowercase kebab-case (letters, digits, hyphens)"
2458
- ).optional().describe(
2459
- "Pin the worktree's slug (names its branch `wt/<slug>` and its directory). Omit to auto-mint a collision-free one from the label."
2460
- ),
2450
+ ).optional().describe("Pin the worktree's slug (branch `wt/<slug>`). Omit to auto-mint one from the label."),
2461
2451
  base: z.string().min(1).optional().describe("Git ref the worktree branch is cut from. Default 'origin/main'."),
2462
2452
  async: z.boolean().optional().describe(
2463
- 'Return a real, registered session as soon as it\'s minted (status "starting") instead of blocking `agent_start`\'s response on `git worktree add` + the repo\'s setup hooks, which can run minutes. Provisioning + the driver spawn continue in the background; poll the session\'s `status` (flips to "running" on success, "error" with a readable `lastError` on failure \u2014 it never sits in "starting" forever). Any `prompt` is held and dispatched only once the tree and the driver session both exist. Incompatible with `wait` (there is no first-turn output to block on yet) \u2014 combining the two is rejected. Defaults to true for any spawn that provisions a worktree, UNLESS this call also sets `wait` (which falls back to the old synchronous path instead of conflicting). Pass `false` explicitly to force the old blocking ok/fail contract even without `wait`.'
2453
+ `Return a registered session immediately (status "starting") instead of blocking on provisioning; incompatible with \`wait\`. Defaults true whenever a worktree is provisioned. ${help("worktree")}`
2464
2454
  )
2465
2455
  }).strict()
2466
2456
  ])
2467
2457
  ).optional().describe(
2468
- "Isolate this session in its OWN git worktree instead of spawning directly in `cwd` \u2014 so a parallel agent can't collide on the working tree. `true` provisions a worktree on a fresh branch `wt/<slug>` cut from origin/main (slug auto-minted from `label`); pass `{ slug, base, async }` to pin either, or opt into an early return (`async`, see that field's own description). The daemon boots the worktree (git worktree add + the repo's agentproto.json setup hooks) and spawns `adapter` THERE; the session's cwd, and every path it edits, live inside the worktree. Honoured only for a ROOT spawn (a spawn made THROUGH an orchestrator inherits its parent's tree \u2014 no second worktree; an EXPLICIT `worktree` on such a nested spawn is REJECTED, not silently ignored \u2014 use `sandbox` to isolate a child) and only when `cwd` is inside a git repo (nothing to isolate otherwise \u21D2 spawns plain, no error). The daemon's `worktrees.isolation` policy may force this ON for every root spawn (`always`) or OFF (`never`, which REJECTS an explicit `worktree`). Ignored for a `sandbox` spawn (the box already isolates). The worktree is NOT auto-removed on session close \u2014 it holds the agent's work; tear it down with `agentproto worktree rm|archive|gc`."
2458
+ `Isolate this session in its OWN git worktree instead of \`cwd\`. \`true\` provisions one on a fresh \`wt/<slug>\` branch; \`{ slug, base, async }\` pins details. ROOT spawns only (rejected on a nested spawn \u2014 use \`sandbox\` there); ignored when \`cwd\` isn't a git repo. NOT auto-removed on session close \u2014 tear down with \`agentproto worktree rm\`. ${help("worktree")}`
2469
2459
  )
2470
2460
  };
2471
2461
  agentStartInputSchema = z.object(agentStartInputShape);
@@ -2509,8 +2499,20 @@ var init_catalog_provider_models = __esm({
2509
2499
  "src/catalog-provider-models.ts"() {
2510
2500
  }
2511
2501
  });
2502
+ var init_app_ui_build = __esm({
2503
+ "src/app-ui-build.ts"() {
2504
+ }
2505
+ });
2506
+
2507
+ // src/app-ui-placeholder.ts
2508
+ var init_app_ui_placeholder = __esm({
2509
+ "src/app-ui-placeholder.ts"() {
2510
+ }
2511
+ });
2512
2512
  var init_app_ui_apps = __esm({
2513
2513
  "src/app-ui-apps.ts"() {
2514
+ init_app_ui_build();
2515
+ init_app_ui_placeholder();
2514
2516
  }
2515
2517
  });
2516
2518
  var MESSAGE_KINDS, MESSAGE_URGENCIES;
@@ -2532,6 +2534,7 @@ var init_agent_tools = __esm({
2532
2534
  "src/agent-tools.ts"() {
2533
2535
  init_transcript_export();
2534
2536
  init_agent_start_schema();
2537
+ init_spawn_field_schemas();
2535
2538
  init_session_spawn();
2536
2539
  init_canonical_posture();
2537
2540
  init_user_presets();
@@ -2602,10 +2605,6 @@ var init_terminal_transcript_writer = __esm({
2602
2605
  init_transcript_writer();
2603
2606
  }
2604
2607
  });
2605
- var init_usage = __esm({
2606
- "src/usage.ts"() {
2607
- }
2608
- });
2609
2608
  var init_context_window = __esm({
2610
2609
  "src/context-window.ts"() {
2611
2610
  }
@@ -2854,6 +2853,7 @@ var init_config_schema = __esm({
2854
2853
  label: z.string().optional(),
2855
2854
  authToken: z.string().optional(),
2856
2855
  resumeSessionsOnBoot: z.boolean().optional(),
2856
+ continueInterruptedOnBoot: z.boolean().optional(),
2857
2857
  idleReapAfterMs: z.number().optional(),
2858
2858
  crashDetectIntervalMs: z.number().optional(),
2859
2859
  restartSweepIntervalMs: z.number().optional(),
@@ -3028,6 +3028,16 @@ var init_config_schema = __esm({
3028
3028
  help: "Eagerly re-spawn eligible agent-cli sessions that died from a daemon restart.",
3029
3029
  default: false
3030
3030
  },
3031
+ {
3032
+ path: "daemon.continueInterruptedOnBoot",
3033
+ schema: bool,
3034
+ apply: "restart",
3035
+ writable: true,
3036
+ section: "daemon",
3037
+ label: "Continue interrupted sessions on boot",
3038
+ help: 'After the eager resume pass, send a one-shot "continue" prompt to sessions the last restart cut off mid-turn.',
3039
+ default: false
3040
+ },
3031
3041
  {
3032
3042
  path: "daemon.idleReapAfterMs",
3033
3043
  schema: num,