sage-mem 2.1.2 → 2.1.3
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.md +1 -1
- package/README.zh.md +1 -1
- package/lib/autodream/tools.js +66 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
**Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.**
|
|
14
14
|
|
|
15
|
-
**Current release: `2.1.
|
|
15
|
+
**Current release: `2.1.3`** — the npm badge above is the dynamic source of truth. **Contract baseline: `1.0.0`** — the first stable release, whose public commitments, the capability boundaries and the list of things it explicitly cannot do are frozen in the
|
|
16
16
|
[frozen contract](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md).
|
|
17
17
|
|
|
18
18
|
If it is useful to you, a Star is the most direct feedback.
|
package/README.zh.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
> File-based cross-session memory for DeepSeek Harness — every memory is a plain markdown file you can open, edit and delete. No database, no worker, no port.
|
|
16
16
|
|
|
17
|
-
**当前发布版本:`2.1.
|
|
17
|
+
**当前发布版本:`2.1.3`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
|
|
18
18
|
[契约冻结](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md)。
|
|
19
19
|
|
|
20
20
|
如果它对你有用,给个 Star 是最直接的反馈。
|
package/lib/autodream/tools.js
CHANGED
|
@@ -489,24 +489,65 @@ export function createToolRunner(opts) {
|
|
|
489
489
|
const readVersions = new Map()
|
|
490
490
|
|
|
491
491
|
/**
|
|
492
|
-
*
|
|
493
|
-
* 也不要拿一个来路不明的版本假装有守卫)。
|
|
492
|
+
* 把「这份内容对应磁盘上的哪个版本」记下来(写回时当守卫)。
|
|
494
493
|
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
494
|
+
* 取不到可信版本(`version` 为 null)就**什么都不记** —— 宁可让写入走「拒绝」,
|
|
495
|
+
* 也不要拿一个来路不明的版本假装有守卫。
|
|
496
|
+
*
|
|
497
|
+
* ⚠️ 这里只登记、**自己不 stat**。版本由 `readEntryWithVersion` 在**实际读取的前后**
|
|
498
|
+
* 各取一次得到;以前是在 `readFile` **之后**才 stat 两次,读 A、盘上已是 B 时两次 stat
|
|
499
|
+
* 都是 B,于是 A 的正文被绑到 B 的版本上(外部审查 2026-10-31,Q01)。
|
|
497
500
|
*/
|
|
498
|
-
|
|
501
|
+
function rememberReadVersion(fullPath, version, truncated) {
|
|
502
|
+
if (!version) return
|
|
503
|
+
const where = memoryPathOf(fullPath, memoryDir)
|
|
504
|
+
if (where.ok) readVersions.set(where.canon, { version, truncated: truncated === true })
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* 「读内容」与「取版本」共用的**唯一入口**(外部审查 2026-10-31,Q01)。
|
|
509
|
+
*
|
|
510
|
+
* 为什么必须是入口、不是两段调用:写回守卫要回答的是「**这份正文**对应哪一个版本」。
|
|
511
|
+
* 分开写时,调用方手里是 `readFile` 的返回值和两次 `stat` 的结果,把三者拼起来那一步最容易错
|
|
512
|
+
* —— 真实代码曾是 `readFile → stat → stat`:读的是 A,两次 stat 都读到 B,
|
|
513
|
+
* 于是 A 的正文被绑到 B 的版本,宿主的 CAS 即使完全正确也救不了这个错配。
|
|
514
|
+
*
|
|
515
|
+
* 顺序固定 **stat → read → stat**,两次版本**相同**才认这份内容就是那个版本的快照;
|
|
516
|
+
* 任何一次取不到(没有宿主 fs 服务 / stat 抛错 / 版本字段缺失)都按「版本未知」处理,
|
|
517
|
+
* 不拿当前版本补票 —— 补票等于替调用方担保一份它没读过的内容。
|
|
518
|
+
*
|
|
519
|
+
* 宿主 `fs` 契约里目前**没有**「原子 read-with-version」这一原语(`readText` 只回正文),
|
|
520
|
+
* 所以退到两次 stat 夹住实际读取。将来宿主提供这个原语,应当优先在 open 句柄上一次性拿到。
|
|
521
|
+
*
|
|
522
|
+
* @returns {Promise<{ok:false, gate:object}|{ok:true, gate:object, content:string, version:string|null}>}
|
|
523
|
+
* `gate` 是 `inspectMemoryEntry` 的结果(2.1.4 起内容读还用它做身份核对)
|
|
524
|
+
*/
|
|
525
|
+
async function readEntryWithVersion(full) {
|
|
526
|
+
const gate = await inspectMemoryEntry(full)
|
|
527
|
+
if (!gate.ok) return { ok: false, gate }
|
|
528
|
+
const fs = hostFs()
|
|
529
|
+
const hostStat = async () => {
|
|
530
|
+
if (!fs || typeof fs.stat !== 'function') return null
|
|
531
|
+
try {
|
|
532
|
+
const info = await fs.stat(await fs.resolve(full))
|
|
533
|
+
return info?.version ?? null
|
|
534
|
+
} catch {
|
|
535
|
+
return null // 取不到就是未知:不要拿「现在」的版本顶替
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const before = await hostStat()
|
|
539
|
+
let content
|
|
499
540
|
try {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
541
|
+
content = await readFile(full, 'utf8')
|
|
542
|
+
} catch (err) {
|
|
543
|
+
// 扫到读之间被删是正常状态;其余读失败照旧抛给上层(外层会转成一次工具失败)。
|
|
544
|
+
if (err?.code === 'ENOENT') return { ok: false, gate: { ok: false, code: 'missing' } }
|
|
545
|
+
throw err
|
|
546
|
+
}
|
|
547
|
+
const after = await hostStat()
|
|
548
|
+
// 两次版本相同才认「内容 = 版本」;只要中间可能被动过就当作未知。
|
|
549
|
+
const version = before && after && before === after ? before : null
|
|
550
|
+
return { ok: true, gate, content, version }
|
|
510
551
|
}
|
|
511
552
|
|
|
512
553
|
/**
|
|
@@ -800,22 +841,21 @@ export function createToolRunner(opts) {
|
|
|
800
841
|
* 过闸门再读:**名字在记忆目录里不等于内容住在记忆目录里**。
|
|
801
842
|
* 注入与面板两条腿都挡住了,这条腿(模型工具)也必须挡 ——
|
|
802
843
|
* 否则外部文件的内容照样会被读进来、送进模型上下文。
|
|
844
|
+
*
|
|
845
|
+
* 内容与版本一起取(`readEntryWithVersion`):守卫要的是「这份正文对应哪个版本」,
|
|
846
|
+
* 分开取会把 A 的正文绑到 B 的版本上(Q01)。
|
|
803
847
|
*/
|
|
804
|
-
const
|
|
805
|
-
if (!
|
|
806
|
-
return gate.code === 'missing'
|
|
848
|
+
const seen = await readEntryWithVersion(full)
|
|
849
|
+
if (!seen.ok) {
|
|
850
|
+
return seen.gate.code === 'missing'
|
|
807
851
|
? `错误:没有这个文件:${basename(full)}`
|
|
808
|
-
: `错误:拒绝读取 ${basename(full)}:${gate.error}`
|
|
852
|
+
: `错误:拒绝读取 ${basename(full)}:${seen.gate.error}`
|
|
809
853
|
}
|
|
810
|
-
const content =
|
|
811
|
-
|
|
812
|
-
throw err
|
|
813
|
-
})
|
|
814
|
-
if (content === null) return `错误:没有这个文件:${basename(full)}`
|
|
815
|
-
// 记下「这份内容对应磁盘上的哪个版本」——写回时用它当守卫(外部复审 R01)。
|
|
854
|
+
const content = seen.content
|
|
855
|
+
// 记下「这份内容对应磁盘上的哪个版本」——写回时用它当守卫(外部复审 R01 / Q01)。
|
|
816
856
|
// 截断的读也要标出来:截断内容不足以作为整份覆盖的依据。
|
|
817
857
|
const truncated = content.length > READ_LIMIT
|
|
818
|
-
|
|
858
|
+
rememberReadVersion(full, seen.version, truncated)
|
|
819
859
|
if (truncated) {
|
|
820
860
|
return `${content.slice(0, READ_LIMIT)}\n\n…(已截断,全文 ${content.length} 字符。注意:截断的读不能作为整份覆盖的依据)`
|
|
821
861
|
}
|
package/package.json
CHANGED