hippo-memory 0.39.0 → 1.0.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 (47) hide show
  1. package/README.md +12 -0
  2. package/dist/cli.js +80 -17
  3. package/dist/cli.js.map +1 -1
  4. package/dist/connectors/slack/transform.d.ts +2 -0
  5. package/dist/connectors/slack/transform.d.ts.map +1 -1
  6. package/dist/connectors/slack/transform.js +3 -0
  7. package/dist/connectors/slack/transform.js.map +1 -1
  8. package/dist/consolidate.d.ts.map +1 -1
  9. package/dist/consolidate.js +9 -3
  10. package/dist/consolidate.js.map +1 -1
  11. package/dist/correction-latency.d.ts +36 -0
  12. package/dist/correction-latency.d.ts.map +1 -0
  13. package/dist/correction-latency.js +74 -0
  14. package/dist/correction-latency.js.map +1 -0
  15. package/dist/db.d.ts.map +1 -1
  16. package/dist/db.js +87 -1
  17. package/dist/db.js.map +1 -1
  18. package/dist/mcp/server.js +1 -1
  19. package/dist/mcp/server.js.map +1 -1
  20. package/dist/provenance-coverage.d.ts +18 -0
  21. package/dist/provenance-coverage.d.ts.map +1 -0
  22. package/dist/provenance-coverage.js +23 -0
  23. package/dist/provenance-coverage.js.map +1 -0
  24. package/dist/src/cli.js +80 -17
  25. package/dist/src/cli.js.map +1 -1
  26. package/dist/src/connectors/slack/transform.js +3 -0
  27. package/dist/src/connectors/slack/transform.js.map +1 -1
  28. package/dist/src/consolidate.js +9 -3
  29. package/dist/src/consolidate.js.map +1 -1
  30. package/dist/src/correction-latency.js +74 -0
  31. package/dist/src/correction-latency.js.map +1 -0
  32. package/dist/src/db.js +87 -1
  33. package/dist/src/db.js.map +1 -1
  34. package/dist/src/mcp/server.js +1 -1
  35. package/dist/src/mcp/server.js.map +1 -1
  36. package/dist/src/provenance-coverage.js +23 -0
  37. package/dist/src/provenance-coverage.js.map +1 -0
  38. package/dist/src/store.js +105 -52
  39. package/dist/src/store.js.map +1 -1
  40. package/dist/store.d.ts +10 -10
  41. package/dist/store.d.ts.map +1 -1
  42. package/dist/store.js +105 -52
  43. package/dist/store.js.map +1 -1
  44. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  45. package/extensions/openclaw-plugin/package.json +1 -1
  46. package/openclaw.plugin.json +1 -1
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -85,6 +85,18 @@ hippo recall "data pipeline issues" --budget 2000
85
85
 
86
86
  ---
87
87
 
88
+ ### What's new in v1.0.0
89
+
90
+ - **Tenant-isolation security release.** v0.40.0's measurement gates surfaced a real cross-tenant data leak on the continuity tables (`task_snapshots`, `session_events`, `session_handoffs`). Schema migration v22 closes the gap: every continuity helper now scopes reads and writes by `tenantId`. Markdown mirror files (`buffer/active-task.md`, `buffer/recent-session.md`) are tenant-scoped too; the default tenant keeps the unsuffixed filename for on-disk back-compat.
91
+ - **Slack envelope fix.** `messageToRememberOpts` now sets `owner: 'user:<slack_user_id>'` so ingested Slack rows pass the v0.40.0 `hippo provenance --strict` gate.
92
+ - **Breaking change for JS callers.** 10 store helpers (`saveActiveTaskSnapshot`, `loadLatestHandoff`, `appendSessionEvent`, etc.) gained a required `tenantId` second argument. TypeScript callers get a compile error. JS callers get a runtime guard error (`assertTenantId`) that detects the most common misbinding (passing a `sess-*` session id) and points at the migration. See CHANGELOG for the full helper list.
93
+ - **Schema v22 migration.** Idempotent, transactional, with smart tenant backfill via unambiguous `task_snapshots.session_id` joins.
94
+
95
+ ### What's new in v0.40.0
96
+
97
+ - **Company Brain measurement gates.** Two new diagnostic commands close the last blocked rows of the Company Brain scorecard. `hippo provenance [--json] [--strict]` audits every `kind='raw'` row for `owner` + `artifact_ref`; `--strict` exits non-zero so CI can block on coverage regressions. `hippo correction-latency [--json]` reports p50 / p95 / max wall-clock lag from receipt to supersession across `superseded_by` chains. Both helpers (`buildProvenanceCoverage`, `buildCorrectionLatency`) are importable from `src/`.
98
+ - **No behavioral change to remember / recall.** Additive only: schema unchanged, retrieval untouched.
99
+
88
100
  ### What's new in v0.39.0
