@monoes/monomindcli 1.11.7 → 1.11.9

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 (222) hide show
  1. package/.claude/agents/github/issue-tracker.md +5 -5
  2. package/.claude/agents/github/pr-manager.md +5 -5
  3. package/.claude/agents/github/release-manager.md +3 -3
  4. package/.claude/agents/github/repo-architect.md +3 -3
  5. package/.claude/agents/github/swarm-issue.md +1 -1
  6. package/.claude/agents/github/sync-coordinator.md +1 -1
  7. package/.claude/agents/github/workflow-automation.md +1 -1
  8. package/.claude/commands/github/repo-architect.md +1 -1
  9. package/.claude/commands/github/sync-coordinator.md +1 -1
  10. package/.claude/commands/mastermind/createorg.md +4 -1
  11. package/.claude/commands/mastermind/help.md +2 -2
  12. package/.claude/commands/mastermind/orgs.md +21 -0
  13. package/.claude/commands/mastermind/orgstatus.md +59 -0
  14. package/.claude/commands/mastermind/runorg.md +4 -2
  15. package/.claude/commands/mastermind/stoporg.md +78 -0
  16. package/.claude/commands/mastermind/swarm.md +1 -1
  17. package/.claude/helpers/handlers/gates-handler.cjs +135 -0
  18. package/.claude/helpers/handlers/task-handler.cjs +7 -3
  19. package/.claude/helpers/hook-handler.cjs +11 -2
  20. package/.claude/helpers/intelligence.cjs +87 -0
  21. package/.claude/helpers/learning-service.mjs +60 -0
  22. package/.claude/helpers/memory.cjs +69 -0
  23. package/.claude/helpers/router.cjs +68 -0
  24. package/.claude/helpers/session.cjs +63 -0
  25. package/.claude/helpers/utils/monograph.cjs +4 -2
  26. package/.claude/helpers/utils/telemetry.cjs +3 -2
  27. package/.claude/skills/agentic-jujutsu/SKILL.md +1 -1
  28. package/.claude/skills/hive-mind-advanced/SKILL.md +4 -4
  29. package/.claude/skills/mastermind/_agent-select.md +2 -2
  30. package/.claude/skills/mastermind/access.md +11 -11
  31. package/.claude/skills/mastermind/adapter-manager.md +13 -13
  32. package/.claude/skills/mastermind/adapters.md +7 -7
  33. package/.claude/skills/mastermind/agent-detail.md +1 -1
  34. package/.claude/skills/mastermind/agents.md +5 -5
  35. package/.claude/skills/mastermind/approval-detail.md +6 -6
  36. package/.claude/skills/mastermind/approve.md +9 -10
  37. package/.claude/skills/mastermind/backup.md +2 -2
  38. package/.claude/skills/mastermind/bootstrap.md +2 -2
  39. package/.claude/skills/mastermind/companies.md +7 -7
  40. package/.claude/skills/mastermind/createorg.md +213 -8
  41. package/.claude/skills/mastermind/diagnose.md +4 -4
  42. package/.claude/skills/mastermind/env.md +1 -1
  43. package/.claude/skills/mastermind/environments.md +8 -8
  44. package/.claude/skills/mastermind/export.md +12 -3
  45. package/.claude/skills/mastermind/goal-detail.md +9 -9
  46. package/.claude/skills/mastermind/goals.md +4 -4
  47. package/.claude/skills/mastermind/heartbeat.md +1 -1
  48. package/.claude/skills/mastermind/idea.md +4 -4
  49. package/.claude/skills/mastermind/import.md +8 -8
  50. package/.claude/skills/mastermind/inbox.md +4 -4
  51. package/.claude/skills/mastermind/instance-settings.md +9 -9
  52. package/.claude/skills/mastermind/instance.md +9 -7
  53. package/.claude/skills/mastermind/invite-landing.md +5 -5
  54. package/.claude/skills/mastermind/invites.md +12 -12
  55. package/.claude/skills/mastermind/issue-detail.md +8 -8
  56. package/.claude/skills/mastermind/monitor.md +11 -11
  57. package/.claude/skills/mastermind/my-issues.md +6 -6
  58. package/.claude/skills/mastermind/new-agent.md +4 -4
  59. package/.claude/skills/mastermind/org-chart.md +8 -6
  60. package/.claude/skills/mastermind/org-settings.md +58 -21
  61. package/.claude/skills/mastermind/orgs.md +98 -0
  62. package/.claude/skills/mastermind/orgstatus.md +194 -0
  63. package/.claude/skills/mastermind/plan-to-tasks.md +1 -1
  64. package/.claude/skills/mastermind/plugin-manager.md +12 -12
  65. package/.claude/skills/mastermind/plugin-settings.md +5 -5
  66. package/.claude/skills/mastermind/plugins.md +5 -5
  67. package/.claude/skills/mastermind/profile.md +2 -2
  68. package/.claude/skills/mastermind/project-detail.md +12 -12
  69. package/.claude/skills/mastermind/project-workspace.md +4 -4
  70. package/.claude/skills/mastermind/projects.md +4 -4
  71. package/.claude/skills/mastermind/review.md +50 -0
  72. package/.claude/skills/mastermind/routine-detail.md +3 -3
  73. package/.claude/skills/mastermind/routines.md +7 -6
  74. package/.claude/skills/mastermind/runorg.md +178 -8
  75. package/.claude/skills/mastermind/search.md +6 -6
  76. package/.claude/skills/mastermind/secrets.md +6 -6
  77. package/.claude/skills/mastermind/skills.md +4 -4
  78. package/.claude/skills/mastermind/stoporg.md +138 -0
  79. package/.claude/skills/mastermind/workspace-detail.md +5 -5
  80. package/.claude/skills/mastermind/workspaces.md +9 -9
  81. package/.claude/skills/performance-analysis/SKILL.md +3 -3
  82. package/.claude/skills/sparc-methodology/SKILL.md +2 -2
  83. package/.claude/skills/swarm-advanced/SKILL.md +4 -4
  84. package/README.md +129 -376
  85. package/dist/src/agents/registry-builder.d.ts +27 -1
  86. package/dist/src/agents/registry-builder.d.ts.map +1 -1
  87. package/dist/src/agents/registry-builder.js +2 -2
  88. package/dist/src/agents/registry-builder.js.map +1 -1
  89. package/dist/src/commands/agent.d.ts.map +1 -1
  90. package/dist/src/commands/agent.js +4 -9
  91. package/dist/src/commands/agent.js.map +1 -1
  92. package/dist/src/commands/analyze.d.ts +1 -1
  93. package/dist/src/commands/analyze.js +1 -1
  94. package/dist/src/commands/claims.d.ts +1 -1
  95. package/dist/src/commands/claims.js +2 -2
  96. package/dist/src/commands/claims.js.map +1 -1
  97. package/dist/src/commands/cleanup.d.ts +1 -1
  98. package/dist/src/commands/cleanup.js +1 -1
  99. package/dist/src/commands/completions.d.ts +1 -1
  100. package/dist/src/commands/completions.js +1 -1
  101. package/dist/src/commands/deployment.d.ts +1 -1
  102. package/dist/src/commands/deployment.js +2 -2
  103. package/dist/src/commands/deployment.js.map +1 -1
  104. package/dist/src/commands/doctor.d.ts +1 -1
  105. package/dist/src/commands/doctor.d.ts.map +1 -1
  106. package/dist/src/commands/doctor.js +69 -4
  107. package/dist/src/commands/doctor.js.map +1 -1
  108. package/dist/src/commands/guidance.d.ts.map +1 -1
  109. package/dist/src/commands/guidance.js +129 -0
  110. package/dist/src/commands/guidance.js.map +1 -1
  111. package/dist/src/commands/index.d.ts.map +1 -1
  112. package/dist/src/commands/index.js +4 -0
  113. package/dist/src/commands/index.js.map +1 -1
  114. package/dist/src/commands/init.d.ts.map +1 -1
  115. package/dist/src/commands/init.js +18 -0
  116. package/dist/src/commands/init.js.map +1 -1
  117. package/dist/src/commands/monovector/import.d.ts +1 -1
  118. package/dist/src/commands/monovector/import.js +1 -1
  119. package/dist/src/commands/monovector/index.d.ts +1 -1
  120. package/dist/src/commands/monovector/index.js +1 -1
  121. package/dist/src/commands/monovector/setup.d.ts +1 -1
  122. package/dist/src/commands/monovector/setup.js +2 -2
  123. package/dist/src/commands/neural.d.ts +1 -1
  124. package/dist/src/commands/neural.js +2 -2
  125. package/dist/src/commands/neural.js.map +1 -1
  126. package/dist/src/commands/performance.d.ts +1 -1
  127. package/dist/src/commands/performance.js +2 -2
  128. package/dist/src/commands/performance.js.map +1 -1
  129. package/dist/src/commands/platforms.d.ts +1 -1
  130. package/dist/src/commands/platforms.js +1 -1
  131. package/dist/src/commands/plugins.d.ts +1 -1
  132. package/dist/src/commands/plugins.d.ts.map +1 -1
  133. package/dist/src/commands/plugins.js +2 -4
  134. package/dist/src/commands/plugins.js.map +1 -1
  135. package/dist/src/commands/providers.d.ts +1 -1
  136. package/dist/src/commands/providers.js +2 -2
  137. package/dist/src/commands/providers.js.map +1 -1
  138. package/dist/src/commands/route.d.ts +1 -1
  139. package/dist/src/commands/route.d.ts.map +1 -1
  140. package/dist/src/commands/route.js +5 -11
  141. package/dist/src/commands/route.js.map +1 -1
  142. package/dist/src/commands/security.d.ts +1 -1
  143. package/dist/src/commands/security.d.ts.map +1 -1
  144. package/dist/src/commands/security.js +140 -91
  145. package/dist/src/commands/security.js.map +1 -1
  146. package/dist/src/dlq/dlq-replayer.d.ts +7 -1
  147. package/dist/src/dlq/dlq-replayer.d.ts.map +1 -1
  148. package/dist/src/dlq/dlq-replayer.js.map +1 -1
  149. package/dist/src/index.d.ts +1 -1
  150. package/dist/src/index.d.ts.map +1 -1
  151. package/dist/src/index.js +10 -26
  152. package/dist/src/index.js.map +1 -1
  153. package/dist/src/init/claudemd-generator.js +2 -2
  154. package/dist/src/init/claudemd-generator.js.map +1 -1
  155. package/dist/src/init/executor.js +3 -3
  156. package/dist/src/init/settings-generator.js +2 -2
  157. package/dist/src/init/settings-generator.js.map +1 -1
  158. package/dist/src/mcp-client.d.ts +5 -0
  159. package/dist/src/mcp-client.d.ts.map +1 -1
  160. package/dist/src/mcp-client.js +7 -0
  161. package/dist/src/mcp-client.js.map +1 -1
  162. package/dist/src/mcp-server.d.ts.map +1 -1
  163. package/dist/src/mcp-server.js +17 -1
  164. package/dist/src/mcp-server.js.map +1 -1
  165. package/dist/src/mcp-tools/a2a-tools.js +6 -6
  166. package/dist/src/mcp-tools/a2a-tools.js.map +1 -1
  167. package/dist/src/mcp-tools/auto-install.d.ts +2 -2
  168. package/dist/src/mcp-tools/auto-install.js +1 -1
  169. package/dist/src/mcp-tools/auto-install.js.map +1 -1
  170. package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
  171. package/dist/src/mcp-tools/hive-mind-tools.js +1 -52
  172. package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
  173. package/dist/src/mcp-tools/index.d.ts +4 -0
  174. package/dist/src/mcp-tools/index.d.ts.map +1 -1
  175. package/dist/src/mcp-tools/index.js +4 -0
  176. package/dist/src/mcp-tools/index.js.map +1 -1
  177. package/dist/src/mcp-tools/monograph-compat.d.ts.map +1 -1
  178. package/dist/src/mcp-tools/monograph-compat.js +1 -2
  179. package/dist/src/mcp-tools/monograph-compat.js.map +1 -1
  180. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  181. package/dist/src/mcp-tools/monograph-tools.js +107 -5
  182. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  183. package/dist/src/mcp-tools/security-tools.d.ts +6 -6
  184. package/dist/src/mcp-tools/security-tools.d.ts.map +1 -1
  185. package/dist/src/mcp-tools/security-tools.js +48 -61
  186. package/dist/src/mcp-tools/security-tools.js.map +1 -1
  187. package/dist/src/memory/memory-bridge.d.ts +0 -1
  188. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  189. package/dist/src/memory/memory-bridge.js +232 -57
  190. package/dist/src/memory/memory-bridge.js.map +1 -1
  191. package/dist/src/memory/memory-initializer.d.ts.map +1 -1
  192. package/dist/src/memory/memory-initializer.js +3 -32
  193. package/dist/src/memory/memory-initializer.js.map +1 -1
  194. package/dist/src/plugins/store/discovery.d.ts.map +1 -1
  195. package/dist/src/plugins/store/discovery.js +0 -69
  196. package/dist/src/plugins/store/discovery.js.map +1 -1
  197. package/dist/src/routing/embed-worker.d.ts +2 -0
  198. package/dist/src/routing/embed-worker.d.ts.map +1 -0
  199. package/dist/src/routing/embed-worker.js +55 -0
  200. package/dist/src/routing/embed-worker.js.map +1 -0
  201. package/dist/src/routing/embedder.d.ts +31 -0
  202. package/dist/src/routing/embedder.d.ts.map +1 -0
  203. package/dist/src/routing/embedder.js +0 -0
  204. package/dist/src/routing/embedder.js.map +1 -0
  205. package/dist/src/routing/llm-caller.d.ts +1 -1
  206. package/dist/src/routing/llm-caller.d.ts.map +1 -1
  207. package/dist/src/routing/llm-caller.js +18 -3
  208. package/dist/src/routing/llm-caller.js.map +1 -1
  209. package/dist/src/routing/route-layer-factory.d.ts +9 -0
  210. package/dist/src/routing/route-layer-factory.d.ts.map +1 -0
  211. package/dist/src/routing/route-layer-factory.js +151 -0
  212. package/dist/src/routing/route-layer-factory.js.map +1 -0
  213. package/dist/src/services/worker-daemon.d.ts.map +1 -1
  214. package/dist/src/services/worker-daemon.js +0 -1
  215. package/dist/src/services/worker-daemon.js.map +1 -1
  216. package/dist/src/suggest.d.ts +1 -1
  217. package/dist/src/suggest.js +1 -1
  218. package/dist/src/ui/server.mjs +5 -2
  219. package/dist/tsconfig.tsbuildinfo +1 -1
  220. package/package.json +9 -10
  221. package/scripts/publish-registry.ts +0 -2
  222. package/scripts/understand-analyze.mjs +1 -1
