sage-mem 0.7.0 → 0.7.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/README.md +4 -2
- package/lib/autodream-audit.js +2 -2
- package/lib/autodream.js +1 -1
- package/lib/client.js +42 -42
- package/lib/index.js +132 -12
- package/lib/typert.host.js +17 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,8 @@ SAGE_MEM_DIR=/path/to/your/memory
|
|
|
123
123
|
- **换个说法也能命中**:打分是**词面重合**(双字组覆盖率),不是语义的——问「我有什么待办」而文件里写的是「代办清单」,差一个字就完全不命中。给这类记忆加一行 `aliases: [待办, todo]` 就能补上
|
|
124
124
|
- **正文超 1500 字符就截断**并标 `…(截断)`,单条记忆不会吃掉整个上下文
|
|
125
125
|
- **同一个问题不会重复注入**(按会话记签名),问第二遍不会又追加一遍
|
|
126
|
+
- **单场会话累计注入到 60 KB 就停**(`MAX_SESSION_BYTES`)。条数上限管的是「一次注多少」,这条管「一场会话总共注多少」—— 长会话里问题一直在换、签名一直在变,累积本来没有上限。到顶之后新的记忆不再注入(这时该让 agent 直接读文件),**压缩或清空之后账面自动归零**,又能注进来
|
|
127
|
+
- **每条记忆的标题带新鲜度**:`(保存于 N 天前)`,**优先读 frontmatter 的 `updated:`,缺失才回落文件 mtime** —— 手写的时间戳是权威,文件系统时间会被复制 / 备份还原 / checkout 整批改掉。理由是陈旧记忆被当事实引用时,「引用」这个动作本身会让它显得更权威而不是更可疑,所以注入文本开头还带一节**引用纪律**(提到路径先确认存在、提到函数先 grep、要动手先核实)。它**独立成节**而不是一条 bullet:Claude Code 实测同一句话埋成 bullet 命中 0/3、独立成节 3/3
|
|
126
128
|
- **`baseline: true` 的记忆每场会话第一步无条件注入**,最多 8 条——给「你是谁」「待办清单」这类每次开场都该在场的记忆用。多条 baseline 之间的先后由 `baselinePriority` 决定(数字越大越前,缺省 0),**不按文件名排**
|
|
127
129
|
- 记忆目录读不到、权限错、frontmatter 格式坏,这些都**降级为不注入并写日志**,不会静默失效
|
|
128
130
|
|
|
@@ -176,7 +178,7 @@ SAGE_MEM_DIR=/path/to/your/memory
|
|
|
176
178
|
|
|
177
179
|
报告落在 `memory/autodream/<runId>.md`——子目录,不会被扫成记忆条目。
|
|
178
180
|
|
|
179
|
-
> **关于自动触发**:整理沿用插件自己的定时器(`ctx.timer`,默认 30
|
|
181
|
+
> **关于自动触发**:整理沿用插件自己的定时器(`ctx.timer`,默认 30 分钟查一次门控),**没有**接进设置页那个「自动化任务」面板。原因是两者的触发模型对不上——那个面板的语义是「到点唤醒一个会话、让它跑一轮对话」,而自动做梦刻意**不走会话**(工具集由插件给、步数有上限、不污染会话记录),两者的触发模型对不上。硬接的代价远大于收益,所以不做。
|
|
180
182
|
|
|
181
183
|
### 改名说明(0.7.0)
|
|
182
184
|
|
|
@@ -233,7 +235,7 @@ sage-mem 只做三件事,全都在你本机:
|
|
|
233
235
|
|---|---|
|
|
234
236
|
| **读记忆** | 只读 `SAGE_MEM_DIR`(默认 `~/.sage-mem/memory`)下的顶层 `*.md`,单文件上限 512 KB |
|
|
235
237
|
| **写记忆** | 经 agent 的文件工具或设置页写同一个目录;`MEMORY.md`、`session-log.md` 等保留名受保护,设置页碰不到 |
|
|
236
|
-
| **注入** | 把选中的记忆作为上下文交给模型(最多 10 条相关 + 8 条 `baseline
|
|
238
|
+
| **注入** | 把选中的记忆作为上下文交给模型(最多 10 条相关 + 8 条 `baseline`,单场会话累计上限 60 KB) |
|
|
237
239
|
|
|
238
240
|
**它不做什么**:不联网、不开端口、不起常驻进程、不需要任何 API key;默认只读写记忆目录与它上一层的 `.sage-mem/` 状态目录(配置、回滚点、报告)。**例外**:输入源选「记忆 + 会话记录」时,它会额外**读** `$DSH_HOME/sessions` 下的会话记录做定向搜索——那会解压你的对话原文(含私密内容)并送进模型上下文,所以默认不开,见「自动做梦」一节;
|
|
239
241
|
不做事件全捕获(存不存由 agent 按规则判断,不是"你说了什么都记")。
|
package/lib/autodream-audit.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* **纯只读**:不修任何东西。修哪条、怎么修,由 autodream agent 或人决定。
|
|
14
14
|
*
|
|
15
|
-
* 这一份解析是 sage-mem
|
|
15
|
+
* 这一份解析是 sage-mem 宿主侧 parseFrontmatter 的同源精简版,故意不共用:
|
|
16
16
|
* 那边带着一长串历史坑的注释与行为(baseline、type 回落),审计只读不写,
|
|
17
17
|
* 重复二十行换零回归风险是划算的。
|
|
18
18
|
*/
|
|
@@ -37,7 +37,7 @@ const NON_ENTRY_FILES = new Set(['session-log.md'])
|
|
|
37
37
|
const YAML_LEAF_RE = /^[A-Za-z0-9_.-]+:\s*(.*)$/
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* 极简 frontmatter 解析,读法与
|
|
40
|
+
* 极简 frontmatter 解析,读法与 宿主侧一致:
|
|
41
41
|
* 顶层 `type:` 优先,没有才下钻 `metadata.type:`。
|
|
42
42
|
* @param {string} content — 文件全文
|
|
43
43
|
* @returns {{name: string, description: string, type: string, hasFrontmatter: boolean}}
|
package/lib/autodream.js
CHANGED
|
@@ -855,7 +855,7 @@ export class AutodreamEngine {
|
|
|
855
855
|
messages: req.messages,
|
|
856
856
|
maxTokens: req.maxTokens,
|
|
857
857
|
signal: req.signal,
|
|
858
|
-
purpose: 'session-title', //
|
|
858
|
+
purpose: 'session-title', // 标记为非交互式调用,避免被当成主对话轮次记账
|
|
859
859
|
}
|
|
860
860
|
if (req.tools && req.tools.length) options.tools = req.tools
|
|
861
861
|
for await (const chunk of llm.stream(options)) {
|
package/lib/client.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sage-mem —
|
|
2
|
+
* sage-mem — 界面侧:DSH 设置页「记忆管理」界面(文件管理器)。
|
|
3
3
|
*
|
|
4
4
|
* 手写 ModuleLoader bundle(`window.__ModuleLoader__.load({ id, factory })` 格式):
|
|
5
5
|
* window.__ModuleLoader__.load({ id, factory }),factory 内 require('react')
|
|
6
6
|
* 可用,module.exports 导出标准 Cordis 插件(apply/inject)。
|
|
7
7
|
*
|
|
8
|
-
* 数据通道:TypertRemoteService
|
|
8
|
+
* 数据通道:TypertRemoteService(宿主侧 MemoryGateway),通过
|
|
9
9
|
* ctx.get("remote.memory") 调用文件列表/读/写/删。remote 返回 {ok, value}
|
|
10
10
|
* 信封。浏览器侧无 zod,schema 手写(codec 只要求 mode==="strict" + parse)。
|
|
11
11
|
*
|
|
@@ -64,7 +64,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
64
64
|
invocation: { kind: "direct" },
|
|
65
65
|
parameters: [],
|
|
66
66
|
result: { mode: "strict", typeSymbol: "sage-starmap/types#StarList", schema: listSchema, create: () => listSchema },
|
|
67
|
-
sourceLocation: { "file": "
|
|
67
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
id: "sage-starmap#starmap/readFile",
|
|
@@ -76,7 +76,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
76
76
|
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-starmap/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
77
77
|
],
|
|
78
78
|
result: { mode: "strict", typeSymbol: "sage-starmap/types#FileContent", schema: readSchema, create: () => readSchema },
|
|
79
|
-
sourceLocation: { "file": "
|
|
79
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
80
80
|
},
|
|
81
81
|
],
|
|
82
82
|
};
|
|
@@ -109,7 +109,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
109
109
|
|
|
110
110
|
// ── 常量与纯函数 ─────────────────────────────────────────────
|
|
111
111
|
//
|
|
112
|
-
//
|
|
112
|
+
// 配色(重做过一版):原来五个色都是高饱和高亮度(#ffd27d / #6fe3ff /
|
|
113
113
|
// #b18cff / #7dffb0),配上当时的星芒与脉冲环,整体读起来像贴纸。
|
|
114
114
|
// 现在统一降饱和、压亮度 —— 深空画布上够辨认,又不抢眼。
|
|
115
115
|
const KIND_META = {
|
|
@@ -373,7 +373,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
373
373
|
".smap-modal{width:min(980px,94vw);height:min(680px,92vh);background:linear-gradient(160deg,#0d1226,#111a33);border:1px solid rgba(140,160,220,.22);border-radius:16px;box-shadow:0 24px 80px rgba(0,0,0,.55);padding:16px 18px;display:flex;flex-direction:column;overflow:hidden;pointer-events:auto;animation:smap-pop-in .22s cubic-bezier(.2,.9,.3,1.15);}",
|
|
374
374
|
"@keyframes smap-fade-in{from{opacity:0}to{opacity:1}}",
|
|
375
375
|
"@keyframes smap-pop-in{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:none}}",
|
|
376
|
-
//
|
|
376
|
+
// 星图整块含 header 与图例,同属一张深色画布。
|
|
377
377
|
//
|
|
378
378
|
// 不这么做的下场是实测出来的:宿主切到**浅色主题**后,这个组件里硬编码的
|
|
379
379
|
// 浅色文字(#dbe4f5 系)落在白色卡片上,标题、搜索框、图例几乎看不见 ——
|
|
@@ -449,7 +449,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
const SMAP_BG_VS = "attribute vec2 aPos; void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }";
|
|
452
|
-
//
|
|
452
|
+
// 底色(重做过一版)。旧版是四层 fbm 噪声算出来的蓝紫星云,还带 uTime 动画 ——
|
|
453
453
|
// 「会呼吸的彩色底」正是廉价感的一大来源,而且每帧都要重算噪声。
|
|
454
454
|
// 现在只剩一条静态径向渐变:中心略亮的中性深灰,边缘更暗。uTime 保留声明
|
|
455
455
|
// 只为让 uniform 列表不变(用不到时 getUniformLocation 返回 null,调用是安全空操作)。
|
|
@@ -484,7 +484,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
484
484
|
"}",
|
|
485
485
|
].join("\n");
|
|
486
486
|
|
|
487
|
-
//
|
|
487
|
+
// 星点(重做过一版)。去掉了三样东西,它们是「塑料感」的来源:
|
|
488
488
|
// 1. 衍射星芒(横竖两条十字 spike)—— 用户明确要求去掉
|
|
489
489
|
// 2. 脉冲扩散环(fract(uTime*0.3) 画出来的同心圆)—— 看起来像贴纸上的亮环
|
|
490
490
|
// 3. 呼吸闪烁(sin(uTime) 调制整体透明度)—— 满屏一起闪,廉价
|
|
@@ -958,7 +958,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
958
958
|
drawBox.t0 = Date.now();
|
|
959
959
|
renderer.render(0);
|
|
960
960
|
|
|
961
|
-
// ──
|
|
961
|
+
// ── 按需重绘(重做过一版)──
|
|
962
962
|
// 旧版是一个常驻 rAF 循环:只要星图页开着就永远满帧重画,后台标签页也一样,
|
|
963
963
|
// 而画面其实早就静止了。新的规矩只有三种情况会画:
|
|
964
964
|
// 1. 入场展示动画期间(约 1.3s,星点按诞生时间依次点亮)
|
|
@@ -1395,7 +1395,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1395
1395
|
invocation: { kind: "direct" },
|
|
1396
1396
|
parameters: [],
|
|
1397
1397
|
result: { mode: "strict", typeSymbol: "sage-mem/types#FileList", schema: listSchema, create: () => listSchema },
|
|
1398
|
-
sourceLocation: { "file": "
|
|
1398
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1399
1399
|
},
|
|
1400
1400
|
{
|
|
1401
1401
|
id: "sage-mem#memory/readFile",
|
|
@@ -1407,7 +1407,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1407
1407
|
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1408
1408
|
],
|
|
1409
1409
|
result: { mode: "strict", typeSymbol: "sage-mem/types#FileContent", schema: readSchema, create: () => readSchema },
|
|
1410
|
-
sourceLocation: { "file": "
|
|
1410
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1411
1411
|
},
|
|
1412
1412
|
{
|
|
1413
1413
|
id: "sage-mem#memory/writeFile",
|
|
@@ -1420,7 +1420,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1420
1420
|
{ name: "content", wire: "content", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileContent", schema: strSchema, create: () => strSchema } },
|
|
1421
1421
|
],
|
|
1422
1422
|
result: { mode: "strict", typeSymbol: "sage-mem/types#WriteResult", schema: writeSchema, create: () => writeSchema },
|
|
1423
|
-
sourceLocation: { "file": "
|
|
1423
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1424
1424
|
},
|
|
1425
1425
|
{
|
|
1426
1426
|
id: "sage-mem#memory/deleteFile",
|
|
@@ -1432,7 +1432,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1432
1432
|
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1433
1433
|
],
|
|
1434
1434
|
result: { mode: "strict", typeSymbol: "sage-mem/types#DeleteResult", schema: deleteSchema, create: () => deleteSchema },
|
|
1435
|
-
sourceLocation: { "file": "
|
|
1435
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1436
1436
|
},
|
|
1437
1437
|
{
|
|
1438
1438
|
id: "sage-mem#starmap/listStars",
|
|
@@ -1442,7 +1442,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1442
1442
|
invocation: { kind: "direct" },
|
|
1443
1443
|
parameters: [],
|
|
1444
1444
|
result: { mode: "strict", typeSymbol: "sage-mem/types#StarmapList", schema: starmapListSchema, create: () => starmapListSchema },
|
|
1445
|
-
sourceLocation: { "file": "
|
|
1445
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1446
1446
|
},
|
|
1447
1447
|
{
|
|
1448
1448
|
id: "sage-mem#starmap/readFile",
|
|
@@ -1454,13 +1454,13 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1454
1454
|
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1455
1455
|
],
|
|
1456
1456
|
result: { mode: "strict", typeSymbol: "sage-mem/types#StarmapRead", schema: starmapReadSchema, create: () => starmapReadSchema },
|
|
1457
|
-
sourceLocation: { "file": "
|
|
1457
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1458
1458
|
},
|
|
1459
1459
|
// ── autodream(自动做梦)──
|
|
1460
1460
|
// 踩到的坑:namespace 不会自动出现。宿主那份 remote 清单是**构建期生成的
|
|
1461
1461
|
// 产物**,只有 memory / starmap 在里面;
|
|
1462
1462
|
// 后加的 autodream 不在,于是 `ctx.get("remote.autodream")` 永远 undefined ——
|
|
1463
|
-
// 界面上表现为「remote.autodream 不可用」,而
|
|
1463
|
+
// 界面上表现为「remote.autodream 不可用」,而 宿主侧日志明确写着
|
|
1464
1464
|
// 「autodream gateway mounted ok」(服务是真的挂上了,只是 client 不知道)。
|
|
1465
1465
|
// 修法就是自己声明 + $mount,和 memory / starmap 一样(现在共十一条:
|
|
1466
1466
|
// 七个原有方法 + listRuns / readDeclaration / rollback / listModels)。
|
|
@@ -1472,7 +1472,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1472
1472
|
invocation: { kind: "direct" },
|
|
1473
1473
|
parameters: [],
|
|
1474
1474
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamConfigResult", schema: objCodec, create: () => objCodec },
|
|
1475
|
-
sourceLocation: { "file": "
|
|
1475
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1476
1476
|
},
|
|
1477
1477
|
{
|
|
1478
1478
|
id: "sage-mem#autodream/setConfig",
|
|
@@ -1484,7 +1484,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1484
1484
|
{ name: "patch", wire: "patch", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamPatch", schema: objCodec, create: () => objCodec } },
|
|
1485
1485
|
],
|
|
1486
1486
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamConfigResult", schema: objCodec, create: () => objCodec },
|
|
1487
|
-
sourceLocation: { "file": "
|
|
1487
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1488
1488
|
},
|
|
1489
1489
|
{
|
|
1490
1490
|
id: "sage-mem#autodream/status",
|
|
@@ -1494,7 +1494,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1494
1494
|
invocation: { kind: "direct" },
|
|
1495
1495
|
parameters: [],
|
|
1496
1496
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamStatus", schema: objCodec, create: () => objCodec },
|
|
1497
|
-
sourceLocation: { "file": "
|
|
1497
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1498
1498
|
},
|
|
1499
1499
|
{
|
|
1500
1500
|
id: "sage-mem#autodream/runNow",
|
|
@@ -1504,11 +1504,11 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1504
1504
|
invocation: { kind: "direct" },
|
|
1505
1505
|
parameters: [
|
|
1506
1506
|
// 契约:runNow({reason})。前端只负责给一个缘由字符串(落进
|
|
1507
|
-
// manifest.reason 给人看);「手动触发跳过门控」由
|
|
1507
|
+
// manifest.reason 给人看);「手动触发跳过门控」由 宿主侧决定。
|
|
1508
1508
|
{ name: "opts", wire: "opts", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamOpts", schema: objCodec, create: () => objCodec } },
|
|
1509
1509
|
],
|
|
1510
1510
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamRunResult", schema: objCodec, create: () => objCodec },
|
|
1511
|
-
sourceLocation: { "file": "
|
|
1511
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1512
1512
|
},
|
|
1513
1513
|
{
|
|
1514
1514
|
id: "sage-mem#autodream/listReports",
|
|
@@ -1518,7 +1518,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1518
1518
|
invocation: { kind: "direct" },
|
|
1519
1519
|
parameters: [],
|
|
1520
1520
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamReportList", schema: arrCodec, create: () => arrCodec },
|
|
1521
|
-
sourceLocation: { "file": "
|
|
1521
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1522
1522
|
},
|
|
1523
1523
|
{
|
|
1524
1524
|
id: "sage-mem#autodream/readReport",
|
|
@@ -1530,7 +1530,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1530
1530
|
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamReportRef", schema: strSchema, create: () => strSchema } },
|
|
1531
1531
|
],
|
|
1532
1532
|
result: { mode: "strict", typeSymbol: "sage-mem/types#FileContent", schema: objCodec, create: () => objCodec },
|
|
1533
|
-
sourceLocation: { "file": "
|
|
1533
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1534
1534
|
},
|
|
1535
1535
|
{
|
|
1536
1536
|
id: "sage-mem#autodream/listSnapshots",
|
|
@@ -1540,9 +1540,9 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1540
1540
|
invocation: { kind: "direct" },
|
|
1541
1541
|
parameters: [],
|
|
1542
1542
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamSnapshotList", schema: arrCodec, create: () => arrCodec },
|
|
1543
|
-
sourceLocation: { "file": "
|
|
1543
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1544
1544
|
},
|
|
1545
|
-
// ──
|
|
1545
|
+
// ── 运行记录 / 整理声明 / 回滚 / 模型目录 ──
|
|
1546
1546
|
// 都是「单个对象参数」形态(与 setConfig(patch) 同形),契约第二节的
|
|
1547
1547
|
// {limit} / {runId} / {snapshotId, scope} 逐字对应这个对象的字段。
|
|
1548
1548
|
{
|
|
@@ -1555,7 +1555,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1555
1555
|
{ name: "opts", wire: "opts", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamOpts", schema: objCodec, create: () => objCodec } },
|
|
1556
1556
|
],
|
|
1557
1557
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamRunList", schema: arrCodec, create: () => arrCodec },
|
|
1558
|
-
sourceLocation: { "file": "
|
|
1558
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1559
1559
|
},
|
|
1560
1560
|
{
|
|
1561
1561
|
id: "sage-mem#autodream/readDeclaration",
|
|
@@ -1567,7 +1567,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1567
1567
|
{ name: "opts", wire: "opts", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamOpts", schema: objCodec, create: () => objCodec } },
|
|
1568
1568
|
],
|
|
1569
1569
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamDeclaration", schema: objCodec, create: () => objCodec },
|
|
1570
|
-
sourceLocation: { "file": "
|
|
1570
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1571
1571
|
},
|
|
1572
1572
|
{
|
|
1573
1573
|
id: "sage-mem#autodream/rollback",
|
|
@@ -1579,7 +1579,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1579
1579
|
{ name: "opts", wire: "opts", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamOpts", schema: objCodec, create: () => objCodec } },
|
|
1580
1580
|
],
|
|
1581
1581
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamRollbackResult", schema: objCodec, create: () => objCodec },
|
|
1582
|
-
sourceLocation: { "file": "
|
|
1582
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1583
1583
|
},
|
|
1584
1584
|
{
|
|
1585
1585
|
id: "sage-mem#autodream/listModels",
|
|
@@ -1589,7 +1589,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1589
1589
|
invocation: { kind: "direct" },
|
|
1590
1590
|
parameters: [],
|
|
1591
1591
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AutodreamModelList", schema: objCodec, create: () => objCodec },
|
|
1592
|
-
sourceLocation: { "file": "
|
|
1592
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1593
1593
|
},
|
|
1594
1594
|
],
|
|
1595
1595
|
};
|
|
@@ -1671,7 +1671,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1671
1671
|
".smem-report-pre{max-height:320px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.6;}",
|
|
1672
1672
|
".smem-autodream-adv>summary{cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);}",
|
|
1673
1673
|
".smem-autodream-adv>div{display:flex;flex-direction:column;gap:8px;padding-top:9px;}",
|
|
1674
|
-
// ──
|
|
1674
|
+
// ── 回滚点区 / 整理声明区 ──
|
|
1675
1675
|
// 新元素一律 .smem-ad-* 前缀,不动主题外的全局样式(契约第六节第 6 条)。
|
|
1676
1676
|
".smem-ad-select{flex:1;min-width:190px;}",
|
|
1677
1677
|
".smem-ad-err{color:var(--dsw-alias-state-error-primary,#ef4444);}",
|
|
@@ -1720,7 +1720,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1720
1720
|
|
|
1721
1721
|
// ── frontmatter 纯逻辑(顶层自包含普通函数,不碰 React state)─────
|
|
1722
1722
|
//
|
|
1723
|
-
//
|
|
1723
|
+
// 这里曾出过一次 P0 级数据丢失事故,根因全在这几行里,三条硬规矩:
|
|
1724
1724
|
// 1) 绝不「重建」frontmatter —— 就地合并,除 description / type 外每一行
|
|
1725
1725
|
// 逐字保留。旧版 buildFrontmatter() 只写 name/description/type 三个字段,
|
|
1726
1726
|
// 保存一次就把 baseline / node_type / originSessionId / updated 全抹掉
|
|
@@ -1730,7 +1730,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1730
1730
|
// 3) 换行必须容忍 CRLF(Windows 编辑器碰一次就是 \r\n);回写沿用原文件的
|
|
1731
1731
|
// 换行风格,从而 LF / CRLF 都能逐字节 round-trip。
|
|
1732
1732
|
|
|
1733
|
-
/** 去掉值两侧成对的引号(与
|
|
1733
|
+
/** 去掉值两侧成对的引号(与 宿主侧 parseFrontmatter 的剥法一致)。 */
|
|
1734
1734
|
function stripQuotes(v) {
|
|
1735
1735
|
const s = typeof v === "string" ? v.trim() : "";
|
|
1736
1736
|
return s.replace(/^["']|["']$/g, "").trim();
|
|
@@ -1764,7 +1764,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1764
1764
|
* 识别记忆类型。锚行首(^ + m),**不看 node_type**:
|
|
1765
1765
|
* 1) 顶层 `type: X`
|
|
1766
1766
|
* 2) metadata 块内的缩进写法 ` type: X`
|
|
1767
|
-
* 优先级与
|
|
1767
|
+
* 优先级与 宿主侧 parseFrontmatter 一致(顶层优先)。找不到返回 ""。
|
|
1768
1768
|
*/
|
|
1769
1769
|
function detectType(text) {
|
|
1770
1770
|
const fm = fmInner(text);
|
|
@@ -1794,7 +1794,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1794
1794
|
}
|
|
1795
1795
|
|
|
1796
1796
|
/**
|
|
1797
|
-
* 取顶层 tags 的值。三种写法都认,与
|
|
1797
|
+
* 取顶层 tags 的值。三种写法都认,与 宿主侧 parseTags 保持同一套规则:
|
|
1798
1798
|
* tags: ["a", "b"] (flow,本插件自己写出去的就是这种)
|
|
1799
1799
|
* tags:\n - a\n - b (block,手写文件常见)
|
|
1800
1800
|
* tags: a, b (松散逗号串)
|
|
@@ -2042,9 +2042,9 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2042
2042
|
}
|
|
2043
2043
|
|
|
2044
2044
|
/**
|
|
2045
|
-
* 「自动做梦」面板 —— 让记忆自己整理一遍(引擎在
|
|
2045
|
+
* 「自动做梦」面板 —— 让记忆自己整理一遍(引擎在 宿主侧 lib/autodream.js)。
|
|
2046
2046
|
*
|
|
2047
|
-
* 数据全走 remote.autodream
|
|
2047
|
+
* 数据全走 remote.autodream(宿主侧的 AutodreamGateway)。两条实现约定:
|
|
2048
2048
|
* 1. **轮询而不是等待**。「立即整理」启动后台任务后立刻返回 —— 一趟可能跑
|
|
2049
2049
|
* 几分钟,把 remote 调用挂在那儿等会撞上前端超时。进度靠每 2 秒拉一次
|
|
2050
2050
|
* status() 显示。
|
|
@@ -2063,7 +2063,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2063
2063
|
const sBody = react.useState(""); const body = sBody[0], setBody = sBody[1];
|
|
2064
2064
|
const sProv = react.useState(""); const prov = sProv[0], setProv = sProv[1];
|
|
2065
2065
|
const sModel = react.useState(""); const model = sModel[0], setModel = sModel[1];
|
|
2066
|
-
// ──
|
|
2066
|
+
// ── 模型目录 / 回滚点 / 整理记录 / 整理声明 ──
|
|
2067
2067
|
const sModels = react.useState(null); const models = sModels[0], setModels = sModels[1];
|
|
2068
2068
|
const sSnaps = react.useState([]); const snaps = sSnaps[0], setSnaps = sSnaps[1];
|
|
2069
2069
|
const sRuns = react.useState([]); const runs = sRuns[0], setRuns = sRuns[1];
|
|
@@ -2083,7 +2083,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2083
2083
|
const rollbackScope = c.rollbackScope === "all" ? "all" : "files";
|
|
2084
2084
|
|
|
2085
2085
|
react.useEffect(function () {
|
|
2086
|
-
if (!remote) { setErr("remote.autodream 不可用 —— 插件的
|
|
2086
|
+
if (!remote) { setErr("remote.autodream 不可用 —— 插件的 宿主侧可能没加载成功,看 DSH 启动日志"); return undefined; }
|
|
2087
2087
|
let alive = true;
|
|
2088
2088
|
function pull() {
|
|
2089
2089
|
callRemote("status", [], function (d) {
|
|
@@ -2116,13 +2116,13 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2116
2116
|
* 所有 remote 调用的统一出口。
|
|
2117
2117
|
*
|
|
2118
2118
|
* 契约的硬约束是「每个调用都 try/catch,失败显示错误文案、不白屏」,而
|
|
2119
|
-
*
|
|
2119
|
+
* 这里还有第三种失败:宿主侧是旧版本、方法根本不在 remote 对象上
|
|
2120
2120
|
* (typeof !== "function")—— 那会在 `.then` 之前就抛 TypeError。
|
|
2121
2121
|
* 三种都收敛到 onErr,面板只会多一行红字。
|
|
2122
2122
|
*/
|
|
2123
2123
|
function callRemote(name, args, onOk, onErr) {
|
|
2124
2124
|
const fn = remote ? remote[name] : null;
|
|
2125
|
-
if (typeof fn !== "function") { if (onErr) onErr("
|
|
2125
|
+
if (typeof fn !== "function") { if (onErr) onErr("宿主侧没有 " + name + " 接口(版本较旧?)"); return; }
|
|
2126
2126
|
let p;
|
|
2127
2127
|
try { p = fn.apply(remote, args || []); }
|
|
2128
2128
|
catch (e) { if (onErr) onErr(detail(e)); return; }
|
|
@@ -2161,7 +2161,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2161
2161
|
function run() {
|
|
2162
2162
|
setErr(null); setNotice(""); setBusy(true);
|
|
2163
2163
|
// 契约:runNow({ reason })。reason 只落进 manifest 给人看;门控的豁免
|
|
2164
|
-
// 由
|
|
2164
|
+
// 由 宿主侧负责(手动点了按钮就不该被「还没到 N 小时」挡回来)。
|
|
2165
2165
|
callRemote("runNow", [{ reason: "设置页手动触发" }], function (d) {
|
|
2166
2166
|
if (d && d.ok === false) setErr(d.error || "启动失败");
|
|
2167
2167
|
else { setNotice("已开始整理 —— 进度每 2 秒刷新一次"); refreshLists(); }
|
|
@@ -2717,7 +2717,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2717
2717
|
]),
|
|
2718
2718
|
// 搜索 / 刷新 / 添加 / 筛选都只属于「文件列表」这个视图。
|
|
2719
2719
|
// 放在 tab 分支外面,会让星图和自动做梦页也顶着一排用不上的控件
|
|
2720
|
-
//
|
|
2720
|
+
// (走查时发现:「+ 添加记忆」出现在星图页上)。
|
|
2721
2721
|
tab === "files" ? h("div", { className: "smem-toolbar", key: "bar" }, [
|
|
2722
2722
|
h("input", { className: "smem-search", key: "q", value: query, placeholder: "搜索文件名 / 描述 / 类型 / 标签…", onChange: function (e) { setQuery(e.target.value); } }),
|
|
2723
2723
|
h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
|
package/lib/index.js
CHANGED
|
@@ -60,6 +60,46 @@ const MAX_CHARS_PER_FILE = envInt('SAGE_MEM_MAX_CHARS', 1500, 200, 20000)
|
|
|
60
60
|
*/
|
|
61
61
|
const MAX_BASELINE = envInt('SAGE_MEM_MAX_BASELINE', 8, 1, 50)
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* 单会话累计注入字节上限 —— 到顶以后这个会话就不再注入新的记忆。
|
|
65
|
+
*
|
|
66
|
+
* 为什么要有:条数上限(MAX_RESULTS)管的是「一次」注入多少,管不了「一场会话注
|
|
67
|
+
* 多少次」。长会话里问题一直在换、检索签名一直在变,于是走一步注一批,累积没有
|
|
68
|
+
* 上限 —— 实测「服务器环境」那个问题一次就注 7,700 字符,八次就到 60 KB。
|
|
69
|
+
* Claude Code 有同一层闸门(`MAX_SESSION_BYTES = 60 * 1024`,到顶**彻底停止**预取),
|
|
70
|
+
* 我们此前只有单条上限、没有会话上限。
|
|
71
|
+
*
|
|
72
|
+
* 60 KB 是与 CC 对齐的起点、不是实测出来的数字 —— 留了 SAGE_MEM_MAX_SESSION_BYTES
|
|
73
|
+
* 覆写口,量过再定。
|
|
74
|
+
*/
|
|
75
|
+
const MAX_SESSION_BYTES = envInt('SAGE_MEM_MAX_SESSION_BYTES', 60 * 1024, 4096, 512 * 1024)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 注入文本里标「保存于 N 天前」的门槛(天)。超了才标;0 = 每条都标。
|
|
79
|
+
*
|
|
80
|
+
* 为什么用「N 天前」而不是日期:模型不擅长日期算术,绝对时间戳几乎不触发陈旧
|
|
81
|
+
* 推理,相对天数才会。为什么需要它:陈旧记忆被当事实引用时,「引用」这个动作
|
|
82
|
+
* 本身会让它显得更权威、而不是更可疑。
|
|
83
|
+
*/
|
|
84
|
+
const STALE_DAYS = envInt('SAGE_MEM_STALE_DAYS', 1, 0, 365)
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 注入文本开头的引用纪律 —— **必须独立成节,不能压成一条 bullet**。
|
|
88
|
+
*
|
|
89
|
+
* Claude Code 对这条做过 A/B:同一句内容埋成 bullet 命中 0/3,独立成节 3/3,
|
|
90
|
+
* 连标题措辞都验过。原因很实在:陈旧记忆被当事实引用时,「引用」这个动作本身
|
|
91
|
+
* 会让它显得更权威而不是更可疑 —— 规矩得在**读这批内容之前**先立。
|
|
92
|
+
*
|
|
93
|
+
* 每次注入都带(约 150 字符),不按会话只带一次:它约束的是「这一批」怎么用,
|
|
94
|
+
* 与批次绑定比与会话绑定更不容易失效。
|
|
95
|
+
*/
|
|
96
|
+
const RECALL_DISCIPLINE = [
|
|
97
|
+
'> **引用纪律**:以下是过去某一刻写下的记录,不是当前事实。',
|
|
98
|
+
'> 提到文件路径先确认存在,提到函数 / 配置项先 grep,要据此动手前先核实当前代码。',
|
|
99
|
+
'> 标了「保存于 N 天前」的尤其注意:其中的 `文件:行号` 很可能已经过期。',
|
|
100
|
+
'> 「记忆里写着 X 存在」不等于「X 现在存在」。',
|
|
101
|
+
].join('\n')
|
|
102
|
+
|
|
63
103
|
/**
|
|
64
104
|
* 结构性保留名:设置页的写/删方法一律不得触碰。
|
|
65
105
|
*
|
|
@@ -173,7 +213,9 @@ async function scanMemoryFiles(ctx, sessionId) {
|
|
|
173
213
|
.map(e => e.name)
|
|
174
214
|
const loaded = await Promise.all(names.map(async (name) => {
|
|
175
215
|
try {
|
|
176
|
-
const
|
|
216
|
+
const full = join(MEMORY_DIR, name)
|
|
217
|
+
// stat 与 readFile 并行 —— 多要一个 mtime 不该变成串行两跳。
|
|
218
|
+
const [content, info] = await Promise.all([readFile(full, 'utf8'), stat(full)])
|
|
177
219
|
const meta = parseFrontmatter(content)
|
|
178
220
|
return {
|
|
179
221
|
file: name,
|
|
@@ -182,6 +224,9 @@ async function scanMemoryFiles(ctx, sessionId) {
|
|
|
182
224
|
aliases: meta.aliases || [],
|
|
183
225
|
baseline: parseBaseline(content),
|
|
184
226
|
baselinePriority: meta.baselinePriority || 0,
|
|
227
|
+
// 新鲜度的两个来源:frontmatter 的 updated(权威)与文件 mtime(退路)。
|
|
228
|
+
updated: meta.updated || '',
|
|
229
|
+
mtimeMs: info.mtimeMs,
|
|
185
230
|
}
|
|
186
231
|
} catch (err) {
|
|
187
232
|
// 只有 ENOENT 是正常状态(扫到读之间被删)。其余——权限错、编码坏、盘掉线——
|
|
@@ -266,7 +311,7 @@ function parseFrontmatter(content) {
|
|
|
266
311
|
// 「baseline 走 parseBaseline 那条带 \r?\n 的正则照旧命中,但这条记忆永远检索不到」,
|
|
267
312
|
// 症状极具欺骗性。下文定义的 parseBaseline 与 extractTitle 早已是 \r?\n。
|
|
268
313
|
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
269
|
-
if (!m) return { name: '', description: '', type: '', tags: [], aliases: [], baselinePriority: 0 }
|
|
314
|
+
if (!m) return { name: '', description: '', type: '', tags: [], aliases: [], updated: '', baselinePriority: 0 }
|
|
270
315
|
const body = m[1]
|
|
271
316
|
const grab = (key) => {
|
|
272
317
|
// [ \t] / [ \t\r] 而不是 \s:\s 会连 CRLF 的 \r 一起吃进捕获组,值尾留一个 \r
|
|
@@ -280,12 +325,21 @@ function parseFrontmatter(content) {
|
|
|
280
325
|
const nested = body.match(/^metadata:\s*\r?\n(?:\s+[^\r\n]+\r?\n)*?\s+type:\s*(\S+)/m)
|
|
281
326
|
if (nested) type = nested[1]
|
|
282
327
|
}
|
|
328
|
+
// `updated` 踩的是与 `type` 同一个坑:它通常写在 `metadata:` 嵌套块里(缩进两格),
|
|
329
|
+
// 顶层 `^updated:` 抓不到 —— 于是「明明写了日期却永远显示不出来」。
|
|
330
|
+
// 抓不到就回落文件 mtime(见 ageDays):手写时间戳是权威的,文件系统时间只是退路。
|
|
331
|
+
let updated = grab('updated')
|
|
332
|
+
if (!updated) {
|
|
333
|
+
const nestedUpdated = body.match(/^metadata:\s*\r?\n(?:\s+[^\r\n]+\r?\n)*?\s+updated:\s*(\S+)/m)
|
|
334
|
+
if (nestedUpdated) updated = nestedUpdated[1]
|
|
335
|
+
}
|
|
283
336
|
return {
|
|
284
337
|
name: grab('name'),
|
|
285
338
|
description: grab('description'),
|
|
286
339
|
type,
|
|
287
340
|
tags: parseTags(body),
|
|
288
341
|
aliases: parseAliases(body),
|
|
342
|
+
updated,
|
|
289
343
|
baselinePriority: parseBaselinePriority(body),
|
|
290
344
|
}
|
|
291
345
|
}
|
|
@@ -337,6 +391,24 @@ function parseBaseline(content) {
|
|
|
337
391
|
return /^(true|yes|1)$/i.test(hit[1].replace(/^["']|["']$/g, '').trim())
|
|
338
392
|
}
|
|
339
393
|
|
|
394
|
+
/**
|
|
395
|
+
* 一条记忆「多久以前写下的」(整数天;null = 无从判断)。
|
|
396
|
+
*
|
|
397
|
+
* **优先 frontmatter 的 `updated`,缺失才回落文件 mtime —— 顺序不能反。**
|
|
398
|
+
* 文件系统时间会被复制、checkout、备份还原整批改掉(我们 memory 目录真被整目录
|
|
399
|
+
* 备份/还原过),它表示的是「这个文件什么时候被动过」,不是「这条记忆什么时候
|
|
400
|
+
* 写的」。手写的时间戳才是权威。
|
|
401
|
+
*
|
|
402
|
+
* 未来日期(时钟偏差 / 手写错)一律当 0:宁可标「今天」也不要标负数。
|
|
403
|
+
*/
|
|
404
|
+
function ageDays(f, nowMs) {
|
|
405
|
+
const fromMeta = Date.parse(f?.updated || '')
|
|
406
|
+
const base = Number.isFinite(fromMeta) ? fromMeta : (Number.isFinite(f?.mtimeMs) ? f.mtimeMs : NaN)
|
|
407
|
+
if (!Number.isFinite(base)) return null
|
|
408
|
+
const days = Math.floor((nowMs - base) / 86400000)
|
|
409
|
+
return days > 0 ? days : 0
|
|
410
|
+
}
|
|
411
|
+
|
|
340
412
|
/**
|
|
341
413
|
* 原子写:先写同目录临时文件,再 rename 覆盖目标。
|
|
342
414
|
*
|
|
@@ -462,6 +534,18 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
462
534
|
const BASELINE_CACHE_MAX = 64
|
|
463
535
|
const baselineInjected = new Set()
|
|
464
536
|
|
|
537
|
+
// 「本会话已经注入掉多少字节」—— 会话级账本,按 session id 记。
|
|
538
|
+
//
|
|
539
|
+
// 为什么不扫会话记录去算:同一轮 assemble 出来的内容会被宿主合进**同一条**
|
|
540
|
+
// user/message(运行时上下文、工作区指令、插件注入共用一条),按消息字节统计
|
|
541
|
+
// 会把别人的内容算进我们的账;按文本找标记又太脆。自己记账是确定性最高的。
|
|
542
|
+
//
|
|
543
|
+
// 为什么要连 messageCount 一起记:压缩 / 清空之后上下文里的记忆已经没了,而
|
|
544
|
+
// session id 不变 —— 只按 id 记账会让「预算用满」变成永久状态,压缩之后再也
|
|
545
|
+
// 注不进来。消息条数变小是压缩发生过的最直接信号,用它把账本归零。
|
|
546
|
+
const INJECT_LEDGER_MAX = 64
|
|
547
|
+
const injectLedger = new Map()
|
|
548
|
+
|
|
465
549
|
// 按问题检索注入:system-prompt/assemble 是异步 waterfall,监听器可 await。
|
|
466
550
|
// 从 agent.session 拿当前 user message,扫 memory 目录按摘要匹配选相关
|
|
467
551
|
// 文件,读全文作为动态 context 段注入——让 agent 第一轮就「想起」。
|
|
@@ -504,9 +588,9 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
504
588
|
//
|
|
505
589
|
// 旧写法 `if (userMessages.length === 0)` 依赖一个真实存在的时序:assemble
|
|
506
590
|
// 触发时当前这条用户消息还没写进 session。已在 DSH 0.1.7-rc.1 上核实为真 ——
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
591
|
+
// 宿主从 pending 队列取走本轮输入
|
|
592
|
+
// 组装 system prompt(本监听器在这里跑)
|
|
593
|
+
// 之后才把这条 user 消息落进 session
|
|
510
594
|
// 所以全新会话首步 pick 到 0 条 user message,分支命中。
|
|
511
595
|
//
|
|
512
596
|
// 但**恢复 / 续聊会话**的历史消息早就在 session 里,条数永远 ≥ 1 → 分支永不
|
|
@@ -546,19 +630,53 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
546
630
|
|
|
547
631
|
if (picked.length === 0) return assembled
|
|
548
632
|
|
|
549
|
-
|
|
633
|
+
// ── 单会话累计预算 ──
|
|
634
|
+
// 账记在 session id 上;消息条数变小 = 压缩/清空发生过 → 账本归零(见上面
|
|
635
|
+
// injectLedger 的注释)。picked 里 baseline 永远排在检索结果之前,所以预算
|
|
636
|
+
// 不够时先被挤掉的是「更晚才想起来的」那些 —— baseline 有优先权。
|
|
637
|
+
const ledgerKey = session.id || 'no-session-id'
|
|
638
|
+
const prevLedger = injectLedger.get(ledgerKey)
|
|
639
|
+
let usedBytes = prevLedger && messages.length >= prevLedger.messageCount ? prevLedger.bytes : 0
|
|
640
|
+
const nowMs = Date.now()
|
|
641
|
+
|
|
642
|
+
const parts = []
|
|
643
|
+
let skippedForBudget = 0
|
|
644
|
+
for (const f of picked) {
|
|
550
645
|
const body = f.content.length > MAX_CHARS_PER_FILE
|
|
551
646
|
? f.content.slice(0, MAX_CHARS_PER_FILE) + '\n…(截断)'
|
|
552
647
|
: f.content
|
|
553
|
-
|
|
554
|
-
|
|
648
|
+
const days = ageDays(f, nowMs)
|
|
649
|
+
const stamp = days !== null && days >= STALE_DAYS ? `(保存于 ${days} 天前)` : ''
|
|
650
|
+
const part = `### ${f.file}${stamp}\n${body}`
|
|
651
|
+
const size = Buffer.byteLength(part, 'utf8')
|
|
652
|
+
if (usedBytes + size > MAX_SESSION_BYTES) { skippedForBudget++; continue }
|
|
653
|
+
usedBytes += size
|
|
654
|
+
parts.push(part)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 被预算挡掉了全部条目 —— 一个字都不注。本会话已经注进去的那批还在上下文里,
|
|
658
|
+
// 每步再塞一条「预算已满」只是白花 token;真压缩过之后账本会自己归零。
|
|
659
|
+
if (parts.length === 0) return assembled
|
|
660
|
+
|
|
661
|
+
injectLedger.set(ledgerKey, { bytes: usedBytes, messageCount: messages.length })
|
|
662
|
+
if (injectLedger.size > INJECT_LEDGER_MAX) {
|
|
663
|
+
injectLedger.delete(injectLedger.keys().next().value)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 状态行:让「注了几条、花了多少、被挡掉几条」在上下文里可见 ——
|
|
667
|
+
// 「某条记忆没被注入」是最难查的一类故障,这里留一个可读的出口。
|
|
668
|
+
const status = `(本次注入 ${parts.length} 条;本会话累计 ${Math.round(usedBytes / 1024)} KB` +
|
|
669
|
+
` / 上限 ${Math.round(MAX_SESSION_BYTES / 1024)} KB` +
|
|
670
|
+
(skippedForBudget > 0 ? `;因预算省略 ${skippedForBudget} 条,需要时直接用文件工具读 ${MEMORY_DIR}` : '') +
|
|
671
|
+
')'
|
|
555
672
|
|
|
556
673
|
return {
|
|
557
674
|
...assembled,
|
|
558
675
|
contexts: [...(assembled.contexts ?? []), {
|
|
559
676
|
name: 'sage-mem:recall',
|
|
560
677
|
order: 1000,
|
|
561
|
-
text: `以下是与你当前问题相关的历史记忆(文件式,来自 ${MEMORY_DIR}):\n\n
|
|
678
|
+
text: `以下是与你当前问题相关的历史记忆(文件式,来自 ${MEMORY_DIR}):\n\n` +
|
|
679
|
+
`${RECALL_DISCIPLINE}\n\n${parts.join('\n\n')}\n\n${status}`,
|
|
562
680
|
}],
|
|
563
681
|
}
|
|
564
682
|
} catch (err) {
|
|
@@ -673,8 +791,8 @@ async function scanStars(ctx) {
|
|
|
673
791
|
}
|
|
674
792
|
|
|
675
793
|
/**
|
|
676
|
-
*
|
|
677
|
-
* 通过 TypertRemoteService 暴露两个 remote
|
|
794
|
+
* 记忆星图(宿主侧)—— 只读扫描记忆目录,把每条记忆解析成一颗「星」。
|
|
795
|
+
* 通过 TypertRemoteService 暴露两个 remote 方法给界面侧:
|
|
678
796
|
* - starmap.listStars() 全部星(元数据,不含正文)
|
|
679
797
|
* - starmap.readFile(n) 单条记忆全文(文件名白名单校验)
|
|
680
798
|
*/
|
|
@@ -825,7 +943,9 @@ export const __testables = {
|
|
|
825
943
|
parseBaselinePriority,
|
|
826
944
|
parseBaseline,
|
|
827
945
|
parseTags,
|
|
946
|
+
ageDays,
|
|
828
947
|
selectRelevant,
|
|
829
948
|
selectBaseline,
|
|
830
|
-
limits: { MAX_RESULTS, MAX_CHARS_PER_FILE, MAX_BASELINE },
|
|
949
|
+
limits: { MAX_RESULTS, MAX_CHARS_PER_FILE, MAX_BASELINE, MAX_SESSION_BYTES, STALE_DAYS },
|
|
950
|
+
recallDiscipline: RECALL_DISCIPLINE,
|
|
831
951
|
}
|
package/lib/typert.host.js
CHANGED
|
@@ -264,7 +264,7 @@ export const TYPERT = {
|
|
|
264
264
|
invocation: { kind: 'direct' },
|
|
265
265
|
parameters: [],
|
|
266
266
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#FileList', schema: listResultSchema, create: () => listResultSchema },
|
|
267
|
-
sourceLocation: { "file": "
|
|
267
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
268
268
|
},
|
|
269
269
|
{
|
|
270
270
|
id: 'sage-mem#memory/readFile',
|
|
@@ -276,7 +276,7 @@ export const TYPERT = {
|
|
|
276
276
|
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileName', schema: fileNameSchema, create: () => fileNameSchema } },
|
|
277
277
|
],
|
|
278
278
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#FileContent', schema: readResultSchema, create: () => readResultSchema },
|
|
279
|
-
sourceLocation: { "file": "
|
|
279
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
280
280
|
},
|
|
281
281
|
{
|
|
282
282
|
id: 'sage-mem#memory/writeFile',
|
|
@@ -289,7 +289,7 @@ export const TYPERT = {
|
|
|
289
289
|
{ name: 'content', wire: 'content', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileContent', schema: fileContentSchema, create: () => fileContentSchema } },
|
|
290
290
|
],
|
|
291
291
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#WriteResult', schema: writeResultSchema, create: () => writeResultSchema },
|
|
292
|
-
sourceLocation: { "file": "
|
|
292
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
293
293
|
},
|
|
294
294
|
{
|
|
295
295
|
id: 'sage-mem#memory/deleteFile',
|
|
@@ -301,7 +301,7 @@ export const TYPERT = {
|
|
|
301
301
|
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileName', schema: fileNameSchema, create: () => fileNameSchema } },
|
|
302
302
|
],
|
|
303
303
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#DeleteResult', schema: deleteResultSchema, create: () => deleteResultSchema },
|
|
304
|
-
sourceLocation: { "file": "
|
|
304
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
305
305
|
},
|
|
306
306
|
{
|
|
307
307
|
id: 'sage-mem#starmap/listStars',
|
|
@@ -311,7 +311,7 @@ export const TYPERT = {
|
|
|
311
311
|
invocation: { kind: 'direct' },
|
|
312
312
|
parameters: [],
|
|
313
313
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#StarmapList', schema: starmapListSchema, create: () => starmapListSchema },
|
|
314
|
-
sourceLocation: { "file": "
|
|
314
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
315
315
|
},
|
|
316
316
|
{
|
|
317
317
|
id: 'sage-mem#starmap/readFile',
|
|
@@ -323,7 +323,7 @@ export const TYPERT = {
|
|
|
323
323
|
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileName', schema: fileNameSchema, create: () => fileNameSchema } },
|
|
324
324
|
],
|
|
325
325
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#StarmapRead', schema: starmapReadSchema, create: () => starmapReadSchema },
|
|
326
|
-
sourceLocation: { "file": "
|
|
326
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
327
327
|
},
|
|
328
328
|
// ── autodream 的七个方法 ──
|
|
329
329
|
// 全部挂在 package `sage-mem` 下(和 memory 同一份 manifest),
|
|
@@ -338,7 +338,7 @@ export const TYPERT = {
|
|
|
338
338
|
invocation: { kind: 'direct' },
|
|
339
339
|
parameters: [],
|
|
340
340
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamConfigResult', schema: autodreamConfigResultSchema, create: () => autodreamConfigResultSchema },
|
|
341
|
-
sourceLocation: { "file": "
|
|
341
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
342
342
|
},
|
|
343
343
|
{
|
|
344
344
|
id: 'sage-mem#autodream/setConfig',
|
|
@@ -350,7 +350,7 @@ export const TYPERT = {
|
|
|
350
350
|
{ name: 'patch', wire: 'patch', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamPatch', schema: autodreamPatchSchema, create: () => autodreamPatchSchema } },
|
|
351
351
|
],
|
|
352
352
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamConfigResult', schema: autodreamConfigResultSchema, create: () => autodreamConfigResultSchema },
|
|
353
|
-
sourceLocation: { "file": "
|
|
353
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
354
354
|
},
|
|
355
355
|
{
|
|
356
356
|
id: 'sage-mem#autodream/status',
|
|
@@ -360,7 +360,7 @@ export const TYPERT = {
|
|
|
360
360
|
invocation: { kind: 'direct' },
|
|
361
361
|
parameters: [],
|
|
362
362
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamStatus', schema: autodreamStatusSchema, create: () => autodreamStatusSchema },
|
|
363
|
-
sourceLocation: { "file": "
|
|
363
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
364
364
|
},
|
|
365
365
|
{
|
|
366
366
|
id: 'sage-mem#autodream/runNow',
|
|
@@ -372,7 +372,7 @@ export const TYPERT = {
|
|
|
372
372
|
{ name: 'opts', wire: 'opts', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamOpts', schema: autodreamOptsSchema, create: () => autodreamOptsSchema } },
|
|
373
373
|
],
|
|
374
374
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamRunResult', schema: autodreamRunResultSchema, create: () => autodreamRunResultSchema },
|
|
375
|
-
sourceLocation: { "file": "
|
|
375
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
id: 'sage-mem#autodream/listReports',
|
|
@@ -382,7 +382,7 @@ export const TYPERT = {
|
|
|
382
382
|
invocation: { kind: 'direct' },
|
|
383
383
|
parameters: [],
|
|
384
384
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamReportList', schema: reportListSchema, create: () => reportListSchema },
|
|
385
|
-
sourceLocation: { "file": "
|
|
385
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
388
|
id: 'sage-mem#autodream/readReport',
|
|
@@ -394,7 +394,7 @@ export const TYPERT = {
|
|
|
394
394
|
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamReportRef', schema: reportRefSchema, create: () => reportRefSchema } },
|
|
395
395
|
],
|
|
396
396
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#FileContent', schema: readResultSchema, create: () => readResultSchema },
|
|
397
|
-
sourceLocation: { "file": "
|
|
397
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
398
398
|
},
|
|
399
399
|
{
|
|
400
400
|
id: 'sage-mem#autodream/listSnapshots',
|
|
@@ -404,7 +404,7 @@ export const TYPERT = {
|
|
|
404
404
|
invocation: { kind: 'direct' },
|
|
405
405
|
parameters: [],
|
|
406
406
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamSnapshotList', schema: snapshotListSchema, create: () => snapshotListSchema },
|
|
407
|
-
sourceLocation: { "file": "
|
|
407
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
408
408
|
},
|
|
409
409
|
{
|
|
410
410
|
id: 'sage-mem#autodream/listRuns',
|
|
@@ -416,7 +416,7 @@ export const TYPERT = {
|
|
|
416
416
|
{ name: 'opts', wire: 'opts', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamOpts', schema: autodreamOptsSchema, create: () => autodreamOptsSchema } },
|
|
417
417
|
],
|
|
418
418
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamRunList', schema: runListSchema, create: () => runListSchema },
|
|
419
|
-
sourceLocation: { "file": "
|
|
419
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
420
420
|
},
|
|
421
421
|
{
|
|
422
422
|
id: 'sage-mem#autodream/readDeclaration',
|
|
@@ -428,7 +428,7 @@ export const TYPERT = {
|
|
|
428
428
|
{ name: 'opts', wire: 'opts', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamOpts', schema: autodreamOptsSchema, create: () => autodreamOptsSchema } },
|
|
429
429
|
],
|
|
430
430
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamDeclaration', schema: declarationSchema, create: () => declarationSchema },
|
|
431
|
-
sourceLocation: { "file": "
|
|
431
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
432
432
|
},
|
|
433
433
|
{
|
|
434
434
|
id: 'sage-mem#autodream/rollback',
|
|
@@ -440,7 +440,7 @@ export const TYPERT = {
|
|
|
440
440
|
{ name: 'opts', wire: 'opts', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamOpts', schema: autodreamOptsSchema, create: () => autodreamOptsSchema } },
|
|
441
441
|
],
|
|
442
442
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamRollbackResult', schema: rollbackResultSchema, create: () => rollbackResultSchema },
|
|
443
|
-
sourceLocation: { "file": "
|
|
443
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
444
444
|
},
|
|
445
445
|
{
|
|
446
446
|
id: 'sage-mem#autodream/listModels',
|
|
@@ -450,7 +450,7 @@ export const TYPERT = {
|
|
|
450
450
|
invocation: { kind: 'direct' },
|
|
451
451
|
parameters: [],
|
|
452
452
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AutodreamModelList', schema: modelListSchema, create: () => modelListSchema },
|
|
453
|
-
sourceLocation: { "file": "
|
|
453
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
454
454
|
},
|
|
455
455
|
],
|
|
456
456
|
model: {
|
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.7.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|