@xrn1997/dsh-session-delete 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +7 -25
  2. package/lib/client.js +972 -588
  3. package/lib/index.js +553 -286
  4. package/lib/types/api/dispatch.d.ts +2 -2
  5. package/lib/types/api/route-table.d.ts +1 -1
  6. package/lib/types/api/trash-rows.d.ts +13 -6
  7. package/lib/types/client/batch-bar.d.ts +6 -6
  8. package/lib/types/client/batch-confirm.d.ts +5 -3
  9. package/lib/types/client/batch-menu-item.d.ts +2 -2
  10. package/lib/types/client/batch-select.d.ts +7 -50
  11. package/lib/types/client/batch-selection-store.d.ts +30 -0
  12. package/lib/types/client/copy.d.ts +4 -1
  13. package/lib/types/client/delete-confirm.d.ts +2 -0
  14. package/lib/types/client/host-rows.d.ts +35 -0
  15. package/lib/types/client/main-view.d.ts +37 -0
  16. package/lib/types/client/receipt.d.ts +43 -0
  17. package/lib/types/client/remote.d.ts +16 -6
  18. package/lib/types/client/store.d.ts +17 -0
  19. package/lib/types/client/trash-panel.d.ts +1 -1
  20. package/lib/types/client/trash-store.d.ts +2 -0
  21. package/lib/types/client/ui/icons.d.ts +3 -3
  22. package/lib/types/client/undo-toast.d.ts +19 -3
  23. package/lib/types/client/use-column-resize.d.ts +2 -2
  24. package/lib/types/client/use-panel-width.d.ts +1 -1
  25. package/lib/types/client/use-trash-panel-controller.d.ts +55 -0
  26. package/lib/types/client/write-commit.d.ts +10 -0
  27. package/lib/types/deps.d.ts +14 -0
  28. package/lib/types/index.d.ts +6 -13
  29. package/lib/types/log/zstd-frames.d.ts +1 -1
  30. package/lib/types/ports/agent-handles.d.ts +25 -0
  31. package/lib/types/ports/home.d.ts +19 -0
  32. package/lib/types/ports/host-port.d.ts +58 -28
  33. package/lib/types/remote/map-error.d.ts +2 -2
  34. package/lib/types/shared/wire.d.ts +11 -9
  35. package/lib/types/trash/detail.d.ts +1 -1
  36. package/lib/types/trash/entry.d.ts +4 -4
  37. package/lib/types/trash/manifest.d.ts +2 -1
  38. package/lib/types/trash/reconcile.d.ts +2 -2
  39. package/lib/types/trash/storage-manifest.d.ts +12 -0
  40. package/lib/types/verbs/batch.d.ts +1 -1
  41. package/lib/types/verbs/delete-session.d.ts +8 -11
  42. package/lib/types/verbs/errors.d.ts +4 -0
  43. package/lib/types/verbs/member-journal.d.ts +4 -3
  44. package/lib/types/verbs/member-presence.d.ts +35 -0
  45. package/lib/types/verbs/orphan-subagents.d.ts +14 -38
  46. package/lib/types/verbs/permanent-removal.d.ts +113 -0
  47. package/lib/types/verbs/purge-trash.d.ts +9 -5
  48. package/lib/types/verbs/restore-session.d.ts +6 -6
  49. package/package.json +1 -1
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 回收站面板之外发生的写入(菜单删除、批量删除、撤销恢复)的**跨槽位收尾顺序**:
3
+ * 主视图收尾 → 失效回收站 → 撤销通告。面板自己的写入,本地 refresh / 缓存的
4
+ * 生命周期仍住在 controller 与 query 里。
5
+ */
6
+ export declare function commitExternalDelete(input: {
7
+ sessionIds: readonly string[];
8
+ publishUndo(): void;
9
+ }): void;
10
+ export declare function commitExternalRestore(sessionIds: readonly string[]): void;
@@ -0,0 +1,14 @@
1
+ import type { FsPort } from './ports/fs-port.js';
2
+ import type { HostPort } from './ports/host-port.js';
3
+ import type { ManifestPort } from './trash/manifest.js';
4
+ /** Node 半的基础依赖面:组合根只装配这一颗;动词之间不互相 import 依赖面 interface。 */
5
+ export interface CoreDeps {
6
+ fs: FsPort;
7
+ host: HostPort;
8
+ manifest: ManifestPort;
9
+ home: string;
10
+ }
11
+ /** 只有删除读时钟;其余读 / 计划 / 恢复 / 永久删除只用 CoreDeps。 */
12
+ export type DeleteDeps = CoreDeps & {
13
+ now(): number;
14
+ };
@@ -1,34 +1,27 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
- import { type ManifestPort } from './trash/manifest.js';
3
2
  export declare const name = "@xrn1997/dsh-session-delete";
4
3
  /**
5
4
  * 插件依赖:**只写字符串数组**(隔离数据根上实测:写成 `{ required, optional }` 会让插件停在
6
5
  * `pending (waiting for services: required, optional)`)。
7
6
  *
8
- * 三项都是在隔离数据根上实测注入成功、且本插件真正用到的宿主服务。三个**故意不写**的:
7
+ * 三项都是在隔离数据根上实测注入成功、且本插件真正用到的宿主服务。**不进 inject 的**:
9
8
  * - `remote`:那是**浏览器半**的服务(asar `dsh-api-gateway/lib/client.js`:`super(ctx, "remote")`),
10
9
  * Node 半挂的是 `ctx.typertGateway`。把它写进 Node 半的 inject 会让插件永远 pending(同 `slots` 那次的形状)。
11
10
  * - `sessions` / `sessionProjectionCache`:两者都按 `ctx.get(...)` **可选读**——`sessions` 只给 `evictLive`
12
- * 用(活跃判据走 waterfall,见设计稿 §5;服务缺席回 `unknown`),`sessionProjectionCache` 是标题的尽力读面。
11
+ * 用(活跃判据走 waterfall,见 src/ports/host-port.ts;服务缺席回 `unknown`),`sessionProjectionCache` 是标题的尽力读面。
13
12
  * inject 一个可能没挂的服务 = 整个插件 pending。
14
13
  * 它取不到时 `titleOf` 返回空串,正是契约允许的降级。
14
+ * - `agents`:只给 handle 捕获用(`ctx.get('agents')`;缺席时 `ctx.inject(['agents'])` 等服务就绪再装)。
15
+ * 它同样可能晚于本插件激活,写进插件 inject 只会让整个插件 pending。
15
16
  *
16
17
  * `webServer` 是**浏览器半取数通道的承载者**(那条薄线):插件用它注册 `/dsh-session-delete-api`
17
18
  * 前缀路由,浏览器半同源 fetch 那六个段名。它是宿主 web 组成里的硬依赖(`dsh-host-webserver` 的
18
19
  * `super(ctx, "webServer")`),服务名与 `register({kind:'prefix', path, handler})` 的形状按
19
20
  * `dsh-host-webserver/lib/index.js` 与 `README.md` 原文读实,不是猜的。
20
- */
21
- export declare const inject: string[];
22
- /**
23
- * 清单的真实绑定:官方 storage domain → `ManifestPort`(`StorageDomainLike` 在动词层保持一种读法)。
24
- * 域**懒开**且成功句柄只开一次(`apply` 是同步的,而 `open()` 是异步的),并挂一条 `ctx.effect` 清理:
25
- * 插件卸载时关掉句柄并把 `close()` 的落地交回框架(句柄归调用方所有,这是 `open()` 的 JSDoc 明写的契约;
26
- * 2026-10-17 起不再 `void` 走掉——没关严的句柄会让下一份同名域撞 `already-open`)。
27
21
  *
28
- * **失败不缓存**:`open()` 被拒是瞬态(域被别的持有者占着、装载竞态),把第一次的 rejected promise
29
- * 缓存起来等于"这个会话永远删不掉"——下一次读会重新走 `open()`,成功才落下活句柄。
22
+ * 清单(storage domain 绑定)与数据根各有一个家:`src/trash/storage-manifest.ts`、`src/ports/home.ts`。
30
23
  */
