@xrn1997/dsh-session-delete 0.2.1 → 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 (57) hide show
  1. package/README.md +46 -124
  2. package/icon.svg +18 -18
  3. package/lib/client.js +1749 -962
  4. package/lib/index.js +1841 -985
  5. package/lib/types/api/dispatch.d.ts +24 -44
  6. package/lib/types/api/route-table.d.ts +35 -0
  7. package/lib/types/api/trash-rows.d.ts +31 -0
  8. package/lib/types/client/batch-bar.d.ts +6 -6
  9. package/lib/types/client/batch-confirm.d.ts +5 -3
  10. package/lib/types/client/batch-menu-item.d.ts +2 -2
  11. package/lib/types/client/batch-select.d.ts +7 -50
  12. package/lib/types/client/batch-selection-store.d.ts +30 -0
  13. package/lib/types/client/copy.d.ts +16 -1
  14. package/lib/types/client/delete-confirm.d.ts +2 -0
  15. package/lib/types/client/host-rows.d.ts +35 -0
  16. package/lib/types/client/main-view.d.ts +37 -0
  17. package/lib/types/client/receipt.d.ts +43 -0
  18. package/lib/types/client/remote.d.ts +62 -76
  19. package/lib/types/client/store.d.ts +17 -0
  20. package/lib/types/client/trash-confirm.d.ts +39 -0
  21. package/lib/types/client/trash-panel.d.ts +4 -2
  22. package/lib/types/client/trash-store.d.ts +2 -0
  23. package/lib/types/client/trash-view.d.ts +38 -0
  24. package/lib/types/client/ui/icons.d.ts +3 -3
  25. package/lib/types/client/undo-toast.d.ts +21 -5
  26. package/lib/types/client/use-column-resize.d.ts +2 -2
  27. package/lib/types/client/use-panel-width.d.ts +1 -1
  28. package/lib/types/client/use-trash-panel-controller.d.ts +55 -0
  29. package/lib/types/client/use-trash-query.d.ts +32 -0
  30. package/lib/types/client/use-trash-selection.d.ts +32 -0
  31. package/lib/types/client/write-commit.d.ts +10 -0
  32. package/lib/types/deps.d.ts +14 -0
  33. package/lib/types/index.d.ts +9 -15
  34. package/lib/types/log/zstd-frames.d.ts +1 -1
  35. package/lib/types/ports/agent-handles.d.ts +25 -0
  36. package/lib/types/ports/home.d.ts +19 -0
  37. package/lib/types/ports/host-port.d.ts +70 -20
  38. package/lib/types/remote/map-error.d.ts +3 -3
  39. package/lib/types/shared/wire.d.ts +107 -11
  40. package/lib/types/trash/detail.d.ts +6 -24
  41. package/lib/types/trash/entry.d.ts +29 -6
  42. package/lib/types/trash/groups.d.ts +45 -0
  43. package/lib/types/trash/manifest.d.ts +2 -1
  44. package/lib/types/trash/reconcile.d.ts +2 -2
  45. package/lib/types/trash/storage-manifest.d.ts +12 -0
  46. package/lib/types/verbs/{apply.d.ts → batch.d.ts} +1 -1
  47. package/lib/types/verbs/delete-session.d.ts +12 -18
  48. package/lib/types/verbs/errors.d.ts +17 -6
  49. package/lib/types/verbs/member-journal.d.ts +27 -0
  50. package/lib/types/verbs/member-presence.d.ts +35 -0
  51. package/lib/types/verbs/orphan-subagents.d.ts +58 -0
  52. package/lib/types/verbs/permanent-removal.d.ts +113 -0
  53. package/lib/types/verbs/purge-trash.d.ts +19 -3
  54. package/lib/types/verbs/restore-session.d.ts +15 -13
  55. package/locale/en.json +6 -6
  56. package/locale/zh.json +6 -6
  57. package/package.json +105 -105
@@ -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,31 @@ 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
- * 取不到摘要就不发(恢复本身已经完成)。 */
53
+ * 正常恢复走 {@link HostPresencePort.announceRestoredBatch}(整批一次预取);这一条单条路是**删除组回滚**
54
+ * 用的(成员已经收过"已移除"、退回原处后补发)。取不到摘要就不发。 */
33
55
  announceRestored(sessionId: string): Promise<void>;
