@xrn1997/dsh-session-delete 0.3.1 → 0.5.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 (83) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +141 -129
  3. package/cordis.patch.yml +3 -3
  4. package/lib/client.js +2811 -1264
  5. package/lib/index.js +2368 -1277
  6. package/lib/types/api/attachment-orphans.d.ts +11 -0
  7. package/lib/types/api/attachment.d.ts +23 -0
  8. package/lib/types/api/dispatch.d.ts +13 -29
  9. package/lib/types/api/request-trust.d.ts +45 -0
  10. package/lib/types/api/route-table.d.ts +52 -18
  11. package/lib/types/api/trash-rows.d.ts +3 -3
  12. package/lib/types/client/attachment-view.d.ts +27 -0
  13. package/lib/types/client/batch-bar.d.ts +1 -1
  14. package/lib/types/client/batch-confirm.d.ts +2 -2
  15. package/lib/types/client/batch-menu-item.d.ts +1 -1
  16. package/lib/types/client/batch-select.d.ts +1 -1
  17. package/lib/types/client/busy-gate.d.ts +6 -2
  18. package/lib/types/client/confirm-close.d.ts +14 -0
  19. package/lib/types/client/context.d.ts +3 -2
  20. package/lib/types/client/copy.d.ts +102 -42
  21. package/lib/types/client/delete-confirm.d.ts +1 -3
  22. package/lib/types/client/delete-menu-item.d.ts +1 -1
  23. package/lib/types/client/host-rows.d.ts +5 -1
  24. package/lib/types/client/main-view.d.ts +1 -1
  25. package/lib/types/client/maintenance-parts.d.ts +25 -0
  26. package/lib/types/client/maintenance-settings.d.ts +22 -0
  27. package/lib/types/client/markdown.d.ts +9 -2
  28. package/lib/types/client/orphan-maintenance.d.ts +16 -13
  29. package/lib/types/client/receipt.d.ts +44 -2
  30. package/lib/types/client/remote.d.ts +38 -50
  31. package/lib/types/client/store.d.ts +14 -3
  32. package/lib/types/client/trash-confirm.d.ts +2 -15
  33. package/lib/types/client/trash-detail.d.ts +48 -6
  34. package/lib/types/client/trash-entry.d.ts +1 -1
  35. package/lib/types/client/trash-nav.d.ts +12 -8
  36. package/lib/types/client/trash-panel.d.ts +4 -3
  37. package/lib/types/client/ui/Checkbox.d.ts +1 -1
  38. package/lib/types/client/ui/Modal.d.ts +2 -2
  39. package/lib/types/client/ui/Toast.d.ts +3 -1
  40. package/lib/types/client/ui/icons.d.ts +7 -12
  41. package/lib/types/client/ui/top-layer.d.ts +10 -0
  42. package/lib/types/client/undo-notices.d.ts +52 -0
  43. package/lib/types/client/undo-toast.d.ts +6 -59
  44. package/lib/types/client/use-column-resize.d.ts +9 -20
  45. package/lib/types/client/use-maintenance-section.d.ts +52 -0
  46. package/lib/types/client/use-maintenance.d.ts +17 -0
  47. package/lib/types/client/use-panel-width.d.ts +1 -1
  48. package/lib/types/client/use-trash-panel-controller.d.ts +54 -16
  49. package/lib/types/client/use-trash-query.d.ts +39 -34
  50. package/lib/types/client/use-trash-selection.d.ts +15 -12
  51. package/lib/types/client/write-seats.d.ts +36 -28
  52. package/lib/types/client/write-settlement.d.ts +11 -17
  53. package/lib/types/client/write.d.ts +21 -8
  54. package/lib/types/deps.d.ts +18 -7
  55. package/lib/types/domain/closure.d.ts +44 -0
  56. package/lib/types/index.d.ts +6 -2
  57. package/lib/types/log/session-log.d.ts +4 -3
  58. package/lib/types/log/zstd-frames.d.ts +13 -8
  59. package/lib/types/ports/agent-handles.d.ts +1 -1
  60. package/lib/types/ports/connection-port.d.ts +33 -0
  61. package/lib/types/ports/fs-port.d.ts +24 -9
  62. package/lib/types/ports/host-port.d.ts +21 -50
  63. package/lib/types/ports/presence-port.d.ts +31 -0
  64. package/lib/types/shared/wire.d.ts +175 -38
  65. package/lib/types/trash/attachments.d.ts +92 -0
  66. package/lib/types/trash/detail.d.ts +46 -16
  67. package/lib/types/trash/entry.d.ts +10 -4
  68. package/lib/types/trash/groups.d.ts +9 -11
  69. package/lib/types/trash/manifest.d.ts +4 -4
  70. package/lib/types/trash/storage-manifest.d.ts +1 -1
  71. package/lib/types/verbs/batch-carried.d.ts +1 -3
  72. package/lib/types/verbs/delete-session.d.ts +2 -2
  73. package/lib/types/verbs/errors.d.ts +2 -0
  74. package/lib/types/verbs/live-admission.d.ts +4 -7
  75. package/lib/types/verbs/member-journal.d.ts +3 -4
  76. package/lib/types/verbs/member-presence.d.ts +16 -37
  77. package/lib/types/verbs/move-group.d.ts +2 -1
  78. package/lib/types/verbs/orphan-subagents.d.ts +19 -12
  79. package/lib/types/verbs/permanent-removal.d.ts +60 -25
  80. package/lib/types/verbs/restore-session.d.ts +7 -13
  81. package/lib/types/verbs/trash-scope.d.ts +36 -0
  82. package/package.json +117 -106
  83. package/lib/types/verbs/purge-trash.d.ts +0 -30
@@ -12,39 +12,35 @@
12
12
  * - **面板恢复用 `panelSeat: true` 明说**(「缺席」不再承载语义):外部恢复(撤销横幅)由 settlement
13
13
  * 调 `afterRestore` 重开;面板恢复自带「回来」的结算,只 `forgetMainView`、不重开、不抢焦点。
14
14
  * 判据住 `src/client/main-view.ts` 的 `forgetMainView`。