31
- export declare function createStorageManifest(ctx: Context): ManifestPort;
24
+ export declare const inject: string[];
32
25
  /**
33
26
  * 装配顺序:**清单 → 端口 → 动词依赖 → 前缀路由**(`DeleteDeps` 就是前两者的合一)。
34
27
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 多帧 zstd 流的**帧边界**(设计稿 2026-10-15 §1.1)。
2
+ * 多帧 zstd 流的**帧边界**(2026-10-15 实测)。
3
3
  *
4
4
  * 为什么要有这个文件:会话日志是一次次 append 出来的**帧序列**,而 Node 的 `zstdDecompressSync`
5
5
  * **与** `createZstdDecompress()` 流式**都只给第一帧**(实测:本机一个 3.8 MB 的文件,两个 API 都只回
@@ -0,0 +1,25 @@
1
+ /** 捕获所需的 ctx 窄面:可选服务读法(`get`)与按依赖注入的子上下文(`inject`,晚挂载的 `agents` 靠它补装)。
2
+ * 旧 ctx / 假件可以没有 `inject`——那就只在 apply 时 `get` 一次。 */
3
+ export interface AgentCtxLike {
4
+ get(name: string): unknown;
5
+ inject?(names: readonly string[], callback: (child: {
6
+ get(name: string): unknown;
7
+ }) => void): void;
8
+ }
9
+ /** 一次捕获交回的句柄表:按会话 id `dispose`。没捕到 / 已经 dispose 过都是 no-op;
10
+ * dispose 本身的失败**原样抛出**(调用方 `evictLive` 把它折成 `unknown`,不通告)。 */
11
+ export interface AgentHandleStore {
12
+ dispose(sessionId: string): Promise<void>;
13
+ }
14
+ /** 在 `ctx.agents` 上包一层 `resume` / `create`,把返回的 agent handle 的 dispose 按会话 id 记下。
15
+ * 服务缺席时走 `ctx.inject(['agents'])` **等服务挂上再装**——live profile 的插件激活顺序可能早于
16
+ * `agents` 服务就绪;只在 apply 时 `get` 一次会让捕获静默缺席(真机症状:重启前在回收站的能冷恢复,
17
+ * 刚删除的会话恢复后旧 agent 拦住 prompt)。形状不符 / 属性不可写都只是**不捕获**,不影响插件激活。
18
+ *
19
+ * **安装时机**:先 `get('agents')`,没装成再 `inject(['agents'])`(两者只装一次,符号表兜住)。
20
+ * **symbol 表**:句柄表挂在服务实例的 {@link CAPTURED_AGENT_HANDLES} 上;同一个 agents 服务上的第二次
21
+ * 安装(`get` 与 `inject` 各一次,或第二个 `createHostPort`)看见它就直接共用,不把 wrapper 套第二层。
22
+ * **读回判据**:cordis 的 traceable 服务代理每次读函数都会新包一层(`get` 里的 `createShadowMethod`),
23
+ * 所以只能看 own property descriptor 的身份;拿 `service[name]` 读回比身份是必然的假警报。
24
+ */
25
+ export declare function captureAgentHandles(ctx: AgentCtxLike): AgentHandleStore;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 宿主数据根。官方解析器在 asar `dsh/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js`:
3
+ * `resolveDshHome(configured, env)` 的优先级 = 显式配置 > `$DSH_HOME`(**空白视为未设**,免得非法覆盖把 home
4
+ * 解析成当前工作目录)> `~/.dsh`,并 `resolve()` 规范化、展开 `~/`。
5
+ *
6
+ * **只镜像后两级,且这与平台的同款设施等价**——"显式配置"那一级对插件**不可得**,三条实证:
7
+ * ① 平台自己的 helper `dshHomePath(...segments)` 逐字是 `join(resolveDshHome(), ...segments)`,
8
+ * **同样不传 `configured`**;
9
+ * ② `ctx.profileContext` 按 launcher 原文只装「profile 位置 / 启动 bundle 名 / 调用覆盖 / 遥测开关」,
10
+ * 里面没有数据根;
11
+ * ③ 宿主自己的服务也不是"问出来"的——它们各自声明 `Config.dshHome`,由**装配那一行的人**填。
12
+ * ⇒ 后果与边界(判据在 `src/verbs/delete-session.ts` 与 `src/verbs/restore-session.ts` 的预检):显式配置过的
13
+ * profile 上我们算出的根与宿主不一致 ⇒ 文件探测落空 ⇒ 删除/恢复**响亮拒绝、零副作用**(`not-found` /
14
+ * `trash-empty` / `project-missing`);**不会**动到别的树(sessions 与回收站同源 ⇒ 同卷)。
15
+ * 本机两条路径都因此对得上:桌面版走 `~/.dsh`(实测 profile 在其下),隔离探针靠 `$DSH_HOME`。
16
+ *
17
+ * `homeDir` 只用于测试注入假主目录;默认值仍是 `homedir()`,行为与搬家前一致。
18
+ */
19
+ export declare function resolveHome(env?: NodeJS.ProcessEnv, homeDir?: string): string;
@@ -1,7 +1,14 @@
1
1
  /** 摘内存的三种落地(见 {@link HostPort.evictLive}):`not-live` / `evicted` 都意味着**宿主列表已经不含它**;
2
2
  * `unknown` 意味着读不到或摘不掉,**不能**据此通告客户端(发了就是假话,重连后那一行会自己长回来)。 */
3
3
  export type EvictionOutcome = 'evicted' | 'not-live' | 'unknown';