@@ -75,8 +75,17 @@ const MAX_INIT_ATTEMPTS = 3;
75
75
  let initAttempts = 0;
76
76
  /** Backpressure flag for A-MEM Zettelkasten linking — see bridgeStoreEntry. */
77
77
  let _amemInFlight = false;
78
- /** OR-3: Count SONA learning failures so callers can detect degraded state. */
79
- let _sonaErrorCount = 0;
78
+ // In-process HNSW index built lazily on first bridgeSearchHNSW call, updated on every write.
79
+ // Keyed to the singleton registry: only one DB path is in use per MCP server process.
80
+ let _hnswIndex = null;
81
+ let _hnswIndexBuilt = false;
82
+ let _hnswBuildFailedAt = 0;
83
+ const HNSW_RETRY_INTERVAL_MS = 60_000;
84
+ // Ghost-entry tracking: upserts (replace an existing row) and soft-deletes leave stale
85
+ // ids in the HNSW graph that SQL filters drop at query time — wasting candidate slots.
86
+ // When the dirty count crosses the rebuild threshold, the next search rebuilds from DB.
87
+ let _hnswDirtyCount = 0;
88
+ const HNSW_REBUILD_THRESHOLD = 50;
80
89
  /**
81
90
  * Resolve database path with path traversal protection.
82
91
  * Only allows paths within or below the project's .swarm directory,
@@ -153,6 +162,7 @@ async function getRegistry(dbPath) {
153
162
  hierarchicalMemory: true,
154
163
  memoryConsolidation: true,
155
164
  memoryGraph: true, // issue #1214: enable MemoryGraph for graph-aware ranking
165
+ causalGraph: true, // required by bridgeRecordCausalEdge for A-MEM edge storage
156
166
  },
157
167
  });
158
168
  }
@@ -356,14 +366,74 @@ function getDb(registry) {
356
366
  db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`);
357
367
  db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_key ON memory_entries(key)`);
358
368
  db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_status ON memory_entries(status)`);
369
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_expires ON memory_entries(expires_at)`);
359
370
  }
360
371
  catch {
361
372
  // Table already exists or db is read-only — that's fine
362
373
  }
363
374
  _dbInitialized.add(db);
375
+ // Warn if any existing rows were written with a different embedding dimension.
376
+ // This degrades semantic search silently; warn once per DB open so operators know.
377
+ try {
378
+ const dimRow = db.prepare(`SELECT embedding_dimensions FROM memory_entries WHERE embedding_dimensions IS NOT NULL AND status = 'active' LIMIT 1`).get();
379
+ if (dimRow && dimRow.embedding_dimensions !== BRIDGE_EMBEDDING_DIMS) {
380
+ console.warn(`[MemoryBridge] Dimension mismatch: DB has ${dimRow.embedding_dimensions}-dim embeddings, bridge expects ${BRIDGE_EMBEDDING_DIMS}. Semantic search will skip mismatched rows.`);
381
+ }
382
+ }
383
+ catch { /* Non-fatal */ }
364
384
  return { db, agentdb };
