hippo-memory 0.36.0 → 0.38.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 (205) hide show
  1. package/README.md +62 -254
  2. package/dist/api.d.ts +20 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +23 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/benchmarks/e1.3/incident-recall-eval.js +74 -0
  7. package/dist/benchmarks/e1.3/incident-recall-eval.js.map +1 -0
  8. package/dist/benchmarks/e1.3/scenarios.json +2587 -0
  9. package/dist/benchmarks/e1.3/slack-1000-event-smoke.js +102 -0
  10. package/dist/benchmarks/e1.3/slack-1000-event-smoke.js.map +1 -0
  11. package/dist/cli.js +449 -0
  12. package/dist/cli.js.map +1 -1
  13. package/dist/connectors/slack/backfill.d.ts +42 -0
  14. package/dist/connectors/slack/backfill.d.ts.map +1 -0
  15. package/dist/connectors/slack/backfill.js +76 -0
  16. package/dist/connectors/slack/backfill.js.map +1 -0
  17. package/dist/connectors/slack/deletion.d.ts +14 -0
  18. package/dist/connectors/slack/deletion.d.ts.map +1 -0
  19. package/dist/connectors/slack/deletion.js +46 -0
  20. package/dist/connectors/slack/deletion.js.map +1 -0
  21. package/dist/connectors/slack/dlq.d.ts +21 -0
  22. package/dist/connectors/slack/dlq.d.ts.map +1 -0
  23. package/dist/connectors/slack/dlq.js +23 -0
  24. package/dist/connectors/slack/dlq.js.map +1 -0
  25. package/dist/connectors/slack/idempotency.d.ts +5 -0
  26. package/dist/connectors/slack/idempotency.d.ts.map +1 -0
  27. package/dist/connectors/slack/idempotency.js +13 -0
  28. package/dist/connectors/slack/idempotency.js.map +1 -0
  29. package/dist/connectors/slack/ingest.d.ts +27 -0
  30. package/dist/connectors/slack/ingest.d.ts.map +1 -0
  31. package/dist/connectors/slack/ingest.js +48 -0
  32. package/dist/connectors/slack/ingest.js.map +1 -0
  33. package/dist/connectors/slack/ratelimit.d.ts +9 -0
  34. package/dist/connectors/slack/ratelimit.d.ts.map +1 -0
  35. package/dist/connectors/slack/ratelimit.js +18 -0
  36. package/dist/connectors/slack/ratelimit.js.map +1 -0
  37. package/dist/connectors/slack/scope.d.ts +16 -0
  38. package/dist/connectors/slack/scope.d.ts.map +1 -0
  39. package/dist/connectors/slack/scope.js +13 -0
  40. package/dist/connectors/slack/scope.js.map +1 -0
  41. package/dist/connectors/slack/signature.d.ts +12 -0
  42. package/dist/connectors/slack/signature.d.ts.map +1 -0
  43. package/dist/connectors/slack/signature.js +20 -0
  44. package/dist/connectors/slack/signature.js.map +1 -0
  45. package/dist/connectors/slack/tenant-routing.d.ts +13 -0
  46. package/dist/connectors/slack/tenant-routing.d.ts.map +1 -0
  47. package/dist/connectors/slack/tenant-routing.js +17 -0
  48. package/dist/connectors/slack/tenant-routing.js.map +1 -0
  49. package/dist/connectors/slack/transform.d.ts +20 -0
  50. package/dist/connectors/slack/transform.d.ts.map +1 -0
  51. package/dist/connectors/slack/transform.js +31 -0
  52. package/dist/connectors/slack/transform.js.map +1 -0
  53. package/dist/connectors/slack/types.d.ts +35 -0
  54. package/dist/connectors/slack/types.d.ts.map +1 -0
  55. package/dist/connectors/slack/types.js +23 -0
  56. package/dist/connectors/slack/types.js.map +1 -0
  57. package/dist/connectors/slack/web-client.d.ts +12 -0
  58. package/dist/connectors/slack/web-client.d.ts.map +1 -0
  59. package/dist/connectors/slack/web-client.js +43 -0
  60. package/dist/connectors/slack/web-client.js.map +1 -0
  61. package/dist/db.d.ts.map +1 -1
  62. package/dist/db.js +105 -1
  63. package/dist/db.js.map +1 -1
  64. package/dist/goals.d.ts +73 -0
  65. package/dist/goals.d.ts.map +1 -0
  66. package/dist/goals.js +227 -0
  67. package/dist/goals.js.map +1 -0
  68. package/dist/importers.js +3 -3
  69. package/dist/importers.js.map +1 -1
  70. package/dist/mcp/server.js +1 -1
  71. package/dist/server.d.ts.map +1 -1
  72. package/dist/server.js +174 -2
  73. package/dist/server.js.map +1 -1
  74. package/dist/src/ambient.js +147 -0
  75. package/dist/src/ambient.js.map +1 -0
  76. package/dist/src/api.js +343 -0
  77. package/dist/src/api.js.map +1 -0
  78. package/dist/src/audit.js +152 -0
  79. package/dist/src/audit.js.map +1 -0
  80. package/dist/src/auth.js +65 -0
  81. package/dist/src/auth.js.map +1 -0
  82. package/dist/src/autolearn.js +143 -0
  83. package/dist/src/autolearn.js.map +1 -0
  84. package/dist/src/capture.js +512 -0
  85. package/dist/src/capture.js.map +1 -0
  86. package/dist/src/cli.js +5338 -0
  87. package/dist/src/cli.js.map +1 -0
  88. package/dist/src/client.js +181 -0
  89. package/dist/src/client.js.map +1 -0
  90. package/dist/src/config.js +108 -0
  91. package/dist/src/config.js.map +1 -0
  92. package/dist/src/connectors/slack/backfill.js +76 -0
  93. package/dist/src/connectors/slack/backfill.js.map +1 -0
  94. package/dist/src/connectors/slack/deletion.js +46 -0
  95. package/dist/src/connectors/slack/deletion.js.map +1 -0
  96. package/dist/src/connectors/slack/dlq.js +23 -0
  97. package/dist/src/connectors/slack/dlq.js.map +1 -0
  98. package/dist/src/connectors/slack/idempotency.js +13 -0
  99. package/dist/src/connectors/slack/idempotency.js.map +1 -0
  100. package/dist/src/connectors/slack/ingest.js +48 -0
  101. package/dist/src/connectors/slack/ingest.js.map +1 -0
  102. package/dist/src/connectors/slack/ratelimit.js +18 -0
  103. package/dist/src/connectors/slack/ratelimit.js.map +1 -0
  104. package/dist/src/connectors/slack/scope.js +13 -0
  105. package/dist/src/connectors/slack/scope.js.map +1 -0
  106. package/dist/src/connectors/slack/signature.js +20 -0
  107. package/dist/src/connectors/slack/signature.js.map +1 -0
  108. package/dist/src/connectors/slack/tenant-routing.js +17 -0
  109. package/dist/src/connectors/slack/tenant-routing.js.map +1 -0
  110. package/dist/src/connectors/slack/transform.js +31 -0
  111. package/dist/src/connectors/slack/transform.js.map +1 -0
  112. package/dist/src/connectors/slack/types.js +23 -0
  113. package/dist/src/connectors/slack/types.js.map +1 -0
  114. package/dist/src/connectors/slack/web-client.js +43 -0
  115. package/dist/src/connectors/slack/web-client.js.map +1 -0
  116. package/dist/src/consolidate.js +517 -0
  117. package/dist/src/consolidate.js.map +1 -0
  118. package/dist/src/dag.js +104 -0
  119. package/dist/src/dag.js.map +1 -0
  120. package/dist/src/dashboard.js +409 -0
  121. package/dist/src/dashboard.js.map +1 -0
  122. package/dist/src/db.js +643 -0
  123. package/dist/src/db.js.map +1 -0
  124. package/dist/src/embeddings.js +344 -0
  125. package/dist/src/embeddings.js.map +1 -0
  126. package/dist/src/eval-suite.js +289 -0
  127. package/dist/src/eval-suite.js.map +1 -0
  128. package/dist/src/eval.js +187 -0
  129. package/dist/src/eval.js.map +1 -0
  130. package/dist/src/extract.js +87 -0
  131. package/dist/src/extract.js.map +1 -0
  132. package/dist/src/goals.js +227 -0
  133. package/dist/src/goals.js.map +1 -0
  134. package/dist/src/handoff.js +30 -0
  135. package/dist/src/handoff.js.map +1 -0
  136. package/dist/src/hooks.js +582 -0
  137. package/dist/src/hooks.js.map +1 -0
  138. package/dist/src/importers.js +399 -0
  139. package/dist/src/importers.js.map +1 -0
  140. package/dist/src/index.js +25 -0
  141. package/dist/src/index.js.map +1 -0
  142. package/dist/src/invalidation.js +94 -0
  143. package/dist/src/invalidation.js.map +1 -0
  144. package/dist/src/mcp/framing.js +45 -0
  145. package/dist/src/mcp/framing.js.map +1 -0
  146. package/dist/src/mcp/server.js +510 -0
  147. package/dist/src/mcp/server.js.map +1 -0
  148. package/dist/src/memory.js +280 -0
  149. package/dist/src/memory.js.map +1 -0
  150. package/dist/src/multihop.js +32 -0
  151. package/dist/src/multihop.js.map +1 -0
  152. package/dist/src/path-context.js +32 -0
  153. package/dist/src/path-context.js.map +1 -0
  154. package/dist/src/physics-config.js +26 -0
  155. package/dist/src/physics-config.js.map +1 -0
  156. package/dist/src/physics-state.js +163 -0
  157. package/dist/src/physics-state.js.map +1 -0
  158. package/dist/src/physics.js +361 -0
  159. package/dist/src/physics.js.map +1 -0
  160. package/dist/src/postinstall.js +68 -0
  161. package/dist/src/postinstall.js.map +1 -0
  162. package/dist/src/raw-archive.js +72 -0
  163. package/dist/src/raw-archive.js.map +1 -0
  164. package/dist/src/refine-llm.js +147 -0
  165. package/dist/src/refine-llm.js.map +1 -0
  166. package/dist/src/replay.js +117 -0
  167. package/dist/src/replay.js.map +1 -0
  168. package/dist/src/salience.js +74 -0
  169. package/dist/src/salience.js.map +1 -0
  170. package/dist/src/scheduler.js +67 -0
  171. package/dist/src/scheduler.js.map +1 -0
  172. package/dist/src/scope.js +35 -0
  173. package/dist/src/scope.js.map +1 -0
  174. package/dist/src/search.js +801 -0
  175. package/dist/src/search.js.map +1 -0
  176. package/dist/src/server-detect.js +70 -0
  177. package/dist/src/server-detect.js.map +1 -0
  178. package/dist/src/server.js +784 -0
  179. package/dist/src/server.js.map +1 -0
  180. package/dist/src/shared.js +309 -0
  181. package/dist/src/shared.js.map +1 -0
  182. package/dist/src/sso.js +22 -0
  183. package/dist/src/sso.js.map +1 -0
  184. package/dist/src/store.js +1390 -0
  185. package/dist/src/store.js.map +1 -0
  186. package/dist/src/tenant.js +17 -0
  187. package/dist/src/tenant.js.map +1 -0
  188. package/dist/src/trace.js +64 -0
  189. package/dist/src/trace.js.map +1 -0
  190. package/dist/src/working-memory.js +149 -0
  191. package/dist/src/working-memory.js.map +1 -0
  192. package/dist/src/yaml.js +98 -0
  193. package/dist/src/yaml.js.map +1 -0
  194. package/dist/store.d.ts +9 -1
  195. package/dist/store.d.ts.map +1 -1
  196. package/dist/store.js +30 -2
  197. package/dist/store.js.map +1 -1
  198. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  199. package/extensions/openclaw-plugin/package.json +1 -1
  200. package/openclaw.plugin.json +1 -1
  201. package/package.json +2 -2
  202. package/dist/import.d.ts +0 -31
  203. package/dist/import.d.ts.map +0 -1
  204. package/dist/import.js +0 -307
  205. package/dist/import.js.map +0 -1
