@kenz1117/dsh-engram 0.7.6 → 0.7.8

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/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, statSync } from "node:fs";
2
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import { homedir } from "node:os";
@@ -16,6 +16,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
16
16
  */
17
17
  /** 合法配置键集合(未知键 loud 失败)。 */
18
18
  const CONFIG_KEYS = /* @__PURE__ */ new Set([
19
+ "legacyMigration",
19
20
  "dbDir",
20
21
  "injectProfile",
21
22
  "profileTopN",
@@ -47,6 +48,7 @@ const INGEST_MODES = /* @__PURE__ */ new Set([
47
48
  /** Schemastery 校验面(cordis.yml 读取时校验)。 */
48
49
  const Config = z.object({
49
50
  dbDir: z.string(),
51
+ legacyMigration: z.string(),
50
52
  injectProfile: z.boolean(),
51
53
  profileTopN: z.number().step(1).min(1).max(64),
52
54
  modelCacheDir: z.string(),
@@ -77,6 +79,7 @@ const Config = z.object({
77
79
  */
78
80
  function resolveConfig(config = {}) {
79
81
  for (const key of Object.keys(config)) if (!CONFIG_KEYS.has(key)) throw new Error(`dsh-engram: unknown config key "${key}"`);
82
+ if (config.legacyMigration !== void 0 && config.legacyMigration !== "eager" && config.legacyMigration !== "conservative") throw new Error("dsh-engram: legacyMigration must be one of eager|conservative");
80
83
  if (config.ingest !== void 0 && !INGEST_MODES.has(config.ingest)) throw new Error(`dsh-engram: ingest must be one of off|light|eager, got "${String(config.ingest)}"`);
81
84
  if (config.profileTopN !== void 0 && (!Number.isInteger(config.profileTopN) || config.profileTopN < 1 || config.profileTopN > 64)) throw new Error("dsh-engram: profileTopN must be an integer in [1, 64]");
82
85
  const hasProvider = config.provider !== void 0;
@@ -93,6 +96,7 @@ function resolveConfig(config = {}) {
93
96
  const dbDir = config.dbDir ?? join(homedir(), ".dsh", "engram");
94
97
  return {
95
98
  dbDir,
99
+ legacyMigration: config.legacyMigration ?? "eager",
96
100
  injectProfile: config.injectProfile ?? true,
97
101
  profileTopN: config.profileTopN ?? 8,
98
102
  modelCacheDir: config.modelCacheDir ?? join(dbDir, "models"),
@@ -2511,17 +2515,17 @@ function registerEngramRoutes(ctx, deps) {
2511
2515
  //#region src/project/identity.ts
2512
2516
  /**
2513
2517
  * 项目标识:git origin URL 归一化 → sha256 短哈希分库名;无 git 或无 origin 时
2514
- * 回退 cwd 编码(v0.4 现状算法)。纯文件读(.git/config、worktree 的 gitdir/commondir
2518
+ * 回退完整 cwd 哈希。纯文件读(.git/config、worktree 的 gitdir/commondir
2515
2519
  * 指针),不起子进程。启动时负责旧库文件向新标识的 rename 迁移。
2516
2520
  * @module @kenz1117/dsh-engram/project/identity
2517
2521
  */
2518
- /** v0.4 旧命名:cwd 的 hex 编码前 24 位(只覆盖 cwd 前 12 字符,同前缀目录会撞库)。保留仅供迁移识别。 */
2522
+ /** v0.4 旧命名:cwd 的 hex 编码前 24 位(只覆盖 cwd 前 12 UTF-8 字节,同前缀目录会撞库)。保留仅供迁移识别。 */
2519
2523
  function legacyProjectDbName(cwd) {
2520
2524
  return `project-${Buffer.from(cwd).toString("hex").slice(0, 24)}.db`;
2521
2525
  }
2522
2526
  /**
2523
2527
  * 无 git 时的 cwd 命名:cwd 全量 sha256 前 24 位。
2524
- * 旧算法只取 cwd 前 12 字符,`C:\Users\Adm…` 这类同前缀目录会共用同一个库;
2528
+ * 旧算法只取 cwd 前 12 UTF-8 字节,`C:\Users\Adm…` 这类同前缀目录会共用同一个库;
2525
2529
  * 新算法对每个目录唯一,启动时把旧库 rename 迁移到新名(见 migrateProjectDb)。
2526
2530
  */
2527
2531
  function cwdProjectDbName(cwd) {
@@ -2611,37 +2615,83 @@ function resolveProjectIdentity(cwd) {
2611
2615
  const origin = configPath === void 0 ? void 0 : readOriginUrl(configPath);
2612
2616
  const normalized = origin === void 0 ? void 0 : normalizeOriginUrl(origin);
2613
2617
  if (normalized === void 0) return {
2618
+ cwd,
2614
2619
  dbName: cwdProjectDbName(cwd),
2615
2620
  source: "cwd",
2616
2621
  legacyDbName
2617
2622
  };
2618
2623
  return {
2624
+ cwd,
2619
2625
  dbName: `project-${createHash("sha256").update(normalized).digest("hex").slice(0, 24)}.db`,
2620
2626
  source: "origin",
2621
2627
  legacyDbName
2622
2628
  };
2623
2629
  }
2630
+ function readMigrationPointer(dbDir, identity) {
2631
+ const path = join(dbDir, `${identity.legacyDbName}.migrated-to`);
2632
+ let text;
2633
+ try {
2634
+ text = readFileSync(path, "utf8");
2635
+ } catch (error) {
2636
+ if (error.code === "ENOENT") return void 0;
2637
+ throw error;
2638
+ }
2639
+ const value = JSON.parse(text);
2640
+ if (typeof value !== "object" || value === null || !("migratedTo" in value) || typeof value.migratedTo !== "string" || !/^project-[0-9a-f]{24}\.db$/.test(value.migratedTo) || !("claimedByCwd" in value) || typeof value.claimedByCwd !== "string" || !("claimedAt" in value) || typeof value.claimedAt !== "string" || !Number.isFinite(Date.parse(value.claimedAt))) throw new Error(`dsh-engram: invalid migration pointer ${path}; inspect it manually`);
2641
+ return value;
2642
+ }
2624
2643
  /**
2625
- * 旧库迁移:新名库不存在而旧名库存在时同目录 rename(零数据搬运);两者都存在时
2626
- * 不合并、不动文件,返回 kept-both 由调用方告警。origin 标识(v0.4 cwd 编码 → 仓库
2627
- * 哈希)与 cwd 兜底(截断命名 → 全量哈希)两种升级都走这里。
2628
- *
2629
- * 注意旧 cwd 命名只覆盖前 12 字符:同一前缀下的多个目录可能共用过一个旧库,
2630
- * 迁移只能把库判给先打开它的那个目录,调用方应对 kept-both 与本次迁移都告警。
2644
+ * Eager keeps the existing rename policy, recording its ambiguous ownership in JSON.
2645
+ * Conservative leaves legacy bytes untouched; the caller may open an empty new store.
2646
+ * Coexisting databases are never merged or overwritten. This is not a cross-process lock
2647
+ * or an atomic transaction across the rename and sidecar write: stop other hosts first.
2631
2648
  */
2632
- function migrateProjectDb(dbDir, identity) {
2649
+ function migrateProjectDb(dbDir, identity, policy = "eager") {
2633
2650
  if (identity.dbName === identity.legacyDbName) return "none";
2634
2651
  const next = join(dbDir, identity.dbName);
2635
2652
  const legacy = join(dbDir, identity.legacyDbName);
2636
2653
  const hasNext = existsSync(next);
2637
2654
  const hasLegacy = existsSync(legacy);
2638
2655
  if (hasNext && hasLegacy) return "kept-both";
2656
+ const pointer = readMigrationPointer(dbDir, identity);
2657
+ if (pointer !== void 0) {
2658
+ if (hasLegacy || pointer.migratedTo !== identity.dbName) return "already-migrated";
2659
+ return "none";
2660
+ }
2639
2661
  if (!hasNext && hasLegacy) {
2662
+ if (policy === "conservative") return "deferred";
2640
2663
  renameSync(legacy, next);
2664
+ try {
2665
+ writeFileSync(`${legacy}.migrated-to`, JSON.stringify({
2666
+ migratedTo: identity.dbName,
2667
+ claimedByCwd: identity.cwd,
2668
+ claimedAt: (/* @__PURE__ */ new Date()).toISOString()
2669
+ }) + "\n", {
2670
+ flag: "wx",
2671
+ mode: 384
2672
+ });
2673
+ } catch (error) {
2674
+ throw new Error(`dsh-engram: renamed ${identity.legacyDbName} to ${identity.dbName}, but could not record migration; inspect both paths before retrying`, { cause: error });
2675
+ }
2641
2676
  return "renamed";
2642
2677
  }
2643
2678
  return "none";
2644
2679
  }
2680
+ /** Shared guidance for plugin boot and session-cwd migration checks. */
2681
+ function migrationWarning(dbDir, identity, outcome) {
2682
+ const names = `旧库 ${identity.legacyDbName};新库 ${identity.dbName}`;
2683
+ const manual = "人工处理前请停止相关宿主并备份所有数据库及 sidecar;新库可能已写入记忆,请勿直接覆盖,先用 engram_review / engram_export 审核并人工重新归属";
2684
+ switch (outcome) {
2685
+ case "none": return;
2686
+ case "renamed": return `[dsh-engram] 项目记忆库已迁移(${names}),已记录 .migrated-to;旧库可能包含同前缀目录的记忆。${manual}`;
2687
+ case "kept-both": return `[dsh-engram] 新旧项目记忆库并存,未合并或移动(${names})。${manual}`;
2688
+ case "deferred": return `[dsh-engram] conservative 模式暂缓旧库迁移(${names});旧库保持不变,将打开新的空库。${manual}`;
2689
+ case "already-migrated": {
2690
+ const pointer = readMigrationPointer(dbDir, identity);
2691
+ return `[dsh-engram] 旧库已有迁移记录(${names}):${JSON.stringify(pointer)};先前迁移的库可能含有当前目录的记忆。${manual}`;
2692
+ }
2693
+ }
2694
+ }
2645
2695
  //#endregion
2646
2696
  //#region src/project/registry.ts
2647
2697
  /**
@@ -5993,9 +6043,9 @@ function apply(ctx, config = {}) {
5993
6043
  mode: 448
5994
6044
  });
5995
6045
  const identity = resolveProjectIdentity(process.cwd());
5996
- const migration = migrateProjectDb(resolved.dbDir, identity);
5997
- if (migration === "renamed") console.warn(`[dsh-engram] 项目记忆库已从 cwd 命名迁移到 git origin 标识:${identity.dbName}`);
5998
- else if (migration === "kept-both") console.warn(`[dsh-engram] 检测到新旧两个项目记忆库并存,未合并(保留新库 ${identity.dbName};旧库 ${identity.legacyDbName} 请人工处理后删除)`);
6046
+ const migration = migrateProjectDb(resolved.dbDir, identity, resolved.legacyMigration);
6047
+ const warning = migrationWarning(resolved.dbDir, identity, migration);
6048
+ if (warning !== void 0) console.warn(warning);
5999
6049
  const rankBoost = {
6000
6050
  recencyWeight: resolved.rankRecencyWeight,
6001
6051
  proofWeight: resolved.rankProofWeight,
@@ -6039,9 +6089,9 @@ function apply(ctx, config = {}) {
6039
6089
  const cached = cwdDbNames.get(cwd);
6040
6090
  if (cached !== void 0) return cached;
6041
6091
  const projectIdentity = resolveProjectIdentity(cwd);
6042
- const migration = migrateProjectDb(resolved.dbDir, projectIdentity);
6043
- if (migration === "renamed") console.warn(`[dsh-engram] 项目记忆库已迁移到新标识命名(${cwd} → ${projectIdentity.dbName})`);
6044
- else if (migration === "kept-both") console.warn(`[dsh-engram] 检测到新旧项目库并存,未合并(保留新库 ${projectIdentity.dbName};旧库 ${projectIdentity.legacyDbName} 请人工处理后删除)`);
6092
+ const migration = migrateProjectDb(resolved.dbDir, projectIdentity, resolved.legacyMigration);
6093
+ const warning = migrationWarning(resolved.dbDir, projectIdentity, migration);
6094
+ if (warning !== void 0) console.warn(warning);
6045
6095
  cwdDbNames.set(cwd, projectIdentity.dbName);
6046
6096
  return projectIdentity.dbName;
6047
6097
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kenz1117/dsh-engram",
3
3
  "description": "Cross-session long-term memory for DeepSeek Harness (memory palace · AGI Architecture Exploration on dsh-market): dual-scope SQLite memory graph, hybrid retrieval, provenance audit, and a knowledge flywheel.",
4
- "version": "0.7.6",
4
+ "version": "0.7.8",
5
5
  "icon": "icon.svg",
6
6
  "logo": "icon-256.svg",
7
7
  "publishConfig": {