@xrn1997/dsh-session-delete 0.3.0 → 0.4.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 (74) hide show
  1. package/README.md +21 -15
  2. package/lib/client.js +3317 -2173
  3. package/lib/index.js +1691 -964
  4. package/lib/types/api/attachment-orphans.d.ts +11 -0
  5. package/lib/types/api/attachment.d.ts +23 -0
  6. package/lib/types/api/dispatch.d.ts +9 -29
  7. package/lib/types/api/request-trust.d.ts +45 -0
  8. package/lib/types/api/route-table.d.ts +28 -14
  9. package/lib/types/client/attachment-view.d.ts +27 -0
  10. package/lib/types/client/batch-bar.d.ts +3 -2
  11. package/lib/types/client/batch-confirm.d.ts +3 -3
  12. package/lib/types/client/batch-menu-item.d.ts +1 -1
  13. package/lib/types/client/batch-select.d.ts +1 -1
  14. package/lib/types/client/busy-gate.d.ts +6 -0
  15. package/lib/types/client/confirm-close.d.ts +11 -0
  16. package/lib/types/client/copy.d.ts +48 -38
  17. package/lib/types/client/delete-confirm.d.ts +0 -2
  18. package/lib/types/client/host-rows.d.ts +18 -3
  19. package/lib/types/client/main-view.d.ts +8 -3
  20. package/lib/types/client/maintenance-parts.d.ts +25 -0
  21. package/lib/types/client/maintenance-settings.d.ts +22 -0
  22. package/lib/types/client/markdown.d.ts +9 -1
  23. package/lib/types/client/orphan-maintenance.d.ts +45 -0
  24. package/lib/types/client/receipt.d.ts +75 -5
  25. package/lib/types/client/remote.d.ts +49 -69
  26. package/lib/types/client/store.d.ts +31 -0
  27. package/lib/types/client/trash-confirm.d.ts +2 -15
  28. package/lib/types/client/trash-nav.d.ts +30 -0
  29. package/lib/types/client/trash-panel.d.ts +4 -3
  30. package/lib/types/client/ui/Checkbox.d.ts +1 -1
  31. package/lib/types/client/ui/Modal.d.ts +2 -2
  32. package/lib/types/client/ui/icons.d.ts +6 -11
  33. package/lib/types/client/ui/top-layer.d.ts +10 -0
  34. package/lib/types/client/undo-notices.d.ts +52 -0
  35. package/lib/types/client/undo-toast.d.ts +6 -52
  36. package/lib/types/client/use-column-resize.d.ts +9 -20
  37. package/lib/types/client/use-maintenance.d.ts +45 -0
  38. package/lib/types/client/use-panel-width.d.ts +1 -1
  39. package/lib/types/client/use-trash-panel-controller.d.ts +27 -23
  40. package/lib/types/client/use-trash-query.d.ts +39 -25
  41. package/lib/types/client/write-seats.d.ts +44 -0
  42. package/lib/types/client/write-settlement.d.ts +66 -0
  43. package/lib/types/client/write.d.ts +54 -0
  44. package/lib/types/deps.d.ts +20 -9
  45. package/lib/types/index.d.ts +8 -3
  46. package/lib/types/log/session-log.d.ts +4 -3
  47. package/lib/types/log/zstd-frames.d.ts +14 -8
  48. package/lib/types/ports/agent-handles.d.ts +1 -1
  49. package/lib/types/ports/connection-port.d.ts +33 -0
  50. package/lib/types/ports/fs-port.d.ts +13 -1
  51. package/lib/types/ports/host-port.d.ts +21 -63
  52. package/lib/types/ports/presence-port.d.ts +22 -0
  53. package/lib/types/shared/wire.d.ts +77 -26
  54. package/lib/types/trash/attachments.d.ts +85 -0
  55. package/lib/types/trash/detail.d.ts +29 -6
  56. package/lib/types/trash/entry.d.ts +3 -3
  57. package/lib/types/trash/groups.d.ts +18 -14
  58. package/lib/types/trash/ledger.d.ts +20 -0
  59. package/lib/types/trash/manifest.d.ts +2 -1
  60. package/lib/types/trash/reconcile.d.ts +4 -3
  61. package/lib/types/trash/storage-manifest.d.ts +1 -1
  62. package/lib/types/verbs/batch-carried.d.ts +20 -0
  63. package/lib/types/verbs/delete-session.d.ts +6 -5
  64. package/lib/types/verbs/errors.d.ts +4 -10
  65. package/lib/types/verbs/live-admission.d.ts +10 -0
  66. package/lib/types/verbs/member-journal.d.ts +25 -7
  67. package/lib/types/verbs/member-presence.d.ts +33 -24
  68. package/lib/types/verbs/move-group.d.ts +20 -0
  69. package/lib/types/verbs/orphan-subagents.d.ts +11 -5
  70. package/lib/types/verbs/permanent-removal.d.ts +31 -24
  71. package/lib/types/verbs/purge-trash.d.ts +21 -8
  72. package/lib/types/verbs/restore-session.d.ts +12 -16
  73. package/package.json +4 -2
  74. package/lib/types/client/write-commit.d.ts +0 -10
@@ -5,9 +5,10 @@
5
5
  * `~/.dsh/storages/` 那份清单从此是**内部索引**,不用手碰。
6
6
  *
7
7
  * 五条口径(都不是口味):
