hippo-memory 1.60.0 → 1.61.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 +1 -1
- package/dist/ablation.js +9 -27
- package/dist/agent-memories/apply.js +4 -1
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/codex.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/agent-memories/legacy.js +1 -1
- package/dist/agent-memories/source.js +1 -1
- package/dist/agent-memories/sync.js +8 -3
- package/dist/ambient-store.d.ts +14 -0
- package/dist/ambient-store.js +90 -0
- package/dist/ambient.d.ts +23 -0
- package/dist/ambient.js +72 -50
- package/dist/api/assemble.d.ts +93 -0
- package/dist/api/assemble.js +152 -0
- package/dist/api/audit.d.ts +17 -0
- package/dist/api/audit.js +23 -0
- package/dist/api/auth.d.ts +79 -0
- package/dist/api/auth.js +178 -0
- package/dist/api/context-types.d.ts +105 -0
- package/dist/api/context-types.js +3 -0
- package/dist/api/context.d.ts +31 -0
- package/dist/api/context.js +705 -0
- package/dist/api/dormant.d.ts +30 -0
- package/dist/api/dormant.js +140 -0
- package/dist/api/drill-down.d.ts +84 -0
- package/dist/api/drill-down.js +123 -0
- package/dist/api/forget.d.ts +57 -0
- package/dist/api/forget.js +87 -0
- package/dist/api/goals.d.ts +18 -0
- package/dist/api/goals.js +33 -0
- package/dist/api/learn.d.ts +31 -0
- package/dist/api/learn.js +88 -0
- package/dist/api/outcome.d.ts +61 -0
- package/dist/api/outcome.js +66 -0
- package/dist/api/promote.d.ts +54 -0
- package/dist/api/promote.js +203 -0
- package/dist/api/quarantine.d.ts +24 -0
- package/dist/api/quarantine.js +121 -0
- package/dist/api/recall-types.d.ts +390 -0
- package/dist/api/recall-types.js +3 -0
- package/dist/api/recall.d.ts +36 -0
- package/dist/api/recall.js +634 -0
- package/dist/api/remember.d.ts +35 -0
- package/dist/api/remember.js +43 -0
- package/dist/api/sleep.d.ts +136 -0
- package/dist/api/sleep.js +271 -0
- package/dist/api/tokens.d.ts +26 -0
- package/dist/api/tokens.js +60 -0
- package/dist/api/types.d.ts +56 -0
- package/dist/api/types.js +38 -0
- package/dist/api.d.ts +20 -1262
- package/dist/api.js +25 -2727
- package/dist/audit.d.ts +3 -0
- package/dist/audit.js +6 -3
- package/dist/auth.d.ts +45 -4
- package/dist/auth.js +125 -48
- package/dist/autolearn.js +2 -2
- package/dist/capture/command.d.ts +33 -0
- package/dist/capture/command.js +264 -0
- package/dist/capture/compact.d.ts +44 -0
- package/dist/capture/compact.js +354 -0
- package/dist/capture/extract.d.ts +21 -0
- package/dist/capture/extract.js +464 -0
- package/dist/capture/transcript.d.ts +40 -0
- package/dist/capture/transcript.js +193 -0
- package/dist/capture-error.js +2 -1
- package/dist/churn-git.js +4 -2
- package/dist/cli/audit.d.ts +3 -0
- package/dist/cli/audit.js +159 -0
- package/dist/cli/auth.d.ts +2 -0
- package/dist/cli/auth.js +171 -0
- package/dist/cli/briefs.d.ts +4 -0
- package/dist/cli/briefs.js +435 -0
- package/dist/cli/card.d.ts +3 -0
- package/dist/cli/card.js +333 -0
- package/dist/cli/context.d.ts +15 -0
- package/dist/cli/context.js +366 -0
- package/dist/cli/continuity.d.ts +6 -0
- package/dist/cli/continuity.js +445 -0
- package/dist/cli/curate.d.ts +19 -0
- package/dist/cli/curate.js +555 -0
- package/dist/cli/dag.d.ts +5 -0
- package/dist/cli/dag.js +177 -0
- package/dist/cli/decisions.d.ts +4 -0
- package/dist/cli/decisions.js +528 -0
- package/dist/cli/eval.d.ts +5 -0
- package/dist/cli/eval.js +213 -0
- package/dist/cli/explain.d.ts +4 -0
- package/dist/cli/explain.js +150 -0
- package/dist/cli/goals.d.ts +2 -0
- package/dist/cli/goals.js +196 -0
- package/dist/cli/hook-blocks.d.ts +18 -0
- package/dist/cli/hook-blocks.js +233 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +305 -0
- package/dist/cli/maintenance.d.ts +4 -0
- package/dist/cli/maintenance.js +179 -0
- package/dist/cli/playbooks.d.ts +4 -0
- package/dist/cli/playbooks.js +556 -0
- package/dist/cli/projects.js +1 -1
- package/dist/cli/recall.d.ts +7 -0
- package/dist/cli/recall.js +597 -0
- package/dist/cli/remember.d.ts +5 -0
- package/dist/cli/remember.js +442 -0
- package/dist/cli/serve.d.ts +5 -0
- package/dist/cli/serve.js +40 -0
- package/dist/cli/session-hooks.d.ts +29 -0
- package/dist/cli/session-hooks.js +637 -0
- package/dist/cli/setup.d.ts +4 -0
- package/dist/cli/setup.js +376 -0
- package/dist/cli/shared.d.ts +15 -19
- package/dist/cli/shared.js +50 -341
- package/dist/cli/slack.d.ts +2 -0
- package/dist/cli/slack.js +171 -0
- package/dist/cli/status.d.ts +18 -0
- package/dist/cli/status.js +400 -0
- package/dist/cli/transfer.d.ts +10 -0
- package/dist/cli/transfer.js +438 -0
- package/dist/cli/usage.d.ts +85 -0
- package/dist/cli/usage.js +741 -0
- package/dist/cli.d.ts +71 -120
- package/dist/cli.js +170 -8469
- package/dist/client.js +15 -8
- package/dist/compaction-record.js +6 -4
- package/dist/connectors/github/backfill.js +94 -87
- package/dist/connectors/github/cli-impl.js +4 -3
- package/dist/connectors/github/dlq.js +67 -54
- package/dist/connectors/github/ingest.js +34 -35
- package/dist/connectors/github/tenant-routing.js +3 -2
- package/dist/connectors/github/webhook.js +135 -216
- package/dist/connectors/slack/dlq.js +49 -61
- package/dist/connectors/slack/ingest.js +55 -48
- package/dist/connectors/slack/tenant-routing.js +4 -3
- package/dist/connectors/slack/webhook.js +72 -74
- package/dist/consolidate/conflicts.d.ts +10 -0
- package/dist/consolidate/conflicts.js +178 -0
- package/dist/consolidate/decay.d.ts +12 -0
- package/dist/consolidate/decay.js +145 -0
- package/dist/consolidate/llm-passes.d.ts +3 -0
- package/dist/consolidate/llm-passes.js +141 -0
- package/dist/consolidate/merge.d.ts +7 -0
- package/dist/consolidate/merge.js +251 -0
- package/dist/consolidate/physics-pass.d.ts +3 -0
- package/dist/consolidate/physics-pass.js +60 -0
- package/dist/consolidate/run.d.ts +69 -0
- package/dist/consolidate/run.js +76 -0
- package/dist/consolidate/sleep.d.ts +18 -0
- package/dist/consolidate/sleep.js +209 -0
- package/dist/consolidate/traces.d.ts +4 -0
- package/dist/consolidate/traces.js +178 -0
- package/dist/context-auto.js +7 -11
- package/dist/context-render.d.ts +1 -1
- package/dist/context-render.js +1 -1
- package/dist/customer-notes.d.ts +3 -0
- package/dist/customer-notes.js +6 -4
- package/dist/dag.js +6 -5
- package/dist/dashboard-actions.d.ts +20 -0
- package/dist/dashboard-actions.js +88 -0
- package/dist/dashboard-params.d.ts +45 -0
- package/dist/dashboard-params.js +127 -0
- package/dist/dashboard-queries.d.ts +15 -0
- package/dist/dashboard-queries.js +355 -0
- package/dist/dashboard-snapshot.d.ts +138 -0
- package/dist/dashboard-snapshot.js +308 -0
- package/dist/dashboard-types.d.ts +163 -0
- package/dist/dashboard-types.js +3 -0
- package/dist/dashboard.d.ts +6 -7
- package/dist/dashboard.js +228 -202
- package/dist/db/busy.d.ts +5 -0
- package/dist/db/busy.js +24 -0
- package/dist/db/continuity.d.ts +5 -0
- package/dist/db/continuity.js +145 -0
- package/dist/db/meta.d.ts +8 -0
- package/dist/db/meta.js +35 -0
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.js +138 -0
- package/dist/db/migrations/index.d.ts +5 -0
- package/dist/db/migrations/index.js +109 -0
- package/dist/db/migrations/types.d.ts +14 -0
- package/dist/db/migrations/types.js +2 -0
- package/dist/db/migrations/v01.d.ts +3 -0
- package/dist/db/migrations/v01.js +38 -0
- package/dist/db/migrations/v02.d.ts +3 -0
- package/dist/db/migrations/v02.js +21 -0
- package/dist/db/migrations/v03.d.ts +3 -0
- package/dist/db/migrations/v03.js +22 -0
- package/dist/db/migrations/v04.d.ts +3 -0
- package/dist/db/migrations/v04.js +28 -0
- package/dist/db/migrations/v05.d.ts +3 -0
- package/dist/db/migrations/v05.js +21 -0
- package/dist/db/migrations/v06.d.ts +3 -0
- package/dist/db/migrations/v06.js +25 -0
- package/dist/db/migrations/v07.d.ts +3 -0
- package/dist/db/migrations/v07.js +13 -0
- package/dist/db/migrations/v08.d.ts +3 -0
- package/dist/db/migrations/v08.js +8 -0
- package/dist/db/migrations/v09.d.ts +3 -0
- package/dist/db/migrations/v09.js +13 -0
- package/dist/db/migrations/v10.d.ts +3 -0
- package/dist/db/migrations/v10.js +17 -0
- package/dist/db/migrations/v11.d.ts +3 -0
- package/dist/db/migrations/v11.js +15 -0
- package/dist/db/migrations/v12.d.ts +3 -0
- package/dist/db/migrations/v12.js +11 -0
- package/dist/db/migrations/v13.d.ts +3 -0
- package/dist/db/migrations/v13.js +15 -0
- package/dist/db/migrations/v14.d.ts +3 -0
- package/dist/db/migrations/v14.js +66 -0
- package/dist/db/migrations/v15.d.ts +3 -0
- package/dist/db/migrations/v15.js +42 -0
- package/dist/db/migrations/v16.d.ts +3 -0
- package/dist/db/migrations/v16.js +61 -0
- package/dist/db/migrations/v17.d.ts +3 -0
- package/dist/db/migrations/v17.js +46 -0
- package/dist/db/migrations/v18.d.ts +3 -0
- package/dist/db/migrations/v18.js +60 -0
- package/dist/db/migrations/v19.d.ts +3 -0
- package/dist/db/migrations/v19.js +28 -0
- package/dist/db/migrations/v20.d.ts +3 -0
- package/dist/db/migrations/v20.js +42 -0
- package/dist/db/migrations/v21.d.ts +3 -0
- package/dist/db/migrations/v21.js +16 -0
- package/dist/db/migrations/v22.d.ts +3 -0
- package/dist/db/migrations/v22.js +82 -0
- package/dist/db/migrations/v23.d.ts +3 -0
- package/dist/db/migrations/v23.js +48 -0
- package/dist/db/migrations/v24.d.ts +3 -0
- package/dist/db/migrations/v24.js +72 -0
- package/dist/db/migrations/v25.d.ts +3 -0
- package/dist/db/migrations/v25.js +46 -0
- package/dist/db/migrations/v26.d.ts +3 -0
- package/dist/db/migrations/v26.js +23 -0
- package/dist/db/migrations/v27.d.ts +3 -0
- package/dist/db/migrations/v27.js +57 -0
- package/dist/db/migrations/v28.d.ts +3 -0
- package/dist/db/migrations/v28.js +38 -0
- package/dist/db/migrations/v29.d.ts +3 -0
- package/dist/db/migrations/v29.js +78 -0
- package/dist/db/migrations/v30.d.ts +3 -0
- package/dist/db/migrations/v30.js +92 -0
- package/dist/db/migrations/v31.d.ts +3 -0
- package/dist/db/migrations/v31.js +74 -0
- package/dist/db/migrations/v32.d.ts +3 -0
- package/dist/db/migrations/v32.js +102 -0
- package/dist/db/migrations/v33.d.ts +3 -0
- package/dist/db/migrations/v33.js +104 -0
- package/dist/db/migrations/v34.d.ts +3 -0
- package/dist/db/migrations/v34.js +93 -0
- package/dist/db/migrations/v35.d.ts +3 -0
- package/dist/db/migrations/v35.js +98 -0
- package/dist/db/migrations/v36.d.ts +3 -0
- package/dist/db/migrations/v36.js +98 -0
- package/dist/db/migrations/v37.d.ts +3 -0
- package/dist/db/migrations/v37.js +219 -0
- package/dist/db/migrations/v38.d.ts +3 -0
- package/dist/db/migrations/v38.js +277 -0
- package/dist/db/migrations/v39.d.ts +3 -0
- package/dist/db/migrations/v39.js +59 -0
- package/dist/db/migrations/v40.d.ts +3 -0
- package/dist/db/migrations/v40.js +74 -0
- package/dist/db/migrations/v41.d.ts +3 -0
- package/dist/db/migrations/v41.js +43 -0
- package/dist/db/migrations/v42.d.ts +3 -0
- package/dist/db/migrations/v42.js +41 -0
- package/dist/db/migrations/v43.d.ts +3 -0
- package/dist/db/migrations/v43.js +67 -0
- package/dist/db/migrations/v44.d.ts +3 -0
- package/dist/db/migrations/v44.js +28 -0
- package/dist/db/migrations/v45.d.ts +3 -0
- package/dist/db/migrations/v45.js +30 -0
- package/dist/db/migrations/v46.d.ts +3 -0
- package/dist/db/migrations/v46.js +25 -0
- package/dist/db/migrations/v47.d.ts +3 -0
- package/dist/db/migrations/v47.js +17 -0
- package/dist/db/migrations/v48.d.ts +3 -0
- package/dist/db/migrations/v48.js +10 -0
- package/dist/db/migrations/v49.d.ts +3 -0
- package/dist/db/migrations/v49.js +31 -0
- package/dist/db/migrations/v50.d.ts +3 -0
- package/dist/db/migrations/v50.js +67 -0
- package/dist/db/migrations/v51.d.ts +3 -0
- package/dist/db/migrations/v51.js +14 -0
- package/dist/db/migrations/v52.d.ts +3 -0
- package/dist/db/migrations/v52.js +7 -0
- package/dist/db/open.d.ts +23 -0
- package/dist/db/open.js +146 -0
- package/dist/db/sqlite.d.ts +21 -0
- package/dist/db/sqlite.js +8 -0
- package/dist/db/tables.d.ts +7 -0
- package/dist/db/tables.js +38 -0
- package/dist/db.d.ts +6 -46
- package/dist/db.js +5 -3049
- package/dist/decisions.d.ts +4 -1
- package/dist/decisions.js +9 -7
- package/dist/dedupe.js +3 -2
- package/dist/delivery-recorder.js +4 -1
- package/dist/doctor.js +3 -3
- package/dist/embedding-provider.d.ts +1 -1
- package/dist/embedding-provider.js +4 -3
- package/dist/embeddings.d.ts +9 -52
- package/dist/embeddings.js +43 -297
- package/dist/env.d.ts +75 -0
- package/dist/env.js +119 -0
- package/dist/eval-suite.js +1 -1
- package/dist/eval.js +2 -2
- package/dist/extract.js +1 -1
- package/dist/gated-write.js +3 -1
- package/dist/goals.d.ts +3 -1
- package/dist/goals.js +18 -0
- package/dist/graph/read.d.ts +73 -0
- package/dist/graph/read.js +325 -0
- package/dist/graph/rows.d.ts +45 -0
- package/dist/graph/rows.js +51 -0
- package/dist/graph/types.d.ts +83 -0
- package/dist/graph/types.js +11 -0
- package/dist/graph/write.d.ts +93 -0
- package/dist/{graph.js → graph/write.js} +6 -384
- package/dist/graph-extract.js +2 -1
- package/dist/graph-recall.d.ts +1 -1
- package/dist/graph-recall.js +2 -2
- package/dist/graph-stream.js +1 -1
- package/dist/graph-view.d.ts +1 -1
- package/dist/graph-view.js +1 -1
- package/dist/half-life-migration.d.ts +1 -1
- package/dist/half-life-migration.js +2 -1
- package/dist/hooks/codex-session.d.ts +8 -0
- package/dist/hooks/codex-session.js +76 -0
- package/dist/hooks/codex-wrapper.d.ts +55 -0
- package/dist/hooks/codex-wrapper.js +288 -0
- package/dist/hooks/json-hooks.d.ts +63 -0
- package/dist/hooks/json-hooks.js +356 -0
- package/dist/hooks/opencode.d.ts +50 -0
- package/dist/hooks/opencode.js +202 -0
- package/dist/hooks/shared.d.ts +54 -0
- package/dist/hooks/shared.js +77 -0
- package/dist/http-retry.d.ts +2 -0
- package/dist/http-retry.js +4 -3
- package/dist/http-util.d.ts +3 -0
- package/dist/http-util.js +10 -0
- package/dist/{importers.d.ts → importers/core.d.ts} +13 -17
- package/dist/importers/core.js +141 -0
- package/dist/importers/markdown-parse.d.ts +41 -0
- package/dist/importers/markdown-parse.js +132 -0
- package/dist/importers/markdown.d.ts +3 -0
- package/dist/importers/markdown.js +92 -0
- package/dist/importers/sources.d.ts +7 -0
- package/dist/importers/sources.js +229 -0
- package/dist/importers/vault.d.ts +11 -0
- package/dist/importers/vault.js +352 -0
- package/dist/incidents.d.ts +3 -0
- package/dist/incidents.js +7 -5
- package/dist/index.d.ts +25 -6
- package/dist/index.js +23 -6
- package/dist/invalidation.js +2 -1
- package/dist/judgment.js +2 -1
- package/dist/keyset.d.ts +13 -0
- package/dist/keyset.js +8 -0
- package/dist/local-embedding.d.ts +13 -0
- package/dist/local-embedding.js +165 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +19 -1
- package/dist/mcp/admin-tools.d.ts +8 -0
- package/dist/mcp/admin-tools.js +116 -0
- package/dist/mcp/format.d.ts +27 -0
- package/dist/mcp/format.js +135 -0
- package/dist/mcp/memory-tools.d.ts +5 -0
- package/dist/mcp/memory-tools.js +83 -0
- package/dist/mcp/protocol.d.ts +83 -0
- package/dist/mcp/protocol.js +55 -0
- package/dist/mcp/recall-tools.d.ts +6 -0
- package/dist/mcp/recall-tools.js +320 -0
- package/dist/mcp/request.d.ts +10 -0
- package/dist/mcp/request.js +163 -0
- package/dist/mcp/server.d.ts +4 -75
- package/dist/mcp/server.js +7 -1190
- package/dist/mcp/session-state.d.ts +11 -0
- package/dist/mcp/session-state.js +28 -0
- package/dist/mcp/stdio.d.ts +8 -0
- package/dist/mcp/stdio.js +79 -0
- package/dist/mcp/tools.d.ts +11 -0
- package/dist/mcp/tools.js +247 -0
- package/dist/memory.d.ts +3 -0
- package/dist/memory.js +27 -1
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +2 -1
- package/dist/owner-validation.js +2 -1
- package/dist/physics-state.js +10 -7
- package/dist/policies.d.ts +3 -0
- package/dist/policies.js +8 -6
- package/dist/postinstall.js +3 -2
- package/dist/predictions/planning-fallacy.d.ts +100 -0
- package/dist/predictions/planning-fallacy.js +190 -0
- package/dist/{predictions.d.ts → predictions/store.d.ts} +7 -102
- package/dist/predictions/store.js +434 -0
- package/dist/processes.d.ts +3 -0
- package/dist/processes.js +7 -5
- package/dist/project-briefs.d.ts +3 -0
- package/dist/project-briefs.js +7 -5
- package/dist/project-identity.js +3 -2
- package/dist/project-merge.js +3 -1
- package/dist/quarantine.d.ts +2 -1
- package/dist/quarantine.js +8 -5
- package/dist/raw-archive.js +1 -1
- package/dist/recall-history.js +3 -2
- package/dist/recall-pipeline.d.ts +2 -2
- package/dist/recall-pipeline.js +16 -8
- package/dist/recall-scope.js +1 -1
- package/dist/recall-trace.d.ts +1 -1
- package/dist/refine-llm.js +2 -1
- package/dist/reject-flow.js +6 -1
- package/dist/rerankers/clef.js +6 -5
- package/dist/rerankers/jev.d.ts +1 -1
- package/dist/rerankers/jev.js +4 -4
- package/dist/rerankers/llm.d.ts +4 -2
- package/dist/rerankers/llm.js +58 -38
- package/dist/rerankers/types.d.ts +1 -1
- package/dist/salience.js +1 -1
- package/dist/scheduler.d.ts +1 -0
- package/dist/scheduler.js +26 -4
- package/dist/scope.js +4 -3
- package/dist/search/as-of.d.ts +10 -0
- package/dist/search/as-of.js +22 -0
- package/dist/search/bm25-search.d.ts +14 -0
- package/dist/search/bm25-search.js +43 -0
- package/dist/search/bm25.d.ts +15 -0
- package/dist/search/bm25.js +54 -0
- package/dist/search/boosts.d.ts +54 -0
- package/dist/search/boosts.js +94 -0
- package/dist/search/breakdown.d.ts +7 -0
- package/dist/search/breakdown.js +20 -0
- package/dist/search/explain.d.ts +25 -0
- package/dist/search/explain.js +31 -0
- package/dist/search/finalize.d.ts +8 -0
- package/dist/search/finalize.js +52 -0
- package/dist/search/fusion.d.ts +27 -0
- package/dist/search/fusion.js +42 -0
- package/dist/search/hybrid-score.d.ts +20 -0
- package/dist/search/hybrid-score.js +73 -0
- package/dist/search/hybrid.d.ts +46 -0
- package/dist/search/hybrid.js +64 -0
- package/dist/search/physics-search.d.ts +29 -0
- package/dist/search/physics-search.js +162 -0
- package/dist/search/rerank.d.ts +10 -0
- package/dist/search/rerank.js +72 -0
- package/dist/search/temporal.d.ts +15 -0
- package/dist/search/temporal.js +45 -0
- package/dist/search/types.d.ts +91 -0
- package/dist/search/types.js +2 -0
- package/dist/search/vector.d.ts +30 -0
- package/dist/search/vector.js +71 -0
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +2 -1
- package/dist/server/auth.d.ts +52 -0
- package/dist/server/auth.js +221 -0
- package/dist/server/client-ip.d.ts +25 -0
- package/dist/server/client-ip.js +92 -0
- package/dist/server/cursor.d.ts +23 -0
- package/dist/server/cursor.js +58 -0
- package/dist/server/lifecycle.d.ts +7 -0
- package/dist/server/lifecycle.js +29 -0
- package/dist/server/mcp-http.d.ts +5 -0
- package/dist/server/mcp-http.js +199 -0
- package/dist/server/request.d.ts +33 -0
- package/dist/server/request.js +103 -0
- package/dist/server/routes/admin.d.ts +9 -0
- package/dist/server/routes/admin.js +158 -0
- package/dist/server/routes/customer-notes.d.ts +7 -0
- package/dist/server/routes/customer-notes.js +112 -0
- package/dist/server/routes/decisions.d.ts +7 -0
- package/dist/server/routes/decisions.js +133 -0
- package/dist/server/routes/incidents.d.ts +7 -0
- package/dist/server/routes/incidents.js +126 -0
- package/dist/server/routes/memories.d.ts +10 -0
- package/dist/server/routes/memories.js +179 -0
- package/dist/server/routes/policies.d.ts +8 -0
- package/dist/server/routes/policies.js +151 -0
- package/dist/server/routes/predictions.d.ts +7 -0
- package/dist/server/routes/predictions.js +164 -0
- package/dist/server/routes/processes.d.ts +7 -0
- package/dist/server/routes/processes.js +161 -0
- package/dist/server/routes/project-briefs.d.ts +8 -0
- package/dist/server/routes/project-briefs.js +136 -0
- package/dist/server/routes/recall.d.ts +8 -0
- package/dist/server/routes/recall.js +340 -0
- package/dist/server/routes/skills.d.ts +8 -0
- package/dist/server/routes/skills.js +150 -0
- package/dist/server/types.d.ts +56 -0
- package/dist/server/types.js +2 -0
- package/dist/server/validation.d.ts +14 -0
- package/dist/server/validation.js +91 -0
- package/dist/server.d.ts +7 -61
- package/dist/server.js +69 -2362
- package/dist/session-digest.d.ts +1 -1
- package/dist/session-digest.js +9 -2
- package/dist/shared.d.ts +3 -1
- package/dist/shared.js +26 -22
- package/dist/skills.d.ts +3 -0
- package/dist/skills.js +7 -5
- package/dist/stdin.js +2 -2
- package/dist/store/audit-event.d.ts +19 -0
- package/dist/store/audit-event.js +33 -0
- package/dist/store/candidates.d.ts +42 -0
- package/dist/store/candidates.js +152 -0
- package/dist/store/conflicts.d.ts +44 -0
- package/dist/store/conflicts.js +444 -0
- package/dist/store/delete-and-batch.d.ts +63 -0
- package/dist/store/delete-and-batch.js +310 -0
- package/dist/store/entry-reads.d.ts +92 -0
- package/dist/store/entry-reads.js +255 -0
- package/dist/store/entry-row.d.ts +67 -0
- package/dist/store/entry-row.js +208 -0
- package/dist/store/entry-writes.d.ts +46 -0
- package/dist/store/entry-writes.js +148 -0
- package/dist/store/handoffs.d.ts +26 -0
- package/dist/store/handoffs.js +184 -0
- package/dist/store/index-and-stats.d.ts +52 -0
- package/dist/store/index-and-stats.js +214 -0
- package/dist/store/markdown.d.ts +10 -0
- package/dist/store/markdown.js +108 -0
- package/dist/store/mirrors.d.ts +49 -0
- package/dist/store/mirrors.js +312 -0
- package/dist/store/open.d.ts +15 -0
- package/dist/store/open.js +223 -0
- package/dist/store/rows.d.ts +178 -0
- package/dist/store/rows.js +158 -0
- package/dist/store/search-rows.d.ts +86 -0
- package/dist/store/search-rows.js +254 -0
- package/dist/store/sessions.d.ts +83 -0
- package/dist/store/sessions.js +272 -0
- package/dist/store/summaries.d.ts +94 -0
- package/dist/store/summaries.js +377 -0
- package/dist/store/tenant-lookup.d.ts +12 -0
- package/dist/store/tenant-lookup.js +16 -0
- package/dist/store-cards.js +2 -1
- package/dist/summary-dirty.d.ts +4 -0
- package/dist/summary-dirty.js +32 -0
- package/dist/support-bundle.js +4 -3
- package/dist/tenant.js +2 -2
- package/dist/tokenize.d.ts +2 -0
- package/dist/tokenize.js +16 -0
- package/dist/transcript-tail.d.ts +7 -0
- package/dist/transcript-tail.js +48 -0
- package/dist/vector-store.d.ts +27 -0
- package/dist/vector-store.js +210 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/working-memory.js +1 -1
- package/dist/yaml.js +36 -11
- package/dist-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist-ui/assets/index-DPN7cP19.js +33 -0
- package/dist-ui/assets/index-dFloKRVr.css +1 -0
- package/dist-ui/index.html +3 -25
- 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 +1 -1
- package/dist/capture.d.ts +0 -155
- package/dist/capture.js +0 -1295
- package/dist/consolidate.d.ts +0 -58
- package/dist/consolidate.js +0 -1124
- package/dist/graph.d.ts +0 -245
- package/dist/hooks.d.ts +0 -208
- package/dist/hooks.js +0 -1076
- package/dist/importers.js +0 -900
- package/dist/predictions.js +0 -620
- package/dist/search.d.ts +0 -320
- package/dist/search.js +0 -970
- package/dist/store.d.ts +0 -776
- package/dist/store.js +0 -3473
- package/dist-ui/assets/d3-BiWEKnn4.js +0 -1
- package/dist-ui/assets/index-BhT8RvO6.js +0 -61
- package/dist-ui/assets/index-RoXXJ5dq.css +0 -1
- package/dist-ui/assets/three-BDgTxR1l.js +0 -4112
package/dist/mcp/server.js
CHANGED
|
@@ -7,1195 +7,12 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Usage: hippo mcp (or npx hippo-memory mcp)
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { fitBudget, estimateTokens } from '../search.js';
|
|
17
|
-
import { evalNow } from '../ablation.js';
|
|
18
|
-
import { loadStrengthRows, loadTextsHoldingWords, writeEntry, readEntry, listMemoryConflicts, resolveConflict, countCreatedSinceLastSleep } from '../store.js';
|
|
19
|
-
import { shareMemory, listPeers, getGlobalRoot, initGlobal } from '../shared.js';
|
|
20
|
-
import { consolidate } from '../consolidate.js';
|
|
21
|
-
import { fetchGitLog, extractLessons, partitionLessons, isGitRepo } from '../autolearn.js';
|
|
22
|
-
import { dropHeldCopies, duplicateKey, longestWord, storedTextKeys } from '../same-text.js';
|
|
23
|
-
import { loadConfig } from '../config.js';
|
|
24
|
-
import { confidenceLabel } from '../memory.js';
|
|
25
|
-
import { resolveTenantId } from '../tenant.js';
|
|
26
|
-
import { retrieve as apiRetrieve, remember as apiRemember, outcome as apiOutcome, drillDown as apiDrillDown, assemble as apiAssemble, getContext as apiGetContext, buildSuppressionSummary } from '../api.js';
|
|
27
|
-
import { autoDetectContext } from '../context-auto.js';
|
|
28
|
-
import { resolveProjectIdentity, findHippoStoreDir } from '../project-identity.js';
|
|
29
|
-
import { computePredictionBaserate } from '../predictions.js';
|
|
30
|
-
import { appendAuditEvent, auditQueryFields } from '../audit.js';
|
|
31
|
-
import { RejectedValueError } from '../rejection.js';
|
|
32
|
-
import { detectAnchoring, hashQueryText, biasHintEnabled, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from '../recall-history.js';
|
|
33
|
-
import { detectAvailabilityBias } from '../availability.js';
|
|
34
|
-
// v0.33 / J1 — Module-level per-(tenant, session) recall-history ring map
|
|
35
|
-
// for the MCP pipeline. Separate from CLI/HTTP rings per plan v3
|
|
36
|
-
// architecture (per-pipeline rings; no IPC).
|
|
37
|
-
const sessionRecallHistoryMcp = new Map();
|
|
38
|
-
/** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
|
|
39
|
-
export function __resetSessionRecallHistoryMcp() {
|
|
40
|
-
sessionRecallHistoryMcp.clear();
|
|
41
|
-
}
|
|
42
|
-
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
43
|
-
import { recordTokenUse } from '../token-ledger.js';
|
|
44
|
-
import { PACKAGE_VERSION } from '../version.js';
|
|
45
|
-
import { validateToolArgs } from './tool-args.js';
|
|
46
|
-
// ── Find hippo root ──
|
|
47
|
-
/** Same bounded walk as the CLI (ends at home, so HIPPO_HOME wins over ~/.hippo); cwd/opts are the test seam. */
|
|
48
|
-
export function findHippoRoot(cwd = process.cwd(), opts) {
|
|
49
|
-
const local = findHippoStoreDir(cwd, opts);
|
|
50
|
-
if (local !== null)
|
|
51
|
-
return local;
|
|
52
|
-
// Global fallback (respects $HIPPO_HOME / $XDG_DATA_HOME)
|
|
53
|
-
const global = getGlobalRoot();
|
|
54
|
-
return fs.existsSync(global) ? global : null;
|
|
55
|
-
}
|
|
56
|
-
/** JSON-RPC reply for a request that threw: typed API errors keep their text; anything else is logged and answered generically. */
|
|
57
|
-
export function mcpErrorResponse(id, err, requestId = randomUUID()) {
|
|
58
|
-
const { status, message } = mapApiError(err);
|
|
59
|
-
if (status !== 500)
|
|
60
|
-
return { jsonrpc: '2.0', id, error: { code: -32603, message } };
|
|
61
|
-
log.error(`mcp request failed: ${err instanceof Error ? err.message : String(err)}`, { requestId });
|
|
62
|
-
return {
|
|
63
|
-
jsonrpc: '2.0',
|
|
64
|
-
id,
|
|
65
|
-
error: { code: -32603, message: `${INTERNAL_ERROR_MESSAGE} (request id ${requestId})`, data: { requestId } },
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* The api-layer actor for a tool call. Stdio (no ctx) is the local operator
|
|
70
|
-
* and runs as admin; over HTTP the transport's authenticated role is used, so
|
|
71
|
-
* a member key never acts as admin through MCP.
|
|
72
|
-
*/
|
|
73
|
-
function mcpActor(ctx) {
|
|
74
|
-
const actor = { subject: ctx?.actor ?? 'mcp', role: ctx?.role ?? 'admin', scopes: ctx?.scopes };
|
|
75
|
-
if (ctx?.viaAuthResolver)
|
|
76
|
-
actor.viaAuthResolver = true;
|
|
77
|
-
return actor;
|
|
78
|
-
}
|
|
79
|
-
// MCP stdio transport spec: messages are newline-delimited JSON-RPC, no embedded newlines.
|
|
80
|
-
// https://modelcontextprotocol.io/specification/.../basic/transports#stdio
|
|
81
|
-
function send(msg) {
|
|
82
|
-
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
83
|
-
}
|
|
84
|
-
function isJsonString(v) {
|
|
85
|
-
return typeof v === 'string';
|
|
86
|
-
}
|
|
87
|
-
function isJsonBoolean(v) {
|
|
88
|
-
return typeof v === 'boolean';
|
|
89
|
-
}
|
|
90
|
-
function isJsonObjectRecord(v) {
|
|
91
|
-
return v !== undefined && v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
92
|
-
}
|
|
93
|
-
import { formatHandoffEvidenceLine } from '../handoff.js';
|
|
94
|
-
import { assembleCost, assembleText, drillCost, drillText, printedTokens } from '../context-render.js';
|
|
95
|
-
function handoffLines(h) {
|
|
96
|
-
const lines = [`- Summary: ${h.summary}`];
|
|
97
|
-
if (h.nextAction)
|
|
98
|
-
lines.push(`- Next action: ${h.nextAction}`);
|
|
99
|
-
if ((h.artifacts ?? []).length > 0)
|
|
100
|
-
lines.push(`- Artifacts: ${(h.artifacts ?? []).join(', ')}`);
|
|
101
|
-
if (h.outcome)
|
|
102
|
-
lines.push(`- Outcome: ${h.outcome}`);
|
|
103
|
-
if (h.targetRuntime)
|
|
104
|
-
lines.push(`- Target runtime: ${h.targetRuntime}`);
|
|
105
|
-
if (h.cardId)
|
|
106
|
-
lines.push(`- Card: ${h.cardId}`);
|
|
107
|
-
if ((h.constraints ?? []).length > 0)
|
|
108
|
-
lines.push(`- Constraints: ${(h.constraints ?? []).join(', ')}`);
|
|
109
|
-
if (h.evidence)
|
|
110
|
-
lines.push(`- Evidence: ${formatHandoffEvidenceLine(h.evidence)}`);
|
|
111
|
-
return lines;
|
|
112
|
-
}
|
|
113
|
-
function trailLines(events) {
|
|
114
|
-
return events.map((e) => {
|
|
115
|
-
const preview = e.content.length > 200 ? e.content.slice(0, 200) + '…' : e.content;
|
|
116
|
-
return `- [${e.event_type}] ${preview}`;
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
function formatContinuityBlock(block) {
|
|
120
|
-
const lines = ['## Continuity'];
|
|
121
|
-
if (block.activeSnapshot) {
|
|
122
|
-
lines.push('');
|
|
123
|
-
lines.push('### Active Task Snapshot');
|
|
124
|
-
lines.push(`- Task: ${block.activeSnapshot.task}`);
|
|
125
|
-
lines.push(`- Summary: ${block.activeSnapshot.summary}`);
|
|
126
|
-
lines.push(`- Next: ${block.activeSnapshot.next_step}`);
|
|
127
|
-
}
|
|
128
|
-
if (block.sessionHandoff) {
|
|
129
|
-
lines.push('');
|
|
130
|
-
lines.push('### Session Handoff');
|
|
131
|
-
lines.push(...handoffLines(block.sessionHandoff));
|
|
132
|
-
}
|
|
133
|
-
if (block.recentSessionEvents.length > 0) {
|
|
134
|
-
lines.push('');
|
|
135
|
-
lines.push('### Recent Session Trail');
|
|
136
|
-
lines.push(...trailLines(block.recentSessionEvents));
|
|
137
|
-
}
|
|
138
|
-
if (lines.length === 1) {
|
|
139
|
-
lines.push('');
|
|
140
|
-
lines.push('(no active task snapshot, handoff, or recent events for this tenant)');
|
|
141
|
-
}
|
|
142
|
-
return lines.join('\n');
|
|
143
|
-
}
|
|
144
|
-
const NO_MEMORIES = 'No relevant memories found.';
|
|
145
|
-
function memoriesHeading(count) {
|
|
146
|
-
return `Found ${count} memories:\n`;
|
|
147
|
-
}
|
|
148
|
-
function formatMemory(r) {
|
|
149
|
-
const conf = confidenceLabel(r.entry).text;
|
|
150
|
-
const tags = r.entry.tags.length > 0 ? ` tags: ${r.entry.tags.join(', ')}` : '';
|
|
151
|
-
return `[${conf}]${tags} (strength=${r.entry.strength.toFixed(2)})\n${r.entry.content}\n`;
|
|
152
|
-
}
|
|
153
|
-
function formatMemories(results) {
|
|
154
|
-
if (results.length === 0)
|
|
155
|
-
return NO_MEMORIES;
|
|
156
|
-
return [memoriesHeading(results.length), ...results.map(formatMemory)].join('\n');
|
|
157
|
-
}
|
|
158
|
-
/** What a memory costs the budget: the text formatMemories prints for it. */
|
|
159
|
-
const memoryCost = (r) => printedTokens(formatMemory(r));
|
|
160
|
-
// The widest heading or the empty-list line, whichever costs more, so either prints inside the budget.
|
|
161
|
-
function memoriesReserve(budget) {
|
|
162
|
-
return Math.max(printedTokens(memoriesHeading(budget)), estimateTokens(NO_MEMORIES));
|
|
163
|
-
}
|
|
164
|
-
function snapshotPiece(s) {
|
|
165
|
-
return [
|
|
166
|
-
'## Active Task Snapshot',
|
|
167
|
-
`- Task: ${s.task}`,
|
|
168
|
-
`- Status: ${s.status}`,
|
|
169
|
-
`- Updated: ${s.updated_at}`,
|
|
170
|
-
'',
|
|
171
|
-
'### Summary',
|
|
172
|
-
s.summary,
|
|
173
|
-
'',
|
|
174
|
-
'### Next step',
|
|
175
|
-
s.next_step,
|
|
176
|
-
'',
|
|
177
|
-
'',
|
|
178
|
-
].join('\n');
|
|
179
|
-
}
|
|
180
|
-
function handoffPiece(h) {
|
|
181
|
-
return ['## Session Handoff', ...handoffLines(h), '', ''].join('\n');
|
|
182
|
-
}
|
|
183
|
-
function trailPiece(events) {
|
|
184
|
-
return ['## Recent Session Trail', ...trailLines(events), '', ''].join('\n');
|
|
185
|
-
}
|
|
186
|
-
// Sections print ahead of the memories in hippo_context, so getContext pays for each as printed before any memory.
|
|
187
|
-
const contextCost = {
|
|
188
|
-
entry: memoryCost,
|
|
189
|
-
fixed: (budget) => memoriesReserve(budget),
|
|
190
|
-
snapshot: (s) => estimateTokens(snapshotPiece(s)),
|
|
191
|
-
handoff: (h) => estimateTokens(handoffPiece(h)),
|
|
192
|
-
trail: (events) => estimateTokens(trailPiece(events)),
|
|
193
|
-
};
|
|
194
|
-
// Rows the ranked list already shows drop out of this section, so pricing every row bounds what it prints.
|
|
195
|
-
function tailSection(rows) {
|
|
196
|
-
if (rows.length === 0)
|
|
197
|
-
return '';
|
|
198
|
-
const lines = ['', '## Fresh tail / substituted summaries'];
|
|
199
|
-
for (const r of rows) {
|
|
200
|
-
const tag = r.isSummary ? '[summary]' : '[tail]';
|
|
201
|
-
const head = r.content.length > 200 ? r.content.slice(0, 200) + '…' : r.content;
|
|
202
|
-
if (r.isSummary && r.substitutedFor && r.substitutedFor.length > 0) {
|
|
203
|
-
lines.push(`- ${tag} ${r.id} (covers ${r.substitutedFor.length} rows): ${head}`);
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
lines.push(`- ${tag} ${r.id}: ${head}`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return '\n' + lines.join('\n');
|
|
210
|
-
}
|
|
211
|
-
// J3.2: the hint depends on the query alone, so api.retrieve's copy is the one shown; JSON.stringify fences the phrase.
|
|
212
|
-
function planningSection(r) {
|
|
213
|
-
if (r.planningFallacyHint) {
|
|
214
|
-
const h = r.planningFallacyHint;
|
|
215
|
-
return `## Planning fallacy hint\nClass: ${h.classTag}\n${h.baserateSummary}\n(detected: ${JSON.stringify(h.detectedPhrase)})\n\n---\n\n`;
|
|
216
|
-
}
|
|
217
|
-
if (r.planningFallacyWatching) {
|
|
218
|
-
const w = r.planningFallacyWatching;
|
|
219
|
-
return `## Planning fallacy watch\nReason: ${w.reason}\n${w.suggestion}\n(detected: ${JSON.stringify(w.detectedPhrase)})\n\n---\n\n`;
|
|
220
|
-
}
|
|
221
|
-
return '';
|
|
222
|
-
}
|
|
223
|
-
// ── Tool definitions ──
|
|
224
|
-
// HTTP sets no budget cap; 25x the 4000 recall default leaves room for large-context clients while bounding one call's work.
|
|
225
|
-
const MAX_BUDGET_TOKENS = 100_000;
|
|
226
|
-
// Same ceiling as the HTTP list routes' parseListLimit.
|
|
227
|
-
const MAX_LIST_LIMIT = 1000;
|
|
228
|
-
const TOOLS = [
|
|
229
|
-
{
|
|
230
|
-
name: 'hippo_recall',
|
|
231
|
-
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.',
|
|
232
|
-
inputSchema: {
|
|
233
|
-
type: 'object',
|
|
234
|
-
properties: {
|
|
235
|
-
query: { type: 'string', description: 'What to search for in memory (natural language)' },
|
|
236
|
-
budget: {
|
|
237
|
-
type: 'number',
|
|
238
|
-
minimum: 0,
|
|
239
|
-
maximum: MAX_BUDGET_TOKENS,
|
|
240
|
-
description: `Max tokens to return (default: config.defaultBudget, 4000; max ${MAX_BUDGET_TOKENS})`,
|
|
241
|
-
},
|
|
242
|
-
include_continuity: {
|
|
243
|
-
type: 'boolean',
|
|
244
|
-
description: 'Append continuity context (active snapshot + handoff + last 5 session events) below the memory results. Useful at session boot.',
|
|
245
|
-
},
|
|
246
|
-
scope: {
|
|
247
|
-
type: 'string',
|
|
248
|
-
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.',
|
|
249
|
-
},
|
|
250
|
-
fresh_tail_count: {
|
|
251
|
-
type: 'number',
|
|
252
|
-
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.',
|
|
253
|
-
},
|
|
254
|
-
fresh_tail_session_id: {
|
|
255
|
-
type: 'string',
|
|
256
|
-
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).',
|
|
257
|
-
},
|
|
258
|
-
summarize_overflow: {
|
|
259
|
-
type: 'boolean',
|
|
260
|
-
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.',
|
|
261
|
-
},
|
|
262
|
-
scorer_window: {
|
|
263
|
-
type: 'number',
|
|
264
|
-
description: 'How many of the top-ranked memories the fresh-tail and summarize-overflow appendix is worked out against. The main list ranks the whole tenant store, so scorer_window does not narrow it. Default 200. Rejected as RecallContractError code=invalid_scorer_window if 0/negative/non-finite/non-numeric.',
|
|
265
|
-
},
|
|
266
|
-
session_id: {
|
|
267
|
-
type: 'string',
|
|
268
|
-
maxLength: 256,
|
|
269
|
-
description: 'Optional session id (v1.7.4). When set AND (tenant, session) has active goals, applies the dlPFC goal-stack boost to the ranked memories before formatting. Mirrors fresh_tail_session_id shape (256-char cap).',
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
required: ['query'],
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
name: 'hippo_assemble',
|
|
277
|
-
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.',
|
|
278
|
-
inputSchema: {
|
|
279
|
-
type: 'object',
|
|
280
|
-
properties: {
|
|
281
|
-
session_id: {
|
|
282
|
-
type: 'string',
|
|
283
|
-
description: 'Session identifier. Returns clean empty result if no kind=raw rows match.',
|
|
284
|
-
},
|
|
285
|
-
budget: {
|
|
286
|
-
type: 'number',
|
|
287
|
-
minimum: 0,
|
|
288
|
-
maximum: MAX_BUDGET_TOKENS,
|
|
289
|
-
description: `Token budget for the assembled context (default 4000; max ${MAX_BUDGET_TOKENS}). Eviction kicks in over budget.`,
|
|
290
|
-
},
|
|
291
|
-
fresh_tail_count: {
|
|
292
|
-
type: 'number',
|
|
293
|
-
description: 'Recent raw rows always kept verbatim (default 10). These are never evicted.',
|
|
294
|
-
},
|
|
295
|
-
summarize_older: {
|
|
296
|
-
type: 'boolean',
|
|
297
|
-
description: 'When true (default), older raws sharing a level-2 parent summary get substituted. Set false to keep every older raw as-is.',
|
|
298
|
-
},
|
|
299
|
-
scope: {
|
|
300
|
-
type: 'string',
|
|
301
|
-
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.',
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
required: ['session_id'],
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
name: 'hippo_drill',
|
|
309
|
-
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.',
|
|
310
|
-
inputSchema: {
|
|
311
|
-
type: 'object',
|
|
312
|
-
properties: {
|
|
313
|
-
summary_id: {
|
|
314
|
-
type: 'string',
|
|
315
|
-
description: 'ID of the level-2 (or higher) summary to drill into. Must be a summary, not a leaf — leaves are not drillable.',
|
|
316
|
-
},
|
|
317
|
-
limit: {
|
|
318
|
-
type: 'number',
|
|
319
|
-
minimum: 0,
|
|
320
|
-
maximum: MAX_LIST_LIMIT,
|
|
321
|
-
description: `Max children to return (default 50; max ${MAX_LIST_LIMIT}).`,
|
|
322
|
-
},
|
|
323
|
-
budget: {
|
|
324
|
-
type: 'number',
|
|
325
|
-
minimum: 0,
|
|
326
|
-
maximum: MAX_BUDGET_TOKENS,
|
|
327
|
-
description: `Max total token cost (~ chars/4) of returned children (max ${MAX_BUDGET_TOKENS}). Truncates chronologically.`,
|
|
328
|
-
},
|
|
329
|
-
depth: {
|
|
330
|
-
type: 'integer',
|
|
331
|
-
minimum: 1,
|
|
332
|
-
maximum: 10,
|
|
333
|
-
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.',
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
required: ['summary_id'],
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
name: 'hippo_remember',
|
|
341
|
-
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.',
|
|
342
|
-
inputSchema: {
|
|
343
|
-
type: 'object',
|
|
344
|
-
properties: {
|
|
345
|
-
text: {
|
|
346
|
-
type: 'string',
|
|
347
|
-
description: 'The memory to store (1-2 sentences, specific and concrete)',
|
|
348
|
-
},
|
|
349
|
-
error: { type: 'boolean', description: 'Mark as error memory (doubles half-life)' },
|
|
350
|
-
pin: { type: 'boolean', description: 'Pin memory (never decays)' },
|
|
351
|
-
tag: { type: 'string', description: 'Optional tag for categorization' },
|
|
352
|
-
},
|
|
353
|
-
required: ['text'],
|
|
354
|
-
},
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
name: 'hippo_outcome',
|
|
358
|
-
description: 'Report whether recalled memories were useful. Strengthens good memories (+5 days half-life) and weakens bad ones (-3 days). Call after completing work.',
|
|
359
|
-
inputSchema: {
|
|
360
|
-
type: 'object',
|
|
361
|
-
properties: {
|
|
362
|
-
good: {
|
|
363
|
-
type: 'boolean',
|
|
364
|
-
description: 'true = memories helped, false = memories were irrelevant',
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
required: ['good'],
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
name: 'hippo_context',
|
|
372
|
-
description: 'Smart context injection: auto-detects current task from git state and returns relevant memories plus the active task snapshot, session handoff and recent session trail (the same bundle as GET /v1/context). Use at the start of any session. Memories and those sections are scope-filtered: a no-scope caller does NOT see ANY <source>:private:* (slack, github, ...) or legacy-quarantine rows.',
|
|
373
|
-
inputSchema: {
|
|
374
|
-
type: 'object',
|
|
375
|
-
properties: {
|
|
376
|
-
budget: {
|
|
377
|
-
type: 'number',
|
|
378
|
-
minimum: 0,
|
|
379
|
-
maximum: MAX_BUDGET_TOKENS,
|
|
380
|
-
description: `Max tokens (default: config.defaultContextBudget, 3000; max ${MAX_BUDGET_TOKENS})`,
|
|
381
|
-
},
|
|
382
|
-
scope: {
|
|
383
|
-
type: 'string',
|
|
384
|
-
description: 'Restrict memories, snapshot, handoff and trail to this scope exactly. When omitted, default-deny applies to ANY <source>:private:* (slack, github, ...) and unknown-legacy rows.',
|
|
385
|
-
},
|
|
386
|
-
},
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
name: 'hippo_status',
|
|
391
|
-
description: 'Check memory health: counts, strengths, at-risk memories, last consolidation time.',
|
|
392
|
-
inputSchema: {
|
|
393
|
-
type: 'object',
|
|
394
|
-
properties: {},
|
|
395
|
-
},
|
|
396
|
-
},
|
|
397
|
-
{
|
|
398
|
-
name: 'hippo_learn',
|
|
399
|
-
description: 'Scan recent git commits for lessons from fix/revert/bug/refactor/perf patterns. Run after coding sessions.',
|
|
400
|
-
inputSchema: {
|
|
401
|
-
type: 'object',
|
|
402
|
-
properties: {
|
|
403
|
-
days: { type: 'number', description: 'Days to scan back (default: 7)' },
|
|
404
|
-
},
|
|
405
|
-
},
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
name: 'hippo_conflicts',
|
|
409
|
-
description: 'List open memory conflicts — contradictory memories that need resolution.',
|
|
410
|
-
inputSchema: {
|
|
411
|
-
type: 'object',
|
|
412
|
-
properties: {},
|
|
413
|
-
},
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
name: 'hippo_resolve',
|
|
417
|
-
description: 'Resolve a memory conflict by keeping one memory and weakening or deleting the other.',
|
|
418
|
-
inputSchema: {
|
|
419
|
-
type: 'object',
|
|
420
|
-
properties: {
|
|
421
|
-
conflict_id: { type: 'number', description: 'The conflict ID to resolve' },
|
|
422
|
-
keep: { type: 'string', description: 'ID of the memory to keep' },
|
|
423
|
-
forget: { type: 'boolean', description: 'Delete the loser instead of weakening (default: false)' },
|
|
424
|
-
rejectLoser: { type: 'boolean', description: 'Tombstone the loser\'s value too, so it refuses re-ingestion (implies removal; default: false)' },
|
|
425
|
-
reason: { type: 'string', description: 'Reason recorded on the tombstone when rejectLoser is set (default: a conflict-context string)' },
|
|
426
|
-
},
|
|
427
|
-
required: ['conflict_id', 'keep'],
|
|
428
|
-
},
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
name: 'hippo_share',
|
|
432
|
-
description: 'Share a memory to the global store for cross-project use with transfer scoring.',
|
|
433
|
-
inputSchema: {
|
|
434
|
-
type: 'object',
|
|
435
|
-
properties: {
|
|
436
|
-
id: { type: 'string', description: 'Memory ID to share' },
|
|
437
|
-
force: { type: 'boolean', description: 'Share even if transfer score is low' },
|
|
438
|
-
},
|
|
439
|
-
required: ['id'],
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
name: 'hippo_peers',
|
|
444
|
-
description: 'List all projects that have contributed memories to the global shared store.',
|
|
445
|
-
inputSchema: {
|
|
446
|
-
type: 'object',
|
|
447
|
-
properties: {},
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
name: 'hippo_predict_baserate',
|
|
452
|
-
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.',
|
|
453
|
-
inputSchema: {
|
|
454
|
-
type: 'object',
|
|
455
|
-
properties: {
|
|
456
|
-
class_tag: {
|
|
457
|
-
type: 'string',
|
|
458
|
-
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 ...`).',
|
|
459
|
-
},
|
|
460
|
-
},
|
|
461
|
-
required: ['class_tag'],
|
|
462
|
-
},
|
|
463
|
-
},
|
|
464
|
-
];
|
|
465
|
-
const TOOLS_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
|
|
466
|
-
// api.retrieve rejects these itself, so MCP and HTTP callers get the same typed error code for the same bad value.
|
|
467
|
-
const ARGS_CHECKED_BY_API = new Map([['hippo_recall', new Set(['scorer_window'])]]);
|
|
468
|
-
// ── Track last recalled IDs for outcome feedback ──
|
|
469
|
-
//
|
|
470
|
-
// Keyed per-client so two HTTP-MCP clients hitting the same tenant cannot
|
|
471
|
-
// poison each other's outcome feedback. The key is `ctx.clientKey` when the
|
|
472
|
-
// transport supplies one (HTTP-MCP via src/server.ts builds
|
|
473
|
-
// hash(bearer+remoteAddr)); stdio and any caller without a clientKey falls
|
|
474
|
-
// back to `'stdio-${pid}'` (one process = one client) or
|
|
475
|
-
// `${tenantId}:default` if a McpContext is constructed in tests without a
|
|
476
|
-
// pid-bound transport.
|
|
477
|
-
const lastRecalledIds = new Map();
|
|
478
|
-
const autoSleepInFlight = new Set();
|
|
479
|
-
function resolveClientKey(ctx) {
|
|
480
|
-
if (ctx?.clientKey)
|
|
481
|
-
return ctx.clientKey;
|
|
482
|
-
if (ctx?.tenantId)
|
|
483
|
-
return `stdio-${process.pid}:${ctx.tenantId}`;
|
|
484
|
-
return `stdio-${process.pid}:default`;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Zero-install first run (`npx -y hippo-memory mcp` with no store anywhere):
|
|
488
|
-
* create the global store instead of failing every tool call, and say so on
|
|
489
|
-
* stderr (stdout carries the protocol). `hippo init` in a project later adds
|
|
490
|
-
* a project store, which then takes precedence.
|
|
491
|
-
*/
|
|
492
|
-
function createGlobalStoreOnFirstRun() {
|
|
493
|
-
initGlobal();
|
|
494
|
-
const root = getGlobalRoot();
|
|
495
|
-
log.warn(`no memory store found; created the global store at ${root}. Run \`hippo init\` in a project for a project store.`);
|
|
496
|
-
return root;
|
|
497
|
-
}
|
|
498
|
-
// ── Token ledger (ROADMAP TE0) ──
|
|
499
|
-
const MCP_TOKEN_SURFACES = new Map([
|
|
500
|
-
['hippo_recall', 'mcp_recall'],
|
|
501
|
-
['hippo_context', 'mcp_context'],
|
|
502
|
-
]);
|
|
503
|
-
/**
|
|
504
|
-
* Record the memory text a recall or context tool returned. Best-effort: a
|
|
505
|
-
* ledger failure never fails the tool call. Other tools are not recorded.
|
|
506
|
-
*/
|
|
507
|
-
function recordMcpTokens(toolName, output, ctx) {
|
|
508
|
-
const surface = MCP_TOKEN_SURFACES.get(toolName);
|
|
509
|
-
if (!surface || !output)
|
|
510
|
-
return;
|
|
511
|
-
try {
|
|
512
|
-
const hippoRoot = ctx?.hippoRoot ?? findHippoRoot();
|
|
513
|
-
if (!hippoRoot)
|
|
514
|
-
return;
|
|
515
|
-
const db = openHippoDb(hippoRoot);
|
|
516
|
-
try {
|
|
517
|
-
recordTokenUse(db, {
|
|
518
|
-
tenantId: ctx?.tenantId ?? resolveTenantId({}),
|
|
519
|
-
surface,
|
|
520
|
-
event: 'inject',
|
|
521
|
-
items: 0,
|
|
522
|
-
tokens: estimateTokens(output),
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
finally {
|
|
526
|
-
closeHippoDb(db);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
catch {
|
|
530
|
-
// Ledger is best-effort.
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
// ── Tool execution ──
|
|
534
|
-
async function executeTool(name, args, ctx) {
|
|
535
|
-
// When a transport hands us a context (HTTP path), trust it: the HTTP
|
|
536
|
-
// server already resolved hippoRoot from its bound opts and tenantId
|
|
537
|
-
// from the Bearer token (or the loopback fallback). The stdio path
|
|
538
|
-
// continues to walk from cwd / fall back to the global root, and to
|
|
539
|
-
// resolve tenant from HIPPO_TENANT.
|
|
540
|
-
const hippoRoot = ctx?.hippoRoot ?? findHippoRoot() ?? createGlobalStoreOnFirstRun();
|
|
541
|
-
const config = loadConfig(hippoRoot);
|
|
542
|
-
// A5: every store read in this server returns to the caller and is
|
|
543
|
-
// tenant-isolated. Resolved once per tool call: prefer the transport's
|
|
544
|
-
// ctx.tenantId so an HTTP Bearer for tenant B doesn't drop to HIPPO_TENANT.
|
|
545
|
-
const tenantId = ctx?.tenantId ?? resolveTenantId({});
|
|
546
|
-
switch (name) {
|
|
547
|
-
case 'hippo_recall': {
|
|
548
|
-
const query = String(args.query || '');
|
|
549
|
-
const budget = Number(args.budget) || config.defaultBudget;
|
|
550
|
-
const includeContinuity = Boolean(args.include_continuity);
|
|
551
|
-
const explicitScope = isJsonString(args.scope) && args.scope.length > 0
|
|
552
|
-
? args.scope
|
|
553
|
-
: undefined;
|
|
554
|
-
const freshTailCountArg = Number(args.fresh_tail_count);
|
|
555
|
-
const freshTailCount = Number.isFinite(freshTailCountArg) && freshTailCountArg > 0
|
|
556
|
-
? freshTailCountArg
|
|
557
|
-
: undefined;
|
|
558
|
-
const freshTailSessionId = isJsonString(args.fresh_tail_session_id) && args.fresh_tail_session_id.length > 0
|
|
559
|
-
? args.fresh_tail_session_id
|
|
560
|
-
: undefined;
|
|
561
|
-
const summarizeOverflow = isJsonBoolean(args.summarize_overflow)
|
|
562
|
-
? args.summarize_overflow
|
|
563
|
-
: undefined;
|
|
564
|
-
// Number-coerce, never typeof-check: "abc" must reach api.retrieve and fail as invalid_scorer_window, the same code HTTP returns.
|
|
565
|
-
const scorerWindow = args.scorer_window === undefined
|
|
566
|
-
? undefined
|
|
567
|
-
: Number(args.scorer_window);
|
|
568
|
-
// session_id drives the goal-stack boost inside api.retrieve; same trim and 256-char cap as fresh_tail_session_id.
|
|
569
|
-
const sessionIdRaw = isJsonString(args.session_id) ? args.session_id.trim() : '';
|
|
570
|
-
const sessionId = sessionIdRaw.length > 0 && sessionIdRaw.length <= 256
|
|
571
|
-
? sessionIdRaw
|
|
572
|
-
: undefined;
|
|
573
|
-
const apiCtx = {
|
|
574
|
-
hippoRoot,
|
|
575
|
-
tenantId,
|
|
576
|
-
actor: mcpActor(ctx),
|
|
577
|
-
};
|
|
578
|
-
const recallExtra = {};
|
|
579
|
-
if (freshTailCount !== undefined)
|
|
580
|
-
recallExtra.freshTailCount = freshTailCount;
|
|
581
|
-
if (freshTailSessionId !== undefined)
|
|
582
|
-
recallExtra.freshTailSessionId = freshTailSessionId;
|
|
583
|
-
if (summarizeOverflow !== undefined)
|
|
584
|
-
recallExtra.summarizeOverflow = summarizeOverflow;
|
|
585
|
-
if (scorerWindow !== undefined)
|
|
586
|
-
recallExtra.scorerWindow = scorerWindow;
|
|
587
|
-
if (sessionId !== undefined)
|
|
588
|
-
recallExtra.sessionId = sessionId;
|
|
589
|
-
const anchorRing = biasHintEnabled('anchoring') && sessionId
|
|
590
|
-
? getOrCreateRing(sessionRecallHistoryMcp, buildSessionKey(tenantId, sessionId))
|
|
591
|
-
: null;
|
|
592
|
-
const queryHash = hashQueryText(query);
|
|
593
|
-
const out = {};
|
|
594
|
-
// RecallContractError throws reach the MCP caller raw, as mcp-recall-fresh-tail-policy.test.ts pins.
|
|
595
|
-
await apiRetrieve(apiCtx, {
|
|
596
|
-
query,
|
|
597
|
-
limit: 50,
|
|
598
|
-
scope: explicitScope,
|
|
599
|
-
includeContinuity,
|
|
600
|
-
mode: config.physics?.enabled !== false ? 'physics' : 'hybrid',
|
|
601
|
-
// The hint is computed below over the list MCP shows; the window band's copy would emit its audit row twice.
|
|
602
|
-
suppressAvailabilityHint: true,
|
|
603
|
-
keepHeldCopies: true,
|
|
604
|
-
...recallExtra,
|
|
605
|
-
showRanked: ({ ranked, pool, droppedByScope }, apiResult) => {
|
|
606
|
-
// Sections are paid in print order, ahead of the memories and after the heading; one that does not fit is dropped whole.
|
|
607
|
-
let left = budget - memoriesReserve(budget);
|
|
608
|
-
const pays = (piece) => {
|
|
609
|
-
const tokens = estimateTokens(piece);
|
|
610
|
-
if (tokens > left)
|
|
611
|
-
return false;
|
|
612
|
-
left -= tokens;
|
|
613
|
-
return true;
|
|
614
|
-
};
|
|
615
|
-
const planPiece = planningSection(apiResult);
|
|
616
|
-
const showPlan = planPiece !== '' && pays(planPiece);
|
|
617
|
-
const tailRows = apiResult.results.filter((r) => r.isFreshTail || r.isSummary);
|
|
618
|
-
const showTail = tailRows.length > 0 && pays(tailSection(tailRows));
|
|
619
|
-
const continuityPiece = includeContinuity && apiResult.continuity ? `\n\n${formatContinuityBlock(apiResult.continuity)}` : '';
|
|
620
|
-
const showContinuity = continuityPiece !== '' && pays(continuityPiece);
|
|
621
|
-
// J1, J2 and C5: the hints and Cutoff block describe the list MCP shows, not the window band in apiResult.
|
|
622
|
-
const render = (cut) => {
|
|
623
|
-
const list = dropHeldCopies(cut, (r) => r.entry); // after every cut, so a merged row cut here never hides its sources
|
|
624
|
-
const anchoring = anchorRing ? detectAnchoring(snapshotRing(anchorRing), queryHash, list[0]?.entry.id ?? null) : null;
|
|
625
|
-
const availability = biasHintEnabled('availability')
|
|
626
|
-
? detectAvailabilityBias({
|
|
627
|
-
topK: list.map((r) => ({ id: r.entry.id, created: r.entry.created })),
|
|
628
|
-
pool: pool.map((e) => ({ id: e.id, created: e.created })),
|
|
629
|
-
})
|
|
630
|
-
: null;
|
|
631
|
-
const shownIds = new Set(list.map((r) => r.entry.id));
|
|
632
|
-
const shownKeys = storedTextKeys(list.map((r) => r.entry));
|
|
633
|
-
const tail = showTail
|
|
634
|
-
? dropHeldCopies(tailRows.filter((r) => !shownIds.has(r.id) && !shownKeys.has(duplicateKey(r.content))), (r) => r)
|
|
635
|
-
: [];
|
|
636
|
-
const s = buildSuppressionSummary({
|
|
637
|
-
totalCandidates: pool.length + droppedByScope,
|
|
638
|
-
droppedPreRank: droppedByScope + cut.length - list.length, // the bucket CLI and API recall put hidden copies in
|
|
639
|
-
droppedByBudget: Math.max(0, pool.length - cut.length), // an upper bound: rows that never matched count too
|
|
640
|
-
summarySubstitutionsAdded: tail.filter((r) => r.isSummary).length,
|
|
641
|
-
freshTailAdded: tail.filter((r) => r.isFreshTail && !r.isSummary).length,
|
|
642
|
-
suppressedByInterference: anchoring?.reason === 'memory_dominance' ? 1 : 0,
|
|
643
|
-
});
|
|
644
|
-
// Anchoring is the stronger pull, so it prints first; the Cutoff block sits above the list, where the agent reads it.
|
|
645
|
-
let text = anchoring ? `## Anchoring hint\n${anchoring.summary}\n[anchored_on: ${anchoring.memoryId}]\n\n---\n\n` : '';
|
|
646
|
-
if (availability)
|
|
647
|
-
text += `## Availability bias\n${availability.summary}\n\n---\n\n`;
|
|
648
|
-
if (showPlan)
|
|
649
|
-
text += planPiece;
|
|
650
|
-
const cutoffClauses = [];
|
|
651
|
-
if (s.droppedByBudget > 0)
|
|
652
|
-
cutoffClauses.push(`${s.droppedByBudget} dropped to fit limit`);
|
|
653
|
-
if (s.droppedPreRank > 0)
|
|
654
|
-
cutoffClauses.push(`${s.droppedPreRank} filtered pre-rank`);
|
|
655
|
-
if (s.summarySubstitutionsAdded > 0)
|
|
656
|
-
cutoffClauses.push(`${s.summarySubstitutionsAdded} summary substitutions added`);
|
|
657
|
-
if (s.freshTailAdded > 0)
|
|
658
|
-
cutoffClauses.push(`${s.freshTailAdded} fresh-tail added`);
|
|
659
|
-
if (s.suppressedByInterference > 0)
|
|
660
|
-
cutoffClauses.push(`${s.suppressedByInterference} suppressed by interference`);
|
|
661
|
-
if (cutoffClauses.length > 0) {
|
|
662
|
-
text += `## Cutoff\nShowing ${list.length} of ${s.totalCandidates} candidates; ${cutoffClauses.join('; ')}.\n\n---\n\n`;
|
|
663
|
-
}
|
|
664
|
-
// The window band's fresh-tail and summary rows follow the ranked list, or the MCP fields go unanswered.
|
|
665
|
-
text += formatMemories(list) + tailSection(tail) + (showContinuity ? continuityPiece : '');
|
|
666
|
-
return { anchoring, availability, text, list };
|
|
667
|
-
};
|
|
668
|
-
let results = fitBudget(ranked, Math.max(0, left), 1, memoryCost);
|
|
669
|
-
let rendered = render(results);
|
|
670
|
-
// The hints, Cutoff block and heading vary with the list, so the lowest-ranked entry goes until the whole response fits.
|
|
671
|
-
while (results.length > 1 && estimateTokens(rendered.text) > budget) {
|
|
672
|
-
results = results.slice(0, -1);
|
|
673
|
-
rendered = render(results);
|
|
674
|
-
}
|
|
675
|
-
out.rendered = rendered;
|
|
676
|
-
return rendered.list.map((r) => r.entry.id);
|
|
677
|
-
},
|
|
678
|
-
});
|
|
679
|
-
if (!out.rendered)
|
|
680
|
-
throw new Error('hippo_recall: api.retrieve returned without calling showRanked');
|
|
681
|
-
const { anchoring: mcpAnchoringHint, availability: mcpAvailabilityHint, list: shown, text: recallText } = out.rendered;
|
|
682
|
-
lastRecalledIds.set(resolveClientKey(ctx), shown.map((r) => r.entry.id));
|
|
683
|
-
if (biasHintEnabled('anchoring')) {
|
|
684
|
-
if (anchorRing) {
|
|
685
|
-
// Appended after the final detect: anchoredOn feeds the cooldown for the next recall on this session.
|
|
686
|
-
appendRecall(anchorRing, queryHash, shown[0]?.entry.id ?? null, mcpAnchoringHint?.memoryId);
|
|
687
|
-
if (mcpAnchoringHint?.reason === 'memory_dominance') {
|
|
688
|
-
const dbForAudit = openHippoDb(hippoRoot);
|
|
689
|
-
try {
|
|
690
|
-
appendAuditEvent(dbForAudit, {
|
|
691
|
-
tenantId,
|
|
692
|
-
actor: ctx?.actor ?? 'mcp',
|
|
693
|
-
op: 'recall_anchor_detected_memory_dominance',
|
|
694
|
-
targetId: mcpAnchoringHint.memoryId,
|
|
695
|
-
metadata: {
|
|
696
|
-
memory_id: mcpAnchoringHint.memoryId,
|
|
697
|
-
query_count: mcpAnchoringHint.queryCount ?? null,
|
|
698
|
-
},
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
finally {
|
|
702
|
-
closeHippoDb(dbForAudit);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
else if (mcpAnchoringHint?.reason === 'query_repeat') {
|
|
706
|
-
const dbForAudit = openHippoDb(hippoRoot);
|
|
707
|
-
try {
|
|
708
|
-
appendAuditEvent(dbForAudit, {
|
|
709
|
-
tenantId,
|
|
710
|
-
actor: ctx?.actor ?? 'mcp',
|
|
711
|
-
op: 'recall_anchor_detected_query_repeat',
|
|
712
|
-
targetId: mcpAnchoringHint.memoryId,
|
|
713
|
-
metadata: { memory_id: mcpAnchoringHint.memoryId },
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
finally {
|
|
717
|
-
closeHippoDb(dbForAudit);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
else {
|
|
722
|
-
// Telemetry: caller had no sessionId so ring tracking skipped.
|
|
723
|
-
// Per the recall-audit convention at api.ts:854, use SHA-256/16
|
|
724
|
-
// for prompt hashing (NOT hashQueryText which is FNV-1a 32-bit
|
|
725
|
-
// for recall matching; brute-force trivial for low-entropy
|
|
726
|
-
// queries). Codex round-2 P2 catch.
|
|
727
|
-
const dbForAudit = openHippoDb(hippoRoot);
|
|
728
|
-
try {
|
|
729
|
-
appendAuditEvent(dbForAudit, {
|
|
730
|
-
tenantId,
|
|
731
|
-
actor: ctx?.actor ?? 'mcp',
|
|
732
|
-
op: 'recall_anchor_skipped_no_session',
|
|
733
|
-
targetId: undefined,
|
|
734
|
-
metadata: auditQueryFields(query),
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
finally {
|
|
738
|
-
closeHippoDb(dbForAudit);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
if (mcpAvailabilityHint) {
|
|
743
|
-
const dbForAudit = openHippoDb(hippoRoot);
|
|
744
|
-
try {
|
|
745
|
-
appendAuditEvent(dbForAudit, {
|
|
746
|
-
tenantId,
|
|
747
|
-
actor: ctx?.actor ?? 'mcp',
|
|
748
|
-
op: 'recall_availability_detected',
|
|
749
|
-
metadata: {
|
|
750
|
-
recent_fraction: mcpAvailabilityHint.recentFraction,
|
|
751
|
-
older_passed_over: mcpAvailabilityHint.olderCandidatesPassedOver,
|
|
752
|
-
returned_count: mcpAvailabilityHint.returnedCount,
|
|
753
|
-
},
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
finally {
|
|
757
|
-
closeHippoDb(dbForAudit);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
return recallText;
|
|
761
|
-
}
|
|
762
|
-
case 'hippo_assemble': {
|
|
763
|
-
const sessionId = String(args.session_id || '');
|
|
764
|
-
if (!sessionId)
|
|
765
|
-
return 'No session_id provided.';
|
|
766
|
-
const budget = Number(args.budget);
|
|
767
|
-
const freshTailCount = Number(args.fresh_tail_count);
|
|
768
|
-
const summarizeOlder = args.summarize_older !== false;
|
|
769
|
-
const apiCtx = {
|
|
770
|
-
hippoRoot,
|
|
771
|
-
tenantId,
|
|
772
|
-
actor: mcpActor(ctx),
|
|
773
|
-
};
|
|
774
|
-
const explicitScope = isJsonString(args.scope) && args.scope.length > 0
|
|
775
|
-
? args.scope
|
|
776
|
-
: undefined;
|
|
777
|
-
const assembleExtra = {};
|
|
778
|
-
if (Number.isFinite(budget) && budget > 0)
|
|
779
|
-
assembleExtra.budget = budget;
|
|
780
|
-
if (Number.isFinite(freshTailCount) && freshTailCount >= 0)
|
|
781
|
-
assembleExtra.freshTailCount = freshTailCount;
|
|
782
|
-
if (explicitScope !== undefined)
|
|
783
|
-
assembleExtra.scope = explicitScope;
|
|
784
|
-
const r = apiAssemble(apiCtx, sessionId, {
|
|
785
|
-
summarizeOlder,
|
|
786
|
-
...assembleExtra,
|
|
787
|
-
cost: assembleCost(sessionId),
|
|
788
|
-
});
|
|
789
|
-
return assembleText(r);
|
|
790
|
-
}
|
|
791
|
-
case 'hippo_drill': {
|
|
792
|
-
const summaryId = String(args.summary_id || '');
|
|
793
|
-
if (!summaryId)
|
|
794
|
-
return 'No summary_id provided.';
|
|
795
|
-
const limit = Number(args.limit);
|
|
796
|
-
const budget = Number(args.budget);
|
|
797
|
-
// The inputSchema rejects a depth outside 1..10 before this runs, so no silent clamp hides the cap.
|
|
798
|
-
const depth = args.depth === undefined ? undefined : Number(args.depth);
|
|
799
|
-
const apiCtx = {
|
|
800
|
-
hippoRoot,
|
|
801
|
-
tenantId,
|
|
802
|
-
actor: mcpActor(ctx),
|
|
803
|
-
};
|
|
804
|
-
const drillExtra = {};
|
|
805
|
-
if (Number.isFinite(limit) && limit > 0)
|
|
806
|
-
drillExtra.limit = limit;
|
|
807
|
-
if (Number.isFinite(budget) && budget > 0)
|
|
808
|
-
drillExtra.budget = budget;
|
|
809
|
-
if (depth !== undefined)
|
|
810
|
-
drillExtra.depth = depth;
|
|
811
|
-
const r = apiDrillDown(apiCtx, summaryId, { ...drillExtra, cost: drillCost });
|
|
812
|
-
if ('failure' in r) {
|
|
813
|
-
// v1.6.4: only not_drillable is caller-actionable. not_found
|
|
814
|
-
// intentionally collapses cross-tenant + scope-blocked + missing
|
|
815
|
-
// (codex round 3 P1: distinguishing scope_blocked would leak
|
|
816
|
-
// private-row existence on this surface).
|
|
817
|
-
if (r.failure === 'not_drillable') {
|
|
818
|
-
return `Id ${summaryId} is a leaf row, not a level-2+ summary; nothing to drill into.`;
|
|
819
|
-
}
|
|
820
|
-
return `No drillable summary at id=${summaryId}.`;
|
|
821
|
-
}
|
|
822
|
-
return drillText(r);
|
|
823
|
-
}
|
|
824
|
-
case 'hippo_predict_baserate': {
|
|
825
|
-
// J3 reference-class / planning-fallacy detector. Reads from the E2
|
|
826
|
-
// predictions table; returns text-only response matching the existing
|
|
827
|
-
// MCP tool convention (no structured JSON over the wire). Direct call
|
|
828
|
-
// to computePredictionBaserate; helper opens its own db + emits audit
|
|
829
|
-
// (single source of truth, no caller-site drift).
|
|
830
|
-
const classTag = String(args.class_tag || '').trim();
|
|
831
|
-
if (!classTag)
|
|
832
|
-
return 'No class_tag provided. Usage: pass class_tag matching a class used in past predictions (e.g. "migration-effort").';
|
|
833
|
-
const baserate = computePredictionBaserate(hippoRoot, tenantId, classTag, ctx?.actor ?? 'mcp');
|
|
834
|
-
if (baserate.nClosed === 0) {
|
|
835
|
-
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.`;
|
|
836
|
-
}
|
|
837
|
-
const lines = [baserate.summary, ''];
|
|
838
|
-
lines.push(`n_closed: ${baserate.nClosed}`);
|
|
839
|
-
lines.push(`n_ratio_eligible: ${baserate.nRatioEligible}`);
|
|
840
|
-
if (baserate.meanEstimate !== null)
|
|
841
|
-
lines.push(`mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
|
|
842
|
-
if (baserate.meanActual !== null)
|
|
843
|
-
lines.push(`mean_actual: ${baserate.meanActual.toFixed(3)}`);
|
|
844
|
-
if (baserate.meanRatio !== null)
|
|
845
|
-
lines.push(`mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
|
|
846
|
-
if (baserate.p50Ratio !== null)
|
|
847
|
-
lines.push(`p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
|
|
848
|
-
if (baserate.mae !== null)
|
|
849
|
-
lines.push(`mae: ${baserate.mae.toFixed(3)}`);
|
|
850
|
-
return lines.join('\n');
|
|
851
|
-
}
|
|
852
|
-
case 'hippo_remember': {
|
|
853
|
-
const text = String(args.text || '');
|
|
854
|
-
if (!text)
|
|
855
|
-
return 'No text provided.';
|
|
856
|
-
const tags = [];
|
|
857
|
-
if (args.error)
|
|
858
|
-
tags.push('error');
|
|
859
|
-
if (args.tag)
|
|
860
|
-
tags.push(String(args.tag));
|
|
861
|
-
// Route through api.ts so audit_log captures the caller identity
|
|
862
|
-
// uniformly with CLI/REST: the auth-resolved ctx.actor under HTTP-MCP,
|
|
863
|
-
// 'mcp' for stdio (no ctx). api.ts.remember writes the memory + audit
|
|
864
|
-
// row in one transaction-friendly path; we re-read the entry to surface
|
|
865
|
-
// the half-life used in the MCP human-readable response.
|
|
866
|
-
const apiCtx = {
|
|
867
|
-
hippoRoot,
|
|
868
|
-
tenantId,
|
|
869
|
-
actor: mcpActor(ctx),
|
|
870
|
-
};
|
|
871
|
-
const result = apiRemember(apiCtx, {
|
|
872
|
-
content: text,
|
|
873
|
-
tags,
|
|
874
|
-
});
|
|
875
|
-
const entry = readEntry(hippoRoot, result.id, tenantId);
|
|
876
|
-
// Auto-sleep: one run per store at a time, triggered by what arrived since the last one.
|
|
877
|
-
if (config.autoSleep.enabled &&
|
|
878
|
-
!autoSleepInFlight.has(hippoRoot) &&
|
|
879
|
-
countCreatedSinceLastSleep(hippoRoot, tenantId) >= config.autoSleep.threshold) {
|
|
880
|
-
autoSleepInFlight.add(hippoRoot);
|
|
881
|
-
// Fire-and-forget (never block the response); an unhandled rejection would kill the server, so log it.
|
|
882
|
-
consolidate(hippoRoot)
|
|
883
|
-
.catch((err) => {
|
|
884
|
-
log.error(`auto-sleep consolidate failed (tenant ${tenantId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
885
|
-
})
|
|
886
|
-
.finally(() => autoSleepInFlight.delete(hippoRoot));
|
|
887
|
-
}
|
|
888
|
-
const halfLife = entry?.half_life_days ?? config.defaultHalfLifeDays;
|
|
889
|
-
const tagStr = entry?.tags.join(', ') || tags.join(', ') || 'none';
|
|
890
|
-
const warnings = (result.warnings ?? []).map((w) => `\nWarning: ${w}`).join('');
|
|
891
|
-
return `Remembered [${result.id}] (half-life: ${halfLife}d, tags: ${tagStr})${warnings}`;
|
|
892
|
-
}
|
|
893
|
-
case 'hippo_outcome': {
|
|
894
|
-
const good = Boolean(args.good);
|
|
895
|
-
const clientKey = resolveClientKey(ctx);
|
|
896
|
-
const ids = lastRecalledIds.get(clientKey) ?? [];
|
|
897
|
-
if (ids.length === 0)
|
|
898
|
-
return 'No recent recalls to apply outcome to.';
|
|
899
|
-
// Route through src/api.ts so audit_log captures the caller identity
|
|
900
|
-
// (auth-resolved ctx.actor under HTTP-MCP, 'mcp' for stdio) and tenant
|
|
901
|
-
// scoping is enforced uniformly (same surface as recall/remember).
|
|
902
|
-
// outcome() also handles cross-tenant id skip silently.
|
|
903
|
-
const apiCtx = {
|
|
904
|
-
hippoRoot,
|
|
905
|
-
tenantId,
|
|
906
|
-
actor: mcpActor(ctx),
|
|
907
|
-
};
|
|
908
|
-
const { applied } = apiOutcome(apiCtx, ids, good);
|
|
909
|
-
return `Applied ${good ? 'positive' : 'negative'} outcome to ${applied} memories`;
|
|
910
|
-
}
|
|
911
|
-
case 'hippo_context': {
|
|
912
|
-
const budget = args.budget === undefined
|
|
913
|
-
? config.defaultContextBudget
|
|
914
|
-
: Number(args.budget);
|
|
915
|
-
if (!Number.isFinite(budget) || budget < 0)
|
|
916
|
-
return 'budget must be a non-negative number.';
|
|
917
|
-
if (budget === 0)
|
|
918
|
-
return '';
|
|
919
|
-
if (budget < memoriesReserve(budget))
|
|
920
|
-
return ''; // not even the heading fits, so nothing prints, as at budget 0
|
|
921
|
-
const exactScope = isJsonString(args.scope) && args.scope.length > 0
|
|
922
|
-
? args.scope
|
|
923
|
-
: undefined;
|
|
924
|
-
// The served store names the project (an HTTP daemon runs from anywhere); the global root names none, so stdio falls back to its launch cwd.
|
|
925
|
-
const storeProject = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot))).name;
|
|
926
|
-
const result = await apiGetContext({ hippoRoot, tenantId, actor: mcpActor(ctx) }, {
|
|
927
|
-
q: autoDetectContext(),
|
|
928
|
-
budget,
|
|
929
|
-
exactScope,
|
|
930
|
-
currentProject: storeProject !== '' ? storeProject : resolveProjectIdentity(process.cwd()).name,
|
|
931
|
-
cost: contextCost,
|
|
932
|
-
});
|
|
933
|
-
lastRecalledIds.set(resolveClientKey(ctx), result.entries.map((r) => r.entry.id));
|
|
934
|
-
return (result.activeSnapshot ? snapshotPiece(result.activeSnapshot) : '')
|
|
935
|
-
+ (result.sessionHandoff ? handoffPiece(result.sessionHandoff) : '')
|
|
936
|
-
+ (result.recentEvents ? trailPiece(result.recentEvents) : '')
|
|
937
|
-
+ formatMemories(result.entries);
|
|
938
|
-
}
|
|
939
|
-
case 'hippo_status': {
|
|
940
|
-
// Every row counts toward the averages, so this scans the store, but without its text.
|
|
941
|
-
const entries = loadStrengthRows(hippoRoot, tenantId);
|
|
942
|
-
const now = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
|
|
943
|
-
let atRisk = 0;
|
|
944
|
-
let totalStrength = 0;
|
|
945
|
-
for (const e of entries) {
|
|
946
|
-
const s = calculateStrength(e, now);
|
|
947
|
-
totalStrength += s;
|
|
948
|
-
if (s < 0.1 && !e.pinned)
|
|
949
|
-
atRisk++;
|
|
950
|
-
}
|
|
951
|
-
const avgStrength = entries.length > 0 ? (totalStrength / entries.length).toFixed(2) : '0';
|
|
952
|
-
const pinned = entries.filter((e) => e.pinned).length;
|
|
953
|
-
const errors = entries.filter((e) => e.tags.includes('error')).length;
|
|
954
|
-
const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId).length;
|
|
955
|
-
return [
|
|
956
|
-
`Memories: ${entries.length} (${pinned} pinned, ${errors} errors)`,
|
|
957
|
-
`Avg strength: ${avgStrength}`,
|
|
958
|
-
`At risk (<0.1): ${atRisk}`,
|
|
959
|
-
`Open conflicts: ${conflicts}`,
|
|
960
|
-
`Half-life default: ${config.defaultHalfLifeDays}d`,
|
|
961
|
-
].join('\n');
|
|
962
|
-
}
|
|
963
|
-
case 'hippo_learn': {
|
|
964
|
-
const days = Number(args.days) || 7;
|
|
965
|
-
if (!isGitRepo(process.cwd()))
|
|
966
|
-
return 'No git history found.';
|
|
967
|
-
const gitLog = fetchGitLog(process.cwd(), days);
|
|
968
|
-
if (!gitLog.trim())
|
|
969
|
-
return 'No fix/revert/bug commits found in the specified period.';
|
|
970
|
-
const parsedLessons = extractLessons(gitLog, config.gitLearnPatterns);
|
|
971
|
-
// DF4: admission gate lives at the write path, not in extractLessons
|
|
972
|
-
// (a published API surface that only parses). Bare subjects like
|
|
973
|
-
// "fixed signals" are dropped here, before they ever become a memory.
|
|
974
|
-
// Gating the loop INPUT is correct here, unlike the CLI path: this
|
|
975
|
-
// loop only writes. The CLI's loop also runs invalidation, so there
|
|
976
|
-
// the gate has to sit on the write alone or a migration subject stops
|
|
977
|
-
// superseding stale memories. Same predicate, different placement,
|
|
978
|
-
// because the loops do different work.
|
|
979
|
-
const { kept: lessons, dropped } = partitionLessons(parsedLessons);
|
|
980
|
-
const lowInfo = dropped.length;
|
|
981
|
-
let added = 0;
|
|
982
|
-
let skipped = 0;
|
|
983
|
-
let rejected = 0;
|
|
984
|
-
const keys = storedTextKeys(loadTextsHoldingWords(hippoRoot, tenantId, lessons.map(longestWord)));
|
|
985
|
-
for (const lesson of lessons) {
|
|
986
|
-
if (keys.has(duplicateKey(lesson))) {
|
|
987
|
-
skipped++;
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
const entry = createMemory(lesson, {
|
|
991
|
-
layer: Layer.Episodic,
|
|
992
|
-
tags: ['git-learned'],
|
|
993
|
-
source: 'git',
|
|
994
|
-
confidence: 'observed',
|
|
995
|
-
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
996
|
-
tenantId,
|
|
997
|
-
});
|
|
998
|
-
// AT1 (plan §3 containment): a refused lesson must not crash the
|
|
999
|
-
// MCP learn call or lose the rest of the git log scan.
|
|
1000
|
-
try {
|
|
1001
|
-
writeEntry(hippoRoot, entry, { actor: ctx?.actor ?? 'mcp' });
|
|
1002
|
-
}
|
|
1003
|
-
catch (err) {
|
|
1004
|
-
if (err instanceof RejectedValueError) {
|
|
1005
|
-
rejected++;
|
|
1006
|
-
continue;
|
|
1007
|
-
}
|
|
1008
|
-
throw err;
|
|
1009
|
-
}
|
|
1010
|
-
keys.add(duplicateKey(lesson));
|
|
1011
|
-
added++;
|
|
1012
|
-
}
|
|
1013
|
-
const rejectedSuffix = rejected > 0 ? `, ${rejected} rejected values skipped` : '';
|
|
1014
|
-
const lowInfoSuffix = lowInfo > 0 ? `, ${lowInfo} low-information subjects dropped` : '';
|
|
1015
|
-
return `Git learn: ${added} new, ${skipped} duplicates skipped${rejectedSuffix}${lowInfoSuffix} (scanned ${days} days)`;
|
|
1016
|
-
}
|
|
1017
|
-
case 'hippo_conflicts': {
|
|
1018
|
-
const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId);
|
|
1019
|
-
if (conflicts.length === 0)
|
|
1020
|
-
return 'No open conflicts.';
|
|
1021
|
-
return conflicts.map((c) => `conflict_${c.id}: ${c.memory_a_id} <-> ${c.memory_b_id} (score=${c.score.toFixed(2)}) — ${c.reason}`).join('\n');
|
|
1022
|
-
}
|
|
1023
|
-
case 'hippo_resolve': {
|
|
1024
|
-
const conflictId = Number(args.conflict_id);
|
|
1025
|
-
const keepId = String(args.keep || '');
|
|
1026
|
-
const forget = Boolean(args.forget);
|
|
1027
|
-
// AT1: optional rejectLoser + reason, threaded straight through to
|
|
1028
|
-
// resolveConflict's opts (plan §5 — mirrors the CLI's --reject-loser).
|
|
1029
|
-
const rejectLoser = Boolean(args.rejectLoser);
|
|
1030
|
-
const reason = isJsonString(args.reason) ? args.reason : undefined;
|
|
1031
|
-
if (isNaN(conflictId) || !keepId)
|
|
1032
|
-
return 'Required: conflict_id and keep.';
|
|
1033
|
-
const result = resolveConflict(hippoRoot, conflictId, keepId, forget, tenantId, {
|
|
1034
|
-
rejectLoserValue: rejectLoser,
|
|
1035
|
-
reason,
|
|
1036
|
-
// P2 fix: resolveConflict's opts.rejectedBy defaults to 'cli' when
|
|
1037
|
-
// omitted — this call site never passed it, so the tombstone's
|
|
1038
|
-
// rejected_by AND the conflict_resolve audit's actor both landed as
|
|
1039
|
-
// 'cli' even though the caller was MCP. ctx.actor carries the
|
|
1040
|
-
// auth-resolved actor for HTTP-MCP (see McpContext above); stdio
|
|
1041
|
-
// callers pass no ctx, so 'mcp' is the honest fallback there.
|
|
1042
|
-
rejectedBy: ctx?.actor ?? 'mcp',
|
|
1043
|
-
});
|
|
1044
|
-
if (!result)
|
|
1045
|
-
return 'Could not resolve. Check the conflict ID and --keep value.';
|
|
1046
|
-
const action = rejectLoser ? 'rejected (tombstoned) and removed' : forget ? 'deleted' : 'weakened';
|
|
1047
|
-
return `Resolved conflict ${conflictId}: kept ${keepId}, ${action} ${result.loserId}`;
|
|
1048
|
-
}
|
|
1049
|
-
case 'hippo_share': {
|
|
1050
|
-
const shareId = String(args.id || '');
|
|
1051
|
-
if (!shareId)
|
|
1052
|
-
return 'Required: id (memory ID to share).';
|
|
1053
|
-
const force = Boolean(args.force);
|
|
1054
|
-
// Pass tenantId so shareMemory's readEntry filters by tenant. Without
|
|
1055
|
-
// this, a Bearer for tenant A could call hippo_share with tenant B's
|
|
1056
|
-
// id and copy the row to the global store. The 'Memory not found'
|
|
1057
|
-
// error matches the cross-tenant deny shape elsewhere in the code.
|
|
1058
|
-
const shared = shareMemory(hippoRoot, shareId, { force, tenantId });
|
|
1059
|
-
if (!shared)
|
|
1060
|
-
return 'Transfer score too low. Use force=true to override.';
|
|
1061
|
-
return `Shared [${shared.id}] to global store. Source: ${shared.source}`;
|
|
1062
|
-
}
|
|
1063
|
-
case 'hippo_peers': {
|
|
1064
|
-
// D4 v1.12.10: tenant-scope the cross-project peer discovery.
|
|
1065
|
-
// tenantId is the caller's tenant (matches hippo_share above);
|
|
1066
|
-
// passing undefined would restore the pre-D4 host-wide behaviour.
|
|
1067
|
-
const peers = listPeers(undefined, tenantId);
|
|
1068
|
-
if (peers.length === 0)
|
|
1069
|
-
return 'No peers found.';
|
|
1070
|
-
return peers.map((p) => `${p.project}: ${p.count} memories (latest: ${p.latest.slice(0, 10)})`).join('\n');
|
|
1071
|
-
}
|
|
1072
|
-
default:
|
|
1073
|
-
// handleMcpRequest rejects names missing from TOOLS, so reaching here means TOOLS and this switch drifted apart.
|
|
1074
|
-
throw new Error(`hippo-mcp: tool ${name} is declared but has no handler`);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
// ── Request handling ──
|
|
1078
|
-
/**
|
|
1079
|
-
* Transport-agnostic MCP dispatcher. Both the stdio loop (below) and the
|
|
1080
|
-
* HTTP/SSE transport in src/server.ts route every incoming JSON-RPC message
|
|
1081
|
-
* through this single function. Returns null for notifications (no response
|
|
1082
|
-
* expected) and a McpResponse otherwise. Errors thrown by `executeTool` are
|
|
1083
|
-
* the caller's problem — wrap with try/catch on the transport side.
|
|
1084
|
-
*/
|
|
1085
|
-
export async function handleMcpRequest(req, ctx) {
|
|
1086
|
-
const { id, method, params } = req;
|
|
1087
|
-
switch (method) {
|
|
1088
|
-
case 'initialize':
|
|
1089
|
-
return {
|
|
1090
|
-
jsonrpc: '2.0',
|
|
1091
|
-
id,
|
|
1092
|
-
result: {
|
|
1093
|
-
protocolVersion: '2024-11-05',
|
|
1094
|
-
capabilities: { tools: {} },
|
|
1095
|
-
serverInfo: { name: 'hippo-memory', version: PACKAGE_VERSION },
|
|
1096
|
-
},
|
|
1097
|
-
};
|
|
1098
|
-
case 'notifications/initialized':
|
|
1099
|
-
return null;
|
|
1100
|
-
case 'tools/list':
|
|
1101
|
-
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
1102
|
-
case 'tools/call': {
|
|
1103
|
-
const nameValue = params?.name;
|
|
1104
|
-
const toolName = isJsonString(nameValue) ? nameValue : '';
|
|
1105
|
-
const tool = TOOLS_BY_NAME.get(toolName);
|
|
1106
|
-
if (!tool) {
|
|
1107
|
-
return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${toolName.slice(0, 128)}` } };
|
|
1108
|
-
}
|
|
1109
|
-
const argumentsValue = params?.arguments;
|
|
1110
|
-
if (argumentsValue !== undefined && argumentsValue !== null && !isJsonObjectRecord(argumentsValue)) {
|
|
1111
|
-
return { jsonrpc: '2.0', id, error: { code: -32602, message: `${toolName}: arguments must be an object` } };
|
|
1112
|
-
}
|
|
1113
|
-
const toolArgs = isJsonObjectRecord(argumentsValue) ? argumentsValue : {};
|
|
1114
|
-
// The MCP spec reports input validation as a tool result with isError, so the model can read it and retry.
|
|
1115
|
-
const problems = validateToolArgs(tool.inputSchema, toolArgs, ARGS_CHECKED_BY_API.get(toolName));
|
|
1116
|
-
if (problems.length > 0) {
|
|
1117
|
-
return {
|
|
1118
|
-
jsonrpc: '2.0',
|
|
1119
|
-
id,
|
|
1120
|
-
result: { content: [{ type: 'text', text: `Invalid arguments for ${toolName}: ${problems.join('; ')}` }], isError: true },
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
const output = await executeTool(toolName, toolArgs, ctx);
|
|
1124
|
-
recordMcpTokens(toolName, output, ctx);
|
|
1125
|
-
return {
|
|
1126
|
-
jsonrpc: '2.0',
|
|
1127
|
-
id,
|
|
1128
|
-
result: {
|
|
1129
|
-
content: [{ type: 'text', text: output || 'Done.' }],
|
|
1130
|
-
},
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
default:
|
|
1134
|
-
// Notifications (no id) must not receive a response
|
|
1135
|
-
if (method.startsWith('notifications/'))
|
|
1136
|
-
return null;
|
|
1137
|
-
return {
|
|
1138
|
-
jsonrpc: '2.0',
|
|
1139
|
-
id,
|
|
1140
|
-
error: { code: -32601, message: `Method not found: ${method}` },
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
// ── Stdio transport ──
|
|
1145
|
-
import { parseFrame } from './framing.js';
|
|
1146
|
-
let buffer = Buffer.alloc(0);
|
|
1147
|
-
function dispatch(body) {
|
|
1148
|
-
let req;
|
|
1149
|
-
try {
|
|
1150
|
-
// SAFETY: malformed JSON is caught below and the frame is skipped; the
|
|
1151
|
-
// JSON-RPC shape itself is validated field-by-field next (req.method
|
|
1152
|
-
// truthiness check), matching the src/server.ts HTTP transport's own
|
|
1153
|
-
// `JSON.parse(raw) as McpRequest` boundary cast.
|
|
1154
|
-
req = JSON.parse(body);
|
|
1155
|
-
}
|
|
1156
|
-
catch {
|
|
1157
|
-
log.debug('mcp: skipped a frame that is not valid JSON');
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
if (!req.method)
|
|
1161
|
-
return;
|
|
1162
|
-
if (req.method.startsWith('notifications/')) {
|
|
1163
|
-
handleMcpRequest(req).catch((err) => {
|
|
1164
|
-
log.error(`mcp notification ${req.method} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1165
|
-
});
|
|
1166
|
-
return;
|
|
1167
|
-
}
|
|
1168
|
-
handleMcpRequest(req).then((resp) => { if (resp)
|
|
1169
|
-
send(resp); }).catch((err) => {
|
|
1170
|
-
send(mcpErrorResponse(req.id, err));
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
/**
|
|
1174
|
-
* Wire stdin/stdout to the dispatcher. Idempotent — only the entrypoint
|
|
1175
|
-
* (cli.ts `hippo mcp`, or running this file directly) should call this.
|
|
1176
|
-
* src/server.ts imports `handleMcpRequest` without invoking this, so the
|
|
1177
|
-
* HTTP daemon does not steal stdin or exit when its parent closes a pipe.
|
|
1178
|
-
*/
|
|
1179
|
-
export function startStdioLoop() {
|
|
1180
|
-
process.stdin.on('data', (chunk) => {
|
|
1181
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
1182
|
-
while (true) {
|
|
1183
|
-
const result = parseFrame(buffer);
|
|
1184
|
-
if (result.kind === 'incomplete')
|
|
1185
|
-
break;
|
|
1186
|
-
buffer = result.rest;
|
|
1187
|
-
if (result.kind === 'message')
|
|
1188
|
-
dispatch(result.body);
|
|
1189
|
-
}
|
|
1190
|
-
});
|
|
1191
|
-
process.stdin.on('end', () => process.exit(0));
|
|
1192
|
-
process.on('uncaughtException', (err) => {
|
|
1193
|
-
log.error(`mcp uncaught: ${err instanceof Error ? err.message : String(err)}`);
|
|
1194
|
-
});
|
|
1195
|
-
process.on('unhandledRejection', (err) => {
|
|
1196
|
-
log.error(`mcp unhandled: ${err instanceof Error ? err.message : String(err)}`);
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
10
|
+
export { __resetSessionRecallHistoryMcp } from './session-state.js';
|
|
11
|
+
export { findHippoRoot, mcpErrorResponse } from './protocol.js';
|
|
12
|
+
export { handleMcpRequest } from './request.js';
|
|
13
|
+
export { startStdioLoop } from './stdio.js';
|
|
14
|
+
import { envMcpStdio } from '../env.js';
|
|
15
|
+
import { startStdioLoop } from './stdio.js';
|
|
1199
16
|
// Auto-start when invoked as the main module (node dist/mcp/server.js or via
|
|
1200
17
|
// the cli's `import('./mcp/server.js')`). Importing this file from another
|
|
1201
18
|
// module (e.g. src/server.ts wiring up the HTTP/SSE transport) will NOT
|
|
@@ -1208,7 +25,7 @@ const isMainModule = (() => {
|
|
|
1208
25
|
const argv1 = process.argv[1] ?? '';
|
|
1209
26
|
if (argv1.endsWith('mcp/server.js') || argv1.endsWith('mcp\\server.js'))
|
|
1210
27
|
return true;
|
|
1211
|
-
if (
|
|
28
|
+
if (envMcpStdio())
|
|
1212
29
|
return true;
|
|
1213
30
|
// ESM main-module check
|
|
1214
31
|
const mainUrl = `file://${argv1.replace(/\\/g, '/')}`;
|