@epoch-agent/server 0.1.0 → 0.2.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
@@ -1,5 +1,6 @@
1
- import { Lang, WireSkillImportFailure, WireSkillImportForm, WireCapabilitiesResponse, WireSkillBodyResponse, EpochUserContent, AgentEvent, WireEnvelope, CustomCommandDef, ExpandedCommand, ModelRef, SetSelectionResult, WireCommandExpansion, WireCustomCommand, WireTurnState, WireSessionFinish, ApprovalOutcome, QuestionAnswer, ProviderType, ModelSelectionOrigin, WireModelCaveat, WireModelRejection, PermissionLevel, WireModelSource, WireModelProbeStatus, ScheduleDefinition, ScheduleRun, ScheduleBackendKind, OperationType, ScheduleTrigger, WireScheduleIssue, ScheduleRunStatus, TrustLevel, WireSandboxReason, WireAuditOutcome, WireAuditCode, WireToolVerdict, WireToolGateAxis, WireSecurityResponse, WireRewindScope, WireRewindRequest, WireCheckpointSummary, WireRewindPreview, WireRewindResponse, ActiveAgentRole, WireSettingValue, WireSettingLayer, WireSettingWriteLayer, WireSettingFutile, WireSettingApply, WireSettingsResponse, EpochConfig, WireCompressionLine, WireProviderSummary, WireToolSummary, Diagnostic, WireReplayMessage, WireFileChange, WireAuthGuard, WireWorkspaceDiffResponse, BackgroundTaskInfo, WireTasksResponse, WireBackgroundTask, WireScheduleRow } from '@epoch-agent/protocol';
2
- import { SerializableApprovalRequest, SerializableQuestionRequest, LevelChangeResult } from '@epoch-agent/runtime';
1
+ import { Lang, WireSkillImportFailure, WireSkillImportForm, WireCapabilitiesResponse, WireSkillBodyResponse, EpochUserContent, AgentEvent, WireEnvelope, CustomCommandDef, ExpandedCommand, ModelRef, SetSelectionResult, WireCommandExpansion, WireCustomCommand, WireGoalPhase, WireGoalRefusal, WireTurnState, WireSessionFinish, ApprovalOutcome, QuestionAnswer, SessionSurfaceView, WireSessionReference, WireFileReference, ProviderType, ModelSelectionOrigin, WireModelCaveat, WireModelRejection, ContextBreakdown, PermissionLevel, WirePluginSourceType, WirePluginRefuseReason, WirePluginEntry, WireModelSource, WireModelProbeStatus, ScheduleDefinition, ScheduleRun, ScheduleBackendKind, OperationType, ScheduleTrigger, WireScheduleIssue, ScheduleRunStatus, TrustLevel, WireSandboxReason, WireAuditOutcome, WireAuditCode, WireCachedApproval, WireCachedDecision, WireToolVerdict, WireToolGateAxis, WireSecurityResponse, WireRewindScope, WireRewindRequest, WireCheckpointSummary, WireRewindPreview, WireRewindResponse, ActiveAgentRole, WireSettingValue, WireSettingLayer, WireSettingWriteLayer, WireSettingFutile, WireSettingApply, WireSettingsResponse, EpochConfig, WireCompressionLine, WireProviderSummary, WireToolSummary, Diagnostic, WireReplayMessage, WireFileChange, WireAuthGuard, WireWorkspaceDiffResponse, BackgroundTaskInfo, WireTasksResponse, WireBackgroundTask, WireScheduleRow } from '@epoch-agent/protocol';
2
+ import { SerializableApprovalRequest, SerializableQuestionRequest, ApprovalRevokeResult, LevelChangeResult, WorkspaceFilesView } from '@epoch-agent/runtime';
3
+ import { ServerResponse } from 'node:http';
3
4
 
4
5
  /**
5
6
  * 绑定策略 —— 「监听哪儿、给不给 token、拒不拒绝启动」。
@@ -396,6 +397,12 @@ interface RoleAddView {
396
397
  description: string;
397
398
  prompt?: string;
398
399
  tools?: readonly string[];
400
+ /**
401
+ * 技能索引白名单(2026-08-27)。⚠️ **这一层不校验它**,和 `tools` 刻意不同 ——
402
+ * 判据全文在 protocol 的 `WireRoleAddRequest.skills` 上(技能表会在会话中途变,
403
+ * 把「此刻没装」判成 400 是拒掉一个明天就成立的名字)。
404
+ */
405
+ skills?: readonly string[];
399
406
  maxTurns?: number;
400
407
  }
401
408
  /**
@@ -608,15 +615,32 @@ interface RoleView {
608
615
  name: string;
609
616
  description: string;
610
617
  tools?: readonly string[];
618
+ /**
619
+ * 技能索引白名单(`AgentRole.skills`,2026-08-27)。⚠️ **镜像上漏掉这一格不会
620
+ * 红** —— 真源上它是可选的,少声明照样满足这份镜像,表现只是 `toWireRole()`
621
+ * 一声不吭地读不到它(判据同下面 `McpView.source` 那条 ⚠️)。它是这一屏上
622
+ * 「一个身份自己声明了哪几条技能」的唯一来源。
623
+ */
624
+ skills?: readonly string[];
611
625
  maxTurns?: number;
612
626
  source: 'builtin' | 'user' | 'project' | 'plugin' | 'host';
613
627
  }
614
628
  /**
615
- * `RoleMergeNotice` 的镜像。**只取 `role` `cut`** —— `detail` 是中文文案,
616
- * 浏览器要用自己的 catalog 组句(同诊断的规矩),下发它等于让英文界面照抄中文。
629
+ * `RoleMergeNotice` 的镜像。**只取 `role` / `kind` / `cut`** —— `detail` 是中文
630
+ * 文案,浏览器要用自己的 catalog 组句(同诊断的规矩),下发它等于让英文界面
631
+ * 照抄中文。
632
+ *
633
+ * ⚠️ **`kind` 这一格是必须的,不是「顺手多带一样」**(2026-08-27)。加上它之前
634
+ * 这一族 notice 全是工具收窄,于是 `toWireRole()` 里一句 `find(n => n.role === …)`
635
+ * 就够了;技能收窄一旦也产 notice,那句 `find` 会**抓到技能那一条并把技能名画成
636
+ * 划掉的工具芯片** —— 两边都是 `string[]`,编译器一个字都不会说。
637
+ * 记账全文在
638
+ * [RECORD-skill-index-narrowing](../../../docs/verify/VERIFY_RECORD-skill-index-narrowing.md) §四第 3 条。
617
639
  */
618
640
  interface RoleNoticeView {
619
641
  role: string;
642
+ /** 收窄的是哪一轴。真源是 core 的 `RoleMergeNotice.kind` */
643
+ kind: 'tools' | 'skills';
620
644
  cut: readonly string[];
621
645
  }
622
646
  /** `SkillMeta` 在服务端眼里的样子。`scope` 同上一条 ⚠️ */
@@ -661,6 +685,14 @@ interface RuntimeCapabilities {
661
685
  * 没起来时是 `null` —— 两者对这一层是同一件事:没有正文可发。
662
686
  */
663
687
  skillBody(name: string): string | null;
688
+ /**
689
+ * 每条技能此刻进没进索引,名字 → 三档(2026-08-27)。**这一层不自己算** ——
690
+ * 判据是它算不出来:封顶的丢弃顺序(`DROP_RANK`)和角色白名单的交集语义
691
+ * 都在 core 的 `prompt-index.ts` 里,而这个包不许 import core。按
692
+ * `role.skills.includes()` 在这儿再判一遍就是第二份实现,
693
+ * 而它和真索引分叉之后两边都答得出「我算了」。
694
+ */
695
+ skillIndexResidency(role: string | null): ReadonlyMap<string, 'indexed' | 'capped' | 'role'>;
664
696
  /**
665
697
  * 从**本机目录**导入技能(方案 42 §六)。**进程级**,同 {@link mcp}。
666
698
  *
@@ -706,6 +738,14 @@ interface RuntimeCapabilities {
706
738
  *
707
739
  * @param bound 这个会话绑的工作区(`workspaces.of(sessionId)`)。没绑就是 null,
708
740
  * 那时「项目」那一层整个不存在 —— 不是「有但是空的」
741
+ * @param role 这个会话的**底座身份名**(`LiveSession.role?.name`),没绑就是 null。
742
+ * 只有技能那一栏读它:一个带 `skills` 白名单的身份会让某几条技能不进索引,
743
+ * 于是那几行行尾那个「常驻开销」是假话(2026-08-27 那笔账)。
744
+ *
745
+ * ⚠️ **它是必传的一格,不是可选参数。** 这个函数今天只有一个调用方,加个
746
+ * 默认 `null` 只会让下一个调用方在**默认不收窄**这条路上静默走对一半 ——
747
+ * 而走错的表现是屏幕上多出一个假数字,没有任何东西会红。判据同
748
+ * `toWireSkill` 的 `residency` 那一格。
709
749
  */
710
750
  declare function collectCapabilities(runtime: RuntimeCapabilities, tools: readonly {
711
751
  name: string;
@@ -717,7 +757,7 @@ declare function collectCapabilities(runtime: RuntimeCapabilities, tools: readon
717
757
  trust: {
718
758
  trusted: boolean;
719
759
  };
720
- } | null): WireCapabilitiesResponse;
760
+ } | null, role: string | null): WireCapabilitiesResponse;
721
761
  /**
722
762
  * 一份 `SKILL.md` 最多发多少字节。形状照
723
763
  * [workspace/diff.ts](./workspace/diff.ts) 的 `MAX_DIFF_BYTES`。
@@ -980,6 +1020,138 @@ interface ExpansionOutcome {
980
1020
  */
