dsh-prime-memory 0.22.2 → 0.24.1
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/CHANGELOG.en.md +421 -0
- package/CHANGELOG.ja.md +416 -0
- package/CHANGELOG.ko.md +416 -0
- package/CHANGELOG.md +120 -0
- package/dist/client.js +1266 -845
- package/dist/config.d.ts +131 -1341
- package/dist/config.js +89 -4
- package/dist/conflict-service.d.ts +7 -1
- package/dist/conflict-service.js +7 -1
- package/dist/contract.d.ts +211 -0
- package/dist/domain-gate.d.ts +11 -0
- package/dist/domain-gate.js +15 -0
- package/dist/hooks/obsolete-notice.d.ts +26 -0
- package/dist/hooks/obsolete-notice.js +64 -0
- package/dist/hooks/recall.d.ts +39 -0
- package/dist/hooks/recall.js +110 -30
- package/dist/index.d.ts +1 -1127
- package/dist/index.js +52 -2
- package/dist/memory-types.d.ts +69 -0
- package/dist/memory-types.js +165 -0
- package/dist/memport/format.d.ts +6 -1
- package/dist/memport/format.js +42 -2
- package/dist/pipeline/l1.d.ts +44 -2
- package/dist/pipeline/l1.js +163 -9
- package/dist/pipeline/md-sync.d.ts +65 -0
- package/dist/pipeline/md-sync.js +456 -0
- package/dist/pipeline/ruminate.d.ts +46 -1
- package/dist/pipeline/ruminate.js +109 -0
- package/dist/pipeline/runner.d.ts +10 -0
- package/dist/pipeline/runner.js +13 -2
- package/dist/pipeline/workspace-relink.d.ts +60 -0
- package/dist/pipeline/workspace-relink.js +73 -0
- package/dist/projection/recall.d.ts +51 -13
- package/dist/projection/recall.js +47 -12
- package/dist/prompts/examples.d.ts +73 -0
- package/dist/prompts/examples.js +178 -0
- package/dist/prompts/l1-dedup.d.ts +9 -3
- package/dist/prompts/l1-dedup.js +34 -13
- package/dist/prompts/l1-extraction.d.ts +5 -3
- package/dist/prompts/l1-extraction.js +49 -4
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +16 -1
- package/dist/stats.d.ts +12 -0
- package/dist/stats.js +352 -17
- package/dist/store/embedding-source.d.ts +8 -0
- package/dist/store/embedding-source.js +39 -2
- package/dist/store/file-versions.d.ts +2 -0
- package/dist/store/file-versions.js +2 -0
- package/dist/store/graph-store.d.ts +14 -0
- package/dist/store/graph-store.js +35 -0
- package/dist/store/l1-snapshot.d.ts +26 -1
- package/dist/store/l1-snapshot.js +53 -8
- package/dist/store/l1.d.ts +94 -3
- package/dist/store/l1.js +169 -4
- package/dist/store/memory-md-archive.d.ts +84 -0
- package/dist/store/memory-md-archive.js +155 -0
- package/dist/store/memory-md-disable.d.ts +74 -0
- package/dist/store/memory-md-disable.js +302 -0
- package/dist/store/memory-md-export.d.ts +50 -0
- package/dist/store/memory-md-export.js +227 -0
- package/dist/store/memory-md-ledger.d.ts +94 -0
- package/dist/store/memory-md-ledger.js +243 -0
- package/dist/store/memory-md-orphan.d.ts +27 -0
- package/dist/store/memory-md-orphan.js +153 -0
- package/dist/store/memory-md-parse.d.ts +35 -0
- package/dist/store/memory-md-parse.js +85 -0
- package/dist/store/memory-md-run.d.ts +48 -0
- package/dist/store/memory-md-run.js +151 -0
- package/dist/store/memory-md-skeleton.d.ts +18 -0
- package/dist/store/memory-md-skeleton.js +61 -0
- package/dist/store/memory-md-sync.d.ts +59 -0
- package/dist/store/memory-md-sync.js +152 -0
- package/dist/store/memory-md-types.d.ts +243 -0
- package/dist/store/memory-md-types.js +10 -0
- package/dist/store/memory-md.d.ts +117 -0
- package/dist/store/memory-md.js +230 -0
- package/dist/store/obsolete-notices.d.ts +39 -0
- package/dist/store/obsolete-notices.js +153 -0
- package/dist/store/recall-feedback.d.ts +75 -0
- package/dist/store/recall-feedback.js +213 -0
- package/dist/store/recall-usage.d.ts +11 -0
- package/dist/store/recall-usage.js +29 -0
- package/dist/store/session-recalled.d.ts +38 -3
- package/dist/store/session-recalled.js +156 -9
- package/dist/store/sqlite.d.ts +66 -0
- package/dist/store/sqlite.js +233 -15
- package/dist/store/supersede.d.ts +1 -1
- package/dist/store/supersede.js +1 -1
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +26 -7
- package/dist/tools/slots.d.ts +2 -1
- package/dist/tools/slots.js +2 -1
- package/dist/types.d.ts +24 -7
- package/dist/types.js +10 -2
- package/dist/util/maturity.d.ts +20 -3
- package/dist/util/maturity.js +18 -1
- package/dist/util/md-name.d.ts +129 -0
- package/dist/util/md-name.js +366 -0
- package/dist/util/md-normalize.d.ts +23 -0
- package/dist/util/md-normalize.js +131 -0
- package/dist/workspace-route.d.ts +22 -0
- package/dist/workspace-route.js +66 -0
- package/dsh.plugin.json +1 -1
- package/package.json +6 -5
package/dist/stats.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { closeSync, openSync, readSync, statSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { EFFORT_CHOICES, resolveDataDir } from './config.js';
|
|
18
|
+
import { typeCatalogEntries } from './memory-types.js';
|
|
18
19
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
19
20
|
import { emptyRecallStats } from './hooks/recall.js';
|
|
20
21
|
import { buildRouteChain, decideSendableEffort, LAYER_DEFAULT_BUDGETS, layerChainOrNull, resolveModelContextWindow, resolveModelEfforts, resolveModelRoute } from './llm.js';
|
|
@@ -35,6 +36,16 @@ import { startWingBackfill } from './wing-backfill.js';
|
|
|
35
36
|
import { errDetail } from './util/filelog.js';
|
|
36
37
|
import { snapshotTokenCost } from './token-cost.js';
|
|
37
38
|
import { computeMaturity } from './util/maturity.js';
|
|
39
|
+
import { isFeedbackScore } from './store/recall-feedback.js';
|
|
40
|
+
import { relinkWorkspaces } from './pipeline/workspace-relink.js';
|
|
41
|
+
import { reconcileLedger } from './store/memory-md-export.js';
|
|
42
|
+
import { mdRunStatus, runProjection } from './store/memory-md-run.js';
|
|
43
|
+
import { disableProjectionFile } from './store/memory-md-disable.js';
|
|
44
|
+
import { MEMORY_TYPE_DIRS, UNSORTED_DIR, resolveMemoriesRoot, targetFor } from './store/memory-md.js';
|
|
45
|
+
import { ensureTypeSkeleton } from './store/memory-md-skeleton.js';
|
|
46
|
+
import { sweepOrphanFiles } from './store/memory-md-orphan.js';
|
|
47
|
+
import { collectProjectionFiles } from './store/memory-md-archive.js';
|
|
48
|
+
import { syncFromFiles } from './pipeline/md-sync.js';
|
|
38
49
|
const require = createRequire(import.meta.url);
|
|
39
50
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
40
51
|
/**
|
|
@@ -101,6 +112,13 @@ export const MEMORY_ENDPOINTS = [
|
|
|
101
112
|
'dsh-memory/memport-scan',
|
|
102
113
|
'dsh-memory/skills-list',
|
|
103
114
|
'dsh-memory/session-recalled',
|
|
115
|
+
'dsh-memory/recall-feedback',
|
|
116
|
+
'dsh-memory/workspace-relink',
|
|
117
|
+
'dsh-memory/mem-export',
|
|
118
|
+
'dsh-memory/mem-run-status',
|
|
119
|
+
'dsh-memory/mem-reconcile',
|
|
120
|
+
'dsh-memory/mem-sync',
|
|
121
|
+
'dsh-memory/mem-orphans',
|
|
104
122
|
'dsh-memory/skills-action',
|
|
105
123
|
];
|
|
106
124
|
/** HTTP 路由前缀(客户端 fetch `/dsh-memory/rpc/<短方法名>`)。 */
|
|
@@ -138,6 +156,23 @@ function writeJson(res, status, body) {
|
|
|
138
156
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
139
157
|
res.end(JSON.stringify(body));
|
|
140
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* 面板检索软超时(用户裁决:删除后检索面短暂变慢不得挂死面板):竞速到期即
|
|
161
|
+
* 返回业务错误(客户端显示可重试的提示,而不是永远「加载中」)。底层检索
|
|
162
|
+
* 稍后自然完成时其结果被 race 吸收,不产生未处理拒绝。
|
|
163
|
+
*/
|
|
164
|
+
function raceTimeout(p, ms, message) {
|
|
165
|
+
let timer;
|
|
166
|
+
const timeout = new Promise((_, reject) => {
|
|
167
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
168
|
+
});
|
|
169
|
+
return Promise.race([p, timeout]).finally(() => {
|
|
170
|
+
if (timer !== undefined)
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** 面板关键词检索的软超时上限:正常检索是毫秒级,8s 足够宽裕。 */
|
|
175
|
+
const SEARCH_SOFT_TIMEOUT_MS = 8_000;
|
|
141
176
|
/** 读取 JSON 请求体(上限 8MB:面板最大载荷为 list-records 分页与图谱检索)。 */
|
|
142
177
|
function readJsonBody(req) {
|
|
143
178
|
return new Promise((resolve, reject) => {
|
|
@@ -365,6 +400,8 @@ function sanitizeSettings(s) {
|
|
|
365
400
|
/** 端点分发表(导出供测试直调:可精确注入 rebuild/ruminate 等可选控制器,验证 deps 接线)。 */
|
|
366
401
|
export async function handleEndpoint(endpoint, payload, deps) {
|
|
367
402
|
const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo, roomRegistry, memorySources, skillStore } = deps;
|
|
403
|
+
// 退场/恢复/新增的文件跟随已收口在 `L1Store` 的汇流口(`store/memory-md-sync.ts`),
|
|
404
|
+
// 本分发层不再自己建点火器——那 8 处事件挂钩已被真机证伪(会漏)。
|
|
368
405
|
switch (endpoint) {
|
|
369
406
|
case 'dsh-memory/stats':
|
|
370
407
|
return buildStats(cfg, stores, status);
|
|
@@ -402,19 +439,243 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
402
439
|
return v;
|
|
403
440
|
}
|
|
404
441
|
case 'dsh-memory/session-recalled': {
|
|
405
|
-
// G-live:返回本会话(按
|
|
442
|
+
// G-live:返回本会话(按 sessionKey,与投影 header.id / pill sessionId 同源)已注入、
|
|
443
|
+
// 尚未被压缩覆盖的召回记忆。
|
|
406
444
|
const p = (payload ?? {});
|
|
407
445
|
const sessionId = expectSessionId(p.sessionId);
|
|
408
446
|
const ids = stores.recalled?.list(sessionId) ?? [];
|
|
409
447
|
if (ids.length === 0)
|
|
410
|
-
return { sessionId, records: [] };
|
|
448
|
+
return { sessionId, records: [], typeCatalog: typeCatalogEntries() };
|
|
449
|
+
// 压缩水位之上的那批:正文已被摘要取代。与 retired 正交(真实实现见 session-recalled.ts)。
|
|
450
|
+
const superseded = stores.recalled?.supersededOf(sessionId);
|
|
411
451
|
const halfLife = effectiveCfg(cfg, live).recall?.decayHalfLifeDays ?? 0;
|
|
412
452
|
const byId = new Map(stores.l1.getByIds(ids).map((r) => [r.id, r]));
|
|
413
453
|
const records = ids
|
|
414
454
|
.map((id) => byId.get(id))
|
|
415
455
|
.filter((r) => r !== undefined)
|
|
416
|
-
.map((r) =>
|
|
417
|
-
|
|
456
|
+
.map((r) => {
|
|
457
|
+
const ui = hitToUiRecord(r, {
|
|
458
|
+
agingWeight: agingWeightOf(r.updatedAt, halfLife),
|
|
459
|
+
usage: stores.usage?.get(r.id) ?? null,
|
|
460
|
+
userRelevance: stores.feedback?.meanOf(r.id) ?? null,
|
|
461
|
+
userMark: stores.feedback?.lastOf(r.id) ?? null,
|
|
462
|
+
});
|
|
463
|
+
return superseded?.has(r.id) === true ? { ...ui, superseded: true } : ui;
|
|
464
|
+
});
|
|
465
|
+
return { sessionId, records, typeCatalog: typeCatalogEntries() };
|
|
466
|
+
}
|
|
467
|
+
case 'dsh-memory/recall-feedback': {
|
|
468
|
+
// 人在召回卡上给一条召回记忆打分:0..5 = 相关度(只聚合,不改写任何排序分);
|
|
469
|
+
// -1 = 声明过时,做两件事:退场该记录(软删可恢复)+ 排一条下一轮向模型的播报
|
|
470
|
+
//(取走即离队,不常驻)。条目**不**从本会话活召回集摘掉——它得留在列表里显示删除线。
|
|
471
|
+
if (cfg.recallFeedback?.enabled !== true)
|
|
472
|
+
throw new Error('召回反馈未开启(cfg.recallFeedback.enabled)');
|
|
473
|
+
const feedback = stores.feedback;
|
|
474
|
+
if (feedback === undefined)
|
|
475
|
+
throw new Error('召回反馈存储未装配');
|
|
476
|
+
const p = (payload ?? {});
|
|
477
|
+
const sessionId = expectSessionId(typeof p.sessionId === 'string' ? p.sessionId : undefined);
|
|
478
|
+
if (typeof p.id !== 'string' || p.id.length === 0)
|
|
479
|
+
throw new Error('id 缺失');
|
|
480
|
+
if (p.id.length > 512)
|
|
481
|
+
throw new Error('id 过长(≤512 字符)');
|
|
482
|
+
const id = p.id;
|
|
483
|
+
if (!isFeedbackScore(p.score))
|
|
484
|
+
throw new Error('score 必须是 -1 或 0..5');
|
|
485
|
+
// 结构性护栏(代替全局高权限模式):只能给**本会话真召回过、且还活着**的记录打分。
|
|
486
|
+
// 否则一个面对 web 的写端点就能退场任意 id;而这张卡上本来也只列得出这些。
|
|
487
|
+
const alive = stores.recalled?.list(sessionId) ?? [];
|
|
488
|
+
if (!alive.includes(id))
|
|
489
|
+
throw new Error('该记忆不在本会话的召回集里(可能已被压缩覆盖)');
|
|
490
|
+
const entry = feedback.rate(id, p.score);
|
|
491
|
+
let retired = false;
|
|
492
|
+
let announced = false;
|
|
493
|
+
// 退场**前**的记录快照(带 workspaceId/name)——失效联动要靠它算落点,
|
|
494
|
+
// 而 `retire` 只改 `valid_to`,不会动这两列,所以取在退场前后等价,取在前更直白。
|
|
495
|
+
let rec;
|
|
496
|
+
if (p.score === -1) {
|
|
497
|
+
rec = stores.l1.getByIds([id])[0];
|
|
498
|
+
const retiredAt = new Date().toISOString();
|
|
499
|
+
retired = stores.l1.retire([id], { at: retiredAt, reason: 'obsolete' }) > 0;
|
|
500
|
+
// 不再摘除:条目要留在列表里显示删除线。检索侧已由 detachL1FromRetrieval 阻断再注入。
|
|
501
|
+
// 并且标成**受保护**:这张列表不再被压缩清掉,但仍有上限淘汰——判过时的条目
|
|
502
|
+
// 是人的裁决凭证,不能因为“后面又召回了几条”而被挤出去。
|
|
503
|
+
stores.recalled?.protect(sessionId, [id]);
|
|
504
|
+
if (cfg.recallFeedback?.announce !== false && stores.notices !== undefined && rec !== undefined) {
|
|
505
|
+
const title = (rec.content.split('\n').find((l) => l.trim().length > 0) ?? rec.content).trim().slice(0, 120);
|
|
506
|
+
stores.notices.push(sessionId, { id, title, at: Date.now() });
|
|
507
|
+
announced = true;
|
|
508
|
+
}
|
|
509
|
+
deps.logger.info(`[memory] 用户声明记忆过时并退场(session=${sessionId}, ${retired ? '已退场' : '本已退场'}): ${id}`);
|
|
510
|
+
// 失效联动(N11,用户裁决改异步):退场成立后**后台**去动投射文件——
|
|
511
|
+
// 文件改名/建凭证不再阻塞本 RPC(面板只等库侧结果);它内部已收敛所有
|
|
512
|
+
// 失败为 'failed',这里再兜一层 catch 只落日志。响应不再带 fileDisabled
|
|
513
|
+
// (面板无消费方),磁盘凭证的对账走 mem-reconcile。
|
|
514
|
+
if (retired && rec !== undefined && cfg.memExport?.enabled === true && stores.mdLedger !== undefined) {
|
|
515
|
+
void disableProjectionFile({
|
|
516
|
+
// 传退场**后**的快照:渲染出的 frontmatter 才带 `retired: true`,
|
|
517
|
+
// 凭证自己就能说清它为什么存在。
|
|
518
|
+
record: { ...rec, validTo: Date.parse(retiredAt) || Date.now() },
|
|
519
|
+
cfgRoot: cfg.memExport.root,
|
|
520
|
+
dataDir: deps.dataDir,
|
|
521
|
+
ledger: stores.mdLedger,
|
|
522
|
+
logger: deps.logger,
|
|
523
|
+
})
|
|
524
|
+
.then((outcome) => deps.logger.info(`[memory] 投射文件失效联动(后台): ${id} → ${outcome}`))
|
|
525
|
+
.catch((err) => deps.logger.warn(`[memory] 投射文件失效联动(后台)异常: ${err instanceof Error ? err.message : String(err)}`));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
id,
|
|
530
|
+
score: p.score,
|
|
531
|
+
rating: { last: entry.last, rated: entry.rated, mean: entry.mean, obsolete: entry.obsolete },
|
|
532
|
+
retired,
|
|
533
|
+
announced,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
case 'dsh-memory/workspace-relink': {
|
|
537
|
+
// 存量记忆的 workspace 回填(内容路由)。**缺省干跑**:先看命中率与分布报告,再决定真写。
|
|
538
|
+
// 真写额外要求高权限门(它改的是库里的列,虽然只改 `workspace_id`)。
|
|
539
|
+
const wsSrc = effectiveCfg(cfg, live).workspaceSource;
|
|
540
|
+
if (wsSrc?.enabled !== true)
|
|
541
|
+
throw new Error('内容路由未开启(cfg.workspaceSource.enabled)');
|
|
542
|
+
const p = (payload ?? {});
|
|
543
|
+
const dryRun = p.dryRun !== false;
|
|
544
|
+
if (!dryRun && live?.get().memoryMutate !== true)
|
|
545
|
+
throw new Error('真写归属列需要高权限模式(memoryMutate)');
|
|
546
|
+
return relinkWorkspaces({
|
|
547
|
+
l1: {
|
|
548
|
+
list: (o) => stores.l1.list(o),
|
|
549
|
+
setWorkspaceIds: (items) => stores.l1.setWorkspaceIds(items),
|
|
550
|
+
},
|
|
551
|
+
routes: wsSrc.routes,
|
|
552
|
+
dryRun,
|
|
553
|
+
maxRecords: wsSrc.maxRecords,
|
|
554
|
+
timeBudgetMs: wsSrc.timeBudgetMs,
|
|
555
|
+
logger: deps.logger,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
case 'dsh-memory/mem-export': {
|
|
559
|
+
// 单向导出:库 → md 文件 + 账本。**不写检索面**(用例钉住:导出前后 hashRecords 相同)。
|
|
560
|
+
// 先查配置门再查装配门:“整面默认关”时该听到的是“没开”,不是“没装配”。
|
|
561
|
+
// 护栏读**生效**配置(含面板上的 `mdExportMaxRecords` 覆盖):只看启动快照
|
|
562
|
+
// 就会“调了没反应”,而这条路径的护栏正是给人调的。
|
|
563
|
+
const exp = effectiveCfg(cfg, live).memExport;
|
|
564
|
+
if (exp?.enabled !== true)
|
|
565
|
+
throw new Error('md 投射未开启(cfg.memExport.enabled)');
|
|
566
|
+
const md = stores.mdLedger;
|
|
567
|
+
if (md === undefined)
|
|
568
|
+
throw new Error('md 投射账本未装配');
|
|
569
|
+
// 命名回填:没名字的记录先拿到全库唯一的 `name`,再算落点。
|
|
570
|
+
// 不回填就是真机那个形状:1166 条 name 全空、42 条挤进 17 个同名文件,
|
|
571
|
+
// 文件互相覆盖 + 导出每轮重刷 + 反向同步把一份内容回灌给多条。
|
|
572
|
+
// 它只写 `name` 列(不在快照哈希字段表里),所以 hashRecords 仍逐字不变。
|
|
573
|
+
const all = stores.l1.all();
|
|
574
|
+
const naming = stores.l1.assignMissingNames(all, { persist: true });
|
|
575
|
+
if (naming.failed > 0) {
|
|
576
|
+
deps.logger?.warn(`[memory] md 投射命名回填失败 ${naming.failed} 条(下轮导出会重试)`);
|
|
577
|
+
}
|
|
578
|
+
// 类型目录骨架(pm-name-distill Phase 4):37 类目录各放一个 `_index.md`(类名 + 库里条数)。
|
|
579
|
+
// 目录集合从词表真源派生(在 ensureTypeSkeleton 内),这里只喂 countsByType 与 roots。
|
|
580
|
+
// 单根必补;workspace 根**按需**——只给本轮 `targetFor` 算出真有落点(inWorkspace)的根补,
|
|
581
|
+
// 不建空 workspace 根(消失的 workspace 已由 targetFor 护栏退回单根,这里不绕过它造目录树)。
|
|
582
|
+
// 配置门在前(enabled!==true 已在上头抛错),故关着时该端点根本到不了这里 → 骨架零动作。
|
|
583
|
+
const countsByType = new Map();
|
|
584
|
+
const skeletonRoots = new Set([resolveMemoriesRoot(exp.root, deps.dataDir, deps.logger)]);
|
|
585
|
+
for (const r of all) {
|
|
586
|
+
const dir = MEMORY_TYPE_DIRS.has(r.type) ? r.type : UNSORTED_DIR;
|
|
587
|
+
countsByType.set(dir, (countsByType.get(dir) ?? 0) + 1);
|
|
588
|
+
const t = targetFor(r, exp.root, deps.dataDir, deps.logger);
|
|
589
|
+
if (t.inWorkspace)
|
|
590
|
+
skeletonRoots.add(t.root);
|
|
591
|
+
}
|
|
592
|
+
await ensureTypeSkeleton({ roots: [...skeletonRoots], countsByType, logger: deps.logger });
|
|
593
|
+
// 滚动执行到清完:先把导出游标推到表尾,再把后缀收敛游标推到表尾。
|
|
594
|
+
// 单轮护栏(`maxRecords`/`timeBudgetMs`)不变,跨轮另有轮数与总墙钟兜底;
|
|
595
|
+
// 进度经 `dsh-memory/mem-run-status` 读(面板画进度条)。
|
|
596
|
+
const run = await runProjection({
|
|
597
|
+
records: all,
|
|
598
|
+
cfgRoot: exp.root,
|
|
599
|
+
dataDir: deps.dataDir,
|
|
600
|
+
ledger: md,
|
|
601
|
+
maxRecords: exp.maxRecords,
|
|
602
|
+
timeBudgetMs: exp.timeBudgetMs,
|
|
603
|
+
logger: deps.logger,
|
|
604
|
+
});
|
|
605
|
+
return { ...run, named: naming.named, nameFailed: naming.failed };
|
|
606
|
+
}
|
|
607
|
+
case 'dsh-memory/mem-run-status': {
|
|
608
|
+
// 只读进度快照。故意不查配置门:任务没在跑时也要能读到上一次的 `notice`
|
|
609
|
+
// (“预算用尽、还剩 N 条”这句话必须在人下次点击之前也看得见)。
|
|
610
|
+
return mdRunStatus();
|
|
611
|
+
}
|
|
612
|
+
case 'dsh-memory/mem-reconcile': {
|
|
613
|
+
// 只读对账:不写库、不写文件,只按账本基线出四态报告(所以不需高权限门)。
|
|
614
|
+
// 同样读生效配置:对账与导出共用护栏,两边半生效会让人看不出差异从哪来。
|
|
615
|
+
const exp = effectiveCfg(cfg, live).memExport;
|
|
616
|
+
if (exp?.enabled !== true)
|
|
617
|
+
throw new Error('md 投射未开启(cfg.memExport.enabled)');
|
|
618
|
+
const md = stores.mdLedger;
|
|
619
|
+
if (md === undefined)
|
|
620
|
+
throw new Error('md 投射账本未装配');
|
|
621
|
+
return reconcileLedger({
|
|
622
|
+
records: stores.l1.all(),
|
|
623
|
+
cfgRoot: exp.root,
|
|
624
|
+
dataDir: deps.dataDir,
|
|
625
|
+
ledger: md,
|
|
626
|
+
maxRecords: exp.maxRecords,
|
|
627
|
+
timeBudgetMs: exp.timeBudgetMs,
|
|
628
|
+
logger: deps.logger,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
case 'dsh-memory/mem-sync': {
|
|
632
|
+
// P2 反向同步:文件 → 库。与导出的**风险不对称**——导出只写文件,本项会写库,
|
|
633
|
+
// 所以多两道门:要先开投射(没导出过 = 账本为空 = 判不出“谁变了”),要高权限。
|
|
634
|
+
const syn = cfg.mdSync;
|
|
635
|
+
if (syn?.enabled !== true)
|
|
636
|
+
throw new Error('md 反向同步未开启(cfg.mdSync.enabled)');
|
|
637
|
+
if (cfg.memExport?.enabled !== true)
|
|
638
|
+
throw new Error('md 反向同步需先开启 md 投射(cfg.memExport.enabled)');
|
|
639
|
+
if (!live?.get().memoryMutate)
|
|
640
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能回灌文件侧修改。');
|
|
641
|
+
const md = stores.mdLedger;
|
|
642
|
+
if (md === undefined)
|
|
643
|
+
throw new Error('md 投射账本未装配');
|
|
644
|
+
return syncFromFiles({
|
|
645
|
+
store: stores.l1,
|
|
646
|
+
ledger: md,
|
|
647
|
+
cfgRoot: cfg.memExport.root,
|
|
648
|
+
dataDir: deps.dataDir,
|
|
649
|
+
maxFiles: syn.maxFiles,
|
|
650
|
+
timeBudgetMs: syn.timeBudgetMs,
|
|
651
|
+
logger: deps.logger,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
case 'dsh-memory/mem-orphans': {
|
|
655
|
+
// P4.4 孤儿归档:只搬“账本记着我们写过、但现在没记录认领它”的投射文件。
|
|
656
|
+
// 门序与同步侧一致:配置门 → 装配门 → 高权限门(干跑不需要)。**默认干跑**:
|
|
657
|
+
// 搬文件在用户眼里和删文件差不离,必须显式要。
|
|
658
|
+
const exp = cfg.memExport;
|
|
659
|
+
if (exp?.enabled !== true)
|
|
660
|
+
throw new Error('md 投射未开启(cfg.memExport.enabled)');
|
|
661
|
+
const md = stores.mdLedger;
|
|
662
|
+
if (md === undefined)
|
|
663
|
+
throw new Error('md 投射账本未装配');
|
|
664
|
+
const p = (payload ?? {});
|
|
665
|
+
const dryRun = p.dryRun !== false;
|
|
666
|
+
if (!dryRun && live?.get().memoryMutate !== true) {
|
|
667
|
+
throw new Error('孤儿文件归档需要高权限模式(memoryMutate)');
|
|
668
|
+
}
|
|
669
|
+
return sweepOrphanFiles({
|
|
670
|
+
records: stores.l1.all(),
|
|
671
|
+
cfgRoot: exp.root,
|
|
672
|
+
dataDir: deps.dataDir,
|
|
673
|
+
ledger: md,
|
|
674
|
+
dryRun,
|
|
675
|
+
maxFiles: exp.maxRecords,
|
|
676
|
+
timeBudgetMs: exp.timeBudgetMs,
|
|
677
|
+
logger: deps.logger,
|
|
678
|
+
});
|
|
418
679
|
}
|
|
419
680
|
case 'dsh-memory/session-mode-set': {
|
|
420
681
|
if (!modes)
|
|
@@ -735,7 +996,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
735
996
|
const page = stores.l1.list({ tag: slug, retired: false, limit: 200, offset });
|
|
736
997
|
if (page.items.length === 0)
|
|
737
998
|
break;
|
|
738
|
-
const
|
|
999
|
+
const pageIds = page.items.map((r) => r.id);
|
|
1000
|
+
const n = stores.l1.retire(pageIds, { at: new Date().toISOString(), reason: 'manual' });
|
|
739
1001
|
retiredRecords += n;
|
|
740
1002
|
if (page.items.length < 200)
|
|
741
1003
|
break;
|
|
@@ -987,6 +1249,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
987
1249
|
memoryMutate: false,
|
|
988
1250
|
conflictFreeze: live?.get()?.conflictFreeze === true,
|
|
989
1251
|
skillsEnabled: live?.get()?.skillsEnabled === true,
|
|
1252
|
+
mdExportMaxRecords: live?.get()?.mdExportMaxRecords ?? 0,
|
|
990
1253
|
}),
|
|
991
1254
|
// 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
|
|
992
1255
|
ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
|
|
@@ -1029,7 +1292,12 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1029
1292
|
if (typeof patch[key] === 'boolean')
|
|
1030
1293
|
clean[key] = patch[key];
|
|
1031
1294
|
}
|
|
1032
|
-
//
|
|
1295
|
+
// md 投射护栏运行时覆盖:数字才收,钳 0~10000(与 live schema 同上限);0 = 跟随部署默认。
|
|
1296
|
+
// 不加进白名单的话,面板上那个输入框会“写成功但被静默丢”——看得到数字、不生效。
|
|
1297
|
+
if (typeof patch.mdExportMaxRecords === 'number' && Number.isFinite(patch.mdExportMaxRecords)) {
|
|
1298
|
+
clean.mdExportMaxRecords = Math.min(10_000, Math.max(0, Math.floor(patch.mdExportMaxRecords)));
|
|
1299
|
+
}
|
|
1300
|
+
// 运行时统一路由链:结构校验后整体写入(空数组 = 回到跟随部署配置)
|
|
1033
1301
|
if (patch.distillChain !== undefined) {
|
|
1034
1302
|
const err = validateDistillChain(patch.distillChain);
|
|
1035
1303
|
if (err)
|
|
@@ -1180,12 +1448,16 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1180
1448
|
const wingCatalog = offset === 0
|
|
1181
1449
|
? [...WING_CATALOG.map((h) => ({ id: h.id, label: h.label })), { id: WING_FALLBACK, label: '跨域' }]
|
|
1182
1450
|
: undefined;
|
|
1451
|
+
// 类型词表目录随首屏下发(task_1.5:单一真源在 `memory-types.ts`,client 不手抄)。
|
|
1452
|
+
const typeCatalog = offset === 0 ? typeCatalogEntries() : undefined;
|
|
1183
1453
|
// 关键词路径:复用检索唯一缝(与召回同源),取回后做场景/Wing 过滤 + 手工分页。
|
|
1184
1454
|
// 检索侧单次上限 200:分页窗口触达上限时显式标记 truncated(结果可能不完整)。
|
|
1185
1455
|
if (p.query && p.query.trim()) {
|
|
1186
1456
|
const SEARCH_CAP = 200;
|
|
1187
1457
|
const wanted = offset + limit + 1;
|
|
1188
|
-
|
|
1458
|
+
// 软超时护栏(用户裁决):删除/繁重后台动作后的短暂检索迟滞不得把面板
|
|
1459
|
+
// 永久钉在「加载中」。到期返回可重试的业务错误。
|
|
1460
|
+
const hits = await raceTimeout(stores.l1.search(p.query, Math.min(wanted, SEARCH_CAP), { type: p.type || undefined }), SEARCH_SOFT_TIMEOUT_MS, `检索超时(${Math.round(SEARCH_SOFT_TIMEOUT_MS / 1000)}s):检索面短暂繁忙,请重试或缩小关键词`);
|
|
1189
1461
|
let filtered = p.scene ? hits.filter((h) => h.scene_name === p.scene) : hits;
|
|
1190
1462
|
// Wing / Room 过滤(检索命中不含 metadata):按 id 批量取回元数据后过滤
|
|
1191
1463
|
let metaById = null;
|
|
@@ -1218,18 +1490,40 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1218
1490
|
return true;
|
|
1219
1491
|
});
|
|
1220
1492
|
}
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1493
|
+
const page = filtered.slice(offset, offset + limit);
|
|
1494
|
+
const pageIds = page.map((h) => h.id);
|
|
1495
|
+
// 检索命中(`L1SearchHit`)只带 7 个字段,不含 createdAt/timestamps/version/
|
|
1496
|
+
// metadata/validTo——卡片上的「创建/活跃时间」与 R7 来源锚点因此恒为占位值。
|
|
1497
|
+
// 旧缺陷有两层:① 这些字段从未补过;② `metaById` 只在勾了 Wing/tag 过滤时才建,
|
|
1498
|
+
// 于是连 metadata 都不补——锚点在默认视图下永远显示 `-`。这里按 id 回查主表
|
|
1499
|
+
// 做完整水合,与浏览路径同口径;正文/得分仍以命中为准(FTS 可能给的是摘要)。
|
|
1500
|
+
const fullById = new Map(stores.l1.getByIds(pageIds).map((r) => [r.id, r]));
|
|
1223
1501
|
const resp = {
|
|
1224
|
-
items:
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1502
|
+
items: page.map((h) => {
|
|
1503
|
+
const full = fullById.get(h.id);
|
|
1504
|
+
return hitToUiRecord({
|
|
1505
|
+
...full,
|
|
1506
|
+
id: h.id,
|
|
1507
|
+
content: h.content,
|
|
1508
|
+
type: h.type,
|
|
1509
|
+
priority: h.priority,
|
|
1510
|
+
scene_name: h.scene_name,
|
|
1511
|
+
family: h.family,
|
|
1512
|
+
score: h.score,
|
|
1513
|
+
metadata: full?.metadata ?? metaById?.get(h.id),
|
|
1514
|
+
}, {
|
|
1515
|
+
agingWeight: agingWeightOf(full?.updatedAt, stores.l1.decayHalfLife),
|
|
1516
|
+
usage: stores.usage?.get(h.id) ?? null,
|
|
1517
|
+
userRelevance: stores.feedback?.meanOf(h.id) ?? null,
|
|
1518
|
+
userMark: stores.feedback?.lastOf(h.id) ?? null,
|
|
1519
|
+
});
|
|
1520
|
+
}),
|
|
1228
1521
|
hasMore: filtered.length > offset + limit,
|
|
1229
1522
|
total: null,
|
|
1230
1523
|
truncated: wanted > SEARCH_CAP,
|
|
1231
1524
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
1232
1525
|
wingCatalog,
|
|
1526
|
+
typeCatalog,
|
|
1233
1527
|
usageSummary: stores.usage?.summary() ?? null,
|
|
1234
1528
|
};
|
|
1235
1529
|
return resp;
|
|
@@ -1241,12 +1535,15 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1241
1535
|
items: items.map((r) => hitToUiRecord(r, {
|
|
1242
1536
|
agingWeight: agingWeightOf(r.updatedAt, stores.l1.decayHalfLife),
|
|
1243
1537
|
usage: stores.usage?.get(r.id) ?? null,
|
|
1538
|
+
userRelevance: stores.feedback?.meanOf(r.id) ?? null,
|
|
1539
|
+
userMark: stores.feedback?.lastOf(r.id) ?? null,
|
|
1244
1540
|
})),
|
|
1245
1541
|
hasMore: offset + items.length < total,
|
|
1246
1542
|
total,
|
|
1247
1543
|
truncated: false,
|
|
1248
1544
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
1249
1545
|
wingCatalog,
|
|
1546
|
+
typeCatalog,
|
|
1250
1547
|
usageSummary: stores.usage?.summary() ?? null,
|
|
1251
1548
|
};
|
|
1252
1549
|
return resp;
|
|
@@ -1282,7 +1579,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1282
1579
|
// (live.conflictFreeze 覆盖静态 cfg.conflictFreeze.enabled)。此前这里直接读
|
|
1283
1580
|
// cfg.conflictFreeze.enabled —— 面板开关写的是 live,而部署静态值恒 false,
|
|
1284
1581
|
// 于是开关已开、settings.yaml 已落 true,本页仍报"矛盾冻结未开启"。
|
|
1285
|
-
return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true, usage: stores.usage, decayHalfLifeDays: effectiveCfg(cfg, live).recall?.decayHalfLifeDays }, { limit: Number(p.limit) || undefined });
|
|
1582
|
+
return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true, usage: stores.usage, decayHalfLifeDays: effectiveCfg(cfg, live).recall?.decayHalfLifeDays, feedback: stores.feedback }, { limit: Number(p.limit) || undefined });
|
|
1286
1583
|
}
|
|
1287
1584
|
// ── §C 矛盾冻结裁决(task_25):与 memory_resolve_conflict 工具共用同一形状 ──
|
|
1288
1585
|
// 端点层同样不给"提示文案"出口的例外只有一条:**队列未开启**不是调用错误而是
|
|
@@ -1373,6 +1670,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1373
1670
|
throw new Error('ids 缺失');
|
|
1374
1671
|
const r = await stores.l1.restore(ids);
|
|
1375
1672
|
deps.logger.info(`[memory] 恢复已退场记忆 ${r.restored} 条(补向量 ${r.vectorsWritten} 条)`);
|
|
1673
|
+
// 掉 `.disable` 后缀由 `l1.restore()` 里的钩子承接(与退场对称的那一半)。
|
|
1376
1674
|
const resp = r;
|
|
1377
1675
|
return resp;
|
|
1378
1676
|
}
|
|
@@ -1403,6 +1701,14 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1403
1701
|
}
|
|
1404
1702
|
}
|
|
1405
1703
|
if (dryRun) {
|
|
1704
|
+
// 待删清单要给人看:`targets` 只是计数,人无法从计数判断“这批是不是该删的那批”。
|
|
1705
|
+
// 取前 50 条预览(全量列表会把面板撑爆,而判据只需抽看)。
|
|
1706
|
+
const preview = stores.l1.getByIds(targets.slice(0, 50)).map((r) => ({
|
|
1707
|
+
id: r.id,
|
|
1708
|
+
name: r.name ?? '',
|
|
1709
|
+
type: r.type,
|
|
1710
|
+
retiredAt: r.validTo === undefined ? '' : new Date(r.validTo).toISOString(),
|
|
1711
|
+
}));
|
|
1406
1712
|
const resp = {
|
|
1407
1713
|
dryRun: true,
|
|
1408
1714
|
targets: targets.length,
|
|
@@ -1411,6 +1717,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1411
1717
|
dir: '',
|
|
1412
1718
|
name: '',
|
|
1413
1719
|
diffs: [],
|
|
1720
|
+
items: preview,
|
|
1414
1721
|
};
|
|
1415
1722
|
return resp;
|
|
1416
1723
|
}
|
|
@@ -1418,7 +1725,18 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1418
1725
|
const resp = { dryRun: false, targets: 0, purged: 0, aborted: false, dir: '', name: '', diffs: [] };
|
|
1419
1726
|
return resp;
|
|
1420
1727
|
}
|
|
1421
|
-
|
|
1728
|
+
// 删前先把该批记录抓在手里:删完再读就什么都读不到了(归档正文与预览清单都要用它们)。
|
|
1729
|
+
const purgeRecords = stores.l1.getByIds(targets);
|
|
1730
|
+
// N12:投射面开着时把该批的投射文件(含 `.disable`)一并收进快照目录。
|
|
1731
|
+
// 关着时 `files` 为 undefined ⇒ `exportThenPurge` 行为与改动前逐字一致。
|
|
1732
|
+
const files = cfg.memExport?.enabled === true
|
|
1733
|
+
? await collectProjectionFiles(purgeRecords, cfg.memExport.root, deps.dataDir, deps.logger)
|
|
1734
|
+
: undefined;
|
|
1735
|
+
const r = await stores.l1.purgeRetired(targets, 'cleanup-retired', { files });
|
|
1736
|
+
// F19:图投影的两张索引表全仓原本**只增不减**。记录行删了而投影行还在,
|
|
1737
|
+
// 从快照恢复回来的记录会被当成“已投影”而永不重投影(检索图静默缺一条,不报错)。
|
|
1738
|
+
// 只在真删成功时抹:干跑与中止都不动图——图是派生索引,不该因为一次预览被改。
|
|
1739
|
+
const graphForgotten = !r.aborted && r.purged > 0 ? (stores.graph?.forgetRecords(targets) ?? 0) : 0;
|
|
1422
1740
|
const resp = {
|
|
1423
1741
|
dryRun: false,
|
|
1424
1742
|
targets: targets.length,
|
|
@@ -1427,6 +1745,16 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1427
1745
|
dir: r.dir,
|
|
1428
1746
|
name: r.name,
|
|
1429
1747
|
diffs: r.diffs,
|
|
1748
|
+
otherChanged: r.otherChanged,
|
|
1749
|
+
archiveMd: r.archiveMd,
|
|
1750
|
+
files: r.files,
|
|
1751
|
+
graphForgotten,
|
|
1752
|
+
items: purgeRecords.slice(0, 50).map((x) => ({
|
|
1753
|
+
id: x.id,
|
|
1754
|
+
name: x.name ?? '',
|
|
1755
|
+
type: x.type,
|
|
1756
|
+
retiredAt: x.validTo === undefined ? '' : new Date(x.validTo).toISOString(),
|
|
1757
|
+
})),
|
|
1430
1758
|
};
|
|
1431
1759
|
return resp;
|
|
1432
1760
|
}
|
|
@@ -1889,10 +2217,17 @@ export function hitToUiRecord(r, extra) {
|
|
|
1889
2217
|
retiredReason: retired ? (mark?.reason ?? 'unknown') : null,
|
|
1890
2218
|
agingWeight: extra?.agingWeight ?? null,
|
|
1891
2219
|
usage: extra?.usage ?? null,
|
|
1892
|
-
// G3 成熟度:读时派生(priority + used +
|
|
1893
|
-
maturity: computeMaturity({
|
|
2220
|
+
// G3 成熟度:读时派生(priority + used + 老化 + 人工最近一笔),无额外端点/存储。
|
|
2221
|
+
maturity: computeMaturity({
|
|
2222
|
+
priority: r.priority ?? 60,
|
|
2223
|
+
usage: extra?.usage ?? null,
|
|
2224
|
+
agingWeight: extra?.agingWeight ?? null,
|
|
2225
|
+
mark: extra?.userMark ?? null,
|
|
2226
|
+
}),
|
|
1894
2227
|
// G1 使用场景:从命名空间保留键读出(功能关/无键 → undefined)。
|
|
1895
2228
|
usageScenario: Array.isArray(r.metadata?.dsh_usage_scenario) ? r.metadata?.dsh_usage_scenario : undefined,
|
|
2229
|
+
// 人给相关度均值:读时从 sidecar 派生(不进存储/快照);未打分 → 字段缺席。
|
|
2230
|
+
userRelevance: extra?.userRelevance ?? undefined,
|
|
1896
2231
|
};
|
|
1897
2232
|
}
|
|
1898
2233
|
/**
|
|
@@ -97,6 +97,14 @@ export declare class EmbeddingManager {
|
|
|
97
97
|
private applyBusy;
|
|
98
98
|
private reindex;
|
|
99
99
|
private reindexCancel;
|
|
100
|
+
/**
|
|
101
|
+
* 上一次重建的留痕(类型就地取自契约,不另开 import)。
|
|
102
|
+
*
|
|
103
|
+
* 为什么必须存:进度条只在 `running` 期间存在,而“待补 0 条”的重建会瞬间跑完,
|
|
104
|
+
* 界面上与“点了没反应”长得一模一样——真机反馈就是这么读的。留痕存**前后待补数**,
|
|
105
|
+
* 让“效果”是两个可比数字而不是一句“我转了一下”。
|
|
106
|
+
*/
|
|
107
|
+
private reindexLast;
|
|
100
108
|
/**
|
|
101
109
|
* 向量计数缓存(分级 TTL)。
|
|
102
110
|
*
|
|
@@ -187,6 +187,14 @@ export class EmbeddingManager {
|
|
|
187
187
|
cancelled: false,
|
|
188
188
|
};
|
|
189
189
|
reindexCancel = false;
|
|
190
|
+
/**
|
|
191
|
+
* 上一次重建的留痕(类型就地取自契约,不另开 import)。
|
|
192
|
+
*
|
|
193
|
+
* 为什么必须存:进度条只在 `running` 期间存在,而“待补 0 条”的重建会瞬间跑完,
|
|
194
|
+
* 界面上与“点了没反应”长得一模一样——真机反馈就是这么读的。留痕存**前后待补数**,
|
|
195
|
+
* 让“效果”是两个可比数字而不是一句“我转了一下”。
|
|
196
|
+
*/
|
|
197
|
+
reindexLast;
|
|
190
198
|
/**
|
|
191
199
|
* 向量计数缓存(分级 TTL)。
|
|
192
200
|
*
|
|
@@ -471,6 +479,27 @@ export class EmbeddingManager {
|
|
|
471
479
|
async reindexNow() {
|
|
472
480
|
this.reindexCancel = false;
|
|
473
481
|
this.reindex = { running: true, l1Done: 0, l1Total: 0, l0Done: 0, l0Total: 0, startedAt: Date.now(), cancelled: false };
|
|
482
|
+
// 重建**前**的待补数:先破缓存再取,否则拿到的可能是 TTL 里的旧值(那会让“前后差”假报 0)。
|
|
483
|
+
// 口径故意与界面用的同一个(相减法),不拿精确口径去对展示数字,否则两边永远对不上。
|
|
484
|
+
this.invalidateVecCache();
|
|
485
|
+
const v0 = this.vectorCounts();
|
|
486
|
+
const missingBefore = { l1: v0.l1.missing, l0: v0.l0.missing };
|
|
487
|
+
const finish = (cancelled, error) => {
|
|
488
|
+
this.invalidateVecCache();
|
|
489
|
+
const v1 = this.vectorCounts();
|
|
490
|
+
const at = Date.now();
|
|
491
|
+
this.reindexLast = {
|
|
492
|
+
finishedAt: at,
|
|
493
|
+
durationMs: Math.max(0, at - this.reindex.startedAt),
|
|
494
|
+
processed: this.reindex.l1Done + this.reindex.l0Done,
|
|
495
|
+
planned: this.reindex.l1Total + this.reindex.l0Total,
|
|
496
|
+
failed: failedTotal,
|
|
497
|
+
cancelled,
|
|
498
|
+
...(error === undefined ? {} : { error }),
|
|
499
|
+
missingBefore,
|
|
500
|
+
missingAfter: { l1: v1.l1.missing, l0: v1.l0.missing },
|
|
501
|
+
};
|
|
502
|
+
};
|
|
474
503
|
let failedTotal = 0;
|
|
475
504
|
try {
|
|
476
505
|
const r1 = await this.deps.l1.reindex({
|
|
@@ -493,6 +522,7 @@ export class EmbeddingManager {
|
|
|
493
522
|
this.reindex.cancelled = cancelled;
|
|
494
523
|
this.reindex.running = false;
|
|
495
524
|
this.invalidateVecCache(); // 收尾:计数必须立刻反映重建结果(TTL 可能还压着 1s 旧值)
|
|
525
|
+
finish(cancelled);
|
|
496
526
|
return { cancelled, failedTotal };
|
|
497
527
|
}
|
|
498
528
|
catch (err) {
|
|
@@ -500,6 +530,7 @@ export class EmbeddingManager {
|
|
|
500
530
|
this.reindex.running = false;
|
|
501
531
|
this.reindex.error = message;
|
|
502
532
|
this.invalidateVecCache();
|
|
533
|
+
finish(false, message);
|
|
503
534
|
return { cancelled: false, failedTotal, error: `重嵌入失败: ${message}` };
|
|
504
535
|
}
|
|
505
536
|
}
|
|
@@ -538,7 +569,7 @@ export class EmbeddingManager {
|
|
|
538
569
|
download: this.downloader.getProgress(),
|
|
539
570
|
apply: { phase: this.applyPhase, message: this.applyMessage, startedAt: this.applyStartedAt, busy: this.applyBusy },
|
|
540
571
|
local: this.localSvc ? { state: this.localSvc.getState(), error: this.localSvc.getLoadError() } : null,
|
|
541
|
-
reindex: { ...this.reindex },
|
|
572
|
+
reindex: { ...this.reindex, last: this.reindexLast },
|
|
542
573
|
vectors: this.vectorsCached(),
|
|
543
574
|
activeNote: this.activeNote,
|
|
544
575
|
};
|
|
@@ -562,7 +593,13 @@ export class EmbeddingManager {
|
|
|
562
593
|
const count = (kind) => {
|
|
563
594
|
const skip = this.deps.db.getVecSkipSet(kind);
|
|
564
595
|
const embedded = kind === 'l1' ? this.deps.db.countL1Vec() : this.deps.db.countL0Vec();
|
|
565
|
-
|
|
596
|
+
// 相减法还有第二条前提,之前漏了:**分母必须是活跃行**。退场行按设计就合法地没有
|
|
597
|
+
// 向量(撤检索面时 vec 行被 detach),把它们算进 total 会让 missing 恒等于退场数、
|
|
598
|
+
// 永不归零;而真正驱动补齐的 `countVecMissing`/`getL1ForReindex` 都排除退场行,于是
|
|
599
|
+
// 界面上同时出现“待补 114 条”与“重建索引无事可做”。本机实测:1280 总 / 114 退场 /
|
|
600
|
+
// 1166 活跃且全部已嵌,正是这个形状。`countL1Retired` 失败给 -1,按 0 处理退回旧行为。
|
|
601
|
+
const retired = kind === 'l1' ? Math.max(0, this.deps.db.countL1Retired()) : 0;
|
|
602
|
+
const total = (kind === 'l1' ? this.deps.db.countL1() : this.deps.db.countL0()) - retired;
|
|
566
603
|
// 缺失数走**相减**而非 LEFT JOIN:见下方 vectorsCached 上方的说明。
|
|
567
604
|
const missing = embedded < 0 || total < 0 ? -1 : Math.max(0, total - embedded - skip.size);
|
|
568
605
|
return { embedded, total, missing, skipped: skip.size };
|
|
@@ -27,3 +27,5 @@ export declare const SESSION_RECALLED_FILE_VERSION = 1;
|
|
|
27
27
|
export declare const PENDING_FILE_VERSION = 1;
|
|
28
28
|
/** `reconcile-state.json`。 */
|
|
29
29
|
export declare const RECONCILE_STATE_FILE_VERSION = 1;
|
|
30
|
+
/** `mem-md-ledger.json`(md 投射指纹账本,N9;派生态,删了可重建)。*/
|
|
31
|
+
export declare const MEM_MD_LEDGER_FILE_VERSION = 1;
|
|
@@ -27,3 +27,5 @@ export const SESSION_RECALLED_FILE_VERSION = 1;
|
|
|
27
27
|
export const PENDING_FILE_VERSION = 1;
|
|
28
28
|
/** `reconcile-state.json`。 */
|
|
29
29
|
export const RECONCILE_STATE_FILE_VERSION = 1;
|
|
30
|
+
/** `mem-md-ledger.json`(md 投射指纹账本,N9;派生态,删了可重建)。*/
|
|
31
|
+
export const MEM_MD_LEDGER_FILE_VERSION = 1;
|