@xrn1997/dsh-session-delete 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +20 -14
  2. package/lib/client.js +1686 -1053
  3. package/lib/index.js +1387 -771
  4. package/lib/types/api/attachment-orphans.d.ts +11 -0
  5. package/lib/types/api/attachment.d.ts +23 -0
  6. package/lib/types/api/dispatch.d.ts +9 -29
  7. package/lib/types/api/request-trust.d.ts +45 -0
  8. package/lib/types/api/route-table.d.ts +28 -14
  9. package/lib/types/client/attachment-view.d.ts +27 -0
  10. package/lib/types/client/batch-bar.d.ts +1 -1
  11. package/lib/types/client/batch-confirm.d.ts +2 -2
  12. package/lib/types/client/batch-menu-item.d.ts +1 -1
  13. package/lib/types/client/batch-select.d.ts +1 -1
  14. package/lib/types/client/confirm-close.d.ts +11 -0
  15. package/lib/types/client/copy.d.ts +48 -38
  16. package/lib/types/client/delete-confirm.d.ts +0 -2
  17. package/lib/types/client/main-view.d.ts +1 -1
  18. package/lib/types/client/maintenance-parts.d.ts +25 -0
  19. package/lib/types/client/maintenance-settings.d.ts +22 -0
  20. package/lib/types/client/markdown.d.ts +9 -1
  21. package/lib/types/client/orphan-maintenance.d.ts +29 -11
  22. package/lib/types/client/receipt.d.ts +42 -2
  23. package/lib/types/client/remote.d.ts +32 -48
  24. package/lib/types/client/trash-confirm.d.ts +2 -15
  25. package/lib/types/client/trash-panel.d.ts +4 -3
  26. package/lib/types/client/ui/Checkbox.d.ts +1 -1
  27. package/lib/types/client/ui/Modal.d.ts +2 -2
  28. package/lib/types/client/ui/icons.d.ts +6 -11
  29. package/lib/types/client/ui/top-layer.d.ts +10 -0
  30. package/lib/types/client/undo-notices.d.ts +52 -0
  31. package/lib/types/client/undo-toast.d.ts +5 -59
  32. package/lib/types/client/use-column-resize.d.ts +9 -20
  33. package/lib/types/client/use-maintenance.d.ts +45 -0
  34. package/lib/types/client/use-panel-width.d.ts +1 -1
  35. package/lib/types/client/use-trash-panel-controller.d.ts +24 -7
  36. package/lib/types/client/use-trash-query.d.ts +35 -32
  37. package/lib/types/client/write-seats.d.ts +5 -13
  38. package/lib/types/client/write-settlement.d.ts +10 -16
  39. package/lib/types/client/write.d.ts +8 -5
  40. package/lib/types/deps.d.ts +18 -7
  41. package/lib/types/index.d.ts +6 -2
  42. package/lib/types/log/session-log.d.ts +4 -3
  43. package/lib/types/log/zstd-frames.d.ts +14 -8
  44. package/lib/types/ports/agent-handles.d.ts +1 -1
  45. package/lib/types/ports/connection-port.d.ts +33 -0
  46. package/lib/types/ports/fs-port.d.ts +13 -1
  47. package/lib/types/ports/host-port.d.ts +17 -45
  48. package/lib/types/ports/presence-port.d.ts +22 -0
  49. package/lib/types/shared/wire.d.ts +70 -26
  50. package/lib/types/trash/attachments.d.ts +85 -0
  51. package/lib/types/trash/detail.d.ts +28 -8
  52. package/lib/types/trash/entry.d.ts +3 -3
  53. package/lib/types/trash/groups.d.ts +4 -11
  54. package/lib/types/trash/storage-manifest.d.ts +1 -1
  55. package/lib/types/verbs/delete-session.d.ts +2 -2
  56. package/lib/types/verbs/errors.d.ts +2 -0
  57. package/lib/types/verbs/live-admission.d.ts +4 -7
  58. package/lib/types/verbs/member-journal.d.ts +3 -4
  59. package/lib/types/verbs/member-presence.d.ts +14 -37
  60. package/lib/types/verbs/orphan-subagents.d.ts +9 -8
  61. package/lib/types/verbs/permanent-removal.d.ts +24 -18
  62. package/lib/types/verbs/purge-trash.d.ts +8 -3
  63. package/lib/types/verbs/restore-session.d.ts +7 -13
  64. package/package.json +2 -1
@@ -1,43 +1,46 @@
1
1
  import type { BusyGate } from './busy-gate.js';
