@xrn1997/dsh-session-delete 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/lib/client.js +1496 -985
- package/lib/index.js +821 -710
- package/lib/types/client/batch-bar.d.ts +2 -1
- package/lib/types/client/batch-confirm.d.ts +1 -1
- package/lib/types/client/busy-gate.d.ts +6 -0
- package/lib/types/client/copy.d.ts +2 -2
- package/lib/types/client/host-rows.d.ts +18 -3
- package/lib/types/client/main-view.d.ts +7 -2
- package/lib/types/client/orphan-maintenance.d.ts +27 -0
- package/lib/types/client/receipt.d.ts +33 -3
- package/lib/types/client/remote.d.ts +18 -22
- package/lib/types/client/store.d.ts +31 -0
- package/lib/types/client/trash-nav.d.ts +30 -0
- package/lib/types/client/undo-toast.d.ts +11 -3
- package/lib/types/client/use-trash-panel-controller.d.ts +7 -20
- package/lib/types/client/use-trash-query.d.ts +20 -9
- package/lib/types/client/write-seats.d.ts +52 -0
- package/lib/types/client/write-settlement.d.ts +72 -0
- package/lib/types/client/write.d.ts +51 -0
- package/lib/types/deps.d.ts +2 -2
- package/lib/types/index.d.ts +2 -1
- package/lib/types/ports/host-port.d.ts +4 -18
- package/lib/types/shared/wire.d.ts +7 -0
- package/lib/types/trash/detail.d.ts +5 -2
- package/lib/types/trash/groups.d.ts +17 -6
- package/lib/types/trash/ledger.d.ts +20 -0
- package/lib/types/trash/manifest.d.ts +2 -1
- package/lib/types/trash/reconcile.d.ts +4 -3
- package/lib/types/verbs/batch-carried.d.ts +20 -0
- package/lib/types/verbs/delete-session.d.ts +4 -3
- package/lib/types/verbs/errors.d.ts +2 -10
- package/lib/types/verbs/live-admission.d.ts +13 -0
- package/lib/types/verbs/member-journal.d.ts +22 -3
- package/lib/types/verbs/member-presence.d.ts +38 -6
- package/lib/types/verbs/move-group.d.ts +20 -0
- package/lib/types/verbs/orphan-subagents.d.ts +8 -3
- package/lib/types/verbs/permanent-removal.d.ts +7 -6
- package/lib/types/verbs/purge-trash.d.ts +14 -6
- package/lib/types/verbs/restore-session.d.ts +8 -6
- package/package.json +3 -2
- package/lib/types/client/write-commit.d.ts +0 -10
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一次逐条 `/apply` 写入的**唯一生命周期**:apply → 解信封 → `readReceipt` → settlement → refresh。
|
|
3
|
+
*
|
|
4
|
+
* 收尾顺序与 `anySuccess` 门住 `src/client/write-settlement.ts`(为什么按 action 收见 ADR-0013);
|
|
5
|
+
* 这里只管时机、信封判读、整趟的闸与 `refresh` 的交法。清空回收站是聚合回执(`/empty`),收尾仍住
|
|
6
|
+
* `use-trash-query.ts` 的 `emptyTrash`。
|
|
7
|
+
*
|
|
8
|
+
* 五条判据:
|
|
9
|
+
* - `ok` 只说**信封**成不成;逐条成败在 `failures` 里,调用方自己决定怎么上屏。
|
|
10
|
+
* - 顶层失败把同一句原文铺到每个 id,不结算;`refresh` 照样交(成败都在 outcome 里)。
|
|
11
|
+
* - **信封成功就结算**(逐条全失败也跑:批量写入的失效通告不能漏);`anySuccess` 由这里算好,
|
|
12
|
+
* 按 action 的呈现材质由 settlement 按它上闸。调用方只交呈现材质,再按 `outcome` 决定是否真的重读。
|
|
13
|
+
* 结算在 `refresh` 之前:列表重读看得到收尾后的现场。
|
|
14
|
+
* - `single: true` 时把 N=1 模式交给 `receipt.ts` 的读法;单条 null 在那一处折成 failed,
|
|
15
|
+
* 这里不再重复守卫。
|
|
16
|
+
* - **闸包住整趟**:给了 `gate` 就把 apply → settle → refresh 一起包进去——refresh 落在闸外时,
|
|
17
|
+
* 旧清单还在屏上,同一个动作能被再点一次;调用方不再自己 `around`。
|
|
18
|
+
*/
|
|
19
|
+
import type { BusyGate } from './busy-gate.js';
|
|
20
|
+
import { type OneShotDeps } from './remote.js';
|
|
21
|
+
import { type SettledFacts, type WritePresentation } from './write-settlement.js';
|
|
22
|
+
import type { ApplyAction, RemoteFailure } from '../shared/wire.js';
|
|
23
|
+
export type WriteSuccess<A extends ApplyAction> = SettledFacts<A> & {
|
|
24
|
+
readonly ok: true;
|
|
25
|
+
/** 逐条失败原文(failed / neutral / missing),按 id。 */
|
|
26
|
+
readonly failures: ReadonlyMap<string, string>;
|
|
27
|
+
};
|
|
28
|
+
export interface WriteFailure<A extends ApplyAction> {
|
|
29
|
+
readonly ok: false;
|
|
30
|
+
readonly action: A;
|
|
31
|
+
readonly ids: readonly string[];
|
|
32
|
+
readonly failure: RemoteFailure;
|
|
33
|
+
/** 顶层失败时同一句原文铺到每个请求 id。 */
|
|
34
|
+
readonly failures: ReadonlyMap<string, string>;
|
|
35
|
+
}
|
|
36
|
+
export type WriteOutcome<A extends ApplyAction> = WriteSuccess<A> | WriteFailure<A>;
|
|
37
|
+
export interface WriteInput<A extends ApplyAction> {
|
|
38
|
+
readonly deps: OneShotDeps;
|
|
39
|
+
readonly action: A;
|
|
40
|
+
readonly ids: readonly string[];
|
|
41
|
+
/** 单条写入面:把 N=1 模式交给 receipt 读法(null 折成协议错)。 */
|
|
42
|
+
readonly single?: boolean;
|
|
43
|
+
/** 信封成功时交给 settlement 的呈现材质(`successIds` 可能为空);按 action 的收尾顺序住 `write-settlement.ts`。 */
|
|
44
|
+
readonly presentation?: WritePresentation<A>;
|
|
45
|
+
/** 这一趟的闸:给了就把 apply → settle → refresh 整趟包进 busy。refresh 不许落在闸外。 */
|
|
46
|
+
readonly gate?: BusyGate;
|
|
47
|
+
/** 写完(顶层失败或信封成功,无论逐条成败)后的重读策略;成败与 `anySuccess` 都在 outcome 里,
|
|
48
|
+
* 是否真的重读由调用方决定。 */
|
|
49
|
+
readonly refresh?: (outcome: WriteOutcome<A>) => void | Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export declare function runWrite<A extends ApplyAction>(input: WriteInput<A>): Promise<WriteOutcome<A>>;
|
package/lib/types/deps.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { FsPort } from './ports/fs-port.js';
|
|
2
2
|
import type { HostPort } from './ports/host-port.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { TrashLedger } from './trash/ledger.js';
|
|
4
4
|
/** Node 半的基础依赖面:组合根只装配这一颗;动词之间不互相 import 依赖面 interface。 */
|
|
5
5
|
export interface CoreDeps {
|
|
6
6
|
fs: FsPort;
|
|
7
7
|
host: HostPort;
|
|
8
|
-
|
|
8
|
+
ledger: TrashLedger;
|
|
9
9
|
home: string;
|
|
10
10
|
}
|
|
11
11
|
/** 只有删除读时钟;其余读 / 计划 / 恢复 / 永久删除只用 CoreDeps。 */
|
package/lib/types/index.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ export declare const name = "@xrn1997/dsh-session-delete";
|
|
|
19
19
|
* `super(ctx, "webServer")`),服务名与 `register({kind:'prefix', path, handler})` 的形状按
|
|
20
20
|
* `dsh-host-webserver/lib/index.js` 与 `README.md` 原文读实,不是猜的。
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* 清册的三层各有各的家:raw 绑定 `src/trash/storage-manifest.ts`、读面(对账 + 读)`src/trash/ledger.ts`、
|
|
23
|
+
* 数据根 `src/ports/home.ts`。这里只把它们接起来,任何一层都不在组合根里长判据。
|
|
23
24
|
*/
|
|
24
25
|
export declare const inject: string[];
|
|
25
26
|
/**
|
|
@@ -49,23 +49,9 @@ export interface HostPresencePort {
|
|
|
49
49
|
/** **如实通告**客户端"这个会话没了":发宿主自己声明并转发的 `api-session/removed`,客户端当场删行。
|
|
50
50
|
* 冷会话没有活体可摘,但同样需要这一条——否则那一行要等到重连(`handleConnected` → 重列)才消失。 */
|
|
51
51
|
announceRemoved(sessionId: string): Promise<void>;
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
announceRestored(sessionId: string): Promise<void>;
|
|
56
|
-
/** 宿主会话表的一次预取:`[{ sessionId, summary }]`,`summary` 是宿主**原样**那一条。
|
|
57
|
-
* 给 {@link announceRestoredBatch} 当 `known` 用——整批只拉一次表。服务没挂回空表,不抛。 */
|
|
58
|
-
restoredSummariesOf(): Promise<readonly {
|
|
59
|
-
readonly sessionId: string;
|
|
60
|
-
readonly summary: unknown;
|
|
61
|
-
}[]>;
|
|
62
|
-
/** 一组会话的通告(2026-10-17 批量重构):`known` 是调用方**整批预取**的摘要表——给了就一次都不
|
|
63
|
-
* 再拉表(每条一次 `controller.list()` 在 100 条的批量里就是 100 遍全表扫,那是那次"恢复很慢"的
|
|
64
|
-
* 头号来源);没给则自己拉一次。缺席 id 不发,不影响已完成的恢复。 */
|
|
65
|
-
announceRestoredBatch(ids: readonly string[], known?: readonly {
|
|
66
|
-
readonly sessionId: string;
|
|
67
|
-
readonly summary: unknown;
|
|
68
|
-
}[]): Promise<void>;
|
|
52
|
+
/** 一组会话的「回来了」通告:implementation 拉一次宿主会话表,按 id 逐条发 added。
|
|
53
|
+
* 单条就是长度为 1 的批;批量只调一次。缺席 id 不发,不影响已完成的恢复。 */
|
|
54
|
+
announceRestored(sessionIds: readonly string[]): Promise<void>;
|
|
69
55
|
}
|
|
70
56
|
/**
|
|
71
57
|
* 一次语料读的结果:**缺席显式化**——`available: false` 说的是"这一趟看不见语料"(`sessionQuery` 没挂,
|
|
@@ -96,7 +82,7 @@ export interface HostCorpusPort {
|
|
|
96
82
|
* 键按调用方传进来的写法回(同一个父的 `session-x` / `x` 两种写法是同一个根,见 `membersFor`)。 */
|
|
97
83
|
subagentMembersOf(parentIds: readonly string[]): Promise<CorpusRead<Record<string, readonly SubagentMember[]>>>;
|
|
98
84
|
}
|
|
99
|
-
/** 复合别名:adapter(`createHostPort`)与调用方把这
|
|
85
|
+
/** 复合别名:adapter(`createHostPort`)与调用方把这 15 个方法当一个端口用;三个角色是它的切面。 */
|
|
100
86
|
export type HostPort = HostIdentityPort & HostPresencePort & HostCorpusPort;
|
|
101
87
|
export interface SessionIdentity {
|
|
102
88
|
readonly sessionId: string;
|
|
@@ -212,6 +212,13 @@ export type RouteCode = (typeof ROUTE_CODES)[number];
|
|
|
212
212
|
*/
|
|
213
213
|
export type AllCodeSuffix = VerbCode | RouteCode;
|
|
214
214
|
export declare const SESSION_DELETE_CODES: Record<AllCodeSuffix, `sessiondelete/${AllCodeSuffix}`>;
|
|
215
|
+
/** `error.code` 是不是本仓的失败形状(`sessiondelete/<后缀>`)。**只认前缀**:后缀在不在册是
|
|
216
|
+
* {@link verbCodeOf} 的问题。任何要「已经是本仓的码就原样对待」的调用点都问这一句,不在调用点
|
|
217
|
+
* 再写一份前缀字面量。 */
|
|
218
|
+
export declare function isSessionDeleteCodeError(error: unknown): boolean;
|
|
219
|
+
/** `error.code` 折回那个动词词:前缀与后缀两件都成立才交回(后缀要在 {@link VERB_CODES} 册上)。
|
|
220
|
+
* 路由自检码、别域的码、非字符串 code 一律 `undefined`。 */
|
|
221
|
+
export declare function verbCodeOf(error: unknown): VerbCode | undefined;
|
|
215
222
|
/** 回收站目录名(相对宿主数据根)。Node 半拼路径,浏览器半在页脚念它——同一个字面量。 */
|
|
216
223
|
export declare const TRASH_DIR_NAME = "session-trash";
|
|
217
224
|
/** 页脚/文案里带尾斜杠的那一版(`splitOn` 按它切一刀再把目录名包成 code)。 */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TrashLedger } from './ledger.js';
|
|
2
2
|
import type { FsPort } from '../ports/fs-port.js';
|
|
3
3
|
import type { TrashDetailMessageWire, TrashDetailWire } from '../shared/wire.js';
|
|
4
4
|
/** 每条文字的上限(字符)。超出即截断并置 `truncated`。 */
|
|
@@ -10,8 +10,11 @@ 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
|
+
/** 详情自己直接要的三件(判目录在不在、挑日志、读日志)。对账不在这条路上,所以不需要 `listDirs`。 */
|
|
13
14
|
fs: Pick<FsPort, 'exists' | 'listFiles' | 'readFile'>;
|
|
14
|
-
|
|
15
|
+
/** 详情只读清册的 **raw 面**:`/detail` 声明为 pure,读一次不能顺手对账——对账会写清册行,那是写动作,
|
|
16
|
+
* 必须排在到达序链上。代价是有意的:只有目录、清册还没这一行的手工搬入条目读不到(`/list` 对账后可见)。 */
|
|
17
|
+
ledger: Pick<TrashLedger, 'raw'>;
|
|
15
18
|
home: string;
|
|
16
19
|
};
|
|
17
20
|
/** 两个载荷闸(可调是为把策略单独验穿;生产走的就是这两个默认值,用例里钉着)。 */
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* **接得住孩子的只有组根**——自己不记 `parentEntryId` 的那一行。父行不在、或父行自己也是孩子时,
|
|
10
10
|
* 孩子必须自己占一行,否则它的字节会被头部合计数两遍(而它自己既看不见也清不掉)。
|
|
11
11
|
*
|
|
12
|
-
* 三个函数都是**纯函数**、都吃一份快照:`/apply`
|
|
13
|
-
*
|
|
12
|
+
* 三个函数都是**纯函数**、都吃一份快照:`/apply` 那条路会在同一批里多次重问;批量层持有一份
|
|
13
|
+
* 会收缩的当前视图(`verbs/batch-carried.ts`),收缩时机归调用方——groups module 不持有可变状态,
|
|
14
|
+
* 也就没有"这份快照谁负责更新"的第二套契约。
|
|
14
15
|
*/
|
|
15
16
|
import type { TrashEntry } from './entry.js';
|
|
16
17
|
/** 一条露面的组根,连同它接住的孩子(顺序与清册一致)。 */
|
|
@@ -35,11 +36,21 @@ export declare function visibleGroups(rows: readonly TrashEntry[]): TrashGroupVi
|
|
|
35
36
|
* 调用方据此报 `trash-empty` / 判"已被同批前一条带走"。
|
|
36
37
|
*/
|
|
37
38
|
export declare function groupOf(rows: readonly TrashEntry[], id: string): TrashEntry[];
|
|
39
|
+
/** 一条 id 在「原始快照 vs 当前视图」里的三态。判别联合:**只有 `fresh` 带当前这一组**——
|
|
40
|
+
* 组语义只对"还在"的 id 有意义,carried / absent 没有可交的组,调用方也就无从误用。 */
|
|
41
|
+
export type GroupClaim = {
|
|
42
|
+
readonly verdict: 'fresh';
|
|
43
|
+
readonly group: readonly TrashEntry[];
|
|
44
|
+
} | {
|
|
45
|
+
readonly verdict: 'carried';
|
|
46
|
+
} | {
|
|
47
|
+
readonly verdict: 'absent';
|
|
48
|
+
};
|
|
38
49
|
/**
|
|
39
|
-
*
|
|
50
|
+
* 「这条是被**同批前一条**带走的,还是整批开始前就没有,还是仍在?」
|
|
40
51
|
*
|
|
41
|
-
*
|
|
52
|
+
* 幂等语义只属于 `carried`:后来者本来就不在清册里(用户点了条幽灵 id、或对账刚摘掉它)必须照样响亮报
|
|
42
53
|
* `trash-empty`,不能被"已被带走"这句话掩盖成成功。**所以判据是快照的原始形状,不是当前形状**:
|
|
43
|
-
*
|
|
54
|
+
* 原始快照里认得出(它自己、或指向它的孩子任一在),而当前快照里已经空了 ⇒ 同批带走的。
|
|
44
55
|
*/
|
|
45
|
-
export declare function
|
|
56
|
+
export declare function claimOf(original: readonly TrashEntry[], current: readonly TrashEntry[], id: string): GroupClaim;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 清册的两个读面:**真相读走 `list()`(先对账,再交行);pure 读面走 `raw()`(只回索引,不写行)**。
|
|
3
|
+
*
|
|
4
|
+
* 外部事实源是 `session-trash/` 里的目录;`manifest.list()` 只是 raw 索引。
|
|
5
|
+
* 消费方只认这个 seam:写动作与真相读用 `list()`,不再各自记得"读前先 reconcile";`/orphans` 与 `/detail`
|
|
6
|
+
* 声明为 pure,只许 `raw()`——一次 pure 读若顺手对账,就等于把一个写动作带到了到达序链之外。
|
|
7
|
+
* **它不是 `ManifestPort`**:同名 `list()` 的语义已经换成真相读,拿它当 raw 端口用会漏对账或递归对账。
|
|
8
|
+
* 两条写口从 raw 端口取;raw 端口本身只留给本 module 与 `src/trash/reconcile.ts` 的 implementation。
|
|
9
|
+
*/
|
|
10
|
+
import type { FsPort } from '../ports/fs-port.js';
|
|
11
|
+
import type { TrashEntry } from './entry.js';
|
|
12
|
+
import type { ManifestPort } from './manifest.js';
|
|
13
|
+
export interface TrashLedger extends Pick<ManifestPort, 'add' | 'remove'> {
|
|
14
|
+
/** 真相读:先对账、再交行。`/list`、`/empty` 与写动词都认这一条。 */
|
|
15
|
+
list(): Promise<TrashEntry[]>;
|
|
16
|
+
/** raw 索引快照:**不对账、不写行**。只给声明为 pure 的读面(`/orphans` 扫描、`/detail` 详情);
|
|
17
|
+
* 要知道"此刻真有的一切"用 `list()`。 */
|
|
18
|
+
raw(): Promise<TrashEntry[]>;
|
|
19
|
+
}
|
|
20
|
+
export declare function createTrashLedger(raw: ManifestPort, fs: FsPort, home: string): TrashLedger;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TrashEntry } from './entry.js';
|
|
2
|
-
/**
|
|
2
|
+
/** 回收站清单的 **raw 索引端口**:只给 `src/trash/ledger.ts` 与对账实现;动词与路由只认 ledger,
|
|
3
|
+
* 不认 storage domain 的形状。`list()` 不保证与 `session-trash/` 目录一致(对账在 ledger 的 `list()`)。 */
|
|
3
4
|
export interface ManifestPort {
|
|
4
5
|
list(): Promise<TrashEntry[]>;
|
|
5
6
|
add(e: TrashEntry): Promise<void>;
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* `~/.dsh/storages/` 那份清单从此是**内部索引**,不用手碰。
|
|
6
6
|
*
|
|
7
7
|
* 五条口径(都不是口味):
|
|
8
|
-
* ①
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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、
|
|
@@ -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;
|
|
@@ -2,7 +2,7 @@ import { type TrashEntry } from '../trash/entry.js';
|
|
|
2
2
|
import type { DeleteDeps } 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
|
|
21
|
+
* interface 只有**一个 id**:批量的性质不在这里(同批已带走集归 `verbs/batch-carried.ts`,
|
|
22
|
+
* 组语义归 `trash/groups.ts`)。
|
|
22
23
|
* 结果带回这次真的搬走的整组(`movedSessionIds`)——它是动词交回的内部数据,不再靠回调借用动词的手。
|
|
23
24
|
*/
|
|
24
25
|
export declare function deleteSession(deps: DeleteDeps): (sessionId: string) => Promise<DeleteResult>;
|
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
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 {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 活体准入:一次删除 / 永久删除 / 孤儿清理之前,逐个问「这个会话在宿主内存里 attach 着吗」——
|
|
3
|
+
* 任一个在跑就整组拒绝、零副作用(词表见 CONTEXT.md 的「活体」条;裁决见 ADR-0008)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么单独一个 module:删除的逐个成员与永久删除计划的 targets + 盘上后代,此前各手写一遍同一条
|
|
6
|
+
* 循环、同一句原文。两处只要有一处改了措辞或漏查一个 id,客户端拿到的就是两种「还在跑」。
|
|
7
|
+
*
|
|
8
|
+
* 边界:**什么时候查归调用方**。永久删除的准入仍住在计划里、在任何写之前(ADR-0006:计划就是执行前的闸);
|
|
9
|
+
* 这个 module 只拥有「查什么、怎么判、怎么说」。
|
|
10
|
+
*/
|
|
11
|
+
import type { HostIdentityPort } from '../ports/host-port.js';
|
|
12
|
+
/** 逐个问,任一在跑就抛 `live`。名单的挑选(哪些成员算这一组)归调用方,这里只按传入顺序问完。 */
|
|
13
|
+
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 {
|
|
17
|
+
/** 稳定的领域键,给 `restored(key)` 用。 */
|
|
18
|
+
readonly key: JournalStepKey;
|
|
2
19
|
/** 这一步在做什么。失败原文按「`<label>:<原错误>`」拼——与迁移前那 22 处 `rollbackReason` 的措辞同形。 */
|
|
3
20
|
readonly label: string;
|
|
4
21
|
/** 做这一步。抛什么都由 journal 折成 `sessiondelete/io`(除非它自己已经是一个动词码)。 */
|
|
5
22
|
run(): Promise<void>;
|
|
6
|
-
/** 反向动作。**`run`
|
|
7
|
-
undo?(): Promise<
|
|
23
|
+
/** 反向动作。**`run` 成功之后**才会被登记;返回这一步退没退成与原因。 */
|
|
24
|
+
undo?(): Promise<StepUndo>;
|
|
8
25
|
/** 这一步**自己**失败时也跑 `undo`:给"可能已经半做完"的非原子步骤用(取消归档是唯一的例子)。 */
|
|
9
26
|
readonly undoOnFailure?: boolean;
|
|
10
27
|
}
|
|
@@ -12,12 +29,14 @@ export interface Journal {
|
|
|
12
29
|
/** 按顺序跑完。中途失败 ⇒ 已做成的按相反顺序退回,然后把原错误(带 `rollbackFailures`)抛出去。 */
|
|
13
30
|
run(): Promise<void>;
|
|
14
31
|
/** 把**已经做成**的步骤按相反顺序退回去(整组回滚用:组里后面那个成员搬不动时叫这一句)。 */
|
|
15
|
-
undo(): Promise<
|
|
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
42
|
* `details.rollbackFailures`,绝不让原错误独走(2026-10-19 `f2a7757`;那些失败句子的合并与措辞住
|
|
@@ -3,19 +3,23 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 为什么要有它:delete(成功与回滚)/ restore 回滚 / purge(两个分支)/ orphan-clean
|
|
5
5
|
* 六处调用曾经各写一遍
|
|
6
|
-
* `evictLive → !== 'unknown' → announceRemoved
|
|
7
|
-
*
|
|
6
|
+
* `evictLive → !== 'unknown' → announceRemoved`;删除回滚还要自己拼目录与账本两个布尔
|
|
7
|
+
* 才发 `announceRestored`。
|
|
8
8
|
* 这条协议说错一次就是客户端留着假行、或删掉真行,所以只留一个家。
|
|
9
9
|
*
|
|
10
10
|
* 边界:
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
11
|
+
* - 批量恢复自己发起的那次通告(`announceRestored(ids)`)归 `verbs/batch.ts`(ADR-0002):同一个方法、
|
|
12
|
+
* 同一份预取,只是调用方是那一批的主人。这里只判单成员事实。
|
|
13
|
+
* - 本模块不决定哪个键算归位:动词在 `tail.onUndone` 里把 `restored(key)` 读成事实再交进来,真值表住这里;
|
|
14
|
+
* `run` / `undo` 之后补一条通告的**形状**由 {@link withPresenceTail} 拥有。
|
|
13
15
|
* - 本模块用到的三条(`evictLive` / `announceRemoved` / `announceRestored`)按契约不抛(evictLive 三态、
|
|
14
16
|
* announce* 内部吞异常),这里不再叠 catch。
|
|
15
17
|
*/
|
|
16
18
|
import type { HostPresencePort } from '../ports/host-port.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
import type { Journal, JournalUndo } from './member-journal.js';
|
|
20
|
+
/** 本模块实际用到的三条宿主能力。`Pick` 此刻覆盖 `HostPresencePort` 的三条方法,仍留着它当闸:
|
|
21
|
+
* 将来端口多出第四条时,这里不会被动跟着变宽——这里是单成员协议,批量恢复由 `verbs/batch.ts`
|
|
22
|
+
* 自己发起,不该因为共用同一颗 adapter 就在类型上声明依赖。 */
|
|
19
23
|
export type MemberPresenceHost = Pick<HostPresencePort, 'evictLive' | 'announceRemoved' | 'announceRestored'>;
|
|
20
24
|
export interface MemberBackFacts {
|
|
21
25
|
/** 目录搬回原处了吗。 */
|
|
@@ -33,3 +37,31 @@ export interface MemberPresence {
|
|
|
33
37
|
memberBack(sessionId: string, facts: MemberBackFacts): Promise<void>;
|
|
34
38
|
}
|
|
35
39
|
export declare function createMemberPresence(host: MemberPresenceHost): MemberPresence;
|
|
40
|
+
/** 一次 undo 之后要发哪一条通告:`leave` = 成员已不在可见面,`return` = 已回到原可见状态。 */
|
|
41
|
+
export type PresenceAnnounce = {
|
|
42
|
+
readonly kind: 'leave';
|
|
43
|
+
readonly backInTrash: boolean;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: 'return';
|
|
46
|
+
readonly diskBack: boolean;
|
|
47
|
+
readonly workspaceBack: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** 动词给工厂的那一格:哪个成员、run 成不成功要不要补离场、undo 之后按逐步结果发哪一条。 */
|
|
50
|
+
export interface PresenceTail {
|
|
51
|
+
readonly sessionId: string;
|
|
52
|
+
/** `run()` 成功后补离场通告(删除用)。省略 = run 不动在场(恢复的 run 本来就不该发东西)。 */
|
|
53
|
+
readonly onDone?: 'leave';
|
|
54
|
+
/** `undo()` 完成后按逐步结果发通告。 */
|
|
55
|
+
onUndone(undo: JournalUndo): PresenceAnnounce;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 给一次成员移动的 journal 补上「在场那条尾巴」:run 完全成功之后离场、undo 之后按事实归位或离场。
|
|
59
|
+
*
|
|
60
|
+
* 为什么是个工厂:删除与恢复此前各手拼一个匿名 journal,形状逐字相同(`journal.run` 加上
|
|
61
|
+
* `presence.memberGone`/`journal.undo` 加上 `presence.memberBack`),只差键 → 事实的映射。
|
|
62
|
+
* 两处各写一遍,就是「补在哪一拍」这条顺序又有第二个家。
|
|
63
|
+
*
|
|
64
|
+
* 边界:`memberGone` 的 `backInTrash:false` 早退与 `memberBack` 的两件齐备真值表仍归 `createMemberPresence`;
|
|
65
|
+
* 这里只把事实原样递过去,不替它判断。
|
|
66
|
+
*/
|
|
67
|
+
export declare function withPresenceTail(presence: MemberPresence, 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,4 +1,5 @@
|
|
|
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
4
|
import type { CoreDeps } from '../deps.js';
|
|
4
5
|
/** 组里一个人的盘上身份(字节是**扫描那一刻**量的,清理时按目录重量一次,不拿这份快照冒充准数)。 */
|
|
@@ -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
|
-
/**
|
|
35
|
-
*
|
|
35
|
+
/** **本函数是纯扫描,不对账、不写清册行**(`/orphans` 声明为 pure,一次扫描不能顺手写行)。清册默认读
|
|
36
|
+
* `ledger.raw()`;写路径(`orphan-clean`)可以在调它之前先 `ledger.list()` 对账,并把交回的行递进来,
|
|
37
|
+
* 免得同一份索引再读一遍——否则刚被人手工搬进 `session-trash/` 的父目录还没有行,会被当成"父没了"
|
|
38
|
+
* 直接删盘。
|
|
36
39
|
*
|
|
37
40
|
* `listSessions()` 回 `{ available: false }` 时**必须抛**:那是宿主的会话语料服务没挂,不是"这个家没有孤儿"。
|
|
38
41
|
* 拿缺席当空语料 ⇒ 报告说"零条孤儿"、界面上一句"没有要清理的",而盘上真有几百 MB——那是假话,
|
|
39
42
|
* 所以这里按 `io` 响亮失败(AGENTS.md:`available: false` 意味着服务缺席,扫描方不许冒充读数)。 */
|
|
40
|
-
export declare function loadOrphanGroups(deps: CoreDeps
|
|
43
|
+
export declare function loadOrphanGroups(deps: CoreDeps,
|
|
44
|
+
/** 调用方已对账过的清册行(`ledger.list()`);缺席则读 `ledger.raw()`。 */
|
|
45
|
+
trash?: readonly TrashEntry[]): Promise<readonly OrphanGroup[]>;
|
|
41
46
|
/** 把一组组收成那份报告:根清单 + 闭包总数/总字节 + 失踪父的**去重计数** + 按项目目录的分摊。
|
|
42
47
|
* `groups` 按字节从多到少排(界面上"哪家还剩得最多"要看第一行)。 */
|
|
43
48
|
export declare function reportOrphans(groups: readonly OrphanGroup[]): OrphanReportWire;
|
|
@@ -64,22 +64,22 @@ export interface RemovalPlan {
|
|
|
64
64
|
readonly trash: readonly PlannedTrashRow[];
|
|
65
65
|
readonly members: readonly PlannedMemberRemoval[];
|
|
66
66
|
readonly unarchive: readonly string[];
|
|
67
|
+
/** 计划量到的合计;成功回执的 freedBytes 只念它。 */
|
|
67
68
|
readonly bytes: number;
|
|
68
69
|
}
|
|
69
70
|
/**
|
|
70
71
|
* purge 的删除计划:`targets` 由调用方按组语义(`trash/groups.ts`)选好,这里只量、只筛、只查活体。
|
|
72
|
+
* **计划函数就是执行前的闸**——调用方拿到计划才允许写,所以准入必须发生在任何写之前。
|
|
71
73
|
*
|
|
72
74
|
* - `trash` 按 targets 原序逐个实测(目录缺席的行 `present:false` / 0 字节,但清册行仍要摘)。
|
|
73
75
|
* - 盘上后代经 `selectStrandedMembers` 选出(一次问全部 targets、排除全部清册 id、bare id 全局只计划一次)。
|
|
74
76
|
* - `members` 只收盘上还有目录的成员(目录缺席没有可删的字节);`unarchive` = targets + 这些成员:
|
|
75
77
|
* 只摘这一次真要碰的 id。
|
|
76
|
-
* -
|
|
77
|
-
*
|
|
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[]
|
|
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
|
*
|
|
@@ -94,7 +94,8 @@ export declare function planOrphanRemoval(deps: CoreDeps, members: readonly {
|
|
|
94
94
|
readonly projectDir: string;
|
|
95
95
|
}[]): Promise<RemovalPlan>;
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
97
|
+
* 按计划执行一次永久删除:只写、不再量、不再判活体(计划就是执行前的闸)。回执的字节念计划
|
|
98
|
+
* (`plan.bytes`),执行期不再累加一遍。
|
|
98
99
|
*
|
|
99
100
|
* 顺序(与迁移前的 purge / orphan-clean 一致):整批摘归档集 → 摘有账本的成员的账本 → 逐条回收站行
|
|
100
101
|
* (在才删目录 → 摘清册行 → 通告成员离场)→ 逐条盘上成员(删目录 → 通告成员离场)。目录删掉就没有
|
|
@@ -3,10 +3,12 @@ import type { CoreDeps } from '../deps.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* 彻底删除:从回收站里**真的**抹掉一条(或整组、或整站),并连带那些"还留在盘上、没被带走"的后代。
|
|
5
5
|
*
|
|
6
|
-
* 与另外两个动词的关系(ADR-0002):它的 interface
|
|
7
|
-
|
|
6
|
+
* 与另外两个动词的关系(ADR-0002):它的 interface 是"这一组目标 + 一份只读清册快照
|
|
7
|
+
*(targets 缺席 = 清空)";目标由 `verbs/batch-carried.ts` 按组语义算好,批量的"已被同批前一条
|
|
8
|
+
* 带走"判据不在这里。`requestedId` 是唯一的例外参数,但它**不参与任何判据**:只是让
|
|
9
|
+
* `trash-empty` 的原文点得出用户点的是哪一条(原文要上屏,点名承重),批量语义仍在批量层。
|
|
8
10
|
*
|
|
9
|
-
*
|
|
11
|
+
* **这条动词只做两件事:拿目标、把计划交给执行器**。量目录、筛盘上后代、活体准入与"先退归档集、
|
|
10
12
|
* 再摘账本、最后删目录"的顺序都住 `verbs/permanent-removal.ts`——计划只读,执行器只写,计划就是
|
|
11
13
|
* 执行前的闸(别在这里再长出一段手写步骤)。
|
|
12
14
|
*
|
|
@@ -14,9 +16,15 @@ import type { CoreDeps } from '../deps.js';
|
|
|
14
16
|
* 带部分计数的 `details` 回执只属于 `orphan-clean`,purge 的 `removed` / `freedBytes` 仍只在成功回执里给)。
|
|
15
17
|
* 对应地,`kind:'trash'` 的计划失败时执行器只抛那一步的 `io` 与原文、不带 details。
|
|
16
18
|
*/
|
|
17
|
-
export declare function purgeTrash(deps: CoreDeps): (
|
|
18
|
-
/**
|
|
19
|
-
|
|
19
|
+
export declare function purgeTrash(deps: CoreDeps): (
|
|
20
|
+
/** 这一组(只读);`undefined` = 清空。批量层由 `batch-carried.ts` 的 `claimOf` 给出,直调者
|
|
21
|
+
* 按 `trash/groups.ts` 的 `groupOf` 自己算;ghost 父的组只有孩子(不含 `requestedId`),照样清
|
|
22
|
+
* 这一组。 */
|
|
23
|
+
targets: readonly TrashEntry[] | undefined,
|
|
24
|
+
/** **只读**清册快照:批量层已经读过一次就递下来;收缩归 `batch-carried.ts`,动词不碰它。 */
|
|
25
|
+
rows: readonly TrashEntry[],
|
|
26
|
+
/** 只用于 `trash-empty` 的原文要点名用户点的那一条;不参与任何判据(批量语义归 `batch-carried.ts`)。 */
|
|
27
|
+
requestedId?: string) => Promise<{
|
|
20
28
|
removed: number;
|
|
21
29
|
freedBytes: number;
|
|
22
30
|
}>;
|
|
@@ -5,7 +5,7 @@ import type { CoreDeps } from '../deps.js';
|
|
|
5
5
|
* 恢复 = 把删除那次**按原路退回**,而且按组走:当年是按组一起搬进来的(父 + 它在盘上的孩子),
|
|
6
6
|
* 只回父就会在回收站里留下一堆点不到的孩子行——清单只列父行,那些行既看不见也清不掉。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 三段与删除对称(成员循环与整组回滚的实现住 `verbs/move-group.ts`):
|
|
9
9
|
* ① **预检整组**(一个字节都不写):每个成员的回收站目录与项目目录都得在。缺席的判据与原来一致
|
|
10
10
|
* (回收站目录不在 = `trash-empty`;原项目目录不在 = `project-missing`,且绝不静默建目录)。
|
|
11
11
|
* 预检在写之前,是为了不让"父已经回到 `sessions/`、孩子因为项目目录没了而回不去"这种半组留在外面。
|
|
@@ -15,13 +15,15 @@ import type { CoreDeps } from '../deps.js';
|
|
|
15
15
|
* ③ 中途失败时 journal 把这个成员自己退回回收站;**已经恢复完的**兄弟由整组 catch 按相反顺序一起退回。
|
|
16
16
|
*
|
|
17
17
|
* **通告不在这里**:一次批量只该拉一遍宿主会话表、只发一次 `api-session/added`(2026-10-17 那次
|
|
18
|
-
* "恢复很慢"就是逐条发)。通告归 `verbs/batch.ts`(ADR-0002),所以这个动词的 interface
|
|
19
|
-
* "一个条目 id
|
|
18
|
+
* "恢复很慢"就是逐条发)。通告归 `verbs/batch.ts`(ADR-0002),所以这个动词的 interface 是
|
|
19
|
+
* "一个条目 id + 这一组进、回收站那一行出",没有批量专用的开关。
|
|
20
20
|
*/
|
|
21
21
|
export declare function restoreSession(deps: CoreDeps): (entryId: string,
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
|
|
22
|
+
/** 这一组(只读):批量层由 `batch-carried.ts` 的 `claimOf` 给出,直调者按 `trash/groups.ts`
|
|
23
|
+
* 的 `groupOf` 自己算。ghost 父(父行已摘、只剩孩子)的组只有孩子、不含 `entryId`,这里照旧
|
|
24
|
+
* 报 `trash-empty`;其余调用按这一组整体恢复。动词**不收缩**它——收缩与同批已带走集都归
|
|
25
|
+
* `verbs/batch-carried.ts`,组语义归 `trash/groups.ts`。 */
|
|
26
|
+
group: readonly TrashEntry[]) => Promise<TrashEntry>;
|
|
25
27
|
/**
|
|
26
28
|
* 这一次恢复**到底挂不挂账本**:交出能挂的那个 `workspaceId`,挂不了就 `undefined`。
|
|
27
29
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xrn1997/dsh-session-delete",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "在 DeepSeek Harness 里删除会话:删除=移入回收站(可恢复),另设显式彻底删除",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
"dshReleases": {
|
|
74
74
|
"0.2.0-rc.2": "compatible",
|
|
75
75
|
"0.2.1-rc.1": "compatible",
|
|
76
|
-
"0.2.1-rc.2": "compatible"
|
|
76
|
+
"0.2.1-rc.2": "compatible",
|
|
77
|
+
"0.2.1-rc.3": "compatible"
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
},
|
|
@@ -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;
|