@kira905/dsh-memory-toolkit 0.1.3 → 0.2.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.
- package/CHANGELOG.md +92 -0
- package/README.en.md +225 -2
- package/README.md +180 -2
- package/lib/deploy-guard.mjs +463 -275
- package/lib/guard-http.mjs +59 -62
- package/lib/pitfall-form-map.mjs +94 -84
- package/lib/pitfall-score.mjs +262 -185
- package/lib/session-file-name.mjs +116 -100
- package/lib/zstd-frames.mjs +88 -67
- package/memory-db-tools/README.md +29 -1
- package/memory-db-tools/audit-analyze.mjs +1 -1
- package/memory-db-tools/audit-export.mjs +60 -16
- package/memory-db-tools/audit-struct-probe.mjs +29 -8
- package/memory-db-tools/lib/known-devices.mjs +137 -0
- package/memory-engine/README.md +58 -4
- package/memory-engine/bench/bench.mjs +1 -1
- package/memory-engine/cli/band-sentinel.mjs +164 -0
- package/memory-engine/cli/distill-rollback.mjs +112 -44
- package/memory-engine/cli/distill-sessions.mjs +237 -0
- package/memory-engine/cli/distill.mjs +196 -95
- package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
- package/memory-engine/cli/doc-index.mjs +47 -38
- package/memory-engine/cli/domain-tags.mjs +246 -162
- package/memory-engine/cli/layer-migrate.mjs +176 -0
- package/memory-engine/cli/layer-move.mjs +189 -0
- package/memory-engine/cli/pitfall-hook.mjs +136 -70
- package/memory-engine/cli/recall.mjs +243 -110
- package/memory-engine/cli/selfcheck.mjs +93 -44
- package/memory-engine/cli/serve-start.mjs +221 -140
- package/memory-engine/cli/sweep.mjs +83 -44
- package/memory-engine/cli/sync.mjs +93 -57
- package/memory-engine/config/audit-devices.json +6 -0
- package/memory-engine/config/bands.json +7 -0
- package/memory-engine/config/layers.json +7 -0
- package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
- package/memory-engine/dict/lexicon-config.json +11 -0
- package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
- package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
- package/memory-engine/lib/band-sentinel.mjs +417 -0
- package/memory-engine/lib/cli-strict.mjs +128 -0
- package/memory-engine/lib/cli-util.mjs +147 -125
- package/memory-engine/lib/config.mjs +83 -96
- package/memory-engine/lib/dirty-ledger.mjs +127 -94
- package/memory-engine/lib/distill-rollback.mjs +173 -133
- package/memory-engine/lib/distill.mjs +878 -585
- package/memory-engine/lib/doc-index-store.mjs +208 -235
- package/memory-engine/lib/doc-index.mjs +351 -350
- package/memory-engine/lib/doc-scan.mjs +178 -156
- package/memory-engine/lib/doc-summarize.mjs +220 -205
- package/memory-engine/lib/domain-tags.mjs +358 -277
- package/memory-engine/lib/embedder.mjs +175 -194
- package/memory-engine/lib/hashes.mjs +22 -7
- package/memory-engine/lib/layer-migrate.mjs +330 -0
- package/memory-engine/lib/layer-move.mjs +307 -0
- package/memory-engine/lib/layers.mjs +191 -0
- package/memory-engine/lib/lexicon-recall.mjs +473 -0
- package/memory-engine/lib/memory-source.mjs +78 -80
- package/memory-engine/lib/pitfall-source.mjs +115 -114
- package/memory-engine/lib/policy-source.mjs +279 -261
- package/memory-engine/lib/recall-log.mjs +285 -0
- package/memory-engine/lib/recall.mjs +565 -240
- package/memory-engine/lib/selfcheck.mjs +142 -106
- package/memory-engine/lib/session-distill.mjs +603 -0
- package/memory-engine/lib/session-source.mjs +421 -0
- package/memory-engine/lib/sweep.mjs +107 -130
- package/memory-engine/lib/sync.mjs +110 -140
- package/memory-engine/lib/tick-hook.mjs +123 -148
- package/memory-engine/lib/vectors-store.mjs +213 -188
- package/memory-engine/test/12-layering.test.mjs +539 -0
- package/memory-engine/test/13-session-distill.test.mjs +616 -0
- package/memory-engine/tools/bridge-smoke.mjs +94 -55
- package/memory-engine/tools/fetch-model.mjs +167 -87
- package/memory-engine/tools/smoke-recall.mjs +94 -38
- package/memory-engine/tools/verify-recall-chain.mjs +413 -0
- package/package.json +1 -1
|
@@ -1,167 +1,196 @@
|
|
|
1
|
-
//
|
|
1
|
+
// `memories_dirty` 台账:兜底轮转扫出来的「正文被外部改过」清单。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// 方案给的修法 = 把「§3.1 全表 `text_hash` 段轮转」的**产物**(差异清单)作为提炼 job 的补充输入源。
|
|
3
|
+
// 为什么必须落盘:轮转一旦发现差异,就会顺手把向量和 text_hash 刷成新的,差异信号当场消失;
|
|
4
|
+
// 那一刻要是没人看见,这条改动就再也不会出现在任何增量窗口里。所以差异一出现就先记账,
|
|
5
|
+
// 提炼侧日后按台账把漏掉的输入补回来。
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
// ⇒ 检测到差异的**那一刻**就落台账;提炼 job 事后按台账补齐输入。
|
|
12
|
-
//
|
|
13
|
-
// 落点:`vectors.db`(独立向量库,`sync:false` 不进任何仓,可重建)。
|
|
14
|
-
// 放这里而不是新开 state 文件:它**本来就是**向量层的派生产物(与 `vectors.text_hash` 同源),
|
|
15
|
-
// 且向量库随 `~/.dsh/memory-vectors/` 一起被清理/重建,台账不会成为孤儿。
|
|
16
|
-
//
|
|
17
|
-
// 语义:一条 = 一个 `ref_id`(memory.id)。
|
|
18
|
-
// · `kind='mismatch'`:正文哈希与向量库记录不符 ⇒ **正文被改过**(本台账的唯一入账类型)
|
|
19
|
-
// · `consumed_at`:提炼 job 消费过(= 已并入某次提炼输入)
|
|
20
|
-
// ⚠️ **不删行**:删了就没法回答"这条改动是谁消费的";保留 + 定期 prune。
|
|
7
|
+
// 放哪儿:和向量库同一只库(它本来就是向量层的派生物,库被清理/重建时一起走,不会变成孤儿)。
|
|
8
|
+
// 一行 = 一个 ref_id;consumed_at 只表示"提炼侧取过了",行不删(要留痕),靠定期 prune 收掉。
|
|
9
|
+
|
|
21
10
|
import { DatabaseSync } from 'node:sqlite';
|
|
22
11
|
|
|
23
|
-
|
|
12
|
+
const TABLE = 'memories_dirty';
|
|
13
|
+
const COLUMNS = 'ref_id, kind, old_hash, new_hash, first_at, last_at, hits, consumed_at';
|
|
14
|
+
|
|
15
|
+
/** 建表语句(幂等;与向量表同库,只加表、不动既有表)。 */
|
|
24
16
|
export const DIRTY_DDL = `
|
|
25
17
|
CREATE TABLE IF NOT EXISTS memories_dirty (
|
|
26
|
-
ref_id
|
|
27
|
-
kind
|
|
28
|
-
old_hash
|
|
29
|
-
new_hash
|
|
30
|
-
first_at
|
|
31
|
-
last_at
|
|
32
|
-
hits
|
|
18
|
+
ref_id TEXT NOT NULL PRIMARY KEY,
|
|
19
|
+
kind TEXT NOT NULL,
|
|
20
|
+
old_hash TEXT,
|
|
21
|
+
new_hash TEXT,
|
|
22
|
+
first_at INTEGER NOT NULL,
|
|
23
|
+
last_at INTEGER NOT NULL,
|
|
24
|
+
hits INTEGER NOT NULL DEFAULT 1,
|
|
33
25
|
consumed_at INTEGER
|
|
34
26
|
);
|
|
35
27
|
CREATE INDEX IF NOT EXISTS memories_dirty_open ON memories_dirty (consumed_at, last_at);
|
|
36
28
|
`;
|
|
37
29
|
|
|
38
|
-
export const DIRTY_SCHEMA = 1;
|
|
30
|
+
export const DIRTY_SCHEMA = 1; // 表结构版本;列一旦增减就地加一
|
|
39
31
|
|
|
40
32
|
export function ensureDirtySchema(db) {
|
|
41
33
|
db.exec(DIRTY_DDL);
|
|
42
34
|
return true;
|
|
43
35
|
}
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
// 表在不在:不在就当"还没轮转过",一律返回空结果,不抛错(老库也要能读)。
|
|
38
|
+
function tablePresent(db) {
|
|
39
|
+
const hit = db.prepare("SELECT 1 AS hit FROM sqlite_master WHERE type = 'table' AND name = ?").get(TABLE);
|
|
40
|
+
return Boolean(hit);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 三条热语句备一次就够:本模块的写入方都是短命脚本,连接不会活太久。
|
|
44
|
+
function prepareWrites(db) {
|
|
45
|
+
return {
|
|
46
|
+
peek: db.prepare(`SELECT ref_id, new_hash, consumed_at FROM ${TABLE} WHERE ref_id = ?`),
|
|
47
|
+
spawn: db.prepare(
|
|
48
|
+
`INSERT INTO ${TABLE} (ref_id, kind, old_hash, new_hash, first_at, last_at, hits, consumed_at)
|
|
49
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, NULL)`,
|
|
50
|
+
),
|
|
51
|
+
bump: db.prepare(
|
|
52
|
+
`UPDATE ${TABLE} SET kind = ?, old_hash = ?, new_hash = ?, last_at = ?, hits = hits + 1, consumed_at = ?
|
|
53
|
+
WHERE ref_id = ?`,
|
|
54
|
+
),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 库里的一行 → 对外形状(驼峰;数值列统一转 number)
|
|
59
|
+
function toEntry(row) {
|
|
60
|
+
const consumed = row.consumed_at;
|
|
61
|
+
return {
|
|
62
|
+
refId: String(row.ref_id),
|
|
63
|
+
kind: row.kind,
|
|
64
|
+
oldHash: row.old_hash,
|
|
65
|
+
newHash: row.new_hash,
|
|
66
|
+
firstAt: Number(row.first_at),
|
|
67
|
+
lastAt: Number(row.last_at),
|
|
68
|
+
hits: Number(row.hits),
|
|
69
|
+
consumedAt: consumed == null ? null : Number(consumed),
|
|
70
|
+
};
|
|
47
71
|
}
|
|
48
72
|
|
|
49
73
|
/**
|
|
50
|
-
*
|
|
74
|
+
* 记一批差异(幂等 upsert)。
|
|
51
75
|
*
|
|
52
|
-
*
|
|
53
|
-
* ·
|
|
54
|
-
* ·
|
|
55
|
-
*
|
|
76
|
+
* 重复记账的口径只有两条,写死在这里:
|
|
77
|
+
* · 同一个 ref_id 再被扫到 ⇒ hits 加一、last_at 刷新、new_hash 跟着更新;
|
|
78
|
+
* · 只有当 new_hash 相对库里存的那份**又变了**,才把 consumed_at 清掉让它重新待消费。
|
|
79
|
+
* 反过来,同一条差异被反复扫到**不许**复活 —— 否则每轮轮转都会把它重新消费一遍。
|
|
56
80
|
*
|
|
57
|
-
* @param {object} db
|
|
81
|
+
* @param {object} db 向量库连接(可写)
|
|
58
82
|
* @param {Array<{refId:string|number, kind?:string, oldHash?:string|null, newHash?:string|null}>} items
|
|
59
83
|
* @param {{now?:number}} [opts]
|
|
60
84
|
* @returns {{recorded:number, reopened:number}}
|
|
61
85
|
*/
|
|
62
86
|
export function recordDirty(db, items, opts = {}) {
|
|
63
|
-
|
|
87
|
+
const batch = items || [];
|
|
88
|
+
if (!batch.length) return { recorded: 0, reopened: 0 };
|
|
64
89
|
ensureDirtySchema(db);
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const ins = db.prepare(`INSERT INTO memories_dirty (ref_id, kind, old_hash, new_hash, first_at, last_at, hits, consumed_at)
|
|
68
|
-
VALUES (?, ?, ?, ?, ?, ?, 1, NULL)`);
|
|
69
|
-
const upd = db.prepare(`UPDATE memories_dirty SET kind = ?, old_hash = ?, new_hash = ?, last_at = ?, hits = hits + 1,
|
|
70
|
-
consumed_at = ? WHERE ref_id = ?`);
|
|
90
|
+
const at = Number(opts.now ?? Date.now());
|
|
91
|
+
const writes = prepareWrites(db);
|
|
71
92
|
let recorded = 0;
|
|
72
93
|
let reopened = 0;
|
|
73
94
|
db.exec('BEGIN IMMEDIATE');
|
|
74
95
|
try {
|
|
75
|
-
for (const
|
|
76
|
-
const refId = String(
|
|
96
|
+
for (const item of batch) {
|
|
97
|
+
const refId = String(item.refId);
|
|
77
98
|
if (!refId) continue;
|
|
78
|
-
const kind = String(
|
|
79
|
-
const oldHash =
|
|
80
|
-
const newHash =
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
83
|
-
|
|
99
|
+
const kind = String(item.kind ?? 'mismatch');
|
|
100
|
+
const oldHash = item.oldHash ?? null;
|
|
101
|
+
const newHash = item.newHash ?? null;
|
|
102
|
+
const before = writes.peek.get(refId);
|
|
103
|
+
if (!before) {
|
|
104
|
+
writes.spawn.run(refId, kind, oldHash, newHash, at, at);
|
|
84
105
|
recorded += 1;
|
|
85
106
|
continue;
|
|
86
107
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
upd.run(kind, oldHash, newHash, now, changedAgain ? null : prev.consumed_at, refId);
|
|
108
|
+
const rewritten = newHash != null && before.new_hash != null && before.new_hash !== newHash;
|
|
109
|
+
writes.bump.run(kind, oldHash, newHash, at, rewritten ? null : before.consumed_at, refId);
|
|
110
|
+
if (rewritten && before.consumed_at != null) reopened += 1;
|
|
91
111
|
recorded += 1;
|
|
92
112
|
}
|
|
93
113
|
db.exec('COMMIT');
|
|
94
114
|
} catch (e) {
|
|
95
|
-
try {
|
|
115
|
+
try {
|
|
116
|
+
db.exec('ROLLBACK');
|
|
117
|
+
} catch {
|
|
118
|
+
// 回滚本身失败也不能盖掉原始错误
|
|
119
|
+
}
|
|
96
120
|
throw e;
|
|
97
121
|
}
|
|
98
122
|
return { recorded, reopened };
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
/**
|
|
102
|
-
*
|
|
126
|
+
* 读台账:默认只给未消费的;要全量就显式传 includeConsumed(或 openOnly:false)。
|
|
103
127
|
* @param {object} db
|
|
104
128
|
* @param {{includeConsumed?:boolean, openOnly?:boolean, limit?:number}} [opts]
|
|
105
129
|
*/
|
|
106
130
|
export function listDirty(db, opts = {}) {
|
|
107
|
-
if (!
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
firstAt: Number(r.first_at), lastAt: Number(r.last_at), hits: Number(r.hits),
|
|
116
|
-
consumedAt: r.consumed_at == null ? null : Number(r.consumed_at),
|
|
117
|
-
}));
|
|
131
|
+
if (!tablePresent(db)) return [];
|
|
132
|
+
const wantAll = opts.includeConsumed === true;
|
|
133
|
+
const onlyOpen = opts.openOnly !== false && !wantAll;
|
|
134
|
+
const cap = Number(opts.limit ?? 0);
|
|
135
|
+
const filter = onlyOpen ? 'WHERE consumed_at IS NULL' : '';
|
|
136
|
+
const page = cap > 0 ? `LIMIT ${cap}` : '';
|
|
137
|
+
const sql = `SELECT ${COLUMNS} FROM ${TABLE} ${filter} ORDER BY last_at DESC ${page}`;
|
|
138
|
+
return db.prepare(sql).all().map(toEntry);
|
|
118
139
|
}
|
|
119
140
|
|
|
120
|
-
/**
|
|
141
|
+
/** 标记已消费(提炼侧把这批 id 并进输入之后调)。只动还没消费过的行,天然幂等。 */
|
|
121
142
|
export function markConsumed(db, refIds, opts = {}) {
|
|
122
|
-
|
|
123
|
-
if (!
|
|
124
|
-
const
|
|
125
|
-
const stmt = db.prepare(
|
|
126
|
-
let
|
|
143
|
+
const ids = refIds || [];
|
|
144
|
+
if (!ids.length || !tablePresent(db)) return 0;
|
|
145
|
+
const at = Number(opts.now ?? Date.now());
|
|
146
|
+
const stmt = db.prepare(`UPDATE ${TABLE} SET consumed_at = ? WHERE ref_id = ? AND consumed_at IS NULL`);
|
|
147
|
+
let touched = 0;
|
|
127
148
|
db.exec('BEGIN IMMEDIATE');
|
|
128
149
|
try {
|
|
129
|
-
for (const id of
|
|
150
|
+
for (const id of ids) touched += Number(stmt.run(at, String(id)).changes || 0);
|
|
130
151
|
db.exec('COMMIT');
|
|
131
152
|
} catch (e) {
|
|
132
|
-
try {
|
|
153
|
+
try {
|
|
154
|
+
db.exec('ROLLBACK');
|
|
155
|
+
} catch {
|
|
156
|
+
// 同上:保留原始错误
|
|
157
|
+
}
|
|
133
158
|
throw e;
|
|
134
159
|
}
|
|
135
|
-
return
|
|
160
|
+
return touched;
|
|
136
161
|
}
|
|
137
162
|
|
|
138
|
-
/**
|
|
163
|
+
/** 台账概览(状态面板 / 日报用)。表不在 ⇒ table:false 的空壳,不抛错。 */
|
|
139
164
|
export function dirtyStats(db) {
|
|
140
|
-
if (!
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
if (!tablePresent(db)) return { table: false, total: 0, open: 0, consumed: 0, lastAt: null };
|
|
166
|
+
const agg = db
|
|
167
|
+
.prepare(
|
|
168
|
+
`SELECT COUNT(*) AS total,
|
|
169
|
+
SUM(CASE WHEN consumed_at IS NULL THEN 1 ELSE 0 END) AS open_count,
|
|
170
|
+
SUM(CASE WHEN consumed_at IS NOT NULL THEN 1 ELSE 0 END) AS consumed_count,
|
|
171
|
+
MAX(last_at) AS last_at
|
|
172
|
+
FROM ${TABLE}`,
|
|
173
|
+
)
|
|
174
|
+
.get();
|
|
145
175
|
return {
|
|
146
176
|
table: true,
|
|
147
|
-
total: Number(
|
|
148
|
-
open: Number(
|
|
149
|
-
consumed: Number(
|
|
150
|
-
lastAt:
|
|
177
|
+
total: Number(agg.total ?? 0),
|
|
178
|
+
open: Number(agg.open_count ?? 0),
|
|
179
|
+
consumed: Number(agg.consumed_count ?? 0),
|
|
180
|
+
lastAt: agg.last_at == null ? null : Number(agg.last_at),
|
|
151
181
|
};
|
|
152
182
|
}
|
|
153
183
|
|
|
154
|
-
/**
|
|
184
|
+
/** 定期收尾:只清"已消费且够老"的行,没消费过的永远留着。 */
|
|
155
185
|
export function pruneDirty(db, opts = {}) {
|
|
156
|
-
if (!
|
|
157
|
-
const now = Number(opts.now ?? Date.now());
|
|
186
|
+
if (!tablePresent(db)) return 0;
|
|
158
187
|
const keepDays = Number(opts.keepDays ?? 90);
|
|
159
|
-
const cutoff = now - keepDays * 86_400_000;
|
|
160
|
-
const
|
|
161
|
-
return Number(
|
|
188
|
+
const cutoff = Number(opts.now ?? Date.now()) - keepDays * 86_400_000;
|
|
189
|
+
const res = db.prepare(`DELETE FROM ${TABLE} WHERE consumed_at IS NOT NULL AND consumed_at < ?`).run(cutoff);
|
|
190
|
+
return Number(res.changes || 0);
|
|
162
191
|
}
|
|
163
192
|
|
|
164
|
-
/**
|
|
193
|
+
/** 便捷入口:按库路径开一只写连接,跑完 fn 就关(台账写入方都是短命脚本)。 */
|
|
165
194
|
export function withDirtyDb(dbPath, fn) {
|
|
166
195
|
const db = new DatabaseSync(dbPath);
|
|
167
196
|
try {
|
|
@@ -169,6 +198,10 @@ export function withDirtyDb(dbPath, fn) {
|
|
|
169
198
|
ensureDirtySchema(db);
|
|
170
199
|
return fn(db);
|
|
171
200
|
} finally {
|
|
172
|
-
try {
|
|
201
|
+
try {
|
|
202
|
+
db.close();
|
|
203
|
+
} catch {
|
|
204
|
+
// 关不掉不算错:调用方多半已经在 finally 里兜着
|
|
205
|
+
}
|
|
173
206
|
}
|
|
174
207
|
}
|