@@ -0,0 +1,102 @@
1
+ /**
2
+ * E1.3 1000-event Slack smoke benchmark.
3
+ *
4
+ * ROADMAP success criterion: ingest 1000 synthetic Slack events, replay each
5
+ * one twice, and prove (a) exactly `count` unique memories are written
6
+ * (idempotency holds), (b) zero outbound HTTP calls escape the connector
7
+ * layer, (c) per-call latency stays under 500ms.
8
+ *
9
+ * The no-outbound-HTTP invariant is proven (review patch #5) by replacing
10
+ * `globalThis.fetch` with a function that THROWS on any call. If a future
11
+ * code path inside the connector ever reaches for fetch (e.g. a user
12
+ * resolution shortcut), this smoke fails loud rather than silently lying
13
+ * with a hardcoded 0. The original fetch is restored in `finally` so the
14
+ * spy never poisons later tests.
15
+ */
16
+ import { ingestMessage } from '../../src/connectors/slack/ingest.js';
17
+ import { loadAllEntries } from '../../src/store.js';
18
+ export async function runSlackSmoke(opts) {
19
+ const ctx = {
20
+ hippoRoot: opts.hippoRoot,
21
+ tenantId: 'default',
22
+ actor: 'connector:slack',
23
+ };
24
+ const start = Date.now();
25
+ let calls = 0;
26
+ // Review patch #5: spy globalThis.fetch with a throwing function. The smoke
27
+ // fails LOUD if anything in the ingest path ever calls fetch().
28
+ const origFetch = globalThis.fetch;
29
+ let outboundHttp = 0;
30
+ globalThis.fetch = ((..._args) => {
31
+ outboundHttp++;
32
+ throw new Error('outbound HTTP forbidden during slack smoke');
33
+ });
34
+ try {
35
+ for (let pass = 0; pass < opts.replay; pass++) {
36
+ for (let i = 0; i < opts.count; i++) {
37
+ ingestMessage(ctx, {
38
+ teamId: 'T1',
39
+ channel: { id: 'C1', is_private: false },
40
+ message: {
41
+ type: 'message',
42
+ channel: 'C1',
43
+ user: 'U1',
44
+ // Use a content body well over the 3-char minimum enforced by
45
+ // memory.ts so synthetic events survive validation.
46
+ text: `slack synthetic event ${i}`,
47
+ ts: `${1700000000 + i}.000100`,
48
+ },
49
+ eventId: `Ev-${i}`,
50
+ });
51
+ calls++;
52
+ }
53
+ }
54
+ }
55
+ finally {
56
+ globalThis.fetch = origFetch;
57
+ }
58
+ const uniqueMemories = loadAllEntries(opts.hippoRoot).filter((e) => e.tags.includes('source:slack')).length;
59
+ return {
60
+ uniqueMemories,
61
+ totalIngestCalls: calls,
62
+ outboundHttp,
63
+ durationMs: Date.now() - start,
64
+ };
65
+ }
66
+ // Standalone CLI: 1000 events, replay=2, fail loud on regression.
67
+ // Use pathToFileURL for cross-platform `import.meta.url` matching (Windows
68
+ // emits `file:///C:/...` which `file://${process.argv[1]}` cannot match).
69
+ const { pathToFileURL } = await import('url');
70
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
71
+ const { mkdtempSync, rmSync } = await import('fs');
72
+ const { tmpdir } = await import('os');
73
+ const { join } = await import('path');
74
+ const { initStore } = await import('../../src/store.js');
75
+ const root = mkdtempSync(join(tmpdir(), 'hippo-slack-smoke-cli-'));
76
+ try {
77
+ initStore(root);
78
+ const r = await runSlackSmoke({ hippoRoot: root, count: 1000, replay: 2 });
79
+ console.log(JSON.stringify(r, null, 2));
80
+ if (r.uniqueMemories !== 1000) {
81
+ console.error('FAIL: uniqueMemories !== 1000');
82
+ process.exit(1);
83
+ }
84
+ if (r.outboundHttp !== 0) {
85
+ console.error('FAIL: outboundHttp !== 0');
86
+ process.exit(1);
87
+ }
88
+ if (r.totalIngestCalls !== 2000) {
89
+ console.error('FAIL: totalIngestCalls !== 2000');
90
+ process.exit(1);
91
+ }
92
+ if (r.durationMs / r.totalIngestCalls > 500) {
93
+ console.error('FAIL: per-call latency >500ms');
94
+ process.exit(1);
95
+ }
96
+ console.log('PASS');
97
+ }
98
+ finally {
99
+ rmSync(root, { recursive: true, force: true });
100
+ }
101
+ }
102
+ //# sourceMappingURL=slack-1000-event-smoke.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-1000-event-smoke.js","sourceRoot":"","sources":["../../../benchmarks/e1.3/slack-1000-event-smoke.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -41,6 +41,8 @@ import { loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
41
41
  import { computeSystemEnergy, vecNorm } from './physics.js';