8
- * ① **每次读清单都对账**(`GET /list` 的正文就是"对账 + 读"):挂载、刷新、别处动过后的自动重读、
9
- * 客户端启动探测全走这条 ⇒ "改完目录不用再做任何事,下一次读就对了"。代价 = 每次读多一次目录枚举
10
- * (本机 124 条,毫秒级);**零漂移零写入**(快路直接返回原清单)。
8
+ * ① **每次真相读都对账**(`src/trash/ledger.ts` 的 `list()`;`GET /list` 的正文就是"对账 + 读"):挂载、
9
+ * 刷新、别处动过后的自动重读、客户端挂载后的首次列表读全走这条 ⇒ "改完目录不用再做任何事,下一次读就对了"。
10
+ * pure 读面(`/orphans`、`/detail`)走 `raw()`,不对账——对账会写行,必须排在到达序链上。
11
+ * 代价 = 每次真相读多一次目录枚举(本机 124 条,毫秒级);**零漂移零写入**(快路直接返回原清单)。
11
12
  * ② **回收站根不在 ⇒ 整套跳过、一个字节都不动**:那不是"回收站空了",是"这块地不在原处"
12
13
  * (人为搬走、盘没挂上)——照常对账会把全部行的元数据一次判没。
13
14
  * ③ **重建行只写"文件里真有的事实"**:`id` = 目录名、`sessionId`/`cwd` 读日志 header、
@@ -4,7 +4,7 @@ import { type ManifestPort } from './manifest.js';
4
4
  * 清单的真实绑定:官方 storage domain → `ManifestPort`(`StorageDomainLike` 在动词层保持一种读法)。
5
5
  * 域**懒开**且成功句柄只开一次(`apply` 是同步的,而 `open()` 是异步的),并挂一条 `ctx.effect` 清理:
6
6
  * 插件卸载时关掉句柄并把 `close()` 的落地交回框架(句柄归调用方所有,这是 `open()` 的 JSDoc 明写的契约;
7
- * 2026-10-17 起不再 `void` 走掉——没关严的句柄会让下一份同名域撞 `already-open`)。
7
+ * 不能 `void` 走掉——没关严的句柄会让下一份同名域撞 `already-open`)。
8
8
  *
9
9
  * **失败不缓存**:`open()` 被拒是瞬态(域被别的持有者占着、装载竞态),把第一次的 rejected promise
10
10
  * 缓存起来等于"这个会话永远删不掉"——下一次读会重新走 `open()`,成功才落下活句柄。
@@ -0,0 +1,20 @@
1
+ import type { TrashEntry } from '../trash/entry.js';
2
+ import { type GroupClaim } from '../trash/groups.js';
3
+ export interface EntryBatchCarried {
4
+ claim(id: string): GroupClaim;
5
+ take(id: string): void;
6
+ /** 收缩中的当前视图(`take` 过的那一组已经不在里面)。 */
7
+ rows(): readonly TrashEntry[];
8
+ }
9
+ /** session 域没有组可交:claim 只回答 fresh / carried。 */
10
+ export type SessionClaim = {
11
+ readonly verdict: 'fresh' | 'carried';
12
+ };
13
+ export interface SessionBatchCarried {
14
+ claim(id: string): SessionClaim;
15
+ take(id: string): void;
16
+ }
17
+ /** 恢复 / 彻底删除:原始快照认得、当前视图已经空了 ⇒ 同批前一条带走的。 */
18
+ export declare function createEntryCarried(original: readonly TrashEntry[]): EntryBatchCarried;
19
+ /** 删除:会话 id 域;`take` 只收这次真的搬走的成员(父 + 盘上的孩子)。 */
20
+ export declare function createSessionCarried(): SessionBatchCarried;
@@ -1,8 +1,8 @@
1
1
  import { type TrashEntry } from '../trash/entry.js';
2
- import type { DeleteDeps } from '../deps.js';
2
+ import type { DeleteSessionDeps } from '../deps.js';
3
3
  export interface DeleteResult {
4
4
  readonly entry: TrashEntry;
5
- /** 这一次真的搬进回收站的成员(父 + 盘上存在的孩子);批量层用它建幂等账本。 */
5
+ /** 这一次真的搬进回收站的成员(父 + 盘上存在的孩子);批量层用它建同批已带走集。 */
6
6
  readonly movedSessionIds: readonly string[];
7
7
  }
8
8
  /**
@@ -10,7 +10,7 @@ export interface DeleteResult {
10
10
  * 整排藏了 ⇒ 那些目录既看不见也继续占着字节(本机实测 41 条父留了 320 个目录 / 74.6 MB)。
11
11
  * 所以这里把父在盘上的孩子一起搬进来,各自成目录、各自一行,清册用 `parentEntryId` 记它是谁的孩子。
12
12
  *
13
- * 三段,顺序承重:
13
+ * 三段,顺序承重(成员循环与整组回滚的实现住 `verbs/move-group.ts`):
14
14
  * ① **先点组、先验活体**(一个字节都不写)。任一成员在跑就整组拒绝——不替用户终止正在跑的活,
15
15
  * 也绝不允许"父已经搬进回收站、孩子因为在跑而搬不动"这种半套。
16
16
  * ② **逐个搬**:每个成员是一份 `journal`(`verbs/member-journal.ts`),搬不动时 journal 自己把
@@ -18,7 +18,8 @@ export interface DeleteResult {
18
18
  * ③ **组级回滚**:某一个成员搬不动时,把**已经搬进去的**那些按相反顺序退回原处(清册行跟着摘、
19
19
  * 账本跟着挂),失败照抛——不留"半组在回收站、半组在原地"的世界。
20
20
  *
21
- * interface 只有**一个 id**:批量的性质不在这里(同批"已被前一条带走"的判据归 `verbs/batch.ts`)。
21
+ * interface 只有**一个 id**:批量的性质不在这里(同批已带走集归 `verbs/batch-carried.ts`,
22
+ * 组语义归 `trash/groups.ts`)。
22
23
  * 结果带回这次真的搬走的整组(`movedSessionIds`)——它是动词交回的内部数据,不再靠回调借用动词的手。
23
24
  */
