@xrn1997/dsh-session-delete 0.4.0 → 0.6.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 (70) hide show
  1. package/README.md +39 -10
  2. package/cordis.patch.yml +4 -0
  3. package/icon.svg +18 -18
  4. package/lib/client.js +1897 -859
  5. package/lib/index.js +1803 -1058
  6. package/lib/types/api/attachment-orphans.d.ts +9 -3
  7. package/lib/types/api/attachment.d.ts +17 -7
  8. package/lib/types/api/connection-route.d.ts +23 -0
  9. package/lib/types/api/dispatch.d.ts +25 -14
  10. package/lib/types/api/route-table.d.ts +47 -11
  11. package/lib/types/api/trash-rows.d.ts +3 -3
  12. package/lib/types/client/batch-bar.d.ts +1 -1
  13. package/lib/types/client/batch-pick.d.ts +19 -0
  14. package/lib/types/client/batch-select.d.ts +7 -5
  15. package/lib/types/client/busy-gate.d.ts +6 -2
  16. package/lib/types/client/confirm-close.d.ts +4 -1
  17. package/lib/types/client/context.d.ts +3 -2
  18. package/lib/types/client/copy.d.ts +54 -4
  19. package/lib/types/client/delete-confirm.d.ts +1 -1
  20. package/lib/types/client/delete-menu-item.d.ts +1 -1
  21. package/lib/types/client/host-rows.d.ts +29 -30
  22. package/lib/types/client/main-view.d.ts +2 -1
  23. package/lib/types/client/markdown.d.ts +6 -7
  24. package/lib/types/client/orphan-maintenance.d.ts +10 -25
  25. package/lib/types/client/pending-open.d.ts +17 -0
  26. package/lib/types/client/receipt.d.ts +3 -1
  27. package/lib/types/client/remote.d.ts +21 -18
  28. package/lib/types/client/store.d.ts +15 -10
  29. package/lib/types/client/trash-detail.d.ts +48 -6
  30. package/lib/types/client/trash-entry.d.ts +1 -1
  31. package/lib/types/client/trash-nav.d.ts +12 -8
  32. package/lib/types/client/ui/Toast.d.ts +3 -1
  33. package/lib/types/client/ui/icons.d.ts +1 -1
  34. package/lib/types/client/undo-toast.d.ts +2 -1
  35. package/lib/types/client/use-maintenance-section.d.ts +52 -0
  36. package/lib/types/client/use-maintenance.d.ts +7 -35
  37. package/lib/types/client/use-trash-panel-controller.d.ts +35 -14
  38. package/lib/types/client/use-trash-query.d.ts +10 -8
  39. package/lib/types/client/use-trash-selection.d.ts +15 -12
  40. package/lib/types/client/write-seats.d.ts +32 -16
  41. package/lib/types/client/write-settlement.d.ts +1 -1
  42. package/lib/types/client/write.d.ts +13 -3
  43. package/lib/types/config.d.ts +45 -0
  44. package/lib/types/deps.d.ts +8 -3
  45. package/lib/types/domain/closure.d.ts +47 -0
  46. package/lib/types/index.d.ts +13 -12
  47. package/lib/types/log/zstd-frames.d.ts +2 -3
  48. package/lib/types/ports/attachments-port.d.ts +40 -0
  49. package/lib/types/ports/fs-port.d.ts +15 -12
  50. package/lib/types/ports/host-port.d.ts +4 -5
  51. package/lib/types/ports/presence-port.d.ts +9 -0
  52. package/lib/types/shared/wire.d.ts +137 -31
  53. package/lib/types/trash/attachments.d.ts +10 -1
  54. package/lib/types/trash/detail.d.ts +32 -22
  55. package/lib/types/trash/entry.d.ts +7 -1
  56. package/lib/types/trash/groups.d.ts +6 -1
  57. package/lib/types/trash/manifest.d.ts +4 -4
  58. package/lib/types/verbs/batch-carried.d.ts +1 -3
  59. package/lib/types/verbs/live-admission.d.ts +1 -1
  60. package/lib/types/verbs/member-presence.d.ts +3 -1
  61. package/lib/types/verbs/move-group.d.ts +2 -1
  62. package/lib/types/verbs/orphan-subagents.d.ts +12 -5
  63. package/lib/types/verbs/permanent-removal.d.ts +39 -10
  64. package/lib/types/verbs/trash-scope.d.ts +36 -0
  65. package/locale/en.json +6 -6
  66. package/locale/zh.json +6 -6
  67. package/package.json +13 -3
  68. package/lib/types/api/request-trust.d.ts +0 -45
  69. package/lib/types/ports/connection-port.d.ts +0 -33
  70. package/lib/types/verbs/purge-trash.d.ts +0 -35
