@kira905/dsh-memory-toolkit 0.0.0-stage → 0.1.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.md +42 -0
- package/LICENSE +661 -0
- package/README.en.md +110 -0
- package/README.md +102 -2
- package/dsh-memory-local/FORK-BASE.json +24 -0
- package/dsh-memory-local/README.md +113 -0
- package/dsh-memory-local/build.mjs +101 -0
- package/dsh-memory-local/cordis.patch.yml +20 -0
- package/dsh-memory-local/deploy.mjs +306 -0
- package/dsh-memory-local/entity-status.mjs +75 -0
- package/dsh-memory-local/lib/bridge.mjs +108 -0
- package/dsh-memory-local/lib/host-adapter.js +1171 -0
- package/dsh-memory-local/lib/index.d.ts +128 -0
- package/dsh-memory-local/lib/index.js +944 -0
- package/dsh-memory-local/lib/injector.mjs +400 -0
- package/dsh-memory-local/lib/memory-read.mjs +116 -0
- package/dsh-memory-local/lib/module-gate.mjs +68 -0
- package/dsh-memory-local/lib/pointer.mjs +152 -0
- package/dsh-memory-local/lib/prod-gate.mjs +121 -0
- package/dsh-memory-local/lib/recall-log.mjs +118 -0
- package/dsh-memory-local/lib/recall.mjs +192 -0
- package/dsh-memory-local/lib/schema-migrate.mjs +365 -0
- package/dsh-memory-local/lib/user-message.mjs +32 -0
- package/dsh-memory-local/lib/vectors-read.mjs +110 -0
- package/dsh-memory-local/package.json +51 -0
- package/dsh-memory-local/restore.mjs +259 -0
- package/dsh-memory-local/rollback-injector-fix.mjs +166 -0
- package/dsh-memory-local/src/bridge.mjs +108 -0
- package/dsh-memory-local/src/host-adapter.js +1171 -0
- package/dsh-memory-local/src/index.d.ts +128 -0
- package/dsh-memory-local/src/index.js +944 -0
- package/dsh-memory-local/src/injector.mjs +400 -0
- package/dsh-memory-local/src/memory-read.mjs +116 -0
- package/dsh-memory-local/src/module-gate.mjs +68 -0
- package/dsh-memory-local/src/pointer.mjs +152 -0
- package/dsh-memory-local/src/recall-log.mjs +118 -0
- package/dsh-memory-local/src/recall.mjs +192 -0
- package/dsh-memory-local/src/schema-migrate.mjs +365 -0
- package/dsh-memory-local/src/user-message.mjs +32 -0
- package/dsh-memory-local/src/vectors-read.mjs +110 -0
- package/dsh-memory-local/test/fixtures-s2b.mjs +216 -0
- package/dsh-memory-local/test/probe-entity.mjs +170 -0
- package/dsh-memory-local/test/regression.mjs +270 -0
- package/dsh-memory-local/test/s2b.test.mjs +568 -0
- package/dsh-memory-local/test/schema-migrate.test.mjs +334 -0
- package/dsh-memory-local/test/test-adapter-converge.mjs +127 -0
- package/dsh-memory-local/test/visible-injections.test.mjs +239 -0
- package/dsh-memory-local/verify-after-restart.mjs +181 -0
- package/lib/deploy-guard.mjs +391 -0
- package/lib/guard-http.mjs +93 -0
- package/lib/pitfall-form-map.mjs +130 -0
- package/lib/pitfall-score.mjs +185 -0
- package/lib/session-file-name.mjs +160 -0
- package/lib/zstd-frames.mjs +78 -0
- package/memory-db-tools/README.md +68 -0
- package/memory-db-tools/_test/test-memory-scope.mjs +302 -0
- package/memory-db-tools/audit-analyze.mjs +352 -0
- package/memory-db-tools/audit-dump-ids.mjs +245 -0
- package/memory-db-tools/audit-export.mjs +270 -0
- package/memory-db-tools/audit-hit-probe.mjs +227 -0
- package/memory-db-tools/audit-ledger-probe.mjs +236 -0
- package/memory-db-tools/audit-persona-overlap.mjs +277 -0
- package/memory-db-tools/audit-struct-probe.mjs +213 -0
- package/memory-db-tools/audit-verify-unchanged.mjs +276 -0
- package/memory-db-tools/check-memory-db.mjs +8 -0
- package/memory-db-tools/dump-memory-db.mjs +12 -0
- package/memory-db-tools/lib/memory-scope.mjs +446 -0
- package/memory-db-tools/list-memory-summary.mjs +477 -0
- package/memory-db-tools/memory-identity-check.mjs +101 -0
- package/memory-db-tools/memory-schema-migrate.mjs +290 -0
- package/memory-db-tools/memory-scope-fixture.mjs +197 -0
- package/memory-db-tools/read-memories.mjs +26 -0
- package/memory-db-tools/read-memory-db.mjs +20 -0
- package/memory-db-tools/refine-memories.mjs +247 -0
- package/memory-db-tools/show-mem-times.mjs +22 -0
- package/memory-engine/README.md +70 -0
- package/memory-engine/bench/bench.mjs +221 -0
- package/memory-engine/cli/distill-rollback.mjs +86 -0
- package/memory-engine/cli/distill.mjs +156 -0
- package/memory-engine/cli/doc-index-rebuild.mjs +155 -0
- package/memory-engine/cli/doc-index.mjs +48 -0
- package/memory-engine/cli/domain-tags.mjs +251 -0
- package/memory-engine/cli/pitfall-hook.mjs +91 -0
- package/memory-engine/cli/recall.mjs +132 -0
- package/memory-engine/cli/selfcheck.mjs +57 -0
- package/memory-engine/cli/serve-start.mjs +247 -0
- package/memory-engine/cli/sweep.mjs +58 -0
- package/memory-engine/cli/sync.mjs +69 -0
- package/memory-engine/lib/cli-util.mjs +197 -0
- package/memory-engine/lib/config.mjs +135 -0
- package/memory-engine/lib/dirty-ledger.mjs +174 -0
- package/memory-engine/lib/distill-rollback.mjs +178 -0
- package/memory-engine/lib/distill.mjs +898 -0
- package/memory-engine/lib/doc-index-store.mjs +325 -0
- package/memory-engine/lib/doc-index.mjs +529 -0
- package/memory-engine/lib/doc-scan.mjs +271 -0
- package/memory-engine/lib/doc-summarize.mjs +305 -0
- package/memory-engine/lib/domain-tags.mjs +429 -0
- package/memory-engine/lib/embedder.mjs +242 -0
- package/memory-engine/lib/hashes.mjs +12 -0
- package/memory-engine/lib/memory-source.mjs +127 -0
- package/memory-engine/lib/pitfall-source.mjs +170 -0
- package/memory-engine/lib/policy-source.mjs +371 -0
- package/memory-engine/lib/recall.mjs +331 -0
- package/memory-engine/lib/selfcheck.mjs +131 -0
- package/memory-engine/lib/sweep.mjs +159 -0
- package/memory-engine/lib/sync.mjs +180 -0
- package/memory-engine/lib/tick-hook.mjs +218 -0
- package/memory-engine/lib/vectors-store.mjs +292 -0
- package/memory-engine/package-lock.json +1086 -0
- package/memory-engine/package.json +22 -0
- package/memory-engine/serve.mjs +171 -0
- package/memory-engine/test/01-store.test.mjs +127 -0
- package/memory-engine/test/02-sync.test.mjs +196 -0
- package/memory-engine/test/03-sweep.test.mjs +145 -0
- package/memory-engine/test/04-recall.test.mjs +214 -0
- package/memory-engine/test/05-selfcheck.test.mjs +175 -0
- package/memory-engine/test/06-doc-index.test.mjs +928 -0
- package/memory-engine/test/07-domain-tags.test.mjs +298 -0
- package/memory-engine/test/08-distill.test.mjs +686 -0
- package/memory-engine/test/09-pitfall-source.test.mjs +365 -0
- package/memory-engine/test/10-tick-hook.test.mjs +252 -0
- package/memory-engine/test/11-policy-source.test.mjs +224 -0
- package/memory-engine/test/_tickhook-worker.mjs +56 -0
- package/memory-engine/test/fixtures.mjs +205 -0
- package/memory-engine/test/harness.mjs +73 -0
- package/memory-engine/test/run.mjs +35 -0
- package/memory-engine/tools/bridge-smoke.mjs +80 -0
- package/memory-engine/tools/fetch-model.mjs +128 -0
- package/memory-engine/tools/smoke-recall.mjs +76 -0
- package/package.json +53 -4
- package/persona-memory-audit.mjs +217 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FORK-EDIT(2026-09-21): 记忆引擎 v2 · S3b —— `memories` 表结构自检(方案 §3.8 / §3.9)
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须有这个文件(三条硬理由):
|
|
5
|
+
* ① **表已存在的库上 `CREATE TABLE IF NOT EXISTS` 整条 DDL 不生效** ⇒ 新列永远加不上(§3.8 明文),
|
|
6
|
+
* 所以只能 `PRAGMA table_info` 查列 → 缺哪列补哪列 `ALTER TABLE memories ADD COLUMN …`;
|
|
7
|
+
* ② 加列必须发生在**库被打开的那一瞬间**(插件是这张表唯一的常驻打开者),
|
|
8
|
+
* 且**失败绝不阻断 DSH 启动** ⇒ 全链路包 try,失败只落日志 + 结构化降级标记;
|
|
9
|
+
* ③ 两列口径(列定义 / 默认值来源 / 索引名)的**权威源**是
|
|
10
|
+
* 本仓 `memory-db-tools/lib/memory-scope.mjs`(逐字同源)——
|
|
11
|
+
* 本文件是它在 fork 内的**最小自持镜像**:插件部署到 profile 后只带 `lib/`,
|
|
12
|
+
* **无法 import 那条绝对路径**(也不该依赖 DSH 工作目录的绝对路径),故必须镜像一份。
|
|
13
|
+
* 两者的一致性由 `test/schema-migrate.test.mjs` 的**逐字对照断言**守住(漂移即测试红)。
|
|
14
|
+
*
|
|
15
|
+
* 本模块零 peer 依赖(只用 `node:sqlite` 与 node 内置模块)⇒ 可在源码目录直接单测,无需部署。
|
|
16
|
+
*/
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
const { existsSync, readFileSync, mkdirSync, appendFileSync } = fs;
|
|
21
|
+
const { dirname, join } = path;
|
|
22
|
+
|
|
23
|
+
/** 计量单位:加列前的既有列(§3.8 起点,与权威源 LEGACY_COLUMNS 一致)。 */
|
|
24
|
+
export const LEGACY_COLUMNS = Object.freeze(["id", "text", "tags", "pinned", "created_at", "updated_at"]);
|
|
25
|
+
|
|
26
|
+
/** visibility 取值域(§3.3-1)。 */
|
|
27
|
+
export const VISIBILITY_VALUES = Object.freeze(["private_owner", "family", "public"]);
|
|
28
|
+
|
|
29
|
+
/** visibility 默认值(§3.3-1 / 家人档 §3.4)。 */
|
|
30
|
+
export const DEFAULT_VISIBILITY = "private_owner";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `owner_user` 缺省**回落**常量。
|
|
34
|
+
* ⚠️ 它不是硬编码人名,而是"本机 owner 的 userid"的兜底值:正常取值来自身份表里
|
|
35
|
+
* `role === 'owner'` 的那条;取不到才回落这里,并在自检日志里**报一行**(§3.3-3)。
|
|
36
|
+
*/
|
|
37
|
+
export const FALLBACK_OWNER_USER = "LiJianXiao";
|
|
38
|
+
|
|
39
|
+
/** 身份表文件名(家人档 §3.3:`.dsh/secretary-users.json`,不进任何仓)。 */
|
|
40
|
+
export const IDENTITY_TABLE_NAME = "secretary-users.json";
|
|
41
|
+
|
|
42
|
+
/** 两列定义(与权威源 NEW_COLUMNS 逐字一致)。`defaultKind: 'owner'` = 默认值运行时解析。 */
|
|
43
|
+
export const NEW_COLUMNS = Object.freeze([
|
|
44
|
+
Object.freeze({
|
|
45
|
+
name: "owner_user",
|
|
46
|
+
type: "TEXT",
|
|
47
|
+
notNull: true,
|
|
48
|
+
defaultKind: "owner",
|
|
49
|
+
doc: "谁的(取值域 = 宿主账号体系的 user id)"
|
|
50
|
+
}),
|
|
51
|
+
Object.freeze({
|
|
52
|
+
name: "visibility",
|
|
53
|
+
type: "TEXT",
|
|
54
|
+
notNull: true,
|
|
55
|
+
defaultKind: "literal",
|
|
56
|
+
defaultLiteral: DEFAULT_VISIBILITY,
|
|
57
|
+
doc: "private_owner | family | public"
|
|
58
|
+
})
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/** visibility 索引(§3.8;家人侧过滤用)。 */
|
|
62
|
+
export const VISIBILITY_INDEX_NAME = "memories_visibility";
|
|
63
|
+
export const VISIBILITY_INDEX_SQL = `CREATE INDEX IF NOT EXISTS ${VISIBILITY_INDEX_NAME} ON memories (visibility, updated_at DESC)`;
|
|
64
|
+
|
|
65
|
+
/** SQL 字符串字面量转义(单引号翻倍)。 */
|
|
66
|
+
export function quoteLiteral(s) {
|
|
67
|
+
return `'${String(s).replace(/'/g, "''")}'`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 解析身份表文本 → `{ ok, userids, owner, error }`。
|
|
72
|
+
*
|
|
73
|
+
* **三种形态都认**(2026-10-11 卡【翻译官·批4·家人档地基】扩了一档;与权威源 `memory-scope.mjs` 同解):
|
|
74
|
+
* ① 平面 map(历史权威形态)`{ "<userid>": { role, name, caps } }`,`_` 前缀键与元数据键跳过;
|
|
75
|
+
* ② **`{version, default:"deny", users:{…}}`** —— 该形态是**新形态**
|
|
76
|
+
* §5.6 定稿的**新形态**(`users` 是 **map** 不是数组)。⚠️ 旧实现只认 `users` 是**数组**的包装
|
|
77
|
+
* ⇒ 新表一落盘会被**读成空表**(owner 回落常量 / 全部 owner 判"未登记")= **静默失真**;
|
|
78
|
+
* ③ 数组 `[{userid,…}]` 与 `{ users: [ … ] }`(家人档定稿前的历史形态)。
|
|
79
|
+
* @param {string} text - 身份表 JSON 文本。
|
|
80
|
+
* @returns {{ok:boolean,userids:string[],owner:string|null,error:string|null}}
|
|
81
|
+
*/
|
|
82
|
+
export function parseIdentityTable(text) {
|
|
83
|
+
let doc;
|
|
84
|
+
try {
|
|
85
|
+
doc = JSON.parse(String(text));
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return { ok: false, userids: [], owner: null, error: `身份表 JSON 解析失败:${e.message}` };
|
|
88
|
+
}
|
|
89
|
+
const userids = [];
|
|
90
|
+
let owner = null;
|
|
91
|
+
const take = (userid, rec) => {
|
|
92
|
+
// ① 键侧的 `_` 注释约定 / 元数据键:**一律跳过**(值侧也一样 —— 见下)
|
|
93
|
+
if (typeof userid === "string") {
|
|
94
|
+
const k = userid.trim();
|
|
95
|
+
if (k.startsWith("_") || k === "$schema" || METADATA_KEYS.has(k)) return;
|
|
96
|
+
}
|
|
97
|
+
// ② 简写形态:值就是 userid 字符串(`{"<userid>": "角色"}` / `["uid",…]`)
|
|
98
|
+
if (typeof rec === "string") {
|
|
99
|
+
const v = rec.trim();
|
|
100
|
+
if (v && !v.startsWith("_") && !METADATA_KEYS.has(v) && !userids.includes(v)) userids.push(v);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (typeof userid !== "string") return;
|
|
104
|
+
const id = userid.trim();
|
|
105
|
+
if (id.length === 0) return;
|
|
106
|
+
if (!userids.includes(id)) userids.push(id);
|
|
107
|
+
if (owner === null && rec && typeof rec === "object" && rec.role === "owner") owner = id;
|
|
108
|
+
};
|
|
109
|
+
// ③ 数组根:**只认"记录数组/字符串数组"**(`[1,2]` 这类必须判不认 —— 否则"认不出"会悄悄变成"认出来是空的")
|
|
110
|
+
if (Array.isArray(doc)) {
|
|
111
|
+
if (!isRecordArray(doc)) return { ok: false, userids: [], owner: null, error: "身份表根节点是数组,但元素不是记录" };
|
|
112
|
+
for (const e of doc) (typeof e === "string") ? take(e, {}) : take(e.userid ?? e.user ?? e.id, e);
|
|
113
|
+
return { ok: true, userids, owner, error: null };
|
|
114
|
+
}
|
|
115
|
+
if (!doc || typeof doc !== "object") return { ok: false, userids: [], owner: null, error: "身份表根节点既不是对象也不是数组" };
|
|
116
|
+
const inner = doc.users ?? doc.members ?? null;
|
|
117
|
+
if (Array.isArray(inner)) { // 历史包装 `{ users: [ … ] }`
|
|
118
|
+
if (!isRecordArray(inner)) return { ok: false, userids: [], owner: null, error: "`users` 是数组但元素不是记录" };
|
|
119
|
+
for (const e of inner) (typeof e === "string") ? take(e, {}) : take(e.userid ?? e.user ?? e.id, e);
|
|
120
|
+
return { ok: true, userids, owner, error: null };
|
|
121
|
+
}
|
|
122
|
+
if (inner && typeof inner === "object") { // ② 新形态 `{ users: { <userid>: {…} } }`
|
|
123
|
+
for (const [uid, rec] of Object.entries(inner)) take(uid, rec);
|
|
124
|
+
return { ok: true, userids, owner, error: null };
|
|
125
|
+
}
|
|
126
|
+
for (const [userid, rec] of Object.entries(doc)) take(userid, rec); // ① 平面 map(`_` 前缀 / 元数据键由 take 内跳过)
|
|
127
|
+
return { ok: true, userids, owner, error: null };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 表级元数据键(不是 userid)。 */
|
|
131
|
+
const METADATA_KEYS = new Set(["version", "schemaVersion", "default", "product", "updatedAt", "note", "$schema"]);
|
|
132
|
+
|
|
133
|
+
/** `[{userid,…}]` / `["uid",…]` ⇒ true;`[1,2]` ⇒ false。 */
|
|
134
|
+
function isRecordArray(v) {
|
|
135
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string" || (x !== null && typeof x === "object" && !Array.isArray(x)));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 定位身份表:`base` 可能是 `.dsh` 目录、`.dsh` 的父目录(USERPROFILE),或身份表文件本身。
|
|
140
|
+
* @returns {string} 候选路径中第一个存在的;都不存在则返回首选候选(供错误信息展示)。
|
|
141
|
+
*/
|
|
142
|
+
export function identityTablePath(base) {
|
|
143
|
+
if (typeof base === "string" && /\.json$/i.test(base)) return base;
|
|
144
|
+
const root = base && base.length > 0 ? base : ".";
|
|
145
|
+
const candidates = [join(root, IDENTITY_TABLE_NAME), join(root, ".dsh", IDENTITY_TABLE_NAME)];
|
|
146
|
+
for (const c of candidates) {
|
|
147
|
+
try {
|
|
148
|
+
if (existsSync(c)) return c;
|
|
149
|
+
} catch {
|
|
150
|
+
/* 忽略:继续试下一个候选 */
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return candidates[0];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 只读身份表文件;**永不抛错**(读不到就返回不 ok,由调用方回落常量)。
|
|
158
|
+
* @param {string} [home] - DSH home(`.dsh` 目录)或身份表文件路径。
|
|
159
|
+
* @returns {{ok:boolean,path:string,exists:boolean,userids:string[],owner:string|null,error:string|null}}
|
|
160
|
+
*/
|
|
161
|
+
export function readIdentityTable(home) {
|
|
162
|
+
const p = identityTablePath(home);
|
|
163
|
+
let text;
|
|
164
|
+
try {
|
|
165
|
+
if (!existsSync(p)) return { ok: false, path: p, exists: false, userids: [], owner: null, error: "身份表不存在(家人档身份表未落地)" };
|
|
166
|
+
text = readFileSync(p, "utf8");
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return { ok: false, path: p, exists: true, userids: [], owner: null, error: `身份表读不到:${e && e.message ? e.message : e}` };
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, path: p, exists: true, ...parseIdentityTable(text) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 解析 `owner_user` 的默认值(§3.3-3 的取值优先级):
|
|
175
|
+
* ① 身份表里 `role === 'owner'` 的那条 ② 缺省回落常量 + 一行说明。
|
|
176
|
+
* @param {{ok:boolean,exists?:boolean,owner?:string|null,error?:string|null,path?:string}} identity
|
|
177
|
+
* @returns {{ownerUser:string,source:'identity-table'|'fallback',notes:string[]}}
|
|
178
|
+
*/
|
|
179
|
+
export function resolveDefaultOwner(identity) {
|
|
180
|
+
const notes = [];
|
|
181
|
+
if (identity && identity.ok && identity.owner) return { ownerUser: identity.owner, source: "identity-table", notes };
|
|
182
|
+
if (identity && identity.exists && identity.ok && !identity.owner) {
|
|
183
|
+
notes.push(`身份表 ${identity.path} 存在但没有任何 role==='owner' 的条目 → 回落常量`);
|
|
184
|
+
} else if (identity && identity.error) {
|
|
185
|
+
notes.push(`身份表不可用(${identity.error}) → 回落常量`);
|
|
186
|
+
}
|
|
187
|
+
notes.push(`owner_user 默认值回落常量 '${FALLBACK_OWNER_USER}'(身份表缺位时的人工兜底,非硬编码设计)`);
|
|
188
|
+
return { ownerUser: FALLBACK_OWNER_USER, source: "fallback", notes };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 读某表列名(PRAGMA table_info;表不存在/失败返回 [])。 */
|
|
192
|
+
export function columnsOf(db, table = "memories") {
|
|
193
|
+
try {
|
|
194
|
+
return db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name);
|
|
195
|
+
} catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 索引是否已存在(查 sqlite_master;失败按"不存在"处理,交由 DDL 的 IF NOT EXISTS 兜底)。 */
|
|
201
|
+
export function hasIndex(db, name) {
|
|
202
|
+
try {
|
|
203
|
+
return db.prepare("SELECT 1 AS x FROM sqlite_master WHERE type='index' AND name=?").get(name) !== undefined;
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 单列的 `ALTER TABLE ADD COLUMN` 语句(与权威源 buildAddColumnSql 逐字同构)。
|
|
211
|
+
* ⚠️ SQLite 的 ADD COLUMN 默认值**必须是常量表达式** ⇒ "从身份表取默认值"只能在这里
|
|
212
|
+
* 先把常量解析出来再拼进 DDL,而不是库端函数。
|
|
213
|
+
*/
|
|
214
|
+
export function buildAddColumnSql(col, defaultLiteral, table = "memories") {
|
|
215
|
+
const parts = [`ALTER TABLE ${table} ADD COLUMN ${col.name} ${col.type}`];
|
|
216
|
+
if (col.notNull) parts.push("NOT NULL");
|
|
217
|
+
parts.push(`DEFAULT ${quoteLiteral(defaultLiteral)}`);
|
|
218
|
+
return parts.join(" ");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 迁移计划(**只读**,不改库):算缺哪些列、要跑哪些语句、默认值取什么。
|
|
223
|
+
* @returns {{columns:string[],missing:string[],present:string[],indexExists:boolean,statements:string[],defaultOwner:object,notes:string[]}}
|
|
224
|
+
*/
|
|
225
|
+
export function migrationPlan(db, { identity = null, table = "memories" } = {}) {
|
|
226
|
+
const resolved = resolveDefaultOwner(identity);
|
|
227
|
+
const columns = columnsOf(db, table);
|
|
228
|
+
const missing = NEW_COLUMNS.filter((c) => !columns.includes(c.name));
|
|
229
|
+
const present = NEW_COLUMNS.filter((c) => columns.includes(c.name)).map((c) => c.name);
|
|
230
|
+
const statements = missing.map((c) => buildAddColumnSql(c, c.defaultKind === "owner" ? resolved.ownerUser : c.defaultLiteral, table));
|
|
231
|
+
const indexExists = hasIndex(db, VISIBILITY_INDEX_NAME);
|
|
232
|
+
if (!indexExists) statements.push(VISIBILITY_INDEX_SQL);
|
|
233
|
+
return { columns, missing: missing.map((c) => c.name), present, indexExists, statements, defaultOwner: resolved, notes: resolved.notes };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 执行迁移(单事务;语句来自 migrationPlan,列已存在则天然无语句可跑)。返回实际执行的语句数组。 */
|
|
237
|
+
export function applyMigration(db, plan) {
|
|
238
|
+
const ran = [];
|
|
239
|
+
db.exec("BEGIN IMMEDIATE");
|
|
240
|
+
try {
|
|
241
|
+
for (const sql of plan.statements) {
|
|
242
|
+
db.exec(sql);
|
|
243
|
+
ran.push(sql);
|
|
244
|
+
}
|
|
245
|
+
db.exec("COMMIT");
|
|
246
|
+
} catch (e) {
|
|
247
|
+
try {
|
|
248
|
+
db.exec("ROLLBACK");
|
|
249
|
+
} catch {
|
|
250
|
+
/* 忽略:回滚失败时向上抛原始错误(由调用方降级处理) */
|
|
251
|
+
}
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
return ran;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 高层入口(**插件启动自检**用):幂等加列 + 建索引,**全链路包 try,永不抛**。
|
|
259
|
+
*
|
|
260
|
+
* 设计约束(方案 §3.8 风险与对策③):
|
|
261
|
+
* · 自检失败 ⇒ **不加列 + 功能降级,绝不阻断 DSH 启动** ⇒ 本函数返回结构化结果,不 throw;
|
|
262
|
+
* · 只有"发生了变更 / 失败 / 降级"才写日志(幂等无变化时不写,避免每次启动刷日志)。
|
|
263
|
+
*
|
|
264
|
+
* @param {object} db - 已打开的 `node:sqlite` DatabaseSync 连接(写模式)。
|
|
265
|
+
* @param {object} [opts]
|
|
266
|
+
* @param {string} [opts.home] - DSH home(`.dsh` 目录);缺省由 dbPath 推导(`<home>/memory/memory.db`)。
|
|
267
|
+
* @param {string} [opts.dbPath] - 库路径,用于推导 home 与写日志。
|
|
268
|
+
* @param {string} [opts.logFile] - 日志文件路径;缺省 `<home>/logs/memory-schema.log`。
|
|
269
|
+
* @param {(entry:object)=>void} [opts.onLog] - 额外日志回调(测试用)。
|
|
270
|
+
* @returns {{ok:boolean,degraded:boolean,added:string[],present:string[],indexCreated:boolean,statements:string[],ownerUser:string,ownerSource:string,notes:string[],error:string|null,skipped:string|null}}
|
|
271
|
+
*/
|
|
272
|
+
export function ensureMemoryColumns(db, opts = {}) {
|
|
273
|
+
const result = {
|
|
274
|
+
ok: false,
|
|
275
|
+
degraded: false,
|
|
276
|
+
added: [],
|
|
277
|
+
present: [],
|
|
278
|
+
indexCreated: false,
|
|
279
|
+
statements: [],
|
|
280
|
+
ownerUser: FALLBACK_OWNER_USER,
|
|
281
|
+
ownerSource: "fallback",
|
|
282
|
+
notes: [],
|
|
283
|
+
error: null,
|
|
284
|
+
skipped: null
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
// ① 表不存在(空库尚未建表)⇒ 本次跳过,交给建表 DDL
|
|
288
|
+
const columns = columnsOf(db);
|
|
289
|
+
if (columns.length === 0) {
|
|
290
|
+
result.ok = true;
|
|
291
|
+
result.skipped = "memories 表不存在 ⇒ 本次自检跳过(由建表 DDL 负责)";
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
// ② 身份表 → owner_user 默认值
|
|
295
|
+
const home = opts.home ?? deriveHome(opts.dbPath);
|
|
296
|
+
const identity = readIdentityTable(home);
|
|
297
|
+
const resolved = resolveDefaultOwner(identity);
|
|
298
|
+
result.ownerUser = resolved.ownerUser;
|
|
299
|
+
result.ownerSource = resolved.source;
|
|
300
|
+
result.notes = resolved.notes;
|
|
301
|
+
// ③ 只读规划
|
|
302
|
+
const plan = migrationPlan(db, { identity });
|
|
303
|
+
result.present = plan.present;
|
|
304
|
+
result.statements = plan.statements;
|
|
305
|
+
// ④ 无变更 ⇒ 幂等返回(不写日志)
|
|
306
|
+
if (plan.statements.length === 0) {
|
|
307
|
+
result.ok = true;
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
// ⑤ 执行(单事务)
|
|
311
|
+
const ran = applyMigration(db, plan);
|
|
312
|
+
result.statements = ran;
|
|
313
|
+
result.added = ran
|
|
314
|
+
.filter((s) => /ADD COLUMN/.test(s))
|
|
315
|
+
.map((s) => (s.match(/ADD COLUMN (\w+)/) || [])[1])
|
|
316
|
+
.filter(Boolean);
|
|
317
|
+
result.indexCreated = ran.some((s) => s.includes(VISIBILITY_INDEX_NAME));
|
|
318
|
+
result.ok = true;
|
|
319
|
+
writeSchemaLog(opts, {
|
|
320
|
+
event: "migrate",
|
|
321
|
+
ok: true,
|
|
322
|
+
added: result.added,
|
|
323
|
+
indexCreated: result.indexCreated,
|
|
324
|
+
ownerUser: result.ownerUser,
|
|
325
|
+
ownerSource: result.ownerSource,
|
|
326
|
+
dbPath: opts.dbPath ?? null
|
|
327
|
+
});
|
|
328
|
+
return result;
|
|
329
|
+
} catch (e) {
|
|
330
|
+
// ⑥ 失败 ⇒ 降级 + 落日志,**绝不抛**(绝不阻断 DSH 启动)
|
|
331
|
+
result.ok = false;
|
|
332
|
+
result.degraded = true;
|
|
333
|
+
result.error = e && e.message ? String(e.message) : String(e);
|
|
334
|
+
writeSchemaLog(opts, { event: "migrate", ok: false, degraded: true, error: result.error, dbPath: opts.dbPath ?? null });
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 写自检日志(JSON 行、追加写)。**日志失败绝不影响调用方**(自检本身也不许因日志炸)。
|
|
341
|
+
* @returns {string|null} 实际写入的文件路径,失败返回 null。
|
|
342
|
+
*/
|
|
343
|
+
export function writeSchemaLog(opts = {}, entry = {}) {
|
|
344
|
+
try {
|
|
345
|
+
const file = opts.logFile ?? join(opts.home ?? deriveHome(opts.dbPath), "logs", "memory-schema.log");
|
|
346
|
+
if (typeof opts.onLog === "function") {
|
|
347
|
+
try {
|
|
348
|
+
opts.onLog({ at: new Date().toISOString(), pid: process.pid, ...entry });
|
|
349
|
+
} catch {
|
|
350
|
+
/* 回调失败不影响日志落盘 */
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
354
|
+
appendFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), pid: process.pid, ...entry })}\n`, "utf8");
|
|
355
|
+
return file;
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** 由库路径推导 DSH home:`<home>/memory/memory.db` → `<home>`。 */
|
|
362
|
+
export function deriveHome(dbPath) {
|
|
363
|
+
if (typeof dbPath !== "string" || dbPath.length === 0) return ".";
|
|
364
|
+
return dirname(dirname(dbPath));
|
|
365
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FORK-EDIT(2026-09-21): 用户消息构造器(移植 `@deepseek-ai/dsh-llm` 的 message.ts 语义)
|
|
3
|
+
*
|
|
4
|
+
* 官方 `createUserMessage` = `freezeMessage({ ...input, role: 'user', id: MessageId(randomUUID()) })`:
|
|
5
|
+
* 先 structuredClone、再深冻结,保证消息发布后不可变(llm 包对消息做 deepFreeze)。
|
|
6
|
+
* 本文件是等价实现(官方把它内联打包在 dsh-time-context / dsh-tmux-context 等包里,
|
|
7
|
+
* 没有可 import 的公开入口;形态保持一致 ⇒ 注入消息与官方插件注入的消息同构)。
|
|
8
|
+
*/
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
function deepFreeze(value, seen) {
|
|
12
|
+
if (value === null || typeof value !== 'object') return value;
|
|
13
|
+
// AbortSignal 是活体取消通道,冻结它会破坏 abort(官方同口径)
|
|
14
|
+
if (typeof AbortSignal !== 'undefined' && value instanceof AbortSignal) return value;
|
|
15
|
+
const visited = seen || new WeakSet();
|
|
16
|
+
if (visited.has(value)) return value;
|
|
17
|
+
visited.add(value);
|
|
18
|
+
for (const key of Object.keys(value)) deepFreeze(value[key], visited);
|
|
19
|
+
return Object.freeze(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 构造一条已识别、已冻结的 user 消息。
|
|
24
|
+
* @param {{content:Array, source:object}} input
|
|
25
|
+
*/
|
|
26
|
+
export function createUserMessage(input) {
|
|
27
|
+
return deepFreeze({
|
|
28
|
+
...input,
|
|
29
|
+
id: crypto.randomUUID(),
|
|
30
|
+
role: 'user',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FORK-EDIT(2026-09-21): 向量库只读读取器 —— 从 S1 离线层 `memory-engine/lib/vectors-store.mjs`
|
|
3
|
+
* 移植(只取召回路径需要的只读部分:blobToVec / loadMatrix)。
|
|
4
|
+
*
|
|
5
|
+
* 移植来源:本仓 `memory-engine/lib/vectors-store.mjs`(记忆引擎 v2 · S1 交付)
|
|
6
|
+
* 口径(方案 §3.1 / §3.3):**只读**打开向量库;向量是可重建的派生物,插件永不写它。
|
|
7
|
+
* 为什么不直接 import 那个文件:插件部署后自持于 profiles\web\node_modules\dsh-memory\,
|
|
8
|
+
* 不能依赖仓库外的工具目录(逃逸 import 会被 deploy.mjs gate1 拒绝)。
|
|
9
|
+
* 语义与本文件来源逐字一致(仅去掉写路径函数)。
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 取 blob 里的 float32 向量。
|
|
16
|
+
* 注意:sqlite 返回的 Buffer 可能是池化分配(byteOffset 不对齐 4 字节)⇒ 必须切片后重建。
|
|
17
|
+
* @param {Buffer} blob
|
|
18
|
+
* @returns {Float32Array}
|
|
19
|
+
*/
|
|
20
|
+
export function blobToVec(blob) {
|
|
21
|
+
const ab = blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength);
|
|
22
|
+
return new Float32Array(ab);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 载入整个矩阵(召回用;万级候选纯内存暴力检索,方案 §3.3)。
|
|
27
|
+
* @returns {{refIds: string[], refIdsNum: number[], dim: number, data: Float32Array, count: number, textHashes: string[], models: string[]}}
|
|
28
|
+
*/
|
|
29
|
+
export function loadMatrix(db, source, model = null) {
|
|
30
|
+
const rows = model
|
|
31
|
+
? db
|
|
32
|
+
.prepare(
|
|
33
|
+
'SELECT ref_id, model, dim, vec, text_hash FROM vectors WHERE source = ? AND model = ? ORDER BY CAST(ref_id AS INTEGER)',
|
|
34
|
+
)
|
|
35
|
+
.all(source, model)
|
|
36
|
+
: db
|
|
37
|
+
.prepare(
|
|
38
|
+
'SELECT ref_id, model, dim, vec, text_hash FROM vectors WHERE source = ? ORDER BY CAST(ref_id AS INTEGER)',
|
|
39
|
+
)
|
|
40
|
+
.all(source);
|
|
41
|
+
if (!rows.length) {
|
|
42
|
+
return {
|
|
43
|
+
refIds: [],
|
|
44
|
+
refIdsNum: [],
|
|
45
|
+
dim: 0,
|
|
46
|
+
data: new Float32Array(0),
|
|
47
|
+
count: 0,
|
|
48
|
+
textHashes: [],
|
|
49
|
+
models: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const dim = rows[0].dim;
|
|
53
|
+
const data = new Float32Array(rows.length * dim);
|
|
54
|
+
rows.forEach((r, i) => {
|
|
55
|
+
if (r.dim !== dim) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`向量库维度不一致:ref_id=${r.ref_id} dim=${r.dim},首个为 ${dim}(应全量重建)`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const v = blobToVec(r.vec);
|
|
61
|
+
if (v.length !== dim) {
|
|
62
|
+
throw new Error(`向量 blob 长度异常:ref_id=${r.ref_id} len=${v.length} 期望 ${dim}`);
|
|
63
|
+
}
|
|
64
|
+
data.set(v, i * dim);
|
|
65
|
+
});
|
|
66
|
+
const models = [...new Set(rows.map((r) => r.model))];
|
|
67
|
+
return {
|
|
68
|
+
refIds: rows.map((r) => String(r.ref_id)),
|
|
69
|
+
refIdsNum: rows.map((r) => Number(r.ref_id)),
|
|
70
|
+
dim,
|
|
71
|
+
data,
|
|
72
|
+
count: rows.length,
|
|
73
|
+
textHashes: rows.map((r) => r.text_hash),
|
|
74
|
+
models,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 只读打开向量库(召回侧)。文件不存在 → 抛错,由调用方 fail-open 兜住。
|
|
80
|
+
* 铁律:readOnly 打开;不建表、不建目录、**绝不创建库文件**(创建是离线层 sync 的事)。
|
|
81
|
+
* @param {string} dbPath
|
|
82
|
+
*/
|
|
83
|
+
export function openVectorsReader(dbPath) {
|
|
84
|
+
if (!fs.existsSync(dbPath)) throw new Error(`向量库不存在: ${dbPath}`);
|
|
85
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
86
|
+
db.exec('PRAGMA busy_timeout = 5000;');
|
|
87
|
+
return {
|
|
88
|
+
dbPath,
|
|
89
|
+
db,
|
|
90
|
+
meta(k) {
|
|
91
|
+
try {
|
|
92
|
+
const r = db.prepare('SELECT v FROM meta WHERE k = ?').get(k);
|
|
93
|
+
return r ? r.v : null;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
count(source) {
|
|
99
|
+
const r = db.prepare('SELECT COUNT(*) AS n FROM vectors WHERE source = ?').get(source);
|
|
100
|
+
return Number(r.n);
|
|
101
|
+
},
|
|
102
|
+
close() {
|
|
103
|
+
try {
|
|
104
|
+
db.close();
|
|
105
|
+
} catch {
|
|
106
|
+
/* 幂等 */
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-memory-local",
|
|
3
|
+
"version": "0.1.0-local.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Self-maintained fork of the official dsh-memory plugin: durable cross-session memory (memory_write / memory_search / memory_forget + the memory:recall prompt section). Source of truth for 小会计 · 记忆引擎 v2 (G24).",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"deepseek-harness",
|
|
10
|
+
"memory",
|
|
11
|
+
"sqlite",
|
|
12
|
+
"fork"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"types": "lib/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./lib/index.d.ts",
|
|
20
|
+
"default": "./lib/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"lib",
|
|
27
|
+
"cordis.patch.yml"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dsh": {
|
|
34
|
+
"bundle": {
|
|
35
|
+
"patch": "./cordis.patch.yml"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6 || ^0.2.0-rc.1",
|
|
41
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.2.0-rc.1"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "node build.mjs",
|
|
46
|
+
"test": "node test/regression.mjs",
|
|
47
|
+
"deploy": "node deploy.mjs",
|
|
48
|
+
"restore": "node restore.mjs",
|
|
49
|
+
"verify": "node verify-after-restart.mjs"
|
|
50
|
+
}
|
|
51
|
+
}
|