34
56
  /** 宿主会话表的一次预取:`[{ sessionId, summary }]`,`summary` 是宿主**原样**那一条。
35
57
  * 给 {@link announceRestoredBatch} 当 `known` 用——整批只拉一次表。服务没挂回空表,不抛。 */
@@ -44,21 +66,47 @@ export interface HostPort {
44
66
  readonly sessionId: string;
45
67
  readonly summary: unknown;
46
68
  }[]): Promise<void>;
47
- unarchive(sessionId: string): Promise<void>;
48
- detach(sessionId: string, workspaceId: string): Promise<void>;
49
- attach(sessionId: string, workspaceId: string): Promise<void>;
50
- /** 归档是 registry **全局**的,与工作区账目无关 ⇒ `workspaceId` 只是调用点对称用的可选入参
51
- * (未分组的会话没有工作区可传)。 */
52
- archive(sessionId: string, workspaceId?: string): Promise<void>;
53
- /** 数一组父会话在盘上的**后代**条数(孩子与孙代都算——回收站清单上那个只读计数,宿主客户端把
54
- * `origin:"subagent"` 的行藏掉了 ⇒ 不说就没人知道删了父还剩多少)。键按**传进来的那个写法**回;
55
- * 没有后代的不表现在结果里(缺省即 0);`sessionQuery` 没挂回 `{}`,不抛。 */
56
- subagentCounts(parentIds: readonly string[]): Promise<Record<string, number>>;
57
- /** 列出这个父会话**在盘上的全部后代**(删除时一起带走)。来历与缺席行为见实现处:服务没挂回 `[]`。 */
58
- subagentMembers(sessionId: string): Promise<readonly {
59
- readonly sessionId: string;
60
- readonly projectDir: string;
61
- }[]>;
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;
101
+ export interface SessionIdentity {
102
+ readonly sessionId: string;
103
+ readonly parentSessionId?: string;
104
+ readonly projectDir: string;
105
+ readonly subagent: boolean;
106
+ }
107
+ export interface SubagentMember {
108
+ readonly sessionId: string;
109
+ readonly projectDir: string;
62
110
  }
63
111
  /** 无 cwd 的会话落在 `<sessions>/_no-cwd/`(asar `dsh-session-persistence-jsonl/lib/index.js`
64
112
  * 的 `projectDir(root, cwd)`:`if (cwd === void 0) return join(root, "_no-cwd");`)。
@@ -84,6 +132,8 @@ export declare const NO_CWD_PROJECT_DIR = "_no-cwd";
84
132
  * (手工放进回收站的目录,从日志 header 的 `cwd` 反推它原本属于哪个项目目录)——算法只有这一处家。
85
133
  */
86
134
  export declare function projectKey(cwd: string): string;
135
+ /** 会话 id 去掉 `session-` 前缀后的形状(同一个父在两代里写法不同,见 {@link HostCorpusPort.subagentMembersOf})。 */
136
+ export declare function bareSessionId(id: string): string;
87
137
  /** 这次失败是不是"账本里没那个工作区"(而不是另有故障)。恢复侧靠它把两种失败分开:
88
138
  * 前者按「未分组」收,后者照旧回滚报 `io`——不给真故障静默改归属的机会。 */
89
139
  export declare function isWorkspaceMissingError(error: unknown): boolean;
@@ -92,6 +142,6 @@ export declare function isWorkspaceMissingError(error: unknown): boolean;
92
142
  *
93
143
  * 每个方法的依据(服务 / 方法 / 入参)与原文切片见各自的实现处注释。
94
144
  * 出错一律**原样抛**(不在这里裹动词码):动词层把原始错误挂在 `cause` 上,
95
- * `toRemoteFailure` 再按 `name` 把它映射回 `sessiondelete/<码>`(设计稿 §6 的两行映射)。
145
+ * `toRemoteFailure` 再按 `name` 把它映射回 `sessiondelete/<码>`(src/remote/map-error.ts 的两行映射)。
96
146
  */
97
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,12 +8,12 @@ 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
  *
