sage-mem 2.1.14 → 2.1.15
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/memory/checkpoint.js +50 -32
- 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.15`** — 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.15`** —— 顶部 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/memory/checkpoint.js
CHANGED
|
@@ -497,26 +497,22 @@ export async function writeCheckpoint(taskId, text, opts) {
|
|
|
497
497
|
/** 注入用的短句柄硬上限(字节)。它要跟正文抢上下文,不能自己长起来。 */
|
|
498
498
|
const HANDLE_MAX_BYTES = 160
|
|
499
499
|
|
|
500
|
-
/**
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const b = Buffer.byteLength(ch, 'utf8')
|
|
507
|
-
if (used + b > maxBytes) break
|
|
508
|
-
out += ch
|
|
509
|
-
used += b
|
|
510
|
-
}
|
|
511
|
-
return out
|
|
512
|
-
}
|
|
500
|
+
/**
|
|
501
|
+
* 句柄里相对路径的**基准标记**:相对路径只说「相对谁」,不说清基准就是歧义的
|
|
502
|
+
* (去记忆目录里找?去状态根里找?)。生成与解析**共用这一个常量** ——
|
|
503
|
+
* 两边各写一份字符串,改了一边就会静默给出读不到的路径(Q12 的旧句柄正是这么坏的)。
|
|
504
|
+
*/
|
|
505
|
+
const HANDLE_BASE_MARK = '状态根下'
|
|
513
506
|
|
|
514
507
|
/**
|
|
515
|
-
* 注入用的短句柄(一行,≤ 160 字节):说清「有这个检查点、revision
|
|
508
|
+
* 注入用的短句柄(一行,≤ 160 字节):说清「有这个检查点、revision 几、去哪读」。
|
|
516
509
|
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
510
|
+
* 预算是硬的,按优先级退化:**可解析路径与 revision 不可丢**,项目名是可选线索
|
|
511
|
+
* (放不下就整个丢掉),「续做前先读」是纯提示(再放不下就丢它)。taskId 非法返回 ''。
|
|
512
|
+
*
|
|
513
|
+
* ⚠️ **id 只出现一次**:完整相对路径 `tasks/<id>/WORKING.md` 已经含它了。
|
|
514
|
+
* 旧版在头部又重复了一遍 id,预算被耗光后退化成一条**不存在的路径**
|
|
515
|
+
* `tasks/WORKING.md`(2026-10-31 外部审查 Q12)—— 短了,但不再是准确的读取指令。
|
|
520
516
|
*
|
|
521
517
|
* revision 缺省按 0(与 renderCheckpoint 的缺省一致)。
|
|
522
518
|
*/
|
|
@@ -526,21 +522,43 @@ export function checkpointHandleText({ taskId, revision, project } = {}) {
|
|
|
526
522
|
const rev = Number.isInteger(revision) && revision >= 0 ? revision : 0
|
|
527
523
|
const proj = String(project ?? '').replace(/\s+/g, ' ').trim()
|
|
528
524
|
|
|
529
|
-
|
|
530
|
-
const
|
|
525
|
+
// 身份只表达一次:id 在路径里,revision 在头部。
|
|
526
|
+
const head = `检查点 revision ${rev}:`
|
|
527
|
+
const rel = `${TASKS_DIR_NAME}/${id}/${CHECKPOINT_FILE_NAME}`
|
|
531
528
|
const tail = ',续做前先读'
|
|
532
|
-
const build = (p) =>
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
if (Buffer.byteLength(line, 'utf8') > HANDLE_MAX_BYTES) {
|
|
542
|
-
// 到这儿只剩「id 太长」一种可能:完整相对路径(id 出现两次)放不下,退成不重复的写法。
|
|
543
|
-
line = `${head}读 ${TASKS_DIR_NAME}/${CHECKPOINT_FILE_NAME}`
|
|
544
|
-
}
|
|
529
|
+
const build = (p, withTail) =>
|
|
530
|
+
`${head}${HANDLE_BASE_MARK} ${rel}${withTail ? tail : ''}${p === '' ? '' : `(项目 ${p})`}`
|
|
531
|
+
|
|
532
|
+
let line = build(proj, true)
|
|
533
|
+
if (Buffer.byteLength(line, 'utf8') > HANDLE_MAX_BYTES) line = build('', true)
|
|
534
|
+
if (Buffer.byteLength(line, 'utf8') > HANDLE_MAX_BYTES) line = build('', false)
|
|
535
|
+
// 到这儿只剩「revision 是天文数字」一种可能,合法整数不会触发。宁可给一条超长句柄,
|
|
536
|
+
// 也不截断路径:截出来的路径定位不到真实文件,比多占几个字节更糟。
|
|
545
537
|
return line
|
|
546
538
|
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 从短句柄里解析回检查点身份(生成规则见 `checkpointHandleText`)。
|
|
542
|
+
*
|
|
543
|
+
* 为什么需要它:句柄是**给模型执行的读取指令**,必须能被机器核对「它说的是不是真实文件」。
|
|
544
|
+
* 旧版退化成 `读 tasks/WORKING.md`,字节数达标、也含 id —— 只断言「≤160 字节 + 包含 id」
|
|
545
|
+
* 的测试正是这么放它过去的。断言要看的是**用同一解析规则能不能定位到真实文件**。
|
|
546
|
+
*
|
|
547
|
+
* 解析要求基准标记 + `tasks/<id>/WORKING.md`:没有基准的裸相对路径是歧义的,按失败处理。
|
|
548
|
+
*
|
|
549
|
+
* @param {string} text
|
|
550
|
+
* @returns {{taskId: string, revision: number} | null}
|
|
551
|
+
*/
|
|
552
|
+
export function parseCheckpointHandle(text) {
|
|
553
|
+
if (typeof text !== 'string') return null
|
|
554
|
+
const file = CHECKPOINT_FILE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
555
|
+
const re = new RegExp(
|
|
556
|
+
`检查点\\s+revision\\s+(\\d+)[::]\\s*${HANDLE_BASE_MARK}\\s+${TASKS_DIR_NAME}/([A-Za-z0-9._-]{1,64})/${file}`,
|
|
557
|
+
)
|
|
558
|
+
const m = text.match(re)
|
|
559
|
+
if (!m) return null
|
|
560
|
+
const id = normalizeTaskId(m[2])
|
|
561
|
+
if (id === null) return null
|
|
562
|
+
const revision = Number(m[1])
|
|
563
|
+
return Number.isSafeInteger(revision) ? { taskId: id, revision } : null
|
|
564
|
+
}
|
package/package.json
CHANGED