@modusensus/dsh-mneme 0.6.8 → 0.6.10
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/LICENSE +21 -21
- package/README.md +468 -463
- package/cordis.patch.yml +15 -15
- package/lib/api.js +783 -783
- package/lib/client.js +1754 -1757
- package/lib/commands.js +64 -64
- package/lib/config.js +298 -288
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +488 -439
- package/lib/dream/sleep.js +561 -561
- package/lib/dream/tag-extractor.js +156 -156
- package/lib/dream.js +958 -935
- package/lib/embedding.js +154 -154
- package/lib/entities/extractor.js +242 -242
- package/lib/hot-memory.js +53 -53
- package/lib/index.js +361 -361
- package/lib/inject.js +208 -208
- package/lib/local-embedder.js +282 -282
- package/lib/mirror.js +170 -170
- package/lib/parser/tag.js +59 -59
- package/lib/parser/wiki-link.js +38 -38
- package/lib/quality-filter.js +123 -123
- package/lib/reranker.js +218 -218
- package/lib/search/adaptive.js +22 -22
- package/lib/search/bm25.js +96 -96
- package/lib/search/tag-boost.js +61 -61
- package/lib/service.js +1726 -1726
- package/lib/settings.js +172 -172
- package/lib/store.js +2238 -2238
- package/lib/summarize.js +236 -236
- package/lib/tools.js +290 -290
- package/lib/vector-index.js +116 -116
- package/package.json +80 -80
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +133 -133
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/e2e-dsh.js +218 -218
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +52 -52
- package/src/api.js +783 -783
- package/src/commands.js +64 -64
- package/src/config.js +298 -288
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +488 -439
- package/src/dream/sleep.js +561 -561
- package/src/dream/tag-extractor.js +156 -156
- package/src/dream.js +958 -935
- package/src/embedding.js +154 -154
- package/src/entities/extractor.js +242 -242
- package/src/hot-memory.js +53 -53
- package/src/index.js +361 -361
- package/src/inject.js +208 -208
- package/src/local-embedder.js +282 -282
- package/src/mirror.js +170 -170
- package/src/parser/tag.js +59 -59
- package/src/parser/wiki-link.js +38 -38
- package/src/quality-filter.js +123 -123
- package/src/reranker.js +218 -218
- package/src/search/adaptive.js +22 -22
- package/src/search/bm25.js +96 -96
- package/src/search/tag-boost.js +61 -61
- package/src/service.js +1726 -1726
- package/src/settings.js +172 -172
- package/src/store.js +2238 -2238
- package/src/summarize.js +236 -236
- package/src/tools.js +290 -290
- package/src/vector-index.js +116 -116
- package/test/api.test.js +594 -594
- package/test/audit.test.js +448 -448
- package/test/benchmark.test.js +35 -35
- package/test/boundary-v0625.test.js +82 -82
- package/test/client.test.js +368 -368
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +69 -69
- package/test/config.test.js +50 -50
- package/test/conflict-freeze.test.js +290 -290
- package/test/directory.test.js +134 -134
- package/test/dream.test.js +1060 -903
- package/test/entities.test.js +522 -522
- package/test/epistemic.test.js +298 -298
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-api.test.js +175 -175
- package/test/helpers/dream-mock.js +82 -82
- package/test/hot-memory.test.js +174 -174
- package/test/inject.test.js +103 -103
- package/test/llm-audit.test.js +279 -279
- package/test/local-embedder.test.js +227 -227
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror.test.js +249 -249
- package/test/normalize-decisions.test.js +120 -120
- package/test/peer-blockers.test.js +190 -190
- package/test/policy-epoch.test.js +259 -259
- package/test/provenance.test.js +103 -103
- package/test/quality-filter.test.js +118 -118
- package/test/reasoning-effort.test.js +199 -199
- package/test/recall-evals.test.js +235 -235
- package/test/recall-layer.test.js +315 -315
- package/test/receipt-chain.test.js +451 -451
- package/test/reflection.test.js +226 -226
- package/test/reranker.test.js +240 -240
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/service-search.test.js +199 -199
- package/test/service.test.js +435 -435
- package/test/settings.test.js +118 -118
- package/test/sleep.test.js +365 -365
- package/test/store.test.js +436 -436
- package/test/stress.test.js +209 -209
- package/test/summarize.test.js +191 -191
- package/test/tag-boost.test.js +125 -125
- package/test/tag.test.js +312 -312
- package/test/tools.test.js +285 -285
- package/test/vector-index.test.js +221 -221
- package/test/wiki-link.test.js +332 -332
package/test/llm-audit.test.js
CHANGED
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { EventEmitter } from "node:events";
|
|
4
|
-
import { createStore } from "../src/store.js";
|
|
5
|
-
import { createService } from "../src/service.js";
|
|
6
|
-
import { createDreamScheduler } from "../src/dream.js";
|
|
7
|
-
import { createSummarizer } from "../src/summarize.js";
|
|
8
|
-
import { createApi } from "../src/api.js";
|
|
9
|
-
import { createSettings } from "../src/settings.js";
|
|
10
|
-
import { mockCtx } from "./helpers/dream-mock.js";
|
|
11
|
-
|
|
12
|
-
// Bug8: LLM audit trail. Every background LLM call (autoDream consolidation +
|
|
13
|
-
// summary, autoSummarize compression) records tokens/time/status into
|
|
14
|
-
// llm_audit_logs. Failures are captured as status='error' and never block the
|
|
15
|
-
// calling feature. Read back through the API (/llm-audit + /llm-audit/stats).
|
|
16
|
-
// Gating: runDream treats llmAudit.enabled === false as off; summarize treats
|
|
17
|
-
// llmAudit.enabled !== false as on.
|
|
18
|
-
|
|
19
|
-
function dreamSetup(over = {}) {
|
|
20
|
-
const store = createStore(":memory:");
|
|
21
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
22
|
-
return {
|
|
23
|
-
store,
|
|
24
|
-
service,
|
|
25
|
-
config: { dreamProvider: "deepseek", dreamModel: "deepseek-chat", llmAudit: { enabled: true }, ...over }
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test("autoDream writes llm_audit_logs rows for consolidation and summary", async () => {
|
|
30
|
-
const { store, service, config } = dreamSetup();
|
|
31
|
-
service.saveWithDedupe({ type: "project", title: "旧1", content: "第一段内容" });
|
|
32
|
-
service.saveWithDedupe({ type: "project", title: "旧2", content: "第二段内容" });
|
|
33
|
-
const ctx = mockCtx({
|
|
34
|
-
onConsolidation: (listText) => {
|
|
35
|
-
const ids = [...listText.matchAll(/id=([^\s|]+)\s*\|\s*type=\w+\s*\|\s*importance=\d+/g)].map((m) => m[1]);
|
|
36
|
-
return JSON.stringify(ids.map((id) => ({ action: "keep", ids: [id] })));
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
40
|
-
const result = await dream.runDream(ctx, service, config);
|
|
41
|
-
assert.equal(result.ok, true);
|
|
42
|
-
|
|
43
|
-
const rows = service.listLlmAudits();
|
|
44
|
-
assert.equal(rows.length, 2, "one consolidation + one summary audit row");
|
|
45
|
-
const consolidate = rows.find((r) => r.operation_type === "dream_consolidate");
|
|
46
|
-
const summarize = rows.find((r) => r.operation_type === "dream_summarize");
|
|
47
|
-
assert.ok(consolidate, "dream_consolidate row present");
|
|
48
|
-
assert.ok(summarize, "dream_summarize row present");
|
|
49
|
-
assert.equal(consolidate.trigger_source, "autoDream");
|
|
50
|
-
assert.equal(summarize.trigger_source, "autoDream");
|
|
51
|
-
assert.equal(consolidate.status, "success");
|
|
52
|
-
assert.equal(summarize.status, "success");
|
|
53
|
-
// Issue #25: config route (dreamProvider/dreamModel) wins over the agent
|
|
54
|
-
// default from mockCtx — the audit must reflect the actually-used route.
|
|
55
|
-
assert.equal(consolidate.model_id, "deepseek:deepseek-chat");
|
|
56
|
-
assert.equal(summarize.model_id, "deepseek:deepseek-chat");
|
|
57
|
-
assert.ok(Array.isArray(consolidate.related_memory_ids) && consolidate.related_memory_ids.length === 2,
|
|
58
|
-
"consolidation audit records the snapshot ids");
|
|
59
|
-
assert.ok(consolidate.total_tokens >= 0 && summarize.total_tokens >= 0);
|
|
60
|
-
assert.ok(typeof consolidate.duration_ms === "number" && consolidate.duration_ms >= 0);
|
|
61
|
-
store.close();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("autoDream LLM failure is audited as status=error and never blocks the run", async () => {
|
|
65
|
-
const { store, service, config } = dreamSetup();
|
|
66
|
-
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
67
|
-
// Consolidation stream finishes with kind "error" → streamText returns
|
|
68
|
-
// undefined → the run fails safe; runAuditedLlm must still leave an
|
|
69
|
-
// error audit row before the failure propagates.
|
|
70
|
-
const ctx = {
|
|
71
|
-
logger: { warn: () => {} },
|
|
72
|
-
llm: {
|
|
73
|
-
async *stream() {
|
|
74
|
-
yield { type: "text-delta", index: 0, text: "" };
|
|
75
|
-
yield { type: "finish", reason: { kind: "error" } };
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
80
|
-
const result = await dream.runDream(ctx, service, config);
|
|
81
|
-
assert.equal(result.ok, false, "aborted/errored stream fails the run");
|
|
82
|
-
const rows = service.listLlmAudits();
|
|
83
|
-
assert.equal(rows.length, 1, "failed consolidation still audited");
|
|
84
|
-
assert.equal(rows[0].operation_type, "dream_consolidate");
|
|
85
|
-
assert.equal(rows[0].status, "error");
|
|
86
|
-
assert.ok(rows[0].error_message, "error message recorded");
|
|
87
|
-
// the audit write did not block the failure path
|
|
88
|
-
assert.equal(result.error, "llm failed");
|
|
89
|
-
store.close();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("autoDream throwing LLM is audited as status=error", async () => {
|
|
93
|
-
const { store, service, config } = dreamSetup();
|
|
94
|
-
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
95
|
-
const ctx = {
|
|
96
|
-
logger: { warn: () => {} },
|
|
97
|
-
llm: {
|
|
98
|
-
async *stream() {
|
|
99
|
-
throw new Error("network down");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
104
|
-
const result = await dream.runDream(ctx, service, config);
|
|
105
|
-
assert.equal(result.ok, false);
|
|
106
|
-
const rows = service.listLlmAudits();
|
|
107
|
-
assert.equal(rows.length, 1, "throwing call still audited");
|
|
108
|
-
assert.equal(rows[0].status, "error");
|
|
109
|
-
assert.match(rows[0].error_message, /network down/);
|
|
110
|
-
store.close();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("autoDream audit is skipped when llmAudit.enabled === false", async () => {
|
|
114
|
-
const { store, service, config } = dreamSetup({ llmAudit: { enabled: false } });
|
|
115
|
-
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
116
|
-
const ctx = mockCtx();
|
|
117
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
118
|
-
await dream.runDream(ctx, service, config);
|
|
119
|
-
assert.equal(service.listLlmAudits().length, 0, "no audit rows when disabled");
|
|
120
|
-
store.close();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("autoSummarize writes an llm_audit_logs row (trigger_source autoSummarize)", async () => {
|
|
124
|
-
const store = createStore(":memory:");
|
|
125
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
126
|
-
const events = [];
|
|
127
|
-
const ctx = {
|
|
128
|
-
on(name, fn) {
|
|
129
|
-
events.push({ name, fn });
|
|
130
|
-
return () => {};
|
|
131
|
-
},
|
|
132
|
-
logger: { warn: () => {} },
|
|
133
|
-
llm: {
|
|
134
|
-
async *stream() {
|
|
135
|
-
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
136
|
-
{ type: "decision", title: "选型", content: "确定用 node:sqlite", importance: 4 }
|
|
137
|
-
]) };
|
|
138
|
-
yield { type: "finish", reason: { kind: "stop" } };
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
const config = {
|
|
143
|
-
autoSummarize: true,
|
|
144
|
-
summarizeProvider: "deepseek",
|
|
145
|
-
summarizeModel: "deepseek-chat",
|
|
146
|
-
llmAudit: { enabled: true }
|
|
147
|
-
};
|
|
148
|
-
createSummarizer(ctx, service, config);
|
|
149
|
-
const handler = events.find((e) => e.name === "session/event").fn;
|
|
150
|
-
const session = {
|
|
151
|
-
id: "s1",
|
|
152
|
-
requestHeader: () => ({ config: {} }),
|
|
153
|
-
events: [
|
|
154
|
-
{ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text: "帮我选型" }] } },
|
|
155
|
-
{ seq: 2, type: "turn/end" }
|
|
156
|
-
]
|
|
157
|
-
};
|
|
158
|
-
await handler(session, { seq: 2, type: "turn/end" });
|
|
159
|
-
const rows = service.listLlmAudits();
|
|
160
|
-
assert.equal(rows.length, 1, "one autoSummarize audit row");
|
|
161
|
-
assert.equal(rows[0].trigger_source, "autoSummarize");
|
|
162
|
-
assert.equal(rows[0].operation_type, "summarize_compress");
|
|
163
|
-
assert.equal(rows[0].model_id, "deepseek:deepseek-chat");
|
|
164
|
-
assert.equal(rows[0].status, "success");
|
|
165
|
-
store.close();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// --- API surface ------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
class FakeRes extends EventEmitter {
|
|
171
|
-
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
172
|
-
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
173
|
-
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function req(path) {
|
|
177
|
-
const r = new EventEmitter();
|
|
178
|
-
r.url = path;
|
|
179
|
-
r.method = "GET";
|
|
180
|
-
r.headers = {};
|
|
181
|
-
return r;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function apiSetup() {
|
|
185
|
-
const store = createStore(":memory:");
|
|
186
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
187
|
-
const settings = createSettings(store.db);
|
|
188
|
-
const routes = [];
|
|
189
|
-
const ctx = {
|
|
190
|
-
webServer: {
|
|
191
|
-
register(route) {
|
|
192
|
-
routes.push(route);
|
|
193
|
-
return () => {};
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
createApi(ctx, service, settings, { add: () => {}, remove: () => false, list: () => [] });
|
|
198
|
-
return { store, service, routes };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
test("GET /api/dsh-mneme/semantic/llm-audit paginates and filters by source", async () => {
|
|
202
|
-
const { store, service, routes } = apiSetup();
|
|
203
|
-
for (let i = 0; i < 3; i++) {
|
|
204
|
-
service.saveLlmAudit({
|
|
205
|
-
trigger_source: "autoDream",
|
|
206
|
-
operation_type: "dream_consolidate",
|
|
207
|
-
model_id: "deepseek:deepseek-chat",
|
|
208
|
-
input_tokens: 10, output_tokens: 5, status: "success"
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
service.saveLlmAudit({
|
|
212
|
-
trigger_source: "autoSummarize",
|
|
213
|
-
operation_type: "summarize_compress",
|
|
214
|
-
model_id: "deepseek:deepseek-chat",
|
|
215
|
-
input_tokens: 3, output_tokens: 1, status: "success"
|
|
216
|
-
});
|
|
217
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/semantic/llm-audit");
|
|
218
|
-
|
|
219
|
-
// full page
|
|
220
|
-
let res = new FakeRes();
|
|
221
|
-
await route.handler(req("/api/dsh-mneme/semantic/llm-audit?page=1&pageSize=2"), res);
|
|
222
|
-
let data = JSON.parse(res.body);
|
|
223
|
-
assert.equal(res.statusCode, 200);
|
|
224
|
-
assert.equal(data.total, 4);
|
|
225
|
-
assert.equal(data.page, 1);
|
|
226
|
-
assert.equal(data.pageSize, 2);
|
|
227
|
-
assert.equal(data.items.length, 2);
|
|
228
|
-
|
|
229
|
-
// second page
|
|
230
|
-
res = new FakeRes();
|
|
231
|
-
await route.handler(req("/api/dsh-mneme/semantic/llm-audit?page=2&pageSize=2"), res);
|
|
232
|
-
data = JSON.parse(res.body);
|
|
233
|
-
assert.equal(data.items.length, 2, "page 2 has the remaining rows");
|
|
234
|
-
assert.equal(data.total, 4);
|
|
235
|
-
|
|
236
|
-
// source filter
|
|
237
|
-
res = new FakeRes();
|
|
238
|
-
await route.handler(req("/api/dsh-mneme/semantic/llm-audit?source=autoSummarize"), res);
|
|
239
|
-
data = JSON.parse(res.body);
|
|
240
|
-
assert.equal(data.total, 1);
|
|
241
|
-
assert.equal(data.items[0].trigger_source, "autoSummarize");
|
|
242
|
-
store.close();
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test("GET /api/dsh-mneme/semantic/llm-audit/stats aggregates by source and status", async () => {
|
|
246
|
-
const { store, service, routes } = apiSetup();
|
|
247
|
-
service.saveLlmAudit({
|
|
248
|
-
timestamp: new Date().toISOString(),
|
|
249
|
-
trigger_source: "autoDream", operation_type: "dream_consolidate",
|
|
250
|
-
model_id: "m", input_tokens: 100, output_tokens: 40, duration_ms: 25, status: "success"
|
|
251
|
-
});
|
|
252
|
-
service.saveLlmAudit({
|
|
253
|
-
timestamp: new Date().toISOString(),
|
|
254
|
-
trigger_source: "autoDream", operation_type: "dream_summarize",
|
|
255
|
-
model_id: "m", input_tokens: 30, output_tokens: 10, duration_ms: 8, status: "success"
|
|
256
|
-
});
|
|
257
|
-
service.saveLlmAudit({
|
|
258
|
-
timestamp: new Date().toISOString(),
|
|
259
|
-
trigger_source: "autoSummarize", operation_type: "summarize_compress",
|
|
260
|
-
model_id: "m", input_tokens: 5, output_tokens: 2, duration_ms: 4, status: "error"
|
|
261
|
-
});
|
|
262
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/semantic/llm-audit/stats");
|
|
263
|
-
const res = new FakeRes();
|
|
264
|
-
await route.handler(req("/api/dsh-mneme/semantic/llm-audit/stats?days=7"), res);
|
|
265
|
-
const stats = JSON.parse(res.body);
|
|
266
|
-
assert.equal(res.statusCode, 200);
|
|
267
|
-
assert.equal(stats.days, 7);
|
|
268
|
-
assert.equal(stats.total_calls, 3);
|
|
269
|
-
assert.equal(stats.input_tokens, 135);
|
|
270
|
-
assert.equal(stats.output_tokens, 52);
|
|
271
|
-
assert.equal(stats.total_tokens, 187);
|
|
272
|
-
assert.equal(stats.total_duration_ms, 37);
|
|
273
|
-
const bySource = stats.by_source.find((s) => s.source === "autoDream");
|
|
274
|
-
assert.equal(bySource.c, 2);
|
|
275
|
-
assert.equal(bySource.total_tokens, 180);
|
|
276
|
-
const errStatus = stats.by_status.find((s) => s.status === "error");
|
|
277
|
-
assert.equal(errStatus.c, 1);
|
|
278
|
-
store.close();
|
|
279
|
-
});
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { createStore } from "../src/store.js";
|
|
5
|
+
import { createService } from "../src/service.js";
|
|
6
|
+
import { createDreamScheduler } from "../src/dream.js";
|
|
7
|
+
import { createSummarizer } from "../src/summarize.js";
|
|
8
|
+
import { createApi } from "../src/api.js";
|
|
9
|
+
import { createSettings } from "../src/settings.js";
|
|
10
|
+
import { mockCtx } from "./helpers/dream-mock.js";
|
|
11
|
+
|
|
12
|
+
// Bug8: LLM audit trail. Every background LLM call (autoDream consolidation +
|
|
13
|
+
// summary, autoSummarize compression) records tokens/time/status into
|
|
14
|
+
// llm_audit_logs. Failures are captured as status='error' and never block the
|
|
15
|
+
// calling feature. Read back through the API (/llm-audit + /llm-audit/stats).
|
|
16
|
+
// Gating: runDream treats llmAudit.enabled === false as off; summarize treats
|
|
17
|
+
// llmAudit.enabled !== false as on.
|
|
18
|
+
|
|
19
|
+
function dreamSetup(over = {}) {
|
|
20
|
+
const store = createStore(":memory:");
|
|
21
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
22
|
+
return {
|
|
23
|
+
store,
|
|
24
|
+
service,
|
|
25
|
+
config: { dreamProvider: "deepseek", dreamModel: "deepseek-chat", llmAudit: { enabled: true }, ...over }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("autoDream writes llm_audit_logs rows for consolidation and summary", async () => {
|
|
30
|
+
const { store, service, config } = dreamSetup();
|
|
31
|
+
service.saveWithDedupe({ type: "project", title: "旧1", content: "第一段内容" });
|
|
32
|
+
service.saveWithDedupe({ type: "project", title: "旧2", content: "第二段内容" });
|
|
33
|
+
const ctx = mockCtx({
|
|
34
|
+
onConsolidation: (listText) => {
|
|
35
|
+
const ids = [...listText.matchAll(/id=([^\s|]+)\s*\|\s*type=\w+\s*\|\s*importance=\d+/g)].map((m) => m[1]);
|
|
36
|
+
return JSON.stringify(ids.map((id) => ({ action: "keep", ids: [id] })));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
40
|
+
const result = await dream.runDream(ctx, service, config);
|
|
41
|
+
assert.equal(result.ok, true);
|
|
42
|
+
|
|
43
|
+
const rows = service.listLlmAudits();
|
|
44
|
+
assert.equal(rows.length, 2, "one consolidation + one summary audit row");
|
|
45
|
+
const consolidate = rows.find((r) => r.operation_type === "dream_consolidate");
|
|
46
|
+
const summarize = rows.find((r) => r.operation_type === "dream_summarize");
|
|
47
|
+
assert.ok(consolidate, "dream_consolidate row present");
|
|
48
|
+
assert.ok(summarize, "dream_summarize row present");
|
|
49
|
+
assert.equal(consolidate.trigger_source, "autoDream");
|
|
50
|
+
assert.equal(summarize.trigger_source, "autoDream");
|
|
51
|
+
assert.equal(consolidate.status, "success");
|
|
52
|
+
assert.equal(summarize.status, "success");
|
|
53
|
+
// Issue #25: config route (dreamProvider/dreamModel) wins over the agent
|
|
54
|
+
// default from mockCtx — the audit must reflect the actually-used route.
|
|
55
|
+
assert.equal(consolidate.model_id, "deepseek:deepseek-chat");
|
|
56
|
+
assert.equal(summarize.model_id, "deepseek:deepseek-chat");
|
|
57
|
+
assert.ok(Array.isArray(consolidate.related_memory_ids) && consolidate.related_memory_ids.length === 2,
|
|
58
|
+
"consolidation audit records the snapshot ids");
|
|
59
|
+
assert.ok(consolidate.total_tokens >= 0 && summarize.total_tokens >= 0);
|
|
60
|
+
assert.ok(typeof consolidate.duration_ms === "number" && consolidate.duration_ms >= 0);
|
|
61
|
+
store.close();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("autoDream LLM failure is audited as status=error and never blocks the run", async () => {
|
|
65
|
+
const { store, service, config } = dreamSetup();
|
|
66
|
+
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
67
|
+
// Consolidation stream finishes with kind "error" → streamText returns
|
|
68
|
+
// undefined → the run fails safe; runAuditedLlm must still leave an
|
|
69
|
+
// error audit row before the failure propagates.
|
|
70
|
+
const ctx = {
|
|
71
|
+
logger: { warn: () => {} },
|
|
72
|
+
llm: {
|
|
73
|
+
async *stream() {
|
|
74
|
+
yield { type: "text-delta", index: 0, text: "" };
|
|
75
|
+
yield { type: "finish", reason: { kind: "error" } };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
80
|
+
const result = await dream.runDream(ctx, service, config);
|
|
81
|
+
assert.equal(result.ok, false, "aborted/errored stream fails the run");
|
|
82
|
+
const rows = service.listLlmAudits();
|
|
83
|
+
assert.equal(rows.length, 1, "failed consolidation still audited");
|
|
84
|
+
assert.equal(rows[0].operation_type, "dream_consolidate");
|
|
85
|
+
assert.equal(rows[0].status, "error");
|
|
86
|
+
assert.ok(rows[0].error_message, "error message recorded");
|
|
87
|
+
// the audit write did not block the failure path
|
|
88
|
+
assert.equal(result.error, "llm failed");
|
|
89
|
+
store.close();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("autoDream throwing LLM is audited as status=error", async () => {
|
|
93
|
+
const { store, service, config } = dreamSetup();
|
|
94
|
+
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
95
|
+
const ctx = {
|
|
96
|
+
logger: { warn: () => {} },
|
|
97
|
+
llm: {
|
|
98
|
+
async *stream() {
|
|
99
|
+
throw new Error("network down");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
104
|
+
const result = await dream.runDream(ctx, service, config);
|
|
105
|
+
assert.equal(result.ok, false);
|
|
106
|
+
const rows = service.listLlmAudits();
|
|
107
|
+
assert.equal(rows.length, 1, "throwing call still audited");
|
|
108
|
+
assert.equal(rows[0].status, "error");
|
|
109
|
+
assert.match(rows[0].error_message, /network down/);
|
|
110
|
+
store.close();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("autoDream audit is skipped when llmAudit.enabled === false", async () => {
|
|
114
|
+
const { store, service, config } = dreamSetup({ llmAudit: { enabled: false } });
|
|
115
|
+
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
116
|
+
const ctx = mockCtx();
|
|
117
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
118
|
+
await dream.runDream(ctx, service, config);
|
|
119
|
+
assert.equal(service.listLlmAudits().length, 0, "no audit rows when disabled");
|
|
120
|
+
store.close();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("autoSummarize writes an llm_audit_logs row (trigger_source autoSummarize)", async () => {
|
|
124
|
+
const store = createStore(":memory:");
|
|
125
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
126
|
+
const events = [];
|
|
127
|
+
const ctx = {
|
|
128
|
+
on(name, fn) {
|
|
129
|
+
events.push({ name, fn });
|
|
130
|
+
return () => {};
|
|
131
|
+
},
|
|
132
|
+
logger: { warn: () => {} },
|
|
133
|
+
llm: {
|
|
134
|
+
async *stream() {
|
|
135
|
+
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
136
|
+
{ type: "decision", title: "选型", content: "确定用 node:sqlite", importance: 4 }
|
|
137
|
+
]) };
|
|
138
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const config = {
|
|
143
|
+
autoSummarize: true,
|
|
144
|
+
summarizeProvider: "deepseek",
|
|
145
|
+
summarizeModel: "deepseek-chat",
|
|
146
|
+
llmAudit: { enabled: true }
|
|
147
|
+
};
|
|
148
|
+
createSummarizer(ctx, service, config);
|
|
149
|
+
const handler = events.find((e) => e.name === "session/event").fn;
|
|
150
|
+
const session = {
|
|
151
|
+
id: "s1",
|
|
152
|
+
requestHeader: () => ({ config: {} }),
|
|
153
|
+
events: [
|
|
154
|
+
{ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text: "帮我选型" }] } },
|
|
155
|
+
{ seq: 2, type: "turn/end" }
|
|
156
|
+
]
|
|
157
|
+
};
|
|
158
|
+
await handler(session, { seq: 2, type: "turn/end" });
|
|
159
|
+
const rows = service.listLlmAudits();
|
|
160
|
+
assert.equal(rows.length, 1, "one autoSummarize audit row");
|
|
161
|
+
assert.equal(rows[0].trigger_source, "autoSummarize");
|
|
162
|
+
assert.equal(rows[0].operation_type, "summarize_compress");
|
|
163
|
+
assert.equal(rows[0].model_id, "deepseek:deepseek-chat");
|
|
164
|
+
assert.equal(rows[0].status, "success");
|
|
165
|
+
store.close();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// --- API surface ------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
class FakeRes extends EventEmitter {
|
|
171
|
+
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
172
|
+
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
173
|
+
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function req(path) {
|
|
177
|
+
const r = new EventEmitter();
|
|
178
|
+
r.url = path;
|
|
179
|
+
r.method = "GET";
|
|
180
|
+
r.headers = {};
|
|
181
|
+
return r;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function apiSetup() {
|
|
185
|
+
const store = createStore(":memory:");
|
|
186
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
187
|
+
const settings = createSettings(store.db);
|
|
188
|
+
const routes = [];
|
|
189
|
+
const ctx = {
|
|
190
|
+
webServer: {
|
|
191
|
+
register(route) {
|
|
192
|
+
routes.push(route);
|
|
193
|
+
return () => {};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
createApi(ctx, service, settings, { add: () => {}, remove: () => false, list: () => [] });
|
|
198
|
+
return { store, service, routes };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
test("GET /api/dsh-mneme/semantic/llm-audit paginates and filters by source", async () => {
|
|
202
|
+
const { store, service, routes } = apiSetup();
|
|
203
|
+
for (let i = 0; i < 3; i++) {
|
|
204
|
+
service.saveLlmAudit({
|
|
205
|
+
trigger_source: "autoDream",
|
|
206
|
+
operation_type: "dream_consolidate",
|
|
207
|
+
model_id: "deepseek:deepseek-chat",
|
|
208
|
+
input_tokens: 10, output_tokens: 5, status: "success"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
service.saveLlmAudit({
|
|
212
|
+
trigger_source: "autoSummarize",
|
|
213
|
+
operation_type: "summarize_compress",
|
|
214
|
+
model_id: "deepseek:deepseek-chat",
|
|
215
|
+
input_tokens: 3, output_tokens: 1, status: "success"
|
|
216
|
+
});
|
|
217
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/semantic/llm-audit");
|
|
218
|
+
|
|
219
|
+
// full page
|
|
220
|
+
let res = new FakeRes();
|
|
221
|
+
await route.handler(req("/api/dsh-mneme/semantic/llm-audit?page=1&pageSize=2"), res);
|
|
222
|
+
let data = JSON.parse(res.body);
|
|
223
|
+
assert.equal(res.statusCode, 200);
|
|
224
|
+
assert.equal(data.total, 4);
|
|
225
|
+
assert.equal(data.page, 1);
|
|
226
|
+
assert.equal(data.pageSize, 2);
|
|
227
|
+
assert.equal(data.items.length, 2);
|
|
228
|
+
|
|
229
|
+
// second page
|
|
230
|
+
res = new FakeRes();
|
|
231
|
+
await route.handler(req("/api/dsh-mneme/semantic/llm-audit?page=2&pageSize=2"), res);
|
|
232
|
+
data = JSON.parse(res.body);
|
|
233
|
+
assert.equal(data.items.length, 2, "page 2 has the remaining rows");
|
|
234
|
+
assert.equal(data.total, 4);
|
|
235
|
+
|
|
236
|
+
// source filter
|
|
237
|
+
res = new FakeRes();
|
|
238
|
+
await route.handler(req("/api/dsh-mneme/semantic/llm-audit?source=autoSummarize"), res);
|
|
239
|
+
data = JSON.parse(res.body);
|
|
240
|
+
assert.equal(data.total, 1);
|
|
241
|
+
assert.equal(data.items[0].trigger_source, "autoSummarize");
|
|
242
|
+
store.close();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("GET /api/dsh-mneme/semantic/llm-audit/stats aggregates by source and status", async () => {
|
|
246
|
+
const { store, service, routes } = apiSetup();
|
|
247
|
+
service.saveLlmAudit({
|
|
248
|
+
timestamp: new Date().toISOString(),
|
|
249
|
+
trigger_source: "autoDream", operation_type: "dream_consolidate",
|
|
250
|
+
model_id: "m", input_tokens: 100, output_tokens: 40, duration_ms: 25, status: "success"
|
|
251
|
+
});
|
|
252
|
+
service.saveLlmAudit({
|
|
253
|
+
timestamp: new Date().toISOString(),
|
|
254
|
+
trigger_source: "autoDream", operation_type: "dream_summarize",
|
|
255
|
+
model_id: "m", input_tokens: 30, output_tokens: 10, duration_ms: 8, status: "success"
|
|
256
|
+
});
|
|
257
|
+
service.saveLlmAudit({
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
trigger_source: "autoSummarize", operation_type: "summarize_compress",
|
|
260
|
+
model_id: "m", input_tokens: 5, output_tokens: 2, duration_ms: 4, status: "error"
|
|
261
|
+
});
|
|
262
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/semantic/llm-audit/stats");
|
|
263
|
+
const res = new FakeRes();
|
|
264
|
+
await route.handler(req("/api/dsh-mneme/semantic/llm-audit/stats?days=7"), res);
|
|
265
|
+
const stats = JSON.parse(res.body);
|
|
266
|
+
assert.equal(res.statusCode, 200);
|
|
267
|
+
assert.equal(stats.days, 7);
|
|
268
|
+
assert.equal(stats.total_calls, 3);
|
|
269
|
+
assert.equal(stats.input_tokens, 135);
|
|
270
|
+
assert.equal(stats.output_tokens, 52);
|
|
271
|
+
assert.equal(stats.total_tokens, 187);
|
|
272
|
+
assert.equal(stats.total_duration_ms, 37);
|
|
273
|
+
const bySource = stats.by_source.find((s) => s.source === "autoDream");
|
|
274
|
+
assert.equal(bySource.c, 2);
|
|
275
|
+
assert.equal(bySource.total_tokens, 180);
|
|
276
|
+
const errStatus = stats.by_status.find((s) => s.status === "error");
|
|
277
|
+
assert.equal(errStatus.c, 1);
|
|
278
|
+
store.close();
|
|
279
|
+
});
|