dsh-rewind-plugin 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +17 -2
- package/README.md +3 -3
- package/SECURITY.md +9 -3
- package/docs/README.md +1 -0
- package/docs/compat/audit.md +134 -122
- package/docs/format.md +5 -4
- package/docs/harness-reference.md +1 -1
- package/docs/snapshot-auto-cleanup.md +75 -56
- package/docs/snapshot-auto-cleanup.zh.md +36 -21
- package/lib/client.js +7 -64
- package/lib/index.js +199 -39
- package/lib/types/client/index.d.ts +12 -12
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/index.d.ts +6 -3
- package/lib/types/locales.d.ts +5 -0
- package/lib/types/snapshot-cleanup.d.ts +28 -8
- package/lib/types/snapshot.d.ts +80 -2
- package/package.json +6 -1
|
@@ -1,42 +1,57 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 快照清理
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
插件会在每次修改文件前保存一份备份,方便你把代码回退到之前的状态。这些备份叫**快照**,按消息分组、按会话存放。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`/snapshot-auto-cleanup` 帮你管理这些快照——既可以回收旧会话占用的磁盘,也可以清空**当前**会话、让它从"现在"重新开始记录回退历史。
|
|
6
|
+
|
|
7
|
+
## 它能做什么
|
|
8
|
+
|
|
9
|
+
**自动清理**(默认关闭):插件会记住哪些会话你已不再使用,并定期移除它们的快照以节省磁盘。它**绝不**触碰你当前活跃的会话,也**绝不**触碰你的对话。
|
|
10
|
+
|
|
11
|
+
**手动:**即使自动清理是关闭的,你也可以自己执行清理:
|
|
12
|
+
|
|
13
|
+
- `/snapshot-auto-cleanup run [--apply]` — 先预演,再真正移除你已有一阵子没用的会话的快照。
|
|
14
|
+
- `/snapshot-auto-cleanup run --current [--apply]` — 先预演,再真正清空**当前**会话的快照。这会让它从"现在"重新开始记录回退历史(你的对话不受影响)。若此刻有正在进行中的回合,插件会先暂停它,再清空。
|
|
6
15
|
|
|
7
16
|
## 命令
|
|
8
17
|
|
|
9
18
|
```
|
|
10
|
-
/snapshot-auto-cleanup
|
|
11
|
-
/snapshot-auto-cleanup on|off
|
|
12
|
-
/snapshot-auto-cleanup max-age <天数>
|
|
13
|
-
/snapshot-auto-cleanup run
|
|
14
|
-
/snapshot-auto-cleanup run --apply
|
|
19
|
+
/snapshot-auto-cleanup 查看当前设置
|
|
20
|
+
/snapshot-auto-cleanup on|off 开启/关闭自动清理
|
|
21
|
+
/snapshot-auto-cleanup max-age <天数> 会话失活多少天后移除其快照
|
|
22
|
+
/snapshot-auto-cleanup run 预演:看自动清理会移除什么
|
|
23
|
+
/snapshot-auto-cleanup run --apply 真正移除这些快照
|
|
24
|
+
/snapshot-auto-cleanup run --current 预演:看清空本会话快照会怎样
|
|
25
|
+
/snapshot-auto-cleanup run --current --apply 真正清空本会话快照
|
|
15
26
|
```
|
|
16
27
|
|
|
17
|
-
`run`
|
|
28
|
+
`run` 一开始总是预演;加 `--apply` 才真正执行。`run` 无论自动清理是否开启都能用。
|
|
18
29
|
|
|
19
|
-
##
|
|
30
|
+
## 设置
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
设置保存在 `<dsh home>/snapshot-cleanup.json`:
|
|
22
33
|
|
|
23
34
|
```json
|
|
24
35
|
{ "enabled": false, "maxAgeDays": 30 }
|
|
25
36
|
```
|
|
26
37
|
|
|
27
38
|
- `enabled` — 是否运行自动清理(默认 `false`)。
|
|
28
|
-
- `maxAgeDays` —
|
|
39
|
+
- `maxAgeDays` — 会话失活多少天后移除其快照(默认 `30`)。只接受正数,因此一个损坏的设置永远不可能"删掉一切"。
|
|
40
|
+
|
|
41
|
+
你可以用环境变量 `DSH_SNAPSHOT_CLEANUP_CONFIG` 指向别的文件。该文件只在运行 `/snapshot-auto-cleanup` 时写入;文件缺失按安全默认值(关闭)处理。
|
|
42
|
+
|
|
43
|
+
## 自动清理何时运行
|
|
44
|
+
|
|
45
|
+
自动清理每次运行(宿主启动)**最多检查一次**,发生在首次活动时(你发出一条消息或某次工具调用完成),且仅在**已开启**且距上次检查**已满 24 小时**时才执行。这个 24 小时时钟会写入 `<dsh home>/snapshot-cleanup-last-sweep.json`,重启不会重置。它后台运行,从不阻塞你在做的事。
|
|
29
46
|
|
|
30
|
-
|
|
47
|
+
想让改动立即生效,请用 `run`;自动清理要到下次运行才会采用新设置。
|
|
31
48
|
|
|
32
|
-
##
|
|
49
|
+
## 安全
|
|
33
50
|
|
|
34
|
-
|
|
51
|
+
- 只会移除回退用的**快照**(那份文件备份)。你的对话绝不会被动到,插件也绝不改写或删除你的会话历史。
|
|
52
|
+
- 自动清理**永不**移除你**当前活跃**会话的快照——只清理已失活超过阈值的会话。
|
|
53
|
+
- `run --current` 会清空当前会话的快照。这对该会话的文件回退历史是**不可逆**的:你无法回退到清空之前的代码状态,但你的对话保持完整,会话会从"现在"开始重新记录快照。
|
|
35
54
|
|
|
36
|
-
##
|
|
55
|
+
## 已知限制
|
|
37
56
|
|
|
38
|
-
-
|
|
39
|
-
- "不活跃"以 mtime 判定:仍在写入的会话会不断把"最新成员"时间戳往后推,因此永远到不了可被清理的失活阈值。
|
|
40
|
-
- 去重 `ref` 链接是会话内相对的,所以移除整目录不会让别处产生悬空链接。
|
|
41
|
-
- 权衡:开启自动清理后,某个长时间闲置才被重新打开的会话,只能从剩余(最近 100 组)锚点回退,旧快照已不存在;对话日志永不受影响。
|
|
42
|
-
- 手动删除整个存储目录依然安全(下次捕获时自动重建);自动清理只是把这种删除限定到长期不活跃的会话。
|
|
57
|
+
插件每个会话只保留**最近 100 条消息**的快照。若你在一个很长的会话里多次回退或压缩,这 100 个名额常会被"已经无法再回退到"的消息占满,你可能无法回退到想要的位置。(Claude Code 也是同样行为。)想恢复到一个干净状态,运行 `/snapshot-auto-cleanup run --current --apply` 清空当前会话、重新开始。
|
package/lib/client.js
CHANGED
|
@@ -986,8 +986,7 @@ var zh = {
|
|
|
986
986
|
"popover.impact.restore": "\u8FD8\u539F {path}",
|
|
987
987
|
"popover.impact.delete": "\u5220\u9664 {path}",
|
|
988
988
|
"popover.confirm": "\u786E\u8BA4\u56DE\u9000",
|
|
989
|
-
"popover.back": "\u8FD4\u56DE"
|
|
990
|
-
"guard.hint": "/rewind \u624B\u52A8\u8F93\u5165\u4E0D\u63A5\u53D7\u53C2\u6570\uFF0C\u8BF7\u76F4\u63A5\u8F93\u5165 /rewind \u6253\u5F00\u56DE\u9000\u83DC\u5355"
|
|
989
|
+
"popover.back": "\u8FD4\u56DE"
|
|
991
990
|
};
|
|
992
991
|
var en = {
|
|
993
992
|
"button.aria": "Rewind to this message",
|
|
@@ -1013,8 +1012,7 @@ var en = {
|
|
|
1013
1012
|
"popover.impact.restore": "Restore {path}",
|
|
1014
1013
|
"popover.impact.delete": "Delete {path}",
|
|
1015
1014
|
"popover.confirm": "Confirm rewind",
|
|
1016
|
-
"popover.back": "Back"
|
|
1017
|
-
"guard.hint": "/rewind takes no typed arguments \u2014 enter /rewind to open the rewind picker."
|
|
1015
|
+
"popover.back": "Back"
|
|
1018
1016
|
};
|
|
1019
1017
|
|
|
1020
1018
|
// src/client/index.ts
|
|
@@ -1069,8 +1067,7 @@ function apply(ctx) {
|
|
|
1069
1067
|
const card = textarea?.closest("[data-composer-card]");
|
|
1070
1068
|
return card ?? textarea ?? document.body;
|
|
1071
1069
|
};
|
|
1072
|
-
|
|
1073
|
-
name: "rewind",
|
|
1070
|
+
const rewindPopupSpec = {
|
|
1074
1071
|
// The picker exists exactly while the surface has a reachable user
|
|
1075
1072
|
// message: a fresh session (no candidates) falls through to the host
|
|
1076
1073
|
// command, which fails with "no user messages" — matching the harness's
|
|
@@ -1105,66 +1102,12 @@ function apply(ctx) {
|
|
|
1105
1102
|
});
|
|
1106
1103
|
}
|
|
1107
1104
|
}
|
|
1108
|
-
});
|
|
1109
|
-
const PARAM_REWIND = /^\s*\/rewind\s+\S+/i;
|
|
1110
|
-
const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR2);
|
|
1111
|
-
const hasParamRewindDraft = () => {
|
|
1112
|
-
const textarea = composerTextarea();
|
|
1113
|
-
return textarea !== null && PARAM_REWIND.test(textarea.value);
|
|
1114
|
-
};
|
|
1115
|
-
let guardHintEl = null;
|
|
1116
|
-
let guardHintTimer;
|
|
1117
|
-
const showGuardHint = () => {
|
|
1118
|
-
if (guardHintEl !== null) guardHintEl.remove();
|
|
1119
|
-
if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
|
|
1120
|
-
const textarea = composerTextarea();
|
|
1121
|
-
if (textarea === null) return;
|
|
1122
|
-
const card = textarea.closest("[data-composer-card]");
|
|
1123
|
-
const hint = document.createElement("div");
|
|
1124
|
-
hint.className = "dsh-rewind-guard-hint";
|
|
1125
|
-
hint.setAttribute("role", "status");
|
|
1126
|
-
hint.textContent = t("guard.hint");
|
|
1127
|
-
document.body.appendChild(hint);
|
|
1128
|
-
const rect = card instanceof HTMLElement ? card.getBoundingClientRect() : textarea.getBoundingClientRect();
|
|
1129
|
-
hint.style.left = `${Math.round(rect.left)}px`;
|
|
1130
|
-
hint.style.bottom = `${Math.round(window.innerHeight - rect.top + 8)}px`;
|
|
1131
|
-
guardHintEl = hint;
|
|
1132
|
-
guardHintTimer = window.setTimeout(() => {
|
|
1133
|
-
hint.remove();
|
|
1134
|
-
if (guardHintEl === hint) guardHintEl = null;
|
|
1135
|
-
guardHintTimer = void 0;
|
|
1136
|
-
}, 3200);
|
|
1137
|
-
};
|
|
1138
|
-
const onParamRewindSubmit = (event) => {
|
|
1139
|
-
if (!hasParamRewindDraft()) return;
|
|
1140
|
-
event.preventDefault();
|
|
1141
|
-
event.stopPropagation();
|
|
1142
|
-
showGuardHint();
|
|
1143
|
-
};
|
|
1144
|
-
const onKeyDownGuard = (event) => {
|
|
1145
|
-
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
|
|
1146
|
-
onParamRewindSubmit(event);
|
|
1147
1105
|
};
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const button = target.closest("button");
|
|
1153
|
-
if (button === null) return;
|
|
1154
|
-
const card = button.closest("[data-composer-card]");
|
|
1155
|
-
if (card === null) return;
|
|
1156
|
-
const all = card.querySelectorAll("button");
|
|
1157
|
-
if (all[all.length - 1] !== button) return;
|
|
1158
|
-
if (button.querySelector("rect") !== null) return;
|
|
1159
|
-
onParamRewindSubmit(event);
|
|
1160
|
-
};
|
|
1161
|
-
document.addEventListener("keydown", onKeyDownGuard, true);
|
|
1162
|
-
document.addEventListener("click", onClickGuard, true);
|
|
1106
|
+
for (const name2 of ["rewind", "undo"]) {
|
|
1107
|
+
yield commandUi.decorate({ name: name2, ...rewindPopupSpec });
|
|
1108
|
+
}
|
|
1109
|
+
const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR2);
|
|
1163
1110
|
yield () => {
|
|
1164
|
-
document.removeEventListener("keydown", onKeyDownGuard, true);
|
|
1165
|
-
document.removeEventListener("click", onClickGuard, true);
|
|
1166
|
-
if (guardHintEl !== null) guardHintEl.remove();
|
|
1167
|
-
if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
|
|
1168
1111
|
style.remove();
|
|
1169
1112
|
};
|
|
1170
1113
|
}, "dsh-rewind client lifecycle");
|
package/lib/index.js
CHANGED
|
@@ -8,7 +8,7 @@ var en = {
|
|
|
8
8
|
"usage.title": "Usage:",
|
|
9
9
|
"usage.noArgs": " /rewind (no args) withdraw the most recent user message",
|
|
10
10
|
"usage.seq": " /rewind @<seq> chat|both rewind to the given message (chat = conversation only / both = conversation + files)",
|
|
11
|
-
"usage.blocked": "
|
|
11
|
+
"usage.blocked": " /rewind or /undo open the rewind picker",
|
|
12
12
|
"describeTarget.seq": "seq {seq}",
|
|
13
13
|
"describeTarget.index": "message {index}",
|
|
14
14
|
"plan.rewinding": "Rewind to seq {targetSeq}, removing {count} node(s) from the model context (conversation log kept).",
|
|
@@ -32,7 +32,7 @@ var en = {
|
|
|
32
32
|
"chooseMode": "Rewind to {target}. Choose a mode:\n /rewind {target} chat conversation only\n /rewind {target} both conversation + file restore",
|
|
33
33
|
"command.description": "Rewind the conversation back to an earlier user message (optionally restoring files)",
|
|
34
34
|
"cleanup.description": "Manage automatic cleanup of session snapshot backups",
|
|
35
|
-
"cleanup.inputHint": "on | off | max-age <days> | run [--apply]
|
|
35
|
+
"cleanup.inputHint": "on | off | max-age <days> | run [--apply] [--current]",
|
|
36
36
|
"cleanup.status": "Auto-cleanup: {state}. Max age: {days} day(s). Config: {path} ({present}).",
|
|
37
37
|
"cleanup.enabled": "enabled",
|
|
38
38
|
"cleanup.disabled": "disabled",
|
|
@@ -41,19 +41,24 @@ var en = {
|
|
|
41
41
|
"cleanup.onOk": "Auto-cleanup enabled.",
|
|
42
42
|
"cleanup.offOk": "Auto-cleanup disabled \u2014 all snapshots kept.",
|
|
43
43
|
"cleanup.maxAgeOk": "Auto-cleanup max age set to {days} day(s).",
|
|
44
|
-
"cleanup.cfgInvalid":
|
|
44
|
+
"cleanup.cfgInvalid": 'Snapshot cleanup config invalid: {detail}. Nothing was executed; use "on|off|max-age" to reset the config.',
|
|
45
45
|
"cleanup.saveFailed": "Could not save cleanup config: {detail}.",
|
|
46
|
-
"cleanup.runDry": "Dry-run: would remove {deleted} session snapshot backup(s). Re-run with --apply to delete.",
|
|
46
|
+
"cleanup.runDry": "Dry-run: would remove {deleted} session snapshot backup(s), freeing {freed} bytes. Re-run with --apply to delete.",
|
|
47
47
|
"cleanup.runApply": "Removed {deleted} session snapshot backup(s), freeing {freed} bytes; {kept} kept, {remaining} bytes remain.",
|
|
48
48
|
"cleanup.runFailed": "Cleanup failed: {detail}.",
|
|
49
49
|
"cleanup.skipped": "({skipped} active session(s) skipped.)",
|
|
50
|
-
"cleanup.
|
|
50
|
+
"cleanup.clearDry": "Dry-run: would clear {entries} snapshot(s) of the current session, freeing {bytes} bytes. Re-run with --apply to delete.",
|
|
51
|
+
"cleanup.clearApply": "Cleared {entries} snapshot(s) of the current session, freeing {bytes} bytes. This session now records snapshots fresh from its current state.",
|
|
52
|
+
"cleanup.clearActive": "Could not clear session {sessionId}: the session is still running and could not be stopped. Try again once it is idle.",
|
|
53
|
+
"cleanup.clearCancelled": "Clear cancelled.",
|
|
54
|
+
"cleanup.clearFailed": "Could not clear session {sessionId}: {detail}.",
|
|
55
|
+
"cleanup.usage": "Usage:\n /snapshot-auto-cleanup show status\n /snapshot-auto-cleanup on|off enable/disable auto-cleanup\n /snapshot-auto-cleanup max-age <days> set the idle cutoff\n /snapshot-auto-cleanup run [--apply] dry-run, or execute with --apply\n /snapshot-auto-cleanup run --current [--apply] dry-run/clear this session's snapshots"
|
|
51
56
|
};
|
|
52
57
|
var zh = {
|
|
53
58
|
"usage.title": "\u7528\u6CD5\uFF1A",
|
|
54
59
|
"usage.noArgs": " /rewind \uFF08\u65E0\u53C2\u6570\uFF09\u64A4\u56DE\u6700\u8FD1\u4E00\u6761\u7528\u6237\u6D88\u606F",
|
|
55
60
|
"usage.seq": " /rewind @<seq> chat|both \u56DE\u9000\u5230\u6307\u5B9A\u6D88\u606F\uFF08chat \u4EC5\u5BF9\u8BDD / both \u5BF9\u8BDD+\u6587\u4EF6\uFF09",
|
|
56
|
-
"usage.blocked": "
|
|
61
|
+
"usage.blocked": " /rewind \u6216 /undo \u6253\u5F00\u56DE\u9000\u9009\u62E9\u9762\u677F",
|
|
57
62
|
"describeTarget.seq": "seq {seq}",
|
|
58
63
|
"describeTarget.index": "\u7B2C {index} \u6761\u6D88\u606F",
|
|
59
64
|
"plan.rewinding": "\u5C06\u56DE\u9000\u5230 seq {targetSeq}\uFF0C\u4ECE\u6A21\u578B\u4E0A\u4E0B\u6587\u79FB\u9664 {count} \u4E2A\u8282\u70B9\uFF08\u5BF9\u8BDD\u65E5\u5FD7\u4FDD\u7559\uFF09\u3002",
|
|
@@ -77,7 +82,7 @@ var zh = {
|
|
|
77
82
|
"chooseMode": "\u5C06\u56DE\u9000\u5230 {target}\u3002\u9009\u62E9\u6A21\u5F0F\uFF1A\n /rewind {target} chat \u4EC5\u56DE\u9000\u5BF9\u8BDD\n /rewind {target} both \u56DE\u9000\u5BF9\u8BDD\u5E76\u8FD8\u539F\u6587\u4EF6",
|
|
78
83
|
"command.description": "\u5728\u540C\u7A97\u53E3\u5185\u5C06\u5BF9\u8BDD\u56DE\u9000\u5230\u66F4\u65E9\u7684\u7528\u6237\u6D88\u606F\uFF08\u53EF\u540C\u65F6\u8FD8\u539F\u6587\u4EF6\uFF09",
|
|
79
84
|
"cleanup.description": "\u7BA1\u7406\u4F1A\u8BDD\u5FEB\u7167\u5907\u4EFD\u7684\u81EA\u52A8\u6E05\u7406",
|
|
80
|
-
"cleanup.inputHint": "on | off | max-age <\u5929\u6570> | run [--apply]
|
|
85
|
+
"cleanup.inputHint": "on | off | max-age <\u5929\u6570> | run [--apply] [--current]",
|
|
81
86
|
"cleanup.status": "\u81EA\u52A8\u6E05\u7406\uFF1A{state}\u3002\u6700\u5927\u4FDD\u7559\u5929\u6570\uFF1A{days} \u5929\u3002\u914D\u7F6E\uFF1A{path}\uFF08{present}\uFF09\u3002",
|
|
82
87
|
"cleanup.enabled": "\u5DF2\u5F00\u542F",
|
|
83
88
|
"cleanup.disabled": "\u5DF2\u5173\u95ED",
|
|
@@ -86,13 +91,18 @@ var zh = {
|
|
|
86
91
|
"cleanup.onOk": "\u5DF2\u5F00\u542F\u81EA\u52A8\u6E05\u7406\u3002",
|
|
87
92
|
"cleanup.offOk": "\u5DF2\u5173\u95ED\u81EA\u52A8\u6E05\u7406\u2014\u2014\u4FDD\u7559\u5168\u90E8\u5FEB\u7167\u3002",
|
|
88
93
|
"cleanup.maxAgeOk": "\u5DF2\u5C06\u81EA\u52A8\u6E05\u7406\u7684\u6700\u5927\u4FDD\u7559\u5929\u6570\u8BBE\u4E3A {days} \u5929\u3002",
|
|
89
|
-
"cleanup.cfgInvalid": "\u5FEB\u7167\u6E05\u7406\u914D\u7F6E\u65E0\u6548\uFF1A{detail}\u3002\
|
|
94
|
+
"cleanup.cfgInvalid": "\u5FEB\u7167\u6E05\u7406\u914D\u7F6E\u65E0\u6548\uFF1A{detail}\u3002\u672A\u6267\u884C\u4EFB\u4F55\u64CD\u4F5C\uFF1B\u8BF7\u7528\u300Con|off|max-age\u300D\u91CD\u8BBE\u914D\u7F6E\u4EE5\u4FEE\u590D\u3002",
|
|
90
95
|
"cleanup.saveFailed": "\u65E0\u6CD5\u4FDD\u5B58\u6E05\u7406\u914D\u7F6E\uFF1A{detail}\u3002",
|
|
91
|
-
"cleanup.runDry": "\u9884\u6F14\uFF1A\u5C06\u5220\u9664 {deleted} \u4E2A\u4F1A\u8BDD\u7684\u5FEB\u7167\u5907\u4EFD\u3002\u52A0 --apply \u6B63\u5F0F\u5220\u9664\u3002",
|
|
96
|
+
"cleanup.runDry": "\u9884\u6F14\uFF1A\u5C06\u5220\u9664 {deleted} \u4E2A\u4F1A\u8BDD\u7684\u5FEB\u7167\u5907\u4EFD\uFF0C\u91CA\u653E {freed} \u5B57\u8282\u3002\u52A0 --apply \u6B63\u5F0F\u5220\u9664\u3002",
|
|
92
97
|
"cleanup.runApply": "\u5DF2\u5220\u9664 {deleted} \u4E2A\u4F1A\u8BDD\u7684\u5FEB\u7167\u5907\u4EFD\uFF0C\u91CA\u653E {freed} \u5B57\u8282\uFF1B\u4FDD\u7559 {kept} \u4E2A\uFF0C\u5269\u4F59 {remaining} \u5B57\u8282\u3002",
|
|
93
98
|
"cleanup.runFailed": "\u6E05\u7406\u5931\u8D25\uFF1A{detail}\u3002",
|
|
94
99
|
"cleanup.skipped": "\uFF08\u8DF3\u8FC7\u4E86 {skipped} \u4E2A\u6D3B\u52A8\u4F1A\u8BDD\u3002\uFF09",
|
|
95
|
-
"cleanup.
|
|
100
|
+
"cleanup.clearDry": "\u9884\u6F14\uFF1A\u5C06\u6E05\u9664\u5F53\u524D\u4F1A\u8BDD\u7684 {entries} \u4E2A\u5FEB\u7167\uFF0C\u91CA\u653E {bytes} \u5B57\u8282\u3002\u52A0 --apply \u6B63\u5F0F\u5220\u9664\u3002",
|
|
101
|
+
"cleanup.clearApply": "\u5DF2\u6E05\u9664\u5F53\u524D\u4F1A\u8BDD\u7684 {entries} \u4E2A\u5FEB\u7167\uFF0C\u91CA\u653E {bytes} \u5B57\u8282\u3002\u8BE5\u4F1A\u8BDD\u5DF2\u91CD\u7F6E\u4E3A\u4ECE\u5F53\u524D\u72B6\u6001\u91CD\u65B0\u8BB0\u5F55\u5FEB\u7167\u3002",
|
|
102
|
+
"cleanup.clearActive": "\u65E0\u6CD5\u6E05\u9664\u4F1A\u8BDD {sessionId}\uFF1A\u4F1A\u8BDD\u4ECD\u5728\u8FD0\u884C\u4E14\u672A\u80FD\u505C\u6B62\uFF0C\u8BF7\u5F85\u5176\u7A7A\u95F2\u540E\u91CD\u8BD5\u3002",
|
|
103
|
+
"cleanup.clearCancelled": "\u6E05\u7A7A\u5DF2\u53D6\u6D88\u3002",
|
|
104
|
+
"cleanup.clearFailed": "\u65E0\u6CD5\u6E05\u9664\u4F1A\u8BDD {sessionId}\uFF1A{detail}\u3002",
|
|
105
|
+
"cleanup.usage": "\u7528\u6CD5\uFF1A\n /snapshot-auto-cleanup \u67E5\u770B\u72B6\u6001\n /snapshot-auto-cleanup on|off \u5F00\u542F/\u5173\u95ED\u81EA\u52A8\u6E05\u7406\n /snapshot-auto-cleanup max-age <\u5929\u6570> \u8BBE\u7F6E\u5931\u6D3B\u9608\u503C\uFF08\u5929\uFF09\n /snapshot-auto-cleanup run [--apply] \u9884\u6F14\uFF0C\u6216\u52A0 --apply \u6267\u884C\n /snapshot-auto-cleanup run --current [--apply] \u9884\u6F14/\u6E05\u9664\u672C\u4F1A\u8BDD\u5FEB\u7167"
|
|
96
106
|
};
|
|
97
107
|
var HOST_DICTS = { en, zh };
|
|
98
108
|
function translate(lang, key, params = {}) {
|
|
@@ -231,8 +241,9 @@ function execSessionCwd(exec, requestedPath) {
|
|
|
231
241
|
import { createHash } from "node:crypto";
|
|
232
242
|
import { lstat, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
233
243
|
import { dirname, join } from "node:path";
|
|
234
|
-
import {
|
|
235
|
-
var
|
|
244
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
245
|
+
var SNAPSHOT_DIR_NAME = "rewind-snapshots";
|
|
246
|
+
var DEFAULT_SNAPSHOT_ROOT = join(resolveDshHome(), SNAPSHOT_DIR_NAME);
|
|
236
247
|
var SNAPSHOT_ROOT_ENV = "DSH_REWIND_SNAPSHOT_DIR";
|
|
237
248
|
var MAX_ANCHOR_GROUPS = 100;
|
|
238
249
|
function isLinkEntry(entry) {
|
|
@@ -337,11 +348,6 @@ async function dirSizeAndLastActive(dir) {
|
|
|
337
348
|
return { size, lastActiveMs };
|
|
338
349
|
}
|
|
339
350
|
var SnapshotStore = class _SnapshotStore {
|
|
340
|
-
constructor(root = process.env[SNAPSHOT_ROOT_ENV] ?? DEFAULT_SNAPSHOT_ROOT, opts) {
|
|
341
|
-
this.root = root;
|
|
342
|
-
this.dedup = opts?.dedup ?? true;
|
|
343
|
-
}
|
|
344
|
-
root;
|
|
345
351
|
/** Debounce window for the per-commit prune (keeps the readdir+sort off the hot path). */
|
|
346
352
|
static PRUNE_INTERVAL_MS = 1e3;
|
|
347
353
|
lastPruneAt = 0;
|
|
@@ -360,6 +366,8 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
360
366
|
lastEntryTime = 0;
|
|
361
367
|
/** Store options; `dedup` toggles in-place content dedup (default on). */
|
|
362
368
|
dedup;
|
|
369
|
+
/** Resolved checkpoint store root (absolute); see the constructor's fallback. */
|
|
370
|
+
root;
|
|
363
371
|
/**
|
|
364
372
|
* In-memory per-path "most recent entry" for content dedup, keyed by
|
|
365
373
|
* `<sessionId>\0<path>`. Each value holds the entry's effective `before`
|
|
@@ -370,6 +378,10 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
370
378
|
lastEntry = /* @__PURE__ */ new Map();
|
|
371
379
|
/** Sessions whose dedup state has been seeded from disk this process. */
|
|
372
380
|
seededSessions = /* @__PURE__ */ new Set();
|
|
381
|
+
constructor(root, opts) {
|
|
382
|
+
this.dedup = opts?.dedup ?? true;
|
|
383
|
+
this.root = root ?? process.env[SNAPSHOT_ROOT_ENV] ?? join(resolveDshHome(opts?.dshHome), SNAPSHOT_DIR_NAME);
|
|
384
|
+
}
|
|
373
385
|
/** Absolute path of one session's snapshot directory (id sanitized). */
|
|
374
386
|
sessionDir(sessionId) {
|
|
375
387
|
return join(this.root, safeSessionId(sessionId));
|
|
@@ -1181,6 +1193,103 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
1181
1193
|
}
|
|
1182
1194
|
return paths;
|
|
1183
1195
|
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Summarize a session's on-disk footprint for a clear dry-run: anchor-group
|
|
1198
|
+
* count, committed checkpoint-entry count, restore-journal count, and total
|
|
1199
|
+
* bytes. Walks with `lstat` (never follows a symlink, so a hostile symlink
|
|
1200
|
+
* cannot escape the store root or inflate the measurement) and skips
|
|
1201
|
+
* dot-prefixed temp leftovers and non-`.json` members — they are never
|
|
1202
|
+
* checkpoint entries.
|
|
1203
|
+
*/
|
|
1204
|
+
async sessionStats(sessionId) {
|
|
1205
|
+
const sessionDir = this.sessionDir(sessionId);
|
|
1206
|
+
let names;
|
|
1207
|
+
try {
|
|
1208
|
+
names = await readdir(sessionDir);
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (error.code === "ENOENT") return { anchorGroups: 0, entries: 0, journals: 0, bytes: 0 };
|
|
1211
|
+
throw error;
|
|
1212
|
+
}
|
|
1213
|
+
let anchorGroups = 0;
|
|
1214
|
+
let entries = 0;
|
|
1215
|
+
let journals = 0;
|
|
1216
|
+
let bytes = 0;
|
|
1217
|
+
for (const name2 of names) {
|
|
1218
|
+
if (name2.startsWith(".")) continue;
|
|
1219
|
+
const full = join(sessionDir, name2);
|
|
1220
|
+
let st;
|
|
1221
|
+
try {
|
|
1222
|
+
st = await lstat(full);
|
|
1223
|
+
} catch {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (st.isDirectory()) {
|
|
1227
|
+
if (!Number.isSafeInteger(Number(name2))) continue;
|
|
1228
|
+
anchorGroups++;
|
|
1229
|
+
let files;
|
|
1230
|
+
try {
|
|
1231
|
+
files = await readdir(full);
|
|
1232
|
+
} catch {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
for (const file of files) {
|
|
1236
|
+
if (!file.endsWith(".json")) continue;
|
|
1237
|
+
entries++;
|
|
1238
|
+
const fileSt = await lstat(join(full, file)).catch(() => void 0);
|
|
1239
|
+
if (fileSt !== void 0) bytes += fileSt.size;
|
|
1240
|
+
}
|
|
1241
|
+
} else if (name2.startsWith(_SnapshotStore.JOURNAL_PREFIX) && name2.endsWith(".json")) {
|
|
1242
|
+
journals++;
|
|
1243
|
+
bytes += st.size;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return { anchorGroups, entries, journals, bytes };
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Remove a session's ENTIRE snapshot directory — every anchor group, every
|
|
1250
|
+
* checkpoint entry, and every restore journal — and reset the store's
|
|
1251
|
+
* in-memory dedup state so the session starts recording fresh from the
|
|
1252
|
+
* current workspace state. This is the manual "get rid of this session's
|
|
1253
|
+
* records NOW" action on the ACTIVE session the user is driving (it is never
|
|
1254
|
+
* targetable by id; that is a directory-manipulation concern the user can do
|
|
1255
|
+
* directly).
|
|
1256
|
+
*
|
|
1257
|
+
* SEMANTICS — clearing is an explicit abandonment: issuing the command means
|
|
1258
|
+
* the user accepts that this session's snapshot archive goes away. It is
|
|
1259
|
+
* therefore NOT gated on the state of any restore journal. A clear and a
|
|
1260
|
+
* restore are both slash commands the host runs to completion for an agent,
|
|
1261
|
+
* so they never interleave — any non-terminal journal present on disk is a
|
|
1262
|
+
* stale orphan from a previous (crashed) process, and discarding it is the
|
|
1263
|
+
* correct, safe resolution of that abandoned restore.
|
|
1264
|
+
*
|
|
1265
|
+
* SAFETY (this module's real concern is the plugin's ongoing BEHAVIOR, not
|
|
1266
|
+
* losing snapshots):
|
|
1267
|
+
* - Only the session dir is removed; dedup refs are session-relative, so
|
|
1268
|
+
* there is no cross-session dangling to materialize (the same rationale as
|
|
1269
|
+
* {@link pruneStale}'s whole-dir removal).
|
|
1270
|
+
* - The in-memory dedup state (`lastEntry` / `seededSessions`) is ALWAYS
|
|
1271
|
+
* reset on an apply — even when the dir was already empty. A stale
|
|
1272
|
+
* in-memory entry (e.g. a session whose dir was removed out-of-band) would
|
|
1273
|
+
* otherwise link a later `recordEntry` to a deleted prior entry, leaving a
|
|
1274
|
+
* dangling ref that breaks restore resolution. This is the primary
|
|
1275
|
+
* correctness guarantee.
|
|
1276
|
+
*
|
|
1277
|
+
* `dryRun` computes the report without touching disk or memory.
|
|
1278
|
+
*/
|
|
1279
|
+
async clearSession(sessionId, opts) {
|
|
1280
|
+
const dryRun = opts?.dryRun ?? false;
|
|
1281
|
+
const stats = await this.sessionStats(sessionId);
|
|
1282
|
+
if (!dryRun) {
|
|
1283
|
+
if (stats.anchorGroups > 0 || stats.journals > 0) {
|
|
1284
|
+
await rm(this.sessionDir(sessionId), { recursive: true, force: true });
|
|
1285
|
+
}
|
|
1286
|
+
this.seededSessions.delete(sessionId);
|
|
1287
|
+
for (const key of this.lastEntry.keys()) {
|
|
1288
|
+
if (key.startsWith(`${sessionId}\0`)) this.lastEntry.delete(key);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return { sessionId, ...stats, dryRun };
|
|
1292
|
+
}
|
|
1184
1293
|
};
|
|
1185
1294
|
function hashPath(path) {
|
|
1186
1295
|
return createHash("sha256").update(path).digest("hex").slice(0, 8);
|
|
@@ -1211,18 +1320,18 @@ async function reconcileTracked(store, sessionId, anchorSeq, tracked, probe = de
|
|
|
1211
1320
|
// src/snapshot-cleanup.ts
|
|
1212
1321
|
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1213
1322
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1214
|
-
import {
|
|
1323
|
+
import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
|
|
1215
1324
|
var CLEANUP_CONFIG_FILENAME = "snapshot-cleanup.json";
|
|
1216
1325
|
var CLEANUP_CONFIG_ENV = "DSH_SNAPSHOT_CLEANUP_CONFIG";
|
|
1217
1326
|
var DEFAULT_MAX_AGE_DAYS = 30;
|
|
1218
1327
|
var DEFAULT_CLEANUP_CONFIG = { enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS };
|
|
1219
1328
|
var AUTO_SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1220
|
-
function resolveCleanupConfigPath() {
|
|
1221
|
-
return process.env[CLEANUP_CONFIG_ENV] ?? join2(
|
|
1329
|
+
function resolveCleanupConfigPath(dshHome) {
|
|
1330
|
+
return process.env[CLEANUP_CONFIG_ENV] ?? join2(resolveDshHome2(dshHome), CLEANUP_CONFIG_FILENAME);
|
|
1222
1331
|
}
|
|
1223
1332
|
var STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
|
|
1224
|
-
function resolveCleanupStatePath() {
|
|
1225
|
-
return join2(dirname2(resolveCleanupConfigPath()), STATE_FILENAME);
|
|
1333
|
+
function resolveCleanupStatePath(dshHome) {
|
|
1334
|
+
return join2(dirname2(resolveCleanupConfigPath(dshHome)), STATE_FILENAME);
|
|
1226
1335
|
}
|
|
1227
1336
|
async function loadLastSweepAt(path) {
|
|
1228
1337
|
try {
|
|
@@ -1319,10 +1428,21 @@ function parseCleanupCommand(rawInput) {
|
|
|
1319
1428
|
if (!Number.isInteger(days) || days <= 0) return { error: '"max-age" must be a positive integer (days)' };
|
|
1320
1429
|
return { action: "max-age", value: days };
|
|
1321
1430
|
}
|
|
1431
|
+
case "run-apply":
|
|
1432
|
+
return { error: 'the "run-apply" abbreviation was removed; use "run --apply"' };
|
|
1322
1433
|
case "run": {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1434
|
+
let apply2 = false;
|
|
1435
|
+
let current = false;
|
|
1436
|
+
for (const rawFlag of parts.slice(1)) {
|
|
1437
|
+
if (rawFlag === "--apply") {
|
|
1438
|
+
apply2 = true;
|
|
1439
|
+
} else if (rawFlag === "--current") {
|
|
1440
|
+
current = true;
|
|
1441
|
+
} else {
|
|
1442
|
+
return { error: `unknown /snapshot-auto-cleanup run flag "${rawFlag}"` };
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return { action: "run", target: current ? "current" : "rules", apply: apply2 };
|
|
1326
1446
|
}
|
|
1327
1447
|
default:
|
|
1328
1448
|
return { error: `unknown /snapshot-auto-cleanup subcommand "${parts[0]}"` };
|
|
@@ -1640,13 +1760,13 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
1640
1760
|
return executeRewind(ctx, store, fs, invocation, target, mode, inflight);
|
|
1641
1761
|
}
|
|
1642
1762
|
var autoSweepChecked = false;
|
|
1643
|
-
async function maybeRunAutoCleanup(ctx, store, sessionId) {
|
|
1763
|
+
async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome) {
|
|
1644
1764
|
if (autoSweepChecked) return;
|
|
1645
1765
|
autoSweepChecked = true;
|
|
1646
1766
|
await runAutoCleanupCheck({
|
|
1647
1767
|
pruner: store,
|
|
1648
|
-
configPath: resolveCleanupConfigPath(),
|
|
1649
|
-
statePath: resolveCleanupStatePath(),
|
|
1768
|
+
configPath: resolveCleanupConfigPath(dshHome),
|
|
1769
|
+
statePath: resolveCleanupStatePath(dshHome),
|
|
1650
1770
|
log: (msg) => ctx.logger.warn(msg)
|
|
1651
1771
|
}, sessionId);
|
|
1652
1772
|
}
|
|
@@ -1661,10 +1781,10 @@ function formatCleanupReport(report) {
|
|
|
1661
1781
|
return report.skippedActive > 0 ? `${text}
|
|
1662
1782
|
${t("cleanup.skipped", { skipped: report.skippedActive })}` : text;
|
|
1663
1783
|
}
|
|
1664
|
-
async function handleSnapshotCleanup(store, invocation) {
|
|
1784
|
+
async function handleSnapshotCleanup(store, invocation, dshHome, trackedBySession) {
|
|
1665
1785
|
const parsed = parseCleanupCommand(invocation.rawInput);
|
|
1666
1786
|
if ("error" in parsed) return { kind: "error", text: t("cleanup.usage") };
|
|
1667
|
-
const configPath = resolveCleanupConfigPath();
|
|
1787
|
+
const configPath = resolveCleanupConfigPath(dshHome);
|
|
1668
1788
|
switch (parsed.action) {
|
|
1669
1789
|
case "status": {
|
|
1670
1790
|
const loaded = await loadCleanupConfig(configPath);
|
|
@@ -1700,17 +1820,20 @@ async function handleSnapshotCleanup(store, invocation) {
|
|
|
1700
1820
|
}
|
|
1701
1821
|
return { kind: "success", text: t("cleanup.maxAgeOk", { days: parsed.value }) };
|
|
1702
1822
|
}
|
|
1703
|
-
case "run":
|
|
1704
|
-
|
|
1823
|
+
case "run": {
|
|
1824
|
+
const apply2 = parsed.apply;
|
|
1825
|
+
if (parsed.target === "current") {
|
|
1826
|
+
return handleClearCurrent(store, invocation, apply2, trackedBySession);
|
|
1827
|
+
}
|
|
1705
1828
|
const loaded = await loadCleanupConfig(configPath);
|
|
1706
1829
|
if (!loaded.ok) return { kind: "error", text: t("cleanup.cfgInvalid", { detail: loaded.error }) };
|
|
1707
1830
|
try {
|
|
1708
1831
|
const report = await store.pruneStale({
|
|
1709
1832
|
keepActiveId: invocation.agent.session.id,
|
|
1710
1833
|
maxAgeDays: loaded.config.maxAgeDays,
|
|
1711
|
-
dryRun:
|
|
1834
|
+
dryRun: !apply2
|
|
1712
1835
|
});
|
|
1713
|
-
if (!report.dryRun) await saveLastSweepAt(resolveCleanupStatePath(), Date.now());
|
|
1836
|
+
if (!report.dryRun) await saveLastSweepAt(resolveCleanupStatePath(dshHome), Date.now());
|
|
1714
1837
|
return { kind: "success", text: formatCleanupReport(report) };
|
|
1715
1838
|
} catch (error) {
|
|
1716
1839
|
return { kind: "error", text: t("cleanup.runFailed", { detail: error instanceof Error ? error.message : String(error) }) };
|
|
@@ -1718,8 +1841,39 @@ async function handleSnapshotCleanup(store, invocation) {
|
|
|
1718
1841
|
}
|
|
1719
1842
|
}
|
|
1720
1843
|
}
|
|
1844
|
+
function formatClearReport(report) {
|
|
1845
|
+
const key = report.dryRun ? "cleanup.clearDry" : "cleanup.clearApply";
|
|
1846
|
+
return t(key, {
|
|
1847
|
+
entries: report.entries,
|
|
1848
|
+
bytes: report.bytes
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
async function handleClearCurrent(store, invocation, apply2, trackedBySession) {
|
|
1852
|
+
const { agent } = invocation;
|
|
1853
|
+
const sessionId = agent.session.id;
|
|
1854
|
+
if (apply2) {
|
|
1855
|
+
if (agent.status !== "idle") {
|
|
1856
|
+
agent.cancel({ kind: "user" }, { keepInbox: true });
|
|
1857
|
+
const stopped = await waitForAgentIdle(agent, invocation.signal);
|
|
1858
|
+
if (!stopped) {
|
|
1859
|
+
return { kind: "error", text: t("cleanup.clearActive", { sessionId }) };
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (invocation.signal.aborted) {
|
|
1863
|
+
return { kind: "error", text: t("cleanup.clearCancelled") };
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
try {
|
|
1867
|
+
const report = await store.clearSession(sessionId, { dryRun: !apply2 });
|
|
1868
|
+
if (!report.dryRun) trackedBySession.delete(sessionId);
|
|
1869
|
+
return { kind: "success", text: formatClearReport(report) };
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
return { kind: "error", text: t("cleanup.clearFailed", { detail: error instanceof Error ? error.message : String(error), sessionId }) };
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1721
1874
|
function apply(ctx, config) {
|
|
1722
|
-
const
|
|
1875
|
+
const dshHome = config?.dshHome;
|
|
1876
|
+
const store = new SnapshotStore(config?.snapshotDir, { dedup: config?.dedup, dshHome });
|
|
1723
1877
|
const pending = /* @__PURE__ */ new Map();
|
|
1724
1878
|
const anchorCache = /* @__PURE__ */ new WeakMap();
|
|
1725
1879
|
const inflight = /* @__PURE__ */ new Set();
|
|
@@ -1732,16 +1886,22 @@ function apply(ctx, config) {
|
|
|
1732
1886
|
}
|
|
1733
1887
|
});
|
|
1734
1888
|
ctx.effect(function* () {
|
|
1889
|
+
const rewindHandler = (invocation) => handleRewind(ctx, store, fsService, invocation, inflight);
|
|
1735
1890
|
yield ctx.commands.register({
|
|
1736
1891
|
name: "rewind",
|
|
1737
1892
|
description: t("command.description"),
|
|
1738
|
-
handler:
|
|
1893
|
+
handler: rewindHandler
|
|
1894
|
+
});
|
|
1895
|
+
yield ctx.commands.register({
|
|
1896
|
+
name: "undo",
|
|
1897
|
+
description: t("command.description"),
|
|
1898
|
+
handler: rewindHandler
|
|
1739
1899
|
});
|
|
1740
1900
|
yield ctx.commands.register({
|
|
1741
1901
|
name: "snapshot-auto-cleanup",
|
|
1742
1902
|
description: t("cleanup.description"),
|
|
1743
1903
|
input: { hint: t("cleanup.inputHint") },
|
|
1744
|
-
handler: (invocation) => handleSnapshotCleanup(store, invocation)
|
|
1904
|
+
handler: (invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession)
|
|
1745
1905
|
});
|
|
1746
1906
|
}, "dsh-rewind command");
|
|
1747
1907
|
ctx.on("session/event", (session, event) => {
|
|
@@ -1751,7 +1911,7 @@ function apply(ctx, config) {
|
|
|
1751
1911
|
void (async () => {
|
|
1752
1912
|
try {
|
|
1753
1913
|
const sessionId = session.id;
|
|
1754
|
-
void maybeRunAutoCleanup(ctx, store, sessionId);
|
|
1914
|
+
void maybeRunAutoCleanup(ctx, store, sessionId, dshHome);
|
|
1755
1915
|
let tracked = trackedBySession.get(sessionId);
|
|
1756
1916
|
if (tracked === void 0) {
|
|
1757
1917
|
tracked = await store.trackedPaths(sessionId);
|
|
@@ -1777,7 +1937,7 @@ function apply(ctx, config) {
|
|
|
1777
1937
|
});
|
|
1778
1938
|
scope.on("tools/post-execute", async (exec, result, next) => {
|
|
1779
1939
|
try {
|
|
1780
|
-
void maybeRunAutoCleanup(ctx, store, exec.agent?.session?.id);
|
|
1940
|
+
void maybeRunAutoCleanup(ctx, store, exec.agent?.session?.id, dshHome);
|
|
1781
1941
|
await commitEntry(store, pending, anchorCache, trackedBySession, exec, result);
|
|
1782
1942
|
} catch (error) {
|
|
1783
1943
|
ctx.logger.warn(`[dsh-rewind] checkpoint commit failed for ${exec.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-rewind client half: the `/rewind` command decoration, the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* dsh-rewind client half: the `/rewind` command decoration, the locale
|
|
3
|
+
* registration, and the session-scoped portal bridge that renders the
|
|
4
|
+
* per-message ↶ rewind button (see
|
|
5
5
|
* `portals.tsx` for the button itself).
|
|
6
6
|
*
|
|
7
7
|
* The button is NOT injected by hand into the DOM anymore: the plugin
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
* drift.
|
|
15
15
|
*
|
|
16
16
|
* The text-driven flow is the harness's STANDARD command decoration
|
|
17
|
-
* (`ctx.commandUi.decorate`): a bare `/rewind`
|
|
18
|
-
* completion, or typed in full and Entered —
|
|
19
|
-
* popupSelect shell (search, ↑↓/Enter, Esc) listing
|
|
20
|
-
* instead of executing the command. Picking one
|
|
21
|
-
* the ↶ button: the mode popover, both-impact
|
|
22
|
-
* hiding and the composer refill
|
|
23
|
-
* forms (`/rewind @<seq> chat|both`,
|
|
24
|
-
* channels the ↶ button and the popover
|
|
25
|
-
*
|
|
17
|
+
* (`ctx.commandUi.decorate`): a bare `/rewind` (or its alias `/undo`) —
|
|
18
|
+
* picked from the slash-menu completion, or typed in full and Entered —
|
|
19
|
+
* opens the harness's own popupSelect shell (search, ↑↓/Enter, Esc) listing
|
|
20
|
+
* the rewind candidates instead of executing the command. Picking one
|
|
21
|
+
* continues the SAME flow as the ↶ button: the mode popover, both-impact
|
|
22
|
+
* confirmation, execution, row hiding and the composer refill
|
|
23
|
+
* (`runRewindAndFill`). The parameterized forms (`/rewind @<seq> chat|both`,
|
|
24
|
+
* `/rewind preview …`) stay internal channels the ↶ button and the popover
|
|
25
|
+
* drive through `session.command`.
|
|
26
26
|
*
|
|
27
27
|
* @module dsh-rewind/client
|
|
28
28
|
*/
|
|
@@ -25,7 +25,6 @@ export declare const zh: {
|
|
|
25
25
|
'popover.impact.delete': string;
|
|
26
26
|
'popover.confirm': string;
|
|
27
27
|
'popover.back': string;
|
|
28
|
-
'guard.hint': string;
|
|
29
28
|
};
|
|
30
29
|
/** The rewind namespace key union. */
|
|
31
30
|
export type RewindKey = keyof typeof zh;
|
|
@@ -61,5 +60,4 @@ export declare const en: {
|
|
|
61
60
|
'popover.impact.delete': string;
|
|
62
61
|
'popover.confirm': string;
|
|
63
62
|
'popover.back': string;
|
|
64
|
-
'guard.hint': string;
|
|
65
63
|
};
|