@@ -9,22 +9,25 @@ export interface TrashSelection {
9
9
  /** 键盘 ↑↓ 与下钻走这个顺序:**只含展开的组**(折叠的行不在 DOM 里,方向键别停过去)。 */
10
10
  readonly ordered: readonly TrashEntryLike[];
11
11
  readonly allCollapsed: boolean;
12
- isCollapsed(projectDir: string): boolean;
13
- enterSelect(): void;
14
- exitSelect(): void;
15
- toggleGroup(projectDir: string): void;
16
- toggleAllGroups(): void;
17
- toggleCheck(entryId: string): void;
12
+ /** 方法一律写成**属性声明**:面板会把它们摘下来传给导航/行组件,实现体也都不读 `this`。
13
+ * 方法简写带来的隐式 `this: TrashSelection` 会让 `unbound-method` 误报,且会诱导调用点
14
+ * 为了消警报去包箭头函数——那会让 `TrashSelection` 每次渲染换新身份(见 ADR 外的回归教训)。 */
15
+ isCollapsed: (projectDir: string) => boolean;
16
+ enterSelect: () => void;
17
+ exitSelect: () => void;
18
+ toggleGroup: (projectDir: string) => void;
19
+ toggleAllGroups: () => void;
20
+ toggleCheck: (entryId: string) => void;
18
21
  /** 组头那颗:说的是"这个项目的全部会话",折叠起来的条目也算。勾满再点 = 整组取消。 */
19
- toggleGroupCheck(group: {
22
+ toggleGroupCheck: (group: {
20
23
  entries: readonly TrashEntryLike[];
21
- }): void;
22
- groupFullyChecked(group: {
24
+ }) => void;
25
+ groupFullyChecked: (group: {
23
26
  entries: readonly TrashEntryLike[];
24
- }): boolean;
25
- groupPartiallyChecked(group: {
27
+ }) => boolean;
28
+ groupPartiallyChecked: (group: {
26
29
  entries: readonly TrashEntryLike[];
27
- }): boolean;
30
+ }) => boolean;
28
31
  }
