sage-mem 2.1.19 → 2.1.20

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.19`** — 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.20`** — 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.19`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
17
+ **当前发布版本:`2.1.20`** —— 顶部 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
@@ -978,6 +978,22 @@ const StarmapRuntime = (function (require, _react) {
978
978
  return makeCanvas2DRenderer(canvasEl, layout, visBox, hoverBox);
979
979
  }
980
980
 
981
+ /**
982
+ * 星图动画的统一时钟(Q15)。
983
+ *
984
+ * rAF 回调的时间戳与 `performance.now()` 是**同一个原点**(单调、从页面加载起算),
985
+ * 而 `Date.now()` 是 epoch 毫秒 —— 两套混用会让相位变成大负数。旧版入场动画就是这个
986
+ * bug:`t0 = Date.now()` 去减 rAF 的 `ts`,`ts - t0` 恒为大负数 → 永远「入场没结束」,
987
+ * 但每帧画完又不接着排帧,于是入场只画了一帧、相位还是负的。
988
+ *
989
+ * 星图里所有「过了多少毫秒」都走这里(freshness 那种墙钟判断除外,那本来就该用 Date.now)。
990
+ */
991
+ function starmapNow() {
992
+ return (typeof performance !== "undefined" && performance && typeof performance.now === "function")
993
+ ? performance.now()
994
+ : Date.now();
995
+ }
996
+
981
997
  // ── 星图主体(canvas + 图例 + 详情面板)──
982
998
  function StarMap(props) {
983
999
  const ctx = props.ctx || appCtx;
@@ -1108,16 +1124,18 @@ const StarmapRuntime = (function (require, _react) {
1108
1124
  if (!data || !layout || !canvasEl) return undefined;
1109
1125
  const renderer = createRenderer(canvasEl, layout, visBox, hoverBox);
1110
1126
  drawBox.renderer = renderer;
1111
- drawBox.t0 = Date.now();
1127
+ // t0 与 rAF 的 ts 必须同一时钟(见 starmapNow)。旧版用 Date.now 当 t0,
1128
+ // 于是相位是 epoch 级的大负数,入场窗口永远关不上。
1129
+ drawBox.t0 = starmapNow();
1112
1130
  renderer.render(0);
1113
1131
 
1114
1132
  // ── 按需重绘(重做过一版)──
1115
1133
  // 旧版是一个常驻 rAF 循环:只要星图页开着就永远满帧重画,后台标签页也一样,
1116
1134
  // 而画面其实早就静止了。新的规矩只有三种情况会画:
1117
1135
  // 1. 入场展示动画期间(约 1.3s,星点按诞生时间依次点亮)
1118
- // 2. 有人调 markDirty()(hover / 数据或筛选变化 / 尺寸变化)
1136
+ // 2. 有人调 markDirty()(hover / 数据或筛选变化 / 尺寸变化 / 回放)
1119
1137
  // 3. 诞生回放进行中
1120
- // 其余时间一帧都不画。另外帧率封顶 30fps,页面不可见时直接跳过。
1138
+ // 其余时间一帧都不画。另外帧率封顶 30fps,页面不可见时暂停(回来再补一帧)。
1121
1139
  const FRAME_MS = 1000 / 30;
1122
1140
  const ENTRANCE_MS = 1300;
1123
1141
  let raf = 0;
@@ -1126,26 +1144,51 @@ const StarmapRuntime = (function (require, _react) {
1126
1144
  function schedule() {
1127
1145
  if (!raf) raf = window.requestAnimationFrame(tick);
1128
1146
  }
1147
+ /** 还需要连续帧的两种状态:入场动画没走完 / 诞生回放激活。 */
1148
+ function animating(ts) {
1149
+ return (ts - drawBox.t0 < ENTRANCE_MS) || replayBox.active === true;
1150
+ }
1129
1151
  function tick(ts) {
1130
1152
  raf = 0;
1131
1153
  if (!drawBox.renderer) return;
1132
- if (ts - drawBox.t0 < ENTRANCE_MS || replayBox.active) dirty = true;
1154
+ if (animating(ts)) dirty = true;
1155
+ // 页面不可见:这一帧不画,也**不**再排下一帧(旧版直接 return,结果从后台
1156
+ // 切回来就再也不画了)。visibilitychange 回到可见时会补一帧。
1157
+ if (typeof document !== "undefined" && document && document.hidden) {
1158
+ dirty = true;
1159
+ return;
1160
+ }
1133
1161
  if (!dirty) return;
1134
- if (typeof document !== "undefined" && document.hidden) return;
1135
1162
  if (ts - lastDraw < FRAME_MS) { schedule(); return; }
1136
1163
  dirty = false;
1137
1164
  lastDraw = ts;
1138
1165
  drawBox.renderer.render((ts - drawBox.t0) / 1000);
1166
+ // 入场 / 回放没结束就接着排下一帧;静止之后一帧都不排(不允许常驻空转)。
1167
+ if (animating(ts)) schedule();
1139
1168
  }
1140
1169
  drawBox.markDirty = function () { dirty = true; schedule(); };
1170
+ // 从后台标签页回来:补一帧(隐藏期间不排帧,靠这个事件唤醒)。
1171
+ const onVisibility = function () {
1172
+ if (typeof document === "undefined" || !document) return;
1173
+ if (document.hidden) { dirty = true; return; }
1174
+ dirty = true;
1175
+ schedule();
1176
+ };
1177
+ if (typeof document !== "undefined" && document && typeof document.addEventListener === "function") {
1178
+ document.addEventListener("visibilitychange", onVisibility);
1179
+ }
1141
1180
  schedule();
1142
1181
 
1143
1182
  return () => {
1144
1183
  window.cancelAnimationFrame(raf);
1184
+ if (typeof document !== "undefined" && document && typeof document.removeEventListener === "function") {
1185
+ document.removeEventListener("visibilitychange", onVisibility);
1186
+ }
1145
1187
  drawBox.renderer = null;
1146
1188
  drawBox.markDirty = null;
1147
1189
  replayBox.active = false;
1148
1190
  window.cancelAnimationFrame(replayBox.raf);
1191
+ replayBox.raf = 0;
1149
1192
  if (renderer.destroy) renderer.destroy();
1150
1193
  };
1151
1194
  }, [data, layout, canvasEl, themeKey]);
@@ -1163,7 +1206,7 @@ const StarmapRuntime = (function (require, _react) {
1163
1206
  if (drawBox.renderer.setGeometry) drawBox.renderer.setGeometry();
1164
1207
  // 交给按需重绘那一套:下一帧画(<33ms),不再这里直接 render
1165
1208
  if (drawBox.markDirty) drawBox.markDirty();
1166
- else drawBox.renderer.render((Date.now() - drawBox.t0) / 1000);
1209
+ else drawBox.renderer.render((starmapNow() - drawBox.t0) / 1000);
1167
1210
  }
1168
1211
  }, [data, layout, kindsOn, query, showArchive]);
1169
1212
 
@@ -1298,7 +1341,9 @@ const StarmapRuntime = (function (require, _react) {
1298
1341
  function stopReplay() {
1299
1342
  replayBox.active = false;
1300
1343
  window.cancelAnimationFrame(replayBox.raf);
1344
+ replayBox.raf = 0;
1301
1345
  if (drawBox.renderer && drawBox.renderer.setReveal) drawBox.renderer.setReveal(null);
1346
+ // 停回放要回到「全亮」:markDirty 让绘制循环补这一帧。
1302
1347
  if (drawBox.markDirty) drawBox.markDirty();
1303
1348
  setReplaying(false);
1304
1349
  setReplayDate(null);
@@ -1316,17 +1361,22 @@ const StarmapRuntime = (function (require, _react) {
1316
1361
  window.cancelAnimationFrame(replayBox.raf);
1317
1362
  const span = Math.max(mx - mn, 60000) + 2400;
1318
1363
  const dur = 18000;
1319
- const t0 = Date.now();
1364
+ let t0 = null;
1320
1365
  replayBox.active = true;
1321
1366
  setReplaying(true);
1367
+ // 关键:回放期间由「按需绘制」那套逐帧重画。setReveal 只改渲染器的变量,
1368
+ // 不标脏 / 不唤醒绘制循环的话屏幕上什么都不动(旧版就是这样,回放是空的)。
1369
+ if (drawBox.markDirty) drawBox.markDirty();
1322
1370
  let lastLabel = 0;
1323
- const tick = () => {
1371
+ const tick = (ts) => {
1324
1372
  if (!replayBox.active) return;
1325
- const p = Math.min(1, (Date.now() - t0) / dur);
1373
+ // 进度也用 rAF 的单调时间戳,和绘制循环同一个时钟(旧版用 Date.now 又混了一次)。
1374
+ if (t0 === null) t0 = ts;
1375
+ const p = Math.min(1, (ts - t0) / dur);
1326
1376
  const cur = mn - 1200 + span * p;
1327
1377
  if (drawBox.renderer && drawBox.renderer.setReveal) drawBox.renderer.setReveal(cur);
1328
- if (Date.now() - lastLabel > 250) {
1329
- lastLabel = Date.now();
1378
+ if (ts - lastLabel > 250) {
1379
+ lastLabel = ts;
1330
1380
  setReplayDate(Math.min(mx, cur));
1331
1381
  }
1332
1382
  if (p >= 1) { stopReplay(); return; }
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.19",
4
+ "version": "2.1.20",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "icon": "./icon.png",