hippo-memory 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/bin/hippo.js +2 -2
  2. package/dist/api.d.ts +28 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +31 -7
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +107 -12
  8. package/dist/cli.js.map +1 -1
  9. package/dist/connectors/github/backfill.js +4 -4
  10. package/dist/connectors/github/cli-impl.js +6 -6
  11. package/dist/connectors/github/dlq.js +14 -14
  12. package/dist/connectors/slack/backfill.js +1 -1
  13. package/dist/connectors/slack/dlq.js +10 -10
  14. package/dist/connectors/slack/workspaces.js +4 -4
  15. package/dist/dag.js +6 -6
  16. package/dist/dashboard.js +7 -7
  17. package/dist/goals.d.ts +11 -0
  18. package/dist/goals.d.ts.map +1 -1
  19. package/dist/goals.js +61 -49
  20. package/dist/goals.js.map +1 -1
  21. package/dist/graph-extract.d.ts +28 -12
  22. package/dist/graph-extract.d.ts.map +1 -1
  23. package/dist/graph-extract.js +138 -20
  24. package/dist/graph-extract.js.map +1 -1
  25. package/dist/hooks.js +24 -24
  26. package/dist/physics-state.js +27 -27
  27. package/dist/predictions.js +67 -67
  28. package/dist/refine-llm.js +13 -13
  29. package/dist/search.d.ts +33 -0
  30. package/dist/search.d.ts.map +1 -1
  31. package/dist/search.js.map +1 -1
  32. package/dist/server.d.ts.map +1 -1
  33. package/dist/server.js +7 -0
  34. package/dist/server.js.map +1 -1
  35. package/dist/src/api.js +31 -7
  36. package/dist/src/api.js.map +1 -1
  37. package/dist/src/cli.js +107 -12
  38. package/dist/src/cli.js.map +1 -1
  39. package/dist/src/connectors/github/backfill.js +4 -4
  40. package/dist/src/connectors/github/cli-impl.js +6 -6
  41. package/dist/src/connectors/github/dlq.js +14 -14
  42. package/dist/src/connectors/slack/backfill.js +1 -1
  43. package/dist/src/connectors/slack/dlq.js +10 -10
  44. package/dist/src/connectors/slack/workspaces.js +4 -4
  45. package/dist/src/dag.js +6 -6
  46. package/dist/src/dashboard.js +7 -7
  47. package/dist/src/goals.js +61 -49
  48. package/dist/src/goals.js.map +1 -1
  49. package/dist/src/graph-extract.js +138 -20
  50. package/dist/src/graph-extract.js.map +1 -1
  51. package/dist/src/hooks.js +24 -24
  52. package/dist/src/physics-state.js +27 -27
  53. package/dist/src/predictions.js +67 -67
  54. package/dist/src/refine-llm.js +13 -13
  55. package/dist/src/search.js.map +1 -1
  56. package/dist/src/server.js +7 -0
  57. package/dist/src/server.js.map +1 -1
  58. package/dist/src/store.js +260 -260
  59. package/dist/src/version.js +1 -1
  60. package/dist/src/working-memory.js +19 -19
  61. package/dist/store.js +260 -260
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/working-memory.js +19 -19
  65. package/dist-ui/index.html +12 -12
  66. package/extensions/openclaw-plugin/index.ts +650 -650
  67. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  68. package/extensions/openclaw-plugin/package.json +1 -1
  69. package/openclaw.plugin.json +1 -1
  70. package/package.json +1 -1
  71. package/dist/benchmarks/e1.3/scenarios.json +0 -2587