29
32
  export declare function useTrashSelection({ entries, groups, }: {
30
33
  entries: readonly TrashEntryLike[] | null;
@@ -4,41 +4,57 @@
4
4
  * 收尾顺序住 `write-settlement.ts`(ADR-0013),面板的重读策略住 `use-trash-query.ts`;
5
5
  * 座位只拥有「这次是谁在写、交哪些材质、闸在哪」。
6
6
  *
7
- * 边界:不判回执、不决定收尾顺序、不拥有各面的本地状态;面板座位的闸由面板传入,不另造第二枚。
7
+ * 边界:不自己判读回执(verdict 判读住 `receipt.ts`,失败表怎么上屏归各写入面——撤销那一趟的
8
+ * 横幅改写住 `restoreFromUndo`)、不决定收尾顺序、不拥有各面的本地状态;面板座位的闸由面板传入,
9
+ * 不另造第二枚;闸挡回来的那一趟**什么都不落地**——座位交回 `undefined`,不伪造 outcome,
10
+ * 收尾留给在飞的那一趟。
8
11
  * 无主子会话清理**不是**座位:它只在插件设置页的维护卡里手动触发(ADR-0021),
9
12
  * 读数、闸与重查都归 `use-maintenance.ts`。
10
13
  */
14
+ import type { Translate } from './copy.js';
11
15
  import type { BusyGate } from './busy-gate.js';
12
16
  import type { OneShotDeps, TrashEntryLike } from './remote.js';
17
+ import { type UndoNotice } from './undo-notices.js';
13
18
  import { type WriteOutcome } from './write.js';
14
- /** 会话行菜单的单删座位:一条会话、`single` 读法、成功发单条撤销横幅。交回这条会话的失败原文。 */
19
+ /** 会话行菜单的单删座位:一条会话、`single` 读法、成功发单条撤销横幅。交回这条会话的失败原文。
20
+ * 三种形状各有意思,不许互相冒充:字符串 = 这一条失败了(原文);`null` = 跑了、这一条没有失败要说;
21
+ * `undefined` = 闸把这一趟挡回来了、什么都没跑(框不收,等在飞那一趟的结论——先收就会把它的失败原文
22
+ * 落在已经卸载的确认框上)。 */
15
23
  export declare function deleteSessionFromMenu(deps: OneShotDeps, input: {
16
24
  sessionId: string;
17
25
  title: string;
18
- }, gate: BusyGate): Promise<string | null>;
26
+ }, gate: BusyGate): Promise<string | null | undefined>;
19
27
  /** 批量删除座位(宿主会话列表的多选 → 批量确认框):一次 `/apply`,撤销名单只收 ok 支的条目 id(跟着组走的幂等条不单列)。
20
- * 只收 id:标题是浮条的呈现,不参与这一次写入。 */
21
- export declare function deleteSessionsFromBatch(deps: OneShotDeps, ids: readonly string[], gate: BusyGate): Promise<ReadonlyMap<string, string>>;
22
- /** 外部恢复座位(撤销横幅):没有 busy 面,不传闸;主视图重开归 settlement,这里只让清单失效。 */
23
- export declare function restoreFromUndo(deps: OneShotDeps, input: {
24
- ids: readonly string[];
25
- single: boolean;
26
- }): Promise<WriteOutcome<'restore'>>;
28
+ * 只收 id:标题是浮条的呈现,不参与这一次写入。**被闸挡回时交回 `undefined`**——这一趟没跑,
29
+ * 浮条与确认框都不许照着它收缩名单或关框(那会吞掉在飞那一趟的结果)。 */
30
+ export declare function deleteSessionsFromBatch(deps: OneShotDeps, ids: readonly string[], gate: BusyGate): Promise<ReadonlyMap<string, string> | undefined>;
31
+ /** 外部恢复座位:拥有整趟——ids 组装、runWrite、三种回执形状各改写一次横幅、成功收自己。
32
+ * 闸在调用方(横幅组件每条通知一枚);座位不传闸(双套会让外层 busy 提前落)。
33
+ * 单条与批量都走**一次** `applyOutcomes('restore', ids)`(`runWrite` 判读;单条带 `single: true`,
34
+ * null 是协议错);失败就把原因换到同一条横幅上,不静默。多条逐条失败时横幅要说清**条数与各条
35
+ * 原因**(原因去重)——只显第一条会把条数与其余原因折没(issue #8 第 5 条);单条失败仍原文上屏。
36
+ * settlement 负责重开删除时被关掉的会话(只认 main-view 自己记过的)并让清单重读;
37
+ * 座位**不传 `dismissUndo`**——部分失败的回滚会让 settlement 先熄灭横幅,随后 `failNotice` 因
38
+ * `ownsNotice` 落空而吞掉失败原文。 */
39
+ export declare function restoreFromUndo(deps: OneShotDeps, notice: UndoNotice, t: Translate): Promise<void>;
27
40
  /** 面板两个 `/apply` 座位共用的那一半:依赖、条目 id、闸、重读与缓存失效。 */
28
41
  interface PanelWriteInput<A extends 'restore' | 'purge'> {
29
42
  deps: OneShotDeps;
30
43
  ids: readonly string[];
31
44
  gate: BusyGate;
32
- refreshAfterWrite(outcome: WriteOutcome<A>): Promise<void>;
33
- clearDetailCache(ids: readonly string[]): void;
45
+ /** 属性声明:这两枚回调被摘下来交给 `runWrite`(转交而不是当方法调),实现不读 `this`。 */
46
+ refreshAfterWrite: (outcome: WriteOutcome<A>) => Promise<void>;
47
+ clearDetailCache: (ids: readonly string[]) => void;
34
48
  }
35
49
  /** 面板恢复另要两样:单条读法,以及 already-gone 回执缺行时反查 sessionId 的当前清单。 */
36
50
  interface PanelRestoreInput extends PanelWriteInput<'restore'> {
37
51
  single?: boolean;
38
52
  entries: readonly TrashEntryLike[] | null;
39
53
  }
40
- /** 面板恢复座位:面板已替这条完成「回来」的结算 ⇒ `panelSeat`;撤销横幅与详情缓存都按落地条目收。 */
41
- export declare function restoreFromPanel(input: PanelRestoreInput): Promise<WriteOutcome<'restore'>>;
42
- /** 面板彻底删除座位:永久删除也让指向这些条目的撤销横幅熄灭(settlement 的 purge 支负责)。 */
43
- export declare function purgeFromPanel(input: PanelWriteInput<'purge'>): Promise<WriteOutcome<'purge'>>;
54
+ /** 面板恢复座位:面板已替这条完成「回来」的结算 ⇒ `panelSeat`;撤销横幅与详情缓存都按落地条目收。
55
+ * 闸被面板那一趟占着时交回 `undefined`(连点不发第二条),面板照它什么都不改。 */
56
+ export declare function restoreFromPanel(input: PanelRestoreInput): Promise<WriteOutcome<'restore'> | undefined>;
57
+ /** 面板彻底删除座位:永久删除也让指向这些条目的撤销横幅熄灭(settlement 的 purge 支负责)。
58
+ * 与恢复座位同一条:闸被占着时交回 `undefined`,这一趟不改任何界面后果。 */
59
+ export declare function purgeFromPanel(input: PanelWriteInput<'purge'>): Promise<WriteOutcome<'purge'> | undefined>;
44
60
  export {};