89
101
 
90
102
  - Security hardening release: 5 CRITICAL cross-tenant fixes (CVE candidates), GDPR Path A on archive (true RTBF), MCP per-client isolation, Slack ingestion race + idempotency hardening, auth timing leak reduction.
package/dist/cli.js CHANGED
@@ -53,6 +53,8 @@ import { importChatGPT, importClaude, importCursor, importGenericFile, importMar
53
53
  import { cmdCapture } from './capture.js';
54
54
  import { auditMemories, appendAuditEvent, } from './audit.js';
55
55
  import { listApiKeys, revokeApiKey } from './auth.js';
56
+ import { buildProvenanceCoverage } from './provenance-coverage.js';
57
+ import { buildCorrectionLatency } from './correction-latency.js';
56
58
  import * as api from './api.js';
57
59
  import * as client from './client.js';
58
60
  import { detectServer, removePidfile } from './server-detect.js';
@@ -2498,7 +2500,7 @@ function cmdSnapshot(hippoRoot, args, flags) {
2498
2500
  console.error('Usage: hippo snapshot save --task <task> --summary <summary> --next-step <step> [--source <source>] [--session <session-id>]');
2499
2501
  process.exit(1);
2500
2502
  }
2501
- const snapshot = saveActiveTaskSnapshot(hippoRoot, {
2503
+ const snapshot = saveActiveTaskSnapshot(hippoRoot, resolveTenantId({}), {
2502
2504
  task,
2503
2505
  summary,
2504
2506
  next_step: nextStep,
@@ -2514,7 +2516,7 @@ function cmdSnapshot(hippoRoot, args, flags) {
2514
2516
  return;
2515
2517
  }
2516
2518
  if (subcommand === 'clear') {
2517
- const cleared = clearActiveTaskSnapshot(hippoRoot, String(flags['status'] ?? 'cleared'));
2519
+ const cleared = clearActiveTaskSnapshot(hippoRoot, resolveTenantId({}), String(flags['status'] ?? 'cleared'));
2518
2520
  if (!cleared) {
2519
2521
  console.log('No active task snapshot to clear.');
2520
2522
  return;
@@ -2523,7 +2525,7 @@ function cmdSnapshot(hippoRoot, args, flags) {
2523
2525
  return;
2524
2526
  }
2525
2527
  if (subcommand === 'show') {
2526
- const snapshot = loadActiveTaskSnapshot(hippoRoot);
2528
+ const snapshot = loadActiveTaskSnapshot(hippoRoot, resolveTenantId({}));
2527
2529
  if (!snapshot) {
2528
2530
  if (flags['json']) {
2529
2531
  console.log(JSON.stringify({ snapshot: null }));
@@ -2556,7 +2558,7 @@ function cmdSession(hippoRoot, args, flags) {
2556
2558
  console.error('Usage: hippo session log --id <session-id> --content <text> [--type <type>] [--task <task>] [--source <source>]');
2557
2559
  process.exit(1);
2558
2560
  }
2559
- const event = appendSessionEvent(hippoRoot, {
2561
+ const event = appendSessionEvent(hippoRoot, resolveTenantId({}), {
2560
2562
  session_id: sessionId,
2561
2563
  task: task || null,
2562
2564
  event_type: eventType || 'note',
@@ -2569,7 +2571,7 @@ function cmdSession(hippoRoot, args, flags) {
2569
2571
  return;
2570
2572
  }
2571
2573
  if (subcommand === 'show') {
2572
- const events = listSessionEvents(hippoRoot, {
2574
+ const events = listSessionEvents(hippoRoot, resolveTenantId({}), {
2573
2575
  session_id: sessionId || undefined,
2574
2576
  task: task || undefined,
2575
2577
  limit,
@@ -2582,8 +2584,8 @@ function cmdSession(hippoRoot, args, flags) {
2582
2584
  return;
2583
2585
  }
2584
2586
  if (subcommand === 'latest') {
2585
- const snapshot = loadActiveTaskSnapshot(hippoRoot);
2586
- const events = listSessionEvents(hippoRoot, {
2587
+ const snapshot = loadActiveTaskSnapshot(hippoRoot, resolveTenantId({}));
2588
+ const events = listSessionEvents(hippoRoot, resolveTenantId({}), {
2587
2589
  session_id: sessionId || snapshot?.session_id || undefined,
2588
2590
  limit,
2589
2591
  });
@@ -2616,7 +2618,7 @@ function cmdSession(hippoRoot, args, flags) {
2616
2618
  const metadata = { ended_at: new Date().toISOString() };
2617
2619
  if (summary)
2618
2620
  metadata.summary = summary;
2619
- const event = appendSessionEvent(hippoRoot, {
2621
+ const event = appendSessionEvent(hippoRoot, resolveTenantId({}), {
2620
2622
  session_id: sessionId,
2621
2623
  task: task || null,
2622
2624
  event_type: 'session_complete',
@@ -2628,7 +2630,7 @@ function cmdSession(hippoRoot, args, flags) {
2628
2630
  return;
2629
2631
  }
2630
2632
  if (subcommand === 'resume') {
2631
- const handoff = loadLatestHandoff(hippoRoot, sessionId || undefined);
2633
+ const handoff = loadLatestHandoff(hippoRoot, resolveTenantId({}), sessionId || undefined);
2632
2634
  if (!handoff) {
2633
2635
  console.log('No handoff to resume from.');
2634
2636
  return;
@@ -2701,7 +2703,7 @@ function cmdHandoff(hippoRoot, args, flags) {
2701
2703
  const artifacts = Array.isArray(artifactFlag)
2702
2704
  ? artifactFlag
2703
2705
  : (typeof artifactFlag === 'string' ? [artifactFlag] : []);
2704
- const handoff = saveSessionHandoff(hippoRoot, {
2706
+ const handoff = saveSessionHandoff(hippoRoot, resolveTenantId({}), {
2705
2707
  version: 1,
2706
2708
  sessionId,
2707
2709
  repoRoot: process.cwd(),
@@ -2721,7 +2723,7 @@ function cmdHandoff(hippoRoot, args, flags) {
2721
2723
  }
2722
2724
  if (subcommand === 'latest') {
2723
2725
  const sessionId = String(flags['session'] ?? flags['id'] ?? '').trim() || undefined;
2724
- const handoff = loadLatestHandoff(hippoRoot, sessionId);
2726
+ const handoff = loadLatestHandoff(hippoRoot, resolveTenantId({}), sessionId);
2725
2727
  if (!handoff) {
2726
2728
  if (flags['json']) {
2727
2729
  console.log(JSON.stringify({ handoff: null }));
@@ -2749,7 +2751,7 @@ function cmdHandoff(hippoRoot, args, flags) {
2749
2751
  console.error(`Invalid handoff ID: ${idArg}`);
2750
2752
  process.exit(1);
2751
2753
  }
2752
- const handoff = loadHandoffById(hippoRoot, handoffId);
2754
+ const handoff = loadHandoffById(hippoRoot, resolveTenantId({}), handoffId);
2753
2755
  if (!handoff) {
2754
2756
  if (flags['json']) {
2755
2757
  console.log(JSON.stringify({ handoff: null }));
@@ -2774,9 +2776,9 @@ function cmdCurrent(hippoRoot, args, flags) {
2774
2776
  const subcommand = args[0] ?? 'show';
2775
2777
  if (subcommand === 'show') {
2776
2778
  const asJson = Boolean(flags['json']);
2777
- const snapshot = loadActiveTaskSnapshot(hippoRoot);
2779
+ const snapshot = loadActiveTaskSnapshot(hippoRoot, resolveTenantId({}));
2778
2780
  const sessionId = snapshot?.session_id ?? undefined;
2779
- const events = listSessionEvents(hippoRoot, {
2781
+ const events = listSessionEvents(hippoRoot, resolveTenantId({}), {
2780
2782
  session_id: sessionId,
2781
2783
  limit: 5,
2782
2784
  });
@@ -2865,12 +2867,12 @@ async function cmdContext(hippoRoot, args, flags) {
2865
2867
  let totalTokens = 0;
2866
2868
  // Task snapshots / session events live in the local store. Skip when
2867
2869
  // local isn't initialized — loading would auto-create .hippo in the cwd.
2868
- const activeSnapshot = hasLocal ? loadActiveTaskSnapshot(hippoRoot) : null;
2870
+ const activeSnapshot = hasLocal ? loadActiveTaskSnapshot(hippoRoot, resolveTenantId({})) : null;
2869
2871
  const sessionHandoff = hasLocal && activeSnapshot?.session_id
2870
- ? loadLatestHandoff(hippoRoot, activeSnapshot.session_id)
2872
+ ? loadLatestHandoff(hippoRoot, resolveTenantId({}), activeSnapshot.session_id)
2871
2873
  : null;
2872
2874
  const recentSessionEvents = hasLocal && activeSnapshot?.session_id
2873
- ? listSessionEvents(hippoRoot, { session_id: activeSnapshot.session_id, limit: 5 })
2875
+ ? listSessionEvents(hippoRoot, resolveTenantId({}), { session_id: activeSnapshot.session_id, limit: 5 })
2874
2876
  : [];
2875
2877
  if (localEntries.length === 0 && globalEntries.length === 0 && !activeSnapshot && !sessionHandoff && recentSessionEvents.length === 0) {
2876
2878
  return;
@@ -4621,6 +4623,11 @@ Commands:
4621
4623
  --threshold <n> Overlap threshold 0-1 (default: 0.7)
4622
4624
  status Show memory health stats
4623
4625
  audit [--fix] Check memory quality (--fix removes junk)
4626
+ provenance Provenance coverage gate for kind='raw' rows
4627
+ --json Output as JSON
4628
+ --strict Exit non-zero when coverage < 100%
4629
+ correction-latency Wall-clock lag from receipt to supersession (p50/p95/max)
4630
+ --json Output as JSON
4624
4631
  outcome Apply feedback to last recall
4625
4632
  --good Memories were helpful
4626
4633
  --bad Memories were irrelevant
@@ -5008,6 +5015,62 @@ async function main() {
5008
5015
  }
5009
5016
  break;
5010
5017
  }
5018
+ case 'correction-latency': {
5019
+ requireInit(hippoRoot);
5020
+ const entries = loadAllEntries(hippoRoot);
5021
+ const report = buildCorrectionLatency(entries);
5022
+ if (flags['json']) {
5023
+ console.log(JSON.stringify(report, null, 2));
5024
+ }
5025
+ else if (report.count === 0) {
5026
+ console.log('No supersessions found. Correction latency is undefined.');
5027
+ }
5028
+ else {
5029
+ const fmt = (ms) => {
5030
+ if (ms === null)
5031
+ return 'n/a';
5032
+ if (ms < 1000)
5033
+ return `${ms}ms`;
5034
+ if (ms < 60_000)
5035
+ return `${(ms / 1000).toFixed(1)}s`;
5036
+ if (ms < 3_600_000)
5037
+ return `${(ms / 60_000).toFixed(1)}m`;
5038
+ return `${(ms / 3_600_000).toFixed(1)}h`;
5039
+ };
5040
+ console.log(`Corrections: ${report.count} total (${report.extractionCount} extraction-driven, ${report.manualCount} manual)`);
5041
+ console.log(`Latency p50: ${fmt(report.p50Ms)}, p95: ${fmt(report.p95Ms)}, max: ${fmt(report.maxMs)}`);
5042
+ if (report.extractionCount === 0 && report.manualCount > 0) {
5043
+ console.log(`\nAll ${report.manualCount} corrections were manual supersedes: no measurable observation lag.`);
5044
+ console.log(`To measure latency, route corrections through extraction (set new.extracted_from to the raw receipt).`);
5045
+ }
5046
+ }
5047
+ break;
5048
+ }
5049
+ case 'provenance': {
5050
+ requireInit(hippoRoot);
5051
+ const entries = loadAllEntries(hippoRoot);
5052
+ const coverage = buildProvenanceCoverage(entries);
5053
+ if (flags['json']) {
5054
+ console.log(JSON.stringify(coverage, null, 2));
5055
+ }
5056
+ else if (coverage.rawTotal === 0) {
5057
+ console.log('No kind=raw memories present. Coverage gate trivially satisfied.');
5058
+ }
5059
+ else {
5060
+ const pct = (coverage.coverage * 100).toFixed(1);
5061
+ console.log(`Provenance coverage: ${coverage.rawWithEnvelope}/${coverage.rawTotal} raw rows envelope-complete (${pct}%)`);
5062
+ if (coverage.gaps.length > 0) {
5063
+ console.log(`\nGaps:`);
5064
+ for (const g of coverage.gaps) {
5065
+ console.log(` ${g.id}: missing ${g.missing.join(', ')}`);
5066
+ }
5067
+ }
5068
+ }
5069
+ if (flags['strict'] && coverage.coverage < 1) {
5070
+ process.exit(1);
5071
+ }
5072
+ break;
5073
+ }
5011
5074
  case 'status':
5012
5075
  cmdStatus(hippoRoot);
5013
5076
  break;