@epoch-agent/server 0.20.1 → 0.22.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 +77 -21
- package/dist/index.d.ts +196 -8
- package/dist/index.js +136 -19
- package/dist/web/assets/index-B9qjNzNP.js +378 -0
- package/dist/web/assets/index-CaCOKEeN.css +1 -0
- package/dist/web/index.html +13 -7
- package/package.json +3 -3
- package/dist/web/assets/index-DB70ukf7.css +0 -1
- package/dist/web/assets/index-yz-IciZ8.js +0 -378
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ tarball 里真的有那些字节。
|
|
|
86
86
|
|
|
87
87
|
| 端点 | 说明 |
|
|
88
88
|
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
89
|
+
| `POST /api/appearance` | `{theme?, lang?}` → 记下界面外观(**进程级、无会话**,见下) |
|
|
89
90
|
| `GET /api/health` | **不鉴权**,只回 `{ok, version}` |
|
|
90
91
|
| `GET /api/config` | 模型 / 权限级别 / 工作区 / 品牌 / 启动诊断 / 绑没绑在回环之外(`?lang=` 见下) |
|
|
91
92
|
| `GET /api/events` | SSE,负载是 `WireEnvelope` |
|
|
@@ -178,6 +179,41 @@ tarball 里真的有那些字节。
|
|
|
178
179
|
清单以外还要知道的六件事(鉴权两段式、`Origin` 校验、`seq` 与 `Last-Event-ID` 续传、
|
|
179
180
|
断连不等于人走了)写在 [docs/EMBEDDING.md §10](../../docs/EMBEDDING.md)。
|
|
180
181
|
|
|
182
|
+
### `POST /api/appearance` —— 界面外观偏好(2026-09-29)
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
POST /api/appearance {theme?: 'system'|'light'|'dark', lang?: 'system'|'zh'|'en'}
|
|
186
|
+
→ 200 {ok: true, theme?, lang?} ← 盘上现在那一份
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**它治的是「用户选的外观活不过一次进程重启」**:主题和语言原来只写在浏览器的
|
|
190
|
+
`localStorage` 里,而它**按 origin 隔离** —— 嵌入宿主用 `port: 0` 时每次启动都是
|
|
191
|
+
一个新的 `http://127.0.0.1:<新端口>`,上次那份读不到(升级必然重启一次,
|
|
192
|
+
所以它在升级那里最显眼)。这一条把偏好落进 `~/.epoch/appearance.json`,
|
|
193
|
+
下一次启动由 `createWebServer()` 拼进首屏 URL(`?theme=&lang=`)。
|
|
194
|
+
|
|
195
|
+
四条边界:
|
|
196
|
+
|
|
197
|
+
- **无会话,而且只能是 POST。** 路径上没有 `:sessionId`(判据同
|
|
198
|
+
`PUT /api/last-session`:落点是**进程级**文件);非 GET 是硬要求 ——
|
|
199
|
+
`auth.ts` 的 Origin 校验只在非安全方法上要求,而这条路会往用户磁盘写文件。
|
|
200
|
+
- **载荷是补丁,所以不是 PUT。** 缺席的键 = **那一格别动**,不是「设成跟随系统」;
|
|
201
|
+
同一个 URL 上提交 `{theme}` 和 `{lang}` 得到两种结果,那不是幂等的全量替换。
|
|
202
|
+
- **读没有端点。** 读那条路**必须发生在第一帧之前**,所以它是 `server.url` 上的
|
|
203
|
+
两个 query 参数,而服务端拼的是「盘上那份压过 `ui` 初值」之后的结果。
|
|
204
|
+
⚠️ 每次首屏导航(那次换 cookie 的 303)都会按**当下**盘上那份重盖一遍 ——
|
|
205
|
+
`server.url` 是启动那一刻的常量,而盘上那份会变。
|
|
206
|
+
- **认不出来的值回 400,一个字节都不落盘。** 值域的判定在消费侧
|
|
207
|
+
(写入口 = 这里,读出口 = `storedUiPreset` 和内联脚本),存储那一层只做持久化 ——
|
|
208
|
+
手改出来的坏值读得出来、用不上、也不报错。第一方界面**不看看回执**
|
|
209
|
+
(它那一发是 fire-and-forget,写不进去就静默降级),那两档状态码是给排查的人
|
|
210
|
+
和自己画前端的宿主的:503 = 宿主没接这条路,500 = 写盘失败(带原话)。
|
|
211
|
+
|
|
212
|
+
宿主那一侧的完整语义(`ui` 的准确含义、那张三档对照表、为什么偏好不落在
|
|
213
|
+
`config.yaml`)在 [docs/EMBEDDING.md §9](../../docs/EMBEDDING.md);
|
|
214
|
+
存储本体和四条「为什么不塞进 `display.*`」的判据在
|
|
215
|
+
[core/src/appearance.ts](../core/src/appearance.ts)。
|
|
216
|
+
|
|
181
217
|
### `/api/host/*` —— 宿主把一句话送进输入框(2026-09-28)
|
|
182
218
|
|
|
183
219
|
```
|
|
@@ -1658,14 +1694,21 @@ role 上。
|
|
|
1658
1694
|
3. **[bind.ts](src/bind.ts) 那条「非回环不给 token 就拒绝启动」不放松**,也不为这个
|
|
1659
1695
|
字段开例外
|
|
1660
1696
|
|
|
1661
|
-
|
|
1697
|
+
**开过工之后不给改**(决定 18):中途换地盘会让前面那些工具调用里的 `./src/a.ts`
|
|
1662
1698
|
指向另一个目录,而记录里没有任何地方写着从哪条开始换的。
|
|
1663
1699
|
|
|
1664
|
-
⚠️
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1700
|
+
⚠️ **2026-09-30:这条锁的边界从「绑过没有」挪到了「开过工没有」。** 判据就是上面
|
|
1701
|
+
那句话自己 —— 还没开过工就**一条工具调用都没有**,锁在那段窗口里没有对象。
|
|
1702
|
+
所以「挑一个 → 想换一个」在一段还没发过消息的会话上是允许的,而开过工之后照旧
|
|
1703
|
+
一个字不能改。判据只有一份(`WorkspaceControl.canChange`),三处共用:runtime 里
|
|
1704
|
+
`bind()` 那道 `locked`、这条端点那道 409 闸、以及网线上给界面的 `changeable`。
|
|
1705
|
+
三处各判一次的表现是「界面画着一张 picker、按下去 409」。
|
|
1706
|
+
|
|
1707
|
+
⚠️ **所以「换一个工作区」的唯一形态**(开过工之后)**仍然是新建一个会话**
|
|
1708
|
+
—— 2026-08-15 多会话工厂落地之后,`POST /api/sessions` 带另一个目录**不再是
|
|
1709
|
+
409 `workspace-locked`**,而是建一个绑在那儿的新会话(见上面那张表)。那个码没有
|
|
1710
|
+
作废:它守的仍然是同一条规矩,只是这条路上再也走不到它了,真正会撞到它的是
|
|
1711
|
+
runtime 那一层(`SessionWorkspaces.bind`)和这条端点那个 409 闸。
|
|
1669
1712
|
|
|
1670
1713
|
### 「这个会话绑在哪儿」是一条独立端点(2026-08-15)
|
|
1671
1714
|
|
|
@@ -1685,6 +1728,11 @@ role 上。
|
|
|
1685
1728
|
| 建好了、**还没选**地盘 | 200 + `{binding:{state:'unbound'}}` |
|
|
1686
1729
|
| 这个进程手里**没有**这段会话 | 409 `not-live-session` |
|
|
1687
1730
|
|
|
1731
|
+
⚠️ **`bound` / `none` 还各带一格 `changeable`**(2026-09-30):这次决定还能不能改,
|
|
1732
|
+
判据只有一条 —— 这段会话**开过工没有**(和上面那道 409 闸、以及 runtime 里
|
|
1733
|
+
`bind()` 那道 `locked` 读的是同一个方法,见下面 POST 那一节)。`unbound` 不带它:
|
|
1734
|
+
那一档还没有决定,一直可挑。这一格是**界面画菜单还是画 diff 门**的依据。
|
|
1735
|
+
|
|
1688
1736
|
第 3 行和第 5 行是这条端点原本全部的难点。绑定活在进程内存里、**不落盘**,所以上
|
|
1689
1737
|
一个进程留下的会话在 `workspaces.of()` 那儿也是 `null` —— 和「这个会话真的不使用
|
|
1690
1738
|
工作区」在数据上一模一样。合成一个的表现很具体:一段昨天在某个仓库里干了一整轮活
|
|
@@ -1771,27 +1819,35 @@ body: {root: '<绝对路径>'} | {none: true}
|
|
|
1771
1819
|
「宿主看得见的那一面」上,路径校验、信任闸门、`locked` 幂等三样一个字都没动 ——
|
|
1772
1820
|
这条端点只是第二个调用点。
|
|
1773
1821
|
|
|
1774
|
-
**只有 `unbound
|
|
1822
|
+
**只有 `unbound`,或者还没开过工的已定会话,收得动:**
|
|
1823
|
+
|
|
1824
|
+
| 这个会话此刻 | 结果 |
|
|
1825
|
+
| -------------------------------- | ----------------------------------------------------- |
|
|
1826
|
+
| `unbound` 且 idle | 绑 / 转 `none`,200 回**改完之后**的整份 binding |
|
|
1827
|
+
| `unbound` 但在跑 | 409 `busy` —— 等这一轮结束再挑(2026-08-20) |
|
|
1828
|
+
| `bound` / `none`,**还没开过工** | 同上第一行 —— 换一个「还没被任何工具调用用上的选择」 |
|
|
1829
|
+
| `bound` / `none`,**开过工了** | 409 `workspace-locked` —— 决定 18「开过工之后不给改」 |
|
|
1830
|
+
| 上面任一档,但这一轮在跑 | 409 `busy` |
|
|
1775
1831
|
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
| `unbound` 但在跑 | 409 `busy` —— 等这一轮结束再挑(2026-08-20) |
|
|
1780
|
-
| `bound` | 409 `workspace-locked` —— 决定 18「绑定后不给改」 |
|
|
1781
|
-
| `none` | 409 `workspace-locked` —— **「不使用」也是一次决定** |
|
|
1832
|
+
⚠️ 第三行 2026-09-30 才有(判据在 `WorkspaceControl.canChange` 上,读的是会话库
|
|
1833
|
+
——「这段会话开过工没有」)。**它不是放宽**:那条锁要护的是「前面那些工具调用里的
|
|
1834
|
+
相对路径」,而那段窗口里一条都没有。
|
|
1782
1835
|
|
|
1783
|
-
⚠️
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
(2026-08-20
|
|
1836
|
+
⚠️ `workspace-locked` 和 `busy` **不是同一句话**,别合并:前者说的是「已经定了,
|
|
1837
|
+
不能改」(下一步是新建会话),后者说的是「现在不行,等一下」(下一步还是
|
|
1838
|
+
同一个按钮)。合成一句的话,用户会去建一个他不需要的会话。`busy` 是迟绑那一轮
|
|
1839
|
+
(2026-08-20)补的:绑定这一下会真的改引擎脚下的目录,而一次 `run()` 底下可能有
|
|
1787
1840
|
二十次工具调用 —— 半路换根等于同一轮里前几次按 A、后几次按 B 解析相对路径,而历史里
|
|
1788
1841
|
那条 `[系统]` 留痕是**轮次开始时**插的,盖不住这一档。响应头带 `X-Epoch-Turn-State`,
|
|
1789
1842
|
和 `POST /messages` 那条 409 同一对形状。
|
|
1790
1843
|
|
|
1791
|
-
⚠️
|
|
1792
|
-
**明确按过**的一档(决定 18
|
|
1793
|
-
|
|
1794
|
-
|
|
1844
|
+
⚠️ `none` 那一档是这条端点唯一一处会被读错的地方:它看着像「还空着」,但它是用户
|
|
1845
|
+
**明确按过**的一档(决定 18 侧栏「任务」组的成员)。**开过工之后**让它还能被改,
|
|
1846
|
+
等于给一段说过「不要地盘」的会话补一个地盘 —— 而它前面那些工具调用里的相对路径
|
|
1847
|
+
已经按服务进程的启动目录解析过了,判据逐字同 `bound` 那一行。而还没开过工时它和
|
|
1848
|
+
`bound` 一样放行:底栏那张菜单里「不使用工作区」和「换一个目录」本来就是同一层的
|
|
1849
|
+
两项,一项能改、另一项点了没反应,用户看到的是「菜单里有一项坏了」。
|
|
1850
|
+
**不做 `PATCH`,也不做换绑** —— 这条端点只是在那个窗口里允许「改一个还没用上的选择」。
|
|
1795
1851
|
|
|
1796
1852
|
请求体**两支恰好一支**:两支都给 / 都不给一律 400 `bad-workspace`。「两支都给」
|
|
1797
1853
|
不许挑一支执行 —— 那两支通向的是两段完全不同的会话。**这一层一次路径校验都不做**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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, WireHubEvent, 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';
|
|
1
|
+
import { AppearanceTheme, AppearanceLang, 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, WireHubEvent, 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, AppearanceControl, WorkspaceFilesView } from '@epoch-agent/runtime';
|
|
3
3
|
import { ServerResponse } from 'node:http';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -15,6 +15,7 @@ import { ServerResponse } from 'node:http';
|
|
|
15
15
|
* 整个文件是纯函数 —— 判定不能藏在 CLI 的 action 回调里,那种地方写不了用例,
|
|
16
16
|
* 而这条判定恰恰是验收第 12 / 13 条。
|
|
17
17
|
*/
|
|
18
|
+
|
|
18
19
|
/** 默认端口。4400 是方案里举的那个,没被常见服务占用 */
|
|
19
20
|
declare const DEFAULT_WEB_PORT = 4400;
|
|
20
21
|
/** 默认监听地址 —— 只有回环。改这个默认值需要重读 §6.1 */
|
|
@@ -68,10 +69,15 @@ declare const UI_LANG_PARAM = "lang";
|
|
|
68
69
|
*
|
|
69
70
|
* `'system'` 不是「不给」—— 它是明确的第三档(跟随系统),和不传这个字段
|
|
70
71
|
* (用户上次自己选的那个还算数)是两件事。
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ **2026-09-29 起它从 protocol 派生**(`AppearanceTheme`),不再在这里
|
|
74
|
+
* 自己写一份字面量:同一个值域现在有四个消费者(界面 / 这儿的首屏 URL /
|
|
75
|
+
* 引擎落盘那份 / 宿主这个入参),而 `web` 是 private 包、`core` 不许依赖它 ——
|
|
76
|
+
* 除了 protocol 没有第三处放得下。判据在 protocol 的 `wire-appearance.ts` 文件头。
|
|
71
77
|
*/
|
|
72
|
-
type UiThemePref =
|
|
78
|
+
type UiThemePref = AppearanceTheme;
|
|
73
79
|
/** 同上,对应 `localStorage['epoch.lang']`。值域跟着 protocol 的 `LANGS` 走 */
|
|
74
|
-
type UiLangPref =
|
|
80
|
+
type UiLangPref = AppearanceLang;
|
|
75
81
|
/**
|
|
76
82
|
* 宿主给界面的首屏初值(方案 54 §七)。
|
|
77
83
|
*
|
|
@@ -79,6 +85,13 @@ type UiLangPref = 'system' | 'zh' | 'en';
|
|
|
79
85
|
* 「外观」分区仍然改得动;下次启动宿主再给一次初值。刻意**没有**「强制主题、
|
|
80
86
|
* 界面上不许改」的开关:那是把宿主的偏好升成锁,而外观是今天用户在这张界面上
|
|
81
87
|
* 唯一能自己调的东西(方案 54 §七末尾 + §十一)。
|
|
88
|
+
*
|
|
89
|
+
* ⚠️⚠️ **2026-09-29 起它还多了一条:用户自己选过的那一档压得过它。**
|
|
90
|
+
* 上面那句「下次启动宿主再给一次初值」原来的意思是「宿主每次都给、给了就生效」,
|
|
91
|
+
* 而那个意思在「宿主用 `port: 0`」的现实下等于**用户的选择活不过一次重启**
|
|
92
|
+
* (localStorage 按 origin 隔离,每次都是新 origin)。现在首屏带的是
|
|
93
|
+
* {@link resolveUiPreset} 算出来的那一份:**盘上有就用盘上的,缺的那一格才轮到
|
|
94
|
+
* 这里**。所以 `ui` 的准确语义是「我的产品默认外观」,而不是「每次启动重置外观」。
|
|
82
95
|
*/
|
|
83
96
|
interface UiPreset {
|
|
84
97
|
theme?: UiThemePref;
|
|
@@ -89,8 +102,9 @@ interface UiPreset {
|
|
|
89
102
|
*
|
|
90
103
|
* @param port 必须是**实际监听**的端口(`--port 0` 时由内核分配),
|
|
91
104
|
* 不是命令行里那个
|
|
92
|
-
* @param ui
|
|
93
|
-
*
|
|
105
|
+
* @param ui 这次首屏该带哪几档。**调用方算完再传**({@link resolveUiPreset}:
|
|
106
|
+
* 用户选过的压过宿主初值)。一个键都没有就一个参数都不拼 ——
|
|
107
|
+
* URL 逐字节和以前一样(`epoch web` 没配 `ui` 时走的就是这条)
|
|
94
108
|
*/
|
|
95
109
|
declare function firstScreenUrl(host: string, port: number, token: string, ui?: UiPreset): string;
|
|
96
110
|
|
|
@@ -1753,6 +1767,20 @@ interface SessionHubOptions {
|
|
|
1753
1767
|
* 后事,等于把一条不变量的执行摊到每个调用点上,漏一处就是一次静默泄漏。
|
|
1754
1768
|
*/
|
|
1755
1769
|
onCooled?: (sessionId: string) => void;
|
|
1770
|
+
/**
|
|
1771
|
+
* 读某一段会话**此刻**的标题。答不出就 `null`(不在目录里 / DB 起不来)。
|
|
1772
|
+
*
|
|
1773
|
+
* 这是 `session-title-changed` 那一帧唯一的真源(2026-09-29)。Hub 自己不认识
|
|
1774
|
+
* 会话目录 —— 判据同 {@link SessionHubOptions.onCooled}:那是引擎那一侧的东西,
|
|
1775
|
+
* 而 Hub 在这条链上只负责发帧。
|
|
1776
|
+
*
|
|
1777
|
+
* ⚠️ **为什么读的是「此刻」**:标题在轮次开头就落库了(`core` 那句
|
|
1778
|
+
* `store.beginTurn()` → `appendMessage`),而这里是在 `drive()` 的**落盘节拍**
|
|
1779
|
+
* 上读 —— 两者都发生在这一轮跑完之前。那个节拍只在落盘进度真的往前走了才触发,
|
|
1780
|
+
* 所以读到 `null` 是常态(这一段还没有标题)。
|
|
1781
|
+
* **读到 `null` 就什么都不做**,不许拿它当「标题被清空了」发出去。
|
|
1782
|
+
*/
|
|
1783
|
+
readTitle?: (sessionId: string) => string | null;
|
|
1756
1784
|
}
|
|
1757
1785
|
/**
|
|
1758
1786
|
* `start()` 的结果。
|
|
@@ -1835,6 +1863,8 @@ declare class SessionHub {
|
|
|
1835
1863
|
private readonly clock;
|
|
1836
1864
|
/** 见 {@link SessionHubOptions.onCooled} */
|
|
1837
1865
|
private readonly onCooled;
|
|
1866
|
+
/** 见 {@link SessionHubOptions.readTitle} */
|
|
1867
|
+
private readonly readTitle;
|
|
1838
1868
|
private readonly registry;
|
|
1839
1869
|
private readonly sinks;
|
|
1840
1870
|
/**
|
|
@@ -2064,19 +2094,36 @@ declare class SessionHub {
|
|
|
2064
2094
|
*
|
|
2065
2095
|
* 为一帧发给**零个**收件人的帧去改一份三边共用的契约,换不回任何东西。
|
|
2066
2096
|
*
|
|
2067
|
-
* ##
|
|
2097
|
+
* ## 所以产地是四条端点,各自的判据
|
|
2068
2098
|
*
|
|
2069
2099
|
* | 产地 | 什么时候发 | 为什么 |
|
|
2070
2100
|
* | --- | --- | --- |
|
|
2071
2101
|
* | `POST /api/sessions`(`workspace/handlers.ts`) | **只在 `outcome.created`** | 复用那一支没多出一行来,发了等于让所有标签页白重取一次 |
|
|
2072
2102
|
* | `DELETE /api/sessions/:id`(`api.ts`) | 删成了就发 | 不发的话**别的**标签页会一直挂着一行已经没了的会话,点进去才拿 404 |
|
|
2073
2103
|
* | `PATCH /api/sessions/:id`(`api.ts`,2026-09-16) | **只在 `archived` 真的翻了** | 列表默认不列归档的那几段,所以归档 = 少一行、取回 = 多一行。判据全文在那个调用点上 |
|
|
2104
|
+
* | `POST /api/sessions/:id/workspace`(`workspace/handlers.ts`,2026-09-30) | **只在那一行真的换了组的时候**(`bind` 那一支看 `outcome.changed`、「不使用工作区」那一支看调用前是不是已经是 `none`) | 它改的是那一行上的 `workspace` 格 —— 而侧栏「空间 / 任务 / 早先的会话」三组正是读它分出来的。判据全文在那个调用点上,和下面「置顶」那一档的分界见下一节 |
|
|
2074
2105
|
*
|
|
2075
2106
|
* ⚠️ 第三个产地**不在「进出 Hub」这条路上**,而上一版这段话正是按那条路数的
|
|
2076
2107
|
* (原文「那是第一版……`hub-register-sites.test.ts` 已经钉住了会话只能从那三处
|
|
2077
2108
|
* 变活」)。钉住的是**活性**,而这一帧的宾语是**列表端点答出来的那几行** ——
|
|
2078
2109
|
* 两者在归档这一档上分叉:一段会话被收进归档时进出 Hub 的表一个字没动,
|
|
2079
2110
|
* 可侧栏那份列表少了一行。别再拿「经不经过 Hub」当这一帧的判据。
|
|
2111
|
+
* ⚠️ 第四个产地(绑定)**同样不经过 Hub**,而且它比归档那一档更远:它在
|
|
2112
|
+
* 字面上不增不减任何一行,改的只是**其中一行上的一个格子**。
|
|
2113
|
+
*
|
|
2114
|
+
* ## ⚠️ 绑定那一档和「置顶不发」看着像,分界是「那句话会不会骗人」
|
|
2115
|
+
*
|
|
2116
|
+
* 绑定和置顶一样不改成员数,但**只有它发**:
|
|
2117
|
+
*
|
|
2118
|
+
* - 置顶只是**顺序旧一拍** —— 那一行还在列表里、点得开、内容也对,而按下去
|
|
2119
|
+
* 的那一页自己会重取。过期看得见,但**不会骗人**,所以不发(判据全文在
|
|
2120
|
+
* protocol 的 `sessions-changed` 上);
|
|
2121
|
+
* - 绑定把那一行**放进了错误的分组**:屏幕说「这段会话在 epoch-agent 里干活」,
|
|
2122
|
+
* 而它其实在 `lw` 里 —— 那句话是假的,而且**不会自己修**,得用户刷新页面、
|
|
2123
|
+
* 或者切到一段列表里没有的会话才把它捡回来(2026-09-30 用户报的正是这个)。
|
|
2124
|
+
*
|
|
2125
|
+
* 代价如实记:一次换绑 = **每个标签页**各重取一次整份列表(本机 loopback GET,
|
|
2126
|
+
* 50 行封顶)。绑定是用户手动、低频的一次动作,不在一轮里反复发生,认下它。
|
|
2080
2127
|
*
|
|
2081
2128
|
* 另外两处刻意**不发**:
|
|
2082
2129
|
*
|
|
@@ -2085,6 +2132,33 @@ declare class SessionHub {
|
|
|
2085
2132
|
* 而那一行**本来就在列表里**(列表的另一半来自 DB)。列表没变,不该发。
|
|
2086
2133
|
*/
|
|
2087
2134
|
notifySessionsChanged(sessionId: string): void;
|
|
2135
|
+
/**
|
|
2136
|
+
* 广播「某一行上的**标题**变了」(2026-09-29)。语义、为什么带载荷、以及
|
|
2137
|
+
* 「别的会话那一行也覆写得到」全在 protocol 的 `session-title-changed` 上。
|
|
2138
|
+
*
|
|
2139
|
+
* ## 两个产地
|
|
2140
|
+
*
|
|
2141
|
+
* | 产地 | 什么时候发 |
|
|
2142
|
+
* | --- | --- |
|
|
2143
|
+
* | {@link drive} 的落盘节拍 | 自动截出来的那个标题(`core` 拿首条 user 消息截的),一轮跑着跑着就长出来了 |
|
|
2144
|
+
* | `PATCH /api/sessions/:id`(`api.ts` 的 `patchSession`) | 用户手动改名 |
|
|
2145
|
+
*
|
|
2146
|
+
* ## 为什么自动那一支挂在落盘节拍上,而不是「user 帧发完就发」
|
|
2147
|
+
*
|
|
2148
|
+
* 标题是**跟着那条 user 行一起落的**(`core` 的 `manager.ts` 的 `appendMessage`)。
|
|
2149
|
+
* 落盘节拍正是「库里往前走了」这件事的采样点,而它已经在那儿了 —— 借它顺手读一次
|
|
2150
|
+
* 标题,比另开一个「哪一帧之后再读一次库」的判断便宜,也不会漏掉别的落盘路径。
|
|
2151
|
+
*
|
|
2152
|
+
* ⚠️ **它不许只发给「正看着这一段」的人**:这一帧的宾语是**一行**,而侧栏吃所有
|
|
2153
|
+
* 会话的帧(判据在 protocol 那一帧的 JSDoc 上)。`publish` 那一层本来就是扇出到
|
|
2154
|
+
* 所有连接,所以这里不需要额外处理 —— 记这一笔是为了别有人顺手加个过滤。
|
|
2155
|
+
*
|
|
2156
|
+
* ⚠️ **`announcedTitle` 的写点只有这一处**(读它的是 `drive()`):两个产地各更新
|
|
2157
|
+
* 一遍的话,「哪一格才是已经宣布过的那个」就有了两份实现,而它们只要有一处忘了
|
|
2158
|
+
* 更新,症状是**一轮跑完多发一帧内容一样的标题** —— 不致命、但没人会去查。
|
|
2159
|
+
* 会话可能已经不在 Hub 里(被冷却 / 从没注册过),那时这一格没地方记,照发。
|
|
2160
|
+
*/
|
|
2161
|
+
notifySessionTitleChanged(sessionId: string, title: string): void;
|
|
2088
2162
|
/**
|
|
2089
2163
|
* 广播「插件那张表变了」(2026-09-24)。语义和「为什么不带 `sessionId`」全在
|
|
2090
2164
|
* protocol 的 `plugins-changed` 上。
|
|
@@ -3132,6 +3206,45 @@ interface PluginPreviewView {
|
|
|
3132
3206
|
name: string;
|
|
3133
3207
|
version: string;
|
|
3134
3208
|
};
|
|
3209
|
+
/** `InstallDependency`(core,经 runtime 原样透出)的镜像 —— 见 `WirePluginDependency` */
|
|
3210
|
+
dependencies?: readonly DependencyView[];
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* 一个依赖。**手写一份镜像**,理由同 {@link PluginPreviewView}:这一层只依赖
|
|
3214
|
+
* protocol + runtime,而 core 那个类型是经 runtime 透出来的,直接 import 会把
|
|
3215
|
+
* 分层绕过去(`check-layers` 会红)。
|
|
3216
|
+
*
|
|
3217
|
+
* ⚠️ 镜像的代价是**加一格要两边一起加**,而漏了的表现是「core 算了、界面没有」——
|
|
3218
|
+
* 所以 `toWireDependency` 是照着这个形状逐格写的,不是 spread 过去的。
|
|
3219
|
+
*/
|
|
3220
|
+
interface DependencyView {
|
|
3221
|
+
name: string;
|
|
3222
|
+
status: 'installed' | 'plan' | 'unresolved';
|
|
3223
|
+
requiredBy: string;
|
|
3224
|
+
source?: string;
|
|
3225
|
+
reason?: string;
|
|
3226
|
+
versionUnmet?: {
|
|
3227
|
+
required: string;
|
|
3228
|
+
current: string;
|
|
3229
|
+
reason: string;
|
|
3230
|
+
};
|
|
3231
|
+
manifest?: {
|
|
3232
|
+
name: string;
|
|
3233
|
+
version: string;
|
|
3234
|
+
};
|
|
3235
|
+
inventory?: {
|
|
3236
|
+
commands: readonly string[];
|
|
3237
|
+
roles: readonly string[];
|
|
3238
|
+
skills: readonly string[];
|
|
3239
|
+
hooks: readonly {
|
|
3240
|
+
type: string;
|
|
3241
|
+
count: number;
|
|
3242
|
+
}[];
|
|
3243
|
+
denyRules: number;
|
|
3244
|
+
mcpServers: readonly string[];
|
|
3245
|
+
ignoredBuckets: readonly string[];
|
|
3246
|
+
jsTools: boolean;
|
|
3247
|
+
};
|
|
3135
3248
|
}
|
|
3136
3249
|
/** 七档拒绝的共同形状(runtime 的 `PluginRefusal`) */
|
|
3137
3250
|
interface RefusalView {
|
|
@@ -3804,6 +3917,16 @@ interface WorkspaceView {
|
|
|
3804
3917
|
decideNone(sessionId: string): void;
|
|
3805
3918
|
/** 这个会话是哪一档。**不答「这个进程认不认识它」**,那一问由 Hub 答 */
|
|
3806
3919
|
stateOf(sessionId: string): WorkspaceBindingState;
|
|
3920
|
+
/**
|
|
3921
|
+
* 这次决定还能不能改(2026-09-30)。
|
|
3922
|
+
*
|
|
3923
|
+
* **两处问它,而且必须是同一个答案**:`bindWorkspace` 那道 409 闸,
|
|
3924
|
+
* 以及 GET / POST 两条路下发给界面的 `changeable`。三处各判一次的表现是
|
|
3925
|
+
* 「界面上画着一张 picker、按下去 409」—— 那句话读起来像功能坏了。
|
|
3926
|
+
*
|
|
3927
|
+
* 判据全文在 runtime 的 `WorkspaceControl.canChange` 上。
|
|
3928
|
+
*/
|
|
3929
|
+
canChange(sessionId: string): boolean;
|
|
3807
3930
|
release(sessionId: string): void;
|
|
3808
3931
|
/** 本机「已知工作区」清单,最近使用的在前 */
|
|
3809
3932
|
known(): ReadonlyArray<{
|
|
@@ -4624,6 +4747,15 @@ interface CatalogRow {
|
|
|
4624
4747
|
* 每一行都答得出。上面那两格可选说的是另一件事(v7 之前的老行没记过)。
|
|
4625
4748
|
*/
|
|
4626
4749
|
archived: boolean;
|
|
4750
|
+
/**
|
|
4751
|
+
* 被置顶了吗(2026-09-29 接下网线)。
|
|
4752
|
+
*
|
|
4753
|
+
* 和 `archived` **同一档来路、同一档理由**:`sessions.pinned` 从会话库 v1 起
|
|
4754
|
+
* 就在、`NOT NULL DEFAULT 0`,每一行都答得出,所以它是必填而不是可选。
|
|
4755
|
+
*
|
|
4756
|
+
* ⚠️ **两件事正交**:归档说「列不列」,置顶说「排哪儿」。一行可以两个都是真。
|
|
4757
|
+
*/
|
|
4758
|
+
pinned: boolean;
|
|
4627
4759
|
}
|
|
4628
4760
|
/** 一条 FTS5 命中 —— runtime 的 `SessionHit` 结构上满足它 */
|
|
4629
4761
|
interface CatalogHit {
|
|
@@ -4648,14 +4780,31 @@ interface SessionCatalog {
|
|
|
4648
4780
|
/**
|
|
4649
4781
|
* ⚠️ `archived` 是三态(方案 64 PR-2):不给 = 只列没归档的,
|
|
4650
4782
|
* `true` = **只**列归档的。判据同 runtime 的 `SessionControl.list`。
|
|
4783
|
+
*
|
|
4784
|
+
* `pinnedOnly: true` 只列置顶的那些(2026-09-29)。要它的是
|
|
4785
|
+
* {@link listSessionSummaries}:那段函数必须把「很久没动过、按时间排已经掉出
|
|
4786
|
+
* 这一页」的置顶会话单独取回来,否则用户置顶它就是白置顶。
|
|
4651
4787
|
*/
|
|
4652
4788
|
list(limit?: number, opts?: {
|
|
4653
4789
|
archived?: boolean;
|
|
4790
|
+
pinnedOnly?: boolean;
|
|
4654
4791
|
}): CatalogRow[];
|
|
4655
4792
|
get(sessionId: string): CatalogRow | null;
|
|
4656
4793
|
rename(sessionId: string, title: string): CatalogRow | null;
|
|
4657
4794
|
/** 收进归档 / 取回。**不是删除** —— 判据在 runtime 的 `SessionControl.setArchived` 上 */
|
|
4658
4795
|
setArchived(sessionId: string, archived: boolean): CatalogRow | null;
|
|
4796
|
+
/**
|
|
4797
|
+
* 置顶 / 取消置顶(2026-09-29)。
|
|
4798
|
+
*
|
|
4799
|
+
* 收成一个带布尔的方法,形状**逐字同** {@link setArchived}:调用点(web 那张
|
|
4800
|
+
* 行菜单)拿到的本来就是一个布尔,拆成 `pin()` / `unpin()` 两个只会让每个
|
|
4801
|
+
* 调用点自己写一次 `if`。
|
|
4802
|
+
*
|
|
4803
|
+
* ⚠️ **它不推 `updatedAt`**(置顶不是「活动」——判据在 runtime 的
|
|
4804
|
+
* `SessionControl.setPinned` 上),所以这一发不改这一行**按时间排**的位置。
|
|
4805
|
+
* 变的是排序里置顶那一档,判据在 {@link compareSummaries} 上。
|
|
4806
|
+
*/
|
|
4807
|
+
setPinned(sessionId: string, pinned: boolean): CatalogRow | null;
|
|
4659
4808
|
delete(sessionId: string): Promise<CatalogDeletion>;
|
|
4660
4809
|
search(query: string, limit?: number): CatalogHit[];
|
|
4661
4810
|
}
|
|
@@ -4910,6 +5059,26 @@ interface WebRuntimeView extends RuntimeCapabilities, RuntimeSecurity, RuntimeSe
|
|
|
4910
5059
|
*/
|
|
4911
5060
|
session: (HubSession & ModelSessionFacts) | null;
|
|
4912
5061
|
sessionId: string;
|
|
5062
|
+
/**
|
|
5063
|
+
* 不绑工作区时引擎**实际**跑在哪个目录(2026-09-30)。
|
|
5064
|
+
* `EpochRuntime.defaultWorkDir` 结构上满足它。
|
|
5065
|
+
*
|
|
5066
|
+
* ⚠️ **它是可选字段**,同 `diagnosticsIn?` / `lastOpenSession?` 那一条:
|
|
5067
|
+
* 自己拼 `WebRuntimeView` 的宿主可以不给 —— 那时 `GET /api/config` 整个不下发
|
|
5068
|
+
* `defaultWorkDir`,界面退回「选择工作区」那个纯动作(两种形态都画得出来)。
|
|
5069
|
+
* 把 `EpochRuntime` 整个传进来的宿主什么都不用做:它恒有这一格。
|
|
5070
|
+
*
|
|
5071
|
+
* ⚠️ **它是进程级的一份,所有会话共用**:绑定表答不出来时每个会话都回落到它
|
|
5072
|
+
* (`session-factory.ts` 的 `site()` 那一行 `now?.workspace.root ?? opts.defaultWorkDir`)。
|
|
5073
|
+
* 别读成「引导会话的工作目录」—— 引导会话只是**恰好**也跑在它上面。
|
|
5074
|
+
*
|
|
5075
|
+
* ⚠️ 在这儿**不要**用 `process.cwd()` 重算:宿主可以显式传 `workDir` 起引擎
|
|
5076
|
+
* (`buildRuntime({ workDir })`),而那是**宿主进程**的 cwd,两者不是一回事。
|
|
5077
|
+
*
|
|
5078
|
+
* 上网线的形态、以及它为什么非下发不可,在 protocol 的
|
|
5079
|
+
* `WireConfigResponse.defaultWorkDir` 上。
|
|
5080
|
+
*/
|
|
5081
|
+
defaultWorkDir?: string;
|
|
4913
5082
|
/**
|
|
4914
5083
|
* 多会话工厂(方案 30 §6.3)。`EpochRuntime.sessionFactory` 结构上满足它。
|
|
4915
5084
|
*
|
|
@@ -5128,6 +5297,19 @@ interface WebRuntimeView extends RuntimeCapabilities, RuntimeSecurity, RuntimeSe
|
|
|
5128
5297
|
* 回 503 —— **不是假装记下来了**(同 `sessions` 为 null 时那两条的口径)。
|
|
5129
5298
|
*/
|
|
5130
5299
|
lastOpenSession?: LastOpenSessionControl;
|
|
5300
|
+
/**
|
|
5301
|
+
* 界面外观偏好(主题 / 语言,2026-09-29)。`EpochRuntime.appearance`
|
|
5302
|
+
* 结构上正好满足它。判据(含「为什么不是 `config.yaml` 的 `display`」四条)
|
|
5303
|
+
* 在 core 的 `appearance.ts` 文件头。
|
|
5304
|
+
*
|
|
5305
|
+
* ## ⚠️ 可选,理由逐字同上面 `lastOpenSession`
|
|
5306
|
+
*
|
|
5307
|
+
* 自己手拼 `WebRuntimeView` 的嵌入宿主和 `__tests__/harness.ts` 那份假 runtime
|
|
5308
|
+
* 都是手拼的,写成必填等于这一笔改动把它们全打断。不给时:首屏 URL 上一个
|
|
5309
|
+
* 外观参数都不多拼、那次 303 也不重盖(行为逐字等于这一轮之前),
|
|
5310
|
+
* 而 `POST /api/appearance` 回 503 —— **不是假装记下来了**。
|
|
5311
|
+
*/
|
|
5312
|
+
appearance?: AppearanceControl;
|
|
5131
5313
|
/**
|
|
5132
5314
|
* `@:` 引用另一段会话那一片(方案 53 PR-4)。SQLite 起不来时为 null。
|
|
5133
5315
|
* `EpochRuntime.sessionReferences` 结构上正好满足它。
|
|
@@ -6122,7 +6304,13 @@ interface CreateWebServerOptions {
|
|
|
6122
6304
|
* `private: true` —— 宿主 import 不到,只能给 webview 挂一个 preload、
|
|
6123
6305
|
* 在页面脚本之前手写 localStorage。能用,但它依赖的是一个我们随时能改的名字。
|
|
6124
6306
|
*
|
|
6125
|
-
* ⚠️ **是初值,不是锁**,判据见 {@link UiPreset}
|
|
6307
|
+
* ⚠️ **是初值,不是锁**,判据见 {@link UiPreset}。⚠️⚠️ **2026-09-29 起它还有一个
|
|
6308
|
+
* 更容易读反的地方:用户自己选过的那一档压得过它。** 值不直接拼进 URL,而是先过
|
|
6309
|
+
* {@link resolveUiPreset}(`<homeDir>/appearance.json` 里那一份优先,缺的那一格
|
|
6310
|
+
* 才轮到这里)—— 所以准确的语义是「**我的产品默认外观**」,不是「每次启动重置外观」。
|
|
6311
|
+
* 这一条在宿主用 `port: 0` 时尤其要紧:localStorage 按 origin 隔离,
|
|
6312
|
+
* 每次启动都是一个新 origin,于是「宿主每次都给」曾经等于「用户的选择活不过一次
|
|
6313
|
+
* 重启」。判据全文在 docs/EMBEDDING.md §9「给 ui,别去写 localStorage」那一节。
|
|
6126
6314
|
*/
|
|
6127
6315
|
ui?: UiPreset;
|
|
6128
6316
|
/** 环形缓冲容量,缺省 512 */
|