@agentproto/runtime 3.6.1 → 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.
Files changed (39) hide show
  1. package/README.md +8 -3
  2. package/dist/app-ui-build.d.ts +96 -0
  3. package/dist/app-ui-build.mjs +250 -0
  4. package/dist/app-ui-build.mjs.map +1 -0
  5. package/dist/app-ui-delivery.d.ts +75 -0
  6. package/dist/app-ui-delivery.mjs +144 -0
  7. package/dist/app-ui-delivery.mjs.map +1 -0
  8. package/dist/app-ui-placeholder.d.ts +73 -0
  9. package/dist/app-ui-placeholder.mjs +89 -0
  10. package/dist/app-ui-placeholder.mjs.map +1 -0
  11. package/dist/catalog-models.d.ts +5 -3
  12. package/dist/catalog-models.mjs.map +1 -1
  13. package/dist/config-schema.d.ts +134 -0
  14. package/dist/config-schema.mjs +3697 -0
  15. package/dist/config-schema.mjs.map +1 -0
  16. package/dist/config.d.ts +22 -2
  17. package/dist/config.mjs +3615 -4
  18. package/dist/config.mjs.map +1 -1
  19. package/dist/{context-continuity-ib9_bVYM.d.ts → context-continuity-Bn99UtVC.d.ts} +40 -1
  20. package/dist/{heartbeat-COGpMrJS.d.ts → heartbeat-BJtxbDb9.d.ts} +13 -1
  21. package/dist/heartbeat.d.ts +1 -1
  22. package/dist/index.d.ts +3057 -151
  23. package/dist/index.mjs +17669 -7188
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/mcp-imports.d.ts +2 -1
  26. package/dist/resume-strategies.mjs +27 -1
  27. package/dist/resume-strategies.mjs.map +1 -1
  28. package/dist/{session-config-DbWP9RRj.d.ts → session-config-ByfVeEQJ.d.ts} +1 -1
  29. package/dist/session-presence.mjs +3613 -6
  30. package/dist/session-presence.mjs.map +1 -1
  31. package/dist/session-story.d.ts +10 -0
  32. package/dist/{spawn-defaults-CwI9Puwy.d.ts → spawn-defaults-CZj39Znq.d.ts} +146 -2
  33. package/dist/user-presets.d.ts +8 -2
  34. package/dist/user-presets.mjs +2 -1
  35. package/dist/user-presets.mjs.map +1 -1
  36. package/docs/TRANSMITTER.md +356 -0
  37. package/docs/mcp-tools/agent_start.md +476 -0
  38. package/docs/mcp-tools/app_run.md +47 -0
  39. package/package.json +47 -23
package/dist/index.d.ts CHANGED
@@ -1,35 +1,37 @@
1
1
  import { DoctypeSpec } from '@agentproto/manifest';
2
- import { B as BuildHeartbeatAgent } from './heartbeat-COGpMrJS.js';
3
- export { H as HeartbeatAgent, a as HeartbeatRunner, R as RuntimeEvent, b as RuntimeEvents, p as parseDuration } from './heartbeat-COGpMrJS.js';
2
+ import { B as BuildHeartbeatAgent } from './heartbeat-BJtxbDb9.js';
3
+ export { H as HeartbeatAgent, a as HeartbeatRunner, R as RuntimeEvent, b as RuntimeEvents, p as parseDuration } from './heartbeat-BJtxbDb9.js';
4
4
  import { z } from 'zod';
5
5
  import { AcpMcpServer, AcpPermissionResolution } from '@agentproto/acp';
6
6
  import { SessionMode } from '@agentproto/acp/client';
7
7
  export { SessionMode } from '@agentproto/acp/client';
8
8
  import { ChildProcess } from 'node:child_process';
9
- import { R as RouteSpec, S as SessionConfig, P as Posture, C as ContextProfile, E as EffortLevel, A as AuthMethod, a as CanonicalPosture } from './session-config-DbWP9RRj.js';
10
- export { D as DeclaredAdapterMode, c as composeMode, d as decomposeMode } from './session-config-DbWP9RRj.js';
9
+ import { R as RouteSpec, S as SessionConfig, P as Posture, C as ContextProfile, E as EffortLevel, A as AuthMethod, a as CanonicalPosture } from './session-config-ByfVeEQJ.js';
10
+ export { D as DeclaredAdapterMode, c as composeMode, d as decomposeMode } from './session-config-ByfVeEQJ.js';
11
11
  import { CostBudget, AuthProfile, CostBudgetScope } from '@agentproto/auth';
12
12
  export { CostBudget, CostBudgetScope } from '@agentproto/auth';
13
- import { R as ResolvedContextContinuityPolicy } from './context-continuity-ib9_bVYM.js';
13
+ import { S as SpawnBrowserMode, R as ResolveHeadlessBrowser, C as ContextContinuityPolicy, a as ResolvedContextContinuityPolicy } from './context-continuity-Bn99UtVC.js';
14
14
  import { SandboxMode } from '@agentproto/command-sandbox';
15
15
  import { AdapterHandle, SetupField, AdapterResolver, AdapterLister, CredsStore, HarnessCapabilities, AdapterEntry } from '@agentproto/provider-kit';
16
- import { R as ResolvedAuthSpec, D as DeclaredAdapterOption, A as AdapterAuthDescriptor } from './spawn-defaults-CwI9Puwy.js';
17
- export { a as AuthEcho, b as AuthResolutionError, C as CLAUDE_CODE_OAUTH_SOURCE, c as CredentialSource, d as DefaultsAdapterAuthConfig, e as DefaultsAdapterConfig, f as ResolveSubscriptionCredentialInput, g as ResolvedSpawnAuthMaterial, h as ResolvedSpawnDefaults, S as SpawnDefaultsConfig, i as SubscriptionCredentialResolution, j as SubscriptionSourceError, k as credentialFingerprint, n as normalizeSkillsOption, r as resolveAuthSpec, l as resolveSpawnDefaults, m as resolveSubscriptionCredential } from './spawn-defaults-CwI9Puwy.js';
16
+ import { S as SpawnDefaultsConfig, D as DefaultsAdapterAuthConfig, R as ResolvedAuthSpec, a as DeclaredAdapterOption, A as AdapterAuthDescriptor } from './spawn-defaults-CZj39Znq.js';
17
+ export { b as AuthEcho, c as AuthResolutionError, C as CLAUDE_CODE_OAUTH_SOURCE, d as CredentialSource, e as DefaultsAdapterConfig, f as DeferredToolsConfig, g as ResolveSubscriptionCredentialInput, h as ResolvedSpawnAuthMaterial, i as ResolvedSpawnDefaults, j as SubscriptionCredentialResolution, k as SubscriptionSourceError, l as credentialFingerprint, n as normalizeSkillsOption, r as resolveAuthSpec, m as resolveDeferredToolsGatewayOption, o as resolveSpawnDefaults, p as resolveSubscriptionCredential } from './spawn-defaults-CZj39Znq.js';
18
18
  import { CatalogModelsQuery, CatalogModelsResponse } from './catalog-models.js';
19
19
  export { CatalogAdapterInput, CatalogAdapterModelInput, CatalogPricing, CatalogProduct, CatalogRoute, CatalogRouteSummary, CatalogVendor, buildCatalogModels } from './catalog-models.js';
20
20
  import { FooterSession } from './pr-provenance.js';
21
21
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
- import { SandboxProvider } from '@agentproto/sandbox';
22
+ import { SandboxProvider, SandboxSpec } from '@agentproto/sandbox';
23
23
  import { BrainManager, ExportedSessionLike } from '@agentproto/workspace-brain';
24
24
  export { BrainManager, BrainStats, IngestReport, IngestResult } from '@agentproto/workspace-brain';
25
25
  import * as _agentproto_tool from '@agentproto/tool';
26
26
  import { ToolContext } from '@agentproto/tool';
27
27
  import { DriverHandle } from '@agentproto/driver';
28
- export { UserPreset, UserPresetsFile, deleteUserPreset, getUserPreset, listUserPresets, loadUserPresets, saveUserPreset, userPresetsPath } from './user-presets.js';
28
+ import { Attestation, LedgerKey, RubricDigest, ReviewPrRef, LaneResult, ReviewLaneExecutor, LaneOutcome } from '@agentproto/review';
29
+ import { UserPreset } from './user-presets.js';
30
+ export { UserPresetsFile, deleteUserPreset, getUserPreset, listUserPresets, loadUserPresets, saveUserPreset, userPresetsPath } from './user-presets.js';
31
+ import { WorktreeIsolationMode, SpawnAttachMode, SpawnDedupeMode } from './config.js';
29
32
  import { ResolvedModel } from '@agentproto/model-catalog';
30
33
  import { FrameSink, E2eFrameSink } from '@agentproto/acp/tunnel';
31
34
  import { DaemonIdentity } from '@agentproto/secrets/identity';
32
- import { WorktreeIsolationMode } from './config.js';
33
35
  import { WorkspaceFs } from './workspace-fs.js';
34
36
  export { createWorkspaceFs } from './workspace-fs.js';
35
37
  export { ConversationMeta, ConversationStore, ConversationTurn, fileConversationStore } from './conversations.js';
@@ -449,13 +451,64 @@ type UsageSource = "adapter" | "computed" | "no-pricing" | "none";
449
451
  interface TokenPricing {
450
452
  inputPer1M: number;
451
453
  outputPer1M: number;
454
+ /** Multiplier on `inputPer1M` for cache-read tokens (Anthropic: ~0.1).
455
+ * Absent = 1 (no discount), same default as the catalog. */
456
+ cacheReadMultiplier?: number;
457
+ /** Multiplier on `inputPer1M` for cache-creation tokens (Anthropic:
458
+ * ~1.25). Absent = 1. */
459
+ cacheWriteMultiplier?: number;
460
+ }
461
+ /**
462
+ * Token-detail and activity fields shared by every usage shape below. All
463
+ * optional and never zero-filled: absent means "this source doesn't report
464
+ * it", a present 0 is a measured zero.
465
+ *
466
+ * - `cacheReadTokens` / `cacheWriteTokens`: prompt-cache reads / cache
467
+ * creation, cumulative. Kept OUT of `tokensIn` (which is uncached input
468
+ * only when a cache split is known).
469
+ * - `reasoningTokens`: thinking/reasoning tokens, cumulative. A subset of
470
+ * `tokensOut` for providers that bill them as output (Anthropic).
471
+ * - `turns`: completed turns (the descriptor's `turnsCompleted`).
472
+ * - `toolCalls`: tool calls the daemon observed, cumulative.
473
+ * - `durationMs`: wall time spent inside turns, cumulative.
474
+ */
475
+ interface UsageDetail {
476
+ cacheReadTokens?: number;
477
+ cacheWriteTokens?: number;
478
+ reasoningTokens?: number;
479
+ turns?: number;
480
+ toolCalls?: number;
481
+ durationMs?: number;
452
482
  }
483
+ /** Copy only the present (`number`) `UsageDetail` fields of `src`. */
484
+ declare function pickUsageDetail(src: UsageDetail): UsageDetail;
453
485
  /** Pluggable pricing accessor — defaults to the in-repo catalog's
454
486
  * `resolvePricing`, overridable in tests. Returns undefined for an
455
487
  * unknown model (the caller then tags `no-pricing`). */
456
488
  type PricingResolver = (model: string) => TokenPricing | undefined;
489
+ /**
490
+ * What an adapter's usage reader (`readUsage`: hermes state.db, opencode.db,
491
+ * the claude-code transcript JSONL) returns. Every field is cumulative for
492
+ * the session and optional; `null` from the reader means "nothing found".
493
+ */
494
+ interface UsageReadResult {
495
+ model?: string;
496
+ costUsd?: number;
497
+ tokensIn?: number;
498
+ tokensOut?: number;
499
+ cacheReadTokens?: number;
500
+ cacheWriteTokens?: number;
501
+ reasoningTokens?: number;
502
+ }
503
+ /** Where a reader should look for the session's native store — the spawn's
504
+ * cwd and isolated provider config dir (`CLAUDE_CONFIG_DIR` for
505
+ * claude-code). Readers that key only on the session id ignore it. */
506
+ interface UsageReadContext {
507
+ cwd?: string;
508
+ configDir?: string;
509
+ }
457
510
  /** Raw signals gathered over a turn, handed to `deriveSessionUsage`. */
