sage-mem 0.9.1 → 0.9.2
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/lib/autodream/rollback.js +62 -1
- package/lib/autodream/snapshot.js +18 -1
- package/lib/autodream-audit.js +28 -1
- package/package.json +8 -8
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import { REPORT_DIR } from './config.js'
|
|
10
10
|
import { readJson, safeName, snapshotEntries, stampCompact, stampHuman } from './util.js'
|
|
11
|
+
import { restoreMemory } from '../memory/archive.js'
|
|
11
12
|
import { randomUUID } from 'node:crypto'
|
|
12
|
-
import { copyFile, mkdir, readdir, rename, stat, writeFile } from 'node:fs/promises'
|
|
13
|
+
import { copyFile, mkdir, readdir, rename, rm, stat, writeFile } from 'node:fs/promises'
|
|
13
14
|
import { join } from 'node:path'
|
|
14
15
|
|
|
15
16
|
/** 读回滚留痕(面板也许要显示「最近谁退回过」)。 */
|
|
@@ -86,6 +87,34 @@ export async function rollback(opts = {}) {
|
|
|
86
87
|
const protectionId = `pre-rollback-${stamp}-${randomUUID().replace(/-/g, '').slice(0, 4)}`
|
|
87
88
|
const protection = await this.snapshot(protectionId, 'pre-rollback')
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* ⚠️ 保护快照不完整就**中止**,一个字都不许写。
|
|
92
|
+
*
|
|
93
|
+
* 它是「滚错了还能再退回来」这句承诺的唯一支撑。残缺时继续往下滚,等于拿一张假保险
|
|
94
|
+
* 去覆盖当前版本 —— 而当前版本恰恰是此刻最想保住的东西。残缺的回滚点比没有回滚点
|
|
95
|
+
* **更危险**,这与 `engine.js` 开工前那条(`snapshot.failed.length` 就中止)是同一条原则,
|
|
96
|
+
* 只是回滚这条路上一直漏了。
|
|
97
|
+
*
|
|
98
|
+
* 顺手把它删掉:它落进 `snapshots/` 就会被 `listSnapshots()` 当成一份可选的回滚点,
|
|
99
|
+
* 而按它恢复只会把「没存下来的那些文件」误判成「快照里没有」→ 停进 `archive/`。
|
|
100
|
+
*/
|
|
101
|
+
if (protection.failed.length || protection.count !== protection.expected) {
|
|
102
|
+
await rm(protection.dir, { recursive: true, force: true }).catch(() => {})
|
|
103
|
+
const why = protection.failed.length
|
|
104
|
+
? `有 ${protection.failed.length} 个文件没存下来(${protection.failed.slice(0, 3).join('、')}${
|
|
105
|
+
protection.failed.length > 3 ? '…' : ''
|
|
106
|
+
})`
|
|
107
|
+
: `只存下 ${protection.count}/${protection.expected} 个文件`
|
|
108
|
+
this.log('warn', `回滚中止:保护快照不完整,${why}`)
|
|
109
|
+
this.phase = ''
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error:
|
|
113
|
+
`回滚前保护快照不完整(${why})。为免滚错之后退不回来,这次回滚已中止,` +
|
|
114
|
+
'记忆目录一个字没动。请检查文件占用与磁盘空间后重试。',
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
// ② 恢复
|
|
90
119
|
this.phase = '恢复文件'
|
|
91
120
|
const snapNames = await snapshotEntries(snapDir)
|
|
@@ -155,6 +184,38 @@ export async function rollback(opts = {}) {
|
|
|
155
184
|
}
|
|
156
185
|
}
|
|
157
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 策略自动归档也要退回来 —— 它按设计**不写进 `changes`**。
|
|
190
|
+
*
|
|
191
|
+
* `engine.js` 把两本账分开了:模型改的进 `changes`,确定性归档策略移走的进
|
|
192
|
+
* `archive.archived`(已落盘在运行记录 `manifest.json` 里)。只遍历 `changes`
|
|
193
|
+
* 就会整段漏掉,而这趟运行照样被标成「已回滚」、面板照报「恢复 0 个文件…已回滚」——
|
|
194
|
+
* 用户以为退回来了,文件其实还躺在 `archive/` 里。
|
|
195
|
+
*
|
|
196
|
+
* 恢复走 `memory/archive.js` 的共享实现(与手动恢复、模型工具同一份):它会顺手
|
|
197
|
+
* 去掉 `archived_at` / `archived_reason` 两行留痕,否则「恢复」回来的是一份带着
|
|
198
|
+
* 「我已被归档」标记的活记忆。
|
|
199
|
+
*
|
|
200
|
+
* ⚠️ 只恢复**运行前快照里存在**的文件:策略候选是运行末尾扫出来的,理论上可能包含
|
|
201
|
+
* 这趟新造的文件,把它搬回来等于凭空激活一条运行前不存在的记忆(Q06 的形态)。
|
|
202
|
+
*/
|
|
203
|
+
for (const a of runManifest.archive?.archived ?? []) {
|
|
204
|
+
if (!a?.ok || !a.file) continue
|
|
205
|
+
const name = a.file
|
|
206
|
+
if (restored.includes(name)) continue // 上面 `changes` 那段已经把它放回来了
|
|
207
|
+
if (!snapSet.has(name)) {
|
|
208
|
+
skipped.push(`${name}(运行前快照里没有,说明是这趟新造的,回滚不激活它)`)
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
if (await stat(join(this.memoryDir, name)).catch(() => null)) {
|
|
212
|
+
skipped.push(`${name}(顶层已有同名文件,不覆盖)`)
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
const res = await restoreMemory(this.memoryDir, name)
|
|
216
|
+
if (res.ok) restored.push(name)
|
|
217
|
+
else skipped.push(`${name}(恢复失败:${res.error})`)
|
|
218
|
+
}
|
|
158
219
|
}
|
|
159
220
|
|
|
160
221
|
// ③ 留痕
|
|
@@ -36,7 +36,24 @@ export async function snapshot(id, kind = 'run') {
|
|
|
36
36
|
if (err?.code === 'EEXIST') throw new Error(`回滚点 id 撞车:${id} 已存在,拒绝覆盖已有的回滚点`)
|
|
37
37
|
throw err
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* ⚠️ 枚举失败**不能**当成「目录是空的」。
|
|
41
|
+
*
|
|
42
|
+
* 旧写法是 `.catch(() => [])`:读不动时静默返回空列表,于是生成一份 `count=0 / expected=0 /
|
|
43
|
+
* failed=[]` 的快照 —— 它在任何一个下游检查里都长得像「一次成功的快照」,而实际上它
|
|
44
|
+
* 什么都没存。回滚据此认为「退得回去」,`scope:'all'` 时更会把顶层文件全部当成
|
|
45
|
+
* 「快照里没有」而停进 `archive/`。
|
|
46
|
+
*
|
|
47
|
+
* `ENOENT` 是唯一合法的「这里没有文件」(记忆目录还没建)。其余一律出声:宁可让调用方
|
|
48
|
+
* 在写任何东西之前中止,也不要交出一份假的回滚点。
|
|
49
|
+
*/
|
|
50
|
+
let entries
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(this.memoryDir, { withFileTypes: true })
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err?.code === 'ENOENT') entries = []
|
|
55
|
+
else throw new Error(`快照失败:读不了记忆目录(${err?.message ?? err}),拒绝生成一份不完整的回滚点`)
|
|
56
|
+
}
|
|
40
57
|
const files = entries.filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.md'))
|
|
41
58
|
const recorded = []
|
|
42
59
|
/** 没存下来的文件。**不是「警告一下就继续」**:调用方要拿它决定是否中止。 */
|
package/lib/autodream-audit.js
CHANGED
|
@@ -129,6 +129,33 @@ async function readDirSnapshot(dir) {
|
|
|
129
129
|
return { files, all, skipped: all.length - files.length, indexText, archiveKeys, archiveCount }
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* 把**代码**从正文里拿掉,只留散文 —— 用来避免把代码里的 `[[…]]` 当成链接。
|
|
134
|
+
*
|
|
135
|
+
* 两类都要剥:**围栏代码块**(跨行,```` ``` ```` 起止)与**行内代码**
|
|
136
|
+
* (`` `x` ``、`` `` x `` `` —— 后者的内容里可以再出现反引号,所以闭合必须找
|
|
137
|
+
* **等长**的那一串,不能用「遇到下一枚反引号就算完」)。
|
|
138
|
+
*
|
|
139
|
+
* ⚠️ **按行剥行内代码**:行内代码本来就极少跨行,按行做能把「反引号不配对」时的
|
|
140
|
+
* 损失限制在一行内,不至于把后面真正的链接整段吞掉。2026-10-09 实测踩过一次:
|
|
141
|
+
* 只认单反引号时,`` `` `[[双链]]` `` `` 这种写法会被漏掉、照样报假断链。
|
|
142
|
+
* @param {string} raw — 文件全文
|
|
143
|
+
*/
|
|
144
|
+
function stripCode(raw) {
|
|
145
|
+
const out = []
|
|
146
|
+
let fence = null
|
|
147
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
148
|
+
const m = line.match(/^\s*(`{3,})/)
|
|
149
|
+
if (fence !== null) {
|
|
150
|
+
if (m && m[1].length >= fence.length) fence = null // 围栏闭合
|
|
151
|
+
continue // 围栏内的行整行丢弃(含起止那两行)
|
|
152
|
+
}
|
|
153
|
+
if (m) { fence = m[1]; continue }
|
|
154
|
+
out.push(line.replace(/(`+)[^\n]*?\1/g, ''))
|
|
155
|
+
}
|
|
156
|
+
return out.join('\n')
|
|
157
|
+
}
|
|
158
|
+
|
|
132
159
|
/**
|
|
133
160
|
* 七查主入口。
|
|
134
161
|
* @param {string} dir — 记忆根目录
|
|
@@ -220,7 +247,7 @@ export async function auditMemoryDir(dir) {
|
|
|
220
247
|
* `` `[[双链]]` `` 写在行内代码或围栏代码块里,是在**讲链接语法本身**,不是一条链接。
|
|
221
248
|
* 直接在原文上 `matchAll` 会把它报成断链(2026-10-09 实测的假阳性)。
|
|
222
249
|
*/
|
|
223
|
-
const prose = raw
|
|
250
|
+
const prose = stripCode(raw)
|
|
224
251
|
for (const m of prose.matchAll(/\[\[([^\]]+)\]\]/g)) {
|
|
225
252
|
const target = m[1].trim()
|
|
226
253
|
if (linkKeys.has(target)) continue
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sage-mem",
|
|
3
3
|
"description": "File-based memory plugin for DeepSeek Harness (DSH) — cross-session memory in plain markdown files, with a memory star-map view, type/tag filtering, retrieval aliases, and an optional autodream pass that consolidates memories on demand or on a schedule — with per-run rollback points, a per-file change declaration (what changed and why), and a separately selectable model for the consolidation pass",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"cordis.patch.yml",
|
|
16
16
|
"NOTICE"
|
|
17
17
|
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node test/run.mjs",
|
|
20
|
+
"build:client": "node scripts/build-client.mjs",
|
|
21
|
+
"images": "node scripts/make-readme-images.mjs"
|
|
22
|
+
},
|
|
18
23
|
"license": "Apache-2.0",
|
|
19
24
|
"engines": {
|
|
20
25
|
"node": ">=18",
|
|
@@ -85,10 +90,5 @@
|
|
|
85
90
|
"ai-agent",
|
|
86
91
|
"agent",
|
|
87
92
|
"claude-code"
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
"test": "node test/run.mjs",
|
|
91
|
-
"build:client": "node scripts/build-client.mjs",
|
|
92
|
-
"images": "node scripts/make-readme-images.mjs"
|
|
93
|
-
}
|
|
94
|
-
}
|
|
93
|
+
]
|
|
94
|
+
}
|