42
42
  import { loadConfig } from './config.js';
43
43
  import { openHippoDb, closeHippoDb } from './db.js';
44
+ import { getActiveGoalsWithDb, MAX_FINAL_MULTIPLIER, pushGoal, getActiveGoals, completeGoal, suspendGoal, resumeGoal } from './goals.js';
45
+ import { rowToGoal } from './goals.js';
44
46
  import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
45
47
  import { extractInvalidationTarget, invalidateMatching } from './invalidation.js';
46
48
  import { extractPathTags } from './path-context.js';
@@ -62,6 +64,9 @@ import { wmPush, wmRead, wmClear, wmFlush } from './working-memory.js';
62
64
  import { multihopSearch } from './multihop.js';
63
65
  import { computeSalience } from './salience.js';
64
66
  import { computeAmbientState, renderAmbientSummary } from './ambient.js';
67
+ import { listDlq } from './connectors/slack/dlq.js';
68
+ import { backfillChannel } from './connectors/slack/backfill.js';
69
+ import { slackHistoryFetcher } from './connectors/slack/web-client.js';
65
70
  // ---------------------------------------------------------------------------
66
71
  // Helpers
67
72
  // ---------------------------------------------------------------------------
@@ -817,6 +822,142 @@ async function cmdRecall(hippoRoot, query, flags) {
817
822
  .map((r) => (r.entry.tags?.includes(goalTag) ? { ...r, score: r.score * 1.5 } : r))
818
823
  .sort((a, b) => b.score - a.score);
819
824
  }
