@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/lib/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)
|