@wangxiaocuo/dsh-session-trash 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.en.md +25 -8
- package/README.md +25 -8
- package/client.js +117 -6
- package/index.js +2 -2
- package/lib/cleanup.js +23 -16
- package/lib/eligibility.js +5 -5
- package/lib/errors.js +9 -6
- package/lib/fsops.js +8 -6
- package/lib/http.js +45 -19
- package/lib/inventory.js +40 -16
- package/lib/members.js +6 -6
- package/lib/state.js +11 -9
- package/lib/store.js +45 -16
- package/lib/trash-service.js +103 -66
- package/package.json +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录这个插件的所有重要变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.6.5] - 2026-11-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 回收站页可以清理不含会话日志的空无主对象(`POST /orphans/forget`);可能含日志的无主对象继续保留,供人工处理。
|
|
12
|
+
- CI 增加 `lib/**` 覆盖率门槛(行 85 / 分支 70 / 函数 84)和 README 的 CI badge。
|
|
13
|
+
- 新增 tag 触发的 release workflow:typecheck、单测、smoke、真实 Host e2e 通过后 `npm publish --provenance` 并创建 GitHub Release;使用 npm Trusted Publishing,无需 `NPM_TOKEN`。
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `GET /state` 改为纯只读;页面打开时由客户端显式调用 `POST /sweep` 触发节流维护,读请求不再可能触发永久删除。
|
|
18
|
+
- 锁竞争:Host 在报 `LOCKED` 前短暂重试,客户端对 `LOCKED` 自动重试一次;`trash()` 的 lineage/preflight 移到锁外,缩短持锁时间。
|
|
19
|
+
- 收紧 `lib/errors.js` 和 `lib/fsops.js` 的类型(`any` 换成 `unknown` 与具体形状)。
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
|
|
23
|
+
- 覆盖页面维护扫描(成功/节流/失败)、空无主对象清理、锁重试和客户端 LOCKED 重试。
|
|
24
|
+
|
|
25
|
+
## [0.6.4] - 2026-11-11
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- e2e 支持 `DSH_E2E_DSH_BIN` 启动 npm 安装的 `@deepseek-ai/dsh` web Host,不再依赖桌面版;Linux/Windows 也能跑真实 Host 端到端测试。
|
|
30
|
+
- CI 新增真实 Host e2e job(Ubuntu + Windows),失败时输出 check annotations。
|
|
31
|
+
- 新增 `pnpm typecheck`(`tsc --checkJs`,先覆盖 `lib/` 和 `index.js`)并接入 CI;DSH 服务通过 `types/dsh-context.d.ts` 的 ambient 声明提供类型。
|
|
32
|
+
- 新增 jsdom + 真实 React 渲染测试(`test/client-dom.test.js`),覆盖行菜单的方向键/Escape/Tab/外点关闭,以及确认对话框的焦点陷阱和焦点恢复。
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- ActionMenu:Escape 不再冒泡到宿主设置弹层;Tab 会关闭菜单并让焦点自然移动;分隔符使用 `role="separator"`。
|
|
37
|
+
- ConfirmDialog:打开它的行或按钮被移除后,焦点回退到列表面板,不再丢到 `body`。
|
|
38
|
+
|
|
5
39
|
## [0.6.3] - 2026-11-11
|
|
6
40
|
|
|
7
41
|
### Added
|
|
@@ -78,6 +112,8 @@
|
|
|
78
112
|
|
|
79
113
|
- 回收站工具栏的「永久删除全部」与二次确认;`missing` 记录只移出索引,不删除会话文件。
|
|
80
114
|
|
|
115
|
+
[0.6.5]: https://github.com/wangxiaocuo/dsh-session-trash/compare/v0.6.4...v0.6.5
|
|
116
|
+
[0.6.4]: https://github.com/wangxiaocuo/dsh-session-trash/compare/v0.6.3...v0.6.4
|
|
81
117
|
[0.6.3]: https://github.com/wangxiaocuo/dsh-session-trash/compare/v0.6.2...v0.6.3
|
|
82
118
|
[0.6.2]: https://github.com/wangxiaocuo/dsh-session-trash/compare/v0.6.1...v0.6.2
|
|
83
119
|
[0.6.1]: https://github.com/wangxiaocuo/dsh-session-trash/compare/v0.6.0...v0.6.1
|
package/README.en.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# dsh-session-trash
|
|
2
2
|
|
|
3
|
+
[](https://github.com/wangxiaocuo/dsh-session-trash/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
[简体中文](./README.md) | English
|
|
4
6
|
|
|
5
7
|
[GitHub](https://github.com/wangxiaocuo/dsh-session-trash) · [npm](https://www.npmjs.com/package/@wangxiaocuo/dsh-session-trash) · [Changelog](./CHANGELOG.md)
|
|
6
8
|
|
|
7
9
|
A session trash for DeepSeek Harness. Move cold sessions out of the sidebar without deleting their logs, restore them within the retention window, or delete the logs permanently. Built for DSH 0.2.2-rc.1 on Desktop and Web. Host and Client ship in one package, with no build step.
|
|
8
10
|
|
|
9
|
-
Status: feature complete. CI (GitHub Actions) runs the unit
|
|
11
|
+
Status: feature complete. CI (GitHub Actions) runs the unit tests, the Host smoke test, and the real `dsh web` end-to-end suite on Ubuntu (Node 20/22) and Windows (Node 22), booting the Host from an npm-installed DSH runtime; macOS also ran the suite against the real desktop Host manually. Windows skips the cases that rely on POSIX permission bits.
|
|
10
12
|
|
|
11
13
|
## Features
|
|
12
14
|
|
|
@@ -113,13 +115,15 @@ All routes live under `/plugins/dsh-session-trash/api/` and are registered as ex
|
|
|
113
115
|
|
|
114
116
|
| Method | Path | Description |
|
|
115
117
|
|---|---|---|
|
|
116
|
-
| `GET` | `/state` | Config, index state, entries, stats, and cleanup diagnostics;
|
|
118
|
+
| `GET` | `/state` | Config, index state, entries, stats, and cleanup diagnostics (read-only; never sweeps) |
|
|
119
|
+
| `POST` | `/sweep` | No body; runs one throttled page-open maintenance sweep; whether expired entries are deleted depends on `enableAutoCleanup` |
|
|
117
120
|
| `POST` | `/preflight` | `{ sessionId }` returns `{ eligible: true, descendants }` or `{ eligible: false, code, message, details? }`; `descendants` is the number of hidden children that move along |
|
|
118
121
|
| `POST` | `/trash` | `{ sessionId }`; moves the whole session tree (parent plus hidden subagent children) and returns `{ ok, entry }` |
|
|
119
122
|
| `POST` | `/restore` | `{ sessionId, mode: 'normal' \| 'orphan' }` |
|
|
120
123
|
| `POST` | `/purge` | `{ sessionId }`; permanently deletes the session log |
|
|
121
124
|
| `POST` | `/purge-all` | No body; permanently deletes every entry in the trash, dropping `missing` index records only; returns `{ ok, purged, forgotten, failed }` |
|
|
122
125
|
| `POST` | `/forget` | `{ sessionId }`; removes only a `missing` index entry |
|
|
126
|
+
| `POST` | `/orphans/forget` | No body; removes empty unindexed object directories that hold no session log; returns `{ ok, removed }` |
|
|
123
127
|
| `POST` | `/reveal` | `{ sessionId }`; opens the original workspace |
|
|
124
128
|
| `POST` | `/cleanup` | No body; runs one cleanup sweep |
|
|
125
129
|
| `POST` | `/rebuild` | No body; rebuilds the index only in protection mode |
|
|
@@ -165,19 +169,22 @@ The index schema is v2; a v1 index is upgraded automatically on startup and pres
|
|
|
165
169
|
- Orphan restore never recreates the original workspace directory and never reattaches the session to a workspace.
|
|
166
170
|
- Archive and pin state survive trash and restore. The Trash page does not manage either set.
|
|
167
171
|
- Permanent deletion removes the session log only. Attachments, images, projection-cache rows, and search-index rows are not deleted.
|
|
172
|
+
- Unindexed objects (object directories whose `meta.json` is damaged or unreadable) are never deleted automatically; the page can only clean empty leftovers that hold no session log, and anything that may contain logs is left for manual inspection.
|
|
168
173
|
- `/state` returns all entry summaries in one response. Search, filtering, and paging happen in the Client.
|
|
169
174
|
- There is no cross-window live push. Another window's change appears after a refresh or when the window regains focus.
|
|
170
|
-
- The Windows and Linux
|
|
175
|
+
- The desktop apps on Windows and Linux have not been verified manually; CI boots an npm-installed `dsh web` Host, so desktop-shell UI and desktop-only features (such as reveal in the file manager) are still verified on macOS only. On Windows, cases that rely on POSIX permission bits are skipped, and long paths over 260 characters / symlink copying depend on system configuration (the related tests skip themselves when unsupported).
|
|
171
176
|
- macOS path comparison is case-insensitive. On a case-sensitive APFS volume, paths that differ only by case are treated as the same location.
|
|
172
|
-
- Multiple DSH processes sharing one `DSH_HOME` serialize trash operations through `.lock`.
|
|
177
|
+
- Multiple DSH processes sharing one `DSH_HOME` serialize trash operations through `.lock`. The Host retries briefly before reporting `LOCKED`, and the Client retries once automatically; a process paused for more than 10 minutes can lose the lock to another process.
|
|
173
178
|
|
|
174
179
|
## Development
|
|
175
180
|
|
|
176
181
|
```bash
|
|
177
182
|
pnpm install
|
|
178
|
-
pnpm test # unit tests
|
|
183
|
+
pnpm test # unit tests + real-DOM component tests (jsdom)
|
|
184
|
+
pnpm typecheck # tsc --checkJs (currently lib/ and index.js)
|
|
185
|
+
pnpm test:coverage # coverage (CI thresholds apply to lib/**)
|
|
179
186
|
pnpm test:smoke # Host smoke test (real Cordis, plain Node)
|
|
180
|
-
npm run test:e2e # real dsh web end-to-end (
|
|
187
|
+
npm run test:e2e # real dsh web end-to-end (desktop app or npm-installed DSH)
|
|
181
188
|
```
|
|
182
189
|
|
|
183
190
|
The Host smoke test wires the plugin into a real Cordis context through the `@deepseek-ai/cordis` devDependency, creates its own temporary `DSH_HOME`, and removes it afterwards; it does not need the desktop app. Set `DSH_SMOKE_KEEP=1` to keep the temporary home for debugging. `pnpm test` enumerates the test files through `test/run-unit.mjs`, so it does not depend on shell glob expansion and behaves the same on Windows and POSIX shells.
|
|
@@ -188,9 +195,19 @@ What CI verifies:
|
|
|
188
195
|
|---|---|
|
|
189
196
|
| Unit tests, Host smoke test | CI: Ubuntu Node 20/22, Windows Node 22 |
|
|
190
197
|
| Package contents (`npm pack`) | CI: Ubuntu |
|
|
191
|
-
|
|
|
198
|
+
| Coverage thresholds (`lib/**`, lines 85 / branches 70 / functions 84) | CI: Ubuntu Node 22 |
|
|
199
|
+
| Real `dsh web` end to end (npm-installed DSH) | CI: Ubuntu, Windows (Node 22) |
|
|
200
|
+
| Real `dsh web` end to end (desktop app) | macOS + DSH 0.2.2-rc.1 (run manually) |
|
|
201
|
+
|
|
202
|
+
`npm run test:e2e` boots a real `dsh web` Host in a temporary `DSH_HOME` and runs the full flow. By default it discovers the desktop app in the platform's usual install locations (or uses `DSH_ELECTRON`); set `DSH_E2E_DSH_BIN` to the `lib/bin.js` of an npm-installed `@deepseek-ai/dsh` to boot the Host with plain Node instead - CI and Linux/Windows use that mode, with no desktop app required. When no Host is found it prints a skip message and exits 0. The cleanup-failure scenario that relies on POSIX permission bits is skipped on Windows. Set `DSH_E2E_KEEP=1` to keep the temporary home and logs.
|
|
203
|
+
|
|
204
|
+
### Releasing (maintainers)
|
|
205
|
+
|
|
206
|
+
1. Bump the version in `package.json` and update `CHANGELOG.md`, then commit and push.
|
|
207
|
+
2. Create and push a tag: `git tag -a vX.Y.Z -m "..." && git push origin vX.Y.Z`.
|
|
208
|
+
3. The tag runs [release.yml](.github/workflows/release.yml): typecheck, unit tests, smoke test, real-host e2e, then `npm publish --provenance` and a GitHub release.
|
|
192
209
|
|
|
193
|
-
|
|
210
|
+
Publishing uses npm Trusted Publishing (OIDC), so no `NPM_TOKEN` is required: add repository `wangxiaocuo/dsh-session-trash` and workflow `release.yml` under the package's Settings -> Publishing access -> Trusted publishers on npmjs.com. To use an automation token instead, uncomment the `NODE_AUTH_TOKEN` environment variable in release.yml and add the matching secret.
|
|
194
211
|
|
|
195
212
|
Project layout:
|
|
196
213
|
|
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# dsh-session-trash
|
|
2
2
|
|
|
3
|
+
[](https://github.com/wangxiaocuo/dsh-session-trash/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
简体中文 | [English](./README.en.md)
|
|
4
6
|
|
|
5
7
|
[GitHub](https://github.com/wangxiaocuo/dsh-session-trash) · [npm](https://www.npmjs.com/package/@wangxiaocuo/dsh-session-trash) · [更新记录](./CHANGELOG.md)
|
|
6
8
|
|
|
7
9
|
DeepSeek Harness 的会话回收站插件。冷会话可以从侧边栏移出,日志保留在本地;保留期内可以恢复,到期后自动删除日志。面向 DSH 0.2.2-rc.1 的 Desktop 和 Web,Host 和 Client 在同一个包里,不需要构建步骤。
|
|
8
10
|
|
|
9
|
-
当前状态:功能已实现。CI(GitHub Actions)在 Ubuntu(Node 20/22)和 Windows(Node 22
|
|
11
|
+
当前状态:功能已实现。CI(GitHub Actions)在 Ubuntu(Node 20/22)和 Windows(Node 22)上跑单元测试、Host 冒烟测试和真实 `dsh web` 端到端测试(Host 由 npm 安装的 DSH runtime 启动);macOS 另外在真实桌面版 Host 上手动跑通过 e2e。Windows 上依赖 POSIX 权限位的用例会显式跳过。
|
|
10
12
|
|
|
11
13
|
## 功能
|
|
12
14
|
|
|
@@ -112,13 +114,15 @@ npm 安装的插件在「插件」页打开「会话回收站」的详情,展
|
|
|
112
114
|
|
|
113
115
|
| 方法 | 路径 | 说明 |
|
|
114
116
|
|---|---|---|
|
|
115
|
-
| `GET` | `/state` |
|
|
117
|
+
| `GET` | `/state` | 配置、索引状态、条目、统计和清理诊断(只读,不触发清理) |
|
|
118
|
+
| `POST` | `/sweep` | 无请求体,按节流执行一次页面维护扫描;回收站页面打开时调用,是否删除过期条目由 `enableAutoCleanup` 决定 |
|
|
116
119
|
| `POST` | `/preflight` | `{ sessionId }` 返回 `{ eligible: true, descendants }` 或 `{ eligible: false, code, message, details? }`;`descendants` 是一并移入的隐藏子会话数 |
|
|
117
120
|
| `POST` | `/trash` | `{ sessionId }`,移动整棵会话树(父会话 + 隐藏 subagent 子会话),返回 `{ ok, entry }` |
|
|
118
121
|
| `POST` | `/restore` | `{ sessionId, mode: 'normal' \| 'orphan' }` |
|
|
119
122
|
| `POST` | `/purge` | `{ sessionId }`,永久删除会话日志 |
|
|
120
123
|
| `POST` | `/purge-all` | 无请求体,永久删除回收站中的全部条目;`missing` 记录仅移除索引;返回 `{ ok, purged, forgotten, failed }` |
|
|
121
124
|
| `POST` | `/forget` | `{ sessionId }`,仅移除 `missing` 条目的索引记录 |
|
|
125
|
+
| `POST` | `/orphans/forget` | 无请求体,删除不含会话日志的空无主对象目录;返回 `{ ok, removed }` |
|
|
122
126
|
| `POST` | `/reveal` | `{ sessionId }`,打开原工作区 |
|
|
123
127
|
| `POST` | `/cleanup` | 无请求体,手动执行一次清理 |
|
|
124
128
|
| `POST` | `/rebuild` | 无请求体,仅在索引保护模式下重建 |
|
|
@@ -164,19 +168,22 @@ npm 安装的插件在「插件」页打开「会话回收站」的详情,展
|
|
|
164
168
|
- 孤儿恢复不会重建原工作区目录,也不会重新挂到工作区。
|
|
165
169
|
- 归档和置顶状态会跨回收、恢复保留,回收站页面不管理这两个集合。
|
|
166
170
|
- 永久删除只删除会话日志。附件、图片、投影缓存和搜索索引不会被删除。
|
|
171
|
+
- 无主对象(`meta.json` 损坏或无法识别的对象目录)不会自动删除;页面只能清理其中不含会话日志的空目录,可能含日志的需要人工处理。
|
|
167
172
|
- `/state` 一次返回全部条目摘要,页面在客户端搜索、筛选和分页。
|
|
168
173
|
- 没有跨窗口实时推送。另一个窗口的变更要等刷新或窗口重新获得焦点后出现。
|
|
169
|
-
- Windows
|
|
174
|
+
- Windows/Linux 的桌面版尚未手动验证;CI 用 npm 安装的 `dsh web` 覆盖真实 Host 行为,但桌面壳 UI 与桌面专属能力(例如「在文件管理器中显示」)仍只在 macOS 上验证。Windows 上依赖 POSIX 权限位的用例会跳过;超过 260 字符的长路径和符号链接复制受系统设置影响,对应用例在不支持时自动跳过。
|
|
170
175
|
- macOS 的路径比较按大小写不敏感处理。在大小写敏感的 APFS 卷上,仅大小写不同的两个路径会被视为同一位置。
|
|
171
|
-
- 多个 DSH 进程共享同一个 `DSH_HOME` 时,回收站操作通过 `.lock`
|
|
176
|
+
- 多个 DSH 进程共享同一个 `DSH_HOME` 时,回收站操作通过 `.lock` 串行;Host 在报 `LOCKED` 前会短暂重试,客户端也会自动重试一次;进程暂停超过 10 分钟后,锁可能被另一个进程接管。
|
|
172
177
|
|
|
173
178
|
## 开发
|
|
174
179
|
|
|
175
180
|
```bash
|
|
176
181
|
pnpm install
|
|
177
|
-
pnpm test # 单元测试
|
|
182
|
+
pnpm test # 单元测试 + 真实 DOM 组件测试(jsdom)
|
|
183
|
+
pnpm typecheck # tsc --checkJs(当前覆盖 lib/ 和 index.js)
|
|
184
|
+
pnpm test:coverage # 覆盖率(CI 的门槛只作用于 lib/**)
|
|
178
185
|
pnpm test:smoke # Host 冒烟测试(真实 Cordis,普通 Node 即可)
|
|
179
|
-
npm run test:e2e # 真实 dsh web
|
|
186
|
+
npm run test:e2e # 真实 dsh web 端到端(桌面版或 npm 安装的 DSH)
|
|
180
187
|
```
|
|
181
188
|
|
|
182
189
|
Host 冒烟测试通过 `@deepseek-ai/cordis` 这个 devDependency 装配真实插件上下文,自己创建并清理临时 `DSH_HOME`,不需要桌面版;设置 `DSH_SMOKE_KEEP=1` 可以保留临时目录用于排查。`pnpm test` 通过 `test/run-unit.mjs` 枚举测试文件,不依赖 shell 展开 glob,Windows 和 POSIX shell 行为一致。
|
|
@@ -187,9 +194,19 @@ Host 冒烟测试通过 `@deepseek-ai/cordis` 这个 devDependency 装配真实
|
|
|
187
194
|
|---|---|
|
|
188
195
|
| 单元测试、Host 冒烟测试 | CI:Ubuntu Node 20/22、Windows Node 22 |
|
|
189
196
|
| 打包内容(`npm pack`) | CI:Ubuntu |
|
|
190
|
-
|
|
|
197
|
+
| 覆盖率门槛(`lib/**`,行 85 / 分支 70 / 函数 84) | CI:Ubuntu Node 22 |
|
|
198
|
+
| 真实 `dsh web` 端到端(npm 安装的 DSH) | CI:Ubuntu、Windows(Node 22) |
|
|
199
|
+
| 真实 `dsh web` 端到端(桌面版) | macOS + DSH 0.2.2-rc.1(手动执行) |
|
|
200
|
+
|
|
201
|
+
`npm run test:e2e` 会启动真实 `dsh web` Host,在临时 `DSH_HOME` 中跑完整流程。默认按平台在常见安装位置查找桌面版(也可以用 `DSH_ELECTRON` 指定);设置 `DSH_E2E_DSH_BIN` 指向 npm 安装的 `@deepseek-ai/dsh` 的 `lib/bin.js` 时,改用普通 Node 启动 Host——CI 和 Linux/Windows 都走这条路径,不依赖桌面版。找不到 Host 时打印跳过信息并以 0 退出。依赖 POSIX 权限位的「清理失败」场景在 Windows 上会跳过。设置 `DSH_E2E_KEEP=1` 可以保留临时目录和日志。
|
|
202
|
+
|
|
203
|
+
### 发布(维护者)
|
|
204
|
+
|
|
205
|
+
1. 更新 `package.json` 版本和 `CHANGELOG.md`,提交并推送。
|
|
206
|
+
2. 打 tag 并推送:`git tag -a vX.Y.Z -m "..." && git push origin vX.Y.Z`。
|
|
207
|
+
3. tag 会触发 [release.yml](.github/workflows/release.yml):跑 typecheck、单测、smoke 和真实 Host e2e,然后 `npm publish --provenance` 并创建 GitHub Release。
|
|
191
208
|
|
|
192
|
-
|
|
209
|
+
发布使用 npm Trusted Publishing(OIDC),不需要 `NPM_TOKEN`:在 npmjs.com 的包设置 → Publishing access → Trusted publishers 里添加仓库 `wangxiaocuo/dsh-session-trash` 和 workflow `release.yml`。如果改用 automation token,取消 release.yml 里 `NODE_AUTH_TOKEN` 的注释并添加同名 secret。
|
|
193
210
|
|
|
194
211
|
项目结构:
|
|
195
212
|
|
package/client.js
CHANGED
|
@@ -126,6 +126,13 @@ window.__ModuleLoader__.load({
|
|
|
126
126
|
corruptBanner: '回收站索引损坏,已进入保护模式。现有会话不会被自动修改。',
|
|
127
127
|
recoveredBanner: '回收站索引已从备份恢复。',
|
|
128
128
|
unindexedBanner: '回收站目录中有 {count} 个未索引对象,插件不会自动删除它们。',
|
|
129
|
+
orphansClean: '清理空目录',
|
|
130
|
+
orphansTitle: '清理空的无主对象?',
|
|
131
|
+
orphansBody: '只删除回收站中不含会话日志的残留目录;可能含日志的无主对象会保留。',
|
|
132
|
+
orphansConfirm: '清理空目录',
|
|
133
|
+
orphansDoneOne: '已清理 {count} 个空的无主对象',
|
|
134
|
+
orphansDoneOther: '已清理 {count} 个空的无主对象',
|
|
135
|
+
orphansNone: '没有可安全清理的空目录',
|
|
129
136
|
rebuild: '尝试重建索引',
|
|
130
137
|
rebuilding: '正在重建…',
|
|
131
138
|
rebuildDone: '索引已重建,共 {count} 个条目',
|
|
@@ -281,6 +288,13 @@ window.__ModuleLoader__.load({
|
|
|
281
288
|
corruptBanner: 'The trash index is damaged, so protection mode is on. No session is changed automatically.',
|
|
282
289
|
recoveredBanner: 'The trash index was recovered from its backup.',
|
|
283
290
|
unindexedBanner: 'The trash directory holds {count} unindexed objects. The plugin will not delete them.',
|
|
291
|
+
orphansClean: 'Clean empty leftovers',
|
|
292
|
+
orphansTitle: 'Remove empty unindexed objects?',
|
|
293
|
+
orphansBody: 'Only leftover directories without session logs are removed; unindexed objects that may contain logs are kept.',
|
|
294
|
+
orphansConfirm: 'Clean empty objects',
|
|
295
|
+
orphansDoneOne: 'Removed {count} empty unindexed object',
|
|
296
|
+
orphansDoneOther: 'Removed {count} empty unindexed objects',
|
|
297
|
+
orphansNone: 'No empty leftovers to clean',
|
|
284
298
|
rebuild: 'Try rebuilding the index',
|
|
285
299
|
rebuilding: 'Rebuilding…',
|
|
286
300
|
rebuildDone: 'Index rebuilt with {count} entries',
|
|
@@ -889,8 +903,14 @@ window.__ModuleLoader__.load({
|
|
|
889
903
|
if (wrapRef.current !== null && !wrapRef.current.contains(event.target)) setOpen(false)
|
|
890
904
|
}
|
|
891
905
|
const onKeyDown = (event) => {
|
|
906
|
+
if (event.key === 'Tab') {
|
|
907
|
+
// Let focus leave the menu naturally; just stop leaving it open.
|
|
908
|
+
setOpen(false)
|
|
909
|
+
return
|
|
910
|
+
}
|
|
892
911
|
if (event.key !== 'Escape') return
|
|
893
912
|
event.preventDefault()
|
|
913
|
+
event.stopPropagation()
|
|
894
914
|
setOpen(false)
|
|
895
915
|
if (buttonRef.current !== null) buttonRef.current.focus()
|
|
896
916
|
}
|
|
@@ -992,9 +1012,11 @@ window.__ModuleLoader__.load({
|
|
|
992
1012
|
},
|
|
993
1013
|
items.map((item, index) =>
|
|
994
1014
|
h(
|
|
995
|
-
|
|
1015
|
+
React.Fragment,
|
|
996
1016
|
{ key: item.key },
|
|
997
|
-
item.separatorBefore === true && index > 0
|
|
1017
|
+
item.separatorBefore === true && index > 0
|
|
1018
|
+
? h('div', { className: 'dsh-trash__menuSeparator', role: 'separator' })
|
|
1019
|
+
: null,
|
|
998
1020
|
h(
|
|
999
1021
|
'button',
|
|
1000
1022
|
{
|
|
@@ -1034,7 +1056,7 @@ window.__ModuleLoader__.load({
|
|
|
1034
1056
|
* }} props dialog content and handlers.
|
|
1035
1057
|
* @returns {import('react').ReactElement} the dialog overlay.
|
|
1036
1058
|
*/
|
|
1037
|
-
function ConfirmDialog({ kind, entry, count, t, onCancel, onConfirm }) {
|
|
1059
|
+
function ConfirmDialog({ kind, entry, count, t, onCancel, onConfirm, fallbackRef }) {
|
|
1038
1060
|
const dialogRef = React.useRef(null)
|
|
1039
1061
|
const cancelRef = React.useRef(null)
|
|
1040
1062
|
const titleId = `dsh-trash-dialog-title-${kind}`
|
|
@@ -1068,7 +1090,12 @@ window.__ModuleLoader__.load({
|
|
|
1068
1090
|
document.addEventListener('keydown', onKeyDown, true)
|
|
1069
1091
|
return () => {
|
|
1070
1092
|
document.removeEventListener('keydown', onKeyDown, true)
|
|
1071
|
-
if (previous !== null && typeof previous.focus === 'function'
|
|
1093
|
+
if (previous !== null && typeof previous.focus === 'function' && previous.isConnected !== false) {
|
|
1094
|
+
previous.focus()
|
|
1095
|
+
return
|
|
1096
|
+
}
|
|
1097
|
+
const fallback = fallbackRef === null || fallbackRef === undefined ? null : fallbackRef.current
|
|
1098
|
+
if (fallback !== null && typeof fallback.focus === 'function') fallback.focus()
|
|
1072
1099
|
}
|
|
1073
1100
|
}, [onCancel])
|
|
1074
1101
|
|
|
@@ -1089,6 +1116,10 @@ window.__ModuleLoader__.load({
|
|
|
1089
1116
|
heading = format(t('purgeAllTitle'), { count })
|
|
1090
1117
|
body = t('purgeAllBody')
|
|
1091
1118
|
confirmLabel = t('purgeAllConfirm')
|
|
1119
|
+
} else if (kind === 'orphans') {
|
|
1120
|
+
heading = t('orphansTitle')
|
|
1121
|
+
body = t('orphansBody')
|
|
1122
|
+
confirmLabel = t('orphansConfirm')
|
|
1092
1123
|
} else {
|
|
1093
1124
|
heading = t('forgetTitle')
|
|
1094
1125
|
body = t('forgetBody')
|
|
@@ -1151,6 +1182,7 @@ window.__ModuleLoader__.load({
|
|
|
1151
1182
|
const [cleaning, setCleaning] = React.useState(false)
|
|
1152
1183
|
const [rebuilding, setRebuilding] = React.useState(false)
|
|
1153
1184
|
const [purgingAll, setPurgingAll] = React.useState(false)
|
|
1185
|
+
const [forgettingOrphans, setForgettingOrphans] = React.useState(false)
|
|
1154
1186
|
const [announcement, setAnnouncement] = React.useState('')
|
|
1155
1187
|
const [clock, setClock] = React.useState(() => Date.now())
|
|
1156
1188
|
const [query, setQuery] = React.useState('')
|
|
@@ -1165,6 +1197,8 @@ window.__ModuleLoader__.load({
|
|
|
1165
1197
|
const activeWritesRef = React.useRef(0)
|
|
1166
1198
|
/** Abort scopes of in-flight writes, all cancelled on unmount. */
|
|
1167
1199
|
const writeScopesRef = React.useRef(new Set())
|
|
1200
|
+
/** The page-open sweep is requested once per mounted section. */
|
|
1201
|
+
const pageSweepRef = React.useRef(false)
|
|
1168
1202
|
const closeDialog = React.useCallback(() => setDialog(null), [])
|
|
1169
1203
|
|
|
1170
1204
|
/**
|
|
@@ -1266,7 +1300,7 @@ window.__ModuleLoader__.load({
|
|
|
1266
1300
|
* | { kind: 'network' }
|
|
1267
1301
|
* >} the outcome.
|
|
1268
1302
|
*/
|
|
1269
|
-
async function
|
|
1303
|
+
async function sendMutationOnce(pathname, body) {
|
|
1270
1304
|
const scope = openWriteScope()
|
|
1271
1305
|
try {
|
|
1272
1306
|
const { status, ok, payload } = await requestJson(pathname, { method: 'POST', body, signal: scope.signal })
|
|
@@ -1280,6 +1314,30 @@ window.__ModuleLoader__.load({
|
|
|
1280
1314
|
}
|
|
1281
1315
|
}
|
|
1282
1316
|
|
|
1317
|
+
/**
|
|
1318
|
+
* Send one write request. A LOCKED answer means the Host rejected the
|
|
1319
|
+
* call before touching anything, so it is safe to retry once after a
|
|
1320
|
+
* short jitter instead of surfacing a spurious failure.
|
|
1321
|
+
*
|
|
1322
|
+
* @param {string} pathname endpoint below the API prefix.
|
|
1323
|
+
* @param {object} [body] JSON request body.
|
|
1324
|
+
* @returns {Promise<any>} the outcome, see {@link sendMutationOnce}.
|
|
1325
|
+
*/
|
|
1326
|
+
async function sendMutation(pathname, body) {
|
|
1327
|
+
let outcome = await sendMutationOnce(pathname, body)
|
|
1328
|
+
if (outcome.kind === 'response' && outcome.ok === false) {
|
|
1329
|
+
const code =
|
|
1330
|
+
outcome.payload !== null && outcome.payload !== undefined && outcome.payload.error !== undefined
|
|
1331
|
+
? outcome.payload.error.code
|
|
1332
|
+
: undefined
|
|
1333
|
+
if (code === 'LOCKED') {
|
|
1334
|
+
await new Promise((resolve) => setTimeout(resolve, 350 + Math.floor(Math.random() * 350)))
|
|
1335
|
+
outcome = await sendMutationOnce(pathname, body)
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return outcome
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1283
1341
|
/**
|
|
1284
1342
|
* @param {'timeout' | 'network'} kind transport failure.
|
|
1285
1343
|
* @returns {string} the localized message to show.
|
|
@@ -1315,6 +1373,12 @@ window.__ModuleLoader__.load({
|
|
|
1315
1373
|
}
|
|
1316
1374
|
setView({ status: 'ready', data: payload, error: null })
|
|
1317
1375
|
setRowErrors({})
|
|
1376
|
+
// Read-only /state never deletes; ask the Host for the throttled
|
|
1377
|
+
// maintenance sweep explicitly, once per mounted page.
|
|
1378
|
+
if (!pageSweepRef.current) {
|
|
1379
|
+
pageSweepRef.current = true
|
|
1380
|
+
requestJson('sweep', { method: 'POST' }).catch(() => {})
|
|
1381
|
+
}
|
|
1318
1382
|
})
|
|
1319
1383
|
.catch(() => {
|
|
1320
1384
|
if (controller.signal.aborted) {
|
|
@@ -1402,7 +1466,8 @@ window.__ModuleLoader__.load({
|
|
|
1402
1466
|
const loading = view.status === 'loading'
|
|
1403
1467
|
const refreshing = view.status === 'refreshing'
|
|
1404
1468
|
const readsBusy = loading || refreshing
|
|
1405
|
-
const pendingCount =
|
|
1469
|
+
const pendingCount =
|
|
1470
|
+
pendingRows.size + (cleaning ? 1 : 0) + (purgingAll ? 1 : 0) + (rebuilding ? 1 : 0) + (forgettingOrphans ? 1 : 0)
|
|
1406
1471
|
/** Any write (row action or maintenance) is in flight. */
|
|
1407
1472
|
const mutationBusy = pendingCount > 0
|
|
1408
1473
|
const corrupt = data !== null && data.index !== undefined && data.index.status === 'corrupt'
|
|
@@ -1745,6 +1810,39 @@ window.__ModuleLoader__.load({
|
|
|
1745
1810
|
}
|
|
1746
1811
|
}
|
|
1747
1812
|
|
|
1813
|
+
/**
|
|
1814
|
+
* Remove empty unindexed object directories. They contain no session
|
|
1815
|
+
* log, only leftover plugin metadata, so this is safe to offer.
|
|
1816
|
+
*
|
|
1817
|
+
* @returns {Promise<void>} completion of the operation.
|
|
1818
|
+
*/
|
|
1819
|
+
async function performPurgeOrphans() {
|
|
1820
|
+
if (!beginWrite()) return
|
|
1821
|
+
setForgettingOrphans(true)
|
|
1822
|
+
try {
|
|
1823
|
+
const outcome = await sendMutation('orphans/forget')
|
|
1824
|
+
if (!aliveRef.current || outcome.kind === 'cancelled') return
|
|
1825
|
+
if (outcome.kind !== 'response') {
|
|
1826
|
+
ctx.contextMenus.notify(transportMessage(outcome.kind), false)
|
|
1827
|
+
return
|
|
1828
|
+
}
|
|
1829
|
+
if (!outcome.ok) {
|
|
1830
|
+
ctx.contextMenus.notify(describeActionFailure(outcome.status, outcome.payload, t), false)
|
|
1831
|
+
return
|
|
1832
|
+
}
|
|
1833
|
+
const payload = outcome.payload
|
|
1834
|
+
const removed =
|
|
1835
|
+
payload !== null && payload !== undefined && typeof payload.removed === 'number' ? payload.removed : 0
|
|
1836
|
+
const message = removed > 0 ? countText(t, 'orphansDone', removed) : t('orphansNone')
|
|
1837
|
+
bumpRevision()
|
|
1838
|
+
ctx.contextMenus.notify(message, removed > 0)
|
|
1839
|
+
announce(message)
|
|
1840
|
+
} finally {
|
|
1841
|
+
endWrite()
|
|
1842
|
+
if (aliveRef.current) setForgettingOrphans(false)
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1748
1846
|
/**
|
|
1749
1847
|
* @param {object} entry wire entry.
|
|
1750
1848
|
* @param {string} key menu identity.
|
|
@@ -1905,6 +2003,16 @@ window.__ModuleLoader__.load({
|
|
|
1905
2003
|
'div',
|
|
1906
2004
|
{ className: 'dsh-trash__banner dsh-trash__banner--info', role: 'status', key: 'unindexed' },
|
|
1907
2005
|
h('span', { className: 'dsh-trash__bannerText' }, format(t('unindexedBanner'), { count: unindexed })),
|
|
2006
|
+
h(
|
|
2007
|
+
'button',
|
|
2008
|
+
{
|
|
2009
|
+
type: 'button',
|
|
2010
|
+
className: 'dsh-trash__button',
|
|
2011
|
+
onClick: () => setDialog({ kind: 'orphans' }),
|
|
2012
|
+
disabled: loading || mutationBusy,
|
|
2013
|
+
},
|
|
2014
|
+
t('orphansClean'),
|
|
2015
|
+
),
|
|
1908
2016
|
),
|
|
1909
2017
|
)
|
|
1910
2018
|
}
|
|
@@ -2269,6 +2377,7 @@ window.__ModuleLoader__.load({
|
|
|
2269
2377
|
className: 'dsh-trash__panel',
|
|
2270
2378
|
key: 'panel',
|
|
2271
2379
|
ref: panelRef,
|
|
2380
|
+
tabIndex: -1,
|
|
2272
2381
|
'aria-busy': readsBusy ? 'true' : 'false',
|
|
2273
2382
|
},
|
|
2274
2383
|
panelBody,
|
|
@@ -2289,12 +2398,14 @@ window.__ModuleLoader__.load({
|
|
|
2289
2398
|
count: dialog.count,
|
|
2290
2399
|
t,
|
|
2291
2400
|
onCancel: closeDialog,
|
|
2401
|
+
fallbackRef: panelRef,
|
|
2292
2402
|
onConfirm: () => {
|
|
2293
2403
|
const current = dialog
|
|
2294
2404
|
setDialog(null)
|
|
2295
2405
|
if (current.kind === 'orphan') void performRestore(current.entry, 'orphan')
|
|
2296
2406
|
else if (current.kind === 'purge') void performPurge(current.entry)
|
|
2297
2407
|
else if (current.kind === 'purgeAll') void performPurgeAll()
|
|
2408
|
+
else if (current.kind === 'orphans') void performPurgeOrphans()
|
|
2298
2409
|
else void performForget(current.entry)
|
|
2299
2410
|
},
|
|
2300
2411
|
})
|
package/index.js
CHANGED
|
@@ -34,7 +34,7 @@ function createLogger(ctx) {
|
|
|
34
34
|
const prefix = 'dsh-session-trash'
|
|
35
35
|
let sink = null
|
|
36
36
|
try {
|
|
37
|
-
const factory = ctx.get('logger')
|
|
37
|
+
const factory = /** @type {any} */ (ctx.get('logger'))
|
|
38
38
|
if (typeof factory === 'function') {
|
|
39
39
|
const instance = factory(prefix)
|
|
40
40
|
if (instance !== null && instance !== undefined && typeof instance.info === 'function') sink = instance
|
|
@@ -65,7 +65,7 @@ function createLogger(ctx) {
|
|
|
65
65
|
* recovery, and the retention sweep.
|
|
66
66
|
*
|
|
67
67
|
* @param {import('@deepseek-ai/cordis').Context} ctx plugin context.
|
|
68
|
-
* @param {
|
|
68
|
+
* @param {any} config validated plugin configuration.
|
|
69
69
|
* @returns {void}
|
|
70
70
|
*/
|
|
71
71
|
export function apply(ctx, config) {
|
package/lib/cleanup.js
CHANGED
|
@@ -14,7 +14,7 @@ export const CLEANUP_RETRY_MAX_MS = 60 * 60 * 1000
|
|
|
14
14
|
const STOPPED_RESULT = Object.freeze({ purged: 0, failed: 0, settled: 0 })
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* @param {
|
|
17
|
+
* @param {any} error arbitrary failure.
|
|
18
18
|
* @returns {string} a concise message.
|
|
19
19
|
*/
|
|
20
20
|
function messageOf(error) {
|
|
@@ -22,7 +22,7 @@ function messageOf(error) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* @param {
|
|
25
|
+
* @param {any} error arbitrary failure.
|
|
26
26
|
* @returns {string} a bounded diagnostic message for `/state`.
|
|
27
27
|
*/
|
|
28
28
|
function errorText(error) {
|
|
@@ -60,18 +60,18 @@ function resolveTimer(ctx) {
|
|
|
60
60
|
*
|
|
61
61
|
* @param {{
|
|
62
62
|
* ctx: import('@deepseek-ai/cordis').Context,
|
|
63
|
-
* trashService: { sweep: (options: { allowPurge: boolean, now: number, reason: string }) => Promise<
|
|
63
|
+
* trashService: { sweep: (options: { allowPurge: boolean, now: number, reason: string }) => Promise<any> },
|
|
64
64
|
* config: { sweepIntervalHours: number, enableAutoCleanup: boolean },
|
|
65
65
|
* logger: { info: Function, warn: Function, error: Function, debug: Function },
|
|
66
66
|
* }} deps host services and the configured cadence.
|
|
67
67
|
* @returns {{
|
|
68
68
|
* start: () => void,
|
|
69
69
|
* stop: () => void,
|
|
70
|
-
* runNow: (options?: { manual?: boolean, reason?: string }) => Promise<
|
|
71
|
-
*
|
|
70
|
+
* runNow: (options?: { manual?: boolean, reason?: string }) => Promise<any>,
|
|
71
|
+
* sweepOnPageOpen: () => Promise<{ ran: boolean, reason?: string, result?: object }>,
|
|
72
72
|
* nextSweepAt: () => number | null,
|
|
73
73
|
* lastSweepAt: () => number,
|
|
74
|
-
* diagnostics: () =>
|
|
74
|
+
* diagnostics: () => any,
|
|
75
75
|
* }} the cleanup service.
|
|
76
76
|
*/
|
|
77
77
|
export function createCleanupService({ ctx, trashService, config, logger }) {
|
|
@@ -91,7 +91,7 @@ export function createCleanupService({ ctx, trashService, config, logger }) {
|
|
|
91
91
|
let disposeRetry = null
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* @param {
|
|
94
|
+
* @param {any} error sweep failure.
|
|
95
95
|
* @returns {void}
|
|
96
96
|
*/
|
|
97
97
|
function report(error) {
|
|
@@ -148,7 +148,7 @@ export function createCleanupService({ ctx, trashService, config, logger }) {
|
|
|
148
148
|
* because an automatic sweep may have been started with `allowPurge: false`.
|
|
149
149
|
*
|
|
150
150
|
* @param {{ manual?: boolean, reason?: string }} [options] manual trigger and diagnostic label.
|
|
151
|
-
* @returns {Promise<
|
|
151
|
+
* @returns {Promise<any>} the sweep summary.
|
|
152
152
|
*/
|
|
153
153
|
async function runNow(options = {}) {
|
|
154
154
|
if (stopped) return { ...STOPPED_RESULT }
|
|
@@ -294,17 +294,24 @@ export function createCleanupService({ ctx, trashService, config, logger }) {
|
|
|
294
294
|
runNow,
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
|
-
*
|
|
297
|
+
* Run the throttled sweep the trash page requests when it opens. The
|
|
298
298
|
* timer-backed composition uses a one-hour throttle; without a timer the
|
|
299
|
-
* configured sweep interval throttles instead.
|
|
299
|
+
* configured sweep interval throttles instead. This is an explicit POST so
|
|
300
|
+
* the read-only `/state` projection never deletes anything.
|
|
300
301
|
*
|
|
301
|
-
* @returns {
|
|
302
|
+
* @returns {Promise<{ ran: boolean, reason?: string, result?: object }>} the sweep outcome.
|
|
302
303
|
*/
|
|
303
|
-
|
|
304
|
-
if (stopped) return
|
|
304
|
+
async sweepOnPageOpen() {
|
|
305
|
+
if (stopped) return { ran: false, reason: 'stopped' }
|
|
305
306
|
const throttle = timer === undefined || !config.enableAutoCleanup ? intervalMs : PAGE_OPEN_THROTTLE_MS
|
|
306
|
-
if (lastAttemptAt !== 0 && Date.now() - lastAttemptAt <= throttle) return
|
|
307
|
-
|
|
307
|
+
if (lastAttemptAt !== 0 && Date.now() - lastAttemptAt <= throttle) return { ran: false, reason: 'throttled' }
|
|
308
|
+
try {
|
|
309
|
+
const result = await runNow({ reason: 'page-open' })
|
|
310
|
+
return { ran: true, result }
|
|
311
|
+
} catch (error) {
|
|
312
|
+
report(error)
|
|
313
|
+
return { ran: false, reason: 'failed' }
|
|
314
|
+
}
|
|
308
315
|
},
|
|
309
316
|
|
|
310
317
|
/**
|
|
@@ -324,7 +331,7 @@ export function createCleanupService({ ctx, trashService, config, logger }) {
|
|
|
324
331
|
/**
|
|
325
332
|
* Read-only cleanup diagnostics for `/state` and the settings page.
|
|
326
333
|
*
|
|
327
|
-
* @returns {
|
|
334
|
+
* @returns {any} a JSON-safe diagnostic snapshot.
|
|
328
335
|
*/
|
|
329
336
|
diagnostics() {
|
|
330
337
|
return {
|
package/lib/eligibility.js
CHANGED
|
@@ -7,14 +7,14 @@ export const PREFLIGHT_TTL_MS = 2000
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {{ sessionId: string, header: object }} DescendantRef
|
|
10
|
-
* @typedef {{ sessionId: string, header:
|
|
10
|
+
* @typedef {{ sessionId: string, header: any, location: { path: string }, formatStatus?: string }} PlannedMember
|
|
11
11
|
* @typedef {{ eligible: true, members: PlannedMember[] } | { eligible: false, code: string, message: string, details?: object }} PreflightResult
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {string} code refusal code.
|
|
16
16
|
* @param {string} message owner-facing explanation.
|
|
17
|
-
* @param {
|
|
17
|
+
* @param {any} [details] optional structured context.
|
|
18
18
|
* @returns {{ eligible: false, code: string, message: string, details?: object }} a refusal.
|
|
19
19
|
*/
|
|
20
20
|
function refuse(code, message, details) {
|
|
@@ -24,7 +24,7 @@ function refuse(code, message, details) {
|
|
|
24
24
|
/**
|
|
25
25
|
* Collect every stored session whose lineage descends from `sessionId`.
|
|
26
26
|
*
|
|
27
|
-
* @param {
|
|
27
|
+
* @param {any} persistence persistence service.
|
|
28
28
|
* @param {string} sessionId root session.
|
|
29
29
|
* @returns {Promise<DescendantRef[]>} descendant identity and stored header.
|
|
30
30
|
*/
|
|
@@ -59,7 +59,7 @@ async function descendantsFromListing(persistence, sessionId) {
|
|
|
59
59
|
* A grandchild whose direct parent is no longer stored still proves that the
|
|
60
60
|
* root session owns a lineage the trash must not split.
|
|
61
61
|
*
|
|
62
|
-
* @param {readonly
|
|
62
|
+
* @param {readonly any[]} nodes descendant nodes from the trace.
|
|
63
63
|
* @returns {DescendantRef[]} every descendant session identity and header.
|
|
64
64
|
*/
|
|
65
65
|
function collectTraceDescendants(nodes) {
|
|
@@ -123,7 +123,7 @@ export function createEligibility({ ctx, store, logger }) {
|
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
125
|
* @param {string} sessionId descendant session to validate.
|
|
126
|
-
* @returns {Promise<
|
|
126
|
+
* @returns {Promise<any>} either `{ member }` with the planned member or `{ refusal }`.
|
|
127
127
|
*/
|
|
128
128
|
async function planDescendant(sessionId) {
|
|
129
129
|
let snapshot
|