365
385
  }
366
386
  // ===== Bridge functions — match memory-initializer.ts signatures =====
387
+ /**
388
+ * Build or return the in-process HNSWIndex, populated from all active rows in the DB.
389
+ * Called lazily on first semantic search; subsequent calls return the cached index.
390
+ */
391
+ async function getOrBuildHnswIndex(db) {
392
+ // Trigger a lazy rebuild when ghost-entry accumulation crosses the threshold.
393
+ // Ghosts come from: upsert-replace (old id stays in graph), soft-delete (id
394
+ // filtered by SQL but wastes candidate slots). Rebuild reads fresh rows from DB.
395
+ if (_hnswIndexBuilt && _hnswDirtyCount >= HNSW_REBUILD_THRESHOLD) {
396
+ _hnswIndexBuilt = false;
397
+ _hnswDirtyCount = 0;
398
+ }
399
+ if (_hnswIndexBuilt)
400
+ return _hnswIndex;
401
+ // Enforce retry cooldown after a build failure to avoid rapid retry loops
402
+ if (_hnswBuildFailedAt > 0 && Date.now() - _hnswBuildFailedAt < HNSW_RETRY_INTERVAL_MS)
403
+ return null;
404
+ _hnswIndexBuilt = true; // Lock prevents concurrent re-build
405
+ try {
406
+ const memPkg = await import('@monoes/memory');
407
+ if (!memPkg?.HNSWIndex)
408
+ return null;
409
+ const rows = db.prepare(`SELECT id, embedding FROM memory_entries WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) AND embedding IS NOT NULL`).all(Date.now());
410
+ const valid = [];
411
+ for (const row of rows) {
412
+ const emb = safeParseEmbedding(row.embedding);
413
+ if (emb && emb.length === BRIDGE_EMBEDDING_DIMS)
414
+ valid.push({ id: row.id, emb });
415
+ }
416
+ const index = new memPkg.HNSWIndex({
417
+ dimensions: BRIDGE_EMBEDDING_DIMS,
418
+ M: 16,
419
+ efConstruction: 200,
420
+ maxElements: Math.max(valid.length + 1000, 10000),
421
+ metric: 'cosine',
422
+ });
423
+ for (const { id, emb } of valid) {
424
+ await index.addPoint(id, new Float32Array(emb));
425
+ }
426
+ _hnswIndex = index;
427
+ _hnswBuildFailedAt = 0;
428
+ return index;
429
+ }
430
+ catch {
431
+ // Reset lock so the next caller can retry after the cooldown
432
+ _hnswIndexBuilt = false;
433
+ _hnswBuildFailedAt = Date.now();
434
+ return null;
435
+ }
436
+ }
367
437
  /**
368
438
  * Store an entry via AgentDB v1.
369
439
  * Phase 2-5: Routes through MutationGuard → TieredCache → DB → AttestationLog.
@@ -414,6 +484,17 @@ export async function bridgeStoreEntry(options) {
414
484
  // Embedding failed — store without
415
485
  }
416
486
  }
487
+ // If upserting, check whether an existing row will be replaced. INSERT OR REPLACE
488
+ // always generates a new id, so the old id becomes a ghost in the HNSW graph.
489
+ // Track that here so getOrBuildHnswIndex can trigger a lazy rebuild.
490
+ if (options.upsert) {
491
+ try {
492
+ const existing = ctx.db.prepare(`SELECT id FROM memory_entries WHERE key = ? AND namespace = ? AND status = 'active' LIMIT 1`).get(key, namespace);
493
+ if (existing)
494
+ _hnswDirtyCount++;
495
+ }
496
+ catch { /* Non-fatal */ }
497
+ }
417
498
  // better-sqlite3 uses synchronous .run() with positional params
