@modusensus/dsh-mneme 0.8.6 → 0.8.8
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 +19 -148
- package/bin/cli.mjs +689 -603
- package/lib/api-standalone.js +57 -1
- package/lib/api.js +34 -11
- package/lib/client.js +349 -61
- package/lib/config.js +118 -7
- package/lib/content-hash.js +54 -0
- package/lib/continuity.js +220 -0
- package/lib/document-index.js +94 -0
- package/lib/document.js +93 -11
- package/lib/dream.js +138 -39
- package/lib/guide.js +18 -0
- package/lib/index.js +87 -12
- package/lib/inject.js +6 -2
- package/lib/lang.js +15 -4
- package/lib/maintenance.js +142 -0
- package/lib/mirror.js +147 -15
- package/lib/peak-hours.js +117 -0
- package/lib/recall-stats.js +52 -2
- package/lib/scope.js +34 -6
- package/lib/service.js +86 -7
- package/lib/settings.js +16 -2
- package/lib/store.js +318 -21
- package/lib/summarize.js +52 -113
- package/lib/tools.js +37 -11
- package/lib/write-admission.js +259 -0
- package/package.json +4 -4
- package/src/api-standalone.js +57 -1
- package/src/api.js +34 -11
- package/src/config.js +118 -7
- package/src/content-hash.js +54 -0
- package/src/continuity.js +220 -0
- package/src/document-index.js +94 -0
- package/src/document.js +93 -11
- package/src/dream.js +138 -39
- package/src/guide.js +18 -0
- package/src/index.js +87 -12
- package/src/inject.js +6 -2
- package/src/lang.js +15 -4
- package/src/maintenance.js +142 -0
- package/src/mirror.js +147 -15
- package/src/peak-hours.js +117 -0
- package/src/recall-stats.js +52 -2
- package/src/scope.js +34 -6
- package/src/service.js +86 -7
- package/src/settings.js +16 -2
- package/src/store.js +318 -21
- package/src/summarize.js +52 -113
- package/src/tools.js +37 -11
- package/src/write-admission.js +259 -0
- package/test/api.test.js +21 -3
- package/test/client.test.js +103 -6
- package/test/content-hash.test.js +204 -0
- package/test/continuity.test.js +333 -0
- package/test/document-dir.test.js +233 -0
- package/test/document.test.js +151 -3
- package/test/dream-failure-backoff.test.js +173 -0
- package/test/dream-peak-hours.test.js +194 -0
- package/test/inject-parent-gate.test.js +106 -0
- package/test/inject-pools.test.js +8 -3
- package/test/llm-audit.test.js +42 -0
- package/test/maintenance.test.js +391 -0
- package/test/mirror-types.test.js +208 -0
- package/test/reasoning-effort.test.js +6 -0
- package/test/recall-evals.test.js +18 -0
- package/test/recall-stats.test.js +66 -0
- package/test/summarize-reasoning-effort.test.js +189 -0
- package/test/write-admission.test.js +345 -0
package/lib/api-standalone.js
CHANGED
|
@@ -197,7 +197,7 @@ async function handlePutBody(res, service, logger, id, text) {
|
|
|
197
197
|
* port (updated to the OS-assigned one after `ready` resolves when asked to
|
|
198
198
|
* bind port 0), `ready` resolves once listening and rejects if the bind fails.
|
|
199
199
|
*/
|
|
200
|
-
export function createStandaloneApi({ service, store, config = {}, logger, settings, port, host }) {
|
|
200
|
+
export function createStandaloneApi({ service, store, config = {}, logger, settings, port, host, maintenance }) {
|
|
201
201
|
const persisted = settings?.getExternalApi?.() ?? {};
|
|
202
202
|
|
|
203
203
|
let token = typeof persisted.token === "string" ? persisted.token : "";
|
|
@@ -409,6 +409,62 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
|
|
|
409
409
|
return;
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
// --- POST /maintenance/reclaim: 无损回收(#275 第一批,手动入口)-------
|
|
413
|
+
// 默认 dry-run:只有显式 confirm:true 才真改数据。这两步都是不可逆的内容丢弃
|
|
414
|
+
// (历史 run 的输入快照置空、归档行向量置空),所以不给「带 body 就执行」这种
|
|
415
|
+
// 省事路径——确认必须建立在看得见的数字上。
|
|
416
|
+
if (req.method === "POST" && pathname === "/maintenance/reclaim") {
|
|
417
|
+
void readBody(req).then((text) => {
|
|
418
|
+
let body;
|
|
419
|
+
try {
|
|
420
|
+
body = JSON.parse(text || "{}");
|
|
421
|
+
} catch {
|
|
422
|
+
sendJson(res, 400, { error: "invalid-json" });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
426
|
+
sendJson(res, 400, { error: "invalid-body" });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (!maintenance) {
|
|
430
|
+
sendJson(res, 503, { error: "maintenance-unavailable" });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const dryRun = body.dryRun !== false;
|
|
434
|
+
// 只收 JSON 数字:`Number()` 会把 ""/[]/false 都化成 0,而 0 在这里是
|
|
435
|
+
// 「清掉全部快照」——最坏解释绝不能让一个畸形输入静默拿到。
|
|
436
|
+
const olderThanDays = body.olderThanDays === undefined || body.olderThanDays === null
|
|
437
|
+
? undefined
|
|
438
|
+
: body.olderThanDays;
|
|
439
|
+
if (olderThanDays !== undefined && (!Number.isInteger(olderThanDays) || olderThanDays < 0)) {
|
|
440
|
+
sendJson(res, 400, { error: "invalid-older-than-days" });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (body.vacuum !== undefined && typeof body.vacuum !== "boolean") {
|
|
444
|
+
sendJson(res, 400, { error: "invalid-vacuum" });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!dryRun && body.confirm !== true) {
|
|
448
|
+
// 拒执行,但把 dry-run 报告一并回给调用方:确认的依据就是这份数字。报告
|
|
449
|
+
// 必须描述**将要执行的那个动作**,所以 vacuum 原样带上——否则调用方看到
|
|
450
|
+
// 的是「不 VACUUM」的数字,却带着 vacuum 去执行。
|
|
451
|
+
sendJson(res, 400, {
|
|
452
|
+
error: "confirm-required",
|
|
453
|
+
hint: "先看 dry-run 报告,确认后带 {\"dryRun\":false,\"confirm\":true} 再发",
|
|
454
|
+
report: maintenance.reclaim({ olderThanDays, vacuum: body.vacuum === true, dryRun: true })
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
sendJson(res, 200, maintenance.reclaim({ olderThanDays, vacuum: body.vacuum === true, dryRun }));
|
|
460
|
+
} catch (error) {
|
|
461
|
+
logger?.warn?.(`[dsh-mneme] storage reclaim failed: ${String(error)}`);
|
|
462
|
+
sendJson(res, 500, { error: "internal" });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
412
468
|
// --- POST /bootstrap: cold-start memories from repo files (#220) -------
|
|
413
469
|
// 显式收 dir(插件没有工作区根目录概念,不猜路径);确定性解析零 LLM;
|
|
414
470
|
// 幂等骑 saveWithDedupe 的 (type, title, scope) 去重 + _overwrite 刷新。
|
package/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { URL } from "node:url";
|
|
2
2
|
import { timingSafeEqual, randomBytes } from "node:crypto";
|
|
3
3
|
import { FEATURE_FLAG_SPEC } from "./settings.js";
|
|
4
|
-
import { TYPE_FILE, renderMirrorText, parseHumanEdits } from "./mirror.js";
|
|
4
|
+
import { TYPE_FILE, renderMirrorText, renderFileHeader, parseHumanEdits, MIRROR_READONLY_TYPES } from "./mirror.js";
|
|
5
5
|
import { langOf } from "./lang.js";
|
|
6
6
|
import { computeHeat } from "./heat.js";
|
|
7
7
|
import { describeStreamFailure, resolveRoute } from "./dream.js";
|
|
@@ -788,7 +788,11 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
788
788
|
const page = Math.max(1, Number(url.searchParams.get("page") ?? 1) || 1);
|
|
789
789
|
const pageSize = Math.min(200, Math.max(1, Number(url.searchParams.get("pageSize") ?? 50) || 50));
|
|
790
790
|
const source = url.searchParams.get("source") ?? undefined;
|
|
791
|
-
|
|
791
|
+
// 写入准入(#254)的 session_key 是内部计数键——session_id 不是对外的归属
|
|
792
|
+
// 标识(见 scope.js 文件头),而本端点按注释在 apiToken 之下也保持开放,所以
|
|
793
|
+
// 不回传它。按会话离线聚合直接读库。
|
|
794
|
+
const items = (service.listLlmAudits?.({ limit: pageSize, offset: (page - 1) * pageSize, source }) ?? [])
|
|
795
|
+
.map((row) => (row.session_key === undefined ? row : { ...row, session_key: undefined }));
|
|
792
796
|
const total = service.countLlmAudits?.({ source }) ?? items.length;
|
|
793
797
|
sendJson(res, 200, { items, total, page, pageSize });
|
|
794
798
|
} catch {
|
|
@@ -973,9 +977,15 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
973
977
|
});
|
|
974
978
|
|
|
975
979
|
// --- 导出(面板备份/迁移)---------------------------------------------------
|
|
976
|
-
// 只读。json:全字段行(含 archived/forgotten,布尔化),updated_at DESC
|
|
977
|
-
// markdown
|
|
978
|
-
//
|
|
980
|
+
// 只读。json:全字段行(含 archived/forgotten,布尔化),updated_at DESC。
|
|
981
|
+
// markdown:一个文档 = 文档级 frontmatter + 按类型分节。frontmatter 只放文档
|
|
982
|
+
// 最前一份(分节不再各自带文件头,否则 9 个 `---` 块串在一个 .md 里,外部
|
|
983
|
+
// frontmatter 解析器只认第一段、其余降级成正文),它的 coverage: all 就是一句
|
|
984
|
+
// 看得见的声明:这份导出含 archived/forgotten(document 指针行除外——它两个
|
|
985
|
+
// 落点都不进,见 mirror.js 的 MIRROR_EXCLUDED_TYPES)。条目块与磁盘镜像同构(共用
|
|
986
|
+
// renderMirrorText 的条目渲染),所以导出文本能被 /import 原样吃回;「同构」
|
|
987
|
+
// 止于条目——磁盘镜像只写活跃集,导出的行集更宽,两者的覆盖声明因此不同
|
|
988
|
+
// (active-only vs all)。
|
|
979
989
|
// 全量一次性取回(store.all 按 updated_at DESC)——导出是一次性备份动作,
|
|
980
990
|
// 不需要流式。
|
|
981
991
|
register({
|
|
@@ -1002,13 +1012,21 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1002
1012
|
}
|
|
1003
1013
|
const byType = {};
|
|
1004
1014
|
for (const row of rows) {
|
|
1005
|
-
|
|
1015
|
+
// 没有 TYPE_FILE 条目的 type 不进 markdown 导出,只读 type(#296 的
|
|
1016
|
+
// document 指针行)同样不进:导出/导入是 round-trip 通道,只读视图没有
|
|
1017
|
+
// 可回填的正文。两个排除都在 mirror.js 那张表里(MIRROR_EXCLUDED_TYPES /
|
|
1018
|
+
// MIRROR_READONLY_TYPES)。json 分支是全字段导出,不受影响。
|
|
1019
|
+
if (TYPE_FILE[row.type] && !MIRROR_READONLY_TYPES.has(row.type)) (byType[row.type] ??= []).push(row);
|
|
1006
1020
|
}
|
|
1007
1021
|
const sections = [];
|
|
1008
1022
|
for (const type of Object.keys(TYPE_FILE)) {
|
|
1009
|
-
if (byType[type]?.length)
|
|
1023
|
+
if (byType[type]?.length) {
|
|
1024
|
+
sections.push(renderMirrorText(type, byType[type], langOf(config), { fileHeader: false }));
|
|
1025
|
+
}
|
|
1010
1026
|
}
|
|
1011
|
-
|
|
1027
|
+
const included = Object.values(byType).flat();
|
|
1028
|
+
const docHeader = renderFileHeader("memory-export", included, "all");
|
|
1029
|
+
sendAttachment(res, 200, "text/markdown; charset=utf-8", `dsh-mneme-export-${stamp}.md`, docHeader + sections.join("\n"));
|
|
1012
1030
|
} catch {
|
|
1013
1031
|
sendJson(res, 500, { error: "internal" });
|
|
1014
1032
|
}
|
|
@@ -1016,9 +1034,8 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1016
1034
|
});
|
|
1017
1035
|
|
|
1018
1036
|
// --- 导入(Markdown 镜像回填)----------------------------------------------
|
|
1019
|
-
// 写路径(requireAuth)。body {type, markdown}:type 是镜像 TYPE_FILE
|
|
1020
|
-
//
|
|
1021
|
-
// 的文本。解析复用 readHumanEdits 的纯函数核心 parseHumanEdits(同一实现,
|
|
1037
|
+
// 写路径(requireAuth)。body {type, markdown}:type 是镜像 TYPE_FILE 键,
|
|
1038
|
+
// markdown 是与镜像文件同构的文本。解析复用 readHumanEdits 的纯函数核心 parseHumanEdits(同一实现,
|
|
1022
1039
|
// 行为一致是硬约束),合并走 mergeHumanEdits(只吃 title/content;digest 命
|
|
1023
1040
|
// 中或无差异的条目在 service 侧自动跳过)。解析出 0 条不算错误。
|
|
1024
1041
|
register({
|
|
@@ -1037,6 +1054,12 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1037
1054
|
sendJson(res, 400, { error: "invalid-type" });
|
|
1038
1055
|
return;
|
|
1039
1056
|
}
|
|
1057
|
+
// 只读 type 是合法 type,只是没有可回填的正文(#296):单独的码,调用方
|
|
1058
|
+
// 不会被误导去改 type 名重试。
|
|
1059
|
+
if (MIRROR_READONLY_TYPES.has(body.type)) {
|
|
1060
|
+
sendJson(res, 400, { error: "readonly-type" });
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1040
1063
|
if (typeof body.markdown !== "string" || !body.markdown.trim()) {
|
|
1041
1064
|
sendJson(res, 400, { error: "invalid-markdown" });
|
|
1042
1065
|
return;
|