@@ -37,7 +37,7 @@ export interface SettledFacts<A extends ApplyAction> {
37
37
  */
38
38
  export type WritePresentationByAction = {
39
39
  delete: {
40
- /** 外部写入座位:让回收站面板重读。面板写不传(自己的 `refreshAfterWrite` 就是重读)。 */
40
+ /** 外部写入座位:让回收站面板重读。面板写不传(自己的清单重读 `reload` 不走这条通告)。 */
41
41
  invalidate?(): void;
42
42
  /** 撤销发布:settlement 从回执摘要取出落地条目 id 与请求名单、组好形之后交来;名单**非空**——
43
43
  * 全被同批前一条带走时没有落点,settlement 不发布没有可撤销动作的横幅。座位只决定发布哪一形与那一形带的标题。 */
@@ -14,7 +14,8 @@
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';
@@ -40,7 +41,7 @@ export interface WriteInput<A extends ApplyAction> {
40
41
  readonly action: A;
41
42
  readonly ids: readonly string[];
42
43
  /** 单条写入面:把 N=1 模式交给 receipt 读法(null 折成协议错)。 */
43
- readonly single?: boolean;
44
+ readonly single?: boolean | undefined;
44
45
  /** restore 的 already-gone 回执不带行,它的 sessionId 只能由调用方从当前清单里查。 */
45
46
  readonly sessionIdOfEntry?: (entryId: string) => string | undefined;
46
47
  /** 信封成功时交给 settlement 的呈现材质(摘要里的成功名单可能为空);按 action 的收尾顺序住 `write-settlement.ts`。 */
@@ -51,4 +52,13 @@ export interface WriteInput<A extends ApplyAction> {
51
52
  * 是否真的重读由调用方决定。 */
52
53
  readonly refresh?: (outcome: WriteOutcome<A>) => void | Promise<void>;
53
54
  }
54
- 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>>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 可调项(Config,ADR-0037):三只旋钮 + 本地 Standard Schema。
3
+ *
4
+ * 为什么不用 `@deepseek-ai/schemastery` 写 schema:那是宿主包,值 import 会破本仓「跨插件一律
5
+ * `import type`」的硬约束;Cordis 的 Config 接受 Standard Schema(`@standard-schema/spec`),
6
+ * 手写这一小段即可,不需要把宿主 schema 库搬进插件。代价是宿主配置页拿不到字段级 JSON Schema
7
+ * ——配置以 profile 的 `cordis.patch.yml` 为准(README「可调项」有示例)。
8
+ *
9
+ * 这里只做**默认值、归一与 schema**,不持有运行期现场:`apply` 归一一次,结果作为
10
+ * `CoreDeps.config` 注入;读方从自己的 deps 读,不 import 模块级单例。
11
+ * 客户端 6 秒撤销时长不在这里:它是浏览器半的产品决定,不跨半传。
12
+ */
13
+ export interface SessionDeleteConfig {
14
+ /** 设置页维护卡「孤儿附件」预览清单最多列多少条(数量与字节仍按全量报告)。 */
15
+ readonly orphanPreviewLimit: number;
16
+ /** `GET /attachment` 单次响应字节上限(服务路径与直接 fs fallback 共用)。 */
17
+ readonly maxAttachmentBytes: number;
18
+ /** 附件引用扫描里单个 JSON 引用源的字节上限;超过就按「读不出」fail closed。 */
19
+ readonly maxJsonRefBytes: number;
20
+ }
21
+ export declare const DEFAULT_CONFIG: SessionDeleteConfig;
22
+ /**
23
+ * 把 patch 里的 `config` 归一成完整配置:缺省取默认值,未知键 / 非整数 / 越界都抛。
24
+ * 抛错会让插件激活失败(响亮),而不是悄悄用半套配置跑。
25
+ */
26
+ export declare function normalizeConfig(value: unknown): SessionDeleteConfig;
27
+ /**
28
+ * 插件 Config:Cordis 接受 Standard Schema,`~standard.validate` 交回归一后的配置。
29
+ * 宿主在激活前校验;`apply` 再归一一次(同一份事实,不会漂)。
30
+ */
31
+ export declare const Config: {
32
+ readonly '~standard': {
33
+ readonly version: 1;
34
+ readonly vendor: "dsh-session-delete";
35
+ readonly validate: (value: unknown) => {
36
+ value: SessionDeleteConfig;
37
+ issues?: never;
38
+ } | {
39
+ issues: {
40
+ message: string;
41
+ }[];
42
+ value?: never;
43
+ };
44
+ };
45
+ };
@@ -1,16 +1,19 @@
1
1
  import type { CoreFsPort, FsPort } from './ports/fs-port.js';
2
2
  import type { HostCorePort } from './ports/host-port.js';
3
+ import type { AttachmentsPort } from './ports/attachments-port.js';
3
4
  import type { PresenceCapability } from './ports/presence-port.js';
5
+ import type { SessionDeleteConfig } from './config.js';
4
6
  import type { TrashLedger } from './trash/ledger.js';
5
7
  /** Node 半的基础依赖面:组合根只装配这一颗;动词之间不互相 import 依赖面 interface。
6
8
  * `fs` 不含 `removeDir` / `removeFile`,`host` 不含在场三件——两条危险能力各走 {@link RemovalDeps} 与
7
- * {@link CoreDeps.presence}。 */
9
+ * {@link CoreDeps.presence}。`config` 是 `apply` 归一后的只读快照:读方从 deps 读,不 import 模块级单例。 */
8
10
  export interface CoreDeps {
9
11
  fs: CoreFsPort;
10
12
  host: HostCorePort;
11
13
  ledger: TrashLedger;
12
14
  home: string;
13
15
  presence: PresenceCapability;
16
+ config: SessionDeleteConfig;
14
17
  }
15
18
  /** 会话删除动词的依赖面:只多读时钟;它不删目录(回滚只把搬走的搬回)。 */
16
19
  export type DeleteSessionDeps = CoreDeps & {
@@ -21,5 +24,7 @@ export type RemovalDeps = CoreDeps & {
21
24
  fs: CoreFsPort & Pick<FsPort, 'removeDir' | 'removeFile'>;
22
25
  };
23
26
  /** 组合根装出的全量面:`/apply` 的分发同时够到删除的时钟与永久删除的 `removeDir` / `removeFile`
24
- * (后者也供孤儿附件清理段使用)。 */
25
- export type DeleteDeps = DeleteSessionDeps & RemovalDeps;
27
+ * (后者也供孤儿附件清理段使用)。`attachments` 只给 `attachment` 读段(ADR-0035);动词不拿它。 */
28
+ export type DeleteDeps = DeleteSessionDeps & RemovalDeps & {
29
+ readonly attachments: AttachmentsPort;
30
+ };
@@ -0,0 +1,47 @@
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
+ * 后代也必须是 subagent 起源:普通 fork 的 `parentSession` 只是来源记录,它不进组、也不穿过它收孩子
38
+ * (否则从 subagent 分出去的分支会被当成孤儿永久删掉)。
39
+ */
40
+ export declare function orphanRootsOf<N extends ClosureNode & {
41
+ readonly parentSessionId?: string;
42
+ readonly subagent: boolean;
43
+ }>(corpus: readonly N[], known: Iterable<string>): {
44
+ root: N;
45
+ parentSessionId: string;
46
+ members: N[];
47
+ }[];
@@ -1,5 +1,8 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
+ import { Config } from './config.js';
2
3
  export declare const name = "@xrn1997/dsh-session-delete";
4
+ /** 可调项的 Config(Standard Schema);为什么不用宿主 schema 库见 ADR-0037。 */
5
+ export { Config };
3
6
  /**
4
7
  * 插件依赖:**只写字符串数组**(隔离数据根上实测:写成 `{ required, optional }` 会让插件停在
5
8
  * `pending (waiting for services: required, optional)`)。
@@ -14,24 +17,22 @@ export declare const name = "@xrn1997/dsh-session-delete";
14
17
  * - `agents`:只给 handle 捕获用(`ctx.get('agents')`;缺席时 `ctx.inject(['agents'])` 等服务就绪再装)。
15
18
  * 它同样可能晚于本插件激活,写进插件 inject 只会让整个插件 pending。
16
19
  *
17
- * `webServer` 是**浏览器半取数通道的承载者**(那条薄线):插件用它注册 `/dsh-session-delete-api`
18
- * 前缀路由,浏览器半同源 fetch 那九个段名。它是宿主 web 组成里的硬依赖(`dsh-host-webserver` 的
19
- * `super(ctx, "webServer")`),服务名与 `register({kind:'prefix', path, handler})` 的形状按
20
- * `dsh-host-webserver/lib/index.js` 与 `README.md` 原文读实,不是猜的。
21
- *
22
- * `connection` 只喂请求栅栏的远程支(`src/api/request-trust.ts` ②),按 `ctx.get` 可选读:
23
- * 服务缺席时插件照常激活,远程客户端 fail-closed。它不进 `inject`——写进去会让没有 web 组成的
24
- * profile 整个停在 pending。
20
+ * `connection` 是浏览器半取数通道的唯一承载者:九段注册到 `connection.fetch.register` 的
21
+ * `/api/dsh-session-delete-api/*`,Host/Origin/浏览器认证由宿主 `/api` bridge 完成(ADR-0033)。
22
+ * 支持面是 web UI 与桌面端,二者的标准组合都装 `client-connection`;服务缺席时插件停在 pending
23
+ * ——可见的激活失败,而不是静默地没有入口。`webServer` 不再直接使用:exact route 仍由宿主的
24
+ * `connection` 挂在其 `/api` 前缀上,插件不需要自己碰物理服务器。
25
25
  *
26
26
  * 清册的三层各有各的家:raw 绑定 `src/trash/storage-manifest.ts`、读面(对账 + 读)`src/trash/ledger.ts`、
27
27
  * 数据根 `src/ports/home.ts`。这里只把它们接起来,任何一层都不在组合根里长判据。
28
28
  */
29
29
  export declare const inject: string[];
30
30
  /**
31
- * 装配顺序:**清单 → 端口 → 在场能力 → 动词依赖 → 前缀路由**(`DeleteDeps` 把这些依赖面合成一颗)。
31
+ * 装配顺序:**配置归一 → 清单 → 端口 → 在场能力 → 动词依赖 → 分发器 → exact Fetch 路由**
32
+ * (`DeleteDeps` 把这些依赖面合成一颗;`config` 是归一后的只读快照)。
32
33
  *
33
34
  * 路由用 `ctx.effect` 挂、disposer 归装载它的那根 fiber:插件卸载即摘路由(`register` 返回的
34
- * disposer 是官方契约)。**同一进程里只注册一次**这件事由宿主把住:重复的 `(kind, path)`
35
- * 在 `register` 里直接抛(`webserver: duplicate prefix route "…"`),响亮失败而不是悄悄覆盖。
35
+ * disposer 是官方契约)。**同一进程里只注册一次**这件事由宿主把住:重复的 `(path, method)`
36
+ * 在 `connection.fetch.register` 里直接抛,响亮失败而不是悄悄覆盖。
36
37
  */
37
- export declare function apply(ctx: Context): void;
38
+ export declare function apply(ctx: Context, rawConfig?: unknown): void;
@@ -20,9 +20,8 @@
20
20
  * 的 zstd 版本(明文一大、FCS 上到 4 字节的 `0xa0` 就会算错),帧就切歪,会话日志直接读不出来。
21
21
  *
22
22
  * **尾巴那 4 字节按实测判据收,不按 descriptor 的 bit2**:规则是「跳过去必须正好撞上下一帧的 magic
23
- * 或到 EOF」——猜错不会静默切歪:末尾的自证与块界护栏会抛(护栏是必需的:块头越界时 `|` 读回 0,
24
- * 而 0 的 `Last_Block` 位没置位,少了它块循环每圈只 +3 字节却永不 break,整个进程会卡死;
25
- * 用例覆盖这一条)。
23
+ * 或到 EOF」——猜错不会静默切歪:末尾的自证与块界护栏会抛(护栏是必需的:块头可能被文件截断,
24
+ * 不抛的话块循环每圈只 +3 字节却永不 break,整个进程会卡死;用例覆盖这一条)。
26
25
  */
27
26
  export interface FrameRange {
28
27
  start: number;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 宿主 `attachments` 服务的窄镜像(ADR-0035):只声明本插件读取附件用到的两条。
3
+ *
4
+ * - 服务是**可选**的:`ctx.get('attachments')` 每次调用现查(服务可能晚于插件激活),
5
+ * `available()` 为假时调用方退回旧的直接 fs 路径。
6
+ * - `readImage` 会按记录里的引用**校验字节**;`readFileStream` 校验并分块读出。
7
+ * 两者都不在本文件里重写校验逻辑。
8
+ * - 不 import 宿主 `attachments` 服务的类型包:形状按该服务的运行期契约抄成窄镜像。
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ export type ImageMediaTypeLike = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
12
+ export interface ImageAttachmentRefLike {
13
+ readonly attachmentId: string;
14
+ readonly mediaType: ImageMediaTypeLike;
15
+ readonly bytes: number;
16
+ readonly width: number;
17
+ readonly height: number;
18
+ readonly name?: string;
19
+ }
20
+ export interface FileAttachmentRefLike {
21
+ readonly attachmentId: string;
22
+ readonly name: string;
23
+ readonly bytes: number;
24
+ }
25
+ export interface StoredImageAttachmentLike {
26
+ readonly ref: ImageAttachmentRefLike;
27
+ readonly data: Uint8Array;
28
+ }
29
+ export interface AttachmentsPort {
30
+ /** 服务在场且成员形状可用;调用方用它决定走服务还是直接 fs。 */
31
+ available(): boolean;
32
+ readImage(ref: ImageAttachmentRefLike): Promise<StoredImageAttachmentLike>;
33
+ readFileStream(ref: FileAttachmentRefLike): AsyncIterable<Uint8Array>;
34
+ }
35
+ /** 服务在 `available()` 之后消失/形状变了:调用方可以退回直接 fs,而不是把这次读取判成 io。 */
36
+ export declare class AttachmentsUnavailableError extends Error {
37
+ constructor(message?: string);
38
+ }
39
+ /** 每次调用惰性解析服务:插件与 `attachments` 的激活顺序没有承诺。 */
40
+ export declare function createAttachmentsPort(ctx: Context): AttachmentsPort;
@@ -1,28 +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>;
7
+ ensureDir: (p: string) => Promise<void>;
5
8
  /** 搬目录:**目标已存在一律失败**,跨卷(EXDEV)也失败并说清楚——不替换、也不偷偷改成复制。
6
9
  * Linux 的 `rename` 会静默替代空目录而 Windows 不会,这个差别不能交给操作系统决定。 */
7
- rename(from: string, to: string): Promise<void>;
8
- removeDir(p: string): Promise<void>;
9
- sizeOfDir(p: string): Promise<number>;
10
+ rename: (from: string, to: string) => Promise<void>;
11
+ removeDir: (p: string) => Promise<void>;
12
+ sizeOfDir: (p: string) => Promise<number>;
10
13
  /** 目录下的条目名(**不递归**)。回收站详情要用它在那条目录里挑 `*.jsonl.zstd`。 */
11
- listFiles(p: string): Promise<string[]>;
14
+ listFiles: (p: string) => Promise<string[]>;
12
15
  /** 目录下的**子目录**名(不递归;**跟随软链**)。回收站对账用它找出"清单里还没有行"的条目目录——
13
16
  * 与 `listFiles` 分开是为了让"拒收文件"发生在挑选阶段(回收站根里落进来的杂散文件当外人)。 */
14
- listDirs(p: string): Promise<string[]>;
17
+ listDirs: (p: string) => Promise<string[]>;
15
18
  /** 解析符号链接后的真实路径。递归走目录时防环用:跟随软链之后,目录图不再是树。 */
16
- realpath(p: string): Promise<string>;
19
+ realpath: (p: string) => Promise<string>;
17
20
  /** 整个文件读进内存。实测本机回收站里最大单文件 4.66 MB;**内存口径由逐帧解压把住**(见 `src/log/session-log.ts`),不在这里。 */
18
- readFile(p: string): Promise<Buffer>;
21
+ readFile: (p: string) => Promise<Buffer>;
19
22
  /** 只读的单文件读数:附件渲染与独占计划都用它(不递归、不跟随目录)。 */
20
- statFile(p: string): Promise<{
23
+ statFile: (p: string) => Promise<{
21
24
  size: number;
22
25
  }>;
23
26
  /** 删单个文件(不是目录)。危险能力,只挂到 `RemovalDeps` 上。 */
24
- removeFile(p: string): Promise<void>;
27
+ removeFile: (p: string) => Promise<void>;
25
28
  }
26
- /** 动词的基础面:读与搬移都有,**不拿删除**。删目录与删单文件只挂在 `RemovalDeps` 上(ADR-0006)。 */
29
+ /** 动词的基础面:读与搬移都有,**不拿删除**。删目录与删单文件只挂在 `RemovalDeps` 上(ADR-0027)。 */
27
30
  export type CoreFsPort = Omit<FsPort, 'removeDir' | 'removeFile'>;
28
31
  export declare function createFsPort(): FsPort;
@@ -19,9 +19,9 @@ export interface HostIdentityPort {
19
19
  unarchive(sessionId: string): Promise<void>;
20
20
  detach(sessionId: string, workspaceId: string): Promise<void>;
21
21
  attach(sessionId: string, workspaceId: string): Promise<void>;
22
- /** 归档是 registry **全局**的,与工作区账目无关 ⇒ `workspaceId` 只是调用点对称用的可选入参
23
- * (未分组的会话没有工作区可传)。 */
24
- archive(sessionId: string, workspaceId?: string): Promise<void>;
22
+ /** 归档是 registry **全局**的,与工作区账目无关 ⇒ 只收 `sessionId`,没有工作区入参
23
+ * (实现从不读工作区;未分组的会话同样只有这一条归档路)。 */
24
+ archive(sessionId: string): Promise<void>;
25
25
  }
26
26
  /**
27
27
  * 角色二:**在场通告**——摘掉宿主内存里的活体,并如实通告客户端"这个会话没了 / 回来了"。
@@ -82,6 +82,7 @@ export interface SessionIdentity {
82
82
  readonly projectDir: string;
83
83
  readonly subagent: boolean;
84
84
  }
85
+ /** 闭包里的一条后代:`sessionId` 是宿主交回的原写法(身份只认它,归一比对交给闭包部件)。 */
85
86
  export interface SubagentMember {
86
87
  readonly sessionId: string;
87
88
  readonly projectDir: string;
@@ -95,8 +96,6 @@ export declare const NO_CWD_PROJECT_DIR = "_no-cwd";
95
96
  * → `--D-develop-GitHub-dsh-session-delete--`。`projectDirOf` 与回收站对账重建共用这一份;
96
97
  * 宿主换代先复核算法。 */
97
98
  export declare function projectKey(cwd: string): string;
98
- /** 会话 id 去掉 `session-` 前缀后的形状(同一个父在两代里写法不同,见 {@link HostCorpusPort.subagentMembersOf})。 */
99
- export declare function bareSessionId(id: string): string;
100
99
  /** 这次失败是不是"账本里没那个工作区"(而不是另有故障)。恢复侧靠它把两种失败分开:
101
100
  * 前者按「未分组」收,后者照旧回滚报 `io`——不给真故障静默改归属的机会。 */
102
101
  export declare function isWorkspaceMissingError(error: unknown): boolean;
@@ -3,12 +3,20 @@
3
3
  *
4
4
  * 生产实现住 `src/verbs/member-presence.ts`;组合根装配一次,挂在 `CoreDeps.presence`。这样
5
5
  * “谁可以摘活体、谁可以发通告”由依赖面回答,而不是靠源码扫描维持。
6
+ *
7
+ * 能力面另带一枚**不导出的品牌字段**:形状对了但没盖章的对象满足不了 `PresenceCapability`
8
+ * (`tests/deps.test.ts` 钉住)。品牌是类型层的(`declare` 出来的 unique symbol 没有运行时值),
9
+ * 所以仓内不存在任何能"顺手带上这格"的出口——唯一的盖章处是 `createMemberPresence` 里那一处显式断言。
6
10
  */
11
+ declare const PRESENCE_BRAND: unique symbol;
7
12
  export interface MemberBackFacts {
8
13
  readonly diskBack: boolean;
9
14
  readonly workspaceBack: boolean;
10
15
  }
11
16
  export interface PresenceCapability {
17
+ /** 品牌字段:值不承载语义,只为让结构相似的手拼对象满足不了这一面——键名是不导出的 unique symbol,
18
+ * 别的模块连键都写不出来,唯一的盖章处住 `src/verbs/member-presence.ts`。 */
19
+ readonly [PRESENCE_BRAND]: true;
12
20
  /** 成员已经不在宿主可见列表:摘活体;三态不是 unknown 时发 removed。
13
21
  * `backInTrash: false` 表示目录没退回回收站(恢复回滚失败),此时什么都不做;`facts` 缺席按
14
22
  * 「正常退回」处理(`createMemberPresence` 的缺省),不再留第三种含义。 */
@@ -20,3 +28,4 @@ export interface PresenceCapability {
20
28
  /** 批量恢复的「回来了」通告;单条就是长度为 1 的批。 */
21
29
  announceRestored(ids: readonly string[]): Promise<void>;
22
30
  }
31
+ export {};