sage-mem 1.6.4 → 1.7.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.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +199 -3
- package/lib/index.js +14 -1
- package/lib/memory/open-folder.js +8 -1
- package/lib/recall/debug.js +55 -6
- package/lib/recall/index.js +1 -1
- package/lib/recall/injector.js +43 -21
- package/lib/typert.host.js +32 -0
- package/package.json +1 -1
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: `1.
|
|
15
|
+
**Current release: `1.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
|
|
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.
|
|
@@ -389,7 +389,7 @@ The two things are moved **differently**, spelled out:
|
|
|
389
389
|
| `SAGE_MEM_MAX_BASELINE` | `8` (1–50) | number of `baseline` slots |
|
|
390
390
|
| `SAGE_MEM_STALE_DAYS` | `1` (0–365) | threshold for marking a heading 「保存于 N 天前」(saved N days ago); 0 = mark every one |
|
|
391
391
|
| `SAGE_MEM_TASK_ID` | empty | scope of task checkpoints; empty falls back to the session id |
|
|
392
|
-
| `SAGE_MEM_DEBUG` | off | debug logging |
|
|
392
|
+
| `SAGE_MEM_DEBUG` | off | debug logging; when on, the panel's **工具 ▾ → 注入调试** shows the last 8 injection attempts (read-only, never written to disk) |
|
|
393
393
|
|
|
394
394
|
Value semantics: **out-of-range values are clamped to the boundary, illegal values (including pure whitespace) fall back to the default.**
|
|
395
395
|
|
|
@@ -482,7 +482,7 @@ Version-by-version compatibility record:
|
|
|
482
482
|
| 0.2.0-rc.2 | compatible |
|
|
483
483
|
| 0.2.1-rc.1 | compatible |
|
|
484
484
|
|
|
485
|
-
**How to read `compatible`.** It means this plugin's codec and manifest contract has been checked against that host generation (code-level comparison plus the unit/contract suite), **not** that an end-to-end probe was run on that exact host build. The table is a declaration; how each claim was checked is summarized here. Real-host probes so far: `headless` (2026-10-15) and `web` (2026-10-16); the `desktop` profile has **not** been probed yet (see [release-checklist.md](https://github.com/gezi-wen/sage-mem/blob/main/docs/release-checklist.md) §4b). CI runs
|
|
485
|
+
**How to read `compatible`.** It means this plugin's codec and manifest contract has been checked against that host generation (code-level comparison plus the unit/contract suite), **not** that an end-to-end probe was run on that exact host build. The table is a declaration; how each claim was checked is summarized here. Real-host probes so far: `headless` (2026-10-15) and `web` (2026-10-16); the `desktop` profile has **not** been probed yet (see [release-checklist.md](https://github.com/gezi-wen/sage-mem/blob/main/docs/release-checklist.md) §4b). CI runs on Ubuntu against **Node 18.x / 20.x / 24.x** with the `0.2.0-rc.1` peer generation; macOS and Windows are not CI-covered, and there is no desktop-host probe yet.
|
|
486
486
|
|
|
487
487
|
Two points worth spelling out:
|
|
488
488
|
|
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
|
-
**当前发布版本:`1.
|
|
17
|
+
**当前发布版本:`1.7.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 是最直接的反馈。
|
|
@@ -389,7 +389,7 @@ sage-mem 给记忆目录的写入加了**版本守卫**:写入前记下当前
|
|
|
389
389
|
| `SAGE_MEM_MAX_BASELINE` | `8`(1–50) | `baseline` 名额 |
|
|
390
390
|
| `SAGE_MEM_STALE_DAYS` | `1`(0–365) | 标题标「保存于 N 天前」的门槛;0 = 每条都标 |
|
|
391
391
|
| `SAGE_MEM_TASK_ID` | 空 | 任务检查点的作用域;空则回落会话 id |
|
|
392
|
-
| `SAGE_MEM_DEBUG` | 关 |
|
|
392
|
+
| `SAGE_MEM_DEBUG` | 关 | 调试日志;打开后面板「工具 ▾ → 注入调试」显示最近 8 次注入的账(只读、不落盘) |
|
|
393
393
|
|
|
394
394
|
取值语义:**越界夹到边界,非法值(含纯空格)回落默认。**
|
|
395
395
|
|
|
@@ -484,7 +484,7 @@ sage-mem 是纯 DSH 插件,用 `package.json` 的 **`engines.dsh`** 声明宿
|
|
|
484
484
|
| 0.2.0-rc.2 | compatible |
|
|
485
485
|
| 0.2.1-rc.1 | compatible |
|
|
486
486
|
|
|
487
|
-
**`compatible` 怎么读**:它表示本插件的 codec / manifest 契约在该宿主代上核对过(代码层比对 + 单元/契约测试),**不表示**在那个确切宿主构建上跑过端到端探针。这张表是声明,核对方式总结在下面。真实宿主探针目前只跑过 `headless`(2026-10-15)与 `web`(2026-10-16);`desktop` profile **还没跑过**(见 [release-checklist.md](https://github.com/gezi-wen/sage-mem/blob/main/docs/release-checklist.md) §4b)。CI
|
|
487
|
+
**`compatible` 怎么读**:它表示本插件的 codec / manifest 契约在该宿主代上核对过(代码层比对 + 单元/契约测试),**不表示**在那个确切宿主构建上跑过端到端探针。这张表是声明,核对方式总结在下面。真实宿主探针目前只跑过 `headless`(2026-10-15)与 `web`(2026-10-16);`desktop` profile **还没跑过**(见 [release-checklist.md](https://github.com/gezi-wen/sage-mem/blob/main/docs/release-checklist.md) §4b)。CI 在 Ubuntu 上覆盖 **Node 18.x / 20.x / 24.x**,peer 钉 `0.2.0-rc.1` 一代;macOS 与 Windows 没有 CI 覆盖,desktop 宿主探针也还没有。
|
|
488
488
|
|
|
489
489
|
两点值得说明:
|
|
490
490
|
|
package/lib/client.js
CHANGED
|
@@ -1818,6 +1818,19 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1818
1818
|
result: { mode: "strict", typeSymbol: "sage-mem/types#RawWriteResult", schema: objCodec, create: () => objCodec },
|
|
1819
1819
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1820
1820
|
},
|
|
1821
|
+
{
|
|
1822
|
+
// 1.7.0:最近几次注入的账。**宽松 codec**(与 audit / 设置那批同一套理由):
|
|
1823
|
+
// 面板只按字段取值、缺失就降级显示,host 将来加字段不该让整条调用报错。
|
|
1824
|
+
// 每一条记录的字段口径见 lib/recall/debug.js 的 makeDebug 注释。
|
|
1825
|
+
id: "sage-mem#memory/recallDebug",
|
|
1826
|
+
service: "memory",
|
|
1827
|
+
namespace: "memory",
|
|
1828
|
+
method: "recallDebug",
|
|
1829
|
+
invocation: { kind: "direct" },
|
|
1830
|
+
parameters: [],
|
|
1831
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#RecallDebug", schema: objCodec, create: () => objCodec },
|
|
1832
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1833
|
+
},
|
|
1821
1834
|
{
|
|
1822
1835
|
id: "sage-mem#starmap/listStars",
|
|
1823
1836
|
service: "starmap",
|
|
@@ -2053,6 +2066,24 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2053
2066
|
".smem-drawer-item:hover{background:var(--dsw-alias-bg-layer-2);}",
|
|
2054
2067
|
'.smem-drawer-item[data-on="1"]{background:var(--dsw-alias-bg-layer-2);font-weight:600;}',
|
|
2055
2068
|
".smem-drawer-item .smem-status{margin-left:auto;}",
|
|
2069
|
+
// ── 注入调试面板(1.7.0)──
|
|
2070
|
+
// 只读账目:时间/会话/结论一行,问题一行,两组得分表,字节与预算一行。
|
|
2071
|
+
// 颜色**只用宿主变量**(主题两档都要能读);没有一处在硬编码背景上反色。
|
|
2072
|
+
".smem-dbg-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
|
|
2073
|
+
".smem-dbg-time{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
2074
|
+
".smem-dbg-sess{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;color:var(--dsw-alias-label-secondary);}",
|
|
2075
|
+
".smem-dbg-out{font-size:12px;font-weight:600;}",
|
|
2076
|
+
".smem-dbg-ok{color:var(--dsw-alias-brand-primary,#4d76e6);}",
|
|
2077
|
+
".smem-dbg-warn{color:var(--dsw-alias-state-warn-primary,#d97706);}",
|
|
2078
|
+
".smem-dbg-dim{color:var(--dsw-alias-label-secondary);}",
|
|
2079
|
+
".smem-dbg-query{font-size:12.5px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;}",
|
|
2080
|
+
".smem-dbg-row{display:flex;gap:8px;align-items:flex-start;font-size:12px;}",
|
|
2081
|
+
".smem-dbg-scores{display:flex;flex-wrap:wrap;gap:4px 10px;}",
|
|
2082
|
+
".smem-dbg-score{display:inline-flex;gap:4px;align-items:baseline;}",
|
|
2083
|
+
".smem-dbg-file{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;overflow-wrap:anywhere;}",
|
|
2084
|
+
".smem-dbg-num{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);font-size:11.5px;}",
|
|
2085
|
+
".smem-dbg-hist{display:flex;flex-direction:column;gap:2px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:6px;}",
|
|
2086
|
+
".smem-dbg-hist-row{display:flex;gap:8px;align-items:baseline;font-size:11.5px;}",
|
|
2056
2087
|
".smem-status--bad{color:var(--dsw-alias-state-error-primary,#dc2626);font-weight:600;}",
|
|
2057
2088
|
// 头部带:三个 tab 共用的一条「我在哪 + 现在什么状态 + 主操作」。
|
|
2058
2089
|
".smem-head-job{font-size:12.5px;font-weight:600;}",
|
|
@@ -2658,6 +2689,99 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
2658
2689
|
? h("button", { type: "button", className: "smem-btn", key: "close", onClick: props.onClose }, "收起")
|
|
2659
2690
|
: null);
|
|
2660
2691
|
|
|
2692
|
+
/**
|
|
2693
|
+
* 「注入调试」面板(1.7.0)。
|
|
2694
|
+
*
|
|
2695
|
+
* 只读:没有「清空 / 重跑」按钮 —— 账是注入链在 `SAGE_MEM_DEBUG=1` 时自动记的,
|
|
2696
|
+
* 人能做的是看,以及点「刷新」再取一次。入口只在开关打开时出现(宿主告诉我们)。
|
|
2697
|
+
*
|
|
2698
|
+
* 每一条记录的口径写在 `lib/recall/debug.js` 的 makeDebug 注释里;这里只负责把
|
|
2699
|
+
* 「为什么这一步没注入」翻译成人话 —— 那是这个面板存在的唯一理由。**尤其是
|
|
2700
|
+
* `empty + deduped`**:每轮多步里它占大多数,不解释就会被当成「插件坏了」。
|
|
2701
|
+
*/
|
|
2702
|
+
function RecallDebugPanel(props) {
|
|
2703
|
+
const data = props.data || {};
|
|
2704
|
+
const recs = Array.isArray(data.records) ? data.records : [];
|
|
2705
|
+
const latest = recs.length ? recs[0] : null;
|
|
2706
|
+
const pad = (n) => (n < 10 ? "0" + n : String(n));
|
|
2707
|
+
function clock(ms) {
|
|
2708
|
+
if (typeof ms !== "number") return "";
|
|
2709
|
+
const d = new Date(ms);
|
|
2710
|
+
return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
|
|
2711
|
+
}
|
|
2712
|
+
function outcomeText(r) {
|
|
2713
|
+
if (!r) return "";
|
|
2714
|
+
if (r.outcome === "injected") return "本步注入 " + (Array.isArray(r.injected) ? r.injected.length : 0) + " 条正文";
|
|
2715
|
+
if (r.outcome === "card-only") return "正文没进,只发了状态卡";
|
|
2716
|
+
return r.deduped ? "同题已注入过,本步去重" : "本步没有可注入的内容";
|
|
2717
|
+
}
|
|
2718
|
+
function outcomeClass(r) {
|
|
2719
|
+
if (!r) return "";
|
|
2720
|
+
if (r.outcome === "injected") return " smem-dbg-ok";
|
|
2721
|
+
if (r.outcome === "card-only") return " smem-dbg-warn";
|
|
2722
|
+
return " smem-dbg-dim";
|
|
2723
|
+
}
|
|
2724
|
+
function scoreRows(list, emptyText) {
|
|
2725
|
+
const arr = Array.isArray(list) ? list : [];
|
|
2726
|
+
if (!arr.length) return h("span", { className: "smem-muted", key: "none" }, emptyText || "无");
|
|
2727
|
+
return h("div", { className: "smem-dbg-scores", key: "s" }, arr.map(function (x, i) {
|
|
2728
|
+
return h("span", { className: "smem-dbg-score", key: String((x && x.file) || i) + "-" + i }, [
|
|
2729
|
+
h("span", { className: "smem-dbg-file", key: "f" }, String((x && x.file) || "?")),
|
|
2730
|
+
h("span", { className: "smem-dbg-num", key: "n" }, x && typeof x.score === "number" ? String(x.score) : "?"),
|
|
2731
|
+
]);
|
|
2732
|
+
}));
|
|
2733
|
+
}
|
|
2734
|
+
const injected = latest && Array.isArray(latest.injected) ? latest.injected : [];
|
|
2735
|
+
const histogram = recs.slice(1).map(function (r, i) {
|
|
2736
|
+
return h("div", { className: "smem-dbg-hist-row", key: "h" + i }, [
|
|
2737
|
+
h("span", { className: "smem-dbg-time", key: "t" }, clock(r.at)),
|
|
2738
|
+
h("span", { className: "smem-dbg-out" + outcomeClass(r), key: "o" }, outcomeText(r)),
|
|
2739
|
+
h("span", { className: "smem-muted", key: "n" }, String(r.chars || 0) + " 字节"),
|
|
2740
|
+
]);
|
|
2741
|
+
});
|
|
2742
|
+
return h("div", { className: "smem-tool-panel smem-tool-panel--cap", key: "recall-debug" }, [
|
|
2743
|
+
h("div", { className: "smem-tool-head", key: "head" }, [
|
|
2744
|
+
h("span", { className: "smem-tool-title", key: "t" }, "注入调试"),
|
|
2745
|
+
h("span", { className: "smem-muted", key: "s" }, "SAGE_MEM_DEBUG=1 · 最近 " + String(recs.length) + " 次"),
|
|
2746
|
+
h("button", { type: "button", className: "smem-btn", key: "again", disabled: props.busy, onClick: props.onReload }, props.busy ? "读取中…" : "刷新"),
|
|
2747
|
+
toolClose(props),
|
|
2748
|
+
]),
|
|
2749
|
+
!latest
|
|
2750
|
+
? h("div", { className: "smem-empty", key: "empty" }, "开关已开,但还没有一次注入 —— 开一轮对话再回来看。")
|
|
2751
|
+
: h("div", { key: "body" }, [
|
|
2752
|
+
h("div", { className: "smem-dbg-head", key: "meta" }, [
|
|
2753
|
+
h("span", { className: "smem-dbg-time", key: "t" }, clock(latest.at)),
|
|
2754
|
+
h("span", { className: "smem-dbg-sess", key: "sid" }, String(latest.sessionId || "").slice(0, 8)),
|
|
2755
|
+
h("span", { className: "smem-dbg-out" + outcomeClass(latest), key: "o" }, outcomeText(latest)),
|
|
2756
|
+
]),
|
|
2757
|
+
h("div", { className: "smem-dbg-query", key: "q" }, "问题:" + (String(latest.query || "").trim() || "(无)")),
|
|
2758
|
+
h("div", { className: "smem-muted", key: "stat" },
|
|
2759
|
+
"扫了 " + String(latest.scanned || 0) + " 条 · 摘要命中 " + String(latest.hits || 0) +
|
|
2760
|
+
" · 正文命中 " + String(Array.isArray(latest.body) ? latest.body.length : 0) +
|
|
2761
|
+
" · baseline " + String(latest.baseline || 0)),
|
|
2762
|
+
h("div", { className: "smem-dbg-row", key: "sum" }, [h("span", { className: "smem-label", key: "l" }, "摘要得分"), scoreRows(latest.scores)]),
|
|
2763
|
+
h("div", { className: "smem-dbg-row", key: "body" }, [h("span", { className: "smem-label", key: "l" }, "正文命中"), scoreRows(latest.body)]),
|
|
2764
|
+
h("div", { className: "smem-dbg-row", key: "inj" }, [
|
|
2765
|
+
h("span", { className: "smem-label", key: "l" }, "实际注入"),
|
|
2766
|
+
injected.length
|
|
2767
|
+
? h("span", { className: "smem-dbg-scores", key: "v" }, injected.map(function (f) {
|
|
2768
|
+
return h("span", { className: "smem-dbg-file", key: String(f) }, String(f));
|
|
2769
|
+
}))
|
|
2770
|
+
: h("span", { className: "smem-muted", key: "v" }, "无"),
|
|
2771
|
+
]),
|
|
2772
|
+
h("div", { className: "smem-muted", key: "bytes" },
|
|
2773
|
+
"正文字节 " + String(latest.chars || 0) + "(本代累计 " + String(latest.sessionChars || 0) + ")" +
|
|
2774
|
+
" · 状态卡 " + (latest.cardChars ? String(latest.cardChars) + " 字节" : "未发") +
|
|
2775
|
+
" · 预算挡掉 " + String(latest.skippedByBudget || 0) + " 条"),
|
|
2776
|
+
h("div", { className: "smem-ar-hint", key: "hint" },
|
|
2777
|
+
"注入每走一步都会尝试一次;同题同命中集不会重复注入 —— 所以「空」多半是去重生效,不是坏了。"),
|
|
2778
|
+
histogram.length
|
|
2779
|
+
? h("div", { className: "smem-dbg-hist", key: "hist" }, histogram)
|
|
2780
|
+
: null,
|
|
2781
|
+
]),
|
|
2782
|
+
]);
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2661
2785
|
/**
|
|
2662
2786
|
* 稳定的 remote 引用。
|
|
2663
2787
|
*
|
|
@@ -4051,6 +4175,15 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4051
4175
|
const rawBusy = sRawBusy[0], setRawBusy = sRawBusy[1];
|
|
4052
4176
|
const sRawLoading = react.useState(false);
|
|
4053
4177
|
const rawLoading = sRawLoading[0], setRawLoading = sRawLoading[1];
|
|
4178
|
+
// ── 注入调试(1.7.0)──
|
|
4179
|
+
// 入口**只在 SAGE_MEM_DEBUG=1 时出现**:挂载时问宿主一次(enabled 由宿主每次现读
|
|
4180
|
+
// 环境变量),关着就什么都不显示、抽屉里也不多一项。
|
|
4181
|
+
const sDbgOn = react.useState(false);
|
|
4182
|
+
const dbgOn = sDbgOn[0], setDbgOn = sDbgOn[1];
|
|
4183
|
+
const sDbg = react.useState(null);
|
|
4184
|
+
const dbg = sDbg[0], setDbg = sDbg[1];
|
|
4185
|
+
const sDbgBusy = react.useState(false);
|
|
4186
|
+
const dbgBusy = sDbgBusy[0], setDbgBusy = sDbgBusy[1];
|
|
4054
4187
|
// renderSlot:shell 会经 props 传进来(见 80-apply 的 children 声明),
|
|
4055
4188
|
// 但本插件改用并列 tab 渲染星图,不再调用它 —— 声明保留给 shell,这里不取。
|
|
4056
4189
|
|
|
@@ -4086,6 +4219,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4086
4219
|
// 头部带的两个徽标要有数据才立得住:体检(只读扫描)与待重启项。
|
|
4087
4220
|
runAudit();
|
|
4088
4221
|
loadSettings();
|
|
4222
|
+
// 1.7.0:只读问一次「调试开关开着吗」—— 关着时抽屉里不会多出「注入调试」。
|
|
4223
|
+
// 这是唯一一处为调试视图多付的调用(一次本地只读 remote,不碰磁盘)。
|
|
4224
|
+
loadRecallDebug();
|
|
4089
4225
|
/**
|
|
4090
4226
|
* 更新检查:**静默**跑,失败一律不打扰 ——
|
|
4091
4227
|
* 网络不通、被 GitHub 限流、仓库还没建 release,都不该在面板上冒一条红。
|
|
@@ -4112,7 +4248,26 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4112
4248
|
react.useEffect(function () {
|
|
4113
4249
|
if (!drawer) return undefined;
|
|
4114
4250
|
if (typeof document === "undefined" || !document.addEventListener) return undefined;
|
|
4115
|
-
|
|
4251
|
+
/**
|
|
4252
|
+
* ⚠️ **必须放过「抽屉内部」的点击**(2026-10-24 真机实测的 bug)。
|
|
4253
|
+
*
|
|
4254
|
+
* 打开抽屉的那一次点击,会在这个 effect 挂上监听之后**继续冒泡到 document**。
|
|
4255
|
+
* DevTools 里记到的顺序:`DOC:工具 ▾@911408 → 弹层 ADD@911410 → REMOVE@911412`
|
|
4256
|
+
* —— 弹层活了 2 毫秒就关掉自己,按钮连变色都来不及,用户看到的是
|
|
4257
|
+
* 「点了完全没反应」。
|
|
4258
|
+
*
|
|
4259
|
+
* 为什么测试没抓到:测试点按钮是直接调 `onClick`,不经过真实的事件冒泡,
|
|
4260
|
+
* 所以「同一次点击既开又关」这条路径在假环境里根本不存在。
|
|
4261
|
+
*
|
|
4262
|
+
* 判据用 `closest("[data-smem-drawer]")` 而不是比 DOM 引用:按钮与弹层条目
|
|
4263
|
+
* 都在带这个属性的 span 里,text / svg 子节点也照样命中;也不依赖事件对象
|
|
4264
|
+
* 上别的字段(测试喂一个只有 closest 的 target 就能复现)。
|
|
4265
|
+
*/
|
|
4266
|
+
function onOutside(e) {
|
|
4267
|
+
const t = e && e.target;
|
|
4268
|
+
if (t && typeof t.closest === "function" && t.closest("[data-smem-drawer]")) return;
|
|
4269
|
+
setDrawer(false);
|
|
4270
|
+
}
|
|
4116
4271
|
function onKey(e) { if (e && (e.key === "Escape" || e.keyCode === 27)) setDrawer(false); }
|
|
4117
4272
|
document.addEventListener("click", onOutside);
|
|
4118
4273
|
document.addEventListener("keydown", onKey);
|
|
@@ -4456,6 +4611,27 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4456
4611
|
.catch(function (e) { setError(String((e && e.message) || e)); });
|
|
4457
4612
|
}
|
|
4458
4613
|
|
|
4614
|
+
/**
|
|
4615
|
+
* 拉一次注入调试账(1.7.0)。
|
|
4616
|
+
*
|
|
4617
|
+
* 返回值同时决定入口出不出现:`enabled === false` 时「工具」抽屉里不渲染
|
|
4618
|
+
* 「注入调试」这一项 —— 这是路线图「关掉调试开关时界面不出现」的落地方式。
|
|
4619
|
+
*
|
|
4620
|
+
* 旧宿主没有 recallDebug → 什么都不做、不报错(与 openMemoryDir / readRaw 同一套降级)。
|
|
4621
|
+
*/
|
|
4622
|
+
function loadRecallDebug() {
|
|
4623
|
+
if (!remote || typeof remote.recallDebug !== "function") return;
|
|
4624
|
+
setDbgBusy(true);
|
|
4625
|
+
remote.recallDebug()
|
|
4626
|
+
.then(function (r) {
|
|
4627
|
+
const d = unwrap(r) || {};
|
|
4628
|
+
setDbgOn(d.enabled === true);
|
|
4629
|
+
setDbg(d);
|
|
4630
|
+
})
|
|
4631
|
+
.catch(function () { setDbgOn(false); })
|
|
4632
|
+
.then(function () { setDbgBusy(false); });
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4459
4635
|
/**
|
|
4460
4636
|
* 打开 / 收起一个工具面板。数据只在第一次打开时拉;已经拉过就复用
|
|
4461
4637
|
* (「刷新」「重新体检」是显式动作,别替用户反复扫目录)。
|
|
@@ -4466,6 +4642,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4466
4642
|
setError(null);
|
|
4467
4643
|
if (next === "candidates" && !cand) loadCandidates();
|
|
4468
4644
|
if (next === "raw" && !settings) loadSettings();
|
|
4645
|
+
// 调试账每次都重新取:它记的是「最近几次」,多一步就多一条 —— 缓存没有意义。
|
|
4646
|
+
if (next === "debug") loadRecallDebug();
|
|
4469
4647
|
}
|
|
4470
4648
|
|
|
4471
4649
|
/**
|
|
@@ -4729,13 +4907,15 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4729
4907
|
tab === "files" ? h("div", { className: "smem-toolbar", key: "bar" }, [
|
|
4730
4908
|
h("input", { className: "smem-search", key: "q", value: query, placeholder: "搜索文件名 / 描述 / 类型 / 标签…", onChange: function (e) { setQuery(e.target.value); } }),
|
|
4731
4909
|
// 工具抽屉:关着时这里只有一个按钮,零占位;面板一次只开一个。
|
|
4732
|
-
|
|
4910
|
+
// `data-smem-drawer` 是「点外部关闭」的排除标记:这一整块里的点击
|
|
4911
|
+
// 都算「抽屉内部」,包括开抽屉的那一次点击本身(见上面 effect 的长注释)。
|
|
4912
|
+
h("span", { className: "smem-drawer", key: "drawer", "data-smem-drawer": "1" }, [
|
|
4733
4913
|
h("button", {
|
|
4734
4914
|
type: "button", key: "btn",
|
|
4735
4915
|
className: "smem-btn" + (drawer ? " smem-btn-primary" : ""),
|
|
4736
4916
|
"aria-expanded": drawer ? "true" : "false",
|
|
4737
4917
|
"aria-haspopup": "menu",
|
|
4738
|
-
title: "归档候选 / 保留名编辑",
|
|
4918
|
+
title: "归档候选 / 保留名编辑" + (dbgOn ? " / 注入调试" : ""),
|
|
4739
4919
|
onClick: function () { setDrawer(!drawer); },
|
|
4740
4920
|
}, "工具 ▾"),
|
|
4741
4921
|
drawer ? h("div", { className: "smem-drawer-pop", key: "pop" }, [
|
|
@@ -4750,6 +4930,12 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4750
4930
|
"data-on": tool === "raw" ? "1" : "0",
|
|
4751
4931
|
onClick: function () { toggleTool("raw"); setDrawer(false); },
|
|
4752
4932
|
}, ["保留名编辑", h("span", { className: "smem-status", key: "s" }, settings && settings.reserved ? (settings.reserved.length + " 个") : "")]),
|
|
4933
|
+
// 1.7.0:只在调试开关打开时出现(关着时抽屉仍是两项,界面零变化)
|
|
4934
|
+
dbgOn ? h("button", {
|
|
4935
|
+
type: "button", className: "smem-drawer-item", key: "d",
|
|
4936
|
+
"data-on": tool === "debug" ? "1" : "0",
|
|
4937
|
+
onClick: function () { toggleTool("debug"); setDrawer(false); },
|
|
4938
|
+
}, ["注入调试", h("span", { className: "smem-status", key: "s" }, dbg && Array.isArray(dbg.records) ? (dbg.records.length + " 次") : "")]) : null,
|
|
4753
4939
|
]) : null,
|
|
4754
4940
|
]),
|
|
4755
4941
|
]) : null,
|
|
@@ -4801,6 +4987,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4801
4987
|
onClose: function () { setRawName(null); setRawText(""); setRawIsNew(false); },
|
|
4802
4988
|
})
|
|
4803
4989
|
: null,
|
|
4990
|
+
// 1.7.0:注入调试。只读面板;数据在打开那一刻现取(最近 8 次),
|
|
4991
|
+
// 「刷新」是显式动作 —— 与体检 / 候选同一条交互约定。
|
|
4992
|
+
tab === "files" && tool === "debug"
|
|
4993
|
+
? h(RecallDebugPanel, {
|
|
4994
|
+
data: dbg,
|
|
4995
|
+
busy: dbgBusy,
|
|
4996
|
+
onReload: loadRecallDebug,
|
|
4997
|
+
onClose: function () { setTool(null); },
|
|
4998
|
+
})
|
|
4999
|
+
: null,
|
|
4804
5000
|
// 三排筛选:状态 / 类型 / 标签。组内 OR、组间 AND,再与搜索框 AND。
|
|
4805
5001
|
tab === "files" && totalCount
|
|
4806
5002
|
? h(FilterBar, {
|
package/lib/index.js
CHANGED
|
@@ -33,7 +33,7 @@ import { mkdir, unlink, writeFile } from 'node:fs/promises'
|
|
|
33
33
|
import { join } from 'node:path'
|
|
34
34
|
import { checkUpdate as checkUpdateImpl } from './update.js'
|
|
35
35
|
import { autodreamConfigSnapshot, clampInt, MAX_RAW_BYTES, pendingLimits, rawName, readReservedFile, readSettingsFile, RESERVED_FILE, SETTINGS_FILE } from './settings.js'
|
|
36
|
-
import { createRecallInjector, createSessionInjectState } from './recall/index.js'
|
|
36
|
+
import { createRecallInjector, createSessionInjectState, readRecallDebug } from './recall/index.js'
|
|
37
37
|
import { mountAutodream, mountMemoryTools, mountObserve, mountWriteGate } from './assembly/index.js'
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -92,6 +92,17 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
92
92
|
return await checkUpdateImpl()
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* 最近几次注入尝试的账(1.7.0 面板「工具 → 注入调试」的唯一数据源)。
|
|
97
|
+
*
|
|
98
|
+
* **只读、不落盘**:数据是注入链在 `SAGE_MEM_DEBUG=1` 时记进进程内存的环形缓冲
|
|
99
|
+
* (见 `lib/recall/debug.js`);开关关着时返回 `{ enabled: false, records: [] }`,
|
|
100
|
+
* 面板据此不显示入口。不提供「清空 / 修改」能力 —— 一份账记下来就是那一刻的事实。
|
|
101
|
+
*/
|
|
102
|
+
async recallDebug() {
|
|
103
|
+
return readRecallDebug()
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
/** 列出 memory 目录全部记忆文件(文件名 + 类型 + 描述 + 大小 + 自定义标签 + 是否锁定)。 */
|
|
96
107
|
async listFiles() {
|
|
97
108
|
const [files, locked] = await Promise.all([scanMemoryFiles(this.ctx, ''), readLockSet(MEMORY_DIR)])
|
|
@@ -577,6 +588,8 @@ markRemote(MemoryGateway, 'getSettings', 'getSettings')
|
|
|
577
588
|
markRemote(MemoryGateway, 'setSettings', 'setSettings')
|
|
578
589
|
markRemote(MemoryGateway, 'readRaw', 'readRaw')
|
|
579
590
|
markRemote(MemoryGateway, 'writeRaw', 'writeRaw')
|
|
591
|
+
// 1.7.0:注入调试账(只读;开关关着时返回 { enabled: false, records: [] })
|
|
592
|
+
markRemote(MemoryGateway, 'recallDebug', 'recallDebug')
|
|
580
593
|
|
|
581
594
|
/**
|
|
582
595
|
* 记忆星图(宿主侧)—— 只读扫描记忆目录,把每条记忆解析成一颗「星」。
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* 进程是**一次性、detached** 的:不等待退出(Explorer / Finder 是常驻进程,等它等于
|
|
11
11
|
* 永不返回),也不持有 stdio。这条路径不改变插件「无 worker / 无端口 / 无常驻进程」的性质。
|
|
12
12
|
*
|
|
13
|
+
* ⚠️ **绝对不要传 `windowsHide: true`**(2026-10-24 实测,A/B 对照):它在 Windows 上
|
|
14
|
+
* 映射成 CREATE_NO_WINDOW,而 `explorer.exe` 是 GUI 进程 —— 传了之后进程起来了、
|
|
15
|
+
* 退出码也正常,**但那个文件夹窗口被藏起来**,用户体验就是「点了没反应」。去掉它,
|
|
16
|
+
* 同一份代码窗口正常弹出。原先那句「Windows 不闪窗」是错的:explorer 本来就没有控制台窗口。
|
|
17
|
+
*
|
|
13
18
|
* 返回 `{ ok, error? }`,不抛:设置页按 ok 显示错误。`spawn` 的 'error' 事件是异步的,
|
|
14
19
|
* 调用方拿不到,所以这里只挂一个空处理器,避免「命令不存在」变成未捕获异常 ——
|
|
15
20
|
* 返回值表达的是「已发起」,不是「窗口已经出现」。
|
|
@@ -47,7 +52,9 @@ export function openMemoryFolder(dir, options = {}) {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
try {
|
|
50
|
-
|
|
55
|
+
// 注意:**没有 windowsHide**。它是 Windows 上「文件夹窗口不出现」的元凶(见文末头注);
|
|
56
|
+
// `stdio: 'ignore'` 已经保证了不持有管道、不弹控制台。
|
|
57
|
+
const child = spawnImpl(plan.command, plan.args, { detached: true, stdio: 'ignore' })
|
|
51
58
|
child.on?.('error', () => {})
|
|
52
59
|
child.unref?.()
|
|
53
60
|
return { ok: true }
|
package/lib/recall/debug.js
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 注入链的调试出口(1.2.0 从 `lib/index.js`
|
|
2
|
+
* 注入链的调试出口(1.2.0 从 `lib/index.js` 原样搬出;1.7.0 起兼作面板的数据源)。
|
|
3
3
|
*
|
|
4
|
-
* `SAGE_MEM_DEBUG=1` 才出声的一行结构化 JSON
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `SAGE_MEM_DEBUG=1` 才出声的一行结构化 JSON,并把同一份 payload 记进进程内的
|
|
5
|
+
* 环形缓冲,供面板「工具 → 注入调试」读。它回答的是「这条记忆为什么没被召回」
|
|
6
|
+
* ——一个光看上下文永远查不出的问题。
|
|
7
7
|
*
|
|
8
|
-
* ⚠️
|
|
9
|
-
* 不成立时连 payload
|
|
8
|
+
* ⚠️ **不开时零输出、零开销**:每个调用点都先 `if (debugEnabled())`,
|
|
9
|
+
* 不成立时连 payload 对象都不会被构造出来,更不会进缓冲。
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ **绝不落盘**:缓冲只在进程内存里(最多 `MAX_DEBUG_RECORDS` 条),重启即空。
|
|
12
|
+
* 调试信息不是记忆,不该写进用户的记忆目录或状态目录 —— 1.7.0 的验收里有一条
|
|
13
|
+
* 「跑完注入后记忆目录/状态目录逐字节不变」在盯着这件事。
|
|
10
14
|
*
|
|
11
15
|
* 为什么开关是**每次现读**而不是模块级常量:原来是常量,靠「测试里用 query 重载
|
|
12
16
|
* `lib/index.js` 重新求值」来切换;1.2.0 把它拆成独立模块后,重载 index.js 不会重载
|
|
13
17
|
* 它,常量就永远停在旧值(`test/access-test.mjs` 第 4 组当场变红)。现读一次环境变量
|
|
14
18
|
* 的成本可以忽略,而它换来了「运行时也能打开调试」这个更诚实的行为。
|
|
15
19
|
*/
|
|
20
|
+
|
|
21
|
+
/** 面板里最多看几次注入尝试。每步 assemble 都会尝试一次,8 条约等于一轮多步。 */
|
|
22
|
+
export const MAX_DEBUG_RECORDS = 8
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 最近几次注入尝试(**旧 → 新**)。
|
|
26
|
+
*
|
|
27
|
+
* 模块级单例:一个进程一个插件事例,环形缓冲天然是全局的。记录里带 sessionId,
|
|
28
|
+
* 多会话混在一起时面板按时间倒序展示。
|
|
29
|
+
*/
|
|
30
|
+
const records = []
|
|
31
|
+
|
|
16
32
|
export function debugEnabled() {
|
|
17
33
|
return process.env.SAGE_MEM_DEBUG === '1'
|
|
18
34
|
}
|
|
@@ -21,3 +37,36 @@ export function debugRecall(payload) {
|
|
|
21
37
|
if (!debugEnabled()) return
|
|
22
38
|
console.log(`[sage-mem] recall ${JSON.stringify(payload)}`)
|
|
23
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 记一次注入尝试(调用点已确认 `debugEnabled()` 成立)。
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ 存**副本**:payload 里的数组是现构造的,但调用方之后若再改它们,
|
|
45
|
+
* 面板读到的就不该跟着变 —— 一份账记下来就是那一刻的事实。
|
|
46
|
+
*/
|
|
47
|
+
export function recordInjection(payload) {
|
|
48
|
+
records.push(JSON.parse(JSON.stringify(payload)))
|
|
49
|
+
if (records.length > MAX_DEBUG_RECORDS) {
|
|
50
|
+
records.splice(0, records.length - MAX_DEBUG_RECORDS)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 面板读口:`{ enabled, records }`,**最新在前**。
|
|
56
|
+
*
|
|
57
|
+
* 关着开关时也返回 `{ enabled: false, records: [] }`(缓冲里本来就不该有东西);
|
|
58
|
+
* 面板据此决定「工具」抽屉里出不出现「注入调试」这一项。
|
|
59
|
+
*/
|
|
60
|
+
export function readRecallDebug() {
|
|
61
|
+
// 深拷贝:调用方(面板 / 测试)拿到的必须是一份**独立的账** —— 改它不该改到内部
|
|
62
|
+
// 缓冲(浅拷贝只拷数组,记录对象还是同一批引用,`records[0].query = …` 会污染)。
|
|
63
|
+
return {
|
|
64
|
+
enabled: debugEnabled(),
|
|
65
|
+
records: records.slice().reverse().map((r) => JSON.parse(JSON.stringify(r))),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 测试专用:清空缓冲(跨用例隔离;生产路径没有任何地方调它)。 */
|
|
70
|
+
export function resetRecallDebug() {
|
|
71
|
+
records.length = 0
|
|
72
|
+
}
|
package/lib/recall/index.js
CHANGED
|
@@ -12,4 +12,4 @@ export { createRecallInjector } from './injector.js'
|
|
|
12
12
|
export { dedupeByFile, selectRecall } from './select.js'
|
|
13
13
|
export { buildCandidates, composeDelivery, composeStatus } from './compose.js'
|
|
14
14
|
export { composeRecallText } from './text.js'
|
|
15
|
-
export { debugEnabled, debugRecall } from './debug.js'
|
|
15
|
+
export { debugEnabled, debugRecall, readRecallDebug } from './debug.js'
|
package/lib/recall/injector.js
CHANGED
|
@@ -19,10 +19,10 @@ import { extractText } from '../memory/retrieval.js'
|
|
|
19
19
|
import { scanMemoryFiles } from '../memory/scan.js'
|
|
20
20
|
import { readCheckpoint } from '../memory/checkpoint.js'
|
|
21
21
|
import { warnLog } from '../memory/util.js'
|
|
22
|
-
import { selectRecall } from './select.js'
|
|
22
|
+
import { selectRecall, dedupeByFile } from './select.js'
|
|
23
23
|
import { buildCandidates, composeDelivery, composeStatus } from './compose.js'
|
|
24
24
|
import { composeRecallText } from './text.js'
|
|
25
|
-
import { debugEnabled, debugRecall } from './debug.js'
|
|
25
|
+
import { debugEnabled, debugRecall, recordInjection } from './debug.js'
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* @param {object} deps
|
|
@@ -116,16 +116,49 @@ export function createRecallInjector({ ctx, state, tools, taskIdOf }) {
|
|
|
116
116
|
})
|
|
117
117
|
const { cardBody, cardPart } = status
|
|
118
118
|
|
|
119
|
+
// ── 调试账(1.7.0)────────────────────────────────────────────────
|
|
120
|
+
// 控制台一行 JSON 与面板「工具 → 注入调试」共用这一份 payload。
|
|
121
|
+
// **只在开关打开时才构造 payload**:关着的时候 makeDebug 返回 null,payload
|
|
122
|
+
// 与缓冲记录都不会被建出来(零开销);debugEnabled() 每次现读,与 debug.js 同一条。
|
|
123
|
+
//
|
|
124
|
+
// 口径(面板上照此措辞,别让人误读):
|
|
125
|
+
// - `chars` = **这一步**真进正文的字节(不含状态卡、不含历史累计);
|
|
126
|
+
// - `sessionChars` = 本代累计正文字节(预算账);
|
|
127
|
+
// - `cardChars` = 这一步发出去的状态卡字节(没发就是 0);
|
|
128
|
+
// - `deduped` = 同题同命中集刚注过,这一步没重复追加 —— 面板上要解释成
|
|
129
|
+
// 「不是坏了,是去重生效」,否则每轮多步里大部分账看着都是空的。
|
|
130
|
+
const debugOn = debugEnabled()
|
|
131
|
+
const makeDebug = debugOn
|
|
132
|
+
? (outcome) => ({
|
|
133
|
+
at: nowMs,
|
|
134
|
+
sessionId,
|
|
135
|
+
query: typeof text === 'string' ? text.slice(0, 160) : '',
|
|
136
|
+
outcome,
|
|
137
|
+
deduped,
|
|
138
|
+
baseline: baselineTaken,
|
|
139
|
+
scanned: files.length,
|
|
140
|
+
hits: relevant.length,
|
|
141
|
+
scores: relevant.map(f => ({ file: f.file, score: Number(f.score.toFixed(4)) })),
|
|
142
|
+
body: dedupeByFile(bodyRanked).slice(0, 5).map(h => ({ file: h.file, score: Number((h.score ?? 0).toFixed(4)) })),
|
|
143
|
+
picked: picked.map(f => f.file),
|
|
144
|
+
injected: injectedFiles,
|
|
145
|
+
chars: Math.max(0, contentBytes - st.contentBytes),
|
|
146
|
+
sessionChars: contentBytes,
|
|
147
|
+
cardChars: cardPart ? Buffer.byteLength(cardPart, 'utf8') : 0,
|
|
148
|
+
skippedByBudget: skippedForBudget,
|
|
149
|
+
card: cardPart ? 'sent' : (status.cardText ? 'deduped-or-over-budget' : 'none'),
|
|
150
|
+
})
|
|
151
|
+
: () => null
|
|
152
|
+
const emitDebug = (payload) => {
|
|
153
|
+
if (!payload) return
|
|
154
|
+
debugRecall(payload)
|
|
155
|
+
recordInjection(payload)
|
|
156
|
+
}
|
|
157
|
+
|
|
119
158
|
// 正文与卡都没有 → 一个字都不注。注意这与旧行为不同:旧行为「正文池满」就到此为止,
|
|
120
159
|
// 现在只要卡池还有额度就会发出卡(说清范围、缺口与下一步动作)。
|
|
121
160
|
if (parts.length === 0 && !cardPart) {
|
|
122
|
-
|
|
123
|
-
debugRecall({
|
|
124
|
-
scanned: files.length, hits: relevant.length,
|
|
125
|
-
scores: relevant.map(f => [f.file, Number(f.score.toFixed(4))]),
|
|
126
|
-
picked: picked.map(f => f.file), injected: [], chars: contentBytes, skippedByBudget: skippedForBudget,
|
|
127
|
-
})
|
|
128
|
-
}
|
|
161
|
+
emitDebug(makeDebug('empty'))
|
|
129
162
|
return assembled
|
|
130
163
|
}
|
|
131
164
|
|
|
@@ -142,18 +175,7 @@ export function createRecallInjector({ ctx, state, tools, taskIdOf }) {
|
|
|
142
175
|
baselineDone: st.baselineDone || baselineTaken > 0,
|
|
143
176
|
})
|
|
144
177
|
|
|
145
|
-
|
|
146
|
-
debugRecall({
|
|
147
|
-
scanned: files.length,
|
|
148
|
-
hits: relevant.length,
|
|
149
|
-
scores: relevant.map(f => [f.file, Number(f.score.toFixed(4))]),
|
|
150
|
-
picked: picked.map(f => f.file),
|
|
151
|
-
injected: injectedFiles,
|
|
152
|
-
chars: contentBytes,
|
|
153
|
-
skippedByBudget: skippedForBudget,
|
|
154
|
-
card: cardPart ? 'sent' : (status.cardText ? 'deduped-or-over-budget' : 'none'),
|
|
155
|
-
})
|
|
156
|
-
}
|
|
178
|
+
emitDebug(makeDebug(parts.length > 0 ? 'injected' : 'card-only'))
|
|
157
179
|
|
|
158
180
|
const recallText = composeRecallText({
|
|
159
181
|
parts,
|
package/lib/typert.host.js
CHANGED
|
@@ -264,6 +264,27 @@ const rawWriteResultSchema = z.object({
|
|
|
264
264
|
// fileContentSchema,别把两者混用。
|
|
265
265
|
const rawFileContentSchema = z.string().max(MAX_RAW_BYTES)
|
|
266
266
|
|
|
267
|
+
// ── 注入调试账(1.7.0)─────────────────────────────────────────────────────
|
|
268
|
+
// 这份形状就是 `lib/recall/debug.js` 里 payload 的**逐字段**镜像。strict 会剥掉
|
|
269
|
+
// 未声明的字段,所以面板要看的每一样都必须写在这里 —— 少写一个,界面上就是
|
|
270
|
+
// 一个永远 undefined 的格子(与 R01 漏四个配置字段同一类错误)。
|
|
271
|
+
// 写法比相邻 schema 紧凑(一行多字段):本文件已有 ≤900 行的结构守卫
|
|
272
|
+
// (test/structure-test.mjs),加一条 remote 不能把文件顶爆。
|
|
273
|
+
const recallDebugScoreSchema = z.object({ 'file': z.string().readonly(), 'score': z.number().readonly() }).readonly()
|
|
274
|
+
const recallDebugRecordSchema = z.object({
|
|
275
|
+
'at': z.number().readonly(), 'sessionId': z.string().readonly(), 'query': z.string().readonly(),
|
|
276
|
+
// injected | card-only | empty:这一步到底注进去了什么
|
|
277
|
+
'outcome': z.string().readonly(), 'deduped': z.boolean().readonly(), 'baseline': z.number().readonly(),
|
|
278
|
+
'scanned': z.number().readonly(), 'hits': z.number().readonly(),
|
|
279
|
+
'scores': z.array(recallDebugScoreSchema).readonly(), 'body': z.array(recallDebugScoreSchema).readonly(),
|
|
280
|
+
'picked': z.array(z.string()).readonly(), 'injected': z.array(z.string()).readonly(),
|
|
281
|
+
// chars = 这一步正文;sessionChars = 本代累计;cardChars = 这一步的卡
|
|
282
|
+
'chars': z.number().readonly(), 'sessionChars': z.number().readonly(), 'cardChars': z.number().readonly(),
|
|
283
|
+
'skippedByBudget': z.number().readonly(), 'card': z.string().readonly(),
|
|
284
|
+
}).readonly()
|
|
285
|
+
// 开关关着时 records 是空数组、enabled 是 false —— 面板据此不显示入口。
|
|
286
|
+
const recallDebugResultSchema = z.object({ 'enabled': z.boolean().readonly(), 'records': z.array(recallDebugRecordSchema).readonly() }).readonly()
|
|
287
|
+
|
|
267
288
|
// ── autodream(夜间记忆整理)───────────────────────────────────────────────
|
|
268
289
|
// 配置的每一项都可写;lastResult 是一个「成功/失败两条分支共用」的结构,
|
|
269
290
|
// 所以除了三个必有字段外全是 optional —— 用 strict 校验两种形态的并集,
|
|
@@ -716,6 +737,17 @@ export const TYPERT = {
|
|
|
716
737
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#RawWriteResult', schema: rawWriteResultSchema, create: () => rawWriteResultSchema },
|
|
717
738
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
718
739
|
},
|
|
740
|
+
{
|
|
741
|
+
// 1.7.0:最近几次注入的账(只读;关着 SAGE_MEM_DEBUG 时 enabled=false)。
|
|
742
|
+
id: 'sage-mem#memory/recallDebug',
|
|
743
|
+
service: 'memory',
|
|
744
|
+
namespace: 'memory',
|
|
745
|
+
method: 'recallDebug',
|
|
746
|
+
invocation: { kind: 'direct' },
|
|
747
|
+
parameters: [],
|
|
748
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#RecallDebug', schema: recallDebugResultSchema, create: () => recallDebugResultSchema },
|
|
749
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
750
|
+
},
|
|
719
751
|
{
|
|
720
752
|
id: 'sage-mem#starmap/listStars',
|
|
721
753
|
service: 'starmap',
|
package/package.json
CHANGED