825
+ // dlPFC depth (B3, v0.38). When HIPPO_SESSION_ID is set (env or
826
+ // --session-id flag) and the (tenant, session) has active goals, boost
827
+ // memories whose tags overlap any active goal's name. Final multiplier is
828
+ // hard-capped at MAX_FINAL_MULTIPLIER (3.0x). Each boosted (memory, goal)
829
+ // pair is logged into goal_recall_log for outcome propagation.
830
+ //
831
+ // Runs AFTER the explicit `--goal <tag>` block so an explicit flag always
832
+ // wins: if the user passed `--goal X`, this block is skipped entirely
833
+ // (gated on `goalTag === ''`).
834
+ //
835
+ // db-handle note (plan-eng-review fix #5): the surrounding cmdRecall path
836
+ // does NOT keep an open db handle in scope at this point — earlier search
837
+ // helpers (loadSearchEntries, hybridSearch, ...) each open and close their
838
+ // own short-lived handles. Reusing isn't practical here; we open a fresh
839
+ // short-lived handle for this block, mirroring the existing CLI pattern
840
+ // (e.g. emitCliAudit). Closed in `finally`.
841
+ const sessionId = (flags['session-id'] !== undefined
842
+ ? String(flags['session-id'])
843
+ : process.env.HIPPO_SESSION_ID ?? '').trim();
844
+ if (sessionId && goalTag === '') {
845
+ // Use the same tenant as the recall path — see cmdRecall:778.
846
+ const tenantIdForGoals = tenantId;
847
+ const dbForGoals = openHippoDb(hippoRoot);
848
+ try {
849
+ const active = getActiveGoalsWithDb(dbForGoals, {
850
+ sessionId,
851
+ tenantId: tenantIdForGoals,
852
+ });
853
+ if (active.length > 0) {
854
+ const goalsByTag = new Map(active.map((g) => [g.goalName, g]));
855
+ // Task 7: load retrieval_policy rows for active goals so per-policy
856
+ // multipliers can compose onto the base goal-tag boost. The composed
857
+ // result is hard-capped at MAX_FINAL_MULTIPLIER (3.0x) BEFORE applying
858
+ // to score — even an `errorPriority: 9.0` policy cannot exceed 3.0x.
859
+ const policiesByGoalId = new Map();
860
+ for (const g of active) {
861
+ if (!g.retrievalPolicyId)
862
+ continue;
863
+ const row = dbForGoals.prepare(`
864
+ SELECT id, goal_id, policy_type, weight_schema_fit, weight_recency, weight_outcome, error_priority
865
+ FROM retrieval_policy WHERE id = ?
866
+ `).get(g.retrievalPolicyId);
867
+ if (row) {
868
+ policiesByGoalId.set(g.id, {
869
+ id: row.id,
870
+ goalId: row.goal_id,
871
+ policyType: row.policy_type,
872
+ weightSchemaFit: row.weight_schema_fit,
873
+ weightRecency: row.weight_recency,
874
+ weightOutcome: row.weight_outcome,
875
+ errorPriority: row.error_priority,
876
+ });
877
+ }
878
+ }
879
+ results = results
880
+ .map((r) => {
881
+ const tags = r.entry.tags ?? [];
882
+ const matches = tags.filter((t) => goalsByTag.has(t));
883
+ if (matches.length === 0)
884
+ return r;
885
+ // Base 2.0x for first match, +0.5x per additional, capped at 3.0x.
886
+ let multiplier = Math.min(2.0 + 0.5 * (matches.length - 1), MAX_FINAL_MULTIPLIER);
887
+ // Compose per-policy multipliers per matched tag.
888
+ for (const tag of matches) {
889
+ const goal = goalsByTag.get(tag);
890
+ const policy = policiesByGoalId.get(goal.id);
891
+ if (!policy)
892
+ continue;
893
+ if (policy.policyType === 'error-prioritized' && tags.includes('error')) {
894
+ multiplier *= policy.errorPriority;
895
+ }
896
+ else if (policy.policyType === 'schema-fit-biased') {
897
+ // Linearly weight schema_fit in [0,1] up to (weightSchemaFit)x.
898
+ // Default 1.0 is a no-op.
899
+ multiplier *=
900
+ 1.0 +
901
+ Math.max(0, policy.weightSchemaFit - 1.0) *
902
+ (r.entry.schema_fit ?? 0.5);
903
+ }
904
+ else if (policy.policyType === 'recency-first') {
905
+ multiplier *= policy.weightRecency;
906
+ }
907
+ else if (policy.policyType === 'hybrid') {
908
+ multiplier *= policy.weightOutcome;
909
+ }
910
+ }
911
+ // Hard cap AFTER all composition.
912
+ multiplier = Math.min(multiplier, MAX_FINAL_MULTIPLIER);
913
+ return {
914
+ ...r,
915
+ score: r.score * multiplier,
916
+ _goalMatches: matches,
917
+ };
918
+ })
919
+ .sort((a, b) => b.score - a.score);
920
+ // Filter to local memories only — global memory IDs aren't in this
921
+ // DB's memories table, so the FK on goal_recall_log.memory_id would
922
+ // fail. dlPFC depth's outcome propagation is session-scoped to local;
923
+ // boost on ranking still applies to global results, just no log row
924
+ // -> no propagation.
925
+ const topKIds = results.slice(0, limit).map((r) => r.entry.id);
926
+ const localIds = new Set();
927
+ if (topKIds.length > 0) {
928
+ const placeholders = topKIds.map(() => '?').join(',');
929
+ const localRows = dbForGoals.prepare(`SELECT id FROM memories WHERE id IN (${placeholders})`).all(...topKIds);
930
+ for (const row of localRows)
931
+ localIds.add(row.id);
932
+ }
933
+ // Log top-K boosted recalls. INSERT OR IGNORE because
934
+ // UNIQUE(memory_id, goal_id) means a re-recall during the same goal
935
+ // life is a no-op for outcome attribution.
936
+ const recalledAt = new Date().toISOString();
937
+ const insertLog = dbForGoals.prepare(`
938
+ INSERT OR IGNORE INTO goal_recall_log
939
+ (goal_id, memory_id, tenant_id, session_id, recalled_at, score)
940
+ VALUES (?, ?, ?, ?, ?, ?)
941
+ `);
942
+ for (const r of results.slice(0, limit)) {
943
+ if (!localIds.has(r.entry.id))
944
+ continue; // global -> skip log insert
945
+ const matches = r._goalMatches;
946
+ if (!matches || matches.length === 0)
947
+ continue;
948
+ for (const tag of matches) {
949
+ const goal = goalsByTag.get(tag);
950
+ if (!goal)
951
+ continue;
952
+ insertLog.run(goal.id, r.entry.id, tenantIdForGoals, sessionId, recalledAt, r.score);
953
+ }
954
+ }
955
+ }
956
+ }
957
+ finally {
958
+ closeHippoDb(dbForGoals);
959
+ }
960
+ }
820
961
  // Pineal salience MVP (RESEARCH.md §"AI Pineal Gland — Intuition and Awareness
