@psiclawops/hypermem 0.1.0 → 0.5.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 (153) hide show
  1. package/ARCHITECTURE.md +4 -3
  2. package/README.md +457 -174
  3. package/dist/background-indexer.d.ts +19 -4
  4. package/dist/background-indexer.d.ts.map +1 -1
  5. package/dist/background-indexer.js +329 -17
  6. package/dist/cache.d.ts +110 -0
  7. package/dist/cache.d.ts.map +1 -0
  8. package/dist/cache.js +495 -0
  9. package/dist/compaction-fence.d.ts +1 -1
  10. package/dist/compaction-fence.js +1 -1
  11. package/dist/compositor.d.ts +114 -27
  12. package/dist/compositor.d.ts.map +1 -1
  13. package/dist/compositor.js +1678 -229
  14. package/dist/content-type-classifier.d.ts +41 -0
  15. package/dist/content-type-classifier.d.ts.map +1 -0
  16. package/dist/content-type-classifier.js +181 -0
  17. package/dist/cross-agent.d.ts +5 -0
  18. package/dist/cross-agent.d.ts.map +1 -1
  19. package/dist/cross-agent.js +5 -0
  20. package/dist/db.d.ts +1 -1
  21. package/dist/db.d.ts.map +1 -1
  22. package/dist/db.js +6 -2
  23. package/dist/desired-state-store.d.ts +1 -1
  24. package/dist/desired-state-store.d.ts.map +1 -1
  25. package/dist/desired-state-store.js +15 -5
  26. package/dist/doc-chunk-store.d.ts +26 -1
  27. package/dist/doc-chunk-store.d.ts.map +1 -1
  28. package/dist/doc-chunk-store.js +114 -1
  29. package/dist/doc-chunker.d.ts +1 -1
  30. package/dist/doc-chunker.js +1 -1
  31. package/dist/dreaming-promoter.d.ts +86 -0
  32. package/dist/dreaming-promoter.d.ts.map +1 -0
  33. package/dist/dreaming-promoter.js +381 -0
  34. package/dist/episode-store.d.ts +2 -1
  35. package/dist/episode-store.d.ts.map +1 -1
  36. package/dist/episode-store.js +4 -4
  37. package/dist/fact-store.d.ts +19 -1
  38. package/dist/fact-store.d.ts.map +1 -1
  39. package/dist/fact-store.js +64 -3
  40. package/dist/fleet-store.d.ts +1 -1
  41. package/dist/fleet-store.js +1 -1
  42. package/dist/fos-mod.d.ts +178 -0
  43. package/dist/fos-mod.d.ts.map +1 -0
  44. package/dist/fos-mod.js +416 -0
  45. package/dist/hybrid-retrieval.d.ts +5 -1
  46. package/dist/hybrid-retrieval.d.ts.map +1 -1
  47. package/dist/hybrid-retrieval.js +7 -3
  48. package/dist/image-eviction.d.ts +49 -0
  49. package/dist/image-eviction.d.ts.map +1 -0
  50. package/dist/image-eviction.js +251 -0
  51. package/dist/index.d.ts +50 -11
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +73 -43
  54. package/dist/keystone-scorer.d.ts +51 -0
  55. package/dist/keystone-scorer.d.ts.map +1 -0
  56. package/dist/keystone-scorer.js +52 -0
  57. package/dist/knowledge-graph.d.ts +1 -1
  58. package/dist/knowledge-graph.js +1 -1
  59. package/dist/knowledge-lint.d.ts +29 -0
  60. package/dist/knowledge-lint.d.ts.map +1 -0
  61. package/dist/knowledge-lint.js +116 -0
  62. package/dist/knowledge-store.d.ts +1 -1
  63. package/dist/knowledge-store.d.ts.map +1 -1
  64. package/dist/knowledge-store.js +8 -2
  65. package/dist/library-schema.d.ts +3 -3
  66. package/dist/library-schema.d.ts.map +1 -1
  67. package/dist/library-schema.js +324 -3
  68. package/dist/message-store.d.ts +15 -2
  69. package/dist/message-store.d.ts.map +1 -1
  70. package/dist/message-store.js +51 -1
  71. package/dist/metrics-dashboard.d.ts +114 -0
  72. package/dist/metrics-dashboard.d.ts.map +1 -0
  73. package/dist/metrics-dashboard.js +260 -0
  74. package/dist/obsidian-exporter.d.ts +57 -0
  75. package/dist/obsidian-exporter.d.ts.map +1 -0
  76. package/dist/obsidian-exporter.js +274 -0
  77. package/dist/obsidian-watcher.d.ts +147 -0
  78. package/dist/obsidian-watcher.d.ts.map +1 -0
  79. package/dist/obsidian-watcher.js +403 -0
  80. package/dist/open-domain.d.ts +46 -0
  81. package/dist/open-domain.d.ts.map +1 -0
  82. package/dist/open-domain.js +125 -0
  83. package/dist/preference-store.d.ts +1 -1
  84. package/dist/preference-store.js +1 -1
  85. package/dist/preservation-gate.d.ts +1 -1
  86. package/dist/preservation-gate.js +1 -1
  87. package/dist/proactive-pass.d.ts +63 -0
  88. package/dist/proactive-pass.d.ts.map +1 -0
  89. package/dist/proactive-pass.js +239 -0
  90. package/dist/profiles.d.ts +44 -0
  91. package/dist/profiles.d.ts.map +1 -0
  92. package/dist/profiles.js +227 -0
  93. package/dist/provider-translator.d.ts +13 -3
  94. package/dist/provider-translator.d.ts.map +1 -1
  95. package/dist/provider-translator.js +63 -9
  96. package/dist/rate-limiter.d.ts +1 -1
  97. package/dist/rate-limiter.js +1 -1
  98. package/dist/repair-tool-pairs.d.ts +38 -0
  99. package/dist/repair-tool-pairs.d.ts.map +1 -0
  100. package/dist/repair-tool-pairs.js +138 -0
  101. package/dist/retrieval-policy.d.ts +51 -0
  102. package/dist/retrieval-policy.d.ts.map +1 -0
  103. package/dist/retrieval-policy.js +77 -0
  104. package/dist/schema.d.ts +2 -2
  105. package/dist/schema.d.ts.map +1 -1
  106. package/dist/schema.js +28 -2
  107. package/dist/secret-scanner.d.ts +1 -1
  108. package/dist/secret-scanner.js +1 -1
  109. package/dist/seed.d.ts +2 -2
  110. package/dist/seed.js +2 -2
  111. package/dist/session-flusher.d.ts +53 -0
  112. package/dist/session-flusher.d.ts.map +1 -0
  113. package/dist/session-flusher.js +69 -0
  114. package/dist/session-topic-map.d.ts +41 -0
  115. package/dist/session-topic-map.d.ts.map +1 -0
  116. package/dist/session-topic-map.js +77 -0
  117. package/dist/spawn-context.d.ts +54 -0
  118. package/dist/spawn-context.d.ts.map +1 -0
  119. package/dist/spawn-context.js +159 -0
  120. package/dist/system-store.d.ts +1 -1
  121. package/dist/system-store.js +1 -1
  122. package/dist/temporal-store.d.ts +80 -0
  123. package/dist/temporal-store.d.ts.map +1 -0
  124. package/dist/temporal-store.js +149 -0
  125. package/dist/topic-detector.d.ts +35 -0
  126. package/dist/topic-detector.d.ts.map +1 -0
  127. package/dist/topic-detector.js +249 -0
  128. package/dist/topic-store.d.ts +1 -1
  129. package/dist/topic-store.js +1 -1
  130. package/dist/topic-synthesizer.d.ts +51 -0
  131. package/dist/topic-synthesizer.d.ts.map +1 -0
  132. package/dist/topic-synthesizer.js +315 -0
  133. package/dist/trigger-registry.d.ts +63 -0
  134. package/dist/trigger-registry.d.ts.map +1 -0
  135. package/dist/trigger-registry.js +163 -0
  136. package/dist/types.d.ts +214 -10
  137. package/dist/types.d.ts.map +1 -1
  138. package/dist/types.js +1 -1
  139. package/dist/vector-store.d.ts +43 -5
  140. package/dist/vector-store.d.ts.map +1 -1
  141. package/dist/vector-store.js +189 -10
  142. package/dist/version.d.ts +34 -0
  143. package/dist/version.d.ts.map +1 -0
  144. package/dist/version.js +34 -0
  145. package/dist/wiki-page-emitter.d.ts +65 -0
  146. package/dist/wiki-page-emitter.d.ts.map +1 -0
  147. package/dist/wiki-page-emitter.js +258 -0
  148. package/dist/work-store.d.ts +1 -1
  149. package/dist/work-store.js +1 -1
  150. package/package.json +15 -5
  151. package/dist/redis.d.ts +0 -188
  152. package/dist/redis.d.ts.map +0 -1
  153. package/dist/redis.js +0 -534
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Background Indexer
2
+ * hypermem Background Indexer
3
3
  *