418
499
  const insertSql = options.upsert
419
500
  ? `INSERT OR REPLACE INTO memory_entries (
@@ -432,6 +513,17 @@ export async function bridgeStoreEntry(options) {
432
513
  // Phase 2: Write-through to TieredCache deferred — full entry populated on first read
433
514
  // Phase 4: AttestationLog write audit
434
515
  await logAttestation(registry, 'store', id, { key, namespace, hasEmbedding: !!embeddingJson });
516
+ // Update in-process HNSW index if built and embedding matches bridge dimensions.
517
+ // Skip if ID already indexed: HNSWIndex has no clean updatePoint and re-inserting
518
+ // an existing ID corrupts neighbor connections without removing the old ones.
519
+ if (_hnswIndex && embeddingJson && dimensions === BRIDGE_EMBEDDING_DIMS) {
520
+ try {
521
+ const emb = safeParseEmbedding(embeddingJson);
522
+ if (emb && !_hnswIndex.has(id))
523
+ void _hnswIndex.addPoint(id, new Float32Array(emb));
524
+ }
525
+ catch { /* Non-fatal */ }
526
+ }
435
527
  const storeResult = {
436
528
  success: true,
437
529
  id,
@@ -526,10 +618,10 @@ export async function bridgeSearchEntries(options) {
526
618
  const stmt = ctx.db.prepare(`