package/dist/src/store.js CHANGED
@@ -535,13 +535,13 @@ function loadSearchRows(db, query, limit, tenantId, scopeFilter) {
535
535
  // F1 (v1.7.0): MEMORY_SEARCH_COLUMNS adds bm25_score as the trailing
536
536
  // result column. Every other column is m.<col> AS <col> so rowToEntry
537
537
  // sees the same shape it always has.
538
- const rows = db.prepare(`
539
- SELECT ${MEMORY_SEARCH_COLUMNS}
540
- FROM memories m
541
- JOIN memories_fts f ON f.id = m.id
542
- WHERE memories_fts MATCH ?${tenantPredicate}${archivedClauseAlias}${scopeClauseAlias}
543
- ORDER BY bm25(memories_fts), m.updated_at DESC
544
- LIMIT ?
538
+ const rows = db.prepare(`
539
+ SELECT ${MEMORY_SEARCH_COLUMNS}
540
+ FROM memories m
541
+ JOIN memories_fts f ON f.id = m.id
542
+ WHERE memories_fts MATCH ?${tenantPredicate}${archivedClauseAlias}${scopeClauseAlias}
543
+ ORDER BY bm25(memories_fts), m.updated_at DESC
544
+ LIMIT ?
545
545
  `).all(ftsQuery, ...tenantParams, ...scopeParams, limit);
546
546
  if (rows.length > 0)
547
547
  return rows;
@@ -556,12 +556,12 @@ function loadSearchRows(db, query, limit, tenantId, scopeFilter) {
556
556
  const like = `%${escapeLike(term)}%`;
557
557
  return [like, like];
558
558
  });
559
- const rows = db.prepare(`
560
- SELECT ${MEMORY_SELECT_COLUMNS}
561
- FROM memories
562
- WHERE (${where})${tenantPredicateNoAlias}${archivedClauseNoAlias}${scopeClauseNoAlias}
563
- ORDER BY updated_at DESC, created DESC
564
- LIMIT ?
559
+ const rows = db.prepare(`
560
+ SELECT ${MEMORY_SELECT_COLUMNS}
561
+ FROM memories
562
+ WHERE (${where})${tenantPredicateNoAlias}${archivedClauseNoAlias}${scopeClauseNoAlias}
563
+ ORDER BY updated_at DESC, created DESC
564
+ LIMIT ?
565
565
  `).all(...params, ...tenantParams, ...scopeParams, limit);
566
566
  if (rows.length > 0)
567
567
  return rows;
@@ -674,60 +674,60 @@ function loadLegacyStatsFile(hippoRoot) {
674
674
  }
675
675
  }
676
676
  function upsertEntryRow(db, entry) {
677
- db.prepare(`
678
- INSERT INTO memories(
679
- id, created, last_retrieved, retrieval_count, strength, half_life_days, layer,
680
- tags_json, emotional_valence, schema_fit, source, outcome_score,
681
- outcome_positive, outcome_negative,
682
- conflicts_with_json, pinned, confidence, content,
683
- parents_json, starred,
684
- trace_outcome, source_session_id,
685
- valid_from, superseded_by,
686
- extracted_from,
687
- dag_level, dag_parent_id,
688
- kind, scope, owner, artifact_ref,
689
- tenant_id,
690
- descendant_count, earliest_at, latest_at,
691
- dag_level_3_built_at,
692
- updated_at
693
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
694
- ON CONFLICT(id) DO UPDATE SET
695
- created = excluded.created,
696
- last_retrieved = excluded.last_retrieved,
697
- retrieval_count = excluded.retrieval_count,
698
- strength = excluded.strength,
699
- half_life_days = excluded.half_life_days,
700
- layer = excluded.layer,
701
- tags_json = excluded.tags_json,
702
- emotional_valence = excluded.emotional_valence,
703
- schema_fit = excluded.schema_fit,
704
- source = excluded.source,
705
- outcome_score = excluded.outcome_score,
706
- outcome_positive = excluded.outcome_positive,
707
- outcome_negative = excluded.outcome_negative,
708
- conflicts_with_json = excluded.conflicts_with_json,
709
- pinned = excluded.pinned,
710
- confidence = excluded.confidence,
711
- content = excluded.content,
712
- parents_json = excluded.parents_json,
713
- starred = excluded.starred,
714
- trace_outcome = excluded.trace_outcome,
715
- source_session_id = excluded.source_session_id,
716
- valid_from = excluded.valid_from,
717
- superseded_by = excluded.superseded_by,
718
- extracted_from = excluded.extracted_from,
719
- dag_level = excluded.dag_level,
720
- dag_parent_id = excluded.dag_parent_id,
721
- kind = excluded.kind,
722
- scope = excluded.scope,
723
- owner = excluded.owner,
724
- artifact_ref = excluded.artifact_ref,
725
- tenant_id = excluded.tenant_id,
726
- descendant_count = excluded.descendant_count,
727
- earliest_at = excluded.earliest_at,
728
- latest_at = excluded.latest_at,
729
- dag_level_3_built_at = excluded.dag_level_3_built_at,
730
- updated_at = datetime('now')
677
+ db.prepare(`
678
+ INSERT INTO memories(
679
+ id, created, last_retrieved, retrieval_count, strength, half_life_days, layer,
680
+ tags_json, emotional_valence, schema_fit, source, outcome_score,
681
+ outcome_positive, outcome_negative,
682
+ conflicts_with_json, pinned, confidence, content,
683
+ parents_json, starred,
684
+ trace_outcome, source_session_id,
685
+ valid_from, superseded_by,
686
+ extracted_from,
687
+ dag_level, dag_parent_id,
688
+ kind, scope, owner, artifact_ref,
689
+ tenant_id,
690
+ descendant_count, earliest_at, latest_at,
691
+ dag_level_3_built_at,
692
+ updated_at
693
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
694
+ ON CONFLICT(id) DO UPDATE SET
695
+ created = excluded.created,
696
+ last_retrieved = excluded.last_retrieved,
697
+ retrieval_count = excluded.retrieval_count,
698
+ strength = excluded.strength,
699
+ half_life_days = excluded.half_life_days,
700
+ layer = excluded.layer,
701
+ tags_json = excluded.tags_json,
702
+ emotional_valence = excluded.emotional_valence,
703
+ schema_fit = excluded.schema_fit,
704
+ source = excluded.source,
705
+ outcome_score = excluded.outcome_score,
706
+ outcome_positive = excluded.outcome_positive,
707
+ outcome_negative = excluded.outcome_negative,
708
+ conflicts_with_json = excluded.conflicts_with_json,
709
+ pinned = excluded.pinned,
710
+ confidence = excluded.confidence,
711
+ content = excluded.content,
712
+ parents_json = excluded.parents_json,
713
+ starred = excluded.starred,
714
+ trace_outcome = excluded.trace_outcome,
715
+ source_session_id = excluded.source_session_id,
716
+ valid_from = excluded.valid_from,
717
+ superseded_by = excluded.superseded_by,
718
+ extracted_from = excluded.extracted_from,
719
+ dag_level = excluded.dag_level,
720
+ dag_parent_id = excluded.dag_parent_id,
721
+ kind = excluded.kind,
722
+ scope = excluded.scope,
723
+ owner = excluded.owner,
724
+ artifact_ref = excluded.artifact_ref,
725
+ tenant_id = excluded.tenant_id,
726
+ descendant_count = excluded.descendant_count,
727
+ earliest_at = excluded.earliest_at,
728
+ latest_at = excluded.latest_at,
729
+ dag_level_3_built_at = excluded.dag_level_3_built_at,
730
+ updated_at = datetime('now')
731
731
  `).run(entry.id, entry.created, entry.last_retrieved, entry.retrieval_count, entry.strength, entry.half_life_days, entry.layer, JSON.stringify(entry.tags ?? []), entry.emotional_valence, entry.schema_fit, entry.source, entry.outcome_score, entry.outcome_positive ?? 0, entry.outcome_negative ?? 0, JSON.stringify(entry.conflicts_with ?? []), entry.pinned ? 1 : 0, entry.confidence, entry.content, JSON.stringify(entry.parents ?? []), entry.starred ? 1 : 0, entry.trace_outcome ?? null, entry.source_session_id ?? null, entry.valid_from ?? entry.created, entry.superseded_by ?? null, entry.extracted_from ?? null, entry.dag_level ?? 0, entry.dag_parent_id ?? null, entry.kind ?? 'distilled', entry.scope ?? null, entry.owner ?? null, entry.artifact_ref ?? null, entry.tenantId ?? 'default', entry.descendant_count ?? 0, entry.earliest_at ?? null, entry.latest_at ?? null, entry.dag_level_3_built_at ?? null);
732
732
  syncFtsRow(db, entry);
733
733
  }
@@ -794,11 +794,11 @@ function syncMirrorFiles(hippoRoot, db) {
794
794
  for (const entry of entries.map(rowToEntry)) {
795
795
  writeMarkdownMirror(hippoRoot, entry);
796
796
  }
797
- const conflicts = db.prepare(`
798
- SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
799
- FROM memory_conflicts
800
- WHERE status = 'open'
801
- ORDER BY updated_at DESC, id DESC
797
+ const conflicts = db.prepare(`
798
+ SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
799
+ FROM memory_conflicts
800
+ WHERE status = 'open'
801
+ ORDER BY updated_at DESC, id DESC
802
802
  `).all();
803
803
  writeConflictMirrors(hippoRoot, conflicts.map(rowToMemoryConflict));
804
804
  writeIndexMirror(hippoRoot, buildIndexFromDb(db));
@@ -1427,16 +1427,16 @@ export function saveActiveTaskSnapshot(hippoRoot, tenantId, snapshot) {
1427
1427
  try {
1428
1428
  db.exec('BEGIN');
1429
1429
  db.prepare(`UPDATE task_snapshots SET status = 'superseded', updated_at = ? WHERE status = 'active' AND tenant_id = ?`).run(now, tenantId);
1430
- const result = db.prepare(`
1431
- INSERT INTO task_snapshots(task, summary, next_step, status, source, session_id, scope, tenant_id, created_at, updated_at)
1432
- VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
1430
+ const result = db.prepare(`
1431
+ INSERT INTO task_snapshots(task, summary, next_step, status, source, session_id, scope, tenant_id, created_at, updated_at)
1432
+ VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
1433
1433
  `).run(snapshot.task, snapshot.summary, snapshot.next_step, snapshot.source ?? 'cli', snapshot.session_id ?? null, snapshot.scope ?? null, tenantId, now, now);
1434
1434
  db.exec('COMMIT');
1435
1435
  const id = Number(result.lastInsertRowid ?? 0);
1436
- const row = db.prepare(`
1437
- SELECT id, task, summary, next_step, status, source, session_id, scope, created_at, updated_at
1438
- FROM task_snapshots
1439
- WHERE id = ?
1436
+ const row = db.prepare(`
1437
+ SELECT id, task, summary, next_step, status, source, session_id, scope, created_at, updated_at
1438
+ FROM task_snapshots
1439
+ WHERE id = ?
1440
1440
  `).get(id);
1441
1441
  if (!row) {
1442
1442
  throw new Error('Failed to reload saved active task snapshot');
@@ -1463,12 +1463,12 @@ export function loadActiveTaskSnapshot(hippoRoot, tenantId) {
1463
1463
  initStore(hippoRoot);
1464
1464
  const db = openHippoDb(hippoRoot);
1465
1465
  try {
1466
- const row = db.prepare(`
1467
- SELECT id, task, summary, next_step, status, source, session_id, scope, created_at, updated_at
1468
- FROM task_snapshots
1469
- WHERE status = 'active' AND tenant_id = ?
1470
- ORDER BY updated_at DESC, id DESC
1471
- LIMIT 1
1466
+ const row = db.prepare(`
1467
+ SELECT id, task, summary, next_step, status, source, session_id, scope, created_at, updated_at
1468
+ FROM task_snapshots
1469
+ WHERE status = 'active' AND tenant_id = ?
1470
+ ORDER BY updated_at DESC, id DESC
1471
+ LIMIT 1
1472
1472
  `).get(tenantId);
1473
1473
  if (!row) {
1474
1474
  removeActiveTaskMirror(hippoRoot, tenantId);
@@ -1509,26 +1509,26 @@ export function appendSessionEvent(hippoRoot, tenantId, event) {
1509
1509
  // v1.2: scope is wired through. Default-deny in api.recall + cmdRecall
1510
1510
  // continuity reads applies to slack:private:* and 'unknown:legacy' rows.
1511
1511
  try {
1512
- const result = db.prepare(`
1513
- INSERT INTO session_events(session_id, task, event_type, content, source, scope, metadata_json, tenant_id, created_at)
1514
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1512
+ const result = db.prepare(`
1513
+ INSERT INTO session_events(session_id, task, event_type, content, source, scope, metadata_json, tenant_id, created_at)
1514
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1515
1515
  `).run(event.session_id, event.task ?? null, event.event_type, event.content, event.source ?? 'cli', event.scope ?? null, JSON.stringify(event.metadata ?? {}), tenantId, now);
1516
1516
  const id = Number(result.lastInsertRowid ?? 0);
1517
- const row = db.prepare(`
1518
- SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
1519
- FROM session_events
1520
- WHERE id = ?
1517
+ const row = db.prepare(`
1518
+ SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
1519
+ FROM session_events
1520
+ WHERE id = ?
1521
1521
  `).get(id);
1522
1522
  if (!row) {
1523
1523
  throw new Error('Failed to reload saved session event');
1524
1524
  }
1525
1525
  const loaded = rowToSessionEvent(row);
1526
- const recentRows = db.prepare(`
1527
- SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
1528
- FROM session_events
1529
- WHERE session_id = ? AND tenant_id = ?
1530
- ORDER BY created_at DESC, id DESC
1531
- LIMIT ?
1526
+ const recentRows = db.prepare(`
1527
+ SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
1528
+ FROM session_events
1529
+ WHERE session_id = ? AND tenant_id = ?
1530
+ ORDER BY created_at DESC, id DESC
1531
+ LIMIT ?
1532
1532
  `).all(loaded.session_id, tenantId, 20);
1533
1533
  const recent = recentRows.map(rowToSessionEvent).reverse();
1534
1534
  writeRecentSessionMirror(hippoRoot, tenantId, recent);
@@ -1556,12 +1556,12 @@ export function listSessionEvents(hippoRoot, tenantId, options = {}) {
1556
1556
  const limit = Math.max(1, Math.trunc(options.limit ?? 8));
1557
1557
  params.push(limit);
1558
1558
  const where = `WHERE ${clauses.join(' AND ')}`;
1559
- const rows = db.prepare(`
1560
- SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
1561
- FROM session_events
1562
- ${where}
1563
- ORDER BY created_at DESC, id DESC
1564
- LIMIT ?
1559
+ const rows = db.prepare(`
1560
+ SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
1561
+ FROM session_events
1562
+ ${where}
1563
+ ORDER BY created_at DESC, id DESC
1564
+ LIMIT ?
1565
1565
  `).all(...params);
1566
1566
  return rows.map(rowToSessionEvent).reverse();
1567
1567
  }
@@ -1578,9 +1578,9 @@ export function findPromotableSessions(hippoRoot, tenantId, sinceMs) {
1578
1578
  initStore(hippoRoot);
1579
1579
  const db = openHippoDb(hippoRoot);
1580
1580
  try {
1581
- const rows = db.prepare(`
1582
- SELECT DISTINCT session_id FROM session_events
1583
- WHERE event_type = 'session_complete' AND created_at >= ? AND tenant_id = ?
1581
+ const rows = db.prepare(`
1582
+ SELECT DISTINCT session_id FROM session_events
1583
+ WHERE event_type = 'session_complete' AND created_at >= ? AND tenant_id = ?
1584
1584
  `).all(new Date(sinceMs).toISOString(), tenantId);
1585
1585
  return rows;
1586
1586
  }
@@ -1597,10 +1597,10 @@ export function traceExistsForSession(hippoRoot, tenantId, session_id) {
1597
1597
  initStore(hippoRoot);
1598
1598
  const db = openHippoDb(hippoRoot);
1599
1599
  try {
1600
- const row = db.prepare(`
1601
- SELECT 1 FROM memories
1602
- WHERE source_session_id = ? AND layer = 'trace' AND tenant_id = ?
1603
- LIMIT 1
1600
+ const row = db.prepare(`
1601
+ SELECT 1 FROM memories
1602
+ WHERE source_session_id = ? AND layer = 'trace' AND tenant_id = ?
1603
+ LIMIT 1
1604
1604
  `).get(session_id, tenantId);
1605
1605
  return !!row;
1606
1606
  }
@@ -1623,38 +1623,38 @@ export function listMemoryConflicts(hippoRoot, status = 'open', tenantId) {
1623
1623
  // each in-tenant, so neither a normal cross-tenant pair nor a stale
1624
1624
  // pre-fix row surfaces (consistent with resolveConflict).
1625
1625
  rows = allStatuses
1626
- ? db.prepare(`
1627
- SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
1628
- mc.status, mc.detected_at, mc.updated_at
1629
- FROM memory_conflicts mc
1630
- JOIN memories ma ON ma.id = mc.memory_a_id
1631
- JOIN memories mb ON mb.id = mc.memory_b_id
1632
- WHERE ma.tenant_id = ? AND mb.tenant_id = ?
1633
- ORDER BY mc.updated_at DESC, mc.id DESC
1626
+ ? db.prepare(`
1627
+ SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
1628
+ mc.status, mc.detected_at, mc.updated_at
1629
+ FROM memory_conflicts mc
1630
+ JOIN memories ma ON ma.id = mc.memory_a_id
1631
+ JOIN memories mb ON mb.id = mc.memory_b_id
1632
+ WHERE ma.tenant_id = ? AND mb.tenant_id = ?
1633
+ ORDER BY mc.updated_at DESC, mc.id DESC
1634
1634
  `).all(tenantId, tenantId)
1635
- : db.prepare(`
1636
- SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
1637
- mc.status, mc.detected_at, mc.updated_at
1638
- FROM memory_conflicts mc
1639
- JOIN memories ma ON ma.id = mc.memory_a_id
1640
- JOIN memories mb ON mb.id = mc.memory_b_id
1641
- WHERE mc.status = ? AND ma.tenant_id = ? AND mb.tenant_id = ?
1642
- ORDER BY mc.updated_at DESC, mc.id DESC
1635
+ : db.prepare(`
1636
+ SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
1637
+ mc.status, mc.detected_at, mc.updated_at
1638
+ FROM memory_conflicts mc
1639
+ JOIN memories ma ON ma.id = mc.memory_a_id
1640
+ JOIN memories mb ON mb.id = mc.memory_b_id
1641
+ WHERE mc.status = ? AND ma.tenant_id = ? AND mb.tenant_id = ?
1642
+ ORDER BY mc.updated_at DESC, mc.id DESC
1643
1643
  `).all(status, tenantId, tenantId);
1644
1644
  }
1645
1645
  else {
1646
1646
  // Unscoped query — legacy direct-mode (CLI, tests, consolidate).
1647
1647
  rows = allStatuses
1648
- ? db.prepare(`
1649
- SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1650
- FROM memory_conflicts
1651
- ORDER BY updated_at DESC, id DESC
1648
+ ? db.prepare(`
1649
+ SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1650
+ FROM memory_conflicts
1651
+ ORDER BY updated_at DESC, id DESC
1652
1652
  `).all()
1653
- : db.prepare(`
1654
- SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1655
- FROM memory_conflicts
1656
- WHERE status = ?
1657
- ORDER BY updated_at DESC, id DESC
1653
+ : db.prepare(`
1654
+ SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1655
+ FROM memory_conflicts
1656
+ WHERE status = ?
1657
+ ORDER BY updated_at DESC, id DESC
1658
1658
  `).all(status);
1659
1659
  }
1660
1660
  return rows.map(rowToMemoryConflict);
@@ -1687,10 +1687,10 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
1687
1687
  score: conflict.score,
1688
1688
  }));
1689
1689
  const detectedKeys = new Set(canonicalDetected.map((conflict) => `${conflict.memory_a_id}::${conflict.memory_b_id}`));
1690
- const openRows = db.prepare(`
1691
- SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1692
- FROM memory_conflicts
1693
- WHERE status = 'open'
1690
+ const openRows = db.prepare(`
1691
+ SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1692
+ FROM memory_conflicts
1693
+ WHERE status = 'open'
1694
1694
  `).all();
1695
1695
  for (const row of openRows) {
1696
1696
  const key = `${row.memory_a_id}::${row.memory_b_id}`;
@@ -1709,20 +1709,20 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
1709
1709
  // Skip cross-tenant pairs — never persist a conflict spanning tenants.
1710
1710
  if (!sameTenant(conflict.memory_a_id, conflict.memory_b_id))
1711
1711
  continue;
1712
- db.prepare(`
1713
- INSERT INTO memory_conflicts(memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at)
1714
- VALUES (?, ?, ?, ?, 'open', ?, ?)
1715
- ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
1716
- reason = excluded.reason,
1717
- score = excluded.score,
1718
- status = 'open',
1719
- updated_at = excluded.updated_at
1712
+ db.prepare(`
1713
+ INSERT INTO memory_conflicts(memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at)
1714
+ VALUES (?, ?, ?, ?, 'open', ?, ?)
1715
+ ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
1716
+ reason = excluded.reason,
1717
+ score = excluded.score,
1718
+ status = 'open',
1719
+ updated_at = excluded.updated_at
1720
1720
  `).run(conflict.memory_a_id, conflict.memory_b_id, conflict.reason, conflict.score, detectedAt, detectedAt);
1721
1721
  }
1722
- const openConflicts = db.prepare(`
1723
- SELECT memory_a_id, memory_b_id
1724
- FROM memory_conflicts
1725
- WHERE status = 'open'
1722
+ const openConflicts = db.prepare(`
1723
+ SELECT memory_a_id, memory_b_id
1724
+ FROM memory_conflicts
1725
+ WHERE status = 'open'
1726
1726
  `).all();
1727
1727
  const refMap = new Map();
1728
1728
  for (const row of openConflicts) {
@@ -1776,17 +1776,17 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
1776
1776
  const memArgs = tenantId !== undefined ? [tenantId] : [];
1777
1777
  try {
1778
1778
  const row = (tenantId !== undefined
1779
- ? db.prepare(`
1780
- SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
1781
- mc.status, mc.detected_at, mc.updated_at
1782
- FROM memory_conflicts mc
1783
- JOIN memories ma ON ma.id = mc.memory_a_id
1784
- JOIN memories mb ON mb.id = mc.memory_b_id
1785
- WHERE mc.id = ? AND ma.tenant_id = ? AND mb.tenant_id = ?
1779
+ ? db.prepare(`
1780
+ SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
1781
+ mc.status, mc.detected_at, mc.updated_at
1782
+ FROM memory_conflicts mc
1783
+ JOIN memories ma ON ma.id = mc.memory_a_id
1784
+ JOIN memories mb ON mb.id = mc.memory_b_id
1785
+ WHERE mc.id = ? AND ma.tenant_id = ? AND mb.tenant_id = ?
1786
1786
  `).get(conflictId, tenantId, tenantId)
1787
- : db.prepare(`
1788
- SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1789
- FROM memory_conflicts WHERE id = ?
1787
+ : db.prepare(`
1788
+ SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1789
+ FROM memory_conflicts WHERE id = ?
1790
1790
  `).get(conflictId));
1791
1791
  if (!row)
1792
1792
  return null;
@@ -1856,15 +1856,15 @@ export function saveSessionHandoff(hippoRoot, tenantId, handoff) {
1856
1856
  // v1.2: scope is wired through. Read-side default-deny in api.recall +
1857
1857
  // cmdRecall continuity excludes slack:private:* and 'unknown:legacy'.
1858
1858
  try {
1859
- const result = db.prepare(`
1860
- INSERT INTO session_handoffs(session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, tenant_id, created_at)
1861
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1859
+ const result = db.prepare(`
1860
+ INSERT INTO session_handoffs(session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, tenant_id, created_at)
1861
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1862
1862
  `).run(handoff.sessionId, handoff.repoRoot ?? null, handoff.taskId ?? null, handoff.summary, handoff.nextAction ?? null, JSON.stringify(handoff.artifacts ?? []), handoff.scope ?? null, tenantId, now);
1863
1863
  const id = Number(result.lastInsertRowid ?? 0);
1864
- const row = db.prepare(`
1865
- SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1866
- FROM session_handoffs
1867
- WHERE id = ?
1864
+ const row = db.prepare(`
1865
+ SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1866
+ FROM session_handoffs
1867
+ WHERE id = ?
1868
1868
  `).get(id);
1869
1869
  if (!row) {
1870
1870
  throw new Error('Failed to reload saved session handoff');
@@ -1885,21 +1885,21 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId) {
1885
1885
  try {
1886
1886
  let row;
1887
1887
  if (sessionId) {
1888
- row = db.prepare(`
1889
- SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1890
- FROM session_handoffs
1891
- WHERE session_id = ? AND tenant_id = ?
1892
- ORDER BY created_at DESC, id DESC
1893
- LIMIT 1
1888
+ row = db.prepare(`
1889
+ SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1890
+ FROM session_handoffs
1891
+ WHERE session_id = ? AND tenant_id = ?
1892
+ ORDER BY created_at DESC, id DESC
1893
+ LIMIT 1
1894
1894
  `).get(sessionId, tenantId);
1895
1895
  }
1896
1896
  else {
1897
- row = db.prepare(`
1898
- SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1899
- FROM session_handoffs
1900
- WHERE tenant_id = ?
1901
- ORDER BY created_at DESC, id DESC
1902
- LIMIT 1
1897
+ row = db.prepare(`
1898
+ SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1899
+ FROM session_handoffs
1900
+ WHERE tenant_id = ?
1901
+ ORDER BY created_at DESC, id DESC
1902
+ LIMIT 1
1903
1903
  `).get(tenantId);
1904
1904
  }
1905
1905
  return row ? rowToSessionHandoff(row) : null;
@@ -1916,10 +1916,10 @@ export function loadHandoffById(hippoRoot, tenantId, id) {
1916
1916
  initStore(hippoRoot);
1917
1917
  const db = openHippoDb(hippoRoot);
1918
1918
  try {
1919
- const row = db.prepare(`
1920
- SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1921
- FROM session_handoffs
1922
- WHERE id = ? AND tenant_id = ?
1919
+ const row = db.prepare(`
1920
+ SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1921
+ FROM session_handoffs
1922
+ WHERE id = ? AND tenant_id = ?
1923
1923
  `).get(id, tenantId);
1924
1924
  return row ? rowToSessionHandoff(row) : null;
1925
1925
  }
@@ -1949,13 +1949,13 @@ export function loadDirtySummaries(hippoRoot, tenantId) {
1949
1949
  initStore(hippoRoot);
1950
1950
  const db = openHippoDb(hippoRoot);
1951
1951
  try {
1952
- const rows = db.prepare(`
1953
- SELECT ${MEMORY_SELECT_COLUMNS}
1954
- FROM memories
1955
- WHERE summary_dirty = 1
1956
- AND tenant_id = ?
1957
- AND kind != 'archived'
1958
- ORDER BY latest_at DESC NULLS LAST, id ASC
1952
+ const rows = db.prepare(`
1953
+ SELECT ${MEMORY_SELECT_COLUMNS}
1954
+ FROM memories
1955
+ WHERE summary_dirty = 1
1956
+ AND tenant_id = ?
1957
+ AND kind != 'archived'
1958
+ ORDER BY latest_at DESC NULLS LAST, id ASC
1959
1959
  `).all(tenantId);
1960
1960
  return rows.map(rowToEntry);
1961
1961
  }
@@ -1983,15 +1983,15 @@ export function markSummaryDirtyInTx(db, summaryId, tenantId, actor) {
1983
1983
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
1984
1984
  // actual level in same round trip so audit metadata stays accurate without
1985
1985
  // a SELECT-before-UPDATE extra DB op on this hot path (5 caller sites).
1986
- const result = db.prepare(`
1987
- UPDATE memories
1988
- SET summary_dirty = 1
1989
- WHERE id = ?
1990
- AND tenant_id = ?
1991
- AND dag_level IN (2, 3)
1992
- AND summary_dirty = 0
1993
- AND kind != 'archived'
1994
- RETURNING dag_level
1986
+ const result = db.prepare(`
1987
+ UPDATE memories
1988
+ SET summary_dirty = 1
1989
+ WHERE id = ?
1990
+ AND tenant_id = ?
1991
+ AND dag_level IN (2, 3)
1992
+ AND summary_dirty = 0
1993
+ AND kind != 'archived'
1994
+ RETURNING dag_level
1995
1995
  `).get(summaryId, tenantId);
1996
1996
  if (result) {
1997
1997
  audit(db, 'summary_marked_dirty', summaryId, { dag_level: result.dag_level, source: 'E2' }, actor, tenantId);
@@ -2017,15 +2017,15 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
2017
2017
  try {
2018
2018
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
2019
2019
  // actual level in same round trip.
2020
- const result = db.prepare(`
2021
- UPDATE memories
2022
- SET summary_dirty = 1
2023
- WHERE id = ?
2024
- AND tenant_id = ?
2025
- AND dag_level IN (2, 3)
2026
- AND summary_dirty = 0
2027
- AND kind != 'archived'
2028
- RETURNING dag_level
2020
+ const result = db.prepare(`
2021
+ UPDATE memories
2022
+ SET summary_dirty = 1
2023
+ WHERE id = ?
2024
+ AND tenant_id = ?
2025
+ AND dag_level IN (2, 3)
2026
+ AND summary_dirty = 0
2027
+ AND kind != 'archived'
2028
+ RETURNING dag_level
2029
2029
  `).get(summaryId, tenantId);
2030
2030
  if (result) {
2031
2031
  // audit() wraps appendAuditEvent in try/catch (v27 heal scenario).
@@ -2061,13 +2061,13 @@ export function loadAllL2Summaries(hippoRoot) {
2061
2061
  initStore(hippoRoot);
2062
2062
  const db = openHippoDb(hippoRoot);
2063
2063
  try {
2064
- const rows = db.prepare(`
2065
- SELECT ${MEMORY_SELECT_COLUMNS}
2066
- FROM memories
2067
- WHERE dag_level = 2
2068
- AND dag_parent_id IS NULL
2069
- AND kind != 'archived'
2070
- ORDER BY created ASC, id ASC
2064
+ const rows = db.prepare(`
2065
+ SELECT ${MEMORY_SELECT_COLUMNS}
2066
+ FROM memories
2067
+ WHERE dag_level = 2
2068
+ AND dag_parent_id IS NULL
2069
+ AND kind != 'archived'
2070
+ ORDER BY created ASC, id ASC
2071
2071
  `).all();
2072
2072
  return rows.map(rowToEntry);
2073
2073
  }
@@ -2088,12 +2088,12 @@ export function loadAllDirtySummaries(hippoRoot) {
2088
2088
  initStore(hippoRoot);
2089
2089
  const db = openHippoDb(hippoRoot);
2090
2090
  try {
2091
- const rows = db.prepare(`
2092
- SELECT ${MEMORY_SELECT_COLUMNS}
2093
- FROM memories
2094
- WHERE summary_dirty = 1
2095
- AND kind != 'archived'
2096
- ORDER BY latest_at DESC NULLS LAST, id ASC
2091
+ const rows = db.prepare(`
2092
+ SELECT ${MEMORY_SELECT_COLUMNS}
2093
+ FROM memories
2094
+ WHERE summary_dirty = 1
2095
+ AND kind != 'archived'
2096
+ ORDER BY latest_at DESC NULLS LAST, id ASC
2097
2097
  `).all();
2098
2098
  return rows.map(rowToEntry);
2099
2099
  }
@@ -2113,13 +2113,13 @@ export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
2113
2113
  initStore(hippoRoot);
2114
2114
  const db = openHippoDb(hippoRoot);
2115
2115
  try {
2116
- const rows = db.prepare(`
2117
- SELECT ${MEMORY_SELECT_COLUMNS}
2118
- FROM memories
2119
- WHERE dag_parent_id = ?
2120
- AND tenant_id = ?
2121
- AND kind != 'archived'
2122
- ORDER BY created ASC
2116
+ const rows = db.prepare(`
2117
+ SELECT ${MEMORY_SELECT_COLUMNS}
2118
+ FROM memories
2119
+ WHERE dag_parent_id = ?
2120
+ AND tenant_id = ?
2121
+ AND kind != 'archived'
2122
+ ORDER BY created ASC
2123
2123
  `).all(summaryId, tenantId);
2124
2124
  return rows.map(rowToEntry);
2125
2125
  }
@@ -2147,28 +2147,28 @@ export function applyRebuildResult(hippoRoot, summary, patch) {
2147
2147
  // ONE prepared UPDATE per branch. Test #8 inspects the SQL string.
2148
2148
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3) on both branches.
2149
2149
  const sql = patch.bumpRebuildCount
2150
- ? `UPDATE memories
2151
- SET content = ?,
2152
- descendant_count = ?,
2153
- earliest_at = ?,
2154
- latest_at = ?,
2155
- last_rebuilt_at = ?,
2156
- rebuild_count = COALESCE(rebuild_count, 0) + 1,
2157
- summary_dirty = 0
2158
- WHERE id = ?
2159
- AND tenant_id = ?
2160
- AND dag_level IN (2, 3)
2161
- AND summary_dirty = 1
2150
+ ? `UPDATE memories
2151
+ SET content = ?,
2152
+ descendant_count = ?,
2153
+ earliest_at = ?,
2154
+ latest_at = ?,
2155
+ last_rebuilt_at = ?,
2156
+ rebuild_count = COALESCE(rebuild_count, 0) + 1,
2157
+ summary_dirty = 0
2158
+ WHERE id = ?
2159
+ AND tenant_id = ?
2160
+ AND dag_level IN (2, 3)
2161
+ AND summary_dirty = 1
2162
2162
  AND kind != 'archived'`
2163
- : `UPDATE memories
2164
- SET descendant_count = ?,
2165
- earliest_at = ?,
2166
- latest_at = ?,
2167
- summary_dirty = 0
2168
- WHERE id = ?
2169
- AND tenant_id = ?
2170
- AND dag_level IN (2, 3)
2171
- AND summary_dirty = 1
2163
+ : `UPDATE memories
2164
+ SET descendant_count = ?,
2165
+ earliest_at = ?,
2166
+ latest_at = ?,
2167
+ summary_dirty = 0
2168
+ WHERE id = ?
2169
+ AND tenant_id = ?
2170
+ AND dag_level IN (2, 3)
2171
+ AND summary_dirty = 1
2172
2172
  AND kind != 'archived'`;
2173
2173
  const result = patch.bumpRebuildCount
2174
2174
  ? db.prepare(sql).run(patch.content, patch.descendant_count, patch.earliest_at, patch.latest_at, nowIso, summary.id, summary.tenantId)
@@ -2236,15 +2236,15 @@ export function clearSummaryDirtyAfterBuild(hippoRoot, summaryId, tenantId, acto
2236
2236
  try {
2237
2237
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
2238
2238
  // actual level so audit metadata stays accurate without an extra SELECT.
2239
- const result = db.prepare(`
2240
- UPDATE memories
2241
- SET summary_dirty = 0
2242
- WHERE id = ?
2243
- AND tenant_id = ?
2244
- AND dag_level IN (2, 3)
2245
- AND summary_dirty = 1
2246
- AND kind != 'archived'
2247
- RETURNING dag_level
2239
+ const result = db.prepare(`
2240
+ UPDATE memories
2241
+ SET summary_dirty = 0
2242
+ WHERE id = ?
2243
+ AND tenant_id = ?
2244
+ AND dag_level IN (2, 3)
2245
+ AND summary_dirty = 1
2246
+ AND kind != 'archived'
2247
+ RETURNING dag_level
2248
2248
  `).get(summaryId, tenantId);
2249
2249
  if (result) {
2250
2250
  // v0.30 / E5: source param distinguishes buildDag-clean (L2) from