dsh-engram 0.5.1 → 0.6.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 +118 -39
- package/README.zh.md +101 -40
- package/docs/CONTEXT-GC-GUIDE.zh.md +15 -9
- package/docs/PROPOSAL-evolution.zh.md +684 -0
- package/docs/RELEASE-NOTES-v0.6.0.md +64 -0
- package/lib/api.js +5 -0
- package/lib/boundary-prune.js +27 -1
- package/lib/client.js +88 -2
- package/lib/index.js +165 -18
- package/lib/jit.js +224 -0
- package/lib/ledger.js +458 -0
- package/lib/settings.js +12 -4
- package/package.json +1 -1
|
@@ -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`
|
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
|
|
package/lib/boundary-prune.js
CHANGED
|
@@ -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
|
-
|
|
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,6 +2707,11 @@ 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",
|
|
2641
2717
|
decision: "#2563eb",
|
|
@@ -3140,6 +3216,7 @@ function EngramSection({ api, t }) {
|
|
|
3140
3216
|
const memNum = wsCounts ? wsCounts.memories : overview.totals.memories;
|
|
3141
3217
|
const taskNum = wsCounts ? wsCounts.tasks : overview.totals.tasks;
|
|
3142
3218
|
const linkNum = wsCounts ? wsCounts.links : overview.totals.links;
|
|
3219
|
+
const ledgerBucket = overview.ledger ? workspace && overview.ledger.byWorkspace[workspace] || overview.ledger.totals : null;
|
|
3143
3220
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: s2.stats, children: [
|
|
3144
3221
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatCard, { num: String(memNum), label: wsCounts ? "\u8BB0\u5FC6 (active)" : "\u8BB0\u5FC6 (active, \u5168\u5C40)" }),
|
|
3145
3222
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatCard, { num: String(taskNum), label: wsCounts ? "\u4EFB\u52A1 (active)" : "\u4EFB\u52A1 (active, \u5168\u5C40)" }),
|
|
@@ -3161,6 +3238,13 @@ function EngramSection({ api, t }) {
|
|
|
3161
3238
|
num: String(gc.archivedMemories + gc.archivedTasks),
|
|
3162
3239
|
label: `GC \u5DF2\u5F52\u6863 \xB7 \u94FE\u63A5-${gc.removedLinks}`
|
|
3163
3240
|
}
|
|
3241
|
+
),
|
|
3242
|
+
ledgerBucket && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
3243
|
+
StatCard,
|
|
3244
|
+
{
|
|
3245
|
+
num: signedTokens(ledgerBucket.netTokens),
|
|
3246
|
+
label: `\u4E0A\u4E0B\u6587\u51C0 token \xB7 \u8BF7\u6C42 ${ledgerBucket.requests}`
|
|
3247
|
+
}
|
|
3164
3248
|
)
|
|
3165
3249
|
] });
|
|
3166
3250
|
})(),
|
|
@@ -3613,10 +3697,12 @@ var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
|
3613
3697
|
var COMMON_FIELDS = [
|
|
3614
3698
|
{ 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
3699
|
{ 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" },
|
|
3700
|
+
{ 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" },
|
|
3701
|
+
{ 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
3702
|
{ 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" },
|
|
3703
|
+
{ 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
3704
|
{ 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" },
|
|
3705
|
+
{ 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" },
|
|
3620
3706
|
{ 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
3707
|
{ key: "expireDays", label: "TTL\uFF08\u5929\uFF09", hint: "0 = \u4E0D\u8FC7\u671F", kind: "num", min: 0, max: 3650 },
|
|
3622
3708
|
{ key: "indexMaxChars", label: "\u7D22\u5F15\u5B57\u7B26\u4E0A\u9650", hint: "[ENGRAM] \u5757 token \u9884\u7B97", kind: "num", min: 0, max: 4e3 }
|
package/lib/index.js
CHANGED
|
@@ -27,6 +27,8 @@ import { installPreferenceCapture } from "./preference.js";
|
|
|
27
27
|
import { installBoundaryPrune } from "./boundary-prune.js";
|
|
28
28
|
import { resultPackDir, archivePrunedResults, resultRecallTool, sweepExpired } from "./result-pack.js";
|
|
29
29
|
import { recordBoundaryPruneFire, recordRecallInjection, readPackTelemetry, engramHome } from "./pack-telemetry.js";
|
|
30
|
+
import { createLedger, estimatePrefixChars } from "./ledger.js";
|
|
31
|
+
import { createJit } from "./jit.js";
|
|
30
32
|
import { renderIndex, renderStartRecall, firstUserMessageText, renderEsr, esrMethodology, buildEsrSnapshot, buildFrozenEsrBlock } from "./index-block.js";
|
|
31
33
|
import { makeTriggerRecorder } from "./trigger.js";
|
|
32
34
|
import { pendingTodosFromSession, sinkPendingTodos } from "./todo-sink.js";
|
|
@@ -92,9 +94,11 @@ const DEFAULTS = {
|
|
|
92
94
|
gcStableRetentionDays: 120,
|
|
93
95
|
// Context GC — the auto-GC that REPLACES DSH's built-in lossy LLM-summary
|
|
94
96
|
// compaction (pi-esr CONTEXT_GC): mechanical eviction + re-fetch pointers,
|
|
95
|
-
// NOT the memory-panel GC above.
|
|
96
|
-
//
|
|
97
|
-
|
|
97
|
+
// NOT the memory-panel GC above. Opt-in since v0.6: rewriting host presets is
|
|
98
|
+
// the highest-maintenance action in the plugin (DSH broke the settings API
|
|
99
|
+
// twice), so the default is the host summarizer; `gcNarrative` off → fully
|
|
100
|
+
// mechanical. Turn this on to replace compaction with pointer summaries.
|
|
101
|
+
gcReplacesCompaction: false,
|
|
98
102
|
gcNarrative: true,
|
|
99
103
|
gcNarrativeMaxTokens: 1024,
|
|
100
104
|
gcNarrativeMaxChars: 4000,
|
|
@@ -109,6 +113,21 @@ const DEFAULTS = {
|
|
|
109
113
|
engramIndexOrder: 40,
|
|
110
114
|
esrMethodOrder: 40.5, // static ESR protocol section (byte-identical every turn)
|
|
111
115
|
esrOrder: 41,
|
|
116
|
+
// Context Ledger (PROPOSAL-evolution §4 v0.6-A): per-session accounting of
|
|
117
|
+
// injected block chars, pruned/compacted savings and prefix-rewrite penalty.
|
|
118
|
+
// In-memory + daily rollup at ~/.dsh/engram/ledger.json; never on the hot
|
|
119
|
+
// path of model calls (one integer add per request). `ledgerCacheDiscount`
|
|
120
|
+
// is the cached-prefix price share (0.1 = cache hit costs 10%, so a rewrite
|
|
121
|
+
// costs 90% of the prefix).
|
|
122
|
+
ledgerEnabled: true,
|
|
123
|
+
ledgerCacheDiscount: 0.1,
|
|
124
|
+
// JIT push (v0.6-C): append at most a few one-line, high-precision hints at
|
|
125
|
+
// deterministic events (error recurrence, file focus, goal completion).
|
|
126
|
+
// Append-only; every push pays a prefix rewrite in the ledger.
|
|
127
|
+
jitEnabled: true,
|
|
128
|
+
jitMaxLines: 3,
|
|
129
|
+
jitMaxLineChars: 220,
|
|
130
|
+
engramJitOrder: 40.75,
|
|
112
131
|
// decision-point triggers (P0-A): promote a session plan to an ESR task
|
|
113
132
|
// when the pending-todo count is at least minTodosForPromote and exceeds
|
|
114
133
|
// active ESR tasks × todoPromoteRatio (default: pending > active).
|
|
@@ -134,7 +153,8 @@ const DEFAULTS = {
|
|
|
134
153
|
// validation + idempotent; a preset with a custom compaction layout — or
|
|
135
154
|
// none, like `minimal` — is never touched). Off = every preset keeps DSH's
|
|
136
155
|
// default LLM summarizer; the host plane (headless/TUI/base) is unaffected.
|
|
137
|
-
|
|
156
|
+
// Opt-in since v0.6: never rewrite presets unless the user asks.
|
|
157
|
+
autoWebCompaction: false,
|
|
138
158
|
// Memory-to-memory semantics (zero LLM): when ON, an entity-anchored write
|
|
139
159
|
// that carries a replacement/negation cue ("改用 / no longer / switched…")
|
|
140
160
|
// auto-marks the matching older same-entity+kind memory as superseded.
|
|
@@ -145,9 +165,13 @@ const DEFAULTS = {
|
|
|
145
165
|
// (deterministic surface-replace, zero LLM). L0 simulation (77 sessions):
|
|
146
166
|
// todo-progress boundaries cover 79% of replay exposure; the repayment
|
|
147
167
|
// gate below lifted cache-aware net savings 12–32% on every threshold.
|
|
148
|
-
// Default
|
|
149
|
-
// pruner service is feature-detected — absent ⇒ disabled with one log.
|
|
150
|
-
boundaryPrune:
|
|
168
|
+
// Default ON since v0.6 (PROPOSAL-evolution §3.5): `false` is the opt-out;
|
|
169
|
+
// the pruner service is feature-detected — absent ⇒ disabled with one log.
|
|
170
|
+
boundaryPrune: true,
|
|
171
|
+
// Adaptive off-switch (v0.6-B): the Context Ledger vetoes further fires in a
|
|
172
|
+
// workspace whose own accounting is net negative after real fires. Evidence
|
|
173
|
+
// can only turn pruning OFF — never on. `false` = repayment gate only.
|
|
174
|
+
boundaryPruneAdaptive: true,
|
|
151
175
|
// Repayment gate: prune only while the session is expected to run at
|
|
152
176
|
// least this many more requests (empirical workspace prior, see
|
|
153
177
|
// lib/boundary-prune.js). 50 = L0 sweep optimum.
|
|
@@ -170,9 +194,10 @@ const DEFAULTS = {
|
|
|
170
194
|
// append a `[middle archived: result_recall(<callId>) 可取]` handle tail to
|
|
171
195
|
// each pruned node; register `result_recall(callId, offset?)` for paged
|
|
172
196
|
// recall — the full ObservationPack pattern (SoL-Pi mech ③; paired
|
|
173
|
-
// EdgeBench −23.58%). Requires
|
|
174
|
-
// archive (L2 without L1 is a no-op with a log line). Default
|
|
175
|
-
|
|
197
|
+
// EdgeBench −23.58%). Requires boundary pruning to have anything to
|
|
198
|
+
// archive (L2 without L1 is a no-op with a log line). Default ON since v0.6:
|
|
199
|
+
// default pruning must be lossless (the archive is the recovery path).
|
|
200
|
+
resultPack: true,
|
|
176
201
|
// result_recall page budget (chars per page). 4000 fits a typical model
|
|
177
202
|
// context band without re-materializing huge results whole.
|
|
178
203
|
resultPackPageChars: 4000,
|
|
@@ -304,6 +329,55 @@ export function apply(ctx, config) {
|
|
|
304
329
|
// todo_write promote hook. Process-local; fed by a tools/result listener.
|
|
305
330
|
const recorder = makeTriggerRecorder(resolved);
|
|
306
331
|
|
|
332
|
+
// Context Ledger: created before the service object so /stats can read it.
|
|
333
|
+
// `ledgerEnabled:false` leaves a null instance (zero listeners, zero writes).
|
|
334
|
+
const ledger = resolved.ledgerEnabled === false
|
|
335
|
+
? null
|
|
336
|
+
: createLedger({
|
|
337
|
+
home: engramHome(),
|
|
338
|
+
cacheDiscount: resolved.ledgerCacheDiscount,
|
|
339
|
+
log,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Ledger-driven off-switch for BoundaryPrune (PROPOSAL-evolution v0.6-B):
|
|
343
|
+
// per-workspace, 30s-cached, reads the same summary /stats exposes. Evidence
|
|
344
|
+
// can only turn pruning OFF. No ledger → allow (repayment gate still runs).
|
|
345
|
+
const boundaryGate = (() => {
|
|
346
|
+
const cache = new Map();
|
|
347
|
+
const TTL_MS = 30_000;
|
|
348
|
+
return (workspace) => {
|
|
349
|
+
if (ledger === null) return true;
|
|
350
|
+
const now = Date.now();
|
|
351
|
+
const hit = cache.get(workspace);
|
|
352
|
+
if (hit !== void 0 && now - hit.at < TTL_MS) return hit.ok;
|
|
353
|
+
let ok = true;
|
|
354
|
+
try {
|
|
355
|
+
const summary = ledger.summary();
|
|
356
|
+
const bucket = summary.byWorkspace[workspace] ?? summary.totals;
|
|
357
|
+
ok = !(bucket.rewriteCount >= 3 && bucket.netChars < 0);
|
|
358
|
+
} catch {
|
|
359
|
+
ok = true; // accounting must never disable the mechanism by accident
|
|
360
|
+
}
|
|
361
|
+
if (cache.size > 64) cache.clear();
|
|
362
|
+
cache.set(workspace, { at: now, ok });
|
|
363
|
+
return ok;
|
|
364
|
+
};
|
|
365
|
+
})();
|
|
366
|
+
|
|
367
|
+
// JIT push (v0.6-C): append-only hints, priced through the ledger.
|
|
368
|
+
const jit = resolved.jitEnabled === false
|
|
369
|
+
? null
|
|
370
|
+
: createJit({
|
|
371
|
+
maxLines: resolved.jitMaxLines,
|
|
372
|
+
maxLineChars: resolved.jitMaxLineChars,
|
|
373
|
+
log,
|
|
374
|
+
onPush: ({ session, workspace, chars }) => {
|
|
375
|
+
ledger?.noteRewrite(session?.id, workspace, {
|
|
376
|
+
chars: estimatePrefixChars(session) + chars,
|
|
377
|
+
});
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
307
381
|
const service = {
|
|
308
382
|
config: resolved,
|
|
309
383
|
getDomain,
|
|
@@ -319,6 +393,10 @@ export function apply(ctx, config) {
|
|
|
319
393
|
/** SoL-Pi activation telemetry (M15/C13): durable boundary-prune fires +
|
|
320
394
|
* [RECALL] injection funnel, read at request time (readPackTelemetry). */
|
|
321
395
|
packTelemetry: () => readPackTelemetry({ home: engramHome() }),
|
|
396
|
+
/** Context Ledger snapshot: spent vs saved context, net of cache rewrites. */
|
|
397
|
+
ledger: () => (ledger === null ? null : ledger.summary()),
|
|
398
|
+
/** JIT push counters (per process; /stats diagnostic surface). */
|
|
399
|
+
jitStats: () => (jit === null ? null : jit.stats()),
|
|
322
400
|
/** Last GC sweep totals (GUI overview observability). */
|
|
323
401
|
gcStats: { lastRun: 0, archivedMemories: 0, archivedTasks: 0, removedLinks: 0 },
|
|
324
402
|
/** Live Context GC status (API overview + durable status file). */
|
|
@@ -452,6 +530,7 @@ export function apply(ctx, config) {
|
|
|
452
530
|
// [RECALL] block.
|
|
453
531
|
if (pendingRecall) return block;
|
|
454
532
|
sessionCache.set(session, block);
|
|
533
|
+
ledger?.noteInjection(session.id, wk(cwd), "index", block.length);
|
|
455
534
|
return block;
|
|
456
535
|
};
|
|
457
536
|
|
|
@@ -500,9 +579,22 @@ export function apply(ctx, config) {
|
|
|
500
579
|
} catch (error) {
|
|
501
580
|
log?.warn?.(`engram esr render failed: ${String(error)}`);
|
|
502
581
|
}
|
|
582
|
+
if (block.length > 0) ledger?.noteInjection(session.id, wk(cwd), "esr", block.length);
|
|
503
583
|
return block;
|
|
504
584
|
};
|
|
505
585
|
|
|
586
|
+
// JIT block: append-only lines accumulated by lib/jit.js (never edited).
|
|
587
|
+
const jitFor = (agent) => {
|
|
588
|
+
const session = agent?.session;
|
|
589
|
+
if (session === void 0) return "";
|
|
590
|
+
const text = jit === null ? "" : jit.textFor(session);
|
|
591
|
+
const cwd = session.header?.cwd;
|
|
592
|
+
if (text.length > 0 && typeof cwd === "string" && cwd.length > 0) {
|
|
593
|
+
ledger?.noteInjection(session.id, wk(cwd), "jit", text.length);
|
|
594
|
+
}
|
|
595
|
+
return text;
|
|
596
|
+
};
|
|
597
|
+
|
|
506
598
|
ctx.effect(() => {
|
|
507
599
|
const disposers = [];
|
|
508
600
|
|
|
@@ -649,6 +741,18 @@ export function apply(ctx, config) {
|
|
|
649
741
|
});
|
|
650
742
|
disposers.push(() => indexDispose());
|
|
651
743
|
|
|
744
|
+
// JIT push block — empty until a deterministic event fires, then
|
|
745
|
+
// append-only (prefix changes at most once per push).
|
|
746
|
+
const jitDispose = systemPrompt.section({
|
|
747
|
+
name: "engram:jit",
|
|
748
|
+
order: resolved.engramJitOrder,
|
|
749
|
+
text: (assemblyContext) => {
|
|
750
|
+
const agent = assemblyContext?.agent;
|
|
751
|
+
return agent === void 0 ? "" : jitFor(agent);
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
disposers.push(() => jitDispose());
|
|
755
|
+
|
|
652
756
|
// Static ESR protocol (pi-esr rule 1): byte-identical every turn, so
|
|
653
757
|
// the model never depends on runtime nudges to know WHEN to use ESR.
|
|
654
758
|
const esrMethodDispose = systemPrompt.section({
|
|
@@ -677,6 +781,39 @@ export function apply(ctx, config) {
|
|
|
677
781
|
});
|
|
678
782
|
disposers.push(disposeTrigger);
|
|
679
783
|
|
|
784
|
+
// Context Ledger ticks: one model request completed → charge the injected
|
|
785
|
+
// blocks (held in the prefix) and credit pruned/compacted chars that would
|
|
786
|
+
// otherwise replay in this request. Session teardown folds the tail into
|
|
787
|
+
// the daily rollup. Both listeners are no-ops when the ledger is disabled.
|
|
788
|
+
if (ledger !== null) {
|
|
789
|
+
const disposeLedgerTick = ctx.on("session/event", (session, event) => {
|
|
790
|
+
if (event?.type !== "assistant/message") return;
|
|
791
|
+
ledger.tick(session?.id, wk(session?.header?.cwd));
|
|
792
|
+
});
|
|
793
|
+
disposers.push(disposeLedgerTick);
|
|
794
|
+
const disposeLedgerSession = ctx.on("session/disposed", (session) => {
|
|
795
|
+
ledger.dispose(session?.id);
|
|
796
|
+
});
|
|
797
|
+
disposers.push(disposeLedgerSession);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// JIT observers: tool results (error recurrence / file focus) and goal
|
|
801
|
+
// completion. `opened` may be undefined early — the observers no-op.
|
|
802
|
+
if (jit !== null) {
|
|
803
|
+
const disposeJitTool = ctx.on("tools/result", (exec, result) => {
|
|
804
|
+
jit.observeToolResult(exec, result, opened);
|
|
805
|
+
});
|
|
806
|
+
disposers.push(disposeJitTool);
|
|
807
|
+
const disposeJitEvent = ctx.on("session/event", (session, event) => {
|
|
808
|
+
jit.observeEvent(session, event, opened);
|
|
809
|
+
});
|
|
810
|
+
disposers.push(disposeJitEvent);
|
|
811
|
+
const disposeJitSession = ctx.on("session/disposed", (session) => {
|
|
812
|
+
jit.dispose(session?.id);
|
|
813
|
+
});
|
|
814
|
+
disposers.push(disposeJitSession);
|
|
815
|
+
}
|
|
816
|
+
|
|
680
817
|
// session-end todo auto-sink: a session whose plan still has pending todos
|
|
681
818
|
// lands them as ESR drafts at its teardown edge, so the plan survives the
|
|
682
819
|
// session (flip `autoSinkTodosOnEnd` off to opt out). Live-toggleable — the
|
|
@@ -765,14 +902,23 @@ export function apply(ctx, config) {
|
|
|
765
902
|
// independent of `resultPack`: /stats must be able to prove the L1
|
|
766
903
|
// mechanism actually fires on the live profile.
|
|
767
904
|
onPruned: (session, report, boundary) => {
|
|
905
|
+
const workspace = wk(session?.header?.cwd);
|
|
768
906
|
recordBoundaryPruneFire({
|
|
769
907
|
home: engramHome(),
|
|
770
908
|
boundary,
|
|
771
909
|
report,
|
|
772
|
-
workspace
|
|
910
|
+
workspace,
|
|
911
|
+
});
|
|
912
|
+
// Ledger: the removed chars stop replaying from the next request;
|
|
913
|
+
// the prefix invalidation is priced with the pre-prune size (the
|
|
914
|
+
// surface is already rewritten when this hook runs).
|
|
915
|
+
const removed = Number.isFinite(report?.charsRemoved) ? report.charsRemoved : 0;
|
|
916
|
+
const prefixChars = estimatePrefixChars(session);
|
|
917
|
+
ledger?.notePrune(session?.id, workspace, {
|
|
918
|
+
charsRemoved: removed,
|
|
919
|
+
rewriteChars: prefixChars + removed,
|
|
773
920
|
});
|
|
774
921
|
if (resolved.resultPack !== true) return;
|
|
775
|
-
const workspace = wk(session?.header?.cwd);
|
|
776
922
|
archivePrunedResults({
|
|
777
923
|
dir: l2Dir,
|
|
778
924
|
session,
|
|
@@ -781,15 +927,16 @@ export function apply(ctx, config) {
|
|
|
781
927
|
log,
|
|
782
928
|
});
|
|
783
929
|
},
|
|
784
|
-
});
|
|
930
|
+
}, { allowFire: boundaryGate });
|
|
785
931
|
disposers.push(disposeBoundaryPrune);
|
|
786
932
|
|
|
787
|
-
// ResultPack L2 tool (default
|
|
788
|
-
// pages back an archived middle. Independent of the memory
|
|
789
|
-
// archive lives on the filesystem. Registers only when
|
|
790
|
-
//
|
|
933
|
+
// ResultPack L2 tool (default ON with L1 since v0.6): `result_recall(callId,
|
|
934
|
+
// offset?)` pages back an archived middle. Independent of the memory
|
|
935
|
+
// domain — the archive lives on the filesystem. Registers only when
|
|
936
|
+
// enabled; without boundaryPrune there is nothing to archive, so it
|
|
937
|
+
// self-disables.
|
|
791
938
|
if (resolved.resultPack === true) {
|
|
792
|
-
if (resolved.boundaryPrune
|
|
939
|
+
if (resolved.boundaryPrune === false) {
|
|
793
940
|
log?.warn?.("engram result-pack: requires boundaryPrune: true to prune middles — result_recall not registered");
|
|
794
941
|
} else {
|
|
795
942
|
const pageChars = Number.isFinite(resolved.resultPackPageChars)
|