@xrn1997/dsh-session-delete 0.3.0 → 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 (74) hide show
  1. package/README.md +21 -15
  2. package/lib/client.js +3317 -2173
  3. package/lib/index.js +1691 -964
  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 +3 -2
  11. package/lib/types/client/batch-confirm.d.ts +3 -3
  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/busy-gate.d.ts +6 -0
  15. package/lib/types/client/confirm-close.d.ts +11 -0
  16. package/lib/types/client/copy.d.ts +48 -38
  17. package/lib/types/client/delete-confirm.d.ts +0 -2
  18. package/lib/types/client/host-rows.d.ts +18 -3
  19. package/lib/types/client/main-view.d.ts +8 -3
  20. package/lib/types/client/maintenance-parts.d.ts +25 -0
  21. package/lib/types/client/maintenance-settings.d.ts +22 -0
  22. package/lib/types/client/markdown.d.ts +9 -1
  23. package/lib/types/client/orphan-maintenance.d.ts +45 -0
  24. package/lib/types/client/receipt.d.ts +75 -5
  25. package/lib/types/client/remote.d.ts +49 -69
  26. package/lib/types/client/store.d.ts +31 -0
  27. package/lib/types/client/trash-confirm.d.ts +2 -15
  28. package/lib/types/client/trash-nav.d.ts +30 -0
  29. package/lib/types/client/trash-panel.d.ts +4 -3
  30. package/lib/types/client/ui/Checkbox.d.ts +1 -1
  31. package/lib/types/client/ui/Modal.d.ts +2 -2
  32. package/lib/types/client/ui/icons.d.ts +6 -11
  33. package/lib/types/client/ui/top-layer.d.ts +10 -0
  34. package/lib/types/client/undo-notices.d.ts +52 -0
  35. package/lib/types/client/undo-toast.d.ts +6 -52
  36. package/lib/types/client/use-column-resize.d.ts +9 -20
  37. package/lib/types/client/use-maintenance.d.ts +45 -0
  38. package/lib/types/client/use-panel-width.d.ts +1 -1
  39. package/lib/types/client/use-trash-panel-controller.d.ts +27 -23
  40. package/lib/types/client/use-trash-query.d.ts +39 -25
  41. package/lib/types/client/write-seats.d.ts +44 -0
  42. package/lib/types/client/write-settlement.d.ts +66 -0
  43. package/lib/types/client/write.d.ts +54 -0
  44. package/lib/types/deps.d.ts +20 -9
  45. package/lib/types/index.d.ts +8 -3
  46. package/lib/types/log/session-log.d.ts +4 -3
  47. package/lib/types/log/zstd-frames.d.ts +14 -8
  48. package/lib/types/ports/agent-handles.d.ts +1 -1
  49. package/lib/types/ports/connection-port.d.ts +33 -0
  50. package/lib/types/ports/fs-port.d.ts +13 -1
  51. package/lib/types/ports/host-port.d.ts +21 -63
  52. package/lib/types/ports/presence-port.d.ts +22 -0
  53. package/lib/types/shared/wire.d.ts +77 -26
  54. package/lib/types/trash/attachments.d.ts +85 -0
  55. package/lib/types/trash/detail.d.ts +29 -6
  56. package/lib/types/trash/entry.d.ts +3 -3
  57. package/lib/types/trash/groups.d.ts +18 -14
  58. package/lib/types/trash/ledger.d.ts +20 -0
  59. package/lib/types/trash/manifest.d.ts +2 -1
  60. package/lib/types/trash/reconcile.d.ts +4 -3
  61. package/lib/types/trash/storage-manifest.d.ts +1 -1
  62. package/lib/types/verbs/batch-carried.d.ts +20 -0
  63. package/lib/types/verbs/delete-session.d.ts +6 -5
  64. package/lib/types/verbs/errors.d.ts +4 -10
  65. package/lib/types/verbs/live-admission.d.ts +10 -0
  66. package/lib/types/verbs/member-journal.d.ts +25 -7
  67. package/lib/types/verbs/member-presence.d.ts +33 -24
  68. package/lib/types/verbs/move-group.d.ts +20 -0
  69. package/lib/types/verbs/orphan-subagents.d.ts +11 -5
  70. package/lib/types/verbs/permanent-removal.d.ts +31 -24
  71. package/lib/types/verbs/purge-trash.d.ts +21 -8
  72. package/lib/types/verbs/restore-session.d.ts +12 -16
  73. package/package.json +4 -2
  74. package/lib/types/client/write-commit.d.ts +0 -10