4
- export interface HostPort {
4
+ /**
5
+ * 角色一:**身份与归属**——会话是谁、落在哪个项目/工作区、活不活/归档没有,以及账本与归档的写面。
6
+ *
7
+ * 为什么拆成角色:宿主能力的端口按角色切成三块,而不是一个什么都有的接口。调用方要的是
8
+ * "一句话能说清的那一组能力",而一个什么都有的端口让每条用例都得凑齐不相干的成员。
9
+ * 这里只切**类型**,adapter 仍是同一颗对象。
10
+ */
11
+ export interface HostIdentityPort {
5
12
  workspaceOf(sessionId: string): Promise<string | undefined>;
6
13
  /** 账本里还有这个工作区吗。**只读的判别**(`registry.get(id) !== undefined`),不抛。
7
14
  *
@@ -20,16 +27,30 @@ export interface HostPort {
20
27
  titleOf(sessionId: string): Promise<string>;
21
28
  isLive(sessionId: string): Promise<boolean>;
22
29
  isArchived(sessionId: string): Promise<boolean>;
30
+ unarchive(sessionId: string): Promise<void>;
31
+ detach(sessionId: string, workspaceId: string): Promise<void>;
32
+ attach(sessionId: string, workspaceId: string): Promise<void>;
33
+ /** 归档是 registry **全局**的,与工作区账目无关 ⇒ `workspaceId` 只是调用点对称用的可选入参
34
+ * (未分组的会话没有工作区可传)。 */
35
+ archive(sessionId: string, workspaceId?: string): Promise<void>;
36
+ }
37
+ /**
38
+ * 角色二:**在场通告**——摘掉宿主内存里的活体,并如实通告客户端"这个会话没了 / 回来了"。
39
+ *
40
+ * 这一组的方法按契约**不抛**(`evictLive` 三态、`announce*` 内部吞异常):它们是删除/恢复的收尾,
41
+ * 收尾失败不把已经完成的那件事翻成失败。
42
+ */
43
+ export interface HostPresencePort {
23
44
  /** 尽力把会话从宿主**内存里的 live store** 摘掉;三态见 {@link EvictionOutcome}。
24
- * **这是本仓走宿主内部入口的两处之一**(另一处是 {@link HostPort.announceRemoved} 发的转发事件,
25
- * 理由与代价见实现处的注释):删除的两半里,"磁盘上的持久化"我们搬得走,
45
+ * **这是本仓走宿主内部入口的几处之一**(另见 `src/ports/agent-handles.ts` 对 `agents.resume/create` 的包装,
46
+ * 以及 {@link HostPresencePort.announceRemoved} 发的转发事件):删除的两半里,"磁盘上的持久化"我们搬得走,
26
47
  * "内存里 attach 着的活体"只有这一条路能请掉。 */
27
48
  evictLive(sessionId: string): Promise<EvictionOutcome>;
28
49
  /** **如实通告**客户端"这个会话没了":发宿主自己声明并转发的 `api-session/removed`,客户端当场删行。
29
50
  * 冷会话没有活体可摘,但同样需要这一条——否则那一行要等到重连(`handleConnected` → 重列)才消失。 */
30
51
  announceRemoved(sessionId: string): Promise<void>;
31
52
  /** **如实通告**客户端"这个会话回来了":摘要取自宿主自己的会话列表,发 `api-session/added`。
32
- * 正常恢复走 {@link HostPort.announceRestoredBatch}(整批一次预取);这一条单条路是**删除组回滚**
53
+ * 正常恢复走 {@link HostPresencePort.announceRestoredBatch}(整批一次预取);这一条单条路是**删除组回滚**
33
54
  * 用的(成员已经收过"已移除"、退回原处后补发)。取不到摘要就不发。 */
34
55
  announceRestored(sessionId: string): Promise<void>;
35
56
  /** 宿主会话表的一次预取:`[{ sessionId, summary }]`,`summary` 是宿主**原样**那一条。
@@ -45,29 +66,38 @@ export interface HostPort {
45
66
  readonly sessionId: string;
46
67
  readonly summary: unknown;
47
68
  }[]): Promise<void>;
48
- unarchive(sessionId: string): Promise<void>;
49
- detach(sessionId: string, workspaceId: string): Promise<void>;
50
- attach(sessionId: string, workspaceId: string): Promise<void>;
51
- /** 归档是 registry **全局**的,与工作区账目无关 ⇒ `workspaceId` 只是调用点对称用的可选入参
52
- * (未分组的会话没有工作区可传)。 */
53
- archive(sessionId: string, workspaceId?: string): Promise<void>;
54
- /** 整个会话语料(live ∪ persisted)的只读身份表。**服务缺席回 `undefined`**,不拿空数组冒充
55
- * “这个家没有会话”;扫描方必须把缺席当失败。 */
56
- listSessions(): Promise<readonly SessionIdentity[] | undefined>;
57
- /** 一次 BFS 点出多个父的全部后代;键按调用方传进来的写法回。
58
- * `sessionQuery` 缺席时回 `{}`(与 delete 的降级一致)——写侧不能把这当成"盘上没有后代",
59
- * 只能当成"这一趟看不见",残留留给 `GET /orphans` 兜底。 */
60
- subagentMembersBatch(parentIds: readonly string[]): Promise<Record<string, readonly SubagentMember[]>>;
61
- /** 数一组父会话在盘上的**后代**条数(孩子与孙代都算——回收站清单上那个只读计数,宿主客户端把
62
- * `origin:"subagent"` 的行藏掉了 ⇒ 不说就没人知道删了父还剩多少)。键按**传进来的那个写法**回;
63
- * 没有后代的不表现在结果里(缺省即 0);`sessionQuery` 没挂回 `{}`,不抛。 */
64
- subagentCounts(parentIds: readonly string[]): Promise<Record<string, number>>;
65
- /** 列出这个父会话**在盘上的全部后代**(删除时一起带走)。来历与缺席行为见实现处:服务没挂回 `[]`。 */
66
- subagentMembers(sessionId: string): Promise<readonly {
67
- readonly sessionId: string;
68
- readonly projectDir: string;
69
- }[]>;
70
69
  }
70
+ /**
71
+ * 一次语料读的结果:**缺席显式化**——`available: false` 说的是"这一趟看不见语料"(`sessionQuery` 没挂,
72
+ * 或它的读面方法缺席 / 不是函数),与"读到了、里面是空的"是两件事,调用方对两者的策略不同(前者只许
73
+ * 降级或响亮失败,后者是可以行动的事实)。**不带原因串**:调用点的策略已经决定了怎么处理,带一个字
74
+ * 就会有人去断言那个字;将来真要区分"没挂"与"方法缺席",再加一个显式字段。
75
+ */
76
+ export type CorpusRead<T> = {
77
+ readonly available: true;
78
+ readonly value: T;
79
+ } | {
80
+ readonly available: false;
81
+ };
82
+ /**
83
+ * 角色三:**会话语料**——整表身份与子会话闭包。
84
+ *
85
+ * 两个读面都把"这一趟看不见语料"写成显式的 {@link CorpusRead}:`{ available: false }` 与
86
+ * "看见了、里面是空的"是两件事。**缺席之后怎么办归调用方,清单见 `CONTEXT.md` 的「语料缺席」**。
87
+ */
88
+ export interface HostCorpusPort {
89
+ /** 整个会话语料(live ∪ persisted)的只读身份表。**服务缺席是显式的**:`sessionQuery` 没挂,或它的
90
+ * `listSessions` 缺席 / 不是函数 ⇒ `{ available: false }`,不拿空数组冒充“这个家没有会话”。 */
91
+ listSessions(): Promise<CorpusRead<readonly SessionIdentity[]>>;
92
+ /** 一次 BFS 点出多个父的全部后代(传递闭包)。
93
+ * **缺席是显式的,且优先于空入参**:`sessionQuery` 没挂,或它的 `filterSessions` 缺席 / 不是函数 ⇒
94
+ * `{ available: false }`,不拿空表冒充"盘上没有后代";入参是不是空的都不改变这一条。
95
+ * 服务在场时,空入参 ⇒ `{ available: true, value: {} }`,一次宿主调用都不发。
96
+ * 键按调用方传进来的写法回(同一个父的 `session-x` / `x` 两种写法是同一个根,见 `membersFor`)。 */
97
+ subagentMembersOf(parentIds: readonly string[]): Promise<CorpusRead<Record<string, readonly SubagentMember[]>>>;
98
+ }
99
+ /** 复合别名:adapter(`createHostPort`)与调用方把这 17 个方法当一个端口用;三个角色是它的切面。 */
100
+ export type HostPort = HostIdentityPort & HostPresencePort & HostCorpusPort;
71
101
  export interface SessionIdentity {
72
102
  readonly sessionId: string;
73
103
  readonly parentSessionId?: string;
@@ -102,7 +132,7 @@ export declare const NO_CWD_PROJECT_DIR = "_no-cwd";
102
132
  * (手工放进回收站的目录,从日志 header 的 `cwd` 反推它原本属于哪个项目目录)——算法只有这一处家。
103
133
  */
104
134
  export declare function projectKey(cwd: string): string;
105
- /** 会话 id 去掉 `session-` 前缀后的形状(同一个父在两代里写法不同,见 {@link HostPort.subagentCounts})。 */
135
+ /** 会话 id 去掉 `session-` 前缀后的形状(同一个父在两代里写法不同,见 {@link HostCorpusPort.subagentMembersOf})。 */
106
136
  export declare function bareSessionId(id: string): string;
107
137
  /** 这次失败是不是"账本里没那个工作区"(而不是另有故障)。恢复侧靠它把两种失败分开:
108
138
  * 前者按「未分组」收,后者照旧回滚报 `io`——不给真故障静默改归属的机会。 */
@@ -112,6 +142,6 @@ export declare function isWorkspaceMissingError(error: unknown): boolean;
112
142
  *
113
143
  * 每个方法的依据(服务 / 方法 / 入参)与原文切片见各自的实现处注释。
114
144
  * 出错一律**原样抛**(不在这里裹动词码):动词层把原始错误挂在 `cause` 上,
115
- * `toRemoteFailure` 再按 `name` 把它映射回 `sessiondelete/<码>`(设计稿 §6 的两行映射)。
145
+ * `toRemoteFailure` 再按 `name` 把它映射回 `sessiondelete/<码>`(src/remote/map-error.ts 的两行映射)。
116
146
  */
117
147
  export declare function createHostPort(ctx: unknown): HostPort;
@@ -1,6 +1,6 @@
1
1
  import { type RemoteFailure } from '../shared/wire.js';
2
2
  /**
3
- * 把任意抛出物折成失败信封里的**那三件**(设计稿 §6 唯一那张映射表)。
3
+ * 把任意抛出物折成失败信封里的**那三件**(本文件唯一那张映射表)。
4
4
  *
5
5
  * - 已经是远程失败(`isDSHRemoteError` 认得的结构标记)⇒ 原样带出它自己的 `code` / `message`,
6
6
  * 挂着的 `details` 一起过桥,不再包一层。
@@ -8,7 +8,7 @@ import { type RemoteFailure } from '../shared/wire.js';
8
8
  * 把它们折成 `sessiondelete/io` 就是把宿主的判断抹成我们的,客户端拿到的 code 从此不能说真话。
9
9
  * - 宿主类型化错误(自身或 `cause` 链上,见 {@link hostErrorOf})⇒ 映射成 `live` / `not-found`,
10
10
  * 并把它自己身上的结构化字段带成 `details`。这一条排在动词码之前:动词会把宿主错误包成笼统的 `io`
11
- * 并把原错误挂在 cause 上(`verbError('io', …, cause)`),不看进 cause 链,§6 那两行映射永远到不了客户端。
11
+ * 并把原错误挂在 cause 上(`verbError('io', …, cause)`),不看进 cause 链,本文件那两行映射永远到不了客户端。
12
12
  * - 动词的 `sessiondelete/<五个码>` ⇒ 原样过桥(`details` 同样带着)。
13
13
  * - 其余(别域的怪 code、前缀外的因、非 Error 抛出物)⇒ `io`,message 保留原文。
14
14
  *
@@ -26,7 +26,7 @@ export declare const SESSION_DELETE_API_PREFIX = "/dsh-session-delete-api";
26
26
  * 一次 HTTP 带一组 id、逐条回结果;单条就是长度为 1 的批量,仓里不再留第二条执行路径。
27
27
  * - `GET /list`:**对账并读**清单(`session-trash/` 里的目录是准)。
28
28
  * - `GET /ping`:最便宜的在/不在(不碰清册、不碰盘、不进链)——客户端探测用它,不用一次全表读。
29
- * - `POST /detail`:单条详情(用 POST 表达一个读:见设计稿 2026-10-15 §3)。
29
+ * - `POST /detail`:单条详情(用 POST 表达一个读:判据就写在 `src/api/route-table.ts` 那一行的上方)。
30
30
  * - `POST /apply`:批量执行一个动作(`delete` / `restore` / `purge` / `count` / `orphan-clean`),逐条回 `ApplyOutcome`。
31
31
  * - `POST /empty`:清空回收站(聚合回执,**不**按条回)。
32
32
  * - `GET /orphans`:无主子会话的扫描报告(纯读,2026-10-19)。**清理不在这里**——它是 `/apply` 的
@@ -95,13 +95,15 @@ export interface TrashEntryWire {
95
95
  * 也不许拿缺席冒充否。
96
96
  */
97
97
  export interface TrashRowWire extends TrashEntryWire {
98
- /** **这一行「彻底删除」会释放多少**:本行 + 跟着进来的孩子 + 盘上还留着的后代。
99
- * 它是**存储侧的事实**(彻底删除删的正是这三样),所以由 Node 半算一次过线;界面上头部合计与
100
- * 单条确认框共用这一个数——不许各算一遍(各算一遍就是两套口径,且客户端那套会重复计数)。 */
98
+ /** **这一行「彻底删除」会释放多少**:这一组在盘上**实测**的字节合计(回收站里本行与孩子的目录 +
99
+ * 盘上还留着的后代),与 purge 的 `freedBytes` 同一口径。算式只住 `verbs/permanent-removal.ts` 的
100
+ * `planVisibleTrash`,Node 半算一次过线、投影原样搬运;界面上头部合计与单条确认框共用这一个数——
101
+ * 不许各算一遍(各算一遍就是两套口径,且客户端那套会重复计数)。 */
101
102
  releaseBytes: number;
102
103
  /** 跟着这条父会话**一起进了回收站**的孩子数(清册里 `parentEntryId` 指向它的行)。 */
103
104
  subagentCount?: number;
104
- /** 那些孩子的字节合计(头部合计要算上它们,不能只加父自己那一格)。 */
105
+ /** 那些孩子**回收站目录的实测字节**合计(`planVisibleTrash` 在盘上现量,与 `releaseBytes` 同一条口径;
106
+ * 不念清册里记的 `sizeBytes`。它们已折进 `releaseBytes`——头部合计照念那个数,别再单独相加)。 */
105
107
  subagentBytes?: number;
106
108
  /** **还留在盘上、没被带走**的孩子数(宿主 `sessionQuery`)。与 `subagentCount` 各说各的。 */
107
109
  strandedSubagents?: number;
@@ -120,7 +122,7 @@ export interface TrashDetailMessageWire {
120
122
  /** 回收站单条详情的线上形状(`POST /detail`)。 */
121
123
  export interface TrashDetailWire {
122
124
  entryId: string;
123
- /** 日志 header 里的原串(**无损**,比从 `projectKey` 反推那一路强,设计稿 §7 组头那条是另一回事)。 */
125
+ /** 日志 header 里的原串(**无损**,比从 `projectKey` 反推那一路强;组头怎么念项目名见 `src/client/trash-view.ts` 的 `projectLabel`,那是另一回事)。 */
124
126
  cwd: string;
125
127
  createdAt: number;
126
128
  lastActivity: number;
@@ -167,7 +169,7 @@ export declare const PARAMS: {
167
169
  readonly ids: "ids";
168
170
  };
169
171
  /**
170
- * 失败信封的**线上形状**(设计稿 §6):`code` 是判别依据、`message` 是上屏原文、
172
+ * 失败信封的**线上形状**(本文件):`code` 是判别依据、`message` 是上屏原文、
171
173
  * `details` 是**可选的结构化附带物**——对齐官方 `rpcErrorSchema` 那三件
172
174
  * (`dsh-client-connection` 原文:`z.object({ code, message, details: z.record(z.string(), z.unknown()) })`)。
173
175
  * 来历两类:宿主类型化错误自己身上的字段(`WorkspaceActiveSessionError` 的 `sessionId` / `activity`…),
@@ -196,9 +198,9 @@ export type RemoteResult<T> = {
196
198
  * 浏览器半是 `src/client/remote.ts` 的 `throwRemoteFailure`。
197
199
  */
198
200
  export declare function isDSHRemoteError(value: unknown): value is RemoteFailure;
199
- /** 失败码的域。与宿主官方域(`workspace/*`、`session/*`…)不重名,见设计稿 §6。 */
201
+ /** 失败码的域。与宿主官方域(`workspace/*`、`session/*`…)不重名,见本文件。 */
200
202
  export declare const SESSION_DELETE_ERROR_DOMAIN = "sessiondelete";
201
- /** 五个**动词码**(设计稿 §6 那张表)。`io` 是唯一兜底。 */
203
+ /** 五个**动词码**(本文件那张表)。`io` 是唯一兜底。 */
202
204
  export declare const VERB_CODES: readonly ["live", "not-found", "trash-empty", "project-missing", "io"];
203
205
  export type VerbCode = (typeof VERB_CODES)[number];
204
206
  /** 三个**路由自检码**:请求本身不对,不是一次操作的结果(它们带 HTTP 状态,见 `api/dispatch.ts`)。 */
@@ -10,7 +10,7 @@ export declare const TOTAL_CHAR_CAP = 500000;
10
10
  export type TrashDetailMessage = TrashDetailMessageWire;
11
11
  export type TrashDetail = TrashDetailWire;
12
12
  export type DetailDeps = {
13
- fs: FsPort;
13
+ fs: Pick<FsPort, 'exists' | 'listFiles' | 'readFile'>;
14
14
  manifest: ManifestPort;
15
15
  home: string;
16
16
  };
@@ -1,5 +1,5 @@
1
1
  /** 回收站的一条记录。`id` 由 `makeEntryId` 生成(`<删除时刻>-<原目录名>`)——**它不是随手起的键**:
2
- * 回收站目录名就是它,恢复也按它反推出原目录名放回(设计稿 §3),所以 id 的形状有契约意义。
2
+ * 回收站目录名就是它,恢复也按它反推出原目录名放回(CONTEXT.md 的「条目」条),所以 id 的形状有契约意义。
3
3
  * 其余字段在删除那一刻就地取。 */
4
4
  export interface TrashEntry {
5
5
  id: string;
@@ -8,8 +8,8 @@ export interface TrashEntry {
8
8
  /** 删除时它所属的工作区;**空串 = 删除时就没有账目**(「未分组」的会话,宿主的"删除工作区"会造出这种),
9
9
  * 恢复时据此跳过 `attach`。 */
10
10
  workspaceId: string;
11
- /** 删除**之前**的归档态。删除会把"原本未归档"的会话归档(藏起来,免得它掉进「未分组」),
12
- * 恢复时按这个字段还原可见性:true ⇒ 仍处归档态,false ⇒ 取消归档放回树里。 */
11
+ /** 删除**之前**的归档态,**只作事实记录**:2026-10-21 起删除不归档、恢复也不按它还原可见性。
12
+ * 旧清单迁移只用它区分"删除时是我们归档的"(false 且当前仍归档)与"用户删前自己归档的"(true,保持归档)。 */
13
13
  wasArchived: boolean;
14
14
  deletedAt: number;
15
15
  /** 删除时**尽力**取的会话标题(来源:宿主 projection cache 服务;底层是
@@ -21,7 +21,7 @@ export interface TrashEntry {
21
21
  * 缺席 = 这一行是父(或它自己就是被单独删掉的)。清单据此把孩子折进父行、不单独列。
22
22
  * 老代留底 / 清册被清后由目录重建出来的行**没有这个字段**(文件侧没有它的副本,与
23
23
  * `workspaceId`/`wasArchived`/`title` 同一类降级),那一趟它们会各占一行——那是"分组信息随清册一起没了"
24
- * 的如实结果,不假装知道(设计稿 2026-10-15 §10)。 */
24
+ * 的如实结果,不假装知道(CONTEXT.md 的「无主子会话」条)。 */
25
25
  parentEntryId?: string;
26
26
  }
27
27
  /**
@@ -8,7 +8,8 @@ export interface ManifestPort {
8
8
  export declare function createMemoryManifest(seed?: TrashEntry[]): ManifestPort;
9
9
  /** 宿主 storage domain 的最小面。这是**接缝**,不是对宿主 API 的断言:
10
10
  * 探针只枚举到 `ctx.storageDomain` 有 `open` / `get` / `domains` / `reserved` 等成员,
11
- * **没有**枚举 `open()` 返回的 domain 句柄面 ⇒ 真实绑定由 `createStorageManifest` 写一层薄适配
11
+ * **没有**枚举 `open()` 返回的 domain 句柄面 ⇒ 真实绑定由 `src/trash/storage-manifest.ts` 的
12
+ * `createHostStorageManifest` 写一层薄适配
12
13
  * (官方读面若只有迭代器/快照,就把迭代器折成 `readAll()`,不在动词层引入第二种读法)。 */
13
14
  export interface StorageDomainLike {
14
15
  readAll(): Promise<TrashEntry[]>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 回收站清单的**对账**:以 `session-trash/` 里的条目目录为准,把清单修回一致(设计稿 2026-10-15 §9)。
2
+ * 回收站清单的**对账**:以 `session-trash/` 里的条目目录为准,把清单修回一致(CONTEXT.md 的「对账」条)。
3
3
  *
4
4
  * 手工动回收站**只需要动那一个目录**——多出来的目录在这里被重建出行、目录没了的行在这里被摘掉;
5
5
  * `~/.dsh/storages/` 那份清单从此是**内部索引**,不用手碰。
@@ -13,7 +13,7 @@
13
13
  * ③ **重建行只写"文件里真有的事实"**:`id` = 目录名、`sessionId`/`cwd` 读日志 header、
14
14
  * `projectDir` 由 `cwd` 经宿主同名算法 `projectKey` 反推、`deletedAt` 从目录名的时间戳解、
15
15
  * `sizeBytes` 现场量。**文件侧没有副本的三件**按缺省落:`workspaceId: ''`(恢复时跳过挂账本,
16
- * 落在「未分组」)、`wasArchived: false`(恢复按"可见"还原)、`title: ''`(详情有日志标题兜底)
16
+ * 落在「未分组」)、`wasArchived: false`(文件侧没有归档事实;旧条目恢复走迁移支)、`title: ''`(详情有日志标题兜底)
17
17
  * ——这是重建的固有代价,不是可以"再读读看"的东西。
18
18
  * ④ **认不出 / 读不出的目录:不动、不列**(名字不像条目、目录里没有日志、header 读不出、尺寸量不出,
19
19
  * 各跳过各的)。它是"外人放进来的东西",不是用户动作失败:跳过即静默,绝不为它删目录或抛掉整次读数。
@@ -0,0 +1,12 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type ManifestPort } from './manifest.js';
3
+ /**
4
+ * 清单的真实绑定:官方 storage domain → `ManifestPort`(`StorageDomainLike` 在动词层保持一种读法)。
5
+ * 域**懒开**且成功句柄只开一次(`apply` 是同步的,而 `open()` 是异步的),并挂一条 `ctx.effect` 清理:
6
+ * 插件卸载时关掉句柄并把 `close()` 的落地交回框架(句柄归调用方所有,这是 `open()` 的 JSDoc 明写的契约;
7
+ * 2026-10-17 起不再 `void` 走掉——没关严的句柄会让下一份同名域撞 `already-open`)。
8
+ *
9
+ * **失败不缓存**:`open()` 被拒是瞬态(域被别的持有者占着、装载竞态),把第一次的 rejected promise
10
+ * 缓存起来等于"这个会话永远删不掉"——下一次读会重新走 `open()`,成功才落下活句柄。
11
+ */
12
+ export declare function createHostStorageManifest(ctx: Context): ManifestPort;
@@ -1,3 +1,3 @@
1
1
  import type { ApplyAction, ApplyOutcome } from '../shared/wire.js';
2
- import type { DeleteDeps } from './delete-session.js';
2
+ import type { DeleteDeps } from '../deps.js';
3
3
  export declare function applyActions(deps: DeleteDeps, action: ApplyAction, ids: readonly string[]): Promise<ApplyOutcome[]>;
@@ -1,13 +1,9 @@
1
1
  import { type TrashEntry } from '../trash/entry.js';
2
- import type { FsPort } from '../ports/fs-port.js';
3
- import type { HostPort } from '../ports/host-port.js';
4
- import type { ManifestPort } from '../trash/manifest.js';
5
- export interface DeleteDeps {
6
- fs: FsPort;
7
- host: HostPort;
8
- manifest: ManifestPort;
9
- home: string;
10
- now(): number;
2
+ import type { DeleteDeps } from '../deps.js';
3
+ export interface DeleteResult {
4
+ readonly entry: TrashEntry;
5
+ /** 这一次真的搬进回收站的成员(父 + 盘上存在的孩子);批量层用它建幂等账本。 */
6
+ readonly movedSessionIds: readonly string[];
11
7
  }
12
8
  /**
13
9
  * 删除 = 移入回收站,**按组走**:用户点掉一条父会话时,宿主的客户端把 `origin:"subagent"` 的孩子
@@ -20,8 +16,9 @@ export interface DeleteDeps {
20
16
  * ② **逐个搬**:每个成员是一份 `journal`(`verbs/member-journal.ts`),搬不动时 journal 自己把
21
17
  * 这个成员已经做成的步骤退回去。
22
18
  * ③ **组级回滚**:某一个成员搬不动时,把**已经搬进去的**那些按相反顺序退回原处(清册行跟着摘、
23
- * 账本跟着挂、可见性跟着还原),失败照抛——不留"半组在回收站、半组在原地"的世界。
19
+ * 账本跟着挂),失败照抛——不留"半组在回收站、半组在原地"的世界。
24
20
  *
25
21
  * interface 只有**一个 id**:批量的性质不在这里(同批"已被前一条带走"的判据归 `verbs/batch.ts`)。
22
+ * 结果带回这次真的搬走的整组(`movedSessionIds`)——它是动词交回的内部数据,不再靠回调借用动词的手。
26
23
  */
27
- export declare function deleteSession(deps: DeleteDeps): (sessionId: string) => Promise<TrashEntry>;
24
+ export declare function deleteSession(deps: DeleteDeps): (sessionId: string) => Promise<DeleteResult>;
@@ -5,6 +5,10 @@ import { type RouteCode, type VerbCode } from '../shared/wire.js';
5
5
  export declare const VERB_CODES: readonly ["live", "not-found", "trash-empty", "project-missing", "io"];
6
6
  export declare const ROUTE_CODES: readonly ["bad-method", "not-found-route", "bad-request"];
7
7
  export type { RouteCode, VerbCode };
8
+ /** `error.code` 是不是本仓的动词码(`sessiondelete/<码>`)。**唯一判据**:`journalFailure` 与永久删除
9
+ * 执行器要"已经是动词码就原样抛"时都问这一句。同文件的 `withRollbackFailures` 还要码的后缀,所以它
10
+ * 就地 `slice` 一次——前缀仍只住 `VERB_CODE_PREFIX` 这一处,别在调用点另写一份字面量。 */
11
+ export declare function isVerbCodeError(error: unknown): boolean;
8
12
  /** 一次失败的**结构**:码、上屏原文、可选 cause(`map-error` 沿 cause 链认宿主类型化错误)、
9
13
  * 可选的结构化附带物(对齐官方 `rpcErrorSchema` 的第三件 `details`)。 */
10
14
  export interface VerbError extends Error {
@@ -1,5 +1,5 @@
1
1
  export interface JournalStep {
2
- /** 这一步在做什么。失败原文按「`<label>:<原错误>`」拼——与既有那 22 处 `rollbackReason` 的措辞同形。 */
2
+ /** 这一步在做什么。失败原文按「`<label>:<原错误>`」拼——与迁移前那 22 处 `rollbackReason` 的措辞同形。 */
3
3
  readonly label: string;
4
4
  /** 做这一步。抛什么都由 journal 折成 `sessiondelete/io`(除非它自己已经是一个动词码)。 */
5
5
  run(): Promise<void>;
@@ -20,7 +20,8 @@ export declare function createJournal(steps: readonly JournalStep[]): Journal;
20
20
  export declare function attempt(label: string, run: () => Promise<unknown>): Promise<readonly string[]>;
21
21
  /**
22
22
  * **组级回滚**:把已经做成的成员按相反顺序退回;某一步退不干净时把失败并进原错误的
23
- * `details.rollbackFailures`,绝不让原错误独走(设计稿第十七批)。删除与恢复两个按组走的动词
24
- * 共用这一处——"相反顺序 + 失败合并"不再各写一遍。
23
+ * `details.rollbackFailures`,绝不让原错误独走(2026-10-19 `f2a7757`;那些失败句子的合并与措辞住
24
+ * `src/verbs/errors.ts`,这里只是唯一的调用方)。删除与恢复两个按组走的动词共用这一处,
25
+ * "相反顺序 + 失败合并"不再各写一遍。
25
26
  */
26
27
  export declare function rollbackJournals(journals: readonly Journal[], error: unknown): Promise<never>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 成员离场 / 归位的通告协议:摘活体、三态判断、成对事件只在这里做。
3
+ *
4
+ * 为什么要有它:delete(成功与回滚)/ restore 回滚 / purge(两个分支)/ orphan-clean
5
+ * 六处调用曾经各写一遍
6
+ * `evictLive → !== 'unknown' → announceRemoved`;删除回滚还要自己拼
7
+ * `movedBack && workspaceBack` 才发 `announceRestored`。
8
+ * 这条协议说错一次就是客户端留着假行、或删掉真行,所以只留一个家。
9
+ *
10
+ * 边界:
11
+ * - 批量恢复的 `announceRestoredBatch` 不进来:一次预取、一批一次通告归 `verbs/batch.ts`(ADR-0002)。
12
+ * - 不读 journal 的失败串:调用方交事实(目录 / 账本是否回去),真值表住这里。
13
+ * - 本模块用到的三条(`evictLive` / `announceRemoved` / `announceRestored`)按契约不抛(evictLive 三态、
14
+ * announce* 内部吞异常),这里不再叠 catch。
15
+ */
16
+ import type { HostPresencePort } from '../ports/host-port.js';
17
+ /** 本模块实际用到的三条宿主能力。取窄面而不是整个 `HostPresencePort`:那条协议里还有批量通告与
18
+ * 摘要预取,它们各有自己的家(`verbs/batch.ts`),这里不该因为共用同一颗 adapter 就声明依赖。 */
19
+ export type MemberPresenceHost = Pick<HostPresencePort, 'evictLive' | 'announceRemoved' | 'announceRestored'>;
20
+ export interface MemberBackFacts {
21
+ /** 目录搬回原处了吗。 */
22
+ readonly diskBack: boolean;
23
+ /** 账本挂回去了吗(本来没有账目时算 true)。 */
24
+ readonly workspaceBack: boolean;
25
+ }
26
+ export interface MemberPresence {
27
+ /** 成员已经不在宿主可见列表:摘活体;三态不是 unknown 时发 removed。
28
+ * `backInTrash: false` 表示目录没退回回收站(恢复回滚失败),此时什么都不做。 */
29
+ memberGone(sessionId: string, facts?: {
30
+ readonly backInTrash?: boolean;
31
+ }): Promise<void>;
32
+ /** 成员已经回到原来的可见状态:目录与账本两件写都回去才发 added。 */
33
+ memberBack(sessionId: string, facts: MemberBackFacts): Promise<void>;
34
+ }
35
+ export declare function createMemberPresence(host: MemberPresenceHost): MemberPresence;
@@ -1,6 +1,6 @@
1
1
  import { type SessionIdentity } from '../ports/host-port.js';
2
2
  import type { OrphanCleanStatsWire, OrphanReportWire } from '../shared/wire.js';
3
- import type { DeleteDeps } from './delete-session.js';
3
+ import type { CoreDeps } from '../deps.js';
4
4
  /** 组里一个人的盘上身份(字节是**扫描那一刻**量的,清理时按目录重量一次,不拿这份快照冒充准数)。 */
5
5
  export interface OrphanMember {
6
6
  readonly sessionId: string;
@@ -13,27 +13,6 @@ export interface OrphanGroup {
13
13
  readonly parentSessionId: string;
14
14
  readonly members: readonly OrphanMember[];
15
15
  }
16
- export interface MeasuredMemberDir {
17
- readonly dir: string;
18
- readonly bytes: number;
19
- }
20
- /** 一个成员被删之前要记下的三件:目录、字节、账本归属(缺席 = 未分组)。purge 与 orphan-clean 共用。 */
21
- export interface MemberRemovalPlan extends MeasuredMemberDir {
22
- readonly sessionId: string;
23
- readonly workspaceId?: string;
24
- }
25
- /** 成员的盘上目录与字节(扫描、清单装配与 purge 共用这一条路径/存在性/度量口径;
26
- * 目录缺席回 `undefined`,不拿一份"记录还在、盘上已经没了"的旧读数冒充盘上残留)。 */
27
- export declare function measureMemberDir(deps: DeleteDeps, member: {
28
- readonly sessionId: string;
29
- readonly projectDir: string;
30
- }): Promise<MeasuredMemberDir | undefined>;
31
- /** 把一组成员折成"删之前要记下的计划";目录不在的成员跳过(记录与盘不一致是那条成员自己的事)。
32
- * `purge` 的盘上后代与 `orphan-clean` 的整组都走这一处,别再各写一遍 measure + workspaceOf。 */
33
- export declare function planMemberRemovals(deps: DeleteDeps, members: readonly {
34
- readonly sessionId: string;
35
- readonly projectDir: string;
36
- }[]): Promise<MemberRemovalPlan[]>;
37
16
  /** {@link orphanGroupsOf} 的中间形状:还没量字节的那一组(纯函数不碰盘)。 */
38
17
  interface RawGroup {
39
18
  readonly root: SessionIdentity;
@@ -44,10 +23,10 @@ interface RawGroup {
44
23
  * 无主子会话 = `origin:"subagent"`、父**既不在当前语料(live ∪ persisted)也不在回收站清册里**的会话。
45
24
  *
46
25
  * 为什么清册里的父不算孤儿:那种是 **stranded**——父还在回收站里等着,孩子本该跟着父一起恢复或一起
47
- * 彻底删除(10-15 稿 §11 的组语义),把它的根当孤儿删掉就是**替用户做了一个没人点过的决定**。孤儿只
26
+ * 彻底删除(组语义见 `src/trash/groups.ts`),把它的根当孤儿删掉就是**替用户做了一个没人点过的决定**。孤儿只
48
27
  * 有一种来路:父被彻底删掉了(或被手工从盘上清掉了),清册里再也没有这一行。
49
28
  *
50
- * 根按**传递闭包**收全部后代(与 `HostPort.subagentCounts` 同一口径:孩子与孙代都算,深度不设上限),
29
+ * 根按**传递闭包**收全部后代(与 `HostCorpusPort.subagentMembersOf` 同一口径:孩子与孙代都算,深度不设上限),
51
30
  * 同一条记录只归一组(`claimed`)——数据里真出现环时结果是"少数一条",不是把一个会话删两遍。
52
31
  * 归堆与去重**两边都去 `session-` 前缀**:同一个父在两代宿主里写法不同(见 `bareSessionId` 的来历)。
53
32
  */
@@ -55,28 +34,25 @@ export declare function orphanGroupsOf(corpus: readonly SessionIdentity[], trash
55
34
  /** 纯读:调用方应先走一次 `/list` 对账(面板的 refresh 就是这样);本函数不写清单。
56
35
  * 写动作 `orphan-clean` 会自己先 reconcile,避免拿一份过期清册判"父没了"。
57
36
  *
58
- * `listSessions()` 回 `undefined` 时**必须抛**:那是宿主的会话语料服务没挂,不是"这个家没有孤儿"。
59
- * 拿 undefined 当空语料 ⇒ 报告说"零条孤儿"、界面上一句"没有要清理的",而盘上真有几百 MB——那是假话,
60
- * 所以这里按 `io` 响亮失败(AGENTS.md:`undefined` 意味着服务缺席,扫描方不许冒充读数)。 */
61
- export declare function loadOrphanGroups(deps: DeleteDeps): Promise<readonly OrphanGroup[]>;
37
+ * `listSessions()` 回 `{ available: false }` 时**必须抛**:那是宿主的会话语料服务没挂,不是"这个家没有孤儿"。
38
+ * 拿缺席当空语料 ⇒ 报告说"零条孤儿"、界面上一句"没有要清理的",而盘上真有几百 MB——那是假话,
39
+ * 所以这里按 `io` 响亮失败(AGENTS.md:`available: false` 意味着服务缺席,扫描方不许冒充读数)。 */
40
+ export declare function loadOrphanGroups(deps: CoreDeps): Promise<readonly OrphanGroup[]>;
62
41
  /** 把一组组收成那份报告:根清单 + 闭包总数/总字节 + 失踪父的**去重计数** + 按项目目录的分摊。
63
42
  * `groups` 按字节从多到少排(界面上"哪家还剩得最多"要看第一行)。 */
64
43
  export declare function reportOrphans(groups: readonly OrphanGroup[]): OrphanReportWire;
65
44
  /**
66
- * 清一组:先**整组 live 准入(零写)**,再收集目录/字节/账本归属,再 unarchive → detach → 删目录。
67
- * 任何 preflight 失败都在第一件写之前抛出;删目录到一半失败时**已删目录不回滚**,失败错误的
68
- * `details` 带 `{ removed, freedBytes, failedSessionId }` 作为部分回执(不伪装成整组没动)。
69
- * 删目录之后照 purge 的规矩摘活体并如实通告。不碰投影缓存与附件。
45
+ * 清一组:`planOrphanRemoval`(只读计划,整组 live 准入在一切写之前)→ `executeRemoval`(只写)。
46
+ * 量目录、摘归档集与账本、删目录与成员通告的顺序和失败形状都住 `verbs/permanent-removal.ts`;
47
+ * 这里只把这一组接上去,别在这再长出一段手写步骤(purge 走的是同一条路)。
70
48
  *
71
49
  * 顺序与删除那一半是**对偶**的,但不是照抄:
72
- * - 删除要先把会话藏起来(归档)再摘账本,免得中间那一拍它掉进「未分组」;这里目录是**删掉**不是搬走,
73
- * 藏不藏都不影响可见性 ⇒ 归档集反而是**在写之前整批摘掉**的:失败时目录还在,下一次还能重试,
50
+ * - 删除那一半不再用归档藏会话(ADR-0008):它摘账本、搬目录,最后才摘活体。这里目录是**删掉**
51
+ * 不是搬走,藏不藏都不影响可见性 ⇒ 归档集反而**在写之前整批摘掉**:失败时目录还在,下一次还能重试,
74
52
  * 不会留下"会话没了但还在归档集里挂着"的幽灵 id。
75
- * - `unarchive` 对未归档的会话是官方幂等 no-op(`registry.unarchiveSession`),所以不需要删除侧那次
53
+ * - `unarchive` 对未归档的会话是官方幂等 no-op(`registry.unarchiveSession`),所以这里不需要那次
76
54
  * `wasArchived` 读——这里没有"还原到删除前"的义务。
77
55
  * - 摘账本只在账本真记着它时做(`workspaceOf` 未命中 = 本来就是「未分组」,不是故障)。
78
- * - `evictLive` 的三态照 `HostPort.evictLive` 的契约:`unknown` 时**不通告**(发了就是假话),
79
- * `evicted` / `not-live` 才发 `api-session/removed`。冷会话没有活体可摘,通告照样必须发。
80
56
  */
81
- export declare function cleanOrphanGroup(deps: DeleteDeps, group: OrphanGroup): Promise<OrphanCleanStatsWire>;
57
+ export declare function cleanOrphanGroup(deps: CoreDeps, group: OrphanGroup): Promise<OrphanCleanStatsWire>;
82
58
  export {};