@psiclawops/hypermem 0.6.2 → 0.8.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/ARCHITECTURE.md +31 -39
- package/README.md +20 -14
- package/bin/hypermem-status.mjs +1 -1
- package/dist/background-indexer.d.ts +14 -3
- package/dist/background-indexer.d.ts.map +1 -1
- package/dist/background-indexer.js +135 -27
- package/dist/budget-policy.d.ts +22 -0
- package/dist/budget-policy.d.ts.map +1 -0
- package/dist/budget-policy.js +27 -0
- package/dist/cache.d.ts +11 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/compositor-utils.d.ts +31 -0
- package/dist/compositor-utils.d.ts.map +1 -0
- package/dist/compositor-utils.js +47 -0
- package/dist/compositor.d.ts +163 -1
- package/dist/compositor.d.ts.map +1 -1
- package/dist/compositor.js +862 -130
- package/dist/content-hash.d.ts +43 -0
- package/dist/content-hash.d.ts.map +1 -0
- package/dist/content-hash.js +75 -0
- package/dist/context-store.d.ts +54 -0
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +102 -0
- package/dist/contradiction-audit-store.d.ts +54 -0
- package/dist/contradiction-audit-store.d.ts.map +1 -0
- package/dist/contradiction-audit-store.js +88 -0
- package/dist/contradiction-detector.d.ts +78 -0
- package/dist/contradiction-detector.d.ts.map +1 -0
- package/dist/contradiction-detector.js +362 -0
- package/dist/contradiction-resolution-policy.d.ts +21 -0
- package/dist/contradiction-resolution-policy.d.ts.map +1 -0
- package/dist/contradiction-resolution-policy.js +17 -0
- package/dist/cross-agent.d.ts +1 -1
- package/dist/cross-agent.js +17 -17
- package/dist/degradation.d.ts +102 -0
- package/dist/degradation.d.ts.map +1 -0
- package/dist/degradation.js +141 -0
- package/dist/dreaming-promoter.d.ts +39 -1
- package/dist/dreaming-promoter.d.ts.map +1 -1
- package/dist/dreaming-promoter.js +70 -4
- package/dist/expertise-store.d.ts +129 -0
- package/dist/expertise-store.d.ts.map +1 -0
- package/dist/expertise-store.js +342 -0
- package/dist/fact-store.d.ts +15 -0
- package/dist/fact-store.d.ts.map +1 -1
- package/dist/fact-store.js +52 -5
- package/dist/index.d.ts +74 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +407 -29
- package/dist/knowledge-lint.d.ts +2 -0
- package/dist/knowledge-lint.d.ts.map +1 -1
- package/dist/knowledge-lint.js +40 -1
- package/dist/library-schema.d.ts +7 -2
- package/dist/library-schema.d.ts.map +1 -1
- package/dist/library-schema.js +307 -2
- package/dist/message-store.d.ts +64 -1
- package/dist/message-store.d.ts.map +1 -1
- package/dist/message-store.js +137 -1
- package/dist/proactive-pass.d.ts +2 -2
- package/dist/proactive-pass.d.ts.map +1 -1
- package/dist/proactive-pass.js +66 -12
- package/dist/replay-recovery.d.ts +29 -0
- package/dist/replay-recovery.d.ts.map +1 -0
- package/dist/replay-recovery.js +82 -0
- package/dist/reranker.d.ts +95 -0
- package/dist/reranker.d.ts.map +1 -0
- package/dist/reranker.js +308 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +46 -1
- package/dist/seed.d.ts +1 -1
- package/dist/seed.js +1 -1
- package/dist/session-flusher.d.ts +4 -4
- package/dist/session-flusher.d.ts.map +1 -1
- package/dist/session-flusher.js +3 -3
- package/dist/spawn-context.d.ts +1 -1
- package/dist/spawn-context.js +1 -1
- package/dist/temporal-store.d.ts +1 -0
- package/dist/temporal-store.d.ts.map +1 -1
- package/dist/tool-artifact-store.d.ts +98 -0
- package/dist/tool-artifact-store.d.ts.map +1 -0
- package/dist/tool-artifact-store.js +244 -0
- package/dist/topic-detector.js +2 -2
- package/dist/topic-store.d.ts +6 -0
- package/dist/topic-store.d.ts.map +1 -1
- package/dist/topic-store.js +39 -0
- package/dist/topic-synthesizer.js +1 -1
- package/dist/trigger-registry.d.ts +1 -1
- package/dist/trigger-registry.js +4 -4
- package/dist/types.d.ts +239 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-store.d.ts +2 -1
- package/dist/vector-store.d.ts.map +1 -1
- package/dist/vector-store.js +3 -0
- package/dist/version.d.ts +10 -10
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +10 -10
- package/package.json +6 -4
package/dist/knowledge-lint.js
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* 3. Coverage gaps — topics with many messages but no synthesis
|
|
8
8
|
*/
|
|
9
9
|
import { LINT_STALE_DAYS } from './topic-synthesizer.js';
|
|
10
|
+
// Orphan topics older than this many days are eligible for pruning.
|
|
11
|
+
// 48h = report threshold, 14d = prune threshold (conservative).
|
|
12
|
+
export const ORPHAN_PRUNE_DAYS = 14;
|
|
10
13
|
// ─── lintKnowledge ──────────────────────────────────────────────
|
|
11
14
|
/**
|
|
12
15
|
* Run lint checks on the knowledge table.
|
|
@@ -25,6 +28,7 @@ export function lintKnowledge(libraryDb) {
|
|
|
25
28
|
const result = {
|
|
26
29
|
staleDecayed: 0,
|
|
27
30
|
orphansFound: 0,
|
|
31
|
+
orphansPruned: 0,
|
|
28
32
|
coverageGaps: [],
|
|
29
33
|
};
|
|
30
34
|
// ── 1. Stale syntheses ─────────────────────────────────────────
|
|
@@ -80,7 +84,42 @@ export function lintKnowledge(libraryDb) {
|
|
|
80
84
|
`).all();
|
|
81
85
|
result.orphansFound = orphans.length;
|
|
82
86
|
if (orphans.length > 0) {
|
|
83
|
-
|
|
87
|
+
const sample = orphans.slice(0, 10).map(o => o.name).join(', ');
|
|
88
|
+
const remainder = Math.max(0, orphans.length - 10);
|
|
89
|
+
console.log(`[lint] ${orphans.length} orphan topic(s) found (< 3 messages, stale > 48h)` +
|
|
90
|
+
(sample ? `; sample: ${sample}` : '') +
|
|
91
|
+
(remainder > 0 ? `; +${remainder} more` : ''));
|
|
92
|
+
}
|
|
93
|
+
// Prune orphan topics older than ORPHAN_PRUNE_DAYS (conservative cleanup).
|
|
94
|
+
// Safety: only prune topics with no knowledge-synthesis entries and no facts
|
|
95
|
+
// referencing them (via source_ref 'topic:<id>' pattern).
|
|
96
|
+
try {
|
|
97
|
+
const prunable = libraryDb.prepare(`
|
|
98
|
+
SELECT t.id, t.name FROM topics t
|
|
99
|
+
WHERE t.message_count < 3
|
|
100
|
+
AND t.updated_at < datetime('now', '-' || ? || ' days')
|
|
101
|
+
AND NOT EXISTS (
|
|
102
|
+
SELECT 1 FROM knowledge k
|
|
103
|
+
WHERE k.agent_id = t.agent_id
|
|
104
|
+
AND k.domain = 'topic-synthesis'
|
|
105
|
+
AND k.key = t.name
|
|
106
|
+
AND k.superseded_by IS NULL
|
|
107
|
+
)
|
|
108
|
+
AND NOT EXISTS (
|
|
109
|
+
SELECT 1 FROM facts f
|
|
110
|
+
WHERE f.source_ref LIKE 'topic:' || t.id || '%'
|
|
111
|
+
)
|
|
112
|
+
`).all(ORPHAN_PRUNE_DAYS);
|
|
113
|
+
if (prunable.length > 0) {
|
|
114
|
+
const ids = prunable.map(p => p.id);
|
|
115
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
116
|
+
libraryDb.prepare(`DELETE FROM topics WHERE id IN (${placeholders})`).run(...ids);
|
|
117
|
+
result.orphansPruned = prunable.length;
|
|
118
|
+
console.log(`[lint] pruned ${prunable.length} orphan topic(s) (< 3 messages, stale > ${ORPHAN_PRUNE_DAYS}d, no syntheses or facts)`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
console.warn('[lint] orphan prune failed (non-fatal):', err.message);
|
|
84
123
|
}
|
|
85
124
|
}
|
|
86
125
|
catch {
|
package/dist/library-schema.d.ts
CHANGED
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
* 9. Work items (fleet kanban)
|
|
17
17
|
* 10. Topics (cross-session thread tracking)
|
|
18
18
|
*/
|
|
19
|
-
import
|
|
20
|
-
export declare const LIBRARY_SCHEMA_VERSION =
|
|
19
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
20
|
+
export declare const LIBRARY_SCHEMA_VERSION = 19;
|
|
21
|
+
export declare function repairLibraryDb(dbPath: string): {
|
|
22
|
+
repaired: boolean;
|
|
23
|
+
backupPath?: string;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
21
26
|
export declare function migrateLibrary(db: DatabaseSync, engineVersion?: string): void;
|
|
22
27
|
//# sourceMappingURL=library-schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"library-schema.d.ts","sourceRoot":"","sources":["../src/library-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"library-schema.d.ts","sourceRoot":"","sources":["../src/library-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAo6BzC,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAkF3G;AAID,wBAAgB,cAAc,CAAC,EAAE,EAAE,YAAY,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAuZ7E"}
|
package/dist/library-schema.js
CHANGED
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
* 9. Work items (fleet kanban)
|
|
17
17
|
* 10. Topics (cross-session thread tracking)
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
20
|
+
import { mkdirSync, renameSync, existsSync } from 'node:fs';
|
|
21
|
+
import { join as pathJoin, dirname } from 'node:path';
|
|
22
|
+
export const LIBRARY_SCHEMA_VERSION = 19;
|
|
20
23
|
function nowIso() {
|
|
21
24
|
return new Date().toISOString();
|
|
22
25
|
}
|
|
@@ -150,12 +153,15 @@ function applyV3Collections(db) {
|
|
|
150
153
|
updated_at TEXT NOT NULL,
|
|
151
154
|
expires_at TEXT,
|
|
152
155
|
superseded_by INTEGER,
|
|
153
|
-
decay_score REAL DEFAULT 0.0
|
|
156
|
+
decay_score REAL DEFAULT 0.0,
|
|
157
|
+
valid_from TEXT,
|
|
158
|
+
invalid_at TEXT
|
|
154
159
|
)
|
|
155
160
|
`);
|
|
156
161
|
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_agent ON facts(agent_id, scope, domain)');
|
|
157
162
|
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_visibility ON facts(visibility, agent_id)');
|
|
158
163
|
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_active ON facts(agent_id, superseded_by, decay_score, confidence DESC)');
|
|
164
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_temporal_validity ON facts(agent_id, valid_from, invalid_at)');
|
|
159
165
|
db.exec(`
|
|
160
166
|
CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts USING fts5(
|
|
161
167
|
content,
|
|
@@ -318,6 +324,7 @@ function applyV3Collections(db) {
|
|
|
318
324
|
)
|
|
319
325
|
`);
|
|
320
326
|
db.exec('CREATE INDEX IF NOT EXISTS idx_topics_agent ON topics(agent_id, status, updated_at DESC)');
|
|
327
|
+
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_topics_dedup ON topics(agent_id, lower(name))');
|
|
321
328
|
// ── Fleet registry ──
|
|
322
329
|
db.exec(`
|
|
323
330
|
CREATE TABLE IF NOT EXISTS fleet_agents (
|
|
@@ -865,6 +872,92 @@ function applyV12FosMod(db) {
|
|
|
865
872
|
}
|
|
866
873
|
}
|
|
867
874
|
}
|
|
875
|
+
// ── Repair utility ───────────────────────────────────────────
|
|
876
|
+
// Safe to call BEFORE opening the main DB connection.
|
|
877
|
+
// Handles the case where library.db has duplicate topics AND B-tree corruption,
|
|
878
|
+
// making in-place DELETE impossible.
|
|
879
|
+
//
|
|
880
|
+
// Strategy:
|
|
881
|
+
// 1. Detect duplicates (read-only: works on corrupt DBs)
|
|
882
|
+
// 2. VACUUM INTO temp file (writes to a new clean file)
|
|
883
|
+
// 3. Dedup + integrity check in temp
|
|
884
|
+
// 4. Backup original via VACUUM INTO backups dir
|
|
885
|
+
// 5. Atomic rename temp → original
|
|
886
|
+
export function repairLibraryDb(dbPath) {
|
|
887
|
+
if (!existsSync(dbPath)) {
|
|
888
|
+
return { repaired: false, message: `DB not found at ${dbPath}. Nothing to repair.` };
|
|
889
|
+
}
|
|
890
|
+
const backupsDir = pathJoin(dirname(dbPath), 'backups');
|
|
891
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
892
|
+
const tempPath = `${dbPath}.repair-${ts}.sqlite`;
|
|
893
|
+
const backupPath = pathJoin(backupsDir, `library.db.pre-repair-${ts}.sqlite`);
|
|
894
|
+
const src = new DatabaseSync(dbPath);
|
|
895
|
+
// Step 1: detect duplicates
|
|
896
|
+
const dupeRow = src.prepare(`
|
|
897
|
+
SELECT COUNT(*) AS cnt FROM (
|
|
898
|
+
SELECT agent_id, lower(name) FROM topics
|
|
899
|
+
GROUP BY agent_id, lower(name) HAVING COUNT(*) > 1
|
|
900
|
+
)
|
|
901
|
+
`).get();
|
|
902
|
+
if (dupeRow.cnt === 0) {
|
|
903
|
+
src.close();
|
|
904
|
+
return { repaired: false, message: 'No duplicate topics found. No repair needed.' };
|
|
905
|
+
}
|
|
906
|
+
console.log(`[hypermem-repair] ${dupeRow.cnt} duplicate topic group(s) found. Starting repair...`);
|
|
907
|
+
// Step 2: VACUUM INTO temp (reads clean pages, writes fresh file)
|
|
908
|
+
try {
|
|
909
|
+
src.exec(`VACUUM INTO '${tempPath}'`);
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
src.close();
|
|
913
|
+
throw new Error(`[hypermem-repair] VACUUM INTO failed: ${err.message}. Cannot auto-repair.`);
|
|
914
|
+
}
|
|
915
|
+
src.close();
|
|
916
|
+
// Step 3: dedup + verify in temp
|
|
917
|
+
const tmp = new DatabaseSync(tempPath);
|
|
918
|
+
try {
|
|
919
|
+
tmp.exec(`
|
|
920
|
+
DELETE FROM topics WHERE id IN (
|
|
921
|
+
WITH ranked AS (
|
|
922
|
+
SELECT id, ROW_NUMBER() OVER (
|
|
923
|
+
PARTITION BY agent_id, lower(name)
|
|
924
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC
|
|
925
|
+
) AS rn FROM topics
|
|
926
|
+
)
|
|
927
|
+
SELECT id FROM ranked WHERE rn > 1
|
|
928
|
+
)
|
|
929
|
+
`);
|
|
930
|
+
tmp.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_topics_dedup ON topics(agent_id, lower(name))');
|
|
931
|
+
const integ = tmp.prepare('PRAGMA integrity_check').get();
|
|
932
|
+
if (integ.integrity_check !== 'ok') {
|
|
933
|
+
throw new Error(`Repaired DB failed integrity check: ${integ.integrity_check}`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
finally {
|
|
937
|
+
tmp.close();
|
|
938
|
+
}
|
|
939
|
+
// Step 4: backup original
|
|
940
|
+
mkdirSync(backupsDir, { recursive: true });
|
|
941
|
+
let savedBackup = false;
|
|
942
|
+
try {
|
|
943
|
+
const srcForBackup = new DatabaseSync(dbPath);
|
|
944
|
+
srcForBackup.exec(`VACUUM INTO '${backupPath}'`);
|
|
945
|
+
srcForBackup.close();
|
|
946
|
+
savedBackup = true;
|
|
947
|
+
console.log(`[hypermem-repair] Backup saved → ${backupPath}`);
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
console.warn('[hypermem-repair] Could not save backup, proceeding with repair anyway.');
|
|
951
|
+
}
|
|
952
|
+
// Step 5: atomic swap
|
|
953
|
+
renameSync(tempPath, dbPath);
|
|
954
|
+
const msg = [
|
|
955
|
+
`Repair complete. ${dupeRow.cnt} duplicate topic group(s) removed.`,
|
|
956
|
+
savedBackup ? `Original backed up to: ${backupPath}` : 'Note: backup could not be saved.',
|
|
957
|
+
].join(' ');
|
|
958
|
+
console.log(`[hypermem-repair] ${msg}`);
|
|
959
|
+
return { repaired: true, backupPath: savedBackup ? backupPath : undefined, message: msg };
|
|
960
|
+
}
|
|
868
961
|
// ── Migration runner ──────────────────────────────────────────
|
|
869
962
|
export function migrateLibrary(db, engineVersion) {
|
|
870
963
|
db.exec(`
|
|
@@ -1016,6 +1109,218 @@ export function migrateLibrary(db, engineVersion) {
|
|
|
1016
1109
|
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1017
1110
|
.run(13, nowIso());
|
|
1018
1111
|
}
|
|
1112
|
+
// ── V14: Temporal validity columns on facts ──────────────────────────
|
|
1113
|
+
// valid_from / invalid_at enable "what was true on date X?" queries.
|
|
1114
|
+
if (currentVersion < 14) {
|
|
1115
|
+
try {
|
|
1116
|
+
db.exec('ALTER TABLE facts ADD COLUMN valid_from TEXT');
|
|
1117
|
+
}
|
|
1118
|
+
catch { /* already exists */ }
|
|
1119
|
+
try {
|
|
1120
|
+
db.exec('ALTER TABLE facts ADD COLUMN invalid_at TEXT');
|
|
1121
|
+
}
|
|
1122
|
+
catch { /* already exists */ }
|
|
1123
|
+
try {
|
|
1124
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_temporal_validity ON facts(agent_id, valid_from, invalid_at)');
|
|
1125
|
+
}
|
|
1126
|
+
catch { /* already exists */ }
|
|
1127
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1128
|
+
.run(14, nowIso());
|
|
1129
|
+
}
|
|
1130
|
+
// ── V15: Expertise tables (domain expertise patterns) ──────────────
|
|
1131
|
+
// expertise_observations: raw learnings from conversations, pipelines, reviews
|
|
1132
|
+
// expertise_patterns: graduated observations with confirming evidence
|
|
1133
|
+
// expertise_evidence: links observations to patterns (confirms/contradicts)
|
|
1134
|
+
if (currentVersion < 15) {
|
|
1135
|
+
db.exec(`
|
|
1136
|
+
CREATE TABLE IF NOT EXISTS expertise_observations (
|
|
1137
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1138
|
+
agent_id TEXT NOT NULL,
|
|
1139
|
+
domain TEXT NOT NULL,
|
|
1140
|
+
context TEXT,
|
|
1141
|
+
observation_text TEXT NOT NULL,
|
|
1142
|
+
source_type TEXT NOT NULL DEFAULT 'conversation',
|
|
1143
|
+
source_ref TEXT,
|
|
1144
|
+
created_at TEXT NOT NULL
|
|
1145
|
+
)
|
|
1146
|
+
`);
|
|
1147
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_expertise_obs_agent ON expertise_observations(agent_id, domain)');
|
|
1148
|
+
db.exec(`
|
|
1149
|
+
CREATE TABLE IF NOT EXISTS expertise_patterns (
|
|
1150
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1151
|
+
agent_id TEXT NOT NULL,
|
|
1152
|
+
domain TEXT NOT NULL,
|
|
1153
|
+
pattern_text TEXT NOT NULL,
|
|
1154
|
+
confidence REAL DEFAULT 0.7,
|
|
1155
|
+
frequency INTEGER DEFAULT 1,
|
|
1156
|
+
first_seen TEXT NOT NULL,
|
|
1157
|
+
last_confirmed TEXT NOT NULL,
|
|
1158
|
+
invalidated_at TEXT,
|
|
1159
|
+
invalidation_reason TEXT,
|
|
1160
|
+
decay_score REAL DEFAULT 0.0,
|
|
1161
|
+
updated_at TEXT
|
|
1162
|
+
)
|
|
1163
|
+
`);
|
|
1164
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_expertise_patterns_agent ON expertise_patterns(agent_id, domain, invalidated_at)');
|
|
1165
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_expertise_patterns_active ON expertise_patterns(agent_id, invalidated_at, confidence DESC)');
|
|
1166
|
+
db.exec(`
|
|
1167
|
+
CREATE TABLE IF NOT EXISTS expertise_evidence (
|
|
1168
|
+
observation_id INTEGER NOT NULL,
|
|
1169
|
+
pattern_id INTEGER NOT NULL,
|
|
1170
|
+
relationship TEXT NOT NULL CHECK(relationship IN ('confirms', 'contradicts')),
|
|
1171
|
+
created_at TEXT NOT NULL,
|
|
1172
|
+
PRIMARY KEY (observation_id, pattern_id)
|
|
1173
|
+
)
|
|
1174
|
+
`);
|
|
1175
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_expertise_evidence_pattern ON expertise_evidence(pattern_id, relationship)');
|
|
1176
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1177
|
+
.run(15, nowIso());
|
|
1178
|
+
}
|
|
1179
|
+
if (currentVersion < 16) {
|
|
1180
|
+
// contradiction_audits table — tracks detected contradictions for review
|
|
1181
|
+
db.exec(`
|
|
1182
|
+
CREATE TABLE IF NOT EXISTS contradiction_audits (
|
|
1183
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1184
|
+
agent_id TEXT NOT NULL,
|
|
1185
|
+
entity_type TEXT NOT NULL CHECK(entity_type IN ('fact')),
|
|
1186
|
+
new_content TEXT NOT NULL,
|
|
1187
|
+
new_domain TEXT,
|
|
1188
|
+
existing_fact_id INTEGER NOT NULL,
|
|
1189
|
+
existing_content TEXT NOT NULL,
|
|
1190
|
+
similarity_score REAL NOT NULL,
|
|
1191
|
+
contradiction_score REAL NOT NULL,
|
|
1192
|
+
reason TEXT NOT NULL,
|
|
1193
|
+
detector TEXT NOT NULL DEFAULT 'heuristic_v1',
|
|
1194
|
+
suggested_resolution TEXT NOT NULL DEFAULT 'review',
|
|
1195
|
+
source_ref TEXT,
|
|
1196
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'accepted', 'dismissed')),
|
|
1197
|
+
resolution_notes TEXT,
|
|
1198
|
+
created_at TEXT NOT NULL,
|
|
1199
|
+
resolved_at TEXT
|
|
1200
|
+
)
|
|
1201
|
+
`);
|
|
1202
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_contradiction_audits_agent_status ON contradiction_audits(agent_id, status, created_at DESC)');
|
|
1203
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_contradiction_audits_existing_fact ON contradiction_audits(existing_fact_id, status)');
|
|
1204
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_contradiction_audits_agent ON contradiction_audits(agent_id, created_at DESC)');
|
|
1205
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1206
|
+
.run(16, nowIso());
|
|
1207
|
+
}
|
|
1208
|
+
if (currentVersion < 17) {
|
|
1209
|
+
// Stamp v17 — previously applied by an older engine build alongside contradiction_audits.
|
|
1210
|
+
// No additional DDL needed; the table was already created in v16.
|
|
1211
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1212
|
+
.run(17, nowIso());
|
|
1213
|
+
}
|
|
1214
|
+
if (currentVersion < 18) {
|
|
1215
|
+
// V18: collapse duplicate topics, then enforce unique constraint.
|
|
1216
|
+
//
|
|
1217
|
+
// Safety pattern for existing users who may have both duplicates AND B-tree corruption
|
|
1218
|
+
// (e.g. from a WAL desync during a gateway crash):
|
|
1219
|
+
// 1. Detect duplicates up front (read-only — works even on malformed DBs)
|
|
1220
|
+
// 2. If dupes found: VACUUM INTO a backup file first (reads clean pages, writes new file)
|
|
1221
|
+
// 3. Attempt in-place DELETE (works on healthy DBs)
|
|
1222
|
+
// 4. If DELETE fails: throw a clear error pointing to the backup + repair command
|
|
1223
|
+
const dupeCheck = db.prepare(`
|
|
1224
|
+
SELECT COUNT(*) AS cnt FROM (
|
|
1225
|
+
SELECT agent_id, lower(name) FROM topics
|
|
1226
|
+
GROUP BY agent_id, lower(name) HAVING COUNT(*) > 1
|
|
1227
|
+
)
|
|
1228
|
+
`).get();
|
|
1229
|
+
let v18BackupPath;
|
|
1230
|
+
if (dupeCheck.cnt > 0) {
|
|
1231
|
+
// Auto-backup via VACUUM INTO before any writes.
|
|
1232
|
+
// VACUUM INTO reads clean pages and writes to a new file — works even on marginally
|
|
1233
|
+
// corrupt DBs where regular writes fail.
|
|
1234
|
+
const home = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
|
|
1235
|
+
const backupsDir = pathJoin(home, '.openclaw', 'hypermem', 'backups');
|
|
1236
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
1237
|
+
v18BackupPath = pathJoin(backupsDir, `library.db.v18-pre-dedup-${ts}.sqlite`);
|
|
1238
|
+
try {
|
|
1239
|
+
mkdirSync(backupsDir, { recursive: true });
|
|
1240
|
+
db.exec(`VACUUM INTO '${v18BackupPath}'`);
|
|
1241
|
+
console.log(`[hypermem-library] V18: backup saved → ${v18BackupPath}`);
|
|
1242
|
+
}
|
|
1243
|
+
catch (e) {
|
|
1244
|
+
console.warn(`[hypermem-library] V18: backup failed (${e.message}), proceeding without backup`);
|
|
1245
|
+
v18BackupPath = undefined;
|
|
1246
|
+
}
|
|
1247
|
+
try {
|
|
1248
|
+
db.exec(`
|
|
1249
|
+
DELETE FROM topics
|
|
1250
|
+
WHERE id IN (
|
|
1251
|
+
WITH ranked AS (
|
|
1252
|
+
SELECT
|
|
1253
|
+
id,
|
|
1254
|
+
ROW_NUMBER() OVER (
|
|
1255
|
+
PARTITION BY agent_id, lower(name)
|
|
1256
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC
|
|
1257
|
+
) AS rn
|
|
1258
|
+
FROM topics
|
|
1259
|
+
)
|
|
1260
|
+
SELECT id FROM ranked WHERE rn > 1
|
|
1261
|
+
)
|
|
1262
|
+
`);
|
|
1263
|
+
}
|
|
1264
|
+
catch (err) {
|
|
1265
|
+
const backupMsg = v18BackupPath
|
|
1266
|
+
? `A VACUUM backup was saved to:\n ${v18BackupPath}\nRun: hypermem repair-library to recover automatically.`
|
|
1267
|
+
: 'Run: hypermem repair-library to recover.';
|
|
1268
|
+
throw new Error(`[hypermem-library] V18 migration: failed to deduplicate topics (${err.message}).\n` +
|
|
1269
|
+
`Your library.db has ${dupeCheck.cnt} duplicate topic group(s) that could not be removed in-place.\n` +
|
|
1270
|
+
`This usually indicates B-tree corruption from a previous gateway crash.\n` +
|
|
1271
|
+
backupMsg);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_topics_dedup ON topics(agent_id, lower(name))');
|
|
1275
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1276
|
+
.run(18, nowIso());
|
|
1277
|
+
}
|
|
1278
|
+
if (currentVersion < 19) {
|
|
1279
|
+
// V19: Extend contradiction_audits status CHECK constraint to include
|
|
1280
|
+
// auto-resolution values. SQLite cannot ALTER CHECK constraints — recreate
|
|
1281
|
+
// the table with the new constraint, preserving all existing rows.
|
|
1282
|
+
// Use an explicit column list (not SELECT *) to guard against column drift.
|
|
1283
|
+
db.exec(`
|
|
1284
|
+
CREATE TABLE contradiction_audits_v19 (
|
|
1285
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1286
|
+
agent_id TEXT NOT NULL,
|
|
1287
|
+
entity_type TEXT NOT NULL CHECK(entity_type IN ('fact')),
|
|
1288
|
+
new_content TEXT NOT NULL,
|
|
1289
|
+
new_domain TEXT,
|
|
1290
|
+
existing_fact_id INTEGER NOT NULL,
|
|
1291
|
+
existing_content TEXT NOT NULL,
|
|
1292
|
+
similarity_score REAL NOT NULL,
|
|
1293
|
+
contradiction_score REAL NOT NULL,
|
|
1294
|
+
reason TEXT NOT NULL,
|
|
1295
|
+
detector TEXT NOT NULL DEFAULT 'heuristic_v1',
|
|
1296
|
+
suggested_resolution TEXT NOT NULL DEFAULT 'review',
|
|
1297
|
+
source_ref TEXT,
|
|
1298
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
1299
|
+
CHECK(status IN ('pending', 'accepted', 'dismissed', 'auto-superseded', 'auto-invalidated')),
|
|
1300
|
+
resolution_notes TEXT,
|
|
1301
|
+
created_at TEXT NOT NULL,
|
|
1302
|
+
resolved_at TEXT
|
|
1303
|
+
);
|
|
1304
|
+
INSERT INTO contradiction_audits_v19
|
|
1305
|
+
(id, agent_id, entity_type, new_content, new_domain, existing_fact_id,
|
|
1306
|
+
existing_content, similarity_score, contradiction_score, reason,
|
|
1307
|
+
detector, suggested_resolution, source_ref, status, resolution_notes,
|
|
1308
|
+
created_at, resolved_at)
|
|
1309
|
+
SELECT
|
|
1310
|
+
id, agent_id, entity_type, new_content, new_domain, existing_fact_id,
|
|
1311
|
+
existing_content, similarity_score, contradiction_score, reason,
|
|
1312
|
+
detector, suggested_resolution, source_ref, status, resolution_notes,
|
|
1313
|
+
created_at, resolved_at
|
|
1314
|
+
FROM contradiction_audits;
|
|
1315
|
+
DROP TABLE contradiction_audits;
|
|
1316
|
+
ALTER TABLE contradiction_audits_v19 RENAME TO contradiction_audits;
|
|
1317
|
+
`);
|
|
1318
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_contradiction_audits_agent_status ON contradiction_audits(agent_id, status, created_at DESC)');
|
|
1319
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_contradiction_audits_existing_fact ON contradiction_audits(existing_fact_id, status)');
|
|
1320
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_contradiction_audits_agent ON contradiction_audits(agent_id, created_at DESC)');
|
|
1321
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1322
|
+
.run(19, nowIso());
|
|
1323
|
+
}
|
|
1019
1324
|
// Always ensure meta exists before stamping the running engine version.
|
|
1020
1325
|
// Some legacy/stale DBs reached schema >=10 without the V10 migration having
|
|
1021
1326
|
// actually created the table, which would make startup fail with
|
package/dist/message-store.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* This is the write-through layer: Redis → here.
|
|
7
7
|
*/
|
|
8
8
|
import type { DatabaseSync } from 'node:sqlite';
|
|
9
|
-
import type { NeutralMessage, StoredMessage, Conversation, ChannelType, ConversationStatus, RecentTurn } from './types.js';
|
|
9
|
+
import type { NeutralMessage, StoredMessage, Conversation, ChannelType, ConversationStatus, RecentTurn, ArchivedMiningQuery, ArchivedMiningResult, MultiContextMiningOptions } from './types.js';
|
|
10
10
|
export declare class MessageStore {
|
|
11
11
|
private readonly db;
|
|
12
12
|
constructor(db: DatabaseSync);
|
|
@@ -91,6 +91,12 @@ export declare class MessageStore {
|
|
|
91
91
|
*
|
|
92
92
|
* Falls back to getRecentMessages if the head message has no parent chain
|
|
93
93
|
* (e.g., legacy data before backfill).
|
|
94
|
+
*
|
|
95
|
+
* @boundary SHARED DAG PRIMITIVE — not for direct call at mining call sites.
|
|
96
|
+
* @policy See specs/DAG_HELPER_POLICY.md for operator-boundary rules.
|
|
97
|
+
* Use mineArchivedContext / mineArchivedContexts for archived context mining,
|
|
98
|
+
* and the active-composition paths for live session history. Direct call sites
|
|
99
|
+
* outside this class should be limited to exceptional diagnostic use.
|
|
94
100
|
*/
|
|
95
101
|
getHistoryByDAGWalk(headMessageId: number, limit?: number): StoredMessage[];
|
|
96
102
|
/**
|
|
@@ -110,6 +116,63 @@ export declare class MessageStore {
|
|
|
110
116
|
* Get message count for a conversation.
|
|
111
117
|
*/
|
|
112
118
|
getMessageCount(conversationId: number): number;
|
|
119
|
+
/**
|
|
120
|
+
* Get the full message chain for an archived or forked context.
|
|
121
|
+
*
|
|
122
|
+
* Throws if the context does not exist or is active (not archived/forked).
|
|
123
|
+
* Returns an empty array if the context has no head message.
|
|
124
|
+
* Delegates to getHistoryByDAGWalk for the actual chain retrieval.
|
|
125
|
+
*/
|
|
126
|
+
getArchivedChain(contextId: number, limit?: number): StoredMessage[];
|
|
127
|
+
/**
|
|
128
|
+
* Default maximum number of contextIds accepted by mineArchivedContexts.
|
|
129
|
+
* Callers may supply a lower value but not a higher one.
|
|
130
|
+
*/
|
|
131
|
+
static readonly ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX = 20;
|
|
132
|
+
/**
|
|
133
|
+
* Hard ceiling for mineArchivedContexts.
|
|
134
|
+
* Values above this are clamped to this number regardless of caller intent.
|
|
135
|
+
* This prevents unbounded DB fan-out on misconfigured or adversarial inputs.
|
|
136
|
+
*/
|
|
137
|
+
static readonly ARCHIVED_MULTI_CONTEXT_HARD_CEILING = 50;
|
|
138
|
+
/**
|
|
139
|
+
* Mine messages from a single archived or forked context.
|
|
140
|
+
*
|
|
141
|
+
* - Rejects active or missing contexts with a clear error.
|
|
142
|
+
* - Hard-caps limit at 200.
|
|
143
|
+
* - Defaults excludeHeartbeats to true.
|
|
144
|
+
* - Optionally filters by ftsQuery (client-side substring match for Sprint 3; SQL FTS is deferred).
|
|
145
|
+
* - Routes through getHistoryByDAGWalk for DAG-native retrieval.
|
|
146
|
+
* - Returns ArchivedMiningResult<StoredMessage[]> with isHistorical: true.
|
|
147
|
+
*
|
|
148
|
+
* This method does NOT widen active composition — it only operates on
|
|
149
|
+
* explicitly non-active (archived/forked) contexts.
|
|
150
|
+
*/
|
|
151
|
+
mineArchivedContext(query: ArchivedMiningQuery): ArchivedMiningResult<StoredMessage[]>;
|
|
152
|
+
/**
|
|
153
|
+
* Mine messages from multiple archived or forked contexts.
|
|
154
|
+
*
|
|
155
|
+
* ## maxContexts gate (Phase 4 Sprint 3, Task 1)
|
|
156
|
+
* Accepts an optional `maxContexts` in opts to control how many contextIds
|
|
157
|
+
* are accepted in a single call:
|
|
158
|
+
* - Default: ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX (20).
|
|
159
|
+
* - Hard ceiling: ARCHIVED_MULTI_CONTEXT_HARD_CEILING (50).
|
|
160
|
+
* - A caller-supplied value above the hard ceiling is clamped to the ceiling
|
|
161
|
+
* (not rejected), so callers need not know the exact constant.
|
|
162
|
+
* - A caller-supplied value at or below the ceiling is used as-is.
|
|
163
|
+
* - If contextIds.length exceeds the effective max, this method THROWS
|
|
164
|
+
* immediately — it does NOT soft-skip or truncate.
|
|
165
|
+
*
|
|
166
|
+
* ## Other behaviors (unchanged from Sprint 2)
|
|
167
|
+
* - Soft-skips active or missing contextIds with a warning (does not throw).
|
|
168
|
+
* - Preserves input order in the result array.
|
|
169
|
+
* - Applies per-context limit and same filters as mineArchivedContext.
|
|
170
|
+
* - Returns one ArchivedMiningResult per valid archived context.
|
|
171
|
+
*
|
|
172
|
+
* This method does NOT widen active composition — it only operates on
|
|
173
|
+
* explicitly non-active (archived/forked) contexts.
|
|
174
|
+
*/
|
|
175
|
+
mineArchivedContexts(contextIds: number[], opts?: MultiContextMiningOptions): ArchivedMiningResult<StoredMessage[]>[];
|
|
113
176
|
/**
|
|
114
177
|
* Infer channel type from session key format.
|
|
115
178
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-store.d.ts","sourceRoot":"","sources":["../src/message-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,UAAU,
|
|
1
|
+
{"version":3,"file":"message-store.d.ts","sourceRoot":"","sources":["../src/message-store.ts"],"names":[],"mappings":"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"}
|