24
- export declare function deleteSession(deps: DeleteDeps): (sessionId: string) => Promise<DeleteResult>;
25
+ export declare function deleteSession(deps: DeleteSessionDeps): (sessionId: string) => Promise<DeleteResult>;
@@ -1,14 +1,6 @@
1
- /** 五个码 / 三个路由自检码的**家都在 `shared/wire.ts`**(ADR-0003):浏览器半也要认它们(比如
2
- * `not-found` 在界面上是中性提示而不是失败),所以它们属于两半共同的契约,不属于 Node 半。
3
- * 这里只转出旧名,免得调用点全改一遍。 */
1
+ /** 失败的**结构与措辞**。码本身、码的前缀读法与动词词表都住在 `shared/wire.ts`(ADR-0003):
2
+ * 浏览器半也要认它们,它们属于两半共同的契约,不属于 Node 半。这里只做动词抛错与回滚措辞。 */
4
3
  import { type RouteCode, type VerbCode } from '../shared/wire.js';
5
- export declare const VERB_CODES: readonly ["live", "not-found", "trash-empty", "project-missing", "io"];
6
- export declare const ROUTE_CODES: readonly ["bad-method", "not-found-route", "bad-request"];
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;
12
4
  /** 一次失败的**结构**:码、上屏原文、可选 cause(`map-error` 沿 cause 链认宿主类型化错误)、
13
5
  * 可选的结构化附带物(对齐官方 `rpcErrorSchema` 的第三件 `details`)。 */
