@modusensus/dsh-mneme 0.7.31 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -27
- package/lib/api.js +139 -6
- package/lib/client.js +369 -32
- package/lib/commands.js +36 -5
- package/lib/config.js +101 -14
- package/lib/dream/decisions.js +147 -14
- package/lib/dream/sleep.js +84 -34
- package/lib/dream.js +250 -60
- package/lib/embedding.js +22 -2
- package/lib/index.js +68 -37
- package/lib/inject.js +54 -13
- package/lib/lang.js +168 -0
- package/lib/local-embedder.js +23 -9
- package/lib/mirror.js +17 -14
- package/lib/reranker.js +23 -8
- package/lib/runtime/adopt.js +176 -0
- package/lib/runtime/closure.js +172 -0
- package/lib/runtime/download.js +247 -0
- package/lib/runtime/layout.js +235 -0
- package/lib/runtime/loader.js +249 -0
- package/lib/runtime/provision.js +237 -0
- package/lib/runtime/tarball.js +130 -0
- package/lib/runtime/verify.js +191 -0
- package/lib/scope.js +83 -0
- package/lib/service.js +374 -55
- package/lib/settings.js +43 -2
- package/lib/store.js +126 -13
- package/lib/summarize.js +108 -35
- package/lib/tools.js +205 -12
- package/package.json +7 -2
- package/runtime-manifest.json +596 -0
- package/scripts/build-runtime-manifest.mjs +217 -0
- package/scripts/mneme-runtime.mjs +277 -0
- package/src/api.js +139 -6
- package/src/commands.js +36 -5
- package/src/config.js +101 -14
- package/src/dream/decisions.js +147 -14
- package/src/dream/sleep.js +84 -34
- package/src/dream.js +250 -60
- package/src/embedding.js +22 -2
- package/src/index.js +68 -37
- package/src/inject.js +54 -13
- package/src/lang.js +168 -0
- package/src/local-embedder.js +23 -9
- package/src/mirror.js +17 -14
- package/src/reranker.js +23 -8
- package/src/runtime/adopt.js +176 -0
- package/src/runtime/closure.js +172 -0
- package/src/runtime/download.js +247 -0
- package/src/runtime/layout.js +235 -0
- package/src/runtime/loader.js +249 -0
- package/src/runtime/provision.js +237 -0
- package/src/runtime/tarball.js +130 -0
- package/src/runtime/verify.js +191 -0
- package/src/scope.js +83 -0
- package/src/service.js +374 -55
- package/src/settings.js +43 -2
- package/src/store.js +126 -13
- package/src/summarize.js +108 -35
- package/src/tools.js +205 -12
- package/test/api.test.js +175 -9
- package/test/client.test.js +152 -10
- package/test/commands.test.js +40 -3
- package/test/config.test.js +17 -1
- package/test/dream-candidate-hybrid.test.js +281 -0
- package/test/dream.test.js +172 -0
- package/test/embedding-ready.test.js +196 -0
- package/test/helpers/runtime-source.js +60 -0
- package/test/helpers/tar-builder.js +80 -0
- package/test/inject.test.js +130 -1
- package/test/lang.test.js +87 -0
- package/test/quality-filter.test.js +68 -0
- package/test/reasoning-effort.test.js +106 -15
- package/test/reindex-backfill.test.js +62 -0
- package/test/runtime-adopt.test.js +206 -0
- package/test/runtime-cli.test.js +89 -0
- package/test/runtime-closure.test.js +193 -0
- package/test/runtime-download.test.js +302 -0
- package/test/runtime-layout.test.js +163 -0
- package/test/runtime-loader.test.js +237 -0
- package/test/runtime-manifest.test.js +158 -0
- package/test/runtime-provision.test.js +101 -0
- package/test/runtime-tarball.test.js +82 -0
- package/test/runtime-verify.test.js +214 -0
- package/test/scope-retrieval.test.js +197 -0
- package/test/scope-storage.test.js +247 -0
- package/test/scope-strict.test.js +165 -0
- package/test/service.test.js +76 -0
- package/test/sleep-action-set.test.js +422 -0
- package/test/store.test.js +14 -0
- package/test/summarize.test.js +224 -0
- package/test/tools.test.js +30 -5
- package/test/webServer-optional.test.js +34 -1
package/lib/store.js
CHANGED
|
@@ -43,7 +43,8 @@ CREATE TABLE IF NOT EXISTS dream_runs (
|
|
|
43
43
|
summary_stored INTEGER NOT NULL DEFAULT 0,
|
|
44
44
|
receipt TEXT NOT NULL,
|
|
45
45
|
policy_epoch INTEGER NOT NULL DEFAULT 0, -- 裁决规则版本:规则升级后旧裁决降级为历史证据
|
|
46
|
-
run_type TEXT NOT NULL DEFAULT 'auto'
|
|
46
|
+
run_type TEXT NOT NULL DEFAULT 'auto', -- auto | sleep:睡眠周期的审计区分
|
|
47
|
+
skipped TEXT -- JSON: degraded 轮被跳过的逐条明细(index/action/ids/error)
|
|
47
48
|
);
|
|
48
49
|
CREATE INDEX IF NOT EXISTS idx_dream_runs_created ON dream_runs(created_at);
|
|
49
50
|
|
|
@@ -309,7 +310,9 @@ function escapeLike(q) {
|
|
|
309
310
|
// 法值一律返回 undefined → 不进 WHERE(忽略而非报错:面板传坏参数时宁可放宽
|
|
310
311
|
// 过滤也不要白屏)。updated_at 列是 toISOString 产生的 UTC "Z" 字符串,字典
|
|
311
312
|
// 序与时间序一致,SQL 里可直接比较。
|
|
312
|
-
|
|
313
|
+
// v0.8.0 A2:导出复用——occurred_at 时间过滤(service 的搜索后置过滤)与
|
|
314
|
+
// updated_at 过滤共用同一套边界归一化,坏参数口径一致。
|
|
315
|
+
export function updatedAtBounds(updatedFrom, updatedTo) {
|
|
313
316
|
const norm = (raw, endOfDay) => {
|
|
314
317
|
if (typeof raw !== "string" || !raw.trim()) return undefined;
|
|
315
318
|
const s = raw.trim();
|
|
@@ -331,6 +334,26 @@ function parseTags(raw) {
|
|
|
331
334
|
}
|
|
332
335
|
}
|
|
333
336
|
|
|
337
|
+
// scope 列(agent_scope/workspace_scope/sensitivity)写入归一化:非空 trim 字符串
|
|
338
|
+
// 原样收下,其余(undefined/null/空串/非字符串)一律 NULL。存储层不做枚举校验——
|
|
339
|
+
// scope 值是宿主身份标签(agentPreset id / 目录路径 / 自由 sensitivity 标签),
|
|
340
|
+
// 语义由调用方负责;这里只保证「脏输入落 NULL,绝不阻塞写入」。
|
|
341
|
+
function normalizeScopeText(raw) {
|
|
342
|
+
if (typeof raw !== "string") return null;
|
|
343
|
+
const s = raw.trim();
|
|
344
|
+
return s ? s : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// occurred_at:事件发生时间(区别于 created_at 的入库时间)。可解析的时间戳
|
|
348
|
+
// 统一归一到 UTC ISO(A2 的时间过滤要按字典序直接比较);解析失败落 NULL,
|
|
349
|
+
// 同样不阻塞写入。
|
|
350
|
+
function normalizeOccurredAt(raw) {
|
|
351
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
352
|
+
const ms = Date.parse(raw.trim());
|
|
353
|
+
if (Number.isNaN(ms)) return null;
|
|
354
|
+
return new Date(ms).toISOString();
|
|
355
|
+
}
|
|
356
|
+
|
|
334
357
|
function toRow(row) {
|
|
335
358
|
if (!row) return undefined;
|
|
336
359
|
return {
|
|
@@ -346,6 +369,10 @@ function toRow(row) {
|
|
|
346
369
|
content_history: parseJsonArray(row.content_history),
|
|
347
370
|
quality_score: row.quality_score !== null && row.quality_score !== undefined ? Number(row.quality_score) : undefined,
|
|
348
371
|
epistemic_status: row.epistemic_status ?? "subjective",
|
|
372
|
+
agent_scope: row.agent_scope ?? undefined,
|
|
373
|
+
workspace_scope: row.workspace_scope ?? undefined,
|
|
374
|
+
sensitivity: row.sensitivity ?? undefined,
|
|
375
|
+
occurred_at: row.occurred_at ?? undefined,
|
|
349
376
|
created_at: row.created_at,
|
|
350
377
|
updated_at: row.updated_at,
|
|
351
378
|
last_accessed_at: row.last_accessed_at ?? undefined,
|
|
@@ -371,7 +398,9 @@ function toDreamRun(row) {
|
|
|
371
398
|
summary_stored: row.summary_stored === 1,
|
|
372
399
|
receipt: row.receipt,
|
|
373
400
|
policy_epoch: row.policy_epoch ?? 0,
|
|
374
|
-
run_type: row.run_type ?? "auto"
|
|
401
|
+
run_type: row.run_type ?? "auto",
|
|
402
|
+
// Issue #104:degraded 轮的逐条跳过明细(JSON 列,NULL = 该轮无跳过项)。
|
|
403
|
+
skipped: row.skipped ? JSON.parse(row.skipped) : undefined
|
|
375
404
|
};
|
|
376
405
|
}
|
|
377
406
|
|
|
@@ -594,9 +623,20 @@ export function createStore(path) {
|
|
|
594
623
|
addColumn("memories", "content_history", "ALTER TABLE memories ADD COLUMN content_history TEXT");
|
|
595
624
|
addColumn("memories", "quality_score", "ALTER TABLE memories ADD COLUMN quality_score REAL");
|
|
596
625
|
|
|
626
|
+
// v0.8.0 A1(issue #17)scope 隔离存储层:四列全部可空、不带 DEFAULT——存量行
|
|
627
|
+
// 零重写,读侧把 NULL 视为未标注(= 全局可见)。是否落值由 scopeEnabled 门控
|
|
628
|
+
// (service/tools 层),存储层只负责归一化(normalizeScopeText/normalizeOccurredAt)。
|
|
629
|
+
addColumn("memories", "agent_scope", "ALTER TABLE memories ADD COLUMN agent_scope TEXT");
|
|
630
|
+
addColumn("memories", "workspace_scope", "ALTER TABLE memories ADD COLUMN workspace_scope TEXT");
|
|
631
|
+
addColumn("memories", "sensitivity", "ALTER TABLE memories ADD COLUMN sensitivity TEXT");
|
|
632
|
+
addColumn("memories", "occurred_at", "ALTER TABLE memories ADD COLUMN occurred_at TEXT");
|
|
633
|
+
|
|
597
634
|
// Legacy dream_runs without policy_epoch → backfill with the default epoch.
|
|
598
635
|
addColumn("dream_runs", "policy_epoch", "ALTER TABLE dream_runs ADD COLUMN policy_epoch INTEGER NOT NULL DEFAULT 0");
|
|
599
636
|
addColumn("dream_runs", "run_type", "ALTER TABLE dream_runs ADD COLUMN run_type TEXT NOT NULL DEFAULT 'auto'");
|
|
637
|
+
// Issue #104:degraded(合法子集已应用)轮被跳过的决策明细。此前只进 logger.warn,
|
|
638
|
+
// 离线回放 dream_runs 无法定位 degraded 成因(跨类型 merge / update 保护期 / unknown id)。
|
|
639
|
+
addColumn("dream_runs", "skipped", "ALTER TABLE dream_runs ADD COLUMN skipped TEXT");
|
|
600
640
|
|
|
601
641
|
// Legacy mirror_state without v0.3.6 generation columns → add each missing
|
|
602
642
|
// column idempotently (old DBs open cleanly, no data loss).
|
|
@@ -637,7 +677,7 @@ export function createStore(path) {
|
|
|
637
677
|
return ts;
|
|
638
678
|
}
|
|
639
679
|
|
|
640
|
-
function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, updatedFrom = null, updatedTo = null } = {}) {
|
|
680
|
+
function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, updatedFrom = null, updatedTo = null, occurredFrom = null, occurredTo = null, visibility = null } = {}) {
|
|
641
681
|
const clauses = [];
|
|
642
682
|
const params = [];
|
|
643
683
|
if (type !== undefined) {
|
|
@@ -664,6 +704,33 @@ export function createStore(path) {
|
|
|
664
704
|
clauses.push("updated_at <= ?");
|
|
665
705
|
params.push(bounds.to);
|
|
666
706
|
}
|
|
707
|
+
// occurred_at 闭区间:与 list() 同口径(COALESCE 回退 created_at),
|
|
708
|
+
// memory_list 的 total 才能和过滤后的行保持一致。
|
|
709
|
+
const occurred = updatedAtBounds(occurredFrom, occurredTo);
|
|
710
|
+
if (occurred.from) {
|
|
711
|
+
clauses.push("COALESCE(occurred_at, created_at) >= ?");
|
|
712
|
+
params.push(occurred.from);
|
|
713
|
+
}
|
|
714
|
+
if (occurred.to) {
|
|
715
|
+
clauses.push("COALESCE(occurred_at, created_at) <= ?");
|
|
716
|
+
params.push(occurred.to);
|
|
717
|
+
}
|
|
718
|
+
// occurred_at 闭区间:与 list() 同过滤(visibility 同口径),memory_list
|
|
719
|
+
// 的 total 才能和过滤后的行保持一致。
|
|
720
|
+
if (visibility) {
|
|
721
|
+
if (visibility.agentScope != null) {
|
|
722
|
+
clauses.push("(agent_scope IS NULL OR agent_scope = ?)");
|
|
723
|
+
params.push(visibility.agentScope);
|
|
724
|
+
} else {
|
|
725
|
+
clauses.push("agent_scope IS NULL");
|
|
726
|
+
}
|
|
727
|
+
if (visibility.workspaceScope != null) {
|
|
728
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope = ?)");
|
|
729
|
+
params.push(visibility.workspaceScope);
|
|
730
|
+
} else {
|
|
731
|
+
clauses.push("workspace_scope IS NULL");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
667
734
|
if (!includeForgotten) {
|
|
668
735
|
clauses.push("forgotten = 0");
|
|
669
736
|
}
|
|
@@ -708,8 +775,8 @@ export function createStore(path) {
|
|
|
708
775
|
: inferEpistemicStatus(memory);
|
|
709
776
|
runAtomically(() => {
|
|
710
777
|
db.prepare(
|
|
711
|
-
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, content_history, quality_score, embedding, epistemic_status, created_at, updated_at)
|
|
712
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
778
|
+
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, content_history, quality_score, embedding, epistemic_status, agent_scope, workspace_scope, sensitivity, occurred_at, created_at, updated_at)
|
|
779
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
713
780
|
).run(
|
|
714
781
|
id,
|
|
715
782
|
type,
|
|
@@ -723,6 +790,10 @@ export function createStore(path) {
|
|
|
723
790
|
Number.isFinite(memory.quality_score) ? memory.quality_score : null,
|
|
724
791
|
embedding,
|
|
725
792
|
epistemicStatus,
|
|
793
|
+
normalizeScopeText(memory.agent_scope),
|
|
794
|
+
normalizeScopeText(memory.workspace_scope),
|
|
795
|
+
normalizeScopeText(memory.sensitivity),
|
|
796
|
+
normalizeOccurredAt(memory.occurred_at),
|
|
726
797
|
now,
|
|
727
798
|
now
|
|
728
799
|
);
|
|
@@ -930,7 +1001,7 @@ export function createStore(path) {
|
|
|
930
1001
|
return rows.map(toRow);
|
|
931
1002
|
}
|
|
932
1003
|
|
|
933
|
-
function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, minImportance = null, source = null, updatedFrom = null, updatedTo = null } = {}) {
|
|
1004
|
+
function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, minImportance = null, source = null, updatedFrom = null, updatedTo = null, occurredFrom = null, occurredTo = null, visibility = null } = {}) {
|
|
934
1005
|
const clauses = [];
|
|
935
1006
|
const params = [];
|
|
936
1007
|
if (type) {
|
|
@@ -958,6 +1029,36 @@ export function createStore(path) {
|
|
|
958
1029
|
clauses.push("updated_at <= ?");
|
|
959
1030
|
params.push(bounds.to);
|
|
960
1031
|
}
|
|
1032
|
+
// v0.8.0 A2(issue #17):occurred_at 闭区间过滤——按「事件发生时间」检索。
|
|
1033
|
+
// 未标注 occurred_at 的行(含全部存量)回退 created_at 比较(COALESCE),
|
|
1034
|
+
// 否则过滤器对旧库近乎不可用;边界归一化与 updated_at 共用 updatedAtBounds。
|
|
1035
|
+
const occurred = updatedAtBounds(occurredFrom, occurredTo);
|
|
1036
|
+
if (occurred.from) {
|
|
1037
|
+
clauses.push("COALESCE(occurred_at, created_at) >= ?");
|
|
1038
|
+
params.push(occurred.from);
|
|
1039
|
+
}
|
|
1040
|
+
if (occurred.to) {
|
|
1041
|
+
clauses.push("COALESCE(occurred_at, created_at) <= ?");
|
|
1042
|
+
params.push(occurred.to);
|
|
1043
|
+
}
|
|
1044
|
+
// v0.8.0 A3(issue #17):strictScope 硬过滤(memory_list 分页路径)。
|
|
1045
|
+
// SQL 与 service.isVisibleInScope 同口径:未标注(NULL)恒可见;当前维度
|
|
1046
|
+
// 解析不到时该维度只放行未标注行(fail-closed)。只有显式传 visibility
|
|
1047
|
+
// 的用户面调用才过滤——dream/质量过滤等内部 store.list 调用不受影响。
|
|
1048
|
+
if (visibility) {
|
|
1049
|
+
if (visibility.agentScope != null) {
|
|
1050
|
+
clauses.push("(agent_scope IS NULL OR agent_scope = ?)");
|
|
1051
|
+
params.push(visibility.agentScope);
|
|
1052
|
+
} else {
|
|
1053
|
+
clauses.push("agent_scope IS NULL");
|
|
1054
|
+
}
|
|
1055
|
+
if (visibility.workspaceScope != null) {
|
|
1056
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope = ?)");
|
|
1057
|
+
params.push(visibility.workspaceScope);
|
|
1058
|
+
} else {
|
|
1059
|
+
clauses.push("workspace_scope IS NULL");
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
961
1062
|
if (!includeForgotten) {
|
|
962
1063
|
clauses.push("forgotten = 0");
|
|
963
1064
|
}
|
|
@@ -1031,11 +1132,18 @@ export function createStore(path) {
|
|
|
1031
1132
|
).get().c;
|
|
1032
1133
|
}
|
|
1033
1134
|
|
|
1034
|
-
/**
|
|
1135
|
+
/**
|
|
1136
|
+
* Candidate rows still missing an embedding, for incremental re-indexing.
|
|
1137
|
+
* Issue #128: active rows only — archived/forgotten rows never participate in
|
|
1138
|
+
* recall or dream clustering, so backfill quota must not be eaten by them
|
|
1139
|
+
* (report measured 25/50 reindex slots landing on archived rows while 88
|
|
1140
|
+
* active rows stayed un-embedded). Same 口径 as embeddedCount()/count().
|
|
1141
|
+
*/
|
|
1035
1142
|
function needsEmbedding(limit = 50) {
|
|
1036
1143
|
return db.prepare(
|
|
1037
1144
|
`SELECT id, title, content FROM memories
|
|
1038
|
-
WHERE embedding IS NULL OR embedding = ''
|
|
1145
|
+
WHERE (embedding IS NULL OR embedding = '')
|
|
1146
|
+
AND forgotten = 0 AND archived = 0
|
|
1039
1147
|
ORDER BY updated_at DESC LIMIT ?`
|
|
1040
1148
|
).all(limit);
|
|
1041
1149
|
}
|
|
@@ -1119,16 +1227,20 @@ export function createStore(path) {
|
|
|
1119
1227
|
const now = nowIso();
|
|
1120
1228
|
const policyEpoch = Number.isInteger(run.policy_epoch) ? run.policy_epoch : 0;
|
|
1121
1229
|
const runType = run.run_type ?? "auto";
|
|
1230
|
+
// Issue #104:degraded 轮的跳过明细(JSON)。与 decisions/outcome 同为可选
|
|
1231
|
+
// 载荷,未提供时落 NULL(而不是 "[]"),审计行只记真实发生过的跳过。
|
|
1232
|
+
const skipped = run.skipped !== undefined ? JSON.stringify(run.skipped) : null;
|
|
1122
1233
|
db.prepare(
|
|
1123
1234
|
`INSERT INTO dream_runs (id, created_at, status, error, provider, model, snapshot_hash,
|
|
1124
|
-
input_count, input, decisions, outcome, applied, summary_stored, receipt, policy_epoch, run_type)
|
|
1125
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1235
|
+
input_count, input, decisions, outcome, applied, summary_stored, receipt, policy_epoch, run_type, skipped)
|
|
1236
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1126
1237
|
ON CONFLICT(id) DO UPDATE SET
|
|
1127
1238
|
created_at=excluded.created_at, status=excluded.status, error=excluded.error,
|
|
1128
1239
|
provider=excluded.provider, model=excluded.model, snapshot_hash=excluded.snapshot_hash,
|
|
1129
1240
|
input_count=excluded.input_count, input=excluded.input, decisions=excluded.decisions,
|
|
1130
1241
|
outcome=excluded.outcome, applied=excluded.applied, summary_stored=excluded.summary_stored,
|
|
1131
|
-
receipt=excluded.receipt, policy_epoch=excluded.policy_epoch, run_type=excluded.run_type
|
|
1242
|
+
receipt=excluded.receipt, policy_epoch=excluded.policy_epoch, run_type=excluded.run_type,
|
|
1243
|
+
skipped=excluded.skipped`
|
|
1132
1244
|
).run(
|
|
1133
1245
|
id,
|
|
1134
1246
|
run.created_at ?? now,
|
|
@@ -1145,7 +1257,8 @@ export function createStore(path) {
|
|
|
1145
1257
|
run.summary_stored ? 1 : 0,
|
|
1146
1258
|
run.receipt,
|
|
1147
1259
|
policyEpoch,
|
|
1148
|
-
runType
|
|
1260
|
+
runType,
|
|
1261
|
+
skipped
|
|
1149
1262
|
);
|
|
1150
1263
|
return getDreamRun(id);
|
|
1151
1264
|
}
|
package/lib/summarize.js
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
-
|
|
3
|
-
const SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内容,提炼值得跨会话记住的原子记忆。
|
|
4
|
-
原子记忆原则:每条记忆只装一个独立事实/偏好/决策,短小、自带完整上下文(把数字、名字、路径、结论等原始细节保留在 content 里,不要抽象概括);宁可拆成多条也绝不合并丢细节。信息量一般提 2-4 条,信息密集的对话可提 4-8 条。
|
|
5
|
-
只输出 JSON 数组,每项形如 {"type":"preference|project|decision|history","title":"简短标题","content":"保留原始细节的一句话","importance":1-5}。
|
|
6
|
-
不要输出任何其他文字。`;
|
|
2
|
+
import { STR, langOf } from "./lang.js";
|
|
7
3
|
|
|
8
4
|
// 编码记忆蒸馏 prompt(codingRetrospect 开启时启用):在通用记忆之外,额外提取
|
|
9
5
|
// 三类编码专属记忆,专治重复踩坑 / 遗忘被否决方案 / 丢失工程约束。字段仍沿用
|
|
10
6
|
// title/content 单列结构(store 无结构化字段),信息浓缩进 content。
|
|
11
|
-
const CODING_SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内容(含用户输入、助手回答、工具调用与结果),提炼值得跨会话记住的原子记忆。
|
|
12
|
-
原子记忆原则:每条记忆只装一个独立事实/偏好/决策,短小、自带完整上下文(把数字、报错信息、命令、路径、结论等原始细节保留在 content 里,不要抽象概括);宁可拆成多条也绝不合并丢细节。信息量一般提 2-4 条,信息密集的对话可提 4-8 条。
|
|
13
|
-
只输出 JSON 数组,每项形如 {"type":"preference|project|decision|history|rejected_solution|pitfall|constraint","title":"简短标题","content":"保留原始细节的一句话","importance":1-5}。
|
|
14
|
-
若对话涉及编码/调试,可额外提取编码类记忆:
|
|
15
|
-
- rejected_solution:被否决/废弃的实现方案(content 含方案简述 + 被否决原因 + 最终采用方案)
|
|
16
|
-
- pitfall:调试踩坑记录(content 含现象/报错 + 根因 + 解决/规避方法)
|
|
17
|
-
- constraint:项目工程约束(content 含约束描述 + 来源)
|
|
18
|
-
普通闲聊、临时无关对话一律不提取编码类记忆。不要输出任何其他文字。`;
|
|
19
|
-
|
|
20
7
|
/** Extract a JSON array from LLM output that may contain prose around it. */
|
|
21
8
|
export function parseSummaryJson(raw) {
|
|
22
9
|
const text = String(raw ?? "");
|
|
@@ -101,7 +88,7 @@ function toProtocolChunk(chunk) {
|
|
|
101
88
|
// Privacy: assistant `reasoning` (private thought) blocks are deliberately NOT
|
|
102
89
|
// collected — distilled memories must never sink private reasoning chains.
|
|
103
90
|
// Only public text blocks (type "text") reach the summarizer.
|
|
104
|
-
function collectMessages(session, maxChars = 8000) {
|
|
91
|
+
function collectMessages(session, maxChars = 8000, language = "zh") {
|
|
105
92
|
// DSH 0.1.2-rc.1 起 Session 改用 snapshotEvents(),兼容旧版 .events
|
|
106
93
|
const events = session.snapshotEvents?.() ?? session.events ?? [];
|
|
107
94
|
const lines = [];
|
|
@@ -123,14 +110,14 @@ function collectMessages(session, maxChars = 8000) {
|
|
|
123
110
|
case "user/message": {
|
|
124
111
|
if (kind !== undefined && kind !== "user") break;
|
|
125
112
|
const text = textOf(data?.content);
|
|
126
|
-
if (text.trim()) lines.push(
|
|
113
|
+
if (text.trim()) lines.push(STR.transcriptUser[language](text));
|
|
127
114
|
break;
|
|
128
115
|
}
|
|
129
116
|
case "assistant/message": {
|
|
130
117
|
const msg = data?.message;
|
|
131
118
|
const blocks = Array.isArray(msg?.content) ? msg.content : [];
|
|
132
119
|
const text = textOf(blocks);
|
|
133
|
-
if (text.trim()) lines.push(
|
|
120
|
+
if (text.trim()) lines.push(STR.transcriptAssistant[language](text));
|
|
134
121
|
// 私有推理块(reasoning)刻意不采集:蒸馏记忆不得沉淀模型私有思考链。
|
|
135
122
|
break;
|
|
136
123
|
}
|
|
@@ -138,19 +125,19 @@ function collectMessages(session, maxChars = 8000) {
|
|
|
138
125
|
const args = typeof data.arguments === "string"
|
|
139
126
|
? data.arguments
|
|
140
127
|
: data.arguments ? JSON.stringify(data.arguments) : "";
|
|
141
|
-
lines.push(
|
|
128
|
+
lines.push(STR.transcriptToolCall[language](data.name ?? "?", trim(args, 300)));
|
|
142
129
|
break;
|
|
143
130
|
}
|
|
144
131
|
case "tool/result": {
|
|
145
132
|
const out = typeof data.output === "string" ? data.output : data.output ? JSON.stringify(data.output) : "";
|
|
146
|
-
const status = data.ok === false ?
|
|
147
|
-
lines.push(
|
|
133
|
+
const status = data.ok === false ? STR.statusFail[language] : STR.statusOk[language];
|
|
134
|
+
lines.push(STR.transcriptToolResult[language](status, trim(out, 500)));
|
|
148
135
|
break;
|
|
149
136
|
}
|
|
150
137
|
case "tool/code-dispatch": {
|
|
151
138
|
const out = typeof data.output === "string" ? data.output : data.output ? JSON.stringify(data.output) : "";
|
|
152
|
-
const status = data.ok === false ?
|
|
153
|
-
lines.push(
|
|
139
|
+
const status = data.ok === false ? STR.statusFail[language] : STR.statusOk[language];
|
|
140
|
+
lines.push(STR.transcriptCode[language](status, trim(out, 500)));
|
|
154
141
|
break;
|
|
155
142
|
}
|
|
156
143
|
default:
|
|
@@ -197,10 +184,54 @@ export function createSummarizer(ctx, service, config) {
|
|
|
197
184
|
if (!config.autoSummarize) return { dispose: () => {} };
|
|
198
185
|
|
|
199
186
|
const inFlight = new Map();
|
|
187
|
+
// Issue #127:per-session 上一次实际开跑时刻(最小间隔闸门用)。与 inFlight
|
|
188
|
+
// 同生命周期,dispose 时一并清空。
|
|
189
|
+
const lastRunAt = new Map();
|
|
200
190
|
let disposed = false;
|
|
201
191
|
|
|
192
|
+
/** 写一条 autoSummarize 的 LLM 审计行(审计失败绝不阻塞蒸馏本身)。 */
|
|
193
|
+
function writeAudit(entry) {
|
|
194
|
+
if (config?.llmAudit?.enabled === false || typeof service.saveLlmAudit !== "function") return;
|
|
195
|
+
try {
|
|
196
|
+
service.saveLlmAudit({
|
|
197
|
+
trigger_source: "autoSummarize",
|
|
198
|
+
operation_type: "summarize_compress",
|
|
199
|
+
related_memory_ids: [],
|
|
200
|
+
...entry
|
|
201
|
+
});
|
|
202
|
+
} catch (auditError) {
|
|
203
|
+
ctx.logger?.warn?.(`dsh-mneme: llm audit write failed: ${String(auditError)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
202
207
|
async function summarize(session) {
|
|
203
208
|
if (disposed || inFlight.has(session.id)) return;
|
|
209
|
+
|
|
210
|
+
const header = session.requestHeader?.()?.config;
|
|
211
|
+
// Config override takes priority, then session header, then nothing.
|
|
212
|
+
const route = (config.summarizeProvider && config.summarizeModel)
|
|
213
|
+
? { provider: config.summarizeProvider, model: config.summarizeModel }
|
|
214
|
+
: (header?.provider && header?.model)
|
|
215
|
+
? { provider: header.provider, model: header.model }
|
|
216
|
+
: undefined;
|
|
217
|
+
|
|
218
|
+
// Issue #127 间隔门:同一会话两次蒸馏之间的最小间隔(0 = 现状,不限)。进入
|
|
219
|
+
// 即打点——失败/degraded 的 run 也占用间隔,与 dreamMinIntervalMinutes 语义
|
|
220
|
+
// 一致,防止失败连发。被挡下的 turn/end 静默跳过,但留一行 status='skipped'
|
|
221
|
+
// 审计,否则「这一轮为什么没蒸馏」对用户完全不可观测。
|
|
222
|
+
const gapMs = (config.summarizeMinIntervalMinutes ?? 0) * 60000;
|
|
223
|
+
const prevClaim = lastRunAt.get(session.id);
|
|
224
|
+
if (gapMs > 0 && Date.now() - (prevClaim ?? 0) < gapMs) {
|
|
225
|
+
writeAudit({
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
model_id: route ? `${route.provider}:${route.model}` : "unknown",
|
|
228
|
+
status: "skipped",
|
|
229
|
+
error_message: "min-interval"
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
lastRunAt.set(session.id, Date.now());
|
|
234
|
+
|
|
204
235
|
const controller = new AbortController();
|
|
205
236
|
inFlight.set(session.id, controller);
|
|
206
237
|
// audit state for the compression call. null = no audit for this run
|
|
@@ -209,18 +240,12 @@ export function createSummarizer(ctx, service, config) {
|
|
|
209
240
|
// so a failed/aborted stream still leaves a status='error' trail without
|
|
210
241
|
// ever blocking the summarization itself.
|
|
211
242
|
let audit = null;
|
|
243
|
+
let abortedRun = false;
|
|
212
244
|
try {
|
|
213
|
-
const header = session.requestHeader?.()?.config;
|
|
214
|
-
// Config override takes priority, then session header, then nothing.
|
|
215
|
-
const route = (config.summarizeProvider && config.summarizeModel)
|
|
216
|
-
? { provider: config.summarizeProvider, model: config.summarizeModel }
|
|
217
|
-
: (header?.provider && header?.model)
|
|
218
|
-
? { provider: header.provider, model: header.model }
|
|
219
|
-
: undefined;
|
|
220
245
|
if (!route) return;
|
|
221
246
|
// 完整转录(Codex 式):蒸馏把整轮对话交给 LLM 提炼原子记忆,不再硬裁
|
|
222
247
|
// 8000 字截断语义;上限由 distillMaxChars 控制(默认 24000,可调大)。
|
|
223
|
-
const messages = collectMessages(session, config.distillMaxChars ?? 24000);
|
|
248
|
+
const messages = collectMessages(session, config.distillMaxChars ?? 24000, langOf(config));
|
|
224
249
|
if (!messages.length) return;
|
|
225
250
|
|
|
226
251
|
if (config?.llmAudit?.enabled !== false && typeof service.saveLlmAudit === "function") {
|
|
@@ -240,7 +265,7 @@ export function createSummarizer(ctx, service, config) {
|
|
|
240
265
|
model: route.model,
|
|
241
266
|
purpose: "summarization",
|
|
242
267
|
messages: [
|
|
243
|
-
{ role: "system", content: [{ type: "text", text: config.codingRetrospect ?
|
|
268
|
+
{ role: "system", content: [{ type: "text", text: config.codingRetrospect ? STR.prompts.codingSummary[langOf(config)] : STR.prompts.summary[langOf(config)] }] },
|
|
244
269
|
...messages
|
|
245
270
|
],
|
|
246
271
|
signal: controller.signal
|
|
@@ -314,8 +339,19 @@ export function createSummarizer(ctx, service, config) {
|
|
|
314
339
|
}
|
|
315
340
|
}
|
|
316
341
|
}, intervalMs);
|
|
317
|
-
if (aborted)
|
|
318
|
-
|
|
342
|
+
if (aborted) {
|
|
343
|
+
abortedRun = true;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const parsed = parseSummaryJson(text || assembledText);
|
|
347
|
+
// Issue #127 条数上限:0 = 不限(现状)。被截断的条数与去重命中数一并进
|
|
348
|
+
// 审计 metadata——此前产出条数完全由模型输出决定,用户无从自查。
|
|
349
|
+
const cap = config.summarizeMaxEntriesPerRun ?? 0;
|
|
350
|
+
const entries = cap > 0 ? parsed.slice(0, cap) : parsed;
|
|
351
|
+
const capped = parsed.length - entries.length;
|
|
352
|
+
const dedupeMode = config.summarizeDedupeMode ?? "off";
|
|
353
|
+
let deduped = 0;
|
|
354
|
+
let dedupeMaxSim = 0;
|
|
319
355
|
for (const entry of entries) {
|
|
320
356
|
// Provenance: the summarizer runs on a real session (turn/end hook), so
|
|
321
357
|
// session.id is always available here — it rides the human-readable
|
|
@@ -323,12 +359,45 @@ export function createSummarizer(ctx, service, config) {
|
|
|
323
359
|
// 编码记忆类型(codingRetrospect)不带 tag:读取侧门控/加权靠 m.type
|
|
324
360
|
// (rejected_solution/pitfall/constraint)区分即可,tag 体系
|
|
325
361
|
// sanitizeTags 不认 `type:` 前缀反而会清空 tags 列(额外一次 UPDATE)。
|
|
362
|
+
const source = `session:${session.id}`;
|
|
363
|
+
// Issue #127 落库前去重(opt-in,默认 off = 现状):命中则并入既有条目
|
|
364
|
+
// (saveWithDedupe 的 _mergeInto 分支),不再新造一行。作用域 = 同会话 +
|
|
365
|
+
// 时间窗;跨会话的同类事实仍交给 autoDream / sleep 的批量裁决。
|
|
366
|
+
const dup = (dedupeMode !== "off" && typeof service.findSessionDuplicate === "function")
|
|
367
|
+
? await service.findSessionDuplicate(
|
|
368
|
+
{ ...entry, source },
|
|
369
|
+
{
|
|
370
|
+
mode: dedupeMode,
|
|
371
|
+
minSim: config.summarizeDedupeMinSim ?? 0.92,
|
|
372
|
+
windowHours: config.summarizeDedupeWindowHours ?? 24,
|
|
373
|
+
source
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
: undefined;
|
|
377
|
+
if (dup) {
|
|
378
|
+
deduped++;
|
|
379
|
+
dedupeMaxSim = Math.max(dedupeMaxSim, dup.sim ?? 0);
|
|
380
|
+
}
|
|
326
381
|
service.saveWithDedupe({
|
|
327
382
|
...entry,
|
|
328
|
-
source
|
|
383
|
+
source,
|
|
384
|
+
...(dup ? { _mergeInto: dup.memory.id } : {})
|
|
329
385
|
});
|
|
330
386
|
}
|
|
387
|
+
if (audit && (capped > 0 || deduped > 0)) {
|
|
388
|
+
audit.metadata = {
|
|
389
|
+
parsed: parsed.length,
|
|
390
|
+
...(capped > 0 ? { capped } : {}),
|
|
391
|
+
...(deduped > 0 ? { deduped, mode: dedupeMode, maxSim: Number(dedupeMaxSim.toFixed(4)) } : {})
|
|
392
|
+
};
|
|
393
|
+
}
|
|
331
394
|
} finally {
|
|
395
|
+
// Issue #127:aborted(会话关闭 / 插件 dispose)不占间隔,避免误伤该会话的
|
|
396
|
+
// 下一次蒸馏;其余情况(含失败)的打点保留,与 dreamMinIntervalMinutes 一致。
|
|
397
|
+
if (abortedRun || controller.signal.aborted) {
|
|
398
|
+
if (prevClaim === undefined) lastRunAt.delete(session.id);
|
|
399
|
+
else lastRunAt.set(session.id, prevClaim);
|
|
400
|
+
}
|
|
332
401
|
if (audit) {
|
|
333
402
|
try {
|
|
334
403
|
service.saveLlmAudit({
|
|
@@ -343,7 +412,10 @@ export function createSummarizer(ctx, service, config) {
|
|
|
343
412
|
duration_ms: Date.now() - audit.startedAt,
|
|
344
413
|
status: audit.status,
|
|
345
414
|
error_message: audit.errorMessage,
|
|
346
|
-
related_memory_ids: []
|
|
415
|
+
related_memory_ids: [],
|
|
416
|
+
// Issue #127:parsed / capped / deduped 的观测数据。零 schema 变更
|
|
417
|
+
// (llm_audit_logs.metadata 已存在),一条 SQL 即可验证节流是否生效。
|
|
418
|
+
metadata: audit.metadata
|
|
347
419
|
});
|
|
348
420
|
} catch (auditError) {
|
|
349
421
|
ctx.logger?.warn?.(`dsh-mneme: llm audit write failed: ${String(auditError)}`);
|
|
@@ -371,6 +443,7 @@ export function createSummarizer(ctx, service, config) {
|
|
|
371
443
|
unsubscribe?.();
|
|
372
444
|
for (const controller of inFlight.values()) controller.abort();
|
|
373
445
|
inFlight.clear();
|
|
446
|
+
lastRunAt.clear();
|
|
374
447
|
}
|
|
375
448
|
};
|
|
376
449
|
}
|