527
619
  SELECT id, key, namespace, content, embedding
528
620
  FROM memory_entries
529
- WHERE status = 'active' ${nsFilter}
530
- LIMIT 1000
621
+ WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) ${nsFilter}
622
+ LIMIT 5000
531
623
  `);
532
- rows = effectiveNamespace !== 'all' ? stmt.all(effectiveNamespace) : stmt.all();
624
+ rows = effectiveNamespace !== 'all' ? stmt.all(Date.now(), effectiveNamespace) : stmt.all(Date.now());
533
625
  }
534
626
  catch {
535
627
  return null;
@@ -613,8 +705,8 @@ export async function bridgeListEntries(options) {
613
705
  // Count
614
706
  let total = 0;
615
707
  try {
616
- const countStmt = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' ${nsFilter}`);
617
- const countRow = countStmt.get(...nsParams);
708
+ const countStmt = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) ${nsFilter}`);
709
+ const countRow = countStmt.get(Date.now(), ...nsParams);
618
710
  total = countRow?.cnt ?? 0;
619
711
  }
620
712
  catch {
@@ -626,11 +718,11 @@ export async function bridgeListEntries(options) {
626
718
  const stmt = ctx.db.prepare(`
627
719
  SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
628
720
  FROM memory_entries
629
- WHERE status = 'active' ${nsFilter}
721
+ WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) ${nsFilter}
630
722
  ORDER BY updated_at DESC
631
723
  LIMIT ? OFFSET ?