4
4
  * Processes message history to extract structured knowledge:
5
5
  * - Facts: atomic pieces of learned information
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import type { DatabaseSync } from 'node:sqlite';
20
20
  import type { IndexerConfig, SessionCursor } from './types.js';
21
+ import { type DreamerConfig } from './dreaming-promoter.js';
21
22
  import type { VectorStore } from './vector-store.js';
22
23
  export interface IndexerStats {
23
24
  agentId: string;
@@ -26,6 +27,8 @@ export interface IndexerStats {
26
27
  episodesRecorded: number;
27
28
  topicsUpdated: number;
28
29
  knowledgeUpserted: number;
30
+ /** Number of superseded fact vectors tombstoned from the vector index this tick. */
31
+ tombstoned: number;
29
32
  elapsedMs: number;
30
33
  /** Number of messages that were post-cursor (unseen by model, high-signal priority). */
31
34
  postCursorMessages: number;
@@ -47,10 +50,13 @@ export declare class BackgroundIndexer {
47
50
  private listAgents?;
48
51
  private getCursor?;
49
52
  private readonly config;
53
+ private readonly dreamerConfig;
50
54
  private intervalHandle;
51
55
  private running;
52
56
  private vectorStore;
53
- constructor(config?: Partial<IndexerConfig>, getMessageDb?: ((agentId: string) => DatabaseSync) | undefined, getLibraryDb?: (() => DatabaseSync) | undefined, listAgents?: (() => string[]) | undefined, getCursor?: CursorFetcher | undefined);
57
+ private synthesizer;
58
+ private tickCount;
59
+ constructor(config?: Partial<IndexerConfig>, getMessageDb?: ((agentId: string) => DatabaseSync) | undefined, getLibraryDb?: (() => DatabaseSync) | undefined, listAgents?: (() => string[]) | undefined, getCursor?: CursorFetcher | undefined, dreamerConfig?: Partial<DreamerConfig>);
54
60
  /**
55
61
  * Set the vector store for embedding new facts/episodes at index time.
56
62
  * Optional — if not set, indexer runs without embedding (FTS5-only mode).
@@ -104,14 +110,23 @@ export declare class BackgroundIndexer {
104
110
  * Parse a duration string like "24h", "7d" into seconds.
105
111
  */
106
112
  private parseDuration;
113
+ /**
114
+ * One-time backfill: embed episodes with sig>=0.5 that were missed by the
115
+ * old >=0.7 vectorization threshold.
116
+ *
117
+ * Gated by a system_state flag 'indexer:episode_backfill_v1' so it runs
118
+ * exactly once even across gateway restarts. Safe to re-run manually
119
+ * (delete the flag row first) if re-backfill is ever needed.
120
+ */
121
+ backfillEpisodeVectors(): Promise<void>;
107
122
  /**
108
123
  * Get current watermarks for all agents.
109
124
  */
110
125
  getWatermarks(libraryDb: DatabaseSync): WatermarkState[];
111
126
  }
112
127
  /**
113
- * Create and start a background indexer connected to HyperMem databases.
128
+ * Create and start a background indexer connected to hypermem databases.
114
129
  * Used by the hook or a standalone daemon.
115
130
  */
116
- export declare function createIndexer(getMessageDb: (agentId: string) => DatabaseSync, getLibraryDb: () => DatabaseSync, listAgents: () => string[], config?: Partial<IndexerConfig>, getCursor?: CursorFetcher, vectorStore?: VectorStore): BackgroundIndexer;
131
+ export declare function createIndexer(getMessageDb: (agentId: string) => DatabaseSync, getLibraryDb: () => DatabaseSync, listAgents: () => string[], config?: Partial<IndexerConfig>, getCursor?: CursorFetcher, vectorStore?: VectorStore, dreamerConfig?: Partial<DreamerConfig>): BackgroundIndexer;
117
132
  //# sourceMappingURL=background-indexer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"background-indexer.d.ts","sourceRoot":"","sources":["../src/background-indexer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAiB,aAAa,EAAe,aAAa,EAAE,MAAM,YAAY,CAAC;AAO3F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;AAEnG,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAgVD,qBAAa,iBAAiB;IAQ1B,OAAO,CAAC,YAAY,CAAC;IACrB,OAAO,CAAC,YAAY,CAAC;IACrB,OAAO,CAAC,UAAU,CAAC;IACnB,OAAO,CAAC,SAAS,CAAC;IAVpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAA4B;gBAG7C,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,EACvB,YAAY,CAAC,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,YAAY,aAAA,EAChD,YAAY,CAAC,GAAE,MAAM,YAAY,aAAA,EACjC,UAAU,CAAC,GAAE,MAAM,MAAM,EAAE,aAAA,EAC3B,SAAS,CAAC,EAAE,aAAa,YAAA;IAanC;;;OAGG;IACH,cAAc,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI;IAIrC;;OAEG;IACH,KAAK,IAAI,IAAI;IAmBb;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAiDrC;;;;;;;;;OASG;YACW,YAAY;IAkK1B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA+B5B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAK/B;;OAEG;IACH,OAAO,CAAC,YAAY;IAsBpB;;OAEG;IACH,OAAO,CAAC,YAAY;IAWpB;;;OAGG;IACH,OAAO,CAAC,UAAU;IA2ClB;;OAEG;IACH,OAAO,CAAC,aAAa;IAarB;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,YAAY,GAAG,cAAc,EAAE;CAezD;AAID;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,YAAY,EAC/C,YAAY,EAAE,MAAM,YAAY,EAChC,UAAU,EAAE,MAAM,MAAM,EAAE,EAC1B,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,EAC/B,SAAS,CAAC,EAAE,aAAa,EACzB,WAAW,CAAC,EAAE,WAAW,GACxB,iBAAiB,CAInB"}
1
+ {"version":3,"file":"background-indexer.d.ts","sourceRoot":"","sources":["../src/background-indexer.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Background Indexer
2
+ * hypermem Background Indexer
3
3
  *
4
4
  * Processes message history to extract structured knowledge:
5
5
  * - Facts: atomic pieces of learned information
@@ -16,12 +16,49 @@
16
16
  * - Bounded: processes N messages per tick to avoid blocking
17
17
  * - Observable: logs extraction stats for monitoring
18
18
  */
19
+ import { lintKnowledge } from './knowledge-lint.js';
19
20
  import { MessageStore } from './message-store.js';
21
+ import { runNoiseSweep, runToolDecay } from './proactive-pass.js';
22
+ import { TopicSynthesizer } from './topic-synthesizer.js';
23
+ import { runDreamingPassForFleet } from './dreaming-promoter.js';
20
24
  import { FactStore } from './fact-store.js';
21
25
  import { EpisodeStore } from './episode-store.js';
22
26
  import { TopicStore } from './topic-store.js';
23
27
  import { KnowledgeStore } from './knowledge-store.js';
28
+ import { TemporalStore } from './temporal-store.js';
24
29
  import { isSafeForSharedVisibility } from './secret-scanner.js';
30
+ // ─── Agent-to-Domain Map ────────────────────────────────────────
31
+ // Maps well-known agent IDs to their primary domain.
32
+ // Used to populate the `domain` column on extracted facts so that
33
+ // domain-scoped retrieval (e.g. getActiveFacts({ domain: 'infrastructure' }))
34
+ // returns results. New agents default to 'general'.
35
+ const AGENT_DOMAIN_MAP = {
36
+ forge: 'infrastructure',
37
+ vigil: 'infrastructure',
38
+ pylon: 'infrastructure',
39
+ plane: 'infrastructure',
40
+ compass: 'product',
41
+ helm: 'product',
42
+ chisel: 'product',
43
+ facet: 'product',
44
+ sentinel: 'security',
45
+ bastion: 'security',
46
+ gauge: 'security',
47
+ clarity: 'ux',
48
+ anvil: 'governance',
49
+ vanguard: 'strategy',
50
+ crucible: 'development',
51
+ relay: 'communications',
52
+ main: 'general',
53
+ 'channel-mini': 'general',
54
+ };
55
+ /**
56
+ * Derive a domain label for a fact based on agent ID.
57
+ * Falls back to 'general' for unknown agents.
58
+ */
59
+ function domainForAgent(agentId) {
60
+ return AGENT_DOMAIN_MAP[agentId] ?? 'general';
61
+ }
25
62
  function extractFactCandidates(content) {
26
63
  const facts = [];
27
64
  if (!content || content.length < 20)
@@ -82,6 +119,29 @@ function extractFactCandidates(content) {
82
119
  * Rejects pattern matches that are code, table fragments, questions,
83
120
  * or too short to be meaningful facts.
84
121
  */
122
+ /**
123
+ * Operational boilerplate phrases that appear frequently across sessions
124
+ * but carry zero signal value. High knn similarity makes them *worse*
125
+ * retrieval candidates — they match everything and contaminate episodes.
126
+ */
127
+ const OPERATIONAL_BOILERPLATE = [
128
+ /timed?\s*out\s*waiting/i,
129
+ /message\s*was\s*delivered/i,
130
+ /no\s*reply\s*(back\s*)?yet/i,
131
+ /picked?\s*it\s*up\s*on\s*(next\s*)?heartbeat/i,
132
+ /session\s*not\s*found/i,
133
+ /\bretrying\b/i,
134
+ /tool\s*call\s*failed/i,
135
+ /exec\s*completed/i,
136
+ /no\s*reply\s*needed/i,
137
+ /still\s*waiting/i,
138
+ /will\s*pick\s*(it\s*)?up\s*(on\s*(next|the))?/i,
139
+ /message\s*is\s*in\s*(his|her|their|the)\s*queue/i,
140
+ /sent\s+to\s+(anvil|compass|clarity|sentinel|vanguard|forge)/i,
141
+ /dispatched\s+(it\s+)?to/i,
142
+ /timed\s*out\s*after/i,
143
+ /\bNO_REPLY\b/,
144
+ ];
85
145
  function isQualityFact(content) {
86
146
  // Too short — sentence fragments
87
147
  if (content.length < 40)
@@ -131,6 +191,30 @@ function isQualityFact(content) {
131
191
  const alphaChars = (content.match(/[a-zA-Z]/g) || []).length;
132
192
  if (alphaChars / content.length < 0.5)
133
193
  return false;
194
+ // TUNE-013: External/untrusted content markers — web search excerpts,
195
+ // external doc pulls, and injected context blocks should never become facts.
196
+ if (/<<<\s*(END_EXTERNAL|BEGIN_EXTERNAL|EXTERNAL_UNTRUSTED|UNTRUSTED_CONTENT)/i.test(content))
197
+ return false;
198
+ if (/EXTERNAL_UNTRUSTED_CONTENT\s+id=/.test(content))
199
+ return false;
200
+ // TUNE-013: Multi-paragraph content — real extracted facts are single sentences.
201
+ // More than 2 newlines means we captured a paragraph or structured block, not a fact.
202
+ const newlineCount = (content.match(/\n/g) || []).length;
203
+ if (newlineCount > 2)
204
+ return false;
205
+ // TUNE-013: URL-heavy content — external source snippets, not actionable facts
206
+ const urlMatches = content.match(/https?:\/\/\S+/g) || [];
207
+ if (urlMatches.length >= 2)
208
+ return false; // one URL in a fact is ok; multiple = source snippet
209
+ // TUNE-013: Content starting with a markdown heading is section text, not a fact
210
+ if (/^#{1,4}\s/.test(content.trim()))
211
+ return false;
212
+ // TUNE-014: Operational boilerplate — phrases common across sessions that produce
213
+ // high knn similarity scores but carry zero signal. They cross-contaminate episodes.
214
+ for (const pattern of OPERATIONAL_BOILERPLATE) {
215
+ if (pattern.test(content))
216
+ return false;
217
+ }
134
218
  return true;
135
219
  }
136
220
  /**
@@ -318,14 +402,33 @@ export class BackgroundIndexer {
318
402
  listAgents;
319
403
  getCursor;
320
404
  config;
405
+ dreamerConfig;
321
406
  intervalHandle = null;
322
407
  running = false;
323
408
  vectorStore = null;
324
- constructor(config, getMessageDb, getLibraryDb, listAgents, getCursor) {
409
+ synthesizer = null;
410
+ tickCount = 0;
411
+ constructor(config, getMessageDb, getLibraryDb, listAgents, getCursor, dreamerConfig) {
325
412
  this.getMessageDb = getMessageDb;
326
413
  this.getLibraryDb = getLibraryDb;
327
414
  this.listAgents = listAgents;
328
415
  this.getCursor = getCursor;
416
+ // Initialize synthesizer if libraryDb accessor is available
417
+ if (getLibraryDb) {
418
+ const libDb = getLibraryDb();
419
+ if (libDb) {
420
+ this.synthesizer = new TopicSynthesizer(libDb, (agentId) => {
421
+ if (!getMessageDb)
422
+ return null;
423
+ try {
424
+ return getMessageDb(agentId);
425
+ }
426
+ catch {
427
+ return null;
428
+ }
429
+ });
430
+ }
431
+ }
329
432
  this.config = {
330
433
  enabled: config?.enabled ?? true,
331
434
  factExtractionMode: config?.factExtractionMode ?? 'tiered',
@@ -333,8 +436,11 @@ export class BackgroundIndexer {
333
436
  topicClosedAfter: config?.topicClosedAfter ?? '7d',
334
437
  factDecayRate: config?.factDecayRate ?? 0.01,
335
438
  episodeSignificanceThreshold: config?.episodeSignificanceThreshold ?? 0.5,
336
- periodicInterval: config?.periodicInterval ?? 300000, // 5 minutes
439
+ periodicInterval: config?.periodicInterval ?? 60000, // 1 minute
440
+ batchSize: config?.batchSize ?? 128,
441
+ maxMessagesPerTick: config?.maxMessagesPerTick ?? 500,
337
442
  };
443
+ this.dreamerConfig = dreamerConfig ?? {};
338
444
  }
339
445
  /**
340
446
  * Set the vector store for embedding new facts/episodes at index time.
@@ -355,13 +461,19 @@ export class BackgroundIndexer {
355
461
  this.tick().catch(err => {
356
462
  console.error('[indexer] Initial tick failed:', err);
357
463
  });
464
+ // Run episode vector backfill once at startup (no-op if already done)
465
+ if (this.vectorStore && this.getLibraryDb) {
466
+ this.backfillEpisodeVectors().catch(err => {
467
+ console.error('[indexer] Episode backfill failed:', err);
468
+ });
469
+ }
358
470
  // Then periodically
359
471
  this.intervalHandle = setInterval(() => {
360
472
  this.tick().catch(err => {
361
473
  console.error('[indexer] Periodic tick failed:', err);
362
474
  });
363
475
  }, this.config.periodicInterval);
364
- console.log(`[indexer] Started with interval ${this.config.periodicInterval}ms`);
476
+ console.log(`[indexer] Started with interval ${this.config.periodicInterval}ms, batchSize ${this.config.batchSize}, maxPerTick ${this.config.maxMessagesPerTick}`);
365
477
  }
366
478
  /**
367
479
  * Stop periodic indexing.
@@ -389,10 +501,16 @@ export class BackgroundIndexer {
389
501
  }
390
502
  const agents = this.listAgents();
391
503
  const libraryDb = this.getLibraryDb();
504
+ let tickTotal = 0;
392
505
  for (const agentId of agents) {
506
+ if (tickTotal >= this.config.maxMessagesPerTick) {
507
+ console.log(`[indexer] maxMessagesPerTick (${this.config.maxMessagesPerTick}) reached — deferring remaining agents`);
508
+ break;
509
+ }
393
510
  try {
394
511
  const stats = await this.processAgent(agentId, libraryDb);
395
- if (stats.messagesProcessed > 0) {
512
+ tickTotal += stats.messagesProcessed;
513
+ if (stats.messagesProcessed > 0 || stats.tombstoned > 0) {
396
514
  results.push(stats);
397
515
  }
398
516
  }
@@ -405,10 +523,84 @@ export class BackgroundIndexer {
405
523
  const totalMessages = results.reduce((s, r) => s + r.messagesProcessed, 0);
406
524
  const totalFacts = results.reduce((s, r) => s + r.factsExtracted, 0);
407
525
  const totalEpisodes = results.reduce((s, r) => s + r.episodesRecorded, 0);
408
- console.log(`[indexer] Tick complete: ${totalMessages} messages → ${totalFacts} facts, ${totalEpisodes} episodes`);
526
+ const totalTombstoned = results.reduce((s, r) => s + r.tombstoned, 0);
527
+ const tombstonedPart = totalTombstoned > 0 ? `, ${totalTombstoned} tombstoned` : '';
528
+ console.log(`[indexer] Tick complete: ${totalMessages} messages → ${totalFacts} facts, ${totalEpisodes} episodes${tombstonedPart}`);
409
529
  }
410
530
  // Run decay on every tick
411
531
  this.applyDecay(libraryDb);
532
+ // Topic synthesis — run for each agent after main indexer tick
533
+ if (this.synthesizer) {
534
+ for (const agentId of agents) {
535
+ try {
536
+ const synthResult = this.synthesizer.tick(agentId);
537
+ if (synthResult.topicsSynthesized > 0) {
538
+ console.log(`[indexer] Synthesized ${synthResult.topicsSynthesized} topics for ${agentId}, ${synthResult.knowledgeEntriesWritten} knowledge entries`);
539
+ }
540
+ }
541
+ catch {
542
+ // Non-fatal
543
+ }
544
+ }
545
+ }
546
+ // Knowledge lint — every LINT_FREQUENCY ticks
547
+ this.tickCount++;
548
+ if (this.tickCount % 10 === 0 && this.getLibraryDb) {
549
+ try {
550
+ const libDb = this.getLibraryDb();
551
+ if (libDb) {
552
+ const lint = lintKnowledge(libDb);
553
+ if (lint.staleDecayed > 0 || lint.coverageGaps.length > 0) {
554
+ console.log(`[indexer] Lint: ${lint.staleDecayed} stale decayed, ${lint.orphansFound} orphans, ${lint.coverageGaps.length} coverage gaps`);
555
+ }
556
+ }
557
+ }
558
+ catch {
559
+ // Non-fatal
560
+ }
561
+ }
562
+ // Dreaming promotion pass — every tickInterval ticks (default 12 = ~1hr)
563
+ const dreamerEnabled = this.dreamerConfig.enabled ?? false;
564
+ const dreamerTickInterval = this.dreamerConfig.tickInterval ?? 12;
565
+ if (dreamerEnabled && this.tickCount % dreamerTickInterval === 0 && this.getLibraryDb) {
566
+ try {
567
+ const libDb = this.getLibraryDb();
568
+ if (libDb) {
569
+ const dreamResults = await runDreamingPassForFleet(agents, libDb, this.dreamerConfig);
570
+ const totalPromoted = dreamResults.reduce((s, r) => s + r.promoted, 0);
571
+ if (totalPromoted > 0) {
572
+ console.log(`[indexer] Dreaming: promoted ${totalPromoted} facts across ${dreamResults.length} agents`);
573
+ }
574
+ }
575
+ }
576
+ catch (err) {
577
+ // Non-fatal — dreaming failures never block indexing
578
+ console.warn('[indexer] Dreaming pass failed (non-fatal):', err.message);
579
+ }
580
+ }
581
+ // Run proactive passes on each agent's message DB
582
+ for (const agentId of agents) {
583
+ const messageDb = this.getMessageDb(agentId);
584
+ if (!messageDb)
585
+ continue;
586
+ // Get active conversations for this agent
587
+ let convRows;
588
+ try {
589
+ convRows = messageDb.prepare(`SELECT id FROM conversations WHERE agent_id = ? AND status = 'active' ORDER BY updated_at DESC LIMIT 10`).all(agentId);
590
+ }
591
+ catch {
592
+ continue;
593
+ }
594
+ for (const conv of convRows) {
595
+ const noiseSweepResult = runNoiseSweep(messageDb, conv.id);
596
+ const toolDecayResult = runToolDecay(messageDb, conv.id);
597
+ // Log only if something changed
598
+ if (noiseSweepResult.messagesDeleted > 0 || toolDecayResult.messagesUpdated > 0) {
599
+ console.log(`[indexer] Proactive pass (conv ${conv.id}): swept ${noiseSweepResult.messagesDeleted} noise msgs, ` +
600
+ `decayed ${toolDecayResult.messagesUpdated} tool results (${toolDecayResult.bytesFreed} bytes freed)`);
601
+ }
602
+ }
603
+ }
412
604
  }
413
605
  finally {
414
606
  this.running = false;
@@ -433,12 +625,19 @@ export class BackgroundIndexer {
433
625
  const episodeStore = new EpisodeStore(libraryDb);
434
626
  const topicStore = new TopicStore(libraryDb);
435
627
  const knowledgeStore = new KnowledgeStore(libraryDb);
628
+ const temporalStore = new TemporalStore(libraryDb);
436
629
  // Get watermark — last processed message ID for this agent
437
630
  const watermark = this.getWatermark(libraryDb, agentId);
438
631
  const lastProcessedId = watermark?.lastMessageId ?? 0;
439
- // Fetch unindexed messages (batch size: 100)
440
- const messages = this.getUnindexedMessages(messageDb, agentId, lastProcessedId, 100);
632
+ // Fetch unindexed messages (batch size from config)
633
+ const messages = this.getUnindexedMessages(messageDb, agentId, lastProcessedId, this.config.batchSize);
441
634
  if (messages.length === 0) {
635
+ // Even with no new messages, run tombstone cleanup in case supersedes
636
+ // were written externally (e.g. via FactStore.markSuperseded()).
637
+ let tombstoned = 0;
638
+ if (this.vectorStore) {
639
+ tombstoned = this.vectorStore.tombstoneSuperseded();
640
+ }
442
641
  return {
443
642
  agentId,
444
643
  messagesProcessed: 0,
@@ -446,6 +645,7 @@ export class BackgroundIndexer {
446
645
  episodesRecorded: 0,
447
646
  topicsUpdated: 0,
448
647
  knowledgeUpserted: 0,
648
+ tombstoned,
449
649
  postCursorMessages: 0,
450
650
  elapsedMs: Date.now() - start,
451
651
  };
@@ -479,6 +679,7 @@ export class BackgroundIndexer {
479
679
  let episodesRecorded = 0;
480
680
  let topicsUpdated = 0;
481
681
  let knowledgeUpserted = 0;
682
+ let supersededFacts = 0;
482
683
  let maxMessageId = lastProcessedId;
483
684
  for (const msg of ordered) {
484
685
  const content = msg.textContent || '';
@@ -493,12 +694,32 @@ export class BackgroundIndexer {
493
694
  try {
494
695
  const fact = factStore.addFact(agentId, factContent, {
495
696
  scope: 'agent',
697
+ domain: domainForAgent(agentId),
496
698
  confidence: factConfidence,
497
699
  sourceType: 'indexer',
498
700
  sourceSessionKey: this.getSessionKeyForMessage(messageDb, msg.conversationId),
499
701
  sourceRef: `msg:${msg.id}`,
500
702
  });
501
703
  factsExtracted++;
704
+ // ── Supersedes detection ─────────────────────────────────
705
+ // Check if the newly extracted fact supersedes an existing one.
706
+ // A supersede is detected when an existing active fact shares the
707
+ // same 60-char prefix (same topic, different phrasing/update).
708
+ if (fact.id) {
709
+ // Index into temporal store (ingest_at as proxy, confidence=0.5)
710
+ temporalStore.indexFact(fact.id, agentId, fact.createdAt);
711
+ const oldFactId = factStore.findSupersedableByContent(agentId, factContent);
712
+ if (oldFactId !== null && oldFactId !== fact.id) {
713
+ const didSupersede = factStore.markSuperseded(oldFactId, fact.id);
714
+ if (didSupersede) {
715
+ supersededFacts++;
716
+ // Immediately remove the stale vector so it can't surface in KNN recall
717
+ if (this.vectorStore) {
718
+ this.vectorStore.removeItem('facts', oldFactId);
719
+ }
720
+ }
721
+ }
722
+ }
502
723
  // Embed new fact for semantic recall (best-effort, non-blocking)
503
724
  if (this.vectorStore && fact.id) {
504
725
  this.vectorStore.indexItem('facts', fact.id, factContent, fact.domain || undefined)
@@ -520,10 +741,12 @@ export class BackgroundIndexer {
520
741
  significance: episode.significance,
521
742
  visibility: episodeVisibility,
522
743
  sessionKey: this.getSessionKeyForMessage(messageDb, msg.conversationId),
744
+ sourceMessageId: msg.id,
523
745
  });
524
746
  episodesRecorded++;
525
- // Embed high-significance episodes (decisions, incidents, deployments)
526
- if (this.vectorStore && recorded?.id && episode.significance >= 0.7) {
747
+ // Embed episodes at sig>=0.5 (lowered from 0.7 — discovery/config_change events
748
+ // at sig=0.5 are real operational events, not noise).
749
+ if (this.vectorStore && recorded?.id && episode.significance >= 0.5) {
527
750
  this.vectorStore.indexItem('episodes', recorded.id, episode.summary, episode.type)
528
751
  .catch(() => { });
529
752
  }
@@ -564,6 +787,12 @@ export class BackgroundIndexer {
564
787
  }
565
788
  // Update watermark
566
789
  this.setWatermark(libraryDb, agentId, maxMessageId);
790
+ // Run tombstone pass: remove vector entries for any facts marked superseded
791
+ // (covers both the supersedes detected above and external markSuperseded calls).
792
+ let tombstoned = 0;
793
+ if (this.vectorStore) {
794
+ tombstoned = this.vectorStore.tombstoneSuperseded();
795
+ }
567
796
  return {
568
797
  agentId,
569
798
  messagesProcessed: messages.length,
@@ -571,6 +800,7 @@ export class BackgroundIndexer {
571
800
  episodesRecorded,
572
801
  topicsUpdated,
573
802
  knowledgeUpserted,
803
+ tombstoned,
574
804
  postCursorMessages: postCursor.length,
575
805
  elapsedMs: Date.now() - start,
576
806
  };
@@ -667,22 +897,25 @@ export class BackgroundIndexer {
667
897
  // Mark dormant topics
668
898
  const dormantThreshold = this.parseDuration(this.config.topicDormantAfter);
669
899
  if (dormantThreshold > 0) {
900
+ // Compute threshold timestamp in JS and pass as parameter — avoids SQL template interpolation.
901
+ const dormantBefore = new Date(Date.now() - dormantThreshold * 1000).toISOString();
670
902
  libraryDb.prepare(`
671
903
  UPDATE topics
672
904
  SET status = 'dormant'
673
905
  WHERE status = 'active'
674
- AND updated_at < datetime('now', '-${dormantThreshold} seconds')
675
- `).run();
906
+ AND updated_at < ?
907
+ `).run(dormantBefore);
676
908
  }
677
909
  // Close old dormant topics
678
910
  const closedThreshold = this.parseDuration(this.config.topicClosedAfter);
679
911
  if (closedThreshold > 0) {
912
+ const closedBefore = new Date(Date.now() - closedThreshold * 1000).toISOString();
680
913
  libraryDb.prepare(`
681
914
  UPDATE topics
682
915
  SET status = 'closed'
683
916
  WHERE status = 'dormant'
684
- AND updated_at < datetime('now', '-${closedThreshold} seconds')
685
- `).run();
917
+ AND updated_at < ?
918
+ `).run(closedBefore);
686
919
  }
687
920
  }
688
921
  /**
@@ -701,6 +934,85 @@ export class BackgroundIndexer {
701
934
  default: return 0;
702
935
  }
703
936
  }
937
+ /**
938
+ * One-time backfill: embed episodes with sig>=0.5 that were missed by the
939
+ * old >=0.7 vectorization threshold.
940
+ *
941
+ * Gated by a system_state flag 'indexer:episode_backfill_v1' so it runs
942
+ * exactly once even across gateway restarts. Safe to re-run manually
943
+ * (delete the flag row first) if re-backfill is ever needed.
944
+ */
945
+ async backfillEpisodeVectors() {
946
+ if (!this.vectorStore || !this.getLibraryDb)
947
+ return;
948
+ const libraryDb = this.getLibraryDb();
949
+ const BACKFILL_FLAG = 'episode_backfill_v1';
950
+ // Ensure system_state table exists (schema may not have been applied yet)
951
+ try {
952
+ libraryDb.prepare(`
953
+ CREATE TABLE IF NOT EXISTS system_state (
954
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
955
+ category TEXT NOT NULL,
956
+ key TEXT NOT NULL,
957
+ value TEXT,
958
+ updated_at TEXT NOT NULL,
959
+ updated_by TEXT,
960
+ ttl TEXT,
961
+ UNIQUE(category, key)
962
+ )
963
+ `).run();
964
+ }
965
+ catch {
966
+ // Table already exists — safe to ignore
967
+ }
968
+ // Check if backfill already completed
969
+ const existing = libraryDb.prepare("SELECT value FROM system_state WHERE category = 'indexer' AND key = ?").get(BACKFILL_FLAG);
970
+ if (existing) {
971
+ // Already done
972
+ return;
973
+ }
974
+ console.log('[indexer] Starting episode vector backfill (sig>=0.5, not yet vectorized)...');
975
+ // Find episodes with sig>=0.5 that have no vec_index_map entry.
976
+ // We join against vec_index_map using a fallback: if the table is in a
977
+ // separate DB (vectors.db), we query it directly via the VectorStore.
978
+ let episodes;
979
+ try {
980
+ episodes = libraryDb.prepare(`
981
+ SELECT id, summary, event_type
982
+ FROM episodes
983
+ WHERE significance >= 0.5
984
+ ORDER BY created_at DESC
985
+ `).all();
986
+ }
987
+ catch {
988
+ console.warn('[indexer] Backfill: could not query episodes table');
989
+ return;
990
+ }
991
+ let queued = 0;
992
+ let skipped = 0;
993
+ for (const ep of episodes) {
994
+ // Check if already vectorized
995
+ if (this.vectorStore.hasItem('episodes', ep.id)) {
996
+ skipped++;
997
+ continue;
998
+ }
999
+ try {
1000
+ await this.vectorStore.indexItem('episodes', ep.id, ep.summary, ep.event_type);
1001
+ queued++;
1002
+ }
1003
+ catch {
1004
+ // Non-fatal — keep going
1005
+ }
1006
+ }
1007
+ // Mark backfill complete
1008
+ const now = new Date().toISOString();
1009
+ libraryDb.prepare(`
1010
+ INSERT INTO system_state (category, key, value, updated_at, updated_by)
1011
+ VALUES ('indexer', ?, ?, ?, 'indexer')
1012
+ ON CONFLICT(category, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
1013
+ `).run(BACKFILL_FLAG, JSON.stringify({ completedAt: now, queued, skipped }), now);
1014
+ console.log(`[indexer] Episode backfill complete: ${queued} queued, ${skipped} already vectorized`);
1015
+ }
704
1016
  /**
705
1017
  * Get current watermarks for all agents.
706
1018
  */
@@ -720,11 +1032,11 @@ export class BackgroundIndexer {
720
1032
  }
721
1033
  // ─── Standalone runner ──────────────────────────────────────────
722
1034
  /**
723
- * Create and start a background indexer connected to HyperMem databases.
1035
+ * Create and start a background indexer connected to hypermem databases.
724
1036
  * Used by the hook or a standalone daemon.
725
1037
  */
726
- export function createIndexer(getMessageDb, getLibraryDb, listAgents, config, getCursor, vectorStore) {
727
- const indexer = new BackgroundIndexer(config, getMessageDb, getLibraryDb, listAgents, getCursor);
1038
+ export function createIndexer(getMessageDb, getLibraryDb, listAgents, config, getCursor, vectorStore, dreamerConfig) {
1039
+ const indexer = new BackgroundIndexer(config, getMessageDb, getLibraryDb, listAgents, getCursor, dreamerConfig);
728
1040
  if (vectorStore)
729
1041
  indexer.setVectorStore(vectorStore);
730
1042
  return indexer;