dsh-engram 0.7.0 → 0.7.2
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 +97 -16
- package/README.zh.md +69 -15
- package/cordis.patch.yml +18 -5
- package/docs/CONTEXT-GC-GUIDE.zh.md +37 -6
- package/docs/RELEASE-NOTES-v0.7.1.md +88 -0
- package/docs/RELEASE-NOTES-v0.7.2.md +48 -0
- package/lib/boundary-prune.js +40 -0
- package/lib/client.js +3 -2
- package/lib/compaction.js +11 -0
- package/lib/context-gc.js +47 -0
- package/lib/index.js +74 -11
- package/lib/web-provision.js +324 -91
- package/package.json +1 -1
- package/scripts/dsh-engram.mjs +196 -53
package/lib/client.js
CHANGED
|
@@ -2209,11 +2209,11 @@ function EngramTelemetry({ api, workspace }) {
|
|
|
2209
2209
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.grid, marginTop: 8 }, children: [
|
|
2210
2210
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
|
|
2211
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" })
|
|
2212
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u8282\u7701 \xB7 \u526A\u679D/\u538B\u7F29\uFF08\u5DF2\u5165\u8D26\uFF09" })
|
|
2213
2213
|
] }),
|
|
2214
2214
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
|
|
2215
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" })
|
|
2216
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u6CE8\u5165\u6210\u672C \xB7 \u7D22\u5F15/\u53EC\u56DE/ESR\uFF08\u6BCF\u8BF7\u6C42\u91CD\u8BA1\uFF09" })
|
|
2217
2217
|
] }),
|
|
2218
2218
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
|
|
2219
2219
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#dc2626" }, children: tokenCount(-Math.round(ledgerBucket.rewritesCostChars / ledger.charsPerToken)) }),
|
|
@@ -2224,6 +2224,7 @@ function EngramTelemetry({ api, workspace }) {
|
|
|
2224
2224
|
] })
|
|
2225
2225
|
] })
|
|
2226
2226
|
] }),
|
|
2227
|
+
ledgerBucket.avoidedTokens === 0 && (ledgerBucket.injectedTokens > 0 || ledgerBucket.rewriteCount > 0) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.sub, marginTop: 6 }, children: "\u8282\u7701\u4ECD\u4E3A 0\uFF1A\u672C\u5DE5\u4F5C\u533A\u8FD8\u6CA1\u6709\u526A\u679D/\u538B\u7F29\u8282\u7701\u5165\u8D26\u2014\u2014L1 \u8FB9\u754C\u526A\u679D\u9700\u8981 host \u5E73\u9762\u7684 toolResultPruner\uFF08DSH \u2265 0.2.0 \u7531 bundle patch \u91CD\u65B0\u542F\u7528\u8BE5\u884C\uFF09\uFF0C\u538B\u7F29\u8282\u7701\u7531 Context GC \u5F15\u64CE\u5728 checkpoint \u65F6\u4E0A\u62A5\uFF1B\u51C0\u503C\u4E3A\u8D1F = \u6CE8\u5165\u6210\u672C + \u91CD\u5199\u7F5A\u91D1\u7684\u5408\u8BA1\u3002" }),
|
|
2227
2228
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.sub, marginTop: 6 }, children: [
|
|
2228
2229
|
"\u53E3\u5F84\uFF1Achars/",
|
|
2229
2230
|
ledger.charsPerToken,
|
package/lib/compaction.js
CHANGED
|
@@ -62,6 +62,17 @@ export function apply(ctx, config = {}) {
|
|
|
62
62
|
if (!domain) return null;
|
|
63
63
|
return collectPointerContext(domain, wk(workspace), report);
|
|
64
64
|
},
|
|
65
|
+
// Context Ledger bridge: this row runs inside the agent preset's
|
|
66
|
+
// isolate realm, where the host plugin's ledger closure is not
|
|
67
|
+
// reachable. The host plugin provides `engramLedger` as a plain
|
|
68
|
+
// (non-isolated) service, so compaction savings are credited to the
|
|
69
|
+
// one durable ledger instead of vanishing with the realm.
|
|
70
|
+
onCompacted: ({ session, workspace, charsSaved }) => {
|
|
71
|
+
try {
|
|
72
|
+
const hub = ctx.get("engramLedger");
|
|
73
|
+
hub?.noteCompaction?.(session?.id, workspace, { charsSaved });
|
|
74
|
+
} catch { /* contained: accounting never breaks compaction */ }
|
|
75
|
+
},
|
|
65
76
|
}
|
|
66
77
|
: {};
|
|
67
78
|
|
package/lib/context-gc.js
CHANGED
|
@@ -73,6 +73,38 @@ export function blockText(content) {
|
|
|
73
73
|
return out.trim();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Recursive visible-text char count over DSH content blocks (code points). */
|
|
77
|
+
function contentTextChars(value) {
|
|
78
|
+
if (typeof value === "string") return [...value].length;
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
let chars = 0;
|
|
81
|
+
for (const part of value) chars += contentTextChars(part);
|
|
82
|
+
return chars;
|
|
83
|
+
}
|
|
84
|
+
if (value === null || typeof value !== "object") return 0;
|
|
85
|
+
let chars = 0;
|
|
86
|
+
// Text blocks, tool-call arguments and nested tool-result content are all
|
|
87
|
+
// replayed context — count each once.
|
|
88
|
+
if (typeof value.text === "string") chars += [...value.text].length;
|
|
89
|
+
if (typeof value.arguments === "string") chars += [...value.arguments].length;
|
|
90
|
+
if (value.content !== undefined) chars += contentTextChars(value.content);
|
|
91
|
+
return chars;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Total visible text characters (Unicode code points) across a message list —
|
|
96
|
+
* the size side of the Context Ledger credit: a compaction replaces this span
|
|
97
|
+
* with the checkpoint, so `span − summary` is the part that stops replaying.
|
|
98
|
+
* @param {Array<object>} messages
|
|
99
|
+
* @returns {number}
|
|
100
|
+
*/
|
|
101
|
+
export function messagesTextChars(messages) {
|
|
102
|
+
if (!Array.isArray(messages)) return 0;
|
|
103
|
+
let chars = 0;
|
|
104
|
+
for (const message of messages) chars += contentTextChars(message?.content);
|
|
105
|
+
return chars;
|
|
106
|
+
}
|
|
107
|
+
|
|
76
108
|
/**
|
|
77
109
|
* The durable checkpoint framing the harness applies to a compaction summary
|
|
78
110
|
* before accepting it (`frameSummary()` in dsh-compaction-basic): the summary
|
|
@@ -762,6 +794,21 @@ export async function loadContextGcEngine(ctx, config = {}, deps = {}) {
|
|
|
762
794
|
}
|
|
763
795
|
summaryText = fit.text;
|
|
764
796
|
|
|
797
|
+
// Context Ledger credit: the span the checkpoint replaces stops
|
|
798
|
+
// replaying, so `span − summary` is a measured saving the accounting
|
|
799
|
+
// layer never had (the host does not expose compaction sizes). Best
|
|
800
|
+
// effort — accounting must never break the compaction path.
|
|
801
|
+
try {
|
|
802
|
+
if (typeof this.gcDeps?.onCompacted === "function") {
|
|
803
|
+
const spanChars = messagesTextChars(messages);
|
|
804
|
+
const summaryChars = [...summaryText].length;
|
|
805
|
+
const charsSaved = spanChars - summaryChars;
|
|
806
|
+
if (charsSaved > 0) {
|
|
807
|
+
this.gcDeps.onCompacted({ session: agent?.session, workspace: ws, charsSaved, spanChars, summaryChars });
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} catch { /* contained: a credit bug must never fail a compaction */ }
|
|
811
|
+
|
|
765
812
|
const result = { summary: [{ type: "text", text: summaryText }] };
|
|
766
813
|
if (narrative.llm && typeof narrative.llm === "object") Object.assign(result, narrative.llm);
|
|
767
814
|
// The harness's durable `compaction/summary` record ALWAYS carries
|
package/lib/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { registerTools } from "./tools.js";
|
|
|
24
24
|
import { makeCaptureHandler } from "./capture.js";
|
|
25
25
|
import { installGoalCapture } from "./goal-capture.js";
|
|
26
26
|
import { installPreferenceCapture } from "./preference.js";
|
|
27
|
-
import { installBoundaryPrune } from "./boundary-prune.js";
|
|
27
|
+
import { boundaryPruneAvailability, 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
30
|
import { createLedger, estimatePrefixChars } from "./ledger.js";
|
|
@@ -359,6 +359,27 @@ export function apply(ctx, config) {
|
|
|
359
359
|
log,
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
+
// Expose the ledger to the per-session web plane. Agent presets mount
|
|
363
|
+
// `dsh-engram/compaction` in their own isolate realm, so a closure from this
|
|
364
|
+
// apply() is unreachable there; a plain (non-isolated) service value is the
|
|
365
|
+
// bridge: the row credits compaction savings through it and `ledger` stays
|
|
366
|
+
// the single writer of the durable rollup.
|
|
367
|
+
if (ledger !== null) {
|
|
368
|
+
try {
|
|
369
|
+
// `ctx.provide` is fiber-scoped (cordis removes the service when this
|
|
370
|
+
// plugin unloads), so the returned disposer needs no bookkeeping here.
|
|
371
|
+
ctx.provide?.("engramLedger", {
|
|
372
|
+
noteCompaction: (sessionId, workspace, opts) => ledger.noteCompaction(sessionId, workspace, opts),
|
|
373
|
+
notePrune: (sessionId, workspace, opts) => ledger.notePrune(sessionId, workspace, opts),
|
|
374
|
+
noteRewrite: (sessionId, workspace, opts) => ledger.noteRewrite(sessionId, workspace, opts),
|
|
375
|
+
summary: () => ledger.summary(),
|
|
376
|
+
flush: () => ledger.flush(),
|
|
377
|
+
});
|
|
378
|
+
} catch (error) {
|
|
379
|
+
log?.warn?.(`engram ledger service unavailable: ${String(error?.message ?? error)}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
362
383
|
// Ledger-driven off-switch for BoundaryPrune (PROPOSAL-evolution v0.6-B):
|
|
363
384
|
// per-workspace, 30s-cached, reads the same summary /stats exposes. Evidence
|
|
364
385
|
// can only turn pruning OFF. No ledger → allow (repayment gate still runs).
|
|
@@ -422,7 +443,9 @@ export function apply(ctx, config) {
|
|
|
422
443
|
/** Live Context GC status (API overview + durable status file). */
|
|
423
444
|
compactionStatus: {
|
|
424
445
|
host: "pending",
|
|
425
|
-
|
|
446
|
+
// `disabled` (not `pending`) when the web opt-in is off: the status file
|
|
447
|
+
// must not look like a wiring run that never finished.
|
|
448
|
+
web: resolved.autoWebCompaction === false ? { action: "disabled", presets: [] } : { action: "pending", presets: [] },
|
|
426
449
|
config: { autoWebCompaction: resolved.autoWebCompaction, gcReplacesCompaction: resolved.gcReplacesCompaction, gcNarrative: resolved.gcNarrative },
|
|
427
450
|
},
|
|
428
451
|
/** Recompute the exact [ENGRAM] block for a workspace (GUI overview cost). */
|
|
@@ -666,6 +689,16 @@ export function apply(ctx, config) {
|
|
|
666
689
|
return null;
|
|
667
690
|
}
|
|
668
691
|
},
|
|
692
|
+
// Context Ledger credit: the Context GC engine measures the span it
|
|
693
|
+
// replaced with a checkpoint (`span − summary` chars) and reports it
|
|
694
|
+
// here; the ledger stops the "savings are always 0" asymmetry.
|
|
695
|
+
onCompacted: ({ session, workspace, charsSaved }) => {
|
|
696
|
+
try {
|
|
697
|
+
ledger?.noteCompaction(session?.id, workspace ?? wk(session?.header?.cwd), { charsSaved });
|
|
698
|
+
} catch (error) {
|
|
699
|
+
log?.warn?.(`engram ledger compaction credit failed: ${String(error)}`);
|
|
700
|
+
}
|
|
701
|
+
},
|
|
669
702
|
});
|
|
670
703
|
disposers.push(() => {
|
|
671
704
|
// The engine's service registration is fiber-scoped and auto-removed on
|
|
@@ -680,6 +713,10 @@ export function apply(ctx, config) {
|
|
|
680
713
|
let hostSettled = false;
|
|
681
714
|
let webSettled = resolved.autoWebCompaction === false;
|
|
682
715
|
let statusTimer = void 0;
|
|
716
|
+
// Authoritative L1 availability, captured where BoundaryPrune is actually
|
|
717
|
+
// installed (the pruner may land after the first status flush on a slow
|
|
718
|
+
// boot; the live check is only a fallback).
|
|
719
|
+
let boundaryPruneState = void 0;
|
|
683
720
|
const flushStatus = () => {
|
|
684
721
|
if (!hostSettled || !webSettled) return;
|
|
685
722
|
if (statusTimer !== void 0) { clearTimeout(statusTimer); statusTimer = void 0; }
|
|
@@ -687,6 +724,9 @@ export function apply(ctx, config) {
|
|
|
687
724
|
planes: resolved.autoWebCompaction === false ? ["host"] : ["host", "web"],
|
|
688
725
|
host: service.compactionStatus.host,
|
|
689
726
|
web: service.compactionStatus.web,
|
|
727
|
+
// L1 availability is the missing credit path behind a "节省 always 0"
|
|
728
|
+
// ledger: record whether BoundaryPrune can actually fire here.
|
|
729
|
+
boundaryPrune: boundaryPruneState ?? boundaryPruneAvailability(ctx, resolved),
|
|
690
730
|
config: service.compactionStatus.config,
|
|
691
731
|
configSource: {
|
|
692
732
|
autoWebCompaction: resolved.autoWebCompaction,
|
|
@@ -713,12 +753,15 @@ export function apply(ctx, config) {
|
|
|
713
753
|
// inside the agent preset's own realm, which a profile patch cannot reach.
|
|
714
754
|
// When the `agentPresets` service is present, swap the stock
|
|
715
755
|
// `compaction-basic` row for `dsh-engram/compaction` in every stock-layout
|
|
716
|
-
// preset
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
756
|
+
// preset. The row is located structurally (indentation-agnostic) inside the
|
|
757
|
+
// `compaction` group, so both preset generations are covered: legacy
|
|
758
|
+
// compositions with `preset.path` (DSH ≤ 0.1.x) and the declarative
|
|
759
|
+
// `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml` files (DSH ≥ 0.2.0,
|
|
760
|
+
// where the row sits at 14 spaces and the id maps to the shipped file).
|
|
761
|
+
// Surgical, idempotent, backed up, validated; a preset with a custom
|
|
762
|
+
// compaction layout (or none) is never touched. The row's engine config is
|
|
763
|
+
// derived from the settings knobs (`gcReplacesCompaction` / `gcNarrative`),
|
|
764
|
+
// so changing them re-provisions the web rows on the next boot. Flip
|
|
722
765
|
// `autoWebCompaction:false` to stop auto-wiring on later boots (it does
|
|
723
766
|
// not un-wire already-wired presets — use `npm run web-compaction:revert`
|
|
724
767
|
// for that). Best-effort: a failure only warns.
|
|
@@ -976,7 +1019,7 @@ export function apply(ctx, config) {
|
|
|
976
1019
|
// over-threshold tool results at todo/goal subtask boundaries through
|
|
977
1020
|
// DSH's own deterministic toolResultPruner, gated by a repayment clock.
|
|
978
1021
|
const l2Dir = resultPackDir();
|
|
979
|
-
const
|
|
1022
|
+
const boundaryHooks = {
|
|
980
1023
|
// ResultPack L2 hook: archive the pruned middles (redacted) + append
|
|
981
1024
|
// a handle tail on each pruned node. Fail-contained in result-pack.js.
|
|
982
1025
|
// Also records durable activation telemetry (SoL-Pi M15/C13) —
|
|
@@ -1008,8 +1051,28 @@ export function apply(ctx, config) {
|
|
|
1008
1051
|
log,
|
|
1009
1052
|
});
|
|
1010
1053
|
},
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1054
|
+
};
|
|
1055
|
+
// Arm L1 through `ctx.inject`, never a bare `ctx.get`: on a full web
|
|
1056
|
+
// profile the host pruner's fiber activates a tick or two after this
|
|
1057
|
+
// plugin (measured ~100 ms with dsh-context + dshmarket mounted), and a
|
|
1058
|
+
// strict lookup at install time is then empty — the old direct install
|
|
1059
|
+
// gave up and L1 stayed dormant for the whole process, silently killing
|
|
1060
|
+
// the only ledger credit path. `ctx.inject` runs the callback as soon as
|
|
1061
|
+
// the service is active (and re-runs it after a reload).
|
|
1062
|
+
if (resolved.boundaryPrune !== false) {
|
|
1063
|
+
ctx.inject(["toolResultPruner"], (prunerCtx) => {
|
|
1064
|
+
const dispose = installBoundaryPrune(prunerCtx, resolved, log, boundaryHooks, {
|
|
1065
|
+
allowFire: boundaryGate,
|
|
1066
|
+
});
|
|
1067
|
+
// L1 provably can (or cannot) fire from here on: record it for the
|
|
1068
|
+
// durable snapshot, then rewrite it — the first flush may have landed
|
|
1069
|
+
// before the pruner activated, and this status line is what users
|
|
1070
|
+
// read when "节省" stays at 0.
|
|
1071
|
+
boundaryPruneState = boundaryPruneAvailability(prunerCtx, resolved);
|
|
1072
|
+
flushStatus();
|
|
1073
|
+
return dispose;
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1013
1076
|
|
|
1014
1077
|
// ResultPack L2 tool (default ON with L1 since v0.6): `result_recall(callId,
|
|
1015
1078
|
// offset?)` pages back an archived middle. Independent of the memory
|