@psiclawops/hypermem 0.7.0 → 0.8.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.
Files changed (79) hide show
  1. package/ARCHITECTURE.md +30 -38
  2. package/README.md +83 -35
  3. package/dist/background-indexer.d.ts +14 -3
  4. package/dist/background-indexer.d.ts.map +1 -1
  5. package/dist/background-indexer.js +126 -18
  6. package/dist/budget-policy.d.ts +22 -0
  7. package/dist/budget-policy.d.ts.map +1 -0
  8. package/dist/budget-policy.js +27 -0
  9. package/dist/cache.d.ts +11 -0
  10. package/dist/cache.d.ts.map +1 -1
  11. package/dist/compositor-utils.d.ts +31 -0
  12. package/dist/compositor-utils.d.ts.map +1 -0
  13. package/dist/compositor-utils.js +47 -0
  14. package/dist/compositor.d.ts +163 -1
  15. package/dist/compositor.d.ts.map +1 -1
  16. package/dist/compositor.js +862 -130
  17. package/dist/content-hash.d.ts +43 -0
  18. package/dist/content-hash.d.ts.map +1 -0
  19. package/dist/content-hash.js +75 -0
  20. package/dist/context-store.d.ts +54 -0
  21. package/dist/context-store.d.ts.map +1 -1
  22. package/dist/context-store.js +102 -0
  23. package/dist/contradiction-audit-store.d.ts +54 -0
  24. package/dist/contradiction-audit-store.d.ts.map +1 -0
  25. package/dist/contradiction-audit-store.js +88 -0
  26. package/dist/contradiction-resolution-policy.d.ts +21 -0
  27. package/dist/contradiction-resolution-policy.d.ts.map +1 -0
  28. package/dist/contradiction-resolution-policy.js +17 -0
  29. package/dist/degradation.d.ts +102 -0
  30. package/dist/degradation.d.ts.map +1 -0
  31. package/dist/degradation.js +141 -0
  32. package/dist/dreaming-promoter.d.ts +38 -0
  33. package/dist/dreaming-promoter.d.ts.map +1 -1
  34. package/dist/dreaming-promoter.js +68 -2
  35. package/dist/index.d.ts +68 -6
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +402 -26
  38. package/dist/knowledge-lint.d.ts +2 -0
  39. package/dist/knowledge-lint.d.ts.map +1 -1
  40. package/dist/knowledge-lint.js +40 -1
  41. package/dist/library-schema.d.ts +7 -2
  42. package/dist/library-schema.d.ts.map +1 -1
  43. package/dist/library-schema.js +236 -1
  44. package/dist/message-store.d.ts +64 -1
  45. package/dist/message-store.d.ts.map +1 -1
  46. package/dist/message-store.js +137 -1
  47. package/dist/open-domain.js +1 -1
  48. package/dist/proactive-pass.d.ts +2 -2
  49. package/dist/proactive-pass.d.ts.map +1 -1
  50. package/dist/proactive-pass.js +66 -12
  51. package/dist/replay-recovery.d.ts +29 -0
  52. package/dist/replay-recovery.d.ts.map +1 -0
  53. package/dist/replay-recovery.js +82 -0
  54. package/dist/reranker.d.ts +95 -0
  55. package/dist/reranker.d.ts.map +1 -0
  56. package/dist/reranker.js +308 -0
  57. package/dist/schema.d.ts +1 -1
  58. package/dist/schema.d.ts.map +1 -1
  59. package/dist/schema.js +46 -1
  60. package/dist/session-flusher.d.ts +2 -2
  61. package/dist/session-flusher.d.ts.map +1 -1
  62. package/dist/session-flusher.js +1 -1
  63. package/dist/temporal-store.js +2 -2
  64. package/dist/tool-artifact-store.d.ts +98 -0
  65. package/dist/tool-artifact-store.d.ts.map +1 -0
  66. package/dist/tool-artifact-store.js +244 -0
  67. package/dist/topic-detector.js +2 -2
  68. package/dist/topic-store.d.ts +6 -0
  69. package/dist/topic-store.d.ts.map +1 -1
  70. package/dist/topic-store.js +39 -0
  71. package/dist/types.d.ts +233 -1
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/vector-store.d.ts +2 -1
  74. package/dist/vector-store.d.ts.map +1 -1
  75. package/dist/vector-store.js +3 -0
  76. package/dist/version.d.ts +10 -10
  77. package/dist/version.d.ts.map +1 -1
  78. package/dist/version.js +10 -10
  79. package/package.json +6 -4