458
- interface UsageComputeInput {
511
+ interface UsageComputeInput extends UsageDetail {
459
512
  /** Requested model id — needed to look up per-token prices. */
460
513
  model?: string;
461
514
  /** Cost the adapter reported directly (readUsage or usage_update.cost).
@@ -469,7 +522,7 @@ interface UsageComputeInput {
469
522
  /** The resolved usage snapshot — the shape `session_usage` returns and the
470
523
  * durable `usage_snapshot` transcript record carries. Cost/token fields are
471
524
  * omitted when absent so a missing value never reads as a measured zero. */
472
- interface SessionUsage {
525
+ interface SessionUsage extends UsageDetail {
473
526
  model?: string;
474
527
  costUsd?: number;
475
528
  tokensIn?: number;
@@ -486,7 +539,7 @@ interface SessionUsage {
486
539
  */
487
540
  declare function deriveSessionUsage(input: UsageComputeInput, resolve?: PricingResolver): SessionUsage;
488
541
  /** Descriptor-shaped fields `projectSessionUsage` reads. */
489
- interface UsageDescriptorFields {
542
+ interface UsageDescriptorFields extends Omit<UsageDetail, "turns"> {
490
543
  model?: string;
491
544
  costUsd?: number;
492
545
  tokensIn?: number;
@@ -494,6 +547,8 @@ interface UsageDescriptorFields {
494
547
  contextSize?: number;
495
548
  contextUsed?: number;
496
549
  usageSource?: UsageSource;
550
+ /** Surfaced as `turns`. */
551
+ turnsCompleted?: number;
497
552
  }
498
553
  /**
499
554
  * Project a session descriptor's persisted usage fields into the
@@ -502,6 +557,37 @@ interface UsageDescriptorFields {
502
557
  */
503
558
  declare function projectSessionUsage(desc: UsageDescriptorFields): SessionUsage;
504
559
 
560
+ /**
561
+ * McpAppsHostService — the daemon as the MCP client that fetches MCP App
562
+ * UIs (spec 2026-01-26) on behalf of a session's chat UI.
563
+ *
564
+ * In a session the MCP client is the harness (claude-code, codex, …); it
565
+ * keeps a tool's `_meta.ui.resourceUri` and the `ui://` resource to itself,
566
+ * and the transcript only carries `mcp__<server>__<tool>` + input + result.
567
+ * So the chat asks the daemon, which resolves `<server>` for that session
568
+ * (mcp-app-resolve.ts), connects through a config-keyed pool
569
+ * (mcp-client-pool.ts), and answers three questions:
570
+ *
571
+ * uiIndex which of this server's tools declare a UI (+ app-only tools)
572
+ * readUi the HTML + `_meta.ui` of one of those `ui://` resources
573
+ * callTool an iframe-initiated `tools/call`, allowlisted and recorded
574
+ *
575
+ * None of these throw: every failure is a `status` (or an `isError`
576
+ * result). Failures are cached per (session, alias) for 60s so a
577
+ * transcript full of calls to a dead server costs one attempt a minute.
578
+ */
579
+
580
+ /** Durable record of an iframe-initiated call, appended to the session's
581
+ * events.jsonl as `kind: "mcp_app_tool_call"`. Never carries args or
582
+ * results. */
583
+ interface McpAppToolCallRecord {
584
+ server: string;
585
+ tool: string;
586
+ originToolCallId: string;
587
+ isError: boolean;
588
+ durationMs: number;
589
+ }
590
+
505
591
  /**
506
592
  * ONE normalized shape for a finished tool/command call — the unification
507
593
  * target for the two disjoint logs this repo has today: `recordCommand`'s
@@ -1685,6 +1771,41 @@ declare function createTaskLedger(opts: {
1685
1771
  persist?: boolean;
1686
1772
  }): TaskLedger;
1687
1773
 
1774
+ /**
1775
+ * The single place every stable `SessionDescriptor.endedReason` /
1776
+ * `SessionOutcome.termination.reason` value is declared, so a UI can render
1777
+ * a label for each one without chasing the call site that set it.
1778
+ *
1779
+ * Two families:
1780
+ * - operator-issued — set ONLY by an explicit `POST /sessions/:id/kill`
1781
+ * or the `agent_kill` MCP tool, distinguishing an operator's deliberate
1782
+ * stop from every automatic teardown below;
1783
+ * - internal/automatic — set by the daemon itself, without an operator
1784
+ * in the loop, for a reason the operator couldn't have expressed.
1785
+ *
1786
+ * Backward compatible by construction: this is a value list, not a closed
1787
+ * validator. A descriptor persisted by an older daemon (or a newer one an
1788
+ * older UI doesn't know about yet) carries a `reason` string outside this
1789
+ * list — {@link isKnownSessionEndReason} says so, and callers should fall
1790
+ * back to a generic label rather than throw.
1791
+ */
1792
+ declare const SESSION_END_REASONS: readonly ["daemon-restart", "idle-reaped", "crashed", "operator-completed", "operator-stopped", "cost-cap-exceeded", "policy-cleanup", "parent-exited", "provider-limit", "forgotten"];
1793
+ type SessionEndReason = (typeof SESSION_END_REASONS)[number];
1794
+ /** True when `reason` is one of {@link SESSION_END_REASONS}. A label
1795
+ * lookup, not a wire-format gate — an unrecognized string is still a
1796
+ * valid `endedReason` from a daemon version this build doesn't know
1797
+ * about; callers should render it with a generic fallback, never reject
1798
+ * it. */
1799
+ declare function isKnownSessionEndReason(reason: string | undefined): reason is SessionEndReason;
1800
+ /**
1801
+ * Classify a driver-reported error message as a provider/subscription
1802
+ * usage cap rather than an ordinary tool/turn failure. Used wherever the
1803
+ * daemon would otherwise tag a dead session with the generic `"crashed"` /
1804
+ * `"error"` reason, so an operator sees "wait for your quota to reset"
1805
+ * instead of chasing a phantom bug.
1806
+ */
1807
+ declare function isProviderLimitError(message: string | undefined): boolean;
1808
+
1688
1809
  /**
1689
1810
  * In-process pub/sub bus for session lifecycle events. Separate from
1690
1811
  * RuntimeEvents (global daemon bus) — session events are scoped to
@@ -1695,7 +1816,7 @@ declare function createTaskLedger(opts: {
1695
1816
  * and session_monitor MCP tool (long-poll multiplexed).
1696
1817
  */
1697
1818
 
1698
- type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:awaiting-input-flagged" | "session:awaiting-question-answered" | "session:permission-request" | "session:permission-resolved" | "session:exited" | "session:reaped" | "session:stalled" | "session:stall-cleared" | "session:watcher-attached" | "session:watcher-detached" | "session:bg-tasks-parked" | "session:bg-tasks-cleared" | "session:resumed" | "session:spawned" | "session:command-done" | "session:model-changed" | "session:config-changed" | "session:renamed" | "session:pinned-changed" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed" | "cron:fired" | "cron:succeeded" | "cron:failed" | "activity:changed" | "task:changed" | "workflow:gate-report" | "workflow:suspended" | "workflow:suspend-resumed" | "session:harness-warning";
1819
+ type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:awaiting-input-flagged" | "session:awaiting-question-answered" | "session:permission-request" | "session:permission-resolved" | "session:exited" | "session:reaped" | "session:stalled" | "session:stall-cleared" | "session:watcher-attached" | "session:watcher-detached" | "session:bg-tasks-parked" | "session:bg-tasks-cleared" | "session:bg-task" | "session:resumed" | "session:spawned" | "session:command-done" | "session:model-changed" | "session:config-changed" | "session:renamed" | "session:pinned-changed" | "session:message" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed" | "cron:fired" | "cron:succeeded" | "cron:failed" | "activity:changed" | "task:changed" | "workflow:gate-report" | "workflow:suspended" | "workflow:suspend-resumed" | "session:harness-warning";
1699
1820
  /**
1700
1821
  * Fixed severity vocabulary for a judge-gate finding (WP-D). Deliberately
1701
1822
  * small and fixed so `policy_status` output is comparable across different
@@ -1769,6 +1890,25 @@ interface SessionTurnEndEvent {
1769
1890
  * a normal, productive turn.
1770
1891
  */
1771
1892
  empty?: boolean;
1893
+ /**
1894
+ * The in-band `error` stream event's message text observed during this
1895
+ * turn, when one occurred (see `SessionDescriptor.lastTurnErrorMessage`'s
1896
+ * doc). Present whenever `reason` is `"error"` AND the adapter emitted an
1897
+ * `error`-kind event before ending the stream — including the case where
1898
+ * the generator simply returned with no explicit turn-end and no thrown
1899
+ * exception, which is exactly what left an errored turn indistinguishable
1900
+ * from a clean one before this field existed. Absent when the adapter
1901
+ * reported `reason:"error"` on its own turn-end without a preceding
1902
+ * `error` event, and absent (not empty string) on every non-error turn.
1903
+ */
1904
+ error?: string;
1905
+ /**
1906
+ * True when the turn was not started by a prompt: the agent woke on its
1907
+ * own (Claude Code's task-notification cycle after a background task
1908
+ * settled) and the registry tracked that work as a turn. Absent on an
1909
+ * ordinary prompted turn.
1910
+ */
1911
+ autonomous?: boolean;
1772
1912
  }
1773
1913
  interface SessionAwaitingInputEvent {
1774
1914
  type: "session:awaiting-input";
@@ -1865,15 +2005,14 @@ interface SessionExitedEvent {
1865
2005
  status: "exited" | "killed" | "error";
1866
2006
  label?: string;
1867
2007
  ts: string;
1868
- /** Mirrors `SessionDescriptor.endedReason` — set when this exit was NOT an
1869
- * operator targeting the session: `"daemon-restart"` (the daemon dying
1870
- * underneath it — crash-discovered-at-boot or a forced shutdown kill) or
1871
- * `"idle-reaped"` (the idle-session reaper retiring a long-idle row to free
1872
- * the adapter process, PR-6), or `"crashed"` (the crash-detect sweep found
1873
- * the adapter's OS process gone between turns). Lets a watcher
1874
- * (completion-policy supervisor, `session_monitor`) tell an automatic
1875
- * teardown apart from a deliberate kill. Absent otherwise. */
1876
- reason?: "daemon-restart" | "idle-reaped" | "crashed";
2008
+ /** Mirrors `SessionDescriptor.endedReason` — see {@link SessionEndReason}
2009
+ * for the full, single-source enum (automatic teardowns like
2010
+ * `"daemon-restart"` / `"idle-reaped"` / `"crashed"` / `"cost-cap-exceeded"`
2011
+ * / `"provider-limit"`, and operator-issued `"operator-completed"` /
2012
+ * `"operator-stopped"`). Lets a watcher (completion-policy supervisor,
2013
+ * `session_monitor`) tell WHY this exit happened rather than just that it
2014
+ * did. Absent for a plain natural exit or an ordinary turn error. */
2015
+ reason?: SessionEndReason;
1877
2016
  }
1878
2017
  /**
1879
2018
  * Emitted when the idle-session reaper (PR-6) retires a long-idle agent-cli
@@ -2024,6 +2163,29 @@ interface SessionBgTasksClearedEvent {
2024
2163
  label?: string;
2025
2164
  ts: string;
2026
2165
  }
2166
+ /**
2167
+ * One lifecycle edge of an agent's background task (a backgrounded Bash
2168
+ * command, a monitor, ...) as the agent itself reported it — over ACP, the
2169
+ * AIR `asyncTasks` extension (see @agentproto/acp's `background-task`
2170
+ * StreamEvent). Unlike {@link SessionBgTasksParkedEvent} (a turn-end
2171
+ * heuristic) this is the real lifecycle: `"started"` when the task is
2172
+ * announced, `"settled"` when it reaches a terminal `status`. Mid-life
2173
+ * progress updates only refresh `SessionDescriptor.backgroundTasks` and are
2174
+ * not emitted. Same bus distribution as every other lifecycle event.
2175
+ */
2176
+ interface SessionBgTaskEvent {
2177
+ type: "session:bg-task";
2178
+ sessionId: string;
2179
+ phase: "started" | "settled";
2180
+ taskId: string;
2181
+ taskKind?: string;
2182
+ description?: string;
2183
+ outputFile?: string;
2184
+ status?: string;
2185
+ summary?: string;
2186
+ label?: string;
2187
+ ts: string;
2188
+ }
2027
2189
  /**
2028
2190
  * Emitted when a dead agent-cli row is brought back IN PLACE — same session
2029
2191
  * id, same descriptor — by the lazy resume-on-prompt path (`maybeResumeAgent`)
@@ -2163,6 +2325,27 @@ interface SessionRenamedEvent {
2163
2325
  renamedByUser?: boolean;
2164
2326
  ts: string;
2165
2327
  }
2328
+ /**
2329
+ * A typed inter-session message (`session-message.ts`) was accepted for
2330
+ * `sessionId` (the RECIPIENT) — once at send (no `delivered`) and again when
2331
+ * it's delivered into the recipient's context (`delivered` set). Lets
2332
+ * `session_monitor` / SSE / the VS Code panel react without polling.
2333
+ */
2334
+ interface SessionMessageEvent {
2335
+ type: "session:message";
2336
+ sessionId: string;
2337
+ messageId: string;
2338
+ fromSessionId?: string;
2339
+ relation: "child" | "parent" | "sibling" | "human" | "system";
2340
+ kind: "report" | "question" | "blocker" | "done" | "notice";
2341
+ urgency: "fyi" | "next-turn" | "steer" | "interrupt";
2342
+ delivered?: {
2343
+ via: "wait" | "steer" | "turn" | "interrupt" | "inbox";
2344
+ at: string;
2345
+ turnSeq?: number;
2346
+ };
2347
+ ts: string;
2348
+ }
2166
2349
  /**
2167
2350
  * Emitted when an operator pins or unpins a session for list visibility
2168
2351
  * (`POST /sessions/:id/pin`, the `session_set_pinned` MCP verb). Like
@@ -2356,16 +2539,22 @@ interface WorkflowApprovalResolvedEvent {
2356
2539
  }
2357
2540
  /**
2358
2541
  * Emitted by the workflow runner (`workflow-runner.ts`) when a `kind:
2359
- * "suspend"` step parks its run awaiting an external event — the run's
2360
- * status flips to `awaiting-input` with a durable `awaitingSuspend` record
2361
- * (AIP-15 conformance rule 7), and `workflow_escalation_resolve`'s suspend
2362
- * form resumes it. Same bus distribution as every other lifecycle event.
2542
+ * "suspend"` step parks its run awaiting an external event (`on` set, no
2543
+ * `reason`) — OR when an agent-backed step signals AIP-58 §3(a)
2544
+ * `run.requestInput` (`reason: "input-required"` + `prompt`/`schema?`, no
2545
+ * `on`). Either way the run's status flips to `awaiting-input` with a
2546
+ * durable `awaitingSuspend` record, and `workflow_escalation_resolve`'s
2547
+ * suspend form resumes it. Same bus distribution as every other lifecycle
2548
+ * event.
2363
2549
  */
2364
2550
  interface WorkflowSuspendedEvent {
2365
2551
  type: "workflow:suspended";
2366
2552
  runId: string;
2367
2553
  stepId: string;
2368
- on: readonly string[];
2554
+ on?: readonly string[];
2555
+ reason?: "input-required";
2556
+ prompt?: string;
2557
+ schema?: Record<string, unknown>;
2369
2558
  ts: string;
2370
2559
  }
2371
2560
  /**
@@ -2393,7 +2582,7 @@ interface SessionHarnessWarningEvent {
2393
2582
  label?: string;
2394
2583
  ts: string;
2395
2584
  }
2396
- type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionAwaitingInputFlaggedEvent | SessionAwaitingQuestionAnsweredEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionReapedEvent | SessionStalledEvent | SessionStallClearedEvent | SessionWatcherAttachedEvent | SessionWatcherDetachedEvent | SessionBgTasksParkedEvent | SessionBgTasksClearedEvent | SessionResumedEvent | SessionSpawnedEvent | SessionCommandDoneEvent | SessionModelChangedEvent | SessionConfigChangedEvent | SessionRenamedEvent | SessionPinnedEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent | ActivityChangedEvent | TaskChangedEvent | WorkflowGateReportEvent | WorkflowApprovalRequestedEvent | WorkflowApprovalResolvedEvent | WorkflowSuspendedEvent | WorkflowSuspendResumedEvent | SessionHarnessWarningEvent;
2585
+ type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionAwaitingInputFlaggedEvent | SessionAwaitingQuestionAnsweredEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionReapedEvent | SessionStalledEvent | SessionStallClearedEvent | SessionWatcherAttachedEvent | SessionWatcherDetachedEvent | SessionBgTasksParkedEvent | SessionBgTasksClearedEvent | SessionBgTaskEvent | SessionResumedEvent | SessionSpawnedEvent | SessionCommandDoneEvent | SessionModelChangedEvent | SessionConfigChangedEvent | SessionRenamedEvent | SessionPinnedEvent | SessionMessageEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent | ActivityChangedEvent | TaskChangedEvent | WorkflowGateReportEvent | WorkflowApprovalRequestedEvent | WorkflowApprovalResolvedEvent | WorkflowSuspendedEvent | WorkflowSuspendResumedEvent | SessionHarnessWarningEvent;
2397
2586
  interface SessionEventBus {
2398
2587
  emit(ev: SessionEvent): void;
2399
2588
  /** Subscribe to a specific event type. Returns an unsubscribe fn. */
@@ -2639,50 +2828,276 @@ declare class TunnelRegistry {
2639
2828
  private loadFromDisk;
2640
2829
  }
2641
2830
 
2831
+ /**
2832
+ * LlmEndpointRegistry — supervised-sidecar manager for the
2833
+ * `@agentproto/llm-endpoint` proxy gateway.
2834
+ *
2835
+ * The daemon spawns the llm-endpoint bin as a CHILD PROCESS (never an
2836
+ * in-process import — the runtime package has zero dependency on
2837
+ * @agentproto/llm-endpoint, exactly like it never imports cloudflared).
2838
+ * This is the single-sidecar analog of {@link TunnelRegistry}: one endpoint,
2839
+ * one port, an in-memory descriptor that tracks pid / port / status /
2840
+ * startedAt, and a start / stop / status lifecycle exposed over MCP.
2841
+ *
2842
+ * Lifecycle (mirrors RemoteController's singleton style — created fresh each
2843
+ * boot, state in memory; the child dies with the daemon so there is nothing
2844
+ * to reconcile on restart):
2845
+ * start(input) → assemble env, spawn `node <bin> serve`, health-probe to
2846
+ * readiness, store descriptor. Idempotent: a call while the
2847
+ * endpoint is already running+healthy returns the existing
2848
+ * descriptor without spawning (like `start_browser`).
2849
+ * status() → descriptor + live `GET /v1/models` health probe.
2850
+ * stop() → SIGTERM the child, mark stopped (mirrors tunnel stop).
2851
+ * shutdown() → stop the child on daemon exit.
2852
+ *
2853
+ * Env on spawn (see {@link assembleLlmEndpointEnv}): the provider API keys
2854
+ * via `injectProviderKeysIntoEnv` (the same store llm-endpoint's own cli.ts
2855
+ * reads), `LLM_ENDPOINT_PORT` (default 18090), and `LLM_ENDPOINT_ACCESS_TOKENS`
2856
+ * when configured. Explicit env passed to `start` always wins.
2857
+ */
2858
+ type LlmEndpointStatus = "starting" | "running" | "stopped" | "error";
2859
+ /** The `llm_endpoint_status` MCP contract — see {@link LlmEndpointRegistry.status}. */
2860
+ interface LlmEndpointStatusReport {
2861
+ running: boolean;
2862
+ pid: number | null;
2863
+ port: number | null;
2864
+ baseUrl: string | null;
2865
+ healthy: boolean;
2866
+ startedAt: string | null;
2867
+ status: LlmEndpointStatus | "never-started";
2868
+ /** Who is actually serving this endpoint. `"daemon"` — this registry's own
2869
+ * spawn (or nothing at all). `"external"` — a healthy process on the same
2870
+ * port that this registry did NOT spawn (e.g. a launchd KeepAlive
2871
+ * sidecar); `pid` is unknown (always `null`) and `injectedProviders`, if
2872
+ * present, was DERIVED by probing `/v1/models` rather than read off our
2873
+ * own key-injection bookkeeping. */
2874
+ owner: "daemon" | "external";
2875
+ /** Whether upstream links persisted via `llm_endpoint_set_upstream_link`
2876
+ * are actually applied to the process answering this port. Always
2877
+ * `false` for `owner:"external"` — this registry never spawned it, so it
2878
+ * never injected `LLM_ENDPOINT_PROFILE_<UPSTREAM>`; the operator must
2879
+ * configure the external service's own env directly. */
2880
+ linksApplied: boolean;
2881
+ lastError?: string;
2882
+ injectedProviders?: string[];
2883
+ linkedProviders?: string[];
2884
+ /** `@agentproto/llm-endpoint`'s own package version, read from the
2885
+ * resolved bin's `package.json` — best-effort, present only for
2886
+ * `owner:"daemon"` (the version of the code this registry would spawn /
2887
+ * did spawn). Undefined when unresolvable, or for `owner:"external"`
2888
+ * (an adopted process exposes no version of its own to probe). */
2889
+ version?: string;
2890
+ }
2891
+ interface LlmEndpointDescriptor {
2892
+ pid: number | null;
2893
+ port: number;
2894
+ baseUrl: string;
2895
+ status: LlmEndpointStatus;
2896
+ startedAt: string;
2897
+ stoppedAt?: string;
2898
+ lastError?: string;
2899
+ /** Provider names whose keys were injected into the child's env (never values). */
2900
+ injectedProviders?: string[];
2901
+ /** Upstream names mapped to an auth-profile link (`LLM_ENDPOINT_PROFILE_<P>`)
2902
+ * in the child's env. Profile IDS, not secrets — but only the provider names
2903
+ * are surfaced here, mirroring `injectedProviders`. */
2904
+ linkedProviders?: string[];
2905
+ /**
2906
+ * Only populated on the value returned by {@link LlmEndpointRegistry.start}
2907
+ * (never stored on the in-memory descriptor): `true` when the call reused an
2908
+ * already-running/starting child instead of spawning — so the caller can
2909
+ * tell a fresh `start` (new port/tokens applied) from a no-op idempotent hit
2910
+ * (old descriptor returned, nothing new applied). Mirrors `start_browser`.
2911
+ */
2912
+ wasAlreadyRunning?: boolean;
2913
+ }
2914
+
2915
+ /**
2916
+ * RemoteController — owns the gateway's "publish to the internet" state.
2917
+ *
2918
+ * One singleton per gateway. Holds:
2919
+ * - the current tunnel provider's child process (cloudflared, …)
2920
+ * - the bearer token gating /mcp once the tunnel is up
2921
+ * - persisted state in `<workspace>/.agentproto/remote.json`
2922
+ *
2923
+ * The controller is the *only* component that mutates auth at runtime.
2924
+ * The HTTP server reads auth via a getter (see `AuthSource` in
2925
+ * http-server.ts), so flipping `enable() / disable()` here takes effect
2926
+ * on the next request — no restart, no race.
2927
+ *
2928
+ * ## Provider model
2929
+ *
2930
+ * Each provider implements `RemoteProvider` (start / stop / status).
2931
+ * v1 ships only `quick` (Cloudflare Quick Tunnel — no API key, no DNS,
2932
+ * ephemeral `*.trycloudflare.com` URL). Named CF Tunnel and Tailscale
2933
+ * land as additional implementations.
2934
+ *
2935
+ * ## Token model
2936
+ *
2937
+ * On enable we generate a 32-byte random bearer token, return it once
2938
+ * to the caller, and persist only its sha256 hash to disk. Same model
2939
+ * as a Stripe API key — losing the plaintext means rotating, not
2940
+ * recovering. This keeps `.agentproto/remote.json` safe to commit by
2941
+ * accident (it leaks the URL, not the credential).
2942
+ *
2943
+ * ## Security note
2944
+ *
2945
+ * The CF API token (when used by named-tunnel provider) is *never*
2946
+ * accepted as MCP tool input. The controller reads it from
2947
+ * `~/.config/agentproto/cloudflare.json` or `$CLOUDFLARE_API_TOKEN`.
2948
+ * Otherwise a remote agent could exfiltrate it through `remote_enable`.
2949
+ */
2950
+
2951
+ interface EnableInput {
2952
+ provider?: "quick";
2953
+ /**
2954
+ * Local port to expose. Defaults to the gateway's own port — i.e.
2955
+ * "publish this MCP server." Pass any other port to expose a
2956
+ * different local service (Supabase 54371, a Vite dev server, …)
2957
+ * without spinning up a separate tunnel manager.
2958
+ */
2959
+ targetPort?: number;
2960
+ /**
2961
+ * Host the tunnel forwards to. Defaults to `127.0.0.1`. Setting it
2962
+ * to `localhost` resolves through DNS (IPv6 first on most systems);
2963
+ * keep `127.0.0.1` unless the target is explicitly IPv6-bound.
2964
+ */
2965
+ targetHost?: string;
2966
+ }
2967
+ interface EnableResult {
2968
+ publicUrl: string;
2969
+ /**
2970
+ * Bearer token issued by this gateway. Only meaningful when the
2971
+ * tunnel exposes the gateway itself (default). For arbitrary-target
2972
+ * tunnels (`targetPort != gateway.port`) the daemon does not gate
2973
+ * the proxied service, and this is omitted.
2974
+ */
2975
+ bearerToken?: string;
2976
+ /**
2977
+ * `<publicUrl>/mcp`. Only emitted when exposing the gateway.
2978
+ */
2979
+ mcpEndpoint?: string;
2980
+ /**
2981
+ * Paste-ready `.mcp.json` snippet. Only emitted when exposing the
2982
+ * gateway.
2983
+ */
2984
+ mcpConfigSnippet?: string;
2985
+ /**
2986
+ * A single link to open on a phone (PHONE-PLAN.md P1.2) — the token rides
2987
+ * in a URL *fragment* (`#token=`), never a `?` query string, since
2988
+ * fragments never reach a server (no Referer, no access/proxy log, no CDN
2989
+ * cache key). Two shapes, chosen by whether this daemon has the
2990
+ * `@agentik/session-chat` app installed with a UI:
2991
+ * - installed: `<publicUrl>/apps/@agentik/session-chat/ui#token=<t>` —
2992
+ * the phone lands directly in Control Center.
2993
+ * - not installed: `https://cli.agentproto.sh/panel#daemon=<publicUrl>&token=<t>`
2994
+ * — the hosted panel, which speaks the same fragment shape (see
2995
+ * agentproto/cli-site's `readFragmentConnection`).
2996
+ * Only emitted when exposing the gateway (same condition as `bearerToken`)
2997
+ * — a passthrough tunnel has no daemon UI to link to.
2998
+ */
2999
+ phoneUrl?: string;
3000
+ provider: "quick";
3001
+ target: {
3002
+ host: string;
3003
+ port: number;
3004
+ };
3005
+ exposesGateway: boolean;
3006
+ /**
3007
+ * Present ONLY for passthrough tunnels (`exposesGateway === false`):
3008
+ * the daemon does not gate that traffic, so the public URL reaches the
3009
+ * upstream service with no auth from the daemon. A machine-readable flag
3010
+ * so UIs/agents can surface the exposure instead of it being buried in
3011
+ * the tool's prose description.
3012
+ */
3013
+ warning?: string;
3014
+ }
3015
+ interface RemoteStatus {
3016
+ enabled: boolean;
3017
+ provider?: "quick";
3018
+ publicUrl?: string;
3019
+ pid?: number | null;
3020
+ createdAt?: string;
3021
+ target?: {
3022
+ host: string;
3023
+ port: number;
3024
+ };
3025
+ exposesGateway?: boolean;
3026
+ /** Last error from the provider, if any (since last enable). */
3027
+ lastError?: string;
3028
+ }
3029
+
2642
3030
  /**
2643
3031
  * Daemon-side pairing registry (design: DESIGN §3/§4, PLAN deliverable 2).
2644
3032
  *
2645
3033
  * Owns everything the daemon needs to be *paired with* over an untrusted
2646
3034
  * rendezvous:
2647
3035
  *
2648
- * - **Offer store** — in-memory, single-use, expiry-checked one-time tokens
2649
- * minted by `pair offer`. A daemon restart voids outstanding offers
2650
- * (acceptable + documented). The P1 `verifyOfferToken` predicate plugs in
2651
- * here and SPENDS the token on the first successful handshake.
3036
+ * - **Offer store** — in-memory, single-use, expiry-checked one-time offer
3037
+ * secrets minted by `pair offer`. A daemon restart voids outstanding offers
3038
+ * (acceptable + documented). The daemon parks on the offer's ROUTE token and
3039
+ * the handshake's `verifyAuthToken` predicate checks the sealed AUTH token,
3040
+ * SPENDING the offer on the first successful handshake (a wrong token spends
3041
+ * nothing).
2652
3042
  * - **Pairings store** — `~/.agentproto/pairings.json` (0600, atomic write),
2653
3043
  * one record per paired client: `{clientPub, name, fingerprint, createdAt,
2654
3044
  * lastSeen, pairRoot, rendezvousUrl}`. `pairRoot` is the long-term shared
2655
3045
  * secret (`derivePairRoot`) from which reconnect routing tokens derive.
2656
3046
  * - **Rendezvous connections** — for a fresh offer, and for every persisted
2657
3047
  * pairing on boot (autoconnect), the daemon dials the rendezvous *outbound*
2658
- * (`side=daemon&t=<token>`), runs `daemonHandshakeOverSink` with the P1
3048
+ * (`side=daemon&t=<route>`), runs `daemonHandshakeOverSink` with the P1
2659
3049
  * crypto, and — on success — serves the spliced, E2E-wrapped channel exactly
2660
3050
  * like `serve --connect` serves a tunnel host. The serving path itself is
2661
3051
  * injected (`serve`) so this module stays free of pty/adapter concerns and
2662
3052
  * the CLI can reuse its `createTunnelServer` config verbatim.
2663
- * - **Reconnect epochs** — a persisted pairing's standing connection uses the
2664
- * pairing-derived routing token `t' = HKDF(pairRoot, "rv-route"‖epoch)`,
2665
- * rotated per UTC day. The daemon parks on BOTH the current and previous
3053
+ * - **Reconnect epochs** — a persisted pairing's standing connection parks on
3054
+ * the pairing-derived route `HKDF(pairRoot, "rv-route"‖epoch)`, rotated per
3055
+ * UTC day. The daemon parks on BOTH the current and previous
2666
3056
  * epoch tokens so a client whose clock straddles midnight still finds it;
2667
3057
  * reconnect-with-backoff keeps the standing connection alive.
2668
- * - **Revocation** — removing a pairing drops its rendezvous connections and
2669
- * stops the daemon parking on its tokens, so it can no longer be reached.
3058
+ * - **Revocation** — removing a pairing drops its rendezvous connections, so
3059
+ * it can no longer be served. For a grace period (`revokedGraceMs`, default
3060
+ * 14 days) the daemon keeps a *tombstone*: it still parks on the pairing's
3061
+ * epoch ROUTES, completes the (daemon-signed) handshake only for a hello
3062
+ * carrying that epoch's AUTH token, and answers with a single E2E-encrypted
3063
+ * `error{code:"pairing_revoked"}` frame before closing — never serving the
3064
+ * channel. That is what lets a client tell "unpaired" from "daemon
3065
+ * offline" (which both otherwise look like a park timeout at the broker)
3066
+ * and stop retrying. The signal is authenticated like every tunnel frame:
3067
+ * it arrives inside the AEAD channel of a handshake whose transcript the
3068
+ * client verified against the pinned daemon key, so the broker can't forge
3069
+ * it; and the broker, which knows the routes, can't elicit it either (no
3070
+ * auth token). A tombstone keeps only the per-epoch route + auth tokens for
3071
+ * its window — the pair root is dropped at revoke.
2670
3072
  *
2671
3073
  * ## Reconnect authentication (why no stable client key)
2672
3074
  *
2673
- * P1's handshake carries only a client *ephemeral* key (regenerated per
2674
- * handshake) — there is no persistent client identity key, and the PLAN forbids
2675
- * changing the handshake. So a reconnect authenticates the client by
2676
- * **possession of the epoch routing token**, which derives from `pairRoot` — a
2677
- * secret only the paired client and daemon share (it fell out of the original
2678
- * ECDH). The daemon dials a pairing's epoch token, and on that connection the
2679
- * `verifyOfferToken` predicate accepts a hello whose offer-token field equals
2680
- * that same epoch token (constant-time). Token possession ⇒ paired party. The
2681
- * stored `fingerprint` (of the first handshake's ephemeral key) is a stable
2682
- * display/revocation handle, not an authentication input.
3075
+ * The handshake carries only a client *ephemeral* key (regenerated per
3076
+ * handshake) — there is no persistent client identity key. So a reconnect
3077
+ * authenticates the client by **possession of the epoch AUTH token**
3078
+ * `HKDF(pairRoot, "rv-auth"‖epoch)`, which derives from `pairRoot` — a secret
3079
+ * only the paired client and daemon share (it fell out of the original ECDH).
3080
+ * The daemon parks on the pairing's epoch ROUTE, and on that connection the
3081
+ * `verifyAuthToken` predicate accepts a hello whose sealed auth equals the same
3082
+ * epoch's auth token (constant-time). The route and auth are separate one-way
3083
+ * HKDF outputs: the broker sees every route and can't compute an auth from it.
3084
+ * (pair/v1 used ONE value for both, so a broker could replay a logged route as
3085
+ * the proof and be served.) The stored `fingerprint` (of the first handshake's
3086
+ * ephemeral key) is a stable display/revocation handle, not an auth input.
3087
+ *
3088
+ * ## Legacy (pair/v1) pairings
3089
+ *
3090
+ * A v1 `pairings.json` is loaded with every record flagged `legacy`: listed and
3091
+ * revocable, never served. The daemon still parks on a legacy pairing's routes
3092
+ * (the epoch route is unchanged from v1) so that pairing's not-yet-upgraded
3093
+ * client reaches it and is told to re-pair — see `respondToLegacyHandshake` —
3094
+ * rather than timing out. v1 records are not upgraded in place: v1 let the
3095
+ * broker pair as a client, so any v1 record may be the broker's own.
2683
3096
  */
2684
3097
 
2685
- declare const PAIRINGS_VERSION: 1;
3098
+ /** `pairings.json` format. v2 = pairings made under pair/v2 (route/auth
3099
+ * split). A v1 file still loads, every record flagged `legacy`. */
3100
+ declare const PAIRINGS_VERSION: 2;
2686
3101
  /** One persisted pairing. Written verbatim to `pairings.json`. */
2687
3102
  interface PairingRecord {
2688
3103
  /** The client ephemeral public key from the FIRST handshake (a snapshot —
@@ -2701,6 +3116,17 @@ interface PairingRecord {
2701
3116
  pairRoot: string;
2702
3117
  /** Rendezvous endpoint to reconnect through. */
2703
3118
  rendezvousUrl: string;
3119
+ /** Set on a pairing made under the retired pair/v1 protocol: listed and
3120
+ * revocable, never served — its client is told to re-pair. */
3121
+ legacy?: true;
3122
+ /** Set when this pairing was made under an offer minted with
3123
+ * `agentproto pair offer --host` (PR-C) — i.e. the offering daemon's own
3124
+ * intent, recorded server-side at handshake time from the OfferEntry it
3125
+ * itself created, never from the (unauthenticated) `scope` param a client
3126
+ * presented in the offer URL. Absent for an ordinary remote-control
3127
+ * pairing. Purely informational here — it does not change how this
3128
+ * pairing is served. */
3129
+ scope?: "host";
2704
3130
  }
2705
3131
  /** Mode of a served channel — first-contact offer vs. an established reconnect. */
2706
3132
  type PairingChannelMode = "offer" | "reconnect";
@@ -2718,8 +3144,9 @@ interface PairingChannelHandle {
2718
3144
  close(): Promise<void>;
2719
3145
  }
2720
3146
  interface CreatedOffer {
2721
- /** The one-time offer token (base64url). */
2722
- token: string;
3147
+ /** The one-time offer secret (base64url) — the URL's `s`. Never on the wire:
3148
+ * the broker only ever sees its derived route. */
3149
+ secret: string;
2723
3150
  /** Offer expiry, unix seconds. */
2724
3151
  exp: number;
2725
3152
  /** The full `agentproto://pair?…` offer URL. */
@@ -2733,12 +3160,21 @@ interface CreatedOffer {
2733
3160
  * falls back to `HOSTED_RENDEZVOUS_URL`. Surfaced so `pair offer` can flag,
2734
3161
  * never silently, that the daemon is relaying through our infrastructure. */
2735
3162
  rendezvousIsHostedDefault: boolean;
3163
+ /** Echoes `CreateOfferInput.scope`. `"host"` when minted with `--host`;
3164
+ * absent for a plain offer. */
3165
+ scope?: "host";
2736
3166
  }
2737
3167
  interface CreateOfferInput {
2738
3168
  /** Time-to-live in ms. Default 10 minutes. */
2739
3169
  ttlMs?: number;
2740
3170
  /** Rendezvous URL override; falls back to the configured default. */
2741
3171
  rendezvousUrl?: string;
3172
+ /** Mint a HOST-scoped offer (PR-C): the accepting side may register this
3173
+ * daemon as a driveable host (`devices add`), not just remote-control it.
3174
+ * Recorded on the resulting `OfferEntry`/`PairingRecord` server-side — this
3175
+ * is the authoritative record of what was granted, independent of the
3176
+ * `scope` param an accepting client's offer URL happens to carry. */
3177
+ scope?: "host";
2742
3178
  }
2743
3179
  interface PairingRegistryDeps {
2744
3180
  /** Lazily load (create on first use) the daemon identity. Kept lazy so a
@@ -2772,23 +3208,184 @@ interface PairingRegistryDeps {
2772
3208
  /** How long a parked daemon socket waits for a client hello before recycling
2773
3209
  * the connection. Kept just under the broker's park timeout. Default 110s. */
2774
3210
  handshakeTimeoutMs?: number;
3211
+ /** How long a revoked pairing is still answered with `pairing_revoked` (see
3212
+ * "Revocation" above). Default 14 days; `0` disables tombstones, so a
3213
+ * revoked client just stops finding the daemon. */
3214
+ revokedGraceMs?: number;
2775
3215
  }
2776
3216
  interface PairingRegistry {
2777
3217
  /** Mint a one-time offer + start dialing the rendezvous for it. */
2778
3218
  createOffer(input?: CreateOfferInput): Promise<CreatedOffer>;
2779
3219
  /** All persisted pairings (copies). Loads `pairings.json` on first call. */
2780
3220
  list(): Promise<PairingRecord[]>;
3221
+ /** Rename a pairing by fingerprint or name (cosmetic only — no effect on
3222
+ * auth or routing). Returns false when nothing matched; throws on an
3223
+ * empty `newName`. */
3224
+ rename(idOrName: string, newName: string): Promise<boolean>;
2781
3225
  /** Drop a pairing by fingerprint or name; stops its rendezvous connections.
2782
3226
  * Returns false when nothing matched. */
2783
3227
  revoke(idOrName: string): Promise<boolean>;
3228
+ /** Is a channel (offer or reconnect) currently served for this fingerprint? */
3229
+ isOnline(fingerprint: string): boolean;
2784
3230
  /** Start standing reconnect connections for every persisted pairing. Call
2785
3231
  * after the gateway is up (so the injected `serve` can reach it). */
2786
3232
  startAutoconnect(): Promise<void>;
2787
3233
  /** Tear down every offer + reconnect connection and served channel. */
2788
3234
  shutdown(): Promise<void>;
2789
3235
  }
3236
+ /**
3237
+ * Read-only snapshot of `pairings.json` for callers that just want the
3238
+ * persisted records — no `dial`/`serve`/`loadIdentity` deps, no rendezvous
3239
+ * connections started (e.g. `agentproto doctor`'s devices check). Mirrors
3240
+ * `ensureLoaded`'s parse + v1-legacy flagging; never throws — a missing or
3241
+ * malformed file just yields `[]`.
3242
+ */
3243
+ declare function readPairingsSnapshot(path?: string): Promise<PairingRecord[]>;
2790
3244
  declare function createPairingRegistry(deps: PairingRegistryDeps): PairingRegistry;
2791
3245
 
3246
+ /**
3247
+ * Daemon-side HOST registry (DEVICES-PLAN PR-C) — the "client" half of
3248
+ * pair/v2, living on the daemon so it's reachable over both an HTTP route
3249
+ * and an MCP tool, not just CLI-local bookkeeping (`client-pairings.ts` /
3250
+ * `pair-transport.ts` are that CLI-local equivalent today).
3251
+ *
3252
+ * Where `pairing-registry.ts` is what a daemon uses to let OTHER machines
3253
+ * pair with and remote-control IT, `host-registry.ts` is what a daemon uses
3254
+ * to register and drive OTHER daemons — reverse pairing. `add()` accepts a
3255
+ * HOST-scoped offer (`agentproto pair offer --host`, offer-url.ts's
3256
+ * `scope`), runs the pair/v2 client handshake, and persists the resulting
3257
+ * pair root; `forwardHttp()` dials on demand, reconnecting over the current
3258
+ * (then previous) epoch token exactly like `pair-transport.ts`'s
3259
+ * `openPairChannel`, forwards one HTTP request over the resulting tunnel
3260
+ * client, and closes. There is no standing connection here — unlike
3261
+ * `PairingRegistry`, a `HostRegistry` never autoconnects or keeps a
3262
+ * reconnect loop parked at the broker; every reach-out is a fresh dial.
3263
+ * That also means shutdown needs no teardown: there is nothing standing to
3264
+ * tear down.
3265
+ *
3266
+ * ## The scope gate
3267
+ *
3268
+ * `add()` REFUSES (throws) any offer whose parsed `scope !== "host"` before
3269
+ * ever dialing anything. This is the default-deny gate PR-C exists to add:
3270
+ * an ordinary "remote-control me" offer (today's `pair offer`, no `--host`)
3271
+ * must not silently become a host registration, even though the underlying
3272
+ * wire capability is identical either way (see offer-url.ts's "Offer scope"
3273
+ * and pairing-registry.ts's `OfferEntry.scope` doc comment for why the
3274
+ * offering daemon's OWN server-side record — not the URL's plaintext
3275
+ * `scope` param — is what's authoritative about what it granted). There is
3276
+ * deliberately no override flag.
3277
+ *
3278
+ * Persisted to `hosts.json` (`~/.agentproto/hosts.json` by default) with the
3279
+ * same atomic-write discipline as `pairings.json` (mkdir, write to a
3280
+ * pid-keyed tmp file at 0600, chmod, rename) and the same lazy-load-on-
3281
+ * first-use pattern.
3282
+ *
3283
+ * ## Online tracking
3284
+ *
3285
+ * `isOnline(fingerprint)` mirrors `PairingRegistry.isOnline`: it's true only
3286
+ * while a `forwardHttp()` call for that fingerprint is in flight (a
3287
+ * reference count, incremented on dial and decremented in a `finally`).
3288
+ * There is no live probe backing `list()` — that would make `devices list`
3289
+ * slow/flaky over N hosts — so `list()`'s `online` is only ever true
3290
+ * immediately after (or during) an actual `forwardHttp`/`devices status`
3291
+ * call, never a background heartbeat. That's enough to make `devices list`
3292
+ * show the right thing right after `devices status`/`exec`, which is what
3293
+ * matters for this PR; a standing/autoconnect host connection with a real
3294
+ * liveness signal is PR-D's to build.
3295
+ */
3296
+
3297
+ /** `hosts.json` format. */
3298
+ declare const HOSTS_VERSION: 1;
3299
+ /** One persisted host — a daemon this daemon can drive. */
3300
+ interface HostRecord {
3301
+ /** The target daemon's identity fingerprint (32 hex). */
3302
+ fingerprint: string;
3303
+ /** Human-facing label; defaults to the fingerprint. */
3304
+ name: string;
3305
+ /** Target daemon's static X25519 public key, standard base64 SPKI DER —
3306
+ * pinned at `add()` time. */
3307
+ daemonX25519Pub: string;
3308
+ /** Target daemon's static Ed25519 public key, standard base64 SPKI DER —
3309
+ * pinned at `add()` time. */
3310
+ daemonEd25519Pub: string;
3311
+ /** Rendezvous endpoint to reach the host through. */
3312
+ rendezvousUrl: string;
3313
+ /** Long-term shared secret (base64), from which epoch routing tokens
3314
+ * derive — same derivation as `PairingRecord.pairRoot`. */
3315
+ pairRoot: string;
3316
+ /** ISO-8601 first-add timestamp. */
3317
+ createdAt: string;
3318
+ /** ISO-8601 of the most recent successful `forwardHttp`. */
3319
+ lastSeen: string;
3320
+ /** Set on a host added under the retired pair/v1 protocol. Present for
3321
+ * parity with `PairingRecord`/`ClientPairing`; pair/v1 offers are refused
3322
+ * by `parseOfferUrl` before `add()` ever sees them, so this is currently
3323
+ * unreachable — kept so a future downgrade path has somewhere to flag it. */
3324
+ legacy?: true;
3325
+ }
3326
+ interface HostRegistryDeps {
3327
+ /** Dial a rendezvous WS and adapt it to a `FrameSink`. Same shape as
3328
+ * `PairingRegistryDeps.dial` — the CLI's `daemonDialRendezvous` satisfies
3329
+ * it directly. Rejects if the dial fails or `signal` aborts. */
3330
+ dial: (wsUrl: string, signal: AbortSignal) => Promise<FrameSink>;
3331
+ /** Path to `hosts.json`. Defaults to `~/.agentproto/hosts.json`. */
3332
+ hostsPath?: string;
3333
+ /** Injectable clock (ms). Defaults to Date.now. */
3334
+ now?: () => number;
3335
+ /** Diagnostic log sink. */
3336
+ log?: (line: string) => void;
3337
+ /** Rendezvous dial ceiling per attempt. Default 15s. */
3338
+ dialTimeoutMs?: number;
3339
+ /** Handshake ceiling per attempt. Default 15s. */
3340
+ handshakeTimeoutMs?: number;
3341
+ }
3342
+ interface ForwardHttpRequest {
3343
+ method: string;
3344
+ path: string;
3345
+ headers?: Record<string, string>;
3346
+ body?: Uint8Array;
3347
+ }
3348
+ interface ForwardHttpResponse {
3349
+ status: number;
3350
+ headers: Record<string, string>;
3351
+ body: Uint8Array;
3352
+ }
3353
+ interface HostRegistry {
3354
+ /**
3355
+ * Accept an offer URL and register the target daemon as a driveable host.
3356
+ * REFUSES (throws a plain `Error`) unless the parsed offer has
3357
+ * `scope === "host"` — no dial is attempted in that case. Otherwise: dials
3358
+ * the offer's rendezvous, runs the pair/v2 client handshake, verifies the
3359
+ * derived peer fingerprint matches the offer's `id` (defence in depth,
3360
+ * mirrors `pair-transport.ts`'s `acceptOffer`), derives the pair root, and
3361
+ * persists a `HostRecord` (upserted by fingerprint — re-adding replaces).
3362
+ */
3363
+ add(offerUrl: string, name?: string): Promise<{
3364
+ fingerprint: string;
3365
+ name: string;
3366
+ rendezvousUrl: string;
3367
+ }>;
3368
+ /** All persisted hosts (copies). Loads `hosts.json` on first call. */
3369
+ list(): Promise<HostRecord[]>;
3370
+ /** Rename a host (fingerprint or current name). Returns false when nothing
3371
+ * matched; throws on an empty `newName`. */
3372
+ rename(idOrName: string, newName: string): Promise<boolean>;
3373
+ /** Drop a host by fingerprint or name. Returns false when nothing matched. */
3374
+ revoke(idOrName: string): Promise<boolean>;
3375
+ /** Is a `forwardHttp` call for this fingerprint in flight right now? */
3376
+ isOnline(fingerprint: string): boolean;
3377
+ /**
3378
+ * On-demand: dial the host, run the client handshake against the current
3379
+ * epoch's tokens (falling back to the previous epoch, bridging clock
3380
+ * skew — same two-attempt shape as `pair-transport.ts`'s
3381
+ * `openPairChannel`), forward one HTTP request over the resulting tunnel
3382
+ * client, then close. Throws with a re-pair hint if every attempt hangs up
3383
+ * on the hello (likely a pair/v1 peer).
3384
+ */
3385
+ forwardHttp(idOrName: string, req: ForwardHttpRequest): Promise<ForwardHttpResponse>;
3386
+ }
3387
+ declare function createHostRegistry(deps: HostRegistryDeps): HostRegistry;
3388
+
2792
3389
  /**
2793
3390
  * Shared types for pluggable sandbox providers.
2794
3391
  *
@@ -2832,6 +3429,17 @@ interface SandboxProviderHandle extends AdapterHandle {
2832
3429
  * credentials (e.g. `local`).
2833
3430
  */
2834
3431
  readonly setupFields?: readonly SetupField[];
3432
+ /**
3433
+ * The directory INSIDE the box a spawned agent session should default to
3434
+ * when the caller passed no explicit `cwd` — e.g. e2b/Box's login home
3435
+ * `/home/user`. Absent for a same-machine passthrough provider (`local`),
3436
+ * where the HOST's own resolved cwd is already a valid path in the box
3437
+ * (they're the same filesystem) and forwarding it as-is is correct.
3438
+ * Presence of this field is also the signal `bootSandboxAgentSession`
3439
+ * uses to reject an explicit HOST-only path (e.g. `/Volumes/...`) instead
3440
+ * of forwarding it into a box where it can never resolve.
3441
+ */
3442
+ readonly defaultCwd?: string;
2835
3443
  }
2836
3444
 
2837
3445
  /**
@@ -3331,12 +3939,21 @@ interface OrchestratorGatewayDeps {
3331
3939
  * spawn is rejected with `sandbox_provider_not_found`, exactly as the
3332
3940
  * root gateway would be without it. */
3333
3941
  resolveSandboxProvider?: SandboxProviderResolver;
3334
- /** Forwarded to `registerSessionTools` — config.json
3335
- * `defaults.agentPromptInterrupt`, the unset-default for `interrupt` on
3336
- * `agent_prompt` / `message_parent`. Threaded here so a child driving its
3337
- * parent/peers (or reporting up via `message_parent`) through this scoped
3338
- * gateway honours the same daemon default as the root `/mcp` surface. */
3942
+ /** EXPLICIT override for config.json `defaults.agentPromptInterrupt` —
3943
+ * wins over the fresh per-call resolve (`resolveMessagingDefaults`,
3944
+ * see the factory body below). Tests inject this to pin a value without
3945
+ * touching the filesystem; production leaves it unset so every request
3946
+ * reads the live config, hot-applying a `config_set` change with no
3947
+ * restart. Forwarded to `registerSessionTools`. */
3339
3948
  defaultAgentPromptInterrupt?: boolean;
3949
+ /** EXPLICIT override for config.json `defaults.messaging.allowSiblings` —
3950
+ * see `defaultAgentPromptInterrupt` above for the override/resolve
3951
+ * precedence. Forwarded to `registerAgentTools`. */
3952
+ messagingAllowSiblings?: boolean;
3953
+ /** EXPLICIT override for config.json `defaults.messaging.agentInterrupt` —
3954
+ * see `defaultAgentPromptInterrupt` above for the override/resolve
3955
+ * precedence. Forwarded to `registerAgentTools`. */
3956
+ messagingAgentInterrupt?: "allow" | "deny";
3340
3957
  }
3341
3958
  type OrchestratorMcpServerFactory = (scope: OrchestratorScope) => Promise<McpServer>;
3342
3959
  /**
@@ -3391,7 +4008,7 @@ interface OrphanReaperRegistry {
3391
4008
  list(opts?: {
3392
4009
  includeArchived?: boolean;
3393
4010
  }): readonly SessionDescriptor[];
3394
- kill(id: string, signal?: NodeJS.Signals): boolean;
4011
+ kill(id: string, signal?: NodeJS.Signals, reason?: SessionEndReason): boolean;
3395
4012
  }
3396
4013
  /**
3397
4014
  * Reap a dying parent's live descendants (WP-B — orphan reaping). A
@@ -3579,6 +4196,77 @@ declare function createActivityProjector(opts: {
3579
4196
  resolvePrState?: PrStateResolver;
3580
4197
  }): ActivityProjector;
3581
4198
 
4199
+ /**
4200
+ * Spawn-time role profiles — the primitive that decides whether a
4201
+ * spawned agent may itself delegate (spawn further children).
4202
+ *
4203
+ * A role is 3 layers:
4204
+ * - `disposition` — a system-prompt fragment. Soft: sets the mindset.
4205
+ * - `toolPolicy` — whether delegation tools are injected into the
4206
+ * child at all. HARD: enforced by the daemon at the `agent_start`
4207
+ * injection point (`session-spawn.ts`), not by the child's own
4208
+ * behaviour. A child cannot widen its own `toolPolicy` — not via
4209
+ * `promptAppend`, not by requesting `orchestrator` itself.
4210
+ * - `skills[]` — role-specific skill set (built-ins ship empty/
4211
+ * unset; a future pack-carried role can populate this).
4212
+ *
4213
+ * Why the asymmetry matters: a process can bring its own disposition
4214
+ * and skills (those are portable, could live in a skill/pack), but it
4215
+ * can never strip tools the daemon already injected into it. Only the
4216
+ * spawner controls that — which is exactly why `toolPolicy` must be
4217
+ * resolved and enforced here, at spawn time, rather than left to a
4218
+ * prompt instruction the model could rationalize past.
4219
+ */
4220
+
4221
+ type DelegationPolicy = "allow" | "deny";
4222
+ interface RoleToolPolicy {
4223
+ delegation: DelegationPolicy;
4224
+ }
4225
+ interface RoleProfile {
4226
+ name: string;
4227
+ disposition: string;
4228
+ toolPolicy: RoleToolPolicy;
4229
+ skills?: string[];
4230
+ /**
4231
+ * Privilege level in the spawn lattice. Higher = more privileged.
4232
+ * The default spawn-gate rule (`canSpawn`) is non-escalation: a
4233
+ * parent may spawn a child only when `child.level <= parent.level`
4234
+ * — peers and subordinates are fine, spawning something MORE
4235
+ * privileged than yourself is not. (Unbounded same-level recursion —
4236
+ * e.g. supervisor spawning supervisor spawning supervisor — is a
4237
+ * pre-existing, deliberate pattern bounded separately by
4238
+ * `maxDepth`/`maxChildren`, not this lattice; see `spawnableRoles`
4239
+ * below for a role that also needs to cap recursion itself.)
4240
+ * `spawnableRoles` (below), when set, opts a role into a closed
4241
+ * allowlist instead — level is then only used for display/ordering.
4242
+ */
4243
+ level: number;
4244
+ /**
4245
+ * Closed allowlist of role names this role may spawn, by name — the
4246
+ * opt-in "figé pipeline" mode. When set, `canSpawn` checks name
4247
+ * membership instead of the level comparison. Undefined (the
4248
+ * default, open mode) ⇒ non-escalation by `level`.
4249
+ */
4250
+ spawnableRoles?: string[];
4251
+ /**
4252
+ * Role-level default for the daemon self-mount's deferred/lazy tool
4253
+ * loading (harness-parity item 3 — see `deferred-tools.ts`). Applied to
4254
+ * the injected `mcpServers` entry (`session-spawn.ts`'s
4255
+ * `shouldInjectDaemonSelfMount` path) as `?deferred=1|0` when the spawn
4256
+ * itself didn't pass an explicit `deferredTools` override. Undefined ⇒
4257
+ * no role-level opinion, the gateway's own boot-time default
4258
+ * (`defaults.mcp.deferredTools` in config.json) applies unchanged.
4259
+ */
4260
+ deferredTools?: boolean;
4261
+ /**
4262
+ * Role-level default for `agent_start.browser` (see `browser-mount.ts`):
4263
+ * `"headless"` gives every spawn of this role its own isolated headless
4264
+ * Chrome. An explicit spawn `browser` wins; undefined ⇒ no opinion (the
4265
+ * user preset, then `defaults.spawn.browser`, decide).
4266
+ */
4267
+ browser?: SpawnBrowserMode;
4268
+ }
4269
+
3582
4270
  /**
3583
4271
  * Daemon-side AGENTS.md resolution + injection (WP-R2).
3584
4272
  *
@@ -3607,7 +4295,44 @@ declare function createActivityProjector(opts: {
3607
4295
  * absent — plus a standing cd-contract sentence regardless of mode.
3608
4296
  */
3609
4297
  type AgentsMdMode = "inline" | "pointer" | "absent";
4298
+ /** The resolved AGENTS.md's absolute path; undefined when mode is "absent". */
4299
+ interface AgentsMdResolution {
4300
+ mode: AgentsMdMode;
4301
+ /** Absolute path of the resolved AGENTS.md. Undefined when mode is
4302
+ * `"absent"`. */
4303
+ path?: string;
4304
+ /** Full text of the AGENTS.md (inline mode only). */
4305
+ content?: string;
4306
+ /** The prompt block to inject (full inline text or a pointer), or
4307
+ * undefined when absent. Excludes the always-present cd-contract line. */
4308
+ block?: string;
4309
+ /** The standing cd-contract sentence, present in every mode. */
4310
+ contractLine: string;
4311
+ }
4312
+
4313
+ interface WorkspaceRulesResolution {
4314
+ /** Absolute path of the resolved RULES.md. Undefined when absent (or a
4315
+ * read failure degraded to absent). */
4316
+ path?: string;
4317
+ /** Full text of the RULES.md, inlined. Present exactly when `block` is. */
4318
+ content?: string;
4319
+ /** The prompt block to inject, or undefined when absent. */
4320
+ block?: string;
4321
+ }
3610
4322
 
4323
+ /** The `appServe` request carried on `agent_start` — an in-box app dir plus
4324
+ * the port to serve its UI on. */
4325
+ interface SandboxAppServeSpec {
4326
+ /** Absolute path to the app's directory INSIDE the box (e.g.
4327
+ * `/home/user/apps/<slug>`). Must not contain a single quote — the
4328
+ * launcher shell-quotes it. */
4329
+ dir: string;
4330
+ /** Port `agentproto app serve` binds inside the box. Must also be exposed
4331
+ * by the provider (the spawn path appends it to `extraPorts` so a
4332
+ * boot-time exposure exists on providers that pre-resolve). Defaults to
4333
+ * `DEFAULT_APP_SERVE_PORT` when omitted. */
4334
+ port?: number;
4335
+ }
3611
4336
  /** What a served app records on the session descriptor and echoes in the
3612
4337
  * spawn result — non-secret, caller-visible. */
3613
4338
  interface SessionAppServeInfo {
@@ -3833,6 +4558,460 @@ declare function loadWorktreeIsolation(loadCfg?: () => Promise<{
3833
4558
  };
3834
4559
  }>): Promise<WorktreeIsolationMode>;
3835
4560
 
4561
+ /**
4562
+ * Policy layer for `agent_start.attach` — the config-driven decision of WHO a
4563
+ * spawn nests under, kept deliberately separate (and pure) from the spawn
4564
+ * plumbing in `session-spawn.ts`, exactly as `worktree-isolation.ts` is for
4565
+ * `agent_start.worktree`.
4566
+ *
4567
+ * The problem it fixes: on the plain `/mcp` path (no scoped orchestrator
4568
+ * gateway), a spawn was attributed to a parent ONLY when the caller passed an
4569
+ * explicit `parentSessionId` hint — so a supervisor that spawned an executor
4570
+ * without wiring itself as an orchestrator produced a depth-0 ORPHAN. The
4571
+ * daemon already knows the caller's own id on that path (the trusted
4572
+ * `?callerSessionId=` query the self-ref `mcpServers` URL carries — PR 7 /
4573
+ * Gap 7), it just wasn't being used for lineage. This module decides whether
4574
+ * to use it.
4575
+ *
4576
+ * Trust boundary (unchanged from the hint): the scoped `/mcp/orchestrator`
4577
+ * gateway's token is the UNSPOOFABLE source — when a `callerScope` is present
4578
+ * the caller derives its parent from the token and this whole module is
4579
+ * bypassed (see `session-spawn.ts`). Everything here is descriptor-only
4580
+ * lineage on the anonymous root path; it never moves the gating `childDepth`
4581
+ * and so never relaxes a depth-gated worktree/role guard.
4582
+ */
4583
+
4584
+ /**
4585
+ * The `agent_start.attach` field. `true` opts in to attaching under the
4586
+ * derived parent (the caller's own id / an explicit hint) even when the
4587
+ * policy is `on-request`; an object additionally pins an explicit parent id;
4588
+ * `false` forces an INDEPENDENT root (no parent, depth 0) regardless of
4589
+ * policy or derivable identity — the deliberate "launch detached" escape
4590
+ * hatch, mirroring `worktree: false`. Omitted ⇒ the policy mode decides.
4591
+ */
4592
+ type AttachField = boolean | {
4593
+ parent?: string;
4594
+ };
4595
+
4596
+ /**
4597
+ * Shared `agent_start` spawn logic — orchestrator scoped sub-gateway
4598
+ * minting, `mcpServers` merge, hermes default-mcpServers safety net,
4599
+ * depth/quota checks. Extracted from `agent-tools.ts` (the MCP tool)
4600
+ * so the HTTP route can reuse it without re-implementing (and
4601
+ * re-drifting from) the same behaviour.
4602
+ */
4603
+
4604
+ /** `agent_start.sandbox`'s inline-spec form, plus the PR3 reuse field. See
4605
+ * `SpawnAgentSessionInput.sandbox`. */
4606
+ type SandboxSpecInput = SandboxSpec & {
4607
+ reuse?: string;
4608
+ };
4609
+ /** Matches `RegisterAgentToolsOptions.buildOrchestratorMcp` in
4610
+ * agent-tools.ts — kept as its own alias here since that's the shape
4611
+ * actually passed in (opts required), not the more permissive
4612
+ * `OrchestratorInjector` (opts optional) from orchestrator-gateway.ts. */
4613
+ type BuildOrchestratorMcp = (opts: {
4614
+ tools?: readonly string[];
4615
+ caller?: OrchestratorScope;
4616
+ maxDepth?: number;
4617
+ maxChildren?: number;
4618
+ role?: string;
4619
+ }) => {
4620
+ entry: AcpMcpServer;
4621
+ bindLifecycle: (sessionId: string) => () => void;
4622
+ /** The minted scope — only `tools` (the effective, narrowed allowlist)
4623
+ * is read here, to tell whether the child can actually reach
4624
+ * `agent_start` through it (see `delegationReachFor`). Optional so
4625
+ * minimal injectors (tests) needn't return it. */
4626
+ scope?: {
4627
+ tools: ReadonlySet<string>;
4628
+ };
4629
+ };
4630
+ interface SpawnAgentSessionDeps {
4631
+ registry: SessionsRegistry;
4632
+ /** Required — callers must check for its absence themselves (the MCP
4633
+ * tool and HTTP route each have their own "not enabled" response
4634
+ * shape for the missing-resolver case). */
4635
+ resolveAgentAdapter: AgentAdapterResolver;
4636
+ /** Optional orchestrator-injection builder (WP3). See
4637
+ * `RegisterAgentToolsOptions.buildOrchestratorMcp` for the full
4638
+ * contract. Omitted → `orchestrator` is rejected with
4639
+ * `orchestrator_not_enabled`. */
4640
+ buildOrchestratorMcp?: BuildOrchestratorMcp;
4641
+ /** The daemon's own plain `/mcp` gateway URL. See
4642
+ * `RegisterAgentToolsOptions.daemonMcpUrl`. */
4643
+ daemonMcpUrl?: string;
4644
+ /** Calling orchestrator's scope (orchestrator WP4). See
4645
+ * `RegisterAgentToolsOptions.callerScope`. */
4646
+ callerScope?: OrchestratorScope;
4647
+ /** Optional webhook notifier — see `RegisterAgentToolsOptions.webhookNotifier`. */
4648
+ webhookNotifier?: WebhookNotifier;
4649
+ /** Loads config.json's `defaults` block (global + per-adapter skills/
4650
+ * options auto-applied at every spawn — see `resolveSpawnDefaults`).
4651
+ * Defaults to reading the real `~/.agentproto/config.json` via
4652
+ * `loadConfig` when omitted; tests inject a stub to avoid touching
4653
+ * the real file. */
4654
+ loadDefaultsConfig?: () => Promise<SpawnDefaultsConfig | undefined>;
4655
+ /** Builds the per-session headless-browser mount for `browser:
4656
+ * "headless"` (see `browser-mount.ts`). Defaults to the real resolver
4657
+ * (installs chrome-devtools-mcp on first use); tests inject a stub. */
4658
+ resolveHeadlessBrowser?: ResolveHeadlessBrowser;
4659
+ /** Loads the custom (pack-carried) role registry, merged with the
4660
+ * two built-ins by `resolveRole`/`canSpawn` — see `role.ts`'s
4661
+ * `mergeRoleRegistry`. Defaults to `loadDefaultRoleRegistry()`
4662
+ * (`~/.agentproto/roles/` + adapter-carried packs) when omitted;
4663
+ * tests inject a stub registry to avoid touching the real
4664
+ * filesystem. */
4665
+ loadRoleRegistry?: () => Promise<Record<string, RoleProfile>>;
4666
+ /** Resolves an `agent_start.sandbox` slug (or an inline spec's own
4667
+ * `.provider`) to a concrete sandbox provider handle. Required for
4668
+ * `input.sandbox` — omitted ⇒ `sandbox_provider_not_found`. */
4669
+ resolveSandboxProvider?: SandboxProviderResolver;
4670
+ /** Provision a git worktree and return the cwd the spawn should land in —
4671
+ * the injected port behind `agent_start.worktree` (see
4672
+ * `worktree-isolation.ts`). Wired at the composition root by a host that
4673
+ * depends on `@agentproto/worktree` (the CLI). Omitted ⇒ a spawn the
4674
+ * policy says to isolate fails with `worktree_provisioner_not_enabled`
4675
+ * rather than silently spawning unisolated. */
4676
+ provisionWorktree?: WorktreeProvisioner;
4677
+ /** Resolves the effective `worktrees.isolation` policy (env > config >
4678
+ * `on-request`). Defaults to reading the real `~/.agentproto/config.json`
4679
+ * via `loadWorktreeIsolation` when omitted; tests inject a stub to pin the
4680
+ * mode without touching the real file or env. */
4681
+ resolveWorktreeIsolation?: () => Promise<WorktreeIsolationMode>;
4682
+ /** Resolves the effective `spawn.attach` policy (env > config > `always`).
4683
+ * Defaults to `loadSpawnAttach` (reads the real config) when omitted;
4684
+ * tests inject a stub to pin the mode without touching env or the file. */
4685
+ resolveSpawnAttach?: () => Promise<SpawnAttachMode>;
4686
+ /** Resolves the effective `spawn.dedupe` policy (env > config > `always`).
4687
+ * Defaults to `loadSpawnDedupe` (reads the real config) when omitted;
4688
+ * tests inject a stub to pin the mode without touching env or the file.
4689
+ * See `spawn-dedupe.ts`. */
4690
+ resolveSpawnDedupe?: () => Promise<SpawnDedupeMode>;
4691
+ /** Resolves the effective `provenance.wrapGh` opt-in (env > config >
4692
+ * `false`). When it resolves true, a `gh` PATH shim is prepended to the
4693
+ * spawned session's env so any `gh pr create` it (or an adapter
4694
+ * subprocess) runs gets the daemon's provenance footer appended to the
4695
+ * created PR's body — see `gh-provenance-shim.ts`. Defaults to
4696
+ * `loadProvenanceWrapGh` (reads the real config) when omitted; tests
4697
+ * inject a stub to pin it without touching env or the file. */
4698
+ resolveProvenanceWrapGh?: () => Promise<boolean>;
4699
+ /** Resolves + shapes the AGENTS.md for a spawn's `cwd` (see
4700
+ * `agents-md.ts`): inline / pointer / absent, plus the standing cd-contract
4701
+ * line. Defaults to the real resolver over `loadAgentsMdInlineMaxKb`'s
4702
+ * config-derived threshold when omitted; tests inject a stub to avoid
4703
+ * touching the filesystem, git, and the real config on every spawn. */
4704
+ resolveAgentsMd?: (cwd: string, inlineMaxKb?: number) => Promise<AgentsMdResolution>;
4705
+ /** Resolves + shapes the per-workspace RULES.md for a spawn (see
4706
+ * `workspace-rules.ts`): the path of the workspace's `RULES.md`, inlined
4707
+ * in full when present, absent otherwise. Injected into EVERY spawn in
4708
+ * the workspace (root and nested — no depth gate). Defaults to the real
4709
+ * resolver over the workspace state bucket when omitted; tests inject a
4710
+ * stub to avoid touching the real `~/.agentproto` directory. */
4711
+ resolveWorkspaceRules?: (slug: string | undefined) => Promise<WorkspaceRulesResolution>;
4712
+ /** Feeds the adapter-capability spawn guard (`checkModelAdapterEligibility`)
4713
+ * — the SAME `listCatalogModels` the `catalog_models` MCP tool /
4714
+ * `GET /catalog/models` route already use (`RegisterAgentToolsOptions.
4715
+ * listCatalogModels`), reused here rather than re-derived. Omitted ⇒ the
4716
+ * guard is skipped — a spawn that would 404 upstream on an adapter's
4717
+ * manifest not covering the requested model still reaches the driver,
4718
+ * same as before this guard existed. */
4719
+ listCatalogModels?: CatalogModelsLister;
4720
+ /** Ensure the local `llm-endpoint` proxy sidecar (`LlmEndpointRegistry`)
4721
+ * is up before a spawn that bills through it. Without this hook, a spawn
4722
+ * whose resolved gateway is `"llm-endpoint"` assumed the sidecar was
4723
+ * already running (started externally, or via the `llm_endpoint_start`
4724
+ * MCP tool) — a fresh install with `features.llmEndpoint` on but nothing
4725
+ * having called that tool yet would spawn fine and only fail once the
4726
+ * adapter made its first request. Wired to the registry's own
4727
+ * (idempotent) `start()` at the composition root; omitted ⇒ unchanged
4728
+ * pre-existing behavior. Best-effort: a rejection here is swallowed by
4729
+ * the caller, not surfaced as a spawn failure — the adapter's own first
4730
+ * request is left to report the real connection error. */
4731
+ ensureLlmEndpointRunning?: () => Promise<void>;
4732
+ }
4733
+ interface SpawnAgentSessionInput {
4734
+ adapter: string;
4735
+ /** Canonical harness slug — recorded on the descriptor; defaults to `adapter`. */
4736
+ harness?: string;
4737
+ cwd?: string;
4738
+ workspaceSlug?: string;
4739
+ /** Trusted-loopback parent-lineage hint (WP-R1). Attributes this spawn to a
4740
+ * logical parent session so it nests under that node in the sessions tree,
4741
+ * filling the gap that otherwise leaves an anonymous `agent_start` (an
4742
+ * agent-to-agent spawn on the root `/mcp`, or a caller that IS a session but
4743
+ * didn't come through the scoped orchestrator gateway) a depth-0 orphan.
4744
+ *
4745
+ * Trust boundary: the scoped `/mcp/orchestrator` gateway derives parent from
4746
+ * its unspoofable scope token, so a hint arriving WITH a `callerScope` is
4747
+ * IGNORED — the scope always wins. The hint is honoured ONLY on the
4748
+ * anonymous root path (no `callerScope`), where there is no scope to
4749
+ * attribute from. `depth` is derived from the resolved parent descriptor
4750
+ * (parent's `depth + 1`, defaulting to 1 when the parent isn't registered)
4751
+ * rather than trusted from the caller. Descriptor-only: a hint never relaxes
4752
+ * the depth-gated worktree/role guards — declaring a logical parent is not
4753
+ * the same as being a nested (scoped) spawn. */
4754
+ parentSessionId?: string;
4755
+ /** Attach policy override for THIS spawn (`agent_start.attach`). `false`
4756
+ * forces an independent root even when a parent is derivable; `true` opts
4757
+ * in to attaching under the derived parent even under an `on-request`
4758
+ * policy; `{ parent }` pins an explicit parent. Omitted ⇒ the daemon's
4759
+ * `spawn.attach` policy decides. Resolved together with `parentSessionId`
4760
+ * and `autoParentSessionId` in `decideSpawnAttach` — see `spawn-attach.ts`.
4761
+ * Ignored WITH a `callerScope` (the scoped orchestrator token always wins). */
4762
+ attach?: AttachField;
4763
+ /** Daemon-DERIVED caller session id — the spawning session's own id, read
4764
+ * from the trusted `?callerSessionId=` query the self-ref `mcpServers` URL
4765
+ * carries (PR 7 / Gap 7), NOT from the caller's arguments. This is the
4766
+ * implicit auto-parent that makes attach-by-default work without the caller
4767
+ * passing its own id. Threaded from `registerAgentTools`' `callerSessionId`
4768
+ * option; lower precedence than an explicit `parentSessionId` hint. */
4769
+ autoParentSessionId?: string;
4770
+ /** Task-board pin for the spawned child, stamped onto its descriptor as
4771
+ * `meta.boardId`. The Task ledger's board resolution prefers this over
4772
+ * the `parentSessionId` lineage walk (see task-ledger.ts's
4773
+ * `resolveBoardId`), so a client spawning several depth-0 roots — a
4774
+ * cowork/operator harness fanning out executors with no shared lineage —
4775
+ * can join them all onto ONE board. Descriptor-only: it never enters the
4776
+ * depth/quota/role gates, and an explicit `boardId` on a task verb still
4777
+ * wins over it. */
4778
+ boardId?: string;
4779
+ /** Source label for this spawn (channel/harness). Descriptor-only. */
4780
+ origin?: string;
4781
+ /** Installed app id this spawn runs on behalf of (`app_run` sets this to
4782
+ * the app's own id). Forwarded to the child process as `APP_ID_ENV` — a
4783
+ * daemon-tool proxy the child builds (mastra-agent's `daemon-mcp-tools.ts`,
4784
+ * P7) auto-injects it into an `app_*` tool call that omits `appId`, since
4785
+ * the model driving the session has no way to know its own appId unless
4786
+ * told. Absent on a spawn made outside `app_run`. */
4787
+ appId?: string;
4788
+ /** Reattach to a pre-existing adapter-native session (claude-code's
4789
+ * conversation id, hermes' chat handle, …) instead of starting
4790
+ * blank. Not exposed on the MCP `agent_start` tool today — only the
4791
+ * HTTP route (`sessions restart`) passes this. */
4792
+ resumeSessionId?: string;
4793
+ /** The caller's initial ask — a plain string (the common case, composed
4794
+ * with the role/AGENTS.md/RULES.md preamble below into one string turn),
4795
+ * or a content block / block array (an attachment-bearing first message,
4796
+ * same loose shape `POST /sessions/:id/prompt` accepts). The block form
4797
+ * is a narrow path: composing daemon-synthesized preamble TEXT onto a
4798
+ * turn that opens with an image isn't meaningful string concatenation,
4799
+ * so it SKIPS the whole preamble composition below (role disposition,
4800
+ * AGENTS.md, RULES.md, parent-lineage line, posture-as-prompt fallback)
4801
+ * and is sent to the adapter verbatim — see `effectivePrompt`'s doc.
4802
+ * Acceptable because this path is human/UI-initiated (an attachment
4803
+ * typed into a chat composer), not the orchestrator-driven role spawns
4804
+ * that actually depend on the composed disposition text. */
4805
+ prompt?: string | Record<string, unknown> | unknown[];
4806
+ label?: string;
4807
+ /** Explicit session title (SPEC-3 FIX C, `agentproto sessions start
4808
+ * --title`). When set, it wins over the first-sentence derivation from
4809
+ * `prompt` — so a CLI-named session isn't limited to whatever its opening
4810
+ * prompt happened to say. Unlike `label`, it fills the `title` slot, so a
4811
+ * spawner `label` still out-ranks it in the display chain. */
4812
+ title?: string;
4813
+ mode?: string;
4814
+ /** Manifest-declared option id → value map (AIP-45 `options`), applied
4815
+ * at spawn time alongside `mode`. Forwarded verbatim to the driver's
4816
+ * `startSession({ options })` → `composeSpawn`'s option patches. */
4817
+ options?: Record<string, boolean | number | string>;
4818
+ /** Normalized, adapter-agnostic skill ids for this session. Merged with
4819
+ * config.json's `defaults.skills` / `defaults.adapters.<slug>.skills`
4820
+ * (global < per-adapter < this field, which REPLACES the union rather
4821
+ * than merging into it when provided — a deliberate exact set). Folded
4822
+ * into `options.skills` per the resolved adapter's declared `skills`
4823
+ * option shape (e.g. hermes' comma-joined string); adapters with no
4824
+ * such option (e.g. claude-code, which auto-discovers skills) ignore
4825
+ * it. See `resolveSpawnDefaults` / `normalizeSkillsOption`. */
4826
+ skills?: string[];
4827
+ model?: string;
4828
+ effort?: string;
4829
+ /** Decomposed route identity. This is canonical transport; `mode` remains
4830
+ * a legacy adapter projection only. */
4831
+ route?: RouteSpec;
4832
+ /** Named billing credential. Resolved from auth-profiles + keychain at the
4833
+ * final spawn boundary; the secret never crosses HTTP/MCP. */
4834
+ access?: {
4835
+ profileRef?: string;
4836
+ };
4837
+ posture?: Posture;
4838
+ contextProfile?: ContextProfile;
4839
+ /** A preloaded user preset. Callers resolve its id at their boundary so a
4840
+ * preset can also provide the adapter before this core is invoked. */
4841
+ preset?: UserPreset;
4842
+ /**
4843
+ * Deterministic billing-auth mode + EXPLICIT credential for adapters that
4844
+ * declare an env-var vocabulary for it (today: claude-code — see
4845
+ * `AgentCliAuth.modes` in `@agentproto/driver-agent-cli`). `mode` wins over
4846
+ * `~/.agentproto/config.json`'s `defaults.adapters.<slug>.auth.mode`
4847
+ * (default `"subscription"`); the credential (`token` for `"subscription"`,
4848
+ * `apiKey` for `"api-key"`) wins over the config field matching the
4849
+ * RESOLVED mode — see `resolveSpawnDefaults`. Never read from the ambient
4850
+ * shell env; never logged (only a fingerprint is recorded on the session
4851
+ * descriptor — see `credentialFingerprint`). Adapters that don't declare
4852
+ * the vocabulary ignore this field entirely.
4853
+ */
4854
+ auth?: DefaultsAdapterAuthConfig;
4855
+ mcpServers?: AcpMcpServer[];
4856
+ orchestrator?: boolean | {
4857
+ tools?: string[];
4858
+ maxDepth?: number;
4859
+ maxChildren?: number;
4860
+ };
4861
+ notifyUrl?: string;
4862
+ wait?: boolean;
4863
+ maxCostUsd?: number;
4864
+ /** Windowed cost-budget cap (phase 4). DISTINCT from the scalar `maxCostUsd`
4865
+ * session-kill: this is recorded on the descriptor/runtime and drives an
4866
+ * auto-attached governance policy (`policy:failed` on windowed overage) — it
4867
+ * never kills the session. Omit ⇒ no windowed budget. */
4868
+ costBudget?: CostBudget;
4869
+ /** Opt-in auto-restart policy (restart-scheduler, PR-2). When set, an
4870
+ * unexpected death (`crashed` and/or `error`, per `restartPolicy.on`) is
4871
+ * proactively revived in place — see `RestartPolicy`'s doc in
4872
+ * `sessions.ts`. Omitted ⇒ today's lazy-resume-only behaviour. */
4873
+ restartPolicy?: RestartPolicy;
4874
+ /** Context-continuity policy for this session — controls warning,
4875
+ * opportunistic compaction, fresh-continuation, and hard-stop thresholds.
4876
+ * Resolved from global → per-adapter → per-model → explicit override. */
4877
+ contextContinuity?: ContextContinuityPolicy;
4878
+ /** Spawn-time role — `"executor"` | `"supervisor"` | omitted. See
4879
+ * `resolveRole` in `role.ts`. Omitted ⇒ derived from spawn depth
4880
+ * against `defaults.defaultRoleDepthCutoff` (default 1). A resolved
4881
+ * role with `toolPolicy.delegation === "deny"` is a HARD gate:
4882
+ * `orchestrator` is ignored outright and the hermes/default
4883
+ * full-gateway injection excludes `DELEGATION_TOOL_NAMES` — neither
4884
+ * this field's own request nor `promptAppend` can re-open it. */
4885
+ role?: string;
4886
+ /** One-off runtime text layered ON TOP of the resolved role's
4887
+ * disposition (never replacing it) and prepended to `prompt`. See
4888
+ * `composeRoleContext`. Cannot widen `toolPolicy` — see `role` above. */
4889
+ promptAppend?: string;
4890
+ /** Per-spawn override for the daemon self-mount's deferred/lazy
4891
+ * `tools/list` loading (harness-parity item 3 — see `deferred-tools.ts`).
4892
+ * Wins over the resolved role's own default (`RoleProfile.deferredTools`
4893
+ * — on for `executor`), which in turn only applies when this is
4894
+ * omitted. Threaded onto the injected `mcpServers` self-mount ref as
4895
+ * `?deferred=1|0` (`shouldInjectDaemonSelfMount` path below); has no
4896
+ * effect on a caller-supplied `mcpServers` (that URL is the caller's to
4897
+ * compose). Omitted entirely ⇒ no override at all — the gateway's own
4898
+ * boot-time `defaults.mcp.deferredTools` default applies unchanged. */
4899
+ deferredTools?: boolean;
4900
+ /** Give the agent its own isolated headless Chrome: a per-session
4901
+ * chrome-devtools-mcp stdio server appended to `mcpServers` (named
4902
+ * `browser`), plus a short prompt hint naming its tools. Wins over the
4903
+ * resolved role's `browser`, then the user preset's, then config
4904
+ * `defaults.spawn.browser`; all unset ⇒ off. Explicit `true` with a
4905
+ * `sandbox` spawn is rejected (no Chrome in the box image yet). */
4906
+ browser?: SpawnBrowserMode;
4907
+ /** Opt this session into Langfuse tracing (prompt/completion + tool spans +
4908
+ * tokens/cost). Effective opt-in is `trace ?? langfuseTracingDefault ?? false`
4909
+ * — see `SpawnAgentInput.trace` in sessions.ts. */
4910
+ trace?: boolean;
4911
+ /** Run this session inside a sandbox instead of on the host — a provider
4912
+ * slug from `list_sandbox_providers` (e.g. `"local"`, `"e2b"`), or an
4913
+ * inline AIP-36 `SandboxSpec`, optionally carrying `reuse: "<sandboxId>"`
4914
+ * to reconnect to an existing box instead of booting fresh. When set,
4915
+ * this branch resolves the provider, boots (or reconnects) the box,
4916
+ * spawns `adapter` on the BOX's own `agent_start`, and wires a
4917
+ * `SandboxAgentSessionProxy` in place of the local `resolveAgentAdapter`
4918
+ * path — see `sandbox-agent-session-proxy.ts`. */
4919
+ sandbox?: string | SandboxSpecInput;
4920
+ /** WP3 — serve an agentproto app's UI from INSIDE the sandbox box and
4921
+ * return its public URL on the descriptor + result. Requires `sandbox`
4922
+ * (a non-sandbox spawn carrying `appServe` is rejected): the app dir is
4923
+ * resolved INSIDE the box (e.g. `/home/user/apps/<slug>` on the
4924
+ * workstation image), installed via the box daemon's `app_install`, and
4925
+ * `agentproto app serve --host 0.0.0.0 --port <port>` is launched
4926
+ * detached through the box's `command_execute`. The port is appended to
4927
+ * the spec's `extraPorts` so the provider resolves its public URL (also
4928
+ * stamped into `sandboxPorts`), and the serve result lands on the
4929
+ * descriptor's `appServe` field. See `sandbox-app-serve.ts`. */
4930
+ appServe?: SandboxAppServeSpec;
4931
+ /** Isolate this session into its own git worktree instead of spawning in
4932
+ * `cwd` directly. `true` provisions a worktree with an auto-minted branch/
4933
+ * slug; `{ slug?, base? }` pins the slug and/or the base ref it's cut from.
4934
+ * Honoured only at spawn depth 0 (a nested spawn inherits the parent's
4935
+ * ground) and only for a `cwd` inside a git repo (nothing to isolate
4936
+ * otherwise ⇒ spawns plain). The daemon's `worktrees.isolation` policy can
4937
+ * force this on (`always`) or off (`never`) regardless — see
4938
+ * `worktree-isolation.ts`. Ignored entirely for a `sandbox` spawn (the box
4939
+ * already isolates). Cleanup is the human's job (`agentproto worktree
4940
+ * rm|archive|gc`); the tree is never auto-removed on session exit. */
4941
+ worktree?: WorktreeField;
4942
+ /** Acknowledge that spawning IN PLACE into a shared, dirty working tree is
4943
+ * intended, silencing the `sharedDirtyCwd` warning a nested implicit spawn
4944
+ * would otherwise emit (see `decideWorktreeIsolation`). Only meaningful for
4945
+ * a nested (depth > 0) spawn with no `worktree` request and no `sandbox`;
4946
+ * ignored everywhere else. Default false — the warning fires. */
4947
+ allowSharedCwd?: boolean;
4948
+ /** Start this session in permission-hold mode: every ACP permission request
4949
+ * is surfaced + parked in the cross-session inbox (`permissions_list` /
4950
+ * `permissions_respond`) instead of auto-answered. Threaded to the driver's
4951
+ * `startSession({ permissionHold })` and recorded on the descriptor. Ignored
4952
+ * for sandbox spawns (the box's own daemon owns permission handling).
4953
+ * Default false — unchanged auto-answer behaviour. */
4954
+ permissionHold?: boolean;
4955
+ /** Opt this child into the direct in-band crash notification: when it
4956
+ * crashes (`markCrashed`), the supervisor-notify subscriber
4957
+ * (`supervisor-notify.ts`) delivers a `[child-crashed] …` notice into its
4958
+ * `parentSessionId`, if any — enqueued immediately when the parent is
4959
+ * alive and idle, queued for its next turn when busy (never
4960
+ * interrupted). Recorded verbatim onto {@link SessionDescriptor
4961
+ * .notifyParentOnCrash}; see that field's doc. Default false — the free
4962
+ * external webhook path (`notifyUrl`) already fires regardless of this
4963
+ * flag. */
4964
+ notifyParentOnCrash?: boolean;
4965
+ /** Exempt this session from the idle-reaper (`isReapable` in
4966
+ * idle-reaper.ts) regardless of how long it sits idle. Stamped straight
4967
+ * onto the descriptor — see `SessionDescriptor.keepAlive`. Default false
4968
+ * — unchanged reap-eligible behaviour. Toggleable later via the
4969
+ * `session_set_keepalive` MCP verb. */
4970
+ keepAlive?: boolean;
4971
+ /** Caller-declared "this is the same logical spawn" token — a PROMISE, not
4972
+ * a guess. A second `agent_start` with the same `(adapter, cwd,
4973
+ * idempotencyKey)` within `SPAWN_CLAIM_WINDOW_MS` of a SUCCESSFUL spawn
4974
+ * returns that spawn's descriptor instead of forking a second process —
4975
+ * the fix for a retried call otherwise silently duplicating a live
4976
+ * agent. Always wins over the daemon's own derived (implicit) key when
4977
+ * both would apply — see `dedupe` and `spawn-dedupe.ts`. Omitting this
4978
+ * does NOT mean "spawn unconditionally" anymore: when the daemon's
4979
+ * `spawn.dedupe` policy is `"always"` (the default) and this spawn
4980
+ * carries a `label`, the daemon derives an implicit key in its place —
4981
+ * see `dedupe` below to opt out per-call. */
4982
+ idempotencyKey?: string;
4983
+ /** Per-call override for the daemon's `spawn.dedupe` policy (implicit
4984
+ * dedupe when no `idempotencyKey` was supplied — see `spawn-dedupe.ts`).
4985
+ * `false` is the escape hatch: never derive an implicit key for THIS
4986
+ * spawn, regardless of policy — mirrors `attach: false` / `worktree:
4987
+ * false`. `true` forces derivation even under an `"on-request"` policy,
4988
+ * mirroring `attach: true`. Omitted ⇒ the policy mode decides. Has no
4989
+ * effect when an explicit `idempotencyKey` is supplied (it already wins
4990
+ * outright) or when this spawn carries no `label` (nothing to derive
4991
+ * from either way). */
4992
+ dedupe?: boolean;
4993
+ /**
4994
+ * OS-level confinement for the adapter's OWN spawned process
4995
+ * (`@agentproto/command-sandbox` — macOS Seatbelt / Linux bubblewrap),
4996
+ * threaded verbatim to `resolved.startSession({ commandSandbox })` →
4997
+ * `AgentCliRuntime.start`. NOT the AIP-36 `sandbox` field above — that
4998
+ * boots a whole nested daemon on a different machine/box (remote-box
4999
+ * session provider); this wraps THIS host's spawn argv so the adapter's
5000
+ * own process tree can't read/write outside `cwd` (and, `"strict"`,
5001
+ * reach the network), confinement an ACP permission seam can never see
5002
+ * since it only covers tool calls the adapter chooses to report. Exposed
5003
+ * on the `agent_start` MCP tool schema (`agent-tools.ts`) as of PR 6b —
5004
+ * when omitted here, the driver falls back to the workspace's
5005
+ * `.agentproto/command-sandbox.json` `adapterSpawn` key (a key distinct
5006
+ * from the top-level `mode` that same file also carries for
5007
+ * `command_execute` — see `@agentproto/command-sandbox`'s module doc for
5008
+ * why the two axes are never shared), or stays unconfined if that's unset
5009
+ * too. Ignored for a `sandbox` spawn (the box's own daemon would need to
5010
+ * apply this itself).
5011
+ */
5012
+ commandSandbox?: SandboxMode;
5013
+ }
5014
+
3836
5015
  /**
3837
5016
  * Shared kernel for the daemon's worktree-status query surface.
3838
5017
  *
@@ -3853,9 +5032,28 @@ interface WorktreeStatusView {
3853
5032
  branch: string | null;
3854
5033
  class: "reclaim" | "salvage" | "hold";
3855
5034
  reclaimable: boolean;
5035
+ /** Uncommitted work in the tree (modified, staged or untracked paths). */
5036
+ dirty: boolean;
5037
+ /** Per-kind counts, present only when `dirty`. */
5038
+ changes?: {
5039
+ modified: number;
5040
+ staged: number;
5041
+ untracked: number;
5042
+ };
5043
+ /** Ahead/behind vs the base ref the integration check compares against
5044
+ * (the repo's default branch, e.g. `origin/main`). `null` for a detached
5045
+ * tip or when git couldn't resolve the base. */
5046
+ base: null | {
5047
+ ref: string;
5048
+ ahead: number;
5049
+ behind: number;
5050
+ };
5051
+ /** Integration state; `number` whenever the forge matched a PR (open,
5052
+ * merged, partial), `url` when the host knows the forge's web URL. */
3856
5053
  pr: null | {
3857
5054
  state: string;
3858
5055
  number?: number;
5056
+ url?: string;
3859
5057
  };
3860
5058
  sessions: Array<{
3861
5059
  id: string;
@@ -3869,13 +5067,24 @@ interface WorktreeStatusView {
3869
5067
  sessionCount: number;
3870
5068
  };
3871
5069
  }
5070
+ /** Narrows a lister call. `paths` computes only those worktrees (compared
5071
+ * resolved) instead of every worktree of the repo: one forge round-trip for
5072
+ * a per-session lookup, not one per worktree. */
5073
+ interface WorktreeStatusListOptions {
5074
+ paths?: readonly string[];
5075
+ }
3872
5076
  /** Injected port: the runtime asks the host to list worktrees for a repo. */
3873
- type WorktreeStatusLister = (repoRoot: string) => Promise<WorktreeStatusView[]>;
5077
+ type WorktreeStatusLister = (repoRoot: string, options?: WorktreeStatusListOptions) => Promise<WorktreeStatusView[]>;
5078
+ interface ToWorktreeStatusViewOptions {
5079
+ /** Web URL of PR `number` on this repo's forge, when the host can build
5080
+ * one (e.g. from a GitHub `origin` remote). */
5081
+ prUrl?: (number: number) => string | undefined;
5082
+ }
3874
5083
  /**
3875
5084
  * Pure projection from a raw `WorktreeStatusEntry` to the view the daemon
3876
5085
  * surfaces. Kept in one place so the MCP tool and HTTP route cannot drift.
3877
5086
  */
3878
- declare function toWorktreeStatusView(entry: unknown): WorktreeStatusView;
5087
+ declare function toWorktreeStatusView(entry: unknown, options?: ToWorktreeStatusViewOptions): WorktreeStatusView;
3879
5088
 
3880
5089
  /**
3881
5090
  * Shared kernel for the daemon's worktree-`gc` surface — the transport twin of
@@ -3910,7 +5119,7 @@ type WorktreeGcClass = "reclaim" | "salvage" | "hold";
3910
5119
  * registration is still there, but the working directory is gone (see
3911
5120
  * `WorktreeGcPlanEntryView.prunable`).
3912
5121
  */
3913
- type WorktreeGcReclaimReason = "dep-bump" | "orphan" | "prunable";
5122
+ type WorktreeGcReclaimReason = "dep-bump" | "orphan" | "prunable" | "squash-merged" | "patch-merged" | "content-merged";
3914
5123
  /**
3915
5124
  * One entry of the dry-run plan — a runtime-local projection of a
3916
5125
  * `GcPlanEntry`. `tree` / `integration` / `liveness` are flattened to their
@@ -4002,6 +5211,13 @@ interface WorktreeGcRunInput {
4002
5211
  * command) omits it and gets exactly today's behavior.
4003
5212
  */
4004
5213
  protectedPaths?: string[];
5214
+ /**
5215
+ * Worktree-relative paths whose dirt is known noise: a worktree dirty ONLY
5216
+ * on these reads clean (and is removed non-force after they are restored).
5217
+ * Omitted → the engine's default (`.opencode/package-lock.json`); `[]`
5218
+ * disables the allowlist.
5219
+ */
5220
+ noisePaths?: string[];
4005
5221
  }
4006
5222
  /**
4007
5223
  * Injected port: the runtime asks the host to plan (and, when `apply`, execute)
@@ -4011,6 +5227,161 @@ interface WorktreeGcRunInput {
4011
5227
  */
4012
5228
  type WorktreeGcRunner = (input: WorktreeGcRunInput) => Promise<WorktreeGcResult>;
4013
5229
 
5230
+ /**
5231
+ * Shared kernel for the daemon's branch-`gc` surface — the sibling of
5232
+ * `worktree-gc.ts`. The engine (`planBranchGc` / `applyBranchGc` /
5233
+ * `recordBranchVerdict`) lives in `@agentproto/worktree`, a dependency the
5234
+ * runtime deliberately does NOT take, so — exactly as the worktree gc surface
5235
+ * does — the runtime only defines runtime-local structural result types and
5236
+ * the injected ports. The CLI host wires the real runners. Repo-root
5237
+ * resolution is shared with the worktree surfaces via
5238
+ * `resolveWorktreeQueryRoot` in `worktree-status.ts`.
5239
+ */
5240
+ type BranchGcKind = "local" | "remote" | "orphan";
5241
+ type BranchGcClass = "reclaim" | "review" | "hold";
5242
+ type BranchGcStatus = "pr-merged" | "merged" | "squash-merged" | "patch-merged" | "content-merged" | "unmerged";
5243
+ type BranchGcReclaimReason = Exclude<BranchGcStatus, "unmerged"> | "reviewed";
5244
+ type BranchGcHoldReason = "protected" | "worktree" | "open-pr" | "pr-check-unavailable" | "young";
5245
+ type BranchGcTriageVerdict = "obsolete" | "superseded" | "salvage" | "in-progress" | "unclear";
5246
+ interface BranchGcCoverageView {
5247
+ residual: number;
5248
+ covered: number;
5249
+ ignored: number;
5250
+ evolved: number;
5251
+ unique: number;
5252
+ deletes: number;
5253
+ }
5254
+ /** One classified ref — mirrors `BranchGcPlanEntry` structurally. */
5255
+ interface BranchGcPlanEntryView {
5256
+ kind: BranchGcKind;
5257
+ name: string;
5258
+ ref: string;
5259
+ sha: string;
5260
+ date: string;
5261
+ author: string;
5262
+ subject: string;
5263
+ remote?: string;
5264
+ status: BranchGcStatus;
5265
+ history: "current" | "pre-rewrite" | "unrelated";
5266
+ ahead: number | null;
5267
+ behind: number | null;
5268
+ conflicts?: boolean;
5269
+ compareBase?: string;
5270
+ mergeBase?: string;
5271
+ mergedTree?: string | null;
5272
+ coverage?: BranchGcCoverageView;
5273
+ residualFiles?: string[];
5274
+ residualFileCount?: number;
5275
+ ageDays: number;
5276
+ class: BranchGcClass;
5277
+ reclaimReason?: BranchGcReclaimReason;
5278
+ mergedPr?: number;
5279
+ holdReason?: BranchGcHoldReason;
5280
+ holdDetail?: string;
5281
+ pushed?: string;
5282
+ verdict?: {
5283
+ triage: BranchGcTriageVerdict;
5284
+ agree: boolean | null;
5285
+ reviewer: string;
5286
+ };
5287
+ }
5288
+ interface BranchGcPlanView {
5289
+ repoRoot: string;
5290
+ repoName: string;
5291
+ base: string;
5292
+ baseSha: string;
5293
+ baseTree: string;
5294
+ remote: string | null;
5295
+ anchor: string | null;
5296
+ prCheck: {
5297
+ available: boolean;
5298
+ reason?: string;
5299
+ };
5300
+ scopes: BranchGcKind[];
5301
+ minAgeDays: number;
5302
+ includeReviewed: boolean;
5303
+ generatedAt: string;
5304
+ otherRemoteRefs: number;
5305
+ /** true = base-remote `fetch --prune` succeeded; false = failed; null = skipped / no remote. */
5306
+ fetched: boolean | null;
5307
+ entries: BranchGcPlanEntryView[];
5308
+ }
5309
+ /** kind → class → count, and kind → (status | "protected") → count. */
5310
+ interface BranchGcSummaryView {
5311
+ byClass: Record<BranchGcKind, Record<BranchGcClass, number>>;
5312
+ byStatus: Record<BranchGcKind, Record<string, number>>;
5313
+ }
5314
+ interface BranchGcOutcomeView {
5315
+ kind: BranchGcKind;
5316
+ name: string;
5317
+ sha: string;
5318
+ result: "deleted" | "held" | "skipped-review" | "aborted-moved" | "aborted-vanished" | "aborted-reclassified" | "failed";
5319
+ reclaimReason?: BranchGcReclaimReason;
5320
+ holdReason?: BranchGcHoldReason;
5321
+ currentSha?: string;
5322
+ from?: BranchGcClass;
5323
+ to?: BranchGcClass;
5324
+ message?: string;
5325
+ }
5326
+ type BranchGcResult = {
5327
+ mode: "plan";
5328
+ plan: BranchGcPlanView;
5329
+ summary: BranchGcSummaryView;
5330
+ } | {
5331
+ mode: "apply";
5332
+ plan: BranchGcPlanView;
5333
+ summary: BranchGcSummaryView;
5334
+ outcomes: BranchGcOutcomeView[];
5335
+ /** Restore log written before the first delete; `null` when nothing was deleted. */
5336
+ restoreLog: string | null;
5337
+ };
5338
+ /** Input to the injected runner. `apply` defaults to false at the tool/route
5339
+ * boundary — a bare call is a dry run — and an apply requires `scopes`. */
5340
+ interface BranchGcRunInput {
5341
+ repoRoot: string;
5342
+ apply: boolean;
5343
+ base?: string;
5344
+ scopes?: BranchGcKind[];
5345
+ minAgeDays?: number;
5346
+ includeReviewed: boolean;
5347
+ anchor?: string;
5348
+ }
5349
+ type BranchGcRunner = (input: BranchGcRunInput) => Promise<BranchGcResult>;
5350
+ interface BranchGcVerdictInput {
5351
+ repoRoot: string;
5352
+ /** Validated by the engine (`branchVerdictSchema`) — the runtime only shapes it. */
5353
+ verdict: unknown;
5354
+ }
5355
+ interface BranchGcVerdictRecordView {
5356
+ repo: string;
5357
+ name: string;
5358
+ sha: string;
5359
+ recordedAt: string;
5360
+ triage: {
5361
+ verdict: BranchGcTriageVerdict;
5362
+ confidence: number;
5363
+ reason: string;
5364
+ salvage?: string;
5365
+ };
5366
+ gate?: {
5367
+ agree: boolean;
5368
+ verdict: BranchGcTriageVerdict;
5369
+ reason: string;
5370
+ evidence: string[];
5371
+ };
5372
+ reviewer: string;
5373
+ }
5374
+ /** Injected port: validate + store one reviewer verdict, keyed by repo + tip sha. Throws on an invalid verdict. */
5375
+ type BranchGcVerdictRecorder = (input: BranchGcVerdictInput) => Promise<BranchGcVerdictRecordView>;
5376
+ interface BranchGcVerdictLookupInput {
5377
+ repoRoot: string;
5378
+ /** Full tip sha — the store's key, together with the repo. */
5379
+ sha: string;
5380
+ }
5381
+ /** Injected port: the stored verdict for this exact tip, or `null` — a read-only
5382
+ * lookup, so a caller can check one tip without re-running a whole `branch_gc` plan. */
5383
+ type BranchGcVerdictReader = (input: BranchGcVerdictLookupInput) => Promise<BranchGcVerdictRecordView | null>;
5384
+
4014
5385
  /**
4015
5386
  * Pluggable adapter resolver — keeps the runtime package free of any
4016
5387
  * @agentproto/cli dep. The host (cli `serve`, playground, embedding
@@ -4136,12 +5507,10 @@ type AgentAdapterResolver = (slug: string) => Promise<{
4136
5507
  }): Promise<AgentSessionLike>;
4137
5508
  /** Display label for the descriptor's `command` field. */
4138
5509
  commandPreview?: string;
4139
- /** Best-effort per-session usage reader (adapter-specific, e.g. hermes state.db). */
4140
- readUsage?: (adapterSessionId: string) => Promise<{
4141
- costUsd?: number;
4142
- tokensIn?: number;
4143
- tokensOut?: number;
4144
- } | null>;
5510
+ /** Best-effort per-session usage reader (adapter-specific, e.g. hermes
5511
+ * state.db, the claude-code transcript JSONL). `ctx` carries the spawn's
5512
+ * cwd + isolated config dir for readers that locate their store by them. */
5513
+ readUsage?: (adapterSessionId: string, ctx?: UsageReadContext) => Promise<UsageReadResult | null>;
4145
5514
  /** AIP-45 `options[]` this adapter's manifest declares (id + type only —
4146
5515
  * no spawn internals). Lets `session-spawn.ts` fold a config-level
4147
5516
  * `defaults.skills` list into `options.skills` using the shape the
@@ -4319,6 +5688,142 @@ interface AuthOptions {
4319
5688
  token?: string;
4320
5689
  }
4321
5690
 
5691
+ /** One background task as mirrored onto `SessionDescriptor.backgroundTasks`. */
5692
+ interface SessionBackgroundTask {
5693
+ taskId: string;
5694
+ /** Friendly task type (`"shell"`, `"monitor"`, ...), when the agent says. */
5695
+ taskKind?: string;
5696
+ description?: string;
5697
+ /** Where the task writes its output, when the agent says. */
5698
+ outputFile?: string;
5699
+ status: "running" | "paused" | "completed" | "failed" | "stopped";
5700
+ summary?: string;
5701
+ toolCallId?: string;
5702
+ /** ISO 8601 — when the daemon first heard of the task. */
5703
+ startedAt: string;
5704
+ }
5705
+
5706
+ /**
5707
+ * Typed inter-session message envelope (AIP-46 §Session messages).
5708
+ *
5709
+ * A PROMPT is an instruction from whoever drives a session; a MESSAGE is a
5710
+ * report between sessions in the same tree whose sender the DAEMON attests.
5711
+ * Everything in `from` is computed here from the caller's verified identity
5712
+ * and the session forest — a send request can never set it.
5713
+ *
5714
+ * Pure module: no registry, no I/O. The registry (sessions.ts) carries a
5715
+ * `SessionMessage` on a queued prompt (`QueuedPrompt.envelope`), renders it
5716
+ * into the model's context with `renderSessionMessages`, and records it as
5717
+ * its own `session-message` transcript record — never a `user-prompt`.
5718
+ */
5719
+ /** Sender's position in the tree, relative to the recipient. */
5720
+ type MessageRelation = "child" | "parent" | "sibling" | "human" | "system";
5721
+ type MessageKind = "report" | "question" | "blocker" | "done" | "notice";
5722
+ type MessageUrgency = "fyi" | "next-turn" | "steer" | "interrupt";
5723
+ type MessageDeliveryVia = "wait" | "steer" | "turn" | "interrupt" | "inbox";
5724
+ declare const MESSAGE_KINDS: readonly MessageKind[];
5725
+ declare const MESSAGE_URGENCIES: readonly MessageUrgency[];
5726
+ interface SessionMessageFrom {
5727
+ /** Absent only for relation `human` | `system`. */
5728
+ sessionId?: string;
5729
+ /** Registry label at send time — display only, never identity. */
5730
+ label?: string;
5731
+ role?: string;
5732
+ adapter?: string;
5733
+ relation: MessageRelation;
5734
+ }
5735
+ interface SessionMessage {
5736
+ /** `msg_<uuid8>`, daemon-minted. */
5737
+ id: string;
5738
+ ts: string;
5739
+ /** Recipient sessionId. */
5740
+ to: string;
5741
+ from: SessionMessageFrom;
5742
+ kind: MessageKind;
5743
+ /** Requested urgency. */
5744
+ urgency: MessageUrgency;
5745
+ /** Stamped on delivery. */
5746
+ delivered?: {
5747
+ via: MessageDeliveryVia;
5748
+ at: string;
5749
+ turnSeq?: number;
5750
+ };
5751
+ /** Thread id; defaults to the first message's id. */
5752
+ correlationId?: string;
5753
+ /** A `SessionMessage.id` this answers. */
5754
+ replyTo?: string;
5755
+ text: string;
5756
+ /** Optional structured payload (≤ `MAX_MESSAGE_DATA_BYTES`). */
5757
+ data?: Record<string, unknown>;
5758
+ ackedAt?: string;
5759
+ }
5760
+ declare const MAX_MESSAGE_DATA_BYTES: number;
5761
+ /** The slice of a session descriptor relation/ACL resolution needs. */
5762
+ interface MessageTreeNode {
5763
+ id: string;
5764
+ parentSessionId?: string;
5765
+ label?: string;
5766
+ role?: string;
5767
+ adapterSlug?: string;
5768
+ }
5769
+ /**
5770
+ * Relation of `sender` to `recipient` in the session forest, or `undefined`
5771
+ * when they're not tree neighbours (cousins, strangers, self). `sender`
5772
+ * absent ⇒ `human` (an HTTP/CLI operator); daemon-originated notices pass
5773
+ * `"system"` explicitly instead of calling this.
5774
+ */
5775
+ declare function resolveRelation(sender: MessageTreeNode | undefined, recipient: MessageTreeNode): Exclude<MessageRelation, "system"> | undefined;
5776
+ /**
5777
+ * ACL: child→parent, parent→direct child, human/system→any. Sibling↔sibling
5778
+ * only when `allowSiblings` (config-gated, default off). Anything else — a
5779
+ * cousin, a stranger, yourself — is refused.
5780
+ */
5781
+ declare function isMessageAllowed(relation: MessageRelation | undefined, opts?: {
5782
+ allowSiblings?: boolean;
5783
+ }): relation is MessageRelation;
5784
+ /** Build a `from` block from the verified sender + the resolved relation. */
5785
+ declare function messageFrom(sender: MessageTreeNode | undefined, relation: MessageRelation): SessionMessageFrom;
5786
+ declare function mintMessageId(): string;
5787
+ declare function createSessionMessage(input: {
5788
+ to: string;
5789
+ from: SessionMessageFrom;
5790
+ text: string;
5791
+ kind?: MessageKind;
5792
+ urgency?: MessageUrgency;
5793
+ replyTo?: string;
5794
+ correlationId?: string;
5795
+ data?: Record<string, unknown>;
5796
+ }): SessionMessage;
5797
+ interface MessageFilter {
5798
+ /** Sender session ids, or `"children"` for any message whose relation is
5799
+ * `child` (i.e. from one of the recipient's own children). */
5800
+ from?: readonly string[] | "children";
5801
+ kind?: readonly MessageKind[];
5802
+ correlationId?: string;
5803
+ }
5804
+ declare function matchesMessageFilter(msg: SessionMessage, filter: MessageFilter): boolean;
5805
+ declare const MESSAGE_TAG = "agentproto-message";
5806
+ /** Child-settable label → attribute-safe: `[A-Za-z0-9._-]`, ≤ 48 chars. */
5807
+ declare function sanitizeLabel(label: string | undefined): string | undefined;
5808
+ /** Escape every envelope/body sentinel inside a message body, so a sender
5809
+ * can't close the tag early and forge a second header. */
5810
+ declare function escapeMessageBody(text: string): string;
5811
+ /** A human prompt is NOT wrapped (humans keep today's UX), but a line that
5812
+ * starts with the envelope sentinel is escaped, so the invariant "text
5813
+ * outside an `<agentproto-message>` tag is the human" holds. Nothing else
5814
+ * in human text changes. */
5815
+ declare function escapeHumanPrompt(text: string): string;
5816
+ declare function renderSessionMessage(msg: SessionMessage): string;
5817
+ /** A coalesced batch → sibling tags, one per message, in order. */
5818
+ declare function renderSessionMessages(msgs: readonly SessionMessage[]): string;
5819
+ /** One-line typed digest of `fyi` messages that never woke the session —
5820
+ * prepended (as a `system-prompt` slice) to the next turn it runs anyway. */
5821
+ declare function renderInboxDigest(msgs: readonly SessionMessage[]): string;
5822
+ /** Taught once, the first time a session RECEIVES a message (recorded as a
5823
+ * `system-prompt`). Children also get the same invariant in their lineage
5824
+ * line at spawn. */
5825
+ declare const MESSAGE_PREAMBLE: string;
5826
+
4322
5827
  /**
4323
5828
  * A per-session observer — the pluggable seam behind the runtime's transcript
4324
5829
  * tap. `sessions.ts` drives one observer per turn (prompt in, each stream event,
@@ -4330,15 +5835,33 @@ interface AuthOptions {
4330
5835
  * calls, so `TranscriptWriter` IS a `SessionObserver` (see transcript-writer.ts).
4331
5836
  */
4332
5837
 
5838
+ /** The sender-side trace of a typed message (`session-message-sent`). */
5839
+ interface SessionMessageSentRecord {
5840
+ messageId: string;
5841
+ to: string;
5842
+ kind: MessageKind;
5843
+ urgency: MessageUrgency;
5844
+ }
4333
5845
  interface SessionObserver {
4334
5846
  /** Record the outgoing message that opens a new turn. `opts.source`, when
4335
5847
  * set, is the prompt's provenance — `agent:<sessionId>` for a prompt
4336
5848
  * injected by another session (`agent_prompt` from a supervisor), absent
4337
5849
  * for a human operator — so a transcript view can attribute the turn to
4338
- * its real author instead of "you". */
5850
+ * its real author instead of "you". `opts.messages`, when set, marks a
5851
+ * typed-message turn: the writer records one `session-message` per
5852
+ * message instead of a `user-prompt`. */
4339
5853
  recordPrompt(sessionId: string, message: unknown, opts?: {
4340
5854
  source?: string;
5855
+ system?: string;
5856
+ messages?: readonly SessionMessage[];
4341
5857
  }): void;
5858
+ /** Record, in the SENDER's transcript, that it sent a typed message.
5859
+ * Optional — observers without a transcript of their own skip it. */
5860
+ recordSessionMessageSent?(sessionId: string, record: SessionMessageSentRecord): void;
5861
+ /** Record a typed message delivered into `sessionId`'s context OUTSIDE a
5862
+ * turn prompt (e.g. as an `inbox_wait` result) — a standalone
5863
+ * `session-message` record. Optional, like `recordSessionMessageSent`. */
5864
+ recordSessionMessage?(sessionId: string, message: SessionMessage): void;
4342
5865
  /** Record one structured stream event (text-delta, tool-call, usage_update, …). */
4343
5866
  recordEvent(sessionId: string, evt: AgentStreamEvent): void;
4344
5867
  /** Record the durable turn-boundary / exit usage snapshot. */
@@ -4361,6 +5884,106 @@ interface SessionObserver {
4361
5884
  */
4362
5885
  declare function composeSessionObservers(observers: readonly SessionObserver[]): SessionObserver;
4363
5886
 
5887
+ /** Where a session's `contextSize` came from — see the module doc. */
5888
+ type ContextSizeSource = "adapter" | "catalog" | "reported";
5889
+
5890
+ /**
5891
+ * Derived session outcome (Level 1) — what an ended agent-cli session
5892
+ * PRODUCED, recorded by the daemon with zero agent cooperation.
5893
+ *
5894
+ * Two axes, never conflated:
5895
+ * - termination — HOW the session stopped (the descriptor's existing
5896
+ * `status` + `endedReason`), copied verbatim into `outcome.termination`;
5897
+ * - outcome — WHAT it produced: its last assistant message, the PRs it
5898
+ * opened, what it cost, and which run / parent it belongs to.
5899
+ *
5900
+ * So a killed or idle-reaped session still shows what it said and did,
5901
+ * instead of a bare "killed".
5902
+ *
5903
+ * Pure by construction (plus one bounded sync tail-read helper for boot
5904
+ * reconcile): no LLM call, no full transcript scan. The registry calls
5905
+ * {@link deriveSessionOutcome} from its single exit funnel (`emitExited`)
5906
+ * and from boot reconcile (`loadHistorySnapshot`).
5907
+ */
5908
+
5909
+ interface SessionOutcomeArtifact {
5910
+ type: "pr" | "commit" | "url";
5911
+ /** The artifact's canonical reference — a PR url, a commit sha, a url. */
5912
+ ref: string;
5913
+ title?: string;
5914
+ }
5915
+ interface SessionOutcomeLink {
5916
+ /** `run` — the workflow run this session was a step of (`ref` = run id,
5917
+ * `title` = `<workflowId>/<stepId>`); `parent` — the orchestrator session
5918
+ * that spawned it; `review` — reserved for the review ledger (Level 2). */
5919
+ rel: "run" | "parent" | "review";
5920
+ ref: string;
5921
+ title?: string;
5922
+ }
5923
+ interface SessionOutcome {
5924
+ /** Level 1 only ever writes `"derived"`; a declared outcome (Level 2) is a
5925
+ * later addition. */
5926
+ source: "derived";
5927
+ /** `produced` — the session said something or left an artifact;
5928
+ * `empty` — no assistant text and no artifacts. */
5929
+ status: "produced" | "empty";
5930
+ /** Last assistant message of the session, trimmed to
5931
+ * {@link OUTCOME_SUMMARY_MAX} chars (the TAIL is kept — the conclusion,
5932
+ * not the preamble). Absent when the session never said anything. */
5933
+ summary?: string;
5934
+ /** Copy of the descriptor's end state at the moment the outcome was
5935
+ * recorded — the termination axis, never mixed into `status`. */
5936
+ termination: {
5937
+ status: string;
5938
+ /** Copy of `SessionDescriptor.endedReason` — see {@link SessionEndReason}
5939
+ * for the full, single-source enum of values (operator kill, cost cap,
5940
+ * provider limit, …). Absent for a plain natural exit or an ordinary
5941
+ * turn error this file doesn't tag. A value outside the known enum is
5942
+ * still valid (a newer/older daemon) — never gate rendering on it being
5943
+ * a member; see `isKnownSessionEndReason`. */
5944
+ reason?: SessionEndReason;
5945
+ /** The reason this row carried BEFORE an operator's "mark as completed"
5946
+ * (`kill(id, signal, "operator-completed")` on an already-terminal
5947
+ * session — the UI's affordance for relabeling a finished-but-not-
5948
+ * operator-tagged row) overwrote `reason` with `"operator-completed"`.
5949
+ * Set ONLY by that relabel, so the true original mechanism (a natural
5950
+ * exit with no reason, `"crashed"`, `"idle-reaped"`, …) is never lost
5951
+ * under the operator's override. Absent otherwise. */
5952
+ previousReason?: SessionEndReason;
5953
+ exitCode?: number;
5954
+ /** True when the session was killed with a turn in flight. */
5955
+ midTurn?: boolean;
5956
+ };
5957
+ cost?: {
5958
+ usd?: number;
5959
+ tokensIn?: number;
5960
+ tokensOut?: number;
5961
+ durationMs?: number;
5962
+ };
5963
+ artifacts?: SessionOutcomeArtifact[];
5964
+ links?: SessionOutcomeLink[];
5965
+ recordedAt: string;
5966
+ }
5967
+ /** The compact list projection of an outcome (`session_list` default). */
5968
+ interface SessionOutcomeCompact {
5969
+ status: SessionOutcome["status"];
5970
+ summary?: string;
5971
+ }
5972
+ interface DeriveSessionOutcomeInput {
5973
+ /** The last assistant message the registry observed (raw, untrimmed). */
5974
+ lastAssistantText?: string;
5975
+ now?: Date;
5976
+ }
5977
+ /**
5978
+ * Build the derived outcome for a session that has just reached a terminal
5979
+ * status. Reads only the descriptor + the caller-supplied last assistant
5980
+ * text; never touches disk.
5981
+ */
5982
+ declare function deriveSessionOutcome(desc: SessionDescriptor, input?: DeriveSessionOutcomeInput): SessionOutcome;
5983
+ /** The compact projection: status + the first
5984
+ * {@link OUTCOME_COMPACT_SUMMARY_MAX} chars of the summary. */
5985
+ declare function compactOutcome(o: SessionOutcome | undefined): SessionOutcomeCompact | undefined;
5986
+
4364
5987
  /**
4365
5988
  * Minimal shape we need from a driver-agent-cli session — kept as a
4366
5989
  * structural type so the runtime package doesn't take a hard
@@ -4384,6 +6007,12 @@ interface AgentSessionLike {
4384
6007
  * drivers that don't model held permissions (sandbox proxy, future
4385
6008
  * transports). */
4386
6009
  respondPermission?(requestId: string, resolution: AcpPermissionResolution): boolean | Promise<boolean>;
6010
+ /** Subscribe to events the agent emits while no turn is in flight — its
6011
+ * own wake-up when a background task settles, and the task lifecycle
6012
+ * itself. Mirrors `@agentproto/driver-agent-cli`'s
6013
+ * `AgentCliRuntimeSession.onOutOfTurnEvent`. Returns an unsubscribe
6014
+ * function. Absent for drivers that can't produce such events. */
6015
+ onOutOfTurnEvent?(listener: (event: AgentStreamEvent) => void): () => void;
4387
6016
  /**
4388
6017
  * Switch the active model on this LIVE session — mirrors
4389
6018
  * `@agentproto/driver-agent-cli`'s `AgentCliRuntimeSession.setModel`
@@ -4420,6 +6049,15 @@ interface AgentSessionLike {
4420
6049
  * routes to restart.
4421
6050
  */
4422
6051
  readonly availableModes?: readonly SessionMode[];
6052
+ /** Whether the agent accepts steering — mirrors
6053
+ * `@agentproto/driver-agent-cli`'s `AgentCliRuntimeSession.steeringSupported`
6054
+ * (ACP `_session/steering`, advertised at initialize). */
6055
+ readonly steeringSupported?: boolean;
6056
+ /** Inject content into the turn in flight — mirrors
6057
+ * `AgentCliRuntimeSession.steer`: `"steered"` (injected), `"promptRequired"`
6058
+ * (no host turn in flight — deliver as a normal prompt), `"unsupported"`.
6059
+ * Never throws. Absent for sessions that can't steer. */
6060
+ steer?(content: unknown): Promise<"steered" | "promptRequired" | "unsupported">;
4423
6061
  close(): Promise<void>;
4424
6062
  }
4425
6063
  /** Result of `SessionsRegistry.setModel` — see that method's doc comment. */
@@ -4586,6 +6224,16 @@ interface AgentStreamEvent {
4586
6224
  * but no `cost`). */
4587
6225
  tokensIn?: number;
4588
6226
  tokensOut?: number;
6227
+ /** "usage_update" model the usage belongs to, when the adapter reports it
6228
+ * (claude-agent-acp's `_meta["_claude/model"]`) — see `context-window.ts`. */
6229
+ model?: string;
6230
+ /** "usage_update" `size` is the adapter's own guess, corrected later by a
6231
+ * cost-bearing frame (claude-agent-acp) — see `context-window.ts`. */
6232
+ sizeInferred?: boolean;
6233
+ /** "usage_update" size as the adapter reported it, recorded only when the
6234
+ * daemon corrected `size` (an inferred window superseded by the catalog or
6235
+ * an earlier authoritative frame) — see `context-window.ts`. */
6236
+ reportedSize?: number;
4589
6237
  /** "permission-resolved" outcome for the "agent-prompt" it answers (same
4590
6238
  * `toolCallId`) — mirrors `session:permission-resolved`'s `decision` so
4591
6239
  * the durable transcript can tell an answered ask from a still-pending
@@ -4612,6 +6260,24 @@ interface AgentStreamEvent {
4612
6260
  qualifiedName?: string;
4613
6261
  };
4614
6262
  }>;
6263
+ /** "background-task" lifecycle edge — see @agentproto/acp's `StreamEvent`'s
6264
+ * `background-task` kind. */
6265
+ phase?: "started" | "updated" | "settled";
6266
+ /** "background-task" task snapshot — see @agentproto/acp's
6267
+ * `BackgroundTaskInfo`. Only `taskId` is guaranteed. */
6268
+ task?: {
6269
+ taskId: string;
6270
+ taskKind?: string;
6271
+ description?: string;
6272
+ outputFile?: string;
6273
+ status?: SessionBackgroundTask["status"];
6274
+ summary?: string;
6275
+ toolCallId?: string;
6276
+ };
6277
+ /** "usage_update" origin of the cycle it closes (claude-agent-acp's
6278
+ * `_meta["_claude/origin"].kind`, e.g. `"task-notification"` for an
6279
+ * autonomous wake) — see @agentproto/acp's `StreamEvent`. */
6280
+ origin?: string;
4615
6281
  }
4616
6282
  /**
4617
6283
  * Env vars the registry injects into every process it spawns on a session's
@@ -4732,6 +6398,13 @@ interface QueuedPrompt {
4732
6398
  * daemon restart. Absent for legacy queued items — `promptOriginLabel`
4733
6399
  * falls back to `source`, then `"user"`. */
4734
6400
  origin?: string;
6401
+ /** Set when this item is a typed inter-session message (`message_parent`,
6402
+ * …) rather than a prompt: the daemon-attested envelope. `message` then
6403
+ * only carries `envelope.text` (for `previewPrompt`); the turn is built
6404
+ * from the envelope — rendered as an `<agentproto-message>` tag, recorded
6405
+ * as a `session-message` transcript record, and coalesced with any
6406
+ * envelope items queued right behind it (never with a plain prompt). */
6407
+ envelope?: SessionMessage;
4735
6408
  }
4736
6409
  /** What `enqueuePrompt` resolved to — lets a caller (e.g. MCP `agent_prompt`)
4737
6410
  * tell the user whether the prompt was delivered/started now or PARKED behind
@@ -4743,6 +6416,22 @@ interface QueuedPrompt {
4743
6416
  interface EnqueuePromptResult {
4744
6417
  queued: boolean;
4745
6418
  }
6419
+ /** What `sendMessage` did with a message — truthful, per AIP-46. */
6420
+ interface SendMessageResult {
6421
+ messageId: string;
6422
+ /** Set when the message already reached the recipient's context (a
6423
+ * waiter, a dispatched turn, an interrupt) or was parked as `fyi`
6424
+ * (`via: "inbox"`); `null` while it's queued behind a busy turn. */
6425
+ delivered: {
6426
+ via: MessageDeliveryVia;
6427
+ } | null;
6428
+ queued: boolean;
6429
+ urgencyApplied: MessageUrgency;
6430
+ }
6431
+ interface WaitForMessagesResult {
6432
+ messages: SessionMessage[];
6433
+ timedOut: boolean;
6434
+ }
4746
6435
  /** One entry in the after-the-fact queue listing (`listQueuedPrompts` /
4747
6436
  * `session_queue_list` / `GET /sessions/:id/queue`). `position` is the
4748
6437
  * array index (0 = next to dispatch). */
@@ -4756,6 +6445,17 @@ interface QueuedPromptView {
4756
6445
  queuedAt: string;
4757
6446
  position: number;
4758
6447
  }
6448
+ /** Provenance stamped onto a `session_continue_fresh` target by
6449
+ * {@link SessionDescriptor.handoff} — which harness the checkpoint moved
6450
+ * from/to and when. `fromHarness === toHarness` for a same-harness
6451
+ * continuation; they differ for a genuine cross-harness handoff
6452
+ * (claude-code -> opencode, etc). */
6453
+ interface SessionHandoff {
6454
+ fromHarness: string;
6455
+ toHarness: string;
6456
+ /** ISO 8601 timestamp the handoff spawn completed. */
6457
+ at: string;
6458
+ }
4759
6459
  interface SessionDescriptor {
4760
6460
  id: string;
4761
6461
  kind: SessionKind;
@@ -4778,30 +6478,36 @@ interface SessionDescriptor {
4778
6478
  startedAt: string;
4779
6479
  endedAt?: string;
4780
6480
  exitCode?: number;
4781
- /** Set alongside `status: "killed"` when the session ended NOT because an
4782
- * operator targeted it (`kill()`) but because of an AUTOMATIC teardown:
4783
- * - `"daemon-restart"` — the daemon process it lived in went away out from
4784
- * under it: a hard crash discovered at next boot
4785
- * (`loadHistorySnapshot`'s wasAlive reclassification), or a graceful
4786
- * shutdown/restart that force-kills whatever's still busy
4787
- * (`shutdownImpl`).
4788
- * - `"idle-reaped"` — the idle-session reaper (`reapIdle`, PR-6) retired a
4789
- * long-idle agent-cli row to free its adapter process. Deliberately kept
4790
- * lazy-resumable (adapterSessionId/cwd intact) so a later prompt revives
4791
- * it, but NEVER eager-resumed on boot — the eager pass (#638) gates on
4792
- * `endedReason === "daemon-restart"`, so an `"idle-reaped"` row is
4793
- * naturally excluded and a resume-storm of dead work is avoided.
4794
- * - `"crashed"` — the crash-detect sweep (`markCrashed`) found the
4795
- * adapter's OS process gone between turns, with no exit event ever
4796
- * emitted for it (agent-cli holds its child alive across turns, so a
4797
- * death outside a turn is otherwise silent until the next RPC throws).
4798
- * `status` is set to `"error"` (not `"killed"`) since nothing targeted
4799
- * it; see `lastError`/`crashedAt` for detail.
4800
- * Absent for every other terminal path (operator kill, natural exit, turn
4801
- * error) — the session's own fault, or at least not an automatic sweep's.
4802
- * Lets the UI show "crashed with the daemon" / "reaped while idle" instead
4803
- * of a bare "killed" that reads as deliberate. */
4804
- endedReason?: "daemon-restart" | "idle-reaped" | "crashed";
6481
+ /** WHY the session ended — every stable value + its meaning is declared
6482
+ * once, in {@link SessionEndReason} (`session-end-reason.ts`), so a UI can
6483
+ * render a label for each without chasing the call site that set it.
6484
+ * Three families:
6485
+ * - automatic daemon-lifecycle teardown (unchanged, pre-existing):
6486
+ * `"daemon-restart"` (the daemon process it lived in went away out from
6487
+ * under it — a hard crash discovered at next boot, or a graceful
6488
+ * shutdown/restart that force-kills whatever's still busy),
6489
+ * `"idle-reaped"` (the idle-session reaper retired a long-idle row —
6490
+ * deliberately kept lazy-resumable but NEVER eager-resumed on boot,
6491
+ * since the eager pass gates on `endedReason === "daemon-restart"`),
6492
+ * `"crashed"` (the crash-detect sweep found the adapter's OS process
6493
+ * gone between turns with no exit event ever emitted for it — `status`
6494
+ * is `"error"`, not `"killed"`, since nothing targeted it);
6495
+ * - operator-issued kill: `"operator-completed"` / `"operator-stopped"`,
6496
+ * set ONLY by an explicit `POST /sessions/:id/kill` or the `agent_kill`
6497
+ * MCP tool (never by an internal automatic teardown), so it reads as
6498
+ * deliberate rather than random;
6499
+ * - other internal/automatic reasons: `"cost-cap-exceeded"` (the
6500
+ * turn-granular `maxCostUsd` cap tripped), `"policy-cleanup"` (a
6501
+ * supervisor gate tore down its own ephemeral judge session),
6502
+ * `"parent-exited"` (an orchestrator's dying subtree reap),
6503
+ * `"provider-limit"` (a driver-reported subscription/usage-cap error,
6504
+ * e.g. Claude Code's "hit your session limit"), `"forgotten"` (a live
6505
+ * session killed as part of an operator `DELETE`).
6506
+ * Absent for every terminal path this file doesn't tag (a plain natural
6507
+ * exit, an ordinary turn error) — the session's own fault, or at least
6508
+ * not something worth a special label. A string outside this list is
6509
+ * still valid (a newer/older daemon) — see `isKnownSessionEndReason`. */
6510
+ endedReason?: SessionEndReason;
4805
6511
  /** Whether a turn was actually in flight the INSTANT `status` flipped to
4806
6512
  * "killed" — captured before anything else runs, because `busy` itself
4807
6513
  * cannot be trusted after the fact: `runAgentTurn`'s `finally` is what
@@ -4819,6 +6525,16 @@ interface SessionDescriptor {
4819
6525
  * `kill()`, `shutdownImpl`'s force-kill, and `loadHistorySnapshot`'s
4820
6526
  * wasAlive reclassification; absent for every other terminal path. */
4821
6527
  killedMidTurn?: boolean;
6528
+ /** What this session PRODUCED — the derived outcome (Level 1) the daemon
6529
+ * records once it reaches a terminal status, alongside (never instead of)
6530
+ * the termination fields above: last assistant message, opened PRs, cost,
6531
+ * run/parent links. agent-cli only. Recorded from the one exit funnel
6532
+ * (`emitExited`) and from boot reconcile for rows that died with the
6533
+ * daemon; first write wins unless a later one is for a new death or is
6534
+ * richer (see `shouldReplaceOutcome`). Dropped when an in-place resume
6535
+ * revives the row. Persisted, so it survives restarts and archiving. See
6536
+ * `session-outcome.ts`. */
6537
+ outcome?: SessionOutcome;
4822
6538
  /** Last time anything was written to stdout/stderr. Lets the UI
4823
6539
  * spot stuck sessions ("running for 2h, last output 12min ago"). */
4824
6540
  lastOutputAt?: string;
@@ -4926,10 +6642,23 @@ interface SessionDescriptor {
4926
6642
  * session sits `busy:false`, `awaitingInput:false`, background tasks
4927
6643
  * pending — a silent dead end. Detection + signal ONLY (same doctrine
4928
6644
  * as `stalledSinceMs`): nothing here re-prompts or wakes the session.
6645
+ * Not stamped for an agent that reports its background-task lifecycle
6646
+ * (see `backgroundTasks`) — that one is tracked and woken for real.
4929
6647
  * Cleared (deleted, never a stale value) on the next turn start and on
4930
6648
  * session exit. Emits `session:bg-tasks-parked` /
4931
6649
  * `session:bg-tasks-cleared`. */
4932
6650
  pendingBgTasks?: number;
6651
+ /** The agent's background tasks that are still running (a backgrounded
6652
+ * Bash command, a monitor, ...), as the agent itself reports them — over
6653
+ * ACP, the AIR `asyncTasks` lifecycle. Unlike `pendingBgTasks` (a
6654
+ * turn-end heuristic) this is the real set: a task is added when it
6655
+ * starts and removed when it settles, so an idle session with a
6656
+ * non-empty list is "waiting on N background tasks", not parked. When
6657
+ * one settles while the session is idle the registry makes sure the
6658
+ * agent is woken (see `SessionsRegistryOptions.backgroundTaskWake`).
6659
+ * Absent when none are running; dropped on exit and on respawn (a new
6660
+ * process owns none of the old one's tasks). */
6661
+ backgroundTasks?: SessionBackgroundTask[];
4933
6662
  /** ISO 8601 timestamp of the last turn that ended because the ADAPTER
4934
6663
  * ITSELF reported a failure — `runAgentTurn` observed a `turn-end` event
4935
6664
  * with `reason:"error"` (session-event-bus.ts's `SessionTurnEndEvent`,
@@ -4938,16 +6667,32 @@ interface SessionDescriptor {
4938
6667
  * `status` to `"error"` directly (a genuinely terminal row the existing
4939
6668
  * crash/error handling already catches). This is the twin gap for a
4940
6669
  * turn that fails IN-BAND while the process stays alive: `status` stays
4941
- * `"running"`, `lastError` stays unset (reserved for `markCrashed`), and
4942
- * nothing else distinguishes the session from one that simply finished
4943
- * a clean turn and is idle. Folded into the same "stalled" activity/UI
4944
- * treatment a mid-turn stall gets (see sessionsTree.logic.ts's
4945
- * `activityFor`) rather than a new state, on the theory that both mean
4946
- * "the last thing this session did needs a look, not a re-prompt on
4947
- * faith". Stamped at turn-end when `turnEndReason === "error"`; cleared
4948
- * the moment a LATER turn completes without one (same reset shape as
6670
+ * `"running"`, `lastError` stays unset (still reserved for `markCrashed`
6671
+ * — see `lastTurnErrorMessage` below for the message text this field's
6672
+ * own turn carries), and nothing else distinguishes the session from
6673
+ * one that simply finished a clean turn and is idle. Folded into the
6674
+ * same "stalled" activity/UI treatment a mid-turn stall gets (see
6675
+ * sessionsTree.logic.ts's `activityFor`) rather than a new state, on
6676
+ * the theory that both mean "the last thing this session did needs a
6677
+ * look, not a re-prompt on faith". Stamped at turn-end when
6678
+ * `turnEndReason === "error"` — including the case where the adapter's
6679
+ * stream generator simply returned after emitting an in-band `error`
6680
+ * event with no explicit turn-end and no thrown exception, which
6681
+ * `runAgentTurn`'s P5 synthetic-reason fallback now classifies as
6682
+ * `"error"` rather than defaulting to `"exited"`. Cleared the moment a
6683
+ * LATER turn completes without one (same reset shape as
4949
6684
  * `resumeAttempts`/`restartAttempts`). Detection + signal only. */
4950
6685
  lastTurnErroredAt?: string;
6686
+ /** The captured error text for the turn `lastTurnErroredAt` timestamps —
6687
+ * `evt.error.message` off the in-band stream `error` event observed
6688
+ * during that turn (see `runAgentTurn`'s `turnErrorMessage`). Distinct
6689
+ * from `lastError` (reserved for `markCrashed`) and from
6690
+ * `rt.lastErrorMessage` (an internal, cross-turn, never-cleared runtime
6691
+ * field — this one is the public, turn-scoped descriptor projection of
6692
+ * it). Absent when the adapter reported `reason:"error"` on its own
6693
+ * turn-end without a preceding `error`-kind stream event. Stamped and
6694
+ * cleared together with `lastTurnErroredAt`. */
6695
+ lastTurnErrorMessage?: string;
4951
6696
  /** True when the LAST completed turn produced zero assistant output and
4952
6697
  * zero tool calls (mirrors `SessionTurnEndEvent.empty` — see that
4953
6698
  * field's doc). Persisted so `monitorSessionWait`'s synchronous
@@ -4977,6 +6722,29 @@ interface SessionDescriptor {
4977
6722
  * the underlying `killedMidTurn`/`endedReason`). The in-place resume path
4978
6723
  * never auto-retries the interrupted prompt. */
4979
6724
  interrupted?: boolean;
6725
+ /** The `SessionsRegistry.bootId` of the FIRST daemon boot that found this
6726
+ * row interrupted — i.e. which restart cut its turn off. Stamped by the
6727
+ * boot reload (`loadHistorySnapshot`) on an interrupted row that doesn't
6728
+ * carry one yet; dropped whenever the row is interrupted AGAIN (so the next
6729
+ * boot restamps it) and on the next successful turn-end (with the markers it
6730
+ * qualifies). `interruptedAtBoot === registry.bootId` is how the
6731
+ * continue-interrupted verb and boot pass pick "interrupted by the LAST
6732
+ * restart" apart from a stale interruption nobody picked back up. PERSISTED
6733
+ * (unlike the derived `interrupted`) — the boot that stamped it may not be
6734
+ * the one reading it. */
6735
+ interruptedAtBoot?: string;
6736
+ /** How many automatic continue prompts the daemon has sent this row
6737
+ * (`daemon.continueInterruptedOnBoot`) since its last successful turn-end.
6738
+ * Incremented BEFORE the prompt is sent, so a continue turn that itself
6739
+ * gets cut off by another restart still counts; reset on the next
6740
+ * successful turn-end. The boot pass stops at
6741
+ * `MAX_AUTO_CONTINUE_ATTEMPTS` — the no-loop bound for a turn that keeps
6742
+ * taking the daemon down with it. PERSISTED; absent until the first one. */
6743
+ autoContinueAttempts?: number;
6744
+ /** `bootId` of the boot whose pass last auto-continued this row — the
6745
+ * at-most-one-auto-continue-per-restart guard. Reset with
6746
+ * `autoContinueAttempts`. */
6747
+ lastAutoContinueBoot?: string;
4980
6748
  /** How many in-place resume attempts have FAILED in a row for this session
4981
6749
  * (§5 cap/backoff). Incremented on every failed `maybeResumeAgent` attempt —
4982
6750
  * the resume throws, the adapter rejects the id, or the spawn returns null —
@@ -5083,6 +6851,9 @@ interface SessionDescriptor {
5083
6851
  * field is. Absent (not `false`) for every descriptor from before this
5084
6852
  * field existed — treated the same as `false` everywhere it's read. */
5085
6853
  archived?: boolean;
6854
+ /** ISO-8601 instant `archiveSession` set `archived: true`; cleared by
6855
+ * `unarchiveSession`. Absent on rows archived before this field existed. */
6856
+ archivedAt?: string;
5086
6857
  /** When `true`, the idle-reaper (`isReapable`, `idle-reaper.ts`) never
5087
6858
  * retires this session regardless of how long it's sat idle. For a
5088
6859
  * supervisor that legitimately parks — waiting on a child, waiting on a
@@ -5137,6 +6908,12 @@ interface SessionDescriptor {
5137
6908
  * `worktreePath` without an id identifies a PATH, which a later worktree
5138
6909
  * may reuse; the pair identifies one specific worktree. */
5139
6910
  worktreeId?: string;
6911
+ /** Absolute path of the PRIMARY checkout `worktreePath` was cut from (the
6912
+ * repo root a `worktree_status` query targets), read at spawn from the
6913
+ * worktree admin dir's `commondir` file (`resolveWorktreeIdentity`).
6914
+ * Absent whenever `worktreePath` is, when `commondir` was unreadable, and
6915
+ * for every session persisted before this field existed. */
6916
+ mainRepoPath?: string;
5140
6917
  /** `true` only when `worktreePath` was provisioned by the `worktrees.isolation`
5141
6918
  * policy WITHOUT an explicit `worktree` request from the caller (see
5142
6919
  * `decideWorktreeIsolation`'s `WorktreeDecision.provision.implicit` in
@@ -5326,15 +7103,34 @@ interface SessionDescriptor {
5326
7103
  /** Cumulative input / output token counts (same source + cadence as costUsd). */
5327
7104
  tokensIn?: number;
5328
7105
  tokensOut?: number;
7106
+ /** Cumulative prompt-cache read / cache-creation / reasoning tokens, from
7107
+ * the adapter's usage reader (claude-code transcript, hermes state.db,
7108
+ * opencode.db). Absent when the adapter doesn't expose them — never
7109
+ * zero-filled. See `UsageDetail` in usage.ts. */
7110
+ cacheReadTokens?: number;
7111
+ cacheWriteTokens?: number;
7112
+ reasoningTokens?: number;
7113
+ /** Tool calls the daemon observed across completed turns (cumulative).
7114
+ * Stamped at the first turn end, so absent = no turn has finished yet. */
7115
+ toolCalls?: number;
7116
+ /** Wall time spent inside turns, cumulative ms. Same cadence as
7117
+ * `toolCalls`. */
7118
+ durationMs?: number;
5329
7119
  /** Context-window size + tokens-in-context from the latest `usage_update`
5330
7120
  * event (ACP adapters that report a context window). Refreshed live as
5331
7121
  * usage_update events arrive, not just at turn-end. */
5332
7122
  contextSize?: number;
5333
7123
  contextUsed?: number;
7124
+ /** Where `contextSize` came from — `"adapter"` (a cost-bearing, authoritative
7125
+ * usage_update; sticky), `"catalog"` (model-catalog window for the model),
7126
+ * or `"reported"` (the adapter's own, possibly inferred, frame size). See
7127
+ * `context-window.ts` for the precedence. */
7128
+ contextSizeSource?: ContextSizeSource;
5334
7129
  /** Where `costUsd` came from — `"adapter"` (adapter's own reader or a
5335
7130
  * usage_update cost block), `"computed"` (tokens × in-repo catalog price),
5336
7131
  * `"no-pricing"` (tokens present but the model isn't in the catalog — cost
5337
- * deliberately left undefined), or `"none"`. Stamped at each turn-end. */
7132
+ * deliberately left undefined), or `"none"`. Stamped live on each
7133
+ * cost-bearing usage_update / reader read, and re-derived at turn-end. */
5338
7134
  usageSource?: UsageSource;
5339
7135
  /** Latest known `available_commands_update` payload (see @agentproto/acp's
5340
7136
  * `StreamEvent`'s `available-commands` kind) — the slash-commands/skills
@@ -5456,7 +7252,7 @@ interface SessionDescriptor {
5456
7252
  * next assistant `text-delta` (the model has the floor, so nothing is
5457
7253
  * pending), at turn start, and in the turn's finally. A claim about the
5458
7254
  * present tense — anything that disproves it must clear it. */
5459
- blockedOn?: "subagent" | "command";
7255
+ blockedOn?: "subagent" | "command" | "inbox";
5460
7256
  /** toolCallId of the tool-call that set `blockedOn`. A tool-result only
5461
7257
  * clears `blockedOn` when its toolCallId matches — so a nested or
5462
7258
  * interleaved tool finishing first can't clear the flag early. */
@@ -5472,9 +7268,9 @@ interface SessionDescriptor {
5472
7268
  * (`markCrashed` → `session:exited` with `status:"error"`/
5473
7269
  * `reason:"crashed"`), the supervisor-notify subscriber
5474
7270
  * (`supervisor-notify.ts`) delivers a `[child-crashed]` notice to its
5475
- * `parentSessionId`, if any — directly (enqueued prompt) when the parent
5476
- * is alive and idle, or stamped onto `pendingChildCrashNotices` and
5477
- * flushed at the parent's next turn when it's busy. The free external
7271
+ * `parentSessionId`, if any — dispatched now when the parent is alive
7272
+ * and idle, or parked in its `promptQueue` and drained as its own turn
7273
+ * when the parent's current turn ends. The free external
5478
7274
  * webhook path (`notifyUrl`) already fires regardless of this flag; this
5479
7275
  * only gates the additional IN-BAND signal into the parent's own
5480
7276
  * session, which a caller that isn't a delegating supervisor doesn't
@@ -5483,14 +7279,42 @@ interface SessionDescriptor {
5483
7279
  notifyParentOnCrash?: boolean;
5484
7280
  /** True when the session was spawned in permission-hold mode. */
5485
7281
  permissionHold?: boolean;
5486
- /** Queued `[child-crashed] …` notices from crashed children, stamped by
5487
- * the supervisor-notify subscriber when THIS session was busy at the
5488
- * time a child it should be told about crashed (mid-turn is never
5489
- * interrupted for this — see `notifyParentOnCrash`). Flushed and cleared
5490
- * by prepending the joined notices onto the next dispatched turn's
5491
- * message (`runAgentTurn`) — so delivery survives a busy parent without
5492
- * ever cancelling its in-flight work. Absent when nothing is queued. */
7282
+ /** Effective OS-level confinement of the adapter's own process
7283
+ * (`agent_start.commandSandbox`, else the workspace's
7284
+ * `.agentproto/command-sandbox.json` `adapterSpawn.mode`), resolved at
7285
+ * spawn with the same precedence the driver applies
7286
+ * (`wrapAgentCliSpawn`). Absent when neither engaged the axis (the spawn
7287
+ * ran unconfined without anyone choosing a mode), for a `sandbox` spawn
7288
+ * (confinement is the box's business), and for sessions persisted before
7289
+ * this field existed. Set at spawn time, immutable thereafter. */
7290
+ commandSandbox?: "off" | "workspace" | "strict";
7291
+ /** @deprecated Retired — child reports and `[child-crashed]` notices for a
7292
+ * busy session are now queued as their own `promptQueue` items
7293
+ * (`source:"child:<id>"`) instead of being string-prepended onto the next
7294
+ * prompt. Only read once, at boot, by `migratePendingChildNotices`, which
7295
+ * moves any notices a pre-upgrade daemon persisted into `promptQueue` and
7296
+ * deletes this field. Never written. */
5493
7297
  pendingChildCrashNotices?: string[];
7298
+ /** True once this session has been taught the `<agentproto-message>`
7299
+ * invariant (`MESSAGE_PREAMBLE`) — sent as a `system-prompt` slice ahead
7300
+ * of the FIRST message turn it receives, never again. */
7301
+ messagePreambleSent?: boolean;
7302
+ /** Durable, bounded inbox of typed messages NOT yet consumed (AIP-46
7303
+ * §Session messages): every accepted message lands here until it's
7304
+ * delivered into a turn (auto-acked), returned by `inbox_wait` with
7305
+ * `ack`, or acked via `inbox_ack`. `fyi` messages only ever live here
7306
+ * (surfaced as a digest). Capped at `INBOX_CAP`, oldest dropped first
7307
+ * (`[inbox] dropped`). Full history lives in the transcript, not here.
7308
+ * New array on every mutation, like `promptQueue`. */
7309
+ inbox?: SessionMessage[];
7310
+ /** What the live agent session can do, read from its driver at attach
7311
+ * time (spawn / resume) — shown in `session_list` so a sender can predict
7312
+ * a message's delivery tier. `steering`: the agent accepts ACP steering
7313
+ * (`_session/steering`) — a `steer` message can be injected into its
7314
+ * running turn instead of waiting for it to end. */
7315
+ capabilities?: {
7316
+ steering: boolean;
7317
+ };
5494
7318
  /** FIFO of prompts that arrived while this session was mid-turn and
5495
7319
  * asked to be QUEUED rather than rejected (`enqueuePrompt`'s
5496
7320
  * `opts.queue` arm — see its doc comment). Index 0 is next to
@@ -5505,8 +7329,7 @@ interface SessionDescriptor {
5505
7329
  * `sessionDescriptorsEqual` diff is a shallow `!==` per field, so an
5506
7330
  * in-place `.shift()` here would silently stop propagating queue
5507
7331
  * changes to the transcript panel. `[]` (not absent) once anything
5508
- * has ever been queued, matching `pendingChildCrashNotices`'s
5509
- * convention above. */
7332
+ * has ever been queued. */
5510
7333
  promptQueue?: QueuedPrompt[];
5511
7334
  /** Best-effort context-handoff digest (Fix D), stashed on a descriptor
5512
7335
  * whose resume degraded to a BLANK spawn — the adapter's own
@@ -5514,8 +7337,7 @@ interface SessionDescriptor {
5514
7337
  * false` — built by `buildResumeContextDigest` from the daemon's own
5515
7338
  * `events.jsonl` transcript, which survives both cases. Flushed and
5516
7339
  * cleared by prepending it onto the first dispatched turn's message
5517
- * (`runAgentTurn`), exactly once, same shape as
5518
- * `pendingChildCrashNotices` above. Gated strictly on the blank-fallback
7340
+ * (`runAgentTurn`), exactly once. Gated strictly on the blank-fallback
5519
7341
  * flags at the two sites that set it (`session-restart-core.ts`,
5520
7342
  * `maybeResumeAgent` here) — never set for a resume that actually
5521
7343
  * restored context, so a real continuation is never double-fed its own
@@ -5547,6 +7369,12 @@ interface SessionDescriptor {
5547
7369
  /** When this session was continued fresh into a new session, the target
5548
7370
  * session id. */
5549
7371
  continuedTo?: string;
7372
+ /** Set alongside `continuedFrom` on the NEW session — the harness the
7373
+ * checkpoint moved from/to and when. Present on every `continue_fresh`
7374
+ * spawn, not just a cross-harness one, so a same-harness continuation
7375
+ * and a genuine handoff are both traceable the same way. See
7376
+ * {@link SessionHandoff}. */
7377
+ handoff?: SessionHandoff;
5550
7378
  /** Source label — the channel/harness this session was spawned from
5551
7379
  * ("codex", "cowork", "vscode", "cron", …). Descriptor-only; groups the
5552
7380
  * session under a source node in the tree. */
@@ -5678,6 +7506,9 @@ interface SessionSummary {
5678
7506
  endedAt?: string;
5679
7507
  exitCode?: number;
5680
7508
  killedMidTurn?: boolean;
7509
+ /** Compact derived outcome (status + 120-char summary preview) — see
7510
+ * `SessionDescriptor.outcome`; the full record is on GET /sessions/:id. */
7511
+ outcome?: SessionOutcomeCompact;
5681
7512
  lastOutputAt?: string;
5682
7513
  lastActivityAt?: string;
5683
7514
  currentPhase?: SessionCurrentPhase;
@@ -5697,6 +7528,9 @@ interface SessionSummary {
5697
7528
  renamedByUser?: boolean;
5698
7529
  activitySummary?: SessionActivitySummary;
5699
7530
  archived?: boolean;
7531
+ /** ISO-8601 instant `archiveSession` set `archived: true`; cleared by
7532
+ * `unarchiveSession`. Absent on rows archived before this field existed. */
7533
+ archivedAt?: string;
5700
7534
  keepAlive?: boolean;
5701
7535
  pinned?: boolean;
5702
7536
  pty?: boolean;
@@ -5705,6 +7539,7 @@ interface SessionSummary {
5705
7539
  cwd?: string;
5706
7540
  worktreePath?: string;
5707
7541
  worktreeId?: string;
7542
+ mainRepoPath?: string;
5708
7543
  /** Resolved AGENTS.md path — see `SessionDescriptor.agentsMd`. */
5709
7544
  agentsMd?: string;
5710
7545
  /** Injection mode — see `SessionDescriptor.agentsMdMode`. */
@@ -5719,18 +7554,21 @@ interface SessionSummary {
5719
7554
  tokensOut?: number;
5720
7555
  contextSize?: number;
5721
7556
  contextUsed?: number;
7557
+ contextSizeSource?: ContextSizeSource;
5722
7558
  usageSource?: UsageSource;
5723
7559
  awaitingInput?: boolean;
5724
7560
  awaitingQuestion?: SessionAwaitingQuestion;
5725
7561
  awaitingPermission?: boolean;
5726
7562
  turnsCompleted?: number;
5727
7563
  busy?: boolean;
5728
- blockedOn?: "subagent" | "command";
7564
+ blockedOn?: "subagent" | "command" | "inbox";
5729
7565
  stalledSinceMs?: number;
5730
7566
  /** Parked-with-background-tasks marker — see
5731
7567
  * `SessionDescriptor.pendingBgTasks`. Stamped at turn-end, cleared on the
5732
7568
  * next turn start / exit. */
5733
7569
  pendingBgTasks?: number;
7570
+ /** Running background tasks — see `SessionDescriptor.backgroundTasks`. */
7571
+ backgroundTasks?: SessionBackgroundTask[];
5734
7572
  /** Adapter-reported turn-error marker — see
5735
7573
  * `SessionDescriptor.lastTurnErroredAt`. Stamped at turn-end, cleared on
5736
7574
  * the next turn that completes without one. */
@@ -5762,6 +7600,8 @@ interface SessionSummary {
5762
7600
  sandboxTeardown?: "kill" | "pause";
5763
7601
  sandboxPorts?: Record<number, string>;
5764
7602
  appServe?: SessionAppServeInfo;
7603
+ /** Effective adapter confinement — see `SessionDescriptor.commandSandbox`. */
7604
+ commandSandbox?: "off" | "workspace" | "strict";
5765
7605
  /** Read-time projection of the box's ledger liveness verdict — see
5766
7606
  * `SessionDescriptor.sandboxAlive`. */
5767
7607
  sandboxAlive?: boolean;
@@ -6096,6 +7936,13 @@ interface SessionsRegistry {
6096
7936
  queue?: boolean;
6097
7937
  force?: boolean;
6098
7938
  queueId?: string;
7939
+ /** A typed inter-session message (see `QueuedPrompt.envelope`). When
7940
+ * set, the turn is built from the envelope and `message` is ignored:
7941
+ * rendered as an `<agentproto-message>` tag, recorded as
7942
+ * `session-message` (never `user-prompt`), never matched as a
7943
+ * structured-question answer, and noted in the SENDER's transcript
7944
+ * as `session-message-sent`. */
7945
+ envelope?: SessionMessage;
6099
7946
  }): Promise<EnqueuePromptResult>;
6100
7947
  /** Cancel one not-yet-dispatched item in `SessionDescriptor.promptQueue`
6101
7948
  * by id — the composer's per-item "remove" action. Idempotent: an
@@ -6106,6 +7953,50 @@ interface SessionsRegistry {
6106
7953
  removeQueuedPrompt(id: string, queueId: string): {
6107
7954
  removed: boolean;
6108
7955
  };
7956
+ /** Deliver a daemon-attested typed message (`msg.to` is the recipient;
7957
+ * `from` must already be computed from the verified caller), routed by
7958
+ * `msg.urgency` (AIP-46 §Delivery tiers):
7959
+ * - a matching pending `waitForMessages` wins over every tier (waiter
7960
+ * first — returned as that call's result, never also injected);
7961
+ * - `fyi` — durable inbox only, no wake;
7962
+ * - idle recipient — any other tier starts its own turn now;
7963
+ * - busy + `next-turn` — queued behind the current turn;
7964
+ * - busy + `steer` — injected into the running turn when the daemon's
7965
+ * OWN prompt is in flight (never an autonomous cycle), the agent can
7966
+ * steer, and the per-recipient rate limit allows; else `next-turn`;
7967
+ * - busy + `interrupt` — cancels the turn and delivers — only when
7968
+ * `allowInterrupt` (the caller's grant); otherwise downgraded to
7969
+ * `steer`.
7970
+ * `urgencyApplied` reports the tier actually used. Throws when the
7971
+ * recipient is missing or not alive. */
7972
+ sendMessage(msg: SessionMessage, opts?: {
7973
+ source?: string;
7974
+ origin?: string;
7975
+ allowInterrupt?: boolean;
7976
+ }): Promise<SendMessageResult>;
7977
+ /** Block until a message matching `filter` is in `id`'s inbox (returns at
7978
+ * once when one already is), or `timeoutMs` elapses. Matched messages are
7979
+ * consumed: stamped `delivered.via:"wait"`, removed from the prompt queue
7980
+ * (so they're never also injected as a turn), and — when `ack` (default)
7981
+ * — removed from the inbox. Sets `blockedOn:"inbox"` while parked. */
7982
+ waitForMessages(id: string, filter: MessageFilter, opts: {
7983
+ timeoutMs: number;
7984
+ ack?: boolean;
7985
+ signal?: AbortSignal;
7986
+ }): Promise<WaitForMessagesResult>;
7987
+ /** Read `id`'s inbox (un-consumed messages), newest last. */
7988
+ listInbox(id: string, filter?: MessageFilter & {
7989
+ limit?: number;
7990
+ }): SessionMessage[] | null;
7991
+ /** Find a message `id` RECEIVED (still in its inbox, or already consumed
7992
+ * and only in its transcript) — how `message_reply` routes to the
7993
+ * original sender. `undefined` when `id` never received `messageId`. */
7994
+ findReceivedMessage(id: string, messageId: string): SessionMessage | undefined;
7995
+ /** Ack (remove) messages from `id`'s inbox — and from its prompt queue, so
7996
+ * an acked-but-still-queued message is never delivered afterwards. */
7997
+ ackInbox(id: string, ids: readonly string[] | "all"): {
7998
+ acked: string[];
7999
+ };
6109
8000
  /** Snapshot a session's prompt queue for inspection — the after-the-fact
6110
8001
  * view of what's sitting in `SessionDescriptor.promptQueue` right now.
6111
8002
  * Runs the shared preview + origin-label derivation so every consumer
@@ -6134,7 +8025,8 @@ interface SessionsRegistry {
6134
8025
  *
6135
8026
  * Implemented as promote-to-front + interrupt: the cancelled turn's
6136
8027
  * own `dispatchQueuedPrompt` (in its finally) then drains the promoted
6137
- * item into a fresh turn. On an idle session (nothing to interrupt) the
8028
+ * item — and only that item; the rest wait for a natural turn-end —
8029
+ * into a fresh turn. On an idle session (nothing to interrupt) the
6138
8030
  * promoted item is dispatched directly. Returns:
6139
8031
  * `{ delivered: false, reason }` — `"no-session"` / `"not-in-queue"`;
6140
8032
  * `{ delivered: true, interrupted: boolean }` otherwise. */
@@ -6143,6 +8035,21 @@ interface SessionsRegistry {
6143
8035
  reason?: string;
6144
8036
  interrupted?: boolean;
6145
8037
  }>;
8038
+ /** A currently-RUNNING background task's info plus a tail of its output
8039
+ * file, for the UI's "click a live background-task row to peek at its
8040
+ * output" affordance. Scoped to `SessionDescriptor.backgroundTasks` on
8041
+ * purpose — a settled task is dropped from that list moments after it
8042
+ * reports terminal status (see `noteBackgroundTask`), so this never
8043
+ * becomes an arbitrary-file-read: the caller supplies a `taskId`, never a
8044
+ * path, and only a path the agent itself already reported for a task
8045
+ * still tracked as running is ever read. Returns `null` when the session
8046
+ * or the task is unknown (the caller surfaces 404); `tail` is `null` when
8047
+ * the task has no `outputFile` or the file can't be read (not yet
8048
+ * created, already rotated away, ...). */
8049
+ readBackgroundTaskTail(id: string, taskId: string): {
8050
+ task: SessionBackgroundTask;
8051
+ tail: string | null;
8052
+ } | null;
6146
8053
  /** Eagerly resume ONE dead-but-resumable agent-cli session IN PLACE,
6147
8054
  * WITHOUT a prompt — the boot-time counterpart to the lazy resume that
6148
8055
  * `sendPrompt`/`enqueuePrompt` trigger on the first prompt after a restart
@@ -6165,6 +8072,17 @@ interface SessionsRegistry {
6165
8072
  * touching the adapter, so the bounded boot pass can call it per row and
6166
8073
  * tally the results. */
6167
8074
  resumeOnBoot(id: string): Promise<EagerResumeOutcome>;
8075
+ /** Opaque id unique to this registry instance — one per daemon boot.
8076
+ * Stamped as `interruptedAtBoot` on rows the boot reload finds interrupted,
8077
+ * so `interruptedAtBoot === bootId` means "interrupted by the restart that
8078
+ * preceded THIS boot" (continue-interrupted). */
8079
+ readonly bootId: string;
8080
+ /** Book one automatic continue prompt against `id` (increment
8081
+ * `autoContinueAttempts`, stamp `lastAutoContinueBoot = bootId`) and
8082
+ * persist. Called by the continue-on-boot pass BEFORE it sends, so the
8083
+ * no-loop bound holds even when the continue turn is itself cut off by the
8084
+ * next restart. No-op for an unknown id. */
8085
+ recordAutoContinue(id: string): void;
6168
8086
  /** Cancel the in-flight turn on a live agent-cli session and leave the
6169
8087
  * session itself alive and idle — the bare "interrupt, no next prompt"
6170
8088
  * primitive `sendPrompt`/`enqueuePrompt`'s `opts.interrupt` arm lacks
@@ -6390,6 +8308,15 @@ interface SessionsRegistry {
6390
8308
  * Initial backfill: synchronously invokes `onLine` once for each
6391
8309
  * line currently in the ring buffer so attaches show context. */
6392
8310
  attach(id: string, onLine: (line: string, stream: "stdout" | "stderr") => void): (() => void) | null;
8311
+ /** Append a `kind: "mcp_app_tool_call"` record (an MCP App iframe's
8312
+ * `tools/call`, proxied by `mcp_app_tool_call`) to the session's
8313
+ * events.jsonl through the same seq-numbered writer the session's own
8314
+ * events use. No-op-safe for an unknown id (the writer creates the dir). */
8315
+ recordMcpAppToolCall(sessionId: string, record: McpAppToolCallRecord): void;
8316
+ /** Tool name the transcript recorded for `toolCallId` (the latest
8317
+ * non-empty `toolName` across its `tool-call` records), or undefined
8318
+ * when the id isn't in the session's events.jsonl. */
8319
+ findToolCallName(sessionId: string, toolCallId: string): Promise<string | undefined>;
6393
8320
  /** Subscribe to a session's structured events.jsonl records as they're
6394
8321
  * written — the live-push half of `GET /sessions/:id/events/stream`'s
6395
8322
  * replay-then-subscribe handoff. Thin passthrough to the transcript
@@ -6433,7 +8360,26 @@ interface SessionsRegistry {
6433
8360
  * bytes at the end; `lastBytes` caps the returned size from the
6434
8361
  * tail. Returns null when the session is missing or not a PTY. */
6435
8362
  readTerminalOutput(id: string, lastBytes?: number): Buffer | null;
6436
- kill(id: string, signal?: NodeJS.Signals): boolean;
8363
+ /** SIGTERM the underlying child + close the agent protocol session.
8364
+ * `reason`, when given, is stamped onto `SessionDescriptor.endedReason`
8365
+ * (see {@link SessionEndReason}) — pass it from every call site that
8366
+ * knows WHY it's killing this session (an operator's explicit stop, a
8367
+ * supervisor tearing down its own judge session, …) rather than leaving
8368
+ * the row's cause of death unlabeled. Omitted preserves today's
8369
+ * behaviour: no `endedReason` is stamped by this call (the row may
8370
+ * already carry one from elsewhere, e.g. a prior `reapIdle`/`markCrashed`
8371
+ * — moot in practice since both already refuse a non-"running" row before
8372
+ * `kill()` would ever run on it).
8373
+ *
8374
+ * A row that's ALREADY terminal (exited/killed/error) normally makes this
8375
+ * a no-op refusal (`false`) — EXCEPT `reason: "operator-completed"`,
8376
+ * which is instead treated as "mark as completed": the persisted
8377
+ * outcome's `termination.reason` is idempotently relabeled to
8378
+ * `"operator-completed"` (the true original mechanism preserved in
8379
+ * `termination.previousReason`) and this returns `true`. Every other
8380
+ * reason (including `"operator-stopped"` or none) on a terminal row
8381
+ * stays the plain no-op. */
8382
+ kill(id: string, signal?: NodeJS.Signals, reason?: SessionEndReason): boolean;
6437
8383
  /** Retire a long-idle agent-cli session to free its adapter process — the
6438
8384
  * primitive the idle-session reaper (`runIdleReapPass`, PR-6) drives on a
6439
8385
  * periodic sweep. Terminates the underlying adapter/child the SAME graceful
@@ -6541,17 +8487,6 @@ interface SessionsRegistry {
6541
8487
  * window keeps aging out naturally rather than being wiped by the
6542
8488
  * give-up itself. Returns false (no-op) for an unknown id. */
6543
8489
  giveUpRestart(id: string, message: string): boolean;
6544
- /** Queue a `[child-crashed] …` notice on a BUSY parent's descriptor
6545
- * (`SessionDescriptor.pendingChildCrashNotices`) for delivery at its next
6546
- * turn (`runAgentTurn`'s flush) — the never-interrupt path
6547
- * `supervisor-notify.ts` takes when the parent can't be prompted
6548
- * directly right now (it's mid-turn). Idempotent: the exact same notice
6549
- * string is never queued twice, so a duplicate event for the same crash
6550
- * can't double-deliver. No-op (returns false) on an unknown id, a
6551
- * non-agent-cli row, or a row that isn't alive (`running`/`starting`) —
6552
- * nothing to flush a notice INTO. Returns true iff the notice was newly
6553
- * queued. */
6554
- stampPendingChildCrashNotice(id: string, notice: string): boolean;
6555
8490
  /** List permission requests currently parked in the pending-permissions
6556
8491
  * inbox across all permission-hold sessions, newest last. Optionally
6557
8492
  * filtered to one session. */
@@ -6563,8 +8498,9 @@ interface SessionsRegistry {
6563
8498
  * clears the session's awaiting-permission state, and emits
6564
8499
  * `session:permission-resolved`. */
6565
8500
  respondPermission(id: string, input: PermissionRespondInput): Promise<PermissionRespondResult>;
6566
- /** Stop tracking a session (after it exited and the user clicked
6567
- * "clear"). Doesn't kill — use `kill` first. */
8501
+ /** Stop tracking a session. A still-live session is torn down through
8502
+ * `kill` first (same teardown as `agent_kill`), so forgetting never
8503
+ * orphans a running process tree. */
6568
8504
  forget(id: string): boolean;
6569
8505
  /** Await every best-effort fire-and-forget per-session write currently in
6570
8506
  * flight — today the `CommandLogEntry` → `ToolCallRecord` chain
@@ -6656,8 +8592,10 @@ interface SpawnAgentInput {
6656
8592
  /** Optional initial prompt to dispatch immediately. The promise
6657
8593
  * the registry returns resolves AFTER the spawn — the prompt
6658
8594
  * runs in the background, projecting events into the ring
6659
- * buffer. Skip to spawn idle. */
6660
- initialPrompt?: string;
8595
+ * buffer. Skip to spawn idle. A plain string, or a content-block
8596
+ * prompt (attachment-bearing first message) — see
8597
+ * `SpawnAgentSessionInput.prompt`'s doc. */
8598
+ initialPrompt?: string | Record<string, unknown> | unknown[];
6661
8599
  /** The daemon-composed SYSTEM slice of {@link initialPrompt} — the part
6662
8600
  * it synthesized ahead of the CALLER's ask (role disposition, lineage
6663
8601
  * line, AGENTS.md, posture preamble). The adapter still receives the
@@ -6749,12 +8687,7 @@ interface SpawnAgentInput {
6749
8687
  * cost/token fields on the descriptor. Adapter-specific (e.g. hermes
6750
8688
  * reads its state.db keyed by the adapter session id). Omit for adapters
6751
8689
  * with no usage source. */
6752
- readUsage?: () => Promise<{
6753
- model?: string;
6754
- costUsd?: number;
6755
- tokensIn?: number;
6756
- tokensOut?: number;
6757
- } | null>;
8690
+ readUsage?: () => Promise<UsageReadResult | null>;
6758
8691
  /** Opt this session into Langfuse tracing (prompt/completion + tool spans +
6759
8692
  * tokens/cost). Effective opt-in is `trace ?? opts.langfuseTracingDefault ?? false`. */
6760
8693
  trace?: boolean;
@@ -6770,6 +8703,9 @@ interface SpawnAgentInput {
6770
8703
  /** What session close does to the box, when `remote` is true — see
6771
8704
  * `SessionDescriptor.sandboxTeardown`. */
6772
8705
  sandboxTeardown?: "kill" | "pause";
8706
+ /** Effective adapter confinement, resolved by the caller — see
8707
+ * `SessionDescriptor.commandSandbox`. */
8708
+ commandSandbox?: "off" | "workspace" | "strict";
6773
8709
  /** Port-to-URL map from the booted sandbox — see
6774
8710
  * `SessionDescriptor.sandboxPorts`. */
6775
8711
  sandboxPorts?: Record<number, string>;
@@ -6808,15 +8744,12 @@ type PendingAgentOutcome = {
6808
8744
  commandPreview?: string;
6809
8745
  resumable?: boolean;
6810
8746
  nativeTerminalResume?: boolean;
6811
- readUsage?: () => Promise<{
6812
- model?: string;
6813
- costUsd?: number;
6814
- tokensIn?: number;
6815
- tokensOut?: number;
6816
- } | null>;
8747
+ readUsage?: () => Promise<UsageReadResult | null>;
6817
8748
  /** Dispatched now that the tree + driver session both exist — never
6818
- * passed to `spawnAgentPending`, which would race the tree. */
6819
- initialPrompt?: string;
8749
+ * passed to `spawnAgentPending`, which would race the tree. A plain
8750
+ * string, or a content-block prompt (attachment-bearing first
8751
+ * message) — see `SpawnAgentSessionInput.prompt`'s doc. */
8752
+ initialPrompt?: string | Record<string, unknown> | unknown[];
6820
8753
  /** The daemon-composed SYSTEM slice of `initialPrompt` (see
6821
8754
  * `SpawnAgentInput.initialPromptSystem`) — threaded through the
6822
8755
  * placeholder so the deferred dispatch records it as a system turn. */
@@ -7092,6 +9025,163 @@ declare function runEagerResumePass(opts: {
7092
9025
  isServed?: (desc: SessionDescriptor) => boolean;
7093
9026
  }): Promise<EagerResumeSummary>;
7094
9027
 
9028
+ /**
9029
+ * Continue-interrupted: pick back up the sessions a daemon restart cut off
9030
+ * mid-turn, by sending each an explicit "continue" prompt.
9031
+ *
9032
+ * Everything below the prompt already exists — the persisted registry is the
9033
+ * snapshot, `maybeResumeAgent` resumes a dead row in place (lazily on its first
9034
+ * prompt, or eagerly at boot via `eager-resume.ts`), and the row keeps the
9035
+ * derived `interrupted` marker until its next successful turn-end. What a
9036
+ * restart does NOT do, by design, is re-run the interrupted prompt (prompts
9037
+ * aren't idempotent). This module is the explicit, opt-in replacement for that:
9038
+ * a NEW prompt that tells the agent its last turn was cut off and to check the
9039
+ * state on disk before carrying on.
9040
+ *
9041
+ * Two callers share one eligibility rule + one sender:
9042
+ * - the manual verb (`session_continue_interrupted` MCP tool,
9043
+ * `POST /sessions/continue-interrupted`, `agentproto sessions
9044
+ * continue-interrupted`) — `mode: "manual"`, dry-run by default;
9045
+ * - the opt-in boot pass (`daemon.continueInterruptedOnBoot`) —
9046
+ * `mode: "boot"`, run by serve.ts right after the eager resume pass.
9047
+ *
9048
+ * Eligibility (both modes): an agent-cli row that is `interrupted` BY THE LAST
9049
+ * RESTART (`interruptedAtBoot === registry.bootId` — a stale interruption from
9050
+ * an older restart that nobody picked up is left alone), resumable under the
9051
+ * attempt cap (`canResume`), and not already busy. The prompt goes through
9052
+ * `enqueuePrompt`, the normal prompt path, so a dead-but-resumable row
9053
+ * lazy-resumes with its billing auth re-resolved exactly as a human prompt
9054
+ * would.
9055
+ *
9056
+ * No-loop rules (boot mode only — a human pressing the button is intent):
9057
+ * - never auto-continue a row whose resume has failed (`resumeAttempts > 0`:
9058
+ * the eager pass, or an earlier attempt, could not bring it back);
9059
+ * - at most one auto-continue per restart (`lastAutoContinueBoot`);
9060
+ * - at most `MAX_AUTO_CONTINUE_ATTEMPTS` auto-continues without a successful
9061
+ * turn-end in between (`autoContinueAttempts`, booked BEFORE sending), so a
9062
+ * continue turn that keeps getting cut off — or keeps taking the daemon
9063
+ * down with it — stops being retried.
9064
+ *
9065
+ * Ordering (boot mode): MUST run after the supervisor is re-armed, same
9066
+ * constraint as `eager-resume.ts` — the continue turn's `turn-end` is exactly
9067
+ * what a re-armed completion policy is waiting for — and after the eager pass,
9068
+ * so a row whose eager resume failed is recognisable (`resumeAttempts`) and
9069
+ * skipped rather than lazily retried.
9070
+ */
9071
+
9072
+ /** The prompt sent when the caller doesn't supply one. */
9073
+ declare const DEFAULT_CONTINUE_PROMPT: string;
9074
+ /** Transcript provenance for the continue prompt (`opts.source`). */
9075
+ declare const CONTINUE_PROMPT_SOURCE = "daemon:continue-interrupted";
9076
+ /** Cap on automatic continue prompts sent to one row without a successful
9077
+ * turn-end in between (boot mode). 2 = the original interruption gets one
9078
+ * auto-continue, and if a second restart cuts THAT turn off it gets one more;
9079
+ * after that only a human (manual verb) re-sends. */
9080
+ declare const MAX_AUTO_CONTINUE_ATTEMPTS = 2;
9081
+ type ContinueInterruptedSkipReason =
9082
+ /** Unknown id (explicit `ids` filter only). */
9083
+ "unknown"
9084
+ /** Not interrupted at all — no turn was cut off. */
9085
+ | "not-interrupted"
9086
+ /** Interrupted by an EARLIER restart, not the last one. */
9087
+ | "stale-interrupt"
9088
+ /** Not in-place resumable (not agent-cli / missing adapter session / archived). */
9089
+ | "not-resumable"
9090
+ /** Already burned through `MAX_RESUME_ATTEMPTS` failed resumes. */
9091
+ | "resume-cap-exhausted"
9092
+ /** Mid-turn already — someone got there first. */
9093
+ | "busy"
9094
+ /** Boot mode: a resume attempt has failed on this row. */
9095
+ | "resume-failed"
9096
+ /** Boot mode: already auto-continued by this boot. */
9097
+ | "already-auto-continued"
9098
+ /** Boot mode: `MAX_AUTO_CONTINUE_ATTEMPTS` reached without recovering. */
9099
+ | "auto-continue-cap"
9100
+ /** Excluded by the cross-process `isServed` gate (another daemon's row). */
9101
+ | "not-served";
9102
+ type ContinueInterruptedOutcome = {
9103
+ id: string;
9104
+ name?: string;
9105
+ status: "eligible";
9106
+ } | {
9107
+ id: string;
9108
+ name?: string;
9109
+ status: "sent";
9110
+ } | {
9111
+ id: string;
9112
+ name?: string;
9113
+ status: "skipped";
9114
+ reason: ContinueInterruptedSkipReason;
9115
+ } | {
9116
+ id: string;
9117
+ name?: string;
9118
+ status: "failed";
9119
+ error: string;
9120
+ };
9121
+ interface ContinueInterruptedResult {
9122
+ dryRun: boolean;
9123
+ /** The prompt that was (or, on a dry run, would be) sent. */
9124
+ prompt: string;
9125
+ /** The boot the candidates were interrupted before (`registry.bootId`). */
9126
+ bootId: string;
9127
+ /** One entry per considered row — every interrupted row when `ids` is
9128
+ * omitted, every requested id otherwise. Eligible-first ordering matches
9129
+ * the send order (newest activity first). */
9130
+ sessions: ContinueInterruptedOutcome[];
9131
+ /** Counts by outcome status. */
9132
+ eligible: number;
9133
+ sent: number;
9134
+ skipped: number;
9135
+ failed: number;
9136
+ }
9137
+ type ContinueInterruptedMode = "manual" | "boot";
9138
+ /** Decide one row. `undefined` ⇒ eligible. Pure — reads the descriptor only. */
9139
+ declare function continueSkipReason(desc: SessionDescriptor, bootId: string, mode: ContinueInterruptedMode): ContinueInterruptedSkipReason | undefined;
9140
+ /**
9141
+ * List — and unless `dryRun` — continue the sessions interrupted by the last
9142
+ * restart. See the module docblock for eligibility, the no-loop rules, and the
9143
+ * ordering contract. No-throw per row: a send that fails admission (row went
9144
+ * busy, resume refused) is reported as `failed` and the pass carries on.
9145
+ */
9146
+ declare function continueInterruptedSessions(opts: {
9147
+ registry: Pick<SessionsRegistry, "list" | "get" | "enqueuePrompt" | "bootId" | "recordAutoContinue">;
9148
+ mode: ContinueInterruptedMode;
9149
+ /** Default true: report what would be sent, send nothing. */
9150
+ dryRun?: boolean;
9151
+ /** Restrict to these session ids. Omitted ⇒ every interrupted row. */
9152
+ ids?: readonly string[];
9153
+ /** Custom prompt. Omitted/blank ⇒ `DEFAULT_CONTINUE_PROMPT`. */
9154
+ prompt?: string;
9155
+ /** Max sends in flight at once (each may lazily resume an adapter). Clamped
9156
+ * to ≥1. Default 4, same as the eager pass. */
9157
+ concurrency?: number;
9158
+ /** Cross-process gate — see `runEagerResumePass`. Omitted ⇒ all served. */
9159
+ isServed?: (desc: SessionDescriptor) => boolean;
9160
+ /** Extra visibility filter (a scoped orchestrator's subtree). Rows it
9161
+ * rejects are dropped silently — never reported, never sent. */
9162
+ visible?: (desc: SessionDescriptor) => boolean;
9163
+ }): Promise<ContinueInterruptedResult>;
9164
+ /** Tally of the boot-time continue pass, for the boot-banner line. */
9165
+ interface ContinueOnBootSummary {
9166
+ /** False when `daemon.continueInterruptedOnBoot` is off (nothing ran). */
9167
+ enabled: boolean;
9168
+ /** Rows passing eligibility at selection time — the banner's denominator. */
9169
+ eligible: number;
9170
+ sent: number;
9171
+ skipped: number;
9172
+ failed: number;
9173
+ }
9174
+ /**
9175
+ * The boot pass: `continueInterruptedSessions` in `mode: "boot"`, not a dry
9176
+ * run, reduced to the banner tally. Caller (the gateway handle, invoked from
9177
+ * serve.ts) is responsible for the ordering contract in the module docblock.
9178
+ */
9179
+ declare function runContinueOnBootPass(opts: {
9180
+ registry: Parameters<typeof continueInterruptedSessions>[0]["registry"];
9181
+ concurrency: number;
9182
+ isServed?: (desc: SessionDescriptor) => boolean;
9183
+ }): Promise<ContinueOnBootSummary>;
9184
+
7095
9185
  /**
7096
9186
  * Daemon-lane PR provenance STAMPER.
7097
9187
  *
@@ -7118,7 +9208,7 @@ declare function runEagerResumePass(opts: {
7118
9208
 
7119
9209
  /** A minimal `gh` runner — resolves with the captured exit code + stdout.
7120
9210
  * Injected so tests drive stamping without spawning a real `gh`. */
7121
- type GhRunner = (args: readonly string[], cwd: string) => Promise<{
9211
+ type GhRunner$1 = (args: readonly string[], cwd: string) => Promise<{
7122
9212
  exitCode: number;
7123
9213
  stdout: string;
7124
9214
  }>;
@@ -7206,7 +9296,7 @@ declare function createPrProvenanceReconciler(opts: {
7206
9296
  * Defaults to reading the session's own events.jsonl via
7207
9297
  * {@link readToolCallRecords}; injectable for tests. */
7208
9298
  listToolCalls?: (sessionId: string) => Promise<ToolCallRecord[]>;
7209
- run?: GhRunner;
9299
+ run?: GhRunner$1;
7210
9300
  host?: string;
7211
9301
  }): PrProvenanceReconciler;
7212
9302
 
@@ -7297,6 +9387,87 @@ interface InboundWatcher {
7297
9387
  shutdown(): void;
7298
9388
  }
7299
9389
 
9390
+ /**
9391
+ * Dynamic MCP-app panels for installed `@agentproto/app-kit` apps that ship
9392
+ * a `ui` block (`defineApp({ ui: { html, title?, tools?, csp? } })`,
9393
+ * `app-tools.ts`'s `performInstall`). Bridges `AppRegistry` → `AgnoMcpApp[]`
9394
+ * so `registerMcpApps` (mcp-apps-adapter.ts) can mount them next to the
9395
+ * built-in panels (agentproto_sessions, agents_overview, ...).
9396
+ *
9397
+ * `mcpServerFactory` (index.ts) rebuilds a fresh McpServer per `/mcp`
9398
+ * request, so this module never throws — a bad app record must not take
9399
+ * down every other request. A collided tool id is still skipped with a
9400
+ * `console.warn` (there's nowhere else for a second panel to live). Every
9401
+ * OTHER failure — a removed app dir, a missing/unreadable `ui.path`, a
9402
+ * failed or in-flight `ui.build` — still mounts the panel, serving a
9403
+ * readable HTML placeholder/error page in place of the real bundle
9404
+ * (app-ui-placeholder.ts) instead of skipping it outright: a `ui://`
9405
+ * resource that silently doesn't exist just shows as a JSON-RPC "resource
9406
+ * not found" in a host, which is what motivated this file's placeholder
9407
+ * page in the first place — a HOST-VISIBLE page, even a stand-in one, beats
9408
+ * a resource the host has to explain away as missing.
9409
+ */
9410
+
9411
+ /**
9412
+ * The MCP Apps wire (spec `2026-01-26`) over `postMessage` JSON-RPC, exposed
9413
+ * as `window.McpApp.connect() -> Promise<{ callTool, sendMessage,
9414
+ * updateModelContext, openLink, onTeardown }>`. No host in this stack injects
9415
+ * an SDK global into the app iframe, so every UI panel that talks through
9416
+ * `window.McpApp` (all of them — see each app's `ui.ts` under
9417
+ * `packages/apps/src`) would otherwise throw `Cannot read properties of
9418
+ * undefined (reading 'connect')` at boot. Injected here, at serve time, so
9419
+ * every installed app's panel is covered without each app shipping its own
9420
+ * copy of the bridge.
9421
+ *
9422
+ * Bridge surface:
9423
+ * - `callTool(name, args)` — JSON-RPC `tools/call`
9424
+ * - `sendMessage(content)` — JSON-RPC `ui/message`
9425
+ * - `updateModelContext({content?, structuredContent?})` — JSON-RPC
9426
+ * `ui/update-model-context`; each call replaces the previous context.
9427
+ * - `openLink(url)` — JSON-RPC `ui/open-link` {url}
9428
+ * - `onTeardown(cb)` — register a cleanup callback invoked on
9429
+ * `ui/resource-teardown` from the host, at which point the bridge also
9430
+ * replies `{result:{}}` automatically.
9431
+ * - `getHostContext()` / `onHostContext(cb)` — the host context captured
9432
+ * from the `ui/initialize` result and merged on every
9433
+ * `ui/notifications/host-context-changed` (the notification carries only
9434
+ * the changed keys, per the ext-apps spec).
9435
+ * - `getToolInput()` / `onToolInput(cb)` — the arguments the panel's own tool
9436
+ * (`app_ui_<slug>`) was invoked with, from `ui/notifications/tool-input`,
9437
+ * falling back to the tool's own result (`ui/notifications/tool-result`,
9438
+ * which `registerMcpApps` fills with `execute()`'s return value) for a host
9439
+ * that forwards results but not inputs. A late subscriber is replayed the
9440
+ * last value, same as `onHostContext`. Lets an MCP-hosted panel resolve a
9441
+ * deep link (e.g. `{view: "wallets/my-profile"}`) the way the browser-served
9442
+ * panel resolves `location.hash`.
9443
+ * - `displayMode` — `{get, available, request, onChange, mountToggle}`, the
9444
+ * shared controller from `@agentproto/app-client/display-mode` (also
9445
+ * published as `window.McpApp.displayMode` once connected). It is what
9446
+ * mounts the floating ⤢ toggle; an app that wants the control in its own
9447
+ * header opts the floating one out (`<meta name="agentproto-display-toggle"
9448
+ * content="none">`, or `connect({displayToggle: "none"})`) and calls
9449
+ * `displayMode.mountToggle(el)` instead.
9450
+ */
9451
+ declare const MCP_APP_BRIDGE_SCRIPT = "<script>\n(function () {\n window.__AGENTPROTO_UI_TRANSPORT__ = \"mcp\";\n if (window.McpApp) return;\n var nextId = 1, pending = {}, teardownCbs = [];\n var hostContext = null, hostContextCbs = [];\n var toolInput = null, toolInputCbs = [];\n function post(m) { window.parent.postMessage(m, \"*\"); }\n function getHostContext() { return hostContext; }\n function onHostContext(cb) {\n hostContextCbs.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (hostContext) { try { cb(hostContext); } catch (_) {} }\n }\n function setHostContext(ctx) {\n if (!ctx || typeof ctx !== \"object\") return;\n // host-context-changed carries only the changed keys \u2014 merge, matching\n // the official ext-apps App behaviour.\n hostContext = Object.assign({}, hostContext || {}, ctx);\n for (var i = 0; i < hostContextCbs.length; i++) {\n try { hostContextCbs[i](hostContext); } catch (_) {}\n }\n }\n function getToolInput() { return toolInput; }\n function onToolInput(cb) {\n toolInputCbs.push(cb);\n // Replay the last known input so a late subscriber isn't stuck blind \u2014\n // same convention as onHostContext.\n if (toolInput) { try { cb(toolInput); } catch (_) {} }\n }\n function setToolInput(input) {\n if (!input || typeof input !== \"object\") return;\n toolInput = input;\n for (var i = 0; i < toolInputCbs.length; i++) {\n try { toolInputCbs[i](toolInput); } catch (_) {}\n }\n }\n // A ui/notifications/tool-result carries the full CallToolResult of the\n // tool call that opened this view (spec 2026-01-26) \u2014 the same envelope\n // registerMcpApps wraps execute()'s return value in (JSON text content, or\n // structuredContent). Read as a fallback source of the tool's arguments\n // for a host that forwards results but not tool-input.\n function extractToolResultInput(result) {\n try {\n if (result && result.structuredContent && typeof result.structuredContent === \"object\") {\n return result.structuredContent;\n }\n var c = result && Array.isArray(result.content) ? result.content[0] : null;\n if (c && c.type === \"text\" && typeof c.text === \"string\") return JSON.parse(c.text);\n } catch (_) {}\n return null;\n }\n window.addEventListener(\"message\", function (e) {\n var m = e.data;\n if (typeof m === \"string\") { try { m = JSON.parse(m); } catch (_) { return; } }\n if (!m || m.jsonrpc !== \"2.0\") return;\n if (m.method === \"ui/resource-teardown\") {\n for (var i = 0; i < teardownCbs.length; i++) teardownCbs[i]();\n post({ jsonrpc: \"2.0\", id: m.id, result: {} });\n return;\n }\n if (m.method === \"ui/notifications/host-context-changed\") { setHostContext(m.params || {}); return; }\n if (m.method === \"ui/notifications/tool-input\") { setToolInput((m.params || {}).arguments); return; }\n if (m.method === \"ui/notifications/tool-result\") { setToolInput(extractToolResultInput(m.params)); return; }\n if (m.id != null && pending[m.id]) { var cb = pending[m.id]; delete pending[m.id]; cb(m.result, m.error); }\n });\n function request(method, params) {\n return new Promise(function (resolve, reject) {\n var id = nextId++;\n pending[id] = function (r, err) { if (err) reject(new Error((err && err.message) || String(err))); else resolve(r); };\n post({ jsonrpc: \"2.0\", id: id, method: method, params: params });\n });\n }\n function requestDisplayMode(mode) { return request(\"ui/request-display-mode\", { mode: mode }); }\n // The shared toggle (@agentproto/app-client/display-mode), injected right\n // after this script. Absent only when the app ships its own\n // window.AgentprotoUI namespace, which suppresses that injection \u2014 the\n // fallback keeps window.McpApp.displayMode callable either way.\n function installDisplayMode(opts) {\n var api = { getHostContext: getHostContext, onHostContext: onHostContext, requestDisplayMode: requestDisplayMode };\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) {\n return window.AgentprotoUI.installDisplayMode(api, opts);\n }\n return {\n get: function () { return (hostContext && hostContext.displayMode) || \"inline\"; },\n available: function () { return (hostContext && hostContext.availableDisplayModes) || []; },\n request: requestDisplayMode,\n onChange: function (cb) { onHostContext(function (c) { cb((c && c.displayMode) || \"inline\", c); }); return function () {}; },\n mountToggle: function () { return null; }\n };\n }\n window.McpApp = {\n connect: function (opts) {\n if (window.parent === window) return Promise.reject(new Error(\"no host (standalone)\"));\n return request(\"ui/initialize\", {\n appInfo: { name: \"agentproto-app\", version: \"1.0\" },\n appCapabilities: { availableDisplayModes: [\"inline\", \"fullscreen\"] },\n protocolVersion: \"2026-01-26\"\n }).then(function (result) {\n // The initialize result carries the initial hostContext (displayMode,\n // availableDisplayModes, theme, safeAreaInsets) \u2014 capture it before\n // notifying the host, so the toggle renders in the right place on\n // its first paint rather than after a host-context-changed.\n if (result && result.hostContext) setHostContext(result.hostContext);\n post({ jsonrpc: \"2.0\", method: \"ui/notifications/initialized\", params: {} });\n var displayMode = installDisplayMode((opts && opts.displayToggle) ? { toggle: opts.displayToggle } : null);\n window.McpApp.displayMode = displayMode;\n return {\n callTool: function (name, args) { return request(\"tools/call\", { name: name, arguments: args || {} }); },\n sendMessage: function (p) {\n var params = typeof p === \"string\" ? { content: p } : (p || {});\n var content = typeof params.content === \"string\" ? [{ type: \"text\", text: params.content }] : (params.content || []);\n return request(\"ui/message\", { role: \"user\", content: content });\n },\n updateModelContext: function (p) { return request(\"ui/update-model-context\", p || {}); },\n openLink: function (url) { return request(\"ui/open-link\", { url: url }); },\n onTeardown: function (cb) { teardownCbs.push(cb); },\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n getToolInput: getToolInput,\n onToolInput: onToolInput,\n displayMode: displayMode\n };\n });\n }\n };\n})();\n</script>\n";
9452
+ /** Inject `MCP_APP_BRIDGE_SCRIPT` followed by the two
9453
+ * `window.AgentprotoUI` scripts (`DISPLAY_MODE_SCRIPT`, then
9454
+ * `RUNNER_SELECT_SCRIPT`) — every installed app UI gets the shared
9455
+ * display-mode toggle and `mountRunnerSelect` for free, same as it gets
9456
+ * `window.McpApp` (see `injectAfterStructuralTag` for the placement rule).
9457
+ * The halves are independently idempotent, so a single
9458
+ * `injectAfterStructuralTag` call inserts only whichever ones the document
9459
+ * doesn't already define — inserting them one call each would place the
9460
+ * second BEFORE the first (all target "right after the same structural
9461
+ * tag"), which is why they're concatenated first instead. `window.
9462
+ * AgentprotoUI` gates BOTH of its scripts as one unit: a document that
9463
+ * already defines that namespace brings its own, and the bridge's
9464
+ * `installDisplayMode` falls back to a controller with no button rather
9465
+ * than fighting it. The bridge guard matches an assignment only — every
9466
+ * app panel CONSUMES `window.McpApp.connect()`, so matching any mention
9467
+ * would skip injection for exactly the documents that need it, leaving
9468
+ * `window.McpApp` undefined in the host iframe. */
9469
+ declare function injectMcpAppBridge(html: string): string;
9470
+
7300
9471
  /**
7301
9472
  * MCP tools that expose the per-workspace brain to agents.
7302
9473
  *
@@ -7420,6 +9591,326 @@ interface WorkspaceBrainSubscriber {
7420
9591
  }
7421
9592
  declare function createWorkspaceBrainSubscriber(opts: WorkspaceBrainSubscriberOptions): WorkspaceBrainSubscriber;
7422
9593
 
9594
+ /**
9595
+ * Review ledger — the daemon-side record of every review attestation.
9596
+ *
9597
+ * Lives under the daemon state dir, NEVER in the reviewed repo's working
9598
+ * tree: `~/.agentproto/reviews/<repo-slug>/<manifestSha>/<binding>/<rangeSha>.json`
9599
+ * — the path IS the ledger key `(repoRemote, manifestSha, binding, rangeSha)`
9600
+ * (the repo slug is a filesystem-safe form of the normalized remote). One
9601
+ * file per key; a re-run of the same key overwrites it with the newer
9602
+ * attestation.
9603
+ *
9604
+ * Every attestation is recorded — including `incomplete` and dirty-tree ones,
9605
+ * so `review_ledger` shows the history honestly — but only a clean `pass` /
9606
+ * `block` is ever served back as a cache hit (see `lookupCached`).
9607
+ *
9608
+ * Each file is an {@link LedgerEntry}: the self-contained attestation (what
9609
+ * `review_export` writes out) plus host-local metadata that is NOT part of
9610
+ * what gets attested (the checkout path, the manifest path, the manifest's
9611
+ * `verdict.exportDir`).
9612
+ *
9613
+ * Beside each entry sits an optional `<rangeSha>.annotations.json`
9614
+ * ({@link LedgerAnnotations}): the MUTABLE follow-up layer — a PR link found
9615
+ * after the fact, and `review_pr` status snapshots appended over time. It is
9616
+ * never hashed into (or exported with) the attestation, so annotating an
9617
+ * entry can't disturb a signature over it.
9618
+ *
9619
+ * Same persistence primitives as the sibling JSON stores (`node:fs/promises`,
9620
+ * write-tmp + rename). The root is injectable so tests never touch
9621
+ * `~/.agentproto`.
9622
+ */
9623
+
9624
+ /** Host-local metadata stored beside an attestation (never exported). */
9625
+ interface LedgerHostMeta {
9626
+ /** Absolute path of the checkout the review ran in. */
9627
+ repoRoot: string;
9628
+ /** Absolute path of the REVIEW.md that was used. */
9629
+ manifestPath: string;
9630
+ /** The manifest's `verdict.exportDir`, resolved against `repoRoot`. */
9631
+ exportDir?: string;
9632
+ }
9633
+ interface LedgerEntry {
9634
+ attestation: Attestation;
9635
+ host: LedgerHostMeta;
9636
+ }
9637
+ /** One `review_pr` status fetch. */
9638
+ interface PrStatusSnapshot {
9639
+ /** ISO timestamp of the fetch. */
9640
+ fetchedAt: string;
9641
+ state: "open" | "merged" | "closed";
9642
+ reviews: Array<{
9643
+ login: string;
9644
+ state: string;
9645
+ submittedAt: string;
9646
+ }>;
9647
+ checks?: Array<{
9648
+ name: string;
9649
+ conclusion: string | null;
9650
+ }>;
9651
+ }
9652
+ /** The mutable annotations sidecar of a ledger entry. */
9653
+ interface LedgerAnnotations {
9654
+ pr?: ReviewPrRef;
9655
+ /** Oldest first — each `review_pr` call appends one. */
9656
+ prStatus?: PrStatusSnapshot[];
9657
+ }
9658
+ interface ReviewLedgerFilter {
9659
+ repoRemote?: string;
9660
+ rangeSha?: string;
9661
+ binding?: string;
9662
+ manifestSha?: string;
9663
+ }
9664
+ interface ReviewLedger {
9665
+ /** Absolute root directory of the ledger. */
9666
+ readonly root: string;
9667
+ put(entry: LedgerEntry): Promise<string>;
9668
+ get(key: LedgerKey): Promise<LedgerEntry | undefined>;
9669
+ /** A reusable verdict for `key`: a clean (not dirty) `pass`/`block` whose
9670
+ * rubric digests still match. `incomplete` is never reused. */
9671
+ lookupCached(key: LedgerKey, rubrics: readonly RubricDigest[]): Promise<LedgerEntry | undefined>;
9672
+ findByRunId(runId: string): Promise<LedgerEntry | undefined>;
9673
+ /** Entries matching `filter`, newest first. */
9674
+ list(filter?: ReviewLedgerFilter): Promise<LedgerEntry[]>;
9675
+ /** The annotations sidecar of `key` (`{}` when there is none). */
9676
+ getAnnotations(key: LedgerKey): Promise<LedgerAnnotations>;
9677
+ /** Read-modify-write the annotations sidecar of `key`; returns the result. */
9678
+ updateAnnotations(key: LedgerKey, update: (current: LedgerAnnotations) => LedgerAnnotations): Promise<LedgerAnnotations>;
9679
+ }
9680
+ /** Record `pr` as the entry's PR link (replacing any previous link). */
9681
+ declare const withPr: (pr: ReviewPrRef) => (a: LedgerAnnotations) => LedgerAnnotations;
9682
+ /** Append one status snapshot. */
9683
+ declare const withPrStatus: (snapshot: PrStatusSnapshot) => (a: LedgerAnnotations) => LedgerAnnotations;
9684
+ declare const defaultReviewLedgerRoot: () => string;
9685
+ /** Filesystem-safe slug of a normalized repo remote
9686
+ * (`github.com/agentproto/ts` → `github.com_agentproto_ts`). */
9687
+ declare function repoSlug(repoRemote: string): string;
9688
+ declare function createReviewLedger(opts?: {
9689
+ root?: string;
9690
+ }): ReviewLedger;
9691
+
9692
+ /**
9693
+ * Review runner — the daemon host for `@agentproto/review`.
9694
+ *
9695
+ * The pure package parses REVIEW.md and compiles a binding into an ordinary
9696
+ * AIP-15 workflow; this module supplies everything that touches the world:
9697
+ *
9698
+ * 1. resolve the repo + range — `merge-base(<target.base>, HEAD)..HEAD`
9699
+ * unless the caller pins `base`/`head`;
9700
+ * 2. ledger pre-check — a clean `pass`/`block` already recorded for the
9701
+ * same `(repoRemote, manifestSha, binding, rangeSha)` (and the same
9702
+ * rubric digests) is returned with `cached: true`, nothing runs;
9703
+ * 3. execute the compiled workflow through `@agentproto/workflow-runtime`'s
9704
+ * `compileWorkflow` + `runWorkflow` — prepare steps (the engine's own
9705
+ * gate runner), then the FREEZE (head resolved after prepare), then the
9706
+ * lanes in parallel through {@link createReviewLaneExecutor}, then the
9707
+ * fan-in verdict;
9708
+ * 4. build the attestation and write it to the ledger. A CANCELLED run
9709
+ * (`cancel()`, or superseded by a newer head — see `supersede`) writes
9710
+ * nothing: nobody asked for its verdict any more.
9711
+ *
9712
+ * Lanes: command lanes run `sh -c <run>` in their own process group with a
9713
+ * hard timeout; agent lanes spawn a CHILD REVIEWER SESSION through
9714
+ * {@link ReviewerSessionHost} — in the daemon, {@link createDaemonReviewerHost},
9715
+ * which uses the same `spawnAgentSession` core `agent_start` uses and kills
9716
+ * the session through the registry on timeout/cancel (a child session, never
9717
+ * an orphan pid). The reviewer gets a pointer-style prompt (range + rubric
9718
+ * path) and writes a structured verdict file under the ledger's run dir —
9719
+ * outside the reviewed tree.
9720
+ *
9721
+ * Async pattern mirrors `WorkflowRunner`: `start()` returns a run record
9722
+ * immediately and executes in the background; `status()` polls; `wait()`
9723
+ * blocks. Runs live in memory for the daemon's lifetime; the ATTESTATION is
9724
+ * what persists (the ledger), and `status()` falls back to it for a run that
9725
+ * finished before a daemon restart.
9726
+ */
9727
+
9728
+ /**
9729
+ * Normalize a git remote URL so SSH and HTTPS clones of the same repo agree:
9730
+ * `git@github.com:agentproto/ts.git` and `https://user@github.com/agentproto/ts`
9731
+ * both become `github.com/agentproto/ts`. Credentials are always stripped.
9732
+ */
9733
+ declare function normalizeRemote(url: string): string;
9734
+ /** Run one command lane: `sh -c <command>` in its own process group, output
9735
+ * captured (tail-bounded), hard-killed on timeout or cancel. */
9736
+ declare function runShellLane(input: {
9737
+ command: string;
9738
+ cwd: string;
9739
+ timeoutMs: number;
9740
+ signal?: AbortSignal;
9741
+ }): Promise<LaneOutcome>;
9742
+ /** Outcome of one reviewer session's run. */
9743
+ type ReviewerRunResult = {
9744
+ status: "ended";
9745
+ sessionId: string;
9746
+ preset: string;
9747
+ model?: string;
9748
+ } | {
9749
+ status: "timeout";
9750
+ sessionId: string;
9751
+ preset: string;
9752
+ model?: string;
9753
+ } | {
9754
+ status: "failed";
9755
+ error: string;
9756
+ sessionId?: string;
9757
+ preset?: string;
9758
+ model?: string;
9759
+ };
9760
+ /**
9761
+ * The agent-lane executor seam: spawn a reviewer session under a harness
9762
+ * preset, run ONE turn with `prompt`, and stop it — killing it through the
9763
+ * session lifecycle on timeout or cancel. The daemon wires
9764
+ * {@link createDaemonReviewerHost}; tests wire a fake.
9765
+ */
9766
+ interface ReviewerSessionHost {
9767
+ run(input: {
9768
+ preset: string;
9769
+ cwd: string;
9770
+ prompt: string;
9771
+ label: string;
9772
+ timeoutMs: number;
9773
+ parentSessionId?: string;
9774
+ signal?: AbortSignal;
9775
+ }): Promise<ReviewerRunResult>;
9776
+ }
9777
+ interface ReviewLaneExecutorContext {
9778
+ runId: string;
9779
+ reviewId: string;
9780
+ repoRoot: string;
9781
+ manifestPath: string;
9782
+ /** Directory the agent lanes' verdict files are written under. */
9783
+ runDir: string;
9784
+ reviewers?: ReviewerSessionHost;
9785
+ parentSessionId?: string;
9786
+ }
9787
+ /** The daemon's {@link ReviewLaneExecutor}: subprocess command lanes, child
9788
+ * reviewer sessions for agent lanes. */
9789
+ declare function createReviewLaneExecutor(ctx: ReviewLaneExecutorContext): ReviewLaneExecutor;
9790
+ interface ReviewRunInput {
9791
+ /** Any directory inside the repo to review. */
9792
+ cwd: string;
9793
+ /** REVIEW.md path — absolute, or relative to `cwd`. Default: `<repo root>/REVIEW.md`. */
9794
+ manifestPath?: string;
9795
+ /** Binding name. Default: the sole binding, or `default`. */
9796
+ binding?: string;
9797
+ /** Range base (ref or sha). Default: `merge-base(<target.base>, HEAD)`. */
9798
+ base?: string;
9799
+ /** Range head (ref or sha). Default: `HEAD`, resolved AFTER prepare. */
9800
+ head?: string;
9801
+ /** Ignore a cached ledger verdict and re-run. */
9802
+ nocache?: boolean;
9803
+ /** Session the agent lanes' reviewer sessions nest under. */
9804
+ parentSessionId?: string;
9805
+ /** Session that requested the review — recorded as
9806
+ * `attestation.requester.sessionId`. Default: `parentSessionId`. */
9807
+ requesterSessionId?: string;
9808
+ /** The PR the range is being reviewed for, when the caller knows it —
9809
+ * recorded as `attestation.pr` and as the entry's annotation link. */
9810
+ pr?: ReviewPrRef;
9811
+ /** Before running, cancel in-flight runs for the same repo + binding + base
9812
+ * whose head DIFFERS (an older push nobody will read the verdict of) — as
9813
+ * long as that run is the same line of work: the same checkout, or a head
9814
+ * that is an ancestor of this one. A sibling branch reviewed from another
9815
+ * worktree off the same base is never cancelled. Gates should pass
9816
+ * `true`; the default is `false`. */
9817
+ supersede?: boolean;
9818
+ }
9819
+ type ReviewRunStatus = "running" | "done" | "failed" | "cancelled";
9820
+ interface ReviewRun {
9821
+ runId: string;
9822
+ status: ReviewRunStatus;
9823
+ startedAt: string;
9824
+ endedAt?: string;
9825
+ reviewId?: string;
9826
+ binding?: string;
9827
+ repoRemote?: string;
9828
+ /** The resolved range base, once known. */
9829
+ baseSha?: string;
9830
+ /** The range head: the pre-prepare head once resolved, then the frozen head. */
9831
+ headSha?: string;
9832
+ /** The checkout the run reviews, once resolved. */
9833
+ repoRoot?: string;
9834
+ /** Set on a run cancelled because a newer head superseded it. */
9835
+ supersededBy?: string;
9836
+ /** Lanes settled so far (live progress while `running`). */
9837
+ lanes: LaneResult[];
9838
+ /** Set once `done`. */
9839
+ attestation?: Attestation;
9840
+ /** True when `attestation` was served from the ledger — nothing ran. */
9841
+ cached?: boolean;
9842
+ /** Where the attestation was written in the ledger. */
9843
+ ledgerPath?: string;
9844
+ error?: string;
9845
+ }
9846
+ interface ReviewRunner {
9847
+ start(input: ReviewRunInput): ReviewRun;
9848
+ status(runId: string): Promise<ReviewRun | undefined>;
9849
+ wait(runId: string): Promise<ReviewRun | undefined>;
9850
+ cancel(runId: string): boolean;
9851
+ readonly ledger: ReviewLedger;
9852
+ }
9853
+ interface CreateReviewRunnerOptions {
9854
+ ledger: ReviewLedger;
9855
+ /** Agent-lane executor. Omitted ⇒ agent lanes are `skipped` (and the
9856
+ * verdict therefore `incomplete`), never silently passed. */
9857
+ reviewers?: ReviewerSessionHost;
9858
+ /** Attestor identity. Default: `agentproto-runtime@<hostname>`. */
9859
+ daemonId?: string;
9860
+ }
9861
+ /** Placeholder values the daemon binds for every review. `{changed}` is a
9862
+ * shell-quoted turbo filter for the packages changed since the range base
9863
+ * (compose dependents with `...{changed}`). */
9864
+ declare function hostPlaceholders(baseSha: string): Record<string, string>;
9865
+ declare function createReviewRunner(opts: CreateReviewRunnerOptions): ReviewRunner;
9866
+
9867
+ /**
9868
+ * PR follow-up for review attestations — the host side of `review_pr`.
9869
+ *
9870
+ * A review attests a git range; whether that range became a PR, and what
9871
+ * happened to the PR afterwards, is only knowable later. This module asks
9872
+ * GitHub through the daemon host's authed `gh` CLI (shelled out — no HTTP
9873
+ * client, no token handling of our own):
9874
+ *
9875
+ * - {@link findPrForCommit}: `headSha → PR` via
9876
+ * `gh api repos/{owner}/{repo}/commits/{sha}/pulls`;
9877
+ * - {@link fetchPrStatus}: the PR's state + reviews (+ check runs on the
9878
+ * PR head, best-effort) as one {@link PrStatusSnapshot}.
9879
+ *
9880
+ * Every failure is a {@link PrLookupError} with a stable `code` — `gh`
9881
+ * missing, `gh` failing (auth, network, 404), a non-GitHub remote, no PR for
9882
+ * the commit — so the tool can return it as structured data instead of
9883
+ * throwing.
9884
+ */
9885
+
9886
+ type PrLookupErrorCode = "gh_unavailable" | "gh_failed" | "unsupported_remote" | "no_pr";
9887
+ declare class PrLookupError extends Error {
9888
+ readonly code: PrLookupErrorCode;
9889
+ constructor(code: PrLookupErrorCode, message: string);
9890
+ }
9891
+ /** Runs `gh <args>` and resolves its stdout. */
9892
+ type GhRunner = (args: readonly string[]) => Promise<string>;
9893
+ /** The real runner: `gh` from the daemon's PATH. A missing binary is
9894
+ * `gh_unavailable`; any non-zero exit is `gh_failed` with gh's stderr. */
9895
+ declare const execGh: GhRunner;
9896
+ /** `github.com/owner/name` → `owner/name`; `undefined` for any other host.
9897
+ * Also accepts the multi-account SSH host-alias convention
9898
+ * (`git@github.com-work:owner/name` normalizes to `github.com-work/owner/name`). */
9899
+ declare function githubRepoOf(repoRemote: string): string | undefined;
9900
+ /** Parse `https://github.com/owner/name/pull/42` (trailing path/query ok). */
9901
+ declare function parsePrUrl(url: string): {
9902
+ repo: string;
9903
+ number: number;
9904
+ } | undefined;
9905
+ /** The PR `headSha` belongs to, or `undefined` when GitHub knows none.
9906
+ * Prefers a PR whose head IS `headSha`, then an open one, then the first. */
9907
+ declare function findPrForCommit(gh: GhRunner, repo: string, headSha: string): Promise<ReviewPrRef | undefined>;
9908
+ /** The PR's head sha (for matching a `prUrl` to ledger entries). */
9909
+ declare function prHeadSha(gh: GhRunner, repo: string, number: number): Promise<string | undefined>;
9910
+ /** One status snapshot of `pr`: state, reviews, and (best-effort — omitted
9911
+ * when that call fails) the check runs on the PR's current head. */
9912
+ declare function fetchPrStatus(gh: GhRunner, pr: ReviewPrRef, now?: () => Date): Promise<PrStatusSnapshot>;
9913
+
7423
9914
  /**
7424
9915
  * `~/.agentproto/harness-presets.json` — the persisted harness→profile binding
7425
9916
  * (mode 0600). A {@link HarnessPreset} names, per adapter harness, WHICH auth
@@ -7527,6 +10018,77 @@ declare function removeHarnessPreset(id: string): Promise<boolean>;
7527
10018
  */
7528
10019
  declare function setDefaultPreset(harnessSlug: string, presetId: string): Promise<HarnessPreset>;
7529
10020
 
10021
+ /**
10022
+ * The daemon's {@link ReviewerSessionHost}: agent review lanes as CHILD
10023
+ * SESSIONS, spawned through the same `spawnAgentSession` core `agent_start`
10024
+ * uses, waited on with the same `monitorSessionWait` core `session_monitor` /
10025
+ * `sessions wait` use, and stopped through `registry.kill` — the normal
10026
+ * session lifecycle, so a timed-out or cancelled reviewer is a killed session
10027
+ * in `session_list`, never an orphan pid.
10028
+ *
10029
+ * A lane's `preset` names the daemon's existing preset surface — no parallel
10030
+ * model/auth config: first a harness preset (`harness-presets.json`: adapter
10031
+ * harness + auth profile + default model), then a user preset
10032
+ * (`presets.json`, handed to `spawnAgentSession` as-is so it expands the
10033
+ * preset exactly like `agent_start.presetId` does).
10034
+ *
10035
+ * Reviewers spawn with role `executor` (no re-delegation), `worktree: false`
10036
+ * (they read the reviewed checkout's history; they never edit), and
10037
+ * `dedupe: false` (two lanes/runs with the same label are distinct
10038
+ * reviewers, never merged).
10039
+ */
10040
+
10041
+ interface DaemonReviewerHostDeps {
10042
+ registry: SessionsRegistry;
10043
+ sessionEvents: SessionEventBus;
10044
+ eventRing: EventRing;
10045
+ resolveAgentAdapter: AgentAdapterResolver;
10046
+ /** Extra spawn deps forwarded to `spawnAgentSession` (sandbox resolver,
10047
+ * catalog models, …). `registry`/`resolveAgentAdapter` always win. */
10048
+ spawnDeps?: Partial<SpawnAgentSessionDeps>;
10049
+ /** Preset lookups — injectable for tests; default to the real stores. */
10050
+ getHarnessPreset?: (id: string) => Promise<HarnessPreset | undefined>;
10051
+ getUserPreset?: (id: string) => Promise<UserPreset | undefined>;
10052
+ }
10053
+ /** Resolve a lane's `preset` to spawn fields — harness preset first, then a
10054
+ * user preset. `undefined` when neither store knows the id. */
10055
+ declare function resolveReviewerPreset(preset: string, lookups?: Pick<DaemonReviewerHostDeps, "getHarnessPreset" | "getUserPreset">): Promise<Pick<SpawnAgentSessionInput, "adapter" | "model" | "access" | "preset"> | undefined>;
10056
+ declare function createDaemonReviewerHost(deps: DaemonReviewerHostDeps): ReviewerSessionHost;
10057
+
10058
+ /**
10059
+ * MCP tools for the review primitive (`@agentproto/review`):
10060
+ *
10061
+ * review_run run a REVIEW.md binding over a git range → attestation
10062
+ * (ledger hit ⇒ the cached verdict, `cached: true`)
10063
+ * review_status poll a run started with `wait: false`
10064
+ * review_cancel cancel a running review (lane sessions are killed)
10065
+ * review_ledger list recorded attestations
10066
+ * review_export write one attestation out as a standalone JSON file
10067
+ * review_pr follow a recorded review to its GitHub PR (via `gh`):
10068
+ * link it, snapshot the PR's state/reviews/checks into the
10069
+ * entry's annotations, return the combined view
10070
+ *
10071
+ * Async pattern mirrors `workflow_start`/`workflow_status`: `review_run`
10072
+ * blocks by default (`wait: true`) — pass `wait: false` for long reviews and
10073
+ * poll `review_status` with the returned `runId`.
10074
+ *
10075
+ * Trust note: `review_run` executes the command checks the repo's own
10076
+ * REVIEW.md declares (`sh -c`), the same trust `workflow_run_file` extends to
10077
+ * a WORKFLOW.md's gate steps — not the `command_execute` allowlist, which
10078
+ * governs ad-hoc commands a remote caller composes.
10079
+ */
10080
+
10081
+ interface RegisterReviewToolsOptions {
10082
+ runner: ReviewRunner;
10083
+ /** The calling session (from `?callerSessionId=`) — agent-lane reviewer
10084
+ * sessions nest under it, and it is the default requester. */
10085
+ callerSessionId?: string;
10086
+ /** `gh` runner for `review_pr` — injectable for tests. Default: the real
10087
+ * `gh` on the daemon's PATH. */
10088
+ gh?: GhRunner;
10089
+ }
10090
+ declare function registerReviewTools(server: McpServer, opts: RegisterReviewToolsOptions): void;
10091
+
7530
10092
  /**
7531
10093
  * Built-in custom-route registration (PR-5).
7532
10094
  *
@@ -7987,6 +10549,76 @@ declare function reapGcEntry(entry: SandboxLedgerEntry, opts: {
7987
10549
  /** States that still name a box the gc might act on. */
7988
10550
  declare const GC_REAPABLE_STATES: readonly SandboxLedgerState[];
7989
10551
 
10552
+ /**
10553
+ * Sandbox ledger reconcile — closes the gap between what
10554
+ * `~/.agentproto/sandboxes.json` believes is still booted/paused/connected
10555
+ * and what the PROVIDER actually still has running. A failed teardown, a
10556
+ * daemon that crashed mid-close, or a provider-side idle-reap the daemon
10557
+ * never heard about all leave a ledger row claiming a box is alive long
10558
+ * after the provider reaped it — real-world evidence: one daemon's ledger
10559
+ * carried ~50 rows in "paused" while the provider had far fewer boxes left.
10560
+ *
10561
+ * This is read-only against the PROVIDER (`SandboxProvider.probe`, the same
10562
+ * primitive `GET /sandboxes/:id/alive` and `agentproto sandbox list` already
10563
+ * use per-row) and never tears a box down — it only ever flips a ledger
10564
+ * row's `state` to `"gone"` via `recordSandboxLiveness` when the provider
10565
+ * confirms the box no longer exists. A box a caller still wants stays
10566
+ * completely untouched; this module has no `stop()`/`pause()`/`connect()`
10567
+ * call anywhere in it.
10568
+ */
10569
+
10570
+ /** The slice of `SandboxProviderHandle` reconcile needs — just the liveness
10571
+ * probe, so tests (and the `local` provider, which has none) can hand back
10572
+ * a handle without a `probe` at all. */
10573
+ interface SandboxReconcileProviderHandle {
10574
+ provider: {
10575
+ probe?(sandboxId: string): Promise<{
10576
+ alive: boolean;
10577
+ state?: string;
10578
+ }>;
10579
+ };
10580
+ }
10581
+ interface SandboxReconcileDeps {
10582
+ /** Resolve a ledger row's provider slug to a handle. Mirrors
10583
+ * `SandboxGcReapDeps.resolveProvider` (`sandbox-gc.ts`) — same DI shape,
10584
+ * narrower need (probe only, never connect/boot). */
10585
+ resolveProvider: (slug: string) => Promise<SandboxReconcileProviderHandle | null>;
10586
+ /** Defaults to `readSandboxLedger` — injectable so callers that already
10587
+ * hold a mocked/scoped ledger reader (tests, a scoped CLI mock) can reuse
10588
+ * it instead of this module reaching for the real `~/.agentproto/
10589
+ * sandboxes.json` on disk. */
10590
+ readLedger?: (path?: string) => SandboxLedgerEntry[];
10591
+ /** Defaults to `recordSandboxLiveness` — same injectability reasoning. */
10592
+ recordLiveness?: (sandboxId: string, alive: boolean, path?: string) => void;
10593
+ ledgerPath?: string;
10594
+ }
10595
+ /** Verdict for one reconciled row. `"skipped"` means the provider has no
10596
+ * `probe()` (e.g. Box today) — never treated as evidence of death. */
10597
+ type SandboxReconcileVerdict = "alive" | "gone" | "unknown" | "skipped";
10598
+ interface SandboxReconcileRow {
10599
+ sandboxId: string;
10600
+ provider: string;
10601
+ verdict: SandboxReconcileVerdict;
10602
+ /** Present when `verdict` is "unknown" — the resolve/probe failure message. */
10603
+ error?: string;
10604
+ }
10605
+ interface SandboxReconcileResult {
10606
+ checked: number;
10607
+ alive: number;
10608
+ gone: number;
10609
+ unknown: number;
10610
+ skipped: number;
10611
+ rows: SandboxReconcileRow[];
10612
+ }
10613
+ /**
10614
+ * Probe every ledger row still claiming to be alive against its provider,
10615
+ * and stamp the verdict back into the ledger (`recordSandboxLiveness` flips
10616
+ * a dead row to `"gone"`; a live or unprobeable row is left as-is). One
10617
+ * broken row's provider resolution or probe call never aborts the pass for
10618
+ * the rest — each row's outcome is independent and best-effort.
10619
+ */
10620
+ declare function reconcileSandboxLedger(deps: SandboxReconcileDeps): Promise<SandboxReconcileResult>;
10621
+
7990
10622
  /**
7991
10623
  * Idle agent-session reaper (PR-6).
7992
10624
  *
@@ -8247,6 +10879,108 @@ declare function formatToolCall(toolName: string, args: unknown): string;
8247
10879
  */
8248
10880
  declare function formatToolResult(toolName: string | undefined, result: unknown, isError: boolean): string | null;
8249
10881
 
10882
+ /**
10883
+ * Pure sizing helper for a `tools/list` response — shared by
10884
+ * `scripts/measure-mcp-tools.mjs` (human-readable before/after report) and
10885
+ * the budget-guard test (`__tests__/tool-schema-budget.test.ts`) so both
10886
+ * measure the exact same thing: the JSON bytes an MCP client actually pays
10887
+ * for per tool (`description` + `inputSchema`), sorted biggest first.
10888
+ */
10889
+ interface MeasuredToolEntry {
10890
+ name: string;
10891
+ bytes: number;
10892
+ }
10893
+ interface ToolListMeasurement {
10894
+ entries: MeasuredToolEntry[];
10895
+ totalBytes: number;
10896
+ }
10897
+ interface MeasurableTool {
10898
+ name: string;
10899
+ description?: string;
10900
+ inputSchema?: unknown;
10901
+ }
10902
+ /** UTF-8 byte size of the `{description, inputSchema}` JSON a client
10903
+ * receives for one tool — the same two fields `tools/list` sends over the
10904
+ * wire, so this tracks prompt-token cost rather than just character count. */
10905
+ declare function measureTool(tool: MeasurableTool): number;
10906
+ declare function measureToolList(tools: readonly MeasurableTool[]): ToolListMeasurement;
10907
+
10908
+ /**
10909
+ * Long-form MCP tool documentation, read from `docs/mcp-tools/<name>.md`.
10910
+ *
10911
+ * The schema `description` on a tool/field is a contract: what it does, and
10912
+ * the one rule that prevents a wrong call — kept short so `tools/list` stays
10913
+ * cheap. Everything else (edge cases, interactions with other fields,
10914
+ * examples, history) lives here instead, fetched on demand by the `tool_help`
10915
+ * MCP tool (`tool-help-mcp.ts`) and the `agentproto help <tool>` CLI verb
10916
+ * (`packages/cli/src/commands/help.ts`) — both call the two functions below,
10917
+ * so the markdown file is the single source both surfaces read.
10918
+ *
10919
+ * `docs/mcp-tools/` sits at the PACKAGE root (a sibling of `src`/`dist`, not
10920
+ * the repo-level `docs/`) so it ships inside the published npm tarball —
10921
+ * see the `files` array in `package.json`. Resolved relative to this
10922
+ * module's own directory rather than by package name: whether this code is
10923
+ * running from `src` (vitest, no build) or `dist` (built/published), the
10924
+ * module's directory is always a direct child of the package root, so one
10925
+ * `..` reaches it either way — no `require.resolve` needed.
10926
+ */
10927
+ /** Full markdown help text for `name`, or `undefined` when no doc exists. */
10928
+ declare function getToolHelp(name: string): string | undefined;
10929
+ /** Every tool name with a help doc available — for a "did you mean" list. */
10930
+ declare function listToolHelpTopics(): string[];
10931
+ /** Extract a `## <topic>` section (case-insensitive) up to the next `##`
10932
+ * heading — shared by the `tool_help` MCP tool and the `agentproto help`
10933
+ * CLI verb so both slice a doc identically. `undefined` when no section
10934
+ * with that heading exists. */
10935
+ declare function extractHelpSection(doc: string, topic: string): string | undefined;
10936
+
10937
+ /**
10938
+ * Subtree usage rollup: one session's own usage plus the summed usage of
10939
+ * every session it (transitively) spawned — the shape `session_usage
10940
+ * { includeSubtree: true }` and `GET /sessions/:id/usage?includeSubtree=1`
10941
+ * return.
10942
+ *
10943
+ * Pure over a descriptor list (walked by `parentSessionId`, the same graph
10944
+ * `collectSubtree` uses for orchestrator scoping), so it's unit-testable
10945
+ * without a registry.
10946
+ *
10947
+ * Absent stays absent: a total is the sum of the sessions that REPORT the
10948
+ * field, and is omitted entirely when none of them do — a subtree of
10949
+ * cost-less sessions never reads as a measured $0.
10950
+ */
10951
+
10952
+ /** Summed usage across the root and all its descendants. */
10953
+ interface SubtreeUsageTotals {
10954
+ /** Sessions counted, root included. */
10955
+ sessions: number;
10956
+ costUsd?: number;
10957
+ tokensIn?: number;
10958
+ tokensOut?: number;
10959
+ cacheReadTokens?: number;
10960
+ cacheWriteTokens?: number;
10961
+ }
10962
+ /** One direct child of the root, with that child's OWN usage (its own
10963
+ * descendants roll up into `subtree`, not here). */
10964
+ interface SubtreeChildUsage {
10965
+ id: string;
10966
+ label?: string;
10967
+ status: SessionDescriptor["status"];
10968
+ costUsd?: number;
10969
+ tokensIn?: number;
10970
+ tokensOut?: number;
10971
+ }
10972
+ interface SessionUsageWithSubtree {
10973
+ self: SessionUsage;
10974
+ subtree: SubtreeUsageTotals;
10975
+ children: SubtreeChildUsage[];
10976
+ }
10977
+ /**
10978
+ * Roll `root`'s usage up over its subtree. `all` should be the FULL
10979
+ * descriptor list (archived included) so an archived intermediate session
10980
+ * doesn't sever the parent→child chain.
10981
+ */
10982
+ declare function rollupSessionSubtree(root: SessionDescriptor, all: readonly SessionDescriptor[]): SessionUsageWithSubtree;
10983
+
8250
10984
  /**
8251
10985
  * Dependency-injection surface for MCP credential resolution.
8252
10986
  *
@@ -8351,6 +11085,26 @@ declare function enrichRollupWithProviderQuota(rollup: UsageRollup, window: stri
8351
11085
  * such lines; skips any malformed or non-`usage_snapshot` line. Never throws. */
8352
11086
  declare function readUsageSnapshots(sessionId: string, baseDir?: string): Promise<UsageSnapshotRecord[]>;
8353
11087
 
11088
+ /**
11089
+ * Smart default for `features.llmEndpoint` (daemon-managed-gateway): an
11090
+ * operator who already configured a named endpoint, an upstream credential
11091
+ * link, or an explicit `llm-endpoint` custom route is clearly USING the
11092
+ * proxy already — booting with the sidecar off (today's default) would
11093
+ * silently break their next `route:{gateway:"llm-endpoint"}` spawn on a
11094
+ * fresh install. An operator who never touched any of that gets the
11095
+ * unchanged, conservative default (off).
11096
+ *
11097
+ * An explicit `configured` value (`config.json`'s `features.llmEndpoint`,
11098
+ * profile-overlaid) always wins, in either direction — this resolver only
11099
+ * fills in the gap when the operator left the flag unset.
11100
+ */
11101
+ /**
11102
+ * Resolve the EFFECTIVE `features.llmEndpoint` flag. `configured` is the
11103
+ * operator's merged config value (`undefined` when never set in either
11104
+ * `config.json` or the active profile).
11105
+ */
11106
+ declare function resolveEffectiveLlmEndpointFlag(configured: boolean | undefined): Promise<boolean>;
11107
+
8354
11108
  /**
8355
11109
  * MCP tools that drive the pairing registry — the daemon's "let a client pair
8356
11110
  * with me over an untrusted rendezvous" surface (design: DESIGN §6).
@@ -8372,6 +11126,90 @@ interface RegisterPairingToolsOptions {
8372
11126
  }
8373
11127
  declare function registerPairingTools(server: McpServer, opts: RegisterPairingToolsOptions): void;
8374
11128
 
11129
+ /**
11130
+ * Device registry — a read view over `PairingRegistry` (+, when wired,
11131
+ * `HostRegistry`) that presents each pairing/host as a "device" (DEVICES-PLAN
11132
+ * PR-A, hosts added PR-C). A paired CLIENT (today's `pair_offer`/`pair
11133
+ * accept`) gets `role: "client"`; a registered HOST (`pair offer --host` +
11134
+ * `devices add`, host-registry.ts) gets `role: "host"`.
11135
+ *
11136
+ * This is deliberately a thin computed view, not a second store: renaming
11137
+ * and revoking a client device mutate the same `pairings.json`
11138
+ * `PairingRegistry` already owns; a host device mutates the same
11139
+ * `hosts.json` `HostRegistry` — so `pair_*`/`/pairings` and
11140
+ * `device_*`/`/devices` are two surfaces over the same two sources of truth.
11141
+ */
11142
+
11143
+ type DeviceRole = "client" | "host";
11144
+ type DeviceKind = "browser" | "cli" | "daemon";
11145
+ interface Device {
11146
+ fingerprint: string;
11147
+ name: string;
11148
+ role: DeviceRole;
11149
+ kind: DeviceKind;
11150
+ rendezvous: string;
11151
+ createdAt: string;
11152
+ lastSeen: string;
11153
+ /** A channel (offer or reconnect) is served for this device right now
11154
+ * (client) or a `forwardHttp` call is in flight for it right now (host).
11155
+ * For a host this is NOT a live heartbeat — see host-registry.ts's
11156
+ * "Online tracking" — it only reflects actual recent/current traffic. */
11157
+ online: boolean;
11158
+ /** A pair/v1 pairing: listed and revocable, but can't connect until
11159
+ * re-paired — see `PairingRecord.legacy`. */
11160
+ legacy?: true;
11161
+ /** Set when this device's pairing/host record was granted under a
11162
+ * HOST-scoped offer (`agentproto pair offer --host`) — see
11163
+ * offer-url.ts's "Offer scope". Surfaced from either side of a pairing so
11164
+ * a user can see, from EITHER daemon, which of their pairings/hosts grant
11165
+ * host control. */
11166
+ scope?: "host";
11167
+ }
11168
+ interface DeviceRegistry {
11169
+ /** Every known device. Read-only. */
11170
+ list(): Promise<Device[]>;
11171
+ /** Rename a device (fingerprint or current name) to a new label. Tries the
11172
+ * pairing registry first, then the host registry. Returns false when
11173
+ * nothing matched. */
11174
+ rename(idOrName: string, newName: string): Promise<boolean>;
11175
+ /** Drop a device by fingerprint or name — same effect as `pair revoke` for
11176
+ * a client device, `devices revoke`-of-a-host for a host device. Tries
11177
+ * the pairing registry first, then the host registry. Returns false when
11178
+ * nothing matched. */
11179
+ revoke(idOrName: string): Promise<boolean>;
11180
+ /** Register a host from an offer URL (delegates to `HostRegistry.add`).
11181
+ * Rejects if no `HostRegistry` was wired into this device registry. */
11182
+ add(offerUrl: string, name?: string): Promise<{
11183
+ fingerprint: string;
11184
+ name: string;
11185
+ rendezvousUrl: string;
11186
+ }>;
11187
+ }
11188
+ declare function createDeviceRegistry(pairing: PairingRegistry, hosts?: HostRegistry): DeviceRegistry;
11189
+
11190
+ /**
11191
+ * MCP tools over the device registry (DEVICES-PLAN PR-A/PR-C) — `device_list`
11192
+ * / `device_rename` / `device_revoke` / `device_add`. A view on top of the
11193
+ * same registries `pair_offer`/`pair_list`/`pair_revoke` (pairing-tools.ts)
11194
+ * and host-registry.ts drive; those keep working unchanged. `device_revoke`
11195
+ * has the exact effect of `pair_revoke` for a client device.
11196
+ *
11197
+ * `device_add` (PR-C) registers a HOST from a `--host`-scoped offer URL —
11198
+ * see `HostRegistry.add`'s scope gate (host-registry.ts). It requires the
11199
+ * gateway to have wired a `HostRegistry` into the `DeviceRegistry`; without
11200
+ * one the call rejects with a clear message rather than a missing-tool error
11201
+ * (this repo always wires one when `pairingRegistry` is wired, so that's
11202
+ * only reachable via a deliberate host misconfiguration).
11203
+ *
11204
+ * Registered beside `pair_*` — same closure-rebind pattern; the registry
11205
+ * singleton lives on the gateway.
11206
+ */
11207
+
11208
+ interface RegisterDeviceToolsOptions {
11209
+ registry: DeviceRegistry;
11210
+ }
11211
+ declare function registerDeviceTools(server: McpServer, opts: RegisterDeviceToolsOptions): void;
11212
+
8375
11213
  /**
8376
11214
  * Rate-limited failure logging for reconnect loops.
8377
11215
  *
@@ -9209,6 +12047,15 @@ interface SessionWaitResult {
9209
12047
  * reported one. `"error"` means the adapter reported a failed turn.
9210
12048
  * Same three-branch coverage as `empty`. */
9211
12049
  reason?: string;
12050
+ /** The matched turn-end's captured error text — `SessionTurnEndEvent.error`
12051
+ * on the ring-replay/bus branches, `SessionDescriptor.lastTurnErrorMessage`
12052
+ * on the sync fast-path (no event object to read there). Present only
12053
+ * when `reason` is `"error"` AND the adapter emitted an in-band `error`
12054
+ * stream event during the turn. Same three-branch coverage as `empty`/
12055
+ * `reason` — this is the field a waiter (a supervisor session, `sessions
12056
+ * wait`, a webhook) needed to actually learn WHY a turn errored instead
12057
+ * of just that it did. */
12058
+ error?: string;
9212
12059
  }
9213
12060
  /**
9214
12061
  * Block until one of the listed sessions fires a matching lifecycle event
@@ -9358,6 +12205,12 @@ interface CreateGatewayOptions {
9358
12205
  * to an `enabled: false` summary. Also surfaced in `daemon_health` /
9359
12206
  * `GET /health` so an operator can see the effective value. */
9360
12207
  resumeSessionsOnBoot?: boolean;
12208
+ /** Opt-in continue-on-boot. Mirrors the `daemon.continueInterruptedOnBoot`
12209
+ * config knob; when true, the `continueInterruptedOnBoot()` handle method
12210
+ * runs the boot continue pass (see `continue-interrupted.ts`); when
12211
+ * false/omitted it short-circuits to an `enabled: false` summary. Surfaced
12212
+ * in `daemon_health` / `GET /health`. */
12213
+ continueInterruptedOnBoot?: boolean;
9361
12214
  /** Idle agent-session reaper threshold in ms (PR-6). Mirrors the
9362
12215
  * `daemon.idleReapAfterMs` config knob; the CLI resolves it (env >
9363
12216
  * config > off) and passes it here. When positive, the gateway starts a
@@ -9558,6 +12411,16 @@ interface CreateGatewayOptions {
9558
12411
  * `worktree_gc` returns a clear "not enabled" error.
9559
12412
  */
9560
12413
  runWorktreeGc?: WorktreeGcRunner;
12414
+ /**
12415
+ * Optional branch-`gc` runner powering `branch_gc` (+ `POST /branches/gc`).
12416
+ * Injected for the same reason as `runWorktreeGc`. Omitted → `branch_gc`
12417
+ * returns a clear "not enabled" error.
12418
+ */
12419
+ runBranchGc?: BranchGcRunner;
12420
+ /** Optional verdict recorder powering `branch_gc_verdict` (+ `POST /branches/gc/verdict`). */
12421
+ recordBranchGcVerdict?: BranchGcVerdictRecorder;
12422
+ /** Optional verdict reader powering `branch_gc_verdict_get`. */
12423
+ readBranchGcVerdict?: BranchGcVerdictReader;
9561
12424
  /**
9562
12425
  * Optional best-effort exit-time reclaim of ONE policy-provisioned
9563
12426
  * (implicit) session's own worktree — powers `SessionDescriptor.
@@ -9601,12 +12464,41 @@ interface CreateGatewayOptions {
9601
12464
  * returns) so the injected `serve` can capture the finished gateway.
9602
12465
  */
9603
12466
  pairingRegistry?: PairingRegistry;
12467
+ /**
12468
+ * Optional HOST registry (see `createHostRegistry`, DEVICES-PLAN PR-C) —
12469
+ * the reverse-pairing "client" half of pair/v2, living daemon-side. When
12470
+ * wired alongside `pairingRegistry`, the gateway layers registered hosts
12471
+ * into `device_list`/`GET /devices`, and enables the `device_add` MCP tool
12472
+ * plus `POST /devices/add` + `POST /devices/:id/exec`. Unlike
12473
+ * `pairingRegistry`, this registry keeps no standing connections — every
12474
+ * `add`/`forwardHttp` dials fresh — so there is nothing for the gateway to
12475
+ * autoconnect or tear down on shutdown.
12476
+ */
12477
+ hostRegistry?: HostRegistry;
9604
12478
  /** Enable the local LLM Endpoint proxy sidecar (route registration,
9605
12479
  * MCP tools, child-process lifecycle). Default false — the endpoint is
9606
12480
  * an opt-in feature; when off, the `llm-endpoint` custom route is not
9607
12481
  * registered and the `llm_endpoint_*` MCP tools are not exposed. */
9608
12482
  llmEndpoint?: boolean;
9609
12483
  }
12484
+ /**
12485
+ * Default always-on set when `deferredTools` is enabled without an
12486
+ * explicit `alwaysOn` override: the core spawn/drive/observe/report loop
12487
+ * most sessions need immediately — including an EXECUTOR (the role
12488
+ * `deferredTools` defaults on for, see `role.ts`), which cannot spawn
12489
+ * (`agent_start`/`agent_prompt` are stripped for it anyway by
12490
+ * `toolPolicy.delegation: "deny"`, so those two names are simply absent
12491
+ * from a deny-role's registry and cost nothing) but still needs to report
12492
+ * back to its parent (`message_parent`), coordinate over the task ledger
12493
+ * (`task_*`), and observe its own session/turn state
12494
+ * (`session_context_status` alongside the pre-existing `session_list` /
12495
+ * `session_monitor` / `session_events_poll`). Everything else (fs_*,
12496
+ * directory_*, command_*, remote_*, browser_*, terminal_*, mcp_*,
12497
+ * routine_*, workflow_*, policy_*, inbound_watcher_*, session_tree,
12498
+ * agent_export, adapter_list, every `app_ui_*` panel tool, ...) starts
12499
+ * deferred — reachable via `tool_search`, never removed from `tools/call`.
12500
+ */
12501
+ declare const DEFAULT_ALWAYS_ON_TOOLS: readonly string[];
9610
12502
  interface GatewayHandle {
9611
12503
  url: string;
9612
12504
  workspace: string;
@@ -9624,6 +12516,10 @@ interface GatewayHandle {
9624
12516
  * `CreateGatewayOptions.pairingRegistry`. Undefined otherwise. Exposed so
9625
12517
  * the CLI can `startAutoconnect()` after boot and `shutdown()` it. */
9626
12518
  pairing?: PairingRegistry;
12519
+ /** HOST registry, when one was wired via `CreateGatewayOptions.hostRegistry`
12520
+ * (DEVICES-PLAN PR-C). Undefined otherwise. No autoconnect/shutdown to
12521
+ * call on it — see that option's doc comment. */
12522
+ hosts?: HostRegistry;
9627
12523
  /** Per-boot bearer token required on mutating /sessions/* routes
9628
12524
  * + WS PTY upgrades. Exposed so an embedding host (e.g. the CLI
9629
12525
  * shell that hosts the gateway in-process) can pass it to child
@@ -9648,6 +12544,16 @@ interface GatewayHandle {
9648
12544
  resumeSessionsOnBoot(opts?: {
9649
12545
  isServed?: (desc: SessionDescriptor) => boolean;
9650
12546
  }): Promise<EagerResumeSummary>;
12547
+ /** Run the opt-in continue-on-boot pass and return its tally — a no-op
12548
+ * returning `{ enabled: false, ... }` when the `continueInterruptedOnBoot`
12549
+ * knob is off. serve.ts invokes this right AFTER `resumeSessionsOnBoot()`
12550
+ * (so a row whose eager resume failed is recognisable and skipped) — and
12551
+ * therefore, like it, after the supervisor was re-armed, so a re-armed
12552
+ * completion policy sees the continue turn's turn-end. Same `isServed`
12553
+ * cross-process gate. */
12554
+ continueInterruptedOnBoot(opts?: {
12555
+ isServed?: (desc: SessionDescriptor) => boolean;
12556
+ }): Promise<ContinueOnBootSummary>;
9651
12557
  stop(): Promise<void>;
9652
12558
  }
9653
12559
  /**
@@ -9664,4 +12570,4 @@ interface GatewayHandle {
9664
12570
  */
9665
12571
  declare function createGateway(opts: CreateGatewayOptions): Promise<GatewayHandle>;
9666
12572
 
9667
- export { type ActivityKind, type ActivityListFilter, type ActivityPolicyLister, type ActivityProjector, type ActivityProjectorRegistry, type ActivityProjectorSession, type ActivityRecord, type ActivitySource, type ActivityState, type ActivityWaitingOn, type ActivityWorkflowLister, AdapterAuthDescriptor, type AdapterCapabilitiesLister, type AdapterInstallResult, type AdapterListEntry, type AgentAdapterInstaller, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type AnthropicReaderConfig, AnthropicRemainingQuotaReader, type AttachPolicyInput, type AttachSandboxOpts, type AttachSandboxResult, AuthMethod, BUCKETS_ROOT, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, CanonicalPosture, type CatalogModelsLister, CatalogModelsQuery, CatalogModelsResponse, type CatalogProviderModel, type CatalogProviderModelsQuery, type CatalogProviderModelsResponse, type CatalogProviderPricing, type ClaudeNativeRef, type CommitSpec, type CompletionPolicySupervisor, ContextProfile, type ConversationIndexRecord, type ConversationNativeRef, type CostBudgetDecision, type CrashDetectSummary, type CrashReaperRegistry, type CreateGatewayOptions, type CreateOfferInput, type CreatedOffer, DEAD_SESSION_STATUSES, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, DeclaredAdapterOption, type DeclaredAdapterPreset, type EagerResumeFailReason, type EagerResumeOutcome, type EagerResumeSkipReason, type EagerResumeSummary, EffortLevel, GC_REAPABLE_STATES, type GateSpec, type GatewayHandle, type HarnessPreset, type HarnessPresetValidationDeps, HarnessPresetValidationError, type HarnessPresetsFile, type HermesNativeRef, INBOUND_PROVIDERS, type IdleReapSummary, type IdleReaperRegistry, type InboundEndpoint, type InboundEndpointStore, type InboundEndpointStoreOptions, type InboundEnqueuePrompt, type InboundIsSessionAlive, type InboundMessage, type InboundProvider, type InboundRestartSession, type InboundRouteMode, type InboundRouterDeps, type InboundRouterLog, type InboundSpawnForContact, type InboundWatcher, type JudgeGateSpec, LEGACY_SESSIONS_FILE, type LocatedConversation, type LocatedConversationByPath, type McpCredentialDeps, type MigrationMarker, type NormalizeInboundResult, type OnFailSpec, type OpenPrResolver, type OrchestratorGatewayDeps, type OrchestratorInjection, type OrchestratorInjector, type OrchestratorInjectorDeps, type OrchestratorMcpServerFactory, type OrchestratorScope, type OrphanReaperRegistry, PAIRINGS_VERSION, POSTURE_NATIVE_ALIASES, POSTURE_PREAMBLES, type PairingChannelContext, type PairingChannelHandle, type PairingChannelMode, type PairingRecord, type PairingRegistry, type PairingRegistryDeps, type PendingPermission, type PermissionRespondInput, type PermissionRespondResult, type PolicyRunState, type PolicyRunStatus, Posture, type PostureResolution, type PrResolvedState, type PrStateResolver, type PresetInfo, type PricingResolver, type QuotaReadableProfile, type QuotaStoreFile, type QuotaStoreOptions, type ReconnectLogGate, type RegisterBrainToolsOptions, type RegisterBrowserInput, type RegisterPairingToolsOptions, type RegisterSessionInput, type RegisterWebSearchToolsOptions, type RemainingQuota, type RemainingQuotaReader, type ResolveNativeLinkInput, ResolvedAuthSpec, ResumeDisabledError, type ReuseResolution, type RouteAwareLaunchConfig, type RouteAwareLaunchConfigInput, RouteSpec, type RuntimeMeta, SESSION_ID_ENV, type SandboxAdapterInfo, type SandboxConnectionDescriptor, type SandboxGcCandidate, type SandboxGcProviderHandle, type SandboxGcReapDeps, type SandboxGcReapResult, type SandboxLedgerEntry, type SandboxLedgerState, type SandboxProviderCapabilities, type SandboxProviderHandle, type SandboxProviderLister, type SandboxProviderResolver, type ScopeTokenRegistry, SessionConfig, type SessionCurrentPhase, type SessionDescriptor, type SessionKind, type SessionObserver, type SessionSnapshots, type SessionStatus, type SessionUsage, type SessionWaitEvent, type SessionWaitResult, type SessionsRegistry, type ShellGateSpec, type SpawnAgentInput, type SpawnSessionInput, type StallWatchdogRegistry, type StallWatchdogSummary, type StoredProfileQuota, TASK_STATUSES, type TaskCaller, type TaskChangedEvent, type TaskClaimInput, type TaskCreateInput, type TaskGateOutcome, type TaskGateRunner, type TaskLedger, type TaskLedgerRegistry, type TaskLedgerSessionSlice, type TaskListFilter, type TaskRecord, type TaskStatus, type TaskUpdateInput, type TaskVerification, type TaskVerifySupervisor, type TaskWriteResult, type TokenPricing, type TransmitterBinding, type TransmitterBindingStore, type TunnelDescriptor, type TunnelProvider, type TunnelStatus, type UsageBucket, type UsageComputeInput, type UsageRollup, type UsageSnapshotRecord, type UsageSource, WORKSPACE_SLUG_ENV, WORKTREE_ISOLATION_ENV, type WatcherDescriptor, type WatcherStartInput, type WebSearchInput, type WebSearchOutput, type WebSearchResult, type WorkspaceBrainSubscriber, type WorkspaceBrainSubscriberOptions, type WorkspaceBrains, WorkspaceFs, type WorktreeAutoReclaimer, type WorktreeDecision, type WorktreeField, type WorktreeGcClass, type WorktreeGcOutcomeView, type WorktreeGcPlanEntryView, type WorktreeGcReclaimReason, type WorktreeGcResult, type WorktreeGcRunInput, type WorktreeGcRunner, WorktreeIsolationMode, type WorktreeProvisionOutcome, type WorktreeProvisionRequest, type WorktreeProvisioner, type WorktreeRequest, type WorktreeStatusLister, type WorktreeStatusView, activityCounts, addHarnessPreset, appendConversationRecord, attachSandbox, bucketDir, bucketSessionsFile, bucketTranscriptDir, buildCatalogProviderModels, buildMcpConfigSnippet, buildRouteAwareLaunchConfig, canonicalForModeId, claudeProjectSlug, collectGcCandidates, composeSessionObservers, conversationIndexPath, createActivityProjector, createFileStepCache, createGateway, createInboundEndpointStore, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createPrProvenanceReconciler, createReconnectLogGate, createScopeTokenRegistry, createSupervisorTaskGateRunner, createTaskLedger, createWorkspaceBrainSubscriber, createWorkspaceBrains, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, defineBraveSearchHttpDriver, defineSerperHttpDriver, deriveSessionUsage, enrichRollupWithProviderQuota, enrichWithRemainingQuota, evaluateCostBudget, filterActivities, findConversationRecord, findNativeMode, formatToolCall, formatToolResult, getDefaultHarnessPreset, getHarnessPreset, getMcpCredentialDeps, harnessPresetsPath, isAgentCliAuthConfigured, isClosedTaskStatus, isSafeBucketSlug, isTerminalActivityState, listBuckets, listHarnessPresets, listPresets, loadHarnessPresets, loadQuotaStore, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, makeSandboxCredsStore, makeSandboxResolver, migrateLegacySessionsFile, migrationMarkerPath, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeInbound, normalizeModeId, normalizeWorktreeField, parseAnthropicRateLimitHeaders, parseTaskStatus, parseWindow, parseWorktreeIsolationMode, policyToActivities, policyWatchesSession, prToActivities, projectSessionUsage, readConversationIndex, readDaemonRegistry, readProfileQuota, readRegisteredSlugs, readRuntimeMeta, readSandboxLedger, readSessionForBrain, readUsageSnapshots, reapGcEntry, reapOrphanedDescendants, recordProfileQuota, recordSandboxBoot, recordSandboxLiveness, recordSandboxOrigin, recordSandboxState, registerBrainTools, registerBuiltinRoutes, registerPairingTools, registerSandboxAttachTool, registerWebSearchTools, removeHarnessPreset, removeSandboxLedgerEntry, resolveBucketSlug, resolveNativeLink, resolvePosture, resolveReuseFromLedger, rollupUsage, runCrashDetectPass, runEagerResumePass, runIdleReapPass, runStallWatchdogPass, sandboxLedgerPath, setDefaultPreset, setMcpCredentialDeps, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, toWorktreeStatusView, turnToActivities, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, updateHarnessPreset, upsertSandboxLedger, verifyInboundSignature, webSearchTool, workflowToActivities, writeDaemonRegistryEntry };
12573
+ export { type ActivityKind, type ActivityListFilter, type ActivityPolicyLister, type ActivityProjector, type ActivityProjectorRegistry, type ActivityProjectorSession, type ActivityRecord, type ActivitySource, type ActivityState, type ActivityWaitingOn, type ActivityWorkflowLister, AdapterAuthDescriptor, type AdapterCapabilitiesLister, type AdapterInstallResult, type AdapterListEntry, type AgentAdapterInstaller, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type AnthropicReaderConfig, AnthropicRemainingQuotaReader, type AttachPolicyInput, type AttachSandboxOpts, type AttachSandboxResult, AuthMethod, BUCKETS_ROOT, type BranchGcClass, type BranchGcCoverageView, type BranchGcHoldReason, type BranchGcKind, type BranchGcOutcomeView, type BranchGcPlanEntryView, type BranchGcPlanView, type BranchGcReclaimReason, type BranchGcResult, type BranchGcRunInput, type BranchGcRunner, type BranchGcStatus, type BranchGcSummaryView, type BranchGcTriageVerdict, type BranchGcVerdictInput, type BranchGcVerdictLookupInput, type BranchGcVerdictReader, type BranchGcVerdictRecordView, type BranchGcVerdictRecorder, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, CONTINUE_PROMPT_SOURCE, CanonicalPosture, type CatalogModelsLister, CatalogModelsQuery, CatalogModelsResponse, type CatalogProviderModel, type CatalogProviderModelsQuery, type CatalogProviderModelsResponse, type CatalogProviderPricing, type ClaudeNativeRef, type CommitSpec, type CompletionPolicySupervisor, ContextProfile, type ContinueInterruptedMode, type ContinueInterruptedOutcome, type ContinueInterruptedResult, type ContinueInterruptedSkipReason, type ContinueOnBootSummary, type ConversationIndexRecord, type ConversationNativeRef, type CostBudgetDecision, type CrashDetectSummary, type CrashReaperRegistry, type CreateGatewayOptions, type CreateOfferInput, type CreateReviewRunnerOptions, type CreatedOffer, DEAD_SESSION_STATUSES, DEFAULT_ALWAYS_ON_TOOLS, DEFAULT_BUCKET, DEFAULT_CONTINUE_PROMPT, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, type DaemonReviewerHostDeps, DeclaredAdapterOption, type DeclaredAdapterPreset, DefaultsAdapterAuthConfig, type Device, type DeviceKind, type DeviceRegistry, type DeviceRole, type EagerResumeFailReason, type EagerResumeOutcome, type EagerResumeSkipReason, type EagerResumeSummary, EffortLevel, type EnableInput, type EnableResult, type ForwardHttpRequest, type ForwardHttpResponse, GC_REAPABLE_STATES, type GateSpec, type GatewayHandle, type GhRunner, HOSTS_VERSION, type HarnessPreset, type HarnessPresetValidationDeps, HarnessPresetValidationError, type HarnessPresetsFile, type HermesNativeRef, type HostRecord, type HostRegistry, type HostRegistryDeps, INBOUND_PROVIDERS, type IdleReapSummary, type IdleReaperRegistry, type InboundEndpoint, type InboundEndpointStore, type InboundEndpointStoreOptions, type InboundEnqueuePrompt, type InboundIsSessionAlive, type InboundMessage, type InboundProvider, type InboundRestartSession, type InboundRouteMode, type InboundRouterDeps, type InboundRouterLog, type InboundSpawnForContact, type InboundWatcher, type JudgeGateSpec, LEGACY_SESSIONS_FILE, type LedgerAnnotations, type LedgerEntry, type LedgerHostMeta, type LlmEndpointDescriptor, type LlmEndpointStatus, type LlmEndpointStatusReport, type LocatedConversation, type LocatedConversationByPath, MAX_AUTO_CONTINUE_ATTEMPTS, MAX_MESSAGE_DATA_BYTES, MCP_APP_BRIDGE_SCRIPT, MESSAGE_KINDS, MESSAGE_PREAMBLE, MESSAGE_TAG, MESSAGE_URGENCIES, type McpCredentialDeps, type MeasurableTool, type MeasuredToolEntry, type MessageDeliveryVia, type MessageFilter, type MessageKind, type MessageRelation, type MessageTreeNode, type MessageUrgency, type MigrationMarker, type NormalizeInboundResult, type OnFailSpec, type OpenPrResolver, type OrchestratorGatewayDeps, type OrchestratorInjection, type OrchestratorInjector, type OrchestratorInjectorDeps, type OrchestratorMcpServerFactory, type OrchestratorScope, type OrphanReaperRegistry, PAIRINGS_VERSION, POSTURE_NATIVE_ALIASES, POSTURE_PREAMBLES, type PairingChannelContext, type PairingChannelHandle, type PairingChannelMode, type PairingRecord, type PairingRegistry, type PairingRegistryDeps, type PendingPermission, type PermissionRespondInput, type PermissionRespondResult, type PolicyRunState, type PolicyRunStatus, Posture, type PostureResolution, PrLookupError, type PrLookupErrorCode, type PrResolvedState, type PrStateResolver, type PrStatusSnapshot, type PresetInfo, type PricingResolver, type QuotaReadableProfile, type QuotaStoreFile, type QuotaStoreOptions, type ReconnectLogGate, type RegisterBrainToolsOptions, type RegisterBrowserInput, type RegisterDeviceToolsOptions, type RegisterPairingToolsOptions, type RegisterReviewToolsOptions, type RegisterSessionInput, type RegisterWebSearchToolsOptions, type RemainingQuota, type RemainingQuotaReader, type RemoteStatus, type ResolveNativeLinkInput, ResolvedAuthSpec, ResumeDisabledError, type ReuseResolution, type ReviewLedger, type ReviewLedgerFilter, type ReviewRun, type ReviewRunInput, type ReviewRunStatus, type ReviewRunner, type ReviewerRunResult, type ReviewerSessionHost, type RouteAwareLaunchConfig, type RouteAwareLaunchConfigInput, RouteSpec, type RuntimeMeta, SESSION_END_REASONS, SESSION_ID_ENV, type SandboxAdapterInfo, type SandboxConnectionDescriptor, type SandboxGcCandidate, type SandboxGcProviderHandle, type SandboxGcReapDeps, type SandboxGcReapResult, type SandboxLedgerEntry, type SandboxLedgerState, type SandboxProviderCapabilities, type SandboxProviderHandle, type SandboxProviderLister, type SandboxProviderResolver, type SandboxReconcileDeps, type SandboxReconcileProviderHandle, type SandboxReconcileResult, type SandboxReconcileRow, type SandboxReconcileVerdict, type ScopeTokenRegistry, SessionConfig, type SessionCurrentPhase, type SessionDescriptor, type SessionEndReason, type SessionKind, type SessionMessage, type SessionMessageFrom, type SessionMessageSentRecord, type SessionObserver, type SessionOutcome, type SessionOutcomeArtifact, type SessionOutcomeCompact, type SessionOutcomeLink, type SessionSnapshots, type SessionStatus, type SessionUsage, type SessionUsageWithSubtree, type SessionWaitEvent, type SessionWaitResult, type SessionsRegistry, type ShellGateSpec, type SpawnAgentInput, SpawnDefaultsConfig, type SpawnSessionInput, type StallWatchdogRegistry, type StallWatchdogSummary, type StoredProfileQuota, type SubtreeChildUsage, type SubtreeUsageTotals, TASK_STATUSES, type TaskCaller, type TaskChangedEvent, type TaskClaimInput, type TaskCreateInput, type TaskGateOutcome, type TaskGateRunner, type TaskLedger, type TaskLedgerRegistry, type TaskLedgerSessionSlice, type TaskListFilter, type TaskRecord, type TaskStatus, type TaskUpdateInput, type TaskVerification, type TaskVerifySupervisor, type TaskWriteResult, type TokenPricing, type ToolListMeasurement, type TransmitterBinding, type TransmitterBindingStore, type TunnelDescriptor, type TunnelProvider, type TunnelStatus, type UsageBucket, type UsageComputeInput, type UsageDetail, type UsageReadContext, type UsageReadResult, type UsageRollup, type UsageSnapshotRecord, type UsageSource, UserPreset, WORKSPACE_SLUG_ENV, WORKTREE_ISOLATION_ENV, type WatcherDescriptor, type WatcherStartInput, type WebSearchInput, type WebSearchOutput, type WebSearchResult, type WorkspaceBrainSubscriber, type WorkspaceBrainSubscriberOptions, type WorkspaceBrains, WorkspaceFs, type WorktreeAutoReclaimer, type WorktreeDecision, type WorktreeField, type WorktreeGcClass, type WorktreeGcOutcomeView, type WorktreeGcPlanEntryView, type WorktreeGcReclaimReason, type WorktreeGcResult, type WorktreeGcRunInput, type WorktreeGcRunner, WorktreeIsolationMode, type WorktreeProvisionOutcome, type WorktreeProvisionRequest, type WorktreeProvisioner, type WorktreeRequest, type WorktreeStatusLister, type WorktreeStatusView, activityCounts, addHarnessPreset, appendConversationRecord, attachSandbox, bucketDir, bucketSessionsFile, bucketTranscriptDir, buildCatalogProviderModels, buildMcpConfigSnippet, buildRouteAwareLaunchConfig, canonicalForModeId, claudeProjectSlug, collectGcCandidates, compactOutcome, composeSessionObservers, continueInterruptedSessions, continueSkipReason, conversationIndexPath, createActivityProjector, createDaemonReviewerHost, createDeviceRegistry, createFileStepCache, createGateway, createHostRegistry, createInboundEndpointStore, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createPrProvenanceReconciler, createReconnectLogGate, createReviewLaneExecutor, createReviewLedger, createReviewRunner, createScopeTokenRegistry, createSessionMessage, createSupervisorTaskGateRunner, createTaskLedger, createWorkspaceBrainSubscriber, createWorkspaceBrains, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, defaultReviewLedgerRoot, defineBraveSearchHttpDriver, defineSerperHttpDriver, deriveSessionOutcome, deriveSessionUsage, enrichRollupWithProviderQuota, enrichWithRemainingQuota, escapeHumanPrompt, escapeMessageBody, evaluateCostBudget, execGh, extractHelpSection, fetchPrStatus, filterActivities, findConversationRecord, findNativeMode, findPrForCommit, formatToolCall, formatToolResult, getDefaultHarnessPreset, getHarnessPreset, getMcpCredentialDeps, getToolHelp, githubRepoOf, harnessPresetsPath, hostPlaceholders, injectMcpAppBridge, isAgentCliAuthConfigured, isClosedTaskStatus, isKnownSessionEndReason, isMessageAllowed, isProviderLimitError, isSafeBucketSlug, isTerminalActivityState, listBuckets, listHarnessPresets, listPresets, listToolHelpTopics, loadHarnessPresets, loadQuotaStore, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, makeSandboxCredsStore, makeSandboxResolver, matchesMessageFilter, measureTool, measureToolList, messageFrom, migrateLegacySessionsFile, migrationMarkerPath, mintMessageId, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeInbound, normalizeModeId, normalizeRemote, normalizeWorktreeField, parseAnthropicRateLimitHeaders, parsePrUrl, parseTaskStatus, parseWindow, parseWorktreeIsolationMode, pickUsageDetail, policyToActivities, policyWatchesSession, prHeadSha, prToActivities, projectSessionUsage, readConversationIndex, readDaemonRegistry, readPairingsSnapshot, readProfileQuota, readRegisteredSlugs, readRuntimeMeta, readSandboxLedger, readSessionForBrain, readUsageSnapshots, reapGcEntry, reapOrphanedDescendants, reconcileSandboxLedger, recordProfileQuota, recordSandboxBoot, recordSandboxLiveness, recordSandboxOrigin, recordSandboxState, registerBrainTools, registerBuiltinRoutes, registerDeviceTools, registerPairingTools, registerReviewTools, registerSandboxAttachTool, registerWebSearchTools, removeHarnessPreset, removeSandboxLedgerEntry, renderInboxDigest, renderSessionMessage, renderSessionMessages, repoSlug, resolveBucketSlug, resolveEffectiveLlmEndpointFlag, resolveNativeLink, resolvePosture, resolveRelation, resolveReuseFromLedger, resolveReviewerPreset, rollupSessionSubtree, rollupUsage, runContinueOnBootPass, runCrashDetectPass, runEagerResumePass, runIdleReapPass, runShellLane, runStallWatchdogPass, sandboxLedgerPath, sanitizeLabel, setDefaultPreset, setMcpCredentialDeps, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, toWorktreeStatusView, turnToActivities, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, updateHarnessPreset, upsertSandboxLedger, verifyInboundSignature, webSearchTool, withPr, withPrStatus, workflowToActivities, writeDaemonRegistryEntry };