dsh-prime-memory 0.11.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.
Files changed (151) hide show
  1. package/CHANGELOG.en.md +28 -0
  2. package/CHANGELOG.ja.md +30 -0
  3. package/CHANGELOG.ko.md +30 -0
  4. package/CHANGELOG.md +1220 -0
  5. package/ENGINEERING-NOTES.md +452 -0
  6. package/INSTALL.en.md +92 -0
  7. package/INSTALL.ja.md +92 -0
  8. package/INSTALL.ko.md +92 -0
  9. package/INSTALL.md +92 -0
  10. package/LICENSE +21 -0
  11. package/README.en.md +458 -0
  12. package/README.ja.md +306 -0
  13. package/README.ko.md +306 -0
  14. package/README.md +425 -0
  15. package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
  16. package/assets/changelog/0.8.9/01-panel.png +0 -0
  17. package/assets/changelog/0.8.9/02-halo.png +0 -0
  18. package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
  19. package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
  20. package/assets/img/EmbeddingSource.png +0 -0
  21. package/assets/img/Hero.png +0 -0
  22. package/assets/img/Layers.png +0 -0
  23. package/assets/img/MemoryTools.png +0 -0
  24. package/assets/img/Modes.png +0 -0
  25. package/assets/img/ToolTrajectory.png +0 -0
  26. package/assets/img/ui-dark.jpg +0 -0
  27. package/assets/img/ui-light.jpg +0 -0
  28. package/assets/readme/bench-dialog.svg +70 -0
  29. package/assets/readme/bench-workflow.svg +79 -0
  30. package/assets/readme/flow.svg +189 -0
  31. package/assets/readme/storage.svg +115 -0
  32. package/cordis.patch.yml +16 -0
  33. package/dist/bench-control.d.ts +34 -0
  34. package/dist/bench-control.js +16 -0
  35. package/dist/client.js +4293 -0
  36. package/dist/config.d.ts +683 -0
  37. package/dist/config.js +129 -0
  38. package/dist/contract.d.ts +820 -0
  39. package/dist/contract.js +1 -0
  40. package/dist/embedding-worker.cjs +176 -0
  41. package/dist/graph/apply.d.ts +37 -0
  42. package/dist/graph/apply.js +270 -0
  43. package/dist/graph/constraints.d.ts +47 -0
  44. package/dist/graph/constraints.js +38 -0
  45. package/dist/graph/search.d.ts +16 -0
  46. package/dist/graph/search.js +115 -0
  47. package/dist/graph/types.d.ts +142 -0
  48. package/dist/graph/types.js +14 -0
  49. package/dist/hooks/capture.d.ts +32 -0
  50. package/dist/hooks/capture.js +194 -0
  51. package/dist/hooks/recall.d.ts +63 -0
  52. package/dist/hooks/recall.js +429 -0
  53. package/dist/index.d.ts +534 -0
  54. package/dist/index.js +344 -0
  55. package/dist/llm-usage.d.ts +26 -0
  56. package/dist/llm-usage.js +37 -0
  57. package/dist/llm.d.ts +153 -0
  58. package/dist/llm.js +530 -0
  59. package/dist/pipeline/graph.d.ts +35 -0
  60. package/dist/pipeline/graph.js +104 -0
  61. package/dist/pipeline/l1.d.ts +19 -0
  62. package/dist/pipeline/l1.js +271 -0
  63. package/dist/pipeline/l2.d.ts +13 -0
  64. package/dist/pipeline/l2.js +83 -0
  65. package/dist/pipeline/l3.d.ts +15 -0
  66. package/dist/pipeline/l3.js +78 -0
  67. package/dist/pipeline/rebuild.d.ts +61 -0
  68. package/dist/pipeline/rebuild.js +307 -0
  69. package/dist/pipeline/ruminate.d.ts +89 -0
  70. package/dist/pipeline/ruminate.js +298 -0
  71. package/dist/pipeline/runner.d.ts +167 -0
  72. package/dist/pipeline/runner.js +638 -0
  73. package/dist/pipeline/trigger.d.ts +40 -0
  74. package/dist/pipeline/trigger.js +75 -0
  75. package/dist/prompts/graph-projection.d.ts +70 -0
  76. package/dist/prompts/graph-projection.js +167 -0
  77. package/dist/prompts/l1-dedup.d.ts +22 -0
  78. package/dist/prompts/l1-dedup.js +251 -0
  79. package/dist/prompts/l1-extraction.d.ts +22 -0
  80. package/dist/prompts/l1-extraction.js +457 -0
  81. package/dist/prompts/persona.d.ts +23 -0
  82. package/dist/prompts/persona.js +240 -0
  83. package/dist/prompts/scene.d.ts +32 -0
  84. package/dist/prompts/scene.js +414 -0
  85. package/dist/runtime-package-lock.json +982 -0
  86. package/dist/settings.d.ts +50 -0
  87. package/dist/settings.js +355 -0
  88. package/dist/stats.d.ts +109 -0
  89. package/dist/stats.js +929 -0
  90. package/dist/store/bm25.d.ts +19 -0
  91. package/dist/store/bm25.js +63 -0
  92. package/dist/store/cost-ledger.d.ts +75 -0
  93. package/dist/store/cost-ledger.js +171 -0
  94. package/dist/store/download-queue.d.ts +79 -0
  95. package/dist/store/download-queue.js +424 -0
  96. package/dist/store/embedding-source.d.ts +118 -0
  97. package/dist/store/embedding-source.js +443 -0
  98. package/dist/store/embedding.d.ts +90 -0
  99. package/dist/store/embedding.js +206 -0
  100. package/dist/store/graph-store.d.ts +94 -0
  101. package/dist/store/graph-store.js +641 -0
  102. package/dist/store/l0.d.ts +40 -0
  103. package/dist/store/l0.js +197 -0
  104. package/dist/store/l1.d.ts +93 -0
  105. package/dist/store/l1.js +297 -0
  106. package/dist/store/local-embedding.d.ts +89 -0
  107. package/dist/store/local-embedding.js +227 -0
  108. package/dist/store/model-catalog.d.ts +48 -0
  109. package/dist/store/model-catalog.js +81 -0
  110. package/dist/store/occupancy.d.ts +30 -0
  111. package/dist/store/occupancy.js +134 -0
  112. package/dist/store/pending.d.ts +36 -0
  113. package/dist/store/pending.js +103 -0
  114. package/dist/store/persona.d.ts +15 -0
  115. package/dist/store/persona.js +60 -0
  116. package/dist/store/recall-dedupe.d.ts +26 -0
  117. package/dist/store/recall-dedupe.js +138 -0
  118. package/dist/store/runtime-installer.d.ts +59 -0
  119. package/dist/store/runtime-installer.js +243 -0
  120. package/dist/store/scenes.d.ts +24 -0
  121. package/dist/store/scenes.js +160 -0
  122. package/dist/store/search-utils.d.ts +38 -0
  123. package/dist/store/search-utils.js +100 -0
  124. package/dist/store/session-modes.d.ts +35 -0
  125. package/dist/store/session-modes.js +144 -0
  126. package/dist/store/sqlite.d.ts +246 -0
  127. package/dist/store/sqlite.js +1491 -0
  128. package/dist/store/state.d.ts +41 -0
  129. package/dist/store/state.js +72 -0
  130. package/dist/token-cost.d.ts +23 -0
  131. package/dist/token-cost.js +185 -0
  132. package/dist/tools/index.d.ts +34 -0
  133. package/dist/tools/index.js +758 -0
  134. package/dist/types.d.ts +139 -0
  135. package/dist/types.js +38 -0
  136. package/dist/util/context-occupancy.d.ts +68 -0
  137. package/dist/util/context-occupancy.js +92 -0
  138. package/dist/util/filelog.d.ts +6 -0
  139. package/dist/util/filelog.js +108 -0
  140. package/dist/util/io.d.ts +18 -0
  141. package/dist/util/io.js +97 -0
  142. package/dist/util/recall-budget.d.ts +31 -0
  143. package/dist/util/recall-budget.js +84 -0
  144. package/dist/util/sanitize.d.ts +11 -0
  145. package/dist/util/sanitize.js +67 -0
  146. package/dist/util/text.d.ts +16 -0
  147. package/dist/util/text.js +61 -0
  148. package/dist/util/tokenizer.d.ts +9 -0
  149. package/dist/util/tokenizer.js +50 -0
  150. package/dsh.plugin.json +22 -0
  151. package/package.json +118 -0
