@modusensus/dsh-mneme 0.8.15 → 0.9.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 +21 -3
- package/assets/icon.svg +2 -2
- package/bin/cli.mjs +4 -2
- package/docs/CONFIGURATION.md +16 -3
- package/docs/CONTINUITY.md +18 -3
- package/docs/DISTILL.md +162 -0
- package/docs/STORAGE.md +11 -2
- package/docs/WORKFLOWS.md +299 -0
- package/lib/api-standalone.js +22 -39
- package/lib/api.js +294 -10
- package/lib/candidates.js +454 -0
- package/lib/capabilities.js +819 -0
- package/lib/client.js +2767 -337
- package/lib/config.js +99 -6
- package/lib/continuity.js +35 -6
- package/lib/dream/decisions.js +37 -1
- package/lib/dream/sleep.js +10 -2
- package/lib/dream.js +7 -1
- package/lib/entities/extractor.js +4 -0
- package/lib/hot-memory.js +62 -0
- package/lib/index.js +72 -18
- package/lib/inject.js +33 -64
- package/lib/maintenance.js +25 -5
- package/lib/pipeline.js +534 -0
- package/lib/recall-stats.js +22 -7
- package/lib/reclaim-http.js +150 -0
- package/lib/safe-slice.js +18 -0
- package/lib/scope.js +16 -4
- package/lib/serve.js +1 -1
- package/lib/service.js +83 -17
- package/lib/session-usage.js +350 -0
- package/lib/settings.js +19 -2
- package/lib/store.js +251 -10
- package/lib/summarize.js +239 -60
- package/lib/tools.js +11 -2
- package/package.json +1 -1
- package/scripts/publish-gate.mjs +197 -0
- package/src/api-standalone.js +22 -39
- package/src/api.js +294 -10
- package/src/candidates.js +454 -0
- package/src/capabilities.js +819 -0
- package/src/config.js +99 -6
- package/src/continuity.js +35 -6
- package/src/dream/decisions.js +37 -1
- package/src/dream/sleep.js +10 -2
- package/src/dream.js +7 -1
- package/src/entities/extractor.js +4 -0
- package/src/hot-memory.js +62 -0
- package/src/index.js +72 -18
- package/src/inject.js +33 -64
- package/src/maintenance.js +25 -5
- package/src/pipeline.js +534 -0
- package/src/recall-stats.js +22 -7
- package/src/reclaim-http.js +150 -0
- package/src/safe-slice.js +18 -0
- package/src/scope.js +16 -4
- package/src/serve.js +1 -1
- package/src/service.js +83 -17
- package/src/session-usage.js +350 -0
- package/src/settings.js +19 -2
- package/src/store.js +251 -10
- package/src/summarize.js +239 -60
- package/src/tools.js +11 -2
- package/test/api-routes-gaps.test.js +681 -0
- package/test/api.test.js +171 -8
- package/test/candidates.test.js +345 -0
- package/test/capabilities.test.js +577 -0
- package/test/client.test.js +715 -4
- package/test/continuity.test.js +84 -0
- package/test/contrast.test.js +314 -0
- package/test/dream-merge-guardrail.test.js +219 -0
- package/test/embed-ready-poll.test.js +94 -0
- package/test/entities.test.js +13 -2
- package/test/failure-query.test.js +147 -0
- package/test/failure-retention.test.js +105 -0
- package/test/graph-api.test.js +36 -2
- package/test/hot-memory.test.js +67 -1
- package/test/inject-recall.test.js +68 -1
- package/test/inject.test.js +23 -0
- package/test/llm-audit.test.js +190 -1
- package/test/maintenance.test.js +112 -2
- package/test/mint-source.test.js +202 -0
- package/test/pipeline.test.js +505 -0
- package/test/placement.test.js +1 -1
- package/test/pre-inject-gate.test.js +10 -0
- package/test/publish-gate.test.js +272 -0
- package/test/recall-retention.test.js +128 -0
- package/test/recall-stats.test.js +89 -4
- package/test/reclaim-http.test.js +209 -0
- package/test/release-workflow.test.js +77 -0
- package/test/scope-distill-stamp.test.js +135 -0
- package/test/serve.test.js +1 -1
- package/test/service.test.js +7 -2
- package/test/session-usage.test.js +308 -0
- package/test/settings.test.js +8 -0
- package/test/store-write-path-columns.test.js +191 -0
- package/test/store.test.js +108 -0
- package/test/summarize.test.js +350 -0
- package/test/supersede-state-tracking.test.js +4 -4
- package/test/tools.test.js +16 -1
package/lib/store.js
CHANGED
|
@@ -13,6 +13,10 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
13
13
|
importance INTEGER NOT NULL DEFAULT 3,
|
|
14
14
|
forgotten INTEGER NOT NULL DEFAULT 0,
|
|
15
15
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
-- Issue #275 第二批:归档时刻的精确列。archived 是标志位,updated_at 只是
|
|
17
|
+
-- 「最后一次写」——对已归档行做一次 update 就会刷它,代理口径因此高估归档
|
|
18
|
+
-- 净增速率。本列只在「从活跃面进入归档面」那一次落值,取消归档置 NULL。
|
|
19
|
+
archived_at TEXT,
|
|
16
20
|
source TEXT,
|
|
17
21
|
content_history TEXT,
|
|
18
22
|
content_hash TEXT,
|
|
@@ -215,6 +219,52 @@ CREATE TABLE IF NOT EXISTS distill_cursors (
|
|
|
215
219
|
updated_at TEXT NOT NULL
|
|
216
220
|
);
|
|
217
221
|
|
|
222
|
+
-- #399 蒸馏窗口的原始公开记录(凭证)。蒸馏此前只把会话压成记忆、原文随即消失,
|
|
223
|
+
-- 于是「这句原文没进任何记忆」永远不可测——留存一份原始副本,蒸馏就从「替换」
|
|
224
|
+
-- 降级成「叠加索引」(arXiv 2609.36059 §3 Eq.1:与原文并存的旁路索引不丢信息)。
|
|
225
|
+
-- 主键 (session_id, seq) 承担幂等:额度耗尽 / 宿主重启后会重跑同一 seq 窗口
|
|
226
|
+
-- (summarize.js 的 max-runs 门刻意不消费游标),重跑必须不产生重复记录。
|
|
227
|
+
-- 落盘文本不受 distillMaxChars / trim() 三刀约束——留存的意义就是整段原文。
|
|
228
|
+
-- 刻意不加 sensitivity 列:机密级是**记忆级**判断,写记录那一刻它还不存在
|
|
229
|
+
-- (一个窗口里什么都有),加了就是往 schema 里写一个做不到的承诺。
|
|
230
|
+
CREATE TABLE IF NOT EXISTS distill_records (
|
|
231
|
+
session_id TEXT NOT NULL,
|
|
232
|
+
seq INTEGER NOT NULL,
|
|
233
|
+
role TEXT NOT NULL, -- user | assistant | agent(lang.js 的三种公开形态)
|
|
234
|
+
text TEXT NOT NULL, -- 未截断的公开正文
|
|
235
|
+
window_id TEXT,
|
|
236
|
+
distilled_at TEXT NOT NULL,
|
|
237
|
+
PRIMARY KEY (session_id, seq)
|
|
238
|
+
);
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_distill_records_window ON distill_records(window_id);
|
|
240
|
+
-- 到期裁剪按 distilled_at 扫(与 llm_audit_logs.timestamp 同一口径)。
|
|
241
|
+
CREATE INDEX IF NOT EXISTS idx_distill_records_distilled_at ON distill_records(distilled_at);
|
|
242
|
+
|
|
243
|
+
-- #399 记忆级溯源:一条记忆由哪个蒸馏窗口的哪个 seq 区间派生。归因用**区间**而非
|
|
244
|
+
-- 逐事件外键——蒸馏窗口本身就是连续 seq 段,窗口级归因第一步够用。
|
|
245
|
+
-- 删除语义:到期裁剪只删 distill_records,本表保留并指向空区间(面板/CLI 显示
|
|
246
|
+
-- 「来源已过期」),绝不级联删记忆;删记忆只清链接、记录保留——一个窗口的凭证
|
|
247
|
+
-- 不该因删掉其中一条派生记忆而消失。
|
|
248
|
+
-- memory_id 是 TEXT 而非 issue 草稿里的 INTEGER:memories.id 是 TEXT 主键,
|
|
249
|
+
-- 类型必须与父表一致(否则 join 恒空)。
|
|
250
|
+
-- ON DELETE CASCADE 在本库**不生效**——store.js 从不打开 PRAGMA foreign_keys,
|
|
251
|
+
-- 全库唯一那条 REFERENCES 也一直是惰性的。与其为一个新表翻转全局 pragma
|
|
252
|
+
-- (会同时改变既有 recall_events 的写入行为),不如在 remove() 里显式清链接:
|
|
253
|
+
-- 语义相同、影响面只在本表。见 remove() 的注释。
|
|
254
|
+
CREATE TABLE IF NOT EXISTS memory_provenance (
|
|
255
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
256
|
+
session_id TEXT NOT NULL,
|
|
257
|
+
seq_start INTEGER NOT NULL,
|
|
258
|
+
seq_end INTEGER NOT NULL,
|
|
259
|
+
created_at TEXT NOT NULL
|
|
260
|
+
);
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_memory_provenance_memory ON memory_provenance(memory_id);
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_memory_provenance_session ON memory_provenance(session_id);
|
|
263
|
+
-- issue 草稿没有唯一约束,但重跑同一窗口会重复插同一链接(幂等测试锁 1 会挂)。
|
|
264
|
+
-- 同一 (记忆, 会话, 区间) 只有一条事实,用唯一索引 + ON CONFLICT DO NOTHING 表达。
|
|
265
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_provenance_link
|
|
266
|
+
ON memory_provenance(memory_id, session_id, seq_start, seq_end);
|
|
267
|
+
|
|
218
268
|
-- entity gene (v0.3.0): named entities mentioned across memories, with
|
|
219
269
|
-- time-boxed attributes (valid_from → valid_until) and typed relations.
|
|
220
270
|
-- Attributes follow the snapshot style: saveAttr invalidates the previous
|
|
@@ -453,6 +503,10 @@ function toRow(row) {
|
|
|
453
503
|
importance: row.importance,
|
|
454
504
|
forgotten: row.forgotten === 1,
|
|
455
505
|
archived: row.archived === 1,
|
|
506
|
+
// Issue #275 第二批:NULL(存量归档行、或落地前的历史数据)读作 undefined,
|
|
507
|
+
// 与 sensitivity/occurred_at 同款——调用方必须显式处理「时刻不可知」,不能
|
|
508
|
+
// 顺手 `?? updated_at` 把代理口径捡回来。
|
|
509
|
+
archived_at: row.archived_at ?? undefined,
|
|
456
510
|
source: row.source ?? undefined,
|
|
457
511
|
content_history: parseJsonArray(row.content_history),
|
|
458
512
|
evidence: parseJsonArray(row.evidence),
|
|
@@ -741,6 +795,11 @@ export function createStore(path) {
|
|
|
741
795
|
};
|
|
742
796
|
|
|
743
797
|
addColumn("memories", "archived", "ALTER TABLE memories ADD COLUMN archived INTEGER NOT NULL DEFAULT 0");
|
|
798
|
+
// Issue #275 第二批:归档时刻精确列。可空不带 DEFAULT——存量库的已归档行
|
|
799
|
+
// 零重写,读侧 NULL = 「本列落地前归档,时刻不可追溯」。刻意不回填:用
|
|
800
|
+
// updated_at 回填等于把被推翻的代理口径重新固化进列里,偏差(偏高)会从
|
|
801
|
+
// 「已知的口径差」变成「看不见的脏数据」。
|
|
802
|
+
addColumn("memories", "archived_at", "ALTER TABLE memories ADD COLUMN archived_at TEXT");
|
|
744
803
|
addColumn("memories", "embedding", "ALTER TABLE memories ADD COLUMN embedding TEXT");
|
|
745
804
|
addColumn("memories", "last_accessed_at", "ALTER TABLE memories ADD COLUMN last_accessed_at TEXT");
|
|
746
805
|
addColumn("memories", "_full_content", "ALTER TABLE memories ADD COLUMN _full_content TEXT");
|
|
@@ -1058,8 +1117,8 @@ export function createStore(path) {
|
|
|
1058
1117
|
: inferEpistemicStatus(memory);
|
|
1059
1118
|
runAtomically(() => {
|
|
1060
1119
|
db.prepare(
|
|
1061
|
-
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, content_history, quality_score, embedding, epistemic_status, agent_scope, workspace_scope, agent_scope_source, workspace_scope_source, scope_decided_at, sensitivity, occurred_at, evidence, doc_path, content_hash, created_at, updated_at)
|
|
1062
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1120
|
+
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, archived_at, source, content_history, quality_score, embedding, epistemic_status, agent_scope, workspace_scope, agent_scope_source, workspace_scope_source, scope_decided_at, sensitivity, occurred_at, evidence, doc_path, content_hash, created_at, updated_at)
|
|
1121
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1063
1122
|
).run(
|
|
1064
1123
|
id,
|
|
1065
1124
|
type,
|
|
@@ -1068,6 +1127,10 @@ export function createStore(path) {
|
|
|
1068
1127
|
tags,
|
|
1069
1128
|
importance,
|
|
1070
1129
|
memory.archived ? 1 : 0,
|
|
1130
|
+
// 铸造即归档的行(store.save({archived:true})):归档时刻就是入库时刻。
|
|
1131
|
+
// 不写这一格的话,archived=1 而 archived_at=NULL 会落在「存量行」语义里,
|
|
1132
|
+
// 精确口径读侧只能把它当不可追溯——明明是当下发生的归档,白丢一条记录。
|
|
1133
|
+
memory.archived ? now : null,
|
|
1071
1134
|
memory.source ?? null,
|
|
1072
1135
|
JSON.stringify(memory.content_history ?? []),
|
|
1073
1136
|
Number.isFinite(memory.quality_score) ? memory.quality_score : null,
|
|
@@ -1218,6 +1281,12 @@ export function createStore(path) {
|
|
|
1218
1281
|
|
|
1219
1282
|
function remove(id) {
|
|
1220
1283
|
runAtomically(() => {
|
|
1284
|
+
// #399:memory_provenance 的 ON DELETE CASCADE 在本库是惰性的(createStore
|
|
1285
|
+
// 从不打开 PRAGMA foreign_keys),所以链接在这里显式清。不用翻转全局 pragma
|
|
1286
|
+
// 换这一条级联:那会同时激活 recall_events 那条既有的 REFERENCES,改变一个
|
|
1287
|
+
// 与 #399 无关的写入路径。记录本身(distill_records)不删——删掉一条派生记忆
|
|
1288
|
+
// 不该让整个窗口的原文凭证消失。
|
|
1289
|
+
db.prepare("DELETE FROM memory_provenance WHERE memory_id = ?").run(id);
|
|
1221
1290
|
db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
1222
1291
|
// Mirror sync must reflect the deletion; bump desired generation so a
|
|
1223
1292
|
// crash between the delete and syncMirror leaves a recoverable debt.
|
|
@@ -1308,10 +1377,27 @@ export function createStore(path) {
|
|
|
1308
1377
|
return getById(id);
|
|
1309
1378
|
}
|
|
1310
1379
|
|
|
1380
|
+
// Issue #275 第二批:archived_at 是「从活跃面进入归档面」那一刻的精确记录。
|
|
1381
|
+
// updated_at 照旧刷新(本函数自 v0.2 起的既有语义,其他读侧依赖它——面板排序、
|
|
1382
|
+
// 快照哈希、CAS),两列职责分开:updated_at = 最后一次写,archived_at = 归档时刻。
|
|
1383
|
+
// 幂等:已是归档态再调 archived=true,不刷新 archived_at(重复归档不是新一次
|
|
1384
|
+
// 归档,否则「重复调 setArchived」又变成一次净增——那正是代理口径被推翻的原因)。
|
|
1311
1385
|
function setArchived(id, archived) {
|
|
1386
|
+
const next = archived ? 1 : 0;
|
|
1312
1387
|
runAtomically(() => {
|
|
1313
|
-
|
|
1314
|
-
|
|
1388
|
+
// nowIso 在事务内取(与改动前一致):单调顶推的时间戳不该被一次回滚白白消耗。
|
|
1389
|
+
const now = nowIso();
|
|
1390
|
+
// CASE 里的 archived 是**更新前**的值(SQLite UPDATE 表达式语义):
|
|
1391
|
+
// 活跃 → 归档:落当前时刻;已归档 → 归档:保留原值(含存量 NULL,不回填);
|
|
1392
|
+
// 任意 → 取消归档:清 NULL。
|
|
1393
|
+
// 幂等刻意选「重复归档不刷新时刻」:否则「对同一行重复调 setArchived(true)」
|
|
1394
|
+
// 会再被算成本窗口新进——那正是本批要消灭的代理口径偏高,换个入口复活而已。
|
|
1395
|
+
db.prepare(
|
|
1396
|
+
`UPDATE memories SET archived = ?,
|
|
1397
|
+
archived_at = CASE WHEN ? = 1 AND archived = 0 THEN ? WHEN ? = 0 THEN NULL ELSE archived_at END,
|
|
1398
|
+
updated_at = ?
|
|
1399
|
+
WHERE id = ?`
|
|
1400
|
+
).run(next, next, now, next, now, id);
|
|
1315
1401
|
incrementGeneration();
|
|
1316
1402
|
});
|
|
1317
1403
|
return getById(id);
|
|
@@ -1479,11 +1565,24 @@ export function createStore(path) {
|
|
|
1479
1565
|
// "chrono" is pure newest-first — the stable order paged browsing (month
|
|
1480
1566
|
// tree, infinite scroll) needs; importance ordering would interleave
|
|
1481
1567
|
// months across pages.
|
|
1482
|
-
|
|
1568
|
+
// #430:chrono 的 **无界档** 不再追加 id 平局键。updated_at 上有 #202 建的
|
|
1569
|
+
// 索引,而带 id 的第二项排序键用不上它——5k 行实测同一份 SQL 只差这一项:
|
|
1570
|
+
// 100.6ms(带 id,EXPLAIN 多一个 USE TEMP B-TREE)对 23.4ms(纯 updated_at,
|
|
1571
|
+
// 直接走 idx_memories_updated)。分页档位必须留平局键(跨页拼接要求稳定,
|
|
1572
|
+
// 同刻两行在新旧页之间可能漏一条或重复一条);无界档没有跨页拼接,今天唯一
|
|
1573
|
+
// 的调用方 syncMirror 只用它算覆盖集,与顺序无关。
|
|
1574
|
+
const orderBy = order === "chrono"
|
|
1575
|
+
? (unbounded ? "updated_at DESC" : "updated_at DESC, id DESC")
|
|
1576
|
+
: "importance DESC, updated_at DESC, id";
|
|
1577
|
+
// Issue #430:与 all() 共用同一份显式列清单(memoryColumns 由 PRAGMA 派生,
|
|
1578
|
+
// 排除 embedding)。toRow 本就不输出向量,SELECT * 只是把全表最重的列拖进
|
|
1579
|
+
// 内存再丢弃——无界档实测 5k 行 263ms → 117ms,窗口档 50ms → 41ms,本函数
|
|
1580
|
+
// 的全部调用方(分页浏览、document 索引、sleep 池、注册器全量扫描)一起受益。
|
|
1581
|
+
const projected = `SELECT ${memoryColumnList} FROM memories`;
|
|
1483
1582
|
const rows = unbounded
|
|
1484
|
-
? db.prepare(
|
|
1583
|
+
? db.prepare(`${projected} ${where} ORDER BY ${orderBy}`).all(...params)
|
|
1485
1584
|
: db.prepare(
|
|
1486
|
-
|
|
1585
|
+
`${projected} ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`
|
|
1487
1586
|
).all(...params, lim, off);
|
|
1488
1587
|
return rows.map(toRow);
|
|
1489
1588
|
}
|
|
@@ -1495,6 +1594,18 @@ export function createStore(path) {
|
|
|
1495
1594
|
return rows.map(toRow);
|
|
1496
1595
|
}
|
|
1497
1596
|
|
|
1597
|
+
// Issue #430:写路径要的「最近若干条正文」。质量过滤的近重复判定只读 content
|
|
1598
|
+
// 这一列 + 前 20 行,此前写成 all().slice(0, 20) —— 全表逐行 toRow(27 字段、
|
|
1599
|
+
// 三次 JSON 解析)之后只截前 20 条正文;5k 行实测 100ms 级 → 0.06ms 级。
|
|
1600
|
+
// 口径与 all() 的前 N 条严格一致:不加 WHERE(archived/forgotten 仍参与近重复
|
|
1601
|
+
// 判定)、updated_at DESC、同刻顺序未定义。limit 走 sanitizePage,非正整数
|
|
1602
|
+
// 落回 20,调用方不必自己防 0/undefined。
|
|
1603
|
+
function recentContents(limit = 20) {
|
|
1604
|
+
const { limit: lim } = sanitizePage(limit, 0, 20);
|
|
1605
|
+
const rows = db.prepare("SELECT content FROM memories ORDER BY updated_at DESC LIMIT ?").all(lim);
|
|
1606
|
+
return rows.map((row) => row.content ?? "");
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1498
1609
|
/** Set (or clear with null) the embedding vector of a memory. */
|
|
1499
1610
|
function setEmbedding(id, vector) {
|
|
1500
1611
|
const json = Array.isArray(vector) && vector.length ? JSON.stringify(vector) : null;
|
|
@@ -1882,6 +1993,30 @@ export function createStore(path) {
|
|
|
1882
1993
|
|
|
1883
1994
|
// --- recall evaluation trail (方案 B: separate from the production audit) -
|
|
1884
1995
|
|
|
1996
|
+
/**
|
|
1997
|
+
* Rolling purge of the recall_runs production audit (v0.7.0): with recordRecall
|
|
1998
|
+
* defaulting ON, the table would otherwise grow unboundedly on a busy store.
|
|
1999
|
+
* Drop rows older than `days` (config.recallRetentionDays, default 90).
|
|
2000
|
+
* created_at is an ISO TEXT string, so the ISO bound compares lexicographically.
|
|
2001
|
+
* Returns the number of rows deleted (bookkeeping — never throws on empty).
|
|
2002
|
+
*
|
|
2003
|
+
* 2026-10-14 找回:这一对(本函数 + index.js 的启动调用)在 dd0ed78「release:
|
|
2004
|
+
* v0.7.12 + 编码记忆(codingRetrospect)蒸馏/…(合并 #73 与 #74)」里被整块弄丢。
|
|
2005
|
+
* 该提交不是 merge(单亲 cb5ec63),store.js 是从一个没有这段的版本重建的,
|
|
2006
|
+
* 于是函数与调用一起蒸发,而配置键 recallRetentionDays、README.md 与
|
|
2007
|
+
* docs/CONFIGURATION.md 的「recall_runs 滚动清理保留天数」一直留到今天。
|
|
2008
|
+
* 实测后果(2026-10-14 快照):本机 recall_runs 18,442 行、跨度 2026-09-08 →
|
|
2009
|
+
* 2026-10-13,即从那一天起只增不减。注意这张表是**活的、只增的**:同一个 10-13
|
|
2010
|
+
* 当天 14:43Z 是 18,442 行、20:47Z 已是 20,029 行,几小时就差一千五——所以任何
|
|
2011
|
+
* 行数只在测它的那一刻成立,引用时必须带时刻,否则日后对不上会被误读成出了错。
|
|
2012
|
+
* 这份审计里现在装的是用户原话,不是无信息的空串。
|
|
2013
|
+
*/
|
|
2014
|
+
function purgeRecallRunsOlderThan(days) {
|
|
2015
|
+
const cutoff = new Date(Date.now() - days * 86400000).toISOString();
|
|
2016
|
+
const result = db.prepare("DELETE FROM recall_runs WHERE created_at < ?").run(cutoff);
|
|
2017
|
+
return result.changes;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1885
2020
|
/**
|
|
1886
2021
|
* Persist one retrieval-evaluation snapshot into recall_evals — the test/eval
|
|
1887
2022
|
* sibling of recall_runs, deliberately stored apart so eval snapshots never
|
|
@@ -2011,8 +2146,16 @@ export function createStore(path) {
|
|
|
2011
2146
|
|
|
2012
2147
|
/**
|
|
2013
2148
|
* Aggregate LLM spend over the last `days`: total calls/tokens/duration/cost,
|
|
2014
|
-
* broken down by trigger_source
|
|
2015
|
-
* /llm-audit/stats endpoint so the Web panel can show
|
|
2149
|
+
* broken down by trigger_source, by status, by operation_type and by day.
|
|
2150
|
+
* Used by the API's /llm-audit/stats endpoint so the Web panel can show
|
|
2151
|
+
* where budget goes.
|
|
2152
|
+
*
|
|
2153
|
+
* by_operation / by_day exist because by_source is too coarse for the
|
|
2154
|
+
* "where did the tokens go" panel: trigger_source merges dream_consolidate
|
|
2155
|
+
* and dream_summarize into one autoDream bucket, hiding the single largest
|
|
2156
|
+
* cost item (37% of 7-day tokens in the 2026-10 audit). by_day feeds the
|
|
2157
|
+
* 7-day trend bars. Both are additive — by_source/by_status keep their
|
|
2158
|
+
* shape (two tests assert on them).
|
|
2016
2159
|
*/
|
|
2017
2160
|
function getLlmAuditStats({ days = 7 } = {}) {
|
|
2018
2161
|
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
@@ -2034,6 +2177,22 @@ export function createStore(path) {
|
|
|
2034
2177
|
const byStatus = db.prepare(
|
|
2035
2178
|
"SELECT status, count(*) AS c FROM llm_audit_logs WHERE timestamp >= ? GROUP BY status"
|
|
2036
2179
|
).all(since);
|
|
2180
|
+
const byOperation = db.prepare(
|
|
2181
|
+
`SELECT operation_type AS operation, count(*) AS c,
|
|
2182
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
2183
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
2184
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens
|
|
2185
|
+
FROM llm_audit_logs WHERE timestamp >= ?
|
|
2186
|
+
GROUP BY operation_type ORDER BY total_tokens DESC`
|
|
2187
|
+
).all(since);
|
|
2188
|
+
const byDay = db.prepare(
|
|
2189
|
+
`SELECT date(timestamp) AS day, count(*) AS c,
|
|
2190
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
2191
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
2192
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens
|
|
2193
|
+
FROM llm_audit_logs WHERE timestamp >= ?
|
|
2194
|
+
GROUP BY date(timestamp) ORDER BY day ASC`
|
|
2195
|
+
).all(since);
|
|
2037
2196
|
return {
|
|
2038
2197
|
days,
|
|
2039
2198
|
since,
|
|
@@ -2044,7 +2203,9 @@ export function createStore(path) {
|
|
|
2044
2203
|
total_duration_ms: total.d,
|
|
2045
2204
|
total_cost_usd: Number(total.cst),
|
|
2046
2205
|
by_source: bySource,
|
|
2047
|
-
by_status: byStatus
|
|
2206
|
+
by_status: byStatus,
|
|
2207
|
+
by_operation: byOperation,
|
|
2208
|
+
by_day: byDay
|
|
2048
2209
|
};
|
|
2049
2210
|
}
|
|
2050
2211
|
|
|
@@ -2053,6 +2214,77 @@ export function createStore(path) {
|
|
|
2053
2214
|
return db.prepare("DELETE FROM llm_audit_logs WHERE timestamp < ?").run(before).changes;
|
|
2054
2215
|
}
|
|
2055
2216
|
|
|
2217
|
+
// --- 蒸馏原始记录与记忆级溯源(#399)-------------------------------------
|
|
2218
|
+
|
|
2219
|
+
/**
|
|
2220
|
+
* 落盘一个蒸馏窗口的公开记录(#399)。
|
|
2221
|
+
*
|
|
2222
|
+
* `INSERT OR IGNORE` 而非 OR REPLACE:主键 (session_id, seq) 的冲突只可能来自
|
|
2223
|
+
* **同一窗口的重跑**(max-runs 门耗尽额度、宿主重启都会重跑同一 seq 段,见
|
|
2224
|
+
* summarize.js 里「max-runs 刻意不消费游标」的注释),而重跑必须幂等——同一
|
|
2225
|
+
* seq 的原文没有第二次「更真」的版本,保留首次落盘时间才诚实。
|
|
2226
|
+
* @param {Array<{seq:number, role:string, text:string, windowId?:string, distilledAt:string}>} records
|
|
2227
|
+
* @returns {number} 实际插入行数(被忽略的冲突不计)
|
|
2228
|
+
*/
|
|
2229
|
+
function saveDistillRecords(records) {
|
|
2230
|
+
if (!Array.isArray(records) || records.length === 0) return 0;
|
|
2231
|
+
const stmt = db.prepare(
|
|
2232
|
+
`INSERT OR IGNORE INTO distill_records (session_id, seq, role, text, window_id, distilled_at)
|
|
2233
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
2234
|
+
);
|
|
2235
|
+
let inserted = 0;
|
|
2236
|
+
for (const r of records) {
|
|
2237
|
+
inserted += stmt.run(r.sessionId, r.seq, r.role, r.text, r.windowId ?? null, r.distilledAt).changes;
|
|
2238
|
+
}
|
|
2239
|
+
return inserted;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
/**
|
|
2243
|
+
* 到期记录统计(#275 回收报告口径,dry-run 用)。`bytes` 是列文本大小上界。
|
|
2244
|
+
* @returns {{rows: number, bytes: number}}
|
|
2245
|
+
*/
|
|
2246
|
+
function distillRecordStats(before) {
|
|
2247
|
+
const row = db.prepare(
|
|
2248
|
+
`SELECT count(*) AS c, COALESCE(SUM(LENGTH(text)), 0) AS b
|
|
2249
|
+
FROM distill_records WHERE distilled_at < ?`
|
|
2250
|
+
).get(before);
|
|
2251
|
+
return { rows: row.c, bytes: row.b };
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/**
|
|
2255
|
+
* 删除到期原始记录。返回实际删除行数。
|
|
2256
|
+
*
|
|
2257
|
+
* 只删 distill_records——memory_provenance 的链接**刻意保留**并从此指向空区间,
|
|
2258
|
+
* 面板/CLI 显示「来源已过期」。级联删链接会把「这条记忆有来源、只是来源到期」
|
|
2259
|
+
* 降级成「这条记忆无来源」,把可观测性削成不可观测(#399 删除语义)。
|
|
2260
|
+
*/
|
|
2261
|
+
function clearDistillRecords(before) {
|
|
2262
|
+
return db.prepare("DELETE FROM distill_records WHERE distilled_at < ?").run(before).changes;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
/**
|
|
2266
|
+
* 记一条记忆级溯源链接(#399):某条记忆派生自某会话的 seq 区间。
|
|
2267
|
+
*
|
|
2268
|
+
* `ON CONFLICT DO NOTHING` 配合 idx_memory_provenance_link 唯一索引吸收同窗口
|
|
2269
|
+
* 重跑的重复链接(否则幂等测试锁会挂)。
|
|
2270
|
+
* @returns {number} 实际插入行数
|
|
2271
|
+
*/
|
|
2272
|
+
function saveMemoryProvenance({ memoryId, sessionId, seqStart, seqEnd, createdAt }) {
|
|
2273
|
+
return db.prepare(
|
|
2274
|
+
`INSERT INTO memory_provenance (memory_id, session_id, seq_start, seq_end, created_at)
|
|
2275
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2276
|
+
ON CONFLICT DO NOTHING`
|
|
2277
|
+
).run(memoryId, sessionId, seqStart, seqEnd, createdAt).changes;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
/** 一条记忆的全部溯源链接(#399 面板/CLI 读取面)。 */
|
|
2281
|
+
function provenanceFor(memoryId) {
|
|
2282
|
+
return db.prepare(
|
|
2283
|
+
`SELECT memory_id, session_id, seq_start, seq_end, created_at
|
|
2284
|
+
FROM memory_provenance WHERE memory_id = ? ORDER BY created_at, session_id, seq_start`
|
|
2285
|
+
).all(memoryId);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2056
2288
|
// --- 存储生命周期:无损回收(#275 第一批)--------------------------------
|
|
2057
2289
|
//
|
|
2058
2290
|
// 两项都零价值判断、零条数变化:行数不变,只把不可重建/不可达的内容丢掉。
|
|
@@ -2919,6 +3151,8 @@ export function createStore(path) {
|
|
|
2919
3151
|
getUnrecalledSince,
|
|
2920
3152
|
list,
|
|
2921
3153
|
all,
|
|
3154
|
+
// #430:有界单列读,写路径(质量过滤)用。
|
|
3155
|
+
recentContents,
|
|
2922
3156
|
search,
|
|
2923
3157
|
setEmbedding,
|
|
2924
3158
|
getEmbeddings,
|
|
@@ -2939,6 +3173,7 @@ export function createStore(path) {
|
|
|
2939
3173
|
getRecallRun,
|
|
2940
3174
|
listRecallRuns,
|
|
2941
3175
|
listRecallRunsSince,
|
|
3176
|
+
purgeRecallRunsOlderThan,
|
|
2942
3177
|
saveRecallEval,
|
|
2943
3178
|
getRecallEval,
|
|
2944
3179
|
listRecallEvals,
|
|
@@ -2949,6 +3184,12 @@ export function createStore(path) {
|
|
|
2949
3184
|
countLlmAudits,
|
|
2950
3185
|
getLlmAuditStats,
|
|
2951
3186
|
deleteOldLlmAudits,
|
|
3187
|
+
// 蒸馏原始记录与记忆级溯源(#399):窗口凭证的写入/统计/裁剪,链接的读写。
|
|
3188
|
+
saveDistillRecords,
|
|
3189
|
+
distillRecordStats,
|
|
3190
|
+
clearDistillRecords,
|
|
3191
|
+
saveMemoryProvenance,
|
|
3192
|
+
provenanceFor,
|
|
2952
3193
|
// 存储生命周期(#275 第一批):无损回收的统计、清理与体积口径。
|
|
2953
3194
|
dreamRunInputStats,
|
|
2954
3195
|
clearDreamRunInputs,
|