dsh-layered-memory 0.7.0 → 0.7.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/dist/store/l0.js CHANGED
@@ -154,17 +154,20 @@ export class L0Store {
154
154
  opts?.onProgress?.(done, total);
155
155
  continue;
156
156
  }
157
+ const pending = [];
157
158
  chunk.forEach((c, j) => {
158
159
  if (isZeroVector(vecs[j])) {
159
160
  skipped++;
160
161
  skippedNow.push(c.id);
161
162
  return;
162
163
  }
163
- if (this.db.updateL0Vec(c.id, vecs[j], ''))
164
- written++;
165
- else
166
- failed++;
164
+ pending.push({ id: c.id, embedding: vecs[j] });
167
165
  });
166
+ if (pending.length > 0) {
167
+ const ok = this.db.updateL0VecBatch(pending, '');
168
+ written += ok;
169
+ failed += pending.length - ok;
170
+ }
168
171
  done += chunk.length;
169
172
  opts?.onProgress?.(done, total);
170
173
  }
package/dist/store/l1.js CHANGED
@@ -221,17 +221,20 @@ export class L1Store {
221
221
  opts?.onProgress?.(done, total);
222
222
  continue;
223
223
  }
224
+ const pending = [];
224
225
  chunk.forEach((c, j) => {
225
226
  if (isZeroVector(vecs[j])) {
226
227
  skipped++;
227
228
  skippedNow.push(c.id);
228
229
  return;
229
230
  }
230
- if (this.db.updateL1Vec(c.id, vecs[j]))
231
- written++;
232
- else
233
- failed++;
231
+ pending.push({ id: c.id, embedding: vecs[j] });
234
232
  });
233
+ if (pending.length > 0) {
234
+ const ok = this.db.updateL1VecBatch(pending);
235
+ written += ok;
236
+ failed += pending.length - ok;
237
+ }
235
238
  done += chunk.length;
236
239
  opts?.onProgress?.(done, total);
237
240
  }