@@ -0,0 +1,15 @@
1
+ import type { MemoryFamily, MemoryLogger } from '../types.js';
2
+ export declare const NAV_HEADER = "## \uD83D\uDDFA\uFE0F Scene Navigation";
3
+ export declare class PersonaStore {
4
+ private readonly logger?;
5
+ private readonly file;
6
+ private readonly family;
7
+ constructor(dataDir: string, family: MemoryFamily, logger?: MemoryLogger | undefined);
8
+ /** 旧布局迁移:persona.md → persona-chat.md(幂等,仅 chat 族执行)。 */
9
+ init(): Promise<void>;
10
+ /** 读取正文(剥离场景导航部分)。 */
11
+ read(): Promise<string | undefined>;
12
+ /** 写入正文(保留已有导航段则拼回尾部)。 */
13
+ write(body: string): Promise<void>;
14
+ }
15
+ export declare function stripSceneNavigation(content: string): string;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * L3 画像存储:persona-<family>.md(chat 族=用户画像 / work 族=Team Operating Doctrine)。
3
+ * 场景导航由工程侧自动追加/剥离(stripSceneNavigation 语义)。
4
+ * init() 把旧布局的 persona.md 一次性改名为 persona-chat.md(历史数据归属 chat 档)。
5
+ */
6
+ import { promises as fs } from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import { atomicWriteText, readTextIfExists } from '../util/io.js';
9
+ export const NAV_HEADER = '## 🗺️ Scene Navigation';
10
+ export class PersonaStore {
11
+ logger;
12
+ file;
13
+ family;
14
+ constructor(dataDir, family, logger) {
15
+ this.logger = logger;
16
+ this.family = family;
17
+ this.file = path.join(dataDir, `persona-${family}.md`);
18
+ }
19
+ /** 旧布局迁移:persona.md → persona-chat.md(幂等,仅 chat 族执行)。 */
20
+ async init() {
21
+ if (this.family !== 'chat')
22
+ return;
23
+ const dataDir = path.dirname(this.file);
24
+ const legacy = path.join(dataDir, 'persona.md');
25
+ try {
26
+ await fs.access(legacy);
27
+ await fs.rename(legacy, this.file);
28
+ this.logger?.info('[memory] 画像文件迁移:persona.md → persona-chat.md');
29
+ }
30
+ catch {
31
+ /* 无旧文件或已迁移 */
32
+ }
33
+ }
34
+ /** 读取正文(剥离场景导航部分)。 */
35
+ async read() {
36
+ const raw = await readTextIfExists(this.file);
37
+ if (!raw)
38
+ return undefined;
39
+ return stripSceneNavigation(raw).trim() || undefined;
40
+ }
41
+ /** 写入正文(保留已有导航段则拼回尾部)。 */
42
+ async write(body) {
43
+ const raw = await readTextIfExists(this.file);
44
+ const nav = raw ? extractSceneNavigation(raw) : undefined;
45
+ const content = nav ? `${body.trim()}\n\n${nav}\n` : `${body.trim()}\n`;
46
+ await atomicWriteText(this.file, content);
47
+ }
48
+ }
49
+ export function stripSceneNavigation(content) {
50
+ const idx = content.indexOf(NAV_HEADER);
51
+ if (idx === -1)
52
+ return content;
53
+ return content.slice(0, idx).trimEnd();
54
+ }
55
+ function extractSceneNavigation(content) {
56
+ const idx = content.indexOf(NAV_HEADER);
57
+ if (idx === -1)
58
+ return undefined;
59
+ return content.slice(idx).trim();
60
+ }
@@ -0,0 +1,26 @@
1
+ import type { MemoryLogger } from '../types.js';
2
+ /** 会话条目上限(按 updatedAt 淘汰最旧;防文件无限增长)。 */
3
+ export declare const RECALL_DEDUPE_SESSION_CAP = 200;
4
+ /** 单会话记录 id 上限(按插入序淘汰最旧;Set 迭代序即插入序)。 */
5
+ export declare const RECALL_DEDUPE_IDS_CAP = 512;
6
+ export declare class RecallDedupeStore {
7
+ private readonly logger?;
8
+ private readonly file;
9
+ private readonly entries;
10
+ private persistFailed;
11
+ /** 串行化持久化写(避免并发原子写撞临时文件名);init 链最前(先载入再落盘,防丢更新)。 */
12
+ private writeChain;
13
+ constructor(dataDir: string, logger?: MemoryLogger | undefined);
14
+ /** 载入持久化映射(合并进内存——构造与载入之间发生的 mark 不丢);失败降级内存态。 */
15
+ private init;
16
+ /** 该会话的已注入集合(热路径同步读;未出现过的会话返回空集合,惰性建条)。 */
17
+ seen(sessionId: string): Set<string>;
18
+ /** 标记本轮实际注入的记录 id(写穿;调用方保证只传模型真实看到的条目)。 */
19
+ mark(sessionId: string, recordIds: string[]): void;
20
+ /** 清空该会话的记录(compact/clear 后上下文已丢失,记忆需可重新注入)。 */
21
+ reset(sessionId: string): void;
22
+ /** 等待在途持久化写完成(测试/停机用)。 */
23
+ flush(): Promise<void>;
24
+ private persist;
25
+ private serialize;
26
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * 召回去重存储:sessionId → 已注入 L1 记录 id 集合的持久化映射。
3
+ *
4
+ * 语义:
5
+ * - 同会话内已注入过的记忆不再重复注入(模型上下文已持有,重复注入浪费 token);
6
+ * - 压制粒度 = 记录 id——去重合并更新会换新 id,新内容天然解除压制重新注入;
7
+ * - compact/clear 事件重置(上下文被压缩/清空,注入内容已丢失);resume 不重置;
8
+ * - 热路径(召回 pre-step)同步内存读取,mark/reset 写穿持久化(session-modes 同款:
9
+ * 串行化原子写 + 失败降级内存态),任何 I/O 失败绝不抛进召回路径。
10
+ */
11
+ import * as path from 'node:path';
12
+ import { errDetail } from '../util/filelog.js';
13
+ import { atomicWriteJson, ensureDir, readJsonIfExists } from '../util/io.js';
14
+ /** 会话条目上限(按 updatedAt 淘汰最旧;防文件无限增长)。 */
15
+ export const RECALL_DEDUPE_SESSION_CAP = 200;
16
+ /** 单会话记录 id 上限(按插入序淘汰最旧;Set 迭代序即插入序)。 */
17
+ export const RECALL_DEDUPE_IDS_CAP = 512;
18
+ /** 条目过期清理(90 天未更新即丢弃,与 session-modes 同款量级)。 */
19
+ const PRUNE_MS = 90 * 24 * 3600_000;
20
+ export class RecallDedupeStore {
21
+ logger;
22
+ file;
23
+ entries = new Map();
24
+ persistFailed = false;
25
+ /** 串行化持久化写(避免并发原子写撞临时文件名);init 链最前(先载入再落盘,防丢更新)。 */
26
+ writeChain;
27
+ constructor(dataDir, logger) {
28
+ this.logger = logger;
29
+ this.file = path.join(dataDir, 'recall-dedupe.json');
30
+ this.writeChain = this.init();
31
+ }
32
+ /** 载入持久化映射(合并进内存——构造与载入之间发生的 mark 不丢);失败降级内存态。 */
33
+ async init() {
34
+ const data = await readJsonIfExists(this.file);
35
+ if (!data?.sessions || typeof data.sessions !== 'object')
36
+ return;
37
+ const now = Date.now();
38
+ let count = 0;
39
+ for (const [sid, entry] of Object.entries(data.sessions)) {
40
+ if (!Array.isArray(entry?.recordIds))
41
+ continue;
42
+ if (now - (entry.updatedAt ?? 0) > PRUNE_MS)
43
+ continue;
44
+ const existing = this.entries.get(sid);
45
+ if (existing) {
46
+ // 合并:构造后、载入完成前已发生的 mark(保留较大 updatedAt)
47
+ for (const id of entry.recordIds)
48
+ existing.ids.add(id);
49
+ existing.updatedAt = Math.max(existing.updatedAt, entry.updatedAt ?? 0);
50
+ }
51
+ else {
52
+ this.entries.set(sid, { ids: new Set(entry.recordIds), updatedAt: entry.updatedAt ?? now });
53
+ }
54
+ count++;
55
+ }
56
+ if (count > 0)
57
+ this.logger?.info(`[memory] 召回去重记录载入 ${count} 个会话`);
58
+ }
59
+ /** 该会话的已注入集合(热路径同步读;未出现过的会话返回空集合,惰性建条)。 */
60
+ seen(sessionId) {
61
+ let entry = this.entries.get(sessionId);
62
+ if (!entry) {
63
+ entry = { ids: new Set(), updatedAt: 0 };
64
+ this.entries.set(sessionId, entry);
65
+ }
66
+ return entry.ids;
67
+ }
68
+ /** 标记本轮实际注入的记录 id(写穿;调用方保证只传模型真实看到的条目)。 */
69
+ mark(sessionId, recordIds) {
70
+ if (recordIds.length === 0)
71
+ return;
72
+ const ids = this.seen(sessionId);
73
+ for (const id of recordIds)
74
+ ids.add(id);
75
+ // 插入序淘汰最旧(Set 迭代序 = 插入序)
76
+ while (ids.size > RECALL_DEDUPE_IDS_CAP) {
77
+ const oldest = ids.values().next().value;
78
+ if (oldest === undefined)
79
+ break;
80
+ ids.delete(oldest);
81
+ }
82
+ const entry = this.entries.get(sessionId);
83
+ entry.updatedAt = Date.now();
84
+ this.writeChain = this.writeChain.then(() => this.persist());
85
+ }
86
+ /** 清空该会话的记录(compact/clear 后上下文已丢失,记忆需可重新注入)。 */
87
+ reset(sessionId) {
88
+ if (!this.entries.has(sessionId))
89
+ return;
90
+ this.entries.delete(sessionId);
91
+ this.writeChain = this.writeChain.then(() => this.persist());
92
+ }
93
+ /** 等待在途持久化写完成(测试/停机用)。 */
94
+ flush() {
95
+ return this.writeChain;
96
+ }
97
+ async persist() {
98
+ try {
99
+ await ensureDir(path.dirname(this.file));
100
+ await atomicWriteJson(this.file, this.serialize());
101
+ this.persistFailed = false;
102
+ }
103
+ catch (err) {
104
+ if (!this.persistFailed) {
105
+ this.persistFailed = true;
106
+ this.logger?.warn(`[memory] 召回去重持久化失败(降级内存态): ${errDetail(err)}`);
107
+ }
108
+ }
109
+ }
110
+ serialize() {
111
+ const now = Date.now();
112
+ // 超期清理 + 条数上限(按 updatedAt 淘汰最旧)
113
+ for (const [sid, e] of this.entries) {
114
+ if (now - e.updatedAt > PRUNE_MS && e.updatedAt > 0)
115
+ this.entries.delete(sid);
116
+ }
117
+ while (this.entries.size > RECALL_DEDUPE_SESSION_CAP) {
118
+ let oldest;
119
+ let oldestAt = Infinity;
120
+ for (const [sid, e] of this.entries) {
121
+ if (e.updatedAt > 0 && e.updatedAt < oldestAt) {
122
+ oldest = sid;
123
+ oldestAt = e.updatedAt;
124
+ }
125
+ }
126
+ if (oldest === undefined)
127
+ break; // 只剩惰性空条目(updatedAt=0),不占文件体积可留待过期清理
128
+ this.entries.delete(oldest);
129
+ }
130
+ const sessions = {};
131
+ for (const [sid, e] of this.entries) {
132
+ if (e.ids.size === 0)
133
+ continue; // 空集合不落盘
134
+ sessions[sid] = { recordIds: [...e.ids], updatedAt: e.updatedAt };
135
+ }
136
+ return { version: 1, sessions };
137
+ }
138
+ }
@@ -0,0 +1,59 @@
1
+ import type { MemoryLogger } from '../types.js';
2
+ /** 钉死的 transformers.js 版本(精确版本,升级插件时在此变更并重装运行时)。 */
3
+ export declare const PINNED_TRANSFORMERS_VERSION = "4.2.0";
4
+ import type { RuntimeProgress } from '../contract.js';
5
+ export type { RuntimeProgress } from '../contract.js';
6
+ /** 子进程抽象(测试注入缝):注册输出行回调 + 终止 + 终态。 */
7
+ export interface SpawnedProcess {
8
+ onStdout(cb: (line: string) => void): void;
9
+ onStderr(cb: (line: string) => void): void;
10
+ kill(): void;
11
+ /** 终态 Promise:退出码(null = 被信号杀死)。 */
12
+ exited: Promise<number | null>;
13
+ }
14
+ export type SpawnImpl = (command: string, args: string[], cwd: string) => SpawnedProcess;
15
+ export declare class RuntimeInstaller {
16
+ readonly runtimeDir: string;
17
+ private readonly target;
18
+ private readonly logger?;
19
+ private readonly spawnImpl;
20
+ /** 随包 lockfile 路径(测试可注入;默认取 dist 根下构建期拷入的资产)。 */
21
+ private readonly lockfileSource;
22
+ private progress;
23
+ private child;
24
+ private current;
25
+ /** 安装超时(npm 卡死不罕见:registry 停滞即永挂,applyBusy 会被锁死)。每次 spawn 独立计时。 */
26
+ private static INSTALL_TIMEOUT_MS;
27
+ constructor(dataDir: string, targetVersion: string, opts?: {
28
+ logger?: MemoryLogger;
29
+ spawnImpl?: SpawnImpl;
30
+ lockfileSource?: string;
31
+ });
32
+ /** 包内模块名(与钉死版本一起构成安装目标)。 */
33
+ static packageName: string;
34
+ /** 进度快照。 */
35
+ getProgress(): RuntimeProgress;
36
+ private pkgJsonPath;
37
+ /** 已就位版本(读 package.json;未安装返回 null)。 */
38
+ installedVersion(): Promise<string | null>;
39
+ /** 是否就绪(版本精确匹配目标)。 */
40
+ isReady(): Promise<boolean>;
41
+ /**
42
+ * 确保运行时就位:版本匹配直接就绪;否则安装(忙时并 await 同一次任务)。
43
+ * 返回是否就绪(失败/取消返回 false 并在 progress.error 说明原因)。
44
+ */
45
+ ensure(): Promise<boolean>;
46
+ /**
47
+ * 取消安装(kill 子进程;node_modules 残留无害,npm 幂等重装)。
48
+ * 间隙兼容:ci 退出到回退 install 起跑之间 child 为 null——此时也置取消态,
49
+ * runNpm 起跑前复查即不再起新进程(否则回退的 npm 会跑到自然结束且无法再取消)。
50
+ */
51
+ cancel(): boolean;
52
+ private pushLine;
53
+ /** 跑一次 npm 子进程(采集尾行 + 超时 kill),返回退出码(null = 被杀死/启动失败)。 */
54
+ private runNpm;
55
+ /** 取消态判定。独立方法而非内联比较:cancel() 在 await 期间跨方法置位
56
+ * phase,TS 的属性流分析不跟踪这种突变,内联比较会被窄化误报"无重叠"。 */
57
+ private wasCancelled;
58
+ private installOnce;
59
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * 运行时安装器:本地嵌入的推理运行时(@huggingface/transformers,纯库、二进制随
3
+ * npm 包分发、零 postinstall)在用户首次开启本地嵌入时才安装——插件 npm 包本体
4
+ * 不带重依赖,不用本地嵌入的用户零成本。
5
+ *
6
+ * - 安装位置:数据目录 runtime/(自带 package.json 锚定,防 npm 向上层目录逃逸安装);
7
+ * - 子进程 npm ci --ignore-scripts --no-audit --no-fund,钉死精确版本 + 随包 lockfile
8
+ * (resources/runtime-package-lock.json,构建期拷入 dist/)锁定完整传递依赖树——
9
+ * 纯 install 只锁直接依赖,传递依赖按 semver 浮动解析,registry 端后续发布/投毒
10
+ * 会随安装时间漂移;lockfile 把树冻结在作者侧。ci 失败(锁与 package.json 漂移等)
11
+ * 自动回退 npm install 精确版本(可用性优先,树不再受锁);
12
+ * - 进度(用户硬性要求:不能傻等):npm 非交互模式无百分比 API,采用不确定进度——
13
+ * 已耗时 + 子进程 stdout/stderr 尾行实时流出 + 可 kill;
14
+ * - 幂等:已装版本 == 目标版本直接就绪;版本漂移(插件升级换了钉死版本)重装覆盖。
15
+ */
16
+ import { spawn } from 'node:child_process';
17
+ import { promises as fs } from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ /** 钉死的 transformers.js 版本(精确版本,升级插件时在此变更并重装运行时)。 */
21
+ export const PINNED_TRANSFORMERS_VERSION = '4.2.0';
22
+ export class RuntimeInstaller {
23
+ runtimeDir;
24
+ target;
25
+ logger;
26
+ spawnImpl;
27
+ /** 随包 lockfile 路径(测试可注入;默认取 dist 根下构建期拷入的资产)。 */
28
+ lockfileSource;
29
+ progress;
30
+ child = null;
31
+ current = null;
32
+ /** 安装超时(npm 卡死不罕见:registry 停滞即永挂,applyBusy 会被锁死)。每次 spawn 独立计时。 */
33
+ static INSTALL_TIMEOUT_MS = 10 * 60_000;
34
+ constructor(dataDir, targetVersion, opts) {
35
+ this.runtimeDir = path.join(dataDir, 'runtime');
36
+ this.target = targetVersion;
37
+ this.logger = opts?.logger;
38
+ this.lockfileSource =
39
+ opts?.lockfileSource ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'runtime-package-lock.json');
40
+ this.spawnImpl =
41
+ opts?.spawnImpl ??
42
+ ((command, args, cwd) => {
43
+ const child = spawn(command, args, {
44
+ cwd,
45
+ // Windows 上 .cmd 必须走 shell(Node 20+ 安全限制);参数全部来自插件常量,无注入面
46
+ shell: process.platform === 'win32',
47
+ windowsHide: true,
48
+ });
49
+ const spawned = {
50
+ onStdout(cb) {
51
+ child.stdout?.on('data', (d) => String(d).split(/\r?\n/).forEach((l) => l && cb(l)));
52
+ },
53
+ onStderr(cb) {
54
+ child.stderr?.on('data', (d) => String(d).split(/\r?\n/).forEach((l) => l && cb(l)));
55
+ },
56
+ // Windows shell:true 下 child 只是 cmd.exe,npm/node 孙进程不随 child.kill()
57
+ // 终止(超时与取消都会"表面停止")——taskkill /T 按进程树杀;启动失败回退裸 kill
58
+ kill: () => {
59
+ if (process.platform === 'win32' && child.pid !== undefined) {
60
+ const tk = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
61
+ tk.on('error', () => child.kill());
62
+ }
63
+ else {
64
+ child.kill();
65
+ }
66
+ },
67
+ // 'error'(如 ENOENT:PATH 无 npm)只发 error 不发 close——不监听会永挂
68
+ exited: new Promise((resolve) => {
69
+ child.on('close', (code) => resolve(code));
70
+ child.on('error', () => resolve(null));
71
+ }),
72
+ };
73
+ return spawned;
74
+ });
75
+ this.progress = {
76
+ phase: 'idle',
77
+ targetVersion: targetVersion,
78
+ installedVersion: null,
79
+ startedAt: 0,
80
+ elapsedMs: 0,
81
+ lastLines: [],
82
+ };
83
+ }
84
+ /** 包内模块名(与钉死版本一起构成安装目标)。 */
85
+ static packageName = '@huggingface/transformers';
86
+ /** 进度快照。 */
87
+ getProgress() {
88
+ const elapsed = this.progress.phase === 'installing' ? Date.now() - this.progress.startedAt : this.progress.elapsedMs;
89
+ return { ...this.progress, lastLines: [...this.progress.lastLines], elapsedMs: elapsed };
90
+ }
91
+ pkgJsonPath() {
92
+ return path.join(this.runtimeDir, 'node_modules', RuntimeInstaller.packageName, 'package.json');
93
+ }
94
+ /** 已就位版本(读 package.json;未安装返回 null)。 */
95
+ async installedVersion() {
96
+ try {
97
+ const raw = await fs.readFile(this.pkgJsonPath(), 'utf8');
98
+ const pkg = JSON.parse(raw);
99
+ return typeof pkg.version === 'string' ? pkg.version : null;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ /** 是否就绪(版本精确匹配目标)。 */
106
+ async isReady() {
107
+ return (await this.installedVersion()) === this.target;
108
+ }
109
+ /**
110
+ * 确保运行时就位:版本匹配直接就绪;否则安装(忙时并 await 同一次任务)。
111
+ * 返回是否就绪(失败/取消返回 false 并在 progress.error 说明原因)。
112
+ */
113
+ async ensure() {
114
+ if (await this.isReady()) {
115
+ this.progress.phase = 'ready';
116
+ this.progress.installedVersion = this.target;
117
+ return true;
118
+ }
119
+ if (this.current)
120
+ return this.current;
121
+ this.current = this.installOnce();
122
+ try {
123
+ return await this.current;
124
+ }
125
+ finally {
126
+ this.current = null;
127
+ }
128
+ }
129
+ /**
130
+ * 取消安装(kill 子进程;node_modules 残留无害,npm 幂等重装)。
131
+ * 间隙兼容:ci 退出到回退 install 起跑之间 child 为 null——此时也置取消态,
132
+ * runNpm 起跑前复查即不再起新进程(否则回退的 npm 会跑到自然结束且无法再取消)。
133
+ */
134
+ cancel() {
135
+ if (this.progress.phase !== 'installing')
136
+ return false;
137
+ this.progress.phase = 'cancelled';
138
+ this.child?.kill();
139
+ return true;
140
+ }
141
+ pushLine(line) {
142
+ const lines = this.progress.lastLines;
143
+ lines.push(line.length > 300 ? line.slice(0, 300) + '…' : line);
144
+ if (lines.length > 5)
145
+ lines.splice(0, lines.length - 5);
146
+ }
147
+ /** 跑一次 npm 子进程(采集尾行 + 超时 kill),返回退出码(null = 被杀死/启动失败)。 */
148
+ async runNpm(args) {
149
+ // 起跑前复查取消:cancel() 在上一进程退出与本进程 spawn 之间的间隙置位时,不再起新进程
150
+ if (this.progress.phase === 'cancelled')
151
+ return null;
152
+ const child = this.spawnImpl('npm', args, this.runtimeDir);
153
+ this.child = child;
154
+ child.onStdout((l) => this.pushLine(l));
155
+ child.onStderr((l) => this.pushLine(l));
156
+ const timeout = setTimeout(() => {
157
+ this.pushLine('安装超时(10 分钟),终止子进程');
158
+ child.kill();
159
+ }, RuntimeInstaller.INSTALL_TIMEOUT_MS);
160
+ const code = await child.exited;
161
+ clearTimeout(timeout);
162
+ this.child = null;
163
+ return code;
164
+ }
165
+ /** 取消态判定。独立方法而非内联比较:cancel() 在 await 期间跨方法置位
166
+ * phase,TS 的属性流分析不跟踪这种突变,内联比较会被窄化误报"无重叠"。 */
167
+ wasCancelled() {
168
+ return this.progress.phase === 'cancelled';
169
+ }
170
+ async installOnce() {
171
+ // 锚定 package.json(带精确依赖):没有它 npm 会向上层目录找最近的 package.json 安装(逃逸事故);
172
+ // npm ci 还要求它与 lockfile 根条目一致——每次安装都写规范化形状,覆盖历史遗留/被 npm 改写的副本。
173
+ await fs.mkdir(this.runtimeDir, { recursive: true });
174
+ const manifest = {
175
+ name: 'dsh-memory-runtime',
176
+ private: true,
177
+ dependencies: { [RuntimeInstaller.packageName]: this.target },
178
+ };
179
+ await fs.writeFile(path.join(this.runtimeDir, 'package.json'), JSON.stringify(manifest, null, 2));
180
+ this.progress = {
181
+ phase: 'installing',
182
+ targetVersion: this.target,
183
+ installedVersion: await this.installedVersion(),
184
+ startedAt: Date.now(),
185
+ elapsedMs: 0,
186
+ lastLines: [],
187
+ };
188
+ this.logger?.info(`[memory] 运行时安装开始: ${RuntimeInstaller.packageName}@${this.target} → ${this.runtimeDir}`);
189
+ // 首选 npm ci + 随包 lockfile(传递依赖树冻结在作者侧);lockfile 资产缺失(npm 包被裁剪等)
190
+ // 或 ci 失败(锁与锚定版本漂移等)回退 npm install 精确版本——可用性优先。
191
+ let code = null;
192
+ let usedCi = false;
193
+ try {
194
+ const lock = await fs.readFile(this.lockfileSource, 'utf8');
195
+ await fs.writeFile(path.join(this.runtimeDir, 'package-lock.json'), lock);
196
+ this.pushLine(`npm ci(随包 lockfile 锁定依赖树,@${this.target},--ignore-scripts)`);
197
+ usedCi = true;
198
+ code = await this.runNpm(['ci', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel', 'notice']);
199
+ }
200
+ catch {
201
+ /* 无随包 lockfile,直接走 install 回退 */
202
+ }
203
+ // ci 阶段被取消(退出码 null)不得落入回退分支——那是"ci 失败"语义,会让
204
+ // 取消后再白跑一次最长 10 分钟的 install
205
+ if (this.wasCancelled()) {
206
+ this.progress.elapsedMs = Date.now() - this.progress.startedAt;
207
+ this.logger?.warn('[memory] 运行时安装已取消(残留无害,重装幂等)');
208
+ return false;
209
+ }
210
+ if (!usedCi || code !== 0) {
211
+ if (usedCi) {
212
+ this.pushLine('npm ci 失败(lockfile 与钉死版本漂移?),回退 npm install');
213
+ this.logger?.warn('[memory] 运行时 npm ci 失败,回退 npm install(传递依赖不再受随包 lockfile 锁定)');
214
+ }
215
+ this.pushLine(`npm install ${RuntimeInstaller.packageName}@${this.target}(--ignore-scripts)`);
216
+ code = await this.runNpm([
217
+ 'install',
218
+ '--ignore-scripts',
219
+ '--no-audit',
220
+ '--no-fund',
221
+ '--loglevel',
222
+ 'notice',
223
+ `${RuntimeInstaller.packageName}@${this.target}`,
224
+ ]);
225
+ }
226
+ this.progress.elapsedMs = Date.now() - this.progress.startedAt;
227
+ const version = await this.installedVersion();
228
+ this.progress.installedVersion = version;
229
+ if (this.wasCancelled()) {
230
+ this.logger?.warn('[memory] 运行时安装已取消(残留无害,重装幂等)');
231
+ return false;
232
+ }
233
+ if (code === 0 && version === this.target) {
234
+ this.progress.phase = 'ready';
235
+ this.logger?.info(`[memory] 运行时安装完成: v${version}`);
236
+ return true;
237
+ }
238
+ this.progress.phase = 'error';
239
+ this.progress.error = `npm 退出码 ${code ?? '被杀死'}${version ? `(就位版本 ${version})` : '(模块未就位)'}`;
240
+ this.logger?.warn(`[memory] 运行时安装失败: ${this.progress.error}`);
241
+ return false;
242
+ }
243
+ }
@@ -0,0 +1,24 @@
1
+ import type { MemoryFamily, MemoryLogger, SceneSummary } from '../types.js';
2
+ export declare class SceneStore {
3
+ private readonly logger?;
4
+ private readonly dir;
5
+ private readonly family;
6
+ constructor(dataDir: string, family: MemoryFamily, logger?: MemoryLogger | undefined);
7
+ init(): Promise<void>;
8
+ /** 旧布局迁移:scenes/ 根下的 .md 移入本族目录。仅 chat 族执行(历史数据归属 chat)。 */
9
+ private migrateLegacyLayout;
10
+ listFiles(): Promise<string[]>;
11
+ /** 列出场景摘要(解析 META 块)。 */
12
+ list(): Promise<SceneSummary[]>;
13
+ read(name: string): Promise<string | undefined>;
14
+ /**
15
+ * 写入/重写场景文件。content 为 [DELETED] 时删除该文件(LLM 的 delete 操作)。
16
+ * 文件名自动归一化(空格→短横线、剔除非法字符),非法则抛错。
17
+ */
18
+ write(name: string, content: string): Promise<string>;
19
+ /** 场景导航索引(召回注入用)。 */
20
+ navigation(): Promise<string>;
21
+ }
22
+ /** 文件名归一化:只允许字母数字 CJK - _ .,以 .md 结尾,去空格/标点。
23
+ * 超长名截断到 120 字符(ENAMETOOLONG 防御);Windows 保留设备名加前缀 _ 避让。 */
24
+ export declare function sanitizeFilename(name: string): string;