981
1021
  declare function expandUserContent(runtime: RuntimeCommands, message: EpochUserContent, model: ModelTurnsView | null, role: TurnRole | null): ExpansionOutcome;
982
1022
 
1023
+ /**
1024
+ * `WebRuntimeView.goalCatalog` —— **server 从 runtime 借的目标那一片**
1025
+ * ([方案 52](../../../../docs/verify/VERIFY_RECORD-52-goal.md) PR-4)。
1026
+ *
1027
+ * ## 为什么是一份结构镜像,而不是 `import type { GoalCatalog }`
1028
+ *
1029
+ * 判据逐字同 [schedule/view.ts](../schedule/view.js),两条,第二条是硬的:
1030
+ *
1031
+ * 1. **`context.ts` 那句话**:这一层只借「用得到的那一小片」。`GoalCatalog` 的
1032
+ * 每个方法都回 core 的 `Goal` / `GoalWrite` / `GoalClear` —— 借整个接口等于
1033
+ * 让 `@epoch-agent/server` 的公开 `.d.ts` 引用一个它没声明依赖的包;
1034
+ * 2. **用例要造得出一个假的**。`server/__tests__/harness.ts` 的假 runtime 是手写
1035
+ * 对象。镜像用 protocol 的类型写完之后,那个假件一行 core 都不用碰。
1036
+ *
1037
+ * ⚠️ **镜像必须和真身逐字段对得上**,否则 `cli/src/commands/web.ts` 那一行
1038
+ * `createWebServer({ runtime })` 当场编译不过 —— 那正是这份镜像的全部强制力。
1039
+ *
1040
+ * ## ⚠️ 借的是 `goalCatalog` 而**不是** `goals`,这一条是这个文件的正题
1041
+ *
1042
+ * `EpochRuntime` 上有两格,答的是两个问题:
1043
+ *
1044
+ * ```
1045
+ * runtime.goals 「**引导会话**那一段的目标」 GoalControl(收窄到一段)
1046
+ * runtime.goalCatalog 「**任意一段**会话的目标」 GoalCatalog(按 id 现查)
1047
+ * ```
1048
+ *
1049
+ * 借前者的下场,这个仓库已经付过一次账了:`WebRuntimeView` 上原来有一个进程级的
1050
+ * `checkpoints`,而三条回退端点都在拿它答任意一个 sessionId ——「编译得过、跑得通、
1051
+ * 只是答错会话」(方案 30 §9.5)。那个字段 2026-08-15 删掉了,`context.ts` 上那段
1052
+ * ⚠️ 写着为什么。这一格从第一天起就借对的那一份。
1053
+ *
1054
+ * ## 为什么不像检查点那样「向工厂要这个会话自己那一份」
1055
+ *
1056
+ * 因为**目标没有内存态**。`GoalService` 每次现读 SQLite(`GoalStore.current`),
1057
+ * 于是「这段会话此刻活不活着」和「它的目标是什么」是两个无关的问题:
1058
+ *
1059
+ * | 那一格 | 真源 | 冷却掉的会话答得出吗 |
1060
+ * | -------------- | -------------------------- | -------------------- |
1061
+ * | 检查点 | 这个进程的 `CheckpointManager` | ❌ 只能回空列表 |
1062
+ * | 计划 | 内存那份优先,回退读盘 | ⚠️ 读盘那份可能旧 |
1063
+ * | **目标** | **只有 SQLite 一份** | ✅ 一样准 |
1064
+ *
1065
+ * 走工厂那条路的话,用户打开一段昨天的会话会看到「这个会话还没有目标」——
1066
+ * 而库里那条目标正躺在那儿,那正是「交代完走开」这份方案要修的病本身。
1067
+ *
1068
+ * ## ⚠️ 这里**没有** `block()`
1069
+ *
1070
+ * runtime 那一侧的 `GoalCatalog` 上也没有(判据在它的 JSDoc 上):`blocked` 是
1071
+ * 模型 / 策略层的态,而那个 `code` 是给程序路由用的键 —— 从 HTTP 这一侧露出一条
1072
+ * 报 `blocked` 的路,等于让浏览器编一个 `needs-approval` 出来,而那个方向是提权
1073
+ * (52 §5.1 结论 3)。网线上那份请求体里同样说不出这个态
1074
+ * (`WireGoalUpdateRequest` 只有三态),**两道各自独立**:这一道是编译期的,
1075
+ * 那一道是契约上的,少任何一道另一道都会变成唯一的防线。
1076
+ */
1077
+
1078
+ /**
1079
+ * core 的 `Goal` 在服务端眼里的样子。
1080
+ *
1081
+ * **没有 `id`** —— 那是 `goals` 表的主键,这一层拿它什么都做不了(一个会话至多
1082
+ * 一个目标,寻址靠 sessionId)。镜像上没有它是**编译期生效**的:投影里写不出
1083
+ * `goal.id`,写了当场不过。判据同 `CheckpointSummaryView` 那处「没有 `cursor`」。
1084
+ *
1085
+ * `sessionId` **留着**,和 `id` 刻意不同:跨会话那条端点要拿它去查标题。
1086
+ */
1087
+ interface GoalView {
1088
+ sessionId: string;
1089
+ objective: string;
1090
+ phase: WireGoalPhase;
1091
+ /** `phase === 'blocked'` 时必有,其余态必无 */
1092
+ block?: {
1093
+ readonly code: string;
1094
+ readonly message: string;
1095
+ };
1096
+ maxRounds: number;
1097
+ roundsUsed: number;
1098
+ /** `phase === 'complete'` 时必有 */
1099
+ completeEvidence?: string;
1100
+ createdAt: number;
1101
+ updatedAt: number;
1102
+ }
1103
+ /**
1104
+ * 一次写的结果 —— core 的 `GoalWrite`。
1105
+ *
1106
+ * **判别联合原样照抄,不摊平成 `{ok, goal?, reason?}`**:摊平之后
1107
+ * 「`ok: true` 却没有 goal」在类型上说得出口,而 handler 那一侧只能补一个
1108
+ * `?? null` —— 那个 `null` 会一路走到网线上,界面读成「写成功了但目标没了」。
1109
+ */
1110
+ type GoalWriteView = {
1111
+ ok: true;
1112
+ goal: GoalView;
1113
+ } | {
1114
+ ok: false;
1115
+ reason: WireGoalRefusal;
1116
+ };
1117
+ /** `clear` 的结果 —— core 的 `GoalClear`。它没有「写完之后那份目标」 */
1118
+ type GoalClearView = {
1119
+ ok: true;
1120
+ cleared: boolean;
1121
+ } | {
1122
+ ok: false;
1123
+ reason: WireGoalRefusal;
1124
+ };
1125
+ /**
1126
+ * 目标那一片能力。`EpochRuntime.goalCatalog` 结构上正好满足它。
1127
+ *
1128
+ * ## ⚠️ 可以为 null,而这和 `schedules` 那一格刻意不同
1129
+ *
1130
+ * 定时任务那一格不可为 null(底下只有一张表和一次平台探测,没有起不来的可能)。
1131
+ * 这一格**会**为 null:目标是**落盘的状态**,会话库起不来时它整个不存在 ——
1132
+ * 判据逐字同 TUI 那一侧的「会话库起不来时 `/goal` 压根不注册」(那时还让它出现在
1133
+ * `/help` 里,等于承诺一个建了就没的东西)。
1134
+ *
1135
+ * 于是那五条端点在这一档回 **503 `no-goal-store`**,而不是一份 `goal: null`:
1136
+ * 后者是「这个会话没有目标」,前者是「问不出来」,两者在界面上是两种画法
1137
+ * (一个是空态加一个「来定一条」的按钮,一个是一句「这台机器上没有会话库」)。
1138
+ * 判据全文在 `WireGoalResponse` 上。
1139
+ */
1140
+ interface GoalCatalogView {
1141
+ current: (sessionId: string) => GoalView | null;
1142
+ /** 建一个。**人专用** —— 判据在 runtime 的 `GoalCatalog.create` 上 */
1143
+ create: (sessionId: string, objective: string, maxRounds?: number) => GoalWriteView;
1144
+ /** 改正文。**不重置轮次、不改 phase** */
1145
+ edit: (sessionId: string, objective: string) => GoalWriteView;
1146
+ /** 改预算。⚠️ 两个方向都会顺手改 phase,判据在 `WireGoalUpdateRequest` 上 */
1147
+ setBudget: (sessionId: string, maxRounds: number) => GoalWriteView;
1148
+ pause: (sessionId: string) => GoalWriteView;
1149
+ resume: (sessionId: string) => GoalWriteView;
1150
+ /** 人替它报完成。依据必填 */
1151
+ complete: (sessionId: string, evidence: string) => GoalWriteView;
1152
+ clear: (sessionId: string) => GoalClearView;
1153
+ }
1154
+
983
1155
  /**
984
1156
  * SessionHub —— 事件总线 + 回合状态机 + 审批生命周期。
985
1157
  *
@@ -1319,6 +1491,214 @@ declare class SessionHub {
1319
1491
  private reap;
1320
1492
  }
1321
1493
 
1494
+ /**
1495
+ * `@` 提及在 Web 那一侧的两半(方案 53 PR-4 的会话 + 方案 63 的文件)——
1496
+ * 补全面板的候选,和发消息那条路上的解析。
1497
+ *
1498
+ * ## 四件事,一个文件
1499
+ *
1500
+ * | 做什么 | 谁调 |
1501
+ * | ----------------------------- | ------------------------------------- |
1502
+ * | `GET .../reference-candidates` | 输入框打 `@:` 那一刻(方案 53 PR-4) |
1503
+ * | `GET .../file-candidates` | 输入框打 `@文件` 那一刻(方案 63) |
1504
+ * | 把解析结果塞进这一条消息 | `api.postMessage`(每条消息都问一次) |
1505
+ *
1506
+ * 不摊进 [api.ts](./api.ts),理由同 [commands.ts](./commands.js):那个文件是
1507
+ * 「一个端点一个函数」,而这里有一份 runtime 能力的**结构镜像**加一段提及解析。
1508
+ *
1509
+ * ## ⚠️ 引擎那一侧一行都不重写 —— 这是这一整份的性质
1510
+ *
1511
+ * 三样东西全部来自 `EpochRuntime.sessionReferences`(判据在它的 JSDoc 上):
1512
+ *
1513
+ * - **候选**(只碰 id / cwd / 标题,一个字的转录文本都不搜,§5.2)
1514
+ * - **解析**(`@:xxx` → 哪一段,或者为什么不是)
1515
+ * - **当前面**(`active = 1` 的那些,§二)
1516
+ *
1517
+ * 而这三样里最要紧的是**可读性判定**:它在 core 里、和[方案 48](../../../docs/verify/VERIFY_RECORD-48-session-query.md)
1518
+ * 共用一处(`core/src/session/authorization.ts` 的 `resolveSessionVisibility`,
1519
+ * 判据全文在那个文件头)。让服务端自己去读 `sessions.db`
1520
+ * 等于给 `@:` 开一条绕过那个判定的读取通道 —— 而 `@:` 存在的前提恰恰是
1521
+ * 「它永远不比 `session_search` 宽松」(方案 53 §4.3)。
1522
+ *
1523
+ * 上限那张表和截断那段算术也不在这儿:它们住在 protocol
1524
+ * (`attachSessionSurface`),和 TUI 那条路**共用同一份**。抄第二遍的形态很具体:
1525
+ * TUI 上一段会话附了 48 条、web 上同一段附了 60 条,两边都不报错,
1526
+ * 而「模型到底看见了什么」从此有两个答案。判据全文在 protocol 的 `mentions.ts`。
1527
+ *
1528
+ * ## `@文件` 那一半(方案 63)的行状
1529
+ *
1530
+ * - **候选**走 `EpochRuntime.workspaceFiles.candidates`(runtime 的收窄面):
1531
+ * 过滤、`@` 表达不出来的去尾、排序、缓存全在那一侧(判据在那个文件的文件头),
1532
+ * 这里只做「按 `:id` 现算工作区根 → 递进去 → 搬上网线」三件事。
1533
+ * - **解析**走同一条路的两步:`resolve` 出界 = `denied`,`readFile` 一趟走完
1534
+ * 边界 / `file_read` 同源判定 / 二进制探测(失败三档码见 runtime 的
1535
+ * `FileReadOutcome`)。**服务端这里没有一个字的文件读取** —— 判据同上面
1536
+ * 「让服务端自己去读 sessions.db」那一条:`@` 存在的前提是它永远不比
1537
+ * `file_read` 宽松,而判定只有权限层那一份。
1538
+ * - **预算**:文件先吃、会话后到,两者共享同一本 200KB 的账 —— 判据逐字同
1539
+ * `core/src/context/mentions.ts` 的 `resolveMentions`(反过来的话两个小文件
1540
+ * 会被一段长会话挤成 `budget-exceeded`,而用户显然更想要那两个文件)。
1541
+ *
1542
+ * ## 这一层自己拿的两个决定
1543
+ *
1544
+ * 1. **`currentSessionId` 一律是 URL 上那个 `:id`,不是 `runtime.sessionId`。**
1545
+ * 后者是**引导会话**。拿它去答别的会话,表现是「引用自己」判不出来
1546
+ * (于是真的把自己塞进自己)、血缘链算成另一段会话的 —— 而它编译得过、
1547
+ * 跑得通、只是答错会话(同 `EpochRuntime.checkpoints` 那段 ⚠️ 骂的那件事)。
1548
+ * 2. **工作区取 `projectContext.rootDir`,不是 `workspace.root`。**
1549
+ * 两者在「在子目录里起的那次会话」上不同(前者是仓库根,后者是启动目录),
1550
+ * 而写入侧往 `sessions.cwd` 里写的是前者(判据在 `core/src/session/manager.ts`
1551
+ * 的 `getLatestByCwd` 和 `authorization.ts` 的 `workspaceCwd` 上)。
1552
+ * 给错的表现是**一条候选都列不出来**,而屏幕上只是一个空面板。
1553
+ */
1554
+
1555
+ /**
1556
+ * 候选清单一次最多回几条。
1557
+ *
1558
+ * 面板上放不下更多,而这个数同时是那条 SQL 的 `LIMIT` —— 用户打得越细,
1559
+ * 回来的越少(过滤在 SQL 里,见文件头)。
1560
+ *
1561
+ * ⚠️ **它不是 TUI 那个 `SESSION_CANDIDATE_POOL`。** 那一个是「挂载时拉一池子回来、
1562
+ * 之后在前端筛」,于是第 41 段会话**永远搜不到而且屏幕上没有提示**(那笔账记在
1563
+ * `docs/verify/VERIFY_RECORD-53-session-reference.md` 第六章)。这一条路每次带着
1564
+ * `?q=` 现问,所以这个数只封「一屏列多少」,不封「能搜到哪几段」。
1565
+ */
1566
+ declare const MAX_CANDIDATES = 20;
1567
+ /**
1568
+ * 一段会话候选在引擎眼里的样子(core 的 `SessionCandidate`)。
1569
+ *
1570
+ * ⚠️ **和网线那一份({@link WireSessionCandidate})字段逐个相同,而它们仍然是两个
1571
+ * 类型**:这一份是镜像(server 不许 import core),那一份是契约。所以
1572
+ * {@link toWireCandidate} 是一次**显式**的搬运而不是 `as` —— 哪天引擎那侧加了一个
1573
+ * 带转录文本的字段(`preview`),它不会顺着 spread 悄悄上网线。
1574
+ */
1575
+ interface SessionCandidateView {
1576
+ sessionId: string;
1577
+ title: string;
1578
+ cwd?: string | undefined;
1579
+ updatedAt: number;
1580
+ messageCount: number;
1581
+ sameWorkspace: boolean;
1582
+ }
1583
+ /** 解析的结果(core 的 `ResolvedReference`)。三个码见方案 53 §4.2 */
1584
+ interface ResolvedReferenceView {
1585
+ sessionId?: string | undefined;
1586
+ reason?: 'invalid-reference' | 'self-reference' | 'not-authorized' | undefined;
1587
+ }
1588
+ /** 一次可读范围的入参。三格逐字同 core 的 `SessionVisibilityInput`(去掉 `db`) */
1589
+ interface VisibilityInput {
1590
+ /** 谁在引用 —— **URL 上那个 `:id`**,见文件头第 1 条 */
1591
+ currentSessionId: string;
1592
+ /** 项目根(`projectContext.rootDir`),见文件头第 2 条 */
1593
+ workspaceCwd?: string | undefined;
1594
+ /** 跨工作区。只能来自已经打开了配置开关的那条路 */
1595
+ allWorkspaces?: boolean;
1596
+ }
1597
+ /**
1598
+ * `EpochRuntime.sessionReferences` 在服务端眼里的样子 —— **三个方法,一个不多**。
1599
+ *
1600
+ * 写镜像而不是 import core,判据同 `commands.ts` / `capability.ts` 的文件头。
1601
+ * 而这一格还多一条:它收窄的东西本身就是一条安全边界 ——
1602
+ * 那份门面刻意「交不出任意 SQL」(`SessionReferences` 的 JSDoc),
1603
+ * 镜像照着它抄,服务端就连绕过判定的**语法**都没有。
1604
+ *
1605
+ * `EpochRuntime.sessionReferences` 结构上正好满足它,
1606
+ * 对不上就是两边长歪了,`cli/src/commands/web.ts` 那一行当场编译不过。
1607
+ */
1608
+ interface SessionReferencesView {
1609
+ /** 补全面板的候选。**只按 id / cwd / 标题过滤**(§5.2) */
1610
+ candidates(input: VisibilityInput & {
1611
+ query: string;
1612
+ limit: number;
1613
+ }): readonly SessionCandidateView[];
1614
+ /** `@:xxx` → 哪一段会话(§1.2) */
1615
+ resolve(input: VisibilityInput & {
1616
+ ref: string;
1617
+ }): ResolvedReferenceView;
1618
+ /** 那一段会话的当前面(§二)。**调用方要先过 `resolve`** */
1619
+ surface(sessionId: string): SessionSurfaceView | undefined;
1620
+ }
1621
+ /**
1622
+ * `GET /api/sessions/:id/reference-candidates` —— 打 `@:` 那一刻面板该列哪几段。
1623
+ *
1624
+ * `:id` 做两件事,同 `tasks.ts` 的那两件:**认门**(不存在的会话 404,不回一份空
1625
+ * 清单冒充「没有可引用的会话」),以及**真的参与判定** —— 自己不在候选里、
1626
+ * 血缘链从它算起、亲和度按它绑的工作区排。
1627
+ *
1628
+ * ## 会话库起不来时 503,不是空清单;而且 503 **排在 404 前面**
1629
+ *
1630
+ * 判据同 `no-goal-store`(`goal/handlers.ts`):「这个宿主没有会话库」和
1631
+ * 「一段可引用的会话都没有」在界面上是两种画法;而顺序上,没有会话库的进程连
1632
+ * 「认不认识这个会话」都答不出来(`ctx.runtime.sessions` 是**同一个库**),
1633
+ * 那时的 404 是一句碰巧为真的话。
1634
+ *
1635
+ * 浏览器那一侧对两者的处置恰好相同(面板不画,见 `mention/use-candidates.ts`)——
1636
+ * 那是浏览器的选择,不是服务端可以把两句话合成一句的理由。
1637
+ */
1638
+ declare function listReferenceCandidates(ctx: ApiContext, res: ServerResponse, sessionId: string,
1639
+ /** `?q=` / `?limit=` 从这儿取 */
1640
+ rawUrl: string | undefined,
1641
+ /** 这一次响应用哪个语言渲染(方案 58)。`undefined` = 这条请求没说,沿用进程语言 */
1642
+ lang?: Lang): void;
1643
+ /**
1644
+ * `GET /api/sessions/:id/file-candidates` —— 打 `@`(后面不是 `:`)那一刻
1645
+ * 面板该列哪几个文件(方案 63)。
1646
+ *
1647
+ * ## 和 `reference-candidates` 同一条路由判据的两处小差异
1648
+ *
1649
+ * 1. **没绑工作区时 409 `no-workspace`**,而不是像会话那条路那样「有库就能答」:
1650
+ * 文件清单压根是**从工作区算出来的**,没有根连「列不出来」这一档都不存在。
1651
+ * 409 而不是 404:这个会话认得,是它的**状态**(还没选地盘)给不出答案 ——
1652
+ * 同 `workspace-locked` 那条的拆码判据。
1653
+ * 2. **`?q=` 过滤在 runtime 那一侧**(`EpochRuntime.workspaceFiles.candidates`),
1654
+ * 判据同会话那条路的「过滤在 SQL 里」:拉一池子回来再在前端筛,
1655
+ * 池子之外的永远搜不到而且屏幕上没有提示。
1656
+ *
1657
+ * 候选**已经是「提交时会被认出来」的那一批**(`@` 表达不出来的路径 runtime 已
1658
+ * 经去尾,只把条数带回来 —— 见 `WireFileCandidatesResponse.unmentionable`)。
1659
+ * 服务端这里一个字都不再筛:再筛一遍就是给同一件事第二个答案。
1660
+ */
1661
+ declare function listFileCandidates(ctx: ApiContext, res: ServerResponse, sessionId: string,
1662
+ /** `?q=` / `?limit=` 从这儿取 */
1663
+ rawUrl: string | undefined,
1664
+ /** 这一次响应用哪个语言渲染(方案 58)。`undefined` = 这条请求没说,沿用进程语言 */
1665
+ lang?: Lang): void;
1666
+ /** {@link attachMentions} 的结果 */
1667
+ interface AttachedMentions {
1668
+ /** 真正发给引擎的那条消息。一个提及都没有时 === 传进来的那条 */
1669
+ message: EpochUserContent;
1670
+ /** 逐段一条会话收据。**一段都没引用时是空数组**,调用方据此决定那个键要不要出现 */
1671
+ references: readonly WireSessionReference[];
1672
+ /** 逐个一条文件收据。**一个都没提时是空数组**,判据同上面那个键 */
1673
+ files: readonly WireFileReference[];
1674
+ }
1675
+ /**
1676
+ * 把这条消息里的 `@` 提及(文件 + 会话)解析成部件,塞在正文**前面**
1677
+ * (方案 53 §二 + 方案 63)。
1678
+ *
1679
+ * ## 三条顺序上的判据
1680
+ *
1681
+ * 1. **抽取读的是用户敲的那一行**,也就是这条消息开头那段文本。数组那一支
1682
+ * 只看第一个部件(同 `commands.ts` 的 `leadingText`)—— 输入框拼数组时文本
1683
+ * 永远在最前面。
1684
+ * 2. **部件排在文本前面**,逐字同 TUI(`app.tsx` 的 `handleSubmit`):
1685
+ * 模型先看见「这是哪个文件 / 哪段会话」再看见问题,和「工具结果先于追问」
1686
+ * 同一个顺序。
1687
+ * 3. **这一步排在斜杠命令展开之后**(见 `api.postMessage`)。反过来的话,
1688
+ * 展开那一侧的 `leadingText` 拿到的第一个部件是附件而不是文本,
1689
+ * 于是**斜杠命令悄悄失效** —— 而屏幕上没有任何区别。
1690
+ *
1691
+ * ## 预算:文件先吃、会话后到,两者共享同一本 200KB 的账(方案 63)
1692
+ *
1693
+ * 顺序逐字同 core 的 `resolveMentions`:会话的当前面动辄几十 KB,而文件提及是
1694
+ * 高频的那一种。反过来的话,一次 `@:某个长会话 @a.ts @b.ts` 会让两个小文件
1695
+ * 双双 `budget-exceeded` —— 而用户显然更想要那两个文件。
1696
+ *
1697
+ * ⚠️ **从 2026-08-26 起这条路上有两个消费方了**:`usedBytes` 不再从 0 起算,
1698
+ * 会话那一半排在文件那一半**之后**,拿到的预算是扣掉文件之后的余额。
1699
+ */
1700
+ declare function attachMentions(ctx: ApiContext, sessionId: string, message: EpochUserContent): AttachedMentions;
1701
+
1322
1702
  /**
1323
1703
  * 一个会话自己那三样(模型 / 权限档 / plan 模式)在**服务端眼里**的样子
1324
1704
  * ([方案 30](../../../../docs/verify/VERIFY_RECORD-30-web-multi-session.md) §六,2026-08-16)。
@@ -1546,6 +1926,51 @@ interface SessionHistoryFacts {
1546
1926
  }[];
1547
1927
  }[];
1548
1928
  }
1929
+ /**
1930
+ * 「这段会话的上下文预算怎么花的」+「现在压一次」—— `/context` 和 `/compact`
1931
+ * 那两条命令在 web 上的底座(2026-08-26)。
1932
+ *
1933
+ * ## 为什么和上面 {@link SessionHistoryFacts} 分开,而不是把它加宽
1934
+ *
1935
+ * 两者都要 `getHistory()`,但**要的是同一串东西的两个不同投影**:那一份问
1936
+ * 「有没有 `type: 'image'` 的 part」,这一份要 `content` 那个文本投影去估 token。
1937
+ * 把 `content` 加进那一份的代价是换模型那条路上凭空多出一个它不看的字段 ——
1938
+ * 而收窄镜像这件事的全部意义就是「只借用得到的那几格」。
1939
+ *
1940
+ * ⚠️ **`compact()` 在这儿而不在别处,因为它换掉的是 `AgentSession.history`**
1941
+ * ——「下一轮送进模型的是什么」。它**不动会话库**,所以 `GET /messages`
1942
+ * 压完之后一行都不会少(判据全文在 `WireCompactResponse` 上)。
1943
+ */
1944
+ interface SessionContextFacts {
1945
+ /**
1946
+ * 历史的**文本投影**。`content` 就是那个投影本身(见 core 的 `EpochMessage`),
1947
+ * 拼起来算 token 比逐条估更接近真实发出去的量。
1948
+ */
1949
+ getHistory(): readonly {
1950
+ content: string;
1951
+ }[];
1952
+ /** 手动压一次。三种结局(没跑 / 跑了没压动 / 压了)全在返回值里,见 `WireCompactResponse` */
1953
+ compact(instruction?: string): Promise<{
1954
+ ran: boolean;
1955
+ before: number;
1956
+ after: number;
1957
+ reason?: string;
1958
+ }>;
1959
+ }
1960
+ /**
1961
+ * **这个会话的循环**在服务端眼里的样子。只借 `contextBreakdown` 一件事。
1962
+ *
1963
+ * ⚠️ **别拿它去 `run()`**,runtime 的 `LiveSession.agent` 上那段警告一字不改地
1964
+ * 适用:开一轮必须经过 `AgentSession`(历史累积 + 持久化 + 审批桥),
1965
+ * 绕过去的表现是「聊完之后重开这个会话,刚才那几轮不见了」。镜像上只有这一个
1966
+ * 方法,正是为了让那条路在这一层**够不着**。
1967
+ */
1968
+ interface SessionLoopView {
1969
+ contextBreakdown(opts: {
1970
+ historyText: string;
1971
+ toolSource?: (name: string) => string | undefined;
1972
+ }): ContextBreakdown;
1973
+ }
1549
1974
 
