sage-mem 2.7.0 → 2.8.0

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.en.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.7.0`** — 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.8.0`** — 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.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.7.0`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
17
+ **当前发布版本:`2.8.0`** —— 顶部 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
@@ -678,9 +678,10 @@ const StarmapRuntime = (function (require, _react) {
678
678
  "uniform float uTime;",
679
679
  "uniform float uHoverIdx;",
680
680
  "uniform float uRevealT;",
681
+ "uniform float uFadeMs;",
681
682
  "float revealFactor(){",
682
683
  " if (vBirth < 0.0) return 1.0;",
683
- " float t = clamp((uRevealT - vBirth) / 900.0, 0.0, 1.0);",
684
+ " float t = clamp((uRevealT - vBirth) / uFadeMs, 0.0, 1.0);",
684
685
  " return t * t * (3.0 - 2.0 * t);",
685
686
  "}",
686
687
  "void main(){",
@@ -725,9 +726,10 @@ const StarmapRuntime = (function (require, _react) {
725
726
  "varying vec4 vColor;",
726
727
  "varying float vBirth;",
727
728
  "uniform float uRevealT;",
729
+ "uniform float uFadeMs;",
728
730
  "void main(){",
729
731
  " if (vBirth < 0.0){ gl_FragColor = vColor; return; }",
730
- " float t = clamp((uRevealT - vBirth) / 900.0, 0.0, 1.0);",
732
+ " float t = clamp((uRevealT - vBirth) / uFadeMs, 0.0, 1.0);",
731
733
  " t = t * t * (3.0 - 2.0 * t);",
732
734
  " gl_FragColor = vec4(vColor.rgb, vColor.a * t);",
733
735
  "}",
@@ -756,6 +758,44 @@ const StarmapRuntime = (function (require, _react) {
756
758
  const SMAP_CORNERS = [-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1];
757
759
  const SMAP_ATTR_SIZE = { aCorner: 2, aMeta0: 3, aColor: 4, aMeta1: 3, aBirth: 1 };
758
760
 
761
+ /**
762
+ * 诞生回放的**数值域**(Q15 尾巴 / 2.7.2)。
763
+ *
764
+ * 为什么不能把 epoch 毫秒直接喂给 shader:`vBirth` 是 Float32 顶点属性,`uRevealT`
765
+ * 也是 Float32 uniform —— 1.7e12 这个量级的 ulp = 2^(40-23) ≈ 131072 ms,而渐亮窗口
766
+ * 只有 900 ms。两个数各自舍入之后,`uRevealT - vBirth` 要么 ≤ 0 要么 ≫ 窗口,
767
+ * 于是 WebGL 后端里每颗星其实都是**瞬亮**;canvas2d 兜底路径用 double 不受影响,
768
+ * 所以肉眼看不出差异 —— 这正是一个藏了五版的精度问题。
769
+ *
770
+ * 修法两层:① 时间域压到**最老的那颗星**(base = 最小 mtime),birth 存相对值,
771
+ * 最大量级只剩「最老的记忆有多老」;② 窗口按跨度取 `max(900, span/40)`,
772
+ * 保证窗口 ≫ 1 ulp(跨度越大时间轴越粗,但 900ms 的固定窗口对粗时间轴毫无意义)。
773
+ *
774
+ * @param {Array<{star?:{mtimeMs?:number}}>} pts 参与绘制的可见点
775
+ * @returns {{base:number, span:number, fadeMs:number}} base=0 表示没有任何时间信息
776
+ */
777
+ function starmapRevealDomain(pts) {
778
+ const list = Array.isArray(pts) ? pts : [];
779
+ let mn = Infinity;
780
+ let mx = -Infinity;
781
+ for (const p of list) {
782
+ const m = p && p.star && typeof p.star.mtimeMs === "number" ? p.star.mtimeMs : 0;
783
+ if (m > 0) {
784
+ if (m < mn) mn = m;
785
+ if (m > mx) mx = m;
786
+ }
787
+ }
788
+ if (!isFinite(mn)) return { base: 0, span: 1, fadeMs: 900 };
789
+ const span = Math.max(mx - mn, 1);
790
+ return { base: mn, span: span, fadeMs: starmapFadeMs(span) };
791
+ }
792
+
793
+ /** 渐亮窗口:至少 900ms;跨度大时取跨度的 1/40(18s 回放走完整段,观感仍是逐颗点亮)。 */
794
+ function starmapFadeMs(span) {
795
+ const s = typeof span === "number" && isFinite(span) && span > 0 ? span : 1;
796
+ return Math.max(900, s / 40);
797
+ }
798
+
759
799
  function pushStarQuad(arr, cx, cy, r, rgb, aBase, twPhase, fresh, idx, birth) {
760
800
  for (let v = 0; v < 6; v++) {
761
801
  arr.push(SMAP_CORNERS[v * 2], SMAP_CORNERS[v * 2 + 1]);
@@ -794,8 +834,8 @@ const StarmapRuntime = (function (require, _react) {
794
834
  gl.viewport(0, 0, canvasEl.width, canvasEl.height);
795
835
  const pal = starmapPalette();
796
836
  const progBg = glProgram(gl, SMAP_BG_VS, smapBgFs(pal.bgGl[0], pal.bgGl[1]), ["aPos"], ["uRes", "uTime"]);
797
- const progStar = glProgram(gl, SMAP_STAR_VS, SMAP_STAR_FS, ["aCorner", "aMeta0", "aColor", "aMeta1", "aBirth"], ["uRes", "uTime", "uHoverIdx", "uRevealT"]);
798
- const progLine = glProgram(gl, SMAP_LINE_VS, SMAP_LINE_FS, ["aPos", "aColor", "aBirth"], ["uRes", "uRevealT"]);
837
+ const progStar = glProgram(gl, SMAP_STAR_VS, SMAP_STAR_FS, ["aCorner", "aMeta0", "aColor", "aMeta1", "aBirth"], ["uRes", "uTime", "uHoverIdx", "uRevealT", "uFadeMs"]);
838
+ const progLine = glProgram(gl, SMAP_LINE_VS, SMAP_LINE_FS, ["aPos", "aColor", "aBirth"], ["uRes", "uRevealT", "uFadeMs"]);
799
839
 
800
840
  // 背景全屏两三角(clip space 直给)
801
841
  const bgQuadBuf = gl.createBuffer();
@@ -825,17 +865,22 @@ const StarmapRuntime = (function (require, _react) {
825
865
  // 脏标记:canvas 晚于 visibility effect 挂载时(JSX 分支决定 ref 时序),
826
866
  // setGeometry 可能从未被调过——render 前自动补传,顺序无关。
827
867
  let geoDirty = true;
868
+ // 诞生回放的时间域(base / span / 渐亮窗口):由 uploadStars 现算,
869
+ // 顶点里的 birth、uniform 的 uRevealT 与 uFadeMs 三处必须共用同一份。
870
+ let revealDomain = { base: 0, span: 1, fadeMs: 900 };
828
871
 
829
872
  function uploadStars() {
830
873
  const arr = [];
831
874
  const nowMs = Date.now();
875
+ revealDomain = starmapRevealDomain(layout.pts.filter(function (p, i) { return !!visBox.flags[i]; }));
832
876
  for (let i = 0; i < layout.pts.length; i++) {
833
877
  if (!visBox.flags[i]) continue;
834
878
  const p = layout.pts[i];
835
879
  const meta = KIND_META[p.star.kind] || KIND_META.special;
836
880
  const arch = p.star.archived === true;
837
881
  const fresh = p.star.mtimeMs > 0 && nowMs - p.star.mtimeMs < FRESH_MS ? 1 : 0;
838
- const birth = p.star.mtimeMs > 0 ? p.star.mtimeMs : 0;
882
+ // -1 在着色器里是「没有诞生时刻,永远全亮」(尘星也是这条分支)。
883
+ const birth = p.star.mtimeMs > 0 ? p.star.mtimeMs - revealDomain.base : -1;
839
884
  if (arch) {
840
885
  // 归档星 = 暗星,两层画:
841
886
  // ① 一层很淡的冷色底盘(走 dust 分支:不带星芒、不参与 hover),
@@ -864,7 +909,8 @@ const StarmapRuntime = (function (require, _react) {
864
909
  const hot = hoverLineIdx >= 0 && (ln[0] === hoverLineIdx || ln[1] === hoverLineIdx);
865
910
  const mul = hot ? 1 : 0.72;
866
911
  const al = hot ? 0.6 : 0.22;
867
- const birth = Math.max(a.star.mtimeMs || 0, b.star.mtimeMs || 0);
912
+ const birthAbs = Math.max(a.star.mtimeMs || 0, b.star.mtimeMs || 0);
913
+ const birth = birthAbs > 0 ? birthAbs - revealDomain.base : -1;
868
914
  arr.push(a.x, a.y, c[0] * mul, c[1] * mul, c[2] * mul, al, birth);
869
915
  arr.push(b.x, b.y, c[0] * mul, c[1] * mul, c[2] * mul, al, birth);
870
916
  }
@@ -878,10 +924,12 @@ const StarmapRuntime = (function (require, _react) {
878
924
  gl.drawArrays(gl.TRIANGLES, 0, count);
879
925
  }
880
926
 
881
- let revealT = null; // null = 全亮;数字 = 诞生回放的当前时刻
927
+ let revealT = null; // null = 全亮;数字 = 诞生回放的当前时刻(墙钟 ms)
882
928
  function render(phase) {
883
929
  if (geoDirty) { uploadStars(); uploadLines(); geoDirty = false; }
884
- const rt = revealT === null ? 8.7e15 : revealT;
930
+ // 全亮用一个足够大的哨兵(1e30 在 Float32 里仍可表示,且远大于任何跨度);
931
+ // 回放时刻换算到与顶点里的 birth 同一个时间域 —— 两边的差才是精度问题的所在。
932
+ const rt = revealT === null ? 1e30 : revealT - revealDomain.base;
885
933
  gl.viewport(0, 0, canvasEl.width, canvasEl.height);
886
934
  // 背景:不透明
887
935
  gl.disable(gl.BLEND);
@@ -899,6 +947,7 @@ const StarmapRuntime = (function (require, _react) {
899
947
  gl.useProgram(progLine.prog);
900
948
  gl.uniform2f(progLine.uni.uRes, W, H);
901
949
  gl.uniform1f(progLine.uni.uRevealT, rt);
950
+ gl.uniform1f(progLine.uni.uFadeMs, revealDomain.fadeMs);
902
951
  gl.bindBuffer(gl.ARRAY_BUFFER, lineBuf);
903
952
  if (progLine.attr.aPos >= 0) { gl.enableVertexAttribArray(progLine.attr.aPos); gl.vertexAttribPointer(progLine.attr.aPos, 2, gl.FLOAT, false, 28, 0); }
904
953
  if (progLine.attr.aColor >= 0) { gl.enableVertexAttribArray(progLine.attr.aColor); gl.vertexAttribPointer(progLine.attr.aColor, 4, gl.FLOAT, false, 28, 8); }
@@ -910,6 +959,7 @@ const StarmapRuntime = (function (require, _react) {
910
959
  gl.uniform1f(progStar.uni.uTime, phase);
911
960
  gl.uniform1f(progStar.uni.uHoverIdx, hoverBox.idx);
912
961
  gl.uniform1f(progStar.uni.uRevealT, rt);
962
+ gl.uniform1f(progStar.uni.uFadeMs, revealDomain.fadeMs);
913
963
  drawQuad(progStar, dustBuf, dustCount);
914
964
  if (starCount > 0) drawQuad(progStar, starBuf, starCount);
915
965
  }
@@ -948,6 +998,10 @@ const StarmapRuntime = (function (require, _react) {
948
998
  canvasEl.width = W * SCALE;
949
999
  canvasEl.height = H * SCALE;
950
1000
  const nowMs = Date.now();
1001
+ // 与 WebGL 路径共用同一份时间域(只取 fadeMs 用;这里都是 double,不存在精度问题,
1002
+ // 但窗口长度必须一致 —— 否则两条路径的点亮节奏对不上,真机上看就是两套动画)。
1003
+ const domain = starmapRevealDomain(layout.pts.filter(function (p, i) { return !!visBox.flags[i]; }));
1004
+ const fadeMs = domain.fadeMs;
951
1005
  function render(phase) {
952
1006
  const c2d = canvasEl.getContext("2d");
953
1007
  if (!c2d) return;
@@ -983,7 +1037,7 @@ const StarmapRuntime = (function (require, _react) {
983
1037
  const birth = Math.max(a.star.mtimeMs || 0, b.star.mtimeMs || 0);
984
1038
  let lrv = 1;
985
1039
  if (rt !== null) {
986
- const lt = Math.max(0, Math.min(1, (rt - birth) / 900));
1040
+ const lt = Math.max(0, Math.min(1, (rt - birth) / fadeMs));
987
1041
  lrv = lt * lt * (3 - 2 * lt);
988
1042
  if (lrv <= 0) continue;
989
1043
  }
@@ -1000,7 +1054,7 @@ const StarmapRuntime = (function (require, _react) {
1000
1054
  const meta = KIND_META[p.star.kind] || KIND_META.special;
1001
1055
  let rv = 1;
1002
1056
  if (rt !== null && p.star.mtimeMs > 0) {
1003
- const st = Math.max(0, Math.min(1, (rt - p.star.mtimeMs) / 900));
1057
+ const st = Math.max(0, Math.min(1, (rt - p.star.mtimeMs) / fadeMs));
1004
1058
  rv = st * st * (3 - 2 * st);
1005
1059
  if (rv <= 0) continue;
1006
1060
  }
@@ -2623,6 +2677,10 @@ const StarmapRuntime = (function (require, _react) {
2623
2677
  ".smem-num:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
2624
2678
  ".smem-status{font-size:12px;color:var(--dsw-alias-label-secondary);}",
2625
2679
  ".smem-autodream-note{font-size:11.5px;color:var(--dsw-alias-label-secondary);line-height:1.5;}",
2680
+ // 2.7.1(Q04):上限设置的诊断清单 —— 逐条列宿主的原话,缩进浅一点、行距紧一点。
2681
+ ".smem-diag-list{margin:4px 0 4px 16px;padding:0;font-size:11.5px;line-height:1.6;color:var(--dsw-alias-label-secondary);}",
2682
+ // 2.8.0(子槽):第三方向 settings.memory.starmap 注入的视图挂在这里,空的时候不占位置。
2683
+ ".smem-extra-slot{margin-top:14px;padding-top:12px;border-top:0.5px solid var(--dsw-alias-border-l2);}",
2626
2684
  ".smem-autodream-list{display:flex;flex-direction:column;gap:6px;}",
2627
2685
  ".smem-report{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 9px;font-size:12px;cursor:pointer;background:transparent;color:inherit;text-align:left;font-family:inherit;}",
2628
2686
  ".smem-report:hover{border-color:var(--dsw-alias-border-l2);}",
@@ -3916,6 +3974,15 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3916
3974
  ? h("div", { className: "smem-empty", key: "empty" },
3917
3975
  "按当前阈值没有可归档的 —— 要么闲置天数还没到该类型的阈值,要么台账里还没有访问记录。")
3918
3976
  : h("div", { className: "smem-cand-list", key: "list" }, [
3977
+ // 2.7.1(Q05 尾巴):锁表读不出来时**不许装作「都没锁」**。
3978
+ // 宿主 2.1.12 起就在 archiveCandidates 的结果里带 lockedUnknown / lockedError,
3979
+ // 但那两个字段一直只在日志里 —— 面板照样把整张清单画成「全部未锁定」。
3980
+ // 这里如实标出来:自动归档会直接拒绝,手动归档要不要继续由人自己判断。
3981
+ c.lockedUnknown === true
3982
+ ? h("div", { className: "smem-cand-note smem-ad-warn", key: "lockunknown", role: "status" },
3983
+ "⚠ 锁表读不出来" + (c.lockedError ? "(" + String(c.lockedError) + ")" : "") +
3984
+ ":下面每一条的锁定状态都是「未知」(不是「都没锁」)。自动归档会直接拒绝执行;手动归档前请自己确认。")
3985
+ : null,
3919
3986
  h("div", { className: "smem-cand-note", key: "note" },
3920
3987
  "候选只按「多久没被注入过」算。归档 = 移进 archive/(不删),随时可以恢复。"),
3921
3988
  offNow
@@ -4203,6 +4270,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4203
4270
  "entry.sidebarUnsupported": "当前宿主不支持侧栏入口。",
4204
4271
  "entry.degraded": "当前宿主不支持侧栏入口,已临时按「只显示设置页」显示。",
4205
4272
  "entry.legacyHost": "宿主模块还是旧版本,重启 DSH 后可选「只显示侧栏」。",
4273
+ "entry.sidebarHidesSlot": "选这一档后,第三方通过「记忆星图」子槽注入的视图也会一起藏掉 —— 那一块挂在设置页上。",
4206
4274
  "entry.notice.saved": "界面入口已更新。",
4207
4275
  "entry.err.noSetSettings": "宿主侧还没有 setSettings 接口(插件版本较旧)",
4208
4276
  "entry.err.saveFailed": "保存失败(未知原因)",
@@ -4404,6 +4472,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4404
4472
  "ad.limits.pending": "有改动待生效",
4405
4473
  "ad.limits.restartWarn": "⚠ 这些值在 DSH 启动时就固化了 —— 要重启 DSH 才生效。下面每行都写清「当前 → 重启后」。",
4406
4474
  "ad.limits.restartNote": "这些值在 DSH 启动时固化:保存后要重启 DSH 才生效 —— 所以每行都会标出「当前生效」与「重启后」。",
4475
+ "ad.limits.diagTitle": "⚠ 上限设置里有 {count} 条问题(本次仍按环境变量 / 默认值生效)",
4476
+ "ad.limits.diagNote": "下面是宿主的原话。改好面板里的值只能修一部分 —— 环境变量与 settings.json 里的坏值也要一并清掉。",
4407
4477
  "ad.limit.maxResults": "一次注入条数",
4408
4478
  "ad.limit.maxChars": "单条字符上限",
4409
4479
  "ad.limit.maxBaseline": "baseline 名额",
@@ -4710,6 +4780,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4710
4780
  "entry.sidebarUnsupported": "This host does not support the sidebar entry.",
4711
4781
  "entry.degraded": "The sidebar entry is unavailable on this host, so the Settings entry is shown instead.",
4712
4782
  "entry.legacyHost": "The host module is still the old version; restart DSH to choose Sidebar only.",
4783
+ "entry.sidebarHidesSlot": "With this mode, views that third-party plugins inject into the “memory starmap” sub-slot disappear too — they live on the Settings page.",
4713
4784
  "entry.notice.saved": "Interface entries updated.",
4714
4785
  "entry.err.noSetSettings": "The host has no setSettings API yet (older plugin version)",
4715
4786
  "entry.err.saveFailed": "Save failed (unknown reason)",
@@ -4909,6 +4980,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4909
4980
  "ad.limits.pending": "changes pending",
4910
4981
  "ad.limits.restartWarn": "⚠ These values are fixed at DSH startup — restart DSH for them to apply. Each row below shows “current → after restart”.",
4911
4982
  "ad.limits.restartNote": "These values are fixed at DSH startup: saving requires a DSH restart to take effect — so each row shows both “currently effective” and “after restart”.",
4983
+ "ad.limits.diagTitle": "⚠ {count} problem(s) with these limits (environment variables / defaults are in effect)",
4984
+ "ad.limits.diagNote": "The lines below are the host's own words. Fixing the values in this panel only covers part of it — bad values in environment variables and settings.json must be cleared too.",
4912
4985
  "ad.limit.maxResults": "Memories per injection",
4913
4986
  "ad.limit.maxChars": "Characters per memory",
4914
4987
  "ad.limit.maxBaseline": "baseline slots",
@@ -5513,6 +5586,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5513
5586
  const fixedReserved = ((memSet && memSet.reserved) || []).filter(function (n) {
5514
5587
  return extraLower.indexOf(String(n).toLowerCase()) < 0;
5515
5588
  });
5589
+ // 2.7.1(Q04 尾巴):宿主从 2.1.11 起就在 getSettings 里带 `limitDiagnostics`
5590
+ // —— 坏 JSON / 逐项越界 / 环境变量写错都在这份数组里。以前它只进日志,
5591
+ // 面板上看不见,于是「我改的值怎么没生效」只能靠猜。现在就地摊开逐条列。
5592
+ const limitDiag = Array.isArray(memSet && memSet.limitDiagnostics) ? memSet.limitDiagnostics : [];
5516
5593
  // 入口模式:新字段优先,旧 sidebarEntry 兜底;坏值 / 旧宿主一律按「两个都显示」。
5517
5594
  // 存了 sidebar 但当前宿主不支持侧栏时,界面按 settings 显示并给一条降级提示。
5518
5595
  const entryModeValue = entryModeFromSettings(memSet);
@@ -5981,6 +6058,12 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5981
6058
  ? h("div", { className: "smem-autodream-note smem-ad-warn", key: "deg" },
5982
6059
  t(entryApiModern === false ? "entry.legacyHost" : "entry.degraded"))
5983
6060
  : null,
6061
+ // 2.8.0:子槽接上渲染之后,「只显示侧栏」会连带把别人注入进来的视图一起藏掉
6062
+ // (设置页 section 一注销,挂在它下面的 `settings.memory.starmap` 就没了)。
6063
+ // 这不是 bug,但选这一档的人得先知道 —— 所以只在选中的时候写一句。
6064
+ entryModeValue === "sidebar" && !entryIsDegraded
6065
+ ? h("div", { className: "smem-autodream-note", key: "slotloss" }, t("entry.sidebarHidesSlot"))
6066
+ : null,
5984
6067
  entryErr ? h("div", { className: "smem-autodream-note smem-ad-warn", key: "err" }, entryErr) : null,
5985
6068
  ]),
5986
6069
  // 自动归档那一段(三档策略 + 三个阈值)**已挪到「自动归档」页签**(v0.9.9):
@@ -5996,6 +6079,17 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5996
6079
  memSet && memSet.restartRequired
5997
6080
  ? h("div", { className: "smem-autodream-note smem-ad-warn", key: "rw" }, t("ad.limits.restartWarn"))
5998
6081
  : h("div", { className: "smem-autodream-note", key: "rn" }, t("ad.limits.restartNote")),
6082
+ // 2.7.1(Q04):诊断逐条列出来。文案不猜原因 —— 数组里每一条都是宿主的原话
6083
+ // (「读 settings.json 失败(ENOENT)」「maxResults 必须是 1..20 的整数」…)。
6084
+ limitDiag.length
6085
+ ? h("div", { className: "smem-autodream-note smem-ad-warn", key: "diag", role: "status" }, [
6086
+ h("div", { key: "t", style: { fontWeight: "600" } }, t("ad.limits.diagTitle", { count: limitDiag.length })),
6087
+ h("ul", { key: "ul", className: "smem-diag-list" }, limitDiag.map(function (d, i) {
6088
+ return h("li", { key: "diag-" + i }, String(d));
6089
+ })),
6090
+ h("div", { key: "n", className: "smem-status" }, t("ad.limits.diagNote")),
6091
+ ])
6092
+ : null,
5999
6093
  // 五个参数排两列(一行两个):同样的信息,行数少一半,扫读反而更清楚。
6000
6094
  h("div", { className: "smem-ad-grid2", key: "grid" }, LIMIT_FIELDS.map(function (f) {
6001
6095
  const cur = memSet && memSet.limits ? memSet.limits[f.key] : null;
@@ -6613,6 +6707,30 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
6613
6707
  ]);
6614
6708
  }
6615
6709
 
6710
+ /**
6711
+ * 渲染 `settings.memory.starmap` 子槽(2.8.0)。
6712
+ *
6713
+ * 1.8.0 起这个子槽就**只声明不渲染**(`Section` 从没调过 `renderSlot`):
6714
+ * 第三方按契约往里面注入视图,一个像素都不会出现。接上之后:
6715
+ * - `renderSlot` 是宿主经 props 传进来的(声明了 children 才有),旧外壳可能没有 → 判类型;
6716
+ * - 返回 null / false 表示没有占用者 —— 此时**不占版面**(空 div 也会破坏间距);
6717
+ * - 出任何异常都吞掉:这是个可选面,绝不能让第三方把整个设置页拖垮。
6718
+ * ⚠️ 连带后果(2.7.0 的入口三选一带来的):「只显示侧栏」会把设置页 section 注销,
6719
+ * 于是注入到这里的内容也跟着消失 —— 所以那一档的文案里点明了一句(`entry.sidebarHidesSlot`)。
6720
+ */
6721
+ function renderMemoryExtraSlot(props) {
6722
+ if (!props || typeof props.renderSlot !== "function") return null;
6723
+ try {
6724
+ // owner 传 `{}`:官方对 root scope 的子槽就是这么调的(`renderSlot("shell.overlay", {})`、
6725
+ // `renderSlot("main", {}, …)`)—— 少传一个参数在对 scope 做检查的宿主上可能直接抛。
6726
+ const node = props.renderSlot("settings.memory.starmap", {});
6727
+ if (node === null || node === undefined || node === false) return null;
6728
+ return h("div", { className: "smem-extra-slot", key: "extra-slot" }, node);
6729
+ } catch (e) {
6730
+ return null;
6731
+ }
6732
+ }
6733
+
6616
6734
  function Section(props) {
6617
6735
  // 走 stableRemote:真实宿主里 `ctx.get()` 每次返回新对象(issue #3 的成因)。
6618
6736
  // 这个面板的 effect 依赖是 `[]`、暂时不会爆,但引用稳定是**所有**面板的共同前提。
@@ -8075,6 +8193,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
8075
8193
  h(AuditPanel, { key: "audit", report: audit, busy: auditBusy, onRun: runAudit, standalone: true }),
8076
8194
  ])
8077
8195
  : h("div", { className: "smem-view", key: "files" }, filesView),
8196
+ // 2.8.0:第三方注入的视图(`settings.memory.starmap` 子槽)。没有占用者时是 null,
8197
+ // 一个 div 都不出 —— 所以默认安装下界面与 2.7.0 逐像素相同。
8198
+ renderMemoryExtraSlot(props),
8078
8199
  // 2.6.0:复制成功的回执铺在视口下方中央(`position:fixed`,不吃点击,10s 自收,
8079
8200
  // 也能自己点 × 关)。渲染在 `.smem-root` 直属层 —— 别塞进带进场动画的
8080
8201
  // `.smem-view`(那个祖先有 transform,`fixed` 会被改写定位)。
@@ -8432,9 +8553,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
8432
8553
  order: 20,
8433
8554
  label: function () { return t("section.label"); },
8434
8555
  // 声明「记忆星图」子槽(沿用 1.8.0 的契约):sage-starmap 之类的
8435
- // 第三方插件向它注入视图。⚠️ 现状:Section 还没有调用 renderSlot,
8436
- // 所以这个子槽目前只声明、不渲染(2.7.0 未改;渲染它是独立事项)。
8437
- // 保留声明是为了不破坏第三方插件的注入面。
8556
+ // 第三方插件向它注入视图。2.8.0 起 `Section` 真的调 `renderSlot` 了
8557
+ // (之前只声明、不渲染 —— 注进来的东西一个像素都不出现)。
8558
+ // ⚠️ 连带后果:注销这个 section 时子槽跟着没(「只显示侧栏」那一档),
8559
+ // 界面文案里已点明(`entry.sidebarHidesSlot`)。
8438
8560
  children: { "settings.memory.starmap": { kind: "list", scope: "root" } },
8439
8561
  }, function (props) {
8440
8562
  return react.createElement(Section, { ...props, ctx: ctx });
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.7.0",
4
+ "version": "2.8.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "icon": "./icon.png",