15
- * - **撤销名单由 settlement 算、座位发布**:delete 支把 `landedEntryIds` 与 `successIds.length` 算成
16
- * `{ entryIds, n }` 交给座位,座位只决定发布哪一形(单条带标题、批量带条数)——那句文案与它的键
17
- * 住 `undo-toast.tsx` 与 `copy.ts`;purge 支在
15
+ * - **撤销名单由摘要给、座位发布**:delete 支从 `summary.landedIds`(域是条目 id)取名单、用
16
+ * `summary.successIds.length` 作条数,组成 `{ entryIds, n }` 交给座位,座位只决定发布哪一形
17
+ * (单条带标题、批量带条数)——那句文案与它的键住 `undo-toast.tsx` 与 `copy.ts`;purge 支在
18
18
  * `anySuccess` 时熄灭指向这些条目的横幅——留着就是一条注定失败的「撤销」。
19
- * - **`count` 不结算**;`orphan-clean` 没有跨槽位收尾(清完由 query 自己的写后刷新收尾)。
19
+ * - **`count` 不结算**;`orphan-clean` 没有跨槽位收尾(维护卡的 `use-maintenance.ts` 清完自己重查)。
20
20
  * 不直接读 DOM、不问宿主:主视图收尾只经 `main-view.ts` 的 `afterDelete` / `afterRestore`
21
21
  * (服务缺席或抛错都静默退回);重读仍归各调用方的 `refresh`(面板那侧的最新票在 `use-trash-query.ts`)。
22
22
  */
23
23
  import type { ApplyAction } from '../shared/wire.js';
24
- import { type Receipt } from './receipt.js';
25
- /** 一次写入里与渲染无关的那部分:动作、请求 id、回执、成功名单与“至少一条落地”。 */
24
+ import type { ReceiptSummary } from './receipt.js';
25
+ /** 一次写入里与渲染无关的那部分:动作、请求 id、以及唯一的那份回执摘要。 */
26
26
  export interface SettledFacts<A extends ApplyAction> {
27
27
  readonly action: A;
28
28
  readonly ids: readonly string[];
29
- readonly receipt: Receipt<A>;
30
- /** 成功支(`ok` / `already-gone`)的请求 id,按请求顺序。 */
31
- readonly successIds: readonly string[];
32
- /** 至少一条真的落地(`ok` / `already-gone`;N=1 的协议空值不算)。 */
33
- readonly anySuccess: boolean;
29
+ readonly summary: ReceiptSummary;
34
30
  }
35
31
  /**
36
32
  * 按 action 切开的一族呈现材质:每个座位只能拿到自己动作那几手,形状对不上的钩子在类型面就不存在。
37
- * - `delete` 的 `publishUndo` 存在即「这个座位要发撤销横幅」;它拿到的是 settlement 算好的落地名单。
33
+ * - `delete` 的 `publishUndo` 存在即「这个座位要发撤销横幅」;它拿到的是 settlement 从摘要取出并组好的落地名单。
38
34
  * - `restore` 的 `panelSeat: true` 才是「面板已替这条完成『回来』的结算」,不再靠缺席某个钩子暗示。
39
35
  * - `count` / `orphan-clean` 没有呈现材质(用 `noPresentation`)。
40
36
  * 钩子都是同步的:settlement 在 `refresh` 前一次跑完,不 await。
41
37
  */
