@kenz1117/dsh-engram 0.7.12 → 0.7.14
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.en.md +8 -2
- package/README.md +8 -2
- package/lib/client.js +2 -2
- package/lib/client.js.map +1 -1
- package/lib/index.js +501 -18
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
23
23
|
"modelCacheDir",
|
|
24
24
|
"hfEndpoint",
|
|
25
25
|
"ingest",
|
|
26
|
+
"reflect",
|
|
26
27
|
"provider",
|
|
27
28
|
"model",
|
|
28
29
|
"decayAfterDays",
|
|
@@ -50,6 +51,11 @@ const INGEST_MODES = /* @__PURE__ */ new Set([
|
|
|
50
51
|
"light",
|
|
51
52
|
"eager"
|
|
52
53
|
]);
|
|
54
|
+
const REFLECT_MODES = /* @__PURE__ */ new Set([
|
|
55
|
+
"off",
|
|
56
|
+
"suggest",
|
|
57
|
+
"auto"
|
|
58
|
+
]);
|
|
53
59
|
/** Schemastery 校验面(cordis.yml 读取时校验)。 */
|
|
54
60
|
const Config = z.object({
|
|
55
61
|
dbDir: z.string(),
|
|
@@ -59,6 +65,7 @@ const Config = z.object({
|
|
|
59
65
|
modelCacheDir: z.string(),
|
|
60
66
|
hfEndpoint: z.string(),
|
|
61
67
|
ingest: z.string(),
|
|
68
|
+
reflect: z.string(),
|
|
62
69
|
provider: z.string(),
|
|
63
70
|
model: z.string(),
|
|
64
71
|
decayAfterDays: z.number().step(1).min(1).max(3650),
|
|
@@ -100,6 +107,7 @@ function resolveConfig(config = {}) {
|
|
|
100
107
|
for (const key of Object.keys(config)) if (!CONFIG_KEYS.has(key)) throw new Error(`dsh-engram: unknown config key "${key}"`);
|
|
101
108
|
if (config.legacyMigration !== void 0 && config.legacyMigration !== "eager" && config.legacyMigration !== "conservative") throw new Error("dsh-engram: legacyMigration must be one of eager|conservative");
|
|
102
109
|
if (config.ingest !== void 0 && !INGEST_MODES.has(config.ingest)) throw new Error(`dsh-engram: ingest must be one of off|light|eager, got "${String(config.ingest)}"`);
|
|
110
|
+
if (config.reflect !== void 0 && !REFLECT_MODES.has(config.reflect)) throw new Error(`dsh-engram: reflect must be one of off|suggest|auto, got "${String(config.reflect)}"`);
|
|
103
111
|
if (config.profileTopN !== void 0 && (!Number.isInteger(config.profileTopN) || config.profileTopN < 1 || config.profileTopN > 64)) throw new Error("dsh-engram: profileTopN must be an integer in [1, 64]");
|
|
104
112
|
const hasProvider = config.provider !== void 0;
|
|
105
113
|
const hasModel = config.model !== void 0;
|
|
@@ -136,6 +144,7 @@ function resolveConfig(config = {}) {
|
|
|
136
144
|
modelCacheDir: config.modelCacheDir ?? join(dbDir, "models"),
|
|
137
145
|
hfEndpoint: config.hfEndpoint,
|
|
138
146
|
ingest: config.ingest ?? "off",
|
|
147
|
+
reflect: config.reflect ?? "off",
|
|
139
148
|
routeOverride: hasProvider && hasModel ? {
|
|
140
149
|
provider: config.provider,
|
|
141
150
|
model: config.model
|
|
@@ -186,6 +195,10 @@ function asEntityId(raw) {
|
|
|
186
195
|
function asFactId(raw) {
|
|
187
196
|
return raw;
|
|
188
197
|
}
|
|
198
|
+
/** 从任意字符串铸造品牌化信念 id(存储层入库前调用)。 */
|
|
199
|
+
function asObservationId(raw) {
|
|
200
|
+
return raw;
|
|
201
|
+
}
|
|
189
202
|
/** 归一化比较键:trim + 合并连续空白 + 小写(消解第一版只做精确归一匹配,不做相似合并)。 */
|
|
190
203
|
function normalizeEntityName(raw) {
|
|
191
204
|
return raw.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -314,17 +327,13 @@ function finishError(finish) {
|
|
|
314
327
|
* @returns 模型输出全文。
|
|
315
328
|
*/
|
|
316
329
|
async function streamText(ctx, params) {
|
|
317
|
-
const messages = [{
|
|
318
|
-
role: "user",
|
|
330
|
+
const messages = [createUserMessage({
|
|
319
331
|
content: [{
|
|
320
332
|
type: "text",
|
|
321
333
|
text: params.userText
|
|
322
334
|
}],
|
|
323
|
-
source: {
|
|
324
|
-
|
|
325
|
-
plugin: "dsh-engram"
|
|
326
|
-
}
|
|
327
|
-
}];
|
|
335
|
+
source: { kind: "plugin:dsh-engram" }
|
|
336
|
+
})];
|
|
328
337
|
const options = {
|
|
329
338
|
provider: params.route.provider,
|
|
330
339
|
model: params.route.model,
|
|
@@ -823,7 +832,7 @@ function collectTexts(events, includeAssistant) {
|
|
|
823
832
|
let minSeq = null;
|
|
824
833
|
for (const event of events) if (event.type === "user/message") {
|
|
825
834
|
const data = event.data;
|
|
826
|
-
if (data?.source?.kind === "plugin") continue;
|
|
835
|
+
if (typeof data?.source?.kind === "string" && data.source.kind.startsWith("plugin")) continue;
|
|
827
836
|
const segments = (data?.content ?? []).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text);
|
|
828
837
|
if (segments.length > 0) {
|
|
829
838
|
texts.push(...segments);
|
|
@@ -871,7 +880,7 @@ function turnSignals(events) {
|
|
|
871
880
|
const toolNames = /* @__PURE__ */ new Set();
|
|
872
881
|
for (const event of events) if (event.type === "user/message") {
|
|
873
882
|
const data = event.data;
|
|
874
|
-
if (data?.source?.kind === "plugin") continue;
|
|
883
|
+
if (typeof data?.source?.kind === "string" && data.source.kind.startsWith("plugin")) continue;
|
|
875
884
|
for (const block of data?.content ?? []) if (block?.type === "text" && typeof block.text === "string") userChars += block.text.length;
|
|
876
885
|
} else if (event.type === "assistant/message") {
|
|
877
886
|
if ((event.data?.content ?? []).some((block) => block?.type === "text" && typeof block.text === "string" && block.text !== "")) completedTurns = 1;
|
|
@@ -1182,7 +1191,7 @@ function collectSessionText(events) {
|
|
|
1182
1191
|
const texts = [];
|
|
1183
1192
|
for (const event of events) if (event.type === "user/message") {
|
|
1184
1193
|
const data = event.data;
|
|
1185
|
-
if (data?.source?.kind === "plugin") continue;
|
|
1194
|
+
if (typeof data?.source?.kind === "string" && data.source.kind.startsWith("plugin")) continue;
|
|
1186
1195
|
const segments = (data?.content ?? []).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text);
|
|
1187
1196
|
if (segments.length > 0) texts.push(...segments);
|
|
1188
1197
|
} else if (event.type === "assistant/message") {
|
|
@@ -1713,6 +1722,241 @@ function cosine$1(a, b) {
|
|
|
1713
1722
|
return denom === 0 ? 0 : dot / denom;
|
|
1714
1723
|
}
|
|
1715
1724
|
//#endregion
|
|
1725
|
+
//#region src/consolidation/reflect.ts
|
|
1726
|
+
/**
|
|
1727
|
+
* 信念巩固(Reflect):把尚未成为任何信念证据的原始记忆交辅助 LLM 巩固为一句话信念
|
|
1728
|
+
* (observation),并对被新证据波及的 stale 信念做新鲜度复核。
|
|
1729
|
+
*
|
|
1730
|
+
* 与 flywheel/distill 的区别:蒸馏是「合并归档」(旧记忆进 supersedes 链后消失于 active),
|
|
1731
|
+
* 巩固是「抽象叠加」(原始记忆全部保留,信念是带证据引用的额外一层;同一主题经向量近邻
|
|
1732
|
+
* 归并持续细化同一条,而非反复新增)。
|
|
1733
|
+
*
|
|
1734
|
+
* 四个动作:
|
|
1735
|
+
* - form :从新证据形成信念;向量近邻达到阈值时并入既有信念(refine),否则新建
|
|
1736
|
+
* - refine :重写 stale 信念的表述并并入新证据(status 回 active)
|
|
1737
|
+
* - still :stale 信念经证据复核仍成立(仅刷新复核时间)
|
|
1738
|
+
* - refute :stale 信念被证据否定(status 置 refuted,保留证据链供审计)
|
|
1739
|
+
* @module @kenz1117/dsh-engram/consolidation/reflect
|
|
1740
|
+
*/
|
|
1741
|
+
/** 取材池大小:从 active 记忆按 importance/confidence 倒序扫描的上限。 */
|
|
1742
|
+
const REFLECT_POOL = 300;
|
|
1743
|
+
/** 单次巩固喂给 LLM 的新证据条数上限(协议内常量:单次辅助调用的可控上下文)。 */
|
|
1744
|
+
const REFLECT_MAX_INPUT = 30;
|
|
1745
|
+
/** 单次喂给 LLM 的待复核 stale 信念上限。 */
|
|
1746
|
+
const REFLECT_MAX_STALE = 5;
|
|
1747
|
+
/** 单条 stale 信念附带的证据原文条数上限。 */
|
|
1748
|
+
const REFLECT_STALE_EVIDENCE_LIMIT = 6;
|
|
1749
|
+
/** stale 证据原文截断字符数(控制总上下文)。 */
|
|
1750
|
+
const REFLECT_EVIDENCE_SLICE = 120;
|
|
1751
|
+
/** 巩固输出 token 上限。 */
|
|
1752
|
+
const REFLECT_MAX_TOKENS = 900;
|
|
1753
|
+
/** 信念向量近邻归并阈值:新信念与既有信念余弦达到该值则并入同一条(持续细化)。 */
|
|
1754
|
+
const REFLECT_MERGE_THRESHOLD = .9;
|
|
1755
|
+
/** 新鲜度阈值:新证据与既有信念余弦达到该值(低于归并阈值,语义更宽)则把信念标 stale 待复核。 */
|
|
1756
|
+
const REFLECT_STALE_THRESHOLD = .86;
|
|
1757
|
+
const REFLECT_SYSTEM = [
|
|
1758
|
+
"下面给出两部分材料:【待巩固记忆】是新近写入的原始记忆;【待复核信念】是此前巩固出、但可能受新证据影响的信念(附原始证据)。",
|
|
1759
|
+
"请对材料做巩固与复核,只输出一个 JSON 数组,每项为以下四种动作之一:",
|
|
1760
|
+
"1. {\"action\":\"form\",\"belief\":\"巩固出的一句话信念(结论/规律/稳定偏好)\",\"sourceIds\":[\"证据记忆id\"]} —— sourceIds 至少 1 个且必须全部来自【待巩固记忆】的 id;多条记忆支撑同一信念时合并成一条,不要为单条琐碎记忆立信念。",
|
|
1761
|
+
"2. {\"action\":\"refine\",\"observationId\":\"待复核信念id\",\"belief\":\"结合新证据后的新表述\",\"sourceIds\":[\"新增证据记忆id\"]} —— 新证据补充或限定了该信念时使用;sourceIds 必须来自【待巩固记忆】,可为空数组(仅表述更准确)。",
|
|
1762
|
+
"3. {\"action\":\"still\",\"observationId\":\"待复核信念id\"} —— 新证据不影响该信念,原表述仍成立。",
|
|
1763
|
+
"4. {\"action\":\"refute\",\"observationId\":\"待复核信念id\",\"reason\":\"被否定的原因(引用证据)\"} —— 新证据与信念矛盾且原信念不再成立。",
|
|
1764
|
+
"observationId 必须取自【待复核信念】给出的 id。材料中没有值得巩固或复核的内容时输出 []。不要输出 JSON 以外的任何内容。"
|
|
1765
|
+
].join("\n");
|
|
1766
|
+
/**
|
|
1767
|
+
* 执行一次信念巩固。纯异步:LLM/解析失败抛错(调用层决定是否落 pending),
|
|
1768
|
+
* 单条动作的写入失败计入 skipped 不中断整批。
|
|
1769
|
+
*/
|
|
1770
|
+
async function reflectObservations(deps) {
|
|
1771
|
+
const outcome = {
|
|
1772
|
+
pool: 0,
|
|
1773
|
+
candidates: 0,
|
|
1774
|
+
staleReviewed: 0,
|
|
1775
|
+
formed: 0,
|
|
1776
|
+
refined: 0,
|
|
1777
|
+
confirmed: 0,
|
|
1778
|
+
refuted: 0,
|
|
1779
|
+
skipped: 0
|
|
1780
|
+
};
|
|
1781
|
+
const pool = await deps.store.topActive(deps.scope, REFLECT_POOL);
|
|
1782
|
+
outcome.pool = pool.length;
|
|
1783
|
+
const known = await deps.store.listObservations({ limit: 1e3 });
|
|
1784
|
+
const evidenced = /* @__PURE__ */ new Set();
|
|
1785
|
+
for (const observation of known) {
|
|
1786
|
+
if (observation.status === "refuted") continue;
|
|
1787
|
+
for (const sourceId of observation.sourceIds) evidenced.add(String(sourceId));
|
|
1788
|
+
}
|
|
1789
|
+
const candidates = pool.filter((record) => !evidenced.has(String(record.id))).slice(0, REFLECT_MAX_INPUT);
|
|
1790
|
+
if (deps.embedder !== void 0 && candidates.length > 0) {
|
|
1791
|
+
const vectors = await deps.embedder.embed(candidates.map((record) => record.content));
|
|
1792
|
+
const staleMarks = /* @__PURE__ */ new Set();
|
|
1793
|
+
for (const vector of vectors) {
|
|
1794
|
+
if (vector === void 0) continue;
|
|
1795
|
+
const nearest = await deps.store.nearestObservation(vector);
|
|
1796
|
+
if (nearest !== void 0 && nearest.record.status === "active" && nearest.similarity >= REFLECT_STALE_THRESHOLD) staleMarks.add(String(nearest.record.id));
|
|
1797
|
+
}
|
|
1798
|
+
for (const id of staleMarks) try {
|
|
1799
|
+
await deps.store.markObservationStale(asObservationId(id));
|
|
1800
|
+
} catch {}
|
|
1801
|
+
}
|
|
1802
|
+
const stale = await deps.store.listObservations({
|
|
1803
|
+
status: "stale",
|
|
1804
|
+
limit: REFLECT_MAX_STALE
|
|
1805
|
+
});
|
|
1806
|
+
outcome.candidates = candidates.length;
|
|
1807
|
+
outcome.staleReviewed = stale.length;
|
|
1808
|
+
if (candidates.length === 0 && stale.length === 0) return outcome;
|
|
1809
|
+
const candidateIds = new Set(candidates.map((record) => String(record.id)));
|
|
1810
|
+
const staleIds = new Set(stale.map((observation) => String(observation.id)));
|
|
1811
|
+
const sections = [];
|
|
1812
|
+
if (candidates.length > 0) {
|
|
1813
|
+
const lines = candidates.map((record) => `[${String(record.id)}] (${record.kind}, importance ${record.importance.toFixed(2)}) ${record.content}`);
|
|
1814
|
+
sections.push(`【待巩固记忆】\n${lines.join("\n")}`);
|
|
1815
|
+
}
|
|
1816
|
+
if (stale.length > 0) {
|
|
1817
|
+
const blocks = [];
|
|
1818
|
+
for (const observation of stale) {
|
|
1819
|
+
const evidence = (await deps.store.getMany(observation.sourceIds.slice(0, REFLECT_STALE_EVIDENCE_LIMIT))).map((record) => ` - ${record.content.slice(0, REFLECT_EVIDENCE_SLICE)}`).join("\n");
|
|
1820
|
+
blocks.push(`[${String(observation.id)}] 证据 ${String(observation.proofCount)} 条:${observation.belief}\n${evidence}`);
|
|
1821
|
+
}
|
|
1822
|
+
sections.push(`【待复核信念】\n${blocks.join("\n")}`);
|
|
1823
|
+
}
|
|
1824
|
+
const userText = sections.join("\n\n");
|
|
1825
|
+
deps.logRequest({
|
|
1826
|
+
route: deps.route,
|
|
1827
|
+
scope: deps.scope,
|
|
1828
|
+
candidateCount: candidates.length,
|
|
1829
|
+
staleCount: stale.length,
|
|
1830
|
+
maxTokens: REFLECT_MAX_TOKENS
|
|
1831
|
+
});
|
|
1832
|
+
const parsed = parseJsonArray(await deps.call({
|
|
1833
|
+
route: deps.route,
|
|
1834
|
+
system: REFLECT_SYSTEM,
|
|
1835
|
+
userText,
|
|
1836
|
+
maxTokens: REFLECT_MAX_TOKENS,
|
|
1837
|
+
purpose: "engram-reflect",
|
|
1838
|
+
signal: deps.signal
|
|
1839
|
+
}));
|
|
1840
|
+
if (parsed === void 0 || parsed.length === 0) return outcome;
|
|
1841
|
+
for (const item of parsed) {
|
|
1842
|
+
const action = item;
|
|
1843
|
+
try {
|
|
1844
|
+
if (action.action === "form") {
|
|
1845
|
+
if (await applyForm(deps, action, candidateIds) === "refined") outcome.refined += 1;
|
|
1846
|
+
else outcome.formed += 1;
|
|
1847
|
+
} else if (action.action === "refine") {
|
|
1848
|
+
const observationId = requireObservationId(action.observationId, staleIds);
|
|
1849
|
+
if (observationId === void 0) {
|
|
1850
|
+
outcome.skipped += 1;
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
if (typeof action.belief !== "string" || action.belief.trim() === "") {
|
|
1854
|
+
outcome.skipped += 1;
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
const sourceIds = requireCandidateIds(action.sourceIds, candidateIds);
|
|
1858
|
+
if (sourceIds === void 0) {
|
|
1859
|
+
outcome.skipped += 1;
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
await refineWithEmbedding(deps, observationId, action.belief.trim(), sourceIds);
|
|
1863
|
+
outcome.refined += 1;
|
|
1864
|
+
} else if (action.action === "still") {
|
|
1865
|
+
const observationId = requireObservationId(action.observationId, staleIds);
|
|
1866
|
+
if (observationId === void 0) {
|
|
1867
|
+
outcome.skipped += 1;
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
await deps.store.confirmObservation(observationId);
|
|
1871
|
+
outcome.confirmed += 1;
|
|
1872
|
+
} else if (action.action === "refute") {
|
|
1873
|
+
const observationId = requireObservationId(action.observationId, staleIds);
|
|
1874
|
+
if (observationId === void 0) {
|
|
1875
|
+
outcome.skipped += 1;
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
await deps.store.refuteObservation(observationId);
|
|
1879
|
+
outcome.refuted += 1;
|
|
1880
|
+
} else outcome.skipped += 1;
|
|
1881
|
+
} catch {
|
|
1882
|
+
outcome.skipped += 1;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
try {
|
|
1886
|
+
await deps.store.audit("reflect", "AUX", JSON.stringify({
|
|
1887
|
+
scope: deps.scope,
|
|
1888
|
+
pool: outcome.pool,
|
|
1889
|
+
candidates: outcome.candidates,
|
|
1890
|
+
staleReviewed: outcome.staleReviewed,
|
|
1891
|
+
formed: outcome.formed,
|
|
1892
|
+
refined: outcome.refined,
|
|
1893
|
+
confirmed: outcome.confirmed,
|
|
1894
|
+
refuted: outcome.refuted,
|
|
1895
|
+
skipped: outcome.skipped
|
|
1896
|
+
}));
|
|
1897
|
+
} catch {}
|
|
1898
|
+
return outcome;
|
|
1899
|
+
}
|
|
1900
|
+
/** 处理 form:校验信念与证据,算向量并决定近邻归并或新建。@returns 落库形态。 */
|
|
1901
|
+
async function applyForm(deps, action, candidateIds) {
|
|
1902
|
+
if (typeof action.belief !== "string" || action.belief.trim() === "") throw new Error("form: belief 缺失");
|
|
1903
|
+
const sourceIds = requireCandidateIds(action.sourceIds, candidateIds);
|
|
1904
|
+
if (sourceIds === void 0 || sourceIds.length === 0) throw new Error("form: sourceIds 越界或为空");
|
|
1905
|
+
const belief = action.belief.trim();
|
|
1906
|
+
if (deps.embedder !== void 0) {
|
|
1907
|
+
const vector = (await deps.embedder.embed([belief]))[0];
|
|
1908
|
+
if (vector !== void 0) {
|
|
1909
|
+
const nearest = await deps.store.nearestObservation(vector);
|
|
1910
|
+
if (nearest !== void 0 && nearest.similarity >= REFLECT_MERGE_THRESHOLD) {
|
|
1911
|
+
await deps.store.refineObservation(nearest.record.id, belief, sourceIds, vector);
|
|
1912
|
+
return "refined";
|
|
1913
|
+
}
|
|
1914
|
+
await deps.store.createObservation({
|
|
1915
|
+
scope: deps.scope,
|
|
1916
|
+
belief,
|
|
1917
|
+
sourceIds,
|
|
1918
|
+
embedding: vector
|
|
1919
|
+
});
|
|
1920
|
+
return "formed";
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
await deps.store.createObservation({
|
|
1924
|
+
scope: deps.scope,
|
|
1925
|
+
belief,
|
|
1926
|
+
sourceIds
|
|
1927
|
+
});
|
|
1928
|
+
return "formed";
|
|
1929
|
+
}
|
|
1930
|
+
/** refine 时同样尝试为新表述算向量(失败则只改文本与证据,保留旧向量)。 */
|
|
1931
|
+
async function refineWithEmbedding(deps, observationId, belief, sourceIds) {
|
|
1932
|
+
if (deps.embedder !== void 0) {
|
|
1933
|
+
const vector = (await deps.embedder.embed([belief]))[0];
|
|
1934
|
+
if (vector !== void 0) {
|
|
1935
|
+
await deps.store.refineObservation(observationId, belief, sourceIds, vector);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
await deps.store.refineObservation(observationId, belief, sourceIds);
|
|
1940
|
+
}
|
|
1941
|
+
/** 校验 observationId 是本次喂入的 stale 信念;非法返回 undefined(跳过该动作)。 */
|
|
1942
|
+
function requireObservationId(raw, staleIds) {
|
|
1943
|
+
if (typeof raw !== "string" || !staleIds.has(raw)) return void 0;
|
|
1944
|
+
return asObservationId(raw);
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* 校验 sourceIds:必须为数组、每个 id 都来自本次候选集;返回品牌化 id 列表。
|
|
1948
|
+
* 非数组或含越界 id 返回 undefined(动作跳过);空数组合法(refine 仅改表述时使用)。
|
|
1949
|
+
*/
|
|
1950
|
+
function requireCandidateIds(raw, candidateIds) {
|
|
1951
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1952
|
+
const ids = [];
|
|
1953
|
+
for (const item of raw) {
|
|
1954
|
+
if (typeof item !== "string" || !candidateIds.has(item)) return void 0;
|
|
1955
|
+
ids.push(asMemoryId(item));
|
|
1956
|
+
}
|
|
1957
|
+
return ids;
|
|
1958
|
+
}
|
|
1959
|
+
//#endregion
|
|
1716
1960
|
//#region src/jev/client.ts
|
|
1717
1961
|
/**
|
|
1718
1962
|
* 调用 Jev /v1/systemone 端点并行回答 Noul 问题。
|
|
@@ -3646,7 +3890,7 @@ function placardImprovementHint(score) {
|
|
|
3646
3890
|
* @module @kenz1117/dsh-engram/store/sqlite
|
|
3647
3891
|
*/
|
|
3648
3892
|
/** 当前 schema 版本;结构性变更必须 +1。可空列与伴随表走增量迁移(见 openEngramStore 的迁移段)。 */
|
|
3649
|
-
const SCHEMA_VERSION =
|
|
3893
|
+
const SCHEMA_VERSION = 12;
|
|
3650
3894
|
/** 增量迁移表:key 为起始版本,value 为升到下一版本的 SQL(可多语句)。
|
|
3651
3895
|
* v2 → v3:nodes 补可空列 outcome(使用效果回报)。
|
|
3652
3896
|
* v3 → v4:新增 nodes_revisions 修订表(update 归档旧条目时的内容快照)。
|
|
@@ -3666,7 +3910,11 @@ const SCHEMA_VERSION = 11;
|
|
|
3666
3910
|
* v10 → v11:facts 事实表——摄取/工具期抽取的一句话事实,挂在实体上(entity_id
|
|
3667
3911
|
* 引用同库 entities.id,无外键)。valid_at/invalid_at 时间窗 + replaced_by 软失效
|
|
3668
3912
|
* 链:新事实可声明取代旧事实(旧事实置 invalid_at 并回指 replaced_by),历史链
|
|
3669
|
-
* 保留可审计;asOf 查询按时间窗过滤(见 factsOfEntity)。
|
|
3913
|
+
* 保留可审计;asOf 查询按时间窗过滤(见 factsOfEntity)。
|
|
3914
|
+
* v11 → v12:observations 巩固信念表——辅助 LLM 从多条原始记忆巩固出的一句话信念,
|
|
3915
|
+
* sources_json 证据 id 列表 + proof_count 证据数(去重并集持续累积),status 三态
|
|
3916
|
+
* active/stale/refuted 支持新鲜度复核;embedding 支持近邻归并(同主题并入同一条而非
|
|
3917
|
+
* 重复新增)。与 nodes 无外键(源记忆可被归档/遗忘,证据链只保留 id 引用)。 */
|
|
3670
3918
|
const MIGRATIONS = {
|
|
3671
3919
|
"2": "ALTER TABLE nodes ADD COLUMN outcome TEXT",
|
|
3672
3920
|
"3": `CREATE TABLE IF NOT EXISTS nodes_revisions (
|
|
@@ -3707,7 +3955,12 @@ const MIGRATIONS = {
|
|
|
3707
3955
|
id TEXT PRIMARY KEY, entity_id TEXT NOT NULL, content TEXT NOT NULL,
|
|
3708
3956
|
valid_at INTEGER NOT NULL, invalid_at INTEGER, replaced_by TEXT,
|
|
3709
3957
|
source_node_id TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
|
|
3710
|
-
CREATE INDEX IF NOT EXISTS facts_entity ON facts (entity_id)
|
|
3958
|
+
CREATE INDEX IF NOT EXISTS facts_entity ON facts (entity_id);`,
|
|
3959
|
+
"11": `CREATE TABLE IF NOT EXISTS observations (
|
|
3960
|
+
id TEXT PRIMARY KEY, scope TEXT NOT NULL, belief TEXT NOT NULL, status TEXT NOT NULL,
|
|
3961
|
+
sources_json TEXT NOT NULL, proof_count INTEGER NOT NULL, embedding BLOB,
|
|
3962
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, last_validated_at INTEGER NOT NULL);
|
|
3963
|
+
CREATE INDEX IF NOT EXISTS observations_status ON observations (status, proof_count, updated_at);`
|
|
3711
3964
|
};
|
|
3712
3965
|
/** RRF 融合常数:score = Σ 1/(K + rank)。 */
|
|
3713
3966
|
const RRF_K = 60;
|
|
@@ -3816,6 +4069,27 @@ function factRowToRecord(row) {
|
|
|
3816
4069
|
function vecToBlob(vector) {
|
|
3817
4070
|
return new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
3818
4071
|
}
|
|
4072
|
+
/** 行 → ObservationRecord;sources_json 由本插件自写,解析失败按空证据兜底(不炸读侧)。 */
|
|
4073
|
+
function observationRowToRecord(row) {
|
|
4074
|
+
let sourceIds = [];
|
|
4075
|
+
try {
|
|
4076
|
+
const parsed = JSON.parse(row.sources_json);
|
|
4077
|
+
if (Array.isArray(parsed)) sourceIds = parsed.filter((item) => typeof item === "string").map(asMemoryId);
|
|
4078
|
+
} catch {
|
|
4079
|
+
sourceIds = [];
|
|
4080
|
+
}
|
|
4081
|
+
return {
|
|
4082
|
+
id: asObservationId(row.id),
|
|
4083
|
+
scope: row.scope,
|
|
4084
|
+
belief: row.belief,
|
|
4085
|
+
status: row.status,
|
|
4086
|
+
sourceIds,
|
|
4087
|
+
proofCount: row.proof_count,
|
|
4088
|
+
createdAt: row.created_at,
|
|
4089
|
+
updatedAt: row.updated_at,
|
|
4090
|
+
lastValidatedAt: row.last_validated_at
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
3819
4093
|
/** 余弦相似度;任一向量零范数时返回 0。 */
|
|
3820
4094
|
function cosine(a, b) {
|
|
3821
4095
|
let dot = 0;
|
|
@@ -3932,6 +4206,10 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
3932
4206
|
id TEXT PRIMARY KEY, entity_id TEXT NOT NULL, content TEXT NOT NULL,
|
|
3933
4207
|
valid_at INTEGER NOT NULL, invalid_at INTEGER, replaced_by TEXT,
|
|
3934
4208
|
source_node_id TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
|
|
4209
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
4210
|
+
id TEXT PRIMARY KEY, scope TEXT NOT NULL, belief TEXT NOT NULL, status TEXT NOT NULL,
|
|
4211
|
+
sources_json TEXT NOT NULL, proof_count INTEGER NOT NULL, embedding BLOB,
|
|
4212
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, last_validated_at INTEGER NOT NULL);
|
|
3935
4213
|
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(node_id UNINDEXED, content, tokenize='unicode61');
|
|
3936
4214
|
CREATE INDEX IF NOT EXISTS nodes_scope_status ON nodes (scope, status);
|
|
3937
4215
|
CREATE INDEX IF NOT EXISTS nodes_kind_created ON nodes (kind, created_at);
|
|
@@ -3939,6 +4217,7 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
3939
4217
|
CREATE INDEX IF NOT EXISTS entities_name ON entities (name);
|
|
3940
4218
|
CREATE INDEX IF NOT EXISTS node_entities_entity ON node_entities (entity_id);
|
|
3941
4219
|
CREATE INDEX IF NOT EXISTS facts_entity ON facts (entity_id);
|
|
4220
|
+
CREATE INDEX IF NOT EXISTS observations_status ON observations (status, proof_count, updated_at);
|
|
3942
4221
|
`);
|
|
3943
4222
|
const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
3944
4223
|
if (versionRow === void 0) {
|
|
@@ -4074,6 +4353,18 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
4074
4353
|
const sqlFactInvalidate = db.prepare("UPDATE facts SET invalid_at = ?, replaced_by = ?, updated_at = ? WHERE id = ?");
|
|
4075
4354
|
const sqlFactGet = db.prepare("SELECT * FROM facts WHERE id = ?");
|
|
4076
4355
|
const sqlPurgeFacts = db.prepare("DELETE FROM facts");
|
|
4356
|
+
const sqlObservationInsert = db.prepare(`INSERT INTO observations
|
|
4357
|
+
(id, scope, belief, status, sources_json, proof_count, embedding, created_at, updated_at, last_validated_at)
|
|
4358
|
+
VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)`);
|
|
4359
|
+
const sqlObservationGet = db.prepare("SELECT * FROM observations WHERE id = ?");
|
|
4360
|
+
const sqlObservationUpdate = db.prepare(`UPDATE observations
|
|
4361
|
+
SET belief = ?, status = ?, sources_json = ?, proof_count = ?,
|
|
4362
|
+
embedding = COALESCE(?, embedding), updated_at = ?, last_validated_at = ? WHERE id = ?`);
|
|
4363
|
+
const sqlObservationSetStatus = db.prepare("UPDATE observations SET status = ?, updated_at = ?, last_validated_at = ? WHERE id = ?");
|
|
4364
|
+
const sqlObservationMarkStale = db.prepare("UPDATE observations SET status = 'stale' WHERE id = ?");
|
|
4365
|
+
const sqlObservationRefute = db.prepare("UPDATE observations SET status = 'refuted', updated_at = ? WHERE id = ?");
|
|
4366
|
+
const sqlObservationActiveVectors = db.prepare("SELECT * FROM observations WHERE status != 'refuted' AND embedding IS NOT NULL");
|
|
4367
|
+
const sqlPurgeObservations = db.prepare("DELETE FROM observations");
|
|
4077
4368
|
/** FTS 道:按 scope 集合检索(占位符动态生成,scope 集合由调用方去重);rooms 非空时只查指定房间。 */
|
|
4078
4369
|
const ftsSearch = (match, scopes, rooms) => {
|
|
4079
4370
|
const placeholders = scopes.map(() => "?").join(",");
|
|
@@ -4878,6 +5169,79 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
4878
5169
|
total
|
|
4879
5170
|
};
|
|
4880
5171
|
},
|
|
5172
|
+
async createObservation(input) {
|
|
5173
|
+
const id = asObservationId(randomUUID());
|
|
5174
|
+
const now = Date.now();
|
|
5175
|
+
const sourceIds = [...new Set(input.sourceIds.map(String))].map(asMemoryId);
|
|
5176
|
+
if (sourceIds.length === 0) throw new EngramError("BAD_INPUT", "createObservation 要求至少 1 条证据记忆");
|
|
5177
|
+
const stored = input.embedding === void 0 ? null : vecToBlob(input.embedding);
|
|
5178
|
+
withTransaction(() => {
|
|
5179
|
+
sqlObservationInsert.run(id, input.scope, input.belief, JSON.stringify(sourceIds.map(String)), sourceIds.length, stored, now, now, now);
|
|
5180
|
+
});
|
|
5181
|
+
return {
|
|
5182
|
+
id,
|
|
5183
|
+
scope: input.scope,
|
|
5184
|
+
belief: input.belief,
|
|
5185
|
+
status: "active",
|
|
5186
|
+
sourceIds,
|
|
5187
|
+
proofCount: sourceIds.length,
|
|
5188
|
+
createdAt: now,
|
|
5189
|
+
updatedAt: now,
|
|
5190
|
+
lastValidatedAt: now
|
|
5191
|
+
};
|
|
5192
|
+
},
|
|
5193
|
+
async refineObservation(id, belief, addSourceIds, embedding) {
|
|
5194
|
+
const existing = sqlObservationGet.get(String(id));
|
|
5195
|
+
if (existing === void 0) return void 0;
|
|
5196
|
+
const merged = [.../* @__PURE__ */ new Set([...observationRowToRecord(existing).sourceIds.map(String), ...addSourceIds.map(String)])].map(asMemoryId);
|
|
5197
|
+
const now = Date.now();
|
|
5198
|
+
const stored = embedding === void 0 ? null : vecToBlob(embedding);
|
|
5199
|
+
withTransaction(() => {
|
|
5200
|
+
sqlObservationUpdate.run(belief, "active", JSON.stringify(merged.map(String)), merged.length, stored, now, now, String(id));
|
|
5201
|
+
});
|
|
5202
|
+
return observationRowToRecord(sqlObservationGet.get(String(id)));
|
|
5203
|
+
},
|
|
5204
|
+
async markObservationStale(id) {
|
|
5205
|
+
if (sqlObservationGet.get(String(id)) === void 0) return void 0;
|
|
5206
|
+
sqlObservationMarkStale.run(String(id));
|
|
5207
|
+
return observationRowToRecord(sqlObservationGet.get(String(id)));
|
|
5208
|
+
},
|
|
5209
|
+
async confirmObservation(id) {
|
|
5210
|
+
if (sqlObservationGet.get(String(id)) === void 0) return void 0;
|
|
5211
|
+
const now = Date.now();
|
|
5212
|
+
sqlObservationSetStatus.run("active", now, now, String(id));
|
|
5213
|
+
return observationRowToRecord(sqlObservationGet.get(String(id)));
|
|
5214
|
+
},
|
|
5215
|
+
async refuteObservation(id) {
|
|
5216
|
+
if (sqlObservationGet.get(String(id)) === void 0) return void 0;
|
|
5217
|
+
const now = Date.now();
|
|
5218
|
+
withTransaction(() => {
|
|
5219
|
+
sqlObservationRefute.run(now, String(id));
|
|
5220
|
+
});
|
|
5221
|
+
return observationRowToRecord(sqlObservationGet.get(String(id)));
|
|
5222
|
+
},
|
|
5223
|
+
async listObservations(filter) {
|
|
5224
|
+
const offset = Math.max(0, filter.offset ?? 0);
|
|
5225
|
+
if (filter.status === void 0) return db.prepare("SELECT * FROM observations ORDER BY proof_count DESC, updated_at DESC LIMIT ? OFFSET ?").all(filter.limit, offset).map(observationRowToRecord);
|
|
5226
|
+
return db.prepare("SELECT * FROM observations WHERE status = ? ORDER BY proof_count DESC, updated_at DESC LIMIT ? OFFSET ?").all(filter.status, filter.limit, offset).map(observationRowToRecord);
|
|
5227
|
+
},
|
|
5228
|
+
async nearestObservation(embedding) {
|
|
5229
|
+
const rows = sqlObservationActiveVectors.all();
|
|
5230
|
+
let best;
|
|
5231
|
+
let bestSim = 0;
|
|
5232
|
+
for (const row of rows) {
|
|
5233
|
+
if (row.embedding === null) continue;
|
|
5234
|
+
const sim = cosine(embedding, blobToVec(row.embedding));
|
|
5235
|
+
if (sim > bestSim) {
|
|
5236
|
+
bestSim = sim;
|
|
5237
|
+
best = row;
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
return best === void 0 ? void 0 : {
|
|
5241
|
+
record: observationRowToRecord(best),
|
|
5242
|
+
similarity: bestSim
|
|
5243
|
+
};
|
|
5244
|
+
},
|
|
4881
5245
|
async audit(op, targetId, detail) {
|
|
4882
5246
|
sqlLog.run(Date.now(), op, targetId, detail);
|
|
4883
5247
|
},
|
|
@@ -4903,6 +5267,7 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
4903
5267
|
sqlPurgeNodeEntities.run();
|
|
4904
5268
|
sqlPurgeEntities.run();
|
|
4905
5269
|
sqlPurgeFacts.run();
|
|
5270
|
+
sqlPurgeObservations.run();
|
|
4906
5271
|
});
|
|
4907
5272
|
},
|
|
4908
5273
|
async close() {
|
|
@@ -5281,7 +5646,7 @@ function assessEvidence(batch, request) {
|
|
|
5281
5646
|
//#endregion
|
|
5282
5647
|
//#region src/tools/create.ts
|
|
5283
5648
|
/**
|
|
5284
|
-
*
|
|
5649
|
+
* 21 个 engram_ 工具的定义与执行器。工具 schema 保持窄参数;
|
|
5285
5650
|
* scope 决定读写哪个分库;嵌入缺失时检索结果显式标记降级。
|
|
5286
5651
|
* @module @kenz1117/dsh-engram/tools/create
|
|
5287
5652
|
*/
|
|
@@ -7211,6 +7576,67 @@ function createEngramTools(baseDeps) {
|
|
|
7211
7576
|
return { text: `蒸馏完成:取材 ${outcome.input} 条,产出 ${outcome.distilled} 条高层规律,归档 ${outcome.superseded} 条旧记忆(supersedes 链已建立,可 engram_review 审计)。` };
|
|
7212
7577
|
}
|
|
7213
7578
|
}),
|
|
7579
|
+
defineTool({
|
|
7580
|
+
name: "engram_reflect",
|
|
7581
|
+
description: "信念巩固:把多条原始记忆经辅助 LLM 巩固为带证据数的高层信念(form 新建/refine 细化/still 维持/refute 否定),同主题自动归并;原始记忆保留不归档。反复出现的模式或新增一批记忆后调用。",
|
|
7582
|
+
parameters: { scope: {
|
|
7583
|
+
type: "string",
|
|
7584
|
+
enum: [
|
|
7585
|
+
"user",
|
|
7586
|
+
"project",
|
|
7587
|
+
"shared"
|
|
7588
|
+
],
|
|
7589
|
+
description: "作用域,默认 user"
|
|
7590
|
+
} },
|
|
7591
|
+
output: {
|
|
7592
|
+
schema: {
|
|
7593
|
+
type: "object",
|
|
7594
|
+
additionalProperties: false,
|
|
7595
|
+
properties: { text: {
|
|
7596
|
+
type: "string",
|
|
7597
|
+
required: true
|
|
7598
|
+
} }
|
|
7599
|
+
},
|
|
7600
|
+
render: (_args, value) => [{
|
|
7601
|
+
type: "text",
|
|
7602
|
+
text: value.text
|
|
7603
|
+
}]
|
|
7604
|
+
},
|
|
7605
|
+
async execute(args, exec) {
|
|
7606
|
+
const scope = scopeOf(args.scope, "user");
|
|
7607
|
+
if (deps.call === void 0) throw new Error("engram_reflect: 辅助 LLM 不可用(宿主未提供 llm 服务),无法巩固");
|
|
7608
|
+
const call = deps.call;
|
|
7609
|
+
const events = exec.agent?.session?.snapshotEvents() ?? [];
|
|
7610
|
+
const route = deps.routeOverride ?? routeFromEvents(events);
|
|
7611
|
+
if (route === void 0) throw new Error("engram_reflect: 无法确定模型路由(会话尚无模型请求),请在 cordis.yml 配置 provider/model");
|
|
7612
|
+
const store = await deps.openStore(scope);
|
|
7613
|
+
const outcome = await reflectObservations({
|
|
7614
|
+
store,
|
|
7615
|
+
embedder: await deps.embedder,
|
|
7616
|
+
scope,
|
|
7617
|
+
call: (params) => call({
|
|
7618
|
+
...params,
|
|
7619
|
+
sessionId: exec.agent === void 0 ? void 0 : String(exec.agent.session.id)
|
|
7620
|
+
}),
|
|
7621
|
+
logRequest: (data) => {
|
|
7622
|
+
store.audit("reflect-request", "AUX", JSON.stringify(data)).catch(() => {});
|
|
7623
|
+
},
|
|
7624
|
+
route,
|
|
7625
|
+
signal: exec.signal
|
|
7626
|
+
});
|
|
7627
|
+
const pending = await store.listAuditDetails("reflect-pending");
|
|
7628
|
+
let consumed = 0;
|
|
7629
|
+
for (const detail of pending) try {
|
|
7630
|
+
if (JSON.parse(detail).scope === scope) {
|
|
7631
|
+
await store.clearAudit("reflect-pending", detail);
|
|
7632
|
+
consumed += 1;
|
|
7633
|
+
}
|
|
7634
|
+
} catch {}
|
|
7635
|
+
const lines = [`信念巩固完成:取材池 ${String(outcome.pool)} 条,新证据 ${String(outcome.candidates)} 条、待复核信念 ${String(outcome.staleReviewed)} 条;新建 ${String(outcome.formed)},细化 ${String(outcome.refined)},维持 ${String(outcome.confirmed)},否定 ${String(outcome.refuted)},跳过 ${String(outcome.skipped)}。`, "信念是叠加在原始记忆之上的抽象层(证据 id 与证据数已留存,原始记忆未归档)。"];
|
|
7636
|
+
if (consumed > 0) lines.push(`同时消费了 ${String(consumed)} 条会话结束时登记的待巩固任务。`);
|
|
7637
|
+
return { text: lines.join("\n") };
|
|
7638
|
+
}
|
|
7639
|
+
}),
|
|
7214
7640
|
defineTool({
|
|
7215
7641
|
name: "engram_examine",
|
|
7216
7642
|
description: "渐进式披露:按 id 批量拉取记忆完整铭牌(content + 房间 + 状态 + 边关系)。仅在已通过 engram_search/timeline/neighbors 拿到候选 id 后调用,避免一次性吞全文。建议 ≤16 个 id,超出会按入参顺序保留前 N 条。",
|
|
@@ -7768,6 +8194,12 @@ function renderProfileWithCurated(curated, records, tokenBudget, itemBudget = DE
|
|
|
7768
8194
|
const COMPRESS_MAX_TOKENS = 800;
|
|
7769
8195
|
/** 压缩辅助调用超时:压缩在 pre-step 关键路径上,必须限时防阻塞首轮请求。 */
|
|
7770
8196
|
const COMPRESS_TIMEOUT_MS = 8e3;
|
|
8197
|
+
/** turn_start 注入的巩固信念条数上限(按证据数、新近度倒序)。 */
|
|
8198
|
+
const OBSERVATION_INJECT_LIMIT = 3;
|
|
8199
|
+
/** 单条信念注入截断字符数。 */
|
|
8200
|
+
const OBSERVATION_INJECT_ITEM_CHARS = 120;
|
|
8201
|
+
/** 信念区块总字符上限(独立于画像 token 预算,避免与画像互相挤压)。 */
|
|
8202
|
+
const OBSERVATION_INJECT_BLOCK_CHARS = 480;
|
|
7771
8203
|
const COMPRESS_SYSTEM = [
|
|
7772
8204
|
"把记忆条目压缩为更短的一句话表述(每条不超过 40 个字符),保留可跨会话复用的关键信息(事实、偏好、决策、方法)。",
|
|
7773
8205
|
"只输出一个 JSON 数组,每项形如 {\"id\": \"原样返回的id\", \"content\": \"压缩后表述\"},条目数量与 id 必须与输入一一对应。",
|
|
@@ -7882,8 +8314,7 @@ async function preStep(ctx, openStore, openStoreForProjectCwd, resolved, embedde
|
|
|
7882
8314
|
text: reminder
|
|
7883
8315
|
}],
|
|
7884
8316
|
source: {
|
|
7885
|
-
kind:
|
|
7886
|
-
plugin: name,
|
|
8317
|
+
kind: `plugin:${name}`,
|
|
7887
8318
|
form: "snapshot",
|
|
7888
8319
|
sections: [{
|
|
7889
8320
|
name,
|
|
@@ -7923,6 +8354,14 @@ async function preStep(ctx, openStore, openStoreForProjectCwd, resolved, embedde
|
|
|
7923
8354
|
}).text;
|
|
7924
8355
|
}
|
|
7925
8356
|
}
|
|
8357
|
+
const observations = await store.listObservations({
|
|
8358
|
+
status: "active",
|
|
8359
|
+
limit: OBSERVATION_INJECT_LIMIT
|
|
8360
|
+
});
|
|
8361
|
+
if (observations.length > 0) {
|
|
8362
|
+
const block = ["Consolidated beliefs (distilled from multiple stored memories; verify against sources before high-stakes use):", ...observations.map((observation) => `- ${observation.belief.slice(0, OBSERVATION_INJECT_ITEM_CHARS)} (${String(observation.proofCount)} sources)`)].join("\n").slice(0, OBSERVATION_INJECT_BLOCK_CHARS);
|
|
8363
|
+
text = `${text}\n\n${block}`;
|
|
8364
|
+
}
|
|
7926
8365
|
const dueLine = dueTotal === 0 ? "" : `\nPalace review due today: ${dueTotal}${dueTotal >= 50 ? "+" : ""} memories. Use engram_review_queue for active recall (recall beats re-reading).`;
|
|
7927
8366
|
const textWithRationale = wrapWithRationale(text, top) + dueLine;
|
|
7928
8367
|
const hash = createHash("sha256").update(textWithRationale).digest("hex");
|
|
@@ -7938,8 +8377,7 @@ async function preStep(ctx, openStore, openStoreForProjectCwd, resolved, embedde
|
|
|
7938
8377
|
text: packet
|
|
7939
8378
|
}],
|
|
7940
8379
|
source: {
|
|
7941
|
-
kind:
|
|
7942
|
-
plugin: name,
|
|
8380
|
+
kind: `plugin:${name}`,
|
|
7943
8381
|
form: "snapshot",
|
|
7944
8382
|
sections: [{
|
|
7945
8383
|
name,
|
|
@@ -8369,6 +8807,51 @@ function apply(ctx, config = {}) {
|
|
|
8369
8807
|
console.warn("[dsh-engram] 会话结束的收尾处理异常(不影响对话):", error);
|
|
8370
8808
|
});
|
|
8371
8809
|
});
|
|
8810
|
+
if (resolved.reflect !== "off") ctx.on("session/disposed", (session) => {
|
|
8811
|
+
const events = session.snapshotEvents();
|
|
8812
|
+
const sessionId = String(session.id);
|
|
8813
|
+
const cwd = sessionCwd(session);
|
|
8814
|
+
const route = resolved.routeOverride ?? routeFromEvents(events);
|
|
8815
|
+
const targets = [{
|
|
8816
|
+
scope: "user",
|
|
8817
|
+
open: () => openStore("user")
|
|
8818
|
+
}];
|
|
8819
|
+
if (cwd !== void 0 && existsSync(join(resolved.dbDir, dbNameForCwd(cwd)))) targets.push({
|
|
8820
|
+
scope: "project",
|
|
8821
|
+
open: () => openStoreForProjectCwd(cwd)
|
|
8822
|
+
});
|
|
8823
|
+
(async () => {
|
|
8824
|
+
for (const target of targets) try {
|
|
8825
|
+
const store = await target.open();
|
|
8826
|
+
if (resolved.reflect === "suggest") {
|
|
8827
|
+
const detail = JSON.stringify({
|
|
8828
|
+
scope: target.scope,
|
|
8829
|
+
sessionId
|
|
8830
|
+
});
|
|
8831
|
+
if (!await store.hasAudit("reflect-pending", detail)) await store.audit("reflect-pending", "AUX", detail);
|
|
8832
|
+
continue;
|
|
8833
|
+
}
|
|
8834
|
+
if (route === void 0) continue;
|
|
8835
|
+
const outcome = await reflectObservations({
|
|
8836
|
+
store,
|
|
8837
|
+
embedder: await embedder,
|
|
8838
|
+
scope: target.scope,
|
|
8839
|
+
call: (params) => streamText(ctx, {
|
|
8840
|
+
...params,
|
|
8841
|
+
sessionId: session.id
|
|
8842
|
+
}),
|
|
8843
|
+
logRequest: (data) => {
|
|
8844
|
+
store.audit("reflect-request", "AUX", JSON.stringify(data)).catch(() => {});
|
|
8845
|
+
},
|
|
8846
|
+
route,
|
|
8847
|
+
signal: AbortSignal.timeout(FINAL_INGEST_TIMEOUT_MS)
|
|
8848
|
+
});
|
|
8849
|
+
if (outcome.formed + outcome.refined + outcome.confirmed + outcome.refuted > 0) console.log(`[dsh-engram] 信念巩固完成(${target.scope}):新建 ${String(outcome.formed)},细化 ${String(outcome.refined)},维持 ${String(outcome.confirmed)},否定 ${String(outcome.refuted)}`);
|
|
8850
|
+
} catch (error) {
|
|
8851
|
+
console.warn(`[dsh-engram] 会话结束的信念巩固异常(${target.scope},不影响对话):`, error);
|
|
8852
|
+
}
|
|
8853
|
+
})();
|
|
8854
|
+
});
|
|
8372
8855
|
const runDecay = async () => {
|
|
8373
8856
|
const targets = [{
|
|
8374
8857
|
label: "user",
|