@kenz1117/dsh-engram 0.7.7 → 0.7.9
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 +26 -10
- package/README.md +26 -10
- package/lib/client.js +3 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +882 -60
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -28,11 +28,15 @@ const CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
28
28
|
"decayAfterDays",
|
|
29
29
|
"decayImportanceBelow",
|
|
30
30
|
"injectTokenBudget",
|
|
31
|
+
"injectItemBudgetStart",
|
|
32
|
+
"injectItemBudgetDecay",
|
|
33
|
+
"injectItemBudgetFloor",
|
|
31
34
|
"rankRecencyWeight",
|
|
32
35
|
"rankProofWeight",
|
|
33
36
|
"queryRewrite",
|
|
34
37
|
"autoSlot",
|
|
35
38
|
"reviewScheduling",
|
|
39
|
+
"assessReminder",
|
|
36
40
|
"historyBackfillDays",
|
|
37
41
|
"historyBackfillMaxTurnsPerSession",
|
|
38
42
|
"historyBackfillMaxTotalTurns",
|
|
@@ -59,11 +63,15 @@ const Config = z.object({
|
|
|
59
63
|
decayAfterDays: z.number().step(1).min(1).max(3650),
|
|
60
64
|
decayImportanceBelow: z.number().min(0).max(1),
|
|
61
65
|
injectTokenBudget: z.number().step(1).min(128).max(8192),
|
|
66
|
+
injectItemBudgetStart: z.number().step(1).min(40).max(2e3),
|
|
67
|
+
injectItemBudgetDecay: z.number().min(.5).max(1),
|
|
68
|
+
injectItemBudgetFloor: z.number().step(1).min(8).max(200),
|
|
62
69
|
rankRecencyWeight: z.number().min(0).max(2),
|
|
63
70
|
rankProofWeight: z.number().min(0).max(2),
|
|
64
71
|
queryRewrite: z.boolean(),
|
|
65
72
|
autoSlot: z.boolean(),
|
|
66
73
|
reviewScheduling: z.boolean(),
|
|
74
|
+
assessReminder: z.boolean(),
|
|
67
75
|
historyBackfillDays: z.number().step(1).min(0).max(3650),
|
|
68
76
|
historyBackfillMaxTurnsPerSession: z.number().step(1).min(1).max(500),
|
|
69
77
|
historyBackfillMaxTotalTurns: z.number().step(1).min(1).max(5e3),
|
|
@@ -88,6 +96,11 @@ function resolveConfig(config = {}) {
|
|
|
88
96
|
if (config.decayAfterDays !== void 0 && (!Number.isInteger(config.decayAfterDays) || config.decayAfterDays < 1 || config.decayAfterDays > 3650)) throw new Error("dsh-engram: decayAfterDays must be an integer in [1, 3650]");
|
|
89
97
|
if (config.decayImportanceBelow !== void 0 && (config.decayImportanceBelow < 0 || config.decayImportanceBelow > 1)) throw new Error("dsh-engram: decayImportanceBelow must be in [0, 1]");
|
|
90
98
|
if (config.injectTokenBudget !== void 0 && (!Number.isInteger(config.injectTokenBudget) || config.injectTokenBudget < 128 || config.injectTokenBudget > 8192)) throw new Error("dsh-engram: injectTokenBudget must be an integer in [128, 8192]");
|
|
99
|
+
if (config.injectItemBudgetStart !== void 0 && (!Number.isInteger(config.injectItemBudgetStart) || config.injectItemBudgetStart < 40 || config.injectItemBudgetStart > 2e3)) throw new Error("dsh-engram: injectItemBudgetStart must be an integer in [40, 2000]");
|
|
100
|
+
if (config.injectItemBudgetDecay !== void 0 && (config.injectItemBudgetDecay < .5 || config.injectItemBudgetDecay > 1)) throw new Error("dsh-engram: injectItemBudgetDecay must be in [0.5, 1]");
|
|
101
|
+
if (config.injectItemBudgetFloor !== void 0 && (!Number.isInteger(config.injectItemBudgetFloor) || config.injectItemBudgetFloor < 8 || config.injectItemBudgetFloor > 200)) throw new Error("dsh-engram: injectItemBudgetFloor must be an integer in [8, 200]");
|
|
102
|
+
if (config.injectItemBudgetStart !== void 0 && config.injectItemBudgetFloor !== void 0 && config.injectItemBudgetFloor > config.injectItemBudgetStart) throw new Error("dsh-engram: injectItemBudgetFloor must not exceed injectItemBudgetStart");
|
|
103
|
+
if (config.assessReminder !== void 0 && typeof config.assessReminder !== "boolean") throw new Error("dsh-engram: assessReminder must be a boolean");
|
|
91
104
|
if (config.rankRecencyWeight !== void 0 && (config.rankRecencyWeight < 0 || config.rankRecencyWeight > 2)) throw new Error("dsh-engram: rankRecencyWeight must be in [0, 2]");
|
|
92
105
|
if (config.rankProofWeight !== void 0 && (config.rankProofWeight < 0 || config.rankProofWeight > 2)) throw new Error("dsh-engram: rankProofWeight must be in [0, 2]");
|
|
93
106
|
if (config.historyBackfillDays !== void 0 && (!Number.isInteger(config.historyBackfillDays) || config.historyBackfillDays < 0 || config.historyBackfillDays > 3650)) throw new Error("dsh-engram: historyBackfillDays must be an integer in [0, 3650] (0 = unlimited)");
|
|
@@ -109,6 +122,10 @@ function resolveConfig(config = {}) {
|
|
|
109
122
|
decayAfterDays: config.decayAfterDays ?? 30,
|
|
110
123
|
decayImportanceBelow: config.decayImportanceBelow ?? .3,
|
|
111
124
|
injectTokenBudget: config.injectTokenBudget ?? 1024,
|
|
125
|
+
injectItemBudgetStart: config.injectItemBudgetStart ?? 160,
|
|
126
|
+
injectItemBudgetDecay: config.injectItemBudgetDecay ?? .9,
|
|
127
|
+
injectItemBudgetFloor: config.injectItemBudgetFloor ?? 24,
|
|
128
|
+
assessReminder: config.assessReminder ?? true,
|
|
112
129
|
rankRecencyWeight: config.rankRecencyWeight ?? .2,
|
|
113
130
|
rankProofWeight: config.rankProofWeight ?? .1,
|
|
114
131
|
queryRewrite: config.queryRewrite ?? true,
|
|
@@ -130,6 +147,8 @@ function resolveConfig(config = {}) {
|
|
|
130
147
|
function asMemoryId(raw) {
|
|
131
148
|
return raw;
|
|
132
149
|
}
|
|
150
|
+
/** episode 情景时间线的时间邻近扩展缺省窗口:锚点 createdAt ± 60 分钟。 */
|
|
151
|
+
const EPISODE_PROXIMITY_MS_DEFAULT = 36e5;
|
|
133
152
|
/** dsh-engram 统一错误:加载/使用期的可诊断失败都抛此类型。 */
|
|
134
153
|
var EngramError = class extends Error {
|
|
135
154
|
/** 机器可读原因码,如 EMBEDDER_DOWNLOAD_FAILED / SCHEMA_INCOMPATIBLE。 */
|
|
@@ -415,7 +434,7 @@ function redacted(kind) {
|
|
|
415
434
|
* @returns 脱敏后的文本;无命中时原样返回。
|
|
416
435
|
*/
|
|
417
436
|
function redactSecrets(text) {
|
|
418
|
-
return text.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, redacted("private-key")).replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi, `Bearer ${redacted("bearer-token")}`).replace(/\bsk-(?:proj-|cp-|ant-|svcacct-)?[A-Za-z0-9_-]{16,}/g, redacted("api-key")).replace(/\bgh[pousr]_[A-Za-z0-9]{20,}/g, redacted("github-token")).replace(/\bAKIA[0-9A-Z]{16}\b/g, redacted("aws-access-key")).replace(/\b(password|passwd|pwd|token|secret|api[_-]?key|access[_-]?key|auth[_-]?token)\b\s*[=:]\s*("[^"\n]*"|'[^'\n]*'|[^'",;\s)\]}]+)/gi, (_match, key) => `${key}=${redacted("secret-value")}`);
|
|
437
|
+
return text.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, redacted("private-key")).replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi, `Bearer ${redacted("bearer-token")}`).replace(/\bsk-(?:proj-|cp-|ant-|svcacct-)?[A-Za-z0-9_-]{16,}/g, redacted("api-key")).replace(/\bgh[pousr]_[A-Za-z0-9]{20,}/g, redacted("github-token")).replace(/\bAKIA[0-9A-Z]{16}\b/g, redacted("aws-access-key")).replace(/\b(password|passwd|pwd|token|secret|api[_-]?key|access[_-]?key|auth[_-]?token)\b\s*[=:]\s*("[^"\n]*"|'[^'\n]*'|[^'",;\s)\]}]+)/gi, (_match, key) => `${key}=${redacted("secret-value")}`).replace(/(密码|口令)[是为::\s]{0,3}([A-Za-z0-9!@#$%^&*()_+=\[\]{}<>/?\\|`~.-]{4,})/g, (_match, key) => `${key}=${redacted("password")}`).replace(/(?<!\d)1[3-9]\d{9}(?!\d)/g, redacted("phone")).replace(/\b[1-9]\d{5}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]\b/g, redacted("id-number"));
|
|
419
438
|
}
|
|
420
439
|
//#endregion
|
|
421
440
|
//#region src/security/recall.ts
|
|
@@ -484,6 +503,42 @@ function omitRecallToolResults(events) {
|
|
|
484
503
|
};
|
|
485
504
|
});
|
|
486
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* 判定一条新内容的处置。
|
|
508
|
+
* 嵌入不可用 / 库内无向量条目 / 最近邻低于 DEFER_COSINE → accept;
|
|
509
|
+
* 最近邻同 kind 且 ≥ MERGE_COSINE → merge;其余达到 DEFER_COSINE 的 → defer。
|
|
510
|
+
* @param store - 目标分库
|
|
511
|
+
* @param kind - 新内容的记忆种类(MERGE 要求同 kind,跨 kind 高度相似按 defer 处理)
|
|
512
|
+
* @param embedding - 新内容的向量;undefined = 嵌入不可用,直接 accept
|
|
513
|
+
*/
|
|
514
|
+
async function decideWrite(store, kind, embedding) {
|
|
515
|
+
if (embedding === void 0) return { disposition: "accept" };
|
|
516
|
+
const nearest = await store.nearestNeighbor(embedding);
|
|
517
|
+
if (nearest === void 0 || nearest.similarity < .88) return { disposition: "accept" };
|
|
518
|
+
if (nearest.similarity >= .92 && nearest.record.kind === kind) return {
|
|
519
|
+
disposition: "merge",
|
|
520
|
+
into: nearest.record,
|
|
521
|
+
similarity: nearest.similarity
|
|
522
|
+
};
|
|
523
|
+
return {
|
|
524
|
+
disposition: "defer",
|
|
525
|
+
neighbor: nearest.record,
|
|
526
|
+
similarity: nearest.similarity
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* MERGE 落地:强化既有条目(accessCount+1 / confidence+0.05)并记 write-merge 审计行。
|
|
531
|
+
* @returns 强化后的既有条目;目标在并发窗口内被物理清除时抛错(loud,调用方按写入失败处理)。
|
|
532
|
+
*/
|
|
533
|
+
async function applyMerge(store, decision, content) {
|
|
534
|
+
const note = JSON.stringify({
|
|
535
|
+
content: content.slice(0, 40),
|
|
536
|
+
similarity: Number(decision.similarity.toFixed(4))
|
|
537
|
+
});
|
|
538
|
+
const record = await store.reinforce(decision.into.id, note);
|
|
539
|
+
if (record === void 0) throw new Error(`write-disposition: 并入目标 ${decision.into.id} 不存在`);
|
|
540
|
+
return record;
|
|
541
|
+
}
|
|
487
542
|
//#endregion
|
|
488
543
|
//#region src/ingest/hook.ts
|
|
489
544
|
/** 档位参数(协议内常量,非部署 tunables)。 */
|
|
@@ -695,6 +750,9 @@ async function ingestPreviousTurn(deps) {
|
|
|
695
750
|
scannedEvents: 0,
|
|
696
751
|
candidates: 0,
|
|
697
752
|
written: 0,
|
|
753
|
+
merged: 0,
|
|
754
|
+
deferred: 0,
|
|
755
|
+
dropped: 0,
|
|
698
756
|
skipped: typeof sliceMode === "number" ? "no-such-turn" : "no-previous-turn"
|
|
699
757
|
};
|
|
700
758
|
const store = await deps.openStore();
|
|
@@ -704,6 +762,9 @@ async function ingestPreviousTurn(deps) {
|
|
|
704
762
|
scannedEvents: slice.length,
|
|
705
763
|
candidates: 0,
|
|
706
764
|
written: 0,
|
|
765
|
+
merged: 0,
|
|
766
|
+
deferred: 0,
|
|
767
|
+
dropped: 0,
|
|
707
768
|
skipped: "already-ingested"
|
|
708
769
|
};
|
|
709
770
|
if (sliceMode === "previous" || deps.throttle === true) {
|
|
@@ -712,6 +773,9 @@ async function ingestPreviousTurn(deps) {
|
|
|
712
773
|
scannedEvents: slice.length,
|
|
713
774
|
candidates: 0,
|
|
714
775
|
written: 0,
|
|
776
|
+
merged: 0,
|
|
777
|
+
deferred: 0,
|
|
778
|
+
dropped: 0,
|
|
715
779
|
skipped: throttled
|
|
716
780
|
};
|
|
717
781
|
}
|
|
@@ -722,6 +786,9 @@ async function ingestPreviousTurn(deps) {
|
|
|
722
786
|
scannedEvents: slice.length,
|
|
723
787
|
candidates: 0,
|
|
724
788
|
written: 0,
|
|
789
|
+
merged: 0,
|
|
790
|
+
deferred: 0,
|
|
791
|
+
dropped: 0,
|
|
725
792
|
skipped: "no-user-content"
|
|
726
793
|
};
|
|
727
794
|
const route = deps.routeOverride ?? routeFromEvents(deps.events);
|
|
@@ -729,6 +796,9 @@ async function ingestPreviousTurn(deps) {
|
|
|
729
796
|
scannedEvents: slice.length,
|
|
730
797
|
candidates: 0,
|
|
731
798
|
written: 0,
|
|
799
|
+
merged: 0,
|
|
800
|
+
deferred: 0,
|
|
801
|
+
dropped: 0,
|
|
732
802
|
skipped: "no-route-in-log"
|
|
733
803
|
};
|
|
734
804
|
const recallNote = hasRecallToolCalls(scoped) ? "(注意:上一轮调用过记忆召回工具(engram_search 等),其返回已省略;助手回答中复述的既有记忆不是新信息,不要提取。)" : "";
|
|
@@ -752,16 +822,28 @@ async function ingestPreviousTurn(deps) {
|
|
|
752
822
|
scannedEvents: slice.length,
|
|
753
823
|
candidates: 0,
|
|
754
824
|
written: 0,
|
|
825
|
+
merged: 0,
|
|
826
|
+
deferred: 0,
|
|
827
|
+
dropped: 0,
|
|
755
828
|
skipped: "unparseable-output"
|
|
756
829
|
};
|
|
757
830
|
const embedder = await deps.embedder;
|
|
758
831
|
const writtenContents = [];
|
|
759
832
|
let written = 0;
|
|
833
|
+
let merged = 0;
|
|
834
|
+
let deferred = 0;
|
|
835
|
+
let dropped = 0;
|
|
760
836
|
for (const item of parsed.slice(0, limits.maxCandidates)) {
|
|
761
837
|
const candidate = item;
|
|
762
|
-
if (typeof candidate.content !== "string" || candidate.content.trim() === "")
|
|
838
|
+
if (typeof candidate.content !== "string" || candidate.content.trim() === "") {
|
|
839
|
+
dropped += 1;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
763
842
|
const content = redactSecrets(sanitizeProtocolText(candidate.content.trim()));
|
|
764
|
-
if (content === "")
|
|
843
|
+
if (content === "") {
|
|
844
|
+
dropped += 1;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
765
847
|
const kind = typeof candidate.kind === "string" && [
|
|
766
848
|
"fact",
|
|
767
849
|
"preference",
|
|
@@ -773,12 +855,19 @@ async function ingestPreviousTurn(deps) {
|
|
|
773
855
|
const scope = deps.perCandidateScope === true && modelScope !== void 0 ? modelScope : deps.writeScope ?? "user";
|
|
774
856
|
const target = deps.resolveStore === void 0 ? store : await deps.resolveStore(scope);
|
|
775
857
|
const importance = typeof candidate.importance === "number" && Number.isFinite(candidate.importance) ? Math.min(1, Math.max(0, candidate.importance)) : .5;
|
|
776
|
-
if (
|
|
777
|
-
|
|
778
|
-
|
|
858
|
+
if (writtenContents.includes(content)) {
|
|
859
|
+
dropped += 1;
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
const vector = embedder === void 0 ? void 0 : (await embedder.embed([content]))[0];
|
|
863
|
+
const decision = await decideWrite(target, kind, vector);
|
|
864
|
+
if (decision.disposition === "merge") {
|
|
865
|
+
await applyMerge(target, decision, content);
|
|
866
|
+
writtenContents.push(content);
|
|
867
|
+
merged += 1;
|
|
868
|
+
continue;
|
|
779
869
|
}
|
|
780
|
-
|
|
781
|
-
await target.write({
|
|
870
|
+
const record = await target.write({
|
|
782
871
|
scope,
|
|
783
872
|
kind,
|
|
784
873
|
content,
|
|
@@ -787,9 +876,13 @@ async function ingestPreviousTurn(deps) {
|
|
|
787
876
|
sourceSessionId: deps.sessionId,
|
|
788
877
|
sourceRound: round,
|
|
789
878
|
...minSeq === null ? {} : { sourceSeq: minSeq },
|
|
790
|
-
...
|
|
879
|
+
...vector === void 0 ? {} : { embedding: vector },
|
|
791
880
|
...deps.history === true ? { initialReviewAt: null } : {}
|
|
792
881
|
});
|
|
882
|
+
if (decision.disposition === "defer") {
|
|
883
|
+
await target.linkEdge(record.id, decision.neighbor.id, "contradicts");
|
|
884
|
+
deferred += 1;
|
|
885
|
+
}
|
|
793
886
|
writtenContents.push(content);
|
|
794
887
|
written += 1;
|
|
795
888
|
}
|
|
@@ -798,6 +891,9 @@ async function ingestPreviousTurn(deps) {
|
|
|
798
891
|
scannedEvents: slice.length,
|
|
799
892
|
candidates: parsed.length,
|
|
800
893
|
written,
|
|
894
|
+
merged,
|
|
895
|
+
deferred,
|
|
896
|
+
dropped,
|
|
801
897
|
skipped: null
|
|
802
898
|
};
|
|
803
899
|
}
|
|
@@ -836,6 +932,62 @@ async function ingestFinalTurn(deps) {
|
|
|
836
932
|
return null;
|
|
837
933
|
}
|
|
838
934
|
}
|
|
935
|
+
/** 会话摘要提示词:一句话概括整场会话,供情景时间线的组头展示。 */
|
|
936
|
+
const SESSION_SUMMARY_SYSTEM = ["概括下面这段会话:用一句话(不超过 60 字)说明这场会话主要做了什么、达成了什么结果。", "直接输出摘要本身,不要前缀、引号或任何解释。"].join("\n");
|
|
937
|
+
/** 会话摘要输出 token 上限(一句话足够)。 */
|
|
938
|
+
const SESSION_SUMMARY_MAX_TOKENS = 160;
|
|
939
|
+
/** 会话摘要的输入截断上限(字符):摘要只需主题级信息,超长会话截尾。 */
|
|
940
|
+
const SESSION_SUMMARY_TEXT_LIMIT = 4e3;
|
|
941
|
+
/** 收集整场会话的对话文本(用户跳过插件注入快照 + 助手),脱敏后拼接并截断。 */
|
|
942
|
+
function collectSessionText(events) {
|
|
943
|
+
const texts = [];
|
|
944
|
+
for (const event of events) if (event.type === "user/message") {
|
|
945
|
+
const data = event.data;
|
|
946
|
+
if (data?.source?.kind === "plugin") continue;
|
|
947
|
+
const segments = (data?.content ?? []).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text);
|
|
948
|
+
if (segments.length > 0) texts.push(...segments);
|
|
949
|
+
} else if (event.type === "assistant/message") {
|
|
950
|
+
const segments = (event.data?.content ?? []).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text);
|
|
951
|
+
if (segments.length > 0) texts.push(...segments);
|
|
952
|
+
}
|
|
953
|
+
const joined = texts.map((text) => redactSecrets(sanitizeProtocolText(text))).filter((text) => text !== "").join("\n");
|
|
954
|
+
return joined.length > SESSION_SUMMARY_TEXT_LIMIT ? `${joined.slice(0, SESSION_SUMMARY_TEXT_LIMIT)}…` : joined;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* 会话循环结束处生成一句话摘要(情景时间线组头的增强信息):
|
|
958
|
+
* 已有摘要直接跳过(重跑幂等,不再起 LLM);无可用路由 / 已中止 / 无对话文本同样跳过。
|
|
959
|
+
* LLM 失败静默:摘要是增强信息,实时路径挂在 dispose 观察器、回填路径在批次循环里,
|
|
960
|
+
* 一次限流/超时都不值得升级为告警或失败。
|
|
961
|
+
*/
|
|
962
|
+
async function ensureSessionSummary(deps) {
|
|
963
|
+
if (deps.signal.aborted) return;
|
|
964
|
+
const route = deps.routeOverride ?? routeFromEvents(deps.events);
|
|
965
|
+
if (route === void 0) return;
|
|
966
|
+
const dialog = collectSessionText(deps.events);
|
|
967
|
+
if (dialog === "") return;
|
|
968
|
+
const store = await deps.openStore();
|
|
969
|
+
if (await store.getSessionSummary(deps.sessionId) !== void 0) return;
|
|
970
|
+
const userText = `概括下面这段会话:\n${dialog}`;
|
|
971
|
+
deps.logRequest({
|
|
972
|
+
route,
|
|
973
|
+
round: deps.round ?? 0,
|
|
974
|
+
userText,
|
|
975
|
+
maxTokens: SESSION_SUMMARY_MAX_TOKENS,
|
|
976
|
+
mode: deps.mode
|
|
977
|
+
});
|
|
978
|
+
try {
|
|
979
|
+
const summary = (await deps.call({
|
|
980
|
+
route,
|
|
981
|
+
system: SESSION_SUMMARY_SYSTEM,
|
|
982
|
+
userText,
|
|
983
|
+
maxTokens: SESSION_SUMMARY_MAX_TOKENS,
|
|
984
|
+
purpose: "engram-session-summary",
|
|
985
|
+
signal: deps.signal
|
|
986
|
+
})).trim();
|
|
987
|
+
if (summary === "") return;
|
|
988
|
+
await store.setSessionSummary(deps.sessionId, summary);
|
|
989
|
+
} catch {}
|
|
990
|
+
}
|
|
839
991
|
/**
|
|
840
992
|
* 重放待补做的末轮摄取(下次会话首次 pre-step 调用)。已有 done 标记或键损坏的
|
|
841
993
|
* pending 直接出队;事件源不可得的保留到下次。单个键失败抛出,剩余键留待下次。
|
|
@@ -1147,6 +1299,17 @@ async function runHistoryBackfill(deps, defaults, overrides, onProgress, signal)
|
|
|
1147
1299
|
}
|
|
1148
1300
|
progress.turnsDone += 1;
|
|
1149
1301
|
}
|
|
1302
|
+
await ensureSessionSummary({
|
|
1303
|
+
events: candidate.events,
|
|
1304
|
+
sessionId: candidate.header.id,
|
|
1305
|
+
openStore: () => candidate.openStore(),
|
|
1306
|
+
call: deps.call,
|
|
1307
|
+
logRequest: deps.logRequest,
|
|
1308
|
+
mode: deps.mode,
|
|
1309
|
+
routeOverride,
|
|
1310
|
+
signal,
|
|
1311
|
+
round: candidate.turns[candidate.turns.length - 1]
|
|
1312
|
+
});
|
|
1150
1313
|
progress.sessionsDone += 1;
|
|
1151
1314
|
onProgress({
|
|
1152
1315
|
...progress,
|
|
@@ -2104,6 +2267,58 @@ function registerEngramRoutes(ctx, deps) {
|
|
|
2104
2267
|
});
|
|
2105
2268
|
return;
|
|
2106
2269
|
}
|
|
2270
|
+
if (req.method === "GET" && route === "episode-timeline") {
|
|
2271
|
+
const scope = scopeOf$1(url.searchParams.get("scope"), "user");
|
|
2272
|
+
const since = toNumber(url.searchParams.get("since"));
|
|
2273
|
+
const until = toNumber(url.searchParams.get("until"));
|
|
2274
|
+
const sessionId = url.searchParams.get("sessionId") ?? void 0;
|
|
2275
|
+
const around = url.searchParams.get("around") ?? void 0;
|
|
2276
|
+
const windowMinutes = Math.min(1440, Math.max(1, toNumber(url.searchParams.get("windowMinutes")) ?? 60));
|
|
2277
|
+
const limit = Math.min(200, Math.max(1, toNumber(url.searchParams.get("limit")) ?? 100));
|
|
2278
|
+
const store = await storeFor(scope);
|
|
2279
|
+
if (around !== void 0 && around !== "") {
|
|
2280
|
+
const view = (await store.episodeTimeline({
|
|
2281
|
+
scopes: [scope],
|
|
2282
|
+
around,
|
|
2283
|
+
proximityMs: windowMinutes * 6e4,
|
|
2284
|
+
limit
|
|
2285
|
+
})).around;
|
|
2286
|
+
json(res, 200, { around: {
|
|
2287
|
+
anchor: {
|
|
2288
|
+
id: view.anchor.id,
|
|
2289
|
+
kind: view.anchor.kind,
|
|
2290
|
+
content: view.anchor.content,
|
|
2291
|
+
createdAt: view.anchor.createdAt
|
|
2292
|
+
},
|
|
2293
|
+
neighbors: view.neighbors.map((record) => ({
|
|
2294
|
+
id: record.id,
|
|
2295
|
+
kind: record.kind,
|
|
2296
|
+
content: record.content,
|
|
2297
|
+
createdAt: record.createdAt
|
|
2298
|
+
}))
|
|
2299
|
+
} });
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
json(res, 200, { groups: (await store.episodeTimeline({
|
|
2303
|
+
scopes: [scope],
|
|
2304
|
+
...since === void 0 ? {} : { since },
|
|
2305
|
+
...until === void 0 ? {} : { until },
|
|
2306
|
+
...sessionId === void 0 || sessionId === "" ? {} : { sessionId },
|
|
2307
|
+
limit
|
|
2308
|
+
})).groups.map((group) => ({
|
|
2309
|
+
sessionId: group.sessionId,
|
|
2310
|
+
...group.summary === void 0 ? {} : { summary: group.summary },
|
|
2311
|
+
startedAt: group.startedAt,
|
|
2312
|
+
endedAt: group.endedAt,
|
|
2313
|
+
episodes: group.episodes.map((record) => ({
|
|
2314
|
+
id: record.id,
|
|
2315
|
+
kind: record.kind,
|
|
2316
|
+
content: record.content,
|
|
2317
|
+
createdAt: record.createdAt
|
|
2318
|
+
}))
|
|
2319
|
+
})) });
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2107
2322
|
if (req.method === "GET" && route === "activity") {
|
|
2108
2323
|
const limit = Math.min(50, Math.max(1, Number(url.searchParams.get("limit") ?? 20) || 20));
|
|
2109
2324
|
json(res, 200, { operations: (await Promise.all(["user", "project"].map(async (scope) => {
|
|
@@ -2378,6 +2593,18 @@ function registerEngramRoutes(ctx, deps) {
|
|
|
2378
2593
|
json(res, 200, await deps.history.models());
|
|
2379
2594
|
return;
|
|
2380
2595
|
}
|
|
2596
|
+
if (req.method === "GET" && route === "profile-block") {
|
|
2597
|
+
const scope = scopeOf$1(url.searchParams.get("scope"), "user");
|
|
2598
|
+
const store = await storeFor(scope);
|
|
2599
|
+
const block = await store.getProfileBlock(scope);
|
|
2600
|
+
const versions = await store.listProfileBlockVersions(scope, 20);
|
|
2601
|
+
json(res, 200, {
|
|
2602
|
+
scope,
|
|
2603
|
+
block: block ?? null,
|
|
2604
|
+
versions
|
|
2605
|
+
});
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2381
2608
|
if (req.method === "GET" && route === "history-backfill") {
|
|
2382
2609
|
json(res, 200, {
|
|
2383
2610
|
defaults: deps.history.defaults,
|
|
@@ -2877,14 +3104,20 @@ function placardImprovementHint(score) {
|
|
|
2877
3104
|
* @module @kenz1117/dsh-engram/store/sqlite
|
|
2878
3105
|
*/
|
|
2879
3106
|
/** 当前 schema 版本;结构性变更必须 +1。可空列与伴随表走增量迁移(见 openEngramStore 的迁移段)。 */
|
|
2880
|
-
const SCHEMA_VERSION =
|
|
3107
|
+
const SCHEMA_VERSION = 9;
|
|
2881
3108
|
/** 增量迁移表:key 为起始版本,value 为升到下一版本的 SQL(可多语句)。
|
|
2882
3109
|
* v2 → v3:nodes 补可空列 outcome(使用效果回报)。
|
|
2883
3110
|
* v3 → v4:新增 nodes_revisions 修订表(update 归档旧条目时的内容快照)。
|
|
2884
3111
|
* v4 → v5:nodes 补 imagery_json 列(意象铭牌:caption + sensoryTags + emotionalValence + provisional)。
|
|
2885
3112
|
* v5 → v6:桩位(slot_room/slot_index)、意象质量分(imagery_score)、SM-2 调度
|
|
2886
3113
|
* (next_review_at/ease_factor/interval_days/review_reps)+ 固定巡游路线表 tour_routes。
|
|
2887
|
-
* 全部可空或带默认值,存量条目零搬运;排桩由 backfillSlots 幂等补齐。
|
|
3114
|
+
* 全部可空或带默认值,存量条目零搬运;排桩由 backfillSlots 幂等补齐。
|
|
3115
|
+
* v6 → v7:画像 curated block 两表(profile_blocks 当前态 + profile_block_versions
|
|
3116
|
+
* 追加式版本链)。scope 作主键,每 scope 至多一个 block;版本链只增不改,rollback 数据源。
|
|
3117
|
+
* v7 → v8:episode 情景时间线两个专用索引(kind+created_at 日期范围扫描、
|
|
3118
|
+
* source_session_id+created_at 会话过滤)。列自 v1 就存在,纯索引迁移,零数据搬运。
|
|
3119
|
+
* v8 → v9:会话摘要表 session_summaries(摄取期 LLM 生成的一句话会话摘要,
|
|
3120
|
+
* 组头展示用;与 nodes 无外键,会话 id 仅作逻辑关联)。 */
|
|
2888
3121
|
const MIGRATIONS = {
|
|
2889
3122
|
"2": "ALTER TABLE nodes ADD COLUMN outcome TEXT",
|
|
2890
3123
|
"3": `CREATE TABLE IF NOT EXISTS nodes_revisions (
|
|
@@ -2901,7 +3134,17 @@ const MIGRATIONS = {
|
|
|
2901
3134
|
CREATE TABLE IF NOT EXISTS tour_routes (
|
|
2902
3135
|
position INTEGER PRIMARY KEY, node_id TEXT NOT NULL);
|
|
2903
3136
|
CREATE INDEX IF NOT EXISTS nodes_slot ON nodes (slot_room, slot_index);
|
|
2904
|
-
CREATE INDEX IF NOT EXISTS nodes_review_due ON nodes (next_review_at)
|
|
3137
|
+
CREATE INDEX IF NOT EXISTS nodes_review_due ON nodes (next_review_at);`,
|
|
3138
|
+
"6": `CREATE TABLE IF NOT EXISTS profile_blocks (
|
|
3139
|
+
scope TEXT PRIMARY KEY, content TEXT NOT NULL, version INTEGER NOT NULL,
|
|
3140
|
+
updated_at INTEGER NOT NULL);
|
|
3141
|
+
CREATE TABLE IF NOT EXISTS profile_block_versions (
|
|
3142
|
+
scope TEXT NOT NULL, version INTEGER NOT NULL, content TEXT NOT NULL,
|
|
3143
|
+
source TEXT NOT NULL, at INTEGER NOT NULL, PRIMARY KEY (scope, version));`,
|
|
3144
|
+
"7": `CREATE INDEX IF NOT EXISTS nodes_kind_created ON nodes (kind, created_at);
|
|
3145
|
+
CREATE INDEX IF NOT EXISTS nodes_session_created ON nodes (source_session_id, created_at);`,
|
|
3146
|
+
"8": `CREATE TABLE IF NOT EXISTS session_summaries (
|
|
3147
|
+
session_id TEXT PRIMARY KEY, summary TEXT NOT NULL, updated_at INTEGER NOT NULL);`
|
|
2905
3148
|
};
|
|
2906
3149
|
/** RRF 融合常数:score = Σ 1/(K + rank)。 */
|
|
2907
3150
|
const RRF_K = 60;
|
|
@@ -3079,8 +3322,18 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
3079
3322
|
importance REAL NOT NULL, superseded_at INTEGER NOT NULL);
|
|
3080
3323
|
CREATE TABLE IF NOT EXISTS tour_routes (
|
|
3081
3324
|
position INTEGER PRIMARY KEY, node_id TEXT NOT NULL);
|
|
3325
|
+
CREATE TABLE IF NOT EXISTS profile_blocks (
|
|
3326
|
+
scope TEXT PRIMARY KEY, content TEXT NOT NULL, version INTEGER NOT NULL,
|
|
3327
|
+
updated_at INTEGER NOT NULL);
|
|
3328
|
+
CREATE TABLE IF NOT EXISTS profile_block_versions (
|
|
3329
|
+
scope TEXT NOT NULL, version INTEGER NOT NULL, content TEXT NOT NULL,
|
|
3330
|
+
source TEXT NOT NULL, at INTEGER NOT NULL, PRIMARY KEY (scope, version));
|
|
3331
|
+
CREATE TABLE IF NOT EXISTS session_summaries (
|
|
3332
|
+
session_id TEXT PRIMARY KEY, summary TEXT NOT NULL, updated_at INTEGER NOT NULL);
|
|
3082
3333
|
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(node_id UNINDEXED, content, tokenize='unicode61');
|
|
3083
3334
|
CREATE INDEX IF NOT EXISTS nodes_scope_status ON nodes (scope, status);
|
|
3335
|
+
CREATE INDEX IF NOT EXISTS nodes_kind_created ON nodes (kind, created_at);
|
|
3336
|
+
CREATE INDEX IF NOT EXISTS nodes_session_created ON nodes (source_session_id, created_at);
|
|
3084
3337
|
`);
|
|
3085
3338
|
const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
3086
3339
|
if (versionRow === void 0) {
|
|
@@ -3184,6 +3437,19 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
3184
3437
|
const sqlPurgeFts = db.prepare("DELETE FROM nodes_fts");
|
|
3185
3438
|
const sqlPurgeLog = db.prepare("DELETE FROM op_log");
|
|
3186
3439
|
const sqlPurgeRoutes = db.prepare("DELETE FROM tour_routes");
|
|
3440
|
+
const sqlPurgeProfileBlocks = db.prepare("DELETE FROM profile_blocks");
|
|
3441
|
+
const sqlPurgeProfileVersions = db.prepare("DELETE FROM profile_block_versions");
|
|
3442
|
+
const sqlPurgeSessionSummaries = db.prepare("DELETE FROM session_summaries");
|
|
3443
|
+
const sqlProfileBlockGet = db.prepare("SELECT scope, content, version, updated_at FROM profile_blocks WHERE scope = ?");
|
|
3444
|
+
const sqlProfileBlockUpsert = db.prepare(`INSERT INTO profile_blocks (scope, content, version, updated_at) VALUES (?, ?, ?, ?)
|
|
3445
|
+
ON CONFLICT(scope) DO UPDATE SET content = excluded.content, version = excluded.version, updated_at = excluded.updated_at`);
|
|
3446
|
+
const sqlProfileVersionInsert = db.prepare("INSERT INTO profile_block_versions (scope, version, content, source, at) VALUES (?, ?, ?, ?, ?)");
|
|
3447
|
+
const sqlProfileVersionsList = db.prepare("SELECT scope, version, content, source, at FROM profile_block_versions WHERE scope = ? ORDER BY version DESC LIMIT ?");
|
|
3448
|
+
const sqlProfileVersionGet = db.prepare("SELECT scope, version, content, source, at FROM profile_block_versions WHERE scope = ? AND version = ?");
|
|
3449
|
+
const sqlSessionSummaryUpsert = db.prepare(`INSERT INTO session_summaries (session_id, summary, updated_at) VALUES (?, ?, ?)
|
|
3450
|
+
ON CONFLICT(session_id) DO UPDATE SET summary = excluded.summary, updated_at = excluded.updated_at`);
|
|
3451
|
+
const sqlSessionSummaryGet = db.prepare("SELECT session_id, summary FROM session_summaries WHERE session_id = ?");
|
|
3452
|
+
const sqlSessionSummaryMany = db.prepare("SELECT session_id, summary FROM session_summaries WHERE session_id IN (SELECT value FROM json_each(?))");
|
|
3187
3453
|
/** FTS 道:按 scope 集合检索(占位符动态生成,scope 集合由调用方去重);rooms 非空时只查指定房间。 */
|
|
3188
3454
|
const ftsSearch = (match, scopes, rooms) => {
|
|
3189
3455
|
const placeholders = scopes.map(() => "?").join(",");
|
|
@@ -3414,6 +3680,78 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
3414
3680
|
return (query.order === "tour" ? db.prepare(`SELECT nodes.* FROM nodes LEFT JOIN tour_routes ON tour_routes.node_id = nodes.id
|
|
3415
3681
|
WHERE ${where} ORDER BY tour_routes.position IS NULL, tour_routes.position ASC, nodes.created_at DESC LIMIT ?`).all(...params) : db.prepare(`SELECT nodes.* FROM nodes WHERE ${where} ORDER BY nodes.created_at DESC LIMIT ?`).all(...params)).map(rowToRecord);
|
|
3416
3682
|
},
|
|
3683
|
+
async episodeTimeline(query) {
|
|
3684
|
+
const limit = Math.max(1, Math.floor(query.limit ?? 100));
|
|
3685
|
+
const placeholders = query.scopes.map(() => "?").join(",");
|
|
3686
|
+
if (query.around !== void 0) {
|
|
3687
|
+
const anchorRow = getRow(query.around);
|
|
3688
|
+
if (anchorRow === void 0) throw new EngramError("NOT_FOUND", `邻近扩展锚点 ${query.around} 不存在`);
|
|
3689
|
+
const window = Math.max(0, Math.floor(query.proximityMs ?? 36e5));
|
|
3690
|
+
const rows = db.prepare(`SELECT * FROM nodes
|
|
3691
|
+
WHERE kind = 'episode' AND status = 'active' AND scope IN (${placeholders})
|
|
3692
|
+
AND id != ? AND created_at >= ? AND created_at <= ?
|
|
3693
|
+
ORDER BY created_at ASC LIMIT ?`).all(...query.scopes, query.around, anchorRow.created_at - window, anchorRow.created_at + window, limit);
|
|
3694
|
+
return {
|
|
3695
|
+
groups: [],
|
|
3696
|
+
around: {
|
|
3697
|
+
anchor: rowToRecord(anchorRow),
|
|
3698
|
+
neighbors: rows.map(rowToRecord)
|
|
3699
|
+
}
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
const params = [
|
|
3703
|
+
...query.scopes,
|
|
3704
|
+
query.since ?? null,
|
|
3705
|
+
query.since ?? null,
|
|
3706
|
+
query.until ?? null,
|
|
3707
|
+
query.until ?? null,
|
|
3708
|
+
query.sessionId ?? null,
|
|
3709
|
+
query.sessionId ?? null,
|
|
3710
|
+
limit
|
|
3711
|
+
];
|
|
3712
|
+
const rows = db.prepare(`SELECT * FROM nodes
|
|
3713
|
+
WHERE kind = 'episode' AND status = 'active' AND scope IN (${placeholders})
|
|
3714
|
+
AND (? IS NULL OR created_at >= ?) AND (? IS NULL OR created_at <= ?)
|
|
3715
|
+
AND (? IS NULL OR source_session_id = ?)
|
|
3716
|
+
ORDER BY created_at DESC LIMIT ?`).all(...params);
|
|
3717
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
3718
|
+
for (const row of rows) {
|
|
3719
|
+
const record = rowToRecord(row);
|
|
3720
|
+
const bucket = bySession.get(record.sourceSessionId);
|
|
3721
|
+
if (bucket === void 0) bySession.set(record.sourceSessionId, [record]);
|
|
3722
|
+
else bucket.push(record);
|
|
3723
|
+
}
|
|
3724
|
+
const groups = [...bySession.entries()].map(([sessionId, episodesDesc]) => {
|
|
3725
|
+
const episodes = [...episodesDesc].reverse();
|
|
3726
|
+
return {
|
|
3727
|
+
sessionId,
|
|
3728
|
+
episodes,
|
|
3729
|
+
startedAt: episodes[0].createdAt,
|
|
3730
|
+
endedAt: episodes[episodes.length - 1].createdAt
|
|
3731
|
+
};
|
|
3732
|
+
});
|
|
3733
|
+
groups.sort((a, b) => b.startedAt - a.startedAt);
|
|
3734
|
+
const sessionIds = groups.map((g) => g.sessionId).filter((id) => id !== null);
|
|
3735
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
3736
|
+
if (sessionIds.length > 0) {
|
|
3737
|
+
const rows = sqlSessionSummaryMany.all(JSON.stringify(sessionIds));
|
|
3738
|
+
for (const row of rows) summaries.set(row.session_id, row.summary);
|
|
3739
|
+
}
|
|
3740
|
+
return { groups: groups.map((group) => {
|
|
3741
|
+
const summary = group.sessionId === null ? void 0 : summaries.get(group.sessionId);
|
|
3742
|
+
return {
|
|
3743
|
+
...group,
|
|
3744
|
+
...summary === void 0 ? {} : { summary }
|
|
3745
|
+
};
|
|
3746
|
+
}) };
|
|
3747
|
+
},
|
|
3748
|
+
async setSessionSummary(sessionId, summary) {
|
|
3749
|
+
sqlSessionSummaryUpsert.run(sessionId, summary, Date.now());
|
|
3750
|
+
sqlLog.run(Date.now(), "session-summary", sessionId, JSON.stringify({ length: summary.length }));
|
|
3751
|
+
},
|
|
3752
|
+
async getSessionSummary(sessionId) {
|
|
3753
|
+
return sqlSessionSummaryGet.get(sessionId)?.summary;
|
|
3754
|
+
},
|
|
3417
3755
|
async update(input) {
|
|
3418
3756
|
const old = getRow(input.id);
|
|
3419
3757
|
if (old === void 0) throw new EngramError("NOT_FOUND", `条目 ${input.id} 不存在`);
|
|
@@ -3572,6 +3910,54 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
3572
3910
|
async listPlacards() {
|
|
3573
3911
|
return sqlListPlacards.all().filter((row) => typeof row.caption === "string" && row.caption !== "");
|
|
3574
3912
|
},
|
|
3913
|
+
async getProfileBlock(scope) {
|
|
3914
|
+
const row = sqlProfileBlockGet.get(scope);
|
|
3915
|
+
if (row === void 0) return void 0;
|
|
3916
|
+
return {
|
|
3917
|
+
scope: row.scope,
|
|
3918
|
+
content: row.content,
|
|
3919
|
+
version: row.version,
|
|
3920
|
+
updatedAt: row.updated_at
|
|
3921
|
+
};
|
|
3922
|
+
},
|
|
3923
|
+
async saveProfileBlock(scope, expectedVersion, content, source) {
|
|
3924
|
+
if (content.trim() === "") throw new EngramError("EMPTY_CONTENT", "画像 block 内容不能为空");
|
|
3925
|
+
const current = sqlProfileBlockGet.get(scope);
|
|
3926
|
+
if (current === void 0 && expectedVersion !== void 0) throw new EngramError("VERSION_CONFLICT", `画像 block 不存在(${scope}),不能带 expectedVersion=${String(expectedVersion)} 编辑;先传 undefined 创建`);
|
|
3927
|
+
if (current !== void 0 && expectedVersion !== current.version) throw new EngramError("VERSION_CONFLICT", `画像 block 版本冲突(${scope}):当前 v${String(current.version)},请求基于 v${String(expectedVersion)};请重读最新版本后再改`);
|
|
3928
|
+
const nextVersion = current === void 0 ? 1 : current.version + 1;
|
|
3929
|
+
const now = Date.now();
|
|
3930
|
+
withTransaction(() => {
|
|
3931
|
+
sqlProfileVersionInsert.run(scope, nextVersion, content, source, now);
|
|
3932
|
+
sqlProfileBlockUpsert.run(scope, content, nextVersion, now);
|
|
3933
|
+
});
|
|
3934
|
+
return {
|
|
3935
|
+
scope,
|
|
3936
|
+
content,
|
|
3937
|
+
version: nextVersion,
|
|
3938
|
+
updatedAt: now
|
|
3939
|
+
};
|
|
3940
|
+
},
|
|
3941
|
+
async listProfileBlockVersions(scope, limit) {
|
|
3942
|
+
return sqlProfileVersionsList.all(scope, Math.max(1, limit)).map((row) => ({
|
|
3943
|
+
scope: row.scope,
|
|
3944
|
+
version: row.version,
|
|
3945
|
+
content: row.content,
|
|
3946
|
+
source: row.source === "rollback" ? "rollback" : "edit",
|
|
3947
|
+
at: row.at
|
|
3948
|
+
}));
|
|
3949
|
+
},
|
|
3950
|
+
async getProfileBlockVersion(scope, version) {
|
|
3951
|
+
const row = sqlProfileVersionGet.get(scope, version);
|
|
3952
|
+
if (row === void 0) return void 0;
|
|
3953
|
+
return {
|
|
3954
|
+
scope: row.scope,
|
|
3955
|
+
version: row.version,
|
|
3956
|
+
content: row.content,
|
|
3957
|
+
source: row.source === "rollback" ? "rollback" : "edit",
|
|
3958
|
+
at: row.at
|
|
3959
|
+
};
|
|
3960
|
+
},
|
|
3575
3961
|
async restore(id) {
|
|
3576
3962
|
if (getRow(id) === void 0) throw new EngramError("NOT_FOUND", `条目 ${id} 不存在`);
|
|
3577
3963
|
sqlSetStatus.run("active", Date.now(), id);
|
|
@@ -3694,6 +4080,27 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
3694
4080
|
async linkEdge(from, to, type) {
|
|
3695
4081
|
sqlEdgeUpsert.run(from, to, type, Date.now());
|
|
3696
4082
|
},
|
|
4083
|
+
async nearestNeighbor(embedding) {
|
|
4084
|
+
const pool = db.prepare("SELECT * FROM nodes WHERE status = 'active' AND embedding IS NOT NULL AND scope IN ('user','project')").all();
|
|
4085
|
+
let best;
|
|
4086
|
+
for (const row of pool) {
|
|
4087
|
+
const sim = cosine(embedding, blobToVec(row.embedding));
|
|
4088
|
+
if (best === void 0 || sim > best.sim) best = {
|
|
4089
|
+
row,
|
|
4090
|
+
sim
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
return best === void 0 ? void 0 : {
|
|
4094
|
+
record: rowToRecord(best.row),
|
|
4095
|
+
similarity: best.sim
|
|
4096
|
+
};
|
|
4097
|
+
},
|
|
4098
|
+
async reinforce(id, note) {
|
|
4099
|
+
if (sqlGet.get(id) === void 0) return void 0;
|
|
4100
|
+
sqlTouch.run(Date.now(), id);
|
|
4101
|
+
sqlLog.run(Date.now(), "write-merge", id, note);
|
|
4102
|
+
return rowToRecord(sqlGet.get(id));
|
|
4103
|
+
},
|
|
3697
4104
|
async supersedeMany(input, oldIds) {
|
|
3698
4105
|
const content = input.content.trim();
|
|
3699
4106
|
if (content === "") throw new EngramError("EMPTY_CONTENT", "content 不能为空");
|
|
@@ -3731,6 +4138,9 @@ async function openEngramStore(path, rankBoost = NO_BOOST, automation = DEFAULT_
|
|
|
3731
4138
|
sqlPurgeFts.run();
|
|
3732
4139
|
sqlPurgeLog.run();
|
|
3733
4140
|
sqlPurgeRoutes.run();
|
|
4141
|
+
sqlPurgeProfileBlocks.run();
|
|
4142
|
+
sqlPurgeProfileVersions.run();
|
|
4143
|
+
sqlPurgeSessionSummaries.run();
|
|
3734
4144
|
});
|
|
3735
4145
|
},
|
|
3736
4146
|
async close() {
|
|
@@ -3962,6 +4372,18 @@ const NEXT_STRATEGIES = [
|
|
|
3962
4372
|
"stop"
|
|
3963
4373
|
];
|
|
3964
4374
|
/**
|
|
4375
|
+
* 构造证据门收尾提醒(agent 面向文本,英文与检索工具输出一致)。
|
|
4376
|
+
* @param pendingCount - 尚未判定的批次数。
|
|
4377
|
+
* @param insufficientStreak - 连续不足判定次数。
|
|
4378
|
+
* @returns 提醒文本;无待判定批次返回 undefined(不需要提醒)。
|
|
4379
|
+
*/
|
|
4380
|
+
function buildAssessReminder(pendingCount, insufficientStreak) {
|
|
4381
|
+
if (pendingCount <= 0) return void 0;
|
|
4382
|
+
const base = `Evidence gate: ${pendingCount} search batch(es) from this conversation have not been assessed. Before answering, call engram_assess with the batchId and your evidence refs — or briefly state why assessment is not needed.`;
|
|
4383
|
+
if (insufficientStreak < 2) return base;
|
|
4384
|
+
return `${base} Multiple insufficient verdicts in a row — try a different room (engram_search room=), engram_timeline, or ask the user for the missing details.`;
|
|
4385
|
+
}
|
|
4386
|
+
/**
|
|
3965
4387
|
* 判断字符串是否为合法策略。
|
|
3966
4388
|
* @param value - 待判定字符串。
|
|
3967
4389
|
*/
|
|
@@ -3981,6 +4403,8 @@ function evidenceRefOf(scope, id, slot) {
|
|
|
3981
4403
|
/** 批次注册表:按会话隔离,超出上限丢最旧的批次。 */
|
|
3982
4404
|
var EvidenceBatches = class {
|
|
3983
4405
|
bySession = /* @__PURE__ */ new Map();
|
|
4406
|
+
/** 每会话连续不足判定的次数(sufficient 判定即清零;提醒据此升级措辞)。 */
|
|
4407
|
+
insufficientStreaks = /* @__PURE__ */ new Map();
|
|
3984
4408
|
counter = 0;
|
|
3985
4409
|
/**
|
|
3986
4410
|
* 注册一个批次。
|
|
@@ -4016,11 +4440,41 @@ var EvidenceBatches = class {
|
|
|
4016
4440
|
return this.bySession.get(sessionId)?.get(batchId);
|
|
4017
4441
|
}
|
|
4018
4442
|
/**
|
|
4019
|
-
*
|
|
4443
|
+
* 记录批次判定结果,并维护连续不足计数(sufficient 清零)。
|
|
4444
|
+
* @param sessionId - 会话标识。
|
|
4445
|
+
* @param batchId - 批次 id。
|
|
4446
|
+
* @param verdict - 代码校验后的判定结果。
|
|
4447
|
+
*/
|
|
4448
|
+
recordVerdict(sessionId, batchId, verdict) {
|
|
4449
|
+
const batch = this.get(sessionId, batchId);
|
|
4450
|
+
if (batch === void 0) return;
|
|
4451
|
+
batch.verdict = verdict;
|
|
4452
|
+
const streak = this.insufficientStreaks.get(sessionId) ?? 0;
|
|
4453
|
+
this.insufficientStreaks.set(sessionId, verdict.sufficient ? 0 : streak + 1);
|
|
4454
|
+
}
|
|
4455
|
+
/**
|
|
4456
|
+
* 会话内尚未判定的批次(注册顺序,最旧在前)。
|
|
4457
|
+
* @param sessionId - 会话标识。
|
|
4458
|
+
*/
|
|
4459
|
+
pendingBatches(sessionId) {
|
|
4460
|
+
const batches = this.bySession.get(sessionId);
|
|
4461
|
+
if (batches === void 0) return [];
|
|
4462
|
+
return [...batches.values()].filter((batch) => batch.verdict === void 0);
|
|
4463
|
+
}
|
|
4464
|
+
/**
|
|
4465
|
+
* 连续不足判定次数(sufficient 判定清零;未判定批次不影响计数)。
|
|
4466
|
+
* @param sessionId - 会话标识。
|
|
4467
|
+
*/
|
|
4468
|
+
insufficientStreak(sessionId) {
|
|
4469
|
+
return this.insufficientStreaks.get(sessionId) ?? 0;
|
|
4470
|
+
}
|
|
4471
|
+
/**
|
|
4472
|
+
* 清理会话的全部批次与判定计数(会话销毁时调用,避免长驻进程累积)。
|
|
4020
4473
|
* @param sessionId - 会话标识。
|
|
4021
4474
|
*/
|
|
4022
4475
|
clear(sessionId) {
|
|
4023
4476
|
this.bySession.delete(sessionId);
|
|
4477
|
+
this.insufficientStreaks.delete(sessionId);
|
|
4024
4478
|
}
|
|
4025
4479
|
};
|
|
4026
4480
|
/** 进程内批次注册表:插件为单实例,会话销毁时调用 clear 释放该会话的批次。 */
|
|
@@ -4065,7 +4519,7 @@ function assessEvidence(batch, request) {
|
|
|
4065
4519
|
//#endregion
|
|
4066
4520
|
//#region src/tools/create.ts
|
|
4067
4521
|
/**
|
|
4068
|
-
*
|
|
4522
|
+
* 19 个 engram_ 工具的定义与执行器。工具 schema 保持窄参数;
|
|
4069
4523
|
* scope 决定读写哪个分库;嵌入缺失时检索结果显式标记降级。
|
|
4070
4524
|
* @module @kenz1117/dsh-engram/tools/create
|
|
4071
4525
|
*/
|
|
@@ -4087,6 +4541,21 @@ function sessionCwdOf(exec) {
|
|
|
4087
4541
|
const cwd = exec.agent?.session?.header?.cwd;
|
|
4088
4542
|
return cwd === void 0 || cwd === "" ? void 0 : cwd;
|
|
4089
4543
|
}
|
|
4544
|
+
/** 工具入参时间解析:ISO 或可解析日期字符串 → epoch 毫秒;缺省 undefined;不可解析 loud 失败。 */
|
|
4545
|
+
function parseTimeParam(raw, toolName, field) {
|
|
4546
|
+
if (raw === void 0) return void 0;
|
|
4547
|
+
const ms = Date.parse(raw);
|
|
4548
|
+
if (Number.isNaN(ms)) throw new Error(`${toolName}: ${field} 不是可解析时间 ${raw}`);
|
|
4549
|
+
return ms;
|
|
4550
|
+
}
|
|
4551
|
+
/** 工具入参正数解析(分钟/条数等计数):缺省 fallback;非有限数字或越界 loud 失败。 */
|
|
4552
|
+
function parseCountParam(raw, toolName, field, fallback, min, max) {
|
|
4553
|
+
if (raw === void 0) return fallback;
|
|
4554
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) throw new Error(`${toolName}: ${field} 必须是有限数字,收到 ${String(raw)}`);
|
|
4555
|
+
const value = Math.floor(raw);
|
|
4556
|
+
if (value < min || value > max) throw new Error(`${toolName}: ${field} 需在 ${String(min)}-${String(max)} 之间,收到 ${String(value)}`);
|
|
4557
|
+
return value;
|
|
4558
|
+
}
|
|
4090
4559
|
/** 历史回填估算的模型可读文本(零成本,先看数再决定跑不跑)。 */
|
|
4091
4560
|
function renderHistoryEstimate(estimate) {
|
|
4092
4561
|
if (estimate.unavailable !== void 0) return `历史回填不可用:${estimate.unavailable}`;
|
|
@@ -4119,7 +4588,7 @@ const STRATEGY_HINT = {
|
|
|
4119
4588
|
answer: "可以据此作答",
|
|
4120
4589
|
search_keyword: "换关键词再检索(engram_search)",
|
|
4121
4590
|
search_room: "收窄到具体房间再检索(engram_search 带 room)",
|
|
4122
|
-
search_timeline: "按时间线找(engram_timeline
|
|
4591
|
+
search_timeline: "按时间线找(engram_timeline 或 engram_episode_timeline 查情景)",
|
|
4123
4592
|
ask_user: "向用户澄清缺失的信息",
|
|
4124
4593
|
stop: "不回答,并向用户说明缺少什么"
|
|
4125
4594
|
};
|
|
@@ -4203,8 +4672,8 @@ async function rewriteQueries(deps, exec, query) {
|
|
|
4203
4672
|
}
|
|
4204
4673
|
}
|
|
4205
4674
|
/**
|
|
4206
|
-
* 构造
|
|
4207
|
-
* stats/export/distill/examine/neighbors/audit_forgotten/tour/ingest_history)。
|
|
4675
|
+
* 构造 19 个工具定义(engram_save/search/assess/timeline/episode_timeline/update/forget/report/review/review_queue/
|
|
4676
|
+
* stats/export/distill/examine/neighbors/audit_forgotten/tour/ingest_history/profile_edit)。
|
|
4208
4677
|
* @param baseDeps - 分库打开器、嵌入器、辅助 LLM、导出目录。
|
|
4209
4678
|
* @returns 可直接 register 的工具定义数组(execute 已绑定会话 cwd 的项目宫殿路由)。
|
|
4210
4679
|
*/
|
|
@@ -4222,10 +4691,18 @@ function createEngramTools(baseDeps) {
|
|
|
4222
4691
|
/** engram_save 呈现文本:矛盾警告 text 优先;批量输出汇总成功与失败。 */
|
|
4223
4692
|
function renderSaveResultText(value) {
|
|
4224
4693
|
if (value.count !== void 0) {
|
|
4694
|
+
const items = value.items ?? [];
|
|
4695
|
+
const merged = items.filter((item) => item.disposition === "merge").length;
|
|
4696
|
+
const deferred = items.filter((item) => item.disposition === "defer").length;
|
|
4225
4697
|
const parts = [`已批量保存 ${value.count} 条记忆`];
|
|
4226
|
-
|
|
4698
|
+
const states = [];
|
|
4699
|
+
if (merged > 0) states.push(`并入强化 ${merged} 条既有记忆`);
|
|
4700
|
+
if (deferred > 0) states.push(`${deferred} 条与现有记忆高度相似待裁决`);
|
|
4701
|
+
if (states.length > 0) parts.push(states.join(","));
|
|
4702
|
+
for (const item of items) {
|
|
4227
4703
|
const slot = item.slot === void 0 ? "" : `, ${item.slot.room}#${item.slot.index}`;
|
|
4228
|
-
|
|
4704
|
+
const state = item.disposition === "merge" ? `, 并入 ${item.mergedInto}` : item.disposition === "defer" ? ", 待裁决" : "";
|
|
4705
|
+
parts.push(`${item.id}(kind=${item.kind}, importance=${item.importance}${slot}${state})`);
|
|
4229
4706
|
}
|
|
4230
4707
|
const failures = value.failed ?? [];
|
|
4231
4708
|
if (failures.length > 0) parts.push(`${failures.length} 条失败:${failures.map((entry) => `#${entry.index + 1} ${entry.reason}`).join(";")}`);
|
|
@@ -4235,9 +4712,20 @@ function createEngramTools(baseDeps) {
|
|
|
4235
4712
|
if (value.text !== void 0) return value.text;
|
|
4236
4713
|
return `已保存记忆 ${value.id}(kind=${value.kind}, importance=${value.importance})。后续会话可用 engram_search 召回。`;
|
|
4237
4714
|
}
|
|
4238
|
-
/**
|
|
4239
|
-
|
|
4715
|
+
/**
|
|
4716
|
+
* 写入单条已清洗内容(四态处置):MERGE 不新建条目只强化既有;
|
|
4717
|
+
* DEFER 写入并建 contradicts 边待裁决;ACCEPT 原样写入。
|
|
4718
|
+
*/
|
|
4719
|
+
async function writeWithDisposition(store, item) {
|
|
4240
4720
|
const { embedding } = item;
|
|
4721
|
+
const decision = await decideWrite(store, item.kind, embedding);
|
|
4722
|
+
if (decision.disposition === "merge") return {
|
|
4723
|
+
disposition: "merge",
|
|
4724
|
+
record: await applyMerge(store, decision, item.content),
|
|
4725
|
+
relatedId: decision.into.id,
|
|
4726
|
+
similarity: decision.similarity,
|
|
4727
|
+
candidates: []
|
|
4728
|
+
};
|
|
4241
4729
|
const record = await store.write({
|
|
4242
4730
|
scope: item.scope,
|
|
4243
4731
|
kind: item.kind,
|
|
@@ -4247,11 +4735,21 @@ function createEngramTools(baseDeps) {
|
|
|
4247
4735
|
...embedding === void 0 ? {} : { embedding },
|
|
4248
4736
|
...item.imagery === void 0 ? {} : { imagery: item.imagery }
|
|
4249
4737
|
});
|
|
4250
|
-
|
|
4251
|
-
|
|
4738
|
+
if (decision.disposition === "defer") {
|
|
4739
|
+
const candidates = await store.findContradictions(embedding);
|
|
4740
|
+
for (const candidate of candidates) await store.linkEdge(record.id, candidate.id, "contradicts");
|
|
4741
|
+
return {
|
|
4742
|
+
disposition: "defer",
|
|
4743
|
+
record,
|
|
4744
|
+
relatedId: decision.neighbor.id,
|
|
4745
|
+
similarity: decision.similarity,
|
|
4746
|
+
candidates
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4252
4749
|
return {
|
|
4750
|
+
disposition: "accept",
|
|
4253
4751
|
record,
|
|
4254
|
-
candidates
|
|
4752
|
+
candidates: []
|
|
4255
4753
|
};
|
|
4256
4754
|
}
|
|
4257
4755
|
/** 门牌参数收敛:非空字符串转 ImageryLabel(感官/情绪维度留空——AI 不需要人脑补丁),非法返回 undefined。 */
|
|
@@ -4326,7 +4824,7 @@ function createEngramTools(baseDeps) {
|
|
|
4326
4824
|
const saved = [];
|
|
4327
4825
|
for (const [position, item] of prepared.entries()) try {
|
|
4328
4826
|
const embedding = vectors?.[position];
|
|
4329
|
-
const
|
|
4827
|
+
const outcome = await writeWithDisposition(store, {
|
|
4330
4828
|
scope,
|
|
4331
4829
|
kind: item.kind,
|
|
4332
4830
|
content: item.content,
|
|
@@ -4335,10 +4833,12 @@ function createEngramTools(baseDeps) {
|
|
|
4335
4833
|
...embedding === void 0 ? {} : { embedding }
|
|
4336
4834
|
});
|
|
4337
4835
|
saved.push({
|
|
4338
|
-
id: record.id,
|
|
4339
|
-
kind: record.kind,
|
|
4340
|
-
importance: record.importance,
|
|
4341
|
-
|
|
4836
|
+
id: outcome.record.id,
|
|
4837
|
+
kind: outcome.record.kind,
|
|
4838
|
+
importance: outcome.record.importance,
|
|
4839
|
+
disposition: outcome.disposition,
|
|
4840
|
+
...outcome.disposition === "merge" || outcome.record.slot === void 0 ? {} : { slot: outcome.record.slot },
|
|
4841
|
+
...outcome.disposition === "merge" && outcome.relatedId !== void 0 ? { mergedInto: outcome.relatedId } : {}
|
|
4342
4842
|
});
|
|
4343
4843
|
} catch (error) {
|
|
4344
4844
|
failed.push({
|
|
@@ -4416,6 +4916,16 @@ function createEngramTools(baseDeps) {
|
|
|
4416
4916
|
id: { type: "string" },
|
|
4417
4917
|
kind: { type: "string" },
|
|
4418
4918
|
importance: { type: "number" },
|
|
4919
|
+
disposition: {
|
|
4920
|
+
type: "string",
|
|
4921
|
+
enum: [
|
|
4922
|
+
"accept",
|
|
4923
|
+
"merge",
|
|
4924
|
+
"defer"
|
|
4925
|
+
]
|
|
4926
|
+
},
|
|
4927
|
+
mergedInto: { type: "string" },
|
|
4928
|
+
similarity: { type: "number" },
|
|
4419
4929
|
text: { type: "string" },
|
|
4420
4930
|
count: { type: "number" },
|
|
4421
4931
|
items: {
|
|
@@ -4436,6 +4946,16 @@ function createEngramTools(baseDeps) {
|
|
|
4436
4946
|
type: "number",
|
|
4437
4947
|
required: true
|
|
4438
4948
|
},
|
|
4949
|
+
disposition: {
|
|
4950
|
+
type: "string",
|
|
4951
|
+
enum: [
|
|
4952
|
+
"accept",
|
|
4953
|
+
"merge",
|
|
4954
|
+
"defer"
|
|
4955
|
+
],
|
|
4956
|
+
required: true
|
|
4957
|
+
},
|
|
4958
|
+
mergedInto: { type: "string" },
|
|
4439
4959
|
slot: {
|
|
4440
4960
|
type: "object",
|
|
4441
4961
|
additionalProperties: false,
|
|
@@ -4493,7 +5013,7 @@ function createEngramTools(baseDeps) {
|
|
|
4493
5013
|
const embedder = await deps.embedder;
|
|
4494
5014
|
const embeddings = embedder === void 0 ? void 0 : await embedder.embed([content.trim()]);
|
|
4495
5015
|
const imagery = placardOf(input.placard);
|
|
4496
|
-
const
|
|
5016
|
+
const outcome = await writeWithDisposition(store, {
|
|
4497
5017
|
scope,
|
|
4498
5018
|
kind: input.kind,
|
|
4499
5019
|
content,
|
|
@@ -4502,13 +5022,28 @@ function createEngramTools(baseDeps) {
|
|
|
4502
5022
|
...embeddings?.[0] === void 0 ? {} : { embedding: embeddings[0] },
|
|
4503
5023
|
...imagery === void 0 ? {} : { imagery }
|
|
4504
5024
|
});
|
|
5025
|
+
const { record, candidates } = outcome;
|
|
5026
|
+
if (outcome.disposition === "merge") {
|
|
5027
|
+
const sim = (outcome.similarity ?? 0).toFixed(2);
|
|
5028
|
+
return {
|
|
5029
|
+
id: record.id,
|
|
5030
|
+
kind: record.kind,
|
|
5031
|
+
importance: record.importance,
|
|
5032
|
+
disposition: "merge",
|
|
5033
|
+
...outcome.relatedId === void 0 ? {} : { mergedInto: outcome.relatedId },
|
|
5034
|
+
...outcome.similarity === void 0 ? {} : { similarity: outcome.similarity },
|
|
5035
|
+
text: `与既有记忆高度重复(相似度 ${sim}),已并入 ${outcome.relatedId} 并强化其置信度与访问计数,未新建条目。若这是修正而非复述,请用 engram_update 归并。`
|
|
5036
|
+
};
|
|
5037
|
+
}
|
|
4505
5038
|
const placardHint = imagery === void 0 || record.imageryScore === void 0 ? "" : `\n${placardImprovementHint(record.imageryScore) ?? ""}`;
|
|
4506
|
-
if (
|
|
5039
|
+
if (outcome.disposition === "defer") {
|
|
4507
5040
|
const listed = candidates.map((candidate) => `「${candidate.content}」(id=${candidate.id})`).join(";");
|
|
4508
5041
|
return {
|
|
4509
5042
|
id: record.id,
|
|
4510
5043
|
kind: record.kind,
|
|
4511
5044
|
importance: record.importance,
|
|
5045
|
+
disposition: "defer",
|
|
5046
|
+
...outcome.similarity === void 0 ? {} : { similarity: outcome.similarity },
|
|
4512
5047
|
text: `已保存 ${record.id}。注意:与现有记忆高度相似——${listed}。若这是修正而非新事实,请用 engram_update 归并,或 engram_forget 去重。${placardHint}`
|
|
4513
5048
|
};
|
|
4514
5049
|
}
|
|
@@ -4517,6 +5052,7 @@ function createEngramTools(baseDeps) {
|
|
|
4517
5052
|
id: record.id,
|
|
4518
5053
|
kind: record.kind,
|
|
4519
5054
|
importance: record.importance,
|
|
5055
|
+
disposition: "accept",
|
|
4520
5056
|
text: `${base}${placardHint}`
|
|
4521
5057
|
};
|
|
4522
5058
|
}
|
|
@@ -4680,6 +5216,7 @@ function createEngramTools(baseDeps) {
|
|
|
4680
5216
|
missing: typeof input.missing === "string" ? input.missing : "",
|
|
4681
5217
|
nextStrategy: typeof input.nextStrategy === "string" ? input.nextStrategy : ""
|
|
4682
5218
|
});
|
|
5219
|
+
if (sessionId !== void 0) evidenceBatches.recordVerdict(sessionId, batch.batchId, verdict);
|
|
4683
5220
|
await (await deps.openStore("user")).audit("assess", batch.batchId, JSON.stringify({
|
|
4684
5221
|
batchId: batch.batchId,
|
|
4685
5222
|
sufficient: verdict.sufficient,
|
|
@@ -4744,14 +5281,8 @@ function createEngramTools(baseDeps) {
|
|
|
4744
5281
|
const input = args;
|
|
4745
5282
|
const scopes = scopesOf(input.scope);
|
|
4746
5283
|
const order = input.order === "tour" ? "tour" : "time";
|
|
4747
|
-
const
|
|
4748
|
-
|
|
4749
|
-
const ms = Date.parse(raw);
|
|
4750
|
-
if (Number.isNaN(ms)) throw new Error(`engram_timeline: ${field} 不是可解析时间 ${raw}`);
|
|
4751
|
-
return ms;
|
|
4752
|
-
};
|
|
4753
|
-
const since = parseTime(input.since, "since");
|
|
4754
|
-
const until = parseTime(input.until, "until");
|
|
5284
|
+
const since = parseTimeParam(input.since, "engram_timeline", "since");
|
|
5285
|
+
const until = parseTimeParam(input.until, "engram_timeline", "until");
|
|
4755
5286
|
const results = await Promise.all(scopes.map(async (scope) => {
|
|
4756
5287
|
return (await deps.openStore(scope)).timeline({
|
|
4757
5288
|
scopes: [scope],
|
|
@@ -4770,6 +5301,106 @@ function createEngramTools(baseDeps) {
|
|
|
4770
5301
|
return { text: renderMemoryPacket(`${body.join("\n") || "时间线为空"}${tail}`, "tool_timeline", input.topic ?? "(对话继续)") };
|
|
4771
5302
|
}
|
|
4772
5303
|
});
|
|
5304
|
+
const episodeTimeline = defineTool({
|
|
5305
|
+
name: "engram_episode_timeline",
|
|
5306
|
+
description: "episode 情景记忆的独立时间线:按日期范围与来源会话浏览经历,输出按会话分组(组间新→旧,组内时间升序),适合回答「那天/那段时间我们做了什么」。around 传锚点记忆 id 时切换为时间邻近扩展——列出锚点创建时刻 ± 窗口内的情景(忽略日期过滤),回答「当时前后还发生了什么」。",
|
|
5307
|
+
parameters: {
|
|
5308
|
+
scope: {
|
|
5309
|
+
type: "string",
|
|
5310
|
+
enum: [
|
|
5311
|
+
"user",
|
|
5312
|
+
"project",
|
|
5313
|
+
"shared",
|
|
5314
|
+
"all"
|
|
5315
|
+
],
|
|
5316
|
+
description: "作用域,默认 all"
|
|
5317
|
+
},
|
|
5318
|
+
since: {
|
|
5319
|
+
type: "string",
|
|
5320
|
+
description: "起始日期(ISO 或可解析日期)"
|
|
5321
|
+
},
|
|
5322
|
+
until: {
|
|
5323
|
+
type: "string",
|
|
5324
|
+
description: "结束日期"
|
|
5325
|
+
},
|
|
5326
|
+
sessionId: {
|
|
5327
|
+
type: "string",
|
|
5328
|
+
description: "只看该来源会话的情景(id 精确匹配,从本工具输出的「会话 …」组头取得)"
|
|
5329
|
+
},
|
|
5330
|
+
around: {
|
|
5331
|
+
type: "string",
|
|
5332
|
+
description: "锚点记忆 id:切换为时间邻近扩展模式(与 since/until/sessionId 互斥,此时忽略它们)"
|
|
5333
|
+
},
|
|
5334
|
+
windowMinutes: {
|
|
5335
|
+
type: "number",
|
|
5336
|
+
description: `邻近窗口分钟数(around 模式,默认 ${String(EPISODE_PROXIMITY_MS_DEFAULT / 6e4)})`
|
|
5337
|
+
},
|
|
5338
|
+
limit: {
|
|
5339
|
+
type: "number",
|
|
5340
|
+
description: `最多返回条数(默认 ${String(100)})`
|
|
5341
|
+
}
|
|
5342
|
+
},
|
|
5343
|
+
output: {
|
|
5344
|
+
schema: {
|
|
5345
|
+
type: "object",
|
|
5346
|
+
additionalProperties: false,
|
|
5347
|
+
properties: { text: {
|
|
5348
|
+
type: "string",
|
|
5349
|
+
required: true
|
|
5350
|
+
} }
|
|
5351
|
+
},
|
|
5352
|
+
render: (_args, value) => [{
|
|
5353
|
+
type: "text",
|
|
5354
|
+
text: value.text
|
|
5355
|
+
}]
|
|
5356
|
+
},
|
|
5357
|
+
async execute(args) {
|
|
5358
|
+
const input = args;
|
|
5359
|
+
const scopes = scopesOf(input.scope);
|
|
5360
|
+
const since = parseTimeParam(input.since, "engram_episode_timeline", "since");
|
|
5361
|
+
const until = parseTimeParam(input.until, "engram_episode_timeline", "until");
|
|
5362
|
+
const limit = parseCountParam(input.limit, "engram_episode_timeline", "limit", 100, 1, 500);
|
|
5363
|
+
const sessionId = input.sessionId?.trim();
|
|
5364
|
+
const aroundId = input.around?.trim();
|
|
5365
|
+
const renderRow = (record) => `${new Date(record.createdAt).toISOString()} [${record.scope}/${record.kind}] ${truncateItem(record.content)}(id=${record.id})`;
|
|
5366
|
+
if (aroundId !== void 0 && aroundId !== "") {
|
|
5367
|
+
const windowMinutes = parseCountParam(input.windowMinutes, "engram_episode_timeline", "windowMinutes", EPISODE_PROXIMITY_MS_DEFAULT / 6e4, 1, 1440);
|
|
5368
|
+
const anchor = asMemoryId(aroundId);
|
|
5369
|
+
for (const scope of scopes) {
|
|
5370
|
+
const store = await deps.openStore(scope);
|
|
5371
|
+
if (await store.get(anchor) === void 0) continue;
|
|
5372
|
+
const around = (await store.episodeTimeline({
|
|
5373
|
+
scopes: [scope],
|
|
5374
|
+
around: anchor,
|
|
5375
|
+
proximityMs: windowMinutes * 6e4,
|
|
5376
|
+
limit
|
|
5377
|
+
})).around;
|
|
5378
|
+
return { text: renderMemoryPacket(enforceBudget(around.neighbors.length === 0 ? [`锚点:${renderRow(around.anchor)}`, "邻近窗口内没有情景记忆。"] : [
|
|
5379
|
+
`锚点:${renderRow(around.anchor)}`,
|
|
5380
|
+
`邻近情景(±${String(windowMinutes)} 分钟,${String(around.neighbors.length)} 条,按时间正序):`,
|
|
5381
|
+
...around.neighbors.map(renderRow)
|
|
5382
|
+
]).join("\n"), "tool_episode_timeline", aroundId) };
|
|
5383
|
+
}
|
|
5384
|
+
throw new Error(`engram_episode_timeline: 锚点 ${aroundId} 不存在`);
|
|
5385
|
+
}
|
|
5386
|
+
const groups = (await Promise.all(scopes.map(async (scope) => {
|
|
5387
|
+
return (await deps.openStore(scope)).episodeTimeline({
|
|
5388
|
+
scopes: [scope],
|
|
5389
|
+
...since === void 0 ? {} : { since },
|
|
5390
|
+
...until === void 0 ? {} : { until },
|
|
5391
|
+
...sessionId === void 0 || sessionId === "" ? {} : { sessionId },
|
|
5392
|
+
limit
|
|
5393
|
+
});
|
|
5394
|
+
}))).flatMap((result) => result.groups).sort((a, b) => b.startedAt - a.startedAt);
|
|
5395
|
+
const lines = [];
|
|
5396
|
+
for (const group of groups) {
|
|
5397
|
+
lines.push(group.sessionId === null ? `[无会话来源] ${new Date(group.startedAt).toISOString()} ~ ${new Date(group.endedAt).toISOString()}(${String(group.episodes.length)} 条)` : `[会话 ${group.sessionId}] ${new Date(group.startedAt).toISOString()} ~ ${new Date(group.endedAt).toISOString()}(${String(group.episodes.length)} 条)`);
|
|
5398
|
+
if (group.summary !== void 0) lines.push(` 摘要:${group.summary}`);
|
|
5399
|
+
for (const record of group.episodes) lines.push(`- ${renderRow(record)}`);
|
|
5400
|
+
}
|
|
5401
|
+
return { text: renderMemoryPacket(enforceBudget(lines).join("\n") || "情景时间线为空", "tool_episode_timeline", sessionId ?? "(对话继续)") };
|
|
5402
|
+
}
|
|
5403
|
+
});
|
|
4773
5404
|
const update = defineTool({
|
|
4774
5405
|
name: "engram_update",
|
|
4775
5406
|
description: "修正一条记忆:写入新条目并把旧条目标记为被取代(链条保留,可审计)。id 来自 engram_search 结果。",
|
|
@@ -5086,6 +5717,7 @@ function createEngramTools(baseDeps) {
|
|
|
5086
5717
|
search,
|
|
5087
5718
|
assess,
|
|
5088
5719
|
timeline,
|
|
5720
|
+
episodeTimeline,
|
|
5089
5721
|
update,
|
|
5090
5722
|
forget,
|
|
5091
5723
|
defineTool({
|
|
@@ -5602,7 +6234,109 @@ function createEngramTools(baseDeps) {
|
|
|
5602
6234
|
}
|
|
5603
6235
|
}),
|
|
5604
6236
|
auditForgotten,
|
|
5605
|
-
ingestHistory
|
|
6237
|
+
ingestHistory,
|
|
6238
|
+
defineTool({
|
|
6239
|
+
name: "engram_profile_edit",
|
|
6240
|
+
description: "编辑会话开始注入的用户画像 curated block(注入时位于自动派生画像之前,是画像的最高优先层)。action=view 查看当前内容与版本历史;action=edit 编辑(乐观锁:须带当前 expectedVersion,版本冲突会报错,重读最新版本后再改;首次创建省略 expectedVersion);action=rollback 回滚(把 toVersion 版本的内容作为新版本写入,历史不改写)。内容入库前做协议剥离与密钥脱敏。",
|
|
6241
|
+
parameters: {
|
|
6242
|
+
action: {
|
|
6243
|
+
type: "string",
|
|
6244
|
+
enum: [
|
|
6245
|
+
"view",
|
|
6246
|
+
"edit",
|
|
6247
|
+
"rollback"
|
|
6248
|
+
],
|
|
6249
|
+
required: true,
|
|
6250
|
+
description: "view 查看 / edit 编辑 / rollback 回滚"
|
|
6251
|
+
},
|
|
6252
|
+
scope: {
|
|
6253
|
+
type: "string",
|
|
6254
|
+
enum: [
|
|
6255
|
+
"user",
|
|
6256
|
+
"project",
|
|
6257
|
+
"shared"
|
|
6258
|
+
],
|
|
6259
|
+
description: "作用域,默认 user"
|
|
6260
|
+
},
|
|
6261
|
+
content: {
|
|
6262
|
+
type: "string",
|
|
6263
|
+
description: "edit 必填:新的画像内容(多行文本,会话开始按原文注入)"
|
|
6264
|
+
},
|
|
6265
|
+
expectedVersion: {
|
|
6266
|
+
type: "integer",
|
|
6267
|
+
description: "edit 带上=并发保护(乐观锁);省略=仅当尚无 block 时创建 v1"
|
|
6268
|
+
},
|
|
6269
|
+
toVersion: {
|
|
6270
|
+
type: "integer",
|
|
6271
|
+
description: "rollback 必填:要回滚到的历史版本号(用 view 查得)"
|
|
6272
|
+
}
|
|
6273
|
+
},
|
|
6274
|
+
output: {
|
|
6275
|
+
schema: {
|
|
6276
|
+
type: "object",
|
|
6277
|
+
additionalProperties: false,
|
|
6278
|
+
properties: { text: {
|
|
6279
|
+
type: "string",
|
|
6280
|
+
required: true
|
|
6281
|
+
} }
|
|
6282
|
+
},
|
|
6283
|
+
render: (_args, value) => [{
|
|
6284
|
+
type: "text",
|
|
6285
|
+
text: value.text
|
|
6286
|
+
}]
|
|
6287
|
+
},
|
|
6288
|
+
async execute(args) {
|
|
6289
|
+
const input = args;
|
|
6290
|
+
const action = input.action === "edit" || input.action === "rollback" || input.action === "view" ? input.action : void 0;
|
|
6291
|
+
if (action === void 0) throw new Error("engram_profile_edit: action 必须是 view / edit / rollback 之一");
|
|
6292
|
+
const scope = scopeOf(input.scope, "user");
|
|
6293
|
+
const store = await deps.openStore(scope);
|
|
6294
|
+
if (action === "view") {
|
|
6295
|
+
const block = await store.getProfileBlock(scope);
|
|
6296
|
+
if (block === void 0) return { text: `${scope} 库暂无 curated 画像 block。用 action=edit(省略 expectedVersion)即可创建 v1;创建后会话开始时优先于自动派生画像注入。` };
|
|
6297
|
+
const versions = await store.listProfileBlockVersions(scope, 10);
|
|
6298
|
+
return { text: [
|
|
6299
|
+
`当前 curated 画像(${scope},v${block.version}):`,
|
|
6300
|
+
block.content,
|
|
6301
|
+
"",
|
|
6302
|
+
`版本历史(最近 ${versions.length} 条,新→旧;rollback 用 toVersion 指定):`,
|
|
6303
|
+
...versions.map((version) => `- v${version.version} · ${version.source === "rollback" ? "回滚" : "编辑"} · ${new Date(version.at).toISOString().slice(0, 16).replace("T", " ")} · ${version.content.length} 字`)
|
|
6304
|
+
].join("\n") };
|
|
6305
|
+
}
|
|
6306
|
+
if (action === "edit") {
|
|
6307
|
+
if (typeof input.content !== "string" || input.content.trim() === "") throw new Error("engram_profile_edit: edit 需要 content 参数");
|
|
6308
|
+
let expectedVersion;
|
|
6309
|
+
if (input.expectedVersion !== void 0) {
|
|
6310
|
+
if (!Number.isInteger(input.expectedVersion)) throw new Error("engram_profile_edit: expectedVersion 必须是整数(用 action=view 查得)");
|
|
6311
|
+
expectedVersion = input.expectedVersion;
|
|
6312
|
+
}
|
|
6313
|
+
const content = redactSecrets(sanitizeProtocolText(input.content));
|
|
6314
|
+
if (content.trim() === "") throw new Error("engram_profile_edit: 清洗后内容为空(原文只含协议标签或密钥)");
|
|
6315
|
+
const block = await store.saveProfileBlock(scope, expectedVersion, content, "edit");
|
|
6316
|
+
await store.audit("profile-edit", scope, JSON.stringify({
|
|
6317
|
+
action: "edit",
|
|
6318
|
+
fromVersion: expectedVersion ?? null,
|
|
6319
|
+
toVersion: block.version,
|
|
6320
|
+
chars: content.length
|
|
6321
|
+
}));
|
|
6322
|
+
return { text: `curated 画像已写入(${scope},v${block.version})。下一轮会话开始时将优先于自动派生画像注入。` };
|
|
6323
|
+
}
|
|
6324
|
+
if (!Number.isInteger(input.toVersion)) throw new Error("engram_profile_edit: rollback 需要 toVersion(目标历史版本号,用 action=view 查得)");
|
|
6325
|
+
const target = await store.getProfileBlockVersion(scope, input.toVersion);
|
|
6326
|
+
if (target === void 0) throw new Error(`engram_profile_edit: ${scope} 库不存在版本 v${input.toVersion}(用 action=view 查看版本历史)`);
|
|
6327
|
+
const current = await store.getProfileBlock(scope);
|
|
6328
|
+
if (current === void 0) throw new Error(`engram_profile_edit: ${scope} 库尚无 curated 画像 block,无从回滚`);
|
|
6329
|
+
const updated = await store.saveProfileBlock(scope, current.version, target.content, "rollback");
|
|
6330
|
+
await store.audit("profile-edit", scope, JSON.stringify({
|
|
6331
|
+
action: "rollback",
|
|
6332
|
+
fromVersion: current.version,
|
|
6333
|
+
toVersion: updated.version,
|
|
6334
|
+
restoredFrom: target.version,
|
|
6335
|
+
chars: target.content.length
|
|
6336
|
+
}));
|
|
6337
|
+
return { text: `已把 v${target.version} 的内容作为 v${updated.version} 写入(${scope};版本链只增不改,可继续回滚)。` };
|
|
6338
|
+
}
|
|
6339
|
+
})
|
|
5606
6340
|
].map((tool) => ({
|
|
5607
6341
|
...tool,
|
|
5608
6342
|
execute: (args, exec) => execSessionCwd.run(sessionCwdOf(exec), () => tool.execute(args, exec))
|
|
@@ -5771,8 +6505,8 @@ function estimateTokens(text) {
|
|
|
5771
6505
|
//#region src/index.ts
|
|
5772
6506
|
/**
|
|
5773
6507
|
* dsh-engram:DeepSeek Harness 跨会话长期记忆插件(host 半)。
|
|
5774
|
-
* 注册
|
|
5775
|
-
*
|
|
6508
|
+
* 注册 18 个 engram_ 工具、会话开始注入画像(curated block 优先于派生画像)、
|
|
6509
|
+
* 自动摄取上一轮对话、蒸馏/衰减飞轮与审计能力。
|
|
5776
6510
|
* @module @kenz1117/dsh-engram
|
|
5777
6511
|
*/
|
|
5778
6512
|
/** Cordis 插件名(loader 诊断与注入 source 使用)。 */
|
|
@@ -5781,31 +6515,41 @@ const name = "dsh-engram";
|
|
|
5781
6515
|
const VERSION = "0.7.6";
|
|
5782
6516
|
/** 必需服务:工具注册表与 LLM 流式端点(摄取/蒸馏的辅助调用)。 */
|
|
5783
6517
|
const inject = ["tools", "llm"];
|
|
6518
|
+
/** 分级递减预算默认值(配置缺省时由 resolveConfig 显式落地,同值于此)。 */
|
|
6519
|
+
const DEFAULT_ITEM_BUDGET = {
|
|
6520
|
+
start: 160,
|
|
6521
|
+
decay: .9,
|
|
6522
|
+
floor: 24
|
|
6523
|
+
};
|
|
5784
6524
|
/**
|
|
5785
|
-
*
|
|
5786
|
-
*
|
|
5787
|
-
*
|
|
5788
|
-
*
|
|
6525
|
+
* 会话开始注入的画像渲染:按重要性降序装填,单条正文按分级递减预算截断
|
|
6526
|
+
* (首条 160 字、逐条 ×0.9、下限 24 字——排名越靠后单条越短,平滑降级替代
|
|
6527
|
+
* 整条降级索引行);token 预算(估算见 estimateTokens:中文按 1.5 token/字、
|
|
6528
|
+
* 其余按 4 字符/token)是外层硬约束:截断后仍装不下的条目先以 40 字索引行
|
|
6529
|
+
* 兜底,索引行也装不下的折成末尾 `+N more; use engram_search` 计数行;计数行同样占用预算。
|
|
5789
6530
|
* @param records - 候选条目(调用方已按重要性排序、按条数截断)。
|
|
5790
6531
|
* @param tokenBudget - 整段画像的 token 预算(含首尾固定行)。
|
|
6532
|
+
* @param itemBudget - 分级递减的单条正文预算(缺省用 DEFAULT_ITEM_BUDGET;配置面在 resolveConfig)。
|
|
5791
6533
|
* @returns 渲染文本与溢出条目(调用方可用辅助 LLM 压缩后重渲染)。
|
|
5792
6534
|
*/
|
|
5793
|
-
function renderProfileDetailed(records, tokenBudget) {
|
|
6535
|
+
function renderProfileDetailed(records, tokenBudget, itemBudget = DEFAULT_ITEM_BUDGET) {
|
|
5794
6536
|
const estimate = estimateTokens;
|
|
5795
6537
|
const header = "User memory profile (dsh-engram, cross-session) — Grand Hall (always present):";
|
|
5796
6538
|
const footer = "Use engram_search to recall details (pass room to search inside one room); use engram_save to persist new facts.";
|
|
5797
6539
|
let remaining = Math.max(0, tokenBudget - estimate(header) - estimate(footer));
|
|
5798
6540
|
const lines = [];
|
|
5799
6541
|
const overflow = [];
|
|
5800
|
-
|
|
6542
|
+
records.forEach((record, index) => {
|
|
6543
|
+
const budget = Math.max(itemBudget.floor, Math.round(itemBudget.start * itemBudget.decay ** index));
|
|
6544
|
+
const content = record.content.length > budget ? `${record.content.slice(0, budget)}…` : record.content;
|
|
5801
6545
|
const slot = record.slot === void 0 ? "" : ` ${record.slot.room}#${record.slot.index}`;
|
|
5802
|
-
const line = `- [${record.kind}]${slot} ${
|
|
6546
|
+
const line = `- [${record.kind}]${slot} ${content}`;
|
|
5803
6547
|
const cost = estimate(line);
|
|
5804
6548
|
if (cost <= remaining) {
|
|
5805
6549
|
lines.push(line);
|
|
5806
6550
|
remaining -= cost;
|
|
5807
6551
|
} else overflow.push(record);
|
|
5808
|
-
}
|
|
6552
|
+
});
|
|
5809
6553
|
let more = 0;
|
|
5810
6554
|
remaining -= overflow.length === 0 ? 0 : estimate(`+${overflow.length} more; use engram_search`);
|
|
5811
6555
|
for (const record of overflow) {
|
|
@@ -5835,6 +6579,28 @@ function renderProfileDetailed(records, tokenBudget) {
|
|
|
5835
6579
|
function renderProfile(records, tokenBudget) {
|
|
5836
6580
|
return renderProfileDetailed(records, tokenBudget).text;
|
|
5837
6581
|
}
|
|
6582
|
+
/** curated block 段的标题行(模型据此知道这一段是人工维护的最高优先画像)。 */
|
|
6583
|
+
const CURATED_HEADER = "Curated profile (user-maintained via engram_profile_edit; authoritative over derived facts):";
|
|
6584
|
+
/**
|
|
6585
|
+
* 画像渲染(curated 优先):有 curated block 时其全文置于派生画像之前(模型可见的
|
|
6586
|
+
* 第一段),并先从 token 预算中扣除自身占用,剩余预算才给派生条目装填;派生段
|
|
6587
|
+
* 照旧走分级递减与索引行/计数行降级。curated 是用户显式维护的意志,即使超出剩余
|
|
6588
|
+
* 预算也完整注入(与 due 行同理);block 缺省时等同 renderProfileDetailed。
|
|
6589
|
+
* @param curated - 当前态 curated block;undefined = 尚未编辑过。
|
|
6590
|
+
* @param records - 派生画像候选条目(调用方已按重要性排序、按条数截断)。
|
|
6591
|
+
* @param tokenBudget - 整段画像的 token 预算(curated 段与派生段共享)。
|
|
6592
|
+
* @param itemBudget - 分级递减的单条正文预算(透传 renderProfileDetailed)。
|
|
6593
|
+
* @returns 渲染文本与派生段的溢出条目。
|
|
6594
|
+
*/
|
|
6595
|
+
function renderProfileWithCurated(curated, records, tokenBudget, itemBudget = DEFAULT_ITEM_BUDGET) {
|
|
6596
|
+
if (curated === void 0) return renderProfileDetailed(records, tokenBudget, itemBudget);
|
|
6597
|
+
const curatedSection = `${CURATED_HEADER}\n${curated.content}`;
|
|
6598
|
+
const detailed = renderProfileDetailed(records, Math.max(0, tokenBudget - estimateTokens(curatedSection)), itemBudget);
|
|
6599
|
+
return {
|
|
6600
|
+
text: `${curatedSection}\n${detailed.text}`,
|
|
6601
|
+
overflow: detailed.overflow
|
|
6602
|
+
};
|
|
6603
|
+
}
|
|
5838
6604
|
/** 压缩辅助调用的输出 token 上限(40 字 × 若干条,短输出足够)。 */
|
|
5839
6605
|
const COMPRESS_MAX_TOKENS = 800;
|
|
5840
6606
|
/** 压缩辅助调用超时:压缩在 pre-step 关键路径上,必须限时防阻塞首轮请求。 */
|
|
@@ -5937,23 +6703,59 @@ async function preStep(ctx, openStore, openStoreForProjectCwd, resolved, embedde
|
|
|
5937
6703
|
console.warn("[dsh-engram] 本轮自动摄取失败(已跳过,不影响对话):", error);
|
|
5938
6704
|
});
|
|
5939
6705
|
}
|
|
6706
|
+
if (step !== 1 && resolved.assessReminder) {
|
|
6707
|
+
const sessionId = String(agent.session.id);
|
|
6708
|
+
const pending = evidenceBatches.pendingBatches(sessionId).length;
|
|
6709
|
+
const reminder = buildAssessReminder(pending, evidenceBatches.insufficientStreak(sessionId));
|
|
6710
|
+
if (reminder !== void 0 && reminder !== state.lastAssessReminder) {
|
|
6711
|
+
state.lastAssessReminder = reminder;
|
|
6712
|
+
return {
|
|
6713
|
+
...decision,
|
|
6714
|
+
messages: [...decision.messages, createUserMessage({
|
|
6715
|
+
content: [{
|
|
6716
|
+
type: "text",
|
|
6717
|
+
text: reminder
|
|
6718
|
+
}],
|
|
6719
|
+
source: {
|
|
6720
|
+
kind: "plugin",
|
|
6721
|
+
plugin: name,
|
|
6722
|
+
form: "snapshot",
|
|
6723
|
+
sections: [{
|
|
6724
|
+
name,
|
|
6725
|
+
text: reminder
|
|
6726
|
+
}]
|
|
6727
|
+
}
|
|
6728
|
+
})]
|
|
6729
|
+
};
|
|
6730
|
+
}
|
|
6731
|
+
}
|
|
5940
6732
|
if (step !== 1) return decision;
|
|
5941
|
-
const
|
|
5942
|
-
|
|
6733
|
+
const store = await openStore("user");
|
|
6734
|
+
const curated = await store.getProfileBlock("user");
|
|
6735
|
+
const top = await store.topActive("user", resolved.profileTopN);
|
|
6736
|
+
if (top.length === 0 && curated === void 0) return decision;
|
|
5943
6737
|
const dueTotal = resolved.reviewScheduling ? (await Promise.all(["user", "project"].map(async (scope) => (await openStore(scope)).dueReviews(Date.now(), 50)))).reduce((sum, rows) => sum + rows.length, 0) : 0;
|
|
5944
|
-
const detailed =
|
|
6738
|
+
const detailed = renderProfileWithCurated(curated, top, resolved.injectTokenBudget, {
|
|
6739
|
+
start: resolved.injectItemBudgetStart,
|
|
6740
|
+
decay: resolved.injectItemBudgetDecay,
|
|
6741
|
+
floor: resolved.injectItemBudgetFloor
|
|
6742
|
+
});
|
|
5945
6743
|
let text = detailed.text;
|
|
5946
6744
|
if (detailed.overflow.length > 0) {
|
|
5947
6745
|
const compressed = await compressProfileOverflow(ctx, agent, resolved.routeOverride, detailed.overflow, signal);
|
|
5948
6746
|
if (compressed !== void 0) {
|
|
5949
6747
|
openStore("user").then((auditStore) => auditStore.audit("compress-request", "AUX", JSON.stringify({ count: detailed.overflow.length }))).catch(() => {});
|
|
5950
|
-
text =
|
|
6748
|
+
text = renderProfileWithCurated(curated, top.map((record) => {
|
|
5951
6749
|
const shorter = compressed.get(record.id);
|
|
5952
6750
|
return shorter === void 0 ? record : {
|
|
5953
6751
|
...record,
|
|
5954
6752
|
content: shorter
|
|
5955
6753
|
};
|
|
5956
|
-
}), resolved.injectTokenBudget
|
|
6754
|
+
}), resolved.injectTokenBudget, {
|
|
6755
|
+
start: resolved.injectItemBudgetStart,
|
|
6756
|
+
decay: resolved.injectItemBudgetDecay,
|
|
6757
|
+
floor: resolved.injectItemBudgetFloor
|
|
6758
|
+
}).text;
|
|
5957
6759
|
}
|
|
5958
6760
|
}
|
|
5959
6761
|
const dueLine = dueTotal === 0 ? "" : `\nPalace review due today: ${dueTotal}${dueTotal >= 50 ? "+" : ""} memories. Use engram_review_queue for active recall (recall beats re-reading).`;
|
|
@@ -6111,6 +6913,7 @@ function apply(ctx, config = {}) {
|
|
|
6111
6913
|
pendingReplayed: false,
|
|
6112
6914
|
lastProfileAgent: null,
|
|
6113
6915
|
lastProfileHash: null,
|
|
6916
|
+
lastAssessReminder: null,
|
|
6114
6917
|
route: void 0
|
|
6115
6918
|
};
|
|
6116
6919
|
/** 会话持久化服务(可选;缺席时历史回填不可用)。 */
|
|
@@ -6354,14 +7157,17 @@ function apply(ctx, config = {}) {
|
|
|
6354
7157
|
if (resolved.ingest !== "off") ctx.on("session/disposed", (session) => {
|
|
6355
7158
|
const mode = resolved.ingest;
|
|
6356
7159
|
if (mode === "off") return;
|
|
7160
|
+
const events = session.snapshotEvents();
|
|
7161
|
+
const sessionId = String(session.id);
|
|
7162
|
+
const cwd = sessionCwd(session);
|
|
6357
7163
|
ingestFinalTurn({
|
|
6358
|
-
events
|
|
6359
|
-
sessionId
|
|
7164
|
+
events,
|
|
7165
|
+
sessionId,
|
|
6360
7166
|
turn: 0,
|
|
6361
7167
|
slice: "last",
|
|
6362
7168
|
openStore: () => openStore("user"),
|
|
6363
7169
|
openAuditStore: () => openStore("user"),
|
|
6364
|
-
...ingestWriteRouting(
|
|
7170
|
+
...ingestWriteRouting(cwd, openStoreForProjectCwd, openStore),
|
|
6365
7171
|
embedder,
|
|
6366
7172
|
mode,
|
|
6367
7173
|
routeOverride: resolved.routeOverride,
|
|
@@ -6371,8 +7177,24 @@ function apply(ctx, config = {}) {
|
|
|
6371
7177
|
}),
|
|
6372
7178
|
logRequest: logIngestRequest,
|
|
6373
7179
|
signal: AbortSignal.timeout(FINAL_INGEST_TIMEOUT_MS)
|
|
7180
|
+
}).then((outcome) => {
|
|
7181
|
+
if (outcome === null) return void 0;
|
|
7182
|
+
return ensureSessionSummary({
|
|
7183
|
+
events,
|
|
7184
|
+
sessionId,
|
|
7185
|
+
openStore: () => cwd === void 0 ? openStore("user") : openStoreForProjectCwd(cwd),
|
|
7186
|
+
call: (params) => streamText(ctx, {
|
|
7187
|
+
...params,
|
|
7188
|
+
sessionId: session.id
|
|
7189
|
+
}),
|
|
7190
|
+
logRequest: logIngestRequest,
|
|
7191
|
+
mode,
|
|
7192
|
+
routeOverride: resolved.routeOverride,
|
|
7193
|
+
signal: AbortSignal.timeout(FINAL_INGEST_TIMEOUT_MS),
|
|
7194
|
+
round: lastTurnNumber(events)
|
|
7195
|
+
});
|
|
6374
7196
|
}).catch((error) => {
|
|
6375
|
-
console.warn("[dsh-engram]
|
|
7197
|
+
console.warn("[dsh-engram] 会话结束的收尾处理异常(不影响对话):", error);
|
|
6376
7198
|
});
|
|
6377
7199
|
});
|
|
6378
7200
|
const runDecay = async () => {
|
|
@@ -6430,4 +7252,4 @@ function apply(ctx, config = {}) {
|
|
|
6430
7252
|
}, "dsh-engram: consolidation timer");
|
|
6431
7253
|
}
|
|
6432
7254
|
//#endregion
|
|
6433
|
-
export { Config, VERSION, apply, inject, name, renderProfile, renderProfileDetailed };
|
|
7255
|
+
export { CURATED_HEADER, Config, DEFAULT_ITEM_BUDGET, VERSION, apply, inject, name, renderProfile, renderProfileDetailed, renderProfileWithCurated };
|