15
15
  * **返回的是线上形状、不是 `Error`**:这份东西唯一的去处是 `src/api/dispatch.ts` 的失败信封
16
- * (以及 `verbs/apply.ts` 的逐条 `error`),而本仓的跨插件 import 红线也不许为此去引宿主的
16
+ * (以及 `verbs/batch.ts` 的逐条 `error`),而本仓的跨插件 import 红线也不许为此去引宿主的
17
17
  * `RemoteError` 类(实例也上不了线:信封只走这三件)。
18
18
  */
19
19
  export declare function toRemoteFailure(error: unknown): RemoteFailure;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 跨半 wire 契约:`/dsh-session-delete-api` 的**前缀、五个端点段、请求字段名**——
2
+ * 跨半 wire 契约:`/dsh-session-delete-api` 的**前缀、六个端点段、请求字段名**——
3
3
  * Node 半(`src/api/dispatch.ts` 注册并分发)与浏览器半(`src/client/remote.ts` 同源 fetch)
4
4
  * 之间唯一的那份真相。两侧都 import 它,不各写一份字面量。
5
5
  *
@@ -22,13 +22,15 @@
22
22
  * 撞了就是整棵插件树 boot 失败——所以这条路径是与生态的硬边界,不是显示好不好看的问题。
23
23
  */
24
24
  export declare const SESSION_DELETE_API_PREFIX = "/dsh-session-delete-api";
25
- /** 五个段名(相对前缀)。2026-10-17 批量重构:写动词从「一条一段」收成**一段 `/apply`**——
25
+ /** 六个段名(相对前缀)。2026-10-17 批量重构:写动词从「一条一段」收成**一段 `/apply`**——
26
26
  * 一次 HTTP 带一组 id、逐条回结果;单条就是长度为 1 的批量,仓里不再留第二条执行路径。
27
27
  * - `GET /list`:**对账并读**清单(`session-trash/` 里的目录是准)。
28
28
  * - `GET /ping`:最便宜的在/不在(不碰清册、不碰盘、不进链)——客户端探测用它,不用一次全表读。
29
- * - `POST /detail`:单条详情(用 POST 表达一个读:见设计稿 2026-10-15 §3)。
30
- * - `POST /apply`:批量执行一个动作(`delete` / `restore` / `purge` / `count`),逐条回 `ApplyOutcome`。
31
- * - `POST /empty`:清空回收站(聚合回执,**不**按条回)。 */
29
+ * - `POST /detail`:单条详情(用 POST 表达一个读:判据就写在 `src/api/route-table.ts` 那一行的上方)。
30
+ * - `POST /apply`:批量执行一个动作(`delete` / `restore` / `purge` / `count` / `orphan-clean`),逐条回 `ApplyOutcome`。
31
+ * - `POST /empty`:清空回收站(聚合回执,**不**按条回)。
32
+ * - `GET /orphans`:无主子会话的扫描报告(纯读,2026-10-19)。**清理不在这里**——它是 `/apply` 的
33
+ * 第五个动作,这一段只负责"有多少、在哪、多大"。 */
32
34
  export declare const ROUTES: {
33
35
  readonly list: "/list";
34
36
  /** 通道的"在不在"专用:没有入参、没有副作用、没有磁盘读。 */
@@ -39,18 +41,23 @@ export declare const ROUTES: {
39
41
  readonly apply: "/apply";
40
42
  /** 清空回收站。 */
41
43
  readonly empty: "/empty";
44
+ /** 纯读:扫描回收站之外的无主子会话(父已不在语料/清册)。 */
45
+ readonly orphans: "/orphans";
42
46
  };
43
- /** `/apply` 的四个动作。id 的含义随动作走:`delete`/`count` 是**宿主会话 id**(还没进回收站),
44
- * `restore`/`purge` 是**回收站条目 id**。 */
45
- export type ApplyAction = 'delete' | 'restore' | 'purge' | 'count';
47
+ /** `/apply` 的五个动作。id 的含义随动作走:`delete`/`count`/`orphan-clean` 是**宿主会话 id**
48
+ * (前两个还没进回收站,第三个压根没有回收站行),`restore`/`purge` 是**回收站条目 id**。 */
49
+ export type ApplyAction = 'delete' | 'restore' | 'purge' | 'count' | 'orphan-clean';
46
50
  /** 每个动作的成功值:`count` 是一个数(拿不到就是 0,界面上那半句整句不出现);
47
51
  * `delete`/`restore` 是回收站那一行(`null` = 这一条已被**同批前一条**带走,幂等 no-op);
48
- * `purge` 是这一组的聚合回执(同批已带走 ⇒ `{removed:0,freedBytes:0}`)。 */
52
+ * `purge` 是这一组的聚合回执(同批已带走 ⇒ `{removed:0,freedBytes:0}`);
53
+ * `orphan-clean` 删的是**盘上目录**(不进回收站),所以清册那一行给不出 `null` 这种幂等 no-op——
54
+ * 组已经不在了就是 `not-found`,见 `verbs/batch.ts`。 */
49
55
  export type ApplyValueByAction = {
50
56
  delete: TrashEntryWire | null;
51
57
  restore: TrashEntryWire | null;
52
58
  purge: PurgeStatsWire;
53
59
  count: number;
60
+ 'orphan-clean': OrphanCleanStatsWire;
54
61
  };
55
62
  /** 一条动作的落地:原始输入 id 一律回带,成败各按信封(成功 `value` / 失败 `error`)。
56
63
  * 按 `action` + `ok` 双重可判别——浏览器半的便捷包装靠它把 `value` 收成各自动作的类型。 */
@@ -67,7 +74,8 @@ export type ApplyOutcome = {
67
74
  error: RemoteFailure;
68
75
  };
69
76
  }[ApplyAction];