2
- import { type OrphanReportWire, type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
3
- import type { WriteOutcome } from './write.js';
2
+ import { type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
4
3
  import type { Translate } from './copy.js';
5
- import type { ApplyAction } from '../shared/wire.js';
6
- export type TrashQueryDeps = Pick<SessionDeleteRemote, 'list' | 'scanOrphans' | 'applyOutcomes' | 'emptyTrash'>;
7
- /** 一次清单读的结局:`stale` = 被后发的读顶掉(什么都没落地,调用方不许拿它当失败清场)。 */
8
- export type ReloadResult = 'ok' | 'failed' | 'stale';
4
+ import type { AttachmentCleanupWire } from '../shared/wire.js';
5
+ export type TrashQueryDeps = Pick<SessionDeleteRemote, 'list' | 'emptyTrash'>;
6
+ /** 清单的唯一状态:加载中 / 有行 / 未知。`unknown` 不向下游交出任何行——旧行不许继续可操作。 */
7
+ export type TrashListState = {
8
+ readonly status: 'loading';
9
+ } | {
10
+ readonly status: 'ready';
11
+ readonly rows: readonly TrashEntryLike[];
12
+ } | {
13
+ readonly status: 'unknown';
14
+ readonly failure: string;
15
+ };
16
+ /** 清单状态对视图的**唯一解读**:行、失败原文、加载中与空态一次交出。
17
+ * 面板与控制器不再各自从 `status` 反推——"未知"不是"空",也不许把失败折成空态。 */
18
+ export interface TrashListView {
19
+ readonly rows: readonly TrashEntryLike[] | null;
20
+ readonly failure: string | null;
21
+ readonly loading: boolean;
22
+ readonly empty: boolean;
23
+ }
24
+ export declare function listView(list: TrashListState): TrashListView;
9
25
  export interface TrashQuery {
10
- /** `null` = 还没读到(第一次读或读失败);与"空的"是两件事。 */
11
- readonly entries: readonly TrashEntryLike[] | null;
12
- readonly listFailure: string | null;
13
- /** `null` = 还没扫到;失败时也置回 `null`(不拿空报告冒充干净)。 */
14
- readonly orphanReport: OrphanReportWire | null;
15
- readonly orphanFailure: string | null;
16
- /** 「这一组已经不在」不是失败:单独一条中性提示。 */
17
- readonly orphanNotice: string | null;
18
- readonly orphanConfirm: boolean;
19
- setOrphanConfirm(open: boolean): void;
20
- /** 「查看并清理」:把上一条失败/中性提示清掉再开预览框(旧提示不该跟新一次预览并排出现)。 */
21
- openOrphanPreview(): void;
22
- /** 只重读清单(对账 + 读)。交回**这一次读的结局**:落地 / 读失败 / 被后发的读顶掉。
23
- * 残留报告与提示的联动不在这里;组合清场归 `refreshAll` / `refreshAfterWrite`。 */
24
- reloadRows(): Promise<ReloadResult>;
25
- /** 只扫残留。调用方负责先让清单对过账(`refreshAll` / `refreshAfterWrite` 已经排好序)。 */
26
- rescanOrphans(): Promise<void>;
27
- /** 先读清单、成了再扫残留;挂载与显式「重试」用它。 */
26
+ readonly list: TrashListState;
27
+ /** 最近一次写动作(purge / 清空)的附件读数,已经拼成一句话:**最新的写赢**。
28
+ * 一次没有附件读数的 purge / 清空、或这一趟写失败(没拿到回执)都会把它清成 `null`,不让上一条的
29
+ * 读数冒充这一趟;删除与恢复不碰它。它是**过去那一趟的回执**、不是清单状态:清单重读读不出来。 */
30
+ readonly attachmentNotice: string | null;
31
+ /** 把一份附件读数摆上屏(`undefined` = 这一趟没有读数,旧提示收掉);句子怎么拼归这里,调用方只管转交。 */
32
+ noteAttachmentCleanup(a: AttachmentCleanupWire | undefined): void;
33
+ /** 重读清单;挂载与显式「重试」用它。面板没有第二条扫描:孤儿检查是设置页维护卡的手动动作。 */
28
34
  refreshAll(): Promise<void>;
29
- /** 写后/失效:只重读清单;是否顺带重扫残留由这次的 `outcome` 从 action / ids / 当前清单自行派生
30
- * (判据住 `needsOrphanRescan`)。失效通告不带 outcome:报告未知或非空才重扫。输给后发读时由赢家补扫;
31
- * 清单读失败时清空报告与提示。 */
32
- refreshAfterWrite(outcome?: WriteOutcome<ApplyAction>): Promise<void>;
33
- /** 清理那批无主子会话(一段 `/apply` 的第五个动作),逐条结果落成"已不在了 / N 组未清理"。 */
34
- cleanOrphans(): Promise<void>;
35
- /** 清空回收站(聚合回执):成功就熄掉撤销横幅并重读,失败就把原文摆到清单那块。交回是否成功。 */
35
+ /** 写后/失效通告:与 `refreshAll` 同一条读。两个名字留着是因为调用点问的是两件事
36
+ * ("用户要看现在的"与"别处动过了,跟上"),不是两条路径。清单读失败时旧行进 `unknown`。 */
37
+ refreshAfterWrite(): Promise<void>;
38
+ /** 清空回收站(聚合回执):成功就熄掉撤销横幅并重读,失败就把清单置为未知并摆上原文。交回是否成功。 */
36
39
  emptyTrash(): Promise<boolean>;
37
40
  }
38
41
  export declare function useTrashQuery({ deps, t, gate, }: {
39
42
  deps: TrashQueryDeps;
40
43
  t: Translate;
41
- /** 面板的一枚全局闸:清空与无主子会话清理这两条写动作把整趟包在里面(面板的 `busy`)。 */
44
+ /** 面板的一枚全局闸:清空这条写动作把整趟包在里面(面板的 `busy`)。 */
42
45
  gate: BusyGate;
43
46
  }): TrashQuery;
@@ -1,12 +1,12 @@
1
1
  /**
2
- * 逐条 `/apply` 的**写入座位**:一次写入发生在哪个界面位置,就在哪个函数里把呈现材质、闸与重读通道绑好。
2
+ * 逐条 `/apply` 的写入座位:一次写入发生在哪个界面位置,就在哪个函数里绑定呈现材质、闸与重读通道。
3
3
  *
4
- * 为什么要有它:六个调用点此前各自拼 `runWrite({ presentation: {…} })`,并各自决定要不要外包一层闸。
5
- * 收尾顺序(`write-settlement.ts`,ADR-0013)与重读策略(`use-trash-query.ts`,ADR-0016)不动;
4
+ * 收尾顺序住 `write-settlement.ts`(ADR-0013),面板的重读策略住 `use-trash-query.ts`;
6
5
  * 座位只拥有「这次是谁在写、交哪些材质、闸在哪」。
7
6
  *
8
- * 边界:不判回执(`receipt.ts`)、不决定收尾顺序、不拥有各面的本地状态(`pending` / 详情缓存 / 行失败)。
9
- * 面板座位的闸由面板传入:`busy` 是它的 UI 状态,座位不另造第二枚。
7
+ * 边界:不判回执、不决定收尾顺序、不拥有各面的本地状态;面板座位的闸由面板传入,不另造第二枚。
8
+ * 无主子会话清理**不是**座位:它只在插件设置页的维护卡里手动触发(ADR-0021),
9
+ * 读数、闸与重查都归 `use-maintenance.ts`。
10
10
  */
11
11
  import type { BusyGate } from './busy-gate.js';
12
12
  import type { OneShotDeps, TrashEntryLike } from './remote.js';
@@ -41,12 +41,4 @@ interface PanelRestoreInput extends PanelWriteInput<'restore'> {
41
41
  export declare function restoreFromPanel(input: PanelRestoreInput): Promise<WriteOutcome<'restore'>>;
42
42
  /** 面板彻底删除座位:永久删除也让指向这些条目的撤销横幅熄灭(settlement 的 purge 支负责)。 */
43
43
  export declare function purgeFromPanel(input: PanelWriteInput<'purge'>): Promise<WriteOutcome<'purge'>>;
44
- interface OrphanCleanSeatInput {
45
- deps: OneShotDeps;
46
- ids: readonly string[];
47
- gate: BusyGate;
48
- refreshAfterWrite(outcome: WriteOutcome<'orphan-clean'>): Promise<void>;
49
- }
50
- /** 无主子会话清理座位:不碰回收站失效与撤销,回执判读仍归 `use-trash-query.ts`。 */
51
- export declare function cleanOrphansFromPanel(input: OrphanCleanSeatInput): Promise<WriteOutcome<'orphan-clean'>>;
52
44
  export {};
@@ -12,29 +12,25 @@
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。
@@ -43,8 +39,8 @@ export type WritePresentationByAction = {
43
39
  delete: {
44
40
  /** 外部写入座位:让回收站面板重读。面板写不传(自己的 `refreshAfterWrite` 就是重读)。 */
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,8 +8,8 @@
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
  * 这里不再重复守卫。
@@ -22,7 +22,8 @@ import { type SettledFacts, type WritePresentation } from './write-settlement.js
22
22
  import type { ApplyAction, RemoteFailure } from '../shared/wire.js';
23
23
  export type WriteSuccess<A extends ApplyAction> = SettledFacts<A> & {
24
24
  readonly ok: true;
25
- /** 逐条失败原文(failed / neutral / missing),按 id。 */
25
+ /** 逐条失败原文(failed / neutral / missing),按 id。这是失败表的**唯一落点**:
26
+ * 摘要只带聚合与落地 id,不再复制一份逐条原文。 */
26
27
  readonly failures: ReadonlyMap<string, string>;
27
28
  };
28
29
  export interface WriteFailure<A extends ApplyAction> {
@@ -40,7 +41,9 @@ export interface WriteInput<A extends ApplyAction> {
40
41
  readonly ids: readonly string[];
41
42
  /** 单条写入面:把 N=1 模式交给 receipt 读法(null 折成协议错)。 */
42
43
  readonly single?: boolean;
43
- /** 信封成功时交给 settlement 的呈现材质(`successIds` 可能为空);按 action 的收尾顺序住 `write-settlement.ts`。 */
44
+ /** restore 的 already-gone 回执不带行,它的 sessionId 只能由调用方从当前清单里查。 */
45
+ readonly sessionIdOfEntry?: (entryId: string) => string | undefined;
46
+ /** 信封成功时交给 settlement 的呈现材质(摘要里的成功名单可能为空);按 action 的收尾顺序住 `write-settlement.ts`。 */
44
47
  readonly presentation?: WritePresentation<A>;
45
48
  /** 这一趟的闸:给了就把 apply → settle → refresh 整趟包进 busy。refresh 不许落在闸外。 */
46
49
  readonly gate?: BusyGate;
@@ -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;
@@ -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,21 @@
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」——猜错不会静默切歪:末尾的自证与块界护栏会抛(护栏是必需的:块头越界时 `|` 读回 0,
24
+ * 而 0 的 `Last_Block` 位没置位,少了它块循环每圈只 +3 字节却永不 break,整个进程会卡死;
25
+ * 用例覆盖这一条)。
20
26
  */
21
27
  export interface FrameRange {
22
28
  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;
@@ -2,15 +2,27 @@ export interface FsPort {
2
2
  exists(p: string): Promise<boolean>;
3
3
  /** 建目录(含中间层)。首删时 `~/.dsh/session-trash/` 还不存在,而 `rename` 不建父目录。 */
4
4
  ensureDir(p: string): Promise<void>;
5
+ /** 搬目录:**目标已存在一律失败**,跨卷(EXDEV)也失败并说清楚——不替换、也不偷偷改成复制。
6
+ * Linux 的 `rename` 会静默替代空目录而 Windows 不会,这个差别不能交给操作系统决定。 */
5
7
  rename(from: string, to: string): Promise<void>;
6
8
  removeDir(p: string): Promise<void>;
7
9
  sizeOfDir(p: string): Promise<number>;
8
10
  /** 目录下的条目名(**不递归**)。回收站详情要用它在那条目录里挑 `*.jsonl.zstd`。 */
9
11
  listFiles(p: string): Promise<string[]>;
10
- /** 目录下的**子目录**名(不递归)。回收站对账用它找出"清单里还没有行"的条目目录——
12
+ /** 目录下的**子目录**名(不递归;**跟随软链**)。回收站对账用它找出"清单里还没有行"的条目目录——
11
13
  * 与 `listFiles` 分开是为了让"拒收文件"发生在挑选阶段(回收站根里落进来的杂散文件当外人)。 */
12
14
  listDirs(p: string): Promise<string[]>;
15
+ /** 解析符号链接后的真实路径。递归走目录时防环用:跟随软链之后,目录图不再是树。 */
16
+ realpath(p: string): Promise<string>;
13
17
  /** 整个文件读进内存。实测本机回收站里最大单文件 4.66 MB;**内存口径由逐帧解压把住**(见 `src/log/session-log.ts`),不在这里。 */
14
18
  readFile(p: string): Promise<Buffer>;
19
+ /** 只读的单文件读数:附件渲染与独占计划都用它(不递归、不跟随目录)。 */
20
+ statFile(p: string): Promise<{
21
+ size: number;
22
+ }>;
23
+ /** 删单个文件(不是目录)。危险能力,只挂到 `RemovalDeps` 上。 */
24
+ removeFile(p: string): Promise<void>;
15
25
  }
26
+ /** 动词的基础面:读与搬移都有,**不拿删除**。删目录与删单文件只挂在 `RemovalDeps` 上(ADR-0006)。 */
27
+ export type CoreFsPort = Omit<FsPort, 'removeDir' | 'removeFile'>;
16
28
  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,不是故障**(存储真故障照抛)。 */
@@ -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,6 +73,9 @@ 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;
@@ -94,40 +86,20 @@ export interface SubagentMember {
94
86
  readonly sessionId: string;
95
87
  readonly projectDir: string;
96
88
  }
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` 重建条目)共用这一个字面量。 */
89
+ /** 无 cwd 的会话落在 `_no-cwd/`;`projectDirOf` 与回收站对账共用这个字面量。
90
+ * 出处:asar `dsh-session-persistence-jsonl/lib/index.js` 的 `projectDir`(`if (cwd === void 0) return join(root, "_no-cwd");`)。 */
100
91
  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
- */
92
+ /** 宿主 `projectKey(cwd)` 的逐字镜像:跨插件不能 import,宿主也没把它暴露成服务。
93
+ * 出处:asar `dsh-session-persistence-jsonl/lib/index.js` 的 `projectKey`;调用链
94
+ * `locate` → `logPath` → `projectDir` → `projectKey`。本机实测 `D:/develop/GitHub/dsh-session-delete`
95
+ * → `--D-develop-GitHub-dsh-session-delete--`。`projectDirOf` 与回收站对账重建共用这一份;
96
+ * 宿主换代先复核算法。 */
120
97
  export declare function projectKey(cwd: string): string;
121
98
  /** 会话 id 去掉 `session-` 前缀后的形状(同一个父在两代里写法不同,见 {@link HostCorpusPort.subagentMembersOf})。 */
122
99
  export declare function bareSessionId(id: string): string;
123
100
  /** 这次失败是不是"账本里没那个工作区"(而不是另有故障)。恢复侧靠它把两种失败分开:
124
101
  * 前者按「未分组」收,后者照旧回滚报 `io`——不给真故障静默改归属的机会。 */
125
102
  export declare function isWorkspaceMissingError(error: unknown): boolean;
126
- /**
127
- * 宿主服务层的**真实绑定**:各方法把动词层的问题翻成宿主官方服务的调用。
128
- *
129
- * 每个方法的依据(服务 / 方法 / 入参)与原文切片见各自的实现处注释。
130
- * 出错一律**原样抛**(不在这里裹动词码):动词层把原始错误挂在 `cause` 上,
131
- * `toRemoteFailure` 再按 `name` 把它映射回 `sessiondelete/<码>`(src/remote/map-error.ts 的两行映射)。
132
- */
103
+ /** 宿主服务的真实绑定;出错原样抛,由 `toRemoteFailure` 按 name 映射。
104
+ * 各方法调用的服务与方法见上方成员注释;错误映射表住 `src/remote/map-error.ts`。 */
133
105
  export declare function createHostPort(ctx: unknown): HostPort;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 在场能力:动词只拿到「成员离场 / 归位 / 一批归位」三条,不再拿到宿主的 `evictLive` / `announce*`。
3
+ *
4
+ * 生产实现住 `src/verbs/member-presence.ts`;组合根装配一次,挂在 `CoreDeps.presence`。这样
5
+ * “谁可以摘活体、谁可以发通告”由依赖面回答,而不是靠源码扫描维持。
6
+ */
7
+ export interface MemberBackFacts {
8
+ readonly diskBack: boolean;
9
+ readonly workspaceBack: boolean;
10
+ }
11
+ export interface PresenceCapability {
12
+ /** 成员已经不在宿主可见列表:摘活体;三态不是 unknown 时发 removed。
13
+ * `backInTrash: false` 表示目录没退回回收站(恢复回滚失败),此时什么都不做;`facts` 缺席按
14
+ * 「正常退回」处理(`createMemberPresence` 的缺省),不再留第三种含义。 */
15
+ memberGone(sessionId: string, facts?: {
16
+ readonly backInTrash: boolean;
17
+ }): Promise<void>;
18
+ /** 成员已经回到原来的可见状态:目录与账本两件写都回去才发 added。 */
19
+ memberBack(sessionId: string, facts: MemberBackFacts): Promise<void>;
20
+ /** 批量恢复的「回来了」通告;单条就是长度为 1 的批。 */
21
+ announceRestored(ids: readonly string[]): Promise<void>;
22
+ }