14
6
  export interface VerbError extends Error {
@@ -17,6 +9,8 @@ export interface VerbError extends Error {
17
9
  cause?: unknown;
18
10
  }
19
11
  export declare function verbError(code: VerbCode, message: string, cause?: unknown, details?: Record<string, unknown>): VerbError;
12
+ /** `trash-empty` 的原文:有具体条目就点名那个 id,拿不到时只报通用那句。 */
13
+ export declare function trashEmpty(entryId?: string): VerbError;
20
14
  /** 路由自检用的失败:带 HTTP 状态语义,**不**冒充五个动词码。 */
21
15
  export declare function routeError(code: RouteCode, httpStatus: number, message: string): VerbError & {
22
16
  httpStatus: number;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 活体准入:删除 / 永久删除 / 孤儿清理之前,逐个问会话是否在宿主内存里 attach 着;
3
+ * 任一在跑就整组拒绝、零副作用(ADR-0008)。
4
+ *
5
+ * 边界:什么时候查归调用方——永久删除的准入在计划里、任何写之前(ADR-0006);
6
+ * 本模块只拥有「查什么、怎么判、怎么说」。
7
+ */
8
+ import type { HostIdentityPort } from '../ports/host-port.js';
9
+ /** 逐个问,任一在跑就抛 `live`。名单的挑选(哪些成员算这一组)归调用方,这里只按传入顺序问完。 */
10
+ export declare function assertNoLive(host: Pick<HostIdentityPort, 'isLive'>, sessionIds: readonly string[]): Promise<void>;
@@ -1,10 +1,27 @@
1
+ /** 一步反向动作的结果:这一 step 的领域键(`key`)是否回到原样 + 没能退干净的原因。
2
+ * `restored` 只回答键所指的那件写;同一步里的附属写失败仍进 `failures`
3
+ * (例如删除的目录回来了、清册行没摘掉:客户端仍要收到 added,失败只算回滚不完整)。 */
4
+ export interface StepUndo {
5
+ readonly restored: boolean;
6
+ readonly failures: readonly string[];
7
+ }
8
+ /** 一步的领域键(`read` / `workspace` / `dir` / `archive` / `manifest`)。`restored(key)` 用它点名
9
+ * 要核的那件写;收成联合,动词手滑写错键在编译期就报。 */
10
+ export type JournalStepKey = 'read' | 'workspace' | 'dir' | 'archive' | 'manifest';
11
+ /** 一次 undo 的汇总:失败串仍按原样合并;`restored(key)` 给动词判「成员归位 / 已移除」用。 */
12
+ export interface JournalUndo {
13
+ readonly failures: readonly string[];
14
+ restored(key: JournalStepKey): boolean;
15
+ }
1
16
  export interface JournalStep {
2
- /** 这一步在做什么。失败原文按「`<label>:<原错误>`」拼——与迁移前那 22 处 `rollbackReason` 的措辞同形。 */
17
+ /** 稳定的领域键,给 `restored(key)` 用。 */
18
+ readonly key: JournalStepKey;
19
+ /** 这一步在做什么。失败原文按「`<label>:<原错误>`」拼。 */
3
20
  readonly label: string;
4
21
  /** 做这一步。抛什么都由 journal 折成 `sessiondelete/io`(除非它自己已经是一个动词码)。 */
5
22
  run(): Promise<void>;
6
- /** 反向动作。**`run` 成功之后**才会被登记;返回**这一步自己**没能退干净的原因列表(空 = 完全退回)。 */
7
- undo?(): Promise<readonly string[]>;
23
+ /** 反向动作。**`run` 成功之后**才会被登记;返回这一步退没退成与原因。 */
24
+ undo?(): Promise<StepUndo>;
8
25
  /** 这一步**自己**失败时也跑 `undo`:给"可能已经半做完"的非原子步骤用(取消归档是唯一的例子)。 */
9
26
  readonly undoOnFailure?: boolean;
10
27
  }
@@ -12,16 +29,17 @@ export interface Journal {
12
29
  /** 按顺序跑完。中途失败 ⇒ 已做成的按相反顺序退回,然后把原错误(带 `rollbackFailures`)抛出去。 */
13
30
  run(): Promise<void>;
14
31
  /** 把**已经做成**的步骤按相反顺序退回去(整组回滚用:组里后面那个成员搬不动时叫这一句)。 */
15
- undo(): Promise<readonly string[]>;
32
+ undo(): Promise<JournalUndo>;
16
33
  }
17
34
  /** 已经把某个前缀做成了的 journal——`undo()` 就退那一个前缀。 */
18
35
  export declare function createJournal(steps: readonly JournalStep[]): Journal;
19
36
  /** 跑一步反向动作并把它自己的失败收成一条原因(不抛:这些失败由调用方汇总成 `rollbackFailures`)。 */
20
37
  export declare function attempt(label: string, run: () => Promise<unknown>): Promise<readonly string[]>;
38
+ /** 一步反向动作的完整结果:主写是否回到原样 + 失败串。附属写失败只进 `failures`,不推翻 `restored`。 */
39
+ export declare function stepUndo(label: string, run: () => Promise<unknown>): Promise<StepUndo>;
21
40
  /**
22
41
  * **组级回滚**:把已经做成的成员按相反顺序退回;某一步退不干净时把失败并进原错误的
23
- * `details.rollbackFailures`,绝不让原错误独走(2026-10-19 `f2a7757`;那些失败句子的合并与措辞住
24
- * `src/verbs/errors.ts`,这里只是唯一的调用方)。删除与恢复两个按组走的动词共用这一处,
25
- * "相反顺序 + 失败合并"不再各写一遍。
42
+ * `details.rollbackFailures`,绝不让原错误独走。那些失败句子的合并与措辞住 `src/verbs/errors.ts`;
43
+ * 删除与恢复两个按组走的动词共用这一处。
26
44
  */
27
45
  export declare function rollbackJournals(journals: readonly Journal[], error: unknown): Promise<never>;
@@ -1,35 +1,44 @@
1
1
  /**
2
2
  * 成员离场 / 归位的通告协议:摘活体、三态判断、成对事件只在这里做。
3
3
  *
4
- * 为什么要有它:delete(成功与回滚)/ restore 回滚 / purge(两个分支)/ orphan-clean
5
- * 六处调用曾经各写一遍
6
- * `evictLive → !== 'unknown' → announceRemoved`;删除回滚还要自己拼
7
- * `movedBack && workspaceBack` 才发 `announceRestored`。
8
- * 这条协议说错一次就是客户端留着假行、或删掉真行,所以只留一个家。
4
+ * 调用方包括 delete、restore 回滚、purge、orphan-clean;协议说错一次就是客户端留假行或删真行。
9
5
  *
10
6
  * 边界:
11
- * - 批量恢复的 `announceRestoredBatch` 不进来:一次预取、一批一次通告归 `verbs/batch.ts`(ADR-0002)。
12
- * - 不读 journal 的失败串:调用方交事实(目录 / 账本是否回去),真值表住这里。
13
- * - 本模块用到的三条(`evictLive` / `announceRemoved` / `announceRestored`)按契约不抛(evictLive 三态、
14
- * announce* 内部吞异常),这里不再叠 catch。
7
+ * - 批量恢复的编排归 `verbs/batch.ts`(ADR-0002);本模块只实现 `announceRestored(ids)`(ADR-0019)。
8
+ * - 哪个键算归位由动词在 `tail.onUndone` 里读成事实再交进来,真值表住这里;
9
+ * `run`/`undo` 之后补通告的形状由 {@link withPresenceTail} 拥有。
10
+ * - `evictLive`/`announce*` 按契约不抛,这里不再叠 catch。
15
11
  */
16
12
  import type { HostPresencePort } from '../ports/host-port.js';
17
- /** 本模块实际用到的三条宿主能力。取窄面而不是整个 `HostPresencePort`:那条协议里还有批量通告与
18
- * 摘要预取,它们各有自己的家(`verbs/batch.ts`),这里不该因为共用同一颗 adapter 就声明依赖。 */
13
+ import type { PresenceCapability } from '../ports/presence-port.js';
14
+ import type { Journal, JournalUndo } from './member-journal.js';
15
+ /** 本模块实际用到的三条宿主能力。`Pick` 此刻覆盖 `HostPresencePort` 的三条方法,仍留着它当闸:
16
+ * 将来端口多出第四条时,这里不会被动跟着变宽——能力面只暴露这三条,批量通告的编排归 `verbs/batch.ts`。 */
19
17
  export type MemberPresenceHost = Pick<HostPresencePort, 'evictLive' | 'announceRemoved' | 'announceRestored'>;
20
- export interface MemberBackFacts {
21
- /** 目录搬回原处了吗。 */
18
+ /** 本仓的在场能力实现:端口形状住 `src/ports/presence-port.ts`,这里只做三态与真值表。
19
+ * `announceRestored` 直接交宿主那一手:能力面与宿主面同名同签名,再包一层只会多一个转调。 */
20
+ export declare function createMemberPresence(host: MemberPresenceHost): PresenceCapability;
21
+ /** 一次 undo 之后要发哪一条通告:`leave` = 成员已不在可见面,`return` = 已回到原可见状态。 */
22
+ export type PresenceAnnounce = {
23
+ readonly kind: 'leave';
24
+ readonly backInTrash: boolean;
25
+ } | {
26
+ readonly kind: 'return';
22
27
  readonly diskBack: boolean;
23
- /** 账本挂回去了吗(本来没有账目时算 true)。 */
24
28
  readonly workspaceBack: boolean;
29
+ };
30
+ /** 动词给工厂的那一格:哪个成员、run 成不成功要不要补离场、undo 之后按逐步结果发哪一条。 */
31
+ export interface PresenceTail {
32
+ readonly sessionId: string;
33
+ /** `run()` 成功后补离场通告(删除用)。省略 = run 不动在场(恢复的 run 本来就不该发东西)。 */
34
+ readonly onDone?: 'leave';
35
+ /** `undo()` 完成后按逐步结果发通告。 */
36
+ onUndone(undo: JournalUndo): PresenceAnnounce;
25
37
  }
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;
38
+ /**
39
+ * 给一次成员移动的 journal 补上「在场那条尾巴」:run 完全成功之后离场,undo 之后按事实归位或离场。
40
+ *
41
+ * 边界:`memberGone` 的 `backInTrash:false` 早退与 `memberBack` 的两件齐备真值表归 `createMemberPresence`;
42
+ * 这里只把事实原样递过去。
43
+ */
44
+ export declare function withPresenceTail(presence: PresenceCapability, journal: Journal, tail: PresenceTail): Journal;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 组级成员移动:组根先跑(拿到 `undefined`),跟随者按序跑并拿到根结果;任一失败就把**已经做成**的
3
+ * 按相反顺序退回。
4
+ *
5
+ * 这里拥有顺序、整组回滚与**组根结果的交回**:根先跑,跟随者按序拿到根结果;调用方直接拿
6
+ * 返回的根结果,不按位置索引结果数组。跟随者自己的结果不向上交回——当前调用方不需要。
7
+ * 单成员的步骤与反向动作仍归 `member-journal.ts`;这条归属的裁决见 ADR-0002。
8
+ */
9
+ import { type Journal } from './member-journal.js';
10
+ export interface GroupMove<M, R> {
11
+ /** 组根:先跑,`move` 收到的 `rootResult` 是 `undefined`。 */
12
+ readonly root: M;
13
+ /** 跟随者:按序跑,每个都拿到根结果(彼此看不到对方的结果)。 */
14
+ readonly followers: readonly M[];
15
+ move(member: M, rootResult: R | undefined): Promise<{
16
+ journal: Journal;
17
+ result: R;
18
+ }>;
19
+ }
20
+ export declare function moveGroup<M, R>({ root, followers, move, }: GroupMove<M, R>): Promise<R>;
@@ -1,6 +1,7 @@
1
1
  import { type SessionIdentity } from '../ports/host-port.js';
2
+ import type { TrashEntry } from '../trash/entry.js';
2
3
  import type { OrphanCleanStatsWire, OrphanReportWire } from '../shared/wire.js';
3
- import type { CoreDeps } from '../deps.js';
4
+ import type { CoreDeps, RemovalDeps } from '../deps.js';
4
5
  /** 组里一个人的盘上身份(字节是**扫描那一刻**量的,清理时按目录重量一次,不拿这份快照冒充准数)。 */
5
6
  export interface OrphanMember {
6
7
  readonly sessionId: string;
@@ -31,13 +32,17 @@ interface RawGroup {
31
32
  * 归堆与去重**两边都去 `session-` 前缀**:同一个父在两代宿主里写法不同(见 `bareSessionId` 的来历)。
32
33
  */
33
34
  export declare function orphanGroupsOf(corpus: readonly SessionIdentity[], trashSessionIds: readonly string[]): RawGroup[];
34
- /** 纯读:调用方应先走一次 `/list` 对账(面板的 refresh 就是这样);本函数不写清单。
35
- * 写动作 `orphan-clean` 会自己先 reconcile,避免拿一份过期清册判"父没了"。
35
+ /** **本函数是纯扫描,不对账、不写清册行**(`/orphans` 声明为 pure,一次扫描不能顺手写行)。
36
+ * 清册行由调用方选好再交进来:`/orphans` 交 `read.rows()`(raw 索引),`orphan-clean` 交
37
+ * `ledger.list()`(对账后的行)——否则刚被人手工搬进 `session-trash/` 的父目录还没有行,会被当成
38
+ * "父没了"直接删盘。扫描不再自己回落到 `ledger.raw()`:读面归属住路由记录(ADR-0004 / ADR-0012)。
36
39
  *
37
40
  * `listSessions()` 回 `{ available: false }` 时**必须抛**:那是宿主的会话语料服务没挂,不是"这个家没有孤儿"。
38
41
  * 拿缺席当空语料 ⇒ 报告说"零条孤儿"、界面上一句"没有要清理的",而盘上真有几百 MB——那是假话,
39
42
  * 所以这里按 `io` 响亮失败(AGENTS.md:`available: false` 意味着服务缺席,扫描方不许冒充读数)。 */
40
- export declare function loadOrphanGroups(deps: CoreDeps): Promise<readonly OrphanGroup[]>;
43
+ export declare function loadOrphanGroups(deps: CoreDeps,
44
+ /** 调用方选好并交来的清册行:`/orphans` 交 `read.rows()`,`orphan-clean` 交 `ledger.list()`。 */
45
+ trash: readonly TrashEntry[]): Promise<readonly OrphanGroup[]>;
41
46
  /** 把一组组收成那份报告:根清单 + 闭包总数/总字节 + 失踪父的**去重计数** + 按项目目录的分摊。
42
47
  * `groups` 按字节从多到少排(界面上"哪家还剩得最多"要看第一行)。 */
43
48
  export declare function reportOrphans(groups: readonly OrphanGroup[]): OrphanReportWire;
@@ -53,6 +58,7 @@ export declare function reportOrphans(groups: readonly OrphanGroup[]): OrphanRep
53
58
  * - `unarchive` 对未归档的会话是官方幂等 no-op(`registry.unarchiveSession`),所以这里不需要那次
54
59
  * `wasArchived` 读——这里没有"还原到删除前"的义务。
55
60
  * - 摘账本只在账本真记着它时做(`workspaceOf` 未命中 = 本来就是「未分组」,不是故障)。
61
+ * - 独占图片的剩余引用源由附件域自己扫:活动会话 + 全部 session-trash + storages/cache。
56
62
  */
57
- export declare function cleanOrphanGroup(deps: CoreDeps, group: OrphanGroup): Promise<OrphanCleanStatsWire>;
63
+ export declare function cleanOrphanGroup(deps: RemovalDeps, group: OrphanGroup): Promise<OrphanCleanStatsWire>;
58
64
  export {};
@@ -1,11 +1,12 @@
1
1
  /**
2
- * 永久删除的**测量、计划与执行**:一次永久删除(`/list` 的只读读数或 purge / orphan-clean 的删除计划)
3
- * 之前要记下的事实(目录、字节、账本归属),以及拿到计划之后按它动盘的那一步。计划函数**只读**——
4
- * 写不写由执行器决定,但活体准入在这里就做掉,调用方拿到计划才允许动盘。
2
+ * 永久删除的测量、计划与执行:先记录目录/字节/账本归属,再按计划动盘。
3
+ * 计划函数只读;活体准入在计划阶段、任何写之前完成。
5
4
  */
6
5
  import { type TrashEntry } from '../trash/entry.js';
7
6
  import type { TrashGroupView } from '../trash/groups.js';
8
- import type { CoreDeps } from '../deps.js';
7
+ import { type ImageRemovalPlan } from '../trash/attachments.js';
8
+ import type { CoreDeps, RemovalDeps } from '../deps.js';
9
+ import { type AttachmentCleanupWire } from '../shared/wire.js';
9
10
  export interface MeasuredMemberDir {
10
11
  readonly dir: string;
11
12
  readonly bytes: number;
@@ -24,13 +25,6 @@ export declare function measureMemberDir(deps: CoreDeps, member: {
24
25
  readonly sessionId: string;
25
26
  readonly projectDir: string;
26
27
  }): Promise<MeasuredMemberDir | undefined>;
27
- /** 把一组成员折成"删之前要记下的计划";目录不在的成员跳过(记录与盘不一致是那条成员自己的事)。
28
- * `purge` 的盘上后代、`orphan-clean` 的整组与 `/list` 的盘上残留都走这一处,别再各写一遍
29
- * measure + workspaceOf。 */
30
- export declare function planMemberRemovals(deps: CoreDeps, members: readonly {
31
- readonly sessionId: string;
32
- readonly projectDir: string;
33
- }[]): Promise<PlannedMemberRemoval[]>;
34
28
  /** 回收站里的一条要删的行:目录、在不在、实测字节(缺席 = 0)。purge 与 `/list` 都由它量。 */
35
29
  export interface PlannedTrashRow {
36
30
  readonly entry: TrashEntry;
@@ -64,22 +58,28 @@ export interface RemovalPlan {
64
58
  readonly trash: readonly PlannedTrashRow[];
65
59
  readonly members: readonly PlannedMemberRemoval[];
66
60
  readonly unarchive: readonly string[];
61
+ /** 计划量到的合计;成功回执的 freedBytes 只念它。 */
67
62
  readonly bytes: number;
63
+ /** 这一次要连带清掉的独占图片对象(`trash/attachments.ts` 的域判定;空计划 = 没有可删的、或
64
+ * 剩余引用源读不出而整体跳过——原因在 `skipped` 里)。附件字节**不进** `bytes`:那是会话目录的
65
+ * 释放量,附件读数单独走回执的 `attachments`。 */
66
+ readonly attachments: ImageRemovalPlan;
68
67
  }
69
68
  /**
70
69
  * purge 的删除计划:`targets` 由调用方按组语义(`trash/groups.ts`)选好,这里只量、只筛、只查活体。
70
+ * **计划函数就是执行前的闸**——调用方拿到计划才允许写,所以准入必须发生在任何写之前。
71
71
  *
72
72
  * - `trash` 按 targets 原序逐个实测(目录缺席的行 `present:false` / 0 字节,但清册行仍要摘)。
73
73
  * - 盘上后代经 `selectStrandedMembers` 选出(一次问全部 targets、排除全部清册 id、bare id 全局只计划一次)。
74
74
  * - `members` 只收盘上还有目录的成员(目录缺席没有可删的字节);`unarchive` = targets + 这些成员:
75
75
  * 只摘这一次真要碰的 id。
76
- * - `admission:'live'` 在量目录与任何写之前逐个查 targets 与**全部枚举到的**盘上后代;
77
- * 任一在跑就整条拒绝,零副作用。
76
+ * - `attachments` 由被删目录(targets + 这些成员)里的图片引用与全部剩余引用源算独占;共享的保留、
77
+ * 剩余引用源读不出就整体跳过(判据住 `trash/attachments.ts`,这里只接线)。
78
+ * - 活体准入在量目录与任何写之前逐个查 targets 与**全部枚举到的**盘上后代;任一在跑就整条拒绝、
79
+ * 零副作用。没有跳过它的开关——计划就是执行前的闸(ADR-0006)。
78
80
  * - `sessionQuery` 缺席时端口回 `{ available: false }` ⇒ members 为空、不抛(盘上残留留给 `GET /orphans` 兜底)。
79
81
  */
80
- export declare function planTrashRemoval(deps: CoreDeps, rows: readonly TrashEntry[], targets: readonly TrashEntry[], options: {
81
- readonly admission: 'none' | 'live';
82
- }): Promise<RemovalPlan>;
82
+ export declare function planTrashRemoval(deps: CoreDeps, rows: readonly TrashEntry[], targets: readonly TrashEntry[]): Promise<RemovalPlan>;
83
83
  /**
84
84
  * orphan-clean 的删除计划:成员是调用方扫出来的传递闭包(`orphanGroupsOf`)。
85
85
  *
@@ -87,6 +87,8 @@ export declare function planTrashRemoval(deps: CoreDeps, rows: readonly TrashEnt
87
87
  * - `unarchive` 覆盖**全部传入成员**(含目录缺席):orphan-clean 在删目录之前整批摘归档集,
88
88
  * 目录缺席的 id 也可能还挂在归档集里;重试时不会再长出一条幽灵 id。
89
89
  * - live 准入覆盖**全部传入成员**(含目录缺席):它们都在这一组的可见面里,任一在跑就整组拒绝。
90
+ * - `attachments` 的剩余引用源由附件域自己扫(活动会话 + 全部 session-trash + storages/cache),
91
+ * 共享图片因此保留(判据住 `trash/attachments.ts`)。
90
92
  * - 回执由 `kind:'orphan'` 派生:删目录阶段失败时执行器要交回 `{removed, freedBytes, failedSessionId}`。
91
93
  */
92
94
  export declare function planOrphanRemoval(deps: CoreDeps, members: readonly {
@@ -94,20 +96,25 @@ export declare function planOrphanRemoval(deps: CoreDeps, members: readonly {
94
96
  readonly projectDir: string;
95
97
  }[]): Promise<RemovalPlan>;
96
98
  /**
97
- * 按计划执行一次永久删除:只写、不再量、不再判活体(计划就是执行前的闸)。
99
+ * 按计划执行一次永久删除:只写、不再量、不再判活体(计划就是执行前的闸)。回执的字节念计划
100
+ * (`plan.bytes`),执行期不再累加一遍。
101
+ *
102
+ * 顺序:整批摘归档集 → 摘有账本的成员的账本 → 逐条回收站行
103
+ * (在才删目录 → 摘清册行 → 通告成员离场)→ 逐条盘上成员(删目录 → 通告成员离场)→ 逐条附件对象。
104
+ * 目录删掉就没有"退回去"这回事,中途失败**不回滚**;已经删掉的由下面的计数器如实交回。
98
105
  *
99
- * 顺序(与迁移前的 purge / orphan-clean 一致):整批摘归档集 → 摘有账本的成员的账本 → 逐条回收站行
100
- * (在才删目录 → 摘清册行 → 通告成员离场)→ 逐条盘上成员(删目录 → 通告成员离场)。目录删掉就没有
101
- * "退回去"这回事,中途失败**不回滚**;已经删掉的由下面的计数器如实交回。
106
+ * 附件清理排在**所有会话目录删完之后**:对象只在会话真的没了之后才动;单文件失败只进
107
+ * `attachments.failed`,不影响(也不回滚)会话删除的回执。
102
108
  *
103
- * 失败形状沿用调用方原来的措辞与结构:
109
+ * 失败形状:
104
110
  * - `kind:'orphan'`(orphan-clean)⇒ `io` + `{removed, freedBytes, failedSessionId}`,不伪装成整组没动;
105
- * - `kind:'trash'`(purge)⇒ 同一个标签、不带 details。
106
- * - orphan-clean 的 unarchive / detach 在旧实现里不包装,这里原样抛出(不改变现有错误形状)。
111
+ * - `kind:'trash'`(purge)⇒ 同一个标签、不带 details;
112
+ * - orphan-clean 的 unarchive / detach 原样抛出。
107
113
  *
108
114
  * 通告只经 `member-presence`(摘活体三态 + `api-session/removed`),不在这里另写一份。
109
115
  */
110
- export declare function executeRemoval(deps: CoreDeps, plan: RemovalPlan): Promise<{
116
+ export declare function executeRemoval(deps: RemovalDeps, plan: RemovalPlan): Promise<{
111
117
  removed: number;
112
118
  freedBytes: number;
119
+ attachments: AttachmentCleanupWire;
113
120
  }>;
@@ -1,22 +1,35 @@
1
1
  import type { TrashEntry } from '../trash/entry.js';
2
- import type { CoreDeps } from '../deps.js';
2
+ import type { RemovalDeps } from '../deps.js';
3
+ import type { AttachmentCleanupWire } from '../shared/wire.js';
3
4
  /**
4
5
  * 彻底删除:从回收站里**真的**抹掉一条(或整组、或整站),并连带那些"还留在盘上、没被带走"的后代。
5
6
  *
6
- * 与另外两个动词的关系(ADR-0002):它的 interface 只有"一个条目 id(或缺席 = 清空)",
7
- * 批量的快照与"已被同批前一条带走"的判据不在这里(归 `verbs/batch.ts`)。
7
+ * 与另外两个动词的关系(ADR-0002):它的 interface 是"这一组目标 + 一份只读清册快照
8
+ *(targets 缺席 = 清空)";目标由 `verbs/batch-carried.ts` 按组语义算好,批量的"已被同批前一条
9
+ * 带走"判据不在这里。`requestedId` 是唯一的例外参数,但它**不参与任何判据**:只是让
10
+ * `trash-empty` 的原文点得出用户点的是哪一条(原文要上屏,点名承重),批量语义仍在批量层。
8
11
  *
9
- * **这条动词只做两件事:选目标、拿计划交给执行器**。量目录、筛盘上后代、活体准入与"先退归档集、
12
+ * **这条动词只做两件事:拿目标、把计划交给执行器**。量目录、筛盘上后代、活体准入与"先退归档集、
10
13
  * 再摘账本、最后删目录"的顺序都住 `verbs/permanent-removal.ts`——计划只读,执行器只写,计划就是
11
14
  * 执行前的闸(别在这里再长出一段手写步骤)。
12
15
  *
13
- * **这条动词不做回滚**:目录删掉就没有"退回去"这回事(2026-10-19 `f2a7757`:删到一半失败时已删目录不回滚;
16
+ * **这条动词不做回滚**:目录删掉就没有"退回去"这回事(删到一半失败时已删目录不回滚;
14
17
  * 带部分计数的 `details` 回执只属于 `orphan-clean`,purge 的 `removed` / `freedBytes` 仍只在成功回执里给)。
15
18
  * 对应地,`kind:'trash'` 的计划失败时执行器只抛那一步的 `io` 与原文、不带 details。
19
+ *
20
+ * 回执里的 `attachments` 原样来自执行器:独占图片在会话目录删完之后才清,附件失败只计数、不改
21
+ * `removed` / `freedBytes`(`skipped` = 有候选或剩余引用源读不出、这一轮部分或整体没清,原因上屏)。
16
22
  */
17
- export declare function purgeTrash(deps: CoreDeps): (entryId?: string,
18
- /** **只读**清册快照:批量层已经读过一次就递下来(ADR-0002);收缩归批量层,动词不碰它。 */
19
- snapshot?: readonly TrashEntry[]) => Promise<{
23
+ export declare function purgeTrash(deps: RemovalDeps): (
24
+ /** 这一组(只读);`undefined` = 清空。批量层由 `batch-carried.ts` 的 `claimOf` 给出,直调者
25
+ * 按 `trash/groups.ts` 的 `groupOf` 自己算;ghost 父的组只有孩子(不含 `requestedId`),照样清
26
+ * 这一组。 */
27
+ targets: readonly TrashEntry[] | undefined,
28
+ /** **只读**清册快照:批量层已经读过一次就递下来;收缩归 `batch-carried.ts`,动词不碰它。 */
29
+ rows: readonly TrashEntry[],
30
+ /** 只用于 `trash-empty` 的原文要点名用户点的那一条;不参与任何判据(批量语义归 `batch-carried.ts`)。 */
31
+ requestedId?: string) => Promise<{
20
32
  removed: number;
21
33
  freedBytes: number;
34
+ attachments: AttachmentCleanupWire;
22
35
  }>;
@@ -2,26 +2,22 @@ import { type TrashEntry } from '../trash/entry.js';
2
2
  import { type HostIdentityPort } from '../ports/host-port.js';
3
3
  import type { CoreDeps } from '../deps.js';
4
4
  /**
5
- * 恢复 = 把删除那次**按原路退回**,而且按组走:当年是按组一起搬进来的(父 + 它在盘上的孩子),
6
- * 只回父就会在回收站里留下一堆点不到的孩子行——清单只列父行,那些行既看不见也清不掉。
5
+ * 恢复:把删除那次按原路退回,并按组走。
7
6
  *
8
- * 三段与删除对称:
9
- * ① **预检整组**(一个字节都不写):每个成员的回收站目录与项目目录都得在。缺席的判据与原来一致
10
- * (回收站目录不在 = `trash-empty`;原项目目录不在 = `project-missing`,且绝不静默建目录)。
11
- * 预检在写之前,是为了不让"父已经回到 `sessions/`、孩子因为项目目录没了而回不去"这种半组留在外面。
12
- * **工作区账本不在这条硬前提里**:宿主的「删除工作区」只删注册、保留日志,记下的工作区没了就按
13
- * 「未分组」收(判据在 {@link attachableWorkspaceId}),照旧回盘——恢复是加法,不该因为一个可选的账目而失败。
14
- * ② **逐个恢复**,顺序与删除相反:文件先回来 → 挂账本 →(旧清单迁移)取消归档 → 摘清册行。
15
- * ③ 中途失败时 journal 把这个成员自己退回回收站;**已经恢复完的**兄弟由整组 catch 按相反顺序一起退回。
7
+ * 三段:
8
+ * 1. 预检整组(不写盘):每个成员的回收站目录与原项目目录都必须在;工作区账本缺席不算硬失败,
9
+ * 恢复后归入「未分组」。预检在任何写之前,避免留下半组。
10
+ * 2. 逐个恢复:文件先回来 → 挂账本 →(旧条目)取消归档 → 摘清册行。
11
+ * 3. 中途失败由 journal 回滚当前成员;已恢复的兄弟由整组按相反顺序退回。
16
12
  *
17
- * **通告不在这里**:一次批量只该拉一遍宿主会话表、只发一次 `api-session/added`(2026-10-17 那次
18
- * "恢复很慢"就是逐条发)。通告归 `verbs/batch.ts`(ADR-0002),所以这个动词的 interface 只有
19
- * "一个条目 id 进、回收站那一行出",没有批量专用的开关。
13
+ * 通告不在本文件:批量层在批尾一次 `announceRestored(ids)`(ADR-0002)。
20
14
  */
21
15
  export declare function restoreSession(deps: CoreDeps): (entryId: string,
22
- /** **只读**清册快照:批量层已经读过一次就递下来,整批因此只读一次(ADR-0002)。
23
- * 动词**不收缩**它——收缩与"已被同批前一条带走"的判据都归 `verbs/batch.ts`。 */
24
- snapshot?: readonly TrashEntry[]) => Promise<TrashEntry>;
16
+ /** 这一组(只读):批量层由 `batch-carried.ts` 的 `claimOf` 给出,直调者按 `trash/groups.ts`
17
+ * 的 `groupOf` 自己算。ghost 父(父行已摘、只剩孩子)的组只有孩子、不含 `entryId`,这里照旧
18
+ * 报 `trash-empty`;其余调用按这一组整体恢复。动词**不收缩**它——收缩与同批已带走集都归
19
+ * `verbs/batch-carried.ts`,组语义归 `trash/groups.ts`。 */
20
+ group: readonly TrashEntry[]) => Promise<TrashEntry>;
25
21
  /**
26
22
  * 这一次恢复**到底挂不挂账本**:交出能挂的那个 `workspaceId`,挂不了就 `undefined`。
27
23
  *
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@xrn1997/dsh-session-delete",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "在 DeepSeek Harness 里删除会话:删除=移入回收站(可恢复),另设显式彻底删除",
5
5
  "type": "module",
6
+ "packageManager": "pnpm@11.25.0",
6
7
  "main": "lib/index.js",
7
8
  "types": "lib/types/index.d.ts",
8
9
  "exports": {
@@ -73,7 +74,8 @@
73
74
  "dshReleases": {
74
75
  "0.2.0-rc.2": "compatible",
75
76
  "0.2.1-rc.1": "compatible",
76
- "0.2.1-rc.2": "compatible"
77
+ "0.2.1-rc.2": "compatible",
78
+ "0.2.1-rc.3": "compatible"
77
79
  }
78
80
  }
79
81
  },
@@ -1,10 +0,0 @@
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;