70
- /** `TrashEntry` 的线上形状(浏览器半自己有一份窄镜像 `TrashEntryLike`)。 */
77
+ /** 清册那一行(`trash/entry.ts` 的 `TrashEntry`)的**线上形状**。
78
+ * 它与落地形状不是同一个东西:线上**只发**这里列的东西,落地形状另有运行期必填清单(见 `entry.ts`)。 */
71
79
  export interface TrashEntryWire {
72
80
  id: string;
73
81
  sessionId: string;
@@ -79,11 +87,81 @@ export interface TrashEntryWire {
79
87
  sizeBytes: number;
80
88
  parentEntryId?: string;
81
89
  }
90
+ /**
91
+ * `GET /list` 真正发出去的那一行:清册那一行 + 六个**现算现给**的只读数(ADR-0003)。
92
+ *
93
+ * 六个只读数都**不落清册**:它们随盘上状态随时变,清册只记文件里真有的事实。所以除了必填的
94
+ * `releaseBytes` 之外全是可选字段,而且「缺席」与「是 0」是两件事——界面上不许拿 0 冒充「没有」,
95
+ * 也不许拿缺席冒充否。
96
+ */
97
+ export interface TrashRowWire extends TrashEntryWire {
98
+ /** **这一行「彻底删除」会释放多少**:这一组在盘上**实测**的字节合计(回收站里本行与孩子的目录 +
99
+ * 盘上还留着的后代),与 purge 的 `freedBytes` 同一口径。算式只住 `verbs/permanent-removal.ts` 的
100
+ * `planVisibleTrash`,Node 半算一次过线、投影原样搬运;界面上头部合计与单条确认框共用这一个数——
101
+ * 不许各算一遍(各算一遍就是两套口径,且客户端那套会重复计数)。 */
102
+ releaseBytes: number;
103
+ /** 跟着这条父会话**一起进了回收站**的孩子数(清册里 `parentEntryId` 指向它的行)。 */
104
+ subagentCount?: number;
105
+ /** 那些孩子**回收站目录的实测字节**合计(`planVisibleTrash` 在盘上现量,与 `releaseBytes` 同一条口径;
106
+ * 不念清册里记的 `sizeBytes`。它们已折进 `releaseBytes`——头部合计照念那个数,别再单独相加)。 */
107
+ subagentBytes?: number;
108
+ /** **还留在盘上、没被带走**的孩子数(宿主 `sessionQuery`)。与 `subagentCount` 各说各的。 */
109
+ strandedSubagents?: number;
110
+ /** 那些盘上后代的实际字节合计;彻底删除/清空会一起释放。 */
111
+ strandedBytes?: number;
112
+ /** 删除时记下的工作区**已经不在账本里**(宿主的「删除工作区」只删注册、保留日志)。 */
113
+ workspaceMissing?: boolean;
114
+ }
115
+ /** 单条详情里的一条消息(`trash/detail.ts` 读出来的那一份)。 */
116
+ export interface TrashDetailMessageWire {
117
+ role: 'user' | 'assistant';
118
+ time: number;
119
+ text: string;
120
+ truncated: boolean;
121
+ }
122
+ /** 回收站单条详情的线上形状(`POST /detail`)。 */
123
+ export interface TrashDetailWire {
124
+ entryId: string;
125
+ /** 日志 header 里的原串(**无损**,比从 `projectKey` 反推那一路强;组头怎么念项目名见 `src/client/trash-view.ts` 的 `projectLabel`,那是另一回事)。 */
126
+ cwd: string;
127
+ createdAt: number;
128
+ lastActivity: number;
129
+ /** 日志内 `session/title` 那条记录;清册标题为空串时客户端拿它兜底。 */
130
+ logTitle: string;
131
+ models: string[];
132
+ turnCount: number;
133
+ userMessageCount: number;
134
+ assistantMessageCount: number;
135
+ toolCallCount: number;
136
+ injectedCount: number;
137
+ messages: TrashDetailMessageWire[];
138
+ omittedByPayloadCap: number;
139
+ }
82
140
  /** 清空回收站的回执(与旧 `purge` 同形)。 */
83
141
  export interface PurgeStatsWire {
84
142
  removed: number;
85
143
  freedBytes: number;
86
144
  }
145
+ /** 无主子会话扫描里**一个项目目录**的汇总(界面上那条"哪一家还剩多少"的读数)。 */
146
+ export interface OrphanProjectGroupWire {
147
+ projectDir: string;
148
+ sessions: number;
149
+ bytes: number;
150
+ }
151
+ /** 一次孤儿扫描的报告(纯读的产物)。`roots` 是每组那个"父没了"的 subagent 自己写的原形状;
152
+ * `missingParents` 按**去前缀后的形状**去重——同一个失踪父在两种写法下只算一个。 */
153
+ export interface OrphanReportWire {
154
+ roots: readonly string[];
155
+ totalSessions: number;
156
+ totalBytes: number;
157
+ missingParents: number;
158
+ groups: readonly OrphanProjectGroupWire[];
159
+ }
160
+ /** 清理一组无主子会话的回执(与 {@link PurgeStatsWire} 同形,但计的是"删掉的会话目录")。 */
161
+ export interface OrphanCleanStatsWire {
162
+ removed: number;
163
+ freedBytes: number;
164
+ }
87
165
  /** 请求体字段名:浏览器半发、Node 半读,同名同义(`empty` 没有入参)。 */
88
166
  export declare const PARAMS: {
89
167
  readonly entryId: "entryId";
@@ -91,7 +169,7 @@ export declare const PARAMS: {
91
169
  readonly ids: "ids";
92
170
  };
93
171
  /**
94
- * 失败信封的**线上形状**(设计稿 §6):`code` 是判别依据、`message` 是上屏原文、
172
+ * 失败信封的**线上形状**(本文件):`code` 是判别依据、`message` 是上屏原文、
95
173
  * `details` 是**可选的结构化附带物**——对齐官方 `rpcErrorSchema` 那三件
96
174
  * (`dsh-client-connection` 原文:`z.object({ code, message, details: z.record(z.string(), z.unknown()) })`)。
97
175
  * 来历两类:宿主类型化错误自己身上的字段(`WorkspaceActiveSessionError` 的 `sessionId` / `activity`…),
@@ -120,6 +198,24 @@ export type RemoteResult<T> = {
120
198
  * 浏览器半是 `src/client/remote.ts` 的 `throwRemoteFailure`。
121
199
  */
122
200
  export declare function isDSHRemoteError(value: unknown): value is RemoteFailure;
201
+ /** 失败码的域。与宿主官方域(`workspace/*`、`session/*`…)不重名,见本文件。 */
202
+ export declare const SESSION_DELETE_ERROR_DOMAIN = "sessiondelete";
203
+ /** 五个**动词码**(本文件那张表)。`io` 是唯一兜底。 */
204
+ export declare const VERB_CODES: readonly ["live", "not-found", "trash-empty", "project-missing", "io"];
205
+ export type VerbCode = (typeof VERB_CODES)[number];
206
+ /** 三个**路由自检码**:请求本身不对,不是一次操作的结果(它们带 HTTP 状态,见 `api/dispatch.ts`)。 */
207
+ export declare const ROUTE_CODES: readonly ["bad-method", "not-found-route", "bad-request"];
208
+ export type RouteCode = (typeof ROUTE_CODES)[number];
209
+ /**
210
+ * 完整码(`sessiondelete/<后缀>`)。**`Record<…, …>` 是闸**:加一个后缀而忘了这里,
211
+ * 编译不过;写错一个字面量,也编译不过。
212
+ */
213
+ export type AllCodeSuffix = VerbCode | RouteCode;
214
+ export declare const SESSION_DELETE_CODES: Record<AllCodeSuffix, `sessiondelete/${AllCodeSuffix}`>;
215
+ /** 回收站目录名(相对宿主数据根)。Node 半拼路径,浏览器半在页脚念它——同一个字面量。 */
216
+ export declare const TRASH_DIR_NAME = "session-trash";
217
+ /** 页脚/文案里带尾斜杠的那一版(`splitOn` 按它切一刀再把目录名包成 code)。 */
218
+ export declare const TRASH_DIR_LABEL = "session-trash/";
123
219
  /**
124
220
  * 宿主 `projectKey` 的 **`~XXXX` 转义还原**。
125
221
  *
@@ -1,34 +1,16 @@
1
1
  import type { ManifestPort } from './manifest.js';
2
2
  import type { FsPort } from '../ports/fs-port.js';
3
+ import type { TrashDetailMessageWire, TrashDetailWire } from '../shared/wire.js';
3
4
  /** 每条文字的上限(字符)。超出即截断并置 `truncated`。 */
4
5
  export declare const PER_MESSAGE_CHAR_CAP = 3000;
5
6
  /** 一次回信的正文总量上限(字符)。 */
6
7
  export declare const TOTAL_CHAR_CAP = 500000;
7
- export interface TrashDetailMessage {
8
- role: 'user' | 'assistant';
9
- time: number;
10
- text: string;
11
- truncated: boolean;
12
- }
13
- export interface TrashDetail {
14
- entryId: string;
15
- /** 日志 header 里的原串(**无损**,比从 `projectKey` 反推那一路强,设计稿 §7 组头那条是另一回事)。 */
16
- cwd: string;
17
- createdAt: number;
18
- lastActivity: number;
19
- /** 日志内 `session/title` 那条记录;清册标题为空串时客户端拿它兜底。 */
20
- logTitle: string;
21
- models: string[];
22
- turnCount: number;
23
- userMessageCount: number;
24
- assistantMessageCount: number;
25
- toolCallCount: number;
26
- injectedCount: number;
27
- messages: TrashDetailMessage[];
28
- omittedByPayloadCap: number;
29
- }
8
+ /** 详情正文里的一条消息 / 整份详情:**线上形状的家在 `shared/wire.ts`**(ADR-0003)——
9
+ * Node 半产出它,浏览器半按同一份类型消费,这里只留旧名做别名。 */
10
+ export type TrashDetailMessage = TrashDetailMessageWire;
11
+ export type TrashDetail = TrashDetailWire;
30
12
  export type DetailDeps = {
31
- fs: FsPort;
13
+ fs: Pick<FsPort, 'exists' | 'listFiles' | 'readFile'>;
32
14
  manifest: ManifestPort;
33
15
  home: string;
34
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,12 +8,12 @@ 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
- /** 删除时**尽力**取的会话标题(来源:`storages/session_projcache/sessions/<会话id>.json`
16
- * 里的 `rows.title.val`);取不到存空串,绝不因此让删除失败。 */
15
+ /** 删除时**尽力**取的会话标题(来源:宿主 projection cache 服务;底层是
16
+ * `storages/session_projcache/sessions/<会话id>.json` 里的 `title` 投影)。取不到存空串,绝不因此让删除失败。 */
17
17
  title: string;
18
18
  /** 删除时用 `fs.sizeOfDir` 量到的会话目录字节数。 */
19
19
  sizeBytes: number;
@@ -21,9 +21,28 @@ 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
+ /**
28
+ * 清册里**每条记录都必须有**的那几格(`TrashEntry` 的非可选字段)——运行期校验器与「文件里真有的事实」
29
+ * 共用这一份清单。
30
+ *
31
+ * **可选字段不在这里**:`parentEntryId`(级联那批加的)缺席是合法的——老行没有它,缺了就是"没有父";
32
+ * 校验器只管这几格的类型,其余键**原样带走**(`parse` 交回的就是 `raw` 本身)⇒ 新字段照旧存得住。
33
+ *
34
+ * 键集与接口由 `tests/wire-shape.test.ts` 双向钉住:接口加一个必填字段而这里没跟 ⇒ 测试红。
35
+ */
36
+ export declare const REQUIRED_ENTRY_FIELDS: {
37
+ readonly id: "string";
38
+ readonly sessionId: "string";
39
+ readonly projectDir: "string";
40
+ readonly workspaceId: "string";
41
+ readonly wasArchived: "boolean";
42
+ readonly deletedAt: "number";
43
+ readonly title: "string";
44
+ readonly sizeBytes: "number";
45
+ };
27
46
  export declare function makeEntryId(deletedAt: number, sessionDirName: string): string;
28
47
  /** `makeEntryId` 的逆。时间戳按 UTC 拆字段再 `Date.UTC` 合回——不交给 `Date.parse`
29
48
  * 去猜(`20260930T130500Z` 不是任何被承认的日期格式,猜出来的是 NaN)。 */
@@ -31,5 +50,9 @@ export declare function parseEntryId(id: string): {
31
50
  deletedAt: number;
32
51
  sessionDirName: string;
33
52
  };
53
+ /** 路径里的每一段都必须是**单段**:这一层不信宿主的 `header.id` 与清册,破坏性动词直接删目录。
54
+ * `/`、`\`、`.`、`..` 与 NUL 都能把递归删除带出 `sessions/` / `session-trash/`。 */
55
+ export declare function isSinglePathSegment(value: string): boolean;
56
+ /** `sessionDirName` 允许空串:恢复的预检用它取**项目根**(`sessions/<projectDir>`),不是会话目录。 */
34
57
  export declare function sessionDirOf(projectDir: string, sessionDirName: string, home: string): string;
35
58
  export declare function trashDirOf(entryId: string, home: string): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * **组语义的唯一家**(ADR-0001):回收站里「这一组有谁」以及「哪些行在清单里露面」。
3
+ *
4
+ * 此前这条关系在四个地方各算一遍,而且已经算出两种答案:
5
+ * `assembleTrashRows` 与 `restoreSession` 都认为「父自己也是孩子时,它接不住孩子」,
6
+ * 而 `purgeTrash` 的 `id === x || parentEntryId === x` 不认这条 ⇒ 嵌套组上它会连带删掉不属于这一组的孙代
7
+ * (不可恢复)。判据原文只在这里写一次:
8
+ *
9
+ * **接得住孩子的只有组根**——自己不记 `parentEntryId` 的那一行。父行不在、或父行自己也是孩子时,
10
+ * 孩子必须自己占一行,否则它的字节会被头部合计数两遍(而它自己既看不见也清不掉)。
11
+ *
12
+ * 三个函数都是**纯函数**、都吃一份快照:`/apply` 那条路会在同一批里多次重问、而快照会被就地收缩,
13
+ * 收缩的时机归调用方——groups module 不持有可变状态,也就没有"这份快照谁负责更新"的第二套契约。
14
+ */
15
+ import type { TrashEntry } from './entry.js';
16
+ /** 一条露面的组根,连同它接住的孩子(顺序与清册一致)。 */
17
+ export interface TrashGroupView {
18
+ readonly root: TrashEntry;
19
+ /** 跟着这条根进来的孩子(`parentEntryId` 指向根的 id)。根自己不在里面。 */
20
+ readonly kids: readonly TrashEntry[];
21
+ }
22
+ /**
23
+ * 清单露面的一行 = 组根,按清册原序。孩子折进根那一行(宿主的客户端本来就把 `origin:"subagent"`
24
+ * 的行整排藏了 ⇒ 我们若单列就会冒出一堆点不到的行)。
25
+ *
26
+ * 折不进去的那两种孩子(父行不在 / 父行自己也是孩子)**自己站出来占一行**——那正是
27
+ * `visibleGroups` 与 `groupOf` 共用同一条判据的地方。
28
+ */
29
+ export declare function visibleGroups(rows: readonly TrashEntry[]): TrashGroupView[];
30
+ /**
31
+ * 一条 id 所属的整组。三种形状,三个答案:
32
+ * - **组根** ⇒ 它自己 + 它接住的孩子(UI 点的是父那一行,删除/恢复/彻底删除都按整组走);
33
+ * - **孩子**(自己记着 `parentEntryId`)⇒ **只回它自己**。清单不单独列它,只有手工/降级路径会点到这里;
34
+ * - **清册里没有这一条 id,但还有孩子指向它**(父行已被别的路径摘掉)⇒ 那一批孩子;仍然没有 ⇒ 空数组,
35
+ * 调用方据此报 `trash-empty` / 判"已被同批前一条带走"。
36
+ */
37
+ export declare function groupOf(rows: readonly TrashEntry[], id: string): TrashEntry[];
38
+ /**
39
+ * 「这条是被**同批前一条**带走的,还是整批开始前就没有?」
40
+ *
41
+ * 幂等语义只属于前者:后来者本来就不在清册里(用户点了条幽灵 id、或对账刚摘掉它)必须照样响亮报
42
+ * `trash-empty`,不能被"已被带走"这句话掩盖成成功。**所以判据是快照的原始形状,不是当前形状**:
43
+ * 原始快照里认得出(它自己、它的父、或它的孩子任一在),而当前快照里已经空了 ⇒ 同批带走的。
44
+ */
45
+ export declare function takenByEarlierInBatch(original: readonly TrashEntry[], current: readonly TrashEntry[], id: string): boolean;
@@ -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"` 的孩子
@@ -17,14 +13,12 @@ export interface DeleteDeps {
17
13
  * 三段,顺序承重:
18
14
  * ① **先点组、先验活体**(一个字节都不写)。任一成员在跑就整组拒绝——不替用户终止正在跑的活,
19
15
  * 也绝不允许"父已经搬进回收站、孩子因为在跑而搬不动"这种半套。
20
- * ② **逐个搬**:每个成员内部沿用原来那套顺序与就地回滚(归档 → 摘账本 → 搬目录 → 写清册),
21
- * 任何一步失败就把这个成员自己退回原地。
16
+ * ② **逐个搬**:每个成员是一份 `journal`(`verbs/member-journal.ts`),搬不动时 journal 自己把
17
+ * 这个成员已经做成的步骤退回去。
22
18
  * ③ **组级回滚**:某一个成员搬不动时,把**已经搬进去的**那些按相反顺序退回原处(清册行跟着摘、
23
- * 账本跟着挂、可见性跟着还原),失败照抛——不留"半组在回收站、半组在原地"的世界。
19
+ * 账本跟着挂),失败照抛——不留"半组在回收站、半组在原地"的世界。
20
+ *
21
+ * interface 只有**一个 id**:批量的性质不在这里(同批"已被前一条带走"的判据归 `verbs/batch.ts`)。
22
+ * 结果带回这次真的搬走的整组(`movedSessionIds`)——它是动词交回的内部数据,不再靠回调借用动词的手。
24
23
  */
25
- export declare function deleteSession(deps: DeleteDeps): (sessionId: string,
26
- /** 批量层(`verbs/apply.ts`)在写之前收一份"这次真正要搬的成员"(父 + 盘上存在的孩子),
27
- * 成功后用它记下"已被同批前一条带走"的 id。单条调用不传。 */
28
- options?: {
29
- readonly onMembers?: (sessionIds: readonly string[]) => void;
30
- }) => Promise<TrashEntry>;
24
+ export declare function deleteSession(deps: DeleteDeps): (sessionId: string) => Promise<DeleteResult>;