1550
1975
  /**
1551
1976
  * 这个会话**此刻**用哪个模型 —— `GET` / `POST /api/sessions/:id/model`
@@ -1727,6 +2152,214 @@ interface RuntimeModel {
1727
2152
  };
1728
2153
  }
1729
2154
 
2155
+ /**
2156
+ * 插件那一页的六条端点(方案 59 §六 E2,2026-08-21):
2157
+ *
2158
+ * ```
2159
+ * GET /api/plugins 装着的 + 市场里有什么 + pendingRestart
2160
+ * POST /api/plugins/install/preview {ref} → 将安装什么,一个字节都不写
2161
+ * POST /api/plugins/install {ref, token} → 真装
2162
+ * POST /api/plugins/update/preview {name} → 更新会带来什么
2163
+ * POST /api/plugins/update {name, token} → 真更新
2164
+ * POST /api/plugins/uninstall {name} → 卸载
2165
+ * ```
2166
+ *
2167
+ * ## ⚠️ 这一层**一条策略判断都没有**
2168
+ *
2169
+ * 谁能装(清单里那些)、装什么(`<市场>/<插件>`)、远程那一档开没开、要不要重启 ——
2170
+ * 四个问题的答案全在 `EpochRuntime.plugins` 那一片里(判据全文在
2171
+ * `runtime/src/plugin-control.ts` 的文件头四节)。这个文件只做三件事:
2172
+ * 把请求体收窄成那一片认得的形状、把结局投影上网线、外加下面那**一道闸**。
2173
+ *
2174
+ * 在这儿重判一次的下场是「同一个问题两个答案」——而那两个答案分叉的那天,
2175
+ * 界面上是一条画成可点、点了却被拒的行(判据同 `PluginSearchHit.installable`
2176
+ * 为什么不从结果里剔掉远程那条)。
2177
+ *
2178
+ * ## ⚠️ 那一道闸:**五条 POST 在回环之外一律 403**,这是这个文件的正题
2179
+ *
2180
+ * 判据全文在 protocol 的 [wire-plugin.ts](../../protocol/src/wire-plugin.ts) 文件头
2181
+ * 第二节,这里记它对这一层的四条直接后果:
2182
+ *
2183
+ * 1. **`ctx.lanExposed` 为真时 403 `plugin-write-lan-exposed`,一个字节都不写。**
2184
+ * 装一包插件会落下 `hooks.json` 和 `mcp.json`,那两样**起子进程** ——
2185
+ * 下一程启动时照着跑。往 `~/.epoch/agents/` 落一份 md 只影响模型看到的字,
2186
+ * 这一条影响这台机器上会跑起什么进程,所以它比 `role-add.ts` 那道**还远一格**;
2187
+ * 2. **`install/preview` 也吃这道闸,虽然它一个字节都不写**:`allowRemote: true`
2188
+ * 的部署上,预览一条远程条目会 spawn `git clone --depth 1` 到 staging。
2189
+ * 按「写不写盘」切一刀会漏掉它;
2190
+ * 3. **那道闸在读请求体之前**,同 `addRole`:被拒的那一发连正文都不该被解析;
2191
+ * 4. **`GET` 刻意不挡。** 那一份里是插件名 / 版本 / 目录路径,和
2192
+ * `GET /api/sessions/:id/capabilities` 早就在发的那些同一档 —— 而挡掉它的
2193
+ * 唯一后果是那一页在 LAN 那一档下一片空白,
2194
+ * 偏偏那时用户最想知道的就是「我装了什么、生效了没有」。
2195
+ * (对比 `GET /api/mcp/config` 为什么连读都挡:那份文件里的 `env` 是明文密钥。)
2196
+ *
2197
+ * ## ⚠️ 这一片可以整个不存在 —— 503 `no-plugin-control`
2198
+ *
2199
+ * `runtime.plugins` 在宿主没给 `hostMarketplaces` 时是 `null`,而那**不是**
2200
+ * 「底下那层起不来了」,是「宿主没打算给用户这条路」(判据在 `build.ts` 那一格的
2201
+ * JSDoc 上)。六条一律 503,判据同 `no-goal-store`:那是一句关于**这个部署**的
2202
+ * 真话,而一份「装了 0 个插件」的空列表会让用户去找一个不存在的安装按钮。
2203
+ *
2204
+ * ⚠️ **默认的 `epoch web` 就落在这一档上**(不给 `--plugins` 就不传
2205
+ * `hostMarketplaces`)。那一格 2026-08-21 答掉了:`epoch web --plugins` 才接上
2206
+ * (传 `{ sources: [] }`),**默认关**,判据在
2207
+ * [验收记录](../../../docs/verify/VERIFY_RECORD-59-plugin-page.md) 第六章。
2208
+ *
2209
+ * ## 为什么单开一个文件
2210
+ *
2211
+ * 同 [role-add.ts](./role-add.ts) 从 `capability.ts` 里搬出来那次,**刀口按题目下
2212
+ * 不按行数**:那个文件答的是「能力页那三栏取什么数」,这一份答的是「怎么往这台
2213
+ * 机器上装一包扩展物」。而且这一份带着一整套结构镜像 + 一道闸门 + 七档拒绝的映射。
2214
+ *
2215
+ * ## 进程级,所以 URL 上没有 `:id`
2216
+ *
2217
+ * 落点是 `~/.epoch/plugins/`(一个进程一份),判据逐字同 `reconnectMcp` /
2218
+ * `importSkills` / `addRole`:挂在 `/sessions/:id/` 下面会得到一条名字是会话级、
2219
+ * 行为是进程级的端点,比不一致更坏。
2220
+ *
2221
+ * ## 文案是**第三种形态**,不是 `sendError`(方案 58 §1.2 那条 📮)
2222
+ *
2223
+ * `detail` 走的是 **200 响应体**,而它在 **runtime / core** 里现渲染
2224
+ * (`plugin_control.*` 那几条 + core 那些安装失败原因),所以 `lang` 要从这一层
2225
+ * 一路穿到 `PluginControlView` 的每一个方法上 —— 漏一处的表现是英文界面上
2226
+ * 装插件失败拿到一句中文。
2227
+ */
2228
+
2229
+ /** 六类扩展物各贡献了几条(core 的 `PluginCounts`,不能直接 import) */
2230
+ interface CountsView {
2231
+ commands: number;
2232
+ roles: number;
2233
+ skills: number;
2234
+ hooks: number;
2235
+ denyRules: number;
2236
+ mcpServers: number;
2237
+ }
2238
+ /** `PluginListEntry`(runtime)的镜像。`marketplace` / `counts` 是**可选**,见投影那两行 */
2239
+ interface PluginEntryView {
2240
+ name: string;
2241
+ version: string;
2242
+ source: string;
2243
+ sourceType: WirePluginSourceType;
2244
+ path: string;
2245
+ linked: boolean;
2246
+ enabled: boolean;
2247
+ installedAt: number;
2248
+ marketplace?: string;
2249
+ active: boolean;
2250
+ counts?: CountsView;
2251
+ }
2252
+ /** `PluginSearchHit`(runtime)的镜像 */
2253
+ interface PluginHitView {
2254
+ marketplace: string;
2255
+ ref: string;
2256
+ entry: {
2257
+ name: string;
2258
+ description?: string;
2259
+ source: string;
2260
+ category?: string;
2261
+ keywords?: readonly string[];
2262
+ };
2263
+ sourceType?: WirePluginSourceType;
2264
+ installable: boolean;
2265
+ }
2266
+ /** `InstallPreview`(core,经 runtime 原样透出)的镜像 —— 「将安装什么」那一屏的内容 */
2267
+ interface PluginPreviewView {
2268
+ manifest: {
2269
+ name: string;
2270
+ version: string;
2271
+ description?: string;
2272
+ author?: {
2273
+ name: string;
2274
+ };
2275
+ homepage?: string;
2276
+ };
2277
+ source: {
2278
+ raw: string;
2279
+ type: WirePluginSourceType;
2280
+ sha256?: string;
2281
+ };
2282
+ inventory: {
2283
+ commands: readonly string[];
2284
+ roles: readonly string[];
2285
+ skills: readonly string[];
2286
+ hooks: readonly {
2287
+ type: string;
2288
+ count: number;
2289
+ }[];
2290
+ denyRules: number;
2291
+ mcpServers: readonly string[];
2292
+ ignoredBuckets: readonly string[];
2293
+ jsTools: boolean;
2294
+ };
2295
+ conflict?: {
2296
+ name: string;
2297
+ version: string;
2298
+ };
2299
+ }
2300
+ /** 七档拒绝的共同形状(runtime 的 `PluginRefusal`) */
2301
+ interface RefusalView {
2302
+ ok: false;
2303
+ reason: WirePluginRefuseReason;
2304
+ detail: string;
2305
+ }
2306
+ type PreviewOutcomeView = {
2307
+ ok: true;
2308
+ preview: PluginPreviewView;
2309
+ token: string;
2310
+ } | RefusalView;
2311
+ /**
2312
+ * 装 / 更新的结局。
2313
+ *
2314
+ * ⚠️ **`record` 上刻意只借了一个 `name`**,尽管 runtime 那边交回来的是一整条
2315
+ * 安装记录:那一条上**没有 `active`**(它是 `list()` 现算的),而这一层要发给
2316
+ * 界面的正是那一格。所以成功之后回头 `list()` 里查一遍才是唯一说得准的做法 ——
2317
+ * 借宽了只会让下一个人顺手拿它拼一个 `active: false`,而更新一个**本来就在生效**
2318
+ * 的插件时那句话是假的。判据在 `WirePluginActionResponse.entry` 上。
2319
+ */
2320
+ type InstallOutcomeView = {
2321
+ ok: true;
2322
+ record: {
2323
+ name: string;
2324
+ };
2325
+ } | RefusalView;
2326
+ type ActionOutcomeView = {
2327
+ ok: true;
2328
+ detail: string;
2329
+ } | RefusalView;
2330
+ /**
2331
+ * `PluginControl`(runtime)在服务端眼里的样子 —— **只有那六个动词加一格布尔**。
2332
+ *
2333
+ * 镜像里只有它们,于是这个包**写不出** core 的 `setPluginEnabled`
2334
+ * (停用一个插件会静默拿掉别人正依赖的一批命令,正当性和「装 / 卸」不是一回事)、
2335
+ * 也写不出「往任意路径装一个插件」——那不是靠 review 盯住的,是编译期的事
2336
+ * (同 `SkillImportControlView` 只有两个动作、`RoleWriteControlView` 只有 `add`)。
2337
+ *
2338
+ * ⚠️ 每个方法都带 `lang?`。**镜像上漏一格不一定会红**(多一个可选参数是宽化,
2339
+ * `EpochRuntime` 照样满足这份镜像),而漏了的表现是这一层递不出去、一声不吭
2340
+ * 地回中文 —— 判据逐字同 `RoleWriteControlView.add` 上那条 ⚠️。
2341
+ */
2342
+ interface PluginControlView {
2343
+ list(): readonly PluginEntryView[];
2344
+ /** 空关键词 = 全部。**这一层永远只传空**,搜索在浏览器里做(判据在 wire 那份文件头) */
2345
+ search(keyword?: string): readonly PluginHitView[];
2346
+ preview(ref: string, lang?: Lang): Promise<PreviewOutcomeView>;
2347
+ install(ref: string, token: string, lang?: Lang): Promise<InstallOutcomeView>;
2348
+ previewUpdate(name: string, lang?: Lang): Promise<PreviewOutcomeView>;
2349
+ update(name: string, token: string, lang?: Lang): Promise<InstallOutcomeView>;
2350
+ uninstall(name: string, lang?: Lang): Promise<ActionOutcomeView>;
2351
+ readonly pendingRestart: boolean;
2352
+ }
2353
+ /**
2354
+ * 装着的一条 → 网线上那一行。
2355
+ *
2356
+ * `marketplace` / `counts` 缺席时下发 **`null` 而不是省掉这个键**,判据同
2357
+ * `WireSkillImportEntry.conflict`:两格在 wire 上写成必填,于是「服务端忘了转
2358
+ * 这一格」会当场编译不过。而 `counts` 漏掉的表现是一个生效中的插件在界面上
2359
+ * 「什么都不带」。
2360
+ */
2361
+ declare function toWirePluginEntry(entry: PluginEntryView): WirePluginEntry;
2362
+
1730
2363
  /**
1731
2364
  * 「我们支持哪几家、这一家有哪些模型」 —— `GET /api/providers` 和
1732
2365
  * `POST /api/providers/:type/models`。
@@ -1817,7 +2450,7 @@ interface RuntimeModelCatalog {
1817
2450
 
1818
2451
  /**
1819
2452
  * `WebRuntimeView.schedules` —— **server 从 runtime 借的定时任务那一片**
1820
- * ([方案 45](../../../../.agents/plans/45-scheduled-automation-plan.md) PR-3)。
2453
+ * ([方案 45](../../../../docs/verify/VERIFY_RECORD-45-automation.md) PR-3)。
1821
2454
  *
1822
2455
  * ## 为什么是一份结构镜像,而不是 `import type { ScheduleControl }`
1823
2456
  *
@@ -1922,11 +2555,22 @@ interface ScheduleCapabilityView {
1922
2555
  /**
1923
2556
  * 定时任务那一片能力。`EpochRuntime.schedules` 结构上正好满足它。
1924
2557
  *
1925
- * ⚠️ **不可为 null**:底下只有一张 SQLite 表和一次平台探测,没有起不来的可能
1926
- * (`build.ts` 上那个字段的 JSDoc 写着同一句)。本平台没有 OS 后端时
1927
- * `capability()` 如实说 `backend: null` —— 那是一个答案,不是一个缺失的能力。
2558
+ * ⚠️ **这一格不可为 null,但「不为 null」不等于「用得了」**(2026-08-26 改准)。
2559
+ * 原来这里写着「底下只有一张 SQLite 表和一次平台探测,**没有起不来的可能**」
2560
+ * —— 后半句是假的:那张表就是 `sessions.db`,打不开的时候这一片一条都列不出来。
2561
+ * 所以多了 {@link ScheduleControlView.storeAvailable} 那一格,十条端点**先过它**
2562
+ * (`handlers.ts` 的 `schedulesOf()`,503 `no-schedule-store`)。
2563
+ *
2564
+ * 本平台没有 OS 后端时 `capability()` 如实说 `backend: null` —— 那是一个答案,
2565
+ * 不是一个缺失的能力;那一格和这一格答的是两件事,别合并。
1928
2566
  */