632
724
  `);
633
- const rows = stmt.all(...nsParams, limit, offset);
725
+ const rows = stmt.all(Date.now(), ...nsParams, limit, offset);
634
726
  for (const row of rows) {
635
727
  entries.push({
636
728
  id: String(row.id),
@@ -670,7 +762,8 @@ export async function bridgeGetEntry(options) {
670
762
  const safeNs = String(namespace).replace(/:/g, '_');
671
763
  const safeKey = String(key).replace(/:/g, '_');
672
764
  const cacheKey = `entry:${safeNs}:${safeKey}`;
673
- const cached = await cacheGet(registry, cacheKey);
765
+ // Bypass cache when agentId is set so agent_reads tracking and collaborative promotion always fire.
766
+ const cached = options.agentId ? null : await cacheGet(registry, cacheKey);
674
767
  if (cached && cached.content) {
675
768
  return {
676
769
  success: true,
@@ -684,7 +777,7 @@ export async function bridgeGetEntry(options) {
684
777
  accessCount: cached.accessCount ?? 0,
685
778
  createdAt: cached.createdAt || new Date().toISOString(),
686
779
  updatedAt: cached.updatedAt || new Date().toISOString(),
687
- hasEmbedding: !!cached.embedding,
780
+ hasEmbedding: cached.hasEmbedding ?? false,
688
781
  tags: cached.tags || [],
689
782
  },
690
783
  };
@@ -692,12 +785,12 @@ export async function bridgeGetEntry(options) {
692
785
  let row;
693
786
  try {
694
787
  const stmt = ctx.db.prepare(`
695
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
788
+ SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags, expires_at
696
789
  FROM memory_entries
697
- WHERE status = 'active' AND key = ? AND namespace = ?
790
+ WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) AND key = ? AND namespace = ?
698
791
  LIMIT 1
