@epoch-agent/server 0.10.1 → 0.12.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/README.md CHANGED
@@ -118,6 +118,7 @@ tarball 里真的有那些字节。
118
118
  | `POST /api/skills/stage` | 把手里那一份字节交上来 → 一条本机路径(**回环绑定才给**,见下) |
119
119
  | `POST /api/skills/remove` | `{name}` → 删掉一份用户级技能(**不可撤销;只有用户级删得掉**,见下) |
120
120
  | `POST /api/roles` | 新建一个身份,写进 `~/.epoch/agents/`(**回环绑定才给**,见下) |
121
+ | `POST /api/roles/remove` | `{name}` → 删掉一份用户级身份(**不可撤销;回环绑定才给**,见下) |
121
122
  | `GET /api/plugins` | 装着的 + 市场里有什么 + `pendingRestart`(**进程级**,方案 59 E2,见下) |
122
123
  | `POST /api/plugins/install/preview` | 「将安装什么」,不写字节(**只收 `<市场>/<插件>`**,见下) |
123
124
  | `POST /api/plugins/install` | `{ref, token}` → 真装(**回环绑定才给;装完不生效**,见下) |
@@ -785,10 +786,60 @@ frontmatter 逐格对上**(`mcp.json` 那条刻意收窄,这一条刻意不
785
786
  - **建完当场重载角色表**,不用重启:能力页刷新出来当场多一行,`delegate_task` 的
786
787
  枚举里也有它了。不重载的话这一下只做了一半(判据在
787
788
  [runtime/src/role-write.ts](../runtime/src/role-write.ts) 的文件头第二节)
788
- - ⚠️ **只有「建」,没有删也没有改**:`RoleWriteControlView` 上只有一个 `add`,
789
- 所以这个包**写不出**别的动作。要加 `remove` 先回去读 protocol 那份文件头第三节
790
- —— 删一个身份不会往 system prompt 里塞字,但会静默拿掉别人正依赖的一段边界,
791
- 那道闸门的判据和「建」不一样
789
+ - ⚠️ **这个文件里只有「建」**:`RoleWriteControlView` 上只有一个 `add`,所以
790
+ [src/role-add.ts](./src/role-add.ts) **写不出**别的动作。删那一条 2026-09-16
791
+ 开了,镜像和 handler 一起住在另一个文件里(下面那一节);**「改」照旧没有**,
792
+ 而那条叮嘱对它仍然成立 —— 先回去读 protocol 那份文件头第三节
793
+
794
+ #### `POST /api/roles/remove` —— **能加就得能删**(2026-09-16)
795
+
796
+ > handler 在 [src/role-remove.ts](./src/role-remove.ts)。**和上面那条分两个文件**,
797
+ > 判据同 `skill-remove.ts` 从 `skill-import.ts` 拆出来那次:镜像跟着 handler 走,
798
+ > 于是这个文件写不出 `add`、那个文件写不出 `remove`,而
799
+ > `RuntimeCapabilities.roleWrite` 是两张镜像的交集。
800
+
801
+ 起因是一句话,和技能那条逐字相同:这一栏原来只有「新建」,拿掉一个建错的身份
802
+ 得让用户自己去 `~/.epoch/agents/` 里翻目录 —— 而那一份里的 `description`
803
+ 是**每一轮都在进** `delegate_task` 工具描述的。
804
+
805
+ ##### ⚠️ 它吃**和「建」同一道**闸,而理由和 `POST /api/skills/remove` 刻意相反
806
+
807
+ 技能删除那条**不吃**闸门,判据逐字是「导入那条也不吃」(只给删加闸 =
808
+ `--host 0.0.0.0` 下技能变成「只能加不能删」)。身份这边照抄那条推理会得出
809
+ **相反**的结论,而那正是对的:**建那一侧本来就要回环**。于是删也要回环,
810
+ 一点不对称都不制造 —— 那一档下用户既建不出也删不掉,fail closed。
811
+
812
+ ⚠️ 哪天上面那道闸松了,这一道要跟着松,**两条一起动别只动半边**。判据全文在
813
+ [protocol/src/wire-agent-role.ts](../protocol/src/wire-agent-role.ts) 文件头第六节。
814
+
815
+ 五档拒绝各对**一个不同的下一步**(**403 之外全是 200 + `ok:false`**):
816
+
817
+ | `reason` | 什么情况 | 用户该做什么 |
818
+ | ----------- | ------------------------------- | ------------------------------------ |
819
+ | `missing` | 请求里没给名字 | (界面不该发出这一发) |
820
+ | `not-found` | 这个名字不在角色表里 | 手上那份清单旧了,刷新 |
821
+ | `not-user` | 它来自内置 / 项目 / 插件 / 宿主 | 那句话里点名了是哪一层,各有各的删法 |
822
+ | `ambiguous` | 目录里两份以上都叫这个名字 | 去那个目录里留下要的那一份 |
823
+ | `failed` | 真的删不动(权限 / 只读盘) | 去回执那条路径那儿看一眼 |
824
+
825
+ - **失败是 200 而不是 `addRole` 那样的 4xx**,分野在**界面形状**:那边是一张
826
+ 表单(失败落在表单底下,改一格再提交),这边是卡片上一枚按钮(失败落在那张卡
827
+ 自己那一行小字上)。`{error:{code,message}}` 那个形状在界面上会走到「请求坏了」
828
+ 那一支去。⚠️ **那道 403 是例外**:它答的不是「这一下没删成」,是「这条路在这一档
829
+ 下不开」
830
+ - **只有用户级删得掉**,而那道闸在 runtime(它才看得见合并后那张表)不在这一层。
831
+ 界面上那颗按钮只画在「我的」那一组的卡上,但那是**体贴,不是安全性质**:
832
+ 手搓一条 `POST /api/roles/remove {"name":"general"}` 会拿到 200 + `not-user`
833
+ - **没删成时 `name` / `path` / `takenOverBy` 是空的**,判据逐字同技能那条
834
+ - ⚠️ **`takenOverBy` 是这条路比技能那条多出来的那一格**:删掉的要是一个**同名
835
+ 覆盖**,下层那一份会浮上来 —— 能力页刷新出来那一行**还在**(只是换了组)。
836
+ 不下发这一格的话,界面对用户说「已经删掉 general」而列表里 general 还在,
837
+ 也就是一次成功的删除长成了一次失败的删除。算它要查**重载之后**那张合并表,
838
+ 所以这一层原样转发、一个字都不判
839
+ - ⚠️ **「谁正用着它」这条路不查**,那是一个判断不是遗漏:服务端只数得到活着的
840
+ 会话,数不到磁盘上休眠的那些,而报一个半真的数字比不报更糟。说清后果那件事
841
+ 归界面上那一问(判据在 protocol 那份文件头 6.3)
842
+ - **删完当场重载角色表**,同上面那条
792
843
 
793
844
  #### `/api/plugins*` 那六条 —— 插件那一页(方案 59 §六 E2,2026-08-21)
794
845
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Lang, WireSkillImportFailure, WireSkillImportForm, WireSkillRemoveFailure, WireSkillStageFailure, WireProjectInventory, 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, WirePermissionRememberFailure, WirePluginSourceType, WirePluginVisibility, WirePluginStatus, WirePluginRefuseReason, WirePluginUploadFieldName, WirePluginUploadFields, 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, WireSettingValueKind, WireSettingsResponse, EpochConfig, TokenUsage, 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';
1
+ import { Lang, AgentRoleSource, WireRoleRemoveFailure, WireSkillImportFailure, WireSkillImportForm, WireSkillRemoveFailure, WireSkillStageFailure, WireProjectInventory, 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, WirePermissionRememberFailure, WirePluginSourceType, WirePluginVisibility, WirePluginStatus, WirePluginRefuseReason, WirePluginUploadForm, WirePluginUploadFieldName, WirePluginUploadFields, 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, WireSettingValueKind, WireSettingsResponse, EpochConfig, TokenUsage, WireCompressionLine, WireProviderSummary, WireToolSummary, Diagnostic, WireReplayMessage, WireFileChange, WireAuthGuard, WireWorkspaceDiffResponse, BackgroundTaskInfo, WireTasksResponse, WireBackgroundTask, WireScheduleRow } from '@epoch-agent/protocol';
2
+ import { SerializableApprovalRequest, SerializableQuestionRequest, RunningSchedule, ApprovalRevokeResult, LevelChangeResult, LastOpenSessionControl, WorkspaceFilesView } from '@epoch-agent/runtime';
3
3
  import { ServerResponse } from 'node:http';
4
4
 
5
5
  /**
@@ -491,13 +491,19 @@ type RoleAddResultView = {
491
491
  /**
492
492
  * `RoleWriteControl`(runtime)在服务端眼里的样子 —— **只有 `add` 这一个动作**。
493
493
  *
494
- * 镜像里只有它,于是这个包**写不出**「删一个身份」「改一个身份」:那不是靠
495
- * review 盯住的,是编译期的事(同 `McpControlView` 只有两个方法、
496
- * `SkillImportControlView` 只有两个动作)。
494
+ * 镜像里只有它,于是**这个文件里写不出「删一个身份」**:那不是靠 review 盯住的,
495
+ * 是编译期的事(同 `McpControlView` 只有两个方法、`SkillImportControlView`
496
+ * 只有两个动作)。
497
497
  *
498
- * ⚠️ 要加 `remove` 的时候**先回去读 protocol 那份文件头第三节**:删一个身份不会
499
- * 往 system prompt 里塞字,但会**静默拿掉**别人正依赖的一段边界 ——
500
- * 那道闸门的判据和「建」不一样,是一次独立的判断。
498
+ * > ⚠️ **2026-09-16:删那条路开了,但它在另一个文件里**
499
+ * > ([role-remove.ts](./role-remove.js) 的 `RoleRemoveControlView`)。
500
+ * > **镜像跟着 handler 走**,判据逐字同 `skill-remove.ts` 从 skill-import 拆出来
501
+ * > 那次:这个文件写不出 `remove`、那个文件写不出 `add`,而
502
+ * > `RuntimeCapabilities.roleWrite` 是两张镜像的交集。
503
+ * >
504
+ * > 这儿原来那条「要加 `remove` 先回去读 protocol 那份文件头第三节」的叮嘱
505
+ * > **已经兑现**:答案在那份文件头新的第六节(删吃和建**同一道**回环闸,
506
+ * > 理由和技能那条刻意相反)。**`edit` 那一半照旧没有,那条叮嘱对它仍然成立。**
501
507
  */
502
508
  interface RoleWriteControlView {
503
509
  /**
@@ -514,6 +520,96 @@ interface RoleWriteControlView {
514
520
  add(input: RoleAddView, lang?: Lang): RoleAddResultView;
515
521
  }
516
522
 
523
+ /**
524
+ * 「删掉一个身份」那条端点 —— `POST /api/roles/remove`(2026-09-16)。
525
+ *
526
+ * ```
527
+ * POST /api/roles/remove {name} → 删掉 ~/.epoch/agents/ 里定义着它的那一份 md
528
+ * ```
529
+ *
530
+ * 起因是一句话,和技能那条逐字相同:**能加就得能删**。身份那一栏 2026-08-18 长出
531
+ * 了「新建」([role-add.ts](./role-add.js)),而拿掉一个建错的身份得让用户自己去
532
+ * `~/.epoch/agents/` 里翻目录 —— 「加一个入口、不给出口」正是决定 20 ① 骂的那一族。
533
+ *
534
+ * ## 一、⚠️ 它**吃那道回环闸**,而理由和 `POST /api/skills/remove` **刻意相反**
535
+ *
536
+ * 技能删除那条**不吃**闸门,判据逐字是「导入那条也不吃」——只给删除加一道闸的
537
+ * 结果是 `--host 0.0.0.0` 下技能变成「只能加不能删」,那正是那一轮要修的病。
538
+ *
539
+ * 身份这边照抄那条推理会得出**相反**的结论,而那正是对的:
540
+ * **加那一侧本来就要回环**(`POST /api/roles` 那一道 403)。于是删也要回环,
541
+ * 一点不对称都不制造 —— 那一档下用户既建不出也删不掉。fail closed。
542
+ *
543
+ * ⚠️ 哪天上面那道闸松了,这一道要跟着松,**两条一起动别只动半边**。
544
+ * 判据全文在 protocol 的 [wire-agent-role.ts](../../protocol/src/wire-agent-role.ts)
545
+ * 文件头第六节。
546
+ *
547
+ * ## 二、⚠️ 那道 403 在**读请求体之前**,别挪进路由层
548
+ *
549
+ * 逐字同 `addRole`:被拒的那一发连正文都不该被解析。而路由那一层认的是
550
+ * 「哪条路」,不是「这一发准不准发」。
551
+ *
552
+ * ## 三、失败是 **200 + `ok:false`**,而 `addRole` 是 400 / 409 —— 分野在界面形状
553
+ *
554
+ * |   | 界面上是什么 | 一次失败落在哪儿 |
555
+ * | -------- | ---------------- | ------------------------------------ |
556
+ * | 新建 | 一张表单 | 表单底下那一句,用户改一格再提交一次 |
557
+ * | 删除 | 卡片上一枚按钮 | **那张卡自己**那一行小字 |
558
+ *
559
+ * `{error:{code,message}}` 那个形状在界面上会走到「请求坏了」那一支去
560
+ * (判据逐字同 `removeSkill`)。⚠️ **上面那道 403 是例外,它仍旧是 `sendError`**:
561
+ * 它答的不是「这一下没删成」,是「这条路在这一档下不开」。
562
+ *
563
+ * ## 四、⚠️ 只有**用户级**删得掉,而那道闸不在这一层
564
+ *
565
+ * 内置 / 项目 / 插件 / 宿主四档由 runtime 的 `RoleWriteControl.remove` 判成
566
+ * `not-user`,并带一句点名是哪一层的话(那四档出路各不相同)。这一层
567
+ * **不自己判 source** —— 判两遍会有一天判得不一样,而只有那一层看得见合并后
568
+ * 那张表(判据逐字同 `skill-remove.ts` 文件头第三节)。
569
+ *
570
+ * 界面上那颗按钮只画在用户级那些卡上,但那是**体贴,不是安全性质**:
571
+ * 手搓一条 `POST /api/roles/remove {"name":"general"}` 会拿到 200 + `ok:false`
572
+ * + `not-user`。
573
+ *
574
+ * ## 五、`takenOverBy` 原样转发,这一层一个字都不判
575
+ *
576
+ * 那一格答的是「删完之后这个名字还在不在」(同名覆盖档下,下层那一份会浮上来,
577
+ * 于是界面上那一行**不会消失**)。算它要查重载之后那张合并表,只有 runtime
578
+ * 那一层做得到 —— 判据全文在 `WireRoleRemoveResponse.takenOverBy` 上。
579
+ */
580
+
581
+ /** 删成了那一下回的东西(runtime 的 `RoleRemoveOutcome` 成功那一支) */
582
+ interface RoleRemoveDoneView {
583
+ name: string;
584
+ path: string;
585
+ /** 删完之后谁接管了这个名字。`null` = 这个名字真的没了 */
586
+ takenOverBy: AgentRoleSource | null;
587
+ }
588
+ type RoleRemoveResultView = ({
589
+ ok: true;
590
+ } & RoleRemoveDoneView) | {
591
+ ok: false;
592
+ reason: WireRoleRemoveFailure;
593
+ detail: string;
594
+ };
595
+ /**
596
+ * `RoleWriteControl`(runtime)在这条 handler 眼里的样子 —— **只有 `remove`**。
597
+ *
598
+ * 和 [role-add.ts](./role-add.js) 那张镜像刻意分开,判据逐字同 `skill-remove.ts`:
599
+ * 镜像跟着 handler 走,于是**这个文件里写不出 `add`**,而
600
+ * `RuntimeCapabilities.roleWrite` 是两张镜像的交集。
601
+ */
602
+ interface RoleRemoveControlView {
603
+ /**
604
+ * @param lang 这一次的 `detail` 用哪个语言说(方案 58)。⚠️ **镜像上这一格是
605
+ * 必须的**:五档拒绝的话全在 runtime / core 里渲染,这一层只转发 ——
606
+ * 少声明它的话 `EpochRuntime` 照样满足这份镜像(多一个可选参数是
607
+ * 宽化),而这一层就递不出去,一声不吭地回中文。判据同
608
+ * `RoleWriteControlView.add` 上那条 ⚠️
609
+ */
610
+ remove(name: string, lang?: Lang): RoleRemoveResultView;
611
+ }
612
+
517
613
  /**
518
614
  * 从本机目录导入技能的两条端点(方案 42 §六,2026-08-18):
519
615
  *
@@ -982,17 +1078,22 @@ interface RuntimeCapabilities {
982
1078
  */
983
1079
  readonly mcp: McpControlView & McpConfigControlView;
984
1080
  /**
985
- * 身份那一栏的写口:建一个(2026-08-18)。**进程级**,同上面那两条。
1081
+ * 身份那一栏的写口:建一个(2026-08-18)+ 删一个(2026-09-16)。
1082
+ * **进程级**,同上面那两条。
1083
+ *
1084
+ * ## ⚠️ 为什么是交集,不是一份镜像
986
1085
  *
987
- * 镜像和 handler 一起住在 [role-add.ts](./role-add.ts)(那边的文件头有全部
988
- * 判据)—— 这一格只是把它挂进 `RuntimeCapabilities`,因为**装配那一行是在
989
- * 这儿对形状的**(同 `mcp` 那一条)。
1086
+ * 判据逐字同上面 `mcp` 那一格:两个方法挂在**同一个** runtime 对象上
1087
+ * (`runtime.roleWrite`),分成两份声明是为了让**镜像跟着 handler 走** ——
1088
+ * [role-add.ts](./role-add.ts) 里写不出 `remove`,[role-remove.ts](./role-remove.ts)
1089
+ * 里写不出 `add`,而改哪条端点的人要读的判据就在那一个文件里。
990
1090
  *
991
- * ⚠️ 那条端点有一道**回环绑定**的闸门(`ctx.lanExposed` 为真就 403),
1091
+ * ⚠️ **两条端点吃的是同一道**回环绑定闸门(`ctx.lanExposed` 为真就 403),
992
1092
  * 而它**不在这份镜像上** —— 镜像答的是「runtime 借得到什么」,闸门答的是
993
- * 「这一发准不准发」。判据全文在 protocol 的 `wire-agent-role.ts` 文件头。
1093
+ * 「这一发准不准发」。判据全文在 protocol 的 `wire-agent-role.ts` 文件头
1094
+ * (建那一道在第四节,删那一道**为什么不照抄技能那条**在第六节)。
994
1095
  */
995
- readonly roleWrite: RoleWriteControlView;
1096
+ readonly roleWrite: RoleWriteControlView & RoleRemoveControlView;
996
1097
  }
997
1098
  /**
998
1099
  * 三栏一次取齐。
@@ -1084,7 +1185,30 @@ interface HubSession {
1084
1185
  signal?: {
1085
1186
  readonly aborted: boolean;
1086
1187
  };
1188
+ /**
1189
+ * 「跑到一半插一句话」的取用口(2026-09-16)。Hub 拿自己那条消息队列
1190
+ * 做一个递进来(`SessionHub.steerFrom`),引擎每次模型请求成形之前拉一次。
1191
+ *
1192
+ * ⚠️ **结构类型,不写 `SteerSource`**:这个文件在依赖链的最底下(文件头
1193
+ * 那条「断开循环依赖」),从这儿 import runtime 会把环接回来。形状由
1194
+ * `AgentSession.run` 的签名在编译期对齐 —— 走散了 `hub.ts` 那一行当场红。
1195
+ */
1196
+ steer?: {
1197
+ pending(): boolean;
1198
+ take(): EpochUserContent[];
1199
+ };
1087
1200
  }): AsyncGenerator<AgentEvent>;