1929
2567
  interface ScheduleControlView {
2568
+ /**
2569
+ * 底下那张表打开了没有。**同步的** —— 十条端点要在碰 store 之前判得出来。
2570
+ *
2571
+ * `false` 时下面每个方法都会抛,所以调用方**必须先看这一格**。
2572
+ */
2573
+ readonly storeAvailable: boolean;
1930
2574
  list: () => ScheduleDefinition[];
1931
2575
  get: (id: string) => ScheduleDefinition | undefined;
1932
2576
  /** 某条任务最近几次运行,最新的在前 */
@@ -1983,6 +2627,20 @@ interface BoundWorkspace {
1983
2627
  /** `trusted: false` = 这个目录的指令文件**不进** system prompt */
1984
2628
  trust: TrustView;
1985
2629
  projectContext: {
2630
+ /**
2631
+ * **项目根** —— `resolveRootDir(workDir)` 的结果(仓库根,找不到就是启动目录)。
2632
+ *
2633
+ * ⚠️ **和上面那个 `workspace.root` 不是同一个值**,别互相顶替:后者是
2634
+ * `resolve(workDir)`(用户敲 `epoch` 那一刻所在的目录),两者在
2635
+ * 「在子目录里起的那次会话」上不同。
2636
+ *
2637
+ * 2026-08-21 加进这份镜像的(方案 53 PR-4):`@:` 那条路要按工作区算候选和
2638
+ * 可读范围,而**写入侧往 `sessions.cwd` 里写的是这一个**(判据在 core 的
2639
+ * `SessionManager.getLatestByCwd` 和 `session/authorization.ts` 的 `workspaceCwd`
2640
+ * 上)。给 `workspace.root` 的表现是**一条候选都列不出来**,
2641
+ * 而屏幕上只是一个空面板 —— 没有任何一处会报错。
2642
+ */
2643
+ rootDir: string;
1986
2644
  /** 真加载了的那几份 */
1987
2645
  instructions: ReadonlyArray<{
1988
2646
  path: string;
@@ -2086,7 +2744,9 @@ interface WorkspaceView {
2086
2744
  * 安全中心的取数与投影 —— `GET /api/sessions/:id/security`(方案 42 PR-3)。
2087
2745
  *
2088
2746
  * 不在 [api.ts](./api.ts) 里,同 [capability.ts](./capability.ts) 的理由:那个
2089
- * 文件是「一个端点一个函数」,而这里有五份 runtime 能力的**结构镜像**加六段投影。
2747
+ * 文件是「一个端点一个函数」,而这里是一叠 runtime 能力的**结构镜像**加同样一叠
2748
+ * 投影函数。(这两个数原来写死成「五份 / 六段」,2026-08-27 加审批缓存那一格时
2749
+ * 双双变成假话 —— 改成链到下面的声明本身,同铁律 14 那条「要么写准,要么别写数字」。)
2090
2750
  *
2091
2751
  * ## 结构镜像,不是 import
2092
2752
  *
@@ -2113,7 +2773,9 @@ interface WorkspaceView {
2113
2773
  *
2114
2774
  * ## 「进程有什么」和「这个会话有什么」
2115
2775
  *
2116
- * 权限规则、托管锁、审计流水、策略目录四样今天是**进程级**的。前三样的判据在
2776
+ * 权限规则、托管锁、审计流水、**审批缓存**、策略目录五样今天是**进程级**的。
2777
+ * (审批缓存那一格是 2026-08-27 加的,它和前三样共用同一条判据 —— 见下一句。)
2778
+ * 前四样的判据在
2117
2779
  * core 的 [permission/shared.ts](../../core/src/permission/shared.ts) 文件头:
2118
2780
  * 它们是关于**用户**和**这台机器**的事实,跟着会话走反而会说假话(审批缓存
2119
2781
  * 分了家,「总是允许」在第二个会话上白点;流水分了家,这一屏会漏掉除引导会话
@@ -2174,7 +2836,7 @@ interface WorkspaceView {
2174
2836
  * 这个进程没在跑那段会话(只剩历史 / 被冷却)时退回进程那一份,那仍然是一句
2175
2837
  * 真话,判据逐字同 `permissionState()` 那条兜底。
2176
2838
  *
2177
- * ⚠️ **`rules` / `shadows` / `managed` / `audit` 四样一个都不许跟着搬。**
2839
+ * ⚠️ **`rules` / `shadows` / `managed` / `audit` / `cached` 五样一个都不许跟着搬。**
2178
2840
  * 判据在 core 的 `permission/shared.ts`:它们**没有**跟着会话分家。搬过去的
2179
2841
  * 具体代价是这一屏只剩下这一个会话判过的那几行 ——
2180
2842
  * **一本漏了行的安全流水比没有账更坏,因为它看起来是完整的。**
@@ -2244,6 +2906,29 @@ interface AuditView {
2244
2906
  entries: readonly AuditRowView[];
2245
2907
  dropped: number;
2246
2908
  }
2909
+ /**
2910
+ * core 的 `CachedApproval` 的镜像 —— 审批缓存里的一行(2026-08-27)。
2911
+ *
2912
+ * **没有 `ttlMs`**,而这一条和上面四份「漏掉中文」不是同一个理由:它不是散文,
2913
+ * 是引擎侧的一个实现细节(`allow-session` 的有效期)。发过去的话浏览器只能拿它
2914
+ * 算一个「还剩几分钟」,而那个数每一秒都在变、这一屏又没有刷新它的路 ——
2915
+ * 一个一打开就开始撒谎的倒计时。真源在引擎里:`listApprovals()` 每次都先
2916
+ * `prune()`,**过期的根本不在这张表里**。
2917
+ *
2918
+ * `scope` / `decision` 直接钉成网线上那两个联合而不是 `string`,同
2919
+ * {@link AuditRowView} 的 `code` 那条机制:core 那边给缓存多存一档决定而忘了同步
2920
+ * 契约时,**装配那一行当场编译不过**。写成 `string` 的话,新档会一路发到浏览器,
2921
+ * 在界面上显示成一个查不到文案的空白格。
2922
+ */
2923
+ interface CachedApprovalView {
2924
+ id: string;
2925
+ toolName: string;
2926
+ target: string;
2927
+ scope: WireCachedApproval['scope'];
2928
+ decision: WireCachedDecision;
2929
+ /** 引擎侧那个字段叫 `createdAt`,网线上叫 `at`(同流水那一列)—— 见投影函数 */
2930
+ createdAt: number;
2931
+ }
2247
2932
  /**
2248
2933
  * `PermissionsControl` 的镜像。前五样都是函数,因为它们**每次读都是当前值** ——
2249
2934
  * 审计流水尤其如此,它每判一次就长一条。
@@ -2291,6 +2976,28 @@ interface PermissionsView {
2291
2976
  };
2292
2977
  managed: () => ManagedView;
2293
2978
  audit: () => AuditView;
2979
+ /**
2980
+ * 这个进程记下了哪些弹窗答案(审批缓存,2026-08-27)。
2981
+ *
2982
+ * **和 {@link audit} 一档,跟着进程不跟着会话**(core 的 `permission/shared.ts`)。
2983
+ * 这也是它搭在这一份载荷里、而不是另开一条 `GET` 的理由之一 ——
2984
+ * 判据全文在 `protocol/wire-approval-cache.ts` 的文件头。
2985
+ *
2986
+ * ⚠️ **每次读都是当前值**(同上面五样都是函数):撤掉一条之后这张表就短一行,
2987
+ * 而那正是撤销那条端点要回读它的原因。
2988
+ */
2989
+ approvals: () => readonly CachedApprovalView[];
2990
+ /**
2991
+ * 撤销一条缓存的决定(2026-08-27)。**判定本体一个字都不在 server** ——
2992
+ * 同下面 `setLevel` 那条:`prune()` → 按 id 删 → `allow-always` 重写盘,
2993
+ * 三步都在 core 的 `ApprovalCache.revoke()` 里。
2994
+ *
2995
+ * 返回类型钉成 `ApprovalRevokeResult` 而不是 `boolean`:runtime 那边把
2996
+ * 「没有权限层」和「这条不在表里」分成了两个码,而它们在界面上是两句完全
2997
+ * 不同的话(一句是「这一节不适用」,一句是「刚才那条已经没了」)。
2998
+ * 收成一个布尔的话,后者会被画成前者的样子。
2999
+ */
3000
+ revokeApproval: (id: string) => ApprovalRevokeResult;
2294
3001
  /**
2295
3002
  * 切到某一档。**判定本体一个字都不在 server** —— 托管挡 bypass、
2296
3003
  * `PermissionManager.setLevel()`、`plan.forget()` 三步都在 runtime 的
@@ -2362,6 +3069,21 @@ interface RuntimeSecurity {
2362
3069
  isolation: IsolationView | null;
2363
3070
  permissions: PermissionsView | null;
2364
3071
  policy: PolicyView;
3072
+ /**
3073
+ * 装配时那份合并后的配置里,沙箱开关那一格(方案 46 §11.3 第四条的后半格)。
3074
+ *
3075
+ * 只镜像 `sandbox.terminal` 这一个键:它是「这次 terminal 的命令包不包」的
3076
+ * **配置事实**,和 {@link RuntimeSecurity.isolation} 那份实测报告是两个正交的
3077
+ * 轴 —— 所以它在载荷上单独一格(`WireSandboxStatus.terminalEnabled`),
3078
+ * 不并进后端那一行、也不并进 `covers` / `excludes` 那两张常量清单(判据逐字
3079
+ * 同 `epoch doctor` 的「开关」那一行)。`EpochRuntime.config` 结构上正好
3080
+ * 满足它,同其余几份镜像的机制:对不上就是装配那一行编译不过。
3081
+ */
3082
+ config: {
3083
+ sandbox?: {
3084
+ terminal?: boolean;
3085
+ };
3086
+ };
2365
3087
  }
2366
3088
  /**
2367
3089
  * 五块一次取齐。
@@ -2579,8 +3301,24 @@ interface LiveSessionView {
2579
3301
  * 换模型那条路要问「**这段**对话里有没有图片」。写成交集而不是各借一个字段,
2580
3302
  * 判据同它原来在 `WebRuntimeView.session` 上那句:它们说的是**同一个对象**,
2581
3303
  * 拆成两个字段的话装配那边可以只填其中一个。
3304
+ *
3305
+ * ⚠️ **2026-08-26 起又多一个 {@link SessionContextFacts}**(`/compact` 那条路要
3306
+ * `compact()`,`/context` 那条路要 `getHistory()` 的**文本**投影)。同一条判据第三次
3307
+ * 用上:三样说的是同一个对象,拆成三个字段的话装配那边可以只填其中一个。
2582
3308
  */
2583
- readonly session: HubSession & SessionHistoryFacts;
3309
+ readonly session: HubSession & SessionHistoryFacts & SessionContextFacts;
3310
+ /**
3311
+ * **这个会话自己那个循环**(2026-08-26)。转出来只为一件事:
3312
+ * `GET /api/sessions/:id/context` 要问「现在的上下文预算怎么构成的」,
3313
+ * 而那是循环才答得上的。
3314
+ *
3315
+ * ⚠️ **不是 `EpochRuntime.contextBreakdown`。** 那个 getter 答的恒是**引导会话**
3316
+ * ——它内部拿的是装配时那个 `agent` 和 `session`。拿它去答任意一个 sessionId,
3317
+ * 就是 2026-08-15 从 `WebRuntimeView` 上删掉 `checkpoints` 的那条 bug 原样重演
3318
+ * (方案 30 §9.5):编译得过、跑得通、只是答错会话,而第二个会话看到的是
3319
+ * 别人的预算构成 —— 且屏幕上没有任何一个字说得出这件事。
3320
+ */
3321
+ readonly agent: SessionLoopView;
2584
3322
  /**
2585
3323
  * **这个会话此刻用哪个模型**。runtime 那边 provider 起不来时一个会话都建不出来,
2586
3324
  * 所以这一格不为 null。
@@ -3204,6 +3942,42 @@ interface WebRuntimeView extends RuntimeCapabilities, RuntimeSecurity, RuntimeSe
3204
3942
  * 而 DELETE / PATCH 回 503 —— 不是假装成功。
3205
3943
  */
3206
3944
  sessions: SessionCatalog | null;
3945
+ /**
3946
+ * `@:` 引用另一段会话那一片(方案 53 PR-4)。SQLite 起不来时为 null。
3947
+ * `EpochRuntime.sessionReferences` 结构上正好满足它。
3948
+ *
3949
+ * ## ⚠️ 借的是**那个收窄面**,不是会话库
3950
+ *
3951
+ * 上面 {@link sessionStore} 是「回放旧会话」的读路,这一格是「把另一段会话塞进
3952
+ * 这一条消息」—— 它只交出候选 / 解析 / 当前面三件事,**交不出任意 SQL**。
3953
+ * 这不是省事:那三件事里最要紧的**可读性判定在 core 里**(和方案 48 共用一处),
3954
+ * 而服务端不许 import core。让服务端自己去 `sessionStore` 上拼一条 SQL 出来,
3955
+ * 等于给 `@:` 开一条绕过那个判定的读取通道 —— 而 `@:` 存在的前提恰恰是
3956
+ * 「它永远不比 `session_search` 宽松」(方案 53 §4.3)。
3957
+ * 判据全文在 `EpochRuntime.sessionReferences` 的 JSDoc 上。
3958
+ *
3959
+ * 为 null 时那条候选端点回 503 `no-session-store`(**不是**一份空清单 ——
3960
+ * 判据同 `no-goal-store`),而发消息那条路上 `@:xxx` 一律得到
3961
+ * `invalid-reference`:说「没找到唯一匹配的会话」比悄悄丢掉那个提及诚实。
3962
+ */
3963
+ sessionReferences: SessionReferencesView | null;
3964
+ /**
3965
+ * `@文件` 补全与解析(方案 63)。`EpochRuntime.workspaceFiles` 结构上正好满足它
3966
+ * —— **对不上就是两边长歪了,装配那一行当场编译不过**。
3967
+ *
3968
+ * ## ⚠️ 不可为 null,判据逐字同 `EpochRuntime.workspaceFiles` 那一格
3969
+ *
3970
+ * 它不依赖任何会起不来的东西:清单 `git ls-files` 失败退 walk,读文件失败有
3971
+ * 各自的码(`FileReadOutcome`)。权限层缺席时它照样在,只是读跳过那道判定。
3972
+ *
3973
+ * ## ⚠️ 它和 {@link sessionReferences} 是同一件事的两半
3974
+ *
3975
+ * 那一格让服务端列会话候选而**交不出任意 SQL**;这一格让服务端列文件候选、
3976
+ * 读正文而**交不出任意 `readFileSync`**。收窄的是同一句话 —— `@` 永远不比
3977
+ * `file_read` 宽松,而判定只有权限层那一份(`toolName: 'file_read'`)。
3978
+ * 服务端这里要读文件就得走 `workspaceFiles.readFile`,没有第二条路。
3979
+ */
3980
+ workspaceFiles: WorkspaceFilesView;
3207
3981
  /**
3208
3982
  * 每会话的工作区绑定([决定 18](../../../design/web-ui/README.md))。
3209
3983
  * `EpochRuntime.workspaces` 结构上满足它。
@@ -3238,6 +4012,54 @@ interface WebRuntimeView extends RuntimeCapabilities, RuntimeSecurity, RuntimeSe
3238
4012
  * 「校验不过就什么都不写」这条不变量会分散到两处去守。
3239
4013
  */
3240
4014
  schedules: ScheduleControlView;
4015
+ /**
4016
+ * 目标(方案 52 PR-4)。`EpochRuntime.goalCatalog` 结构上正好满足它 ——
4017
+ * **对不上就是两边长歪了,`cli/src/commands/web.ts` 那一行当场编译不过**。
4018
+ *
4019
+ * ## ⚠️ 借的是 `goalCatalog` 而**不是** `runtime.goals`
4020
+ *
4021
+ * 后者是**引导会话**那一份(`GoalControl`,收窄到一段会话)。拿它去答 URL 上
4022
+ * 那个 sessionId,就是上面 `sessionFactory` 那段 ⚠️ 讲的那条 bug 的形状 ——
4023
+ * 「编译得过、跑得通、只是答错会话」(方案 30 §9.5)。这一格从第一天起借的
4024
+ * 就是按 id 现查的那一份。
4025
+ *
4026
+ * ## ⚠️ 可为 null,而这和上面 `schedules` 那一格刻意不同
4027
+ *
4028
+ * 目标是**落盘的状态**:会话库起不来时它整个不存在,判据逐字同 TUI 那一侧的
4029
+ * 「会话库起不来时 `/goal` 压根不注册」。为 null 时那四条会话级端点回 503
4030
+ * `no-goal-store`(不是一份 `goal: null` —— 那是「这个会话没有目标」,
4031
+ * 两句话在界面上是两种画法),而 dock 那条角标端点回空数组。
4032
+ * 全文在 [goal/view.ts](./goal/view.js) 的文件头。
4033
+ *
4034
+ * ## ⚠️ 它也**不像检查点那样向工厂要**
4035
+ *
4036
+ * 目标没有内存态 —— `GoalService` 每次现读 SQLite,所以一段冷却掉的会话在这
4037
+ * 一格上和活着的一样准。走工厂那条路的话,用户打开一段昨天的会话会看到
4038
+ * 「这个会话还没有目标」,而库里那条正躺在那儿。那张三格对照表在
4039
+ * `goal/view.ts` 里。
4040
+ */
4041
+ goalCatalog: GoalCatalogView | null;
4042
+ /**
4043
+ * 插件那一页(方案 59 §六 E2,2026-08-21)。`EpochRuntime.plugins` 结构上正好
4044
+ * 满足它 —— **对不上就是两边长歪了,`cli/src/commands/web.ts` 那一行当场编译不过**。
4045
+ *
4046
+ * ## ⚠️ 可为 null,而这一档和 `goalCatalog` / `sessions` 那两格**不同**
4047
+ *
4048
+ * 那两个是「底下那层起不来了」(SQLite 没起来),这一个是**「宿主没打算给用户
4049
+ * 这条路」**:`buildRuntime()` 没收到 `hostMarketplaces` 时它就是 `null`,
4050
+ * 而那是刻意的 —— 它是一个真的新攻击面(往 `~/.epoch/plugins/` 里落东西,
4051
+ * 而落进去的 hook / `mcp.json` 下一程会起子进程),所以是**选进来**的,
4052
+ * 不是白送的(判据在 `build.ts` 那一格的 JSDoc 上)。
4053
+ *
4054
+ * 为 null 时六条端点一律 503 `no-plugin-control`(**不是**一份空列表 ——
4055
+ * 判据同 `no-goal-store`:那两句话在界面上是两种画法)。
4056
+ * ⚠️ **`epoch web` 今天就落在这一档上**,见 [plugin.ts](./plugin.js) 文件头那条 ⚠️。
4057
+ *
4058
+ * 借的是**一整片控制面**而不是几个方法,理由同 `schedules`:那一片本来就是一个
4059
+ * 整体(列 / 搜 / 预览 / 装 / 更新 / 卸 + 一格 `pendingRestart`),而拆成散字段
4060
+ * 之后「装完了、还没生效」这条不变量会分散到两处去守。
4061
+ */
4062
+ plugins: PluginControlView | null;
3241
4063
  dispose(): void;
3242
4064
  }
3243
4065
  interface ApiContext {
@@ -3675,10 +4497,12 @@ declare function toWireTask(sessionId: string, info: BackgroundTaskInfo, registr
3675
4497
  declare function collectTasks(sessionId: string, registry: TaskRegistryView): WireTasksResponse;
3676
4498
 
3677
4499
  /**
3678
- * 定时任务那九条端点([方案 45](../../../../.agents/plans/45-scheduled-automation-plan.md) PR-3)。
4500
+ * 定时任务那十条端点([方案 45](../../../../docs/verify/VERIFY_RECORD-45-automation.md)
4501
+ * PR-3 九条 + PR-4 那条 `pending`)。
3679
4502
  *
3680
4503
  * ```
3681
4504
  * GET /api/schedules 两个 tab 一次取齐
4505
+ * GET /api/schedules/pending 还欠着的那几张欠条(PR-4)
3682
4506
  * POST /api/schedules 建一条
3683
4507
  * GET /api/schedules/:id 一条的全部字段
3684
4508
  * PATCH /api/schedules/:id 改一条(含那个开关)
@@ -3904,4 +4728,4 @@ type CreateWebServerResult = {
3904
4728
  */
3905
4729
  declare function createWebServer(opts: CreateWebServerOptions): Promise<CreateWebServerResult>;
3906
4730
 
3907
- export { type ApiContext, type AuthGuardOptions, type BindDecision, type BindRequest, type BoundWorkspace, type CatalogDeletion, type CatalogHit, type CatalogRow, type CheckpointSummaryView, type CommandsView, type CreateSessionResult, type CreateWebServerOptions, type CreateWebServerResult, DEFAULT_MAX_ACTIVE_SESSIONS, DEFAULT_MAX_QUEUED_MESSAGES, DEFAULT_RING_CAPACITY, DEFAULT_WEB_HOST, DEFAULT_WEB_PORT, EnvelopeRing, type EpochWebServer, type ExpansionOutcome, type FrameSink, type HubSession, type HubSessionState, type LiveSessionView, MAX_DIFF_BYTES, MAX_DIFF_FILES, MAX_RECORDING_FRAMES, MAX_SKILL_BODY_BYTES, type ModelTurnsView, type QueuedMessage, type RewindFilePlanView, type RewindOutcomeView, type RewindPreviewView, type RewindResultView, type RuntimeCapabilities, type RuntimeCheckpoints, type RuntimeCommands, type RuntimeSecurity, type RuntimeSettings, type ScheduleCapabilityView, type ScheduleControlView, type ScheduleCreateView, type ScheduleFireView, type ScheduleRegisterView, type ScheduleSaveView, type ScheduleUpdateView, type SelectionView, type SessionCatalog, type SessionFactoryView, type SessionHistoryFacts, SessionHub, type SessionHubOptions, type SessionModelView, type SessionPermissionsView, type SessionPlanView, type SessionRoleView, type SetLevelView, type SetSelectionView, type StartResult, TASK_TAIL_BYTES, type TaskRegistryView, type TrustView, type TurnDecorator, UI_LANG_PARAM, UI_THEME_PARAM, type UiLangPref, type UiPreset, type UiThemePref, type WebRuntimeView, type WorkspaceBindFailure, type WorkspaceBindOutcome, type WorkspaceCheckpoints, type WorkspaceDiffOptions, type WorkspaceView, collectCapabilities, collectSecurity, collectSettings, collectTasks, collectWorkspaceDiff, createAuthGuard, createWebServer, decideBinding, defaultWebRoot, expandUserContent, firstScreenUrl, generateToken, readRewindInput, resolveArtifactPath, toWireCheckpoint, toWireCommand, toWirePreview, toWireRewindResult, toWireRow, toWireSkillBody, toWireTask, unavailableToHttp };
4731
+ export { type ApiContext, type AttachedMentions, type AuthGuardOptions, type BindDecision, type BindRequest, type BoundWorkspace, type CatalogDeletion, type CatalogHit, type CatalogRow, type CheckpointSummaryView, type CommandsView, type CreateSessionResult, type CreateWebServerOptions, type CreateWebServerResult, DEFAULT_MAX_ACTIVE_SESSIONS, DEFAULT_MAX_QUEUED_MESSAGES, DEFAULT_RING_CAPACITY, DEFAULT_WEB_HOST, DEFAULT_WEB_PORT, EnvelopeRing, type EpochWebServer, type ExpansionOutcome, type FrameSink, type HubSession, type HubSessionState, type LiveSessionView, MAX_CANDIDATES, MAX_DIFF_BYTES, MAX_DIFF_FILES, MAX_RECORDING_FRAMES, MAX_SKILL_BODY_BYTES, type ModelTurnsView, type PluginControlView, type QueuedMessage, type RewindFilePlanView, type RewindOutcomeView, type RewindPreviewView, type RewindResultView, type RuntimeCapabilities, type RuntimeCheckpoints, type RuntimeCommands, type RuntimeSecurity, type RuntimeSettings, type ScheduleCapabilityView, type ScheduleControlView, type ScheduleCreateView, type ScheduleFireView, type ScheduleRegisterView, type ScheduleSaveView, type ScheduleUpdateView, type SelectionView, type SessionCatalog, type SessionContextFacts, type SessionFactoryView, type SessionHistoryFacts, SessionHub, type SessionHubOptions, type SessionLoopView, type SessionModelView, type SessionPermissionsView, type SessionPlanView, type SessionReferencesView, type SessionRoleView, type SetLevelView, type SetSelectionView, type StartResult, TASK_TAIL_BYTES, type TaskRegistryView, type TrustView, type TurnDecorator, UI_LANG_PARAM, UI_THEME_PARAM, type UiLangPref, type UiPreset, type UiThemePref, type WebRuntimeView, type WorkspaceBindFailure, type WorkspaceBindOutcome, type WorkspaceCheckpoints, type WorkspaceDiffOptions, type WorkspaceView, attachMentions, collectCapabilities, collectSecurity, collectSettings, collectTasks, collectWorkspaceDiff, createAuthGuard, createWebServer, decideBinding, defaultWebRoot, expandUserContent, firstScreenUrl, generateToken, listFileCandidates, listReferenceCandidates, readRewindInput, resolveArtifactPath, toWireCheckpoint, toWireCommand, toWirePluginEntry, toWirePreview, toWireRewindResult, toWireRow, toWireSkillBody, toWireTask, unavailableToHttp };