sage-mem 2.1.3 → 2.1.4

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 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.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
15
+ **Current release: `2.1.4`** — 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.3`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
17
+ **当前发布版本:`2.1.4`** —— 顶部 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 是最直接的反馈。
@@ -36,7 +36,7 @@ import { writeFileAtomic, warnLog } from '../memory/util.js'
36
36
  import { buildConflict, listConflicts, readConflict, sameConflict, writeConflict } from '../memory/conflicts.js'
37
37
  import { memoryPathOf } from '../memory/paths.js'
38
38
  import { decideWriteIntent } from '../memory/write-gate.js'
39
- import { inspectMemoryEntry, reservedIdentitySet } from '../memory/scan.js'
39
+ import { inspectMemoryEntry, readVerified, reservedIdentitySet } from '../memory/scan.js'
40
40
 
41
41
  /** 单次 read_memory 返回的正文上限(字符)。超过就截断并明确告诉模型。 */
42
42
  const READ_LIMIT = 20000
@@ -121,7 +121,8 @@ async function scanEntries(memoryDir, ctx) {
121
121
  continue
122
122
  }
123
123
  try {
124
- const content = await readFile(full, 'utf8')
124
+ // 同上:清单也要从闸门检查过的那个句柄读(N03 —— 检查后换文件不能把外部标题带进来)。
125
+ const content = await readVerified(full, gate, 'utf8')
125
126
  const meta = parseFrontmatterLite(content)
126
127
  const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---/, '')
127
128
  const h1 = body.match(/^#\s+(.+)$/m)
@@ -538,9 +539,16 @@ export function createToolRunner(opts) {
538
539
  const before = await hostStat()
539
540
  let content
540
541
  try {
541
- content = await readFile(full, 'utf8')
542
+ /**
543
+ * 内容走**公共句柄读取入口**(外部审查 2026-10-31,N03):`inspectMemoryEntry` 是按路径
544
+ * `lstat`,`readFile(路径)` 是另一次按路径打开 —— 两次之间把普通文件换成符号链接 / 另一份
545
+ * 文件,读的就是没检查过的东西(审查用真符号链接复现出目录外内容)。`readVerified`
546
+ * 改成 open 句柄 + fstat 比对 `dev:ino` / 大小,只从那个句柄读;体积上限也在全文读入
547
+ * **之前**生效,不会先整份读进内存再判。
548
+ */
549
+ content = await readVerified(full, gate, 'utf8')
542
550
  } catch (err) {
543
- // 扫到读之间被删是正常状态;其余读失败照旧抛给上层(外层会转成一次工具失败)。
551
+ // 扫到读之间被删是正常状态;其余读失败(含「检查后被换掉」)照旧抛给上层。
544
552
  if (err?.code === 'ENOENT') return { ok: false, gate: { ok: false, code: 'missing' } }
545
553
  throw err
546
554
  }
@@ -982,10 +990,11 @@ export function createToolRunner(opts) {
982
990
  return '错误:report_contradiction 必须给 note —— 一句话说明这两条到底哪里互斥。'
983
991
  }
984
992
  // 两份正文都存进记录:面板上人要对照着判,只留一边等于没得判。
993
+ // 同样走句柄读取入口(N03):记录里的正文会被人和模型看到,不能来自目录外。
985
994
  const readBody = async (full) => {
986
995
  const gate = await inspectMemoryEntry(full)
987
996
  if (!gate.ok) return null
988
- return await readFile(full, 'utf8').catch(() => null)
997
+ return await readVerified(full, gate, 'utf8').catch(() => null)
989
998
  }
990
999
  const onDisk = await readBody(oldFull)
991
1000
  const incoming = await readBody(newFull)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sage-mem",
3
3
  "description": "Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.",
4
- "version": "2.1.3",
4
+ "version": "2.1.4",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "icon": "./icon.png",