1201
+ /**
1202
+ * 在飞那一轮已经落盘到哪条消息了(按步增量落盘,2026-09-16);
1203
+ * 没有开着的轮时 `null`。`AgentSession.persistedThrough` 结构上满足它。
1204
+ *
1205
+ * **可选**,而这一档是真话不是偷懒:上面那句注释说的「脚本化的假会话」
1206
+ * 一个也不会落盘,纯内存模式的真会话(没有 store)也答不出这个数。
1207
+ * 答不出时 Hub 就不记水位线,于是 `GET /messages` 不带
1208
+ * `inFlightThroughSeq` —— 浏览器退回 2026-09-16 之前那条路(用自己攒的
1209
+ * 那半截),**那条路今天仍然是对的**,只是看不见在飞的那一轮。
1210
+ */
1211
+ persistedThrough?(): number | null;
1088
1212
  }
1089
1213
  /** SSE 连接从 Hub 收帧的出口 */
1090
1214
  type FrameSink = (frame: WireEnvelope) => void;
@@ -1549,10 +1673,34 @@ interface SessionHubOptions {
1549
1673
  * 让调用方能说清楚是哪一种拒绝。
1550
1674
  */
1551
1675
  type StartResult =
1552
- /** 收下了。`queued` = 排在当前这一轮后面,还没开始跑 */
1676
+ /**
1677
+ * 收下了。`queued` = 排在当前这一轮后面,还没开始跑。
1678
+ *
1679
+ * `steered` 把 `queued: true` 那一档再劈成两半(2026-09-16):
1680
+ *
1681
+ * | 两格 | 含义 |
1682
+ * | --------------- | -------------------------------------------------------- |
1683
+ * | `queued: false` | 会话本来空着,这条消息**立刻**开了一轮 |
1684
+ * | `steered: true` | 正在跑的那一轮会在下一次模型请求前把它拌进去(插进本轮) |
1685
+ * | `steered: false`| 排在后面,等这一轮跑完才轮到它(**开新的一轮**) |
1686
+ *
1687
+ * ## 为什么必须分出第三格,而不是让界面自己猜
1688
+ *
1689
+ * 「插进本轮」和「排在后面」对用户是两件事:前者是「它马上会看到」,
1690
+ * 后者是「它得先把手头这摊做完」。`queued: true` 一格答不了这个问题,
1691
+ * 而界面**猜不出来** —— 判据有两条都在服务端手上:队列里排在它前面的那几条
1692
+ * 是不是都能折进去,以及它自己是不是一条带轮级设置的斜杠命令
1693
+ * (判据全文在 `SessionHub.steerFrom` 上)。
1694
+ *
1695
+ * ⚠️ **它是「会被拌进去」而不是「已经拌进去了」。** 真的进上下文那一刻由事件流
1696
+ * 上的 `steer` 那一帧宣布(protocol 的 `AgentEvent`)—— 这一轮在那之前撞上
1697
+ * `maxTurns` 或者被中止时,它会退回队列 / 交还给用户,那时这一格就说早了。
1698
+ * 界面据它选措辞可以,据它把消息画成「已送达」不行。
1699
+ */
1553
1700
  {
1554
1701
  ok: true;
1555
1702
  queued: boolean;
1703
+ steered?: boolean;
1556
1704
  } | {
1557
1705
  ok: false;
1558
1706
  reason: 'unknown-session';
@@ -1634,6 +1782,19 @@ declare class SessionHub {
1634
1782
  has(sessionId: string): boolean;
1635
1783
  /** 这个 id 是被冷却掉的(而不是从来没存在过)吗 */
1636
1784
  isCooled(sessionId: string): boolean;
1785
+ /**
1786
+ * 在飞那一轮的落盘水位线 `(消息 id, 广播序号)`;没在跑 / 答不出时 `null`
1787
+ * (按步增量落盘,2026-09-16)。
1788
+ *
1789
+ * 唯一调用方是 `GET /api/sessions/:id/messages`([api.ts](./api.ts) 的
1790
+ * `listMessages`):它按 `id` 截断回放、把 `seq` 交给浏览器去重。
1791
+ * 判据全文在 `SessionEntry.persisted` 和
1792
+ * `WireMessageListResponse.inFlightThroughSeq` 上。
1793
+ */
1794
+ persistedThrough(sessionId: string): {
1795
+ id: number;
1796
+ seq: number;
1797
+ } | null;
1637
1798
  /** 不认识的会话按 idle 报 —— 调用方要区分的话先问 `has()` */
1638
1799
  stateOf(sessionId: string): WireTurnState;
1639
1800
  /** Hub 手里这些会话此刻的样子。Map 的插入序 = 注册序 */
@@ -1715,15 +1876,37 @@ declare class SessionHub {
1715
1876
  */
1716
1877
  start(sessionId: string, message: EpochUserContent, decorate?: TurnDecorator): StartResult;
1717
1878
  /**
1718
- * 中止本轮,并**丢掉排在后面的消息**。
1879
+ * 中止本轮,并**把排在后面的消息交还给调用方**。
1880
+ *
1881
+ * 不接着跑它们是刻意的,那句判据没变:用户按下「停」要的是「现在别干了」,
1882
+ * 而不是「停掉这一轮然后立刻开始下一轮」—— 后者的表现是按了停之后 agent
1883
+ * 又动起来了。
1719
1884
  *
1720
- * 排队的一起丢是刻意的:用户按下「停」要的是「现在别干了」,而不是
1721
- * 「停掉这一轮然后立刻开始下一轮」—— 后者的表现是按了停之后 agent 又动起来了。
1885
+ * ## ⚠️ 2026-09-16:从「丢掉」改成「交还」
1722
1886
  *
1723
- * @returns 是否真有一轮在跑(**只看这个**,丢掉的队列不算)。
1724
- * `false` 表示这一轮本来就没在跑,不是失败
1887
+ * 在这之前这里是 `entry.queue.length = 0`,一句话都不说。那一档在排队时代
1888
+ * 勉强说得过去(用户刚发出去、还没上屏几秒);而「跑到一半插一句话」落地
1889
+ * 之后它变成一个真会咬人的形状:用户盯着屏幕打了一整段修正意见、发出去、
1890
+ * 看见模型还在往错的方向跑,于是顺手按了停 —— **那段话就没了**,
1891
+ * 输入框是空的,剪贴板里也没有。
1892
+ *
1893
+ * 所以现在原样交出来,由宿主放回输入框。codex 的同一处叫
1894
+ * `submit_pending_steers_after_interrupt`(`tui/src/chatwidget/input_queue.rs`),
1895
+ * 它在「退回输入框」和「整批重提成新一轮」之间可切;这里只做前者 ——
1896
+ * 后者是「我按了停它却又跑起来了」的另一种形状。
1897
+ *
1898
+ * **只交还没被引擎取走的那几条**:已经被 {@link steerFrom} 拌进上下文的那些
1899
+ * 早就不在这个数组里了,它们已经是这段对话的一部分(库里有、屏幕上有),
1900
+ * 再交还一次就成了重复。这一条不靠判断保证,靠的是两条路取同一个数组。
1901
+ *
1902
+ * @returns `running` = 是否真有一轮在跑(**只看这个**,交还的队列不算);
1903
+ * `false` 表示这一轮本来就没在跑,不是失败。
1904
+ * `returned` = 交还的消息,按用户发送的先后;没有就是空数组。
1725
1905
  */
1726
- abort(sessionId: string): boolean;
1906
+ abort(sessionId: string): {
1907
+ running: boolean;
1908
+ returned: EpochUserContent[];
1909
+ };
1727
1910
  /** 补拉挂起的审批 —— 新连上来的页面靠它把弹层恢复出来(§4.3) */
1728
1911
  listPending(sessionId: string): SerializableApprovalRequest[];
1729
1912
  /** 补拉挂起的提问(方案 34 验收 9)。与 `listPending` 同构 */
@@ -1798,6 +1981,13 @@ declare class SessionHub {
1798
1981
  * | --- | --- | --- |
1799
1982
  * | `POST /api/sessions`(`workspace/handlers.ts`) | **只在 `outcome.created`** | 复用那一支没多出一行来,发了等于让所有标签页白重取一次 |
1800
1983
  * | `DELETE /api/sessions/:id`(`api.ts`) | 删成了就发 | 不发的话**别的**标签页会一直挂着一行已经没了的会话,点进去才拿 404 |
1984
+ * | `PATCH /api/sessions/:id`(`api.ts`,2026-09-16) | **只在 `archived` 真的翻了** | 列表默认不列归档的那几段,所以归档 = 少一行、取回 = 多一行。判据全文在那个调用点上 |
1985
+ *
1986
+ * ⚠️ 第三个产地**不在「进出 Hub」这条路上**,而上一版这段话正是按那条路数的
1987
+ * (原文「那是第一版……`hub-register-sites.test.ts` 已经钉住了会话只能从那三处
1988
+ * 变活」)。钉住的是**活性**,而这一帧的宾语是**列表端点答出来的那几行** ——
1989
+ * 两者在归档这一档上分叉:一段会话被收进归档时进出 Hub 的表一个字没动,
1990
+ * 可侧栏那份列表少了一行。别再拿「经不经过 Hub」当这一帧的判据。
1801
1991
  *
1802
1992
  * 另外两处刻意**不发**:
1803
1993
  *
@@ -1861,6 +2051,32 @@ declare class SessionHub {
1861
2051
  * 序列化那一层进出,而真正跑工具的 `run()` 在它外面,等于整层白做。
1862
2052
  */
1863
2053
  private drive;
2054
+ /**
2055
+ * 把这个会话的队列做成引擎能拉的样子 —— 「跑到一半插一句话」(2026-09-16)。
2056
+ *
2057
+ * 队列还是原来那一条(`entry.queue`),这里只是**多开一个取用口**:引擎在每
2058
+ * 次模型请求成形之前来拉一次,拉到就拌进这一轮的上下文。拉不到、或者这一轮
2059
+ * 到了 `maxTurns`,那几条原样留在队列里,由 {@link drainQueue} 按老路开下一轮。
2060
+ * 两条路取的是同一个数组,所以**不会有一条消息被送进去两遍**。
2061
+ *
2062
+ * ## ⚠️ 带 `decorate` 的那种**不许折进当前这一轮**
2063
+ *
2064
+ * `decorate` 是**轮级**的(自定义斜杠命令的工具收窄、临时换模型,见
2065
+ * {@link TurnDecorator})。把一条 `/xxx` 命令折进正在跑的这一轮,它要求的收窄
2066
+ * 就整个落空了 —— 而收窄落空的语义是「本该只给三个工具的那一轮,拿到了全部
2067
+ * 工具」。所以这里只取**队首那一段连续的、没有 `decorate` 的**消息,遇到第一条
2068
+ * 带 `decorate` 的就停手,让它去开自己那一轮。
2069
+ *
2070
+ * codex 在同一处的说法是 `NotSubmittedReason::ActiveTurnNotSteerable` ——
2071
+ * 它的不可插入轮是 `Review` / `Compact`,理由同构:那两种轮次自带一套只属于
2072
+ * 它们的设置。
2073
+ *
2074
+ * ## 为什么 `pending()` 不许取走
2075
+ *
2076
+ * 判据全文在 core 的 `SteerSource` 上:到顶的那一轮取走了却跑不了,
2077
+ * 那句话就凭空蒸发了。这里两个方法因此算的是同一段前缀,只有 `take()` 动数组。
2078
+ */
2079
+ private steerFrom;
1864
2080
  /**
1865
2081
  * 一轮跑完,把排在后面的那条接上(方案 30 §2.3 第 4 条)。
1866
2082
  *
@@ -2869,6 +3085,21 @@ type UploadPreviewOutcomeView = {
2869
3085
  ok: true;
2870
3086
  plan: PackPlanView;
2871
3087
  token: string;
3088
+ /**
3089
+ * 那张表单每一格**从什么值开始**(2026-09-15)。
3090
+ *
3091
+ * ⚠️ **这一层原样转,一格都不算**(口径同这个文件头那句「一条策略判断
3092
+ * 都没有」):四档优先级(宿主锁死 > 宿主初值 > 包里同名格 > 空)在
3093
+ * runtime 的 `resolveUploadPrefill` 上,在这儿兜一次的表现是同一格
3094
+ * 在两处得出两个值。
3095
+ *
3096
+ * ⚠️ 键取自 `WirePluginUploadFieldName` 而不是自造一套:这一份最终原样
3097
+ * 落到 `WirePluginUploadPreviewResponse.prefill` 上,两套名字会逼出一张
3098
+ * 会错的映射表(判据同 runtime 的 `PLUGIN_UPLOAD_FIELDS`)。
3099
+ */
3100
+ prefill: WirePluginUploadForm;
3101
+ /** {@link prefill} 里值是从包里读出来的那几格。只管屏幕上那一句提示 */
3102
+ fromManifest: readonly WirePluginUploadFieldName[];
2872
3103
  } | RefusalView;
2873
3104
  /**
2874
3105
  * 上传的结局。
@@ -3259,6 +3490,13 @@ interface ScheduleControlView {
3259
3490
  runs: (id: string, limit?: number) => ScheduleRun[];
3260
3491
  /** 全部任务的最近运行 —— 「运行记录」那个 tab 的素材 */
3261
3492
  recentRuns: (limit?: number) => ScheduleRun[];
3493
+ /**
3494
+ * **此刻**哪几条在跑(2026-09-15)。素材是那张锁表,判定(心跳 + pid)留在 core。
3495
+ *
3496
+ * ⚠️ **它和上面两格答的不是同一个问题**:那两格要等一轮**跑完**才有行
3497
+ * (执行器收尾时才 `recordRun`),所以「正在跑」在它们眼里是不存在的。
3498
+ */
3499
+ running: () => readonly RunningSchedule[];
3262
3500
  capability: () => Promise<ScheduleCapabilityView>;
3263
3501
  create: (input: ScheduleCreateView, opts?: {
3264
3502
  register?: boolean;
@@ -4252,6 +4490,13 @@ interface CatalogRow {
4252
4490
  workspaceState?: 'bound' | 'none';
4253
4491
  /** `workspaceState === 'bound'` 那一档的目录 */
4254
4492
  workspaceRoot?: string;
4493
+ /**
4494
+ * 被收进归档了吗(方案 64 PR-2)。
4495
+ *
4496
+ * **必填**:这一列从会话库 v1 起就有、`NOT NULL DEFAULT 0`,
4497
+ * 每一行都答得出。上面那两格可选说的是另一件事(v7 之前的老行没记过)。
4498
+ */
4499
+ archived: boolean;
4255
4500
  }
4256
4501
  /** 一条 FTS5 命中 —— runtime 的 `SessionHit` 结构上满足它 */
4257
4502
  interface CatalogHit {
@@ -4273,9 +4518,17 @@ interface CatalogDeletion {
4273
4518
  * 起不来(SQLite 挂了)时整个是 `null`,列表退化成「只有 Hub 里那几个」。
4274
4519
  */
4275
4520
  interface SessionCatalog {
4276
- list(limit?: number): CatalogRow[];
4521
+ /**
4522
+ * ⚠️ `archived` 是三态(方案 64 PR-2):不给 = 只列没归档的,
4523
+ * `true` = **只**列归档的。判据同 runtime 的 `SessionControl.list`。
4524
+ */
4525
+ list(limit?: number, opts?: {
4526
+ archived?: boolean;
4527
+ }): CatalogRow[];
4277
4528
  get(sessionId: string): CatalogRow | null;
4278
4529
  rename(sessionId: string, title: string): CatalogRow | null;
4530
+ /** 收进归档 / 取回。**不是删除** —— 判据在 runtime 的 `SessionControl.setArchived` 上 */
4531
+ setArchived(sessionId: string, archived: boolean): CatalogRow | null;
4279
4532
  delete(sessionId: string): Promise<CatalogDeletion>;
4280
4533
  search(query: string, limit?: number): CatalogHit[];
4281
4534
  }
@@ -4378,6 +4631,16 @@ interface SettingWriteView {
4378
4631
  futile?: SettingFutileView;
4379
4632
  apply: WireSettingApply;
4380
4633
  }
4634
+ /**
4635
+ * runtime 的 `SettingPending` 的镜像 —— 「写进盘了,而那个进程还没在用」(2026-09-16)。
4636
+ *
4637
+ * 引擎侧那个类型是 `SettingWriteTarget` 加一个 `value`,所以这里也是
4638
+ * {@link SettingWriteView} 加一个 `value`:形状从哪一头改都得改两处,而两处都在这个
4639
+ * 文件里,扫一眼就对得上。
4640
+ */
4641
+ interface SettingPendingView extends SettingWriteView {
4642
+ value: WireSettingValue;
4643
+ }
4381
4644
  /**
4382
4645
  * runtime 的 `EffectiveSetting` 的镜像。
4383
4646
  *
@@ -4400,6 +4663,14 @@ interface SettingRowView {
4400
4663
  */
4401
4664
  valueKind?: WireSettingValueKind;
4402
4665
  choices?: readonly string[];
4666
+ /**
4667
+ * 这一行写进盘了、而引擎那个进程还没在用的那个值(2026-09-16)。
4668
+ *
4669
+ * ⚠️ **它和 `value` 不是二选一,两格一起发。** 前者是「引擎此刻按的是什么」,
4670
+ * 这一格是「盘上现在是什么」—— 这一层要做的只是原样转发,别在投影里替界面
4671
+ * 二选一(判据在 protocol 的 `WireSettingPending` 上)。
4672
+ */
4673
+ pending?: SettingPendingView;
4403
4674
  }
4404
4675
  /** runtime 的 `SettingWriteReport` 的镜像。`reason` 是码,不是句子 —— 见文件头第 1 条 */
4405
4676
  type SettingWriteReportView = {
@@ -4617,6 +4888,16 @@ interface WebRuntimeView extends RuntimeCapabilities, RuntimeSecurity, RuntimeSe
4617
4888
  */
4618
4889
  sessionStore: {
4619
4890
  loadMessages(sessionId: string): WireReplayMessage[];
4891
+ /**
4892
+ * 库里那格「这一轮开着」的标记;没有就 null(按步增量落盘,2026-09-16)。
4893
+ * runtime 的 `SessionStore.openTurnFrom` 结构上正好满足它。
4894
+ *
4895
+ * ⚠️ 它**不是**「正在跑」—— 那个问题问 `hub.persistedThrough()`。这一格
4896
+ * 活过进程重启,所以「它非空而 Hub 手上没有这个会话」恰好就是
4897
+ * 「上一个进程没能给那一轮收尾」,也就是 `interruptedTurnFrom` 那一档。
4898
+ * 判据全文在 `SessionMeta.openTurnFrom` 上。
4899
+ */
4900
+ openTurnFrom(sessionId: string): number | null;
4620
4901
  /**
4621
4902
  * 这个会话在工作区里改过哪些文件(方案 42 PR-4)。
4622
4903
  *
@@ -4704,6 +4985,22 @@ interface WebRuntimeView extends RuntimeCapabilities, RuntimeSecurity, RuntimeSe
4704
4985
  * 而 DELETE / PATCH 回 503 —— 不是假装成功。
4705
4986
  */
4706
4987
  sessions: SessionCatalog | null;
4988
+ /**
4989
+ * 「这个项目上次停在哪段会话」(2026-09-14)。`EpochRuntime.lastOpenSession`
4990
+ * 结构上正好满足它。
4991
+ *
4992
+ * ## ⚠️ 可选,而这一档和 `sessions` 那个 `| null` 不是同一种「没有」
4993
+ *
4994
+ * 那一格是「SQLite 起不来」(这个进程有这项能力,只是坏了)。这一格是
4995
+ * **自己手拼 `WebRuntimeView` 的宿主压根没给** —— 嵌入宿主和
4996
+ * `__tests__/harness.ts` 里那份假 runtime 都是手拼的,写成必填等于这一笔
4997
+ * 改动把它们全打断,而它们不给这一格照样能跑(首屏退回引导会话,逐字等于
4998
+ * 这一轮之前的行为)。
4999
+ *
5000
+ * 没给时:`GET /api/config` 不带 `lastSessionId`,`PUT /api/last-session`
5001
+ * 回 503 —— **不是假装记下来了**(同 `sessions` 为 null 时那两条的口径)。
5002
+ */
5003
+ lastOpenSession?: LastOpenSessionControl;
4707
5004
  /**
4708
5005
  * `@:` 引用另一段会话那一片(方案 53 PR-4)。SQLite 起不来时为 null。
4709
5006
  * `EpochRuntime.sessionReferences` 结构上正好满足它。
@@ -5467,12 +5764,13 @@ declare function toWireTask(sessionId: string, info: BackgroundTaskInfo, registr
5467
5764
  declare function collectTasks(sessionId: string, registry: TaskRegistryView): WireTasksResponse;
5468
5765
 
5469
5766
  /**
5470
- * 定时任务那十条端点([方案 45](../../../../docs/verify/VERIFY_RECORD-45-automation.md)
5471
- * PR-3 九条 + PR-4 那条 `pending`)。
5767
+ * 定时任务那十一条端点([方案 45](../../../../docs/verify/VERIFY_RECORD-45-automation.md)
5768
+ * PR-3 九条 + PR-4 那条 `pending` + 2026-09-15 那条 `running`)。
5472
5769
  *
5473
5770
  * ```
5474
5771
  * GET /api/schedules 两个 tab 一次取齐
5475
5772
  * GET /api/schedules/pending 还欠着的那几张欠条(PR-4)
5773
+ * GET /api/schedules/running 此刻哪几条在跑(2026-09-15)
5476
5774
  * POST /api/schedules 建一条
5477
5775
  * GET /api/schedules/:id 一条的全部字段
5478
5776
  * PATCH /api/schedules/:id 改一条(含那个开关)