sage-mem 2.1.24 → 2.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  **Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.**
14
14
 
15
- **Current release: `2.1.24`** — the npm badge above is the dynamic source of truth. **Contract baseline: `1.0.0`** — the first stable release, whose public commitments, the capability boundaries and the list of things it explicitly cannot do are frozen in the
15
+ **Current release: `2.1.25`** — 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.24`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
17
+ **当前发布版本:`2.1.25`** —— 顶部 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/client.js CHANGED
@@ -31,6 +31,10 @@ window.__ModuleLoader__.load({
31
31
  const isNum = (v) => { if (typeof v !== "number") throw new Error("expected number"); };
32
32
  function parseSchema(shape) {
33
33
  return {
34
+ // 字段清单**暴露出来**:`test/client-exec-test.mjs` 第 3.1 组拿它跟宿主 zod 的
35
+ // shape 逐条对表。没有这一项,「strict」只意味着「不查多余键」——
36
+ // 宿主加了字段、客户端没声明这种漂移它挡不住(2026-11-03 实测已漂三处)。
37
+ fields: Object.keys(shape),
34
38
  parse(value) {
35
39
  if (value === null || typeof value !== "object") throw new Error("sage-mem: invalid payload");
36
40
  for (const [key, check] of Object.entries(shape)) {
@@ -1650,13 +1654,21 @@ const StarmapRuntime = (function (require, _react) {
1650
1654
 
1651
1655
  // ── 手写 strict codec(浏览器无 zod,typert 只要求 parse 方法)──
1652
1656
  // 基元(parseSchema / isStr / isBool / isNum / strSchema)在 05-shared.js。
1653
- const fileSchema = parseSchema({ "file": isStr, "type": isStr, "description": isStr, "size": isNum });
1654
- const listSchema = { parse(v) { if (!Array.isArray(v)) throw new Error("expected array"); v.forEach((f) => fileSchema.parse(f)); return v; } };
1657
+ const isStrArr = (v) => { if (!Array.isArray(v)) throw new Error("expected string array"); for (const x of v) isStr(x); };
1658
+ // 字段必须与宿主 `lib/typert.host.js` 的 fileSchema **同宽**(strict 会剥掉未声明字段):
1659
+ // 少了 tags / locked,列表页的「按标签筛」与「锁定」按钮就永远拿到 undefined。
1660
+ const fileSchema = parseSchema({ "file": isStr, "type": isStr, "description": isStr, "size": isNum, "tags": isStrArr, "locked": isBool });
1661
+ const listSchema = { fields: fileSchema.fields, parse(v) { if (!Array.isArray(v)) throw new Error("expected array"); v.forEach((f) => fileSchema.parse(f)); return v; } };
1655
1662
  const readSchema = withOptional(parseSchema({ "name": isStr, "content": isStr }), ["revision"]);
1656
- const writeSchema = withOptional(parseSchema({ "ok": isBool, "file": isStr }), ["code", "revision"]);
1657
- const deleteSchema = parseSchema({ "ok": isBool });
1658
- const starmapStarSchema = parseSchema({ "file": isStr, "kind": isStr, "title": isStr, "desc": isStr, "bytes": isNum, "mtimeMs": isNum });
1659
- const starmapListSchema = { parse(v) { if (!v || typeof v !== "object") throw new Error("expected object"); if (!Array.isArray(v.stars)) throw new Error("expected stars array"); v.stars.forEach((f) => starmapStarSchema.parse(f)); return v; } };
1663
+ const writeSchema = optionalFields(parseSchema({ "ok": isBool, "file": isStr }), { "error": isStr, "code": isStr, "revision": isStr });
1664
+ const deleteSchema = optionalFields(parseSchema({ "ok": isBool }), { "error": isStr });
1665
+ // `archived` 每颗星都有(活动星是 false);archivedAt / archivedReason 只对归档星出现,
1666
+ // 且旧版 autodream 归档的文件没有它们 —— 所以是 optional(与宿主 manifest 一致)。
1667
+ const starmapStarSchema = withOptional(
1668
+ parseSchema({ "file": isStr, "kind": isStr, "title": isStr, "desc": isStr, "bytes": isNum, "mtimeMs": isNum, "archived": isBool }),
1669
+ ["archivedAt", "archivedReason"],
1670
+ );
1671
+ const starmapListSchema = { fields: ["count", "stars"], elementFields: starmapStarSchema.fields, parse(v) { if (!v || typeof v !== "object") throw new Error("expected object"); if (!Array.isArray(v.stars)) throw new Error("expected stars array"); v.stars.forEach((f) => starmapStarSchema.parse(f)); return v; } };
1660
1672
  const starmapReadSchema = parseSchema({ "name": isStr, "content": isStr });
1661
1673
 
1662
1674
  // ── 归档区(v0.9.0 档案馆)──
@@ -1664,15 +1676,24 @@ const StarmapRuntime = (function (require, _react) {
1664
1676
  // 结果 codec 与宿主 manifest 逐字段对齐(ArchiveResult 的 file / error 都可选,
1665
1677
  // 成功回 file、失败回 error,所以这里只硬校 ok)。
1666
1678
  const boolSchema = { parse(v) { isBool(v); return v; } };
1667
- const archiveSchema = parseSchema({ "ok": isBool });
1679
+ // 归档结果(宿主的 archiveResultSchema):**除 ok 外全部 optional**。失败分支没有 file,
1680
+ // 把它当必填会把「失败」判成坏结果 —— 宿主的 typert.host.js 有同一条注释。
1681
+ // `unindexed` / `indexed` 是 boolean,所以不能套 withOptional(那个只做 isStr)。
1682
+ const archiveResultFields = { "file": isStr, "error": isStr, "archivedReason": isStr, "unindexed": isBool, "indexed": isBool, "indexError": isStr };
1683
+ const archiveResultSchema = optionalFields(parseSchema({ "ok": isBool }), archiveResultFields);
1684
+ // archive / restore / deleteArchived / writeArchived 四个 remote 共用这一个形状。
1685
+ const archiveSchema = archiveResultSchema;
1668
1686
  const archivedFileSchema = parseSchema({
1669
1687
  "file": isStr, "type": isStr, "description": isStr, "size": isNum,
1688
+ "tags": isStrArr,
1670
1689
  "archivedAt": isStr, "archivedReason": isStr,
1671
1690
  // 2.1.6(外部审查 Q07):内容身份(归档代次 + 正文 revision)。它不是装饰 ——
1672
1691
  // 「彻底删除」必须把它原样交回宿主,否则只能按名字删,而名字不是身份。
1673
1692
  "revision": isStr,
1674
1693
  });
1675
1694
  const archivedListSchema = {
1695
+ fields: ["count", "files"],
1696
+ elementFields: archivedFileSchema.fields,
1676
1697
  parse(v) {
1677
1698
  if (!v || typeof v !== "object") throw new Error("sage-mem: expected object");
1678
1699
  if (!Array.isArray(v.files)) throw new Error("sage-mem: expected files array");
@@ -1682,7 +1703,9 @@ const StarmapRuntime = (function (require, _react) {
1682
1703
  };
1683
1704
  // 归档文件的读 / 写(B1b):形状与活跃记忆那套的 readSchema / writeSchema 对称。
1684
1705
  const archivedReadSchema = withOptional(parseSchema({ "name": isStr, "content": isStr }), ["revision"]);
1685
- const archivedWriteSchema = withOptional(parseSchema({ "ok": isBool, "file": isStr }), ["code", "revision"]);
1706
+ // 宿主 `archivedWriteResultSchema` 与 `writeResultSchema` **同形、只是类型名不同**
1707
+ // (typert.host.js 的注释明说这一点)——所以直接复用 writeSchema 的字段面。
1708
+ const archivedWriteSchema = writeSchema;
1686
1709
  // 「彻底删除」的入参:`archivedAt`(展示时间)+ `revision`(内容身份,见 archive.js 的
1687
1710
  // archivedRevisionOf)。两个都不是装饰 —— 宿主拿它们核对「要删的是不是用户确认过的那一版」;
1688
1711
  // 只有前者的旧确认在 2.1.6 起会被宿主拒绝(分钟精度的时间不是内容身份)。
@@ -1695,15 +1718,22 @@ const StarmapRuntime = (function (require, _react) {
1695
1718
  * 混合版本环境里「旧宿主 + 新面板」必须还能用,所以这里不能当必填字段拒掉;
1696
1719
  * 但字段给了就必须是字符串 —— 否则面板拿到一个数字或对象会静默走错分支。
1697
1720
  */
1698
- function withOptional(base, keys) {
1721
+ function optionalFields(base, spec) {
1699
1722
  return {
1723
+ // 可选字段并入字段集:对表时宿主 zod 的 optional 字段也在它的 shape 里,
1724
+ // 两边必须一样宽,否则「客户端没声明 optional 字段」会被误报成漂移。
1725
+ fields: (base.fields || []).concat(Object.keys(spec)),
1700
1726
  parse(v) {
1701
1727
  base.parse(v);
1702
- for (let i = 0; i < keys.length; i++) if (keys[i] in v) isStr(v[keys[i]]);
1728
+ for (const [k, check] of Object.entries(spec)) if (k in v) check(v[k]);
1703
1729
  return v;
1704
1730
  },
1705
1731
  };
1706
1732
  }
1733
+ /** 可选字段全是字符串时的简写(读 revision / 写 code / revision)。 */
1734
+ function withOptional(base, keys) {
1735
+ return optionalFields(base, Object.fromEntries(keys.map((k) => [k, isStr])));
1736
+ }
1707
1737
  /**
1708
1738
  * 写协议的第三参(`{ expectedRevision?, create? }`)——**两个字段都可选**。
1709
1739
  * 不传 = 旧调用方,行为与从前一致(宿主会打一条 warn);传了就必须是正确类型。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sage-mem",
3
3
  "description": "Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.",
4
- "version": "2.1.24",
4
+ "version": "2.1.25",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "icon": "./icon.png",