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 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 分钟查一次门控),**没有**接进设置页那个「自动化任务」面板。原因不是偷懒:那个面板的服务到期只做一件事——往一个已存在的会话投一条 user 消息让那个 agent 正常跑一轮;它没有回调、没有条件门、也要求绑定一个会话 id。而自动做梦刻意**不走会话**(工具集由插件给、步数有上限、不污染会话记录),两者的触发模型对不上。硬接的代价远大于收益,所以不做。
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 按规则判断,不是"你说了什么都记")。
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * **纯只读**:不修任何东西。修哪条、怎么修,由 autodream agent 或人决定。
14
14
  *
15
- * 这一份解析是 sage-mem host 半 parseFrontmatter 的同源精简版,故意不共用:
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 解析,读法与 host 半一致:
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 — 客户端半:DSH 设置页「记忆管理」界面(文件管理器)。
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(host 半 MemoryGateway),通过
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": "workspace/sage-starmap/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-starmap/lib/index.js", "line": 1, "column": 1 },
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
- // 配色(2026-09-27 重做):原来五个色都是高饱和高亮度(#ffd27d / #6fe3ff /
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
- // 2026-09-27(B 方案「自洽夜空」):星图整块含 header 与图例,同属一张深色画布。
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
- // 底色(2026-09-27 重做)。旧版是四层 fbm 噪声算出来的蓝紫星云,还带 uTime 动画 ——
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
- // 星点(2026-09-27 重做)。去掉了三样东西,它们就是「塑料感」的来源:
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
- // ── 按需重绘(2026-09-27 重做)──
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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 不可用」,而 host 半日志明确写着
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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 给人看);「手动触发跳过门控」由 host 半决定。
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
1543
+ sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
1544
1544
  },
1545
- // ── 2026-10-01 新增:运行记录 / 整理声明 / 回滚 / 模型目录 ──
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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
- // ── 2026-10-01 新增:回滚点区 / 整理声明区 ──
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
- // 2026-09-25 P0 数据事故的根因全在这几行里,三条硬规矩:
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
- /** 去掉值两侧成对的引号(与 host 半 parseFrontmatter 的剥法一致)。 */
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
- * 优先级与 host 半 parseFrontmatter 一致(顶层优先)。找不到返回 ""。
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 的值。三种写法都认,与 host 半 parseTags 保持同一套规则:
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
- * 「自动做梦」面板 —— 让记忆自己整理一遍(引擎在 host 半 lib/autodream.js)。
2045
+ * 「自动做梦」面板 —— 让记忆自己整理一遍(引擎在 宿主侧 lib/autodream.js)。
2046
2046
  *
2047
- * 数据全走 remote.autodream(host 半的 AutodreamGateway)。两条实现约定:
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
- // ── 2026-10-01 新增:模型目录 / 回滚点 / 整理记录 / 整理声明 ──
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 不可用 —— 插件的 host 半可能没加载成功,看 DSH 启动日志"); return undefined; }
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
- * 这里还有第三种失败:host 半是旧版本、方法根本不在 remote 对象上
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("host 半没有 " + name + " 接口(版本较旧?)"); return; }
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
- // 由 host 半负责(手动点了按钮就不该被「还没到 N 小时」挡回来)。
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
- // (2026-09-27 截图走查时发现:「+ 添加记忆」出现在星图页上)。
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 content = await readFile(join(MEMORY_DIR, name), 'utf8')
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
- // dsh-agent-loop/lib/index.js:906 const claimed = this.inbox.claim(...) // 只从 pending 队列取走
508
- // dsh-agent-loop/lib/index.js:907 await this.loopCtx.systemPrompt.assemble(...) // ← 本监听器在这里跑
509
- // dsh-agent-loop/lib/index.js:1046 this.session.append("user/message", ...) // 落 session 在 preStep 之后
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
- const parts = picked.map(f => {
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
- return `### ${f.file}\n${body}`
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${parts.join('\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
- * 记忆星图(host 半)—— 只读扫描记忆目录,把每条记忆解析成一颗「星」。
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
  }
@@ -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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
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.0",
4
+ "version": "0.7.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {