hippo-memory 1.17.0 → 1.19.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 (106) hide show
  1. package/bin/hippo.js +2 -2
  2. package/dist/api.d.ts +43 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +109 -7
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +109 -11
  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/customer-notes.d.ts.map +1 -1
  16. package/dist/customer-notes.js +5 -1
  17. package/dist/customer-notes.js.map +1 -1
  18. package/dist/dag.js +6 -6
  19. package/dist/dashboard.js +7 -7
  20. package/dist/decisions.d.ts.map +1 -1
  21. package/dist/decisions.js +9 -1
  22. package/dist/decisions.js.map +1 -1
  23. package/dist/goals.d.ts +11 -0
  24. package/dist/goals.d.ts.map +1 -1
  25. package/dist/goals.js +61 -49
  26. package/dist/goals.js.map +1 -1
  27. package/dist/graph-extract.d.ts.map +1 -1
  28. package/dist/graph-extract.js +32 -12
  29. package/dist/graph-extract.js.map +1 -1
  30. package/dist/graph.d.ts +46 -3
  31. package/dist/graph.d.ts.map +1 -1
  32. package/dist/graph.js +116 -8
  33. package/dist/graph.js.map +1 -1
  34. package/dist/hooks.js +24 -24
  35. package/dist/physics-state.js +27 -27
  36. package/dist/policies.d.ts.map +1 -1
  37. package/dist/policies.js +5 -1
  38. package/dist/policies.js.map +1 -1
  39. package/dist/predictions.js +67 -67
  40. package/dist/project-briefs.d.ts.map +1 -1
  41. package/dist/project-briefs.js +6 -1
  42. package/dist/project-briefs.js.map +1 -1
  43. package/dist/refine-llm.js +13 -13
  44. package/dist/search.d.ts +33 -0
  45. package/dist/search.d.ts.map +1 -1
  46. package/dist/search.js.map +1 -1
  47. package/dist/server.d.ts.map +1 -1
  48. package/dist/server.js +7 -0
  49. package/dist/server.js.map +1 -1
  50. package/dist/sleep-redact.d.ts +1 -0
  51. package/dist/sleep-redact.d.ts.map +1 -1
  52. package/dist/sleep-redact.js +6 -0
  53. package/dist/sleep-redact.js.map +1 -1
  54. package/dist/src/api.js +109 -7
  55. package/dist/src/api.js.map +1 -1
  56. package/dist/src/cli.js +109 -11
  57. package/dist/src/cli.js.map +1 -1
  58. package/dist/src/connectors/github/backfill.js +4 -4
  59. package/dist/src/connectors/github/cli-impl.js +6 -6
  60. package/dist/src/connectors/github/dlq.js +14 -14
  61. package/dist/src/connectors/slack/backfill.js +1 -1
  62. package/dist/src/connectors/slack/dlq.js +10 -10
  63. package/dist/src/connectors/slack/workspaces.js +4 -4
  64. package/dist/src/customer-notes.js +5 -1
  65. package/dist/src/customer-notes.js.map +1 -1
  66. package/dist/src/dag.js +6 -6
  67. package/dist/src/dashboard.js +7 -7
  68. package/dist/src/decisions.js +9 -1
  69. package/dist/src/decisions.js.map +1 -1
  70. package/dist/src/goals.js +61 -49
  71. package/dist/src/goals.js.map +1 -1
  72. package/dist/src/graph-extract.js +32 -12
  73. package/dist/src/graph-extract.js.map +1 -1
  74. package/dist/src/graph.js +116 -8
  75. package/dist/src/graph.js.map +1 -1
  76. package/dist/src/hooks.js +24 -24
  77. package/dist/src/physics-state.js +27 -27
  78. package/dist/src/policies.js +5 -1
  79. package/dist/src/policies.js.map +1 -1
  80. package/dist/src/predictions.js +67 -67
  81. package/dist/src/project-briefs.js +6 -1
  82. package/dist/src/project-briefs.js.map +1 -1
  83. package/dist/src/refine-llm.js +13 -13
  84. package/dist/src/search.js.map +1 -1
  85. package/dist/src/server.js +7 -0
  86. package/dist/src/server.js.map +1 -1
  87. package/dist/src/sleep-redact.js +6 -0
  88. package/dist/src/sleep-redact.js.map +1 -1
  89. package/dist/src/store.js +261 -260
  90. package/dist/src/store.js.map +1 -1
  91. package/dist/src/version.js +1 -1
  92. package/dist/src/working-memory.js +19 -19
  93. package/dist/store.d.ts +6 -0
  94. package/dist/store.d.ts.map +1 -1
  95. package/dist/store.js +261 -260
  96. package/dist/store.js.map +1 -1
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/dist/working-memory.js +19 -19
  100. package/dist-ui/index.html +12 -12
  101. package/extensions/openclaw-plugin/index.ts +650 -650
  102. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  103. package/extensions/openclaw-plugin/package.json +1 -1
  104. package/openclaw.plugin.json +1 -1
  105. package/package.json +1 -1
  106. package/dist/benchmarks/e1.3/scenarios.json +0 -2587
package/dist/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));
@@ -852,6 +852,7 @@ export function writeEntry(hippoRoot, entry, opts) {
852
852
  const db = openHippoDb(hippoRoot);
853
853
  try {
854
854
  writeEntryDbOnly(db, entry, opts);
855
+ opts?.afterCommit?.();
855
856
  writeEntryMirrors(hippoRoot, db, entry);
856
857
  }
857
858
  finally {
@@ -1427,16 +1428,16 @@ export function saveActiveTaskSnapshot(hippoRoot, tenantId, snapshot) {
1427
1428
  try {
1428
1429
  db.exec('BEGIN');
1429
1430
  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', ?, ?, ?, ?, ?, ?)
1431
+ const result = db.prepare(`
1432
+ INSERT INTO task_snapshots(task, summary, next_step, status, source, session_id, scope, tenant_id, created_at, updated_at)
1433
+ VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
1433
1434
  `).run(snapshot.task, snapshot.summary, snapshot.next_step, snapshot.source ?? 'cli', snapshot.session_id ?? null, snapshot.scope ?? null, tenantId, now, now);
1434
1435
  db.exec('COMMIT');
1435
1436
  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 = ?
1437
+ const row = db.prepare(`
1438
+ SELECT id, task, summary, next_step, status, source, session_id, scope, created_at, updated_at
1439
+ FROM task_snapshots
1440
+ WHERE id = ?
1440
1441
  `).get(id);
1441
1442
  if (!row) {
1442
1443
  throw new Error('Failed to reload saved active task snapshot');
@@ -1463,12 +1464,12 @@ export function loadActiveTaskSnapshot(hippoRoot, tenantId) {
1463
1464
  initStore(hippoRoot);
1464
1465
  const db = openHippoDb(hippoRoot);
1465
1466
  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
1467
+ const row = db.prepare(`
1468
+ SELECT id, task, summary, next_step, status, source, session_id, scope, created_at, updated_at
1469
+ FROM task_snapshots
1470
+ WHERE status = 'active' AND tenant_id = ?
1471
+ ORDER BY updated_at DESC, id DESC
1472
+ LIMIT 1
1472
1473
  `).get(tenantId);
1473
1474
  if (!row) {
1474
1475
  removeActiveTaskMirror(hippoRoot, tenantId);
@@ -1509,26 +1510,26 @@ export function appendSessionEvent(hippoRoot, tenantId, event) {
1509
1510
  // v1.2: scope is wired through. Default-deny in api.recall + cmdRecall
1510
1511
  // continuity reads applies to slack:private:* and 'unknown:legacy' rows.
1511
1512
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?)
1513
+ const result = db.prepare(`
1514
+ INSERT INTO session_events(session_id, task, event_type, content, source, scope, metadata_json, tenant_id, created_at)
1515
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1515
1516
  `).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
1517
  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 = ?
1518
+ const row = db.prepare(`
1519
+ SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
1520
+ FROM session_events
1521
+ WHERE id = ?
1521
1522
  `).get(id);
1522
1523
  if (!row) {
1523
1524
  throw new Error('Failed to reload saved session event');
1524
1525
  }
1525
1526
  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 ?
1527
+ const recentRows = db.prepare(`
1528
+ SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
1529
+ FROM session_events
1530
+ WHERE session_id = ? AND tenant_id = ?
1531
+ ORDER BY created_at DESC, id DESC
1532
+ LIMIT ?
1532
1533
  `).all(loaded.session_id, tenantId, 20);
1533
1534
  const recent = recentRows.map(rowToSessionEvent).reverse();
1534
1535
  writeRecentSessionMirror(hippoRoot, tenantId, recent);
@@ -1556,12 +1557,12 @@ export function listSessionEvents(hippoRoot, tenantId, options = {}) {
1556
1557
  const limit = Math.max(1, Math.trunc(options.limit ?? 8));
1557
1558
  params.push(limit);
1558
1559
  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 ?
1560
+ const rows = db.prepare(`
1561
+ SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
1562
+ FROM session_events
1563
+ ${where}
1564
+ ORDER BY created_at DESC, id DESC
1565
+ LIMIT ?
1565
1566
  `).all(...params);
1566
1567
  return rows.map(rowToSessionEvent).reverse();
1567
1568
  }
@@ -1578,9 +1579,9 @@ export function findPromotableSessions(hippoRoot, tenantId, sinceMs) {
1578
1579
  initStore(hippoRoot);
1579
1580
  const db = openHippoDb(hippoRoot);
1580
1581
  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 = ?
1582
+ const rows = db.prepare(`
1583
+ SELECT DISTINCT session_id FROM session_events
1584
+ WHERE event_type = 'session_complete' AND created_at >= ? AND tenant_id = ?
1584
1585
  `).all(new Date(sinceMs).toISOString(), tenantId);
1585
1586
  return rows;
1586
1587
  }
@@ -1597,10 +1598,10 @@ export function traceExistsForSession(hippoRoot, tenantId, session_id) {
1597
1598
  initStore(hippoRoot);
1598
1599
  const db = openHippoDb(hippoRoot);
1599
1600
  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
1601
+ const row = db.prepare(`
1602
+ SELECT 1 FROM memories
1603
+ WHERE source_session_id = ? AND layer = 'trace' AND tenant_id = ?
1604
+ LIMIT 1
1604
1605
  `).get(session_id, tenantId);
1605
1606
  return !!row;
1606
1607
  }
@@ -1623,38 +1624,38 @@ export function listMemoryConflicts(hippoRoot, status = 'open', tenantId) {
1623
1624
  // each in-tenant, so neither a normal cross-tenant pair nor a stale
1624
1625
  // pre-fix row surfaces (consistent with resolveConflict).
1625
1626
  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
1627
+ ? db.prepare(`
1628
+ SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
1629
+ mc.status, mc.detected_at, mc.updated_at
1630
+ FROM memory_conflicts mc
1631
+ JOIN memories ma ON ma.id = mc.memory_a_id
1632
+ JOIN memories mb ON mb.id = mc.memory_b_id
1633
+ WHERE ma.tenant_id = ? AND mb.tenant_id = ?
1634
+ ORDER BY mc.updated_at DESC, mc.id DESC
1634
1635
  `).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
1636
+ : db.prepare(`
1637
+ SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
1638
+ mc.status, mc.detected_at, mc.updated_at
1639
+ FROM memory_conflicts mc
1640
+ JOIN memories ma ON ma.id = mc.memory_a_id
1641
+ JOIN memories mb ON mb.id = mc.memory_b_id
1642
+ WHERE mc.status = ? AND ma.tenant_id = ? AND mb.tenant_id = ?
1643
+ ORDER BY mc.updated_at DESC, mc.id DESC
1643
1644
  `).all(status, tenantId, tenantId);
1644
1645
  }
1645
1646
  else {
1646
1647
  // Unscoped query — legacy direct-mode (CLI, tests, consolidate).
1647
1648
  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
1649
+ ? db.prepare(`
1650
+ SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1651
+ FROM memory_conflicts
1652
+ ORDER BY updated_at DESC, id DESC
1652
1653
  `).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
1654
+ : db.prepare(`
1655
+ SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1656
+ FROM memory_conflicts
1657
+ WHERE status = ?
1658
+ ORDER BY updated_at DESC, id DESC
1658
1659
  `).all(status);
1659
1660
  }
1660
1661
  return rows.map(rowToMemoryConflict);
@@ -1687,10 +1688,10 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
1687
1688
  score: conflict.score,
1688
1689
  }));
1689
1690
  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'
1691
+ const openRows = db.prepare(`
1692
+ SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1693
+ FROM memory_conflicts
1694
+ WHERE status = 'open'
1694
1695
  `).all();
1695
1696
  for (const row of openRows) {
1696
1697
  const key = `${row.memory_a_id}::${row.memory_b_id}`;
@@ -1709,20 +1710,20 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
1709
1710
  // Skip cross-tenant pairs — never persist a conflict spanning tenants.
1710
1711
  if (!sameTenant(conflict.memory_a_id, conflict.memory_b_id))
1711
1712
  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
1713
+ db.prepare(`
1714
+ INSERT INTO memory_conflicts(memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at)
1715
+ VALUES (?, ?, ?, ?, 'open', ?, ?)
1716
+ ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
1717
+ reason = excluded.reason,
1718
+ score = excluded.score,
1719
+ status = 'open',
1720
+ updated_at = excluded.updated_at
1720
1721
  `).run(conflict.memory_a_id, conflict.memory_b_id, conflict.reason, conflict.score, detectedAt, detectedAt);
1721
1722
  }
1722
- const openConflicts = db.prepare(`
1723
- SELECT memory_a_id, memory_b_id
1724
- FROM memory_conflicts
1725
- WHERE status = 'open'
1723
+ const openConflicts = db.prepare(`
1724
+ SELECT memory_a_id, memory_b_id
1725
+ FROM memory_conflicts
1726
+ WHERE status = 'open'
1726
1727
  `).all();
1727
1728
  const refMap = new Map();
1728
1729
  for (const row of openConflicts) {
@@ -1776,17 +1777,17 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
1776
1777
  const memArgs = tenantId !== undefined ? [tenantId] : [];
1777
1778
  try {
1778
1779
  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 = ?
1780
+ ? db.prepare(`
1781
+ SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
1782
+ mc.status, mc.detected_at, mc.updated_at
1783
+ FROM memory_conflicts mc
1784
+ JOIN memories ma ON ma.id = mc.memory_a_id
1785
+ JOIN memories mb ON mb.id = mc.memory_b_id
1786
+ WHERE mc.id = ? AND ma.tenant_id = ? AND mb.tenant_id = ?
1786
1787
  `).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 = ?
1788
+ : db.prepare(`
1789
+ SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
1790
+ FROM memory_conflicts WHERE id = ?
1790
1791
  `).get(conflictId));
1791
1792
  if (!row)
1792
1793
  return null;
@@ -1856,15 +1857,15 @@ export function saveSessionHandoff(hippoRoot, tenantId, handoff) {
1856
1857
  // v1.2: scope is wired through. Read-side default-deny in api.recall +
1857
1858
  // cmdRecall continuity excludes slack:private:* and 'unknown:legacy'.
1858
1859
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?)
1860
+ const result = db.prepare(`
1861
+ INSERT INTO session_handoffs(session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, tenant_id, created_at)
1862
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1862
1863
  `).run(handoff.sessionId, handoff.repoRoot ?? null, handoff.taskId ?? null, handoff.summary, handoff.nextAction ?? null, JSON.stringify(handoff.artifacts ?? []), handoff.scope ?? null, tenantId, now);
1863
1864
  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 = ?
1865
+ const row = db.prepare(`
1866
+ SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1867
+ FROM session_handoffs
1868
+ WHERE id = ?
1868
1869
  `).get(id);
1869
1870
  if (!row) {
1870
1871
  throw new Error('Failed to reload saved session handoff');
@@ -1885,21 +1886,21 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId) {
1885
1886
  try {
1886
1887
  let row;
1887
1888
  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
1889
+ row = db.prepare(`
1890
+ SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1891
+ FROM session_handoffs
1892
+ WHERE session_id = ? AND tenant_id = ?
1893
+ ORDER BY created_at DESC, id DESC
1894
+ LIMIT 1
1894
1895
  `).get(sessionId, tenantId);
1895
1896
  }
1896
1897
  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
1898
+ row = db.prepare(`
1899
+ SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1900
+ FROM session_handoffs
1901
+ WHERE tenant_id = ?
1902
+ ORDER BY created_at DESC, id DESC
1903
+ LIMIT 1
1903
1904
  `).get(tenantId);
1904
1905
  }
1905
1906
  return row ? rowToSessionHandoff(row) : null;
@@ -1916,10 +1917,10 @@ export function loadHandoffById(hippoRoot, tenantId, id) {
1916
1917
  initStore(hippoRoot);
1917
1918
  const db = openHippoDb(hippoRoot);
1918
1919
  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 = ?
1920
+ const row = db.prepare(`
1921
+ SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
1922
+ FROM session_handoffs
1923
+ WHERE id = ? AND tenant_id = ?
1923
1924
  `).get(id, tenantId);
1924
1925
  return row ? rowToSessionHandoff(row) : null;
1925
1926
  }
@@ -1949,13 +1950,13 @@ export function loadDirtySummaries(hippoRoot, tenantId) {
1949
1950
  initStore(hippoRoot);
1950
1951
  const db = openHippoDb(hippoRoot);
1951
1952
  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
1953
+ const rows = db.prepare(`
1954
+ SELECT ${MEMORY_SELECT_COLUMNS}
1955
+ FROM memories
1956
+ WHERE summary_dirty = 1
1957
+ AND tenant_id = ?
1958
+ AND kind != 'archived'
1959
+ ORDER BY latest_at DESC NULLS LAST, id ASC
1959
1960
  `).all(tenantId);
1960
1961
  return rows.map(rowToEntry);
1961
1962
  }
@@ -1983,15 +1984,15 @@ export function markSummaryDirtyInTx(db, summaryId, tenantId, actor) {
1983
1984
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
1984
1985
  // actual level in same round trip so audit metadata stays accurate without
1985
1986
  // 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
1987
+ const result = db.prepare(`
1988
+ UPDATE memories
1989
+ SET summary_dirty = 1
1990
+ WHERE id = ?
1991
+ AND tenant_id = ?
1992
+ AND dag_level IN (2, 3)
1993
+ AND summary_dirty = 0
1994
+ AND kind != 'archived'
1995
+ RETURNING dag_level
1995
1996
  `).get(summaryId, tenantId);
1996
1997
  if (result) {
1997
1998
  audit(db, 'summary_marked_dirty', summaryId, { dag_level: result.dag_level, source: 'E2' }, actor, tenantId);
@@ -2017,15 +2018,15 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
2017
2018
  try {
2018
2019
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
2019
2020
  // 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
2021
+ const result = db.prepare(`
2022
+ UPDATE memories
2023
+ SET summary_dirty = 1
2024
+ WHERE id = ?
2025
+ AND tenant_id = ?
2026
+ AND dag_level IN (2, 3)
2027
+ AND summary_dirty = 0
2028
+ AND kind != 'archived'
2029
+ RETURNING dag_level
2029
2030
  `).get(summaryId, tenantId);
2030
2031
  if (result) {
2031
2032
  // audit() wraps appendAuditEvent in try/catch (v27 heal scenario).
@@ -2061,13 +2062,13 @@ export function loadAllL2Summaries(hippoRoot) {
2061
2062
  initStore(hippoRoot);
2062
2063
  const db = openHippoDb(hippoRoot);
2063
2064
  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
2065
+ const rows = db.prepare(`
2066
+ SELECT ${MEMORY_SELECT_COLUMNS}
2067
+ FROM memories
2068
+ WHERE dag_level = 2
2069
+ AND dag_parent_id IS NULL
2070
+ AND kind != 'archived'
2071
+ ORDER BY created ASC, id ASC
2071
2072
  `).all();
2072
2073
  return rows.map(rowToEntry);
2073
2074
  }
@@ -2088,12 +2089,12 @@ export function loadAllDirtySummaries(hippoRoot) {
2088
2089
  initStore(hippoRoot);
2089
2090
  const db = openHippoDb(hippoRoot);
2090
2091
  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
2092
+ const rows = db.prepare(`
2093
+ SELECT ${MEMORY_SELECT_COLUMNS}
2094
+ FROM memories
2095
+ WHERE summary_dirty = 1
2096
+ AND kind != 'archived'
2097
+ ORDER BY latest_at DESC NULLS LAST, id ASC
2097
2098
  `).all();
2098
2099
  return rows.map(rowToEntry);
2099
2100
  }
@@ -2113,13 +2114,13 @@ export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
2113
2114
  initStore(hippoRoot);
2114
2115
  const db = openHippoDb(hippoRoot);
2115
2116
  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
2117
+ const rows = db.prepare(`
2118
+ SELECT ${MEMORY_SELECT_COLUMNS}
2119
+ FROM memories
2120
+ WHERE dag_parent_id = ?
2121
+ AND tenant_id = ?
2122
+ AND kind != 'archived'
2123
+ ORDER BY created ASC
2123
2124
  `).all(summaryId, tenantId);
2124
2125
  return rows.map(rowToEntry);
2125
2126
  }
@@ -2147,28 +2148,28 @@ export function applyRebuildResult(hippoRoot, summary, patch) {
2147
2148
  // ONE prepared UPDATE per branch. Test #8 inspects the SQL string.
2148
2149
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3) on both branches.
2149
2150
  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
2151
+ ? `UPDATE memories
2152
+ SET content = ?,
2153
+ descendant_count = ?,
2154
+ earliest_at = ?,
2155
+ latest_at = ?,
2156
+ last_rebuilt_at = ?,
2157
+ rebuild_count = COALESCE(rebuild_count, 0) + 1,
2158
+ summary_dirty = 0
2159
+ WHERE id = ?
2160
+ AND tenant_id = ?
2161
+ AND dag_level IN (2, 3)
2162
+ AND summary_dirty = 1
2162
2163
  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
2164
+ : `UPDATE memories
2165
+ SET descendant_count = ?,
2166
+ earliest_at = ?,
2167
+ latest_at = ?,
2168
+ summary_dirty = 0
2169
+ WHERE id = ?
2170
+ AND tenant_id = ?
2171
+ AND dag_level IN (2, 3)
2172
+ AND summary_dirty = 1
2172
2173
  AND kind != 'archived'`;
2173
2174
  const result = patch.bumpRebuildCount
2174
2175
  ? db.prepare(sql).run(patch.content, patch.descendant_count, patch.earliest_at, patch.latest_at, nowIso, summary.id, summary.tenantId)
@@ -2236,15 +2237,15 @@ export function clearSummaryDirtyAfterBuild(hippoRoot, summaryId, tenantId, acto
2236
2237
  try {
2237
2238
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
2238
2239
  // 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
2240
+ const result = db.prepare(`
2241
+ UPDATE memories
2242
+ SET summary_dirty = 0
2243
+ WHERE id = ?
2244
+ AND tenant_id = ?
2245
+ AND dag_level IN (2, 3)
2246
+ AND summary_dirty = 1
2247
+ AND kind != 'archived'
2248
+ RETURNING dag_level
2248
2249
  `).get(summaryId, tenantId);
2249
2250
  if (result) {
2250
2251
  // v0.30 / E5: source param distinguishes buildDag-clean (L2) from