@@ -16,7 +16,10 @@
16
16
  * 9. Work items (fleet kanban)
17
17
  * 10. Topics (cross-session thread tracking)
18
18
  */
19
- export const LIBRARY_SCHEMA_VERSION = 15;
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
  }
@@ -321,6 +324,7 @@ function applyV3Collections(db) {
321
324
  )
322
325
  `);
323
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))');
324
328
  // ── Fleet registry ──
325
329
  db.exec(`
326
330
  CREATE TABLE IF NOT EXISTS fleet_agents (
@@ -868,6 +872,92 @@ function applyV12FosMod(db) {
868
872
  }
869
873
  }
870
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
+ }
871
961
  // ── Migration runner ──────────────────────────────────────────
872
962
  export function migrateLibrary(db, engineVersion) {
873
963
  db.exec(`
@@ -1086,6 +1176,151 @@ export function migrateLibrary(db, engineVersion) {
1086
1176
  db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
1087
1177
  .run(15, nowIso());
1088
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
+ }
1089
1324
  // Always ensure meta exists before stamping the running engine version.
1090
1325
  // Some legacy/stale DBs reached schema >=10 without the V10 migration having
1091
1326
  // actually created the table, which would make startup fail with
@@ -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":"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"}
1
+ {"version":3,"file":"message-store.d.ts","sourceRoot":"","sources":["../src/message-store.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@
5
5
  * All messages are stored in provider-neutral format.
6
6
  * This is the write-through layer: Redis → here.
7
7
  */
8
- import { getOrCreateActiveContext, updateContextHead } from './context-store.js';
8
+ import { getOrCreateActiveContext, updateContextHead, getArchivedContext } from './context-store.js';
9
9
  function nowIso() {
10
10
  return new Date().toISOString();
11
11
  }
@@ -332,6 +332,12 @@ export class MessageStore {
332
332
  *
333
333
  * Falls back to getRecentMessages if the head message has no parent chain
334
334
  * (e.g., legacy data before backfill).
335
+ *
336
+ * @boundary SHARED DAG PRIMITIVE — not for direct call at mining call sites.
337
+ * @policy See specs/DAG_HELPER_POLICY.md for operator-boundary rules.
338
+ * Use mineArchivedContext / mineArchivedContexts for archived context mining,
339
+ * and the active-composition paths for live session history. Direct call sites
340
+ * outside this class should be limited to exceptional diagnostic use.
335
341
  */
336
342
  getHistoryByDAGWalk(headMessageId, limit = 50) {
337
343
  try {
@@ -414,6 +420,136 @@ export class MessageStore {
414
420
  .get(conversationId);
415
421
  return row.count;
416
422
  }
423
+ /**
424
+ * Get the full message chain for an archived or forked context.
425
+ *
426
+ * Throws if the context does not exist or is active (not archived/forked).
427
+ * Returns an empty array if the context has no head message.
428
+ * Delegates to getHistoryByDAGWalk for the actual chain retrieval.
429
+ */
430
+ getArchivedChain(contextId, limit) {
431
+ const context = getArchivedContext(this.db, contextId);
432
+ if (!context) {
433
+ throw new Error('getArchivedChain: context must be archived or forked');
434
+ }
435
+ if (context.headMessageId === null) {
436
+ return [];
437
+ }
438
+ return this.getHistoryByDAGWalk(context.headMessageId, limit ?? 200);
439
+ }
440
+ // ─── Archived Mining (Phase 4 Sprint 2 / Sprint 3) ─────────────
441
+ /**
442
+ * Default maximum number of contextIds accepted by mineArchivedContexts.
443
+ * Callers may supply a lower value but not a higher one.
444
+ */
445
+ static ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX = 20;
446
+ /**
447
+ * Hard ceiling for mineArchivedContexts.
448
+ * Values above this are clamped to this number regardless of caller intent.
449
+ * This prevents unbounded DB fan-out on misconfigured or adversarial inputs.
450
+ */
451
+ static ARCHIVED_MULTI_CONTEXT_HARD_CEILING = 50;
452
+ // ─── Archived Mining (Phase 4 Sprint 2) ───────────────────────
453
+ /**
454
+ * Mine messages from a single archived or forked context.
455
+ *
456
+ * - Rejects active or missing contexts with a clear error.
457
+ * - Hard-caps limit at 200.
458
+ * - Defaults excludeHeartbeats to true.
459
+ * - Optionally filters by ftsQuery (client-side substring match for Sprint 3; SQL FTS is deferred).
460
+ * - Routes through getHistoryByDAGWalk for DAG-native retrieval.
461
+ * - Returns ArchivedMiningResult<StoredMessage[]> with isHistorical: true.
462
+ *
463
+ * This method does NOT widen active composition — it only operates on
464
+ * explicitly non-active (archived/forked) contexts.
465
+ */
466
+ mineArchivedContext(query) {
467
+ const { contextId, limit, excludeHeartbeats = true, ftsQuery } = query;
468
+ const context = getArchivedContext(this.db, contextId);
469
+ if (!context) {
470
+ throw new Error(`mineArchivedContext: context ${contextId} does not exist or is not archived/forked. ` +
471
+ `Only archived or forked contexts may be mined.`);
472
+ }
473
+ // Hard cap at 200
474
+ const effectiveLimit = Math.min(limit ?? 200, 200);
475
+ let messages = [];
476
+ if (context.headMessageId !== null) {
477
+ messages = this.getHistoryByDAGWalk(context.headMessageId, effectiveLimit);
478
+ }
479
+ // Apply heartbeat filter (default: exclude)
480
+ if (excludeHeartbeats) {
481
+ messages = messages.filter(m => !m.isHeartbeat);
482
+ }
483
+ // Client-side ftsQuery filter (substring match for Sprint 2)
484
+ if (ftsQuery && ftsQuery.trim().length > 0) {
485
+ const q = ftsQuery.trim().toLowerCase();
486
+ messages = messages.filter(m => (m.textContent ?? '').toLowerCase().includes(q));
487
+ }
488
+ return {
489
+ isHistorical: true,
490
+ contextId: context.id,
491
+ agentId: context.agentId,
492
+ sessionKey: context.sessionKey,
493
+ contextStatus: context.status,
494
+ contextUpdatedAt: context.updatedAt,
495
+ data: messages,
496
+ };
497
+ }
498
+ /**
499
+ * Mine messages from multiple archived or forked contexts.
500
+ *
501
+ * ## maxContexts gate (Phase 4 Sprint 3, Task 1)
502
+ * Accepts an optional `maxContexts` in opts to control how many contextIds
503
+ * are accepted in a single call:
504
+ * - Default: ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX (20).
505
+ * - Hard ceiling: ARCHIVED_MULTI_CONTEXT_HARD_CEILING (50).
506
+ * - A caller-supplied value above the hard ceiling is clamped to the ceiling
507
+ * (not rejected), so callers need not know the exact constant.
508
+ * - A caller-supplied value at or below the ceiling is used as-is.
509
+ * - If contextIds.length exceeds the effective max, this method THROWS
510
+ * immediately — it does NOT soft-skip or truncate.
511
+ *
512
+ * ## Other behaviors (unchanged from Sprint 2)
513
+ * - Soft-skips active or missing contextIds with a warning (does not throw).
514
+ * - Preserves input order in the result array.
515
+ * - Applies per-context limit and same filters as mineArchivedContext.
516
+ * - Returns one ArchivedMiningResult per valid archived context.
517
+ *
518
+ * This method does NOT widen active composition — it only operates on
519
+ * explicitly non-active (archived/forked) contexts.
520
+ */
521
+ mineArchivedContexts(contextIds, opts) {
522
+ // ── maxContexts gate ──────────────────────────────────────────────────
523
+ const { maxContexts: callerMax, ...perContextOpts } = opts ?? {};
524
+ const effectiveMax = callerMax !== undefined
525
+ ? Math.min(callerMax, MessageStore.ARCHIVED_MULTI_CONTEXT_HARD_CEILING)
526
+ : MessageStore.ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX;
527
+ if (contextIds.length > effectiveMax) {
528
+ throw new Error(`mineArchivedContexts: too many contextIds (${contextIds.length}). ` +
529
+ `Effective limit is ${effectiveMax} ` +
530
+ `(hard ceiling: ${MessageStore.ARCHIVED_MULTI_CONTEXT_HARD_CEILING}, ` +
531
+ `default: ${MessageStore.ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX}). ` +
532
+ `Pass fewer contextIds or supply a higher maxContexts (max: ${MessageStore.ARCHIVED_MULTI_CONTEXT_HARD_CEILING}).`);
533
+ }
534
+ // ── end gate ─────────────────────────────────────────────────────────
535
+ const results = [];
536
+ for (const contextId of contextIds) {
537
+ const context = getArchivedContext(this.db, contextId);
538
+ if (!context) {
539
+ console.warn(`[hypermem:message-store] mineArchivedContexts: skipping contextId ${contextId} ` +
540
+ `— does not exist or is not archived/forked (may be active or missing).`);
541
+ continue;
542
+ }
543
+ try {
544
+ results.push(this.mineArchivedContext({ contextId, ...perContextOpts }));
545
+ }
546
+ catch (err) {
547
+ console.warn(`[hypermem:message-store] mineArchivedContexts: skipping contextId ${contextId} ` +
548
+ `— ${err.message}`);
549
+ }
550
+ }
551
+ return results;
552
+ }
417
553
  // ─── Helpers ─────────────────────────────────────────────────
418
554
  /**
419
555
  * Infer channel type from session key format.
@@ -61,7 +61,7 @@ export function buildOpenDomainFtsQuery(query) {
61
61
  .split(/\s+/)
62
62
  .filter(w => w.length >= 3 && !STOP_WORDS.has(w))
63
63
  .slice(0, 6)
64
- .map(w => `${w}*`);
64
+ .map(w => `"${w}"*`);
65
65
  if (terms.length === 0)
66
66
  return null;
67
67
  return terms.join(' OR ');
@@ -42,7 +42,7 @@ export interface ToolDecayResult {
42
42
  * Deletions are wrapped in a single transaction. The FTS5 trigger handles
43
43
  * index cleanup automatically (msg_fts_ad fires on DELETE).
44
44
  */
45
- export declare function runNoiseSweep(db: DatabaseSync, conversationId: number, recentWindowSize?: number): NoiseSweepResult;
45
+ export declare function runNoiseSweep(db: DatabaseSync, conversationId: number, recentWindowSize?: number, maxCandidates?: number): NoiseSweepResult;
46
46
  /**
47
47
  * Truncate oversized tool_results outside the recent window.
48
48
  *
@@ -59,5 +59,5 @@ export declare function runNoiseSweep(db: DatabaseSync, conversationId: number,
59
59
  *
60
60
  * Mutations are committed in a single transaction.
61
61
  */
62
- export declare function runToolDecay(db: DatabaseSync, conversationId: number, recentWindowSize?: number): ToolDecayResult;
62
+ export declare function runToolDecay(db: DatabaseSync, conversationId: number, recentWindowSize?: number, maxCandidates?: number): ToolDecayResult;
63
63
  //# sourceMappingURL=proactive-pass.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proactive-pass.d.ts","sourceRoot":"","sources":["../src/proactive-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AA6CD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,GAC5B,gBAAgB,CA2ElB;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,GAC5B,eAAe,CAiGjB"}
1
+ {"version":3,"file":"proactive-pass.d.ts","sourceRoot":"","sources":["../src/proactive-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAwFD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,EAC7B,aAAa,GAAE,MAAiB,GAC/B,gBAAgB,CAwFlB;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,EAC7B,aAAa,GAAE,MAAiB,GAC/B,eAAe,CAgHjB"}