@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.
package/dist/config.d.ts CHANGED
@@ -70,6 +70,18 @@ interface DaemonConfig {
70
70
  * `agentproto config set daemon.resumeSessionsOnBoot true`. Surfaced in
71
71
  * `daemon_health` / `GET /health`. */
72
72
  resumeSessionsOnBoot?: boolean;
73
+ /** Opt-in continue-on-boot (continue-interrupted). When true, right after
74
+ * the eager resume pass, the boot sends a "the daemon restarted mid-turn,
75
+ * check the state on disk and continue" prompt to every session the LAST
76
+ * restart interrupted mid-turn (`interrupted: true`) that is resumable,
77
+ * under the resume cap, idle, and whose resume didn't fail. Never loops: at
78
+ * most one auto-continue per restart and `MAX_AUTO_CONTINUE_ATTEMPTS` in a
79
+ * row without a successful turn-end. Default false — the interrupted prompt
80
+ * itself is never auto-retried, and a continue prompt is still a prompt
81
+ * (tokens, side effects), so it's opt-in. The manual
82
+ * `session_continue_interrupted` verb works regardless. Set via
83
+ * `agentproto config set daemon.continueInterruptedOnBoot true`. */
84
+ continueInterruptedOnBoot?: boolean;
73
85
  /** Idle agent-session reaper (PR-6). When set to a positive number of
74
86
  * milliseconds, the daemon periodically retires agent-cli sessions that have
75
87
  * been idle (not busy, not awaiting input, `status:"running"`) longer than
package/dist/config.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { join, resolve, dirname } from 'path';
3
+ import '@agentproto/model-catalog/llm';
3
4
  import { createReadStream, promises } from 'fs';
4
5
  import { homedir } from 'os';
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';
@@ -179,6 +179,10 @@ var init_tool_call_record = __esm({
179
179
  "src/tool-call-record.ts"() {
180
180
  }
181
181
  });
182
+ var init_usage = __esm({
183
+ "src/usage.ts"() {
184
+ }
185
+ });
182
186
  function defaultTranscriptBaseDir() {
183
187
  return join(homedir(), ".agentproto", "sessions");
184
188
  }
@@ -192,6 +196,7 @@ var init_transcript_writer = __esm({
192
196
  "src/transcript-writer.ts"() {
193
197
  init_tool_call_record();
194
198
  init_pr_provenance();
199
+ init_usage();
195
200
  }
196
201
  });
197
202
 
@@ -588,11 +593,11 @@ async function discoverHermesSessions(cwd, since, expectedId) {
588
593
  }
589
594
  }
590
595
  async function defaultHermesRunner(adapterSessionId) {
591
- const { spawn: spawn4 } = await import('child_process');
596
+ const { spawn: spawn5 } = await import('child_process');
592
597
  return new Promise((resolve16, reject) => {
593
598
  const chunks = [];
594
599
  const errChunks = [];
595
- const proc = spawn4(
600
+ const proc = spawn5(
596
601
  "hermes",
597
602
  ["sessions", "export", "--session-id", adapterSessionId, "-"],
598
603
  { stdio: ["ignore", "pipe", "pipe"] }
@@ -2199,7 +2204,7 @@ var init_sandbox_spec_schema = __esm({
2199
2204
  }).strict();
2200
2205
  }
2201
2206
  });
2202
- var commandSandboxSchema, attachFieldSchema, contextContinuityInputSchema;
2207
+ var commandSandboxSchema, attachFieldSchema, contentBlockSchema, promptInputSchema, contextContinuityInputSchema;
2203
2208
  var init_spawn_field_schemas = __esm({
2204
2209
  "src/spawn-field-schemas.ts"() {
2205
2210
  commandSandboxSchema = z.enum(["off", "workspace", "strict"]);
@@ -2207,6 +2212,12 @@ var init_spawn_field_schemas = __esm({
2207
2212
  z.boolean(),
2208
2213
  z.object({ parent: z.string().min(1).optional() })
2209
2214
  ]);
2215
+ contentBlockSchema = z.record(z.string(), z.unknown());
2216
+ promptInputSchema = z.union([
2217
+ z.string().min(1),
2218
+ contentBlockSchema,
2219
+ z.array(contentBlockSchema).min(1)
2220
+ ]);
2210
2221
  contextContinuityInputSchema = z.object({
2211
2222
  mode: z.enum(["manual", "ask", "auto"]).optional(),
2212
2223
  warnAtPct: z.number().int().min(0).max(100).optional(),
@@ -2227,7 +2238,7 @@ var init_spawn_field_schemas = __esm({
2227
2238
  });
2228
2239
  }
2229
2240
  });
2230
- var mcpBool, mcpPositiveNumber, agentStartInputShape, agentStartInputSchema;
2241
+ var mcpBool, mcpPositiveNumber, help, agentStartInputShape, agentStartInputSchema;
2231
2242
  var init_agent_start_schema = __esm({
2232
2243
  "src/agent-start-schema.ts"() {
2233
2244
  init_json_tolerant();
@@ -2241,78 +2252,73 @@ var init_agent_start_schema = __esm({
2241
2252
  (v) => typeof v === "string" && v.trim() !== "" ? Number(v) : v,
2242
2253
  z.number().positive()
2243
2254
  );
2255
+ help = (field) => `Details: tool_help {name:"agent_start", topic:"${field}"}`;
2244
2256
  agentStartInputShape = {
2245
2257
  adapter: z.string().min(1).optional().describe(
2246
- "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."
2247
- ),
2248
- harness: z.string().min(1).optional().describe(
2249
- "Canonical harness slug \u2014 alias for `adapter`. Accepts the same values; use whichever field your caller produces."
2258
+ `Adapter slug (e.g. 'claude-code', 'hermes', 'aider'). Omit only when \`presetId\` supplies one. ${help("adapter")}`
2250
2259
  ),
2260
+ harness: z.string().min(1).optional().describe(`Alias for \`adapter\` \u2014 same values, whichever your caller produces.`),
2251
2261
  presetId: z.string().min(1).optional().describe(
2252
- "Saved user spawn preset id from `agentproto preset list`. Its adapter and decomposed axes are applied first; explicit fields on this call override it."
2262
+ "Saved spawn preset id from `agentproto preset list`. Explicit fields on this call override its values."
2253
2263
  ),
2254
2264
  workspaceSlug: z.string().optional().describe(
2255
- "Workspace slug from `agentproto workspace list`. The daemon resolves it to an absolute path. Omit to use the `cwd` field or the active workspace."
2265
+ "Workspace slug from `agentproto workspace list`, resolved to a path. Omit to use `cwd` or the active workspace."
2256
2266
  ),
2257
- cwd: z.string().optional().describe(
2258
- "Absolute path to spawn the agent in. Wins over `workspaceSlug` when both are set."
2259
- ),
2260
- prompt: z.string().optional().describe(
2261
- "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."
2262
- ),
2263
- label: z.string().optional().describe(
2264
- "Free-text label that surfaces in `agent_sessions_list` and the UI \u2014 useful for tagging sessions with a conversation id or operator name."
2267
+ cwd: z.string().optional().describe("Absolute path to spawn the agent in. Wins over `workspaceSlug` when both are set."),
2268
+ prompt: promptInputSchema.optional().describe(
2269
+ '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")
2265
2270
  ),
2271
+ label: z.string().optional().describe("Free-text label shown in `agent_sessions_list` and the UI."),
2266
2272
  mode: z.string().optional().describe(
2267
- "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."
2273
+ `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")}`
2268
2274
  ),
2269
2275
  origin: z.string().min(1).optional().describe(
2270
- "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."
2276
+ "Source label for this spawn (codex, cowork, vscode, cron, \u2026) \u2014 descriptor-only, groups the session under a source node in the tree."
2271
2277
  ),
2272
2278
  parentSessionId: z.string().min(1).optional().describe(
2273
- "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."
2279
+ `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")}`
2274
2280
  ),
2275
2281
  attach: jsonTolerant(attachFieldSchema).optional().describe(
2276
- '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.'
2282
+ `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")}`
2277
2283
  ),
2278
2284
  boardId: z.string().min(1).optional().describe(
2279
- "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."
2285
+ `Task-board pin for the child (\`meta.boardId\`), resolved before parent-lineage. An explicit board on a task verb still wins. ${help("boardId")}`
2280
2286
  ),
2281
2287
  idempotencyKey: z.string().min(1).optional().describe(
2282
- "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`."
2288
+ `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")}`
2283
2289
  ),
2284
2290
  dedupe: mcpBool.optional().describe(
2285
- '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.'
2291
+ `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")}`
2286
2292
  ),
2287
2293
  permissionHold: mcpBool.optional().describe(
2288
- "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."
2294
+ "Hold every ACP permission request in the cross-session inbox (`permissions_list`/`permissions_respond`) instead of auto-answering. ACP adapters only; default false."
2289
2295
  ),
2290
2296
  notifyParentOnCrash: mcpBool.optional().describe(
2291
- "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."
2297
+ `Notify this session's parent directly (in-band) if it later crashes. Independent of the global \`notifyUrl\` webhook, which fires either way. ${help("notifyParentOnCrash")}`
2292
2298
  ),
2293
2299
  allowSharedCwd: mcpBool.optional().describe(
2294
- "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."
2300
+ "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."
2295
2301
  ),
2296
2302
  keepAlive: mcpBool.optional().describe(
2297
- "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`."
2303
+ "Exempt this session from the idle-reaper \u2014 never auto-retired for sitting idle. Default false. Toggle later with `session_set_keepalive`."
2298
2304
  ),
2299
2305
  options: jsonTolerant(
2300
2306
  z.record(z.string(), z.union([z.boolean(), z.number(), z.string()]))
2301
2307
  ).optional().describe(
2302
- "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."
2308
+ "Manifest-declared option id \u2192 value map (AIP-45 `options`), validated against each option's declared type/enum/min/max. Unknown ids reject."
2303
2309
  ),
2304
2310
  skills: jsonTolerant(z.array(z.string())).optional().describe(
2305
- "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."
2311
+ `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")}`
2306
2312
  ),
2307
2313
  model: z.string().optional().describe(
2308
- "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."
2314
+ "Model identifier to pass to the adapter. For claude-code, applied via session/set_config_option \u2014 NOT a CLI flag."
2309
2315
  ),
2310
2316
  effort: z.string().optional().describe(
2311
- "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."
2317
+ "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."
2312
2318
  ),
2313
2319
  route: jsonTolerant(
2314
2320
  z.object({ gateway: z.string().min(1), baseUrl: z.string().url().optional() })
2315
- ).optional().describe("Billing route/gateway. This is independent of model and named access profile."),
2321
+ ).optional().describe("Billing route/gateway. Independent of `model` and `access`."),
2316
2322
  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."),
2317
2323
  posture: z.string().optional().describe("Canonical agent posture (plan, bypass, accept-edits, read-only) or native harness mode."),
2318
2324
  contextProfile: z.string().optional().describe("Context intake profile (for example full or lean)."),
@@ -2324,7 +2330,7 @@ var init_agent_start_schema = __esm({
2324
2330
  apiKey: z.string().optional()
2325
2331
  })
2326
2332
  ).optional().describe(
2327
- "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."
2333
+ `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")}`
2328
2334
  ),
2329
2335
  mcpServers: jsonTolerant(
2330
2336
  z.array(
@@ -2332,42 +2338,34 @@ var init_agent_start_schema = __esm({
2332
2338
  name: z.string(),
2333
2339
  transport: z.enum(["stdio", "http", "sse"]),
2334
2340
  ref: z.string().optional(),
2335
- headers: z.record(z.string(), z.string()).optional().describe(
2336
- "Static HTTP headers sent with every request to an `http` or `sse` MCP server (e.g. a fixed auth token). Ignored for `stdio` transports."
2337
- ),
2341
+ headers: z.record(z.string(), z.string()).optional().describe("Static HTTP headers for `http`/`sse` transports. Ignored for `stdio`."),
2338
2342
  credentialRef: z.string().optional().describe(
2339
- "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`."
2343
+ "Brokered credential resolved into `headers` at spawn time (wins on collision with `headers`); the secret never lives in env/config."
2340
2344
  ),
2341
- args: z.array(z.string()).optional().describe("`stdio` only: argv passed to the `ref` command. Ignored for `http`/`sse`."),
2342
- env: z.record(z.string(), z.string()).optional().describe("`stdio` only: extra environment for the launched server. Ignored for `http`/`sse`.")
2345
+ args: z.array(z.string()).optional().describe("`stdio` only: argv passed to the `ref` command."),
2346
+ env: z.record(z.string(), z.string()).optional().describe("`stdio` only: extra environment for the launched server.")
2343
2347
  })
2344
2348
  )
2345
2349
  ).optional().describe(
2346
- "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."
2350
+ "MCP servers to mount into the spawned agent's session at spawn time. Adapters that don't model MCP mounting ignore it."
2347
2351
  ),
2348
2352
  orchestrator: jsonTolerant(
2349
2353
  z.union([
2350
2354
  z.boolean(),
2351
2355
  z.object({
2352
- tools: z.array(z.string()).optional().describe(
2353
- "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."
2354
- ),
2355
- maxDepth: z.number().int().min(1).max(8).optional().describe(
2356
- "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."
2357
- ),
2358
- maxChildren: z.number().int().min(1).optional().describe(
2359
- "Max concurrently-alive sub-agents this child may spawn (default 8). For a recursive spawn it can only lower the inherited quota."
2360
- )
2356
+ tools: z.array(z.string()).optional().describe("Explicit allowlist \u2286 the default subset \u2014 a child can never widen its own scope."),
2357
+ 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)."),
2358
+ maxChildren: z.number().int().min(1).optional().describe("Max concurrently-alive sub-agents this child may spawn (default 8, lower-only on recursion).")
2361
2359
  })
2362
2360
  ])
2363
2361
  ).optional().describe(
2364
- "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."
2362
+ `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")}`
2365
2363
  ),
2366
2364
  notifyUrl: z.string().url().optional().describe(
2367
2365
  "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."
2368
2366
  ),
2369
2367
  wait: mcpBool.optional().describe(
2370
- "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`."
2368
+ `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.`
2371
2369
  ),
2372
2370
  maxCostUsd: mcpPositiveNumber.optional().describe(
2373
2371
  "Hard ceiling on cumulative session cost (USD). The session is stopped at a turn-end once exceeded."
@@ -2379,45 +2377,39 @@ var init_agent_start_schema = __esm({
2379
2377
  scope: z.enum(["session", "profile"]).describe("Spend surface: this session, or every session on its auth profile.")
2380
2378
  })
2381
2379
  ).optional().describe(
2382
- "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."
2380
+ "Windowed cost-budget cap (DISTINCT from `maxCostUsd`) \u2014 trips `policy:failed` on overage, never kills the session."
2383
2381
  ),
2384
2382
  restartPolicy: jsonTolerant(
2385
2383
  z.object({
2386
- on: z.array(z.enum(["crashed", "error"])).min(1).describe(
2387
- '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.'
2388
- ),
2389
- maxRetries: z.number().int().positive().describe(
2390
- "Rolling-window crash-loop cap: give up (leave the session dead, no further auto-restart) once this many restarts have fired within `windowMs`."
2391
- ),
2384
+ 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."),
2385
+ maxRetries: z.number().int().positive().describe("Rolling-window crash-loop cap: give up after this many restarts within `windowMs`."),
2392
2386
  windowMs: z.number().int().positive().describe("Rolling window (ms) `maxRetries` counts restarts over."),
2393
2387
  baseDelayMs: z.number().int().positive().describe("First restart's backoff delay (ms), before `factor` compounds it."),
2394
2388
  factor: z.number().positive().describe("Exponential backoff multiplier applied per consecutive restart."),
2395
2389
  maxDelayMs: z.number().int().positive().describe("Backoff ceiling (ms) \u2014 the computed delay never grows past this."),
2396
- resume: z.boolean().optional().describe(
2397
- "Reserved for a future explicit resume-vs-fresh-spawn toggle; this PR always revives in place (same session id, same conversation)."
2398
- )
2390
+ resume: z.boolean().optional().describe("Reserved for a future resume-vs-fresh-spawn toggle; this always revives in place today.")
2399
2391
  })
2400
2392
  ).optional().describe(
2401
- "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."
2393
+ `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")}`
2402
2394
  ),
2403
2395
  contextContinuity: jsonTolerant(contextContinuityInputSchema).optional().describe(
2404
2396
  "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."
2405
2397
  ),
2406
2398
  role: z.string().optional().describe(
2407
- "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)."
2399
+ `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")}`
2408
2400
  ),
2409
2401
  promptAppend: z.string().optional().describe(
2410
- "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)."
2402
+ "One-off runtime text layered onto the resolved role's disposition and prepended to `prompt` \u2014 specializes it, cannot reopen a denied tool gate."
2411
2403
  ),
2412
2404
  deferredTools: z.boolean().optional().describe(
2413
- "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."
2405
+ `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")}`
2414
2406
  ),
2415
2407
  browser: z.preprocess(
2416
2408
  // `true` is sugar for the only mode; stringified booleans tolerated.
2417
2409
  (v) => v === true || v === "true" ? "headless" : v === "false" ? false : v,
2418
2410
  z.union([z.literal("headless"), z.literal(false)])
2419
2411
  ).optional().describe(
2420
- '`"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`.'
2412
+ `\`"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")}`
2421
2413
  ),
2422
2414
  trace: z.boolean().optional().describe(
2423
2415
  "Emit Langfuse observability traces for this session (prompt/completion + tool spans + tokens/cost). Off by default; requires langfuse eval-reporter creds configured."
@@ -2426,22 +2418,22 @@ var init_agent_start_schema = __esm({
2426
2418
  z.union([
2427
2419
  z.string().min(1).describe("Sandbox provider slug from `list_sandbox_providers` (e.g. 'local', 'e2b')."),
2428
2420
  sandboxSpecWithReuseSchema.describe(
2429
- "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."
2421
+ "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."
2430
2422
  )
2431
2423
  ])
2432
2424
  ).optional().describe(
2433
- "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)."
2425
+ `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")}`
2434
2426
  ),
2435
2427
  appServe: jsonTolerant(
2436
2428
  z.object({
2437
2429
  dir: z.string().min(1).describe("Absolute path to the app's directory INSIDE the sandbox box (e.g. '/home/user/apps/<slug>')."),
2438
- 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.")
2430
+ 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.")
2439
2431
  }).strict()
2440
2432
  ).optional().describe(
2441
- "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`)."
2433
+ `Serve an agentproto app's UI from INSIDE the sandbox box and return its public URL. Requires \`sandbox\` (rejected otherwise). ${help("appServe")}`
2442
2434
  ),
2443
2435
  commandSandbox: commandSandboxSchema.optional().describe(
2444
- '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.'
2436
+ `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")}`
2445
2437
  ),
2446
2438
  worktree: jsonTolerant(
2447
2439
  z.union([
@@ -2450,17 +2442,15 @@ var init_agent_start_schema = __esm({
2450
2442
  slug: z.string().regex(
2451
2443
  /^[a-z0-9][a-z0-9-]*$/,
2452
2444
  "slug must be lowercase kebab-case (letters, digits, hyphens)"
2453
- ).optional().describe(
2454
- "Pin the worktree's slug (names its branch `wt/<slug>` and its directory). Omit to auto-mint a collision-free one from the label."
2455
- ),
2445
+ ).optional().describe("Pin the worktree's slug (branch `wt/<slug>`). Omit to auto-mint one from the label."),
2456
2446
  base: z.string().min(1).optional().describe("Git ref the worktree branch is cut from. Default 'origin/main'."),
2457
2447
  async: z.boolean().optional().describe(
2458
- '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`.'
2448
+ `Return a registered session immediately (status "starting") instead of blocking on provisioning; incompatible with \`wait\`. Defaults true whenever a worktree is provisioned. ${help("worktree")}`
2459
2449
  )
2460
2450
  }).strict()
2461
2451
  ])
2462
2452
  ).optional().describe(
2463
- "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`."
2453
+ `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")}`
2464
2454
  )
2465
2455
  };
2466
2456
  agentStartInputSchema = z.object(agentStartInputShape);
@@ -2504,8 +2494,20 @@ var init_catalog_provider_models = __esm({
2504
2494
  "src/catalog-provider-models.ts"() {
2505
2495
  }
2506
2496
  });
2497
+ var init_app_ui_build = __esm({
2498
+ "src/app-ui-build.ts"() {
2499
+ }
2500
+ });
2501
+
2502
+ // src/app-ui-placeholder.ts
2503
+ var init_app_ui_placeholder = __esm({
2504
+ "src/app-ui-placeholder.ts"() {
2505
+ }
2506
+ });
2507
2507
  var init_app_ui_apps = __esm({
2508
2508
  "src/app-ui-apps.ts"() {
2509
+ init_app_ui_build();
2510
+ init_app_ui_placeholder();
2509
2511
  }
2510
2512
  });
2511
2513
  var MESSAGE_KINDS, MESSAGE_URGENCIES;
@@ -2527,6 +2529,7 @@ var init_agent_tools = __esm({
2527
2529
  "src/agent-tools.ts"() {
2528
2530
  init_transcript_export();
2529
2531
  init_agent_start_schema();
2532
+ init_spawn_field_schemas();
2530
2533
  init_session_spawn();
2531
2534
  init_canonical_posture();
2532
2535
  init_user_presets();
@@ -2597,10 +2600,6 @@ var init_terminal_transcript_writer = __esm({
2597
2600
  init_transcript_writer();
2598
2601
  }
2599
2602
  });
2600
- var init_usage = __esm({
2601
- "src/usage.ts"() {
2602
- }
2603
- });
2604
2603
  var init_context_window = __esm({
2605
2604
  "src/context-window.ts"() {
2606
2605
  }
@@ -2765,6 +2764,7 @@ var init_config_schema = __esm({
2765
2764
  label: z.string().optional(),
2766
2765
  authToken: z.string().optional(),
2767
2766
  resumeSessionsOnBoot: z.boolean().optional(),
2767
+ continueInterruptedOnBoot: z.boolean().optional(),
2768
2768
  idleReapAfterMs: z.number().optional(),
2769
2769
  crashDetectIntervalMs: z.number().optional(),
2770
2770
  restartSweepIntervalMs: z.number().optional(),
@@ -2939,6 +2939,16 @@ var init_config_schema = __esm({
2939
2939
  help: "Eagerly re-spawn eligible agent-cli sessions that died from a daemon restart.",
2940
2940
  default: false
2941
2941
  },
2942
+ {
2943
+ path: "daemon.continueInterruptedOnBoot",
2944
+ schema: bool,
2945
+ apply: "restart",
2946
+ writable: true,
2947
+ section: "daemon",
2948
+ label: "Continue interrupted sessions on boot",
2949
+ help: 'After the eager resume pass, send a one-shot "continue" prompt to sessions the last restart cut off mid-turn.',
2950
+ default: false
2951
+ },
2942
2952
  {
2943
2953
  path: "daemon.idleReapAfterMs",
2944
2954
  schema: num,