@@ -1,63 +1,17 @@
1
1
  /**
2
2
  * 撤销提示(docs/ui-spec.md 状态 ③):删除成功后一条可撤销的横幅。
3
3
  *
4
- * 为什么是**独立组件 + 模块级通知台**,而不是死在菜单行里:宿主的菜单行随菜单**一起卸载**
5
- * (真产物 Menu 的注释原文 "the rows unmount with the list"),而提示要在菜单关掉之后还活着。
6
- * 宿主的官方配方同形——会话行的动作把请求写进一个模块级 store,`shell.overlay` 里的
7
- * `workspace.row-toast` 读它(asar 原文:`inject("shell.overlay", … id: "workspace.row-toast" …
8
- * inject: rowToastInjected … }, RowActionToast)`,而
9
- * `rowToastInjected = () => ({ hooks: { toast: rowToast }, dismissToast, undoArchive: unarchiveSession })`)。
10
- * 本文件就是那份 store + 那个常驻组件。
11
- *
12
- * 状态源走 `store.ts` 的 `createStore`(内部用 React 自带的 `useSyncExternalStore`)——
13
- * 不引 `@deepseek-ai/dsh-client-store`(它在平台表内,但这里用不着;React 就够,少一条平台依赖)。
4
+ * 现场与发布住在 `undo-notices.ts`;本文件只做组件,并把通知台与外部恢复座位组合起来。
5
+ * 为什么组件要独立:宿主的菜单行随菜单**一起卸载**(真产物 Menu 的注释原文
6
+ * "the rows unmount with the list"),而提示要在菜单关掉之后还活着。宿主的官方配方同形——
7
+ * 会话行的动作把请求写进一个模块级 store,`shell.overlay` 里的 `workspace.row-toast` 读它。
14
8
  */
15
9
  import { type Translate } from './copy.js';
16
- import { type OneShotDeps } from './remote.js';
10
+ import type { OneShotDeps } from './remote.js';
17
11
  export type UndoToastDeps = OneShotDeps;
18
- /**
19
- * 一次待撤销的事。两形(2026-10-17 起):单条删除(主语是会话名)/批量删除(主语是条数)。
20
- * 同一个 Toast 宿主、同一停留时长;撤销动作按形走——单条 `restore`,批量**一次** `applyOutcomes`。
21
- */
22
- export type UndoNotice = {
23
- /** 每次发布递增:Toast 靠它重挂载,同一段文案连续出现两次也能重新计一次停留。 */
24
- seq: number;
25
- /** 撤销失败时把原因顶上来:这时不再有「撤销」动作,只剩原文。 */
26
- failure?: string;
27
- } & ({
28
- kind: 'one';
29
- /** 回收站条目 id——撤销就是拿它去 `restore`。 */
30
- entryId: string;
31
- /** 删除那一刻的会话名(菜单槽投影的 `displayTitle`;取不到时是空串)。 */
32
- title: string;
33
- } | {
34
- kind: 'batch';
35
- /** 撤销名单:只收成功支里非 null 的条目 id(「被同批带走」的成员跟着组走,不单列)。 */
36
- entryIds: readonly string[];
37
- /** 横幅那句的条数 = 那一次成功支的条数(含幂等带走的)——与撤销名单是两个数,各说各的。 */
38
- n: number;
39
- });
40
- /** 删除成功后发布一条撤销提示(单条)。 */
41
- export declare function publishUndoNotice(input: {
42
- entryId: string;
43
- title: string;
44
- }): void;
45
- /** 批量删除成功后发布一条撤销提示(2026-10-17):覆盖上一条,只报这一次移走的条数。 */
46
- export declare function publishUndoBatchNotice(input: {
47
- entryIds: readonly string[];
48
- n: number;
49
- }): void;
50
- /** 撤销成功后把提示收掉(用户点「撤销」、或横幅自己走完停留时间)。 */
51
- export declare function clearUndoNotice(): void;
52
- /** 供组件订阅。 */
53
- export declare function useUndoNotice(): UndoNotice | null;
54
- /** 测试专用:现场与 seq 归零,用例之间互不串味。 */
55
- export declare function resetUndoNotice(): void;
56
- /** 提示停留时长。**6 秒** 定死在本行(比 Toast 默认的 3 秒长)——`docs/ui-spec.md` §2 状态 ③ 只指到这里,不抄这个数。 */
57
- export declare const UNDO_HOLD_MS = 6000;
58
12
  /**
59
13
  * 常驻的撤销提示宿主。挂 `shell.overlay`(框架级浮层;槽位说明原文 "a toast stack or a status
60
- * pill all belong here",见 asar 里 `shell.overlay` 的声明):没有待撤销的事时什么都不渲染。
14
+ * pill all belong here"):没有待撤销的事时什么都不渲染。
61
15
  */
