@agentproto/runtime 4.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -13,8 +13,8 @@ export { CostBudget, CostBudgetScope } from '@agentproto/auth';
13
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 { 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';
16
+ import { S as SpawnDefaultsConfig, D as DefaultsAdapterAuthConfig, R as ResolvedAuthSpec, a as DeclaredAdapterOption, A as AdapterAuthDescriptor } from './spawn-defaults-wQV9AeiG.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-wQV9AeiG.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';
@@ -26,6 +26,7 @@ import * as _agentproto_tool from '@agentproto/tool';
26
26
  import { ToolContext } from '@agentproto/tool';
27
27
  import { DriverHandle } from '@agentproto/driver';
28
28
  import { Attestation, LedgerKey, RubricDigest, ReviewPrRef, LaneResult, ReviewLaneExecutor, LaneOutcome } from '@agentproto/review';
29
+ import { ImportedMcpsConfig } from './mcp-imports.js';
29
30
  import { UserPreset } from './user-presets.js';
30
31
  export { UserPresetsFile, deleteUserPreset, getUserPreset, listUserPresets, loadUserPresets, saveUserPreset, userPresetsPath } from './user-presets.js';
31
32
  import { WorktreeIsolationMode, SpawnAttachMode, SpawnDedupeMode } from './config.js';
@@ -451,13 +452,64 @@ type UsageSource = "adapter" | "computed" | "no-pricing" | "none";
451
452
  interface TokenPricing {
452
453
  inputPer1M: number;
453
454
  outputPer1M: number;
455
+ /** Multiplier on `inputPer1M` for cache-read tokens (Anthropic: ~0.1).
456
+ * Absent = 1 (no discount), same default as the catalog. */
457
+ cacheReadMultiplier?: number;
458
+ /** Multiplier on `inputPer1M` for cache-creation tokens (Anthropic:
459
+ * ~1.25). Absent = 1. */
460
+ cacheWriteMultiplier?: number;
461
+ }
462
+ /**
463
+ * Token-detail and activity fields shared by every usage shape below. All
464
+ * optional and never zero-filled: absent means "this source doesn't report
465
+ * it", a present 0 is a measured zero.
466
+ *
467
+ * - `cacheReadTokens` / `cacheWriteTokens`: prompt-cache reads / cache
468
+ * creation, cumulative. Kept OUT of `tokensIn` (which is uncached input
469
+ * only when a cache split is known).
470
+ * - `reasoningTokens`: thinking/reasoning tokens, cumulative. A subset of
471
+ * `tokensOut` for providers that bill them as output (Anthropic).
472
+ * - `turns`: completed turns (the descriptor's `turnsCompleted`).
473
+ * - `toolCalls`: tool calls the daemon observed, cumulative.
474
+ * - `durationMs`: wall time spent inside turns, cumulative.
475
+ */
476
+ interface UsageDetail {
477
+ cacheReadTokens?: number;
478
+ cacheWriteTokens?: number;
479
+ reasoningTokens?: number;
480
+ turns?: number;
481
+ toolCalls?: number;
482
+ durationMs?: number;
454
483
  }
484
+ /** Copy only the present (`number`) `UsageDetail` fields of `src`. */
485
+ declare function pickUsageDetail(src: UsageDetail): UsageDetail;
455
486
  /** Pluggable pricing accessor — defaults to the in-repo catalog's
456
487
  * `resolvePricing`, overridable in tests. Returns undefined for an
457
488
  * unknown model (the caller then tags `no-pricing`). */
458
489
  type PricingResolver = (model: string) => TokenPricing | undefined;
490
+ /**
491
+ * What an adapter's usage reader (`readUsage`: hermes state.db, opencode.db,
492
+ * the claude-code transcript JSONL) returns. Every field is cumulative for
493
+ * the session and optional; `null` from the reader means "nothing found".
494
+ */
495
+ interface UsageReadResult {
496
+ model?: string;
497
+ costUsd?: number;
498
+ tokensIn?: number;
499
+ tokensOut?: number;
500
+ cacheReadTokens?: number;
501
+ cacheWriteTokens?: number;
502
+ reasoningTokens?: number;
503
+ }
504
+ /** Where a reader should look for the session's native store — the spawn's
505
+ * cwd and isolated provider config dir (`CLAUDE_CONFIG_DIR` for
506
+ * claude-code). Readers that key only on the session id ignore it. */
507
+ interface UsageReadContext {
508
+ cwd?: string;
509
+ configDir?: string;
510
+ }
459
511
  /** Raw signals gathered over a turn, handed to `deriveSessionUsage`. */
460
- interface UsageComputeInput {
512
+ interface UsageComputeInput extends UsageDetail {
461
513
  /** Requested model id — needed to look up per-token prices. */
462
514
  model?: string;
463
515
  /** Cost the adapter reported directly (readUsage or usage_update.cost).
@@ -471,7 +523,7 @@ interface UsageComputeInput {
471
523
  /** The resolved usage snapshot — the shape `session_usage` returns and the
472
524
  * durable `usage_snapshot` transcript record carries. Cost/token fields are
473
525
  * omitted when absent so a missing value never reads as a measured zero. */
474
- interface SessionUsage {
526
+ interface SessionUsage extends UsageDetail {
475
527
  model?: string;
476
528
  costUsd?: number;
477
529
  tokensIn?: number;
@@ -488,7 +540,7 @@ interface SessionUsage {
488
540
  */
489
541
  declare function deriveSessionUsage(input: UsageComputeInput, resolve?: PricingResolver): SessionUsage;
490
542
  /** Descriptor-shaped fields `projectSessionUsage` reads. */
491
- interface UsageDescriptorFields {
543
+ interface UsageDescriptorFields extends Omit<UsageDetail, "turns"> {
492
544
  model?: string;
493
545
  costUsd?: number;
494
546
  tokensIn?: number;
@@ -496,6 +548,8 @@ interface UsageDescriptorFields {
496
548
  contextSize?: number;
497
549
  contextUsed?: number;
498
550
  usageSource?: UsageSource;
551
+ /** Surfaced as `turns`. */
552
+ turnsCompleted?: number;
499
553
  }
500
554
  /**
501
555
  * Project a session descriptor's persisted usage fields into the
@@ -830,7 +884,7 @@ declare function turnToActivities(session: ActivityTurnSession, opts?: {
830
884
  interface ActivityRunStepSlice {
831
885
  index: number;
832
886
  label: string;
833
- status: "pending" | "running" | "done" | "failed" | "skipped";
887
+ status: "pending" | "running" | "done" | "failed" | "skipped" | "cancelled";
834
888
  sessionId?: string;
835
889
  startedAt?: string;
836
890
  endedAt?: string;
@@ -855,8 +909,8 @@ interface ActivityWorkflowRunSlice {
855
909
  * Project one workflow run's steps (stage-barrier semantics): running →
856
910
  * active; a `pending` step waits on the stage barrier — the unfinished
857
911
  * steps of EARLIER stages are the sessions whose turn-ends open it;
858
- * done/failed → terminal; skipped → cancelled; a step still pending inside
859
- * a terminal run settles as cancelled.
912
+ * done/failed → terminal; skipped/cancelled → cancelled; a step still
913
+ * pending inside a terminal run settles as cancelled.
860
914
  */
861
915
  declare function workflowToActivities(run: ActivityWorkflowRunSlice): ActivityRecord[];
862
916
  /**
@@ -1736,7 +1790,7 @@ declare function createTaskLedger(opts: {
1736
1790
  * list — {@link isKnownSessionEndReason} says so, and callers should fall
1737
1791
  * back to a generic label rather than throw.
1738
1792
  */
1739
- 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
+ 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", "steward-completed", "steward-abandoned"];
1740
1794
  type SessionEndReason = (typeof SESSION_END_REASONS)[number];
1741
1795
  /** True when `reason` is one of {@link SESSION_END_REASONS}. A label
1742
1796
  * lookup, not a wire-format gate — an unrecognized string is still a
@@ -1763,7 +1817,7 @@ declare function isProviderLimitError(message: string | undefined): boolean;
1763
1817
  * and session_monitor MCP tool (long-poll multiplexed).
1764
1818
  */
1765
1819
 
1766
- 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";
1820
+ 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:artifact-added" | "session:artifact-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";
1767
1821
  /**
1768
1822
  * Fixed severity vocabulary for a judge-gate finding (WP-D). Deliberately
1769
1823
  * small and fixed so `policy_status` output is comparable across different
@@ -1837,6 +1891,18 @@ interface SessionTurnEndEvent {
1837
1891
  * a normal, productive turn.
1838
1892
  */
1839
1893
  empty?: boolean;
1894
+ /**
1895
+ * The in-band `error` stream event's message text observed during this
1896
+ * turn, when one occurred (see `SessionDescriptor.lastTurnErrorMessage`'s
1897
+ * doc). Present whenever `reason` is `"error"` AND the adapter emitted an
1898
+ * `error`-kind event before ending the stream — including the case where
1899
+ * the generator simply returned with no explicit turn-end and no thrown
1900
+ * exception, which is exactly what left an errored turn indistinguishable
1901
+ * from a clean one before this field existed. Absent when the adapter
1902
+ * reported `reason:"error"` on its own turn-end without a preceding
1903
+ * `error` event, and absent (not empty string) on every non-error turn.
1904
+ */
1905
+ error?: string;
1840
1906
  /**
1841
1907
  * True when the turn was not started by a prompt: the agent woke on its
1842
1908
  * own (Claude Code's task-notification cycle after a background task
@@ -2299,6 +2365,32 @@ interface SessionPinnedEvent {
2299
2365
  pinned: boolean;
2300
2366
  ts: string;
2301
2367
  }
2368
+ /**
2369
+ * Emitted when a new artifact (or a new version of an existing one) is
2370
+ * materialized into the session's artifact store (`session_artifact_add`,
2371
+ * `POST /sessions/:id/artifacts` — see `session-artifacts.ts`). Carries just
2372
+ * enough for a live UI to append/update its inline card and "Artifacts"
2373
+ * section without a round trip — the full record (all versions) is a
2374
+ * `session_artifact_list` / `GET /sessions/:id/artifacts` call away.
2375
+ */
2376
+ interface SessionArtifactAddedEvent {
2377
+ type: "session:artifact-added";
2378
+ sessionId: string;
2379
+ key: string;
2380
+ kind: string;
2381
+ version: number;
2382
+ label?: string;
2383
+ ts: string;
2384
+ }
2385
+ /** Emitted when an artifact's `pinned` flag changes
2386
+ * (`session_artifact_pin`, `POST /sessions/:id/artifacts/:key/pin`). */
2387
+ interface SessionArtifactPinnedEvent {
2388
+ type: "session:artifact-pinned-changed";
2389
+ sessionId: string;
2390
+ key: string;
2391
+ pinned: boolean;
2392
+ ts: string;
2393
+ }
2302
2394
  /**
2303
2395
  * Emitted when a session is first registered in the registry (WP-R3) — both
2304
2396
  * the agent-cli spawn path (`spawnAgent`) and the terminal path (`spawnPty`).
@@ -2517,7 +2609,7 @@ interface SessionHarnessWarningEvent {
2517
2609
  label?: string;
2518
2610
  ts: string;
2519
2611
  }
2520
- 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;
2612
+ type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionAwaitingInputFlaggedEvent | SessionAwaitingQuestionAnsweredEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionReapedEvent | SessionStalledEvent | SessionStallClearedEvent | SessionWatcherAttachedEvent | SessionWatcherDetachedEvent | SessionBgTasksParkedEvent | SessionBgTasksClearedEvent | SessionBgTaskEvent | SessionResumedEvent | SessionSpawnedEvent | SessionCommandDoneEvent | SessionModelChangedEvent | SessionConfigChangedEvent | SessionRenamedEvent | SessionPinnedEvent | SessionArtifactAddedEvent | SessionArtifactPinnedEvent | SessionMessageEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent | ActivityChangedEvent | TaskChangedEvent | WorkflowGateReportEvent | WorkflowApprovalRequestedEvent | WorkflowApprovalResolvedEvent | WorkflowSuspendedEvent | WorkflowSuspendResumedEvent | SessionHarnessWarningEvent;
2521
2613
  interface SessionEventBus {
2522
2614
  emit(ev: SessionEvent): void;
2523
2615
  /** Subscribe to a specific event type. Returns an unsubscribe fn. */
@@ -2763,6 +2855,318 @@ declare class TunnelRegistry {
2763
2855
  private loadFromDisk;
2764
2856
  }
2765
2857
 
2858
+ /**
2859
+ * Typed inter-session message envelope (AIP-46 §Session messages).
2860
+ *
2861
+ * A PROMPT is an instruction from whoever drives a session; a MESSAGE is a
2862
+ * report between sessions in the same tree whose sender the DAEMON attests.
2863
+ * Everything in `from` is computed here from the caller's verified identity
2864
+ * and the session forest — a send request can never set it.
2865
+ *
2866
+ * Pure module: no registry, no I/O. The registry (sessions.ts) carries a
2867
+ * `SessionMessage` on a queued prompt (`QueuedPrompt.envelope`), renders it
2868
+ * into the model's context with `renderSessionMessages`, and records it as
2869
+ * its own `session-message` transcript record — never a `user-prompt`.
2870
+ */
2871
+ /** Sender's position in the tree, relative to the recipient. */
2872
+ type MessageRelation = "child" | "parent" | "sibling" | "human" | "system";
2873
+ type MessageKind = "report" | "question" | "blocker" | "done" | "notice";
2874
+ type MessageUrgency = "fyi" | "next-turn" | "steer" | "interrupt";
2875
+ type MessageDeliveryVia = "wait" | "steer" | "turn" | "interrupt" | "inbox";
2876
+ declare const MESSAGE_KINDS: readonly MessageKind[];
2877
+ declare const MESSAGE_URGENCIES: readonly MessageUrgency[];
2878
+ interface SessionMessageFrom {
2879
+ /** Absent only for relation `human` | `system`. */
2880
+ sessionId?: string;
2881
+ /** Registry label at send time — display only, never identity. */
2882
+ label?: string;
2883
+ role?: string;
2884
+ adapter?: string;
2885
+ relation: MessageRelation;
2886
+ }
2887
+ interface SessionMessage {
2888
+ /** `msg_<uuid8>`, daemon-minted. */
2889
+ id: string;
2890
+ ts: string;
2891
+ /** Recipient sessionId. */
2892
+ to: string;
2893
+ from: SessionMessageFrom;
2894
+ kind: MessageKind;
2895
+ /** Requested urgency. */
2896
+ urgency: MessageUrgency;
2897
+ /** Stamped on delivery. */
2898
+ delivered?: {
2899
+ via: MessageDeliveryVia;
2900
+ at: string;
2901
+ turnSeq?: number;
2902
+ };
2903
+ /** Thread id; defaults to the first message's id. */
2904
+ correlationId?: string;
2905
+ /** A `SessionMessage.id` this answers. */
2906
+ replyTo?: string;
2907
+ text: string;
2908
+ /** Optional structured payload (≤ `MAX_MESSAGE_DATA_BYTES`). */
2909
+ data?: Record<string, unknown>;
2910
+ ackedAt?: string;
2911
+ }
2912
+ declare const MAX_MESSAGE_DATA_BYTES: number;
2913
+ /** The slice of a session descriptor relation/ACL resolution needs. */
2914
+ interface MessageTreeNode {
2915
+ id: string;
2916
+ parentSessionId?: string;
2917
+ label?: string;
2918
+ role?: string;
2919
+ adapterSlug?: string;
2920
+ }
2921
+ /**
2922
+ * Relation of `sender` to `recipient` in the session forest, or `undefined`
2923
+ * when they're not tree neighbours (cousins, strangers, self). `sender`
2924
+ * absent ⇒ `human` (an HTTP/CLI operator); daemon-originated notices pass
2925
+ * `"system"` explicitly instead of calling this.
2926
+ */
2927
+ declare function resolveRelation(sender: MessageTreeNode | undefined, recipient: MessageTreeNode): Exclude<MessageRelation, "system"> | undefined;
2928
+ /**
2929
+ * ACL: child→parent, parent→direct child, human/system→any. Sibling↔sibling
2930
+ * only when `allowSiblings` (config-gated, default off). Anything else — a
2931
+ * cousin, a stranger, yourself — is refused.
2932
+ */
2933
+ declare function isMessageAllowed(relation: MessageRelation | undefined, opts?: {
2934
+ allowSiblings?: boolean;
2935
+ }): relation is MessageRelation;
2936
+ /** Build a `from` block from the verified sender + the resolved relation. */
2937
+ declare function messageFrom(sender: MessageTreeNode | undefined, relation: MessageRelation): SessionMessageFrom;
2938
+ declare function mintMessageId(): string;
2939
+ declare function createSessionMessage(input: {
2940
+ to: string;
2941
+ from: SessionMessageFrom;
2942
+ text: string;
2943
+ kind?: MessageKind;
2944
+ urgency?: MessageUrgency;
2945
+ replyTo?: string;
2946
+ correlationId?: string;
2947
+ data?: Record<string, unknown>;
2948
+ }): SessionMessage;
2949
+ interface MessageFilter {
2950
+ /** Sender session ids, or `"children"` for any message whose relation is
2951
+ * `child` (i.e. from one of the recipient's own children). */
2952
+ from?: readonly string[] | "children";
2953
+ kind?: readonly MessageKind[];
2954
+ correlationId?: string;
2955
+ }
2956
+ declare function matchesMessageFilter(msg: SessionMessage, filter: MessageFilter): boolean;
2957
+ declare const MESSAGE_TAG = "agentproto-message";
2958
+ /** Child-settable label → attribute-safe: `[A-Za-z0-9._-]`, ≤ 48 chars. */
2959
+ declare function sanitizeLabel(label: string | undefined): string | undefined;
2960
+ /** Escape every envelope/body sentinel inside a message body, so a sender
2961
+ * can't close the tag early and forge a second header. */
2962
+ declare function escapeMessageBody(text: string): string;
2963
+ /** A human prompt is NOT wrapped (humans keep today's UX), but a line that
2964
+ * starts with the envelope sentinel is escaped, so the invariant "text
2965
+ * outside an `<agentproto-message>` tag is the human" holds. Nothing else
2966
+ * in human text changes. */
2967
+ declare function escapeHumanPrompt(text: string): string;
2968
+ declare function renderSessionMessage(msg: SessionMessage): string;
2969
+ /** A coalesced batch → sibling tags, one per message, in order. */
2970
+ declare function renderSessionMessages(msgs: readonly SessionMessage[]): string;
2971
+ /** One-line typed digest of `fyi` messages that never woke the session —
2972
+ * prepended (as a `system-prompt` slice) to the next turn it runs anyway. */
2973
+ declare function renderInboxDigest(msgs: readonly SessionMessage[]): string;
2974
+ /** Taught once, the first time a session RECEIVES a message (recorded as a
2975
+ * `system-prompt`). Children also get the same invariant in their lineage
2976
+ * line at spawn. */
2977
+ declare const MESSAGE_PREAMBLE: string;
2978
+
2979
+ /**
2980
+ * Shared types for pluggable sentinel providers (AIP-60 §3, §4).
2981
+ *
2982
+ * The sentinel family is a `@agentproto/provider-kit` consumer, same shape as
2983
+ * the tunnel family (`remote-providers/types.ts`) and the sandbox family
2984
+ * (`sandbox-providers/types.ts`): {@link SentinelProviderHandle} extends the
2985
+ * kit's generic `AdapterHandle` (slug/name/version/description/requiresSetup/
2986
+ * check) with the sentinel-specific `capabilities` plus the create/attach/
2987
+ * cancel/status/poll/ack/parseInbound lifecycle a provider implements.
2988
+ *
2989
+ * This file is deliberately self-contained (no import from `sentinel-store.js`)
2990
+ * so the contract never depends on the store's internals — `SentinelSpec` is
2991
+ * the vocabulary a provider is handed, `sentinel-store.ts` imports it from
2992
+ * here rather than the other way around.
2993
+ */
2994
+
2995
+ /** When a sentinel stops watching. */
2996
+ type SentinelUntil = {
2997
+ kind: "subject_terminal";
2998
+ } | {
2999
+ kind: "at";
3000
+ ms: number;
3001
+ } | {
3002
+ kind: "count";
3003
+ n: number;
3004
+ } | {
3005
+ kind: "never";
3006
+ };
3007
+ /** Where a matching event is delivered.
3008
+ *
3009
+ * `routine` is reserved for AIP-41 `schedule.kind: event` binding; `webhook`
3010
+ * is reserved for forwarding a matched event to an external HTTP callback.
3011
+ * Both are FROZEN SHAPES ONLY — `SentinelStore.create` rejects them with
3012
+ * {@link SentinelTargetNotImplementedError} until a later step wires actual
3013
+ * delivery (design §6/§12 step 4 for `routine`; `webhook` has no design
3014
+ * section yet). Declaring the shape now lets callers/tools reference the
3015
+ * full union before delivery exists. */
3016
+ type SentinelTarget = {
3017
+ kind: "session";
3018
+ sessionId: string;
3019
+ urgency: MessageUrgency;
3020
+ } | {
3021
+ kind: "routine";
3022
+ routineId: string;
3023
+ } | {
3024
+ kind: "webhook";
3025
+ url: string;
3026
+ secret: string;
3027
+ };
3028
+ /** One clause of a sentinel's watch: a subject (exact, or a `*`-suffixed
3029
+ * prefix match against an event's `subjects`) plus an optional per-clause
3030
+ * type glob list. */
3031
+ interface SentinelMatchClause {
3032
+ /** Hierarchical routing key, e.g. "github:agentproto/ts#1428". A trailing
3033
+ * `*` is a prefix match against an event's `subjects`. */
3034
+ subject: string;
3035
+ /** Type globs; undefined = the provider's `defaultTypes(subject)`. */
3036
+ types?: string[];
3037
+ }
3038
+ /**
3039
+ * "Watch any of `match` until `condition`, deliver to `target`."
3040
+ *
3041
+ * `match` is OR semantics: an event matches the sentinel if it matches ANY
3042
+ * clause (subject + that clause's own type glob / provider default). At
3043
+ * least one clause is required — use {@link singleMatch} for a one-clause
3044
+ * spec. The event's own `subject` (not the matching clause's, which may be a
3045
+ * `*` prefix template) is what lands as `correlationId: sentinel:<subject>`
3046
+ * and the `[<scheme>]` inbox-text prefix — see `sentinel-runtime.ts`.
3047
+ *
3048
+ * Dedup key is `(sentinelId, event.id)`, same for every clause — see
3049
+ * {@link SentinelEvent.id}.
3050
+ */
3051
+ interface SentinelSpec {
3052
+ match: SentinelMatchClause[];
3053
+ until: SentinelUntil;
3054
+ target: SentinelTarget;
3055
+ /** Provider slug; undefined = auto-select (not implemented by the runtime
3056
+ * yet — step 2 requires an explicit slug). */
3057
+ provider?: string;
3058
+ /** Fan-out grouping — sentinels created together (e.g. several
3059
+ * PR-related watches for one review) share a `group` id so they can be
3060
+ * listed/removed together later. Purely descriptive: the runtime never
3061
+ * reads it, no MCP tool consumes it yet. */
3062
+ group?: string;
3063
+ /** Human-readable label surfaced by `sentinel_list` / the CLI. Purely
3064
+ * descriptive, same as `group`. */
3065
+ label?: string;
3066
+ }
3067
+
3068
+ /**
3069
+ * SentinelStore — persists `~/.agentproto/sentinels.json` (AIP-60 §2).
3070
+ *
3071
+ * State is in-memory; sentinels persist to disk (debounced async write +
3072
+ * sync flush at shutdown), mirroring `inbound-endpoints.ts`'s
3073
+ * `InboundEndpointStore`. Unlike that store's `markSeen` (deliberately
3074
+ * in-memory only), dedup here is PERSISTED per sentinel, bounded to the last
3075
+ * 1000 event ids (design §2/§9) — a sentinel's dedup window must survive a
3076
+ * daemon restart, since the whole point of a sentinel is to keep watching
3077
+ * across restarts.
3078
+ *
3079
+ * Writes are atomic (write to a per-process tmp file, then rename), same
3080
+ * idiom as `sandbox-ledger.ts` — a crash mid-write must never leave a
3081
+ * corrupt `sentinels.json` behind.
3082
+ */
3083
+
3084
+ type SentinelStatus = "active" | "paused" | "expired" | "orphaned" | "error";
3085
+
3086
+ /**
3087
+ * LlmEndpointRegistry — supervised-sidecar manager for the
3088
+ * `@agentproto/llm-endpoint` proxy gateway.
3089
+ *
3090
+ * The daemon spawns the llm-endpoint bin as a CHILD PROCESS (never an
3091
+ * in-process import — the runtime package has zero dependency on
3092
+ * @agentproto/llm-endpoint, exactly like it never imports cloudflared).
3093
+ * This is the single-sidecar analog of {@link TunnelRegistry}: one endpoint,
3094
+ * one port, an in-memory descriptor that tracks pid / port / status /
3095
+ * startedAt, and a start / stop / status lifecycle exposed over MCP.
3096
+ *
3097
+ * Lifecycle (mirrors RemoteController's singleton style — created fresh each
3098
+ * boot, state in memory; the child dies with the daemon so there is nothing
3099
+ * to reconcile on restart):
3100
+ * start(input) → assemble env, spawn `node <bin> serve`, health-probe to
3101
+ * readiness, store descriptor. Idempotent: a call while the
3102
+ * endpoint is already running+healthy returns the existing
3103
+ * descriptor without spawning (like `start_browser`).
3104
+ * status() → descriptor + live `GET /v1/models` health probe.
3105
+ * stop() → SIGTERM the child, mark stopped (mirrors tunnel stop).
3106
+ * shutdown() → stop the child on daemon exit.
3107
+ *
3108
+ * Env on spawn (see {@link assembleLlmEndpointEnv}): the provider API keys
3109
+ * via `injectProviderKeysIntoEnv` (the same store llm-endpoint's own cli.ts
3110
+ * reads), `LLM_ENDPOINT_PORT` (default 18090), and `LLM_ENDPOINT_ACCESS_TOKENS`
3111
+ * when configured. Explicit env passed to `start` always wins.
3112
+ */
3113
+ type LlmEndpointStatus = "starting" | "running" | "stopped" | "error";
3114
+ /** The `llm_endpoint_status` MCP contract — see {@link LlmEndpointRegistry.status}. */
3115
+ interface LlmEndpointStatusReport {
3116
+ running: boolean;
3117
+ pid: number | null;
3118
+ port: number | null;
3119
+ baseUrl: string | null;
3120
+ healthy: boolean;
3121
+ startedAt: string | null;
3122
+ status: LlmEndpointStatus | "never-started";
3123
+ /** Who is actually serving this endpoint. `"daemon"` — this registry's own
3124
+ * spawn (or nothing at all). `"external"` — a healthy process on the same
3125
+ * port that this registry did NOT spawn (e.g. a launchd KeepAlive
3126
+ * sidecar); `pid` is unknown (always `null`) and `injectedProviders`, if
3127
+ * present, was DERIVED by probing `/v1/models` rather than read off our
3128
+ * own key-injection bookkeeping. */
3129
+ owner: "daemon" | "external";
3130
+ /** Whether upstream links persisted via `llm_endpoint_set_upstream_link`
3131
+ * are actually applied to the process answering this port. Always
3132
+ * `false` for `owner:"external"` — this registry never spawned it, so it
3133
+ * never injected `LLM_ENDPOINT_PROFILE_<UPSTREAM>`; the operator must
3134
+ * configure the external service's own env directly. */
3135
+ linksApplied: boolean;
3136
+ lastError?: string;
3137
+ injectedProviders?: string[];
3138
+ linkedProviders?: string[];
3139
+ /** `@agentproto/llm-endpoint`'s own package version, read from the
3140
+ * resolved bin's `package.json` — best-effort, present only for
3141
+ * `owner:"daemon"` (the version of the code this registry would spawn /
3142
+ * did spawn). Undefined when unresolvable, or for `owner:"external"`
3143
+ * (an adopted process exposes no version of its own to probe). */
3144
+ version?: string;
3145
+ }
3146
+ interface LlmEndpointDescriptor {
3147
+ pid: number | null;
3148
+ port: number;
3149
+ baseUrl: string;
3150
+ status: LlmEndpointStatus;
3151
+ startedAt: string;
3152
+ stoppedAt?: string;
3153
+ lastError?: string;
3154
+ /** Provider names whose keys were injected into the child's env (never values). */
3155
+ injectedProviders?: string[];
3156
+ /** Upstream names mapped to an auth-profile link (`LLM_ENDPOINT_PROFILE_<P>`)
3157
+ * in the child's env. Profile IDS, not secrets — but only the provider names
3158
+ * are surfaced here, mirroring `injectedProviders`. */
3159
+ linkedProviders?: string[];
3160
+ /**
3161
+ * Only populated on the value returned by {@link LlmEndpointRegistry.start}
3162
+ * (never stored on the in-memory descriptor): `true` when the call reused an
3163
+ * already-running/starting child instead of spawning — so the caller can
3164
+ * tell a fresh `start` (new port/tokens applied) from a no-op idempotent hit
3165
+ * (old descriptor returned, nothing new applied). Mirrors `start_browser`.
3166
+ */
3167
+ wasAlreadyRunning?: boolean;
3168
+ }
3169
+
2766
3170
  /**
2767
3171
  * RemoteController — owns the gateway's "publish to the internet" state.
2768
3172
  *
@@ -2970,6 +3374,14 @@ interface PairingRecord {
2970
3374
  /** Set on a pairing made under the retired pair/v1 protocol: listed and
2971
3375
  * revocable, never served — its client is told to re-pair. */
2972
3376
  legacy?: true;
3377
+ /** Set when this pairing was made under an offer minted with
3378
+ * `agentproto pair offer --host` (PR-C) — i.e. the offering daemon's own
3379
+ * intent, recorded server-side at handshake time from the OfferEntry it
3380
+ * itself created, never from the (unauthenticated) `scope` param a client
3381
+ * presented in the offer URL. Absent for an ordinary remote-control
3382
+ * pairing. Purely informational here — it does not change how this
3383
+ * pairing is served. */
3384
+ scope?: "host";
2973
3385
  }
2974
3386
  /** Mode of a served channel — first-contact offer vs. an established reconnect. */
2975
3387
  type PairingChannelMode = "offer" | "reconnect";
@@ -2981,6 +3393,13 @@ interface PairingChannelContext {
2981
3393
  fingerprint: string;
2982
3394
  /** Client label. */
2983
3395
  name: string;
3396
+ /** This pairing's OWN recorded scope (mirrors `PairingRecord.scope`) —
3397
+ * `"host"` when the offer that produced it was minted with
3398
+ * `agentproto pair offer --host`, absent for an ordinary remote-control
3399
+ * pairing. Present on BOTH the offer and every reconnect context, so
3400
+ * `serve` can gate scope-restricted routes (e.g. device-inference
3401
+ * proxying) the same way on first contact and on every later reconnect. */
3402
+ scope?: "host";
2984
3403
  }
2985
3404
  /** Handle to a live served channel — the registry closes it on teardown. */
2986
3405
  interface PairingChannelHandle {
@@ -3003,12 +3422,21 @@ interface CreatedOffer {
3003
3422
  * falls back to `HOSTED_RENDEZVOUS_URL`. Surfaced so `pair offer` can flag,
3004
3423
  * never silently, that the daemon is relaying through our infrastructure. */
3005
3424
  rendezvousIsHostedDefault: boolean;
3425
+ /** Echoes `CreateOfferInput.scope`. `"host"` when minted with `--host`;
3426
+ * absent for a plain offer. */
3427
+ scope?: "host";
3006
3428
  }
3007
3429
  interface CreateOfferInput {
3008
3430
  /** Time-to-live in ms. Default 10 minutes. */
3009
3431
  ttlMs?: number;
3010
3432
  /** Rendezvous URL override; falls back to the configured default. */
3011
3433
  rendezvousUrl?: string;
3434
+ /** Mint a HOST-scoped offer (PR-C): the accepting side may register this
3435
+ * daemon as a driveable host (`devices add`), not just remote-control it.
3436
+ * Recorded on the resulting `OfferEntry`/`PairingRecord` server-side — this
3437
+ * is the authoritative record of what was granted, independent of the
3438
+ * `scope` param an accepting client's offer URL happens to carry. */
3439
+ scope?: "host";
3012
3440
  }
3013
3441
  interface PairingRegistryDeps {
3014
3442
  /** Lazily load (create on first use) the daemon identity. Kept lazy so a
@@ -3052,17 +3480,362 @@ interface PairingRegistry {
3052
3480
  createOffer(input?: CreateOfferInput): Promise<CreatedOffer>;
3053
3481
  /** All persisted pairings (copies). Loads `pairings.json` on first call. */
3054
3482
  list(): Promise<PairingRecord[]>;
3483
+ /** Rename a pairing by fingerprint or name (cosmetic only — no effect on
3484
+ * auth or routing). Returns false when nothing matched; throws on an
3485
+ * empty `newName`. */
3486
+ rename(idOrName: string, newName: string): Promise<boolean>;
3055
3487
  /** Drop a pairing by fingerprint or name; stops its rendezvous connections.
3056
3488
  * Returns false when nothing matched. */
3057
3489
  revoke(idOrName: string): Promise<boolean>;
3490
+ /** Is a channel (offer or reconnect) currently served for this fingerprint? */
3491
+ isOnline(fingerprint: string): boolean;
3058
3492
  /** Start standing reconnect connections for every persisted pairing. Call
3059
3493
  * after the gateway is up (so the injected `serve` can reach it). */
3060
3494
  startAutoconnect(): Promise<void>;
3061
3495
  /** Tear down every offer + reconnect connection and served channel. */
3062
3496
  shutdown(): Promise<void>;
3063
3497
  }
3498
+ /**
3499
+ * Read-only snapshot of `pairings.json` for callers that just want the
3500
+ * persisted records — no `dial`/`serve`/`loadIdentity` deps, no rendezvous
3501
+ * connections started (e.g. `agentproto doctor`'s devices check). Mirrors
3502
+ * `ensureLoaded`'s parse + v1-legacy flagging; never throws — a missing or
3503
+ * malformed file just yields `[]`.
3504
+ */
3505
+ declare function readPairingsSnapshot(path?: string): Promise<PairingRecord[]>;
3064
3506
  declare function createPairingRegistry(deps: PairingRegistryDeps): PairingRegistry;
3065
3507
 
3508
+ /**
3509
+ * Daemon-side HOST registry (DEVICES-PLAN PR-C) — the "client" half of
3510
+ * pair/v2, living on the daemon so it's reachable over both an HTTP route
3511
+ * and an MCP tool, not just CLI-local bookkeeping (`client-pairings.ts` /
3512
+ * `pair-transport.ts` are that CLI-local equivalent today).
3513
+ *
3514
+ * Where `pairing-registry.ts` is what a daemon uses to let OTHER machines
3515
+ * pair with and remote-control IT, `host-registry.ts` is what a daemon uses
3516
+ * to register and drive OTHER daemons — reverse pairing. `add()` accepts a
3517
+ * HOST-scoped offer (`agentproto pair offer --host`, offer-url.ts's
3518
+ * `scope`), runs the pair/v2 client handshake, and persists the resulting
3519
+ * pair root; `forwardHttp()` dials on demand, reconnecting over the current
3520
+ * (then previous) epoch token exactly like `pair-transport.ts`'s
3521
+ * `openPairChannel`, forwards one HTTP request over the resulting tunnel
3522
+ * client, and closes. There is no standing connection here — unlike
3523
+ * `PairingRegistry`, a `HostRegistry` never autoconnects or keeps a
3524
+ * reconnect loop parked at the broker; every reach-out is a fresh dial.
3525
+ * That also means shutdown needs no teardown: there is nothing standing to
3526
+ * tear down.
3527
+ *
3528
+ * ## The scope gate
3529
+ *
3530
+ * `add()` REFUSES (throws) any offer whose parsed `scope !== "host"` before
3531
+ * ever dialing anything. This is the default-deny gate PR-C exists to add:
3532
+ * an ordinary "remote-control me" offer (today's `pair offer`, no `--host`)
3533
+ * must not silently become a host registration, even though the underlying
3534
+ * wire capability is identical either way (see offer-url.ts's "Offer scope"
3535
+ * and pairing-registry.ts's `OfferEntry.scope` doc comment for why the
3536
+ * offering daemon's OWN server-side record — not the URL's plaintext
3537
+ * `scope` param — is what's authoritative about what it granted). There is
3538
+ * deliberately no override flag.
3539
+ *
3540
+ * Persisted to `hosts.json` (`~/.agentproto/hosts.json` by default) with the
3541
+ * same atomic-write discipline as `pairings.json` (mkdir, write to a
3542
+ * pid-keyed tmp file at 0600, chmod, rename) and the same lazy-load-on-
3543
+ * first-use pattern.
3544
+ *
3545
+ * ## Online tracking
3546
+ *
3547
+ * `isOnline(fingerprint)` mirrors `PairingRegistry.isOnline`: it's true only
3548
+ * while a `forwardHttp()` call for that fingerprint is in flight (a
3549
+ * reference count, incremented on dial and decremented in a `finally`).
3550
+ * There is no live probe backing `list()` — that would make `devices list`
3551
+ * slow/flaky over N hosts — so `list()`'s `online` is only ever true
3552
+ * immediately after (or during) an actual `forwardHttp`/`devices status`
3553
+ * call, never a background heartbeat. That's enough to make `devices list`
3554
+ * show the right thing right after `devices status`/`exec`, which is what
3555
+ * matters for this PR; a standing/autoconnect host connection with a real
3556
+ * liveness signal is PR-D's to build.
3557
+ */
3558
+
3559
+ /** `hosts.json` format. */
3560
+ declare const HOSTS_VERSION: 1;
3561
+ /** One persisted host — a daemon this daemon can drive. */
3562
+ interface HostRecord {
3563
+ /** The target daemon's identity fingerprint (32 hex). */
3564
+ fingerprint: string;
3565
+ /** Human-facing label; defaults to the fingerprint. */
3566
+ name: string;
3567
+ /** Target daemon's static X25519 public key, standard base64 SPKI DER —
3568
+ * pinned at `add()` time. */
3569
+ daemonX25519Pub: string;
3570
+ /** Target daemon's static Ed25519 public key, standard base64 SPKI DER —
3571
+ * pinned at `add()` time. */
3572
+ daemonEd25519Pub: string;
3573
+ /** Rendezvous endpoint to reach the host through. */
3574
+ rendezvousUrl: string;
3575
+ /** Long-term shared secret (base64), from which epoch routing tokens
3576
+ * derive — same derivation as `PairingRecord.pairRoot`. */
3577
+ pairRoot: string;
3578
+ /** ISO-8601 first-add timestamp. */
3579
+ createdAt: string;
3580
+ /** ISO-8601 of the most recent successful `forwardHttp`. */
3581
+ lastSeen: string;
3582
+ /** Set on a host added under the retired pair/v1 protocol. Present for
3583
+ * parity with `PairingRecord`/`ClientPairing`; pair/v1 offers are refused
3584
+ * by `parseOfferUrl` before `add()` ever sees them, so this is currently
3585
+ * unreachable — kept so a future downgrade path has somewhere to flag it. */
3586
+ legacy?: true;
3587
+ /** Self-reported by the host at add-time (SANDBOX-VISIBILITY-JOIN) — e.g. a
3588
+ * sandbox provider slug, a provider-side sandbox id, and free-form labels
3589
+ * (PR number, run URL, …). Never verified beyond "the host said so"; purely
3590
+ * descriptive for `device_list`/`devices sessions`. */
3591
+ provider?: string;
3592
+ sandboxId?: string;
3593
+ labels?: Record<string, string>;
3594
+ }
3595
+ /** Optional self-reported metadata `add()` attaches to the resulting
3596
+ * `HostRecord` (see `HostRecord.provider`/`sandboxId`/`labels`). */
3597
+ interface HostJoinMeta {
3598
+ provider?: string;
3599
+ sandboxId?: string;
3600
+ labels?: Record<string, string>;
3601
+ }
3602
+ interface HostRegistryDeps {
3603
+ /** Dial a rendezvous WS and adapt it to a `FrameSink`. Same shape as
3604
+ * `PairingRegistryDeps.dial` — the CLI's `daemonDialRendezvous` satisfies
3605
+ * it directly. Rejects if the dial fails or `signal` aborts. */
3606
+ dial: (wsUrl: string, signal: AbortSignal) => Promise<FrameSink>;
3607
+ /** Path to `hosts.json`. Defaults to `~/.agentproto/hosts.json`. */
3608
+ hostsPath?: string;
3609
+ /** Injectable clock (ms). Defaults to Date.now. */
3610
+ now?: () => number;
3611
+ /** Diagnostic log sink. */
3612
+ log?: (line: string) => void;
3613
+ /** Rendezvous dial ceiling per attempt. Default 15s. */
3614
+ dialTimeoutMs?: number;
3615
+ /** Handshake ceiling per attempt. Default 15s. */
3616
+ handshakeTimeoutMs?: number;
3617
+ }
3618
+ interface ForwardHttpRequest {
3619
+ method: string;
3620
+ path: string;
3621
+ headers?: Record<string, string>;
3622
+ body?: Uint8Array;
3623
+ }
3624
+ interface ForwardHttpResponse {
3625
+ status: number;
3626
+ headers: Record<string, string>;
3627
+ body: Uint8Array;
3628
+ }
3629
+ /** Streaming counterpart of {@link ForwardHttpResponse} — `body` is a Web
3630
+ * `ReadableStream` the caller drains directly (SSE chat/completions, most
3631
+ * notably) instead of a fully-buffered `Uint8Array`. See
3632
+ * {@link HostRegistry.forwardHttpStream}. */
3633
+ interface ForwardHttpStreamResponse {
3634
+ status: number;
3635
+ headers: Record<string, string>;
3636
+ body: ReadableStream<Uint8Array>;
3637
+ }
3638
+ interface HostRegistry {
3639
+ /**
3640
+ * Accept an offer URL and register the target daemon as a driveable host.
3641
+ * REFUSES (throws a plain `Error`) unless the parsed offer has
3642
+ * `scope === "host"` — no dial is attempted in that case. Otherwise: dials
3643
+ * the offer's rendezvous, runs the pair/v2 client handshake, verifies the
3644
+ * derived peer fingerprint matches the offer's `id` (defence in depth,
3645
+ * mirrors `pair-transport.ts`'s `acceptOffer`), derives the pair root, and
3646
+ * persists a `HostRecord` (upserted by fingerprint — re-adding replaces).
3647
+ */
3648
+ add(offerUrl: string, name?: string, meta?: HostJoinMeta): Promise<{
3649
+ fingerprint: string;
3650
+ name: string;
3651
+ rendezvousUrl: string;
3652
+ }>;
3653
+ /** All persisted hosts (copies). Loads `hosts.json` on first call. */
3654
+ list(): Promise<HostRecord[]>;
3655
+ /** Rename a host (fingerprint or current name). Returns false when nothing
3656
+ * matched; throws on an empty `newName`. */
3657
+ rename(idOrName: string, newName: string): Promise<boolean>;
3658
+ /** Drop a host by fingerprint or name. Returns false when nothing matched. */
3659
+ revoke(idOrName: string): Promise<boolean>;
3660
+ /** Is a `forwardHttp` call for this fingerprint in flight right now? */
3661
+ isOnline(fingerprint: string): boolean;
3662
+ /**
3663
+ * On-demand: dial the host, run the client handshake against the current
3664
+ * epoch's tokens (falling back to the previous epoch, bridging clock
3665
+ * skew — same two-attempt shape as `pair-transport.ts`'s
3666
+ * `openPairChannel`), forward one HTTP request over the resulting tunnel
3667
+ * client, then close. Throws with a re-pair hint if every attempt hangs up
3668
+ * on the hello (likely a pair/v1 peer).
3669
+ */
3670
+ forwardHttp(idOrName: string, req: ForwardHttpRequest): Promise<ForwardHttpResponse>;
3671
+ /**
3672
+ * Same dial/handshake/retry shape as {@link forwardHttp}, but for a
3673
+ * streamed response (SSE, NDJSON, long-poll) — the daemon-inference proxy's
3674
+ * `/v1/chat/completions` most notably, which must relay tokens as they
3675
+ * arrive rather than buffering the whole completion first. Unlike
3676
+ * `forwardHttp`, the tunnel client is kept open (and `isOnline` stays true)
3677
+ * until the returned `body` stream is fully drained, errors, or is
3678
+ * cancelled — only then does it close and the online count drop.
3679
+ */
3680
+ forwardHttpStream(idOrName: string, req: ForwardHttpRequest): Promise<ForwardHttpStreamResponse>;
3681
+ }
3682
+ declare function createHostRegistry(deps: HostRegistryDeps): HostRegistry;
3683
+
3684
+ /**
3685
+ * Daemon-side JOIN TOKEN registry (SANDBOX-VISIBILITY-JOIN, stacked on
3686
+ * DEVICES-PLAN PR-C's `host-registry.ts`).
3687
+ *
3688
+ * `host-registry.ts`'s `add()` requires a HUMAN to relay a host-scoped offer
3689
+ * URL from the box to this daemon (`agentproto pair offer --host` on the box,
3690
+ * `agentproto devices add <url>` here) — fine for a laptop, impossible for an
3691
+ * unattended CI sandbox with no one watching its logs to copy a one-time,
3692
+ * 10-minute-TTL secret. A join token flips who initiates: this daemon mints a
3693
+ * long-lived, revocable, reusable credential ahead of time (stored as a
3694
+ * GitHub secret, say); a box started with `AGENTPROTO_JOIN=<token>` dials
3695
+ * in — over the SAME rendezvous/pair-v2 primitives, no protocol change — and
3696
+ * this daemon adds it as a host automatically.
3697
+ *
3698
+ * ## Why this isn't just a long-lived `host-registry.ts` offer
3699
+ *
3700
+ * pair/v2 deliberately gives a CLIENT no persistent identity key (see
3701
+ * `pairing-registry.ts`'s "Reconnect authentication" doc) — only the
3702
+ * DAEMON/offerer side has stable keys a peer can pin and re-dial later.
3703
+ * `HostRegistry.forwardHttp` needs exactly those stable keys to reach a host
3704
+ * on demand, so the box MUST end up as the offerer of its own host-scoped
3705
+ * offer (a normal, single-use `pair offer --host`) with THIS daemon
3706
+ * `add()`-ing it — `host-registry.ts` is not touched by this module at all.
3707
+ *
3708
+ * What the join token replaces is only the relay step: this daemon parks a
3709
+ * STANDING accept loop (`side=daemon`, exactly like a `pair offer`'s park,
3710
+ * except never single-use) on a route deterministically derived from the
3711
+ * token's own secret. A box holding the token dials that route as the
3712
+ * CLIENT — the role a browser/CLI plays consuming a normal offer — and its
3713
+ * sealed hello's `clientName` carries a small JSON envelope: the box's own
3714
+ * self-minted host-scoped offer URL (freshly created, in-process, via its
3715
+ * own `PairingRegistry.createOffer({scope:"host"})` — see
3716
+ * `packages/cli/src/commands/serve.ts`'s `AGENTPROTO_JOIN` boot wiring) plus
3717
+ * a self-reported name/provider/sandboxId/labels. On receipt, this daemon
3718
+ * just calls `HostRegistry.add(offerUrl, name, meta)` — the exact call a
3719
+ * human would type — closes the bootstrap channel, and re-parks for the next
3720
+ * box. No new wire messages, no new crypto: two ordinary pair/v2 handshakes
3721
+ * back to back, the first only ever used to carry one string.
3722
+ *
3723
+ * ## Reuse and revocation
3724
+ *
3725
+ * Unlike an `OfferEntry` (`pairing-registry.ts`), a join token's
3726
+ * `{route, auth}` never rotates and is never "spent" — `useCount`/`maxUses`/
3727
+ * `expiresAt`/`revokedAt` (all checked inside `verifyAuthToken`, which may be
3728
+ * async) gate reuse instead. This is the same trust shape as an existing
3729
+ * epoch reconnect token (valid, unrotated, for its whole day) stretched over
3730
+ * a longer, explicit, revocable window — not a new security primitive.
3731
+ *
3732
+ * ## A known limitation
3733
+ *
3734
+ * One token parks on exactly one fixed route, so exactly one box can be
3735
+ * mid-handshake on it at a time; a second box dialing in the same instant
3736
+ * either waits for the loop to re-park (typical: the ceremony is a single
3737
+ * short-TTL round trip) or, in the worst case, times out and retries with
3738
+ * backoff. Fine for the sequential-CI-review case this exists for; a fleet
3739
+ * of concurrently-joining boxes would want multiple parked slots per token,
3740
+ * deferred (see PR body).
3741
+ *
3742
+ * Persisted to `~/.agentproto/join-tokens.json`, same atomic-write + lazy-
3743
+ * load discipline as `hosts.json`/`pairings.json`. The raw `secret` — not a
3744
+ * hash — is stored, exactly like `pairings.json` stores `pairRoot` in the
3745
+ * clear: it is crypto material this daemon must re-derive `{route, auth}`
3746
+ * from on every boot, not a password to be hashed.
3747
+ */
3748
+
3749
+ /** `join-tokens.json` format. */
3750
+ declare const JOIN_TOKENS_VERSION: 1;
3751
+ /** One persisted join token. `secret` is the crypto material a box's
3752
+ * `AGENTPROTO_JOIN` value carries too — never log or echo it once minted. */
3753
+ interface JoinTokenRecord {
3754
+ /** Short random id (CLI/MCP target), distinct from `secret`. */
3755
+ id: string;
3756
+ /** Human-facing label (e.g. "ci-reviewer"). */
3757
+ name: string;
3758
+ /** One-time-offer-shaped secret (base64url) — re-derives `{route, auth}`
3759
+ * via `deriveOfferTokens`. Never rotates for this token's lifetime. */
3760
+ secret: string;
3761
+ /** Rendezvous endpoint boxes dial through. */
3762
+ rendezvousUrl: string;
3763
+ /** ISO-8601 mint time. */
3764
+ createdAt: string;
3765
+ /** ISO-8601 expiry — always set (`create()` defaults `ttlMs`). */
3766
+ expiresAt: string;
3767
+ /** Reuse ceiling. Absent = unlimited (until `expiresAt`/revoke). */
3768
+ maxUses?: number;
3769
+ /** Successful joins so far. */
3770
+ useCount: number;
3771
+ /** ISO-8601 of the most recent successful join. */
3772
+ lastUsedAt?: string;
3773
+ /** ISO-8601 revoke time. Present ⇒ inactive; kept for `list()` history. */
3774
+ revokedAt?: string;
3775
+ }
3776
+ interface CreateJoinTokenInput {
3777
+ name: string;
3778
+ /** Time-to-live in ms. Default 90 days. */
3779
+ ttlMs?: number;
3780
+ /** Reuse ceiling. Absent = unlimited. */
3781
+ maxUses?: number;
3782
+ /** Rendezvous URL override; falls back to the configured default. */
3783
+ rendezvousUrl?: string;
3784
+ }
3785
+ interface CreatedJoinToken {
3786
+ id: string;
3787
+ name: string;
3788
+ /** The `AGENTPROTO_JOIN` value — an `agentproto://pair?…&scope=host` URL.
3789
+ * Shown ONCE; `list()` never returns it. */
3790
+ token: string;
3791
+ rendezvousUrl: string;
3792
+ expiresAt: string;
3793
+ }
3794
+ interface JoinTokenRegistryDeps {
3795
+ /** Lazily load (create on first use) the daemon identity — same contract
3796
+ * as `PairingRegistryDeps.loadIdentity`. */
3797
+ loadIdentity: () => Promise<DaemonIdentity>;
3798
+ /** Path to `join-tokens.json`. Defaults to `~/.agentproto/join-tokens.json`. */
3799
+ joinTokensPath?: string;
3800
+ /** Configured rendezvous URL default — same three-state contract as
3801
+ * `PairingRegistryDeps.defaultRendezvousUrl` (absent → hosted default;
3802
+ * `""` → explicit opt-out, fails closed without `--rendezvous`). */
3803
+ defaultRendezvousUrl?: string;
3804
+ /** Dial a rendezvous WS and adapt it to a `FrameSink`. Same shape as
3805
+ * `PairingRegistryDeps.dial`/`HostRegistryDeps.dial`. */
3806
+ dial: (wsUrl: string, signal: AbortSignal) => Promise<FrameSink>;
3807
+ /** Called once per successful join, with the box's self-minted host offer
3808
+ * URL and self-reported name/meta — normally `hostRegistry.add`. Errors
3809
+ * are logged and swallowed: a bad/unreachable offer from one join must not
3810
+ * crash the standing accept loop. */
3811
+ addHost: (offerUrl: string, name?: string, meta?: HostJoinMeta) => Promise<unknown>;
3812
+ /** Injectable clock (ms). Defaults to Date.now. */
3813
+ now?: () => number;
3814
+ /** Diagnostic log sink. */
3815
+ log?: (line: string) => void;
3816
+ /** Handshake ceiling per attempt. Default 110s (mirrors PairingRegistry's
3817
+ * park timeout, kept just under the broker's own). */
3818
+ handshakeTimeoutMs?: number;
3819
+ /** Reconnect backoff floor / ceiling (ms) after a failed dial. Defaults 1s/30s. */
3820
+ reconnectMinMs?: number;
3821
+ reconnectMaxMs?: number;
3822
+ }
3823
+ interface JoinTokenRegistry {
3824
+ /** Mint a token and start its standing accept loop. */
3825
+ create(input: CreateJoinTokenInput): Promise<CreatedJoinToken>;
3826
+ /** All persisted tokens (copies), `secret` stripped. Loads on first call. */
3827
+ list(): Promise<Omit<JoinTokenRecord, "secret">[]>;
3828
+ /** Revoke by id or name — stops its accept loop. Returns false if nothing
3829
+ * matched or it was already revoked. */
3830
+ revoke(idOrName: string): Promise<boolean>;
3831
+ /** Start standing accept loops for every active persisted token. Call once
3832
+ * at boot, after the gateway/hostRegistry are wired. */
3833
+ startAutoconnect(): Promise<void>;
3834
+ /** Tear down every accept loop. */
3835
+ shutdown(): Promise<void>;
3836
+ }
3837
+ declare function createJoinTokenRegistry(deps: JoinTokenRegistryDeps): JoinTokenRegistry;
3838
+
3066
3839
  /**
3067
3840
  * Shared types for pluggable sandbox providers.
3068
3841
  *
@@ -3873,6 +4646,29 @@ declare function createActivityProjector(opts: {
3873
4646
  resolvePrState?: PrStateResolver;
3874
4647
  }): ActivityProjector;
3875
4648
 
4649
+ /**
4650
+ * `~/.agentproto/bundles.json` — named sets of imported MCPs + skills (+
4651
+ * optionally the daemon's own `/mcp`), attachable to any harness in one
4652
+ * `agent_start({bundles:["research"]})` call.
4653
+ *
4654
+ * Same load/save/atomic-write pattern as `user-presets.ts`; validation
4655
+ * against the live imported-MCP set is the single boundary every writer
4656
+ * (MCP tools, HTTP routes) goes through (`saveBundle`).
4657
+ */
4658
+
4659
+ declare const bundlesFileSchema: z.ZodObject<{
4660
+ version: z.ZodLiteral<1>;
4661
+ bundles: z.ZodArray<z.ZodObject<{
4662
+ id: z.ZodString;
4663
+ label: z.ZodString;
4664
+ description: z.ZodOptional<z.ZodString>;
4665
+ mcpImports: z.ZodArray<z.ZodString>;
4666
+ includeDaemon: z.ZodOptional<z.ZodBoolean>;
4667
+ skills: z.ZodArray<z.ZodString>;
4668
+ }, z.core.$strip>>;
4669
+ }, z.core.$strip>;
4670
+ type BundlesFile = z.infer<typeof bundlesFileSchema>;
4671
+
3876
4672
  /**
3877
4673
  * Spawn-time role profiles — the primitive that decides whether a
3878
4674
  * spawned agent may itself delegate (spawn further children).
@@ -4113,6 +4909,21 @@ interface WorktreeProvisionRequest {
4113
4909
  /** Free-text label (the session's `label`) the provisioner may fold into a
4114
4910
  * minted slug for human readability. Never load-bearing for correctness. */
4115
4911
  labelHint?: string;
4912
+ /** Absolute path the provisioner should append the setup hooks' FULL
4913
+ * stdout+stderr to (see `worktree.provision`'s `setupLogPath` input and
4914
+ * `@agentproto/worktree`'s `runSetup`). `session-spawn.ts` passes the
4915
+ * spawning session's own transcript dir here, so a failing setup hook's
4916
+ * full output survives even after a subsequently-reclaimed worktree is
4917
+ * gone — the short `HookError` message alone only carries a filtered
4918
+ * tail. Omitted callers (e.g. `agentproto worktree new`, no session to
4919
+ * log into) simply get no persisted log. */
4920
+ setupLogPath?: string;
4921
+ /** Re-run a failing `worktree.setup` command exactly once before giving
4922
+ * up — see `worktree.provision`'s `retrySetupOnFailure` input and
4923
+ * `runSetup`'s doc for why this is a bounded complement, not a
4924
+ * substitute, and why the daemon opts in while `agentproto worktree new`
4925
+ * doesn't. */
4926
+ retrySetupOnFailure?: boolean;
4116
4927
  }
4117
4928
  /** The provisioner's outcome. `isolated: false` is NOT a failure — it means
4118
4929
  * there was nothing to isolate (`cwd` sits in no git repo), and the caller
@@ -4329,6 +5140,15 @@ interface SpawnAgentSessionDeps {
4329
5140
  * `loadConfig` when omitted; tests inject a stub to avoid touching
4330
5141
  * the real file. */
4331
5142
  loadDefaultsConfig?: () => Promise<SpawnDefaultsConfig | undefined>;
5143
+ /** Loads `~/.agentproto/bundles.json` for capability-bundle expansion
5144
+ * (`agent_start.bundles` / config `defaults.bundles`, PLAN D phase 1).
5145
+ * Defaults to the real loader when omitted; tests inject a stub to avoid
5146
+ * touching the real file. */
5147
+ loadBundlesConfig?: () => Promise<BundlesFile>;
5148
+ /** Loads `~/.agentproto/imported-mcps.json` — resolves a bundle's
5149
+ * `mcpImports` ids to their alias/snapshot and detects dangling ones.
5150
+ * Defaults to the real loader when omitted; tests inject a stub. */
5151
+ loadImportedMcpsConfig?: () => Promise<ImportedMcpsConfig>;
4332
5152
  /** Builds the per-session headless-browser mount for `browser:
4333
5153
  * "headless"` (see `browser-mount.ts`). Defaults to the real resolver
4334
5154
  * (installs chrome-devtools-mcp on first use); tests inject a stub. */
@@ -4394,6 +5214,18 @@ interface SpawnAgentSessionDeps {
4394
5214
  * manifest not covering the requested model still reaches the driver,
4395
5215
  * same as before this guard existed. */
4396
5216
  listCatalogModels?: CatalogModelsLister;
5217
+ /** Ensure the local `llm-endpoint` proxy sidecar (`LlmEndpointRegistry`)
5218
+ * is up before a spawn that bills through it. Without this hook, a spawn
5219
+ * whose resolved gateway is `"llm-endpoint"` assumed the sidecar was
5220
+ * already running (started externally, or via the `llm_endpoint_start`
5221
+ * MCP tool) — a fresh install with `features.llmEndpoint` on but nothing
5222
+ * having called that tool yet would spawn fine and only fail once the
5223
+ * adapter made its first request. Wired to the registry's own
5224
+ * (idempotent) `start()` at the composition root; omitted ⇒ unchanged
5225
+ * pre-existing behavior. Best-effort: a rejection here is swallowed by
5226
+ * the caller, not surfaced as a spawn failure — the adapter's own first
5227
+ * request is left to report the real connection error. */
5228
+ ensureLlmEndpointRunning?: () => Promise<void>;
4397
5229
  }
4398
5230
  interface SpawnAgentSessionInput {
4399
5231
  adapter: string;
@@ -4455,7 +5287,19 @@ interface SpawnAgentSessionInput {
4455
5287
  * blank. Not exposed on the MCP `agent_start` tool today — only the
4456
5288
  * HTTP route (`sessions restart`) passes this. */
4457
5289
  resumeSessionId?: string;
4458
- prompt?: string;
5290
+ /** The caller's initial ask — a plain string (the common case, composed
5291
+ * with the role/AGENTS.md/RULES.md preamble below into one string turn),
5292
+ * or a content block / block array (an attachment-bearing first message,
5293
+ * same loose shape `POST /sessions/:id/prompt` accepts). The block form
5294
+ * is a narrow path: composing daemon-synthesized preamble TEXT onto a
5295
+ * turn that opens with an image isn't meaningful string concatenation,
5296
+ * so it SKIPS the whole preamble composition below (role disposition,
5297
+ * AGENTS.md, RULES.md, parent-lineage line, posture-as-prompt fallback)
5298
+ * and is sent to the adapter verbatim — see `effectivePrompt`'s doc.
5299
+ * Acceptable because this path is human/UI-initiated (an attachment
5300
+ * typed into a chat composer), not the orchestrator-driven role spawns
5301
+ * that actually depend on the composed disposition text. */
5302
+ prompt?: string | Record<string, unknown> | unknown[];
4459
5303
  label?: string;
4460
5304
  /** Explicit session title (SPEC-3 FIX C, `agentproto sessions start
4461
5305
  * --title`). When set, it wins over the first-sentence derivation from
@@ -4477,6 +5321,22 @@ interface SpawnAgentSessionInput {
4477
5321
  * such option (e.g. claude-code, which auto-discovers skills) ignore
4478
5322
  * it. See `resolveSpawnDefaults` / `normalizeSkillsOption`. */
4479
5323
  skills?: string[];
5324
+ /** Capability bundle ids (`bundle_list`) to attach (PLAN D phase 1). Merged
5325
+ * with config.json's `defaults.bundles` / `defaults.adapters.<slug>.bundles`
5326
+ * with the same precedence/replace semantics as `skills` — see
5327
+ * `resolveBundleDefaults`. Each bundle expands to its `mcpImports` (mounted
5328
+ * as native MCP servers via the daemon's `/mcp/imported/<id>` passthrough,
5329
+ * never the two-step `mcp_imported_*` indirection), optionally the
5330
+ * daemon's own `/mcp` (`includeDaemon`), and its `skills` (unioned into
5331
+ * the resolved skill list). Ignored (with a warning) for a sandbox spawn —
5332
+ * the box cannot reach this daemon's loopback `/mcp`. */
5333
+ bundles?: string[];
5334
+ /** Explicitly mount the daemon's own scoped `/mcp` for this spawn — the
5335
+ * only way an adapter outside `shouldInjectDaemonSelfMount`'s allowlist
5336
+ * (opencode, codex, gemini, …) gets it. Merged with config's
5337
+ * `defaults.adapters.<slug>.daemonMount` (per-spawn wins). Ignored (with a
5338
+ * warning) for a sandbox spawn. */
5339
+ daemonMount?: boolean;
4480
5340
  model?: string;
4481
5341
  effort?: string;
4482
5342
  /** Decomposed route identity. This is canonical transport; `mode` remains
@@ -4615,6 +5475,12 @@ interface SpawnAgentSessionInput {
4615
5475
  * external webhook path (`notifyUrl`) already fires regardless of this
4616
5476
  * flag. */
4617
5477
  notifyParentOnCrash?: boolean;
5478
+ /** Per-spawn opt-out of sentinel auto-link (AIP-60 §6/step 4): `false`
5479
+ * means a PR this session opens is never auto-watched, regardless of
5480
+ * `config.sentinel.autoWatchPrs`. Recorded verbatim onto
5481
+ * {@link SessionDescriptor.sentinelAutoWatch}; `undefined`/`true` = allowed
5482
+ * (still gated by the config default). */
5483
+ sentinel?: boolean;
4618
5484
  /** Exempt this session from the idle-reaper (`isReapable` in
4619
5485
  * idle-reaper.ts) regardless of how long it sits idle. Stamped straight
4620
5486
  * onto the descriptor — see `SessionDescriptor.keepAlive`. Default false
@@ -4955,6 +5821,8 @@ interface BranchGcPlanView {
4955
5821
  includeReviewed: boolean;
4956
5822
  generatedAt: string;
4957
5823
  otherRemoteRefs: number;
5824
+ /** true = base-remote `fetch --prune` succeeded; false = failed; null = skipped / no remote. */
5825
+ fetched: boolean | null;
4958
5826
  entries: BranchGcPlanEntryView[];
4959
5827
  }
4960
5828
  /** kind → class → count, and kind → (status | "protected") → count. */
@@ -5033,6 +5901,22 @@ interface BranchGcVerdictLookupInput {
5033
5901
  * lookup, so a caller can check one tip without re-running a whole `branch_gc` plan. */
5034
5902
  type BranchGcVerdictReader = (input: BranchGcVerdictLookupInput) => Promise<BranchGcVerdictRecordView | null>;
5035
5903
 
5904
+ /**
5905
+ * Set by the tunnel server (never a caller) on every `http_request` it
5906
+ * forwards from a HOST-scoped pairing (DEVICES-PLAN item 1) —
5907
+ * `buildDaemonTunnelServerOptions`'s `pairingScope` in
5908
+ * `packages/cli/src/util/tunnel-serve.ts`. `httpInjectHeaders` overrides any
5909
+ * same-named header the ORIGINAL caller's frame carried, so this can never be
5910
+ * spoofed by whoever dials in over a pairing — only the daemon's own
5911
+ * server-side scope decision at serve time ever sets it.
5912
+ *
5913
+ * Deliberately NOT paired with an `isLoopback` bypass the way most other
5914
+ * routes are: every `http_request` a tunnel forwards to THIS server lands as
5915
+ * a loopback socket (the daemon dialing its own gateway via `fetch`), so
5916
+ * `isLoopback` is true for ALL forwarded traffic — host-scoped or not — and
5917
+ * cannot distinguish them. `handleDeviceInference` checks this header alone.
5918
+ */
5919
+ declare const HOST_SCOPE_HEADER = "x-agentproto-host-scope";
5036
5920
  /**
5037
5921
  * Pluggable adapter resolver — keeps the runtime package free of any
5038
5922
  * @agentproto/cli dep. The host (cli `serve`, playground, embedding
@@ -5158,12 +6042,10 @@ type AgentAdapterResolver = (slug: string) => Promise<{
5158
6042
  }): Promise<AgentSessionLike>;
5159
6043
  /** Display label for the descriptor's `command` field. */
5160
6044
  commandPreview?: string;
5161
- /** Best-effort per-session usage reader (adapter-specific, e.g. hermes state.db). */
5162
- readUsage?: (adapterSessionId: string) => Promise<{
5163
- costUsd?: number;
5164
- tokensIn?: number;
5165
- tokensOut?: number;
5166
- } | null>;
6045
+ /** Best-effort per-session usage reader (adapter-specific, e.g. hermes
6046
+ * state.db, the claude-code transcript JSONL). `ctx` carries the spawn's
6047
+ * cwd + isolated config dir for readers that locate their store by them. */
6048
+ readUsage?: (adapterSessionId: string, ctx?: UsageReadContext) => Promise<UsageReadResult | null>;
5167
6049
  /** AIP-45 `options[]` this adapter's manifest declares (id + type only —
5168
6050
  * no spawn internals). Lets `session-spawn.ts` fold a config-level
5169
6051
  * `defaults.skills` list into `options.skills` using the shape the
@@ -5313,169 +6195,48 @@ interface AdapterInstallResult {
5313
6195
  * post-install re-list failed (the install itself may still have
5314
6196
  * succeeded — read `ok`). */
5315
6197
  status?: "supported" | "available" | "ready" | "unresolvable";
5316
- /** True when the install failed ONLY because a manifest setup step
5317
- * declares `interactive: true` and this process has no TTY to run it
5318
- * in (e.g. openclaw's `onboard --install-daemon` TUI). The remedy is a
5319
- * real terminal — `agentproto setup <slug>` — which a UI can offer
5320
- * directly as a PTY terminal session. */
5321
- needsInteractiveSetup?: boolean;
5322
- }
5323
- /**
5324
- * Installs an agent CLI adapter by slug and reports the outcome. Wired by
5325
- * the daemon from `@agentproto/cli`'s `installAdapter` (which knows the
5326
- * catalog + the two install classes); the runtime stays cli-free and only
5327
- * exposes the verb. Must never throw for an ordinary install failure —
5328
- * report it via `ok:false` + `message` — so the MCP tool / HTTP route can
5329
- * return a clean result instead of a 500.
5330
- */
5331
- type AgentAdapterInstaller = (slug: string) => Promise<AdapterInstallResult>;
5332
- /** Loads the read-only vendor/product/route catalog (SPEC §5) for
5333
- * `GET /catalog/models` + the `catalog_models` MCP tool. A host wires this
5334
- * from `buildCatalogModels` (`catalog-models.ts`) fed by its installed
5335
- * adapters + `@agentproto/auth`'s `listAuthProfiles()` — the query params
5336
- * are forwarded verbatim from the request. Omitted ⇒ the route/tool
5337
- * report "not enabled" (same convention as `listAgentAdapters`). */
5338
- type CatalogModelsLister = (query: CatalogModelsQuery) => Promise<CatalogModelsResponse>;
5339
- interface AuthOptions {
5340
- mode: "none" | "bearer";
5341
- token?: string;
5342
- }
5343
-
5344
- /** One background task as mirrored onto `SessionDescriptor.backgroundTasks`. */
5345
- interface SessionBackgroundTask {
5346
- taskId: string;
5347
- /** Friendly task type (`"shell"`, `"monitor"`, ...), when the agent says. */
5348
- taskKind?: string;
5349
- description?: string;
5350
- /** Where the task writes its output, when the agent says. */
5351
- outputFile?: string;
5352
- status: "running" | "paused" | "completed" | "failed" | "stopped";
5353
- summary?: string;
5354
- toolCallId?: string;
5355
- /** ISO 8601 — when the daemon first heard of the task. */
5356
- startedAt: string;
5357
- }
5358
-
5359
- /**
5360
- * Typed inter-session message envelope (AIP-46 §Session messages).
5361
- *
5362
- * A PROMPT is an instruction from whoever drives a session; a MESSAGE is a
5363
- * report between sessions in the same tree whose sender the DAEMON attests.
5364
- * Everything in `from` is computed here from the caller's verified identity
5365
- * and the session forest — a send request can never set it.
5366
- *
5367
- * Pure module: no registry, no I/O. The registry (sessions.ts) carries a
5368
- * `SessionMessage` on a queued prompt (`QueuedPrompt.envelope`), renders it
5369
- * into the model's context with `renderSessionMessages`, and records it as
5370
- * its own `session-message` transcript record — never a `user-prompt`.
5371
- */
5372
- /** Sender's position in the tree, relative to the recipient. */
5373
- type MessageRelation = "child" | "parent" | "sibling" | "human" | "system";
5374
- type MessageKind = "report" | "question" | "blocker" | "done" | "notice";
5375
- type MessageUrgency = "fyi" | "next-turn" | "steer" | "interrupt";
5376
- type MessageDeliveryVia = "wait" | "steer" | "turn" | "interrupt" | "inbox";
5377
- declare const MESSAGE_KINDS: readonly MessageKind[];
5378
- declare const MESSAGE_URGENCIES: readonly MessageUrgency[];
5379
- interface SessionMessageFrom {
5380
- /** Absent only for relation `human` | `system`. */
5381
- sessionId?: string;
5382
- /** Registry label at send time — display only, never identity. */
5383
- label?: string;
5384
- role?: string;
5385
- adapter?: string;
5386
- relation: MessageRelation;
5387
- }
5388
- interface SessionMessage {
5389
- /** `msg_<uuid8>`, daemon-minted. */
5390
- id: string;
5391
- ts: string;
5392
- /** Recipient sessionId. */
5393
- to: string;
5394
- from: SessionMessageFrom;
5395
- kind: MessageKind;
5396
- /** Requested urgency. */
5397
- urgency: MessageUrgency;
5398
- /** Stamped on delivery. */
5399
- delivered?: {
5400
- via: MessageDeliveryVia;
5401
- at: string;
5402
- turnSeq?: number;
5403
- };
5404
- /** Thread id; defaults to the first message's id. */
5405
- correlationId?: string;
5406
- /** A `SessionMessage.id` this answers. */
5407
- replyTo?: string;
5408
- text: string;
5409
- /** Optional structured payload (≤ `MAX_MESSAGE_DATA_BYTES`). */
5410
- data?: Record<string, unknown>;
5411
- ackedAt?: string;
5412
- }
5413
- declare const MAX_MESSAGE_DATA_BYTES: number;
5414
- /** The slice of a session descriptor relation/ACL resolution needs. */
5415
- interface MessageTreeNode {
5416
- id: string;
5417
- parentSessionId?: string;
5418
- label?: string;
5419
- role?: string;
5420
- adapterSlug?: string;
6198
+ /** True when the install failed ONLY because a manifest setup step
6199
+ * declares `interactive: true` and this process has no TTY to run it
6200
+ * in (e.g. openclaw's `onboard --install-daemon` TUI). The remedy is a
6201
+ * real terminal — `agentproto setup <slug>` — which a UI can offer
6202
+ * directly as a PTY terminal session. */
6203
+ needsInteractiveSetup?: boolean;
5421
6204
  }
5422
6205
  /**
5423
- * Relation of `sender` to `recipient` in the session forest, or `undefined`
5424
- * when they're not tree neighbours (cousins, strangers, self). `sender`
5425
- * absent ⇒ `human` (an HTTP/CLI operator); daemon-originated notices pass
5426
- * `"system"` explicitly instead of calling this.
5427
- */
5428
- declare function resolveRelation(sender: MessageTreeNode | undefined, recipient: MessageTreeNode): Exclude<MessageRelation, "system"> | undefined;
5429
- /**
5430
- * ACL: child→parent, parent→direct child, human/system→any. Sibling↔sibling
5431
- * only when `allowSiblings` (config-gated, default off). Anything else — a
5432
- * cousin, a stranger, yourself — is refused.
6206
+ * Installs an agent CLI adapter by slug and reports the outcome. Wired by
6207
+ * the daemon from `@agentproto/cli`'s `installAdapter` (which knows the
6208
+ * catalog + the two install classes); the runtime stays cli-free and only
6209
+ * exposes the verb. Must never throw for an ordinary install failure —
6210
+ * report it via `ok:false` + `message` — so the MCP tool / HTTP route can
6211
+ * return a clean result instead of a 500.
5433
6212
  */
5434
- declare function isMessageAllowed(relation: MessageRelation | undefined, opts?: {
5435
- allowSiblings?: boolean;
5436
- }): relation is MessageRelation;
5437
- /** Build a `from` block from the verified sender + the resolved relation. */
5438
- declare function messageFrom(sender: MessageTreeNode | undefined, relation: MessageRelation): SessionMessageFrom;
5439
- declare function mintMessageId(): string;
5440
- declare function createSessionMessage(input: {
5441
- to: string;
5442
- from: SessionMessageFrom;
5443
- text: string;
5444
- kind?: MessageKind;
5445
- urgency?: MessageUrgency;
5446
- replyTo?: string;
5447
- correlationId?: string;
5448
- data?: Record<string, unknown>;
5449
- }): SessionMessage;
5450
- interface MessageFilter {
5451
- /** Sender session ids, or `"children"` for any message whose relation is
5452
- * `child` (i.e. from one of the recipient's own children). */
5453
- from?: readonly string[] | "children";
5454
- kind?: readonly MessageKind[];
5455
- correlationId?: string;
6213
+ type AgentAdapterInstaller = (slug: string) => Promise<AdapterInstallResult>;
6214
+ /** Loads the read-only vendor/product/route catalog (SPEC §5) for
6215
+ * `GET /catalog/models` + the `catalog_models` MCP tool. A host wires this
6216
+ * from `buildCatalogModels` (`catalog-models.ts`) fed by its installed
6217
+ * adapters + `@agentproto/auth`'s `listAuthProfiles()` — the query params
6218
+ * are forwarded verbatim from the request. Omitted ⇒ the route/tool
6219
+ * report "not enabled" (same convention as `listAgentAdapters`). */
6220
+ type CatalogModelsLister = (query: CatalogModelsQuery) => Promise<CatalogModelsResponse>;
6221
+ interface AuthOptions {
6222
+ mode: "none" | "bearer";
6223
+ token?: string;
6224
+ }
6225
+
6226
+ /** One background task as mirrored onto `SessionDescriptor.backgroundTasks`. */
6227
+ interface SessionBackgroundTask {
6228
+ taskId: string;
6229
+ /** Friendly task type (`"shell"`, `"monitor"`, ...), when the agent says. */
6230
+ taskKind?: string;
6231
+ description?: string;
6232
+ /** Where the task writes its output, when the agent says. */
6233
+ outputFile?: string;
6234
+ status: "running" | "paused" | "completed" | "failed" | "stopped";
6235
+ summary?: string;
6236
+ toolCallId?: string;
6237
+ /** ISO 8601 — when the daemon first heard of the task. */
6238
+ startedAt: string;
5456
6239
  }
5457
- declare function matchesMessageFilter(msg: SessionMessage, filter: MessageFilter): boolean;
5458
- declare const MESSAGE_TAG = "agentproto-message";
5459
- /** Child-settable label → attribute-safe: `[A-Za-z0-9._-]`, ≤ 48 chars. */
5460
- declare function sanitizeLabel(label: string | undefined): string | undefined;
5461
- /** Escape every envelope/body sentinel inside a message body, so a sender
5462
- * can't close the tag early and forge a second header. */
5463
- declare function escapeMessageBody(text: string): string;
5464
- /** A human prompt is NOT wrapped (humans keep today's UX), but a line that
5465
- * starts with the envelope sentinel is escaped, so the invariant "text
5466
- * outside an `<agentproto-message>` tag is the human" holds. Nothing else
5467
- * in human text changes. */
5468
- declare function escapeHumanPrompt(text: string): string;
5469
- declare function renderSessionMessage(msg: SessionMessage): string;
5470
- /** A coalesced batch → sibling tags, one per message, in order. */
5471
- declare function renderSessionMessages(msgs: readonly SessionMessage[]): string;
5472
- /** One-line typed digest of `fyi` messages that never woke the session —
5473
- * prepended (as a `system-prompt` slice) to the next turn it runs anyway. */
5474
- declare function renderInboxDigest(msgs: readonly SessionMessage[]): string;
5475
- /** Taught once, the first time a session RECEIVES a message (recorded as a
5476
- * `system-prompt`). Children also get the same invariant in their lineage
5477
- * line at spawn. */
5478
- declare const MESSAGE_PREAMBLE: string;
5479
6240
 
5480
6241
  /**
5481
6242
  * A per-session observer — the pluggable seam behind the runtime's transcript
@@ -5537,6 +6298,94 @@ interface SessionObserver {
5537
6298
  */
5538
6299
  declare function composeSessionObservers(observers: readonly SessionObserver[]): SessionObserver;
5539
6300
 
6301
+ /**
6302
+ * Session-scoped artifact store — durable, content-addressed documents
6303
+ * (image, pdf, html, presentation, site, generic file) attached to an
6304
+ * agent-cli session, kept across restarts and browsable in the session's
6305
+ * "Artifacts" section, distinct from the ephemeral inline card a chat
6306
+ * message renders.
6307
+ *
6308
+ * Deliberately mirrors `transcript-writer.ts`'s `AttachmentEntry`, which
6309
+ * itself mirrors AIP-58's `ArtifactEntry` (`packages/workflow-runtime/src/
6310
+ * types.ts`) field-for-field — `key`/`path`/`sha256`/`size`/`contentType` —
6311
+ * so promoting an attachment to an artifact is a metadata copy, never a
6312
+ * data migration. What this module adds on top of a bare `ArtifactEntry`
6313
+ * is the session-artifact-specific envelope AIP-58 has no notion of:
6314
+ * `kind`, `label`, `pinned`, `createdBy`, `sourceRef`, and version history
6315
+ * under one stable `key`.
6316
+ *
6317
+ * Storage layout, under `sessionTranscriptDir(sessionId)`:
6318
+ *
6319
+ * artifacts/<sha256>.<ext> — one file per single-file version
6320
+ * artifacts/<sha256>/ — one directory per "site" version
6321
+ * artifacts.jsonl — append-only ledger, one JSON line per
6322
+ * mutation (`artifact.added` |
6323
+ * `artifact.pinned`)
6324
+ *
6325
+ * The ledger is the single source of truth, same posture AIP-58 §5 takes
6326
+ * for its run event log: `listSessionArtifacts`/`getSessionArtifact` are
6327
+ * folds over it, never a second store that could disagree with it. A
6328
+ * `key`'s version history is append-only; nothing here ever rewrites a
6329
+ * prior version's file or ledger line.
6330
+ */
6331
+ type ArtifactKind = "image" | "document" | "pdf" | "html" | "presentation" | "site" | "file";
6332
+ type ArtifactCreator = "agent" | "user";
6333
+ interface ArtifactVersionEntry {
6334
+ version: number;
6335
+ /** Content hash — sha256 of the file's bytes for a single-file version,
6336
+ * or a manifest hash (sorted relative paths + sizes) for a "site"
6337
+ * directory version. Not a cryptographic content-integrity guarantee
6338
+ * for the directory case, only a stable change-detection key. */
6339
+ sha256: string;
6340
+ /** Relative to `sessionTranscriptDir(sessionId)` — `"artifacts/<sha>.<ext>"`
6341
+ * for a single file, `"artifacts/<sha>"` (a directory) for a site. */
6342
+ path: string;
6343
+ /** True when `path` names a directory (a "site"). */
6344
+ isDirectory: boolean;
6345
+ size: number;
6346
+ contentType?: string;
6347
+ createdAt: string;
6348
+ createdBy: ArtifactCreator;
6349
+ sourceRef?: string;
6350
+ }
6351
+ interface ArtifactRecord {
6352
+ key: string;
6353
+ kind: ArtifactKind;
6354
+ label?: string;
6355
+ pinned: boolean;
6356
+ updatedAt: string;
6357
+ versions: ArtifactVersionEntry[];
6358
+ }
6359
+ interface AddSessionArtifactInput {
6360
+ /** Stable id this version is filed under. Versions accrue when the same
6361
+ * key is added again with different content. Defaults to a slug of
6362
+ * `name`, else the content hash. */
6363
+ key?: string;
6364
+ kind?: ArtifactKind;
6365
+ label?: string;
6366
+ createdBy: ArtifactCreator;
6367
+ /** e.g. `"attachment:attachments/<sha>.<ext>"`, `"workflow-run:<runId>/
6368
+ * <artifactKey>"`, `"canvakit:<exportPath>"` — free-form provenance. */
6369
+ sourceRef?: string;
6370
+ /** Inline content, base64-encoded. Mutually exclusive with `sourcePath`. */
6371
+ bytes?: string;
6372
+ /** Display name — also the default `key`/extension source. */
6373
+ name?: string;
6374
+ mimeType?: string;
6375
+ /** Absolute path to an existing file or directory (e.g. a canvakit
6376
+ * export) to copy into the store. A directory implies `kind: "site"`
6377
+ * unless overridden. Mutually exclusive with `bytes`. */
6378
+ sourcePath?: string;
6379
+ }
6380
+ interface GetSessionArtifactResult {
6381
+ record: ArtifactRecord;
6382
+ version: ArtifactVersionEntry;
6383
+ /** Absent for a "site" (directory) version — browse it via the raw-serve
6384
+ * route instead; nothing here bounds a whole directory's bytes. */
6385
+ content?: Buffer;
6386
+ truncated: boolean;
6387
+ }
6388
+
5540
6389
  /** Where a session's `contextSize` came from — see the module doc. */
5541
6390
  type ContextSizeSource = "adapter" | "catalog" | "reported";
5542
6391
 
@@ -5560,8 +6409,11 @@ type ContextSizeSource = "adapter" | "catalog" | "reported";
5560
6409
  */
5561
6410
 
5562
6411
  interface SessionOutcomeArtifact {
5563
- type: "pr" | "commit" | "url";
5564
- /** The artifact's canonical reference — a PR url, a commit sha, a url. */
6412
+ type: "pr" | "commit" | "url" | "file";
6413
+ /** The artifact's canonical reference — a PR url, a commit sha, a url,
6414
+ * or (for `type: "file"`) the session artifact's `key`
6415
+ * (`session-artifacts.ts`) — resolve it via `session_artifact_get` /
6416
+ * `GET /sessions/:id/artifacts/:key`. */
5565
6417
  ref: string;
5566
6418
  title?: string;
5567
6419
  }
@@ -5574,12 +6426,26 @@ interface SessionOutcomeLink {
5574
6426
  title?: string;
5575
6427
  }
5576
6428
  interface SessionOutcome {
5577
- /** Level 1 only ever writes `"derived"`; a declared outcome (Level 2) is a
5578
- * later addition. */
5579
- source: "derived";
6429
+ /** Level 1 (`deriveSessionOutcome`) only ever writes `"derived"` — what an
6430
+ * ended session produced, with zero agent/human cooperation. Level 2 adds
6431
+ * `"judged"` (a judge agent decided the verdict, `judgedBy` names the
6432
+ * judge session) and `"declared"` (a deterministic rule or an operator
6433
+ * declared it, `judgedBy` is `"steward-rules"` or absent). See
6434
+ * `registry.closeWithOutcome`. */
6435
+ source: "derived" | "judged" | "declared";
5580
6436
  /** `produced` — the session said something or left an artifact;
5581
6437
  * `empty` — no assistant text and no artifacts. */
5582
6438
  status: "produced" | "empty";
6439
+ /** Level 2 only: what a judge/declaration decided the session's work
6440
+ * amounted to. Absent on a plain Level 1 `"derived"` outcome. */
6441
+ verdict?: "done" | "abandoned" | "blocked" | "needs-input";
6442
+ /** Level 2 only: who reached `verdict` — a session id (a judge agent) or
6443
+ * the literal `"steward-rules"` for a deterministic close with no judge
6444
+ * in the loop. Absent on a plain Level 1 `"derived"` outcome. */
6445
+ judgedBy?: string;
6446
+ /** Level 2 only: free-text note from the judge/declaration explaining
6447
+ * `verdict` — never populated by `deriveSessionOutcome` itself. */
6448
+ note?: string;
5583
6449
  /** Last assistant message of the session, trimmed to
5584
6450
  * {@link OUTCOME_SUMMARY_MAX} chars (the TAIL is kept — the conclusion,
5585
6451
  * not the preamble). Absent when the session never said anything. */
@@ -5625,6 +6491,12 @@ interface SessionOutcomeCompact {
5625
6491
  interface DeriveSessionOutcomeInput {
5626
6492
  /** The last assistant message the registry observed (raw, untrimmed). */
5627
6493
  lastAssistantText?: string;
6494
+ /** Extra `type: "file"` artifacts to merge alongside the PR-derived ones
6495
+ * — the registry passes the session's PINNED `session-artifacts.ts`
6496
+ * records here so the ended block can show them (an unpinned artifact
6497
+ * still lists in the session's "Artifacts" section, just not surfaced
6498
+ * in the terse outcome). */
6499
+ artifacts?: SessionOutcomeArtifact[];
5628
6500
  now?: Date;
5629
6501
  }
5630
6502
  /**
@@ -6155,7 +7027,10 @@ interface SessionDescriptor {
6155
7027
  * `"parent-exited"` (an orchestrator's dying subtree reap),
6156
7028
  * `"provider-limit"` (a driver-reported subscription/usage-cap error,
6157
7029
  * e.g. Claude Code's "hit your session limit"), `"forgotten"` (a live
6158
- * session killed as part of an operator `DELETE`).
7030
+ * session killed as part of an operator `DELETE`), `"steward-completed"` /
7031
+ * `"steward-abandoned"` (the session steward's `closeWithOutcome` —
7032
+ * verdict `"done"` vs. `"abandoned"`; a `"blocked"`/`"needs-input"`
7033
+ * verdict doesn't close at all, see `SessionDescriptor.wrapupFlag`).
6159
7034
  * Absent for every terminal path this file doesn't tag (a plain natural
6160
7035
  * exit, an ordinary turn error) — the session's own fault, or at least
6161
7036
  * not something worth a special label. A string outside this list is
@@ -6188,6 +7063,20 @@ interface SessionDescriptor {
6188
7063
  * revives the row. Persisted, so it survives restarts and archiving. See
6189
7064
  * `session-outcome.ts`. */
6190
7065
  outcome?: SessionOutcome;
7066
+ /** A steward verdict of `"blocked"` or `"needs-input"` from
7067
+ * `registry.closeWithOutcome` (`session-wrapup.ts` / FIX-9A) — recorded
7068
+ * INSTEAD of closing, since neither verdict means the session is done.
7069
+ * The session stays exactly as alive as it was; this is a signal for a
7070
+ * human/orchestrator to look, not a termination. Persisted; overwritten
7071
+ * (not accumulated) by the next flag, and left stale on the descriptor
7072
+ * until something clears it (nothing here does — a later successful
7073
+ * turn/close is a separate, more informative signal than deleting this). */
7074
+ wrapupFlag?: {
7075
+ verdict: "blocked" | "needs-input";
7076
+ note?: string;
7077
+ judgedBy?: string;
7078
+ at: string;
7079
+ };
6191
7080
  /** Last time anything was written to stdout/stderr. Lets the UI
6192
7081
  * spot stuck sessions ("running for 2h, last output 12min ago"). */
6193
7082
  lastOutputAt?: string;
@@ -6225,6 +7114,12 @@ interface SessionDescriptor {
6225
7114
  * every read since it's a live OS query, stale the instant it's
6226
7115
  * written to disk. */
6227
7116
  processAlive?: boolean;
7117
+ /** Summed RSS in bytes of this session's process and all its OS-level
7118
+ * descendants (`processTreeRss`, `process-memory.ts`). Ephemeral,
7119
+ * never persisted — stamped only when a caller opts in (`session_list`'s
7120
+ * `withMemory: true`) since it costs a `ps` spawn; absent otherwise, and
7121
+ * absent for a row with no `pid` or that `ps` didn't report. */
7122
+ rssBytes?: number;
6228
7123
  /** Count of live supervisors currently blocked waiting on this session —
6229
7124
  * HTTP `GET /sessions/:id/wait` long-polls and `session_monitor`
6230
7125
  * subscriptions, both via `monitorSessionWait` (#session-visibility).
@@ -6320,16 +7215,32 @@ interface SessionDescriptor {
6320
7215
  * `status` to `"error"` directly (a genuinely terminal row the existing
6321
7216
  * crash/error handling already catches). This is the twin gap for a
6322
7217
  * turn that fails IN-BAND while the process stays alive: `status` stays
6323
- * `"running"`, `lastError` stays unset (reserved for `markCrashed`), and
6324
- * nothing else distinguishes the session from one that simply finished
6325
- * a clean turn and is idle. Folded into the same "stalled" activity/UI
6326
- * treatment a mid-turn stall gets (see sessionsTree.logic.ts's
6327
- * `activityFor`) rather than a new state, on the theory that both mean
6328
- * "the last thing this session did needs a look, not a re-prompt on
6329
- * faith". Stamped at turn-end when `turnEndReason === "error"`; cleared
6330
- * the moment a LATER turn completes without one (same reset shape as
7218
+ * `"running"`, `lastError` stays unset (still reserved for `markCrashed`
7219
+ * — see `lastTurnErrorMessage` below for the message text this field's
7220
+ * own turn carries), and nothing else distinguishes the session from
7221
+ * one that simply finished a clean turn and is idle. Folded into the
7222
+ * same "stalled" activity/UI treatment a mid-turn stall gets (see
7223
+ * sessionsTree.logic.ts's `activityFor`) rather than a new state, on
7224
+ * the theory that both mean "the last thing this session did needs a
7225
+ * look, not a re-prompt on faith". Stamped at turn-end when
7226
+ * `turnEndReason === "error"` — including the case where the adapter's
7227
+ * stream generator simply returned after emitting an in-band `error`
7228
+ * event with no explicit turn-end and no thrown exception, which
7229
+ * `runAgentTurn`'s P5 synthetic-reason fallback now classifies as
7230
+ * `"error"` rather than defaulting to `"exited"`. Cleared the moment a
7231
+ * LATER turn completes without one (same reset shape as
6331
7232
  * `resumeAttempts`/`restartAttempts`). Detection + signal only. */
6332
7233
  lastTurnErroredAt?: string;
7234
+ /** The captured error text for the turn `lastTurnErroredAt` timestamps —
7235
+ * `evt.error.message` off the in-band stream `error` event observed
7236
+ * during that turn (see `runAgentTurn`'s `turnErrorMessage`). Distinct
7237
+ * from `lastError` (reserved for `markCrashed`) and from
7238
+ * `rt.lastErrorMessage` (an internal, cross-turn, never-cleared runtime
7239
+ * field — this one is the public, turn-scoped descriptor projection of
7240
+ * it). Absent when the adapter reported `reason:"error"` on its own
7241
+ * turn-end without a preceding `error`-kind stream event. Stamped and
7242
+ * cleared together with `lastTurnErroredAt`. */
7243
+ lastTurnErrorMessage?: string;
6333
7244
  /** True when the LAST completed turn produced zero assistant output and
6334
7245
  * zero tool calls (mirrors `SessionTurnEndEvent.empty` — see that
6335
7246
  * field's doc). Persisted so `monitorSessionWait`'s synchronous
@@ -6359,6 +7270,32 @@ interface SessionDescriptor {
6359
7270
  * the underlying `killedMidTurn`/`endedReason`). The in-place resume path
6360
7271
  * never auto-retries the interrupted prompt. */
6361
7272
  interrupted?: boolean;
7273
+ /** The `SessionsRegistry.bootId` of the daemon boot that followed the
7274
+ * restart which cut this row's turn off. The graceful shutdown that
7275
+ * interrupts a turn parks `INTERRUPTED_PENDING_BOOT` here; the next boot
7276
+ * reload (`loadHistorySnapshot`) swaps that — or, after an unclean exit, a
7277
+ * row still alive on disk — for its own `bootId`. Nothing else is ever
7278
+ * stamped: an interrupted row carrying no marker was persisted by a daemon
7279
+ * predating it, i.e. by an older restart, and stays stale. Cleared on the
7280
+ * next successful turn-end (with the markers it
7281
+ * qualifies). `interruptedAtBoot === registry.bootId` is how the
7282
+ * continue-interrupted verb and boot pass pick "interrupted by the LAST
7283
+ * restart" apart from a stale interruption nobody picked back up. PERSISTED
7284
+ * (unlike the derived `interrupted`) — the boot that stamped it may not be
7285
+ * the one reading it. */
7286
+ interruptedAtBoot?: string;
7287
+ /** How many automatic continue prompts the daemon has sent this row
7288
+ * (`daemon.continueInterruptedOnBoot`) since its last successful turn-end.
7289
+ * Incremented BEFORE the prompt is sent, so a continue turn that itself
7290
+ * gets cut off by another restart still counts; reset on the next
7291
+ * successful turn-end. The boot pass stops at
7292
+ * `MAX_AUTO_CONTINUE_ATTEMPTS` — the no-loop bound for a turn that keeps
7293
+ * taking the daemon down with it. PERSISTED; absent until the first one. */
7294
+ autoContinueAttempts?: number;
7295
+ /** `bootId` of the boot whose pass last auto-continued this row — the
7296
+ * at-most-one-auto-continue-per-restart guard. Reset with
7297
+ * `autoContinueAttempts`. */
7298
+ lastAutoContinueBoot?: string;
6362
7299
  /** How many in-place resume attempts have FAILED in a row for this session
6363
7300
  * (§5 cap/backoff). Incremented on every failed `maybeResumeAgent` attempt —
6364
7301
  * the resume throws, the adapter rejects the id, or the spawn returns null —
@@ -6717,6 +7654,19 @@ interface SessionDescriptor {
6717
7654
  /** Cumulative input / output token counts (same source + cadence as costUsd). */
6718
7655
  tokensIn?: number;
6719
7656
  tokensOut?: number;
7657
+ /** Cumulative prompt-cache read / cache-creation / reasoning tokens, from
7658
+ * the adapter's usage reader (claude-code transcript, hermes state.db,
7659
+ * opencode.db). Absent when the adapter doesn't expose them — never
7660
+ * zero-filled. See `UsageDetail` in usage.ts. */
7661
+ cacheReadTokens?: number;
7662
+ cacheWriteTokens?: number;
7663
+ reasoningTokens?: number;
7664
+ /** Tool calls the daemon observed across completed turns (cumulative).
7665
+ * Stamped at the first turn end, so absent = no turn has finished yet. */
7666
+ toolCalls?: number;
7667
+ /** Wall time spent inside turns, cumulative ms. Same cadence as
7668
+ * `toolCalls`. */
7669
+ durationMs?: number;
6720
7670
  /** Context-window size + tokens-in-context from the latest `usage_update`
6721
7671
  * event (ACP adapters that report a context window). Refreshed live as
6722
7672
  * usage_update events arrive, not just at turn-end. */
@@ -6776,6 +7726,20 @@ interface SessionDescriptor {
6776
7726
  * — so this carries no secrets today. Should the shape ever grow
6777
7727
  * headers/tokens, NEVER log this field's contents. */
6778
7728
  mcpServers?: AcpMcpServer[];
7729
+ /** Resolved skills list at spawn time — explicit ∪ preset ∪ config
7730
+ * defaults (`resolveSpawnDefaults`'s own merge), recorded regardless of
7731
+ * whether the adapter actually CONSUMES a `skills` spawn option (today
7732
+ * only hermes does — see `normalizeSkillsOption`). Persisted so it
7733
+ * survives restart/resume like `mcpServers`, and so a read surface
7734
+ * (`session_capabilities`) can show what was resolved even for an
7735
+ * adapter (claude-code) that auto-discovers skills on its own and never
7736
+ * reads this list back. */
7737
+ skills?: string[];
7738
+ /** Capability bundle ids (`bundle_list`) resolved for this spawn (PLAN D
7739
+ * phase 1) — ids only, never the expanded imported-MCP snapshots (those
7740
+ * live only in `mcpServers` above, as reference-only proxy URLs). Absent
7741
+ * ⇒ no bundle was attached. */
7742
+ bundles?: string[];
6779
7743
  /** Provider-specific resume hints sniffed from the session's
6780
7744
  * output. claude-code prints `claude --resume <uuid>` on exit;
6781
7745
  * we capture that uuid as `claudeResumeId`. On `restart`, when a
@@ -6878,6 +7842,12 @@ interface SessionDescriptor {
6878
7842
  * want. Threaded like `permissionHold` — set at spawn time, immutable
6879
7843
  * thereafter. */
6880
7844
  notifyParentOnCrash?: boolean;
7845
+ /** Per-spawn opt-out of sentinel auto-link (AIP-60 §6/step 4,
7846
+ * `agent_start.sentinel: false`). `undefined`/`true` = allowed (subject to
7847
+ * `config.sentinel.autoWatchPrs`); `false` = this session's opened PRs are
7848
+ * never auto-watched, no matter the config. Set once at spawn, immutable
7849
+ * thereafter — same threading as `notifyParentOnCrash`. */
7850
+ sentinelAutoWatch?: boolean;
6881
7851
  /** True when the session was spawned in permission-hold mode. */
6882
7852
  permissionHold?: boolean;
6883
7853
  /** Effective OS-level confinement of the adapter's own process
@@ -6912,9 +7882,17 @@ interface SessionDescriptor {
6912
7882
  * time (spawn / resume) — shown in `session_list` so a sender can predict
6913
7883
  * a message's delivery tier. `steering`: the agent accepts ACP steering
6914
7884
  * (`_session/steering`) — a `steer` message can be injected into its
6915
- * running turn instead of waiting for it to end. */
7885
+ * running turn instead of waiting for it to end. `commandsSupported`:
7886
+ * the live agent session is the ACP protocol arm (has a working `steer`
7887
+ * method — only the ACP arm implements it, per `AgentSessionLike`'s own
7888
+ * doc), which is what actually emits `available_commands_update`
7889
+ * notifications; false for the print/proprietary arms regardless of
7890
+ * `steeringSupported`, since a harness can be ACP without advertising
7891
+ * the steering extension. Read by `session_capabilities` to derive its
7892
+ * `arm`/`commandsSupported` fields without re-deriving the signal. */
6916
7893
  capabilities?: {
6917
7894
  steering: boolean;
7895
+ commandsSupported: boolean;
6918
7896
  };
6919
7897
  /** FIFO of prompts that arrived while this session was mid-turn and
6920
7898
  * asked to be QUEUED rather than rejected (`enqueuePrompt`'s
@@ -7368,6 +8346,28 @@ type PermissionRespondResult = {
7368
8346
  error: "not_found" | "session_gone" | "no_matching_option" | "unsupported";
7369
8347
  message: string;
7370
8348
  };
8349
+ /** Input to `SessionsRegistry.closeWithOutcome`. `verdict: "done" |
8350
+ * "abandoned"` records the Level 2 fields onto the session's
8351
+ * `SessionOutcome` and actually closes it; `"blocked" | "needs-input"`
8352
+ * records the SAME `note`/`judgedBy` onto `SessionDescriptor.wrapupFlag`
8353
+ * instead and never touches the session's liveness. See {@link
8354
+ * SessionOutcome} / {@link SessionDescriptor.wrapupFlag} for the field
8355
+ * meanings. */
8356
+ interface CloseWithOutcomeInput {
8357
+ verdict: "done" | "abandoned" | "blocked" | "needs-input";
8358
+ /** Overrides the outcome's derived summary when given (e.g. a judge's own
8359
+ * written summary) — trimmed the same way `deriveSessionOutcome` trims
8360
+ * `lastAssistantText`. Omitted keeps whatever `deriveSessionOutcome`
8361
+ * computed from the session's own last assistant message. Ignored for a
8362
+ * `"blocked"`/`"needs-input"` verdict (there's no outcome to summarize —
8363
+ * nothing closed). */
8364
+ summary?: string;
8365
+ note?: string;
8366
+ /** A judge session id, or `"steward-rules"` for a deterministic close with
8367
+ * no judge in the loop. */
8368
+ judgedBy?: string;
8369
+ source: "judged" | "declared";
8370
+ }
7371
8371
  interface SessionsRegistry {
7372
8372
  spawn(input: SpawnSessionInput): SessionDescriptor;
7373
8373
  /** Adopt a ChildProcess that was spawned outside the registry —
@@ -7636,6 +8636,21 @@ interface SessionsRegistry {
7636
8636
  reason?: string;
7637
8637
  interrupted?: boolean;
7638
8638
  }>;
8639
+ /** A currently-RUNNING background task's info plus a tail of its output
8640
+ * file, for the UI's "click a live background-task row to peek at its
8641
+ * output" affordance. Scoped to `SessionDescriptor.backgroundTasks` on
8642
+ * purpose — a settled task is dropped from that list moments after it
8643
+ * reports terminal status (see `noteBackgroundTask`), so this never
8644
+ * becomes an arbitrary-file-read: the caller supplies a `taskId`, never a
8645
+ * path, and only a path the agent itself already reported for a task
8646
+ * still tracked as running is ever read. Returns `null` when the session
8647
+ * or the task is unknown (the caller surfaces 404); `tail` is `null` when
8648
+ * the task has no `outputFile` or the file can't be read (not yet
8649
+ * created, already rotated away, ...). */
8650
+ readBackgroundTaskTail(id: string, taskId: string): {
8651
+ task: SessionBackgroundTask;
8652
+ tail: string | null;
8653
+ } | null;
7639
8654
  /** Eagerly resume ONE dead-but-resumable agent-cli session IN PLACE,
7640
8655
  * WITHOUT a prompt — the boot-time counterpart to the lazy resume that
7641
8656
  * `sendPrompt`/`enqueuePrompt` trigger on the first prompt after a restart
@@ -7658,6 +8673,17 @@ interface SessionsRegistry {
7658
8673
  * touching the adapter, so the bounded boot pass can call it per row and
7659
8674
  * tally the results. */
7660
8675
  resumeOnBoot(id: string): Promise<EagerResumeOutcome>;
8676
+ /** Opaque id unique to this registry instance — one per daemon boot.
8677
+ * Stamped as `interruptedAtBoot` on rows the boot reload finds interrupted,
8678
+ * so `interruptedAtBoot === bootId` means "interrupted by the restart that
8679
+ * preceded THIS boot" (continue-interrupted). */
8680
+ readonly bootId: string;
8681
+ /** Book one automatic continue prompt against `id` (increment
8682
+ * `autoContinueAttempts`, stamp `lastAutoContinueBoot = bootId`) and
8683
+ * persist. Called by the continue-on-boot pass BEFORE it sends, so the
8684
+ * no-loop bound holds even when the continue turn is itself cut off by the
8685
+ * next restart. No-op for an unknown id. */
8686
+ recordAutoContinue(id: string): void;
7661
8687
  /** Cancel the in-flight turn on a live agent-cli session and leave the
7662
8688
  * session itself alive and idle — the bare "interrupt, no next prompt"
7663
8689
  * primitive `sendPrompt`/`enqueuePrompt`'s `opts.interrupt` arm lacks
@@ -7850,6 +8876,29 @@ interface SessionsRegistry {
7850
8876
  * touches `keepAlive`, reaper eligibility, or any notification path —
7851
8877
  * pin is quiet, structural state only. Throws when the id is unknown. */
7852
8878
  setPinned(id: string, pinned: boolean): SessionDescriptor;
8879
+ /** Materialize a new artifact (or version of one) into the session's
8880
+ * durable artifact store (`session_artifact_add` MCP verb / `POST
8881
+ * /sessions/:id/artifacts`) — see `session-artifacts.ts`. Emits
8882
+ * `session:artifact-added` on a real (non-deduped) write. Throws when
8883
+ * the id is unknown. */
8884
+ addSessionArtifact(id: string, input: AddSessionArtifactInput): ArtifactRecord;
8885
+ /** Current-state fold of the session's artifact store — every key's
8886
+ * version history, most-recently-updated first. Empty array for a
8887
+ * known session with no artifacts yet. Throws when the id is unknown. */
8888
+ listSessionArtifacts(id: string): ArtifactRecord[];
8889
+ /** One artifact version's metadata plus its bounded file content (single-
8890
+ * file kinds only — a "site" version's content is browsed via the raw-
8891
+ * serve HTTP route instead). Undefined when the key/version doesn't
8892
+ * exist. Throws when the session id is unknown. */
8893
+ getSessionArtifact(id: string, key: string, opts?: {
8894
+ version?: number;
8895
+ maxBytes?: number;
8896
+ }): GetSessionArtifactResult | undefined;
8897
+ /** Set or clear an artifact's pin (`session_artifact_pin` MCP verb /
8898
+ * `POST /sessions/:id/artifacts/:key/pin`). Emits
8899
+ * `session:artifact-pinned-changed`. Undefined when the key doesn't
8900
+ * exist. Throws when the session id is unknown. */
8901
+ setArtifactPinned(id: string, key: string, pinned: boolean): ArtifactRecord | undefined;
7853
8902
  /**
7854
8903
  * Manually override `awaitingInput`/`awaitingQuestion` (the
7855
8904
  * `session_flag_status` MCP verb) — the ONE write path for this pair
@@ -7955,6 +9004,34 @@ interface SessionsRegistry {
7955
9004
  * reason (including `"operator-stopped"` or none) on a terminal row
7956
9005
  * stays the plain no-op. */
7957
9006
  kill(id: string, signal?: NodeJS.Signals, reason?: SessionEndReason): boolean;
9007
+ /** Close a LIVE agent-cli session with a Level 2 (judged/declared) outcome
9008
+ * — the primitive the session steward (FIX-9A/9B) drives once a wrap-up
9009
+ * plan (`planSessionWrapup`, `session-wrapup.ts`) puts a session in the
9010
+ * `close`/`stuck` class, or a judge agent reaches a verdict on a `judge`
9011
+ * one. Verdict `"done"` or `"abandoned"` actually closes: terminates the
9012
+ * same graceful way `kill()` does, tags `endedReason: "steward-completed"`
9013
+ * (`"done"`) or `"steward-abandoned"` (`"abandoned"`), and — like
9014
+ * `reapIdle`, unlike a plain `kill()` — CLEARS the in-memory
9015
+ * `agentSession` binding so the row stays lazy-resumable in place. The
9016
+ * recorded outcome's `source`/`verdict`/`judgedBy`/`note` are stamped onto
9017
+ * the row's `SessionOutcome` (see `session-outcome.ts`) on top of what
9018
+ * `deriveSessionOutcome` would otherwise compute (last assistant message,
9019
+ * PRs, cost) — a declared close still shows what the session actually
9020
+ * produced. Verdict `"blocked"` or `"needs-input"` is NOT a completion —
9021
+ * nothing is terminated; the verdict is recorded as
9022
+ * `SessionDescriptor.wrapupFlag` instead, and the session is left exactly
9023
+ * as alive as it was.
9024
+ *
9025
+ * Refuses (returns false, no-op) a session that is not a live
9026
+ * (`running`/`starting`) agent-cli row, or that AT THE MOMENT OF THE CALL
9027
+ * is `busy`/`awaitingInput`/`awaitingPermission` or has a background task
9028
+ * outstanding (`pendingBgTasks`/`backgroundTasks`) — a plan computed
9029
+ * moments earlier can be stale; this is the re-check that keeps an
9030
+ * autonomous close from ever landing on a session that just picked up a
9031
+ * turn, asked a question, or still has something in flight. Applies to
9032
+ * BOTH branches (close and flag-only) — a stale plan is refused either
9033
+ * way, never silently acted on. */
9034
+ closeWithOutcome(id: string, input: CloseWithOutcomeInput): boolean;
7958
9035
  /** Retire a long-idle agent-cli session to free its adapter process — the
7959
9036
  * primitive the idle-session reaper (`runIdleReapPass`, PR-6) drives on a
7960
9037
  * periodic sweep. Terminates the underlying adapter/child the SAME graceful
@@ -8062,6 +9139,18 @@ interface SessionsRegistry {
8062
9139
  * window keeps aging out naturally rather than being wiped by the
8063
9140
  * give-up itself. Returns false (no-op) for an unknown id. */
8064
9141
  giveUpRestart(id: string, message: string): boolean;
9142
+ /** Record a daemon-authored DISPLAY-ONLY notice into an agent-cli
9143
+ * session's transcript — the same `kind: "notice"` event `markCrashed`/
9144
+ * `giveUpRestart`/`interruptInFlightTurn` already stamp on their own
9145
+ * teardown/interrupt paths (see transcript-writer.ts's "notice" case),
9146
+ * exposed generically for a caller with just a session id, no `rt`
9147
+ * in hand. Appends to `events.jsonl` (so `session_story`/`live_session`
9148
+ * render it as a system message) and the ring buffer — nothing else:
9149
+ * it does NOT touch `promptQueue`, the inbox, or `busy`, so it can never
9150
+ * enqueue a prompt, wake an idle session into a turn, or interrupt a busy
9151
+ * one. Returns false (no-op) for an unknown id or a non-`agent-cli`
9152
+ * session. */
9153
+ recordNotice(id: string, text: string): boolean;
8065
9154
  /** List permission requests currently parked in the pending-permissions
8066
9155
  * inbox across all permission-hold sessions, newest last. Optionally
8067
9156
  * filtered to one session. */
@@ -8167,8 +9256,10 @@ interface SpawnAgentInput {
8167
9256
  /** Optional initial prompt to dispatch immediately. The promise
8168
9257
  * the registry returns resolves AFTER the spawn — the prompt
8169
9258
  * runs in the background, projecting events into the ring
8170
- * buffer. Skip to spawn idle. */
8171
- initialPrompt?: string;
9259
+ * buffer. Skip to spawn idle. A plain string, or a content-block
9260
+ * prompt (attachment-bearing first message) — see
9261
+ * `SpawnAgentSessionInput.prompt`'s doc. */
9262
+ initialPrompt?: string | Record<string, unknown> | unknown[];
8172
9263
  /** The daemon-composed SYSTEM slice of {@link initialPrompt} — the part
8173
9264
  * it synthesized ahead of the CALLER's ask (role disposition, lineage
8174
9265
  * line, AGENTS.md, posture preamble). The adapter still receives the
@@ -8190,6 +9281,13 @@ interface SpawnAgentInput {
8190
9281
  * the resume/re-spawn path can re-mount the same toolset (orchestrator
8191
9282
  * WP1). */
8192
9283
  mcpServers?: AcpMcpServer[];
9284
+ /** Resolved skills list at spawn time (`resolveSpawnDefaults`'s merged
9285
+ * explicit ∪ preset ∪ config-defaults result) — recorded verbatim onto
9286
+ * {@link SessionDescriptor.skills}. See that field's doc. */
9287
+ skills?: string[];
9288
+ /** Capability bundle ids resolved for this spawn — recorded verbatim onto
9289
+ * {@link SessionDescriptor.bundles}; see that field's doc. */
9290
+ bundles?: string[];
8193
9291
  /** Persistent isolated-config dir this spawn passed to
8194
9292
  * `startSession({ configDir })` — recorded verbatim onto
8195
9293
  * {@link SessionDescriptor.adapterConfigDir} so restart/lazy-resume can
@@ -8260,12 +9358,7 @@ interface SpawnAgentInput {
8260
9358
  * cost/token fields on the descriptor. Adapter-specific (e.g. hermes
8261
9359
  * reads its state.db keyed by the adapter session id). Omit for adapters
8262
9360
  * with no usage source. */
8263
- readUsage?: () => Promise<{
8264
- model?: string;
8265
- costUsd?: number;
8266
- tokensIn?: number;
8267
- tokensOut?: number;
8268
- } | null>;
9361
+ readUsage?: () => Promise<UsageReadResult | null>;
8269
9362
  /** Opt this session into Langfuse tracing (prompt/completion + tool spans +
8270
9363
  * tokens/cost). Effective opt-in is `trace ?? opts.langfuseTracingDefault ?? false`. */
8271
9364
  trace?: boolean;
@@ -8298,6 +9391,9 @@ interface SpawnAgentInput {
8298
9391
  /** Recorded verbatim onto {@link SessionDescriptor.notifyParentOnCrash} —
8299
9392
  * see that field's doc. Default false. */
8300
9393
  notifyParentOnCrash?: boolean;
9394
+ /** Recorded verbatim onto {@link SessionDescriptor.sentinelAutoWatch} — see
9395
+ * that field's doc. `undefined` = allowed. */
9396
+ sentinelAutoWatch?: boolean;
8301
9397
  /** Exempt this session from the idle-reaper (`isReapable` in
8302
9398
  * idle-reaper.ts) regardless of how long it sits idle — stamped straight
8303
9399
  * onto `SessionDescriptor.keepAlive`. Default false. */
@@ -8322,15 +9418,12 @@ type PendingAgentOutcome = {
8322
9418
  commandPreview?: string;
8323
9419
  resumable?: boolean;
8324
9420
  nativeTerminalResume?: boolean;
8325
- readUsage?: () => Promise<{
8326
- model?: string;
8327
- costUsd?: number;
8328
- tokensIn?: number;
8329
- tokensOut?: number;
8330
- } | null>;
9421
+ readUsage?: () => Promise<UsageReadResult | null>;
8331
9422
  /** Dispatched now that the tree + driver session both exist — never
8332
- * passed to `spawnAgentPending`, which would race the tree. */
8333
- initialPrompt?: string;
9423
+ * passed to `spawnAgentPending`, which would race the tree. A plain
9424
+ * string, or a content-block prompt (attachment-bearing first
9425
+ * message) — see `SpawnAgentSessionInput.prompt`'s doc. */
9426
+ initialPrompt?: string | Record<string, unknown> | unknown[];
8334
9427
  /** The daemon-composed SYSTEM slice of `initialPrompt` (see
8335
9428
  * `SpawnAgentInput.initialPromptSystem`) — threaded through the
8336
9429
  * placeholder so the deferred dispatch records it as a system turn. */
@@ -8606,6 +9699,170 @@ declare function runEagerResumePass(opts: {
8606
9699
  isServed?: (desc: SessionDescriptor) => boolean;
8607
9700
  }): Promise<EagerResumeSummary>;
8608
9701
 
9702
+ /**
9703
+ * Continue-interrupted: pick back up the sessions a daemon restart cut off
9704
+ * mid-turn, by sending each an explicit "continue" prompt.
9705
+ *
9706
+ * Everything below the prompt already exists — the persisted registry is the
9707
+ * snapshot, `maybeResumeAgent` resumes a dead row in place (lazily on its first
9708
+ * prompt, or eagerly at boot via `eager-resume.ts`), and the row keeps the
9709
+ * derived `interrupted` marker until its next successful turn-end. What a
9710
+ * restart does NOT do, by design, is re-run the interrupted prompt (prompts
9711
+ * aren't idempotent). This module is the explicit, opt-in replacement for that:
9712
+ * a NEW prompt that tells the agent its last turn was cut off and to check the
9713
+ * state on disk before carrying on.
9714
+ *
9715
+ * Two callers share one eligibility rule + one sender:
9716
+ * - the manual verb (`session_continue_interrupted` MCP tool,
9717
+ * `POST /sessions/continue-interrupted`, `agentproto sessions
9718
+ * continue-interrupted`) — `mode: "manual"`, dry-run by default;
9719
+ * - the opt-in boot pass (`daemon.continueInterruptedOnBoot`) —
9720
+ * `mode: "boot"`, run by serve.ts right after the eager resume pass.
9721
+ *
9722
+ * Eligibility (both modes): an agent-cli row that is `interrupted` BY THE LAST
9723
+ * RESTART (`interruptedAtBoot === registry.bootId` — a stale interruption from
9724
+ * an older restart that nobody picked up is left alone), resumable under the
9725
+ * attempt cap (`canResume`), not already busy, and whose `cwd` still exists
9726
+ * (a removed worktree fails the adapter respawn every time — knowable up front,
9727
+ * so the dry run must not call such a row eligible). The prompt goes through
9728
+ * `enqueuePrompt`, the same entry MCP `agent_prompt` uses, so a
9729
+ * dead-but-resumable row lazy-resumes with its billing auth re-resolved exactly
9730
+ * as a human prompt would. When that resume fails, the outcome says so instead
9731
+ * of passing on the bare post-resume "not alive" admission error.
9732
+ *
9733
+ * No-loop rules (boot mode only — a human pressing the button is intent):
9734
+ * - never auto-continue a row whose resume has failed (`resumeAttempts > 0`:
9735
+ * the eager pass, or an earlier attempt, could not bring it back);
9736
+ * - at most one auto-continue per restart (`lastAutoContinueBoot`);
9737
+ * - at most `MAX_AUTO_CONTINUE_ATTEMPTS` auto-continues without a successful
9738
+ * turn-end in between (`autoContinueAttempts`, booked BEFORE sending), so a
9739
+ * continue turn that keeps getting cut off — or keeps taking the daemon
9740
+ * down with it — stops being retried.
9741
+ *
9742
+ * Ordering (boot mode): MUST run after the supervisor is re-armed, same
9743
+ * constraint as `eager-resume.ts` — the continue turn's `turn-end` is exactly
9744
+ * what a re-armed completion policy is waiting for — and after the eager pass,
9745
+ * so a row whose eager resume failed is recognisable (`resumeAttempts`) and
9746
+ * skipped rather than lazily retried.
9747
+ */
9748
+
9749
+ /** The prompt sent when the caller doesn't supply one. */
9750
+ declare const DEFAULT_CONTINUE_PROMPT: string;
9751
+ /** Transcript provenance for the continue prompt (`opts.source`). */
9752
+ declare const CONTINUE_PROMPT_SOURCE = "daemon:continue-interrupted";
9753
+ /** Cap on automatic continue prompts sent to one row without a successful
9754
+ * turn-end in between (boot mode). 2 = the original interruption gets one
9755
+ * auto-continue, and if a second restart cuts THAT turn off it gets one more;
9756
+ * after that only a human (manual verb) re-sends. */
9757
+ declare const MAX_AUTO_CONTINUE_ATTEMPTS = 2;
9758
+ type ContinueInterruptedSkipReason =
9759
+ /** Unknown id (explicit `ids` filter only). */
9760
+ "unknown"
9761
+ /** Not interrupted at all — no turn was cut off. */
9762
+ | "not-interrupted"
9763
+ /** Interrupted by an EARLIER restart, not the last one. */
9764
+ | "stale-interrupt"
9765
+ /** Not in-place resumable (not agent-cli / missing adapter session / archived). */
9766
+ | "not-resumable"
9767
+ /** Already burned through `MAX_RESUME_ATTEMPTS` failed resumes. */
9768
+ | "resume-cap-exhausted"
9769
+ /** Mid-turn already — someone got there first. */
9770
+ | "busy"
9771
+ /** The row's `cwd` is gone (removed worktree) — the resume would fail. */
9772
+ | "cwd-missing"
9773
+ /** Boot mode: a resume attempt has failed on this row. */
9774
+ | "resume-failed"
9775
+ /** Boot mode: already auto-continued by this boot. */
9776
+ | "already-auto-continued"
9777
+ /** Boot mode: `MAX_AUTO_CONTINUE_ATTEMPTS` reached without recovering. */
9778
+ | "auto-continue-cap"
9779
+ /** Excluded by the cross-process `isServed` gate (another daemon's row). */
9780
+ | "not-served";
9781
+ type ContinueInterruptedOutcome = {
9782
+ id: string;
9783
+ name?: string;
9784
+ status: "eligible";
9785
+ } | {
9786
+ id: string;
9787
+ name?: string;
9788
+ status: "sent";
9789
+ } | {
9790
+ id: string;
9791
+ name?: string;
9792
+ status: "skipped";
9793
+ reason: ContinueInterruptedSkipReason;
9794
+ } | {
9795
+ id: string;
9796
+ name?: string;
9797
+ status: "failed";
9798
+ error: string;
9799
+ };
9800
+ interface ContinueInterruptedResult {
9801
+ dryRun: boolean;
9802
+ /** The prompt that was (or, on a dry run, would be) sent. */
9803
+ prompt: string;
9804
+ /** The boot the candidates were interrupted before (`registry.bootId`). */
9805
+ bootId: string;
9806
+ /** One entry per considered row — every interrupted row when `ids` is
9807
+ * omitted, every requested id otherwise. Eligible-first ordering matches
9808
+ * the send order (newest activity first). */
9809
+ sessions: ContinueInterruptedOutcome[];
9810
+ /** Counts by outcome status. */
9811
+ eligible: number;
9812
+ sent: number;
9813
+ skipped: number;
9814
+ failed: number;
9815
+ }
9816
+ type ContinueInterruptedMode = "manual" | "boot";
9817
+ /** Decide one row. `undefined` ⇒ eligible. Pure — reads the descriptor only. */
9818
+ declare function continueSkipReason(desc: SessionDescriptor, bootId: string, mode: ContinueInterruptedMode): ContinueInterruptedSkipReason | undefined;
9819
+ /**
9820
+ * List — and unless `dryRun` — continue the sessions interrupted by the last
9821
+ * restart. See the module docblock for eligibility, the no-loop rules, and the
9822
+ * ordering contract. No-throw per row: a send that fails admission (row went
9823
+ * busy, resume refused) is reported as `failed` and the pass carries on.
9824
+ */
9825
+ declare function continueInterruptedSessions(opts: {
9826
+ registry: Pick<SessionsRegistry, "list" | "get" | "enqueuePrompt" | "bootId" | "recordAutoContinue">;
9827
+ mode: ContinueInterruptedMode;
9828
+ /** Default true: report what would be sent, send nothing. */
9829
+ dryRun?: boolean;
9830
+ /** Restrict to these session ids. Omitted ⇒ every interrupted row. */
9831
+ ids?: readonly string[];
9832
+ /** Custom prompt. Omitted/blank ⇒ `DEFAULT_CONTINUE_PROMPT`. */
9833
+ prompt?: string;
9834
+ /** Max sends in flight at once (each may lazily resume an adapter). Clamped
9835
+ * to ≥1. Default 4, same as the eager pass. */
9836
+ concurrency?: number;
9837
+ /** Cross-process gate — see `runEagerResumePass`. Omitted ⇒ all served. */
9838
+ isServed?: (desc: SessionDescriptor) => boolean;
9839
+ /** Extra visibility filter (a scoped orchestrator's subtree). Rows it
9840
+ * rejects are dropped silently — never reported, never sent. */
9841
+ visible?: (desc: SessionDescriptor) => boolean;
9842
+ /** Filesystem probe for the `cwd-missing` check. Default `existsSync`. */
9843
+ cwdExists?: (cwd: string) => boolean;
9844
+ }): Promise<ContinueInterruptedResult>;
9845
+ /** Tally of the boot-time continue pass, for the boot-banner line. */
9846
+ interface ContinueOnBootSummary {
9847
+ /** False when `daemon.continueInterruptedOnBoot` is off (nothing ran). */
9848
+ enabled: boolean;
9849
+ /** Rows passing eligibility at selection time — the banner's denominator. */
9850
+ eligible: number;
9851
+ sent: number;
9852
+ skipped: number;
9853
+ failed: number;
9854
+ }
9855
+ /**
9856
+ * The boot pass: `continueInterruptedSessions` in `mode: "boot"`, not a dry
9857
+ * run, reduced to the banner tally. Caller (the gateway handle, invoked from
9858
+ * serve.ts) is responsible for the ordering contract in the module docblock.
9859
+ */
9860
+ declare function runContinueOnBootPass(opts: {
9861
+ registry: Parameters<typeof continueInterruptedSessions>[0]["registry"];
9862
+ concurrency: number;
9863
+ isServed?: (desc: SessionDescriptor) => boolean;
9864
+ }): Promise<ContinueOnBootSummary>;
9865
+
8609
9866
  /**
8610
9867
  * Daemon-lane PR provenance STAMPER.
8611
9868
  *
@@ -8811,6 +10068,89 @@ interface InboundWatcher {
8811
10068
  shutdown(): void;
8812
10069
  }
8813
10070
 
10071
+ /**
10072
+ * `capabilities_inventory` — one read that answers "what MCP servers and
10073
+ * skills does this daemon actually know about, and which harnesses can
10074
+ * reach them?" Backs both the `capabilities_inventory` MCP tool
10075
+ * (session-tools.ts) and its HTTP twin (`GET /capabilities/inventory`,
10076
+ * http-server.ts) so the two surfaces can never drift.
10077
+ *
10078
+ * Read-only, never throws: a failure in one source (MCP discovery, the
10079
+ * proxy registry, an adapter package that fails to import) becomes an
10080
+ * `error` string on that block alone — the rest of the inventory still
10081
+ * returns. Redaction: nothing here ever carries command/args/env/headers/
10082
+ * url — only the identity/status fields a UI needs (same discipline as
10083
+ * `compactDiscoveredMcp` / `compactImportedMcpEntry`).
10084
+ *
10085
+ * The `skills` block's per-adapter `metadata.skills` / `options` read uses
10086
+ * the same dynamic-`import()` + duck-typed-export technique as
10087
+ * `role-registry.ts`'s adapter-carried role discovery — reused rather than
10088
+ * reinvented, and importantly NOT a `packages/cli` dependency: it walks the
10089
+ * exact adapter list `listAgentAdapters` already returned (so `byHarness`
10090
+ * is guaranteed the same installed set as `adapter_list`), importing each
10091
+ * adapter's own already-installed package by its `packageName`.
10092
+ */
10093
+
10094
+ interface CapabilitiesInventoryImportedMcp {
10095
+ id: string;
10096
+ alias?: string;
10097
+ name: string;
10098
+ type: string;
10099
+ source: string;
10100
+ status: "connected" | "idle" | "error" | "unknown";
10101
+ error?: string;
10102
+ /** Only present when `status === "connected"` — never connects just to count. */
10103
+ toolCount?: number;
10104
+ usedBySessions: string[];
10105
+ }
10106
+ interface CapabilitiesInventoryDiscoveredMcp {
10107
+ id: string;
10108
+ source: string;
10109
+ scope: string;
10110
+ name: string;
10111
+ type: string;
10112
+ imported: boolean;
10113
+ }
10114
+ interface CapabilitiesInventoryMcp {
10115
+ imported: CapabilitiesInventoryImportedMcp[];
10116
+ discovered: CapabilitiesInventoryDiscoveredMcp[];
10117
+ /** Which harnesses get the daemon's own `/mcp` (and so the imported pool)
10118
+ * mounted by default — derived from `shouldInjectDaemonSelfMount` +
10119
+ * each installed adapter's declared protocol, never a hardcoded list. */
10120
+ daemonMountByHarness: Record<string, "default" | "on-request" | "none">;
10121
+ error?: string;
10122
+ }
10123
+ interface SkillPackInventoryEntry {
10124
+ name: string;
10125
+ version?: string;
10126
+ path: string;
10127
+ skills: string[];
10128
+ }
10129
+ interface SkillsByHarnessEntry {
10130
+ adapter: string;
10131
+ target?: {
10132
+ format: string;
10133
+ dir: string;
10134
+ unit?: string;
10135
+ };
10136
+ installed: string[];
10137
+ native: string[];
10138
+ spawnOption: boolean;
10139
+ }
10140
+ interface CapabilitiesInventorySkills {
10141
+ packs: SkillPackInventoryEntry[];
10142
+ byHarness: SkillsByHarnessEntry[];
10143
+ defaults: {
10144
+ global: string[];
10145
+ byAdapter: Record<string, string[]>;
10146
+ };
10147
+ error?: string;
10148
+ }
10149
+ interface CapabilitiesInventory {
10150
+ mcp: CapabilitiesInventoryMcp;
10151
+ skills: CapabilitiesInventorySkills;
10152
+ }
10153
+
8814
10154
  /**
8815
10155
  * Dynamic MCP-app panels for installed `@agentproto/app-kit` apps that ship
8816
10156
  * a `ui` block (`defineApp({ ui: { html, title?, tools?, csp? } })`,
@@ -8820,8 +10160,16 @@ interface InboundWatcher {
8820
10160
  *
8821
10161
  * `mcpServerFactory` (index.ts) rebuilds a fresh McpServer per `/mcp`
8822
10162
  * request, so this module never throws — a bad app record must not take
8823
- * down every other request. Failures (a collided tool id, an unreadable
8824
- * `ui.path`) are skipped with a `console.warn`, not surfaced to the caller.
10163
+ * down every other request. A collided tool id is still skipped with a
10164
+ * `console.warn` (there's nowhere else for a second panel to live). Every
10165
+ * OTHER failure — a removed app dir, a missing/unreadable `ui.path`, a
10166
+ * failed or in-flight `ui.build` — still mounts the panel, serving a
10167
+ * readable HTML placeholder/error page in place of the real bundle
10168
+ * (app-ui-placeholder.ts) instead of skipping it outright: a `ui://`
10169
+ * resource that silently doesn't exist just shows as a JSON-RPC "resource
10170
+ * not found" in a host, which is what motivated this file's placeholder
10171
+ * page in the first place — a HOST-VISIBLE page, even a stand-in one, beats
10172
+ * a resource the host has to explain away as missing.
8825
10173
  */
8826
10174
 
8827
10175
  /**
@@ -9064,6 +10412,9 @@ interface PrStatusSnapshot {
9064
10412
  name: string;
9065
10413
  conclusion: string | null;
9066
10414
  }>;
10415
+ /** The PR's head commit at fetch time — absent from snapshots taken
10416
+ * before this field existed. */
10417
+ headSha?: string;
9067
10418
  }
9068
10419
  /** The mutable annotations sidecar of a ledger entry. */
9069
10420
  interface LedgerAnnotations {
@@ -9076,6 +10427,13 @@ interface ReviewLedgerFilter {
9076
10427
  rangeSha?: string;
9077
10428
  binding?: string;
9078
10429
  manifestSha?: string;
10430
+ /** Keep only entries whose `attestation.requester.sessionId` is one of
10431
+ * these — backs `review_ledger({requesterSessionId, subtree})`: a single
10432
+ * id, or (with `subtree: true`) that session's whole subtree, resolved by
10433
+ * the caller before reaching the ledger. Answered from an in-memory
10434
+ * `requesterSessionId -> ledger keys` index (built lazily on first use,
10435
+ * kept current on every `put()`), never a full re-scan. */
10436
+ requesterSessionIds?: readonly string[];
9079
10437
  }
9080
10438
  interface ReviewLedger {
9081
10439
  /** Absolute root directory of the ledger. */
@@ -9258,12 +10616,27 @@ interface ReviewRun {
9258
10616
  /** Where the attestation was written in the ledger. */
9259
10617
  ledgerPath?: string;
9260
10618
  error?: string;
10619
+ /** The session that requested this run (`input.requesterSessionId ??
10620
+ * input.parentSessionId`), when known — mirrors
10621
+ * `Attestation.requester.sessionId` for a run that hasn't (or never will)
10622
+ * reach the ledger, so `review_ledger({includeRunning: true})` and the
10623
+ * `session_tree` badge (WP-review-panel) can attribute an in-flight/
10624
+ * cancelled/failed run to its requester without waiting for an
10625
+ * attestation. */
10626
+ requesterSessionId?: string;
9261
10627
  }
9262
10628
  interface ReviewRunner {
9263
10629
  start(input: ReviewRunInput): ReviewRun;
9264
10630
  status(runId: string): Promise<ReviewRun | undefined>;
9265
10631
  wait(runId: string): Promise<ReviewRun | undefined>;
9266
10632
  cancel(runId: string): boolean;
10633
+ /** Every run this daemon process has started, newest first — running runs
10634
+ * plus ones that settled (done/failed/cancelled) without a daemon
10635
+ * restart in between. Runs live in memory only for the process lifetime
10636
+ * (a restart loses this list, same as the rest of `runs` — the ATTESTATION
10637
+ * is what persists, in the ledger). Backs `review_ledger({includeRunning:
10638
+ * true})` and the `session_tree` review badge. */
10639
+ list(): ReviewRun[];
9267
10640
  readonly ledger: ReviewLedger;
9268
10641
  }
9269
10642
  interface CreateReviewRunnerOptions {
@@ -9273,6 +10646,14 @@ interface CreateReviewRunnerOptions {
9273
10646
  reviewers?: ReviewerSessionHost;
9274
10647
  /** Attestor identity. Default: `agentproto-runtime@<hostname>`. */
9275
10648
  daemonId?: string;
10649
+ /** Called once a run with a `requesterSessionId` reaches `done` — never
10650
+ * for `failed`/`cancelled` (no verdict to report). Display-only: the
10651
+ * daemon writes `text` into the requester's transcript as a `notice`
10652
+ * (see `sessions.ts`'s `recordNotice`) so `session_story`/`live_session`
10653
+ * show it — this never enqueues a prompt, touches the inbox, or starts a
10654
+ * turn. Fire-and-forget from the runner's perspective; a throw here is
10655
+ * swallowed. */
10656
+ notifyRequester?: (sessionId: string, text: string) => void;
9276
10657
  }
9277
10658
  /** Placeholder values the daemon binds for every review. `{changed}` is a
9278
10659
  * shell-quoted turbo filter for the packages changed since the range base
@@ -9502,6 +10883,12 @@ interface RegisterReviewToolsOptions {
9502
10883
  /** `gh` runner for `review_pr` — injectable for tests. Default: the real
9503
10884
  * `gh` on the daemon's PATH. */
9504
10885
  gh?: GhRunner;
10886
+ /** Resolve a session id to its subtree (itself + every session it
10887
+ * transitively spawned) — backs `review_ledger({requesterSessionId,
10888
+ * subtree: true})`. Omitted ⇒ `subtree` is ignored (scoped to the exact
10889
+ * id only), so a caller without sessions-registry access still works,
10890
+ * just without the subtree expansion. */
10891
+ resolveSubtree?: (sessionId: string) => readonly string[];
9505
10892
  }
9506
10893
  declare function registerReviewTools(server: McpServer, opts: RegisterReviewToolsOptions): void;
9507
10894
 
@@ -10295,6 +11682,108 @@ declare function formatToolCall(toolName: string, args: unknown): string;
10295
11682
  */
10296
11683
  declare function formatToolResult(toolName: string | undefined, result: unknown, isError: boolean): string | null;
10297
11684
 
11685
+ /**
11686
+ * Pure sizing helper for a `tools/list` response — shared by
11687
+ * `scripts/measure-mcp-tools.mjs` (human-readable before/after report) and
11688
+ * the budget-guard test (`__tests__/tool-schema-budget.test.ts`) so both
11689
+ * measure the exact same thing: the JSON bytes an MCP client actually pays
11690
+ * for per tool (`description` + `inputSchema`), sorted biggest first.
11691
+ */
11692
+ interface MeasuredToolEntry {
11693
+ name: string;
11694
+ bytes: number;
11695
+ }
11696
+ interface ToolListMeasurement {
11697
+ entries: MeasuredToolEntry[];
11698
+ totalBytes: number;
11699
+ }
11700
+ interface MeasurableTool {
11701
+ name: string;
11702
+ description?: string;
11703
+ inputSchema?: unknown;
11704
+ }
11705
+ /** UTF-8 byte size of the `{description, inputSchema}` JSON a client
11706
+ * receives for one tool — the same two fields `tools/list` sends over the
11707
+ * wire, so this tracks prompt-token cost rather than just character count. */
11708
+ declare function measureTool(tool: MeasurableTool): number;
11709
+ declare function measureToolList(tools: readonly MeasurableTool[]): ToolListMeasurement;
11710
+
11711
+ /**
11712
+ * Long-form MCP tool documentation, read from `docs/mcp-tools/<name>.md`.
11713
+ *
11714
+ * The schema `description` on a tool/field is a contract: what it does, and
11715
+ * the one rule that prevents a wrong call — kept short so `tools/list` stays
11716
+ * cheap. Everything else (edge cases, interactions with other fields,
11717
+ * examples, history) lives here instead, fetched on demand by the `tool_help`
11718
+ * MCP tool (`tool-help-mcp.ts`) and the `agentproto help <tool>` CLI verb
11719
+ * (`packages/cli/src/commands/help.ts`) — both call the two functions below,
11720
+ * so the markdown file is the single source both surfaces read.
11721
+ *
11722
+ * `docs/mcp-tools/` sits at the PACKAGE root (a sibling of `src`/`dist`, not
11723
+ * the repo-level `docs/`) so it ships inside the published npm tarball —
11724
+ * see the `files` array in `package.json`. Resolved relative to this
11725
+ * module's own directory rather than by package name: whether this code is
11726
+ * running from `src` (vitest, no build) or `dist` (built/published), the
11727
+ * module's directory is always a direct child of the package root, so one
11728
+ * `..` reaches it either way — no `require.resolve` needed.
11729
+ */
11730
+ /** Full markdown help text for `name`, or `undefined` when no doc exists. */
11731
+ declare function getToolHelp(name: string): string | undefined;
11732
+ /** Every tool name with a help doc available — for a "did you mean" list. */
11733
+ declare function listToolHelpTopics(): string[];
11734
+ /** Extract a `## <topic>` section (case-insensitive) up to the next `##`
11735
+ * heading — shared by the `tool_help` MCP tool and the `agentproto help`
11736
+ * CLI verb so both slice a doc identically. `undefined` when no section
11737
+ * with that heading exists. */
11738
+ declare function extractHelpSection(doc: string, topic: string): string | undefined;
11739
+
11740
+ /**
11741
+ * Subtree usage rollup: one session's own usage plus the summed usage of
11742
+ * every session it (transitively) spawned — the shape `session_usage
11743
+ * { includeSubtree: true }` and `GET /sessions/:id/usage?includeSubtree=1`
11744
+ * return.
11745
+ *
11746
+ * Pure over a descriptor list (walked by `parentSessionId`, the same graph
11747
+ * `collectSubtree` uses for orchestrator scoping), so it's unit-testable
11748
+ * without a registry.
11749
+ *
11750
+ * Absent stays absent: a total is the sum of the sessions that REPORT the
11751
+ * field, and is omitted entirely when none of them do — a subtree of
11752
+ * cost-less sessions never reads as a measured $0.
11753
+ */
11754
+
11755
+ /** Summed usage across the root and all its descendants. */
11756
+ interface SubtreeUsageTotals {
11757
+ /** Sessions counted, root included. */
11758
+ sessions: number;
11759
+ costUsd?: number;
11760
+ tokensIn?: number;
11761
+ tokensOut?: number;
11762
+ cacheReadTokens?: number;
11763
+ cacheWriteTokens?: number;
11764
+ }
11765
+ /** One direct child of the root, with that child's OWN usage (its own
11766
+ * descendants roll up into `subtree`, not here). */
11767
+ interface SubtreeChildUsage {
11768
+ id: string;
11769
+ label?: string;
11770
+ status: SessionDescriptor["status"];
11771
+ costUsd?: number;
11772
+ tokensIn?: number;
11773
+ tokensOut?: number;
11774
+ }
11775
+ interface SessionUsageWithSubtree {
11776
+ self: SessionUsage;
11777
+ subtree: SubtreeUsageTotals;
11778
+ children: SubtreeChildUsage[];
11779
+ }
11780
+ /**
11781
+ * Roll `root`'s usage up over its subtree. `all` should be the FULL
11782
+ * descriptor list (archived included) so an archived intermediate session
11783
+ * doesn't sever the parent→child chain.
11784
+ */
11785
+ declare function rollupSessionSubtree(root: SessionDescriptor, all: readonly SessionDescriptor[]): SessionUsageWithSubtree;
11786
+
10298
11787
  /**
10299
11788
  * Dependency-injection surface for MCP credential resolution.
10300
11789
  *
@@ -10399,6 +11888,63 @@ declare function enrichRollupWithProviderQuota(rollup: UsageRollup, window: stri
10399
11888
  * such lines; skips any malformed or non-`usage_snapshot` line. Never throws. */
10400
11889
  declare function readUsageSnapshots(sessionId: string, baseDir?: string): Promise<UsageSnapshotRecord[]>;
10401
11890
 
11891
+ /**
11892
+ * MCP surface for the sentinel primitive (AIP-60 §6): `sentinel_watch`,
11893
+ * `sentinel_list`, `sentinel_unwatch`, `sentinel_poll_now`.
11894
+ *
11895
+ * `sentinel_watch`'s `sessionId` defaults to the calling session — same
11896
+ * "trusted `?callerSessionId=`" pattern `message-tools.ts` uses — so a
11897
+ * session watching its own PR doesn't need to know its own id.
11898
+ *
11899
+ * Provider auto-select isn't implemented yet (design §6): `provider`
11900
+ * defaults to `local-gh`, the only built-in that ships as of this step.
11901
+ *
11902
+ * `createSentinelWatch`/`cancelSentinelWatch`/`sentinelView` are exported so
11903
+ * `http-server.ts`'s `POST/GET/DELETE /sentinels` routes (the daemon-HTTP
11904
+ * surface `agentproto sentinel` talks to) share the exact same
11905
+ * validation/sugar-resolution logic as this MCP surface — same pattern as
11906
+ * `TunnelRegistry` backing both `tunnel_create` and `POST /tunnels`, except
11907
+ * here the shared logic is a plain function rather than a stateful registry
11908
+ * method, since sentinel creation also needs the provider handle.
11909
+ */
11910
+
11911
+ /** Compact wire view of a sentinel for tool/HTTP results — the internal-only
11912
+ * fields (`handle`, `seen`, `terminalSubjects`) never leave the daemon. */
11913
+ interface SentinelView {
11914
+ id: string;
11915
+ provider: string;
11916
+ status: SentinelStatus;
11917
+ match: SentinelMatchClause[];
11918
+ until: SentinelUntil;
11919
+ target: SentinelTarget;
11920
+ group?: string;
11921
+ label?: string;
11922
+ createdTs: number;
11923
+ eventCount: number;
11924
+ lastEventTs?: number;
11925
+ lastError?: string;
11926
+ }
11927
+
11928
+ /**
11929
+ * Smart default for `features.llmEndpoint` (daemon-managed-gateway): an
11930
+ * operator who already configured a named endpoint, an upstream credential
11931
+ * link, or an explicit `llm-endpoint` custom route is clearly USING the
11932
+ * proxy already — booting with the sidecar off (today's default) would
11933
+ * silently break their next `route:{gateway:"llm-endpoint"}` spawn on a
11934
+ * fresh install. An operator who never touched any of that gets the
11935
+ * unchanged, conservative default (off).
11936
+ *
11937
+ * An explicit `configured` value (`config.json`'s `features.llmEndpoint`,
11938
+ * profile-overlaid) always wins, in either direction — this resolver only
11939
+ * fills in the gap when the operator left the flag unset.
11940
+ */
11941
+ /**
11942
+ * Resolve the EFFECTIVE `features.llmEndpoint` flag. `configured` is the
11943
+ * operator's merged config value (`undefined` when never set in either
11944
+ * `config.json` or the active profile).
11945
+ */
11946
+ declare function resolveEffectiveLlmEndpointFlag(configured: boolean | undefined): Promise<boolean>;
11947
+
10402
11948
  /**
10403
11949
  * MCP tools that drive the pairing registry — the daemon's "let a client pair
10404
11950
  * with me over an untrusted rendezvous" surface (design: DESIGN §6).
@@ -10420,6 +11966,102 @@ interface RegisterPairingToolsOptions {
10420
11966
  }
10421
11967
  declare function registerPairingTools(server: McpServer, opts: RegisterPairingToolsOptions): void;
10422
11968
 
11969
+ /**
11970
+ * Device registry — a read view over `PairingRegistry` (+, when wired,
11971
+ * `HostRegistry`) that presents each pairing/host as a "device" (DEVICES-PLAN
11972
+ * PR-A, hosts added PR-C). A paired CLIENT (today's `pair_offer`/`pair
11973
+ * accept`) gets `role: "client"`; a registered HOST (`pair offer --host` +
11974
+ * `devices add`, host-registry.ts) gets `role: "host"`.
11975
+ *
11976
+ * This is deliberately a thin computed view, not a second store: renaming
11977
+ * and revoking a client device mutate the same `pairings.json`
11978
+ * `PairingRegistry` already owns; a host device mutates the same
11979
+ * `hosts.json` `HostRegistry` — so `pair_*`/`/pairings` and
11980
+ * `device_*`/`/devices` are two surfaces over the same two sources of truth.
11981
+ */
11982
+
11983
+ type DeviceRole = "client" | "host";
11984
+ type DeviceKind = "browser" | "cli" | "daemon";
11985
+ interface Device {
11986
+ fingerprint: string;
11987
+ name: string;
11988
+ role: DeviceRole;
11989
+ kind: DeviceKind;
11990
+ rendezvous: string;
11991
+ createdAt: string;
11992
+ lastSeen: string;
11993
+ /** A channel (offer or reconnect) is served for this device right now
11994
+ * (client) or a `forwardHttp` call is in flight for it right now (host).
11995
+ * For a host this is NOT a live heartbeat — see host-registry.ts's
11996
+ * "Online tracking" — it only reflects actual recent/current traffic. */
11997
+ online: boolean;
11998
+ /** A pair/v1 pairing: listed and revocable, but can't connect until
11999
+ * re-paired — see `PairingRecord.legacy`. */
12000
+ legacy?: true;
12001
+ /** Set when this device's pairing/host record was granted under a
12002
+ * HOST-scoped offer (`agentproto pair offer --host`) — see
12003
+ * offer-url.ts's "Offer scope". Surfaced from either side of a pairing so
12004
+ * a user can see, from EITHER daemon, which of their pairings/hosts grant
12005
+ * host control. */
12006
+ scope?: "host";
12007
+ /** Self-reported by a host at join time (SANDBOX-VISIBILITY-JOIN) — see
12008
+ * `HostRecord.provider`/`sandboxId`/`labels`. Absent for a client device. */
12009
+ provider?: string;
12010
+ sandboxId?: string;
12011
+ labels?: Record<string, string>;
12012
+ }
12013
+ interface DeviceRegistry {
12014
+ /** Every known device. Read-only. */
12015
+ list(): Promise<Device[]>;
12016
+ /** Rename a device (fingerprint or current name) to a new label. Tries the
12017
+ * pairing registry first, then the host registry. Returns false when
12018
+ * nothing matched. */
12019
+ rename(idOrName: string, newName: string): Promise<boolean>;
12020
+ /** Drop a device by fingerprint or name — same effect as `pair revoke` for
12021
+ * a client device, `devices revoke`-of-a-host for a host device. Tries
12022
+ * the pairing registry first, then the host registry. Returns false when
12023
+ * nothing matched. */
12024
+ revoke(idOrName: string): Promise<boolean>;
12025
+ /** Register a host from an offer URL (delegates to `HostRegistry.add`).
12026
+ * Rejects if no `HostRegistry` was wired into this device registry. */
12027
+ add(offerUrl: string, name?: string): Promise<{
12028
+ fingerprint: string;
12029
+ name: string;
12030
+ rendezvousUrl: string;
12031
+ }>;
12032
+ /** Forward one HTTP request to a HOST device on demand (delegates to
12033
+ * `HostRegistry.forwardHttp`) — the basis for `device_sessions`. Rejects
12034
+ * if no `HostRegistry` was wired, or if `idOrName` doesn't match a host. */
12035
+ forwardHttp(idOrName: string, req: ForwardHttpRequest): Promise<ForwardHttpResponse>;
12036
+ }
12037
+ declare function createDeviceRegistry(pairing: PairingRegistry, hosts?: HostRegistry): DeviceRegistry;
12038
+
12039
+ /**
12040
+ * MCP tools over the device registry (DEVICES-PLAN PR-A/PR-C) — `device_list`
12041
+ * / `device_rename` / `device_revoke` / `device_add`. A view on top of the
12042
+ * same registries `pair_offer`/`pair_list`/`pair_revoke` (pairing-tools.ts)
12043
+ * and host-registry.ts drive; those keep working unchanged. `device_revoke`
12044
+ * has the exact effect of `pair_revoke` for a client device.
12045
+ *
12046
+ * `device_add` (PR-C) registers a HOST from a `--host`-scoped offer URL —
12047
+ * see `HostRegistry.add`'s scope gate (host-registry.ts). It requires the
12048
+ * gateway to have wired a `HostRegistry` into the `DeviceRegistry`; without
12049
+ * one the call rejects with a clear message rather than a missing-tool error
12050
+ * (this repo always wires one when `pairingRegistry` is wired, so that's
12051
+ * only reachable via a deliberate host misconfiguration).
12052
+ *
12053
+ * Registered beside `pair_*` — same closure-rebind pattern; the registry
12054
+ * singleton lives on the gateway.
12055
+ */
12056
+
12057
+ interface RegisterDeviceToolsOptions {
12058
+ registry: DeviceRegistry;
12059
+ /** Optional (SANDBOX-VISIBILITY-JOIN) — when wired, registers
12060
+ * `join_token_create`/`join_token_list`/`join_token_revoke`. */
12061
+ joinTokens?: JoinTokenRegistry;
12062
+ }
12063
+ declare function registerDeviceTools(server: McpServer, opts: RegisterDeviceToolsOptions): void;
12064
+
10423
12065
  /**
10424
12066
  * Rate-limited failure logging for reconnect loops.
10425
12067
  *
@@ -11257,6 +12899,15 @@ interface SessionWaitResult {
11257
12899
  * reported one. `"error"` means the adapter reported a failed turn.
11258
12900
  * Same three-branch coverage as `empty`. */
11259
12901
  reason?: string;
12902
+ /** The matched turn-end's captured error text — `SessionTurnEndEvent.error`
12903
+ * on the ring-replay/bus branches, `SessionDescriptor.lastTurnErrorMessage`
12904
+ * on the sync fast-path (no event object to read there). Present only
12905
+ * when `reason` is `"error"` AND the adapter emitted an in-band `error`
12906
+ * stream event during the turn. Same three-branch coverage as `empty`/
12907
+ * `reason` — this is the field a waiter (a supervisor session, `sessions
12908
+ * wait`, a webhook) needed to actually learn WHY a turn errored instead
12909
+ * of just that it did. */
12910
+ error?: string;
11260
12911
  }
11261
12912
  /**
11262
12913
  * Block until one of the listed sessions fires a matching lifecycle event
@@ -11406,6 +13057,12 @@ interface CreateGatewayOptions {
11406
13057
  * to an `enabled: false` summary. Also surfaced in `daemon_health` /
11407
13058
  * `GET /health` so an operator can see the effective value. */
11408
13059
  resumeSessionsOnBoot?: boolean;
13060
+ /** Opt-in continue-on-boot. Mirrors the `daemon.continueInterruptedOnBoot`
13061
+ * config knob; when true, the `continueInterruptedOnBoot()` handle method
13062
+ * runs the boot continue pass (see `continue-interrupted.ts`); when
13063
+ * false/omitted it short-circuits to an `enabled: false` summary. Surfaced
13064
+ * in `daemon_health` / `GET /health`. */
13065
+ continueInterruptedOnBoot?: boolean;
11409
13066
  /** Idle agent-session reaper threshold in ms (PR-6). Mirrors the
11410
13067
  * `daemon.idleReapAfterMs` config knob; the CLI resolves it (env >
11411
13068
  * config > off) and passes it here. When positive, the gateway starts a
@@ -11659,11 +13316,38 @@ interface CreateGatewayOptions {
11659
13316
  * returns) so the injected `serve` can capture the finished gateway.
11660
13317
  */
11661
13318
  pairingRegistry?: PairingRegistry;
13319
+ /**
13320
+ * Optional HOST registry (see `createHostRegistry`, DEVICES-PLAN PR-C) —
13321
+ * the reverse-pairing "client" half of pair/v2, living daemon-side. When
13322
+ * wired alongside `pairingRegistry`, the gateway layers registered hosts
13323
+ * into `device_list`/`GET /devices`, and enables the `device_add` MCP tool
13324
+ * plus `POST /devices/add` + `POST /devices/:id/exec`. Unlike
13325
+ * `pairingRegistry`, this registry keeps no standing connections — every
13326
+ * `add`/`forwardHttp` dials fresh — so there is nothing for the gateway to
13327
+ * autoconnect or tear down on shutdown.
13328
+ */
13329
+ hostRegistry?: HostRegistry;
13330
+ /**
13331
+ * Optional JOIN TOKEN registry (see `createJoinTokenRegistry`,
13332
+ * SANDBOX-VISIBILITY-JOIN). When wired alongside `pairingRegistry` +
13333
+ * `hostRegistry`, the gateway mounts `POST/GET /devices/join-tokens` +
13334
+ * `DELETE /devices/join-tokens/:id`, the `join_token_create/list/revoke`
13335
+ * MCP tools, and `device_sessions`/`GET /devices/:id/sessions[/…/output]`.
13336
+ * Autoconnect is the caller's to start (after this returns), same as
13337
+ * `pairingRegistry`.
13338
+ */
13339
+ joinTokens?: JoinTokenRegistry;
11662
13340
  /** Enable the local LLM Endpoint proxy sidecar (route registration,
11663
13341
  * MCP tools, child-process lifecycle). Default false — the endpoint is
11664
13342
  * an opt-in feature; when off, the `llm-endpoint` custom route is not
11665
13343
  * registered and the `llm_endpoint_*` MCP tools are not exposed. */
11666
13344
  llmEndpoint?: boolean;
13345
+ /** Mirrors `config.features.deviceInferenceShare` (DEVICES-PLAN item 1).
13346
+ * Default false. Only meaningful alongside `llmEndpoint: true` — passed
13347
+ * straight through to `startHttpServer`'s `deviceInferenceShare`, which
13348
+ * gates `/device-inference/*` (see http-server.ts's `HOST_SCOPE_HEADER`
13349
+ * doc comment for the full scope-gate story). */
13350
+ deviceInferenceShare?: boolean;
11667
13351
  }
11668
13352
  /**
11669
13353
  * Default always-on set when `deferredTools` is enabled without an
@@ -11674,10 +13358,10 @@ interface CreateGatewayOptions {
11674
13358
  * `toolPolicy.delegation: "deny"`, so those two names are simply absent
11675
13359
  * from a deny-role's registry and cost nothing) but still needs to report
11676
13360
  * back to its parent (`message_parent`), coordinate over the task ledger
11677
- * (`task_*`), and observe its own session/turn state
11678
- * (`session_context_status` alongside the pre-existing `session_list` /
11679
- * `session_monitor` / `session_events_poll`). Everything else (fs_*,
11680
- * directory_*, command_*, remote_*, browser_*, terminal_*, mcp_*,
13361
+ * (`task_*`), and observe its own session/turn state and capabilities
13362
+ * (`session_context_status` / `session_capabilities` alongside the
13363
+ * pre-existing `session_list` / `session_monitor` / `session_events_poll`).
13364
+ * Everything else (fs_*, directory_*, command_*, remote_*, browser_*, terminal_*, mcp_*,
11681
13365
  * routine_*, workflow_*, policy_*, inbound_watcher_*, session_tree,
11682
13366
  * agent_export, adapter_list, every `app_ui_*` panel tool, ...) starts
11683
13367
  * deferred — reachable via `tool_search`, never removed from `tools/call`.
@@ -11700,6 +13384,15 @@ interface GatewayHandle {
11700
13384
  * `CreateGatewayOptions.pairingRegistry`. Undefined otherwise. Exposed so
11701
13385
  * the CLI can `startAutoconnect()` after boot and `shutdown()` it. */
11702
13386
  pairing?: PairingRegistry;
13387
+ /** HOST registry, when one was wired via `CreateGatewayOptions.hostRegistry`
13388
+ * (DEVICES-PLAN PR-C). Undefined otherwise. No autoconnect/shutdown to
13389
+ * call on it — see that option's doc comment. */
13390
+ hosts?: HostRegistry;
13391
+ /** JOIN TOKEN registry, when one was wired via
13392
+ * `CreateGatewayOptions.joinTokens` (SANDBOX-VISIBILITY-JOIN). Undefined
13393
+ * otherwise. Exposed so the CLI can `startAutoconnect()` after boot and
13394
+ * `shutdown()` it, same as `pairing`. */
13395
+ joinTokens?: JoinTokenRegistry;
11703
13396
  /** Per-boot bearer token required on mutating /sessions/* routes
11704
13397
  * + WS PTY upgrades. Exposed so an embedding host (e.g. the CLI
11705
13398
  * shell that hosts the gateway in-process) can pass it to child
@@ -11724,6 +13417,16 @@ interface GatewayHandle {
11724
13417
  resumeSessionsOnBoot(opts?: {
11725
13418
  isServed?: (desc: SessionDescriptor) => boolean;
11726
13419
  }): Promise<EagerResumeSummary>;
13420
+ /** Run the opt-in continue-on-boot pass and return its tally — a no-op
13421
+ * returning `{ enabled: false, ... }` when the `continueInterruptedOnBoot`
13422
+ * knob is off. serve.ts invokes this right AFTER `resumeSessionsOnBoot()`
13423
+ * (so a row whose eager resume failed is recognisable and skipped) — and
13424
+ * therefore, like it, after the supervisor was re-armed, so a re-armed
13425
+ * completion policy sees the continue turn's turn-end. Same `isServed`
13426
+ * cross-process gate. */
13427
+ continueInterruptedOnBoot(opts?: {
13428
+ isServed?: (desc: SessionDescriptor) => boolean;
13429
+ }): Promise<ContinueOnBootSummary>;
11727
13430
  stop(): Promise<void>;
11728
13431
  }
11729
13432
  /**
@@ -11740,4 +13443,4 @@ interface GatewayHandle {
11740
13443
  */
11741
13444
  declare function createGateway(opts: CreateGatewayOptions): Promise<GatewayHandle>;
11742
13445
 
11743
- 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, 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 CreateReviewRunnerOptions, type CreatedOffer, DEAD_SESSION_STATUSES, DEFAULT_ALWAYS_ON_TOOLS, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, type DaemonReviewerHostDeps, DeclaredAdapterOption, type DeclaredAdapterPreset, DefaultsAdapterAuthConfig, type EagerResumeFailReason, type EagerResumeOutcome, type EagerResumeSkipReason, type EagerResumeSummary, EffortLevel, type EnableInput, type EnableResult, GC_REAPABLE_STATES, type GateSpec, type GatewayHandle, type GhRunner, 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 LedgerAnnotations, type LedgerEntry, type LedgerHostMeta, type LocatedConversation, type LocatedConversationByPath, MAX_MESSAGE_DATA_BYTES, MCP_APP_BRIDGE_SCRIPT, MESSAGE_KINDS, MESSAGE_PREAMBLE, MESSAGE_TAG, MESSAGE_URGENCIES, type McpCredentialDeps, 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 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 SessionWaitEvent, type SessionWaitResult, type SessionsRegistry, type ShellGateSpec, type SpawnAgentInput, SpawnDefaultsConfig, 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, 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, conversationIndexPath, createActivityProjector, createDaemonReviewerHost, createFileStepCache, createGateway, 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, fetchPrStatus, filterActivities, findConversationRecord, findNativeMode, findPrForCommit, formatToolCall, formatToolResult, getDefaultHarnessPreset, getHarnessPreset, getMcpCredentialDeps, githubRepoOf, harnessPresetsPath, hostPlaceholders, injectMcpAppBridge, isAgentCliAuthConfigured, isClosedTaskStatus, isKnownSessionEndReason, isMessageAllowed, isProviderLimitError, isSafeBucketSlug, isTerminalActivityState, listBuckets, listHarnessPresets, listPresets, loadHarnessPresets, loadQuotaStore, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, makeSandboxCredsStore, makeSandboxResolver, matchesMessageFilter, messageFrom, migrateLegacySessionsFile, migrationMarkerPath, mintMessageId, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeInbound, normalizeModeId, normalizeRemote, normalizeWorktreeField, parseAnthropicRateLimitHeaders, parsePrUrl, parseTaskStatus, parseWindow, parseWorktreeIsolationMode, policyToActivities, policyWatchesSession, prHeadSha, prToActivities, projectSessionUsage, readConversationIndex, readDaemonRegistry, readProfileQuota, readRegisteredSlugs, readRuntimeMeta, readSandboxLedger, readSessionForBrain, readUsageSnapshots, reapGcEntry, reapOrphanedDescendants, reconcileSandboxLedger, recordProfileQuota, recordSandboxBoot, recordSandboxLiveness, recordSandboxOrigin, recordSandboxState, registerBrainTools, registerBuiltinRoutes, registerPairingTools, registerReviewTools, registerSandboxAttachTool, registerWebSearchTools, removeHarnessPreset, removeSandboxLedgerEntry, renderInboxDigest, renderSessionMessage, renderSessionMessages, repoSlug, resolveBucketSlug, resolveNativeLink, resolvePosture, resolveRelation, resolveReuseFromLedger, resolveReviewerPreset, rollupUsage, runCrashDetectPass, runEagerResumePass, runIdleReapPass, runShellLane, runStallWatchdogPass, sandboxLedgerPath, sanitizeLabel, setDefaultPreset, setMcpCredentialDeps, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, toWorktreeStatusView, turnToActivities, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, updateHarnessPreset, upsertSandboxLedger, verifyInboundSignature, webSearchTool, withPr, withPrStatus, workflowToActivities, writeDaemonRegistryEntry };
13446
+ 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 CapabilitiesInventory, type CapabilitiesInventoryMcp, type CapabilitiesInventorySkills, 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 CreateJoinTokenInput, type CreateOfferInput, type CreateReviewRunnerOptions, type CreatedJoinToken, 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, HOST_SCOPE_HEADER, type HarnessPreset, type HarnessPresetValidationDeps, HarnessPresetValidationError, type HarnessPresetsFile, type HermesNativeRef, type HostJoinMeta, 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, JOIN_TOKENS_VERSION, type JoinTokenRecord, type JoinTokenRegistry, type JoinTokenRegistryDeps, 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, type SentinelMatchClause, type SentinelSpec, type SentinelStatus, type SentinelTarget, type SentinelUntil, type SentinelView, 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 SkillPackInventoryEntry, type SkillsByHarnessEntry, 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, createJoinTokenRegistry, 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 };