699
792
  `);
700
- row = stmt.get(key, namespace);
793
+ row = stmt.get(Date.now(), key, namespace);
701
794
  }
702
795
  catch {
703
796
  return null;
@@ -738,6 +831,11 @@ export async function bridgeGetEntry(options) {
738
831
  if (distinctAgents >= 3 && spread >= SPREAD_MS) {
739
832
  ctx.db.prepare("UPDATE memory_entries SET access_level = 'team' WHERE id = ? AND access_level = 'private'").run(row.id);
740
833
  }
834
+ // Probabilistic prune: delete agent_reads rows older than the 24h window (~10% of reads).
835
+ // Keeps the table bounded without per-read overhead.
836
+ if (Math.random() < 0.1) {
837
+ ctx.db.prepare('DELETE FROM agent_reads WHERE read_at < ?').run(cutoff);
838
+ }
741
839
  }
742
840
  catch { /* non-critical */ }
743
841
  }
@@ -766,8 +864,10 @@ export async function bridgeGetEntry(options) {
766
864
  hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
767
865
  tags,
768
866
  };
769
- // Phase 2: Populate cache for next read
770
- await cacheSet(registry, cacheKey, entry);
867
+ // Phase 2: Populate cache for next read (skip TTL'd entries — cache has no expiry awareness)
868
+ if (!row.expires_at) {
869
+ await cacheSet(registry, cacheKey, entry);
870
+ }
771
871
  return { success: true, found: true, cacheHit: false, entry };
772
872
  }
773
873
  catch {
@@ -792,6 +892,13 @@ export async function bridgeDeleteEntry(options) {
792
892
  if (!guardResult.allowed) {
793
893
  return { success: false, deleted: false, key, namespace, remainingEntries: 0, error: `MutationGuard rejected: ${guardResult.reason}` };
794
894
  }
895
+ // Fetch id before soft-delete so we can clean up agent_reads for this entry.
896
+ let deletedEntryId;
897
+ try {
898
+ const existing = ctx.db.prepare(`SELECT id FROM memory_entries WHERE key = ? AND namespace = ? AND status = 'active' LIMIT 1`).get(key, namespace);
899
+ deletedEntryId = existing?.id;
900
+ }
901
+ catch { /* non-fatal */ }
795
902
  // Soft delete using parameterized query
796
903
  let changes = 0;
797
904
  try {
@@ -811,7 +918,17 @@ export async function bridgeDeleteEntry(options) {
811
918
  await cacheInvalidate(registry, `entry:${safeNs}:${safeKey}`);
812
919
  // Phase 4: AttestationLog delete audit
813
920
  if (changes > 0) {
814
- await logAttestation(registry, 'delete', key, { namespace });
921
+ await logAttestation(registry, 'delete', deletedEntryId ?? key, { namespace, key });
922
+ // Soft-deleted entry's id stays in the HNSW graph but SQL filters it out.
923
+ // Track so the next search can trigger a lazy rebuild to clear ghost slots.
924
+ _hnswDirtyCount++;
925
+ // Clean up orphaned agent_reads rows for the deleted entry.
926
+ if (deletedEntryId) {
927
+ try {
928
+ ctx.db.prepare('DELETE FROM agent_reads WHERE entry_id = ?').run(deletedEntryId);
929
+ }
930
+ catch { /* non-fatal */ }
931
+ }
815
932
  }
816
933
  let remaining = 0;
817
934
  try {
@@ -903,10 +1020,10 @@ export async function bridgeGetHNSWStatus(dbPath) {
903
1020
  const ctx = getDb(registry);
904
1021
  if (!ctx)
905
1022
  return null;
906
- // Count entries with embeddings
1023
+ // Count entries with embeddings (exclude TTL-expired to match actual index contents)
907
1024
  let entryCount = 0;
908
1025
  try {
909
- const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND embedding IS NOT NULL`).get();
1026
+ const row = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) AND embedding IS NOT NULL`).get(Date.now());
910
1027
  entryCount = row?.cnt ?? 0;
911
1028
  }
912
1029
  catch {
@@ -914,7 +1031,7 @@ export async function bridgeGetHNSWStatus(dbPath) {
914
1031
  }
915
1032
  return {
916
1033
  available: true,
917
- initialized: true,
1034
+ initialized: _hnswIndexBuilt,
918
1035
  entryCount,
919
1036
  dimensions: BRIDGE_EMBEDDING_DIMS,
920
1037
  };
@@ -938,49 +1055,83 @@ export async function bridgeSearchHNSW(queryEmbedding, options, dbPath) {
938
1055
  try {
939
1056
  const k = options?.k ?? 10;
940
1057
  const threshold = options?.threshold ?? 0.3;
941
- const nsFilter = options?.namespace && options.namespace !== 'all'
942
- ? `AND namespace = ?`
943
- : '';
1058
+ const index = await getOrBuildHnswIndex(ctx.db);
1059
+ if (!index) {
1060
+ // HNSW unavailable — brute-force fallback (capped at 10000 rows)
1061
+ const nsFilter = options?.namespace && options.namespace !== 'all' ? `AND namespace = ?` : '';
1062
+ let rows;
1063
+ try {
1064
+ const stmt = ctx.db.prepare(`
1065
+ SELECT id, key, namespace, content, embedding
1066
+ FROM memory_entries
1067
+ WHERE status = 'active' AND (expires_at IS NULL OR expires_at > ?) AND embedding IS NOT NULL ${nsFilter}
1068
+ LIMIT 10000
1069
+ `);
1070
+ rows = nsFilter ? stmt.all(Date.now(), options.namespace) : stmt.all(Date.now());
1071
+ }
1072
+ catch {
1073
+ return null;
1074
+ }
1075
+ const fallback = [];
1076
+ for (const row of rows) {
1077
+ if (!row.embedding)
1078
+ continue;
1079
+ try {
1080
+ const emb = safeParseEmbedding(row.embedding);
1081
+ if (!emb || emb.length !== queryEmbedding.length)
1082
+ continue;
1083
+ const score = cosineSim(queryEmbedding, emb);
1084
+ if (score >= threshold) {
1085
+ fallback.push({
1086
+ id: String(row.id),
1087
+ key: row.key || String(row.id).substring(0, 15),
1088
+ content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''),
1089
+ score,
1090
+ namespace: row.namespace || 'default',
1091
+ });
1092
+ }
1093
+ }
1094
+ catch { /* skip invalid */ }
1095
+ }
1096
+ fallback.sort((a, b) => b.score - a.score);
1097
+ return fallback.slice(0, k);
1098
+ }
1099
+ // Real HNSW search: distance is cosine distance, similarity = 1 - distance.
1100
+ // When a namespace filter is active, the global top-k*2 candidates may contain
1101
+ // few in-namespace entries. Over-fetch by 50x so the SQL filter has enough
1102
+ // candidates to fill k results from the target namespace.
1103
+ const nsFilter = options?.namespace && options.namespace !== 'all';
1104
+ const candidateCount = nsFilter ? Math.min(10000, k * 50) : k * 2;
1105
+ const hnswResults = await index.search(new Float32Array(queryEmbedding), candidateCount);
1106
+ if (!hnswResults.length)
1107
+ return [];
1108
+ const placeholders = hnswResults.map(() => '?').join(',');
1109
+ const nsWhere = nsFilter ? `AND namespace = ?` : '';
1110
+ const nsParam = nsFilter ? [options.namespace] : [];
944
1111
  let rows;
945
1112
  try {
946
- const stmt = ctx.db.prepare(`
947
- SELECT id, key, namespace, content, embedding
948
- FROM memory_entries
949
- WHERE status = 'active' AND embedding IS NOT NULL ${nsFilter}
950
- LIMIT 10000
951
- `);
952
- rows = nsFilter
953
- ? stmt.all(options.namespace)
954
- : stmt.all();
1113
+ rows = ctx.db.prepare(`SELECT id, key, namespace, content FROM memory_entries WHERE id IN (${placeholders}) AND status = 'active' AND (expires_at IS NULL OR expires_at > ?) ${nsWhere}`).all(...hnswResults.map((r) => r.id), Date.now(), ...nsParam);
955
1114
  }
956
1115
  catch {
957
1116
  return null;
958
1117
  }
1118
+ const rowMap = new Map(rows.map((r) => [r.id, r]));
959
1119
  const results = [];
960
- for (const row of rows) {
961
- if (!row.embedding)
1120
+ for (const { id, distance } of hnswResults) {
1121
+ const row = rowMap.get(id);
1122
+ if (!row)
962
1123
  continue;
963
- try {
964
- const emb = safeParseEmbedding(row.embedding);
965
- if (!emb || emb.length !== queryEmbedding.length)
966
- continue;
967
- const score = cosineSim(queryEmbedding, emb);
968
- if (score >= threshold) {
969
- results.push({
970
- id: String(row.id),
971
- key: row.key || String(row.id).substring(0, 15),
972
- content: (row.content || '').substring(0, 60) +
973
- ((row.content || '').length > 60 ? '...' : ''),
974
- score,
975
- namespace: row.namespace || 'default',
976
- });
977
- }
978
- }
979
- catch {
980
- // Skip invalid embeddings
981
- }
1124
+ const score = 1 - distance;
1125
+ if (score < threshold)
1126
+ continue;
1127
+ results.push({
1128
+ id: row.id,
1129
+ key: row.key || String(id).substring(0, 15),
1130
+ content: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''),
1131
+ score,
1132
+ namespace: row.namespace || 'default',
1133
+ });
982
1134
  }
983
- results.sort((a, b) => b.score - a.score);
984
1135
  return results.slice(0, k);
985
1136
  }
986
1137
  catch {
@@ -1008,6 +1159,17 @@ export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
1008
1159
  }
1009
1160
  const now = Date.now();
1010
1161
  const embeddingJson = JSON.stringify(embedding);
1162
+ // Ghost-displacement check: INSERT OR REPLACE deletes any existing row with the same
1163
+ // (namespace, key) but a different id, leaving that old id as a HNSW ghost without
1164
+ // being tracked by _hnswDirtyCount.
1165
+ if (_hnswIndex) {
1166
+ try {
1167
+ const existing = ctx.db.prepare(`SELECT id FROM memory_entries WHERE namespace = ? AND key = ? LIMIT 1`).get(entry.namespace, entry.key);
1168
+ if (existing && existing.id !== id)
1169
+ _hnswDirtyCount++;
1170
+ }
1171
+ catch { /* Non-fatal */ }
1172
+ }
1011
1173
  ctx.db.prepare(`