62
16
  export declare function UndoToast({ deps, t }: {
63
17
  deps: UndoToastDeps;
@@ -1,26 +1,15 @@
1
1
  /**
2
- * 清单列与详情列之间那根**可拖的宽度把手**(2026-10-15 加,`docs/ui-spec.md` 的「面板与清单」)。
2
+ * 清单列与详情列之间那根可拖的宽度把手(docs/ui-spec.md 的「面板与清单」)。
3
3
  *
4
- * 三件事住在这里,因为它们互相咬合、分开测各自都不成立:
5
- * ① **宽度夹在两个界之间**。下限由调用方给(回收站那条的来历写在 `trash-panel.tsx`:真 Chromium
6
- * 上扫出来的,约束不是"两颗按钮放得下"——它们 `flex:none`,再窄也是文字列缩;而是「删除于 … ·
7
- * 大小」那一行不再被省略号截断),上限 = 面板宽 − `reserve`。**`reserve` 的数值与来历都住在调用方**
8
- * (`DETAIL_RESERVE`,那三件东西怎么加出来的见 `trash-panel.tsx` 该常量的注释与几何用例):
9
- * 这一层不复制那份算式——上一版在这里钉过一个"480 ⇒ 900 那档正好回 420",调用方改到 489 之后
10
- * 它就成了假数,而 900 那档的真实读数是**清单被夹到 411**(比默认窄 9px,还能往里拖)。
11
- * ② **夹在"读"的时候也做一次**。窗口变窄不触发任何写入,若只在写入时夹,之前拖出来的超宽
12
- * 清单会一直留到下次交互 ⇒ `listWidth` 每次渲染都按当前 `panelWidth` 重算。
13
- * ③ **按下才算在拖**。指针捕获挂在把手元素上(`setPointerCapture`),move 只可能来自这一根把手;
14
- * `origin === null` 时 move 直接返回——鼠标扫过不该改布局。抬起不用显式 release:规范里
15
- * pointerup/pointercancel 会隐式释放捕获,多写那一行只会引入"指针已不活跃"的抛错面。
16
- * **但"结束"不能只挂在把手上**:拖动途中把手会被卸载(面板窄过断点、或清单被读空 ⇒ `split`
17
- * 翻了),而卸载后的节点收不到任何事件 ⇒ `resizing` 永远挂着:面板根留着 `.sd-resizing`
18
- * (关文字选中),下次变宽时把手还带着 `data-dragging` 长回来。所以拖住期间另在 window 上兜
19
- * 一层 `pointerup`/`pointercancel`(复用同一个 `stop`)。move 仍然只认把手,捕获那套一点没动。
4
+ * 三件事住在这里:
5
+ * ① 宽度夹在两个界之间:下限由调用方给,上限 = 面板宽 − reserve。
6
+ * reserve 的数值与来历住在调用方(DETAIL_RESERVE),这一层不复制算式。
7
+ * ② 夹在“读”的时候也做一次:窗口变窄不触发写入,listWidth 每次渲染按当前 panelWidth 重算。
8
+ * ③ 按下才算在拖:指针捕获挂在把手元素上;拖动途中把手可能卸载,所以 window 上另兜一层
9
+ * pointerup/pointercancel 复用 stop;move 仍只认把手。
20
10
  *
21
- * 键盘那条不是装饰:`role="separator"` 可聚焦、←→ 各一格、Home 回默认、End 到上限。
22
- * 这一代的 `--dsw-focus-ring-color` 是 transparent(宿主自己那份焦点环靠别的属性撑),
23
- * 所以焦点态的可见性由 `ui.css` 里 `.sd-width-handle:focus-visible::after` 那一条负责。
11
+ * 键盘:role=separator、←→ 各一格、Home 回默认、End 到上限;焦点态可见性由 ui.css 的
12
+ * .sd-width-handle:focus-visible::after 负责。
24
13
  */
25
14
  import { type KeyboardEvent, type PointerEvent } from 'react';
26
15
  export interface ColumnResizeArgs {
@@ -0,0 +1,45 @@
1
+ import type { Translate } from './copy.js';
2
+ import { type AttachmentOrphanReportWire, type OrphanReportWire, type SessionDeleteRemote } from './remote.js';
3
+ /** 维护卡只需要这四段:两份报告 + 孤儿会话的批量写 + 孤儿附件的聚合写。 */
4
+ export type MaintenanceDeps = Pick<SessionDeleteRemote, 'scanOrphans' | 'applyOutcomes' | 'scanAttachmentOrphans' | 'cleanAttachmentOrphans'>;
5
+ /** 一段在飞的动作:检查只读,清理会写。状态行靠它分开说「检查中…」与「清理中…」;`null` = 空闲。 */
6
+ export type MaintenancePhase = 'scanning' | 'cleaning';
7
+ export interface Maintenance {
8
+ readonly orphanReport: OrphanReportWire | null;
9
+ readonly orphanFailure: string | null;
10
+ /** 「这一组已经不在」不是失败:单独一条中性提示。 */
11
+ readonly orphanNotice: string | null;
12
+ /** 上一趟孤儿会话清理顺带清掉的附件读数(`.dsh` 内独占图片副本)。重查读不出来,和
13
+ * `orphanNotice` 一样是过去那一趟的回执;下一次检查 / 清理会被收掉。 */
14
+ readonly orphanAttachmentNotice: string | null;
15
+ readonly orphanConfirm: boolean;
16
+ readonly orphanBusy: boolean;
17
+ /** 在飞的是检查还是清理;`null` = 空闲。写动作在飞时状态行据此说「清理中…」。 */
18
+ readonly orphanPhase: MaintenancePhase | null;
19
+ /** 手动检查(`GET /orphans`)。失败回 `null` + 原文,不折成零条。 */
20
+ scanOrphans(): Promise<void>;
21
+ /** 「查看并清理」:先收掉旧提示再开确认框。 */
22
+ openOrphanPreview(): void;
23
+ /** `/apply` 的 `orphan-clean`:清完必重查;中性 / 失败 / 部分计数按回执说。 */
24
+ cleanOrphans(): Promise<void>;
25
+ setOrphanConfirm(open: boolean): void;
26
+ readonly attachmentReport: AttachmentOrphanReportWire | null;
27
+ readonly attachmentFailure: string | null;
28
+ /** 上一趟清理的回执(已清 / 跳过 / 逐文件失败),重查读不出来。 */
29
+ readonly attachmentNotice: string | null;
30
+ readonly attachmentConfirm: boolean;
31
+ readonly attachmentBusy: boolean;
32
+ /** 在飞的是检查还是清理;`null` = 空闲。 */
33
+ readonly attachmentPhase: MaintenancePhase | null;
34
+ /** 手动检查(`GET /attachment-orphans`)。 */
35
+ scanAttachmentOrphans(): Promise<void>;
36
+ /** 「查看并清理」:先收掉旧提示再开确认框。 */
37
+ openAttachmentPreview(): void;
38
+ /** `POST /attachment-clean`:清完必重查;`removed` / `failed` / `skipped` 按回执说。 */
39
+ cleanAttachmentOrphans(): Promise<void>;
40
+ setAttachmentConfirm(open: boolean): void;
41
+ }
42
+ export declare function useMaintenance({ deps, t }: {
43
+ deps: MaintenanceDeps;
44
+ t: Translate;
45
+ }): Maintenance;
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 量的是**盒子的 border-box 宽**(`getBoundingClientRect().width`),不是 `contentRect.width`:
8
8
  * 后者已经扣掉内边距,而"多宽才分两栏"那个阈值说的是整块盒子——拿扣过内边垫的数去比就是**扣两次**,
9
- * 结果明明够宽却退化成下钻(2026-10-15 真机反馈后定的判据)。
9
+ * 结果明明够宽却退化成下钻(真机反馈后定的判据)。
10
10
  *
11
11
  * 首帧读一次当前宽度,之后跟着 RO 走;读到 0 就是窄档 ⇒ **窄档是默认、宽档要被证明**。
12
12
  * RO 缺席(不可能的宿主)时停在窄档:功能一条不少,只是同屏两栏变成下钻。
@@ -1,33 +1,33 @@
1
+ import type { BusyGate } from './busy-gate.js';
1
2
  import { type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
2
- import type { PendingAction } from './trash-confirm.js';
3
3
  import type { TrashDetailState } from './trash-detail.js';
4
- import type { TrashQuery } from './use-trash-query.js';
5
- /** 面板的一枚全局闸:写动作在飞时 `busy` 为真,`around` 把一次任务整个包在"这一趟还算忙"里。 */
6
- export interface TrashPanelGate {
7
- readonly busy: boolean;
8
- around<T>(task: () => Promise<T>): Promise<T>;
9
- }
10
- /**
11
- * 闸本体(从 `trash-panel.tsx` 的函数体原样收进来):任务落地或抛错都把 `busy` 放回 false。
12
- * 不做引用计数:面板同一时刻只允许一趟写动作(按钮在 `busy` 期间全被按住),多一层计数只会
13
- * 把"这一趟什么时候算完"变成第二个判据。
14
- *
15
- * 返回值经 `useMemo` 稳定:`gate` 进 `restore` / `confirmApply` 的 `useCallback` 依赖,每次渲染
16
- * 换个新对象会让那两个回调跟着失效,进而把引用它们的 effect 也带着重跑。
17
- */
18
- export declare function useTrashPanelGate(): TrashPanelGate;
4
+ import { type TrashQuery } from './use-trash-query.js';
5
+ /** 待确认的动作:一条记录、整个回收站、或多选挑出来的一批(`ids` 已按清单顺序排好)。
6
+ * 类型归状态机,视图只 import 它来渲染。 */
7
+ export type PendingAction = {
8
+ kind: 'entry';
9
+ entry: TrashEntryLike;
10
+ } | {
11
+ kind: 'all';
12
+ } | {
13
+ kind: 'entries';
14
+ ids: readonly string[];
15
+ } | {
16
+ kind: 'restore';
17
+ ids: readonly string[];
18
+ };
19
19
  /**
20
- * 控制器要的面。`query` 只取三样:清单(选中从它里面找)、重读(失效通告与写动作之后用)、
21
- * 清空(面板的写动作那一半)。`ordered` 是键盘/自动选中用的"屏幕上的顺序"(只含展开的组),
20
+ * 控制器要的面。`query` 取五样:清单(选中从它里面找)、`refreshAll`(挂载与显式重试)、
21
+ * `refreshAfterWrite`(失效与写后)、清空(面板的写动作那一半)、`noteAttachmentCleanup`(purge 回执里
22
+ * 附件那半句交给查询侧拼句子上屏)。`ordered` 是键盘/自动选中用的"屏幕上的顺序"(只含展开的组),
22
23
  * 由面板算好传进来——布局常量不搬进控制器。
23
24
  */
24
25
  export interface TrashPanelControllerInput {
25
26
  deps: Pick<SessionDeleteRemote, 'detail' | 'applyOutcomes'>;
26
- gate: TrashPanelGate;
27
- query: Pick<TrashQuery, 'entries' | 'refresh' | 'emptyTrash'>;
27
+ gate: BusyGate;
28
+ query: Pick<TrashQuery, 'list' | 'refreshAll' | 'refreshAfterWrite' | 'emptyTrash' | 'noteAttachmentCleanup'>;
28
29
  ordered: readonly TrashEntryLike[];
29
30
  canAutoSelect: boolean;
30
- focusRow(entryId: string): void;
31
31
  }
32
32
  export interface TrashPanelController {
33
33
  readonly busy: boolean;
@@ -37,6 +37,8 @@ export interface TrashPanelController {
37
37
  readonly detail: TrashDetailState;
38
38
  /** 待确认的那一批(确认框的开合);确认落地后由控制器收起。 */
39
39
  readonly pending: PendingAction | null;
40
+ /** 恢复确认框要摆的这一批(按勾选顺序;清单里已找不到的 id 滤掉)。 */
41
+ readonly pendingEntries: readonly TrashEntryLike[];
40
42
  /** 恢复/彻底删除/批量 的失败原文,按条目 id 存:切走再回来还在。 */
41
43
  readonly actionFailure: Readonly<Record<string, string>>;
42
44
  select(entry: TrashEntryLike, options?: {
@@ -44,8 +46,10 @@ export interface TrashPanelController {
44
46
  }): Promise<void>;
45
47
  retry(): void;
46
48
  back(): void;
47
- setPending(pending: PendingAction | null): void;
48
- /** 单条恢复(`/apply` 的 N=1):成功后丢缓存并重读清单,失败原文落回这一行。 */
49
+ /** 构造待确认动作(唯一入口:新增一种确认只改 `PendingAction` 与确认框,不再加一串转调)。 */
50
+ request(action: PendingAction): void;
51
+ cancelPending(): void;
52
+ /** 单条恢复(`/apply` 的 N=1):成功后丢缓存,写完无论成败都重读清单,失败原文落回这一行。 */
49
53
  restore(entry: TrashEntryLike): Promise<void>;
50
54
  /** 确认框上的「确认」:按 `pending.kind` 分派到空清或 `confirmApply`。 */
51
55
  confirmPurge(): Promise<void>;
@@ -1,32 +1,46 @@
1
- import { type OrphanReportWire, type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
1
+ import type { BusyGate } from './busy-gate.js';
2
+ import { type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
2
3
  import type { Translate } from './copy.js';
3
- export type TrashQueryDeps = Pick<SessionDeleteRemote, 'list' | 'scanOrphans' | 'applyOutcomes' | 'emptyTrash'>;
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;
4
25
  export interface TrashQuery {
5
- /** `null` = 还没读到(第一次读或读失败);与"空的"是两件事。 */
6
- readonly entries: readonly TrashEntryLike[] | null;
7
- readonly listFailure: string | null;
8
- /** `null` = 还没扫到;失败时也置回 `null`(不拿空报告冒充干净)。 */
9
- readonly orphanReport: OrphanReportWire | null;
10
- readonly orphanFailure: string | null;
11
- /** 「这一组已经不在」不是失败:单独一条中性提示。 */
12
- readonly orphanNotice: string | null;
13
- readonly orphanConfirm: boolean;
14
- setOrphanConfirm(open: boolean): void;
15
- /** 「查看并清理」:把上一条失败/中性提示清掉再开预览框(旧提示不该跟新一次预览并排出现)。 */
16
- openOrphanPreview(): void;
17
- /** 交回**这一次读数是否真的落地**:孤儿扫描吃的是对账后的清册,清单没读成就不能扫。 */
18
- reload(): Promise<boolean>;
19
- scanOrphans(): Promise<void>;
20
- /** "重读一遍" = 先对账并读清单,再扫残留;清单没读成就不扫。 */
21
- refresh(): Promise<void>;
22
- /** 清理那批无主子会话(一段 `/apply` 的第五个动作),逐条结果落成"已不在了 / N 组未清理"。 */
23
- cleanOrphans(): Promise<void>;
24
- /** 清空回收站(聚合回执):成功就重读,失败就把原文摆到清单那块。交回是否成功。 */
26
+ readonly list: TrashListState;
27
+ /** 最近一次写动作(purge / 清空)的附件读数,已经拼成一句话:**最新的写赢**。
28
+ * 一次没有附件读数的 purge / 清空、或这一趟写失败(没拿到回执)都会把它清成 `null`,不让上一条的
29
+ * 读数冒充这一趟;删除与恢复不碰它。它是**过去那一趟的回执**、不是清单状态:清单重读读不出来。 */
30
+ readonly attachmentNotice: string | null;
31
+ /** 把一份附件读数摆上屏(`undefined` = 这一趟没有读数,旧提示收掉);句子怎么拼归这里,调用方只管转交。 */
32
+ noteAttachmentCleanup(a: AttachmentCleanupWire | undefined): void;
33
+ /** 重读清单;挂载与显式「重试」用它。面板没有第二条扫描:孤儿检查是设置页维护卡的手动动作。 */
34
+ refreshAll(): Promise<void>;
35
+ /** 写后/失效通告:与 `refreshAll` 同一条读。两个名字留着是因为调用点问的是两件事
36
+ * ("用户要看现在的"与"别处动过了,跟上"),不是两条路径。清单读失败时旧行进 `unknown`。 */
37
+ refreshAfterWrite(): Promise<void>;
38
+ /** 清空回收站(聚合回执):成功就熄掉撤销横幅并重读,失败就把清单置为未知并摆上原文。交回是否成功。 */
25
39
  emptyTrash(): Promise<boolean>;
26
40
  }
27
- export declare function useTrashQuery({ deps, t, around, }: {
41
+ export declare function useTrashQuery({ deps, t, gate, }: {
28
42
  deps: TrashQueryDeps;
29
43
  t: Translate;
30
- /** 把"这一趟还算忙"的一枚全局闸套在一次动作外(面板的 `busy`)。 */
31
- around: <T>(task: () => Promise<T>) => Promise<T>;
44
+ /** 面板的一枚全局闸:清空这条写动作把整趟包在里面(面板的 `busy`)。 */
45
+ gate: BusyGate;
32
46
  }): TrashQuery;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 逐条 `/apply` 的写入座位:一次写入发生在哪个界面位置,就在哪个函数里绑定呈现材质、闸与重读通道。
3
+ *
4
+ * 收尾顺序住 `write-settlement.ts`(ADR-0013),面板的重读策略住 `use-trash-query.ts`;
5
+ * 座位只拥有「这次是谁在写、交哪些材质、闸在哪」。
6
+ *
7
+ * 边界:不判回执、不决定收尾顺序、不拥有各面的本地状态;面板座位的闸由面板传入,不另造第二枚。
8
+ * 无主子会话清理**不是**座位:它只在插件设置页的维护卡里手动触发(ADR-0021),
9
+ * 读数、闸与重查都归 `use-maintenance.ts`。
10
+ */
11
+ import type { BusyGate } from './busy-gate.js';
12
+ import type { OneShotDeps, TrashEntryLike } from './remote.js';
13
+ import { type WriteOutcome } from './write.js';
14
+ /** 会话行菜单的单删座位:一条会话、`single` 读法、成功发单条撤销横幅。交回这条会话的失败原文。 */
15
+ export declare function deleteSessionFromMenu(deps: OneShotDeps, input: {
16
+ sessionId: string;
17
+ title: string;
18
+ }, gate: BusyGate): Promise<string | null>;
19
+ /** 批量删除座位(宿主会话列表的多选 → 批量确认框):一次 `/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'>>;
27
+ /** 面板两个 `/apply` 座位共用的那一半:依赖、条目 id、闸、重读与缓存失效。 */
28
+ interface PanelWriteInput<A extends 'restore' | 'purge'> {
29
+ deps: OneShotDeps;
30
+ ids: readonly string[];
31
+ gate: BusyGate;
32
+ refreshAfterWrite(outcome: WriteOutcome<A>): Promise<void>;
33
+ clearDetailCache(ids: readonly string[]): void;
34
+ }
35
+ /** 面板恢复另要两样:单条读法,以及 already-gone 回执缺行时反查 sessionId 的当前清单。 */
36
+ interface PanelRestoreInput extends PanelWriteInput<'restore'> {
37
+ single?: boolean;
38
+ entries: readonly TrashEntryLike[] | null;
39
+ }
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'>>;
44
+ export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 一次逐条 `/apply` 写入成功后的**收尾顺序**,按 action 各一条固定序列。
3
+ *
4
+ * 调用方只交呈现材质(`WritePresentation`),顺序与 `anySuccess` 门只住这里;`runWrite` 在拿到
5
+ * `WriteSuccess` 后、`refresh` 前调一次。为什么按 action 收成一处见 ADR-0013。
6
+ *
7
+ * 四条判据:
8
+ * - **跨槽位在信封成功时就跑**:`afterDelete` / `afterRestore` 与 `invalidate` 不管逐条成败都跑
9
+ * (回滚失败可能留下半套,回收站清单不能继续摆旧行);呈现钩子(发布/熄灭撤销、遗忘主视图、
10
+ * 清详情缓存)只在至少一条落地(`anySuccess`)时跑——没有落点的撤销是假动作。delete 的撤销
11
+ * 另要求落地名单非空:全部被同批前一条带走时没有可撤销的条目。
12
+ * - **面板恢复用 `panelSeat: true` 明说**(「缺席」不再承载语义):外部恢复(撤销横幅)由 settlement
13
+ * 调 `afterRestore` 重开;面板恢复自带「回来」的结算,只 `forgetMainView`、不重开、不抢焦点。
14
+ * 判据住 `src/client/main-view.ts` 的 `forgetMainView`。
15
+ * - **撤销名单由摘要给、座位发布**:delete 支从 `summary.landedIds`(域是条目 id)取名单、用
16
+ * `summary.successIds.length` 作条数,组成 `{ entryIds, n }` 交给座位,座位只决定发布哪一形
17
+ * (单条带标题、批量带条数)——那句文案与它的键住 `undo-toast.tsx` 与 `copy.ts`;purge 支在
18
+ * `anySuccess` 时熄灭指向这些条目的横幅——留着就是一条注定失败的「撤销」。
19
+ * - **`count` 不结算**;`orphan-clean` 没有跨槽位收尾(维护卡的 `use-maintenance.ts` 清完自己重查)。
20
+ * 不直接读 DOM、不问宿主:主视图收尾只经 `main-view.ts` 的 `afterDelete` / `afterRestore`
21
+ * (服务缺席或抛错都静默退回);重读仍归各调用方的 `refresh`(面板那侧的最新票在 `use-trash-query.ts`)。
22
+ */
23
+ import type { ApplyAction } from '../shared/wire.js';
24
+ import type { ReceiptSummary } from './receipt.js';
25
+ /** 一次写入里与渲染无关的那部分:动作、请求 id、以及唯一的那份回执摘要。 */
26
+ export interface SettledFacts<A extends ApplyAction> {
27
+ readonly action: A;
28
+ readonly ids: readonly string[];
29
+ readonly summary: ReceiptSummary;
30
+ }
31
+ /**
32
+ * 按 action 切开的一族呈现材质:每个座位只能拿到自己动作那几手,形状对不上的钩子在类型面就不存在。
33
+ * - `delete` 的 `publishUndo` 存在即「这个座位要发撤销横幅」;它拿到的是 settlement 从摘要取出并组好的落地名单。
34
+ * - `restore` 的 `panelSeat: true` 才是「面板已替这条完成『回来』的结算」,不再靠缺席某个钩子暗示。
35
+ * - `count` / `orphan-clean` 没有呈现材质(用 `noPresentation`)。
36
+ * 钩子都是同步的:settlement 在 `refresh` 前一次跑完,不 await。
37
+ */
38
+ export type WritePresentationByAction = {
39
+ delete: {
40
+ /** 外部写入座位:让回收站面板重读。面板写不传(自己的 `refreshAfterWrite` 就是重读)。 */
41
+ invalidate?(): void;
42
+ /** 撤销发布:settlement 从回执摘要取出落地条目 id 与请求名单、组好形之后交来;名单**非空**——
43
+ * 全被同批前一条带走时没有落点,settlement 不发布没有可撤销动作的横幅。座位只决定发布哪一形与那一形带的标题。 */
44
+ publishUndo?(landed: {
45
+ readonly entryIds: readonly [string, ...string[]];
46
+ readonly n: number;
47
+ }): void;
48
+ };
49
+ restore: {
50
+ invalidate?(): void;
51
+ /** 面板恢复座位:true = 面板已替这条完成「回来」的结算,只遗忘、不重开、不抢焦点。 */
52
+ panelSeat?: true;
53
+ dismissUndo?(entryIds: readonly string[]): void;
54
+ clearDetailCache?(entryIds: readonly string[]): void;
55
+ };
56
+ purge: {
57
+ dismissUndo?(entryIds: readonly string[]): void;
58
+ clearDetailCache?(entryIds: readonly string[]): void;
59
+ };
60
+ count: Record<string, never>;
61
+ 'orphan-clean': Record<string, never>;
62
+ };
63
+ export type WritePresentation<A extends ApplyAction> = WritePresentationByAction[A];
64
+ /** 没有呈现材质的座位用它:抛掉按 action 的联合,调用点不用各自造一个空对象字面量。 */
65
+ export declare function noPresentation<A extends ApplyAction>(action: A): WritePresentation<A>;
66
+ export declare function settleWrite<A extends ApplyAction>(success: SettledFacts<A>, presentation: WritePresentation<A>): void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 一次逐条 `/apply` 写入的**唯一生命周期**:apply → 解信封 → `readReceipt` → 回执摘要 → settlement → refresh。
3
+ *
4
+ * 收尾顺序与 `anySuccess` 门住 `src/client/write-settlement.ts`(为什么按 action 收见 ADR-0013);
5
+ * 这里只管时机、信封判读、整趟的闸与 `refresh` 的交法。清空回收站是聚合回执(`/empty`),收尾仍住
6
+ * `use-trash-query.ts` 的 `emptyTrash`。
7
+ *
8
+ * 五条判据:
9
+ * - `ok` 只说**信封**成不成;逐条成败在 `failures` 里,调用方自己决定怎么上屏。
10
+ * - 顶层失败把同一句原文铺到每个 id,不结算;`refresh` 照样交(成败都在 outcome 里)。
11
+ * - **信封成功就结算**(逐条全失败也跑:批量写入的失效通告不能漏);`anySuccess` 与落地名单由
12
+ * `receipt.ts` 的回执摘要算好,settlement 按它上闸。调用方只交呈现材质,再按 `outcome` 决定是否真的重读。
13
+ * 结算在 `refresh` 之前:列表重读看得到收尾后的现场。
14
+ * - `single: true` 时把 N=1 模式交给 `receipt.ts` 的读法;单条 null 在那一处折成 failed,
15
+ * 这里不再重复守卫。
16
+ * - **闸包住整趟**:给了 `gate` 就把 apply → settle → refresh 一起包进去——refresh 落在闸外时,
17
+ * 旧清单还在屏上,同一个动作能被再点一次;调用方不再自己 `around`。
18
+ */
19
+ import type { BusyGate } from './busy-gate.js';
20
+ import { type OneShotDeps } from './remote.js';
21
+ import { type SettledFacts, type WritePresentation } from './write-settlement.js';
22
+ import type { ApplyAction, RemoteFailure } from '../shared/wire.js';
23
+ export type WriteSuccess<A extends ApplyAction> = SettledFacts<A> & {
24
+ readonly ok: true;
25
+ /** 逐条失败原文(failed / neutral / missing),按 id。这是失败表的**唯一落点**:
26
+ * 摘要只带聚合与落地 id,不再复制一份逐条原文。 */
27
+ readonly failures: ReadonlyMap<string, string>;
28
+ };
29
+ export interface WriteFailure<A extends ApplyAction> {
30
+ readonly ok: false;
31
+ readonly action: A;
32
+ readonly ids: readonly string[];
33
+ readonly failure: RemoteFailure;
34
+ /** 顶层失败时同一句原文铺到每个请求 id。 */
35
+ readonly failures: ReadonlyMap<string, string>;
36
+ }
37
+ export type WriteOutcome<A extends ApplyAction> = WriteSuccess<A> | WriteFailure<A>;
38
+ export interface WriteInput<A extends ApplyAction> {
39
+ readonly deps: OneShotDeps;
40
+ readonly action: A;
41
+ readonly ids: readonly string[];
42
+ /** 单条写入面:把 N=1 模式交给 receipt 读法(null 折成协议错)。 */
43
+ readonly single?: boolean;
44
+ /** restore 的 already-gone 回执不带行,它的 sessionId 只能由调用方从当前清单里查。 */
45
+ readonly sessionIdOfEntry?: (entryId: string) => string | undefined;
46
+ /** 信封成功时交给 settlement 的呈现材质(摘要里的成功名单可能为空);按 action 的收尾顺序住 `write-settlement.ts`。 */
47
+ readonly presentation?: WritePresentation<A>;
48
+ /** 这一趟的闸:给了就把 apply → settle → refresh 整趟包进 busy。refresh 不许落在闸外。 */
49
+ readonly gate?: BusyGate;
50
+ /** 写完(顶层失败或信封成功,无论逐条成败)后的重读策略;成败与 `anySuccess` 都在 outcome 里,
51
+ * 是否真的重读由调用方决定。 */
52
+ readonly refresh?: (outcome: WriteOutcome<A>) => void | Promise<void>;
53
+ }
54
+ export declare function runWrite<A extends ApplyAction>(input: WriteInput<A>): 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';
3
- import type { ManifestPort } from './trash/manifest.js';
4
- /** Node 半的基础依赖面:组合根只装配这一颗;动词之间不互相 import 依赖面 interface。 */
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';
4
+ import type { TrashLedger } from './trash/ledger.js';
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;
8
- manifest: ManifestPort;
9
+ fs: CoreFsPort;
10
+ host: HostCorePort;
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,15 +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
- * 清单(storage domain 绑定)与数据根各有一个家:`src/trash/storage-manifest.ts`、`src/ports/home.ts`。
22
+ * `connection` 只喂请求栅栏的远程支(`src/api/request-trust.ts` ②),按 `ctx.get` 可选读:
23
+ * 服务缺席时插件照常激活,远程客户端 fail-closed。它不进 `inject`——写进去会让没有 web 组成的
24
+ * profile 整个停在 pending。
25
+ *
26
+ * 清册的三层各有各的家:raw 绑定 `src/trash/storage-manifest.ts`、读面(对账 + 读)`src/trash/ledger.ts`、
27
+ * 数据根 `src/ports/home.ts`。这里只把它们接起来,任何一层都不在组合根里长判据。
23
28
  */
24
29
  export declare const inject: string[];
25
30
  /**
26
- * 装配顺序:**清单 → 端口 → 动词依赖 → 前缀路由**(`DeleteDeps` 就是前两者的合一)。
31
+ * 装配顺序:**清单 → 端口 → 在场能力 → 动词依赖 → 前缀路由**(`DeleteDeps` 把这些依赖面合成一颗)。
27
32
  *
28
33
  * 路由用 `ctx.effect` 挂、disposer 归装载它的那根 fiber:插件卸载即摘路由(`register` 返回的
29
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: {