hippo-memory 1.44.0 → 1.45.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.
- package/README.md +12 -10
- package/bin/hippo.js +3 -1
- package/dist/api.d.ts +21 -21
- package/dist/api.js +69 -42
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +9 -0
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +2 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +151 -72
- package/dist/client.d.ts +4 -21
- package/dist/client.js +3 -112
- package/dist/config.js +2 -1
- package/dist/consolidate.d.ts +3 -0
- package/dist/consolidate.js +44 -42
- package/dist/dag.d.ts +1 -0
- package/dist/dag.js +10 -5
- package/dist/dashboard.js +7 -3
- package/dist/db.js +12 -23
- package/dist/dedupe.d.ts +1 -0
- package/dist/dedupe.js +4 -7
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +81 -8
- package/dist/extract.d.ts +2 -0
- package/dist/extract.js +9 -4
- package/dist/mcp/server.js +21 -36
- package/dist/memory.d.ts +3 -0
- package/dist/memory.js +7 -1
- package/dist/refine-llm.js +3 -2
- package/dist/rerankers/jev.js +3 -2
- package/dist/rerankers/llm.js +3 -2
- package/dist/search.js +3 -3
- package/dist/server.d.ts +5 -0
- package/dist/server.js +39 -29
- package/dist/shared.js +1 -1
- package/dist/store.d.ts +18 -11
- package/dist/store.js +144 -63
- package/dist/version.d.ts +4 -6
- package/dist/version.js +4 -7
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -3
- package/dist/ablation.d.ts.map +0 -1
- package/dist/ablation.js.map +0 -1
- package/dist/ambient.d.ts.map +0 -1
- package/dist/ambient.js.map +0 -1
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js.map +0 -1
- package/dist/audit-prune.d.ts.map +0 -1
- package/dist/audit-prune.js.map +0 -1
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/autolearn.d.ts.map +0 -1
- package/dist/autolearn.js.map +0 -1
- package/dist/availability.d.ts.map +0 -1
- package/dist/availability.js.map +0 -1
- package/dist/benchmarks/e1.3/incident-recall-eval.js +0 -78
- package/dist/benchmarks/e1.3/incident-recall-eval.js.map +0 -1
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js +0 -103
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js.map +0 -1
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js.map +0 -1
- package/dist/card-detail.d.ts.map +0 -1
- package/dist/card-detail.js.map +0 -1
- package/dist/card.d.ts.map +0 -1
- package/dist/card.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/compare.d.ts.map +0 -1
- package/dist/compare.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/connectors/github/backfill.d.ts.map +0 -1
- package/dist/connectors/github/backfill.js.map +0 -1
- package/dist/connectors/github/cli-impl.d.ts.map +0 -1
- package/dist/connectors/github/cli-impl.js.map +0 -1
- package/dist/connectors/github/deletion.d.ts.map +0 -1
- package/dist/connectors/github/deletion.js.map +0 -1
- package/dist/connectors/github/dlq.d.ts.map +0 -1
- package/dist/connectors/github/dlq.js.map +0 -1
- package/dist/connectors/github/idempotency.d.ts.map +0 -1
- package/dist/connectors/github/idempotency.js.map +0 -1
- package/dist/connectors/github/ingest.d.ts.map +0 -1
- package/dist/connectors/github/ingest.js.map +0 -1
- package/dist/connectors/github/octokit-client.d.ts.map +0 -1
- package/dist/connectors/github/octokit-client.js.map +0 -1
- package/dist/connectors/github/ratelimit.d.ts.map +0 -1
- package/dist/connectors/github/ratelimit.js.map +0 -1
- package/dist/connectors/github/scope.d.ts.map +0 -1
- package/dist/connectors/github/scope.js.map +0 -1
- package/dist/connectors/github/signature.d.ts.map +0 -1
- package/dist/connectors/github/signature.js.map +0 -1
- package/dist/connectors/github/tenant-routing.d.ts.map +0 -1
- package/dist/connectors/github/tenant-routing.js.map +0 -1
- package/dist/connectors/github/transform.d.ts.map +0 -1
- package/dist/connectors/github/transform.js.map +0 -1
- package/dist/connectors/github/types.d.ts.map +0 -1
- package/dist/connectors/github/types.js.map +0 -1
- package/dist/connectors/slack/backfill.d.ts.map +0 -1
- package/dist/connectors/slack/backfill.js.map +0 -1
- package/dist/connectors/slack/deletion.d.ts.map +0 -1
- package/dist/connectors/slack/deletion.js.map +0 -1
- package/dist/connectors/slack/dlq.d.ts.map +0 -1
- package/dist/connectors/slack/dlq.js.map +0 -1
- package/dist/connectors/slack/idempotency.d.ts.map +0 -1
- package/dist/connectors/slack/idempotency.js.map +0 -1
- package/dist/connectors/slack/ingest.d.ts.map +0 -1
- package/dist/connectors/slack/ingest.js.map +0 -1
- package/dist/connectors/slack/ratelimit.d.ts.map +0 -1
- package/dist/connectors/slack/ratelimit.js.map +0 -1
- package/dist/connectors/slack/scope.d.ts.map +0 -1
- package/dist/connectors/slack/scope.js.map +0 -1
- package/dist/connectors/slack/signature.d.ts.map +0 -1
- package/dist/connectors/slack/signature.js.map +0 -1
- package/dist/connectors/slack/tenant-routing.d.ts.map +0 -1
- package/dist/connectors/slack/tenant-routing.js.map +0 -1
- package/dist/connectors/slack/transform.d.ts.map +0 -1
- package/dist/connectors/slack/transform.js.map +0 -1
- package/dist/connectors/slack/types.d.ts.map +0 -1
- package/dist/connectors/slack/types.js.map +0 -1
- package/dist/connectors/slack/web-client.d.ts.map +0 -1
- package/dist/connectors/slack/web-client.js.map +0 -1
- package/dist/connectors/slack/workspaces.d.ts.map +0 -1
- package/dist/connectors/slack/workspaces.js.map +0 -1
- package/dist/consolidate.d.ts.map +0 -1
- package/dist/consolidate.js.map +0 -1
- package/dist/correction-latency.d.ts.map +0 -1
- package/dist/correction-latency.js.map +0 -1
- package/dist/customer-notes.d.ts.map +0 -1
- package/dist/customer-notes.js.map +0 -1
- package/dist/dag.d.ts.map +0 -1
- package/dist/dag.js.map +0 -1
- package/dist/dashboard.d.ts.map +0 -1
- package/dist/dashboard.js.map +0 -1
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js.map +0 -1
- package/dist/decisions.d.ts.map +0 -1
- package/dist/decisions.js.map +0 -1
- package/dist/dedupe.d.ts.map +0 -1
- package/dist/dedupe.js.map +0 -1
- package/dist/embedding-provider.d.ts.map +0 -1
- package/dist/embedding-provider.js.map +0 -1
- package/dist/embeddings.d.ts.map +0 -1
- package/dist/embeddings.js.map +0 -1
- package/dist/eval-suite.d.ts.map +0 -1
- package/dist/eval-suite.js.map +0 -1
- package/dist/eval.d.ts.map +0 -1
- package/dist/eval.js.map +0 -1
- package/dist/extensions/openclaw-plugin/index.js.map +0 -1
- package/dist/extract.d.ts.map +0 -1
- package/dist/extract.js.map +0 -1
- package/dist/forward-claim-detector.d.ts.map +0 -1
- package/dist/forward-claim-detector.js.map +0 -1
- package/dist/goals.d.ts.map +0 -1
- package/dist/goals.js.map +0 -1
- package/dist/graph-extract.d.ts.map +0 -1
- package/dist/graph-extract.js.map +0 -1
- package/dist/graph-recall.d.ts.map +0 -1
- package/dist/graph-recall.js.map +0 -1
- package/dist/graph-stream.d.ts.map +0 -1
- package/dist/graph-stream.js.map +0 -1
- package/dist/graph-view.d.ts.map +0 -1
- package/dist/graph-view.js.map +0 -1
- package/dist/graph.d.ts.map +0 -1
- package/dist/graph.js.map +0 -1
- package/dist/handoff.d.ts.map +0 -1
- package/dist/handoff.js.map +0 -1
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js.map +0 -1
- package/dist/importers.d.ts.map +0 -1
- package/dist/importers.js.map +0 -1
- package/dist/incidents.d.ts.map +0 -1
- package/dist/incidents.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invalidation.d.ts.map +0 -1
- package/dist/invalidation.js.map +0 -1
- package/dist/mcp/framing.d.ts.map +0 -1
- package/dist/mcp/framing.js.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js.map +0 -1
- package/dist/memory-value-weights.d.ts.map +0 -1
- package/dist/memory-value-weights.js.map +0 -1
- package/dist/memory-value.d.ts.map +0 -1
- package/dist/memory-value.js.map +0 -1
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js.map +0 -1
- package/dist/multihop.d.ts.map +0 -1
- package/dist/multihop.js.map +0 -1
- package/dist/owner-validation.d.ts.map +0 -1
- package/dist/owner-validation.js.map +0 -1
- package/dist/path-context.d.ts.map +0 -1
- package/dist/path-context.js.map +0 -1
- package/dist/physics-config.d.ts.map +0 -1
- package/dist/physics-config.js.map +0 -1
- package/dist/physics-state.d.ts.map +0 -1
- package/dist/physics-state.js.map +0 -1
- package/dist/physics.d.ts.map +0 -1
- package/dist/physics.js.map +0 -1
- package/dist/policies.d.ts.map +0 -1
- package/dist/policies.js.map +0 -1
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js.map +0 -1
- package/dist/predictions.d.ts.map +0 -1
- package/dist/predictions.js.map +0 -1
- package/dist/processes.d.ts.map +0 -1
- package/dist/processes.js.map +0 -1
- package/dist/project-briefs.d.ts.map +0 -1
- package/dist/project-briefs.js.map +0 -1
- package/dist/project-identity.d.ts.map +0 -1
- package/dist/project-identity.js.map +0 -1
- package/dist/provenance-coverage.d.ts.map +0 -1
- package/dist/provenance-coverage.js.map +0 -1
- package/dist/rate-limit.d.ts.map +0 -1
- package/dist/rate-limit.js.map +0 -1
- package/dist/raw-archive-mirror-cleanup.d.ts.map +0 -1
- package/dist/raw-archive-mirror-cleanup.js.map +0 -1
- package/dist/raw-archive.d.ts.map +0 -1
- package/dist/raw-archive.js.map +0 -1
- package/dist/recall-history.d.ts.map +0 -1
- package/dist/recall-history.js.map +0 -1
- package/dist/recall-scope.d.ts.map +0 -1
- package/dist/recall-scope.js.map +0 -1
- package/dist/recall-trace.d.ts.map +0 -1
- package/dist/recall-trace.js.map +0 -1
- package/dist/refine-llm.d.ts.map +0 -1
- package/dist/refine-llm.js.map +0 -1
- package/dist/reject-flow.d.ts.map +0 -1
- package/dist/reject-flow.js.map +0 -1
- package/dist/rejection.d.ts.map +0 -1
- package/dist/rejection.js.map +0 -1
- package/dist/replay.d.ts.map +0 -1
- package/dist/replay.js.map +0 -1
- package/dist/rerankers/cross-encoder.d.ts.map +0 -1
- package/dist/rerankers/cross-encoder.js.map +0 -1
- package/dist/rerankers/index.d.ts.map +0 -1
- package/dist/rerankers/index.js.map +0 -1
- package/dist/rerankers/jev.d.ts.map +0 -1
- package/dist/rerankers/jev.js.map +0 -1
- package/dist/rerankers/llm.d.ts.map +0 -1
- package/dist/rerankers/llm.js.map +0 -1
- package/dist/rerankers/types.d.ts.map +0 -1
- package/dist/rerankers/types.js.map +0 -1
- package/dist/rrf.d.ts.map +0 -1
- package/dist/rrf.js.map +0 -1
- package/dist/salience.d.ts.map +0 -1
- package/dist/salience.js.map +0 -1
- package/dist/scheduler.d.ts.map +0 -1
- package/dist/scheduler.js.map +0 -1
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js.map +0 -1
- package/dist/search.d.ts.map +0 -1
- package/dist/search.js.map +0 -1
- package/dist/secret-detect.d.ts.map +0 -1
- package/dist/secret-detect.js.map +0 -1
- package/dist/server-detect.d.ts.map +0 -1
- package/dist/server-detect.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/shared.d.ts.map +0 -1
- package/dist/shared.js.map +0 -1
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/sleep-redact.d.ts +0 -58
- package/dist/sleep-redact.d.ts.map +0 -1
- package/dist/sleep-redact.js +0 -80
- package/dist/sleep-redact.js.map +0 -1
- package/dist/src/ablation.js +0 -138
- package/dist/src/ablation.js.map +0 -1
- package/dist/src/ambient.js +0 -148
- package/dist/src/ambient.js.map +0 -1
- package/dist/src/api.js +0 -2109
- package/dist/src/api.js.map +0 -1
- package/dist/src/audit-prune.js +0 -109
- package/dist/src/audit-prune.js.map +0 -1
- package/dist/src/audit.js +0 -213
- package/dist/src/audit.js.map +0 -1
- package/dist/src/auth.js +0 -97
- package/dist/src/auth.js.map +0 -1
- package/dist/src/autolearn.js +0 -174
- package/dist/src/autolearn.js.map +0 -1
- package/dist/src/availability.js +0 -94
- package/dist/src/availability.js.map +0 -1
- package/dist/src/capture.js +0 -1346
- package/dist/src/capture.js.map +0 -1
- package/dist/src/card-detail.js +0 -15
- package/dist/src/card-detail.js.map +0 -1
- package/dist/src/card.js +0 -19
- package/dist/src/card.js.map +0 -1
- package/dist/src/cli.js +0 -9387
- package/dist/src/cli.js.map +0 -1
- package/dist/src/client.js +0 -237
- package/dist/src/client.js.map +0 -1
- package/dist/src/compare.js +0 -122
- package/dist/src/compare.js.map +0 -1
- package/dist/src/config.js +0 -135
- package/dist/src/config.js.map +0 -1
- package/dist/src/connectors/github/backfill.js +0 -281
- package/dist/src/connectors/github/backfill.js.map +0 -1
- package/dist/src/connectors/github/cli-impl.js +0 -234
- package/dist/src/connectors/github/cli-impl.js.map +0 -1
- package/dist/src/connectors/github/deletion.js +0 -85
- package/dist/src/connectors/github/deletion.js.map +0 -1
- package/dist/src/connectors/github/dlq.js +0 -190
- package/dist/src/connectors/github/dlq.js.map +0 -1
- package/dist/src/connectors/github/idempotency.js +0 -27
- package/dist/src/connectors/github/idempotency.js.map +0 -1
- package/dist/src/connectors/github/ingest.js +0 -156
- package/dist/src/connectors/github/ingest.js.map +0 -1
- package/dist/src/connectors/github/octokit-client.js +0 -70
- package/dist/src/connectors/github/octokit-client.js.map +0 -1
- package/dist/src/connectors/github/ratelimit.js +0 -31
- package/dist/src/connectors/github/ratelimit.js.map +0 -1
- package/dist/src/connectors/github/scope.js +0 -13
- package/dist/src/connectors/github/scope.js.map +0 -1
- package/dist/src/connectors/github/signature.js +0 -81
- package/dist/src/connectors/github/signature.js.map +0 -1
- package/dist/src/connectors/github/tenant-routing.js +0 -69
- package/dist/src/connectors/github/tenant-routing.js.map +0 -1
- package/dist/src/connectors/github/transform.js +0 -103
- package/dist/src/connectors/github/transform.js.map +0 -1
- package/dist/src/connectors/github/types.js +0 -100
- package/dist/src/connectors/github/types.js.map +0 -1
- package/dist/src/connectors/slack/backfill.js +0 -78
- package/dist/src/connectors/slack/backfill.js.map +0 -1
- package/dist/src/connectors/slack/deletion.js +0 -59
- package/dist/src/connectors/slack/deletion.js.map +0 -1
- package/dist/src/connectors/slack/dlq.js +0 -224
- package/dist/src/connectors/slack/dlq.js.map +0 -1
- package/dist/src/connectors/slack/idempotency.js +0 -31
- package/dist/src/connectors/slack/idempotency.js.map +0 -1
- package/dist/src/connectors/slack/ingest.js +0 -109
- package/dist/src/connectors/slack/ingest.js.map +0 -1
- package/dist/src/connectors/slack/ratelimit.js +0 -18
- package/dist/src/connectors/slack/ratelimit.js.map +0 -1
- package/dist/src/connectors/slack/scope.js +0 -13
- package/dist/src/connectors/slack/scope.js.map +0 -1
- package/dist/src/connectors/slack/signature.js +0 -27
- package/dist/src/connectors/slack/signature.js.map +0 -1
- package/dist/src/connectors/slack/tenant-routing.js +0 -41
- package/dist/src/connectors/slack/tenant-routing.js.map +0 -1
- package/dist/src/connectors/slack/transform.js +0 -47
- package/dist/src/connectors/slack/transform.js.map +0 -1
- package/dist/src/connectors/slack/types.js +0 -35
- package/dist/src/connectors/slack/types.js.map +0 -1
- package/dist/src/connectors/slack/web-client.js +0 -43
- package/dist/src/connectors/slack/web-client.js.map +0 -1
- package/dist/src/connectors/slack/workspaces.js +0 -62
- package/dist/src/connectors/slack/workspaces.js.map +0 -1
- package/dist/src/consolidate.js +0 -978
- package/dist/src/consolidate.js.map +0 -1
- package/dist/src/correction-latency.js +0 -74
- package/dist/src/correction-latency.js.map +0 -1
- package/dist/src/customer-notes.js +0 -325
- package/dist/src/customer-notes.js.map +0 -1
- package/dist/src/dag.js +0 -352
- package/dist/src/dag.js.map +0 -1
- package/dist/src/dashboard.js +0 -258
- package/dist/src/dashboard.js.map +0 -1
- package/dist/src/db.js +0 -2731
- package/dist/src/db.js.map +0 -1
- package/dist/src/decisions.js +0 -304
- package/dist/src/decisions.js.map +0 -1
- package/dist/src/dedupe.js +0 -141
- package/dist/src/dedupe.js.map +0 -1
- package/dist/src/embedding-provider.js +0 -314
- package/dist/src/embedding-provider.js.map +0 -1
- package/dist/src/embeddings.js +0 -543
- package/dist/src/embeddings.js.map +0 -1
- package/dist/src/eval-suite.js +0 -294
- package/dist/src/eval-suite.js.map +0 -1
- package/dist/src/eval.js +0 -187
- package/dist/src/eval.js.map +0 -1
- package/dist/src/extract.js +0 -117
- package/dist/src/extract.js.map +0 -1
- package/dist/src/forward-claim-detector.js +0 -117
- package/dist/src/forward-claim-detector.js.map +0 -1
- package/dist/src/goals.js +0 -399
- package/dist/src/goals.js.map +0 -1
- package/dist/src/graph-extract.js +0 -314
- package/dist/src/graph-extract.js.map +0 -1
- package/dist/src/graph-recall.js +0 -277
- package/dist/src/graph-recall.js.map +0 -1
- package/dist/src/graph-stream.js +0 -176
- package/dist/src/graph-stream.js.map +0 -1
- package/dist/src/graph-view.js +0 -310
- package/dist/src/graph-view.js.map +0 -1
- package/dist/src/graph.js +0 -762
- package/dist/src/graph.js.map +0 -1
- package/dist/src/handoff.js +0 -65
- package/dist/src/handoff.js.map +0 -1
- package/dist/src/hooks.js +0 -926
- package/dist/src/hooks.js.map +0 -1
- package/dist/src/importers.js +0 -881
- package/dist/src/importers.js.map +0 -1
- package/dist/src/incidents.js +0 -336
- package/dist/src/incidents.js.map +0 -1
- package/dist/src/index.js +0 -32
- package/dist/src/index.js.map +0 -1
- package/dist/src/invalidation.js +0 -129
- package/dist/src/invalidation.js.map +0 -1
- package/dist/src/mcp/framing.js +0 -45
- package/dist/src/mcp/framing.js.map +0 -1
- package/dist/src/mcp/server.js +0 -1298
- package/dist/src/mcp/server.js.map +0 -1
- package/dist/src/memory-value-weights.js +0 -31
- package/dist/src/memory-value-weights.js.map +0 -1
- package/dist/src/memory-value.js +0 -261
- package/dist/src/memory-value.js.map +0 -1
- package/dist/src/memory.js +0 -425
- package/dist/src/memory.js.map +0 -1
- package/dist/src/multihop.js +0 -35
- package/dist/src/multihop.js.map +0 -1
- package/dist/src/owner-validation.js +0 -56
- package/dist/src/owner-validation.js.map +0 -1
- package/dist/src/path-context.js +0 -48
- package/dist/src/path-context.js.map +0 -1
- package/dist/src/physics-config.js +0 -26
- package/dist/src/physics-config.js.map +0 -1
- package/dist/src/physics-state.js +0 -172
- package/dist/src/physics-state.js.map +0 -1
- package/dist/src/physics.js +0 -378
- package/dist/src/physics.js.map +0 -1
- package/dist/src/policies.js +0 -414
- package/dist/src/policies.js.map +0 -1
- package/dist/src/postinstall.js +0 -101
- package/dist/src/postinstall.js.map +0 -1
- package/dist/src/predictions.js +0 -629
- package/dist/src/predictions.js.map +0 -1
- package/dist/src/processes.js +0 -349
- package/dist/src/processes.js.map +0 -1
- package/dist/src/project-briefs.js +0 -492
- package/dist/src/project-briefs.js.map +0 -1
- package/dist/src/project-identity.js +0 -200
- package/dist/src/project-identity.js.map +0 -1
- package/dist/src/provenance-coverage.js +0 -23
- package/dist/src/provenance-coverage.js.map +0 -1
- package/dist/src/rate-limit.js +0 -60
- package/dist/src/rate-limit.js.map +0 -1
- package/dist/src/raw-archive-mirror-cleanup.js +0 -55
- package/dist/src/raw-archive-mirror-cleanup.js.map +0 -1
- package/dist/src/raw-archive.js +0 -91
- package/dist/src/raw-archive.js.map +0 -1
- package/dist/src/recall-history.js +0 -235
- package/dist/src/recall-history.js.map +0 -1
- package/dist/src/recall-scope.js +0 -89
- package/dist/src/recall-scope.js.map +0 -1
- package/dist/src/recall-trace.js +0 -186
- package/dist/src/recall-trace.js.map +0 -1
- package/dist/src/refine-llm.js +0 -156
- package/dist/src/refine-llm.js.map +0 -1
- package/dist/src/reject-flow.js +0 -209
- package/dist/src/reject-flow.js.map +0 -1
- package/dist/src/rejection.js +0 -160
- package/dist/src/rejection.js.map +0 -1
- package/dist/src/replay.js +0 -122
- package/dist/src/replay.js.map +0 -1
- package/dist/src/rerankers/cross-encoder.js +0 -137
- package/dist/src/rerankers/cross-encoder.js.map +0 -1
- package/dist/src/rerankers/index.js +0 -20
- package/dist/src/rerankers/index.js.map +0 -1
- package/dist/src/rerankers/jev.js +0 -119
- package/dist/src/rerankers/jev.js.map +0 -1
- package/dist/src/rerankers/llm.js +0 -80
- package/dist/src/rerankers/llm.js.map +0 -1
- package/dist/src/rerankers/types.js +0 -2
- package/dist/src/rerankers/types.js.map +0 -1
- package/dist/src/rrf.js +0 -61
- package/dist/src/rrf.js.map +0 -1
- package/dist/src/salience.js +0 -74
- package/dist/src/salience.js.map +0 -1
- package/dist/src/scheduler.js +0 -77
- package/dist/src/scheduler.js.map +0 -1
- package/dist/src/scope.js +0 -35
- package/dist/src/scope.js.map +0 -1
- package/dist/src/search.js +0 -1003
- package/dist/src/search.js.map +0 -1
- package/dist/src/secret-detect.js +0 -95
- package/dist/src/secret-detect.js.map +0 -1
- package/dist/src/server-detect.js +0 -231
- package/dist/src/server-detect.js.map +0 -1
- package/dist/src/server.js +0 -3255
- package/dist/src/server.js.map +0 -1
- package/dist/src/shared.js +0 -502
- package/dist/src/shared.js.map +0 -1
- package/dist/src/skills.js +0 -346
- package/dist/src/skills.js.map +0 -1
- package/dist/src/sleep-redact.js +0 -80
- package/dist/src/sleep-redact.js.map +0 -1
- package/dist/src/sso.js +0 -22
- package/dist/src/sso.js.map +0 -1
- package/dist/src/stdin.js +0 -41
- package/dist/src/stdin.js.map +0 -1
- package/dist/src/store.js +0 -3749
- package/dist/src/store.js.map +0 -1
- package/dist/src/tenant.js +0 -17
- package/dist/src/tenant.js.map +0 -1
- package/dist/src/trace.js +0 -72
- package/dist/src/trace.js.map +0 -1
- package/dist/src/version.js +0 -40
- package/dist/src/version.js.map +0 -1
- package/dist/src/working-memory.js +0 -157
- package/dist/src/working-memory.js.map +0 -1
- package/dist/src/yaml.js +0 -107
- package/dist/src/yaml.js.map +0 -1
- package/dist/sso.d.ts +0 -13
- package/dist/sso.d.ts.map +0 -1
- package/dist/sso.js +0 -22
- package/dist/sso.js.map +0 -1
- package/dist/stdin.d.ts.map +0 -1
- package/dist/stdin.js.map +0 -1
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js.map +0 -1
- package/dist/tenant.d.ts.map +0 -1
- package/dist/tenant.js.map +0 -1
- package/dist/trace.d.ts.map +0 -1
- package/dist/trace.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/working-memory.d.ts.map +0 -1
- package/dist/working-memory.js.map +0 -1
- package/dist/yaml.d.ts.map +0 -1
- package/dist/yaml.js.map +0 -1
package/dist/src/mcp/server.js
DELETED
|
@@ -1,1298 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Hippo Memory MCP Server
|
|
4
|
-
*
|
|
5
|
-
* Exposes hippo memory as MCP tools over stdio transport.
|
|
6
|
-
* Uses the programmatic API directly (no child process spawning).
|
|
7
|
-
*
|
|
8
|
-
* Usage: hippo mcp (or npx hippo-memory mcp)
|
|
9
|
-
*/
|
|
10
|
-
import * as fs from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import { createMemory, Layer, calculateStrength, } from '../memory.js';
|
|
13
|
-
import { hybridSearch, physicsSearch, markRetrieved } from '../search.js';
|
|
14
|
-
import { isRecallBoostAblated, evalNow } from '../ablation.js';
|
|
15
|
-
import { loadAllEntries, writeEntry, readEntry, loadFreshActiveTaskSnapshot, listMemoryConflicts, resolveConflict, RECALL_DEFAULT_DENY_SCOPES } from '../store.js';
|
|
16
|
-
import { shareMemory, listPeers, getGlobalRoot } from '../shared.js';
|
|
17
|
-
import { consolidate } from '../consolidate.js';
|
|
18
|
-
import { execSync } from 'child_process';
|
|
19
|
-
import { fetchGitLog, extractLessons, partitionLessons, deduplicateLesson, isGitRepo } from '../autolearn.js';
|
|
20
|
-
import { loadConfig } from '../config.js';
|
|
21
|
-
import { confidenceLabel } from '../memory.js';
|
|
22
|
-
import { resolveTenantId } from '../tenant.js';
|
|
23
|
-
import { recall as apiRecall, remember as apiRemember, outcome as apiOutcome, drillDown as apiDrillDown, assemble as apiAssemble, isPrivateScope, passesScopeFilterForRecall, adminActor, buildSuppressionSummary, ambientSecretAdmit } from '../api.js';
|
|
24
|
-
import { resolveProjectIdentity, classifyOriginProject, findHippoStoreDir } from '../project-identity.js';
|
|
25
|
-
import { computePredictionBaserate } from '../predictions.js';
|
|
26
|
-
import { appendAuditEvent } from '../audit.js';
|
|
27
|
-
import { RejectedValueError } from '../rejection.js';
|
|
28
|
-
import { createHash } from 'node:crypto';
|
|
29
|
-
import { detectAnchoring, hashQueryText, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from '../recall-history.js';
|
|
30
|
-
import { detectAvailabilityBias } from '../availability.js';
|
|
31
|
-
// v0.33 / J1 — Module-level per-(tenant, session) recall-history ring map
|
|
32
|
-
// for the MCP pipeline. Separate from CLI/HTTP rings per plan v3
|
|
33
|
-
// architecture (per-pipeline rings; no IPC).
|
|
34
|
-
const sessionRecallHistoryMcp = new Map();
|
|
35
|
-
/** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
|
|
36
|
-
export function __resetSessionRecallHistoryMcp() {
|
|
37
|
-
sessionRecallHistoryMcp.clear();
|
|
38
|
-
}
|
|
39
|
-
import { applyGoalStackBoost } from '../goals.js';
|
|
40
|
-
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
41
|
-
import { PACKAGE_VERSION } from '../version.js';
|
|
42
|
-
// ── Find hippo root ──
|
|
43
|
-
/** Same bounded walk as the CLI (ends at home, so HIPPO_HOME wins over ~/.hippo); cwd/opts are the test seam. */
|
|
44
|
-
export function findHippoRoot(cwd = process.cwd(), opts) {
|
|
45
|
-
const local = findHippoStoreDir(cwd, opts);
|
|
46
|
-
if (local !== null)
|
|
47
|
-
return local;
|
|
48
|
-
// Global fallback (respects $HIPPO_HOME / $XDG_DATA_HOME)
|
|
49
|
-
const global = getGlobalRoot();
|
|
50
|
-
return fs.existsSync(global) ? global : null;
|
|
51
|
-
}
|
|
52
|
-
// MCP stdio transport spec: messages are newline-delimited JSON-RPC, no embedded newlines.
|
|
53
|
-
// https://modelcontextprotocol.io/specification/.../basic/transports#stdio
|
|
54
|
-
function send(msg) {
|
|
55
|
-
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
56
|
-
}
|
|
57
|
-
function isJsonString(v) {
|
|
58
|
-
return typeof v === 'string';
|
|
59
|
-
}
|
|
60
|
-
function isJsonBoolean(v) {
|
|
61
|
-
return typeof v === 'boolean';
|
|
62
|
-
}
|
|
63
|
-
function isJsonObjectRecord(v) {
|
|
64
|
-
return v !== undefined && v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
65
|
-
}
|
|
66
|
-
import { formatHandoffEvidenceLine } from '../handoff.js';
|
|
67
|
-
function formatContinuityBlock(block) {
|
|
68
|
-
const lines = ['## Continuity'];
|
|
69
|
-
if (block.activeSnapshot) {
|
|
70
|
-
lines.push('');
|
|
71
|
-
lines.push('### Active Task Snapshot');
|
|
72
|
-
lines.push(`- Task: ${block.activeSnapshot.task}`);
|
|
73
|
-
lines.push(`- Summary: ${block.activeSnapshot.summary}`);
|
|
74
|
-
lines.push(`- Next: ${block.activeSnapshot.next_step}`);
|
|
75
|
-
}
|
|
76
|
-
if (block.sessionHandoff) {
|
|
77
|
-
lines.push('');
|
|
78
|
-
lines.push('### Session Handoff');
|
|
79
|
-
lines.push(`- Summary: ${block.sessionHandoff.summary}`);
|
|
80
|
-
if (block.sessionHandoff.nextAction) {
|
|
81
|
-
lines.push(`- Next action: ${block.sessionHandoff.nextAction}`);
|
|
82
|
-
}
|
|
83
|
-
if ((block.sessionHandoff.artifacts ?? []).length > 0) {
|
|
84
|
-
lines.push(`- Artifacts: ${(block.sessionHandoff.artifacts ?? []).join(', ')}`);
|
|
85
|
-
}
|
|
86
|
-
if (block.sessionHandoff.outcome) {
|
|
87
|
-
lines.push(`- Outcome: ${block.sessionHandoff.outcome}`);
|
|
88
|
-
}
|
|
89
|
-
if (block.sessionHandoff.targetRuntime) {
|
|
90
|
-
lines.push(`- Target runtime: ${block.sessionHandoff.targetRuntime}`);
|
|
91
|
-
}
|
|
92
|
-
if (block.sessionHandoff.cardId) {
|
|
93
|
-
lines.push(`- Card: ${block.sessionHandoff.cardId}`);
|
|
94
|
-
}
|
|
95
|
-
if ((block.sessionHandoff.constraints ?? []).length > 0) {
|
|
96
|
-
lines.push(`- Constraints: ${(block.sessionHandoff.constraints ?? []).join(', ')}`);
|
|
97
|
-
}
|
|
98
|
-
if (block.sessionHandoff.evidence) {
|
|
99
|
-
lines.push(`- Evidence: ${formatHandoffEvidenceLine(block.sessionHandoff.evidence)}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (block.recentSessionEvents.length > 0) {
|
|
103
|
-
lines.push('');
|
|
104
|
-
lines.push('### Recent Session Trail');
|
|
105
|
-
for (const e of block.recentSessionEvents) {
|
|
106
|
-
const preview = e.content.length > 200 ? e.content.slice(0, 200) + '…' : e.content;
|
|
107
|
-
lines.push(`- [${e.event_type}] ${preview}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (lines.length === 1) {
|
|
111
|
-
lines.push('');
|
|
112
|
-
lines.push('(no active task snapshot, handoff, or recent events for this tenant)');
|
|
113
|
-
}
|
|
114
|
-
return lines.join('\n');
|
|
115
|
-
}
|
|
116
|
-
function formatMemories(results, hippoRoot) {
|
|
117
|
-
if (results.length === 0)
|
|
118
|
-
return 'No relevant memories found.';
|
|
119
|
-
const config = loadConfig(hippoRoot);
|
|
120
|
-
const lines = [`Found ${results.length} memories:\n`];
|
|
121
|
-
for (const r of results) {
|
|
122
|
-
const conf = confidenceLabel(r.entry).text;
|
|
123
|
-
const tags = r.entry.tags.length > 0 ? ` tags: ${r.entry.tags.join(', ')}` : '';
|
|
124
|
-
lines.push(`[${conf}]${tags} (strength=${r.entry.strength.toFixed(2)})`);
|
|
125
|
-
lines.push(r.entry.content);
|
|
126
|
-
lines.push('');
|
|
127
|
-
}
|
|
128
|
-
return lines.join('\n');
|
|
129
|
-
}
|
|
130
|
-
// ── Tool definitions ──
|
|
131
|
-
const TOOLS = [
|
|
132
|
-
{
|
|
133
|
-
name: 'hippo_recall',
|
|
134
|
-
description: 'Retrieve relevant memories from the project memory store. Returns memories ranked by relevance, strength, and recency within the token budget. Use at session start or when you need context about a topic. Pass include_continuity=true to also surface the active task snapshot, latest matching session handoff, and recent session events as a "## Continuity" appendix.',
|
|
135
|
-
inputSchema: {
|
|
136
|
-
type: 'object',
|
|
137
|
-
properties: {
|
|
138
|
-
query: { type: 'string', description: 'What to search for in memory (natural language)' },
|
|
139
|
-
budget: { type: 'number', description: 'Max tokens to return (default: 1500)' },
|
|
140
|
-
include_continuity: {
|
|
141
|
-
type: 'boolean',
|
|
142
|
-
description: 'Append continuity context (active snapshot + handoff + last 5 session events) below the memory results. Useful at session boot.',
|
|
143
|
-
},
|
|
144
|
-
scope: {
|
|
145
|
-
type: 'string',
|
|
146
|
-
description: 'Restrict results and continuity to memories/rows matching this scope exactly. When omitted, default-deny applies to ANY <source>:private:* (slack, github, ...) and unknown-legacy rows.',
|
|
147
|
-
},
|
|
148
|
-
fresh_tail_count: {
|
|
149
|
-
type: 'number',
|
|
150
|
-
description: 'When > 0, surface the last N kind=raw rows tagged isFreshTail=true regardless of query match. Useful for "what did I just see" continuity. Capped at 200.',
|
|
151
|
-
},
|
|
152
|
-
fresh_tail_session_id: {
|
|
153
|
-
type: 'string',
|
|
154
|
-
description: 'Restrict the fresh-tail window to a specific session. Without this, fresh-tail is tenant-wide (legacy v1.5.2 behaviour, pre-v1.6.3 default).',
|
|
155
|
-
},
|
|
156
|
-
summarize_overflow: {
|
|
157
|
-
type: 'boolean',
|
|
158
|
-
description: 'When true (default), entries that overflow the limit and share a level-2 parent summary cause that summary to be appended in their place. Set false for strict-limit behaviour.',
|
|
159
|
-
},
|
|
160
|
-
scorer_window: {
|
|
161
|
-
type: 'number',
|
|
162
|
-
description: 'Candidate pool size that api.recall evaluates. Affects fresh-tail / summarize-overflow appendix paths and continuity hits. Note: the primary ranked block over MCP is driven by a separate physics/hybrid scorer over the full tenant store, so scorer_window does NOT narrow the main results — only the appendix. Default 200. Rejected as RecallContractError code=invalid_scorer_window if 0/negative/non-finite/non-numeric.',
|
|
163
|
-
},
|
|
164
|
-
session_id: {
|
|
165
|
-
type: 'string',
|
|
166
|
-
maxLength: 256,
|
|
167
|
-
description: 'Optional session id (v1.7.4). When set AND (tenant, session) has active goals, applies the dlPFC goal-stack boost to the primary physics/hybrid result band before formatting AND to api.recall\'s primary BM25 band (so the audit + appendix paths see the same session). Mirrors fresh_tail_session_id shape (256-char cap).',
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
required: ['query'],
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
name: 'hippo_assemble',
|
|
175
|
-
description: 'Build a chronologically-ordered context window for a session. Returns ordered items: fresh-tail raw rows + level-2 summary substitutions for older rows + budget-fit. Hippo-additive vs lossless-claw: eviction picks lowest-strength non-fresh-tail items first instead of oldest-first. Tenant-scoped; default-deny on private scopes.',
|
|
176
|
-
inputSchema: {
|
|
177
|
-
type: 'object',
|
|
178
|
-
properties: {
|
|
179
|
-
session_id: {
|
|
180
|
-
type: 'string',
|
|
181
|
-
description: 'Session identifier. Returns clean empty result if no kind=raw rows match.',
|
|
182
|
-
},
|
|
183
|
-
budget: {
|
|
184
|
-
type: 'number',
|
|
185
|
-
description: 'Token budget for the assembled context (default 4000). Eviction kicks in over budget.',
|
|
186
|
-
},
|
|
187
|
-
fresh_tail_count: {
|
|
188
|
-
type: 'number',
|
|
189
|
-
description: 'Recent raw rows always kept verbatim (default 10). These are never evicted.',
|
|
190
|
-
},
|
|
191
|
-
summarize_older: {
|
|
192
|
-
type: 'boolean',
|
|
193
|
-
description: 'When true (default), older raws sharing a level-2 parent summary get substituted. Set false to keep every older raw as-is.',
|
|
194
|
-
},
|
|
195
|
-
scope: {
|
|
196
|
-
type: 'string',
|
|
197
|
-
description: 'Restrict to memories whose scope matches exactly. When omitted, default-deny applies to ANY <source>:private:* scope and unknown:legacy rows. Pass an explicit scope to assemble a private session with consent.',
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
required: ['session_id'],
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
name: 'hippo_drill',
|
|
205
|
-
description: 'Walk one step down the DAG from a level-2+ topic summary to its direct children. Companion to hippo_recall: when recall returns an item with isSummary=true and substitutedFor=[ids], pass the summary id here to recover the original detail. Tenant-scoped; default-deny on private scopes.',
|
|
206
|
-
inputSchema: {
|
|
207
|
-
type: 'object',
|
|
208
|
-
properties: {
|
|
209
|
-
summary_id: {
|
|
210
|
-
type: 'string',
|
|
211
|
-
description: 'ID of the level-2 (or higher) summary to drill into. Must be a summary, not a leaf — leaves are not drillable.',
|
|
212
|
-
},
|
|
213
|
-
limit: {
|
|
214
|
-
type: 'number',
|
|
215
|
-
description: 'Max children to return (default 50).',
|
|
216
|
-
},
|
|
217
|
-
budget: {
|
|
218
|
-
type: 'number',
|
|
219
|
-
description: 'Max total token cost (~ chars/4) of returned children. Truncates chronologically.',
|
|
220
|
-
},
|
|
221
|
-
depth: {
|
|
222
|
-
type: 'integer',
|
|
223
|
-
minimum: 1,
|
|
224
|
-
maximum: 10,
|
|
225
|
-
description: 'v0.30 / E5: walk N levels down (default 1 = direct children only). Higher values include children of children. Token budget remains GLOBAL across levels. Hard cap 10.',
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
required: ['summary_id'],
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
name: 'hippo_remember',
|
|
233
|
-
description: 'Store a new memory. Use when you learn something non-obvious, hit an error, or discover a useful pattern. Memories decay over time unless retrieved. Errors get 2x half-life.',
|
|
234
|
-
inputSchema: {
|
|
235
|
-
type: 'object',
|
|
236
|
-
properties: {
|
|
237
|
-
text: {
|
|
238
|
-
type: 'string',
|
|
239
|
-
description: 'The memory to store (1-2 sentences, specific and concrete)',
|
|
240
|
-
},
|
|
241
|
-
error: { type: 'boolean', description: 'Mark as error memory (doubles half-life)' },
|
|
242
|
-
pin: { type: 'boolean', description: 'Pin memory (never decays)' },
|
|
243
|
-
tag: { type: 'string', description: 'Optional tag for categorization' },
|
|
244
|
-
},
|
|
245
|
-
required: ['text'],
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
name: 'hippo_outcome',
|
|
250
|
-
description: 'Report whether recalled memories were useful. Strengthens good memories (+5 days half-life) and weakens bad ones (-3 days). Call after completing work.',
|
|
251
|
-
inputSchema: {
|
|
252
|
-
type: 'object',
|
|
253
|
-
properties: {
|
|
254
|
-
good: {
|
|
255
|
-
type: 'boolean',
|
|
256
|
-
description: 'true = memories helped, false = memories were irrelevant',
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
required: ['good'],
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
name: 'hippo_context',
|
|
264
|
-
description: 'Smart context injection: auto-detects current task from git state and returns relevant memories plus the active task snapshot. Use at the start of any session. Memories and snapshot are scope-filtered: a no-scope caller does NOT see ANY <source>:private:* (slack, github, ...) or legacy-quarantine rows.',
|
|
265
|
-
inputSchema: {
|
|
266
|
-
type: 'object',
|
|
267
|
-
properties: {
|
|
268
|
-
budget: { type: 'number', minimum: 0, description: 'Max tokens (default: 1500)' },
|
|
269
|
-
scope: {
|
|
270
|
-
type: 'string',
|
|
271
|
-
description: 'Restrict memories and snapshot to this scope exactly. When omitted, default-deny applies to ANY <source>:private:* (slack, github, ...) and unknown-legacy rows.',
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
name: 'hippo_status',
|
|
278
|
-
description: 'Check memory health: counts, strengths, at-risk memories, last consolidation time.',
|
|
279
|
-
inputSchema: {
|
|
280
|
-
type: 'object',
|
|
281
|
-
properties: {},
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
name: 'hippo_learn',
|
|
286
|
-
description: 'Scan recent git commits for lessons from fix/revert/bug/refactor/perf patterns. Run after coding sessions.',
|
|
287
|
-
inputSchema: {
|
|
288
|
-
type: 'object',
|
|
289
|
-
properties: {
|
|
290
|
-
days: { type: 'number', description: 'Days to scan back (default: 7)' },
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
name: 'hippo_conflicts',
|
|
296
|
-
description: 'List open memory conflicts — contradictory memories that need resolution.',
|
|
297
|
-
inputSchema: {
|
|
298
|
-
type: 'object',
|
|
299
|
-
properties: {},
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
name: 'hippo_resolve',
|
|
304
|
-
description: 'Resolve a memory conflict by keeping one memory and weakening or deleting the other.',
|
|
305
|
-
inputSchema: {
|
|
306
|
-
type: 'object',
|
|
307
|
-
properties: {
|
|
308
|
-
conflict_id: { type: 'number', description: 'The conflict ID to resolve' },
|
|
309
|
-
keep: { type: 'string', description: 'ID of the memory to keep' },
|
|
310
|
-
forget: { type: 'boolean', description: 'Delete the loser instead of weakening (default: false)' },
|
|
311
|
-
rejectLoser: { type: 'boolean', description: 'Tombstone the loser\'s value too, so it refuses re-ingestion (implies removal; default: false)' },
|
|
312
|
-
reason: { type: 'string', description: 'Reason recorded on the tombstone when rejectLoser is set (default: a conflict-context string)' },
|
|
313
|
-
},
|
|
314
|
-
required: ['conflict_id', 'keep'],
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
name: 'hippo_share',
|
|
319
|
-
description: 'Share a memory to the global store for cross-project use with transfer scoring.',
|
|
320
|
-
inputSchema: {
|
|
321
|
-
type: 'object',
|
|
322
|
-
properties: {
|
|
323
|
-
id: { type: 'string', description: 'Memory ID to share' },
|
|
324
|
-
force: { type: 'boolean', description: 'Share even if transfer score is low' },
|
|
325
|
-
},
|
|
326
|
-
required: ['id'],
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
name: 'hippo_peers',
|
|
331
|
-
description: 'List all projects that have contributed memories to the global shared store.',
|
|
332
|
-
inputSchema: {
|
|
333
|
-
type: 'object',
|
|
334
|
-
properties: {},
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
name: 'hippo_predict_baserate',
|
|
339
|
-
description: 'J3 reference-class / planning-fallacy detector. Get base-rate stats for closed predictions in a class. Call this when you make a forward-looking claim (effort estimate, rollout risk, deadline) to anchor on the past track record rather than the inside view. Returns count + mean estimate + mean actual + mean ratio + median ratio + MAE + a human-readable summary. Tenant-scoped.',
|
|
340
|
-
inputSchema: {
|
|
341
|
-
type: 'object',
|
|
342
|
-
properties: {
|
|
343
|
-
class_tag: {
|
|
344
|
-
type: 'string',
|
|
345
|
-
description: 'Cohort label, e.g. "migration-effort", "rollout-risk", "deadline-week". Must match the class_tag used when the predictions were created via hippo_predict (or `hippo predict ...`).',
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
required: ['class_tag'],
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
];
|
|
352
|
-
// ── Track last recalled IDs for outcome feedback ──
|
|
353
|
-
//
|
|
354
|
-
// Keyed per-client so two HTTP-MCP clients hitting the same tenant cannot
|
|
355
|
-
// poison each other's outcome feedback. The key is `ctx.clientKey` when the
|
|
356
|
-
// transport supplies one (HTTP-MCP via src/server.ts builds
|
|
357
|
-
// hash(bearer+remoteAddr)); stdio and any caller without a clientKey falls
|
|
358
|
-
// back to `'stdio-${pid}'` (one process = one client) or
|
|
359
|
-
// `${tenantId}:default` if a McpContext is constructed in tests without a
|
|
360
|
-
// pid-bound transport.
|
|
361
|
-
const lastRecalledIds = new Map();
|
|
362
|
-
function resolveClientKey(ctx) {
|
|
363
|
-
if (ctx?.clientKey)
|
|
364
|
-
return ctx.clientKey;
|
|
365
|
-
if (ctx?.tenantId)
|
|
366
|
-
return `stdio-${process.pid}:${ctx.tenantId}`;
|
|
367
|
-
return `stdio-${process.pid}:default`;
|
|
368
|
-
}
|
|
369
|
-
// ── Tool execution ──
|
|
370
|
-
async function executeTool(name, args, ctx) {
|
|
371
|
-
// When a transport hands us a context (HTTP path), trust it: the HTTP
|
|
372
|
-
// server already resolved hippoRoot from its bound opts and tenantId
|
|
373
|
-
// from the Bearer token (or the loopback fallback). The stdio path
|
|
374
|
-
// continues to walk from cwd / fall back to the global root, and to
|
|
375
|
-
// resolve tenant from HIPPO_TENANT.
|
|
376
|
-
const hippoRoot = ctx?.hippoRoot ?? findHippoRoot();
|
|
377
|
-
if (!hippoRoot)
|
|
378
|
-
return 'No .hippo/ store found. Run: hippo init';
|
|
379
|
-
const config = loadConfig(hippoRoot);
|
|
380
|
-
// A5: every loadAllEntries() in this server returns to the caller and is
|
|
381
|
-
// tenant-isolated. Resolved once per tool call: prefer the transport's
|
|
382
|
-
// ctx.tenantId so an HTTP Bearer for tenant B doesn't drop to HIPPO_TENANT.
|
|
383
|
-
const tenantId = ctx?.tenantId ?? resolveTenantId({});
|
|
384
|
-
switch (name) {
|
|
385
|
-
case 'hippo_recall': {
|
|
386
|
-
const query = String(args.query || '');
|
|
387
|
-
const budget = Number(args.budget) || config.defaultBudget;
|
|
388
|
-
const includeContinuity = Boolean(args.include_continuity);
|
|
389
|
-
const explicitScope = isJsonString(args.scope) && args.scope.length > 0
|
|
390
|
-
? args.scope
|
|
391
|
-
: undefined;
|
|
392
|
-
const freshTailCountArg = Number(args.fresh_tail_count);
|
|
393
|
-
const freshTailCount = Number.isFinite(freshTailCountArg) && freshTailCountArg > 0
|
|
394
|
-
? freshTailCountArg
|
|
395
|
-
: undefined;
|
|
396
|
-
const freshTailSessionId = isJsonString(args.fresh_tail_session_id) && args.fresh_tail_session_id.length > 0
|
|
397
|
-
? args.fresh_tail_session_id
|
|
398
|
-
: undefined;
|
|
399
|
-
const summarizeOverflow = isJsonBoolean(args.summarize_overflow)
|
|
400
|
-
? args.summarize_overflow
|
|
401
|
-
: undefined;
|
|
402
|
-
// v1.7.2 T4 — scorer_window: Number-coerce so non-numeric input
|
|
403
|
-
// (string 'abc', boolean, etc.) reaches api.recall() and produces
|
|
404
|
-
// the same typed RecallContractError(code='invalid_scorer_window')
|
|
405
|
-
// as HTTP. Codex CRITICAL[2]: do NOT use `typeof === 'number'` — that
|
|
406
|
-
// would silently default-200 on string `"5"` while HTTP 400s on the
|
|
407
|
-
// same value. Both transports must agree.
|
|
408
|
-
const scorerWindow = args.scorer_window === undefined
|
|
409
|
-
? undefined
|
|
410
|
-
: Number(args.scorer_window);
|
|
411
|
-
// v1.7.4 -- session_id for the dlPFC goal-stack boost. Mirrors
|
|
412
|
-
// fresh_tail_session_id shape: trim, 256-char cap. When set and the
|
|
413
|
-
// (tenant, session) has active goals, the boost is applied (a) inside
|
|
414
|
-
// api.recall on its primary BM25 band (so the audit + fresh-tail /
|
|
415
|
-
// summary appendix paths see consistent ranking), and (b) below on the
|
|
416
|
-
// physics/hybrid result list before formatMemories (since MCP's
|
|
417
|
-
// user-visible primary ordering does NOT come from api.recall).
|
|
418
|
-
const sessionIdRaw = isJsonString(args.session_id) ? args.session_id.trim() : '';
|
|
419
|
-
const sessionId = sessionIdRaw.length > 0 && sessionIdRaw.length <= 256
|
|
420
|
-
? sessionIdRaw
|
|
421
|
-
: undefined;
|
|
422
|
-
const apiCtx = {
|
|
423
|
-
hippoRoot,
|
|
424
|
-
tenantId,
|
|
425
|
-
actor: adminActor(ctx?.actor ?? 'mcp'),
|
|
426
|
-
};
|
|
427
|
-
// Route through api.recall for audit + (when requested) continuity block.
|
|
428
|
-
// api.recall already applies the same default-deny / exact-match rules
|
|
429
|
-
// we want here, so its continuity output is the source of truth.
|
|
430
|
-
// RecallContractError throws propagate raw to the MCP caller (per the
|
|
431
|
-
// v1.6.5 F5 contract documented in mcp-recall-fresh-tail-policy.test.ts).
|
|
432
|
-
const recallExtra = {};
|
|
433
|
-
if (freshTailCount !== undefined)
|
|
434
|
-
recallExtra.freshTailCount = freshTailCount;
|
|
435
|
-
if (freshTailSessionId !== undefined)
|
|
436
|
-
recallExtra.freshTailSessionId = freshTailSessionId;
|
|
437
|
-
if (summarizeOverflow !== undefined)
|
|
438
|
-
recallExtra.summarizeOverflow = summarizeOverflow;
|
|
439
|
-
if (scorerWindow !== undefined)
|
|
440
|
-
recallExtra.scorerWindow = scorerWindow;
|
|
441
|
-
if (sessionId !== undefined)
|
|
442
|
-
recallExtra.sessionId = sessionId;
|
|
443
|
-
const apiResult = apiRecall(apiCtx, {
|
|
444
|
-
query,
|
|
445
|
-
limit: 50,
|
|
446
|
-
scope: explicitScope,
|
|
447
|
-
includeContinuity,
|
|
448
|
-
// v1.13.x / J2 — MCP computes its OWN availability hint over the
|
|
449
|
-
// physics/hybrid result set below; suppress api.recall's BM25-band copy
|
|
450
|
-
// so one MCP recall does not emit recall_availability_detected twice.
|
|
451
|
-
suppressAvailabilityHint: true,
|
|
452
|
-
// LC1 F2 fix — MCP's user-visible primary ordering comes from the
|
|
453
|
-
// physics/hybrid scorer below, NOT this api.recall call's BM25 band
|
|
454
|
-
// (see the comment above apiRecall). Tracing this call as pipeline
|
|
455
|
-
// 'api' would mislabel training data with ids/ranks/scores the user
|
|
456
|
-
// never actually saw. Real MCP tracing is the reserved 'mcp'
|
|
457
|
-
// pipeline value (schema v40) — a follow-up, not v1 scope.
|
|
458
|
-
suppressRecallTrace: true,
|
|
459
|
-
...recallExtra,
|
|
460
|
-
});
|
|
461
|
-
// Existing physics/hybrid scorer continues to drive user-visible
|
|
462
|
-
// ordering and the strength bump on retrieval. Apply the same scope
|
|
463
|
-
// rule as api.recall: explicit scope = exact match; no scope =
|
|
464
|
-
// default-deny on ANY `<source>:private:*` AND 'unknown:legacy'.
|
|
465
|
-
// v1.2.1: generic-private check via api.isPrivateScope.
|
|
466
|
-
const allEntries = loadAllEntries(hippoRoot, tenantId);
|
|
467
|
-
// v1.12.13 / C5 — WYSIATI counters for the MCP physics/hybrid pipeline.
|
|
468
|
-
// Per the plan-eng-critic round 1 CRIT resolution: MCP's user-visible
|
|
469
|
-
// memory list comes from THIS pipeline (loadAllEntries -> scope filter
|
|
470
|
-
// -> physicsSearch/hybridSearch), NOT from apiResult. The MCP
|
|
471
|
-
// suppressionSummary must describe what the user actually sees, so we
|
|
472
|
-
// track filter activity here and replace apiResult.suppressionSummary
|
|
473
|
-
// in the user-facing response.
|
|
474
|
-
const totalCandidatesCountMcp = allEntries.length;
|
|
475
|
-
const entries = explicitScope
|
|
476
|
-
? allEntries.filter((e) => e.scope === explicitScope)
|
|
477
|
-
: allEntries.filter((e) => {
|
|
478
|
-
const s = e.scope ?? null;
|
|
479
|
-
if (s === null)
|
|
480
|
-
return true;
|
|
481
|
-
if (isPrivateScope(s))
|
|
482
|
-
return false;
|
|
483
|
-
// v1.7.2: read from RECALL_DEFAULT_DENY_SCOPES (single source of truth
|
|
484
|
-
// shared with SQL clause + passesScopeFilterForRecall).
|
|
485
|
-
if (RECALL_DEFAULT_DENY_SCOPES.some((deny) => deny === s))
|
|
486
|
-
return false;
|
|
487
|
-
return true;
|
|
488
|
-
});
|
|
489
|
-
const droppedPreRankCountMcp = allEntries.length - entries.length;
|
|
490
|
-
const usePhysics = config.physics?.enabled !== false;
|
|
491
|
-
let results = usePhysics
|
|
492
|
-
? await physicsSearch(query, entries, { budget, hippoRoot, physicsConfig: config.physics })
|
|
493
|
-
: await hybridSearch(query, entries, { budget, hippoRoot });
|
|
494
|
-
// v1.12.13 / C5 — droppedByBudget for MCP is an UPPER BOUND. The
|
|
495
|
-
// difference (entries.length - results.length) lumps three things
|
|
496
|
-
// together: rows hybridSearch/physicsSearch internally dropped because
|
|
497
|
-
// they scored zero (didn't match the query at all), rows the search
|
|
498
|
-
// engine filtered internally (e.g. superseded when --include-
|
|
499
|
-
// superseded isn't set), and rows that genuinely didn't fit the
|
|
500
|
-
// `budget` token cap. The honest fix needs hybridSearch/physicsSearch
|
|
501
|
-
// to expose their pre-budget-cut scored-count. Until then this is an
|
|
502
|
-
// upper bound that conflates "not relevant" with "no budget" on
|
|
503
|
-
// no-match / sparse-match queries. Plan-eng-critic round 1 MED and
|
|
504
|
-
// codex-review-critic P2 both flagged this; documented + tracked as
|
|
505
|
-
// a v1.12.14 follow-up. Independent-review-critic and code-review-
|
|
506
|
-
// critic both graded as non-blocking for v1.12.13 ship.
|
|
507
|
-
// TODO(c5.1): expose scoredCount from hybridSearch/physicsSearch and
|
|
508
|
-
// compute droppedByBudget = scoredCount - results.length, with the
|
|
509
|
-
// remainder (entries.length - scoredCount) attributed to
|
|
510
|
-
// droppedPreRank or a new "noQueryMatch" counter.
|
|
511
|
-
const droppedByBudgetCountMcp = Math.max(0, entries.length - results.length);
|
|
512
|
-
// v1.7.4 -- dlPFC goal-stack boost on the MCP physics/hybrid result
|
|
513
|
-
// list BEFORE formatMemories. MCP's user-visible primary ordering does
|
|
514
|
-
// NOT come from api.recall (apiResult above), so the boost has to run
|
|
515
|
-
// here too. Helper signature accepts any { entry, score } shape; the
|
|
516
|
-
// physics/hybrid result rows are already in that shape.
|
|
517
|
-
if (sessionId !== undefined) {
|
|
518
|
-
const dbForBoost = openHippoDb(hippoRoot);
|
|
519
|
-
try {
|
|
520
|
-
results = applyGoalStackBoost(dbForBoost, results, {
|
|
521
|
-
sessionId,
|
|
522
|
-
tenantId,
|
|
523
|
-
limit: results.length,
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
finally {
|
|
527
|
-
closeHippoDb(dbForBoost);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
// Mark retrieved and persist
|
|
531
|
-
const retrieved = markRetrieved(results.map((r) => r.entry));
|
|
532
|
-
// EVAL-ONLY ablation (see ablation.ts): skip persistence under the recall
|
|
533
|
-
// flag; ids below stay populated for outcome attribution.
|
|
534
|
-
if (!isRecallBoostAblated()) {
|
|
535
|
-
for (const entry of retrieved)
|
|
536
|
-
writeEntry(hippoRoot, entry, { actor: ctx?.actor ?? 'mcp' });
|
|
537
|
-
}
|
|
538
|
-
lastRecalledIds.set(resolveClientKey(ctx), retrieved.map((e) => e.id));
|
|
539
|
-
// v0.33 / J1 — MCP per-pipeline anchoring detector. UNLIKE J3.2's
|
|
540
|
-
// planningFallacyHint (which is pipeline-invariant because it
|
|
541
|
-
// depends only on queryText + predictions table state), the
|
|
542
|
-
// anchoring hint depends on (a) per-pipeline top-1 ranking (MCP's
|
|
543
|
-
// physics/hybrid winner can differ from api.recall's BM25 winner)
|
|
544
|
-
// and (b) per-pipeline ring buffer. So MCP computes its OWN hint
|
|
545
|
-
// against MCP's own top-1, mirroring the C5 per-pipeline rule.
|
|
546
|
-
let mcpAnchoringHint = null;
|
|
547
|
-
if (process.env.HIPPO_ANCHORING !== 'off') {
|
|
548
|
-
if (sessionId) {
|
|
549
|
-
const ringKey = buildSessionKey(tenantId, sessionId);
|
|
550
|
-
const ring = getOrCreateRing(sessionRecallHistoryMcp, ringKey);
|
|
551
|
-
const queryHash = hashQueryText(query);
|
|
552
|
-
const topId = results[0]?.entry.id ?? null;
|
|
553
|
-
mcpAnchoringHint = detectAnchoring(snapshotRing(ring), queryHash, topId);
|
|
554
|
-
appendRecall(ring, queryHash, topId, mcpAnchoringHint?.memoryId);
|
|
555
|
-
// Pipeline-local audit emission (lockstep with CLI / api.recall).
|
|
556
|
-
if (mcpAnchoringHint?.reason === 'memory_dominance') {
|
|
557
|
-
const dbForAudit = openHippoDb(hippoRoot);
|
|
558
|
-
try {
|
|
559
|
-
appendAuditEvent(dbForAudit, {
|
|
560
|
-
tenantId,
|
|
561
|
-
actor: ctx?.actor ?? 'mcp',
|
|
562
|
-
op: 'recall_anchor_detected_memory_dominance',
|
|
563
|
-
targetId: mcpAnchoringHint.memoryId,
|
|
564
|
-
metadata: {
|
|
565
|
-
memory_id: mcpAnchoringHint.memoryId,
|
|
566
|
-
query_count: mcpAnchoringHint.queryCount ?? null,
|
|
567
|
-
},
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
finally {
|
|
571
|
-
closeHippoDb(dbForAudit);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
else if (mcpAnchoringHint?.reason === 'query_repeat') {
|
|
575
|
-
const dbForAudit = openHippoDb(hippoRoot);
|
|
576
|
-
try {
|
|
577
|
-
appendAuditEvent(dbForAudit, {
|
|
578
|
-
tenantId,
|
|
579
|
-
actor: ctx?.actor ?? 'mcp',
|
|
580
|
-
op: 'recall_anchor_detected_query_repeat',
|
|
581
|
-
targetId: mcpAnchoringHint.memoryId,
|
|
582
|
-
metadata: { memory_id: mcpAnchoringHint.memoryId },
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
finally {
|
|
586
|
-
closeHippoDb(dbForAudit);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
else {
|
|
591
|
-
// Telemetry: caller had no sessionId so ring tracking skipped.
|
|
592
|
-
// Per the recall-audit convention at api.ts:854, use SHA-256/16
|
|
593
|
-
// for prompt hashing (NOT hashQueryText which is FNV-1a 32-bit
|
|
594
|
-
// for recall matching; brute-force trivial for low-entropy
|
|
595
|
-
// queries). Codex round-2 P2 catch.
|
|
596
|
-
const dbForAudit = openHippoDb(hippoRoot);
|
|
597
|
-
try {
|
|
598
|
-
appendAuditEvent(dbForAudit, {
|
|
599
|
-
tenantId,
|
|
600
|
-
actor: ctx?.actor ?? 'mcp',
|
|
601
|
-
op: 'recall_anchor_skipped_no_session',
|
|
602
|
-
targetId: undefined,
|
|
603
|
-
metadata: {
|
|
604
|
-
query_hash: createHash('sha256').update(query).digest('hex').slice(0, 16),
|
|
605
|
-
query_length: query.length,
|
|
606
|
-
},
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
finally {
|
|
610
|
-
closeHippoDb(dbForAudit);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
// v0.32 / J3.2 — auto-injection of reference-class baserate hint
|
|
615
|
-
// when the query carries a forward-prediction phrase. Read from
|
|
616
|
-
// apiResult.planningFallacyHint (already computed inside api.recall
|
|
617
|
-
// with the caller identity threaded through ctx.actor.subject -
|
|
618
|
-
// auth-resolved actor under HTTP-MCP, 'mcp' for stdio). The hint is
|
|
619
|
-
// pipeline-INVARIANT — same (hippoRoot, tenantId, query) inputs
|
|
620
|
-
// produce the same hint regardless of which downstream search
|
|
621
|
-
// pipeline (api.recall band vs physics/hybrid) renders the memory
|
|
622
|
-
// list, so re-computing here would double the audit emission for
|
|
623
|
-
// identical telemetry. C5 per-pipeline rule does NOT apply here
|
|
624
|
-
// because the hint depends on queryText, not on the matched memory
|
|
625
|
-
// set. Prepend BEFORE the memory list so the agent sees it first.
|
|
626
|
-
// v0.33 / J1: Anchoring hint goes ABOVE planning-fallacy hint
|
|
627
|
-
// (anchoring is the stronger cognitive-pull warning).
|
|
628
|
-
// v1.13.3 / C5 follow-up — Build MCP-pipeline suppressionSummary BEFORE
|
|
629
|
-
// the response is assembled so the Cutoff block can render at TOP
|
|
630
|
-
// alongside the other Track J hints. The dogfood
|
|
631
|
-
// (docs/dogfood/2026-05-27-track-j-warnings.md) showed the v1.13.0-v1.13.2
|
|
632
|
-
// bottom-placement was dark: a fresh sub-agent summarised the visible
|
|
633
|
-
// memories with zero mention of the dropped pool. Top-placement + plain-
|
|
634
|
-
// English rewrite fixes the read-rate without any system-prompt addendum.
|
|
635
|
-
const physicsIds = new Set(results.map((r) => r.entry.id));
|
|
636
|
-
const tailOrSummary = apiResult.results.filter((r) => (r.isFreshTail || r.isSummary) && !physicsIds.has(r.id));
|
|
637
|
-
const freshTailAddedMcp = tailOrSummary.filter((r) => r.isFreshTail && !r.isSummary).length;
|
|
638
|
-
const summarySubsAddedMcp = tailOrSummary.filter((r) => r.isSummary).length;
|
|
639
|
-
// v0.33 / J1: suppressedByInterference bumped on MCP's R2 fire.
|
|
640
|
-
const mcpSuppressedByInterference = mcpAnchoringHint?.reason === 'memory_dominance' ? 1 : 0;
|
|
641
|
-
const mcpSuppressionSummary = buildSuppressionSummary({
|
|
642
|
-
totalCandidates: totalCandidatesCountMcp,
|
|
643
|
-
droppedPreRank: droppedPreRankCountMcp,
|
|
644
|
-
droppedByBudget: droppedByBudgetCountMcp,
|
|
645
|
-
summarySubstitutionsAdded: summarySubsAddedMcp,
|
|
646
|
-
freshTailAdded: freshTailAddedMcp,
|
|
647
|
-
suppressedByInterference: mcpSuppressedByInterference,
|
|
648
|
-
});
|
|
649
|
-
// v1.13.x / J2 — MCP per-pipeline availability/recency-bias detector.
|
|
650
|
-
// Like the anchoring hint above (and unlike J3.2's pipeline-invariant
|
|
651
|
-
// planningFallacyHint), this depends on MCP's OWN returned top-K and the
|
|
652
|
-
// scope-filtered candidate pool (entries) it was drawn from, so MCP
|
|
653
|
-
// computes its own hint here. Soft warning only. Gated by
|
|
654
|
-
// HIPPO_AVAILABILITY=off; audit emission is pipeline-local (actor =
|
|
655
|
-
// auth-resolved ctx.actor under HTTP-MCP, 'mcp' for stdio).
|
|
656
|
-
let mcpAvailabilityHint = null;
|
|
657
|
-
if (process.env.HIPPO_AVAILABILITY !== 'off') {
|
|
658
|
-
mcpAvailabilityHint = detectAvailabilityBias({
|
|
659
|
-
topK: results.map((r) => ({ id: r.entry.id, created: r.entry.created })),
|
|
660
|
-
pool: entries.map((e) => ({ id: e.id, created: e.created })),
|
|
661
|
-
});
|
|
662
|
-
if (mcpAvailabilityHint) {
|
|
663
|
-
const dbForAudit = openHippoDb(hippoRoot);
|
|
664
|
-
try {
|
|
665
|
-
appendAuditEvent(dbForAudit, {
|
|
666
|
-
tenantId,
|
|
667
|
-
actor: ctx?.actor ?? 'mcp',
|
|
668
|
-
op: 'recall_availability_detected',
|
|
669
|
-
metadata: {
|
|
670
|
-
recent_fraction: mcpAvailabilityHint.recentFraction,
|
|
671
|
-
older_passed_over: mcpAvailabilityHint.olderCandidatesPassedOver,
|
|
672
|
-
returned_count: mcpAvailabilityHint.returnedCount,
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
finally {
|
|
677
|
-
closeHippoDb(dbForAudit);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
let response = '';
|
|
682
|
-
if (mcpAnchoringHint) {
|
|
683
|
-
response =
|
|
684
|
-
`## Anchoring hint\n` +
|
|
685
|
-
`${mcpAnchoringHint.summary}\n` +
|
|
686
|
-
`[anchored_on: ${mcpAnchoringHint.memoryId}]\n` +
|
|
687
|
-
`\n---\n\n`;
|
|
688
|
-
}
|
|
689
|
-
// v1.13.x / J2 — availability/recency-bias hint, rendered below the
|
|
690
|
-
// anchoring hint and above the planning-fallacy hint. Soft warning only.
|
|
691
|
-
if (mcpAvailabilityHint) {
|
|
692
|
-
response += `## Availability bias\n${mcpAvailabilityHint.summary}\n\n---\n\n`;
|
|
693
|
-
}
|
|
694
|
-
if (apiResult.planningFallacyHint) {
|
|
695
|
-
const h = apiResult.planningFallacyHint;
|
|
696
|
-
const safePhrase = JSON.stringify(h.detectedPhrase);
|
|
697
|
-
response +=
|
|
698
|
-
`## Planning fallacy hint\n` +
|
|
699
|
-
`Class: ${h.classTag}\n` +
|
|
700
|
-
`${h.baserateSummary}\n` +
|
|
701
|
-
`(detected: ${safePhrase})\n` +
|
|
702
|
-
`\n---\n\n`;
|
|
703
|
-
}
|
|
704
|
-
else if (apiResult.planningFallacyWatching) {
|
|
705
|
-
// v1.13.4 / J3.2 follow-up — surface the watching variant when
|
|
706
|
-
// the regex matched but no baserate could be produced
|
|
707
|
-
// (no_class_match / tiebreak). Mutually exclusive with the hint
|
|
708
|
-
// block above. Suggestion text directs the user toward an action
|
|
709
|
-
// (typically: tag a prediction class) that would unblock the
|
|
710
|
-
// hint next time.
|
|
711
|
-
const w = apiResult.planningFallacyWatching;
|
|
712
|
-
const safePhrase = JSON.stringify(w.detectedPhrase);
|
|
713
|
-
response +=
|
|
714
|
-
`## Planning fallacy watch\n` +
|
|
715
|
-
`Reason: ${w.reason}\n` +
|
|
716
|
-
`${w.suggestion}\n` +
|
|
717
|
-
`(detected: ${safePhrase})\n` +
|
|
718
|
-
`\n---\n\n`;
|
|
719
|
-
}
|
|
720
|
-
// v1.13.3 / C5 follow-up — Cutoff block (was "WYSIATI:" line at bottom
|
|
721
|
-
// in v1.13.0-v1.13.2). Top placement so the agent reads the cutoff
|
|
722
|
-
// before scrolling the result list. "Cutoff" is plain English; the old
|
|
723
|
-
// "WYSIATI:" acronym was opaque to agents without Kahneman context per
|
|
724
|
-
// the 2026-05-27 dogfood Trial 1.
|
|
725
|
-
const sMcp = mcpSuppressionSummary;
|
|
726
|
-
const cutoffClauses = [];
|
|
727
|
-
if (sMcp.droppedByBudget > 0)
|
|
728
|
-
cutoffClauses.push(`${sMcp.droppedByBudget} dropped to fit limit`);
|
|
729
|
-
if (sMcp.droppedPreRank > 0)
|
|
730
|
-
cutoffClauses.push(`${sMcp.droppedPreRank} filtered pre-rank`);
|
|
731
|
-
if (sMcp.summarySubstitutionsAdded > 0)
|
|
732
|
-
cutoffClauses.push(`${sMcp.summarySubstitutionsAdded} summary substitutions added`);
|
|
733
|
-
if (sMcp.freshTailAdded > 0)
|
|
734
|
-
cutoffClauses.push(`${sMcp.freshTailAdded} fresh-tail added`);
|
|
735
|
-
if (sMcp.suppressedByInterference > 0)
|
|
736
|
-
cutoffClauses.push(`${sMcp.suppressedByInterference} suppressed by interference`);
|
|
737
|
-
if (cutoffClauses.length > 0) {
|
|
738
|
-
response +=
|
|
739
|
-
`## Cutoff\n` +
|
|
740
|
-
`Showing ${results.length} of ${sMcp.totalCandidates} candidates; ${cutoffClauses.join('; ')}.\n` +
|
|
741
|
-
`\n---\n\n`;
|
|
742
|
-
}
|
|
743
|
-
response += formatMemories(results, hippoRoot);
|
|
744
|
-
// v1.6.3 codex P2 fix. The physics/hybrid scorer drives the primary
|
|
745
|
-
// ranked block above, so user-visible ordering is preserved. But
|
|
746
|
-
// when the v1.5.0+/v1.5.2 RecallOpts are passed, we MUST also surface
|
|
747
|
-
// the fresh-tail and substituted-summary items apiRecall produced —
|
|
748
|
-
// otherwise the advertised MCP fields are silently ignored. Append
|
|
749
|
-
// them as their own section, deduplicated against the physics ranking.
|
|
750
|
-
if (tailOrSummary.length > 0) {
|
|
751
|
-
const lines = ['', '## Fresh tail / substituted summaries'];
|
|
752
|
-
for (const r of tailOrSummary) {
|
|
753
|
-
const tag = r.isSummary ? '[summary]' : '[tail]';
|
|
754
|
-
const head = r.content.length > 200 ? r.content.slice(0, 200) + '…' : r.content;
|
|
755
|
-
if (r.isSummary && r.substitutedFor && r.substitutedFor.length > 0) {
|
|
756
|
-
lines.push(`- ${tag} ${r.id} (covers ${r.substitutedFor.length} rows): ${head}`);
|
|
757
|
-
}
|
|
758
|
-
else {
|
|
759
|
-
lines.push(`- ${tag} ${r.id}: ${head}`);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
response += '\n' + lines.join('\n');
|
|
763
|
-
}
|
|
764
|
-
if (includeContinuity && apiResult.continuity) {
|
|
765
|
-
response += '\n\n' + formatContinuityBlock(apiResult.continuity);
|
|
766
|
-
}
|
|
767
|
-
return response;
|
|
768
|
-
}
|
|
769
|
-
case 'hippo_assemble': {
|
|
770
|
-
const sessionId = String(args.session_id || '');
|
|
771
|
-
if (!sessionId)
|
|
772
|
-
return 'No session_id provided.';
|
|
773
|
-
const budget = Number(args.budget);
|
|
774
|
-
const freshTailCount = Number(args.fresh_tail_count);
|
|
775
|
-
const summarizeOlder = args.summarize_older !== false;
|
|
776
|
-
const apiCtx = {
|
|
777
|
-
hippoRoot,
|
|
778
|
-
tenantId,
|
|
779
|
-
actor: adminActor(ctx?.actor ?? 'mcp'),
|
|
780
|
-
};
|
|
781
|
-
const explicitScope = isJsonString(args.scope) && args.scope.length > 0
|
|
782
|
-
? args.scope
|
|
783
|
-
: undefined;
|
|
784
|
-
const assembleExtra = {};
|
|
785
|
-
if (Number.isFinite(budget) && budget > 0)
|
|
786
|
-
assembleExtra.budget = budget;
|
|
787
|
-
if (Number.isFinite(freshTailCount) && freshTailCount >= 0)
|
|
788
|
-
assembleExtra.freshTailCount = freshTailCount;
|
|
789
|
-
if (explicitScope !== undefined)
|
|
790
|
-
assembleExtra.scope = explicitScope;
|
|
791
|
-
const r = apiAssemble(apiCtx, sessionId, {
|
|
792
|
-
summarizeOlder,
|
|
793
|
-
...assembleExtra,
|
|
794
|
-
});
|
|
795
|
-
const lines = [];
|
|
796
|
-
lines.push(`Session ${r.sessionId} — ${r.items.length} items, ${r.tokens} tokens (raw=${r.totalRaw}, summarized=${r.summarized}, evicted=${r.evicted})`);
|
|
797
|
-
for (const it of r.items) {
|
|
798
|
-
const prefix = it.isSummary ? '[summary]' : it.isFreshTail ? '[tail]' : '[older]';
|
|
799
|
-
lines.push(` ${prefix} ${it.createdAt} ${it.id} - ${it.content}`);
|
|
800
|
-
}
|
|
801
|
-
return lines.join('\n');
|
|
802
|
-
}
|
|
803
|
-
case 'hippo_drill': {
|
|
804
|
-
const summaryId = String(args.summary_id || '');
|
|
805
|
-
if (!summaryId)
|
|
806
|
-
return 'No summary_id provided.';
|
|
807
|
-
const limit = Number(args.limit);
|
|
808
|
-
const budget = Number(args.budget);
|
|
809
|
-
// v0.30 / E5: depth walks N levels (default 1, hard cap 10).
|
|
810
|
-
// independent-review MED #5 fold: reject out-of-range explicitly
|
|
811
|
-
// (no silent clamp) so MCP callers see the constraint at their layer.
|
|
812
|
-
let depth;
|
|
813
|
-
if (args.depth !== undefined) {
|
|
814
|
-
const depthRaw = Number(args.depth);
|
|
815
|
-
if (!Number.isInteger(depthRaw) || depthRaw < 1 || depthRaw > 10) {
|
|
816
|
-
return `depth must be an integer between 1 and 10 (got ${args.depth})`;
|
|
817
|
-
}
|
|
818
|
-
depth = depthRaw;
|
|
819
|
-
}
|
|
820
|
-
const apiCtx = {
|
|
821
|
-
hippoRoot,
|
|
822
|
-
tenantId,
|
|
823
|
-
actor: adminActor(ctx?.actor ?? 'mcp'),
|
|
824
|
-
};
|
|
825
|
-
const drillExtra = {};
|
|
826
|
-
if (Number.isFinite(limit) && limit > 0)
|
|
827
|
-
drillExtra.limit = limit;
|
|
828
|
-
if (Number.isFinite(budget) && budget > 0)
|
|
829
|
-
drillExtra.budget = budget;
|
|
830
|
-
if (depth !== undefined)
|
|
831
|
-
drillExtra.depth = depth;
|
|
832
|
-
const r = apiDrillDown(apiCtx, summaryId, { ...drillExtra });
|
|
833
|
-
if ('failure' in r) {
|
|
834
|
-
// v1.6.4: only not_drillable is caller-actionable. not_found
|
|
835
|
-
// intentionally collapses cross-tenant + scope-blocked + missing
|
|
836
|
-
// (codex round 3 P1: distinguishing scope_blocked would leak
|
|
837
|
-
// private-row existence on this surface).
|
|
838
|
-
if (r.failure === 'not_drillable') {
|
|
839
|
-
return `Id ${summaryId} is a leaf row, not a level-2+ summary; nothing to drill into.`;
|
|
840
|
-
}
|
|
841
|
-
return `No drillable summary at id=${summaryId}.`;
|
|
842
|
-
}
|
|
843
|
-
const lines = [];
|
|
844
|
-
lines.push(`Summary ${r.summary.id} — ${r.summary.descendantCount} descendants${r.summary.earliestAt ? ` (${r.summary.earliestAt} -> ${r.summary.latestAt})` : ''}`);
|
|
845
|
-
lines.push(` ${r.summary.content}`);
|
|
846
|
-
lines.push('');
|
|
847
|
-
lines.push(`Children (${r.children.length}/${r.totalChildren}${r.truncated ? ', truncated' : ''}):`);
|
|
848
|
-
for (const c of r.children) {
|
|
849
|
-
lines.push(` [L${c.dagLevel}] ${c.id} - ${c.content}`);
|
|
850
|
-
}
|
|
851
|
-
return lines.join('\n');
|
|
852
|
-
}
|
|
853
|
-
case 'hippo_predict_baserate': {
|
|
854
|
-
// J3 reference-class / planning-fallacy detector. Reads from the E2
|
|
855
|
-
// predictions table; returns text-only response matching the existing
|
|
856
|
-
// MCP tool convention (no structured JSON over the wire). Direct call
|
|
857
|
-
// to computePredictionBaserate; helper opens its own db + emits audit
|
|
858
|
-
// (single source of truth, no caller-site drift).
|
|
859
|
-
const classTag = String(args.class_tag || '').trim();
|
|
860
|
-
if (!classTag)
|
|
861
|
-
return 'No class_tag provided. Usage: pass class_tag matching a class used in past predictions (e.g. "migration-effort").';
|
|
862
|
-
const baserate = computePredictionBaserate(hippoRoot, tenantId, classTag, ctx?.actor ?? 'mcp');
|
|
863
|
-
if (baserate.nClosed === 0) {
|
|
864
|
-
return `No closed predictions in class "${classTag}" yet. Create one via hippo_predict (or 'hippo predict ...' CLI) and close it with hippo_predict_close once the actual outcome is known. Base rates need closed predictions with numeric actual_value to compute.`;
|
|
865
|
-
}
|
|
866
|
-
const lines = [baserate.summary, ''];
|
|
867
|
-
lines.push(`n_closed: ${baserate.nClosed}`);
|
|
868
|
-
lines.push(`n_ratio_eligible: ${baserate.nRatioEligible}`);
|
|
869
|
-
if (baserate.meanEstimate !== null)
|
|
870
|
-
lines.push(`mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
|
|
871
|
-
if (baserate.meanActual !== null)
|
|
872
|
-
lines.push(`mean_actual: ${baserate.meanActual.toFixed(3)}`);
|
|
873
|
-
if (baserate.meanRatio !== null)
|
|
874
|
-
lines.push(`mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
|
|
875
|
-
if (baserate.p50Ratio !== null)
|
|
876
|
-
lines.push(`p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
|
|
877
|
-
if (baserate.mae !== null)
|
|
878
|
-
lines.push(`mae: ${baserate.mae.toFixed(3)}`);
|
|
879
|
-
return lines.join('\n');
|
|
880
|
-
}
|
|
881
|
-
case 'hippo_remember': {
|
|
882
|
-
const text = String(args.text || '');
|
|
883
|
-
if (!text)
|
|
884
|
-
return 'No text provided.';
|
|
885
|
-
const tags = [];
|
|
886
|
-
if (args.error)
|
|
887
|
-
tags.push('error');
|
|
888
|
-
if (args.tag)
|
|
889
|
-
tags.push(String(args.tag));
|
|
890
|
-
// Route through api.ts so audit_log captures the caller identity
|
|
891
|
-
// uniformly with CLI/REST: the auth-resolved ctx.actor under HTTP-MCP,
|
|
892
|
-
// 'mcp' for stdio (no ctx). api.ts.remember writes the memory + audit
|
|
893
|
-
// row in one transaction-friendly path; we re-read the entry to surface
|
|
894
|
-
// the half-life used in the MCP human-readable response.
|
|
895
|
-
const apiCtx = {
|
|
896
|
-
hippoRoot,
|
|
897
|
-
tenantId,
|
|
898
|
-
actor: adminActor(ctx?.actor ?? 'mcp'),
|
|
899
|
-
};
|
|
900
|
-
const result = apiRemember(apiCtx, {
|
|
901
|
-
content: text,
|
|
902
|
-
tags,
|
|
903
|
-
});
|
|
904
|
-
const entry = readEntry(hippoRoot, result.id, tenantId);
|
|
905
|
-
// Auto-sleep check
|
|
906
|
-
if (config.autoSleep.enabled) {
|
|
907
|
-
const allEntries = loadAllEntries(hippoRoot, tenantId);
|
|
908
|
-
const recentCount = allEntries.filter((e) => {
|
|
909
|
-
const age = (Date.now() - new Date(e.created).getTime()) / (1000 * 60 * 60);
|
|
910
|
-
return age < 24; // created in last 24 hours
|
|
911
|
-
}).length;
|
|
912
|
-
if (recentCount >= config.autoSleep.threshold) {
|
|
913
|
-
// Fire-and-forget by design (never block the remember response), but
|
|
914
|
-
// an unhandled rejection here can kill the MCP server process — any
|
|
915
|
-
// consolidate error (DB contention, or the memoryValue fail-loud
|
|
916
|
-
// throw) must land as a logged line, not a crash.
|
|
917
|
-
consolidate(hippoRoot).catch((err) => {
|
|
918
|
-
console.error(`auto-sleep consolidate failed: ${err instanceof Error ? err.message : err}`);
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
const halfLife = entry?.half_life_days ?? config.defaultHalfLifeDays;
|
|
923
|
-
const tagStr = entry?.tags.join(', ') || tags.join(', ') || 'none';
|
|
924
|
-
return `Remembered [${result.id}] (half-life: ${halfLife}d, tags: ${tagStr})`;
|
|
925
|
-
}
|
|
926
|
-
case 'hippo_outcome': {
|
|
927
|
-
const good = Boolean(args.good);
|
|
928
|
-
const clientKey = resolveClientKey(ctx);
|
|
929
|
-
const ids = lastRecalledIds.get(clientKey) ?? [];
|
|
930
|
-
if (ids.length === 0)
|
|
931
|
-
return 'No recent recalls to apply outcome to.';
|
|
932
|
-
// Route through src/api.ts so audit_log captures the caller identity
|
|
933
|
-
// (auth-resolved ctx.actor under HTTP-MCP, 'mcp' for stdio) and tenant
|
|
934
|
-
// scoping is enforced uniformly (same surface as recall/remember).
|
|
935
|
-
// outcome() also handles cross-tenant id skip silently.
|
|
936
|
-
const apiCtx = {
|
|
937
|
-
hippoRoot,
|
|
938
|
-
tenantId,
|
|
939
|
-
actor: adminActor(ctx?.actor ?? 'mcp'),
|
|
940
|
-
};
|
|
941
|
-
const { applied } = apiOutcome(apiCtx, ids, good);
|
|
942
|
-
return `Applied ${good ? 'positive' : 'negative'} outcome to ${applied} memories`;
|
|
943
|
-
}
|
|
944
|
-
case 'hippo_context': {
|
|
945
|
-
const budget = args.budget === undefined
|
|
946
|
-
? config.defaultContextBudget
|
|
947
|
-
: Number(args.budget);
|
|
948
|
-
if (!Number.isFinite(budget) || budget < 0)
|
|
949
|
-
return 'budget must be a non-negative number.';
|
|
950
|
-
if (budget === 0)
|
|
951
|
-
return '';
|
|
952
|
-
const explicitScope = isJsonString(args.scope) && args.scope.length > 0
|
|
953
|
-
? args.scope
|
|
954
|
-
: undefined;
|
|
955
|
-
// Auto-detect query from git
|
|
956
|
-
let query = '';
|
|
957
|
-
try {
|
|
958
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
959
|
-
const diff = execSync('git diff --cached --stat 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
960
|
-
const log = execSync('git log -1 --pretty=format:"%s" 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
961
|
-
query = [branch, log, diff].filter(Boolean).join(' ');
|
|
962
|
-
}
|
|
963
|
-
catch { /* not a git repo */ }
|
|
964
|
-
if (!query)
|
|
965
|
-
query = 'project context general';
|
|
966
|
-
// v1.2 codex audit: same scope filter as hippo_recall on BOTH the memory
|
|
967
|
-
// results and the snapshot. Pre-v1.2 this surface returned all memories
|
|
968
|
-
// and the snapshot unfiltered, which would have leaked private-channel
|
|
969
|
-
// content to no-scope MCP callers once scope writers shipped.
|
|
970
|
-
const allEntries = loadAllEntries(hippoRoot, tenantId);
|
|
971
|
-
// v39 memory scope isolation: this surface reads the LOCAL store only,
|
|
972
|
-
// but synced-down or legacy rows can still carry another project's
|
|
973
|
-
// origin, and secrets must never ambient-inject outside their owner.
|
|
974
|
-
// Same policy as api.getContext; the scope filter above keeps this
|
|
975
|
-
// surface's own explicit-scope exact-match semantics.
|
|
976
|
-
//
|
|
977
|
-
// Identity resolution handles both transports (codex rounds 4+5):
|
|
978
|
-
// - The SERVED store is authoritative when it is a project store -
|
|
979
|
-
// an HTTP /mcp daemon launched from anywhere still isolates the
|
|
980
|
-
// project it serves.
|
|
981
|
-
// - When the served store is the global root (stdio in a git repo
|
|
982
|
-
// with no local .hippo falls back to it), dirname(store) is home
|
|
983
|
-
// ('' would admit everything), so fall back to the launch cwd -
|
|
984
|
-
// stdio servers launch in the project they serve.
|
|
985
|
-
const mcpStoreIdentity = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot)));
|
|
986
|
-
const mcpProjectName = mcpStoreIdentity.name !== ''
|
|
987
|
-
? mcpStoreIdentity.name
|
|
988
|
-
: resolveProjectIdentity(process.cwd()).name;
|
|
989
|
-
const isolationOff = config.contextProjectIsolation === false;
|
|
990
|
-
const entries = allEntries.filter((e) => {
|
|
991
|
-
if (!passesScopeFilterForRecall(e.scope ?? null, explicitScope))
|
|
992
|
-
return false;
|
|
993
|
-
if (!ambientSecretAdmit(e, mcpProjectName))
|
|
994
|
-
return false;
|
|
995
|
-
if (isolationOff)
|
|
996
|
-
return true;
|
|
997
|
-
return classifyOriginProject(e.origin_project, mcpProjectName) !== 'cross-project';
|
|
998
|
-
});
|
|
999
|
-
const usePhysicsCtx = config.physics?.enabled !== false;
|
|
1000
|
-
const results = usePhysicsCtx
|
|
1001
|
-
? await physicsSearch(query, entries, { budget, hippoRoot, physicsConfig: config.physics })
|
|
1002
|
-
: await hybridSearch(query, entries, { budget, hippoRoot });
|
|
1003
|
-
const retrieved = markRetrieved(results.map((r) => r.entry));
|
|
1004
|
-
// EVAL-ONLY ablation (see ablation.ts): skip persistence under the recall
|
|
1005
|
-
// flag; ids below stay populated for outcome attribution.
|
|
1006
|
-
if (!isRecallBoostAblated()) {
|
|
1007
|
-
for (const entry of retrieved)
|
|
1008
|
-
writeEntry(hippoRoot, entry, { actor: ctx?.actor ?? 'mcp' });
|
|
1009
|
-
}
|
|
1010
|
-
lastRecalledIds.set(resolveClientKey(ctx), retrieved.map((e) => e.id));
|
|
1011
|
-
// DF1 (docs/plans/2026-08-23-df1-snapshot-lifecycle.md, T2): bounded
|
|
1012
|
-
// read, no session id available on this surface (freshness bound
|
|
1013
|
-
// only) — an orphaned snapshot must age out here too, not just on the
|
|
1014
|
-
// UserPromptSubmit path.
|
|
1015
|
-
const rawSnapshot = loadFreshActiveTaskSnapshot(hippoRoot, tenantId);
|
|
1016
|
-
const snapshot = rawSnapshot && passesScopeFilterForRecall(rawSnapshot.scope, explicitScope)
|
|
1017
|
-
? rawSnapshot
|
|
1018
|
-
: null;
|
|
1019
|
-
const snapshotText = snapshot
|
|
1020
|
-
? [
|
|
1021
|
-
'## Active Task Snapshot',
|
|
1022
|
-
`- Task: ${snapshot.task}`,
|
|
1023
|
-
`- Status: ${snapshot.status}`,
|
|
1024
|
-
`- Updated: ${snapshot.updated_at}`,
|
|
1025
|
-
'',
|
|
1026
|
-
'### Summary',
|
|
1027
|
-
snapshot.summary,
|
|
1028
|
-
'',
|
|
1029
|
-
'### Next step',
|
|
1030
|
-
snapshot.next_step,
|
|
1031
|
-
'',
|
|
1032
|
-
].join('\n')
|
|
1033
|
-
: '';
|
|
1034
|
-
const memoryText = formatMemories(results, hippoRoot);
|
|
1035
|
-
return snapshotText ? `${snapshotText}\n${memoryText}` : memoryText;
|
|
1036
|
-
}
|
|
1037
|
-
case 'hippo_status': {
|
|
1038
|
-
const entries = loadAllEntries(hippoRoot, tenantId);
|
|
1039
|
-
const now = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
|
|
1040
|
-
let atRisk = 0;
|
|
1041
|
-
let totalStrength = 0;
|
|
1042
|
-
for (const e of entries) {
|
|
1043
|
-
const s = calculateStrength(e, now);
|
|
1044
|
-
totalStrength += s;
|
|
1045
|
-
if (s < 0.1 && !e.pinned)
|
|
1046
|
-
atRisk++;
|
|
1047
|
-
}
|
|
1048
|
-
const avgStrength = entries.length > 0 ? (totalStrength / entries.length).toFixed(2) : '0';
|
|
1049
|
-
const pinned = entries.filter((e) => e.pinned).length;
|
|
1050
|
-
const errors = entries.filter((e) => e.tags.includes('error')).length;
|
|
1051
|
-
const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId).length;
|
|
1052
|
-
return [
|
|
1053
|
-
`Memories: ${entries.length} (${pinned} pinned, ${errors} errors)`,
|
|
1054
|
-
`Avg strength: ${avgStrength}`,
|
|
1055
|
-
`At risk (<0.1): ${atRisk}`,
|
|
1056
|
-
`Open conflicts: ${conflicts}`,
|
|
1057
|
-
`Half-life default: ${config.defaultHalfLifeDays}d`,
|
|
1058
|
-
].join('\n');
|
|
1059
|
-
}
|
|
1060
|
-
case 'hippo_learn': {
|
|
1061
|
-
const days = Number(args.days) || 7;
|
|
1062
|
-
if (!isGitRepo(process.cwd()))
|
|
1063
|
-
return 'No git history found.';
|
|
1064
|
-
const gitLog = fetchGitLog(process.cwd(), days);
|
|
1065
|
-
if (!gitLog.trim())
|
|
1066
|
-
return 'No fix/revert/bug commits found in the specified period.';
|
|
1067
|
-
const parsedLessons = extractLessons(gitLog, config.gitLearnPatterns);
|
|
1068
|
-
// DF4: admission gate lives at the write path, not in extractLessons
|
|
1069
|
-
// (a published API surface that only parses). Bare subjects like
|
|
1070
|
-
// "fixed signals" are dropped here, before they ever become a memory.
|
|
1071
|
-
// Gating the loop INPUT is correct here, unlike the CLI path: this
|
|
1072
|
-
// loop only writes. The CLI's loop also runs invalidation, so there
|
|
1073
|
-
// the gate has to sit on the write alone or a migration subject stops
|
|
1074
|
-
// superseding stale memories. Same predicate, different placement,
|
|
1075
|
-
// because the loops do different work.
|
|
1076
|
-
const { kept: lessons, dropped } = partitionLessons(parsedLessons);
|
|
1077
|
-
const lowInfo = dropped.length;
|
|
1078
|
-
let added = 0;
|
|
1079
|
-
let skipped = 0;
|
|
1080
|
-
let rejected = 0;
|
|
1081
|
-
for (const lesson of lessons) {
|
|
1082
|
-
if (deduplicateLesson(hippoRoot, lesson, 0.7, tenantId)) {
|
|
1083
|
-
skipped++;
|
|
1084
|
-
continue;
|
|
1085
|
-
}
|
|
1086
|
-
const entry = createMemory(lesson, {
|
|
1087
|
-
layer: Layer.Episodic,
|
|
1088
|
-
tags: ['git-learned'],
|
|
1089
|
-
source: 'git',
|
|
1090
|
-
confidence: 'observed',
|
|
1091
|
-
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
1092
|
-
tenantId,
|
|
1093
|
-
});
|
|
1094
|
-
// AT1 (plan §3 containment): a refused lesson must not crash the
|
|
1095
|
-
// MCP learn call or lose the rest of the git log scan.
|
|
1096
|
-
try {
|
|
1097
|
-
writeEntry(hippoRoot, entry, { actor: ctx?.actor ?? 'mcp' });
|
|
1098
|
-
}
|
|
1099
|
-
catch (err) {
|
|
1100
|
-
if (err instanceof RejectedValueError) {
|
|
1101
|
-
rejected++;
|
|
1102
|
-
continue;
|
|
1103
|
-
}
|
|
1104
|
-
throw err;
|
|
1105
|
-
}
|
|
1106
|
-
added++;
|
|
1107
|
-
}
|
|
1108
|
-
const rejectedSuffix = rejected > 0 ? `, ${rejected} rejected values skipped` : '';
|
|
1109
|
-
const lowInfoSuffix = lowInfo > 0 ? `, ${lowInfo} low-information subjects dropped` : '';
|
|
1110
|
-
return `Git learn: ${added} new, ${skipped} duplicates skipped${rejectedSuffix}${lowInfoSuffix} (scanned ${days} days)`;
|
|
1111
|
-
}
|
|
1112
|
-
case 'hippo_conflicts': {
|
|
1113
|
-
const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId);
|
|
1114
|
-
if (conflicts.length === 0)
|
|
1115
|
-
return 'No open conflicts.';
|
|
1116
|
-
return conflicts.map((c) => `conflict_${c.id}: ${c.memory_a_id} <-> ${c.memory_b_id} (score=${c.score.toFixed(2)}) — ${c.reason}`).join('\n');
|
|
1117
|
-
}
|
|
1118
|
-
case 'hippo_resolve': {
|
|
1119
|
-
const conflictId = Number(args.conflict_id);
|
|
1120
|
-
const keepId = String(args.keep || '');
|
|
1121
|
-
const forget = Boolean(args.forget);
|
|
1122
|
-
// AT1: optional rejectLoser + reason, threaded straight through to
|
|
1123
|
-
// resolveConflict's opts (plan §5 — mirrors the CLI's --reject-loser).
|
|
1124
|
-
const rejectLoser = Boolean(args.rejectLoser);
|
|
1125
|
-
const reason = isJsonString(args.reason) ? args.reason : undefined;
|
|
1126
|
-
if (isNaN(conflictId) || !keepId)
|
|
1127
|
-
return 'Required: conflict_id and keep.';
|
|
1128
|
-
const result = resolveConflict(hippoRoot, conflictId, keepId, forget, tenantId, {
|
|
1129
|
-
rejectLoserValue: rejectLoser,
|
|
1130
|
-
reason,
|
|
1131
|
-
// P2 fix: resolveConflict's opts.rejectedBy defaults to 'cli' when
|
|
1132
|
-
// omitted — this call site never passed it, so the tombstone's
|
|
1133
|
-
// rejected_by AND the conflict_resolve audit's actor both landed as
|
|
1134
|
-
// 'cli' even though the caller was MCP. ctx.actor carries the
|
|
1135
|
-
// auth-resolved actor for HTTP-MCP (see McpContext above); stdio
|
|
1136
|
-
// callers pass no ctx, so 'mcp' is the honest fallback there.
|
|
1137
|
-
rejectedBy: ctx?.actor ?? 'mcp',
|
|
1138
|
-
});
|
|
1139
|
-
if (!result)
|
|
1140
|
-
return 'Could not resolve. Check the conflict ID and --keep value.';
|
|
1141
|
-
const action = rejectLoser ? 'rejected (tombstoned) and removed' : forget ? 'deleted' : 'weakened';
|
|
1142
|
-
return `Resolved conflict ${conflictId}: kept ${keepId}, ${action} ${result.loserId}`;
|
|
1143
|
-
}
|
|
1144
|
-
case 'hippo_share': {
|
|
1145
|
-
const shareId = String(args.id || '');
|
|
1146
|
-
if (!shareId)
|
|
1147
|
-
return 'Required: id (memory ID to share).';
|
|
1148
|
-
const force = Boolean(args.force);
|
|
1149
|
-
// Pass tenantId so shareMemory's readEntry filters by tenant. Without
|
|
1150
|
-
// this, a Bearer for tenant A could call hippo_share with tenant B's
|
|
1151
|
-
// id and copy the row to the global store. The 'Memory not found'
|
|
1152
|
-
// error matches the cross-tenant deny shape elsewhere in the code.
|
|
1153
|
-
const shared = shareMemory(hippoRoot, shareId, { force, tenantId });
|
|
1154
|
-
if (!shared)
|
|
1155
|
-
return 'Transfer score too low. Use force=true to override.';
|
|
1156
|
-
return `Shared [${shared.id}] to global store. Source: ${shared.source}`;
|
|
1157
|
-
}
|
|
1158
|
-
case 'hippo_peers': {
|
|
1159
|
-
// D4 v1.12.10: tenant-scope the cross-project peer discovery.
|
|
1160
|
-
// tenantId is the caller's tenant (matches hippo_share above);
|
|
1161
|
-
// passing undefined would restore the pre-D4 host-wide behaviour.
|
|
1162
|
-
const peers = listPeers(undefined, tenantId);
|
|
1163
|
-
if (peers.length === 0)
|
|
1164
|
-
return 'No peers found.';
|
|
1165
|
-
return peers.map((p) => `${p.project}: ${p.count} memories (latest: ${p.latest.slice(0, 10)})`).join('\n');
|
|
1166
|
-
}
|
|
1167
|
-
default:
|
|
1168
|
-
return `Unknown tool: ${name}`;
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
// ── Request handling ──
|
|
1172
|
-
/**
|
|
1173
|
-
* Transport-agnostic MCP dispatcher. Both the stdio loop (below) and the
|
|
1174
|
-
* HTTP/SSE transport in src/server.ts route every incoming JSON-RPC message
|
|
1175
|
-
* through this single function. Returns null for notifications (no response
|
|
1176
|
-
* expected) and a McpResponse otherwise. Errors thrown by `executeTool` are
|
|
1177
|
-
* the caller's problem — wrap with try/catch on the transport side.
|
|
1178
|
-
*/
|
|
1179
|
-
export async function handleMcpRequest(req, ctx) {
|
|
1180
|
-
const { id, method, params } = req;
|
|
1181
|
-
switch (method) {
|
|
1182
|
-
case 'initialize':
|
|
1183
|
-
return {
|
|
1184
|
-
jsonrpc: '2.0',
|
|
1185
|
-
id,
|
|
1186
|
-
result: {
|
|
1187
|
-
protocolVersion: '2024-11-05',
|
|
1188
|
-
capabilities: { tools: {} },
|
|
1189
|
-
serverInfo: { name: 'hippo-memory', version: PACKAGE_VERSION },
|
|
1190
|
-
},
|
|
1191
|
-
};
|
|
1192
|
-
case 'notifications/initialized':
|
|
1193
|
-
return null;
|
|
1194
|
-
case 'tools/list':
|
|
1195
|
-
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
1196
|
-
case 'tools/call': {
|
|
1197
|
-
const nameValue = params?.name;
|
|
1198
|
-
const toolName = isJsonString(nameValue) ? nameValue : '';
|
|
1199
|
-
const argumentsValue = params?.arguments;
|
|
1200
|
-
const toolArgs = isJsonObjectRecord(argumentsValue) ? argumentsValue : {};
|
|
1201
|
-
const output = await executeTool(toolName, toolArgs, ctx);
|
|
1202
|
-
return {
|
|
1203
|
-
jsonrpc: '2.0',
|
|
1204
|
-
id,
|
|
1205
|
-
result: {
|
|
1206
|
-
content: [{ type: 'text', text: output || 'Done.' }],
|
|
1207
|
-
},
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
default:
|
|
1211
|
-
// Notifications (no id) must not receive a response
|
|
1212
|
-
if (method.startsWith('notifications/'))
|
|
1213
|
-
return null;
|
|
1214
|
-
return {
|
|
1215
|
-
jsonrpc: '2.0',
|
|
1216
|
-
id,
|
|
1217
|
-
error: { code: -32601, message: `Method not found: ${method}` },
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
// ── Stdio transport ──
|
|
1222
|
-
import { parseFrame } from './framing.js';
|
|
1223
|
-
let buffer = Buffer.alloc(0);
|
|
1224
|
-
function dispatch(body) {
|
|
1225
|
-
let req;
|
|
1226
|
-
try {
|
|
1227
|
-
// SAFETY: malformed JSON is caught below and the frame is skipped; the
|
|
1228
|
-
// JSON-RPC shape itself is validated field-by-field next (req.method
|
|
1229
|
-
// truthiness check), matching the src/server.ts HTTP transport's own
|
|
1230
|
-
// `JSON.parse(raw) as McpRequest` boundary cast.
|
|
1231
|
-
req = JSON.parse(body);
|
|
1232
|
-
}
|
|
1233
|
-
catch {
|
|
1234
|
-
return; // skip malformed
|
|
1235
|
-
}
|
|
1236
|
-
if (!req.method)
|
|
1237
|
-
return;
|
|
1238
|
-
if (req.method.startsWith('notifications/')) {
|
|
1239
|
-
handleMcpRequest(req).catch(() => { });
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
handleMcpRequest(req).then((resp) => { if (resp)
|
|
1243
|
-
send(resp); }).catch((err) => {
|
|
1244
|
-
send({ jsonrpc: '2.0', id: req.id, error: { code: -32603, message: err?.message ?? 'Internal error' } });
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
/**
|
|
1248
|
-
* Wire stdin/stdout to the dispatcher. Idempotent — only the entrypoint
|
|
1249
|
-
* (cli.ts `hippo mcp`, or running this file directly) should call this.
|
|
1250
|
-
* src/server.ts imports `handleMcpRequest` without invoking this, so the
|
|
1251
|
-
* HTTP daemon does not steal stdin or exit when its parent closes a pipe.
|
|
1252
|
-
*/
|
|
1253
|
-
export function startStdioLoop() {
|
|
1254
|
-
process.stdin.on('data', (chunk) => {
|
|
1255
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
1256
|
-
while (true) {
|
|
1257
|
-
const result = parseFrame(buffer);
|
|
1258
|
-
if (result.kind === 'incomplete')
|
|
1259
|
-
break;
|
|
1260
|
-
buffer = result.rest;
|
|
1261
|
-
if (result.kind === 'message')
|
|
1262
|
-
dispatch(result.body);
|
|
1263
|
-
}
|
|
1264
|
-
});
|
|
1265
|
-
process.stdin.on('end', () => process.exit(0));
|
|
1266
|
-
process.on('uncaughtException', (err) => {
|
|
1267
|
-
process.stderr.write(`hippo-mcp uncaught: ${err?.message ?? err}\n`);
|
|
1268
|
-
});
|
|
1269
|
-
process.on('unhandledRejection', (err) => {
|
|
1270
|
-
process.stderr.write(`hippo-mcp unhandled: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
// Auto-start when invoked as the main module (node dist/mcp/server.js or via
|
|
1274
|
-
// the cli's `import('./mcp/server.js')`). Importing this file from another
|
|
1275
|
-
// module (e.g. src/server.ts wiring up the HTTP/SSE transport) will NOT
|
|
1276
|
-
// trigger the stdio loop. The cli imports this file specifically to start
|
|
1277
|
-
// stdio; that import is also `import.meta.url === main`-equivalent because
|
|
1278
|
-
// it's executed as the program, so we keep a fallback: if HIPPO_MCP_STDIO=1
|
|
1279
|
-
// or argv1 ends in /mcp/server.js we start.
|
|
1280
|
-
const isMainModule = (() => {
|
|
1281
|
-
try {
|
|
1282
|
-
const argv1 = process.argv[1] ?? '';
|
|
1283
|
-
if (argv1.endsWith('mcp/server.js') || argv1.endsWith('mcp\\server.js'))
|
|
1284
|
-
return true;
|
|
1285
|
-
if (process.env.HIPPO_MCP_STDIO === '1')
|
|
1286
|
-
return true;
|
|
1287
|
-
// ESM main-module check
|
|
1288
|
-
const mainUrl = `file://${argv1.replace(/\\/g, '/')}`;
|
|
1289
|
-
return import.meta.url === mainUrl || import.meta.url === `file:///${argv1.replace(/\\/g, '/')}`;
|
|
1290
|
-
}
|
|
1291
|
-
catch {
|
|
1292
|
-
return false;
|
|
1293
|
-
}
|
|
1294
|
-
})();
|
|
1295
|
-
if (isMainModule) {
|
|
1296
|
-
startStdioLoop();
|
|
1297
|
-
}
|
|
1298
|
-
//# sourceMappingURL=server.js.map
|