@modusensus/dsh-mneme 0.9.4 → 0.10.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 +57 -10
- package/bin/cli.mjs +215 -2
- package/docs/CONFIGURATION.md +25 -2
- package/docs/DAEMON.md +13 -2
- package/docs/DISTILL.md +150 -4
- package/docs/SHARE.md +552 -0
- package/docs/SLEEP.md +21 -7
- package/docs/STORAGE.md +2 -1
- package/lib/api-standalone.js +294 -8
- package/lib/api.js +217 -3
- package/lib/client.js +919 -39
- package/lib/config.js +33 -1
- package/lib/continuity.js +11 -3
- package/lib/document.js +15 -2
- package/lib/dream/decisions.js +176 -30
- package/lib/dream/sleep.js +530 -94
- package/lib/dream.js +174 -21
- package/lib/fence.js +24 -0
- package/lib/index.js +15 -6
- package/lib/inject.js +9 -4
- package/lib/lang.js +38 -16
- package/lib/memory-source.js +9 -0
- package/lib/organize.js +5 -1
- package/lib/pipeline-nudge-auto.js +3 -0
- package/lib/pipeline-nudge.js +41 -8
- package/lib/pipeline-runner.js +15 -0
- package/lib/pipeline.js +62 -1
- package/lib/pre-inject-gate.js +14 -0
- package/lib/profile-extract.js +4 -1
- package/lib/provenance-http.js +251 -0
- package/lib/serve.js +13 -0
- package/lib/service.js +97 -4
- package/lib/session-gate.js +91 -0
- package/lib/settings.js +13 -1
- package/lib/share-http.js +281 -0
- package/lib/share-import.js +234 -0
- package/lib/share.js +335 -0
- package/lib/store.js +797 -28
- package/lib/summarize.js +209 -23
- package/lib/tools.js +26 -6
- package/package.json +4 -2
- package/src/api-standalone.js +294 -8
- package/src/api.js +217 -3
- package/src/config.js +33 -1
- package/src/continuity.js +11 -3
- package/src/document.js +15 -2
- package/src/dream/decisions.js +176 -30
- package/src/dream/sleep.js +530 -94
- package/src/dream.js +174 -21
- package/src/fence.js +24 -0
- package/src/index.js +15 -6
- package/src/inject.js +9 -4
- package/src/lang.js +38 -16
- package/src/memory-source.js +9 -0
- package/src/organize.js +5 -1
- package/src/pipeline-nudge-auto.js +3 -0
- package/src/pipeline-nudge.js +41 -8
- package/src/pipeline-runner.js +15 -0
- package/src/pipeline.js +62 -1
- package/src/pre-inject-gate.js +14 -0
- package/src/profile-extract.js +4 -1
- package/src/provenance-http.js +251 -0
- package/src/serve.js +13 -0
- package/src/service.js +97 -4
- package/src/session-gate.js +91 -0
- package/src/settings.js +13 -1
- package/src/share-http.js +281 -0
- package/src/share-import.js +234 -0
- package/src/share.js +335 -0
- package/src/store.js +797 -28
- package/src/summarize.js +209 -23
- package/src/tools.js +26 -6
- package/test/api.test.js +523 -6
- package/test/audit.test.js +2 -2
- package/test/cli-receive.test.js +390 -0
- package/test/cli-share.test.js +437 -0
- package/test/client-share.test.js +225 -0
- package/test/client.test.js +354 -0
- package/test/config.test.js +8 -0
- package/test/conflict-reviewed.test.js +79 -0
- package/test/continuity.test.js +29 -1
- package/test/dedupe-candidate-window.test.js +180 -0
- package/test/dependency-overrides.test.js +78 -0
- package/test/differentiate-feedback.test.js +248 -0
- package/test/dream-candidate-hybrid.test.js +2 -2
- package/test/dream-decision-evidence.test.js +314 -0
- package/test/dream-fence.test.js +207 -0
- package/test/dream.test.js +87 -5
- package/test/helpers/dream-mock.js +6 -2
- package/test/inject-pools.test.js +2 -1
- package/test/llm-message-source.test.js +1 -1
- package/test/mint-source.test.js +84 -1
- package/test/pipeline-nudge-auto.test.js +26 -2
- package/test/pipeline-nudge.test.js +87 -0
- package/test/pipeline-r1r2.test.js +129 -0
- package/test/pipeline-runner.test.js +26 -0
- package/test/policy-epoch.test.js +2 -2
- package/test/prompt-stance.test.js +128 -0
- package/test/provenance-http.test.js +367 -0
- package/test/readme-claims.test.js +45 -0
- package/test/reasoning-effort.test.js +10 -10
- package/test/release-docs.test.js +7 -1
- package/test/scope-candidates.test.js +7 -4
- package/test/scope-state.test.js +130 -1
- package/test/security-doc.test.js +43 -4
- package/test/serve.test.js +62 -0
- package/test/session-gate.test.js +264 -0
- package/test/settings.test.js +4 -0
- package/test/share-foreign-rows.test.js +144 -0
- package/test/share-http.test.js +144 -0
- package/test/share-import.test.js +577 -0
- package/test/share-imported-guard.test.js +311 -0
- package/test/share-imported-identity.test.js +111 -0
- package/test/share-pack.test.js +511 -0
- package/test/share-receive-http.test.js +756 -0
- package/test/share-routes.test.js +337 -0
- package/test/share-store.test.js +133 -0
- package/test/share-validate.test.js +101 -0
- package/test/sleep-action-set.test.js +16 -11
- package/test/sleep-catchup.test.js +315 -0
- package/test/sleep-failure-backoff.test.js +156 -0
- package/test/sleep.test.js +293 -0
- package/test/standalone-api.test.js +574 -5
- package/test/store.test.js +220 -0
- package/test/summarize-parse-diagnostics.test.js +75 -0
- package/test/summarize.test.js +187 -2
package/src/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { URL } from "node:url";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
3
|
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { timingSafeEqual, randomBytes } from "node:crypto";
|
|
5
6
|
import { FEATURE_FLAG_SPEC } from "./settings.js";
|
|
6
7
|
import { TYPES } from "./store.js";
|
|
@@ -8,12 +9,17 @@ import { TYPE_FILE, renderMirrorText, renderFileHeader, parseHumanEdits, MIRROR_
|
|
|
8
9
|
import { langOf } from "./lang.js";
|
|
9
10
|
import { computeHeat } from "./heat.js";
|
|
10
11
|
import { EFFORT_REJECT_RE, describeStreamFailure, resolveRoute, withEffortFallback } from "./dream.js";
|
|
12
|
+
// Issue #521:面板要按归一化原因显示 skipped,归一化只有一份实现(住在
|
|
13
|
+
// decisions.js,与产生这些 error 的校验器同一文件,避免措辞漂移)。
|
|
14
|
+
import { summarizeSkipped } from "./dream/decisions.js";
|
|
11
15
|
import { getInjectionSnapshot } from "./inject.js";
|
|
12
16
|
import { renderUserSettingsBlock } from "./user-settings.js";
|
|
17
|
+
import { parseProvenanceSelector, provenanceReply } from "./provenance-http.js";
|
|
13
18
|
import { describeLocalRuntime, publicRuntimeStatus } from "./runtime/loader.js";
|
|
14
19
|
import { hostModulesDir, provisionRuntime } from "./runtime/provision.js";
|
|
15
20
|
import { classify, fetchLatestVersion, PACKAGE_VERSION } from "./version-check.js";
|
|
16
21
|
import { parseReclaimQuery, parseReclaimJson, readReclaimFields, reclaimReply, RECLAIM_UNAVAILABLE } from "./reclaim-http.js";
|
|
22
|
+
import { parseShareBody, shareReply, shareFilename, parseReceiveBody, receiveReply, parseUndoBody, undoReply } from "./share-http.js";
|
|
17
23
|
import { readPipelineDoc, loadPipelines, projectPipelineDir } from "./pipeline.js";
|
|
18
24
|
import { loadRunsById, runsDir, RUNS_SUFFIX } from "./pipeline-runs.js";
|
|
19
25
|
import { observationDir, loadObservationsById } from "./pipeline-observations.js";
|
|
@@ -387,6 +393,15 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
387
393
|
...m,
|
|
388
394
|
archived: rows[i].archived === true || rows[i].archived === 1,
|
|
389
395
|
quality_score: rows[i].quality_score ?? null,
|
|
396
|
+
// 外来区的包号:抽屉里的「收起这个包」按 pack_id 整批删,寻址键必须由行自己带上。
|
|
397
|
+
// 与 heat 同一个做法——**缺省就不带键**,面板的撤销按钮因此天然自门控。
|
|
398
|
+
// /search 那一面为什么没有这一格,别说错原因:召回侧的候选行内存里带着
|
|
399
|
+
// share_pack_id(searchMemories 只是把 source 盖成渠道名),toApiList 也从不透出这一列;
|
|
400
|
+
// 真正的事实是这层扩展只长在 /list 的答复上,搜索面没有一处代码去补它。所以搜索态
|
|
401
|
+
// 按不了撤销,那条代价写在 docs/SHARE.md 的车道 D 一节。
|
|
402
|
+
// 扩展点留在 HTTP 层而不是 toApiList:模型工具严格复用后者(src/service.js:2094 与
|
|
403
|
+
// MEMORY_ITEM_SCHEMA),给它们加一格没有第二个消费者,却要多改一处 schema 声明。
|
|
404
|
+
...(rows[i].share_pack_id ? { share_pack_id: rows[i].share_pack_id } : {}),
|
|
390
405
|
...(heatOn ? { heat: computeHeat(rows[i], Date.now(), config ?? {}) } : {})
|
|
391
406
|
}));
|
|
392
407
|
// Total honors the same filters as the rows, or the pager's
|
|
@@ -1293,6 +1308,62 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1293
1308
|
}
|
|
1294
1309
|
});
|
|
1295
1310
|
|
|
1311
|
+
// --- 记忆级溯源读出口(#399 写、#421 读)-----------------------------------
|
|
1312
|
+
// #399 把蒸馏窗口的原文落进 distill_records、把 memory → (session, seq 区间)
|
|
1313
|
+
// 的归因写进 memory_provenance,但只做了写:库里有答案、service.provenanceFor
|
|
1314
|
+
// 能查,外部(面板 / standalone API / 脚本)拿不到,「这条记忆是从哪几句蒸出来
|
|
1315
|
+
// 的」只能开 sqlite 手写 JOIN。本端点补最后一段接线。
|
|
1316
|
+
//
|
|
1317
|
+
// 两个读方向共一条路径(#524):?id=<memoryId> 是「这条记忆从哪几句蒸出来的」,
|
|
1318
|
+
// ?sessionId=<sid> 是「这个会话蒸出了哪几段」。两个主键互斥——都缺 400、都给
|
|
1319
|
+
// 400(择一意味着调用方发一个自己也不知道会被读哪个主键的请求,而两边结果形状
|
|
1320
|
+
// 不同)。共用路径是有意的:多一条路径就多一处要登记的地方(SECURITY.md 两张
|
|
1321
|
+
// 表、README 路由表、security-doc 的清单锁),而两者读的是同一批数据。
|
|
1322
|
+
//
|
|
1323
|
+
// 一个链接一个窗口:区间 + 区间里的原文行(按 seq 升序)。没有链接的记忆
|
|
1324
|
+
// (memory_save / 工具写入 / 冷启动)返回空 windows——「不是蒸馏来的」本身是
|
|
1325
|
+
// 有效答案,不是错误,所以不 404。
|
|
1326
|
+
//
|
|
1327
|
+
// policy 是 **token**,不是 open(#509 维护者评审定):records 带的是蒸馏窗口的
|
|
1328
|
+
// 逐条会话原文,而 /list 是 open 且逐条带 id——两者串起来等于「先枚举 id、再按 id
|
|
1329
|
+
// 捞回原文」,可达结果与 /export(一次拿全库,本仓 2026-10 审计后列 gated)同量级,
|
|
1330
|
+
// 只是换了形状。抽稀不该靠「调用方不知道有这条路」。
|
|
1331
|
+
//
|
|
1332
|
+
// 口径只在 src/provenance-http.js 写一份,两个入口都调它(#411 的教训:两张皮
|
|
1333
|
+
// 整段复制必然漂移)。
|
|
1334
|
+
//
|
|
1335
|
+
// distillRecords.enabled 门(#509 评审整改):这个读出口吃的是**写侧同一枚开关、
|
|
1336
|
+
// 同一个判据**(isDistillRecordsEnabled,与 src/summarize.js:988 逐字同源;本文件
|
|
1337
|
+
// 不重写那个表达式,静态锁盯着)。关着时回 409,错误码取共用模块导出的
|
|
1338
|
+
// DISTILL_RECORDS_DISABLED——「用户关了」而不是「装配坏了」(后者是 503,
|
|
1339
|
+
// src/index.js:589-591 立的可区分性原则),也不是 404(本端点刻意不就「这条记忆
|
|
1340
|
+
// 是否存在」作断言)。cfg 是启动期冻结值,所以面板改开关要到下次重启才对这里生效;
|
|
1341
|
+
// 该键在 settings 白名单与 NESTED_FLAG_PATHS 里都登记着,面板会显示「待重启」
|
|
1342
|
+
// (#481 的教训)。阶梯全貌见 provenance-http.js 文件头。
|
|
1343
|
+
register({
|
|
1344
|
+
kind: "exact",
|
|
1345
|
+
path: "/api/dsh-mneme/memory/provenance",
|
|
1346
|
+
policy: "token",
|
|
1347
|
+
handler(req, res) {
|
|
1348
|
+
try {
|
|
1349
|
+
const url = new URL(req.url, "http://localhost");
|
|
1350
|
+
const parsed = parseProvenanceSelector({
|
|
1351
|
+
memoryId: url.searchParams.get("id"),
|
|
1352
|
+
sessionId: url.searchParams.get("sessionId"),
|
|
1353
|
+
limit: url.searchParams.get("limit")
|
|
1354
|
+
});
|
|
1355
|
+
if (!parsed.ok) {
|
|
1356
|
+
sendJson(res, parsed.status, { error: parsed.error });
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const { status, body } = provenanceReply({ service, selector: parsed.selector, config, warn: (m) => ctx.logger?.warn?.(m) });
|
|
1360
|
+
sendJson(res, status, body);
|
|
1361
|
+
} catch {
|
|
1362
|
+
sendJson(res, 500, { error: "internal" });
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1296
1367
|
// --- ego graph: 1-2 hop neighborhood of one entity (graph panel P1) ---
|
|
1297
1368
|
// Read-only like list/search/semantic, so it stays open when apiToken is set.
|
|
1298
1369
|
// BFS from the root entity over entity_relations (both directions; the
|
|
@@ -1518,6 +1589,133 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1518
1589
|
}
|
|
1519
1590
|
});
|
|
1520
1591
|
|
|
1592
|
+
// --- 分享包(spec §4)------------------------------------------------------
|
|
1593
|
+
// 读侧全库口径,鉴权与 /export 同门(token):它导出的是**脱敏后**的行集,但
|
|
1594
|
+
// 「谁在往外打整库」这件事本身就需要跟写路由一样登记在册。
|
|
1595
|
+
// GET 不开口子:这条路由的存在感来自「有人主动按分享键」,不是给面板轮询用的。
|
|
1596
|
+
// 判据一条都不在这里:哪些行进包、超没超限、抛错回什么码,全在 share-http.js 那份
|
|
1597
|
+
// 共用实现里(#411 的口径——两张皮只许剩鉴权与路径不同)。本侧独有的只有两件事:
|
|
1598
|
+
// Content-Disposition(sendJson 的 headers 第四参,数据面没有这个签名)和把
|
|
1599
|
+
// shareReply 的 warn 接到宿主 logger 上——它兜住抛错回 500,不接日志就是静默失败。
|
|
1600
|
+
register({
|
|
1601
|
+
kind: "exact",
|
|
1602
|
+
path: "/api/dsh-mneme/share",
|
|
1603
|
+
policy: "token",
|
|
1604
|
+
handler(req, res) {
|
|
1605
|
+
try {
|
|
1606
|
+
if (req.method !== "POST") {
|
|
1607
|
+
sendJson(res, 405, { error: "method-not-allowed" });
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
return readBody(req, res).then((text) => {
|
|
1611
|
+
if (text === null) return; // readBody 已经答过 413,再写一次就是双写响应
|
|
1612
|
+
const parsed = parseShareBody(text);
|
|
1613
|
+
if (!parsed.ok) {
|
|
1614
|
+
sendJson(res, 400, { error: parsed.error });
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const out = shareReply({
|
|
1618
|
+
service,
|
|
1619
|
+
packageVersion: PACKAGE_VERSION,
|
|
1620
|
+
homeDir: homedir(),
|
|
1621
|
+
options: parsed.options,
|
|
1622
|
+
warn: (msg) => ctx.logger?.warn?.(msg)
|
|
1623
|
+
});
|
|
1624
|
+
if (out.status === 200) {
|
|
1625
|
+
sendJson(res, 200, out.body, {
|
|
1626
|
+
"Content-Disposition": `attachment; filename="${shareFilename(out.body.pack_id)}"`
|
|
1627
|
+
});
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
sendJson(res, out.status, out.body);
|
|
1631
|
+
}).catch(() => sendJson(res, 500, { error: "internal" }));
|
|
1632
|
+
// .catch 与下面的外层 try 各挡一类:外层那个只包得住同步那半,而 readBody 的回调
|
|
1633
|
+
// 抛出时它早已退栈。共用层从不抛(连 warn 自己坏都被守卫吞掉),所以这条链现在能被
|
|
1634
|
+
// 走到的只剩**面自己**写答复那一步——sendJson 的 writeHead 在真 socket 上会因
|
|
1635
|
+
// 「一个响应两写」抛 ERR_HTTP_HEADERS_SENT。少这一声就是 unhandled rejection +
|
|
1636
|
+
// 一个字节都没回(#410),而下面 /receive 两条一直带着它。
|
|
1637
|
+
} catch {
|
|
1638
|
+
sendJson(res, 500, { error: "internal" });
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
// --- 分享包入库与整包退出(车道 C,spec §5)--------------------------------
|
|
1644
|
+
// 写库的一面(token-write:非 GET 要 Bearer)。判据一条都不在这里:包体合格不合格住在
|
|
1645
|
+
// share.js 的闸门(由 service 的 importSharePack 调),错误码 → 状态的映射住在
|
|
1646
|
+
// share-http.js 那份共用实现里——#411 的口径是两张皮只许剩鉴权与路径不同。本侧独有的
|
|
1647
|
+
// 只有把 warn 接到宿主 logger 上(不接就是一条静默的 500,现场只剩 internal)。
|
|
1648
|
+
//
|
|
1649
|
+
// 非 POST 一律 405。这**故意**与上面的 /maintenance/reclaim 不同(那边掉到末尾 404):
|
|
1650
|
+
// 每条 receive 都有一个数据面双胞胎(C6 的 POST /receive、/receive/undo),那里只认
|
|
1651
|
+
// 405,状态码差一次就是第三处差别——一致性在这里比「照抄本文件的邻居」更硬。
|
|
1652
|
+
//
|
|
1653
|
+
// 外层 try 与 .catch 都要留着:外层那个在 .then 的回调里已经退栈,回调里抛出去就是没人接
|
|
1654
|
+
// 的 rejection,客户端拿到的不是 500 而是「一个字节都没回」(#410)。
|
|
1655
|
+
register({
|
|
1656
|
+
kind: "exact",
|
|
1657
|
+
path: "/api/dsh-mneme/receive",
|
|
1658
|
+
policy: "token-write",
|
|
1659
|
+
handler(req, res) {
|
|
1660
|
+
try {
|
|
1661
|
+
if (req.method !== "POST") {
|
|
1662
|
+
sendJson(res, 405, { error: "method-not-allowed" });
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
return readBody(req, res).then((text) => {
|
|
1666
|
+
if (text === null) return; // readBody 已经答过 413,再写一次就是双写响应
|
|
1667
|
+
const parsed = parseReceiveBody(text);
|
|
1668
|
+
if (!parsed.ok) {
|
|
1669
|
+
sendJson(res, 400, { error: parsed.error });
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
const out = receiveReply({
|
|
1673
|
+
service,
|
|
1674
|
+
pack: parsed.pack,
|
|
1675
|
+
dryRun: parsed.dryRun,
|
|
1676
|
+
warn: (msg) => ctx.logger?.warn?.(msg)
|
|
1677
|
+
});
|
|
1678
|
+
sendJson(res, out.status, out.body);
|
|
1679
|
+
}).catch(() => sendJson(res, 500, { error: "internal" }));
|
|
1680
|
+
} catch {
|
|
1681
|
+
sendJson(res, 500, { error: "internal" });
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
// 退出按 pack_id 收起一整包(物删不归档,理由见 src/share-import.js)。dry-run 同样是
|
|
1687
|
+
// 缺省:没带 confirm 就只报「会删掉几条」,与 reclaim、与入库那侧同一个脾气。
|
|
1688
|
+
register({
|
|
1689
|
+
kind: "exact",
|
|
1690
|
+
path: "/api/dsh-mneme/receive/undo",
|
|
1691
|
+
policy: "token-write",
|
|
1692
|
+
handler(req, res) {
|
|
1693
|
+
try {
|
|
1694
|
+
if (req.method !== "POST") {
|
|
1695
|
+
sendJson(res, 405, { error: "method-not-allowed" });
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
return readBody(req, res).then((text) => {
|
|
1699
|
+
if (text === null) return;
|
|
1700
|
+
const parsed = parseUndoBody(text);
|
|
1701
|
+
if (!parsed.ok) {
|
|
1702
|
+
sendJson(res, 400, { error: parsed.error });
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const out = undoReply({
|
|
1706
|
+
service,
|
|
1707
|
+
packId: parsed.packId,
|
|
1708
|
+
dryRun: parsed.dryRun,
|
|
1709
|
+
warn: (msg) => ctx.logger?.warn?.(msg)
|
|
1710
|
+
});
|
|
1711
|
+
sendJson(res, out.status, out.body);
|
|
1712
|
+
}).catch(() => sendJson(res, 500, { error: "internal" }));
|
|
1713
|
+
} catch {
|
|
1714
|
+
sendJson(res, 500, { error: "internal" });
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1521
1719
|
// --- 导入(Markdown 镜像回填)----------------------------------------------
|
|
1522
1720
|
// 写路径(requireAuth)。body {type, markdown}:type 是镜像 TYPE_FILE 键,
|
|
1523
1721
|
// markdown 是与镜像文件同构的文本。解析复用 readHumanEdits 的纯函数核心 parseHumanEdits(同一实现,
|
|
@@ -1571,7 +1769,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1571
1769
|
policy: "open",
|
|
1572
1770
|
handler(req, res) {
|
|
1573
1771
|
try {
|
|
1574
|
-
const
|
|
1772
|
+
const toRun = (r) => ({
|
|
1575
1773
|
created_at: r.created_at,
|
|
1576
1774
|
status: r.status,
|
|
1577
1775
|
provider: r.provider ?? null,
|
|
@@ -1582,8 +1780,23 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1582
1780
|
// 数据来自 runSleep 写入的 decisions.demotion({demoted,archived} 数组)。
|
|
1583
1781
|
demotion: r.decisions?.demotion
|
|
1584
1782
|
? { demoted: (r.decisions.demotion.demoted ?? []).length, archived: (r.decisions.demotion.archived ?? []).length }
|
|
1585
|
-
: null
|
|
1586
|
-
|
|
1783
|
+
: null,
|
|
1784
|
+
// Issue #521:degraded 轮的 skipped 明细按归一化原因聚合。status:
|
|
1785
|
+
// degraded + applied: 3 从外面看完全是健康轮的样子,不展开 skipped
|
|
1786
|
+
// 看不出「一整个 action 类型 100% 失败」——实测那个 bug 跑了一整天才
|
|
1787
|
+
// 被发现。这里只回聚合(原因 + 动作 + 条数),明细仍留在库里。
|
|
1788
|
+
skippedCount: Array.isArray(r.skipped) ? r.skipped.length : 0,
|
|
1789
|
+
skippedReasons: summarizeSkipped(r.skipped)
|
|
1790
|
+
});
|
|
1791
|
+
// Issue #495:organize 上线后 runs 会混进 run_type="organize",而面板那张
|
|
1792
|
+
// 卡的标题是「最近巩固」——一次 agent 主动整理就会把它顶成整理回执。
|
|
1793
|
+
// 两条路里选了「标注」而不是过滤:runs 仍是全类型 5 条(审计面是证据,
|
|
1794
|
+
// 不因显示面积省略),另给一个只含 auto/sleep 的 lastConsolidation 供那张
|
|
1795
|
+
// 卡取数;面板再用 lastRun 是否为 organize 决定要不要标一句脚注。
|
|
1796
|
+
// 这里不能只靠 runs[0] 的 run_type 过滤——一次整理写两行(dryRun 报告 +
|
|
1797
|
+
// apply 回执),挤空 5 个名额时 runs 里可能一条巩固都没有。
|
|
1798
|
+
const runs = (service.listDreamRuns?.({ limit: 5 }) ?? []).map(toRun);
|
|
1799
|
+
const lastConsolidation = service.listDreamRuns?.({ limit: 1, excludeRunType: "organize" })?.[0];
|
|
1587
1800
|
const pendingConflicts = service.countConflictPending?.() ?? 0;
|
|
1588
1801
|
const ids = new Set();
|
|
1589
1802
|
for (const row of service.listConflictPending?.({ limit: 200, includeResolved: false }) ?? []) {
|
|
@@ -1594,6 +1807,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1594
1807
|
}
|
|
1595
1808
|
sendJson(res, 200, {
|
|
1596
1809
|
lastRun: runs[0] ?? null,
|
|
1810
|
+
lastConsolidation: lastConsolidation ? toRun(lastConsolidation) : null,
|
|
1597
1811
|
runs,
|
|
1598
1812
|
pendingConflicts,
|
|
1599
1813
|
pendingMemoryIds: [...ids].slice(0, 200)
|
package/src/config.js
CHANGED
|
@@ -586,6 +586,16 @@ export const Config = z.object({
|
|
|
586
586
|
// Minimum gap between two sleep runs (hours) — a second idle window within
|
|
587
587
|
// this interval does not retrigger.
|
|
588
588
|
sleepMinIntervalHours: z.natural().min(1).max(168).default(8),
|
|
589
|
+
// Issue #529(C 项的替代实现):sleep 连续失败退避(默认关 = 行为逐字节不变)。
|
|
590
|
+
// 基数是 sleepMinIntervalHours,有效间隔 = 基数 × 2^连续失败数(成功清零),
|
|
591
|
+
// 封顶 max(基数, 24 小时)——不是硬 24 小时:这个键允许配到 168,用户配 48 就是
|
|
592
|
+
// 明说「两天跑一次」,退避只能放宽间隔、不该把用户自己的基数压小。
|
|
593
|
+
// 为什么不按 issue 原案用 snapshot_hash 判据:sleep 的
|
|
594
|
+
// snapshot_hash 是**跑完之后**对 phases 结果算的哈希(dream/sleep.js 的
|
|
595
|
+
// createHash(JSON.stringify(phases))),不是输入快照;同一批输入跑两次结果不同
|
|
596
|
+
// 就会得到不同哈希,用它当「同一输入又在失败」的判据不成立。退避与 #292 的
|
|
597
|
+
// autoDreamFailureBackoff 同构:只放大既有的最小间隔闸,不新增状态面。
|
|
598
|
+
sleepFailureBackoff: z.boolean().default(false),
|
|
589
599
|
// Conflict adjudication strictness:
|
|
590
600
|
// gentle only high-confidence conflicts (threshold 0.92) are resolved
|
|
591
601
|
// normal standard dream-level (threshold 0.85)
|
|
@@ -653,6 +663,22 @@ export const Config = z.object({
|
|
|
653
663
|
// 增长);流式计费按实际用量,不按上限。
|
|
654
664
|
sleepMaxTokens: z.natural().min(256).max(131072).default(8192).description("sleep 冲突消解与模式发现阶段的 LLM 输出预算上限(token)。原为硬编码 2048,sleepActionSet=full 实测需约 7000 导致裁决被截断而整轮失败;流式计费按实际用量,调大不增加成本。"),
|
|
655
665
|
|
|
666
|
+
// Issue #529:冲突阶段此前没有任何输入侧预算——同 scope 的全部相似对被一次性
|
|
667
|
+
// 拼进一个 prompt,实测单请求 163,054 输入 token(服务端 --ctx-size 163,840,
|
|
668
|
+
// 占 99.5%),449 对的池子必然撞墙;而失败的轮次不落 pairs、判 keep 又是
|
|
669
|
+
// no-op,下一轮冷却结束再送**完全相同的输入**,于是 23 轮连续 llm failed。
|
|
670
|
+
// 输出侧早有 sleepMaxTokens(#257),输入侧这一半一直是空的。
|
|
671
|
+
//
|
|
672
|
+
// 每批上限:0 = 不设上限(严格回退到旧的一次性全送,用于排查"是不是分批
|
|
673
|
+
// 引入的差异")。取 12 是因为实测单对约 112~168 输入 token、12 对落在
|
|
674
|
+
// 1.5K~2K 区间,远低于任何服务商的上下文窗口,也不至于让 LLM 在长清单里
|
|
675
|
+
// 走神。
|
|
676
|
+
sleepConflictMaxPairs: z.natural().min(0).max(500).default(12).description("sleep 冲突阶段每批送给 LLM 的候选对上限(0 = 不设上限,回退旧的一次性全送)。实测单对约 112~168 输入 token,默认 12 对约 1.5K~2K。"),
|
|
677
|
+
// 每轮总上限:分批之后仍要有个总闸,否则一个 449 对的池子会拆成 38 批、
|
|
678
|
+
// 每批一次 LLM 往返,单轮跑上几小时(本身也没比撞墙好)。0 = 不设上限。
|
|
679
|
+
// 未入选的对不是"被丢弃"——它们留在 conflict_pending 与下一轮的候选池里。
|
|
680
|
+
sleepConflictMaxPairsPerRun: z.natural().min(0).max(5000).default(60).description("sleep 冲突阶段单轮累计裁定的候选对上限(0 = 不设上限)。默认 60 对 ≈ 5 批,约 10K 输入 token。"),
|
|
681
|
+
|
|
656
682
|
// --- epistemic trust: memory source credibility (v0.4.5) -----------------
|
|
657
683
|
// Distinguish memories by source: observation (measured / witnessed),
|
|
658
684
|
// subjective (opinion / guess) and inferred (derived from other evidence).
|
|
@@ -966,7 +992,13 @@ export const Config = z.object({
|
|
|
966
992
|
// 显式保存,画像正文必须是用户手写的原文,不给模型改写权。
|
|
967
993
|
// 进 LIGHT_MODE_OFF:一次调用要读上百行记忆 + 一轮模型生成,与低资源档互斥(同
|
|
968
994
|
// preInjectGate.enabled 的定性);是预设不是强制,用户显式勾选仍然赢。
|
|
969
|
-
profileExtractEnabled: z.boolean().default(false)
|
|
995
|
+
profileExtractEnabled: z.boolean().default(false),
|
|
996
|
+
|
|
997
|
+
// #539 逐会话无痕:总开关默认关,保持旧用户行为;子代理选择器默认开,但总开关
|
|
998
|
+
// 未开时不生效。手动名单是精确的 session id 匹配。
|
|
999
|
+
sessionGateEnabled: z.boolean().default(false),
|
|
1000
|
+
sessionGateSubagents: z.boolean().default(true),
|
|
1001
|
+
sessionGateSessions: z.array(z.string()).default([])
|
|
970
1002
|
});
|
|
971
1003
|
|
|
972
1004
|
// Fields forced to false by the light-mode preset. Everything not listed here
|
package/src/continuity.js
CHANGED
|
@@ -173,10 +173,10 @@ function noticeAlreadyInSurface(session, text) {
|
|
|
173
173
|
* 挂两条监听(#249 §7 的 `agent/pre-step` 触发位 + 压缩事件面),返回 { dispose }。
|
|
174
174
|
*
|
|
175
175
|
* `store` 直接注入:本模块只写提案表、不碰检索/嵌入,没有用到 service 的任何能力。
|
|
176
|
-
*
|
|
177
|
-
*
|
|
176
|
+
* 调用方负责父/子开关(`autoInject` + `continuityRescueEnabled`),本函数另检查逐会话
|
|
177
|
+
* 无痕闸门;保持"挂上就是开、命中会话就不处理"的单一口径。
|
|
178
178
|
*/
|
|
179
|
-
export function createContinuityRescue(ctx, store) {
|
|
179
|
+
export function createContinuityRescue(ctx, store, sessionGate = null) {
|
|
180
180
|
// 待处理的边缘:sessionId → 压缩事件序号。**真落地**的压缩事件到达时置位,下一次 pre-step 消费后
|
|
181
181
|
// 删除(同一个边缘绝不注入第二次)。只留序号、不留 Session:插件在宿主进程里只 apply
|
|
182
182
|
// 一次、服务所有会话,握着 Session 就是握着它的全量日志不放——压缩完就收工、不再有
|
|
@@ -184,6 +184,10 @@ export function createContinuityRescue(ctx, store) {
|
|
|
184
184
|
const edge = new Map();
|
|
185
185
|
|
|
186
186
|
const unsubscribe = ctx.on("session/event", (session, event) => {
|
|
187
|
+
if (sessionGate?.isOff(session)) {
|
|
188
|
+
if (session?.id) edge.delete(session.id);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
187
191
|
if (!isCompactionEdge(event)) return;
|
|
188
192
|
if (!session?.id) return;
|
|
189
193
|
if (!edge.has(session.id) && edge.size >= MAX_TRACKED_EDGES) {
|
|
@@ -200,6 +204,10 @@ export function createContinuityRescue(ctx, store) {
|
|
|
200
204
|
// reject 是"这一步不该发生",往里面塞消息没有意义;未知形状原样放行。
|
|
201
205
|
if (decision?.kind !== "enter") return decision;
|
|
202
206
|
const session = payload?.agent?.session;
|
|
207
|
+
if (sessionGate?.isOff(session)) {
|
|
208
|
+
if (session?.id) edge.delete(session.id);
|
|
209
|
+
return decision;
|
|
210
|
+
}
|
|
203
211
|
const sessionId = session?.id;
|
|
204
212
|
if (!sessionId) return decision;
|
|
205
213
|
if (!edge.has(sessionId)) return decision;
|
package/src/document.js
CHANGED
|
@@ -16,6 +16,7 @@ import { homedir } from "node:os";
|
|
|
16
16
|
import { isAbsolute, resolve, sep } from "node:path";
|
|
17
17
|
import { scopeKeyOf } from "./scope.js";
|
|
18
18
|
import { cosineSimilarity } from "./dream/clustering.js";
|
|
19
|
+
import { isImportedRow } from "./memory-source.js";
|
|
19
20
|
|
|
20
21
|
// C2 vector 档阈值(#164 拍板 0.92,与 findSessionDuplicate 的 vector 档同源)。
|
|
21
22
|
// 刻意不做 24h 时间窗:那个窗是会话去重的「近邻界」(findSessionDuplicate 注释),
|
|
@@ -82,7 +83,7 @@ const ABSORB_EXEMPT_TYPES = new Set(["document", "summary"]);
|
|
|
82
83
|
*
|
|
83
84
|
* @returns {(payload: object, opts?: object) => Promise<object>}
|
|
84
85
|
* `{ action: "created"|"superseded", memory, superseded?, evidence_kept,
|
|
85
|
-
* evidence_dropped, evidence_archived, degraded }`
|
|
86
|
+
* evidence_dropped, evidence_archived, evidence_skipped, degraded }`
|
|
86
87
|
* @throws flag 关 / 路径非法或文件缺失 / title 或 summary 为空 / evidence 全部
|
|
87
88
|
* 捏造 / 仅 vector 近重复(疑似重复外档,交 agent 裁决而不是静默二选一)。
|
|
88
89
|
*/
|
|
@@ -262,6 +263,10 @@ export function createDocumentRegistrar({ store, config, embedQuery, pushContent
|
|
|
262
263
|
let superseded;
|
|
263
264
|
const loser = explicitTarget ? store.getById(explicitTarget.id) : null;
|
|
264
265
|
if (loser && !loser.archived) {
|
|
266
|
+
// 这里不挂 isImportedRow:loser 只会是 document 行(scopeMatched 全来自
|
|
267
|
+
// store.list({ type: "document" })),而 document 行永远不可能是外来行——
|
|
268
|
+
// 分享包格式排除 document,存储层的 document 只由 saveDocument 从这里铸造。
|
|
269
|
+
// 加谓词是空转,还会让下一个读者误以为 document 行有外来形态。
|
|
265
270
|
store.update(loser.id, {
|
|
266
271
|
content: `${loser.content}${supersededByPointer(created.id)}`,
|
|
267
272
|
content_history: pushContentHistory(loser, "superseded")
|
|
@@ -276,18 +281,25 @@ export function createDocumentRegistrar({ store, config, embedQuery, pushContent
|
|
|
276
281
|
// ABSORB_EXEMPT_TYPES)不吸收,吸收的是原子条;④只翻标志位、内容与审计全留
|
|
277
282
|
// (可恢复),opt-out 走 archiveEvidence。
|
|
278
283
|
// pinnedTypes 拿不到就不做这一步:宁可少做,也不能把保真池当普通行收走。
|
|
284
|
+
// 外来行不吸收(spec §15 一,车道 C8):这条旁路不经任何 service.all() 行池,
|
|
285
|
+
// 车道 B 的九池静态锁看不见它;证据 id 由调用方给,循环里的 setArchived 是
|
|
286
|
+
// 机器发起的批量归档——正是「本库模型不改写别人的记忆」要禁的动作类。
|
|
287
|
+
// 引用本身照旧落库(文档合法地为它背书),只是不替别人撤下活跃面,
|
|
288
|
+
// 跳过条数走 evidence_skipped 报出来,不做静默跳过。
|
|
279
289
|
let evidenceArchived = 0;
|
|
290
|
+
let evidenceSkipped = 0;
|
|
280
291
|
if (archiveEvidence && pinnedTypes && typeof pinnedTypes.has === "function") {
|
|
281
292
|
for (const id of kept) {
|
|
282
293
|
// 事务内重读:期间被别的进程归档/遗忘的行不重复计数,也不误伤 pinned。
|
|
283
294
|
const row = store.getById(id);
|
|
284
295
|
if (!row || row.archived || row.forgotten) continue;
|
|
296
|
+
if (isImportedRow(row)) { evidenceSkipped += 1; continue; }
|
|
285
297
|
if (pinnedTypes.has(row.type) || ABSORB_EXEMPT_TYPES.has(row.type)) continue;
|
|
286
298
|
store.setArchived(id, true);
|
|
287
299
|
evidenceArchived += 1;
|
|
288
300
|
}
|
|
289
301
|
}
|
|
290
|
-
return { created, superseded, evidenceArchived };
|
|
302
|
+
return { created, superseded, evidenceArchived, evidenceSkipped };
|
|
291
303
|
});
|
|
292
304
|
finalize(result.superseded ? [result.created, result.superseded] : [result.created]);
|
|
293
305
|
return {
|
|
@@ -297,6 +309,7 @@ export function createDocumentRegistrar({ store, config, embedQuery, pushContent
|
|
|
297
309
|
evidence_kept: kept.length,
|
|
298
310
|
evidence_dropped: dropped.length,
|
|
299
311
|
evidence_archived: result.evidenceArchived,
|
|
312
|
+
evidence_skipped: result.evidenceSkipped,
|
|
300
313
|
degraded: dropped.length > 0
|
|
301
314
|
};
|
|
302
315
|
};
|