@@ -28,14 +28,17 @@ export declare class RuntimeInstaller {
28
28
  private readonly target;
29
29
  private readonly logger?;
30
30
  private readonly spawnImpl;
31
+ /** 随包 lockfile 路径(测试可注入;默认取 dist 根下构建期拷入的资产)。 */
32
+ private readonly lockfileSource;
31
33
  private progress;
32
34
  private child;
33
35
  private current;
34
- /** 安装超时(npm 卡死不罕见:registry 停滞即永挂,applyBusy 会被锁死)。 */
36
+ /** 安装超时(npm 卡死不罕见:registry 停滞即永挂,applyBusy 会被锁死)。每次 spawn 独立计时。 */
35
37
  private static INSTALL_TIMEOUT_MS;
36
38
  constructor(dataDir: string, targetVersion: string, opts?: {
37
39
  logger?: MemoryLogger;
38
40
  spawnImpl?: SpawnImpl;
41
+ lockfileSource?: string;
39
42
  });
40
43
  /** 包内模块名(与钉死版本一起构成安装目标)。 */
41
44
  static packageName: string;
@@ -56,5 +59,7 @@ export declare class RuntimeInstaller {
56
59
  /** 从 runtime 目录解析已安装的 transformers 模块(LocalEmbeddingService 用)。 */
57
60
  resolveModule(): unknown;
58
61
  private pushLine;
62
+ /** 跑一次 npm 子进程(采集尾行 + 超时 kill),返回退出码(null = 被杀死/启动失败)。 */
63
+ private runNpm;
59
64
  private installOnce;
60
65
  }
@@ -4,7 +4,11 @@
4
4
  * 插件 npm 包本体不带重依赖,不用本地嵌入的用户零成本。
5
5
  *
6
6
  * - 安装位置:数据目录 runtime/(自带 package.json 锚定,防 npm 向上层目录逃逸安装);
7
- * - 子进程 npm install --ignore-scripts --no-audit --no-fund,钉死精确版本;
7
+ * - 子进程 npm ci --ignore-scripts --no-audit --no-fund,钉死精确版本 + 随包 lockfile
8
+ * (resources/runtime-package-lock.json,构建期拷入 dist/)锁定完整传递依赖树——
9
+ * 纯 install 只锁直接依赖的精确版本,传递依赖按 semver 浮动解析,registry 端
10
+ * 后续发布/投毒会随安装时间漂移;lockfile 把树冻结在作者侧。ci 失败(lock 与
11
+ * package.json 漂移等)自动回退 npm install 精确版本(可用性优先,树不再受锁);
8
12
  * - 进度(用户硬性要求:不能傻等):npm 非交互模式无百分比 API,采用不确定进度——
9
13
  * 已耗时 + 子进程 stdout/stderr 尾行实时流出 + 可 kill;
10
14
  * - 幂等:已装版本 == 目标版本直接就绪;版本漂移(插件升级换了钉死版本)重装覆盖。
@@ -13,6 +17,7 @@ import { spawn } from 'node:child_process';
13
17
  import { createRequire } from 'node:module';
14
18
  import { promises as fs } from 'node:fs';
15
19
  import * as path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
16
21
  /** 钉死的 transformers.js 版本(D8:精确版本,升级插件时在此变更并重装运行时)。 */
17
22
  export const PINNED_TRANSFORMERS_VERSION = '4.2.0';
18
23
  export class RuntimeInstaller {
@@ -20,15 +25,19 @@ export class RuntimeInstaller {
20
25
  target;
21
26
  logger;
22
27
  spawnImpl;
28
+ /** 随包 lockfile 路径(测试可注入;默认取 dist 根下构建期拷入的资产)。 */
29
+ lockfileSource;
23
30
  progress;
24
31
  child = null;
25
32
  current = null;
26
- /** 安装超时(npm 卡死不罕见:registry 停滞即永挂,applyBusy 会被锁死)。 */
33
+ /** 安装超时(npm 卡死不罕见:registry 停滞即永挂,applyBusy 会被锁死)。每次 spawn 独立计时。 */
27
34
  static INSTALL_TIMEOUT_MS = 10 * 60_000;
28
35
  constructor(dataDir, targetVersion, opts) {
29
36
  this.runtimeDir = path.join(dataDir, 'runtime');
30
37
  this.target = targetVersion;
31
38
  this.logger = opts?.logger;
39
+ this.lockfileSource =
40
+ opts?.lockfileSource ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'runtime-package-lock.json');
32
41
  this.spawnImpl =
33
42
  opts?.spawnImpl ??
34
43
  ((command, args, cwd) => {
@@ -127,41 +136,71 @@ export class RuntimeInstaller {
127
136
  if (lines.length > 5)
128
137
  lines.splice(0, lines.length - 5);
129
138
  }
139
+ /** 跑一次 npm 子进程(采集尾行 + 超时 kill),返回退出码(null = 被杀死/启动失败)。 */
140
+ async runNpm(args) {
141
+ const child = this.spawnImpl('npm', args, this.runtimeDir);
142
+ this.child = child;
143
+ child.onStdout((l) => this.pushLine(l));
144
+ child.onStderr((l) => this.pushLine(l));
145
+ const timeout = setTimeout(() => {
146
+ this.pushLine('安装超时(10 分钟),终止子进程');
147
+ child.kill();
148
+ }, RuntimeInstaller.INSTALL_TIMEOUT_MS);
149
+ const code = await child.exited;
150
+ clearTimeout(timeout);
151
+ this.child = null;
152
+ return code;
153
+ }
130
154
  async installOnce() {
131
- // 锚定 package.json:没有它 npm 会向上层目录找最近的 package.json 安装(逃逸事故)
155
+ // 锚定 package.json(带精确依赖):没有它 npm 会向上层目录找最近的 package.json 安装(逃逸事故);
156
+ // npm ci 还要求它与 lockfile 根条目一致——每次安装都写规范化形状,覆盖历史遗留/被 npm 改写的副本。
132
157
  await fs.mkdir(this.runtimeDir, { recursive: true });
133
- const manifestPath = path.join(this.runtimeDir, 'package.json');
134
- try {
135
- await fs.access(manifestPath);
136
- }
137
- catch {
138
- await fs.writeFile(manifestPath, JSON.stringify({ name: 'dsh-memory-runtime', private: true }, null, 2));
139
- }
158
+ const manifest = {
159
+ name: 'dsh-memory-runtime',
160
+ private: true,
161
+ dependencies: { [RuntimeInstaller.packageName]: this.target },
162
+ };
163
+ await fs.writeFile(path.join(this.runtimeDir, 'package.json'), JSON.stringify(manifest, null, 2));
140
164
  this.progress = {
141
165
  phase: 'installing',
142
166
  targetVersion: this.target,
143
167
  installedVersion: await this.installedVersion(),
144
168
  startedAt: Date.now(),
145
169
  elapsedMs: 0,
146
- lastLines: [`npm install ${RuntimeInstaller.packageName}@${this.target}(--ignore-scripts)`],
170
+ lastLines: [],
147
171
  };
148
- const child = this.spawnImpl('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel', 'notice', `${RuntimeInstaller.packageName}@${this.target}`], this.runtimeDir);
149
- this.child = child;
150
- child.onStdout((l) => this.pushLine(l));
151
- child.onStderr((l) => this.pushLine(l));
152
- const timeout = setTimeout(() => {
153
- this.pushLine('安装超时(10 分钟),终止子进程');
154
- child.kill();
155
- }, RuntimeInstaller.INSTALL_TIMEOUT_MS);
156
172
  this.logger?.info(`[memory] 运行时安装开始: ${RuntimeInstaller.packageName}@${this.target} → ${this.runtimeDir}`);
157
- const code = await new Promise((resolve) => {
158
- void child.exited.then((c) => {
159
- clearTimeout(timeout);
160
- resolve(c);
161
- });
162
- });
173
+ // 首选 npm ci + 随包 lockfile(传递依赖树冻结在作者侧);lockfile 资产缺失(npm 包被裁剪等)
174
+ // ci 失败(lock 与锚定版本漂移等)回退 npm install 精确版本——可用性优先。
175
+ let code = null;
176
+ let usedCi = false;
177
+ try {
178
+ const lock = await fs.readFile(this.lockfileSource, 'utf8');
179
+ await fs.writeFile(path.join(this.runtimeDir, 'package-lock.json'), lock);
180
+ this.pushLine(`npm ci(随包 lockfile 锁定依赖树,@${this.target},--ignore-scripts)`);
181
+ usedCi = true;
182
+ code = await this.runNpm(['ci', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel', 'notice']);
183
+ }
184
+ catch {
185
+ /* 无随包 lockfile,直接走 install 回退 */
186
+ }
187
+ if (!usedCi || code !== 0) {
188
+ if (usedCi) {
189
+ this.pushLine('npm ci 失败(lockfile 与钉死版本漂移?),回退 npm install');
190
+ this.logger?.warn('[memory] 运行时 npm ci 失败,回退 npm install(传递依赖不再受随包 lockfile 锁定)');
191
+ }
192
+ this.pushLine(`npm install ${RuntimeInstaller.packageName}@${this.target}(--ignore-scripts)`);
193
+ code = await this.runNpm([
194
+ 'install',
195
+ '--ignore-scripts',
196
+ '--no-audit',
197
+ '--no-fund',
198
+ '--loglevel',
199
+ 'notice',
200
+ `${RuntimeInstaller.packageName}@${this.target}`,
201
+ ]);
202
+ }
163
203
  this.progress.elapsedMs = Date.now() - this.progress.startedAt;
164
- this.child = null;
165
204
  const version = await this.installedVersion();
166
205
  this.progress.installedVersion = version;
167
206
  if (this.progress.phase === 'cancelled') {
@@ -176,6 +176,21 @@ export declare class MemoryDb {
176
176
  /** 只更新向量行(重嵌入用)。 */
177
177
  updateL1Vec(id: string, embedding: Float32Array): boolean;
178
178
  updateL0Vec(id: string, embedding: Float32Array, recordedAt: string): boolean;
179
+ /**
180
+ * 批量更新 L1 向量行(重嵌入热路径):单事务写入整批,替代逐条裸写——
181
+ * 逐条每行一次隐式事务,批量场景(万级记录重嵌)开销集中在 fsync 上。
182
+ * 整批失败回退逐条:好行照常入库,坏行只丢自身(向量行 id 寻址,无顺序依赖)。
183
+ * 返回成功写入的行数(零向量行防御性跳过、不计入)。
184
+ */
185
+ updateL1VecBatch(items: Array<{
186
+ id: string;
187
+ embedding: Float32Array;
188
+ }>): number;
189
+ /** L0 版 updateL1VecBatch(语义同:单事务 + 失败回退逐条)。recordedAt 整批统一。 */
190
+ updateL0VecBatch(items: Array<{
191
+ id: string;
192
+ embedding: Float32Array;
193
+ }>, recordedAt: string): number;
179
194
  close(): void;
180
195
  }
181
196
  /** 全零向量(cosine 未定义,不可入向量表)。reindex 侧用它区分"不可嵌入"与"写入失败"。 */
@@ -72,9 +72,12 @@ export class MemoryDb {
72
72
  mkdirSync(dbDir, { recursive: true });
73
73
  const { DatabaseSync: DbSync } = require('node:sqlite');
74
74
  this.db = new DbSync(dbPath, { allowExtension: true });
75
- // 并发读优化 + 有界内存(照搬官方 PRAGMA 组合)
75
+ // 并发读优化 + 有界内存(照搬官方 PRAGMA 组合,synchronous 为本仓新增:
76
+ // WAL 下官方推荐 NORMAL——批量写从"每事务一次 fsync"降为"每 checkpoint 一次",
77
+ // 重嵌入/导入提速明显;代价仅是断电时丢最后若干已提交事务(只丢不损,无损坏风险))
76
78
  this.db.exec('PRAGMA busy_timeout = 5000');
77
79
  this.db.exec('PRAGMA journal_mode = WAL');
80
+ this.db.exec('PRAGMA synchronous = NORMAL');
78
81
  this.db.exec('PRAGMA cache_size = -65536');
79
82
  this.db.exec('PRAGMA mmap_size = 134217728');
80
83
  this.db.exec('PRAGMA wal_autocheckpoint = 1000');
@@ -1179,6 +1182,85 @@ export class MemoryDb {
1179
1182
  return false;
1180
1183
  }
1181
1184
  }
1185
+ /**
1186
+ * 批量更新 L1 向量行(重嵌入热路径):单事务写入整批,替代逐条裸写——
1187
+ * 逐条每行一次隐式事务,批量场景(万级记录重嵌)开销集中在 fsync 上。
1188
+ * 整批失败回退逐条:好行照常入库,坏行只丢自身(向量行 id 寻址,无顺序依赖)。
1189
+ * 返回成功写入的行数(零向量行防御性跳过、不计入)。
1190
+ */
1191
+ updateL1VecBatch(items) {
1192
+ if (this.degraded || !this.stmtDeleteL1Vec || !this.stmtInsertL1Vec || items.length === 0)
1193
+ return 0;
1194
+ try {
1195
+ this.db.exec('BEGIN');
1196
+ try {
1197
+ let written = 0;
1198
+ for (const it of items) {
1199
+ if (isZeroVector(it.embedding))
1200
+ continue;
1201
+ this.stmtDeleteL1Vec.run(it.id);
1202
+ this.stmtInsertL1Vec.run(it.id, vecToBuffer(it.embedding), new Date().toISOString());
1203
+ written++;
1204
+ }
1205
+ this.db.exec('COMMIT');
1206
+ return written;
1207
+ }
1208
+ catch (err) {
1209
+ try {
1210
+ this.db.exec('ROLLBACK');
1211
+ }
1212
+ catch {
1213
+ /* ignore */
1214
+ }
1215
+ throw err;
1216
+ }
1217
+ }
1218
+ catch (err) {
1219
+ this.logger?.warn(`${TAG} L1 向量批量写入失败,回退逐条: ${err instanceof Error ? err.message : String(err)}`);
1220
+ let ok = 0;
1221
+ for (const it of items)
1222
+ if (this.updateL1Vec(it.id, it.embedding))
1223
+ ok++;
1224
+ return ok;
1225
+ }
1226
+ }
1227
+ /** L0 版 updateL1VecBatch(语义同:单事务 + 失败回退逐条)。recordedAt 整批统一。 */
1228
+ updateL0VecBatch(items, recordedAt) {
1229
+ if (this.degraded || !this.stmtDeleteL0Vec || !this.stmtInsertL0Vec || items.length === 0)
1230
+ return 0;
1231
+ try {
1232
+ this.db.exec('BEGIN');
1233
+ try {
1234
+ let written = 0;
1235
+ for (const it of items) {
1236
+ if (isZeroVector(it.embedding))
1237
+ continue;
1238
+ this.stmtDeleteL0Vec.run(it.id);
1239
+ this.stmtInsertL0Vec.run(it.id, vecToBuffer(it.embedding), recordedAt);
1240
+ written++;
1241
+ }
1242
+ this.db.exec('COMMIT');
1243
+ return written;
1244
+ }
1245
+ catch (err) {
1246
+ try {
1247
+ this.db.exec('ROLLBACK');
1248
+ }
1249
+ catch {
1250
+ /* ignore */
1251
+ }
1252
+ throw err;
1253
+ }
1254
+ }
1255
+ catch (err) {
1256
+ this.logger?.warn(`${TAG} L0 向量批量写入失败,回退逐条: ${err instanceof Error ? err.message : String(err)}`);
1257
+ let ok = 0;
1258
+ for (const it of items)
1259
+ if (this.updateL0Vec(it.id, it.embedding, recordedAt))
1260
+ ok++;
1261
+ return ok;
1262
+ }
1263
+ }
1182
1264
  close() {
1183
1265
  try {
1184
1266
  this.db.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-layered-memory",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",