sage-mem 0.9.9 → 0.9.11

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.
Files changed (2) hide show
  1. package/lib/client.js +47 -5
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -1930,6 +1930,15 @@ const StarmapRuntime = (function (require, _react) {
1930
1930
  const CSS = [
1931
1931
  ".smem-root{display:flex;flex-direction:column;gap:12px;font-size:13px;line-height:1.55;color:var(--dsw-alias-label-primary);max-width:760px;}",
1932
1932
  ".smem-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
1933
+ /**
1934
+ * 头部带里的「弹簧」:吃掉本行的剩余空间,把后面的主操作顶到右边。
1935
+ *
1936
+ * 为什么不用 `margin-left:auto`:那个写法在**换行之后依然把它推到最右**,
1937
+ * 于是窗口一窄(或状态徽标一多)主操作就被挤到下一行的右端,看着像跑出了格子。
1938
+ * 弹簧换成 `flex:1 1 auto` 之后 —— 放得下时它撑开(主操作靠右),
1939
+ * 放不下时主操作换行、而弹簧留在上一行,**下一行就是左顶格**。
1940
+ */
1941
+ ".smem-head-spacer{flex:1 1 auto;min-width:0;}",
1933
1942
  ".smem-title{font-size:15px;font-weight:600;}",
1934
1943
  ".smem-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#22c55e);flex:none;}",
1935
1944
  ".smem-dot--bad{background:var(--dsw-alias-state-error-primary,#ef4444);}",
@@ -1998,7 +2007,8 @@ const StarmapRuntime = (function (require, _react) {
1998
2007
  ".smem-status--bad{color:var(--dsw-alias-state-error-primary,#dc2626);font-weight:600;}",
1999
2008
  // 头部带:三个 tab 共用的一条「我在哪 + 现在什么状态 + 主操作」。
2000
2009
  ".smem-head-job{font-size:12.5px;font-weight:600;}",
2001
- ".smem-head-actions{margin-left:auto;display:flex;align-items:center;gap:6px;}",
2010
+ // 头部带主操作:**不用 margin-left:auto**(见 .smem-head-spacer 的注释)
2011
+ ".smem-head-actions{display:flex;align-items:center;gap:6px;}",
2002
2012
  ".smem-tool-panel{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;display:flex;flex-direction:column;gap:8px;}",
2003
2013
  // 独立成屏的工具面板(现在的「体检」):显式占满**可用宽度**。
2004
2014
  // 整个面板的宽度上限在 `.smem-root`(760px)上,这里只要确保它在任何父容器里
@@ -2569,6 +2579,30 @@ const StarmapRuntime = (function (require, _react) {
2569
2579
  ? h("button", { type: "button", className: "smem-btn", key: "close", onClick: props.onClose }, "收起")
2570
2580
  : null);
2571
2581
 
2582
+ /**
2583
+ * 稳定的 remote 引用。
2584
+ *
2585
+ * 起因(issue #3,2026-10-10 外部报告):面板在**渲染期**取 `ctx.get("remote.x")`,
2586
+ * 又把结果放进 `useEffect` 的依赖数组 —— 而真实宿主里 `ctx.get()` 每次返回一个
2587
+ * **新的包装对象**,于是
2588
+ * 渲染 → effect 重跑 → 6 个 RPC + setState → 再渲染 → …
2589
+ * 滚成无限请求风暴(报告者实测每秒上千次;本机观察到的形态是「组件一直在 setState」)。
2590
+ *
2591
+ * 为什么本机测试一直没抓到:mock 的 `ctx.get` 每次返回**同一个**对象 —— 假的安全。
2592
+ * 测试里现在有 `unstableRemoteRefs` 开关来复现真实行为。
2593
+ *
2594
+ * 同一个 ctx 永远拿到同一个 remote。用 WeakMap 而不是普通 Map:ctx 是长生命周期对象,
2595
+ * WeakMap 不会因为这张缓存把它拖住。
2596
+ */
2597
+ const REMOTE_CACHE = new WeakMap();
2598
+ function stableRemote(ctx, name) {
2599
+ if (!ctx || typeof ctx.get !== "function") return null;
2600
+ let byName = REMOTE_CACHE.get(ctx);
2601
+ if (!byName) { byName = new Map(); REMOTE_CACHE.set(ctx, byName); }
2602
+ if (!byName.has(name)) byName.set(name, ctx.get(name) || null);
2603
+ return byName.get(name);
2604
+ }
2605
+
2572
2606
  /**
2573
2607
  * 「体检」结果面板。
2574
2608
  *
@@ -2808,9 +2842,11 @@ const StarmapRuntime = (function (require, _react) {
2808
2842
  * (比如「记忆 + 会话记录」会把私密对话送进模型),默认全是最保守的那一档。
2809
2843
  */
2810
2844
  function AutodreamPanel(props) {
2811
- const remote = props.ctx && props.ctx.get ? props.ctx.get("remote.autodream") : null;
2845
+ // ⚠️ 必须走 stableRemote:真实宿主里 `ctx.get()` 每次返回**新对象**,
2846
+ // 而下面那个 effect 依赖 remote —— 直接用会让 effect 无限重跑(issue #3)。
2847
+ const remote = stableRemote(props.ctx, "remote.autodream");
2812
2848
  // 注入设置走 memory 那半边(autodream 只管整理那趟)
2813
- const memRemote = props.ctx && props.ctx.get ? props.ctx.get("remote.memory") : null;
2849
+ const memRemote = stableRemote(props.ctx, "remote.memory");
2814
2850
  const sCfg = react.useState(null); const cfg = sCfg[0], setCfg = sCfg[1];
2815
2851
  const sSt = react.useState(null); const st = sSt[0], setSt = sSt[1];
2816
2852
  const sErr = react.useState(null); const err = sErr[0], setErr = sErr[1];
@@ -3473,7 +3509,7 @@ const StarmapRuntime = (function (require, _react) {
3473
3509
  * 保存后**一定回读**(`setConfig` 对非法值是「忽略并保留原值」,只信返回值会骗人)。
3474
3510
  */
3475
3511
  function ArchiveSettingsPanel(props) {
3476
- const remote = props.ctx && props.ctx.get ? props.ctx.get("remote.autodream") : null;
3512
+ const remote = stableRemote(props.ctx, "remote.autodream");
3477
3513
  const sCfg = react.useState(null); const cfg = sCfg[0], setCfg = sCfg[1];
3478
3514
  const sDraft = react.useState(null); const draft = sDraft[0], setDraft = sDraft[1];
3479
3515
  const sErr = react.useState(null); const err = sErr[0], setErr = sErr[1];
@@ -3634,7 +3670,9 @@ const StarmapRuntime = (function (require, _react) {
3634
3670
  ]);
3635
3671
  }
3636
3672
  function Section(props) {
3637
- const remote = props.ctx.get("remote.memory");
3673
+ // 走 stableRemote:真实宿主里 `ctx.get()` 每次返回新对象(issue #3 的成因)。
3674
+ // 这个面板的 effect 依赖是 `[]`、暂时不会爆,但引用稳定是**所有**面板的共同前提。
3675
+ const remote = stableRemote(props.ctx, "remote.memory");
3638
3676
  const sItems = react.useState([]);
3639
3677
  const items = sItems[0], setItems = sItems[1];
3640
3678
  const sLoading = react.useState(false);
@@ -4345,6 +4383,10 @@ const StarmapRuntime = (function (require, _react) {
4345
4383
  }, (pendingDiff ? (pendingDiff + " 项") : "") + "待重启生效")
4346
4384
  : null,
4347
4385
  // 主操作:只放当前 tab 的那一个(星图/自动做梦的主操作在各自面板里,不重复摆一份)。
4386
+ // 弹簧:把主操作顶到右边。**换行时它留在上一行**,主操作于是落到下一行、
4387
+ // 左顶格 —— 这正是用 `margin-left:auto` 做不到的(那会把换行后的主操作
4388
+ // 也推到最右,看起来像"跑出格子了")。
4389
+ h("span", { className: "smem-head-spacer", key: "sp" }),
4348
4390
  h("span", { className: "smem-head-actions", key: "act" }, tab === "files" ? [
4349
4391
  h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
4350
4392
  h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
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.9.9",
4
+ "version": "0.9.11",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {