dsh-engram 0.5.1 → 0.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.
@@ -0,0 +1,64 @@
1
+ ## dsh-engram v0.6.0
2
+
3
+ 自 v0.5.1 以来:从「记忆插件」转向**上下文投资组合**——先把省 token 变成可审计的账本,
4
+ 再用账本纠正默认值、否决不划算的剪枝,并按确定性事件把记忆推给模型。
5
+
6
+ ### 功能
7
+
8
+ - **Context Ledger(上下文净收益账本)** — 每个模型请求记一次账:
9
+ `net = avoided − injected − rewriteChars × (1 − cacheDiscount)`。
10
+ 注入的 `[ENGRAM]`/`[RECALL]`/`[ESR]`/`[JIT]` 块按存活请求计成本;BoundaryPrune 剪掉的
11
+ 结果按后续请求计节省;每次剪枝/推送按前缀重写计价(默认缓存折价 0.1)。
12
+ `GET /api/dsh-engram/stats` 与 `/overview` 返回 `ledger`(totals / byWorkspace / byDay),
13
+ 落盘 `~/.dsh/engram/ledger.json`(原子写、按天滚动、delta flush)。
14
+ 设置键:`ledgerEnabled`(默认开)、`ledgerCacheDiscount`(默认 0.1)。
15
+ - **GUI 账本可视化** — 遥测页新增「Context Ledger · 上下文净收益」面板(净 token 大数字、
16
+ 节省/注入/重写罚金三卡、近 14 天红绿柱、JIT 推送次数);记忆概览新增「上下文净 token」卡。
17
+ - **BoundaryPrune 默认开 + 账本否决** — `boundaryPrune` 默认 `true`、`resultPack` 默认 `true`
18
+ (默认剪枝由归档兜底、无损);新增 `boundaryPruneAdaptive`(默认开):某 workspace 真实剪枝
19
+ 后净收益为负且 ≥3 次时,账本自动否决后续剪枝(fail-open,日志一行)。
20
+ - **JIT 推送(v0.6-C)** — 三类确定性事件,命中已有记忆/任务才推,append-only、每会话 ≤3 行、
21
+ 每行 ≤220 字符、无命中沉默:错误复发(同 `cmd:` 第 2 次失败 + 已有 error/procedure)、
22
+ 文件焦点(同文件连续第 2 次编辑 + filePath/dir 锚定记忆)、goal 完成(有 active ESR 任务)。
23
+ 新增 `[JIT]` section(order 40.75)、`/stats.jit` 计数、`jitEnabled` 开关。
24
+ - **设置卡** — 新增「剪枝自适应」「JIT 推送」两个开关。
25
+
26
+ ### 破坏性变更(默认值纠偏)
27
+
28
+ | 键 | v0.5.1 | v0.6.0 |
29
+ |---|---|---|
30
+ | `boundaryPrune` | `false` | **`true`** |
31
+ | `boundaryPruneAdaptive` | — | **`true`** |
32
+ | `resultPack` | `false` | **`true`** |
33
+ | `gcReplacesCompaction` | `true` | **`false`**(opt-in) |
34
+ | `autoWebCompaction` | `true` | **`false`**(opt-in) |
35
+
36
+ - 默认剪枝现在**开启**,但被剪中段默认归档到 `resultpack/`(7 天),可用
37
+ `result_recall(callId)` 召回。要回到纯记忆模式:`boundaryPrune:false`(或
38
+ `resultPack:false` 接受不可召回)。
39
+ - Context GC 接管改为 **opt-in**。已接管的 web 预设保持 wired,但默认挂宿主
40
+ `BasicCompactionEngine`(DSH 自己的 LLM 摘要),除非显式打开
41
+ `gcReplacesCompaction:true`(web 还需 `autoWebCompaction:true`)。
42
+ - 升级后需重启 `dsh web`;账本从下一次会话开始累计。
43
+
44
+ ### 其他
45
+
46
+ - 测试 **359/359**(新增 ledger 13 例、jit 9 例、adaptive gate 4 例、settings/API/client 渲染用例);
47
+ `npm run eval` BENCH PASS;`npm run typecheck` 通过;`lib/client.js` 重建。
48
+ - 文档:`docs/PROPOSAL-evolution.zh.md`(演进提案)、中英 README、`docs/CONTEXT-GC-GUIDE.zh.md`
49
+ (Context GC 改为 opt-in 的 4 步启用)。
50
+ - 真实实例冒烟(`dsh 0.2.0-rc.2`,临时将 profile 的 `dsh-engram` 指向工作树后还原):
51
+ `/overview` 返回 `ledger`、`/stats` 返回 `jit`,defaults 与上表一致,`compaction.host = default`。
52
+
53
+ ## DSH 版本支持
54
+
55
+ - **支持的 DSH 范围**:`>=0.1.2-alpha.2 <0.3.0-0`
56
+ - **peer 声明**:
57
+ - `@deepseek-ai/dsh-compaction`: `>=0.1.2-alpha.2 <0.3.0-0`
58
+ - `@deepseek-ai/dsh-compaction-basic`: `>=0.1.2-alpha.2 <0.3.0-0`
59
+ - `@deepseek-ai/dsh-llm`: `>=0.1.2-alpha.2 <0.3.0-0`
60
+ - `@deepseek-ai/dsh-settings`: `>=0.1.2-alpha.2 <0.3.0-0`
61
+ - `@deepseek-ai/dsh-storage-domain`: `>=0.1.2-alpha.2 <0.3.0-0`
62
+ - **验证版本**:`dsh 0.2.0-rc.2`
63
+ - **最后验证日期**:2026-10-19
64
+ - **安装方式已验证**:`dsh plugin --profile web add dsh-engram`
@@ -0,0 +1,57 @@
1
+ ## dsh-engram v0.7.0
2
+
3
+ 自 v0.6.0 以来:把「记忆」从**事件记录**推进到**可维护的当前真相**——会话结束自动沉淀
4
+ episode、新会话给恢复简报、`esr_node` 升级为实体页、文件锚点让过期真相可被检测。
5
+
6
+ ### 功能
7
+
8
+ - **会话 episode(v0.7-A)** — `session/disposed` 时用零 LLM 规则把事件日志压成一条紧凑
9
+ 纪要:改过哪些文件、跑过哪些命令、哪些失败后修复(`cmd×N→fixed`)、git commit 主题、
10
+ 仍挂着的 todo。只在有实质内容时写(纯 `ls` 会话不写),**永不进 `[ENGRAM]` 索引**,
11
+ 每个 workspace 保留最近 20 条。
12
+ - **恢复简报(v0.7-D)** — 新会话第一次组装时,取最近 7 天最新的 episode,注入
13
+ ≤240 字符的 `[RESUME]` 行 + `engram_detail` 指针;与 `[ENGRAM]`/`[RECALL]` 一起按会话冻结。
14
+ 没有近期 episode 就不注入。
15
+ - **实体页 + 实体卡(v0.7-B)** — `esr_node` 升级为可读页面:`truth`(preference 优先、
16
+ superseded 剔除)/ `timeline` / `open loops`(active 任务 + 证据缺口)/ `conflicts`
17
+ (无序对去重)/ `relations`。`[ENGRAM]` 为活跃实体前置最多 2 张一行卡
18
+ (truth 首条 + mem/open + 下钻指针),被代表的实体不再重复平铺行;
19
+ `engram_detail(entity="<name|id>")` 返回完整页。
20
+ - **可重验证锚点(v0.7-C)** — 带 `filePath` 的记忆在写入时记录
21
+ `{path, mtimeMs, size, hash?}`(≤256KB 存 sha256);会话启动时有界重验证(≤40 条),
22
+ 文件改动/缺失 → 打 `stale` 标签,索引与实体 truth 不再展示,timeline 标注 `· stale`,
23
+ `engram_detail` 仍可重取(不删除)。
24
+ - **分层 TTL(v0.7-C)** — `preference` / `procedure` 永久;`episode` 14 天;`error` 60 天;
25
+ 其余按 `expireDays`(默认 180 天)。仍然只有 GC 归档、没有硬删。
26
+
27
+ ### 兼容性说明
28
+
29
+ - 存储 schema 只**新增**字段:`memories.anchor`(nullable,默认 null)与新的 memory kind
30
+ `episode`。旧数据读取自动补默认值,无需迁移;真实实例冒烟用 256 条既有记忆验证通过。
31
+ - 只有**新写入**且带 `filePath` 的记忆会落锚;既有记忆不会被回溯锚定(避免一次性把历史
32
+ 事实全部标 stale)。需要时可后续加显式 backfill 命令。
33
+ - 新增设置键(均默认开):`episodeEnabled`、`resumeBriefEnabled`、`entityCardsEnabled`、
34
+ `anchorRevalidate`;隐藏可调 `episodeMaxPerWorkspace`、`resumeBriefMaxAgeDays`、
35
+ `resumeBriefMaxChars`、`entityCardsMax`、`entityCardChars`、`episodeTtlDays`、
36
+ `errorTtlDays`。GUI 旋钮总数 34。
37
+ - 升级后需重启 `dsh web`;episode 要等一个会话结束、resume brief 在下一个新会话生效。
38
+
39
+ ### 其他
40
+
41
+ - 测试 **381/381**(新增 episode 9 例、entity 7 例、anchor 6 例);`npm run eval` BENCH PASS;
42
+ `npm run typecheck` 通过;`lib/client.js` 重建。
43
+ - 文档:中英 README(episode / 实体页 / 锚点三节)、`docs/PROPOSAL-evolution.zh.md`
44
+ (v0.7 A/B/C/D 状态)、设置卡新增「会话纪要 / 恢复简报 / 实体卡」开关。
45
+
46
+ ## DSH 版本支持
47
+
48
+ - **支持的 DSH 范围**:`>=0.1.2-alpha.2 <0.3.0-0`
49
+ - **peer 声明**:
50
+ - `@deepseek-ai/dsh-compaction`: `>=0.1.2-alpha.2 <0.3.0-0`
51
+ - `@deepseek-ai/dsh-compaction-basic`: `>=0.1.2-alpha.2 <0.3.0-0`
52
+ - `@deepseek-ai/dsh-llm`: `>=0.1.2-alpha.2 <0.3.0-0`
53
+ - `@deepseek-ai/dsh-settings`: `>=0.1.2-alpha.2 <0.3.0-0`
54
+ - `@deepseek-ai/dsh-storage-domain`: `>=0.1.2-alpha.2 <0.3.0-0`
55
+ - **验证版本**:`dsh 0.2.0-rc.2`
56
+ - **最后验证日期**:2026-10-19
57
+ - **安装方式已验证**:`dsh plugin --profile web add dsh-engram`
package/lib/anchor.js ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * dsh-engram revalidatable file anchors (PROPOSAL-evolution §4 v0.7-C).
3
+ *
4
+ * "The past is treated as truth" is the strongest critique against memory
5
+ * plugins: a fact written last week may describe code that changed yesterday.
6
+ * An anchor makes staleness DETECTABLE instead of trusted:
7
+ *
8
+ * { path, mtimeMs, size, hash?, at }
9
+ *
10
+ * - `makeFileAnchor` records the file's identity when a memory is written
11
+ * (sha256 for small files, mtime+size for every file — cheap default);
12
+ * - `anchorStatus` re-stats (and re-hashes when a hash exists) and returns
13
+ * `valid | changed | missing`;
14
+ * - `collectStaleAnchors` scans a bounded set of active memories so the index
15
+ * / entity truth can drop rows whose source moved, and the caller can tag
16
+ * them `stale` (never deleted — still re-fetchable via engram_detail).
17
+ *
18
+ * Zero LLM, fail-contained: any filesystem error reads as "missing", never
19
+ * throws into a render path.
20
+ */
21
+
22
+ import { createHash } from "node:crypto";
23
+ import { readFileSync, statSync } from "node:fs";
24
+ import path from "node:path";
25
+
26
+ const DEFAULT_MAX_HASH_BYTES = 256 * 1024;
27
+
28
+ function hashFile(file, size) {
29
+ try {
30
+ return createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, 32);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function absolute(filePath, baseDir) {
37
+ const raw = String(filePath ?? "").trim();
38
+ if (raw.length === 0) return "";
39
+ if (path.isAbsolute(raw)) return raw;
40
+ return typeof baseDir === "string" && baseDir.length > 0 ? path.resolve(baseDir, raw) : path.resolve(raw);
41
+ }
42
+
43
+ /**
44
+ * Build an anchor for an existing file, or null when it cannot be stat'ed.
45
+ *
46
+ * @param {string} filePath file path as stored on the memory
47
+ * @param {object} [opts]
48
+ * @param {string} [opts.baseDir] directory for relative paths (the workspace)
49
+ * @param {number} [opts.now]
50
+ * @param {number} [opts.maxHashBytes] hash only files up to this size
51
+ */
52
+ export function makeFileAnchor(filePath, { baseDir = "", now = Date.now(), maxHashBytes = DEFAULT_MAX_HASH_BYTES } = {}) {
53
+ const abs = absolute(filePath, baseDir);
54
+ if (abs.length === 0) return null;
55
+ try {
56
+ const stat = statSync(abs);
57
+ if (!stat.isFile()) return null;
58
+ return {
59
+ path: abs,
60
+ mtimeMs: stat.mtimeMs,
61
+ size: stat.size,
62
+ hash: stat.size <= maxHashBytes ? hashFile(abs, stat.size) : null,
63
+ at: now,
64
+ };
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Revalidate one anchor: `valid` | `changed` | `missing` (`none` when absent).
72
+ */
73
+ export function anchorStatus(anchor) {
74
+ if (anchor === null || anchor === void 0 || typeof anchor.path !== "string" || anchor.path.length === 0) {
75
+ return "none";
76
+ }
77
+ let stat;
78
+ try {
79
+ stat = statSync(anchor.path);
80
+ } catch {
81
+ return "missing";
82
+ }
83
+ if (!stat.isFile()) return "missing";
84
+ if (stat.size !== anchor.size || stat.mtimeMs !== anchor.mtimeMs) {
85
+ // mtime can survive content changes only in exotic cases; when a hash was
86
+ // recorded, let it decide before declaring the file changed.
87
+ if (typeof anchor.hash !== "string") return "changed";
88
+ const hash = hashFile(anchor.path, stat.size);
89
+ return hash === anchor.hash ? "valid" : "changed";
90
+ }
91
+ if (typeof anchor.hash === "string") {
92
+ const hash = hashFile(anchor.path, stat.size);
93
+ return hash === anchor.hash ? "valid" : "changed";
94
+ }
95
+ return "valid";
96
+ }
97
+
98
+ /** Is this memory's source still the same file it was written against? */
99
+ export function memoryIsStale(memory) {
100
+ if (memory === null || typeof memory !== "object") return false;
101
+ const status = anchorStatus(memory.anchor);
102
+ return status === "changed" || status === "missing";
103
+ }
104
+
105
+ /**
106
+ * Bounded scan of active anchored memories whose source moved/vanished.
107
+ * Returns [{ id, status, path }] and never throws.
108
+ */
109
+ export function collectStaleAnchors(store, workspace, { limit = 40 } = {}) {
110
+ if (store === null || typeof store.listMemories !== "function") return [];
111
+ const cap = Number.isFinite(limit) && limit > 0 ? Math.trunc(limit) : 40;
112
+ const rows = store.listMemories(workspace, 200);
113
+ const out = [];
114
+ for (const memory of rows) {
115
+ if (out.length >= cap) break;
116
+ if (memory?.anchor === null || memory?.anchor === void 0) continue;
117
+ if (Array.isArray(memory.tags) && memory.tags.includes("stale")) continue;
118
+ const status = anchorStatus(memory.anchor);
119
+ if (status === "changed" || status === "missing") {
120
+ out.push({ id: memory.id, status, path: memory.anchor.path });
121
+ }
122
+ }
123
+ return out;
124
+ }
package/lib/api.js CHANGED
@@ -163,6 +163,7 @@ export function makeEngramRoutes(service) {
163
163
  captures: service.captureStats ?? { total: 0, git: 0, file: 0, error: 0, preference: 0 },
164
164
  gc: service.gcStats ?? { lastRun: 0, archivedMemories: 0, archivedTasks: 0, removedLinks: 0 },
165
165
  activation: service.packTelemetry ? service.packTelemetry() : { boundaryPrune: null, recall: null },
166
+ ledger: service.ledger ? service.ledger() : null,
166
167
  compaction: service.compactionStatus ?? { host: "unknown", web: { action: "unknown", presets: [] } },
167
168
  config: service.config ?? {},
168
169
  anonymous: {
@@ -517,6 +518,10 @@ export function makeEngramRoutes(service) {
517
518
  // activation record (fires actually landed on this profile) + the
518
519
  // [RECALL] injection-funnel leg (blocks actually injected into sessions).
519
520
  activation: service.packTelemetry ? service.packTelemetry() : { boundaryPrune: null, recall: null },
521
+ // Context Ledger (PROPOSAL-evolution §4 v0.6-A): spent vs saved context,
522
+ // net of prefix rewrites. Persisted rollup + unflushed live sessions.
523
+ ledger: service.ledger ? service.ledger() : null,
524
+ jit: service.jitStats ? service.jitStats() : null,
520
525
  });
521
526
  };
522
527
 
@@ -130,7 +130,8 @@ export function expectedSessionRequests(requestsSoFar, history, workspace, floor
130
130
 
131
131
  /** Install the boundary-prune listener. Returns a disposer. */
132
132
  export function installBoundaryPrune(ctx, config, log, hooks = {}, opts = {}) {
133
- const enabled = config?.boundaryPrune === true;
133
+ // Default ON (v0.6 default correction): explicit `false` is the opt-out.
134
+ const enabled = config?.boundaryPrune !== false;
134
135
  if (!enabled) return () => {};
135
136
  const pruner = ctx.get?.("toolResultPruner");
136
137
  if (!pruner || typeof pruner.pruneSession !== "function") {
@@ -142,6 +143,13 @@ export function installBoundaryPrune(ctx, config, log, hooks = {}, opts = {}) {
142
143
  : DEFAULT_MIN_REMAINING;
143
144
  // History is injectable for tests; production reads the real session corpus.
144
145
  const history = opts?.history ?? workspaceHistory();
146
+ // Adaptive safety valve (PROPOSAL-evolution v0.6-B): the Context Ledger can
147
+ // veto further fires when a workspace's own accounting says pruning is net
148
+ // negative after enough real fires. The repayment gate still runs first;
149
+ // this is purely evidence-based off, never speculative on.
150
+ const adaptive = config?.boundaryPruneAdaptive !== false;
151
+ const allowFire = typeof opts?.allowFire === "function" ? opts.allowFire : () => true;
152
+ const blockedNotified = new Set();
145
153
 
146
154
  // Optional threshold override (ResultPack L0, T2048 landing): the host pruner
147
155
  // ships a fixed threshold (8192) that the plugin cannot change in place. When
@@ -235,6 +243,24 @@ export function installBoundaryPrune(ctx, config, log, hooks = {}, opts = {}) {
235
243
 
236
244
  /** Shared fire path for every boundary source (event + step-cadence). */
237
245
  const fireBoundary = (session, boundary, state, expectedTotal) => {
246
+ if (adaptive) {
247
+ const workspace = wk(session?.header?.cwd);
248
+ let allowed = true;
249
+ try {
250
+ allowed = allowFire(workspace) !== false;
251
+ } catch {
252
+ allowed = true; // a gate bug must never disable pruning
253
+ }
254
+ if (!allowed) {
255
+ if (!blockedNotified.has(workspace)) {
256
+ blockedNotified.add(workspace);
257
+ log?.info?.(
258
+ `engram boundary-prune: ledger gate holds for ${workspace} (net negative) — skipping further prunes`,
259
+ );
260
+ }
261
+ return;
262
+ }
263
+ }
238
264
  const result = activePruner.pruneSession(session);
239
265
  const prunedNow = result?.pruned?.length ?? 0;
240
266
  if (prunedNow > 0) {
package/lib/client.js CHANGED
@@ -2023,6 +2023,11 @@ function pct(v) {
2023
2023
  if (v === null || v === void 0) return "\u2013";
2024
2024
  return `${Math.round(v * 100)}%`;
2025
2025
  }
2026
+ function tokenCount(n) {
2027
+ const sign = n > 0 ? "+" : n < 0 ? "\u2212" : "";
2028
+ const abs = Math.abs(n);
2029
+ return abs >= 1e3 ? `${sign}${(abs / 1e3).toFixed(1)}k` : `${sign}${abs}`;
2030
+ }
2026
2031
  function EngramTelemetry({ api, workspace }) {
2027
2032
  const { dark, vars } = useEngramTheme();
2028
2033
  const [stats, setStats] = (0, import_react5.useState)(null);
@@ -2065,6 +2070,10 @@ function EngramTelemetry({ api, workspace }) {
2065
2070
  }, [stats]);
2066
2071
  const dose = stats && stats.ratios ? stats.ratios : null;
2067
2072
  const sampleText = stats && stats.ratios && stats.ratios.calls < 10 ? `\u6837\u672C\u4E0D\u8DB3\uFF08${stats.ratios.calls} \u6B21\uFF09\uFF0C\u6BD4\u4F8B\u4EC5\u4F9B\u53C2\u8003` : null;
2073
+ const ledger = stats?.ledger ?? null;
2074
+ const ledgerBucket = ledger === null ? null : workspace === "" ? ledger.totals : ledger.byWorkspace?.[workspace] ?? null;
2075
+ const ledgerDays = ledger === null || workspace !== "" ? [] : [...ledger.byDay ?? []].slice(0, 14).reverse();
2076
+ const ledgerMax = Math.max(1, ...ledgerDays.map((d) => Math.abs(d.netTokens)));
2068
2077
  const hb2 = {
2069
2078
  root: { display: "flex", flexDirection: "column", gap: 12, ...vars },
2070
2079
  head: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
@@ -2178,6 +2187,68 @@ function EngramTelemetry({ api, workspace }) {
2178
2187
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u5931\u8D25\u6B21\u6570" })
2179
2188
  ] })
2180
2189
  ] }),
2190
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.panel, children: [
2191
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.panelTitle, display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" }, children: [
2192
+ "Context Ledger \xB7 \u4E0A\u4E0B\u6587\u51C0\u6536\u76CA",
2193
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: hb2.sub, children: workspace === "" ? "\u5168\u90E8\u5DE5\u4F5C\u533A" : workspace })
2194
+ ] }),
2195
+ !ledger ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.sub, children: "\u8D26\u672C\u672A\u542F\u7528\uFF08`ledgerEnabled:false`\uFF09\u6216\u5BBF\u4E3B API \u672A\u5C31\u7EEA\u3002" }) : !ledgerBucket ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.sub, children: "\u8BE5\u5DE5\u4F5C\u533A\u8FD8\u6CA1\u6709\u8D26\u672C\u8BB0\u5F55\u3002" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2196
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "baseline", gap: 10, flexWrap: "wrap" }, children: [
2197
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { ...hb2.statNum, fontSize: 24, color: ledgerBucket.netTokens >= 0 ? "#059669" : "#dc2626" }, children: tokenCount(ledgerBucket.netTokens) }),
2198
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: hb2.statLabel, children: "\u51C0 token\uFF08\u8282\u7701 \u2212 \u6CE8\u5165 \u2212 \u91CD\u5199\u7F5A\u91D1\uFF09" }),
2199
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: hb2.gaugeSub, children: [
2200
+ "\u8BF7\u6C42 ",
2201
+ ledgerBucket.requests,
2202
+ " \xB7 \u4F1A\u8BDD ",
2203
+ ledgerBucket.sessions,
2204
+ " \xB7 open ",
2205
+ ledger.openSessions,
2206
+ stats.jit ? ` \xB7 JIT \u63A8\u9001 ${stats.jit.pushes}` : ""
2207
+ ] })
2208
+ ] }),
2209
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.grid, marginTop: 8 }, children: [
2210
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
2211
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#059669" }, children: tokenCount(ledgerBucket.avoidedTokens) }),
2212
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u8282\u7701 \xB7 \u526A\u679D/\u538B\u7F29" })
2213
+ ] }),
2214
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
2215
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#d97706" }, children: tokenCount(-ledgerBucket.injectedTokens) }),
2216
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u6CE8\u5165\u6210\u672C \xB7 \u7D22\u5F15/\u53EC\u56DE/ESR" })
2217
+ ] }),
2218
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
2219
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#dc2626" }, children: tokenCount(-Math.round(ledgerBucket.rewritesCostChars / ledger.charsPerToken)) }),
2220
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statLabel, children: [
2221
+ "\u91CD\u5199\u7F5A\u91D1 \xB7 ",
2222
+ ledgerBucket.rewriteCount,
2223
+ " \u6B21"
2224
+ ] })
2225
+ ] })
2226
+ ] }),
2227
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.sub, marginTop: 6 }, children: [
2228
+ "\u53E3\u5F84\uFF1Achars/",
2229
+ ledger.charsPerToken,
2230
+ " \u4F30\u7B97 \xB7 \u7F13\u5B58\u6298\u4EF7 ",
2231
+ Math.round(ledger.cacheDiscount * 100),
2232
+ "% \uFF08\u91CD\u5199\u6309 ",
2233
+ Math.round((1 - ledger.cacheDiscount) * 100),
2234
+ "% \u8BA1\uFF09\xB7 \u672A\u8BA1\u6A21\u578B\u8D28\u91CF\u5DEE"
2235
+ ] }),
2236
+ ledgerDays.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { width: 460, height: 96, viewBox: "0 0 460 96", style: { display: "block", maxWidth: "100%", marginTop: 8 }, children: [
2237
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: 0, y1: 48, x2: 460, y2: 48, stroke: dark ? "rgba(148,163,184,.35)" : "rgba(107,114,128,.35)", strokeDasharray: "3 3" }),
2238
+ ledgerDays.map((d, i) => {
2239
+ const w = Math.max(6, Math.min(22, 420 / ledgerDays.length - 4));
2240
+ const x = 8 + i * (w + 4);
2241
+ const h = Math.abs(d.netTokens) / ledgerMax * 38;
2242
+ const positive = d.netTokens >= 0;
2243
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("g", { children: [
2244
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x, y: positive ? 48 - h : 48, width: w, height: Math.max(1, h), rx: 2, fill: positive ? "#059669" : "#dc2626", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("title", { children: `${d.day} \xB7 net ${d.netTokens} tok \xB7 \u8BF7\u6C42 ${d.requests}` }) }),
2245
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("text", { x: x + w / 2, y: 92, fontSize: 8, fill: dark ? "#8b93a7" : "#6b7280", textAnchor: "middle", children: (d.day ?? "").slice(5).replace("-", "/") })
2246
+ ] }, d.day);
2247
+ })
2248
+ ] }),
2249
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.sub, marginTop: 4 }, children: "\u7EFF = \u5F53\u65E5\u51C0\u7701 \xB7 \u7EA2 = \u5F53\u65E5\u51C0\u4E8F\uFF08\u6309\u5929\u6EDA\u52A8\uFF0C\u6700\u8FD1 14 \u5929\uFF09" })
2250
+ ] })
2251
+ ] }),
2181
2252
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.panel, children: [
2182
2253
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.panelTitle, children: "\u8FD1 14 \u5929\u6BCF\u65E5\u6D3B\u52A8 \xB7 mem vs esr" }),
2183
2254
  daily.rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.sub, children: "\u8FD8\u6CA1\u6709\u6309\u5929\u6570\u636E\uFF08\u5DE5\u5177\u8C03\u7528\u4F1A\u5B9E\u65F6\u7D2F\u8BA1\uFF09\u3002" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { width: BW, height: BH, viewBox: `0 0 ${BW} ${BH}`, style: { display: "block", maxWidth: "100%" }, children: daily.rows.map((d, i) => {
@@ -2636,8 +2707,14 @@ function EngramDetail({ target, api, tasks, nodes, links, memories = [], onClose
2636
2707
 
2637
2708
  // client/src/EngramSection.tsx
2638
2709
  var import_jsx_runtime7 = require("react/jsx-runtime");
2710
+ function signedTokens(n) {
2711
+ const sign = n > 0 ? "+" : n < 0 ? "\u2212" : "";
2712
+ const abs = Math.abs(n);
2713
+ return abs >= 1e3 ? `${sign}${(abs / 1e3).toFixed(1)}k` : `${sign}${abs}`;
2714
+ }
2639
2715
  var KIND_COLORS2 = {
2640
2716
  preference: "#b45309",
2717
+ episode: "#0e7490",
2641
2718
  decision: "#2563eb",
2642
2719
  error: "#dc2626",
2643
2720
  procedure: "#7c3aed",
@@ -3140,6 +3217,7 @@ function EngramSection({ api, t }) {
3140
3217
  const memNum = wsCounts ? wsCounts.memories : overview.totals.memories;
3141
3218
  const taskNum = wsCounts ? wsCounts.tasks : overview.totals.tasks;
3142
3219
  const linkNum = wsCounts ? wsCounts.links : overview.totals.links;
3220
+ const ledgerBucket = overview.ledger ? workspace && overview.ledger.byWorkspace[workspace] || overview.ledger.totals : null;
3143
3221
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: s2.stats, children: [
3144
3222
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatCard, { num: String(memNum), label: wsCounts ? "\u8BB0\u5FC6 (active)" : "\u8BB0\u5FC6 (active, \u5168\u5C40)" }),
3145
3223
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatCard, { num: String(taskNum), label: wsCounts ? "\u4EFB\u52A1 (active)" : "\u4EFB\u52A1 (active, \u5168\u5C40)" }),
@@ -3161,6 +3239,13 @@ function EngramSection({ api, t }) {
3161
3239
  num: String(gc.archivedMemories + gc.archivedTasks),
3162
3240
  label: `GC \u5DF2\u5F52\u6863 \xB7 \u94FE\u63A5-${gc.removedLinks}`
3163
3241
  }
3242
+ ),
3243
+ ledgerBucket && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3244
+ StatCard,
3245
+ {
3246
+ num: signedTokens(ledgerBucket.netTokens),
3247
+ label: `\u4E0A\u4E0B\u6587\u51C0 token \xB7 \u8BF7\u6C42 ${ledgerBucket.requests}`
3248
+ }
3164
3249
  )
3165
3250
  ] });
3166
3251
  })(),
@@ -3613,10 +3698,15 @@ var import_jsx_runtime8 = require("react/jsx-runtime");
3613
3698
  var COMMON_FIELDS = [
3614
3699
  { key: "autoCapture", label: "\u81EA\u52A8\u6355\u83B7", hint: "\u96F6 LLM \u4ECE\u5DE5\u5177\u7ED3\u679C\u63D0\u53D6\u8BB0\u5FC6\uFF08git/\u5173\u952E\u6587\u4EF6/\u9519\u8BEF\uFF09", kind: "bool" },
3615
3700
  { key: "autoCapturePreferences", label: "\u504F\u597D\u6355\u83B7", hint: "\u7528\u6237\u7EA0\u6B63/\u504F\u597D\u5B66\u4E60\uFF1A\u68C0\u6D4B\u6D88\u606F\u91CC\u7684\u300C\u4E0D\u8981\u7528X/\u6539\u7528Y/prefer/never use\u300D\u53E5\u5F0F\uFF0C\u81EA\u52A8\u6C89\u6DC0\u4E3A preference \u8BB0\u5FC6\u5E76\u7F6E\u9876 [ENGRAM]\uFF08correx/PAHF \u5F0F\uFF0C\u96F6 LLM\uFF09", kind: "bool" },
3616
- { key: "boundaryPrune", label: "\u8FB9\u754C\u526A\u679D", hint: "ResultPack L1\uFF1Atodo \u5B8C\u6210/goal \u7EC8\u6001\u8FB9\u754C\u4E3B\u52A8\u526A\u6389\u8D85\u9608\u503C\u65E7\u5DE5\u5177\u7ED3\u679C\uFF08DSH \u786E\u5B9A\u6027 surface-replace\u3001\u507F\u8FD8\u95E8\u3001\u96F6 LLM\uFF1B\u9ED8\u8BA4\u5173\uFF09", kind: "bool" },
3701
+ { key: "boundaryPrune", label: "\u8FB9\u754C\u526A\u679D", hint: "ResultPack L1\uFF1Atodo \u5B8C\u6210/goal \u7EC8\u6001\u8FB9\u754C\u4E3B\u52A8\u526A\u6389\u8D85\u9608\u503C\u65E7\u5DE5\u5177\u7ED3\u679C\uFF08DSH \u786E\u5B9A\u6027 surface-replace\u3001\u507F\u8FD8\u95E8\u3001\u96F6 LLM\uFF1B\u9ED8\u8BA4\u5F00\uFF0C\u5173=opt-out\uFF09", kind: "bool" },
3702
+ { key: "boundaryPruneAdaptive", label: "\u526A\u679D\u81EA\u9002\u5E94", hint: "\u8D26\u672C\u5426\u51B3\uFF1A\u8BE5 workspace \u771F\u5B9E\u526A\u679D\u540E\u51C0\u6536\u76CA\u4E3A\u8D1F\uFF08\u22653 \u6B21\uFF09\u65F6\u81EA\u52A8\u505C\u6B62\u7EE7\u7EED\u526A\uFF1B\u8BC1\u636E\u53EA\u80FD\u5173\u3001\u4E0D\u4F1A\u81EA\u884C\u5F00", kind: "bool" },
3617
3703
  { key: "boundaryPruneThresholdChars", label: "\u526A\u679D\u9608\u503C(\u5B57\u7B26)", hint: "\u8986\u76D6\u5BBF\u4E3B 8192\uFF1A\u8BBE 2048 \u53EF\u66F4\u65E9\u5F00\u526A\uFF0C\u5E76\u914D L2 \u5F52\u6863\u627E\u56DE\u88AB\u526A\u4E2D\u6BB5\uFF1B\u7559\u7A7A=\u5BBF\u4E3B\u9ED8\u8BA4\u3002\u5EFA\u8BAE\u540C\u65F6\u628A\u4FDD\u7559\u8FB9\u7F18\u8C03\u5C0F\uFF08\u5982 1024/256\uFF09\u4EE5\u83B7\u5F97\u6700\u5927\u8282\u7701", kind: "num", min: 256, max: 16384, step: 256 },
3618
- { key: "resultPack", label: "\u7ED3\u679C\u5F52\u6863\u53EC\u56DE", hint: "ResultPack L2\uFF1A\u526A\u6389\u7684\u4E2D\u6BB5\u8131\u654F\u5F52\u6863\uFF0C\u8282\u70B9\u5C3E\u90E8\u9644 handle\uFF0C\u6A21\u578B\u53EF\u7528 result_recall(callId) \u6309\u9875\u53EC\u56DE\uFF08\u5B8C\u6574 ObservationPack\uFF1B\u9700\u4E0E\u8FB9\u754C\u526A\u679D\u4E00\u8D77\u5F00\uFF09", kind: "bool" },
3704
+ { key: "resultPack", label: "\u7ED3\u679C\u5F52\u6863\u53EC\u56DE", hint: "ResultPack L2\uFF1A\u526A\u6389\u7684\u4E2D\u6BB5\u8131\u654F\u5F52\u6863\uFF0C\u8282\u70B9\u5C3E\u90E8\u9644 handle\uFF0C\u6A21\u578B\u53EF\u7528 result_recall(callId) \u6309\u9875\u53EC\u56DE\uFF08\u5B8C\u6574 ObservationPack\uFF1B\u9ED8\u8BA4\u5F00\uFF0C\u4FDD\u8BC1\u9ED8\u8BA4\u526A\u679D\u65E0\u635F\uFF1B\u9700\u4E0E\u8FB9\u754C\u526A\u679D\u4E00\u8D77\u5F00\uFF09", kind: "bool" },
3619
3705
  { key: "autoRecallOnStart", label: "\u4F1A\u8BDD\u542F\u52A8\u81EA\u52A8\u53EC\u56DE", hint: "\u65B0\u4F1A\u8BDD\u6309\u9996\u6761\u6D88\u606F\u81EA\u52A8\u628A\u6700\u76F8\u5173\u8BB0\u5FC6\u6CE8\u5165 [RECALL] \u5757\uFF08\u65E0\u9700\u6A21\u578B\u60F3\u8D77\u8C03 engram_recall\uFF09\uFF1B\u5173\u6389=\u56DE\u5230\u7EAF\u62C9\u53D6", kind: "bool" },
3706
+ { key: "jitEnabled", label: "JIT \u63A8\u9001", hint: "\u9519\u8BEF\u590D\u53D1 / \u6587\u4EF6\u7126\u70B9 / goal \u5B8C\u6210\u65F6\uFF0C\u628A\u5DF2\u6709\u8BB0\u5FC6\u538B\u6210\u4E00\u884C\u63A8\u8FDB\u4E0A\u4E0B\u6587\uFF08append-only\u3001\u6BCF\u4F1A\u8BDD\u22643 \u884C\u3001\u65E0\u547D\u4E2D\u6C89\u9ED8\uFF1B\u6BCF\u6B21\u63A8\u9001\u6309\u524D\u7F00\u91CD\u5199\u8BA1\u5165\u8D26\u672C\uFF09", kind: "bool" },
3707
+ { key: "episodeEnabled", label: "\u4F1A\u8BDD\u7EAA\u8981", hint: "\u4F1A\u8BDD\u7ED3\u675F\u65F6\u7528\u96F6 LLM \u89C4\u5219\u751F\u6210\u4E00\u6761 episode\uFF08\u6587\u4EF6/\u547D\u4EE4/\u5931\u8D25\u2192\u4FEE\u590D/\u51B3\u7B56/\u672A\u95ED\u73AF\uFF09\uFF0C\u4E0D\u8FDB\u7D22\u5F15\uFF0C\u4F9B\u6062\u590D\u7B80\u62A5\u4F7F\u7528\uFF1B\u6BCF workspace \u4FDD\u7559\u6700\u8FD1 20 \u6761", kind: "bool" },
3708
+ { key: "resumeBriefEnabled", label: "\u6062\u590D\u7B80\u62A5", hint: "\u65B0\u4F1A\u8BDD\u542F\u52A8\u65F6\u6CE8\u5165\u6700\u8FD1 7 \u5929\u7684\u6700\u65B0 episode\uFF08\u2264240 \u5B57\u7B26 + engram_detail \u6307\u9488\uFF09\uFF0C\u9694\u5929\u7EE7\u7EED\u4E0D\u7528\u91CD\u65B0\u4EA4\u4EE3\u80CC\u666F", kind: "bool" },
3709
+ { key: "entityCardsEnabled", label: "\u5B9E\u4F53\u5361", hint: "[ENGRAM] \u7D22\u5F15\u4E3A\u6D3B\u8DC3\u5B9E\u4F53\uFF08\u6709\u951A\u5B9A\u8BB0\u5FC6\u6216\u672A\u95ED\u73AF\u4EFB\u52A1\uFF09\u6E32\u67D3\u4E00\u884C\u300C\u5F53\u524D\u771F\u76F8 + \u6253\u5F00\u4EFB\u52A1 + engram_detail(entity=\u2026) \u6307\u9488\u300D\uFF0C\u6700\u591A 2 \u5F20\uFF0C\u66FF\u4EE3\u8FD9\u4E9B\u5B9E\u4F53\u7684\u5E73\u94FA\u8BB0\u5FC6\u884C", kind: "bool" },
3620
3710
  { key: "maxRecallPerSession", label: "\u6BCF\u4F1A\u8BDD\u53BB\u91CD\u4E0A\u9650", hint: "recall \u6BCF\u4F1A\u8BDD\u6700\u591A\u4FDD\u7559 N \u6761\uFF08\u8DE8\u4F1A\u8BDD\u53BB\u91CD\uFF09", kind: "num", min: 1, max: 10 },
3621
3711
  { key: "expireDays", label: "TTL\uFF08\u5929\uFF09", hint: "0 = \u4E0D\u8FC7\u671F", kind: "num", min: 0, max: 3650 },
3622
3712
  { key: "indexMaxChars", label: "\u7D22\u5F15\u5B57\u7B26\u4E0A\u9650", hint: "[ENGRAM] \u5757 token \u9884\u7B97", kind: "num", min: 0, max: 4e3 }