1012
1174
  INSERT OR REPLACE INTO memory_entries (
1013
1175
  id, key, namespace, content, type,
@@ -1015,6 +1177,17 @@ export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
1015
1177
  created_at, updated_at, status
1016
1178
  ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, 'active')
1017
1179
  `).run(id, entry.key, entry.namespace, entry.content, embeddingJson, embedding.length, BRIDGE_EMBEDDING_MODEL, now, now);
1180
+ // Update in-process HNSW index if built and dimensions match.
1181
+ // Skip re-insert for existing IDs (no clean updatePoint API on HNSWIndex).
1182
+ if (_hnswIndex && embedding.length === BRIDGE_EMBEDDING_DIMS) {
1183
+ try {
1184
+ if (!_hnswIndex.has(id))
1185
+ void _hnswIndex.addPoint(id, new Float32Array(embedding));
1186
+ else
1187
+ _hnswDirtyCount++;
1188
+ }
1189
+ catch { /* Non-fatal */ }
1190
+ }
1018
1191
  return true;
1019
1192
  }
1020
1193
  catch {
@@ -1463,7 +1636,7 @@ export async function bridgeHealthCheck(dbPath) {
1463
1636
  const s = cache.stats();
1464
1637
  cacheStats = { size: s.size ?? 0, hits: s.hits ?? 0, misses: s.misses ?? 0 };
1465
1638
  }
1466
- return { available: true, controllers, attestationCount, cacheStats, sonaErrorCount: _sonaErrorCount };
1639
+ return { available: true, controllers, attestationCount, cacheStats };
1467
1640
  }
1468
1641
  catch {
1469
1642
  return null;
@@ -1613,9 +1786,11 @@ export async function bridgeBatchOperation(params) {
1613
1786
  break;
1614
1787
  }
1615
1788
  case 'update': {
1616
- // bulkUpdate(table, updates, conditions)
1789
+ // bulkUpdate(table, updates, conditions) — cap content at 1 MB for parity with insert
1790
+ const MAX_CONTENT_BYTES = 1024 * 1024;
1617
1791
  for (const entry of params.entries) {
1618
- await batch.bulkUpdate('episodes', { content: entry.value || entry.content }, { key: entry.key });
1792
+ const content = String(entry.value || entry.content || '').slice(0, MAX_CONTENT_BYTES);
1793
+ await batch.bulkUpdate('episodes', { content }, { key: entry.key });
1619
1794
  }
1620
1795
  result = { updated: params.entries.length };
1621
1796
  break;