@xrn1997/dsh-session-delete 0.2.2 → 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 +10 -28
- package/lib/client.js +2012 -1117
- package/lib/index.js +1271 -893
- package/lib/types/api/dispatch.d.ts +2 -2
- package/lib/types/api/route-table.d.ts +1 -1
- package/lib/types/api/trash-rows.d.ts +13 -6
- package/lib/types/client/batch-bar.d.ts +7 -6
- package/lib/types/client/batch-confirm.d.ts +6 -4
- package/lib/types/client/batch-menu-item.d.ts +2 -2
- package/lib/types/client/batch-select.d.ts +7 -50
- package/lib/types/client/batch-selection-store.d.ts +30 -0
- package/lib/types/client/busy-gate.d.ts +6 -0
- package/lib/types/client/copy.d.ts +4 -1
- package/lib/types/client/delete-confirm.d.ts +2 -0
- package/lib/types/client/host-rows.d.ts +50 -0
- package/lib/types/client/main-view.d.ts +42 -0
- package/lib/types/client/orphan-maintenance.d.ts +27 -0
- package/lib/types/client/receipt.d.ts +73 -0
- package/lib/types/client/remote.d.ts +25 -19
- package/lib/types/client/store.d.ts +48 -0
- package/lib/types/client/trash-nav.d.ts +30 -0
- package/lib/types/client/trash-panel.d.ts +1 -1
- package/lib/types/client/trash-store.d.ts +2 -0
- package/lib/types/client/ui/icons.d.ts +3 -3
- package/lib/types/client/undo-toast.d.ts +30 -6
- package/lib/types/client/use-column-resize.d.ts +2 -2
- package/lib/types/client/use-panel-width.d.ts +1 -1
- package/lib/types/client/use-trash-panel-controller.d.ts +42 -0
- 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 +14 -0
- package/lib/types/index.d.ts +7 -13
- package/lib/types/log/zstd-frames.d.ts +1 -1
- package/lib/types/ports/agent-handles.d.ts +25 -0
- package/lib/types/ports/home.d.ts +19 -0
- package/lib/types/ports/host-port.d.ts +60 -44
- package/lib/types/remote/map-error.d.ts +2 -2
- package/lib/types/shared/wire.d.ts +18 -9
- package/lib/types/trash/detail.d.ts +6 -3
- package/lib/types/trash/entry.d.ts +4 -4
- 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 +4 -2
- package/lib/types/trash/reconcile.d.ts +6 -5
- package/lib/types/trash/storage-manifest.d.ts +12 -0
- package/lib/types/verbs/batch-carried.d.ts +20 -0
- package/lib/types/verbs/batch.d.ts +1 -1
- package/lib/types/verbs/delete-session.d.ts +11 -13
- package/lib/types/verbs/errors.d.ts +2 -6
- package/lib/types/verbs/live-admission.d.ts +13 -0
- package/lib/types/verbs/member-journal.d.ts +26 -6
- package/lib/types/verbs/member-presence.d.ts +67 -0
- package/lib/types/verbs/move-group.d.ts +20 -0
- package/lib/types/verbs/orphan-subagents.d.ts +21 -40
- package/lib/types/verbs/permanent-removal.d.ts +114 -0
- package/lib/types/verbs/purge-trash.d.ts +21 -9
- package/lib/types/verbs/restore-session.d.ts +14 -12
- package/package.json +3 -2
|
@@ -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
|
-
|
|
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,54 +27,63 @@ export interface HostPort {
|
|
|
20
27
|
titleOf(sessionId: string): Promise<string>;
|
|
21
28
|
isLive(sessionId: string): Promise<boolean>;
|
|
22
29
|
isArchived(sessionId: string): Promise<boolean>;
|
|
23
|
-
/** 尽力把会话从宿主**内存里的 live store** 摘掉;三态见 {@link EvictionOutcome}。
|
|
24
|
-
* **这是本仓走宿主内部入口的两处之一**(另一处是 {@link HostPort.announceRemoved} 发的转发事件,
|
|
25
|
-
* 理由与代价见实现处的注释):删除的两半里,"磁盘上的持久化"我们搬得走,
|
|
26
|
-
* "内存里 attach 着的活体"只有这一条路能请掉。 */
|
|
27
|
-
evictLive(sessionId: string): Promise<EvictionOutcome>;
|
|
28
|
-
/** **如实通告**客户端"这个会话没了":发宿主自己声明并转发的 `api-session/removed`,客户端当场删行。
|
|
29
|
-
* 冷会话没有活体可摘,但同样需要这一条——否则那一行要等到重连(`handleConnected` → 重列)才消失。 */
|
|
30
|
-
announceRemoved(sessionId: string): Promise<void>;
|
|
31
|
-
/** **如实通告**客户端"这个会话回来了":摘要取自宿主自己的会话列表,发 `api-session/added`。
|
|
32
|
-
* 正常恢复走 {@link HostPort.announceRestoredBatch}(整批一次预取);这一条单条路是**删除组回滚**
|
|
33
|
-
* 用的(成员已经收过"已移除"、退回原处后补发)。取不到摘要就不发。 */
|
|
34
|
-
announceRestored(sessionId: string): Promise<void>;
|
|
35
|
-
/** 宿主会话表的一次预取:`[{ sessionId, summary }]`,`summary` 是宿主**原样**那一条。
|
|
36
|
-
* 给 {@link announceRestoredBatch} 当 `known` 用——整批只拉一次表。服务没挂回空表,不抛。 */
|
|
37
|
-
restoredSummariesOf(): Promise<readonly {
|
|
38
|
-
readonly sessionId: string;
|
|
39
|
-
readonly summary: unknown;
|
|
40
|
-
}[]>;
|
|
41
|
-
/** 一组会话的通告(2026-10-17 批量重构):`known` 是调用方**整批预取**的摘要表——给了就一次都不
|
|
42
|
-
* 再拉表(每条一次 `controller.list()` 在 100 条的批量里就是 100 遍全表扫,那是那次"恢复很慢"的
|
|
43
|
-
* 头号来源);没给则自己拉一次。缺席 id 不发,不影响已完成的恢复。 */
|
|
44
|
-
announceRestoredBatch(ids: readonly string[], known?: readonly {
|
|
45
|
-
readonly sessionId: string;
|
|
46
|
-
readonly summary: unknown;
|
|
47
|
-
}[]): Promise<void>;
|
|
48
30
|
unarchive(sessionId: string): Promise<void>;
|
|
49
31
|
detach(sessionId: string, workspaceId: string): Promise<void>;
|
|
50
32
|
attach(sessionId: string, workspaceId: string): Promise<void>;
|
|
51
33
|
/** 归档是 registry **全局**的,与工作区账目无关 ⇒ `workspaceId` 只是调用点对称用的可选入参
|
|
52
34
|
* (未分组的会话没有工作区可传)。 */
|
|
53
35
|
archive(sessionId: string, workspaceId?: string): Promise<void>;
|
|
54
|
-
/** 整个会话语料(live ∪ persisted)的只读身份表。**服务缺席回 `undefined`**,不拿空数组冒充
|
|
55
|
-
* “这个家没有会话”;扫描方必须把缺席当失败。 */
|
|
56
|
-
listSessions(): Promise<readonly SessionIdentity[] | undefined>;
|
|
57
|
-
/** 一次 BFS 点出多个父的全部后代;键按调用方传进来的写法回。
|
|
58
|
-
* `sessionQuery` 缺席时回 `{}`(与 delete 的降级一致)——写侧不能把这当成"盘上没有后代",
|
|
59
|
-
* 只能当成"这一趟看不见",残留留给 `GET /orphans` 兜底。 */
|
|
60
|
-
subagentMembersBatch(parentIds: readonly string[]): Promise<Record<string, readonly SubagentMember[]>>;
|
|
61
|
-
/** 数一组父会话在盘上的**后代**条数(孩子与孙代都算——回收站清单上那个只读计数,宿主客户端把
|
|
62
|
-
* `origin:"subagent"` 的行藏掉了 ⇒ 不说就没人知道删了父还剩多少)。键按**传进来的那个写法**回;
|
|
63
|
-
* 没有后代的不表现在结果里(缺省即 0);`sessionQuery` 没挂回 `{}`,不抛。 */
|
|
64
|
-
subagentCounts(parentIds: readonly string[]): Promise<Record<string, number>>;
|
|
65
|
-
/** 列出这个父会话**在盘上的全部后代**(删除时一起带走)。来历与缺席行为见实现处:服务没挂回 `[]`。 */
|
|
66
|
-
subagentMembers(sessionId: string): Promise<readonly {
|
|
67
|
-
readonly sessionId: string;
|
|
68
|
-
readonly projectDir: string;
|
|
69
|
-
}[]>;
|
|
70
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* 角色二:**在场通告**——摘掉宿主内存里的活体,并如实通告客户端"这个会话没了 / 回来了"。
|
|
39
|
+
*
|
|
40
|
+
* 这一组的方法按契约**不抛**(`evictLive` 三态、`announce*` 内部吞异常):它们是删除/恢复的收尾,
|
|
41
|
+
* 收尾失败不把已经完成的那件事翻成失败。
|
|
42
|
+
*/
|
|
43
|
+
export interface HostPresencePort {
|
|
44
|
+
/** 尽力把会话从宿主**内存里的 live store** 摘掉;三态见 {@link EvictionOutcome}。
|
|
45
|
+
* **这是本仓走宿主内部入口的几处之一**(另见 `src/ports/agent-handles.ts` 对 `agents.resume/create` 的包装,
|
|
46
|
+
* 以及 {@link HostPresencePort.announceRemoved} 发的转发事件):删除的两半里,"磁盘上的持久化"我们搬得走,
|
|
47
|
+
* "内存里 attach 着的活体"只有这一条路能请掉。 */
|
|
48
|
+
evictLive(sessionId: string): Promise<EvictionOutcome>;
|
|
49
|
+
/** **如实通告**客户端"这个会话没了":发宿主自己声明并转发的 `api-session/removed`,客户端当场删行。
|
|
50
|
+
* 冷会话没有活体可摘,但同样需要这一条——否则那一行要等到重连(`handleConnected` → 重列)才消失。 */
|
|
51
|
+
announceRemoved(sessionId: string): Promise<void>;
|
|
52
|
+
/** 一组会话的「回来了」通告:implementation 拉一次宿主会话表,按 id 逐条发 added。
|
|
53
|
+
* 单条就是长度为 1 的批;批量只调一次。缺席 id 不发,不影响已完成的恢复。 */
|
|
54
|
+
announceRestored(sessionIds: readonly string[]): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 一次语料读的结果:**缺席显式化**——`available: false` 说的是"这一趟看不见语料"(`sessionQuery` 没挂,
|
|
58
|
+
* 或它的读面方法缺席 / 不是函数),与"读到了、里面是空的"是两件事,调用方对两者的策略不同(前者只许
|
|
59
|
+
* 降级或响亮失败,后者是可以行动的事实)。**不带原因串**:调用点的策略已经决定了怎么处理,带一个字
|
|
60
|
+
* 就会有人去断言那个字;将来真要区分"没挂"与"方法缺席",再加一个显式字段。
|
|
61
|
+
*/
|
|
62
|
+
export type CorpusRead<T> = {
|
|
63
|
+
readonly available: true;
|
|
64
|
+
readonly value: T;
|
|
65
|
+
} | {
|
|
66
|
+
readonly available: false;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* 角色三:**会话语料**——整表身份与子会话闭包。
|
|
70
|
+
*
|
|
71
|
+
* 两个读面都把"这一趟看不见语料"写成显式的 {@link CorpusRead}:`{ available: false }` 与
|
|
72
|
+
* "看见了、里面是空的"是两件事。**缺席之后怎么办归调用方,清单见 `CONTEXT.md` 的「语料缺席」**。
|
|
73
|
+
*/
|
|
74
|
+
export interface HostCorpusPort {
|
|
75
|
+
/** 整个会话语料(live ∪ persisted)的只读身份表。**服务缺席是显式的**:`sessionQuery` 没挂,或它的
|
|
76
|
+
* `listSessions` 缺席 / 不是函数 ⇒ `{ available: false }`,不拿空数组冒充“这个家没有会话”。 */
|
|
77
|
+
listSessions(): Promise<CorpusRead<readonly SessionIdentity[]>>;
|
|
78
|
+
/** 一次 BFS 点出多个父的全部后代(传递闭包)。
|
|
79
|
+
* **缺席是显式的,且优先于空入参**:`sessionQuery` 没挂,或它的 `filterSessions` 缺席 / 不是函数 ⇒
|
|
80
|
+
* `{ available: false }`,不拿空表冒充"盘上没有后代";入参是不是空的都不改变这一条。
|
|
81
|
+
* 服务在场时,空入参 ⇒ `{ available: true, value: {} }`,一次宿主调用都不发。
|
|
82
|
+
* 键按调用方传进来的写法回(同一个父的 `session-x` / `x` 两种写法是同一个根,见 `membersFor`)。 */
|
|
83
|
+
subagentMembersOf(parentIds: readonly string[]): Promise<CorpusRead<Record<string, readonly SubagentMember[]>>>;
|
|
84
|
+
}
|
|
85
|
+
/** 复合别名:adapter(`createHostPort`)与调用方把这 15 个方法当一个端口用;三个角色是它的切面。 */
|
|
86
|
+
export type HostPort = HostIdentityPort & HostPresencePort & HostCorpusPort;
|
|
71
87
|
export interface SessionIdentity {
|
|
72
88
|
readonly sessionId: string;
|
|
73
89
|
readonly parentSessionId?: string;
|
|
@@ -102,7 +118,7 @@ export declare const NO_CWD_PROJECT_DIR = "_no-cwd";
|
|
|
102
118
|
* (手工放进回收站的目录,从日志 header 的 `cwd` 反推它原本属于哪个项目目录)——算法只有这一处家。
|
|
103
119
|
*/
|
|
104
120
|
export declare function projectKey(cwd: string): string;
|
|
105
|
-
/** 会话 id 去掉 `session-` 前缀后的形状(同一个父在两代里写法不同,见 {@link
|
|
121
|
+
/** 会话 id 去掉 `session-` 前缀后的形状(同一个父在两代里写法不同,见 {@link HostCorpusPort.subagentMembersOf})。 */
|
|
106
122
|
export declare function bareSessionId(id: string): string;
|
|
107
123
|
/** 这次失败是不是"账本里没那个工作区"(而不是另有故障)。恢复侧靠它把两种失败分开:
|
|
108
124
|
* 前者按「未分组」收,后者照旧回滚报 `io`——不给真故障静默改归属的机会。 */
|
|
@@ -112,6 +128,6 @@ export declare function isWorkspaceMissingError(error: unknown): boolean;
|
|
|
112
128
|
*
|
|
113
129
|
* 每个方法的依据(服务 / 方法 / 入参)与原文切片见各自的实现处注释。
|
|
114
130
|
* 出错一律**原样抛**(不在这里裹动词码):动词层把原始错误挂在 `cause` 上,
|
|
115
|
-
* `toRemoteFailure` 再按 `name` 把它映射回 `sessiondelete
|
|
131
|
+
* `toRemoteFailure` 再按 `name` 把它映射回 `sessiondelete/<码>`(src/remote/map-error.ts 的两行映射)。
|
|
116
132
|
*/
|
|
117
133
|
export declare function createHostPort(ctx: unknown): HostPort;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RemoteFailure } from '../shared/wire.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 把任意抛出物折成失败信封里的**那三件**(本文件唯一那张映射表)。
|
|
4
4
|
*
|
|
5
5
|
* - 已经是远程失败(`isDSHRemoteError` 认得的结构标记)⇒ 原样带出它自己的 `code` / `message`,
|
|
6
6
|
* 挂着的 `details` 一起过桥,不再包一层。
|
|
@@ -8,7 +8,7 @@ import { type RemoteFailure } from '../shared/wire.js';
|
|
|
8
8
|
* 把它们折成 `sessiondelete/io` 就是把宿主的判断抹成我们的,客户端拿到的 code 从此不能说真话。
|
|
9
9
|
* - 宿主类型化错误(自身或 `cause` 链上,见 {@link hostErrorOf})⇒ 映射成 `live` / `not-found`,
|
|
10
10
|
* 并把它自己身上的结构化字段带成 `details`。这一条排在动词码之前:动词会把宿主错误包成笼统的 `io`
|
|
11
|
-
* 并把原错误挂在 cause 上(`verbError('io', …, cause)`),不看进 cause
|
|
11
|
+
* 并把原错误挂在 cause 上(`verbError('io', …, cause)`),不看进 cause 链,本文件那两行映射永远到不了客户端。
|
|
12
12
|
* - 动词的 `sessiondelete/<五个码>` ⇒ 原样过桥(`details` 同样带着)。
|
|
13
13
|
* - 其余(别域的怪 code、前缀外的因、非 Error 抛出物)⇒ `io`,message 保留原文。
|
|
14
14
|
*
|
|
@@ -26,7 +26,7 @@ export declare const SESSION_DELETE_API_PREFIX = "/dsh-session-delete-api";
|
|
|
26
26
|
* 一次 HTTP 带一组 id、逐条回结果;单条就是长度为 1 的批量,仓里不再留第二条执行路径。
|
|
27
27
|
* - `GET /list`:**对账并读**清单(`session-trash/` 里的目录是准)。
|
|
28
28
|
* - `GET /ping`:最便宜的在/不在(不碰清册、不碰盘、不进链)——客户端探测用它,不用一次全表读。
|
|
29
|
-
* - `POST /detail`:单条详情(用 POST
|
|
29
|
+
* - `POST /detail`:单条详情(用 POST 表达一个读:判据就写在 `src/api/route-table.ts` 那一行的上方)。
|
|
30
30
|
* - `POST /apply`:批量执行一个动作(`delete` / `restore` / `purge` / `count` / `orphan-clean`),逐条回 `ApplyOutcome`。
|
|
31
31
|
* - `POST /empty`:清空回收站(聚合回执,**不**按条回)。
|
|
32
32
|
* - `GET /orphans`:无主子会话的扫描报告(纯读,2026-10-19)。**清理不在这里**——它是 `/apply` 的
|
|
@@ -95,13 +95,15 @@ export interface TrashEntryWire {
|
|
|
95
95
|
* 也不许拿缺席冒充否。
|
|
96
96
|
*/
|
|
97
97
|
export interface TrashRowWire extends TrashEntryWire {
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
/** **这一行「彻底删除」会释放多少**:这一组在盘上**实测**的字节合计(回收站里本行与孩子的目录 +
|
|
99
|
+
* 盘上还留着的后代),与 purge 的 `freedBytes` 同一口径。算式只住 `verbs/permanent-removal.ts` 的
|
|
100
|
+
* `planVisibleTrash`,Node 半算一次过线、投影原样搬运;界面上头部合计与单条确认框共用这一个数——
|
|
101
|
+
* 不许各算一遍(各算一遍就是两套口径,且客户端那套会重复计数)。 */
|
|
101
102
|
releaseBytes: number;
|
|
102
103
|
/** 跟着这条父会话**一起进了回收站**的孩子数(清册里 `parentEntryId` 指向它的行)。 */
|
|
103
104
|
subagentCount?: number;
|
|
104
|
-
/**
|
|
105
|
+
/** 那些孩子**回收站目录的实测字节**合计(`planVisibleTrash` 在盘上现量,与 `releaseBytes` 同一条口径;
|
|
106
|
+
* 不念清册里记的 `sizeBytes`。它们已折进 `releaseBytes`——头部合计照念那个数,别再单独相加)。 */
|
|
105
107
|
subagentBytes?: number;
|
|
106
108
|
/** **还留在盘上、没被带走**的孩子数(宿主 `sessionQuery`)。与 `subagentCount` 各说各的。 */
|
|
107
109
|
strandedSubagents?: number;
|
|
@@ -120,7 +122,7 @@ export interface TrashDetailMessageWire {
|
|
|
120
122
|
/** 回收站单条详情的线上形状(`POST /detail`)。 */
|
|
121
123
|
export interface TrashDetailWire {
|
|
122
124
|
entryId: string;
|
|
123
|
-
/** 日志 header 里的原串(**无损**,比从 `projectKey`
|
|
125
|
+
/** 日志 header 里的原串(**无损**,比从 `projectKey` 反推那一路强;组头怎么念项目名见 `src/client/trash-view.ts` 的 `projectLabel`,那是另一回事)。 */
|
|
124
126
|
cwd: string;
|
|
125
127
|
createdAt: number;
|
|
126
128
|
lastActivity: number;
|
|
@@ -167,7 +169,7 @@ export declare const PARAMS: {
|
|
|
167
169
|
readonly ids: "ids";
|
|
168
170
|
};
|
|
169
171
|
/**
|
|
170
|
-
*
|
|
172
|
+
* 失败信封的**线上形状**(本文件):`code` 是判别依据、`message` 是上屏原文、
|
|
171
173
|
* `details` 是**可选的结构化附带物**——对齐官方 `rpcErrorSchema` 那三件
|
|
172
174
|
* (`dsh-client-connection` 原文:`z.object({ code, message, details: z.record(z.string(), z.unknown()) })`)。
|
|
173
175
|
* 来历两类:宿主类型化错误自己身上的字段(`WorkspaceActiveSessionError` 的 `sessionId` / `activity`…),
|
|
@@ -196,9 +198,9 @@ export type RemoteResult<T> = {
|
|
|
196
198
|
* 浏览器半是 `src/client/remote.ts` 的 `throwRemoteFailure`。
|
|
197
199
|
*/
|
|
198
200
|
export declare function isDSHRemoteError(value: unknown): value is RemoteFailure;
|
|
199
|
-
/** 失败码的域。与宿主官方域(`workspace/*`、`session
|
|
201
|
+
/** 失败码的域。与宿主官方域(`workspace/*`、`session/*`…)不重名,见本文件。 */
|
|
200
202
|
export declare const SESSION_DELETE_ERROR_DOMAIN = "sessiondelete";
|
|
201
|
-
/**
|
|
203
|
+
/** 五个**动词码**(本文件那张表)。`io` 是唯一兜底。 */
|
|
202
204
|
export declare const VERB_CODES: readonly ["live", "not-found", "trash-empty", "project-missing", "io"];
|
|
203
205
|
export type VerbCode = (typeof VERB_CODES)[number];
|
|
204
206
|
/** 三个**路由自检码**:请求本身不对,不是一次操作的结果(它们带 HTTP 状态,见 `api/dispatch.ts`)。 */
|
|
@@ -210,6 +212,13 @@ export type RouteCode = (typeof ROUTE_CODES)[number];
|
|
|
210
212
|
*/
|
|
211
213
|
export type AllCodeSuffix = VerbCode | RouteCode;
|
|
212
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;
|
|
213
222
|
/** 回收站目录名(相对宿主数据根)。Node 半拼路径,浏览器半在页脚念它——同一个字面量。 */
|
|
214
223
|
export declare const TRASH_DIR_NAME = "session-trash";
|
|
215
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
|
-
|
|
14
|
-
|
|
13
|
+
/** 详情自己直接要的三件(判目录在不在、挑日志、读日志)。对账不在这条路上,所以不需要 `listDirs`。 */
|
|
14
|
+
fs: Pick<FsPort, 'exists' | 'listFiles' | 'readFile'>;
|
|
15
|
+
/** 详情只读清册的 **raw 面**:`/detail` 声明为 pure,读一次不能顺手对账——对账会写清册行,那是写动作,
|
|
16
|
+
* 必须排在到达序链上。代价是有意的:只有目录、清册还没这一行的手工搬入条目读不到(`/list` 对账后可见)。 */
|
|
17
|
+
ledger: Pick<TrashLedger, 'raw'>;
|
|
15
18
|
home: string;
|
|
16
19
|
};
|
|
17
20
|
/** 两个载荷闸(可调是为把策略单独验穿;生产走的就是这两个默认值,用例里钉着)。 */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** 回收站的一条记录。`id` 由 `makeEntryId` 生成(`<删除时刻>-<原目录名>`)——**它不是随手起的键**:
|
|
2
|
-
*
|
|
2
|
+
* 回收站目录名就是它,恢复也按它反推出原目录名放回(CONTEXT.md 的「条目」条),所以 id 的形状有契约意义。
|
|
3
3
|
* 其余字段在删除那一刻就地取。 */
|
|
4
4
|
export interface TrashEntry {
|
|
5
5
|
id: string;
|
|
@@ -8,8 +8,8 @@ export interface TrashEntry {
|
|
|
8
8
|
/** 删除时它所属的工作区;**空串 = 删除时就没有账目**(「未分组」的会话,宿主的"删除工作区"会造出这种),
|
|
9
9
|
* 恢复时据此跳过 `attach`。 */
|
|
10
10
|
workspaceId: string;
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
11
|
+
/** 删除**之前**的归档态,**只作事实记录**:2026-10-21 起删除不归档、恢复也不按它还原可见性。
|
|
12
|
+
* 旧清单迁移只用它区分"删除时是我们归档的"(false 且当前仍归档)与"用户删前自己归档的"(true,保持归档)。 */
|
|
13
13
|
wasArchived: boolean;
|
|
14
14
|
deletedAt: number;
|
|
15
15
|
/** 删除时**尽力**取的会话标题(来源:宿主 projection cache 服务;底层是
|
|
@@ -21,7 +21,7 @@ export interface TrashEntry {
|
|
|
21
21
|
* 缺席 = 这一行是父(或它自己就是被单独删掉的)。清单据此把孩子折进父行、不单独列。
|
|
22
22
|
* 老代留底 / 清册被清后由目录重建出来的行**没有这个字段**(文件侧没有它的副本,与
|
|
23
23
|
* `workspaceId`/`wasArchived`/`title` 同一类降级),那一趟它们会各占一行——那是"分组信息随清册一起没了"
|
|
24
|
-
*
|
|
24
|
+
* 的如实结果,不假装知道(CONTEXT.md 的「无主子会话」条)。 */
|
|
25
25
|
parentEntryId?: string;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
@@ -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>;
|
|
@@ -8,7 +9,8 @@ export interface ManifestPort {
|
|
|
8
9
|
export declare function createMemoryManifest(seed?: TrashEntry[]): ManifestPort;
|
|
9
10
|
/** 宿主 storage domain 的最小面。这是**接缝**,不是对宿主 API 的断言:
|
|
10
11
|
* 探针只枚举到 `ctx.storageDomain` 有 `open` / `get` / `domains` / `reserved` 等成员,
|
|
11
|
-
* **没有**枚举 `open()` 返回的 domain 句柄面 ⇒ 真实绑定由 `
|
|
12
|
+
* **没有**枚举 `open()` 返回的 domain 句柄面 ⇒ 真实绑定由 `src/trash/storage-manifest.ts` 的
|
|
13
|
+
* `createHostStorageManifest` 写一层薄适配
|
|
12
14
|
* (官方读面若只有迭代器/快照,就把迭代器折成 `readAll()`,不在动词层引入第二种读法)。 */
|
|
13
15
|
export interface StorageDomainLike {
|
|
14
16
|
readAll(): Promise<TrashEntry[]>;
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 回收站清单的**对账**:以 `session-trash/`
|
|
2
|
+
* 回收站清单的**对账**:以 `session-trash/` 里的条目目录为准,把清单修回一致(CONTEXT.md 的「对账」条)。
|
|
3
3
|
*
|
|
4
4
|
* 手工动回收站**只需要动那一个目录**——多出来的目录在这里被重建出行、目录没了的行在这里被摘掉;
|
|
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、
|
|
14
15
|
* `projectDir` 由 `cwd` 经宿主同名算法 `projectKey` 反推、`deletedAt` 从目录名的时间戳解、
|
|
15
16
|
* `sizeBytes` 现场量。**文件侧没有副本的三件**按缺省落:`workspaceId: ''`(恢复时跳过挂账本,
|
|
16
|
-
* 落在「未分组」)、`wasArchived: false
|
|
17
|
+
* 落在「未分组」)、`wasArchived: false`(文件侧没有归档事实;旧条目恢复走迁移支)、`title: ''`(详情有日志标题兜底)
|
|
17
18
|
* ——这是重建的固有代价,不是可以"再读读看"的东西。
|
|
18
19
|
* ④ **认不出 / 读不出的目录:不动、不列**(名字不像条目、目录里没有日志、header 读不出、尺寸量不出,
|
|
19
20
|
* 各跳过各的)。它是"外人放进来的东西",不是用户动作失败:跳过即静默,绝不为它删目录或抛掉整次读数。
|
|
@@ -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;
|
|
@@ -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,3 +1,3 @@
|
|
|
1
1
|
import type { ApplyAction, ApplyOutcome } from '../shared/wire.js';
|
|
2
|
-
import type { DeleteDeps } from '
|
|
2
|
+
import type { DeleteDeps } from '../deps.js';
|
|
3
3
|
export declare function applyActions(deps: DeleteDeps, action: ApplyAction, ids: readonly string[]): Promise<ApplyOutcome[]>;
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import { type TrashEntry } from '../trash/entry.js';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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"` 的孩子
|
|
14
10
|
* 整排藏了 ⇒ 那些目录既看不见也继续占着字节(本机实测 41 条父留了 320 个目录 / 74.6 MB)。
|
|
15
11
|
* 所以这里把父在盘上的孩子一起搬进来,各自成目录、各自一行,清册用 `parentEntryId` 记它是谁的孩子。
|
|
16
12
|
*
|
|
17
|
-
*
|
|
13
|
+
* 三段,顺序承重(成员循环与整组回滚的实现住 `verbs/move-group.ts`):
|
|
18
14
|
* ① **先点组、先验活体**(一个字节都不写)。任一成员在跑就整组拒绝——不替用户终止正在跑的活,
|
|
19
15
|
* 也绝不允许"父已经搬进回收站、孩子因为在跑而搬不动"这种半套。
|
|
20
16
|
* ② **逐个搬**:每个成员是一份 `journal`(`verbs/member-journal.ts`),搬不动时 journal 自己把
|
|
21
17
|
* 这个成员已经做成的步骤退回去。
|
|
22
18
|
* ③ **组级回滚**:某一个成员搬不动时,把**已经搬进去的**那些按相反顺序退回原处(清册行跟着摘、
|
|
23
|
-
*
|
|
19
|
+
* 账本跟着挂),失败照抛——不留"半组在回收站、半组在原地"的世界。
|
|
24
20
|
*
|
|
25
|
-
* interface 只有**一个 id
|
|
21
|
+
* interface 只有**一个 id**:批量的性质不在这里(同批已带走集归 `verbs/batch-carried.ts`,
|
|
22
|
+
* 组语义归 `trash/groups.ts`)。
|
|
23
|
+
* 结果带回这次真的搬走的整组(`movedSessionIds`)——它是动词交回的内部数据,不再靠回调借用动词的手。
|
|
26
24
|
*/
|
|
27
|
-
export declare function deleteSession(deps: DeleteDeps): (sessionId: string) => Promise<
|
|
25
|
+
export declare function deleteSession(deps: DeleteDeps): (sessionId: string) => Promise<DeleteResult>;
|
|
@@ -1,10 +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
4
|
/** 一次失败的**结构**:码、上屏原文、可选 cause(`map-error` 沿 cause 链认宿主类型化错误)、
|
|
9
5
|
* 可选的结构化附带物(对齐官方 `rpcErrorSchema` 的第三件 `details`)。 */
|
|
10
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 {
|
|
2
|
-
/**
|
|
17
|
+
/** 稳定的领域键,给 `restored(key)` 用。 */
|
|
18
|
+
readonly key: JournalStepKey;
|
|
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,15 +29,18 @@ 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
|
-
* `details.rollbackFailures
|
|
24
|
-
*
|
|
42
|
+
* `details.rollbackFailures`,绝不让原错误独走(2026-10-19 `f2a7757`;那些失败句子的合并与措辞住
|
|
43
|
+
* `src/verbs/errors.ts`,这里只是唯一的调用方)。删除与恢复两个按组走的动词共用这一处,
|
|
44
|
+
* "相反顺序 + 失败合并"不再各写一遍。
|
|
25
45
|
*/
|
|
26
46
|
export declare function rollbackJournals(journals: readonly Journal[], error: unknown): Promise<never>;
|