dsh-prime-memory 0.26.5 → 0.26.6
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.md +39 -0
- package/README.en.md +62 -1
- package/README.md +52 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +8 -0
- package/dist/contract.d.ts +2 -0
- package/dist/embedding-child.cjs +192 -0
- package/dist/embedding-worker.cjs +2 -0
- package/dist/graph/apply.d.ts +9 -0
- package/dist/graph/apply.js +12 -2
- package/dist/graph/search.d.ts +11 -2
- package/dist/graph/search.js +38 -10
- package/dist/hooks/recall.js +28 -1
- package/dist/index.js +8 -1
- package/dist/llm.d.ts +7 -2
- package/dist/llm.js +14 -22
- package/dist/pipeline/graph.d.ts +17 -1
- package/dist/pipeline/graph.js +45 -2
- package/dist/pipeline/l1.js +87 -26
- package/dist/pipeline/md-sync.js +4 -2
- package/dist/pipeline/runner.d.ts +26 -0
- package/dist/pipeline/runner.js +74 -7
- package/dist/pipeline/slot-migrate.d.ts +3 -2
- package/dist/pipeline/slot-migrate.js +3 -2
- package/dist/projection/recall.d.ts +10 -3
- package/dist/projection/recall.js +14 -0
- package/dist/settings.js +3 -0
- package/dist/stats.d.ts +1 -1
- package/dist/stats.js +17 -11
- package/dist/store/embedding-source.d.ts +4 -2
- package/dist/store/embedding-source.js +5 -2
- package/dist/store/graph-store.d.ts +71 -1
- package/dist/store/graph-store.js +221 -8
- package/dist/store/l1.d.ts +5 -0
- package/dist/store/l1.js +18 -2
- package/dist/store/local-embedding.d.ts +9 -5
- package/dist/store/local-embedding.js +137 -17
- package/dist/store/memory-md-disable.d.ts +3 -2
- package/dist/store/memory-md.d.ts +3 -3
- package/dist/store/memory-md.js +6 -4
- package/dist/store/runtime-installer.d.ts +13 -1
- package/dist/store/runtime-installer.js +40 -2
- package/dist/store/sqlite.d.ts +24 -12
- package/dist/store/sqlite.js +97 -31
- package/dist/store/supersede.d.ts +10 -2
- package/dist/store/supersede.js +10 -2
- package/dist/store/validity.d.ts +150 -0
- package/dist/store/validity.js +209 -0
- package/dist/tools/index.d.ts +15 -1
- package/dist/tools/index.js +393 -11
- package/dist/util/json-repair.d.ts +48 -0
- package/dist/util/json-repair.js +285 -0
- package/dist/validity-backfill.d.ts +78 -0
- package/dist/validity-backfill.js +160 -0
- package/dsh.plugin.json +2 -2
- package/package.json +2 -2
package/dist/store/l1.d.ts
CHANGED
|
@@ -252,6 +252,11 @@ export declare class L1Store {
|
|
|
252
252
|
};
|
|
253
253
|
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
254
254
|
getByIds(ids: string[]): MemoryRecord[];
|
|
255
|
+
/**
|
|
256
|
+
* 反向取代链(task_4.4,门3):返回"被 newId 取代"的退场记录 id。
|
|
257
|
+
* 走 MemoryDb 的 LIKE 全扫缝(百~千量级;阈值告警在那边)。
|
|
258
|
+
*/
|
|
259
|
+
findSupersededTargets(newId: string): string[];
|
|
255
260
|
/** 外部导入幂等 pre-check(轨3-P0):ext_key 命中已存在记录则返回其 id,否则 undefined。 */
|
|
256
261
|
findIdByExtKey(extKey: string): string | undefined;
|
|
257
262
|
/**
|
package/dist/store/l1.js
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync, mkdirSync, promises as fs, writeFileSync } from 'node:fs';
|
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import { familyForType, isScopeVisible } from '../types.js';
|
|
12
12
|
import { isRetired } from './supersede.js';
|
|
13
|
+
import { stampValidity } from './validity.js';
|
|
13
14
|
import { exportThenPurge, readSnapshotManifest, readSnapshotRecords, restoreL1Snapshot, selectSnapshotTargets, snapshotDirFor, listSnapshots as listSnapshotsIn } from './l1-snapshot.js';
|
|
14
15
|
import { EmbedHelper, NoopEmbeddingService } from './embedding.js';
|
|
15
16
|
import { appendJsonl, dayKey, ensureDir, readJsonl } from '../util/io.js';
|
|
@@ -392,6 +393,13 @@ export class L1Store {
|
|
|
392
393
|
getByIds(ids) {
|
|
393
394
|
return this.db.getL1ByIds(ids);
|
|
394
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* 反向取代链(task_4.4,门3):返回"被 newId 取代"的退场记录 id。
|
|
398
|
+
* 走 MemoryDb 的 LIKE 全扫缝(百~千量级;阈值告警在那边)。
|
|
399
|
+
*/
|
|
400
|
+
findSupersededTargets(newId) {
|
|
401
|
+
return this.db.findSupersededTargets(newId);
|
|
402
|
+
}
|
|
395
403
|
/** 外部导入幂等 pre-check(轨3-P0):ext_key 命中已存在记录则返回其 id,否则 undefined。 */
|
|
396
404
|
findIdByExtKey(extKey) {
|
|
397
405
|
return this.db.findRecordIdByExtKey(extKey);
|
|
@@ -473,9 +481,14 @@ export class L1Store {
|
|
|
473
481
|
async appendNew(records) {
|
|
474
482
|
if (records.length === 0)
|
|
475
483
|
return;
|
|
484
|
+
// 写侧标注汇流口(task_3.2):所有新写路径(pipeline / memory_add / import / memport /
|
|
485
|
+
// 重建保留…)都经这里,统一盖 dsh_validity 快照;源头盖过的行(pipeline 带 evidence、
|
|
486
|
+
// 工具带 llm_inferred)保留其 validToSource,未盖过的补 'unknown'(fail-open)。
|
|
487
|
+
const stampNow = Date.now();
|
|
476
488
|
for (const r of records) {
|
|
477
489
|
if (!r.family)
|
|
478
490
|
r.family = familyForType(r.type);
|
|
491
|
+
stampValidity(r, stampNow);
|
|
479
492
|
}
|
|
480
493
|
// 起名必须在**追加事实源之前**:名字是文件身份键,进了 JSONL 才能跟记录一起重生。
|
|
481
494
|
// 这批还没进库,所以只算名字不落库(persist=false),由下面的 upsert 一并写。
|
|
@@ -506,7 +519,9 @@ export class L1Store {
|
|
|
506
519
|
async upsert(record) {
|
|
507
520
|
if (!record.family)
|
|
508
521
|
record.family = familyForType(record.type);
|
|
509
|
-
// merge 分支会继承胜方的 name
|
|
522
|
+
// 写侧标注汇流口(与 appendNew 同口径):merge 分支会继承胜方的 name;
|
|
523
|
+
// 两边都没名字时在这里补上,别让新铸的行继续带空名。
|
|
524
|
+
stampValidity(record, Date.now());
|
|
510
525
|
this.assignMissingNames([record], { persist: false });
|
|
511
526
|
const vec = (await this.helper.batch([record.content]))[0];
|
|
512
527
|
if (!this.db.upsertL1(record, vec)) {
|
|
@@ -584,8 +599,9 @@ export class L1Store {
|
|
|
584
599
|
// **只清理确实处于退场态的记录**。这道复核必须在删除发生的地方(而不是调用方):
|
|
585
600
|
// 否则任何调用方传一个 id 列表就能绕过软删、把活动记忆直接物理抹掉——
|
|
586
601
|
// 那等于给"先导出后清理"留了一条硬删后门。
|
|
602
|
+
// 门1 解耦:判据与全仓同为 marker 单源 `isRetired`(不再拿 valid_to 当退场凭据)。
|
|
587
603
|
const known = new Map(this.db.getL1ByIds(ids).map((r) => [r.id, r]));
|
|
588
|
-
const retiredIds = ids.filter((id) => known.get(id)
|
|
604
|
+
const retiredIds = ids.filter((id) => { const r = known.get(id); return r !== undefined && isRetired(r); });
|
|
589
605
|
return exportThenPurge(this.db, this.dataDir, retiredIds, reason, this.logger, new Date(), opts);
|
|
590
606
|
}
|
|
591
607
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -2,7 +2,7 @@ import type { MemoryLogger } from '../types.js';
|
|
|
2
2
|
import type { CatalogEntry } from './model-catalog.js';
|
|
3
3
|
import type { EmbedCallOptions, EmbeddingProviderInfo, EmbeddingService } from './embedding.js';
|
|
4
4
|
type LocalState = 'idle' | 'loading' | 'ready' | 'failed' | 'terminated';
|
|
5
|
-
/** worker
|
|
5
|
+
/** 推理侧启动参数(worker 经 workerData / 子进程经 env DSH_EMBED_CFG;构造后不可变)。 */
|
|
6
6
|
export interface EmbedWorkerData {
|
|
7
7
|
runtimeDir: string;
|
|
8
8
|
modelDir: string;
|
|
@@ -49,11 +49,15 @@ export interface EmbedWorkerChannel {
|
|
|
49
49
|
setOnCrash(cb: (error: string) => void): void;
|
|
50
50
|
}
|
|
51
51
|
export interface LocalEmbeddingOptions {
|
|
52
|
-
/** 数据目录 runtime/(
|
|
52
|
+
/** 数据目录 runtime/(推理侧据此 createRequire 加载 transformers)。 */
|
|
53
53
|
runtimeDir: string;
|
|
54
|
-
/**
|
|
54
|
+
/** 子进程资产路径(默认 dist/embedding-child.cjs;测试可显式指定)。 */
|
|
55
|
+
childPath?: string;
|
|
56
|
+
/** 旧 worker 资产路径(仅 useWorker=true 时生效;默认 dist/embedding-worker.cjs)。 */
|
|
55
57
|
workerPath?: string;
|
|
56
|
-
/**
|
|
58
|
+
/** 回滚逃生门:true 切回 worker_threads 通道(fix #11 前的旧路径)。 */
|
|
59
|
+
useWorker?: boolean;
|
|
60
|
+
/** 通道注入缝(测试用;缺省起真实子进程通道)。 */
|
|
57
61
|
channel?: EmbedWorkerChannel;
|
|
58
62
|
logger?: MemoryLogger;
|
|
59
63
|
maxInputChars?: number;
|
|
@@ -77,7 +81,7 @@ export declare class LocalEmbeddingService implements EmbeddingService {
|
|
|
77
81
|
waitForReady(): Promise<void>;
|
|
78
82
|
embed(text: string, callOpts?: EmbedCallOptions): Promise<Float32Array>;
|
|
79
83
|
embedBatch(texts: string[], callOpts?: EmbedCallOptions): Promise<Float32Array[]>;
|
|
80
|
-
/**
|
|
84
|
+
/** 释放推理子进程与模型(嵌入源切走/关闭时调用;幂等)。terminated 后不可
|
|
81
85
|
* 再复用——防止插件卸载/切走后残留的重嵌循环把模型重新加载常驻(内存泄漏)。 */
|
|
82
86
|
close(): void;
|
|
83
87
|
/** 内层钳制(仅缩短):超时放弃等待(迟到回复由通道按 id 丢弃),调用方降级。 */
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 本地嵌入服务(
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* 本地嵌入服务(子进程隔离版,fix #11):transformers.js 的模型加载与 ONNX 推理
|
|
3
|
+
* 全部在 fork 出的独立子进程执行(resources/embedding-child.cjs),本类只是主线程侧
|
|
4
|
+
* 的协议代理——双重隔离:① onnxruntime-node 的 run/loadModel 是同步调用,留在宿主
|
|
5
|
+
* 事件循环会冻结整个 dsh 页面(0.8.6 修复的真实事故);② 插件 runtime 的 sharp/libvips
|
|
6
|
+
* 与宿主 Electron 的 sharp/libvips 同进程加载会撞 libvips-42.dll 名字(OS LoadLibrary
|
|
7
|
+
* 层冲突)——worker_thread 与主线程同进程,撞名前提仍在(issue #11);子进程自带独立
|
|
8
|
+
* DLL 表,冲突物理消失。
|
|
6
9
|
*
|
|
7
|
-
* - 懒加载:首次嵌入/warmup
|
|
10
|
+
* - 懒加载:首次嵌入/warmup 才让子进程加载模型,close()=kill 释放进程与
|
|
8
11
|
* 模型(嵌入源切走/关闭时调用),terminated 后不可复用;
|
|
9
|
-
* - 模型从数据目录 models/<id>/ 本地加载(
|
|
12
|
+
* - 模型从数据目录 models/<id>/ 本地加载(子进程侧 env.allowRemoteModels=false);
|
|
10
13
|
* - channel 可注入(测试缝):用假通道验证协议与状态机,不触真模型;
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
14
|
+
* - useWorker=true 切回旧 worker_threads 通道(回滚逃生门;若宿主侧 DLL 冲突
|
|
15
|
+
* 将来被根治,可一行切回获得 transfer 零拷贝的微弱性能优势);
|
|
16
|
+
* - callOpts.timeoutMs 经 Promise.race 钉制(迟到回复按 id 丢弃——推理在
|
|
17
|
+
* 子进程无法真正取消,但主线程可以停止等待并降级 FTS);
|
|
13
18
|
* - 池化方式来自模型目录(BGE 系 CLS / Gemma 系 MEAN),normalize 交给
|
|
14
19
|
* pipeline 内建 L2 归一(与远程路径的 sanitizeAndNormalize 语义一致)。
|
|
15
20
|
*/
|
|
21
|
+
import { fork, spawn } from 'node:child_process';
|
|
16
22
|
import * as path from 'node:path';
|
|
17
23
|
import { fileURLToPath } from 'node:url';
|
|
18
24
|
import { Worker } from 'node:worker_threads';
|
|
19
|
-
/** 默认 worker
|
|
25
|
+
/** 默认 worker 资产路径(旧通道,useWorker 逃生门):dist/store/ → dist/embedding-worker.cjs(构建期拷入)。 */
|
|
20
26
|
function defaultWorkerPath() {
|
|
21
27
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'embedding-worker.cjs');
|
|
22
28
|
}
|
|
29
|
+
/** 默认子进程资产路径(fix #11 主通道):dist/store/ → dist/embedding-child.cjs(构建期拷入)。 */
|
|
30
|
+
function defaultChildPath() {
|
|
31
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'embedding-child.cjs');
|
|
32
|
+
}
|
|
23
33
|
/** 真实通道:spawn worker_threads + 自增 id 配对 + 崩溃兜底拒绝。 */
|
|
24
34
|
class RealWorkerChannel {
|
|
25
35
|
worker;
|
|
@@ -86,6 +96,113 @@ class RealWorkerChannel {
|
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* 真实子进程通道(fix #11 默认):fork 独立进程隔离 sharp/libvips DLL 冲突。
|
|
101
|
+
* 与 RealWorkerChannel 同协议语义,差异仅在传输层:
|
|
102
|
+
* - 配置经 env DSH_EMBED_CFG(JSON 字符串,fork env 只收标量);
|
|
103
|
+
* - 向量经 number[][] 序列化回传(IPC JSON 通道不支持 TypedArray;512d×32 条
|
|
104
|
+
* ≈64KB,<5ms,不构成瓶颈),主线程侧收到后重建 Float32Array;
|
|
105
|
+
* - 终止 Windows 走 taskkill /T /F(同 runtime-installer 模式,防只杀包装进程),
|
|
106
|
+
* 其余 SIGTERM + 3s 强杀钉底。
|
|
107
|
+
*/
|
|
108
|
+
class RealChildChannel {
|
|
109
|
+
child;
|
|
110
|
+
pending = new Map();
|
|
111
|
+
nextId = 1;
|
|
112
|
+
terminated = false;
|
|
113
|
+
crashed;
|
|
114
|
+
crashCb;
|
|
115
|
+
constructor(childPath, cfg) {
|
|
116
|
+
this.child = fork(childPath, [], {
|
|
117
|
+
// execPath 显式沿用当前二进制:宿主 Electron 自带 Node runtime,打包后 PATH 里没有 node
|
|
118
|
+
execPath: process.execPath,
|
|
119
|
+
env: { ...process.env, DSH_EMBED_CFG: JSON.stringify(cfg) },
|
|
120
|
+
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
121
|
+
});
|
|
122
|
+
this.child.on('message', (msg) => {
|
|
123
|
+
if (msg && msg.type === 'fatal') {
|
|
124
|
+
this.failAll(`本地嵌入子进程致命错误: ${msg.error ?? '未知'}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const id = msg.id;
|
|
128
|
+
if (typeof id !== 'number')
|
|
129
|
+
return;
|
|
130
|
+
const entry = this.pending.get(id);
|
|
131
|
+
if (!entry)
|
|
132
|
+
return; // 迟到回复(调用方已超时放弃)
|
|
133
|
+
this.pending.delete(id);
|
|
134
|
+
// 子进程侧 vectors 是 number[][](IPC JSON 序列化),本侧重建 Float32Array 保持协议类型不变
|
|
135
|
+
const reply = msg;
|
|
136
|
+
if (reply.ok && reply.type === 'embedded') {
|
|
137
|
+
reply.vectors = reply.vectors.map((v) => new Float32Array(v));
|
|
138
|
+
}
|
|
139
|
+
entry.resolve(reply);
|
|
140
|
+
});
|
|
141
|
+
// error(fork 启动失败等)与 exit(含 fatal 后自杀、OOM 被杀)都钉底拒绝
|
|
142
|
+
this.child.on('error', (err) => this.failAll(`本地嵌入子进程错误: ${err.message}`));
|
|
143
|
+
this.child.on('exit', (code) => {
|
|
144
|
+
if (!this.terminated)
|
|
145
|
+
this.failAll(`本地嵌入子进程退出(code=${code})`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
request(call) {
|
|
149
|
+
// 已释放/已崩溃的通道快速拒绝——send 到死进程是静默无回应,调用方会挂到超时
|
|
150
|
+
if (this.terminated)
|
|
151
|
+
return Promise.reject(new Error('嵌入子进程已释放'));
|
|
152
|
+
if (this.crashed)
|
|
153
|
+
return Promise.reject(new Error(this.crashed));
|
|
154
|
+
const id = this.nextId++;
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
this.pending.set(id, { resolve, reject });
|
|
157
|
+
// 递送失败(通道已关/子进程已死)走 send 回调拒绝,不留挂死的 pending
|
|
158
|
+
this.child.send({ ...call, id }, (err) => {
|
|
159
|
+
if (!err)
|
|
160
|
+
return;
|
|
161
|
+
const entry = this.pending.get(id);
|
|
162
|
+
if (entry) {
|
|
163
|
+
this.pending.delete(id);
|
|
164
|
+
entry.reject(new Error(`嵌入子进程 IPC 发送失败: ${err.message}`));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
terminate() {
|
|
170
|
+
if (this.terminated)
|
|
171
|
+
return;
|
|
172
|
+
this.terminated = true;
|
|
173
|
+
this.failAll('嵌入子进程已释放');
|
|
174
|
+
if (process.platform === 'win32' && this.child.pid !== undefined) {
|
|
175
|
+
// 同 runtime-installer 的 taskkill 模式:Windows 上 child.kill() 信号语义弱,
|
|
176
|
+
// /T 按进程树杀防孤儿;taskkill 自身启动失败回退裸 kill
|
|
177
|
+
const tk = spawn('taskkill', ['/pid', String(this.child.pid), '/T', '/F'], { windowsHide: true });
|
|
178
|
+
tk.on('error', () => this.child.kill());
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.child.kill('SIGTERM');
|
|
182
|
+
// 钉底:子进程卡在同步 native 调用时收不到 SIGTERM → 3s 后强杀;自然退出则撤销定时器
|
|
183
|
+
const force = setTimeout(() => {
|
|
184
|
+
if (this.child.exitCode === null && this.child.signalCode === null)
|
|
185
|
+
this.child.kill('SIGKILL');
|
|
186
|
+
}, 3000);
|
|
187
|
+
this.child.once('exit', () => clearTimeout(force));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
setOnCrash(cb) {
|
|
191
|
+
this.crashCb = cb;
|
|
192
|
+
// 构造与回调注册之间发生的崩溃(fork 即失败等)不丢通知
|
|
193
|
+
if (this.crashed)
|
|
194
|
+
cb(this.crashed);
|
|
195
|
+
}
|
|
196
|
+
failAll(error) {
|
|
197
|
+
for (const [, entry] of this.pending)
|
|
198
|
+
entry.reject(new Error(error));
|
|
199
|
+
this.pending.clear();
|
|
200
|
+
if (!this.terminated && !this.crashed) {
|
|
201
|
+
this.crashed = error;
|
|
202
|
+
this.crashCb?.(error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
89
206
|
export class LocalEmbeddingService {
|
|
90
207
|
state = 'idle';
|
|
91
208
|
loadError = null;
|
|
@@ -96,15 +213,18 @@ export class LocalEmbeddingService {
|
|
|
96
213
|
this.entry = entry;
|
|
97
214
|
this.logger = opts.logger;
|
|
98
215
|
const maxInputChars = opts.maxInputChars && opts.maxInputChars > 0 ? opts.maxInputChars : 5000;
|
|
216
|
+
const cfgObj = {
|
|
217
|
+
runtimeDir: opts.runtimeDir,
|
|
218
|
+
modelDir,
|
|
219
|
+
pooling: entry.pooling,
|
|
220
|
+
dtype: 'q8',
|
|
221
|
+
maxInputChars,
|
|
222
|
+
};
|
|
99
223
|
this.channel =
|
|
100
224
|
opts.channel ??
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
pooling: entry.pooling,
|
|
105
|
-
dtype: 'q8',
|
|
106
|
-
maxInputChars,
|
|
107
|
-
});
|
|
225
|
+
(opts.useWorker
|
|
226
|
+
? new RealWorkerChannel(opts.workerPath ?? defaultWorkerPath(), cfgObj)
|
|
227
|
+
: new RealChildChannel(opts.childPath ?? defaultChildPath(), cfgObj));
|
|
108
228
|
// 崩溃不自愈(换源/重启恢复):拒绝语义沿 EmbedHelper 降级链走 FTS
|
|
109
229
|
this.channel.setOnCrash((error) => {
|
|
110
230
|
if (this.state === 'terminated')
|
|
@@ -186,7 +306,7 @@ export class LocalEmbeddingService {
|
|
|
186
306
|
}
|
|
187
307
|
return reply.vectors;
|
|
188
308
|
}
|
|
189
|
-
/**
|
|
309
|
+
/** 释放推理子进程与模型(嵌入源切走/关闭时调用;幂等)。terminated 后不可
|
|
190
310
|
* 再复用——防止插件卸载/切走后残留的重嵌循环把模型重新加载常驻(内存泄漏)。 */
|
|
191
311
|
close() {
|
|
192
312
|
this.state = 'terminated';
|
|
@@ -12,8 +12,9 @@ export type DisableOutcome = 'renamed' | 'created' | 'already' | 'no-workspace'
|
|
|
12
12
|
export { isRetiredRecord };
|
|
13
13
|
export interface DisableDeps {
|
|
14
14
|
/**
|
|
15
|
-
* 刚被退场的那条记录。调用方应传**退场后**的快照(带 `
|
|
16
|
-
* frontmatter 自然包含
|
|
15
|
+
* 刚被退场的那条记录。调用方应传**退场后**的快照(带 `dsh_superseded` 标记,
|
|
16
|
+
* 门1 解耦后 md 层按 marker 判退场),这样渲染出的 frontmatter 自然包含
|
|
17
|
+
* `retired: true`——凭证自己就说清了它为什么存在。
|
|
17
18
|
*/
|
|
18
19
|
record: MemoryRecord;
|
|
19
20
|
/** `cfg.memExport.root`(只管无 workspace 那批,这里用不到但落点函数要它)。 */
|
|
@@ -63,14 +63,14 @@ export type MdRecordInput = Pick<MemoryRecord, 'id' | 'content' | 'type' | 'prio
|
|
|
63
63
|
*/
|
|
64
64
|
export declare function targetFor(r: MdRecordInput, cfgRoot: string | undefined, dataDir: string, logger?: MemoryLogger): MdTarget;
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
* `
|
|
66
|
+
* 退场判据:与库侧同口径(门1 解耦后**只认 marker**,与 `supersede.isRetired`/
|
|
67
|
+
* `retiredL1Sql` 一致;valid_to 回归现实有效期,不再拿它判退场)。
|
|
68
68
|
*
|
|
69
69
|
* 为什么住在本文件:导出(要不要把文件放回非退场路径)与失效联动(后缀该不该带)
|
|
70
70
|
* 两侧都得用同一个判据,否则“列表说它退了、文件却没后缀”会回来。放 `memory-md-disable.ts`
|
|
71
71
|
* 会让 `memory-md-export.ts` 反向 import 它形成循环(那边已经 import 这边)。
|
|
72
72
|
*/
|
|
73
|
-
export declare function isRetiredRecord(r: Pick<MemoryRecord, '
|
|
73
|
+
export declare function isRetiredRecord(r: Pick<MemoryRecord, 'metadata'>): boolean;
|
|
74
74
|
/**
|
|
75
75
|
* 投射文件的失效路径:`x.memory.md` → `x.memory.md.disable`。
|
|
76
76
|
* 已带后缀的原样返回(幂等靠这个性质,而不是靠调用方记住判重)。
|
package/dist/store/memory-md.js
CHANGED
|
@@ -21,6 +21,7 @@ import { memoryTypeDirs, UNCLASSIFIED_DIR } from '../memory-types.js';
|
|
|
21
21
|
import { deriveName } from '../util/md-name.js';
|
|
22
22
|
import { normalizeContentMd } from '../util/md-normalize.js';
|
|
23
23
|
import { sanitizeFilename } from './scenes.js';
|
|
24
|
+
import { readSupersedeMarker } from './supersede.js';
|
|
24
25
|
/**
|
|
25
26
|
* 允许当目录名的 type 值(pm-name-distill task_1.3:由词表单一真源派生,不再手抄)。
|
|
26
27
|
*
|
|
@@ -127,15 +128,15 @@ function warnMissingWorkspace(logger, ws) {
|
|
|
127
128
|
logger.warn(`[memory] workspace 目录不存在,该条投射落点退回单根: ${ws}`);
|
|
128
129
|
}
|
|
129
130
|
/**
|
|
130
|
-
*
|
|
131
|
-
* `
|
|
131
|
+
* 退场判据:与库侧同口径(门1 解耦后**只认 marker**,与 `supersede.isRetired`/
|
|
132
|
+
* `retiredL1Sql` 一致;valid_to 回归现实有效期,不再拿它判退场)。
|
|
132
133
|
*
|
|
133
134
|
* 为什么住在本文件:导出(要不要把文件放回非退场路径)与失效联动(后缀该不该带)
|
|
134
135
|
* 两侧都得用同一个判据,否则“列表说它退了、文件却没后缀”会回来。放 `memory-md-disable.ts`
|
|
135
136
|
* 会让 `memory-md-export.ts` 反向 import 它形成循环(那边已经 import 这边)。
|
|
136
137
|
*/
|
|
137
138
|
export function isRetiredRecord(r) {
|
|
138
|
-
return
|
|
139
|
+
return readSupersedeMarker(r.metadata) !== undefined;
|
|
139
140
|
}
|
|
140
141
|
/**
|
|
141
142
|
* 投射文件的失效路径:`x.memory.md` → `x.memory.md.disable`。
|
|
@@ -230,7 +231,8 @@ export function renderRecord(r) {
|
|
|
230
231
|
// 「换了 id、正文没动」也误报被改。
|
|
231
232
|
lines.push(fmLine('hash', contentHash(r.content)));
|
|
232
233
|
// 退场标记只在真退场时写:不留 `retired: false` 这种噪声键(键集越稳,人越不容易改错)。
|
|
233
|
-
|
|
234
|
+
// 判据走本文件的 `isRetiredRecord`(门1 解耦后只认 marker,与库侧同口径)。
|
|
235
|
+
if (isRetiredRecord(r))
|
|
234
236
|
lines.push(fmLine('retired', true));
|
|
235
237
|
lines.push('---', '');
|
|
236
238
|
// 正文过一道版式归一(P4.2):无结构存量文本出口就是标准形状。归一是**幂等**的,
|
|
@@ -36,8 +36,20 @@ export declare class RuntimeInstaller {
|
|
|
36
36
|
private pkgJsonPath;
|
|
37
37
|
/** 已就位版本(读 package.json;未安装返回 null)。 */
|
|
38
38
|
installedVersion(): Promise<string | null>;
|
|
39
|
-
/** 是否就绪(版本精确匹配目标)。 */
|
|
39
|
+
/** 是否就绪(版本精确匹配目标 + sharp 平台原生件就位)。 */
|
|
40
40
|
isReady(): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* 当前平台的 sharp 原生件候选路径(测试注入缝与单测共用):
|
|
43
|
+
* 绑定 .node 在 @img/sharp-<os>-<arch>/lib/,libvips 在 @img/sharp-libvips-<os>-<arch>/lib/。
|
|
44
|
+
* linux 有 glibc/musl 双候选;libvips 库文件名含版本串(仅 win 的 libvips-42.dll 稳定),
|
|
45
|
+
* 故统一按 lib 目录内存在 libvips* 文件判定,不钉文件名。
|
|
46
|
+
*/
|
|
47
|
+
sharpNativeCandidates(): Array<{
|
|
48
|
+
binding: string;
|
|
49
|
+
libvipsDir: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** 任一候选同时具备绑定 .node 与 libvips 库文件 → true(ENOENT/读取失败均计缺失)。 */
|
|
52
|
+
private sharpNativePresent;
|
|
41
53
|
/**
|
|
42
54
|
* 确保运行时就位:版本匹配直接就绪;否则安装(忙时并 await 同一次任务)。
|
|
43
55
|
* 返回是否就绪(失败/取消返回 false 并在 progress.error 说明原因)。
|
|
@@ -102,9 +102,47 @@ export class RuntimeInstaller {
|
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
/** 是否就绪(版本精确匹配目标)。 */
|
|
105
|
+
/** 是否就绪(版本精确匹配目标 + sharp 平台原生件就位)。 */
|
|
106
106
|
async isReady() {
|
|
107
|
-
|
|
107
|
+
if ((await this.installedVersion()) !== this.target)
|
|
108
|
+
return false;
|
|
109
|
+
// fix #11 辅助检测:--ignore-scripts 安装半坏(平台原生包缺失/被剪)时,
|
|
110
|
+
// transformers 顶层 require('sharp') 必炸——判存在性,缺失报未就绪触发重装。
|
|
111
|
+
if (!(await this.sharpNativePresent())) {
|
|
112
|
+
this.logger?.warn('[memory] 运行时 sharp 平台原生件缺失(半装状态),isReady=false 触发重装');
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 当前平台的 sharp 原生件候选路径(测试注入缝与单测共用):
|
|
119
|
+
* 绑定 .node 在 @img/sharp-<os>-<arch>/lib/,libvips 在 @img/sharp-libvips-<os>-<arch>/lib/。
|
|
120
|
+
* linux 有 glibc/musl 双候选;libvips 库文件名含版本串(仅 win 的 libvips-42.dll 稳定),
|
|
121
|
+
* 故统一按 lib 目录内存在 libvips* 文件判定,不钉文件名。
|
|
122
|
+
*/
|
|
123
|
+
sharpNativeCandidates() {
|
|
124
|
+
const arch = process.arch; // x64 | arm64
|
|
125
|
+
const osKeys = process.platform === 'linux' ? ['linux', 'linuxmusl'] : process.platform === 'darwin' ? ['darwin'] : ['win32'];
|
|
126
|
+
const img = path.join(this.runtimeDir, 'node_modules', '@img');
|
|
127
|
+
return osKeys.map((k) => ({
|
|
128
|
+
binding: path.join(img, `sharp-${k}-${arch}`, 'lib', `sharp-${k}-${arch}.node`),
|
|
129
|
+
libvipsDir: path.join(img, `sharp-libvips-${k}-${arch}`, 'lib'),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
/** 任一候选同时具备绑定 .node 与 libvips 库文件 → true(ENOENT/读取失败均计缺失)。 */
|
|
133
|
+
async sharpNativePresent() {
|
|
134
|
+
for (const c of this.sharpNativeCandidates()) {
|
|
135
|
+
try {
|
|
136
|
+
await fs.access(c.binding);
|
|
137
|
+
const files = await fs.readdir(c.libvipsDir);
|
|
138
|
+
if (files.some((f) => f.startsWith('libvips')))
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* 本候选不命中,试下一个 */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
108
146
|
}
|
|
109
147
|
/**
|
|
110
148
|
* 确保运行时就位:版本匹配直接就绪;否则安装(忙时并 await 同一次任务)。
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -168,20 +168,22 @@ export declare class MemoryDb {
|
|
|
168
168
|
* **软删**(记忆退场):保留主表行,撤出检索面。
|
|
169
169
|
*
|
|
170
170
|
* 与 `deleteL1Batch` 的差别**只有一处**:不动 `l1_records` 行本身。
|
|
171
|
-
* `
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
171
|
+
* `metadata_json` 写取代标记 → 记录仍能被 `listL1` 列出、能被 `clearRetireMarker`
|
|
172
|
+
* + upsert 恢复;而 FTS 与向量行照旧删除,于是检索面(含去重候选召回)自然看不到它。
|
|
173
|
+
*
|
|
174
|
+
* 门1 解耦(expiry-tiering R1/task_2.3):退场**只写 marker,不再闭合 valid_to**——
|
|
175
|
+
* valid_to 回归纯现实有效期,退场与否由 marker 单源回答(`isRetired`)。
|
|
175
176
|
*
|
|
176
177
|
* 顺序刻意如此:先打标记(可逆的那一半),再撤检索面,且整体在一个事务里。
|
|
177
178
|
* 反过来先撤索引而打标记失败,记录会落在"检索不到、也没被标记"的状态 ——
|
|
178
179
|
* 既查不出来也恢复不了,是最坏的一种中间态。
|
|
179
180
|
*
|
|
180
|
-
* **幂等**:已退场(
|
|
181
|
+
* **幂等**:已退场(带 marker)的 id 不再重复写标记,
|
|
181
182
|
* 保留首次退场的原因与时刻(「谁先取代了它」不该被后一次调用改写)。
|
|
182
183
|
*
|
|
183
|
-
* **不调** `graphStore.markSourcesDeleted
|
|
184
|
-
*
|
|
184
|
+
* **不调** `graphStore.markSourcesDeleted`(那是"来源已物理消失"的传播,软删行一直在),
|
|
185
|
+
* 而是调 `markSourcesRetired`(计划 R9 对偶刀):按"来源是否全部退场态"重算清扫,
|
|
186
|
+
* 全退场的 active/disputed 节点·边标 archived 墓碑(派生投影由当前事实重算,非事件改写)。
|
|
185
187
|
*/
|
|
186
188
|
retireL1Batch(ids: string[], info: SupersedeInfo): number;
|
|
187
189
|
/**
|
|
@@ -193,12 +195,14 @@ export declare class MemoryDb {
|
|
|
193
195
|
* 清掉退场标记(恢复的**前半**)。返回清完标记的记录,供调用方 re-upsert 以重建
|
|
194
196
|
* FTS/向量 —— 那条路径(`upsertL1InTx`)已存在,不在这里重复实现。
|
|
195
197
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
198
|
+
* 门1 解耦(task_2.3 的对偶面):只剥 marker,**不碰 valid_to**——
|
|
199
|
+
* 行若本就带着真实历史有效期(导入/抽取写入),恢复不该把它抹掉;
|
|
200
|
+
* 旧版 retire 写进 valid_to 的退场时刻对新行只影响 classify 落到
|
|
201
|
+
* historical/expired_suspect(仍可检可注入,带标注),由 validity-backfill 逐步纠偏。
|
|
202
|
+
* 返回的 `validTo` 保持原值——upsert 照原样写回,不会静默改列。
|
|
199
203
|
*/
|
|
200
204
|
clearRetireMarker(ids: string[]): MemoryRecord[];
|
|
201
|
-
/** 已退场记录列表(面板用)
|
|
205
|
+
/** 已退场记录列表(面板用):与 `retiredL1Sql()` 同判据(marker 单源)。失败返回空。 */
|
|
202
206
|
listRetiredL1(opts: {
|
|
203
207
|
limit: number;
|
|
204
208
|
offset: number;
|
|
@@ -530,7 +534,15 @@ export declare class MemoryDb {
|
|
|
530
534
|
* 返回扫描行数;存储降级返回 0。
|
|
531
535
|
*/
|
|
532
536
|
scanL1Metadata(cb: (recordId: string, metadata: Record<string, unknown> | null) => void): number;
|
|
533
|
-
/**
|
|
537
|
+
/**
|
|
538
|
+
* 反向取代链查询(task_4.4,门3):"谁被这条记录取代了"——扫 metadata_json 里
|
|
539
|
+
* 取代标记的 `by` 指向本 id 的行。百~千量级全扫可接受(与 `retiredL1Sql` 同属
|
|
540
|
+
* LIKE 面);超过阈值时留 warn——真到万级该建索引,不在本计划面里预先造。
|
|
541
|
+
* **只回退场行**:"被取代者"正是带 marker 的旧行(活跃行若 by 指过来是脏数据);
|
|
542
|
+
* 模式含闭合引号(`"by":"<id>"`),不会前缀误配更长 id。
|
|
543
|
+
*/
|
|
544
|
+
findSupersededTargets(newId: string): string[];
|
|
545
|
+
/** retired 行计数(退场判定与 listRetiredL1 同口径:`retiredL1Sql` marker 单源)。失败返回 0。 */
|
|
534
546
|
retiredL1Count(): number;
|
|
535
547
|
/**
|
|
536
548
|
* Room 计数(**标签自生长分类**):展开 `metadata.tags` 聚合,1 个 slug tag = 1 个 Room。
|