42
38
  export type WritePresentationByAction = {
43
39
  delete: {
44
- /** 外部写入座位:让回收站面板重读。面板写不传(自己的 `refreshAfterWrite` 就是重读)。 */
40
+ /** 外部写入座位:让回收站面板重读。面板写不传(自己的清单重读 `reload` 不走这条通告)。 */
45
41
  invalidate?(): void;
46
- /** 撤销发布:settlement 把落地的条目 id 与请求名单算好后交来;名单**非空**——全被同批前一条带走时
47
- * 没有落点,settlement 不发布没有可撤销动作的横幅。座位只决定发布哪一形与那一形带的标题。 */
42
+ /** 撤销发布:settlement 从回执摘要取出落地条目 id 与请求名单、组好形之后交来;名单**非空**——
43
+ * 全被同批前一条带走时没有落点,settlement 不发布没有可撤销动作的横幅。座位只决定发布哪一形与那一形带的标题。 */
48
44
  publishUndo?(landed: {
49
45
  readonly entryIds: readonly [string, ...string[]];
50
46
  readonly n: number;
@@ -56,8 +52,6 @@ export type WritePresentationByAction = {
56
52
  panelSeat?: true;
57
53
  dismissUndo?(entryIds: readonly string[]): void;
58
54
  clearDetailCache?(entryIds: readonly string[]): void;
59
- /** already-gone 的回执不带行,它的 sessionId 只能由调用方从当前清单里查。 */
60
- sessionIdOfEntry?(entryId: string): string | undefined;
61
55
  };
62
56
  purge: {
63
57
  dismissUndo?(entryIds: readonly string[]): void;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 一次逐条 `/apply` 写入的**唯一生命周期**:apply → 解信封 → `readReceipt` → settlement → refresh。
2
+ * 一次逐条 `/apply` 写入的**唯一生命周期**:apply → 解信封 → `readReceipt` → 回执摘要 → settlement → refresh。
3
3
  *
4
4
  * 收尾顺序与 `anySuccess` 门住 `src/client/write-settlement.ts`(为什么按 action 收见 ADR-0013);
5
5
  * 这里只管时机、信封判读、整趟的闸与 `refresh` 的交法。清空回收站是聚合回执(`/empty`),收尾仍住
@@ -8,13 +8,14 @@
8
8
  * 五条判据:
9
9
  * - `ok` 只说**信封**成不成;逐条成败在 `failures` 里,调用方自己决定怎么上屏。
10
10
  * - 顶层失败把同一句原文铺到每个 id,不结算;`refresh` 照样交(成败都在 outcome 里)。
11
- * - **信封成功就结算**(逐条全失败也跑:批量写入的失效通告不能漏);`anySuccess` 由这里算好,
12
- * 按 action 的呈现材质由 settlement 按它上闸。调用方只交呈现材质,再按 `outcome` 决定是否真的重读。
11
+ * - **信封成功就结算**(逐条全失败也跑:批量写入的失效通告不能漏);`anySuccess` 与落地名单由
12
+ * `receipt.ts` 的回执摘要算好,settlement 按它上闸。调用方只交呈现材质,再按 `outcome` 决定是否真的重读。
13
13
  * 结算在 `refresh` 之前:列表重读看得到收尾后的现场。
14
14
  * - `single: true` 时把 N=1 模式交给 `receipt.ts` 的读法;单条 null 在那一处折成 failed,
15
15
  * 这里不再重复守卫。
16
16
  * - **闸包住整趟**:给了 `gate` 就把 apply → settle → refresh 一起包进去——refresh 落在闸外时,
17
- * 旧清单还在屏上,同一个动作能被再点一次;调用方不再自己 `around`。
17
+ * 旧清单还在屏上,同一个动作能被再点一次;调用方不再自己 `around`。同一枚闸已有任务在飞时
18
+ * 这一趟**不跑**(连点不发第二条),交回 `undefined`:座位照它收「这一趟没跑」,不伪造 outcome。
18
19
  */
19
20
  import type { BusyGate } from './busy-gate.js';
20
21
  import { type OneShotDeps } from './remote.js';
@@ -22,7 +23,8 @@ import { type SettledFacts, type WritePresentation } from './write-settlement.js
22
23
  import type { ApplyAction, RemoteFailure } from '../shared/wire.js';
23
24
  export type WriteSuccess<A extends ApplyAction> = SettledFacts<A> & {
24
25
  readonly ok: true;
25
- /** 逐条失败原文(failed / neutral / missing),按 id。 */
26
+ /** 逐条失败原文(failed / neutral / missing),按 id。这是失败表的**唯一落点**:
27
+ * 摘要只带聚合与落地 id,不再复制一份逐条原文。 */
26
28
  readonly failures: ReadonlyMap<string, string>;
27
29
  };
28
30
  export interface WriteFailure<A extends ApplyAction> {
@@ -39,8 +41,10 @@ export interface WriteInput<A extends ApplyAction> {
39
41
  readonly action: A;
40
42
  readonly ids: readonly string[];
41
43
  /** 单条写入面:把 N=1 模式交给 receipt 读法(null 折成协议错)。 */
42
- readonly single?: boolean;
43
- /** 信封成功时交给 settlement 的呈现材质(`successIds` 可能为空);按 action 的收尾顺序住 `write-settlement.ts`。 */
44
+ readonly single?: boolean | undefined;
45
+ /** restore 的 already-gone 回执不带行,它的 sessionId 只能由调用方从当前清单里查。 */
46
+ readonly sessionIdOfEntry?: (entryId: string) => string | undefined;
47
+ /** 信封成功时交给 settlement 的呈现材质(摘要里的成功名单可能为空);按 action 的收尾顺序住 `write-settlement.ts`。 */
44
48
  readonly presentation?: WritePresentation<A>;
45
49
  /** 这一趟的闸:给了就把 apply → settle → refresh 整趟包进 busy。refresh 不许落在闸外。 */
46
50
  readonly gate?: BusyGate;
@@ -48,4 +52,13 @@ export interface WriteInput<A extends ApplyAction> {
48
52
  * 是否真的重读由调用方决定。 */
49
53
  readonly refresh?: (outcome: WriteOutcome<A>) => void | Promise<void>;
50
54
  }
51
- export declare function runWrite<A extends ApplyAction>(input: WriteInput<A>): Promise<WriteOutcome<A>>;
55
+ /** 签名上的分岔就是判据:**没传闸 ⇒ 这一趟一定跑**,回真结果;**传了闸 ⇒ 可能被同一枚闸的在飞那一趟
56
+ * 挡回**,那时这一趟什么都没做、回 `undefined`。`runWrite` 不替调用方编造成果——挡回来的那趟怎么收尾
57
+ * 归座位与界面(判据住 `busy-gate.ts`)。第二支把 `gate` 钉成 `undefined`:`BusyGate | undefined`
58
+ * 这类"可能真有闸"的输入不许落到「一定跑完」那一支,否则类型会把挡回漏成 `WriteOutcome`。 */
59
+ export declare function runWrite<A extends ApplyAction>(input: WriteInput<A> & {
60
+ readonly gate: BusyGate;
61
+ }): Promise<WriteOutcome<A> | undefined>;
62
+ export declare function runWrite<A extends ApplyAction>(input: WriteInput<A> & {
63
+ readonly gate?: undefined;
64
+ }): Promise<WriteOutcome<A>>;
@@ -1,14 +1,25 @@
1
- import type { FsPort } from './ports/fs-port.js';
2
- import type { HostPort } from './ports/host-port.js';
1
+ import type { CoreFsPort, FsPort } from './ports/fs-port.js';
2
+ import type { HostCorePort } from './ports/host-port.js';
3
+ import type { PresenceCapability } from './ports/presence-port.js';
3
4
  import type { TrashLedger } from './trash/ledger.js';
4
- /** Node 半的基础依赖面:组合根只装配这一颗;动词之间不互相 import 依赖面 interface。 */
5
+ /** Node 半的基础依赖面:组合根只装配这一颗;动词之间不互相 import 依赖面 interface。
6
+ * `fs` 不含 `removeDir` / `removeFile`,`host` 不含在场三件——两条危险能力各走 {@link RemovalDeps} 与
7
+ * {@link CoreDeps.presence}。 */
5
8
  export interface CoreDeps {
6
- fs: FsPort;
7
- host: HostPort;
9
+ fs: CoreFsPort;
10
+ host: HostCorePort;
8
11
  ledger: TrashLedger;
9
12
  home: string;
13
+ presence: PresenceCapability;
10
14
  }
11
- /** 只有删除读时钟;其余读 / 计划 / 恢复 / 永久删除只用 CoreDeps。 */
12
- export type DeleteDeps = CoreDeps & {
15
+ /** 会话删除动词的依赖面:只多读时钟;它不删目录(回滚只把搬走的搬回)。 */
16
+ export type DeleteSessionDeps = CoreDeps & {
13
17
  now(): number;
14
18
  };
19
+ /** 永久删除的执行面:执行器及其分发拿得到 `removeDir` 与 `removeFile`;计划函数仍只收 `CoreDeps`(它们不写)。 */
20
+ export type RemovalDeps = CoreDeps & {
21
+ fs: CoreFsPort & Pick<FsPort, 'removeDir' | 'removeFile'>;
22
+ };
23
+ /** 组合根装出的全量面:`/apply` 的分发同时够到删除的时钟与永久删除的 `removeDir` / `removeFile`
24
+ * (后者也供孤儿附件清理段使用)。 */
25
+ export type DeleteDeps = DeleteSessionDeps & RemovalDeps;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 会话闭包(session closure)的唯一实现:`session-` 前缀归一 + 传递闭包 + 首见去重。
3
+ *
4
+ * 为什么是零依赖纯部件:这三件事在仓里曾有两份闭包实现(端口的分层 BFS、无主子会话的整表 BFS)
5
+ * 与三处手写去重,同一口径靠注释担保——注释不会替代码挡漂移,所以把口径收进这里,一份实现;
6
+ * 端口与动词只做接线(逐层问宿主、SessionIdentity 映射留在各自那层,见各自的头注)。
7
+ * 不碰盘、不碰端口类型:本文件一个 import 都不许有。
8
+ */
9
+ /** 闭包里的一个节点:本部件只认这两个字段;端口的后代记录与语料的 SessionIdentity 都满足它。 */
10
+ export interface ClosureNode {
11
+ readonly sessionId: string;
12
+ readonly projectDir: string;
13
+ }
14
+ /** 会话 id 去掉 `session-` 前缀后的形状:同一个会话在两代宿主里写法不同(v3 代 id 不带前缀、
15
+ * 现行代带),归堆、成组、去重一律按这个形状比——两边都归一,不赌调用方传的是哪一代写法。 */
16
+ export declare function bareSessionId(id: string): string;
17
+ /** 一个 id 的全部可能写法,原写法在前:`session-x` → `[session-x, x]`;`x` → `[x, session-x]`。
18
+ * 逐层问宿主时按这些写法问:宿主那支按 `header.parentSession` 原文比,不替我们归一。 */
19
+ export declare function sessionIdShapes(id: string): string[];
20
+ /** parent(bare) 到 children 索引上的传递闭包:深度不设上限、防环、首见去重(都按 bare 归一后比)。
21
+ * 根的两种写法都认(内部归一次);结果不含根自己。数据里真出现环时代价是「少数一条」,
22
+ * 不是把清单读挂死——环本来不该出现(parentSession 是一条有向边),出现就是手工改过日志或宿主 bug。 */
23
+ export declare function closureOf<N extends ClosureNode>(children: ReadonlyMap<string, readonly N[]>, root: string): N[];
24
+ /** 同一条记录两代写法只认第一次出现(保原序)。全局去重只有这一处实现:谁要按 bare 去重,
25
+ * 交一条列表进来,别在调用点再手写一个 seen 集合。 */
26
+ export declare function firstByBare<N extends ClosureNode>(members: readonly N[]): N[];
27
+ /**
28
+ * 整表语料 → 根闭包组:`parentSessionId` 不在 `known` 里的那些 subagent 各自成一组。`known` 两侧都
29
+ * 归一后再比(传原始 id 即可)。「已知父为什么排除在外」是调用方的领域判据(住
30
+ * `src/verbs/orphan-subagents.ts` 与 `CONTEXT.md` 的「无主子会话」),这一层不替它复述。
31
+ *
32
+ * 组内的后代仍走 `closureOf` 那一份闭包,只是多一条跨组的 `claimed`:一条记录只归第一个认领它的根。
33
+ * 已被前一组收走的那条,连同它整支子树都不再出现在这一组里——后代在那一组已被全部认领,所以
34
+ * 「丢掉已认领的」与「不走进已认领的那支」交回的是同一个集合。环上的代价是「少数一条」,
35
+ * 不是把一次扫描挂死。组序与成员序都按语料原序。
36
+ */
37
+ export declare function orphanRootsOf<N extends ClosureNode & {
38
+ readonly parentSessionId?: string;
39
+ readonly subagent: boolean;
40
+ }>(corpus: readonly N[], known: Iterable<string>): {
41
+ root: N;
42
+ parentSessionId: string;
43
+ members: N[];
44
+ }[];
@@ -15,16 +15,20 @@ export declare const name = "@xrn1997/dsh-session-delete";
15
15
  * 它同样可能晚于本插件激活,写进插件 inject 只会让整个插件 pending。
16
16
  *
17
17
  * `webServer` 是**浏览器半取数通道的承载者**(那条薄线):插件用它注册 `/dsh-session-delete-api`
18
- * 前缀路由,浏览器半同源 fetch 那六个段名。它是宿主 web 组成里的硬依赖(`dsh-host-webserver` 的
18
+ * 前缀路由,浏览器半同源 fetch 那九个段名。它是宿主 web 组成里的硬依赖(`dsh-host-webserver` 的
19
19
  * `super(ctx, "webServer")`),服务名与 `register({kind:'prefix', path, handler})` 的形状按
20
20
  * `dsh-host-webserver/lib/index.js` 与 `README.md` 原文读实,不是猜的。
21
21
  *
22
+ * `connection` 只喂请求栅栏的远程支(`src/api/request-trust.ts` ②),按 `ctx.get` 可选读:
23
+ * 服务缺席时插件照常激活,远程客户端 fail-closed。它不进 `inject`——写进去会让没有 web 组成的
24
+ * profile 整个停在 pending。
25
+ *
22
26
  * 清册的三层各有各的家:raw 绑定 `src/trash/storage-manifest.ts`、读面(对账 + 读)`src/trash/ledger.ts`、
23
27
  * 数据根 `src/ports/home.ts`。这里只把它们接起来,任何一层都不在组合根里长判据。
24
28
  */
25
29
  export declare const inject: string[];
26
30
  /**
27
- * 装配顺序:**清单 → 端口 → 动词依赖 → 前缀路由**(`DeleteDeps` 就是前两者的合一)。
31
+ * 装配顺序:**清单 → 端口 → 在场能力 → 动词依赖 → 前缀路由**(`DeleteDeps` 把这些依赖面合成一颗)。
28
32
  *
29
33
  * 路由用 `ctx.effect` 挂、disposer 归装载它的那根 fiber:插件卸载即摘路由(`register` 返回的
30
34
  * disposer 是官方契约)。**同一进程里只注册一次**这件事由宿主把住:重复的 `(kind, path)`
@@ -16,10 +16,11 @@ export interface SessionLogStats {
16
16
  /** 整条日志最后一个换行之后的字节数(>0 说明尾巴被切了一刀)。 */
17
17
  truncatedTailBytes: number;
18
18
  }
19
+ /** 这个名字是不是规范日志(任意一代)。附件引用扫描用它区分"老一代日志"与"读不懂的旁文件"。 */
20
+ export declare function isCanonicalSessionLogName(name: string): boolean;
19
21
  /**
20
- * 同一个会话目录里**可以并存多代日志**(宿主换代时把老代留在原地当历史语料,本机实测 7 个目录如此),
21
- * 而新代是老代的**迁移后超集**(实测:`firstTs` 与开头那条提问完全相同、记录更多、末尾更晚)
22
- * ⇒ 要读的是版本号最大的那一代,读旧代 = 读到一份截了尾巴的快照;把几代拼起来 = 重复计数。
22
+ * 同一个会话目录里可以并存多代日志;只读版本号最大的那代,因为新代包含旧代的记录且末尾更晚
23
+ * ⇒ 读旧代 = 一份截了尾巴的快照;把几代拼起来 = 重复计数。
23
24
  *
24
25
  * 明文(无 `.zstd`)不在此列:本插件的读法逐帧解 zstd,明文得走另一条解码路。
25
26
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 多帧 zstd 流的**帧边界**(2026-10-15 实测)。
2
+ * 多帧 zstd 流的帧边界。
3
3
  *
4
4
  * 为什么要有这个文件:会话日志是一次次 append 出来的**帧序列**,而 Node 的 `zstdDecompressSync`
5
5
  * **与** `createZstdDecompress()` 流式**都只给第一帧**(实测:本机一个 3.8 MB 的文件,两个 API 都只回
@@ -8,15 +8,20 @@
8
8
  * 帧 = `magic(4) + Frame_Header_Descriptor(1) + [Window 1B 若 !single] + [DID 0/1/2/4] + [FCS]
9
9
  * + 若干块(3B 块头 + 块体)+ [4B 尾巴]`。
10
10
  *
11
- * **真实产物的形态是量出来的**(本机 125 个文件 / 36 904 帧):descriptor **恒 `0x04`**(单段 ⇒ 无
12
- * Window,FCS 1 字节,无字典,校验位没置位),块类型只有 `Compressed`(36 630) 与 `Raw`(620),
13
- * 每帧 **1~120 块**(多数 1 块)——所以多块循环与 Raw 两条路都在生产上跑着,不是纸面分支。
11
+ * **真实产物的形态是量出来的**(本机 125 个文件 / 36 904 帧):descriptor **恒 `0x04`**,按 RFC 8878
12
+ * 表 3 读作「非单段(带 1 字节 Window)+ 无 FCS + 无字典 + **校验位置位**」⇒ 帧头 6 字节,帧尾那 4 字节
13
+ * 就是声明好的 XXH32 Content_Checksum(实测:动它一个位,对面解不通过)。块类型只有
14
+ * `Compressed`(36 630) 与 `Raw`(620),每帧 **1~120 块**(多数 1 块)——所以多块循环与 Raw 两条路都在
15
+ * 生产上跑着,不是纸面分支。
14
16
  * 类型 3 是规范的 Reserved 位(不是"未压缩块"):产物里没出现过,Node 也当场拒解 ⇒ 遇到就抛。
15
17
  *
16
- * **尾巴那 4 字节按实测处理,不按 descriptor 的 bit5**:校验位从未置位,却每帧都带 4 字节;
17
- * 判"读不读这 4 字节"用的是「跳过去必须正好撞上下一帧的 magic 或到 EOF」——猜错不会静默切歪:
18
- * 末尾的自证与块界护栏会抛(护栏是必需的:块头越界时 `|` 读回 0,而 0 的 `Last_Block` 位没置位,
19
- * 少了它块循环每圈只 +3 字节却永不 break,整个进程卡死——2026-10-15 写用例时真卡死过一次)。
18
+ * **位段必须照表 3 取**(1-0 字典 / 2 校验 / 5 单段 / 7-6 FCS 尺寸):Descriptor 的 64 个合法取值里,
19
+ * 取错位段会算错头长的有 48 个,剩下 16 个凑对纯属巧合——在产的 `0x04` 就在其中。换个宿主、换个写帧
20
+ * 的 zstd 版本(明文一大、FCS 上到 4 字节的 `0xa0` 就会算错),帧就切歪,会话日志直接读不出来。
21
+ *
22
+ * **尾巴那 4 字节按实测判据收,不按 descriptor 的 bit2**:规则是「跳过去必须正好撞上下一帧的 magic
23
+ * 或到 EOF」——猜错不会静默切歪:末尾的自证与块界护栏会抛(护栏是必需的:块头可能被文件截断,
24
+ * 不抛的话块循环每圈只 +3 字节却永不 break,整个进程会卡死;用例覆盖这一条)。
20
25
  */
21
26
  export interface FrameRange {
22
27
  start: number;
@@ -1,5 +1,5 @@
1
1
  /** 捕获所需的 ctx 窄面:可选服务读法(`get`)与按依赖注入的子上下文(`inject`,晚挂载的 `agents` 靠它补装)。
2
- * 旧 ctx / 假件可以没有 `inject`——那就只在 apply 时 `get` 一次。 */
2
+ * 有些 ctx / 假件没有 `inject`——那就只在 apply 时 `get` 一次。 */
3
3
  export interface AgentCtxLike {
4
4
  get(name: string): unknown;
5
5
  inject?(names: readonly string[], callback: (child: {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 宿主 `connection` 服务(`dsh-client-connection` 的 `HostConnectionService`,`super(ctx, "connection")`)
3
+ * 的窄镜像:`requestRejection(request)` 一次做宿主 `/api` 的那两道——Host/Origin 栅栏与「已认证 live peer」。
4
+ *
5
+ * **每次请求惰性解析服务**,不在激活时取一次:插件与 `client-connection` 的激活顺序没有承诺,取早了
6
+ * 会让远程支永远缺席(LAN 上全 403 且难排查)。`ctx.get` 是同步查表,代价可以忽略。
7
+ *
8
+ * **状态码在这里翻成裁决**(401 = 归属对、缺身份;403 = 归属不对;其余按归属不对处理),
9
+ * 调用方拿到的就是自己的词表,不需要知道宿主用哪个码表达哪件事。成员名与返回值按 asar
10
+ * `dsh-client-connection/lib/index.js` 原文读实。
11
+ */
12
+ import type { IncomingMessage } from 'node:http';
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ export type ConnectionVerdict = {
15
+ readonly kind: 'allowed';
16
+ }
17
+ /** 组成里没有这个服务(无 web 组成 / 老代次)——调用方 fail-closed。 */
18
+ | {
19
+ readonly kind: 'absent';
20
+ }
21
+ /** 宿主认可归属,但这条请求没带已认证身份。 */
22
+ | {
23
+ readonly kind: 'unauthenticated';
24
+ }
25
+ /** 宿主不认可归属(Host/Origin 栅栏不过,或它给出的其余状态码)。 */
26
+ | {
27
+ readonly kind: 'untrusted';
28
+ };
29
+ export interface ConnectionPort {
30
+ verdictFor(req: IncomingMessage): ConnectionVerdict;
31
+ }
32
+ /** 成员抛错按「归属不对」拒绝(fail-closed)。 */
33
+ export declare function createConnectionPort(ctx: Context): ConnectionPort;
@@ -1,16 +1,31 @@
1
1
  export interface FsPort {
2
- exists(p: string): Promise<boolean>;
2
+ /** 全部成员写成属性声明:端口方法会被**解构出来单独调**(`const { sizeOfDir } = port`,见下方
3
+ * 模块级递归的注释),实现体一律不读 `this`。方法简写的隐式 `this: FsPort` 会让
4
+ * `unbound-method` 把这种正常用法报成可疑;契约(返回的 Promise 形状)一字未动。 */
5
+ exists: (p: string) => Promise<boolean>;
3
6
  /** 建目录(含中间层)。首删时 `~/.dsh/session-trash/` 还不存在,而 `rename` 不建父目录。 */
4
- ensureDir(p: string): Promise<void>;
5
- rename(from: string, to: string): Promise<void>;
6
- removeDir(p: string): Promise<void>;
7
- sizeOfDir(p: string): Promise<number>;
7
+ ensureDir: (p: string) => Promise<void>;
8
+ /** 搬目录:**目标已存在一律失败**,跨卷(EXDEV)也失败并说清楚——不替换、也不偷偷改成复制。
9
+ * Linux 的 `rename` 会静默替代空目录而 Windows 不会,这个差别不能交给操作系统决定。 */
10
+ rename: (from: string, to: string) => Promise<void>;
11
+ removeDir: (p: string) => Promise<void>;
12
+ sizeOfDir: (p: string) => Promise<number>;
8
13
  /** 目录下的条目名(**不递归**)。回收站详情要用它在那条目录里挑 `*.jsonl.zstd`。 */
9
- listFiles(p: string): Promise<string[]>;
10
- /** 目录下的**子目录**名(不递归)。回收站对账用它找出"清单里还没有行"的条目目录——
14
+ listFiles: (p: string) => Promise<string[]>;
15
+ /** 目录下的**子目录**名(不递归;**跟随软链**)。回收站对账用它找出"清单里还没有行"的条目目录——
11
16
  * 与 `listFiles` 分开是为了让"拒收文件"发生在挑选阶段(回收站根里落进来的杂散文件当外人)。 */
12
- listDirs(p: string): Promise<string[]>;
17
+ listDirs: (p: string) => Promise<string[]>;
18
+ /** 解析符号链接后的真实路径。递归走目录时防环用:跟随软链之后,目录图不再是树。 */
19
+ realpath: (p: string) => Promise<string>;
13
20
  /** 整个文件读进内存。实测本机回收站里最大单文件 4.66 MB;**内存口径由逐帧解压把住**(见 `src/log/session-log.ts`),不在这里。 */
14
- readFile(p: string): Promise<Buffer>;
21
+ readFile: (p: string) => Promise<Buffer>;
22
+ /** 只读的单文件读数:附件渲染与独占计划都用它(不递归、不跟随目录)。 */
23
+ statFile: (p: string) => Promise<{
24
+ size: number;
25
+ }>;
26
+ /** 删单个文件(不是目录)。危险能力,只挂到 `RemovalDeps` 上。 */
27
+ removeFile: (p: string) => Promise<void>;
15
28
  }
29
+ /** 动词的基础面:读与搬移都有,**不拿删除**。删目录与删单文件只挂在 `RemovalDeps` 上(ADR-0027)。 */
30
+ export type CoreFsPort = Omit<FsPort, 'removeDir' | 'removeFile'>;
16
31
  export declare function createFsPort(): FsPort;
@@ -1,23 +1,12 @@
1
1
  /** 摘内存的三种落地(见 {@link HostPort.evictLive}):`not-live` / `evicted` 都意味着**宿主列表已经不含它**;
2
2
  * `unknown` 意味着读不到或摘不掉,**不能**据此通告客户端(发了就是假话,重连后那一行会自己长回来)。 */
3
3
  export type EvictionOutcome = 'evicted' | 'not-live' | 'unknown';
4
- /**
5
- * 角色一:**身份与归属**——会话是谁、落在哪个项目/工作区、活不活/归档没有,以及账本与归档的写面。
6
- *
7
- * 为什么拆成角色:宿主能力的端口按角色切成三块,而不是一个什么都有的接口。调用方要的是
8
- * "一句话能说清的那一组能力",而一个什么都有的端口让每条用例都得凑齐不相干的成员。
9
- * 这里只切**类型**,adapter 仍是同一颗对象。
10
- */
4
+ /** 角色一:身份与归属——会话是谁、落在哪个项目/工作区、活不活/归档没有,以及账本与归档的写面。
5
+ * 只切类型,adapter 仍是同一颗对象。 */
11
6
  export interface HostIdentityPort {
12
7
  workspaceOf(sessionId: string): Promise<string | undefined>;
13
- /** 账本里还有这个工作区吗。**只读的判别**(`registry.get(id) !== undefined`),不抛。
14
- *
15
- * **为什么恢复需要它**:宿主的「删除工作区」只删**注册**、保留目录与每一份会话日志
16
- * (asar `WorkspaceRegistry.delete` 的 JSDoc 原文 "Delete one workspace registration while
17
- * retaining its directory and every session log"),那些会话之后显示在「未分组」下。回收站里
18
- * **删在工作区之前**的条目记着那个已被删掉的 `workspaceId`,恢复时若照旧 `attach` 就会
19
- * `workspace not found` 整条失败(会话留在回收站里,用户看起来就是"恢复报错")。所以恢复
20
- * 先问这一句:不在 ⇒ 不挂账本,照常回盘、归入「未分组」(与宿主自己处理这类会话一致)。 */
8
+ /** 账本里还有这个工作区吗:只读判别,不抛。
9
+ * 宿主的「删除工作区」只删注册、保留目录与日志;恢复这类会话时跳过 attach,归入「未分组」。 */
21
10
  workspaceExists(workspaceId: string): Promise<boolean>;
22
11
  /** 会话落盘的**项目目录名**(`<sessions>/<projectKey(cwd)>`);账本里没有这个会话 ⇒ `undefined`。
23
12
  * 与 `workspaceOf` 同一口径:**未命中是正常的 no-op,不是故障**(存储真故障照抛)。 */
@@ -30,9 +19,9 @@ export interface HostIdentityPort {
30
19
  unarchive(sessionId: string): Promise<void>;
31
20
  detach(sessionId: string, workspaceId: string): Promise<void>;
32
21
  attach(sessionId: string, workspaceId: string): Promise<void>;
33
- /** 归档是 registry **全局**的,与工作区账目无关 ⇒ `workspaceId` 只是调用点对称用的可选入参
34
- * (未分组的会话没有工作区可传)。 */
35
- archive(sessionId: string, workspaceId?: string): Promise<void>;
22
+ /** 归档是 registry **全局**的,与工作区账目无关 ⇒ 只收 `sessionId`,没有工作区入参
23
+ * (实现从不读工作区;未分组的会话同样只有这一条归档路)。 */
24
+ archive(sessionId: string): Promise<void>;
36
25
  }
37
26
  /**
38
27
  * 角色二:**在场通告**——摘掉宿主内存里的活体,并如实通告客户端"这个会话没了 / 回来了"。
@@ -69,7 +58,7 @@ export type CorpusRead<T> = {
69
58
  * 角色三:**会话语料**——整表身份与子会话闭包。
70
59
  *
71
60
  * 两个读面都把"这一趟看不见语料"写成显式的 {@link CorpusRead}:`{ available: false }` 与
72
- * "看见了、里面是空的"是两件事。**缺席之后怎么办归调用方,清单见 `CONTEXT.md` 的「语料缺席」**。
61
+ * "看见了、里面是空的"是两件事。**缺席之后怎么办归调用方,清单见 `docs/architecture.md` §2 的「语料缺席」**。
73
62
  */
74
63
  export interface HostCorpusPort {
75
64
  /** 整个会话语料(live ∪ persisted)的只读身份表。**服务缺席是显式的**:`sessionQuery` 没挂,或它的
@@ -84,50 +73,32 @@ export interface HostCorpusPort {
84
73
  }
85
74
  /** 复合别名:adapter(`createHostPort`)与调用方把这 15 个方法当一个端口用;三个角色是它的切面。 */
86
75
  export type HostPort = HostIdentityPort & HostPresencePort & HostCorpusPort;
76
+ /** 动词的宿主面:身份与语料两片;不含在场三件。摘活体与通告只经 `CoreDeps.presence`(`PresenceCapability`)。
77
+ * 用交集而不是 `Omit` 黑名单:`HostPresencePort` 将来长出新方法时,不会自动漏进每个动词的 `host`。 */
78
+ export type HostCorePort = HostIdentityPort & HostCorpusPort;
87
79
  export interface SessionIdentity {
88
80
  readonly sessionId: string;
89
81
  readonly parentSessionId?: string;
90
82
  readonly projectDir: string;
91
83
  readonly subagent: boolean;
92
84
  }
85
+ /** 闭包里的一条后代:`sessionId` 是宿主交回的原写法(身份只认它,归一比对交给闭包部件)。 */
93
86
  export interface SubagentMember {
94
87
  readonly sessionId: string;
95
88
  readonly projectDir: string;
96
89
  }
97
- /** 无 cwd 的会话落在 `<sessions>/_no-cwd/`(asar `dsh-session-persistence-jsonl/lib/index.js`
98
- * 的 `projectDir(root, cwd)`:`if (cwd === void 0) return join(root, "_no-cwd");`)。
99
- * `projectDirOf` 与回收站对账(`src/trash/reconcile.ts` 重建条目)共用这一个字面量。 */
90
+ /** 无 cwd 的会话落在 `_no-cwd/`;`projectDirOf` 与回收站对账共用这个字面量。
91
+ * 出处:asar `dsh-session-persistence-jsonl/lib/index.js` 的 `projectDir`(`if (cwd === void 0) return join(root, "_no-cwd");`)。 */
100
92
  export declare const NO_CWD_PROJECT_DIR = "_no-cwd";
101
- /**
102
- * 宿主的**项目目录名**算法(`projectKey`)逐字镜像。
103
- *
104
- * 为什么是镜像而不是 import:它在 `@deepseek-ai/dsh-session-persistence-jsonl` 里面,是**宿主包**,
105
- * 跨插件值 import 是生态红线,它也不在本插件依赖里;宿主没有把这个名字暴露成任何服务
106
- * (`stat()`/`list()` 的返回只有 `{header, revision, sizeBytes}`,`locate()` 的 `{kind:'jsonl', path}` 只挂在错误上)。
107
- *
108
- * 原文(asar `dsh/node_modules/@deepseek-ai/dsh-session-persistence-jsonl/lib/index.js`):
109
- * `function projectKey(cwd) { … if (ch === "/" || ch === "\\" || ch === ":") { if (!separatorRun) readable += "-"; separatorRun = true; }
110
- * else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) { readable += ch; separatorRun = false; }
111
- * else { readable += "~" + code.toString(16).toUpperCase().padStart(4, "0"); separatorRun = false; }
112
- * return \`--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--\`; }`
113
- * 调用链:`locate(meta)` → `logPath(root, meta.cwd, meta.id, …)` → `projectDir(root, cwd)` → `projectKey(cwd)`。
114
- * 本机实测对得上:`D:/develop/GitHub/dsh-session-delete` → `--D-develop-GitHub-dsh-session-delete--`
115
- * (`~/.dsh/sessions` 下真有这个目录名;`D:/document/deepseek-harness/default-workspace` 同理)。
116
- *
117
- * **两个调用方共用这一份**:`projectDirOf`(删除时定位会话目录)与回收站对账的重建路径
118
- * (手工放进回收站的目录,从日志 header 的 `cwd` 反推它原本属于哪个项目目录)——算法只有这一处家。
119
- */
93
+ /** 宿主 `projectKey(cwd)` 的逐字镜像:跨插件不能 import,宿主也没把它暴露成服务。
94
+ * 出处:asar `dsh-session-persistence-jsonl/lib/index.js` 的 `projectKey`;调用链
95
+ * `locate` → `logPath` → `projectDir` → `projectKey`。本机实测 `D:/develop/GitHub/dsh-session-delete`
96
+ * → `--D-develop-GitHub-dsh-session-delete--`。`projectDirOf` 与回收站对账重建共用这一份;
97
+ * 宿主换代先复核算法。 */
120
98
  export declare function projectKey(cwd: string): string;
121
- /** 会话 id 去掉 `session-` 前缀后的形状(同一个父在两代里写法不同,见 {@link HostCorpusPort.subagentMembersOf})。 */
122
- export declare function bareSessionId(id: string): string;
123
99
  /** 这次失败是不是"账本里没那个工作区"(而不是另有故障)。恢复侧靠它把两种失败分开:
124
100
  * 前者按「未分组」收,后者照旧回滚报 `io`——不给真故障静默改归属的机会。 */
125
101
  export declare function isWorkspaceMissingError(error: unknown): boolean;
126
- /**
127
- * 宿主服务层的**真实绑定**:各方法把动词层的问题翻成宿主官方服务的调用。
128
- *
129
- * 每个方法的依据(服务 / 方法 / 入参)与原文切片见各自的实现处注释。
130
- * 出错一律**原样抛**(不在这里裹动词码):动词层把原始错误挂在 `cause` 上,
131
- * `toRemoteFailure` 再按 `name` 把它映射回 `sessiondelete/<码>`(src/remote/map-error.ts 的两行映射)。
132
- */
102
+ /** 宿主服务的真实绑定;出错原样抛,由 `toRemoteFailure` 按 name 映射。
103
+ * 各方法调用的服务与方法见上方成员注释;错误映射表住 `src/remote/map-error.ts`。 */
133
104
  export declare function createHostPort(ctx: unknown): HostPort;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 在场能力:动词只拿到「成员离场 / 归位 / 一批归位」三条,不再拿到宿主的 `evictLive` / `announce*`。
3
+ *
4
+ * 生产实现住 `src/verbs/member-presence.ts`;组合根装配一次,挂在 `CoreDeps.presence`。这样
5
+ * “谁可以摘活体、谁可以发通告”由依赖面回答,而不是靠源码扫描维持。
6
+ *
7
+ * 能力面另带一枚**不导出的品牌字段**:形状对了但没盖章的对象满足不了 `PresenceCapability`
8
+ * (`tests/deps.test.ts` 钉住)。品牌是类型层的(`declare` 出来的 unique symbol 没有运行时值),
9
+ * 所以仓内不存在任何能"顺手带上这格"的出口——唯一的盖章处是 `createMemberPresence` 里那一处显式断言。
10
+ */
11
+ declare const PRESENCE_BRAND: unique symbol;
12
+ export interface MemberBackFacts {
13
+ readonly diskBack: boolean;
14
+ readonly workspaceBack: boolean;
15
+ }
16
+ export interface PresenceCapability {
17
+ /** 品牌字段:值不承载语义,只为让结构相似的手拼对象满足不了这一面——键名是不导出的 unique symbol,
18
+ * 别的模块连键都写不出来,唯一的盖章处住 `src/verbs/member-presence.ts`。 */
19
+ readonly [PRESENCE_BRAND]: true;
20
+ /** 成员已经不在宿主可见列表:摘活体;三态不是 unknown 时发 removed。
21
+ * `backInTrash: false` 表示目录没退回回收站(恢复回滚失败),此时什么都不做;`facts` 缺席按
22
+ * 「正常退回」处理(`createMemberPresence` 的缺省),不再留第三种含义。 */
23
+ memberGone(sessionId: string, facts?: {
24
+ readonly backInTrash: boolean;
25
+ }): Promise<void>;
26
+ /** 成员已经回到原来的可见状态:目录与账本两件写都回去才发 added。 */
27
+ memberBack(sessionId: string, facts: MemberBackFacts): Promise<void>;
28
+ /** 批量恢复的「回来了」通告;单条就是长度为 1 的批。 */
29
+ announceRestored(ids: readonly string[]): Promise<void>;
30
+ }
31
+ export {};