821
962
  // Module"). When --salience-threshold T is set (T > 0), memories whose
822
963
  // retrieval_count is below T are downweighted: score *= max(0.5, count / T).
@@ -4027,6 +4168,211 @@ function cmdAuditLog(hippoRoot, args, flags) {
4027
4168
  console.error(`Unknown audit subcommand: ${sub}. Expected: list.`);
4028
4169
  process.exit(1);
4029
4170
  }
4171
+ // ---------------------------------------------------------------------------
4172
+ // `hippo goal <push|list|complete|suspend|resume>` — B3 dlPFC depth (Task 10)
4173
+ // ---------------------------------------------------------------------------
4174
+ const GOAL_POLICY_TYPES = [
4175
+ 'schema-fit-biased',
4176
+ 'error-prioritized',
4177
+ 'recency-first',
4178
+ 'hybrid',
4179
+ ];
4180
+ function sanitizeGoalName(s) {
4181
+ // Strip C0 control chars + DEL to prevent terminal escape injection.
4182
+ return s.replace(/[\x00-\x1f\x7f]/g, '?');
4183
+ }
4184
+ function resolveGoalSession(flags) {
4185
+ const sessionId = (flags['session-id'] !== undefined
4186
+ ? String(flags['session-id'])
4187
+ : process.env.HIPPO_SESSION_ID ?? '').trim();
4188
+ if (!sessionId) {
4189
+ console.error('session id required (set HIPPO_SESSION_ID or pass --session-id)');
4190
+ process.exit(1);
4191
+ }
4192
+ const tenantId = (flags['tenant-id'] !== undefined
4193
+ ? String(flags['tenant-id'])
4194
+ : process.env.HIPPO_TENANT ?? 'default').trim() || 'default';
4195
+ return { sessionId, tenantId };
4196
+ }
4197
+ function cmdGoalPush(hippoRoot, args, flags) {
4198
+ const rawName = args.join(' ').trim();
4199
+ if (!rawName) {
4200
+ console.error('Usage: hippo goal push <name> [--policy <type>] [--success "<condition>"] [--level N] [--parent <goalId>]');
4201
+ process.exit(1);
4202
+ }
4203
+ // Sanitize at WRITE time so corrupt names never enter the DB.
4204
+ const name = sanitizeGoalName(rawName);
4205
+ if (name !== rawName) {
4206
+ console.error('note: stripped control characters from goal name');
4207
+ }
4208
+ const { sessionId, tenantId } = resolveGoalSession(flags);
4209
+ let policy;
4210
+ const policyRaw = flags['policy'];
4211
+ if (policyRaw === true) {
4212
+ console.error('--policy requires a value (e.g., --policy error-prioritized)');
4213
+ process.exit(1);
4214
+ }
4215
+ if (typeof policyRaw === 'string') {
4216
+ if (!GOAL_POLICY_TYPES.includes(policyRaw)) {
4217
+ console.error(`Unknown --policy '${policyRaw}'. Expected one of: ${GOAL_POLICY_TYPES.join(' | ')}.`);
4218
+ process.exit(1);
4219
+ }
4220
+ policy = { policyType: policyRaw };
4221
+ }
4222
+ const successRaw = flags['success'];
4223
+ if (successRaw === true) {
4224
+ console.error('--success requires a value (e.g., --success "<condition>")');
4225
+ process.exit(1);
4226
+ }
4227
+ const successCondition = typeof successRaw === 'string' ? successRaw : undefined;
4228
+ const levelRaw = flags['level'];
4229
+ let level;
4230
+ if (levelRaw === true) {
4231
+ console.error('--level requires a value (e.g., --level 1)');
4232
+ process.exit(1);
4233
+ }
4234
+ if (levelRaw !== undefined) {
4235
+ const parsed = Number(levelRaw);
4236
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 2 || !Number.isInteger(parsed)) {
4237
+ console.error('--level must be an integer in [0, 2]');
4238
+ process.exit(1);
4239
+ }
4240
+ level = parsed;
4241
+ }
4242
+ const parentRaw = flags['parent'];
4243
+ if (parentRaw === true) {
4244
+ console.error('--parent requires a value (e.g., --parent <goalId>)');
4245
+ process.exit(1);
4246
+ }
4247
+ const parentGoalId = typeof parentRaw === 'string' ? parentRaw : undefined;
4248
+ const goal = pushGoal(hippoRoot, {
4249
+ sessionId,
4250
+ tenantId,
4251
+ goalName: name,
4252
+ level,
4253
+ parentGoalId,
4254
+ successCondition,
4255
+ policy,
4256
+ });
4257
+ console.log(goal.id);
4258
+ }
4259
+ function listAllGoals(hippoRoot, sessionId, tenantId) {
4260
+ const db = openHippoDb(hippoRoot);
4261
+ try {
4262
+ const rows = db.prepare(`
4263
+ SELECT id, session_id, tenant_id, goal_name, level, parent_goal_id, status,
4264
+ success_condition, retrieval_policy_id, created_at, completed_at, outcome_score
4265
+ FROM goal_stack
4266
+ WHERE tenant_id = ? AND session_id = ?
4267
+ ORDER BY created_at ASC
4268
+ `).all(tenantId, sessionId);
4269
+ return rows.map(rowToGoal);
4270
+ }
4271
+ finally {
4272
+ closeHippoDb(db);
4273
+ }
4274
+ }
4275
+ function cmdGoalList(hippoRoot, flags) {
4276
+ const { sessionId, tenantId } = resolveGoalSession(flags);
4277
+ const showAll = Boolean(flags['all']);
4278
+ const goals = showAll
4279
+ ? listAllGoals(hippoRoot, sessionId, tenantId)
4280
+ : getActiveGoals(hippoRoot, { sessionId, tenantId });
4281
+ if (goals.length === 0) {
4282
+ console.log('(no goals)');
4283
+ return;
4284
+ }
4285
+ // 4-column table: id, status, goal_name, outcome. Plan calls it a "2-column"
4286
+ // table but the assertion list (id, status, goal_name, outcome) needs four;
4287
+ // tests check for substrings ('active', '0.9', name) so column count is
4288
+ // observably four but not asserted.
4289
+ const rows = goals.map(g => ({
4290
+ id: g.id,
4291
+ status: g.status,
4292
+ name: sanitizeGoalName(g.goalName),
4293
+ outcome: g.outcomeScore !== undefined ? g.outcomeScore.toString() : '-',
4294
+ }));
4295
+ const widths = {
4296
+ id: Math.max(2, ...rows.map(r => r.id.length)),
4297
+ status: Math.max(6, ...rows.map(r => r.status.length)),
4298
+ name: Math.max(4, ...rows.map(r => r.name.length)),
4299
+ outcome: Math.max(7, ...rows.map(r => r.outcome.length)),
4300
+ };
4301
+ const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
4302
+ console.log(`${pad('id', widths.id)} ${pad('status', widths.status)} ${pad('name', widths.name)} ${pad('outcome', widths.outcome)}`);
4303
+ for (const r of rows) {
4304
+ console.log(`${pad(r.id, widths.id)} ${pad(r.status, widths.status)} ${pad(r.name, widths.name)} ${pad(r.outcome, widths.outcome)}`);
4305
+ }
4306
+ }
4307
+ function cmdGoalComplete(hippoRoot, args, flags) {
4308
+ const id = args[0];
4309
+ if (!id) {
4310
+ console.error('Usage: hippo goal complete <id> [--outcome <0..1>]');
4311
+ process.exit(1);
4312
+ }
4313
+ let outcomeScore;
4314
+ const outcomeRaw = flags['outcome'];
4315
+ if (outcomeRaw === true) {
4316
+ console.error('--outcome requires a value (e.g., --outcome 0.9)');
4317
+ process.exit(1);
4318
+ }
4319
+ if (outcomeRaw !== undefined) {
4320
+ const parsed = Number(outcomeRaw);
4321
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
4322
+ console.error('--outcome must be a number in [0, 1]');
4323
+ process.exit(1);
4324
+ }
4325
+ outcomeScore = parsed;
4326
+ }
4327
+ completeGoal(hippoRoot, id, { outcomeScore });
4328
+ console.log('ok');
4329
+ }
4330
+ function cmdGoalSuspend(hippoRoot, args) {
4331
+ const id = args[0];
4332
+ if (!id) {
4333
+ console.error('Usage: hippo goal suspend <id>');
4334
+ process.exit(1);
4335
+ }
4336
+ suspendGoal(hippoRoot, id);
4337
+ console.log('ok');
4338
+ }
4339
+ function cmdGoalResume(hippoRoot, args) {
4340
+ const id = args[0];
4341
+ if (!id) {
4342
+ console.error('Usage: hippo goal resume <id>');
4343
+ process.exit(1);
4344
+ }
4345
+ resumeGoal(hippoRoot, id);
4346
+ console.log('ok');
4347
+ }
4348
+ function cmdGoal(hippoRoot, args, flags) {
4349
+ const sub = args[0];
4350
+ if (!sub) {
4351
+ console.error('Usage: hippo goal <push|list|complete|suspend|resume> [args]');
4352
+ process.exit(1);
4353
+ }
4354
+ const subArgs = args.slice(1);
4355
+ switch (sub) {
4356
+ case 'push':
4357
+ cmdGoalPush(hippoRoot, subArgs, flags);
4358
+ return;
4359
+ case 'list':
4360
+ cmdGoalList(hippoRoot, flags);
4361
+ return;
4362
+ case 'complete':
4363
+ cmdGoalComplete(hippoRoot, subArgs, flags);
4364
+ return;
4365
+ case 'suspend':
4366
+ cmdGoalSuspend(hippoRoot, subArgs);
4367
+ return;
4368
+ case 'resume':
4369
+ cmdGoalResume(hippoRoot, subArgs);
4370
+ return;
4371
+ default:
4372
+ console.error(`Unknown goal subcommand: ${sub}. Expected: push | list | complete | suspend | resume.`);
4373
+ process.exit(1);
4374
+ }
4375
+ }
4030
4376
  function cmdAuth(hippoRoot, args, flags) {
4031
4377
  const sub = args[0];
4032
4378
  if (!sub) {
@@ -4055,6 +4401,82 @@ function cmdAuth(hippoRoot, args, flags) {
4055
4401
  process.exit(1);
4056
4402
  }
4057
4403
  }
4404
+ // ---------------------------------------------------------------------------
4405
+ // Slack subcommands (E1.3 — `hippo slack backfill` / `hippo slack dlq list`)
4406
+ // ---------------------------------------------------------------------------
4407
+ function printSlackBackfillUsage() {
4408
+ console.log('hippo slack backfill --channel <id> [--since ISO]');
4409
+ console.log(' --channel Slack channel id (required, e.g. C0123ABC)');
4410
+ console.log(' --since backfill from ISO timestamp (default: cursor)');
4411
+ }
4412
+ function cmdSlackBackfill(hippoRoot, flags) {
4413
+ // M3: detect --help BEFORE token check so operators can read usage in
4414
+ // environments without SLACK_BOT_TOKEN configured.
4415
+ if (flags['help']) {
4416
+ printSlackBackfillUsage();
4417
+ return;
4418
+ }
4419
+ const channel = typeof flags['channel'] === 'string' ? flags['channel'] : undefined;
4420
+ if (!channel) {
4421
+ printSlackBackfillUsage();
4422
+ process.exit(1);
4423
+ }
4424
+ // Real fetcher requires SLACK_BOT_TOKEN with channels:history scope.
4425
+ const token = process.env.SLACK_BOT_TOKEN;
4426
+ if (!token) {
4427
+ console.error('SLACK_BOT_TOKEN is not set. Backfill requires a Slack bot token with channels:history scope.');
4428
+ process.exit(2);
4429
+ }
4430
+ // --since is advisory in V1: the slack_cursors row drives resume, so the
4431
+ // backfill loop always picks up where it last left off. Honoured-by-cursor
4432
+ // semantics keep idempotency clean.
4433
+ const sinceIso = flags['since'];
4434
+ void sinceIso;
4435
+ const fetcher = slackHistoryFetcher(token);
4436
+ const ctx = {
4437
+ hippoRoot,
4438
+ tenantId: process.env.HIPPO_TENANT ?? 'default',
4439
+ actor: 'cli:slack-backfill',
4440
+ };
4441
+ backfillChannel(ctx, {
4442
+ teamId: process.env.SLACK_TEAM_ID ?? 'T_UNKNOWN',
4443
+ channel: { id: channel, is_private: false },
4444
+ fetcher,
4445
+ })
4446
+ .then((r) => {
4447
+ console.log(`backfill ${channel}: ${r.ingested} new messages across ${r.pages} pages`);
4448
+ })
4449
+ .catch((e) => {
4450
+ console.error('backfill failed:', e.message);
4451
+ process.exit(3);
4452
+ });
4453
+ }
4454
+ function cmdSlackDlqList(hippoRoot, _flags) {
4455
+ const db = openHippoDb(hippoRoot);
4456
+ try {
4457
+ const tenantId = process.env.HIPPO_TENANT ?? 'default';
4458
+ const items = listDlq(db, { tenantId });
4459
+ for (const it of items) {
4460
+ console.log(`${it.id}\t${it.receivedAt}\t${it.error}`);
4461
+ }
4462
+ }
4463
+ finally {
4464
+ closeHippoDb(db);
4465
+ }
4466
+ }
4467
+ function cmdSlack(hippoRoot, args, flags) {
4468
+ const sub = args[0];
4469
+ if (sub === 'backfill') {
4470
+ cmdSlackBackfill(hippoRoot, flags);
4471
+ return;
4472
+ }
4473
+ if (sub === 'dlq' && args[1] === 'list') {
4474
+ cmdSlackDlqList(hippoRoot, flags);
4475
+ return;
4476
+ }
4477
+ console.error('Usage: hippo slack <backfill|dlq list> [...]');
4478
+ process.exit(1);
4479
+ }
4058
4480
  function printUsage() {
4059
4481
  console.log(`
4060
4482
  Hippo - biologically-inspired memory system for AI agents
@@ -4106,6 +4528,12 @@ Commands:
4106
4528
  --goal <tag> dlPFC goal-conditioned recall: memories tagged with
4107
4529
  the goal tag get a 1.5x score boost and results are
4108
4530
  re-sorted. Default off. RESEARCH.md §PFC.dlPFC.
4531
+ --session-id <id> Session identifier for dlPFC goal-stack boost.
4532
+ Defaults to \$HIPPO_SESSION_ID. When set and the
4533
+ (tenant, session) has active goals (see
4534
+ 'hippo goal push'), recall auto-boosts memories
4535
+ whose tags match an active goal name. Boost stacks
4536
+ on top of base BM25 score, capped at 3.0x.
4109
4537
  --salience-threshold <n>
4110
4538
  Pineal salience: down-weight memories whose
4111
4539
  retrieval_count is below n. score *= max(0.5,
@@ -4291,6 +4719,21 @@ Commands:
4291
4719
  dashboard Open web dashboard for memory health
4292
4720
  --port <n> Port to serve on (default: 3333)
4293
4721
  mcp Start MCP server (stdio transport)
4722
+ goal <sub> dlPFC goal stack (B3) — scoped per session
4723
+ goal push <name> Push a new active goal; prints the new goal id
4724
+ --policy <type> schema-fit-biased | error-prioritized |
4725
+ recency-first | hybrid
4726
+ --success "<cond>" Optional success condition text
4727
+ --level <n> Goal level (default: 0)
4728
+ --parent <goalId> Parent goal id (for sub-goals)
4729
+ --session-id <s> Override session (defaults to HIPPO_SESSION_ID)
4730
+ --tenant-id <t> Override tenant (defaults to HIPPO_TENANT)
4731
+ goal list Show active goals as a table
4732
+ --all Include suspended/completed goals
4733
+ goal complete <id> Mark a goal completed
4734
+ --outcome <0..1> Outcome score; >=0.7 boosts, <0.3 decays recalled mems
4735
+ goal suspend <id> Move an active goal to suspended
4736
+ goal resume <id> Move a suspended goal back to active (depth-capped)
4294
4737
  auth <sub> Manage API keys (A5 stub auth)
4295
4738
  auth create Mint a new API key (plaintext shown ONCE)
4296
4739
  --label <s> Optional human label
@@ -4488,6 +4931,12 @@ async function main() {
4488
4931
  case 'auth':
4489
4932
  cmdAuth(hippoRoot, args, flags);
4490
4933
  break;
4934
+ case 'goal':
4935
+ cmdGoal(hippoRoot, args, flags);
4936
+ break;
4937
+ case 'slack':
4938
+ cmdSlack(hippoRoot, args, flags);
4939
+ break;
4491
4940
  case 'audit': {
4492
4941
  // `audit list` -> A5 audit-log viewer. Other forms (no sub, --fix) keep
4493
4942
  // the existing memory-quality auditor for backwards compatibility.