dsh-recall-plugin 2.4.6 → 2.4.8
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 +46 -0
- package/README.en.md +126 -35
- package/README.md +129 -40
- package/lib/client.js +824 -199
- package/lib/config.js +11 -4
- package/lib/errors.js +3 -0
- package/lib/exclude-patterns.js +2 -1
- package/lib/index.js +35 -5
- package/lib/intent-journal.js +108 -0
- package/lib/maintenance.js +9 -4
- package/lib/routes-core.js +89 -6
- package/lib/routes-manage.js +24 -6
- package/lib/scripts.posix.js +4 -0
- package/lib/scripts.pwsh.js +4 -0
- package/lib/snapshots.js +74 -4
- package/lib/store.js +7 -5
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
本文件格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
|
|
4
4
|
|
|
5
|
+
## [2.4.8] - 2026-10-03
|
|
6
|
+
|
|
7
|
+
撤回完成事件/回调(issue #19,方案见 [docs/plans/completed/plan-recall-event.md](docs/plans/completed/plan-recall-event.md)):撤回到达终态时广播 cordis 事件 `dsh-recall/complete` / `dsh-recall/failed`,供同宿主任意插件监听(如长时记忆插件清理被撤回回合的记忆)。纯增量 opt-in:不订阅的插件零感知,撤回链路行为逐字节不变(事件发送失败、监听器抛错、下游慢执行均不影响主流程)。
|
|
8
|
+
|
|
9
|
+
### 新增
|
|
10
|
+
|
|
11
|
+
- **事件契约(`src/types/events.ts`,单一事实源)**:`dsh-recall/complete`(`sessionId`/`childSessionId`/`scope`/`cutSeq`/`messageId`/`root`/`count`/`chatReverted`/`archiveRequested`/`time`,`version: 1`)与 `dsh-recall/failed`(`stage: 'execute' | 'fork'`/`sessionId`/`messageId`/`scope`/`cutSeq`/`root`/`code?`/`error`/`time`)。首发即稳定公共契约(非 experimental):新增字段走 minor,破坏性变更走 major 且 `version` 升 2。payload 不含消息正文;事件名与契约文件的绑定由 `tests/types/events-contract.test.ts` 编译期双向断言维持(host 产物因 build-host 逐文件转译不覆盖 `types/`,事件名在 routes-core 本地字面量声明)。
|
|
12
|
+
- **host `notify` 端点(routes-core.ts)**:client 撤回终态上报的接缝——校验(缺 `status`/`sessionId`/`messageId` 复用 `RECALL_BAD_TYPE`,不加新错误码)→ `resolveRoot` 尽力 enrich(失败落 `null` 不阻断)→ `queueMicrotask` + try/catch 调 `ctx.emit`(下游抛错/慢执行不反噬端点)→ 恒 `{ ok: true }`。不进串行队列(零 git 操作)、不依赖 store(避开 lineage-record 的 NO_STORE 早退会吞事件的缺陷);index.ts 以闭包注入 `emitEvent: (e, p) => ctx.emit(e, p)` 保持 ctx 不解构纪律。verify-host 端点白名单同步至 13 项(未同步时两处断言红灯,反向验证实测)。
|
|
13
|
+
- **client 终态上报(recall-node.ts + util.ts)**:`buildRecallNotify` 纯函数按语义决策表组装载荷(六场景矩阵单测直钉),`executeRecall` 五处上报点 fire-and-forget(`.catch` 静默)——execute 拒(透传 `code`)/ execute 抛 → `failed(stage:'execute')`;fork 抛错/返空 → `failed(stage:'fork')`(**绝不发 complete**,防下游误清该回合记忆);撤回链完整走完 → `complete`(`chatReverted` 为对话是否真回退的判据)。STALE 自动重预览是中间态,不上报。版本错位容错:旧 host 无此端点时上报静默失败(404),撤回不受影响;旧版 client 从不上报。
|
|
14
|
+
- **README 双语「事件契约」章节**:事件名、payload 字段表、语义要点(STALE 不发、fork 失败只发 failed、`(sessionId, messageId, cutSeq)` 三元组幂等、隐私面)、semver 演进规则与版本错位矩阵、可直接粘贴的监听示例。
|
|
15
|
+
|
|
16
|
+
### 测试
|
|
17
|
+
|
|
18
|
+
- `tests/unit/routes-notify.test.js`(11 例):参数校验缺失码 / root enrich 成功·抛错·返空 / emit 异步广播与 payload 字段 / 监听器抛错仍恒 `ok:true` 且 `recordError` 留痕 / `stage`-`code` 语义。
|
|
19
|
+
- `tests/unit/recall-notify.test.js`(8 例):`buildRecallNotify` 规格第一节六场景 + scope 透传与字段兜底。
|
|
20
|
+
- `tests/client/recall-node.test.ts` 追加 7 例:五处接线点的调用时机与载荷(含 STALE 不发 notify、上报失败静默不反噬主链)。
|
|
21
|
+
- `tests/types/events-contract.test.ts`:事件名字面量钉死、payload 形状断言、host 本地常量 ↔ 契约常量双向绑定、notify 端点返回类型双向绑定。基线 478 → 497(39 文件),client 83 → 90。
|
|
22
|
+
|
|
23
|
+
## [2.4.7] - 2026-09-30
|
|
24
|
+
|
|
25
|
+
质量加固专项批次([docs/plans/completed/plan-quality-hardening.md](docs/plans/completed/plan-quality-hardening.md) A1–A8,八项分四波:质量基建 → 数据安全 → 测试基建 → i18n)。2026-09-30 该计划连同 `plan-warmup-unhandled-rejection` 已归档到 `docs/plans/completed/`(活体冒烟第九节 R-1〜R-6 全过)。
|
|
26
|
+
|
|
27
|
+
### 新增
|
|
28
|
+
|
|
29
|
+
- **i18n 双语层(A4)**:插件自绘界面全双语(撤回按钮与确认面板、toast、设置三张卡、快照树、最近错误),由新配置项 `locale`(`auto` / `zh` / `en`,默认 `auto` 按 `navigator.language` 判)控制,配置卡片新增「界面」分组的下拉、保存即生效。实现分三层:`src/client/locales/`(zh 事实源 + en + 纯逻辑 `t/translate/resolveLocale`,缺 key 依次回落 zh、key 本身);host 文案键控化(错误响应按 `code` 查 `err.*`、最近错误按 `kind` 渲染、构建产物 root 停用提示改下发命中段名 `artifactSeg` 由 client 本地取词,旧中文字段保留一周期);`tests/unit/locales-parity.test.js` 钉两语言 key 集合 / 占位符集合一致 + 静态扫描 client 源码里的字面量 key 漏配,`tests/client/i18n.test.ts` 钉 zh/en 两条渲染链与语言下拉的保存载荷。v1 明示限制(README「界面语言」节):宿主官方设置表单自己渲染的 `Schema.description()` 仍是中文;带动态细节的 host 错误(回退失败的救援结果、具体校验原因、原始异常)原样展示 host 原文,不套本地化短句。
|
|
30
|
+
- **磁盘格式版本守卫(A3)**:影子仓库新增 per-store `format` marker(`SUPPORTED_FORMAT = 1`,缺席视为 1),读到更高版本或内容损坏时**拒写放行读**(fail-closed)——快照、撤回、tag 清理、索引落盘全短路并 `recordError`,快照列表 / 状态查询等只读路径不受影响,新错误码 `FORMAT_BLOCKED`(client 有对应文案);marker 补戳挂在索引落盘路径(稳态零额外进程),读取带短确认缓存且只缓 affirmative 结果。配套通用 `fileReadCmd`(契约 + 双平台模板),替 A2 与后续小文件读取省掉逐个特化命令。
|
|
31
|
+
- **操作意图 journal 与崩溃恢复(A2,吸收 U4)**:撤回的 both 链「安全快照 → reset → 救援」中途断电时,安全快照 tag 已在磁盘却无任何记录指向它——新增 `src/host/intent-journal.ts`:execute 动磁盘前先写 `recall-intent.json`(messageId / safetyId / safetyOk / phase),回退成功即清;启动预热与 init 端点两处挂 `recover`:先做幂等判定(工作区与目标 tag 一致即只清记录不 reset),需救援时复用 H1 救援路径自动 reset 回安全快照并 `recordError` 留痕,`agentBusy` 忙时延后到下次 init。救援失败提示同时附上意图文件路径(U4 验收语义:中断点可查)。
|
|
32
|
+
- **client UI 测试体系(A1)**:vitest + jsdom 独立配置(`npm run test:client`,CI 新增同步骤),`tests/client/` 6 文件 82 例——撤回节点主链(25)、快照管理(17)、配置卡片(17)、排除卡片(13)、logger 开关(7)、i18n 双语链路(3)。断言纪律:只断行为与结构(className / aria / 调用次数 / fetch 载荷),不为文案字面量上锁(i18n 让路)。期间实测发现并修复一处真实缺陷(见「修复」)。
|
|
33
|
+
- **client 命名空间 logger(A5)**:`src/client/log.ts` 的 `createLogger(ns)` 前缀 `[dsh-recall:<ns>]`,error/warn 恒输出、info/debug 由 `localStorage['dsh-recall.debug']`(`*` 或逗号分隔命名空间,每次调用重读)开关——现场排查无需重启;localStorage 不可用时静默降级。替换全部 5 处裸 `console.*`,装配完成补一行 info(「Host 活 Client 死」类症状可一眼确认)。
|
|
34
|
+
- **`docs/format.md` 磁盘格式 spec(A7)**:逐文件固化存储布局与格式(tag 命名、index/lineage 的原子写与损坏语义差异、stamp 文件、format marker、意图 journal)、兼容纪律(读取侧字段可选化 / 未知字段忽略 / 高版本拒写),并写死「代码与本文漂移即 bug」约定;同时提前实施 plan-p2 的 FORMAT 半(SECURITY 半仍留 P2-2)。
|
|
35
|
+
|
|
36
|
+
### 变更
|
|
37
|
+
|
|
38
|
+
- **开启 `noUncheckedIndexedAccess`(A6)**:编译期堵索引越界,修 14 处(含 `Map.get(...)!.push` 类断言),顺带清掉无注释的既有非空断言;由 CI 既有 typecheck 步骤自动成门禁,不连带开其他严格 flag。
|
|
39
|
+
- **catch 理由注释纪律(A8)**:协作规约新增「catch 必须附降级理由(为什么吞、为何安全、谁兜底)」,全 src 143 处 catch(host 99 + client 44)逐处审计补注;未发现「吞错 + 无 recordError + 无注释」的真问题;不设 CI 启发式门禁(误报高,靠规约 + review)。
|
|
40
|
+
- 配置 schema 增至 10 字段(新增 `locale`,不列进 `cordis.patch.yml`——它按行覆盖、默认值随 schema 下发);`init` 响应与 `config-get` 的 config 子集同步下发 `locale`(老 Host 缺字段时 client 按 `auto` 解析)。
|
|
41
|
+
- 「最近错误」区按 host 的 `kind` 渲染本地化提示(原来的中文原文保留在条目 `title` 上),未分类错误仍原样展示 host message。
|
|
42
|
+
|
|
43
|
+
### 修复
|
|
44
|
+
|
|
45
|
+
- **win32 上磁盘格式守卫会把正常快照库锁死(严重,随本批次实弹掘出)**:新增的通用 `fileReadCmd` 用 `Get-Content -ErrorAction SilentlyContinue` 读文件,而 PowerShell 对**缺席文件**即使吞掉报错仍以退出码 1 收尾(pwsh / Windows PowerShell 5.1 双实测)——runShell 的退出码门禁把它当执行失败抛出,格式守卫便把一个还没有 `format` 标记的正常 store 判成「标记读不到」;由于守卫同时挡在快照捕获与索引落盘上,补戳永远写不下去,**快照、撤回、快照列表载入全部停摆且不可自愈**(POSIX 侧靠 `cat … || true` 无此问题)。修复=读取命令改用 `Test-Path` 分支让缺席以成功收尾(与 POSIX 同语义),并补契约文本钉防回归;沉淀为 compat-audit **I41**。
|
|
46
|
+
- **宿主退出期插件可能中断宿主(fatal load failure)**:启动预热是 fire-and-forget 的异步 IIFE,宿主在预热途中卸载(`dsh --profile headless --help` 打印帮助后立即退出、HMR、装配门禁 dispose)时访问 `sessions` 会抛「inactive context」,未接的 promise rejection 被 cordis 加载器记成 fatal。修复=IIFE 整体接 catch 并保留诊断行(`recall warmup skipped: …`);该项同时关闭了既有待办 [plan-warmup-unhandled-rejection](docs/plans/completed/plan-warmup-unhandled-rejection.md)。
|
|
47
|
+
- **格式守卫的读失败文案不再指认「文件损坏」**:读失败(多为宿主启动早期 shell 未就绪)与标记内容非法此前共用一句「不可读或内容非法」,会把用户引向一个没有问题的文件;现按「高版本 → 请升级插件 / 内容非法 → 检查 format 文件 / 读不到 → 环境未就绪、下一条消息自动重试」三分。
|
|
48
|
+
- **i18n 切换语言的三处半截生效(浏览器实弹掘出)**:① 英文确认句里「发送时状态」与「共 N 个文件将变更」两句拼接缺空格(`sent.1501 files`);② 语言切换后设置卡片的两个分区折叠头(排除配置 / 快照管理)停在挂载时语言,要离开再回来才跟上;③ 保存成功提示用切换**前**的语言。修复=英文词条补句首空格、配置表单在语言落地时回调设置外壳重渲染、语言补丁先本地落地再报成功;三处均补进 client 组件测试。
|
|
49
|
+
- **回退失败时不再吞掉救援结果**:此前的码文案表把 `ROLLBACK_FAILED` 一律显示成「回退失败」四字,会盖掉 host 侧更有用的信息(「已自动恢复到安全快照,请重新预览后重试」或手动救援命令)。A4 起 `err.*` 词典只收 host 文案静态的错误码,`ROLLBACK_FAILED`(连同 `BAD_TYPE` / `SETTINGS_WRITE_FAILED` / `PARTIAL_DELETE` / `ERROR`)回落 host message 原文——中文用户看到的是更完整的救援结果与逃生命令,代价是英文界面下这几类错误可能中英混排(策略与理由见 README「界面语言」)。
|
|
50
|
+
|
|
5
51
|
## [2.4.6] - 2026-09-29
|
|
6
52
|
|
|
7
53
|
### 变更
|
package/README.en.md
CHANGED
|
@@ -7,13 +7,12 @@
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|
|
|
10
|
-
[](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.2.0-rc.2)
|
|
11
11
|

|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
Under any message you've sent, click "↶ Recall" — **your workspace files and the conversation history roll back to just before that message was sent**.
|
|
14
|
-
---
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
As each message is sent, the workspace is first snapshotted into an independent shadow git repository (your project's own git is never touched); a recall restores files from that snapshot and rewinds the conversation through DSH's official `sessions.fork` to the turn boundary before the message, with the original session archived and recoverable. The recalled message's text and attachments are placed back into the input box, ready to edit and resend. Snapshot storage lives under `$DSH_HOME` by default. The main boundary: snapshots are created only **when a message is sent** — messages from before the plugin was enabled have no snapshot and show no recall button.
|
|
17
16
|
|
|
18
17
|
[Changelog](CHANGELOG.md)
|
|
19
18
|
|
|
@@ -27,6 +26,7 @@ Files and conversation roll back together: as each message is sent, the workspac
|
|
|
27
26
|
- [Configuration](#configuration)
|
|
28
27
|
- [Snapshot Maintenance & Cleanup](#snapshot-maintenance--cleanup)
|
|
29
28
|
- [How It Works](#how-it-works)
|
|
29
|
+
- [Event Contract (for same-host plugins)](#event-contract-for-same-host-plugins)
|
|
30
30
|
- [Local Development (without publishing)](#local-development-without-publishing)
|
|
31
31
|
- [License](#license)
|
|
32
32
|
|
|
@@ -45,26 +45,26 @@ Files and conversation roll back together: as each message is sent, the workspac
|
|
|
45
45
|
Abilities with a version in parentheses require that version or later; the rest have no special version requirement.
|
|
46
46
|
|
|
47
47
|
- **Files + conversation, rolled back together**: recalling isn't just about chat history — files the agent modified go back to their original state too; immune to your project's `.gitattributes` conversion, with byte-level fidelity for line endings and binary content (2.1.1+).
|
|
48
|
-
- **Just want a fresh conversation? Leave the files alone** (2.3.24+): the confirmation panel offers a recall-scope choice — the default "Roll back files & conversation" is the full rollback;
|
|
48
|
+
- **Just want a fresh conversation? Leave the files alone** (2.3.24+): the confirmation panel offers a recall-scope choice — the default "Roll back files & conversation" is the full rollback; "Conversation only" keeps your project files byte-for-byte untouched (no safety snapshot either), ideal when you dislike the reply but the file changes are exactly what you want.
|
|
49
49
|
- **Never touches your project's own git, and keeps it clean**: snapshots live in an independent shadow git repository — branches, staging area, and uncommitted changes are untouched; storage stays under `$DSH_HOME` regardless of the session's sandbox permission (workspace-write / read-only sessions work as usual), falling back to an in-project `.dsh-recall-snapshots` only when home itself is unwritable.
|
|
50
|
-
- **See the list before you act — and change your mind as often as you like**: recall first shows the list of files that will change (modified / restored / deleted); nothing runs until you confirm. After a recall you can recall again to an even earlier point, and files overwritten during a recall always remain recoverable
|
|
51
|
-
- **Resend right after a recall** (2.3.15+):
|
|
52
|
-
- **A guarded recall path** (2.0+; auto-rescue 2.1+):
|
|
53
|
-
- **Disk-friendly, self-maintaining**: snapshots use git delta compression and large files are skipped automatically (threshold configurable); periodic lossless `git gc`, session-deletion cleanup, and
|
|
50
|
+
- **See the list before you act — and change your mind as often as you like**: recall first shows the list of files that will change (modified / restored / deleted); nothing runs until you confirm. After a recall you can recall again to an even earlier point, and files overwritten during a recall always remain recoverable.
|
|
51
|
+
- **Resend right after a recall** (2.3.15+): the message's text and attachments (images, files) return to the input box, so you can edit and send again without picking the attachments over.
|
|
52
|
+
- **A guarded recall path** (2.0+; auto-rescue 2.1+): a new snapshot after the preview forces a fresh preview; a "pre-rollback" safety snapshot is taken before every recall, a failed rollback is rescued automatically, and a failed rescue gives you a copy-paste-ready manual recovery command.
|
|
53
|
+
- **Disk-friendly, self-maintaining**: snapshots use git delta compression and large files are skipped automatically (threshold configurable); periodic lossless `git gc`, session-deletion cleanup, and cleanup by cap or retention age — plus a **workspace → session → snapshot** tree manager on the settings page with search and per-level deletion.
|
|
54
54
|
- **Failures speak up and heal** (self-healing 2.1+): failures are classified by root cause (git missing / disk full / permission denied / lock conflict / directory conflict) with actionable guidance (the same fault only bothers you once per 10 minutes; reasons land in the settings card's "Recent errors"); leftover objects are pruned, 3 consecutive failures trigger exponential backoff, concurrent instances yield via heartbeats, and unindexable paths are skipped with a notice instead of failing the whole snapshot (they are neither restored nor deleted on recall).
|
|
55
55
|
|
|
56
56
|
## Known Limitations
|
|
57
57
|
|
|
58
58
|
Boundaries accepted by design and edge cases not yet covered — worth checking before you rely on them.
|
|
59
59
|
|
|
60
|
-
- Snapshots are created **when a message is sent
|
|
60
|
+
- Snapshots are created **when a message is sent**: messages from before the plugin was enabled have no snapshot and show no recall button.
|
|
61
61
|
- Snapshots are **best-effort**: there is a 0.5–1.5 second window between the message arriving and `git add` (on Windows, a single PowerShell start alone costs about 0.4s). If a trivial task finishes editing files inside that window, the snapshot captures the turn's own changes — the file rollback then becomes a no-op (the preview panel reports "0 files will change") while the conversation rollback is unaffected.
|
|
62
62
|
- The first user message of a session cannot roll back the conversation (files only), because fork requires an earlier turn boundary.
|
|
63
63
|
- Recall cannot be initiated while an agent is running in the target workspace (by design — stop the agent first).
|
|
64
64
|
- Supports Windows (PowerShell 5.1/7 + git CLI) and Linux/macOS (bash + git CLI). Windows is thoroughly verified on real machines; Linux has been fully tested on WSL2 (Ubuntu 26.04, bash 5.3 + git 2.53), including Chinese paths, home fallback, session cleanup, and gc; the macOS side is written to be bash 3.2 compatible but has not been tested on real hardware yet.
|
|
65
|
-
- Nested git repositories inside the workspace (subdirectories with their own `.git`) cannot be indexed: the snapshot proceeds for everything else (fail-open, with a
|
|
65
|
+
- Nested git repositories inside the workspace (subdirectories with their own `.git`) cannot be indexed: the snapshot proceeds for everything else (fail-open, with a notice listing the skipped paths), but their contents do not participate in recalls.
|
|
66
66
|
- Extreme cases like filenames containing newlines/TAB are beyond the diff list's parsing capability (negligible probability).
|
|
67
|
-
- **Interplay with dsh-routing-suite (progressive tool-disclosure router)**: if you also run
|
|
67
|
+
- **Interplay with dsh-routing-suite (progressive tool-disclosure router)**: if you also run its router-standard preset, a recall forks a new session via `sessions.fork`, which resets the router's stage to its default (the tool surface temporarily narrows). Symptoms, root cause and the fix are documented in [docs/routing-interplay.md](docs/routing-interplay.md) (Chinese).
|
|
68
68
|
|
|
69
69
|
## Installation
|
|
70
70
|
|
|
@@ -72,35 +72,59 @@ Prerequisites:
|
|
|
72
72
|
|
|
73
73
|
- git CLI: without it the recall button won't appear (a notice shows at the top of the page); DSH itself keeps running.
|
|
74
74
|
- Shell: PowerShell 5.1 / 7 on Windows; bash on Linux/macOS.
|
|
75
|
-
- DSH version: `0.1.2-alpha.1` through `0.2.0-rc.
|
|
76
|
-
- **0.1.7-alpha.1 is a breaking release** (
|
|
75
|
+
- DSH version: `0.1.2-alpha.1` through `0.2.0-rc.2`; later versions within a verified minor line are admitted automatically, while unverified new minor lines are blocked by the startup compatibility gate (window mechanics in the fold below).
|
|
76
|
+
- **0.1.7-alpha.1 is a breaking release** (the shell execution and settings interfaces changed); the plugin ships both seams side by side — the same release works on every 0.1.2–0.1.6 line and on 0.1.7+, with unchanged behaviour on older DSH versions.
|
|
77
77
|
- `0.1.1-rc.2` and earlier are not supported: their client runtime lacks the `sessions`/`workspaces`/`uiWorkspace` services, so the plugin UI silently fails to render.
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
<details>
|
|
80
|
+
<summary>peer window mechanics (read before upgrading dsh)</summary>
|
|
81
|
+
|
|
82
|
+
peerDependencies open a window per minor line, consistent with the `dsh.compatibility.dshReleases` declaration:
|
|
80
83
|
|
|
81
|
-
- Official DSH plugin command: install and auto-mount into the web profile
|
|
82
|
-
```powershell
|
|
83
|
-
dsh plugin --profile web add dsh-recall-plugin
|
|
84
|
-
```
|
|
85
|
-
- Or install directly from git:
|
|
86
|
-
```powershell
|
|
87
|
-
dsh plugin --profile web add github:limbo947/dsh-recall-plugin
|
|
88
84
|
```
|
|
89
|
-
-
|
|
90
|
-
```powershell
|
|
91
|
-
dsh web # run in the foreground
|
|
92
|
-
pm2 restart <your-dsh-name> # if managed by pm2
|
|
85
|
+
>=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7 || >=0.1.7-alpha.1 <0.1.8 || >=0.2.0-rc.1 <0.2.1
|
|
93
86
|
```
|
|
94
87
|
|
|
95
|
-
|
|
88
|
+
Each line is anchored at its first verified version with an upper bound at the next minor, so later prereleases/releases within a verified line are admitted without touching the peer declaration. The 0.2.0 line needs its own tuple: npm's prerelease gate only admits a prerelease from a comparator with the same tuple, so the existing `<0.1.8` segment cannot admit `0.2.0-rc.1`.
|
|
89
|
+
|
|
90
|
+
</details>
|
|
91
|
+
|
|
92
|
+
Install & verify:
|
|
93
|
+
|
|
94
|
+
- Official DSH plugin command (pick the profile for your frontend); installs and auto-mounts into that profile:
|
|
95
|
+
|
|
96
|
+
```powershell
|
|
97
|
+
dsh plugin --profile web add dsh-recall-plugin # Web UI
|
|
98
|
+
dsh plugin --profile desktop add dsh-recall-plugin # Desktop
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- Or install directly from git:
|
|
102
|
+
|
|
103
|
+
```powershell
|
|
104
|
+
dsh plugin --profile web add github:limbo947/dsh-recall-plugin
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- Restart the DSH process (pick whichever matches how you start it):
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
dsh web # run in the foreground
|
|
111
|
+
pm2 restart <your-dsh-name> # if managed by pm2
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- **Verify**: after restarting, hard-refresh the page (Ctrl+Shift+R) and hover over any user message sent after the plugin was enabled — the "↶" appearing next to the copy button means it works. No button? Nine times out of ten the DSH process wasn't restarted, or git CLI isn't on PATH.
|
|
115
|
+
- **Uninstall** (removes both the dependency and the mount layer):
|
|
116
|
+
|
|
117
|
+
```powershell
|
|
118
|
+
dsh plugin --profile web remove dsh-recall-plugin
|
|
119
|
+
```
|
|
96
120
|
|
|
97
|
-
|
|
121
|
+
Snapshot data is kept under `dsh-recall-snapshots/` in home; delete that directory manually if you want it fully gone.
|
|
98
122
|
|
|
99
123
|
## Usage
|
|
100
124
|
|
|
101
|
-
A full recall of one user message sent after the plugin was enabled goes like this
|
|
125
|
+
A full recall of one user message sent after the plugin was enabled goes like this:
|
|
102
126
|
|
|
103
|
-
1. Hover over
|
|
127
|
+
1. Hover over the message (including steering messages inserted while the agent is running) — "↶ Recall" appears to the left of the copy button.
|
|
104
128
|
2. Click it → the confirmation panel shows the list of files that will change (modified / restored / deleted), plus a scope choice: "Roll back files & conversation" (default) or "Conversation only" (files stay as they are).
|
|
105
129
|
3. Click "Confirm rollback" (or "Confirm conversation recall") → files are restored to their state before that message was sent (files are untouched in conversation-only mode); the view switches to a new session (that message and everything after it is removed), while the original session is archived and can be recovered anytime.
|
|
106
130
|
|
|
@@ -119,15 +143,22 @@ All options can be edited visually in the "**Settings → Plugin Config → Reca
|
|
|
119
143
|
| `refillDraft` | true | Refill the recalled message (text and attachments) into the input box after a recall |
|
|
120
144
|
| `snapshotEnabled` | true | Master snapshot switch (off = no new snapshots; existing snapshots remain recallable) |
|
|
121
145
|
| `archiveOriginal` | true | Archive the original session after a recall (off = the original session stays in the session list) |
|
|
146
|
+
| `locale` | auto | UI language: `auto` follows the system (`navigator.language` starting with `zh` → Chinese, otherwise English), `zh` / `en` pin it explicitly. See "UI language" below |
|
|
122
147
|
|
|
123
148
|
The settings card also offers "Restore defaults" (one-click reset of all fields) and a "Recent errors" viewer/clearer.
|
|
124
149
|
|
|
150
|
+
### UI language
|
|
151
|
+
|
|
152
|
+
Every UI surface the plugin draws itself (recall button and confirmation panel, toasts, the three settings cards, the snapshot manager tree, recent errors) is bilingual and driven by `locale` — switch it in the "Interface" group of the config card; saving applies immediately (the settings page switches at once, chat-side wording follows on the next page reload).
|
|
153
|
+
|
|
154
|
+
Two explicit limits: (1) field descriptions rendered by the host's own settings form (i.e. the `Schema.description()` texts) stay in Chinese — the plugin cannot localize those; the plugin's own config card is unaffected. (2) Host-side errors that carry dynamic detail (a failed rollback's rescue outcome, a specific validation reason, a raw exception) are shown verbatim in the panel and "Recent errors" instead of a localized short phrase — we prefer mixed-language output over swallowing the detail you need to troubleshoot.
|
|
155
|
+
|
|
125
156
|
## Snapshot Maintenance & Cleanup
|
|
126
157
|
|
|
127
158
|
The plugin manages disk usage automatically — no manual housekeeping needed:
|
|
128
159
|
|
|
129
|
-
- **Periodic gc**: every 50 snapshots or 24 hours since the last gc (whichever comes first, thresholds configurable), `git gc` runs in the background to pack loose objects
|
|
130
|
-
- **Cap & retention**: up to 500 snapshots per workspace by default (oldest pruned beyond the cap)
|
|
160
|
+
- **Periodic gc**: every 50 snapshots or 24 hours since the last gc (whichever comes first, thresholds configurable), `git gc` runs in the background to pack loose objects — lossless, every snapshot remains recallable. The throttle token lives in `gc.stamp` inside the shadow repository, so restarting DSH does not reset the cycle.
|
|
161
|
+
- **Cap & retention**: up to 500 snapshots per workspace by default (oldest pruned beyond the cap), plus optional age-based retention via `retentionDays`; the two triggers work independently and can both be adjusted or disabled in the config card.
|
|
131
162
|
- **Session-deletion cleanup**: once a session is permanently deleted (its log gone from disk), the next maintenance pass automatically removes all of its snapshots and frees the space. **Archiving is not deletion** — logs of sessions archived by the recall feature itself still exist, so their snapshots are kept and recoverable from the archive. The check is conservative: a session that is merely cold (not in memory) is never cleaned, and when the log's state cannot be verified, it is left alone.
|
|
132
163
|
- **User-defined exclusions**: open the "**Settings → Plugin Config → Recall Plugin**" card (collapsed by default; click the header to expand) to edit snapshot exclusions visually — type a path or pattern and press Enter to add it, one-click append for common patterns (`dist/`, `*.log`, `.env`, …), and saved changes take effect on the very next snapshot/recall, no restart needed. Alternatively, edit `$DSH_HOME/dsh-recall-snapshots/exclude.txt` directly (or `~/.dsh/dsh-recall-snapshots/exclude.txt` when unset; UTF-8): one gitignore-style pattern per line, lines starting with `#` are comments — both paths edit the same configuration. For example:
|
|
133
164
|
|
|
@@ -139,7 +170,7 @@ The plugin manages disk usage automatically — no manual housekeeping needed:
|
|
|
139
170
|
```
|
|
140
171
|
|
|
141
172
|
This applies to all projects (when home is unwritable and a workspace falls back to in-project storage, it gets its own independent exclusion config, listed as a separate card in the settings tab). New exclusions only affect future snapshots; **when recalling to an earlier snapshot, files that weren't excluded at that time are still restored** (returning to the state as it was — that's exactly what recall means). To fully purge a directory that already made it into snapshots, manually delete the corresponding hash directory under `dsh-recall-snapshots/` in home.
|
|
142
|
-
- **Tree-view snapshot manager**: open "**Settings → Plugin Config → Recall Plugin → Snapshot Manager**"
|
|
173
|
+
- **Tree-view snapshot manager**: open "**Settings → Plugin Config → Recall Plugin → Snapshot Manager**" for a three-level tree — workspace (folder name) → session (session title, recall chains grouped into version families) → snapshot (time + message content summary, hover for the full content). Search and "load more" are supported; workspace and session nodes expand/collapse; every level has a delete button on its right, with a confirmation before deletion — deleting a workspace clears all of its snapshots, deleting a session clears all of that session's snapshots, deleting a leaf removes just that single snapshot; a confirmed "Delete all" button sits at the top.
|
|
143
174
|
|
|
144
175
|
## How It Works
|
|
145
176
|
|
|
@@ -154,6 +185,65 @@ When each user message is sent (before the agent touches any files), the workspa
|
|
|
154
185
|
git --git-dir="<store>\git\.git" ls-tree -r --name-only snap-<messageID>
|
|
155
186
|
```
|
|
156
187
|
|
|
188
|
+
- The format and version-compatibility rules for every file in the store directory (index, recall chain, format marker, intent journal, etc.) live in [docs/format.md](docs/format.md).
|
|
189
|
+
|
|
190
|
+
## Event Contract (for same-host plugins)
|
|
191
|
+
|
|
192
|
+
When a recall reaches its terminal state, the plugin broadcasts two public cordis events (2.4.8+). Any plugin on the same host can subscribe — for example, a long-term-memory plugin can purge memories written by the recalled turn:
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
// inside a same-host plugin
|
|
196
|
+
ctx.on('dsh-recall/complete', (e) => {
|
|
197
|
+
if (e.chatReverted) memory.purgeTurn(e.root, e.sessionId, e.cutSeq)
|
|
198
|
+
})
|
|
199
|
+
ctx.on('dsh-recall/failed', (e) => {
|
|
200
|
+
if (e.stage === 'fork') log.warn('files reverted but the conversation did not', e)
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`dsh-recall/complete` — recall succeeded:
|
|
205
|
+
|
|
206
|
+
| Field | Type | Meaning |
|
|
207
|
+
| --- | --- | --- |
|
|
208
|
+
| `version` | `1` | Contract version (see evolution rules below) |
|
|
209
|
+
| `sessionId` | `string` | The original session being recalled |
|
|
210
|
+
| `childSessionId` | `string \| null` | The forked session; `null` for a files-only recall |
|
|
211
|
+
| `scope` | `'both' \| 'session-only'` | Recall scope |
|
|
212
|
+
| `cutSeq` | `number \| null` | Conversation cut point; `null` when the message was the session's first |
|
|
213
|
+
| `messageId` | `string` | ID of the recalled message (snapshot primary key) |
|
|
214
|
+
| `root` | `string \| null` | Workspace root path; `null` if host resolution failed |
|
|
215
|
+
| `count` | `number` | Number of files reverted (always `0` for `session-only`) |
|
|
216
|
+
| `chatReverted` | `boolean` | Whether the conversation actually reverted (downstream gate, see below) |
|
|
217
|
+
| `archiveRequested` | `boolean` | Whether archiving of the original session was initiated (fire-and-forget, not completion) |
|
|
218
|
+
| `time` | `number` | When the host received the report (ms) |
|
|
219
|
+
|
|
220
|
+
`dsh-recall/failed` — recall failed:
|
|
221
|
+
|
|
222
|
+
| Field | Type | Meaning |
|
|
223
|
+
| --- | --- | --- |
|
|
224
|
+
| `version` | `1` | Contract version |
|
|
225
|
+
| `stage` | `'execute' \| 'fork'` | Failure stage: file revert rejected/errored, or files reverted but conversation revert failed |
|
|
226
|
+
| `sessionId` | `string \| null` | The original session being recalled |
|
|
227
|
+
| `messageId` | `string` | ID of the recalled message |
|
|
228
|
+
| `scope` | `'both' \| 'session-only'` | Recall scope |
|
|
229
|
+
| `cutSeq` | `number \| null` | Cut point known to the client (may not have been used) |
|
|
230
|
+
| `root` | `string \| null` | Workspace root path; `null` if resolution failed |
|
|
231
|
+
| `code` | `string?` | Only for `stage='execute'`: host error code (`AGENT_BUSY` / `NO_SNAPSHOT` / …) |
|
|
232
|
+
| `error` | `string` | Error description |
|
|
233
|
+
| `time` | `number` | When the host received the report (ms) |
|
|
234
|
+
|
|
235
|
+
Semantics every consumer should know:
|
|
236
|
+
|
|
237
|
+
- **Only terminal states emit events.** The "preview went stale, auto re-previewing" (STALE) path is an intermediate state and emits nothing; neither does a cancelled/closed panel.
|
|
238
|
+
- **A fork failure emits `failed(stage:'fork')` and never `complete`** — at that point files are reverted but the conversation is not; emitting `complete` would make downstream purge memories of a still-intact conversation. Gate "the conversation actually reverted" on `complete.chatReverted` (a files-only `complete` has `chatReverted:false`).
|
|
239
|
+
- For `failed(stage:'execute')` originating from an exception (rather than a guard rejection), **whether files were reverted is unknown** — treat it conservatively.
|
|
240
|
+
- Retries/repeated recalls may emit multiple events for the same `(sessionId, messageId, cutSeq)` triple; downstream should deduplicate on it.
|
|
241
|
+
- Payloads contain no message bodies — only IDs / seq / paths — and are visible only to plugins on the same host.
|
|
242
|
+
|
|
243
|
+
Versioning: shipped as a stable public contract from day one (not experimental). Additive changes ride minor releases; breaking changes ride a major release with `version` bumped to `2`, so downstream can branch on it.
|
|
244
|
+
|
|
245
|
+
Version skew: an old plugin frontend (<2.4.8) never reports, so events never fire; a new frontend with an old host (<2.4.8) gets a missing endpoint and silently ignores it — recall itself is unaffected.
|
|
246
|
+
|
|
157
247
|
## Local Development (without publishing)
|
|
158
248
|
|
|
159
249
|
Point the profile's dependency for this package at your clone via `link:`; DSH loads the built `lib/` artifacts from the workspace (source lives in `src/`), so run `npm run build` after editing `src/`, then restart DSH — no copying or publishing needed:
|
|
@@ -172,11 +262,12 @@ Note: all source lives in `src/` (host in `src/host/`, browser side in `src/clie
|
|
|
172
262
|
|
|
173
263
|
### Tests
|
|
174
264
|
|
|
175
|
-
- `npm test`: pure-logic unit tests (vitest,
|
|
265
|
+
- `npm test`: pure-logic unit tests (vitest, 39 files / 497 cases, no DSH dependency, runs identically in CI and locally) — config parsing, snapshot parsers, rescue orchestration, error classification, the disk-format guard, the recall intent journal, recall terminal-event payload assembly, i18n dictionaries plus a missing-key scan, script-template same-name-export contract, shell execution dual-channel split and failure grading, settings bridge across both generations, client pure functions, published-package layout, snapshot index persistence, storage caps and retention, etc.;
|
|
266
|
+
- `npm run test:client`: client component tests (vitest + jsdom, 6 files / 90 cases, runs in CI) — the recall node main chain (preview→execute→fork→refill) and terminal-event reporting, snapshot-manager tree and delete flows, config/exclude card error paths, the logger switch matrix, and the zh/en rendering chains; assertions pin behaviour and structure only (className / aria / request payloads), never literal copy;
|
|
176
267
|
- `npm run test:probe`: official-API field probes (requires a local dsh installation; **must run after any dsh upgrade**) — pins fields like `renderMessageImages`/`node`/`cwd`, `atSeq`/`increaseTitle` and the fork-cut anchors of `sessions.fork`, `listSessions` record shape, `AgentRegistry`, the shell execution seam (`execute`/`ShellExecution.result`), the settings surface (`SettingsForms`/profile entry id/volatile gate) plus `loader/volatile-update` and the `Fiber.entry` shape, and goes red on violation;
|
|
177
268
|
- `npm run verify:host`: assembly gate (requires a local dsh installation) — boots the plugin with a real cordis context in two passes (legacy settings stub and a modern-face-only stub), asserting inject declarations, endpoint registration, Config schema, teardown cleanliness and settings-face dispatch, catching assembly regressions before release;
|
|
178
269
|
- `npm run build`: full host+client build (mandatory after any `src/` change); `npm run check:dsh`: dsh version inspection (pre-release).
|
|
179
|
-
- CI (GitHub Actions) runs `npm ci --legacy-peer-deps` + `npm run typecheck` + `npm test` + unified artifact-freshness check (`npm run build && git diff --exit-code lib/`; probes and the assembly gate only run on machines with dsh).
|
|
270
|
+
- CI (GitHub Actions) runs `npm ci --legacy-peer-deps` + `npm run typecheck` + `npm test` + `npm run test:client` + unified artifact-freshness check (`npm run build && git diff --exit-code lib/`; probes and the assembly gate only run on machines with dsh).
|
|
180
271
|
|
|
181
272
|
## License
|
|
182
273
|
|