@monoes/monomindcli 2.16.12 → 2.16.14
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/.claude/agents/core/coder.md +1 -1
- package/.claude/agents/engineering/engineering-devops-automator.md +1 -1
- package/.claude/agents/github/repo-architect.md +1 -1
- package/.claude/agents/testing/production-validator.md +1 -1
- package/.claude/agents/testing/testing-tool-evaluator.md +1 -1
- package/.claude/commands/github/pr-manager.md +1 -1
- package/.claude/helpers/pick-rank.cjs +34 -4
- package/.claude/skills/monolean-audit/SKILL.md +1 -1
- package/README.md +1 -1
- package/dist/src/commands/cleanup-data.d.ts +30 -0
- package/dist/src/commands/cleanup-data.d.ts.map +1 -0
- package/dist/src/commands/cleanup-data.js +138 -0
- package/dist/src/commands/cleanup-data.js.map +1 -0
- package/dist/src/commands/cleanup-scratch.d.ts +43 -0
- package/dist/src/commands/cleanup-scratch.d.ts.map +1 -0
- package/dist/src/commands/cleanup-scratch.js +125 -0
- package/dist/src/commands/cleanup-scratch.js.map +1 -0
- package/dist/src/commands/cleanup.d.ts +2 -39
- package/dist/src/commands/cleanup.d.ts.map +1 -1
- package/dist/src/commands/cleanup.js +5 -247
- package/dist/src/commands/cleanup.js.map +1 -1
- package/dist/src/commands/doctor-gitignore-checks.d.ts +10 -0
- package/dist/src/commands/doctor-gitignore-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-gitignore-checks.js +141 -0
- package/dist/src/commands/doctor-gitignore-checks.js.map +1 -0
- package/dist/src/commands/doctor-helpers-checks.d.ts +10 -0
- package/dist/src/commands/doctor-helpers-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-helpers-checks.js +292 -0
- package/dist/src/commands/doctor-helpers-checks.js.map +1 -0
- package/dist/src/commands/doctor-memory-checks.d.ts +47 -0
- package/dist/src/commands/doctor-memory-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-memory-checks.js +298 -0
- package/dist/src/commands/doctor-memory-checks.js.map +1 -0
- package/dist/src/commands/doctor-monograph-checks.d.ts +9 -0
- package/dist/src/commands/doctor-monograph-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-monograph-checks.js +331 -0
- package/dist/src/commands/doctor-monograph-checks.js.map +1 -0
- package/dist/src/commands/doctor-project-checks.d.ts +7 -66
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +10 -1331
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor-routing-checks.d.ts +10 -0
- package/dist/src/commands/doctor-routing-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-routing-checks.js +194 -0
- package/dist/src/commands/doctor-routing-checks.js.map +1 -0
- package/dist/src/commands/doctor-worker-checks.d.ts +17 -0
- package/dist/src/commands/doctor-worker-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-worker-checks.js +122 -0
- package/dist/src/commands/doctor-worker-checks.js.map +1 -0
- package/dist/src/commands/hooks-routing-commands.js +1 -1
- package/dist/src/commands/hooks-routing-commands.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +6 -15
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/org-control.d.ts +117 -0
- package/dist/src/commands/org-control.d.ts.map +1 -0
- package/dist/src/commands/org-control.js +274 -0
- package/dist/src/commands/org-control.js.map +1 -0
- package/dist/src/commands/org-lifecycle.d.ts +7 -0
- package/dist/src/commands/org-lifecycle.d.ts.map +1 -0
- package/dist/src/commands/org-lifecycle.js +298 -0
- package/dist/src/commands/org-lifecycle.js.map +1 -0
- package/dist/src/commands/org-manage.d.ts +7 -0
- package/dist/src/commands/org-manage.d.ts.map +1 -0
- package/dist/src/commands/org-manage.js +357 -0
- package/dist/src/commands/org-manage.js.map +1 -0
- package/dist/src/commands/org-memory-command.d.ts +3 -0
- package/dist/src/commands/org-memory-command.d.ts.map +1 -0
- package/dist/src/commands/org-memory-command.js +178 -0
- package/dist/src/commands/org-memory-command.js.map +1 -0
- package/dist/src/commands/org-observe-approvals.d.ts +8 -0
- package/dist/src/commands/org-observe-approvals.d.ts.map +1 -0
- package/dist/src/commands/org-observe-approvals.js +211 -0
- package/dist/src/commands/org-observe-approvals.js.map +1 -0
- package/dist/src/commands/org-observe-checkpoint.d.ts +26 -0
- package/dist/src/commands/org-observe-checkpoint.d.ts.map +1 -0
- package/dist/src/commands/org-observe-checkpoint.js +180 -0
- package/dist/src/commands/org-observe-checkpoint.js.map +1 -0
- package/dist/src/commands/org-observe-config.d.ts +8 -0
- package/dist/src/commands/org-observe-config.d.ts.map +1 -0
- package/dist/src/commands/org-observe-config.js +160 -0
- package/dist/src/commands/org-observe-config.js.map +1 -0
- package/dist/src/commands/org-observe-gates.d.ts +4 -0
- package/dist/src/commands/org-observe-gates.d.ts.map +1 -0
- package/dist/src/commands/org-observe-gates.js +182 -0
- package/dist/src/commands/org-observe-gates.js.map +1 -0
- package/dist/src/commands/org-observe-inbox.d.ts +18 -0
- package/dist/src/commands/org-observe-inbox.d.ts.map +1 -0
- package/dist/src/commands/org-observe-inbox.js +154 -0
- package/dist/src/commands/org-observe-inbox.js.map +1 -0
- package/dist/src/commands/org-observe-logs.d.ts +31 -0
- package/dist/src/commands/org-observe-logs.d.ts.map +1 -0
- package/dist/src/commands/org-observe-logs.js +272 -0
- package/dist/src/commands/org-observe-logs.js.map +1 -0
- package/dist/src/commands/org-observe-questions.d.ts +8 -0
- package/dist/src/commands/org-observe-questions.d.ts.map +1 -0
- package/dist/src/commands/org-observe-questions.js +201 -0
- package/dist/src/commands/org-observe-questions.js.map +1 -0
- package/dist/src/commands/org-observe-report.d.ts +8 -0
- package/dist/src/commands/org-observe-report.d.ts.map +1 -0
- package/dist/src/commands/org-observe-report.js +366 -0
- package/dist/src/commands/org-observe-report.js.map +1 -0
- package/dist/src/commands/org-observe-shared.d.ts +20 -0
- package/dist/src/commands/org-observe-shared.d.ts.map +1 -0
- package/dist/src/commands/org-observe-shared.js +55 -0
- package/dist/src/commands/org-observe-shared.js.map +1 -0
- package/dist/src/commands/org-observe.d.ts +9 -101
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +12 -1675
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org-poll.d.ts +57 -0
- package/dist/src/commands/org-poll.d.ts.map +1 -0
- package/dist/src/commands/org-poll.js +194 -0
- package/dist/src/commands/org-poll.js.map +1 -0
- package/dist/src/commands/org-run.d.ts +3 -0
- package/dist/src/commands/org-run.d.ts.map +1 -0
- package/dist/src/commands/org-run.js +449 -0
- package/dist/src/commands/org-run.js.map +1 -0
- package/dist/src/commands/org-serve.d.ts +19 -0
- package/dist/src/commands/org-serve.d.ts.map +1 -0
- package/dist/src/commands/org-serve.js +395 -0
- package/dist/src/commands/org-serve.js.map +1 -0
- package/dist/src/commands/org-subcommands-config.d.ts +8 -0
- package/dist/src/commands/org-subcommands-config.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-config.js +93 -0
- package/dist/src/commands/org-subcommands-config.js.map +1 -0
- package/dist/src/commands/org-subcommands-hil.d.ts +11 -0
- package/dist/src/commands/org-subcommands-hil.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-hil.js +216 -0
- package/dist/src/commands/org-subcommands-hil.js.map +1 -0
- package/dist/src/commands/org-subcommands-observe.d.ts +12 -0
- package/dist/src/commands/org-subcommands-observe.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-observe.js +249 -0
- package/dist/src/commands/org-subcommands-observe.js.map +1 -0
- package/dist/src/commands/org-subcommands-runtime.d.ts +12 -0
- package/dist/src/commands/org-subcommands-runtime.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-runtime.js +163 -0
- package/dist/src/commands/org-subcommands-runtime.js.map +1 -0
- package/dist/src/commands/org.d.ts +3 -151
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +43 -2735
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/never-commit.d.ts +3 -3
- package/dist/src/init/types.d.ts +1 -1
- package/dist/src/init/types.js +1 -1
- package/dist/src/knowledge/document-store.js +1 -1
- package/dist/src/knowledge/document-store.js.map +1 -1
- package/dist/src/mcp-tools/hooks-edit-command.d.ts +10 -0
- package/dist/src/mcp-tools/hooks-edit-command.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-edit-command.js +247 -0
- package/dist/src/mcp-tools/hooks-edit-command.js.map +1 -0
- package/dist/src/mcp-tools/hooks-learning.d.ts +9 -0
- package/dist/src/mcp-tools/hooks-learning.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-learning.js +408 -0
- package/dist/src/mcp-tools/hooks-learning.js.map +1 -0
- package/dist/src/mcp-tools/hooks-metrics-list.d.ts +8 -0
- package/dist/src/mcp-tools/hooks-metrics-list.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-metrics-list.js +205 -0
- package/dist/src/mcp-tools/hooks-metrics-list.js.map +1 -0
- package/dist/src/mcp-tools/hooks-route.d.ts +12 -0
- package/dist/src/mcp-tools/hooks-route.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-route.js +251 -0
- package/dist/src/mcp-tools/hooks-route.js.map +1 -0
- package/dist/src/mcp-tools/hooks-routing.d.ts +6 -27
- package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-routing.js +6 -1727
- package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
- package/dist/src/mcp-tools/hooks-session.d.ts +9 -0
- package/dist/src/mcp-tools/hooks-session.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-session.js +243 -0
- package/dist/src/mcp-tools/hooks-session.js.map +1 -0
- package/dist/src/mcp-tools/hooks-task.d.ts +17 -0
- package/dist/src/mcp-tools/hooks-task.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-task.js +427 -0
- package/dist/src/mcp-tools/hooks-task.js.map +1 -0
- package/dist/src/mcp-tools/hooks-tools.d.ts +2 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +2 -1
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/mcp-tools/memory-tools.js +1 -1
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/memory/bm25-index.d.ts +2 -2
- package/dist/src/memory/bm25-index.js +2 -2
- package/dist/src/memory/embedding-operations.js +2 -2
- package/dist/src/memory/embedding-operations.js.map +1 -1
- package/dist/src/memory/entity-name-key.d.ts +1 -1
- package/dist/src/memory/entity-name-key.js +1 -1
- package/dist/src/memory/hnsw-operations.d.ts +1 -1
- package/dist/src/memory/hnsw-operations.js +1 -1
- package/dist/src/memory/memory-bridge-backend.d.ts +43 -0
- package/dist/src/memory/memory-bridge-backend.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-backend.js +363 -0
- package/dist/src/memory/memory-bridge-backend.js.map +1 -0
- package/dist/src/memory/memory-bridge-core.d.ts +42 -0
- package/dist/src/memory/memory-bridge-core.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-core.js +126 -0
- package/dist/src/memory/memory-bridge-core.js.map +1 -0
- package/dist/src/memory/memory-bridge-entries.d.ts +131 -0
- package/dist/src/memory/memory-bridge-entries.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-entries.js +227 -0
- package/dist/src/memory/memory-bridge-entries.js.map +1 -0
- package/dist/src/memory/memory-bridge-feedback.d.ts +60 -0
- package/dist/src/memory/memory-bridge-feedback.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-feedback.js +118 -0
- package/dist/src/memory/memory-bridge-feedback.js.map +1 -0
- package/dist/src/memory/memory-bridge-paths.d.ts +123 -0
- package/dist/src/memory/memory-bridge-paths.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-paths.js +335 -0
- package/dist/src/memory/memory-bridge-paths.js.map +1 -0
- package/dist/src/memory/memory-bridge-search.d.ts +48 -0
- package/dist/src/memory/memory-bridge-search.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-search.js +383 -0
- package/dist/src/memory/memory-bridge-search.js.map +1 -0
- package/dist/src/memory/memory-bridge-sessions.d.ts +38 -0
- package/dist/src/memory/memory-bridge-sessions.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-sessions.js +109 -0
- package/dist/src/memory/memory-bridge-sessions.js.map +1 -0
- package/dist/src/memory/memory-bridge-store.d.ts +58 -0
- package/dist/src/memory/memory-bridge-store.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-store.js +201 -0
- package/dist/src/memory/memory-bridge-store.js.map +1 -0
- package/dist/src/memory/memory-bridge.d.ts +10 -438
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +14 -1787
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-kg-claims.d.ts +52 -0
- package/dist/src/memory/memory-kg-claims.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-claims.js +99 -0
- package/dist/src/memory/memory-kg-claims.js.map +1 -0
- package/dist/src/memory/memory-kg-index.d.ts +86 -0
- package/dist/src/memory/memory-kg-index.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-index.js +256 -0
- package/dist/src/memory/memory-kg-index.js.map +1 -0
- package/dist/src/memory/memory-kg-ingest.d.ts +44 -0
- package/dist/src/memory/memory-kg-ingest.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-ingest.js +361 -0
- package/dist/src/memory/memory-kg-ingest.js.map +1 -0
- package/dist/src/memory/memory-kg-model.d.ts +168 -0
- package/dist/src/memory/memory-kg-model.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-model.js +158 -0
- package/dist/src/memory/memory-kg-model.js.map +1 -0
- package/dist/src/memory/memory-kg-names.d.ts +64 -0
- package/dist/src/memory/memory-kg-names.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-names.js +187 -0
- package/dist/src/memory/memory-kg-names.js.map +1 -0
- package/dist/src/memory/memory-kg-promote.d.ts +79 -0
- package/dist/src/memory/memory-kg-promote.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-promote.js +248 -0
- package/dist/src/memory/memory-kg-promote.js.map +1 -0
- package/dist/src/memory/memory-kg-rebuild.d.ts +77 -0
- package/dist/src/memory/memory-kg-rebuild.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rebuild.js +299 -0
- package/dist/src/memory/memory-kg-rebuild.js.map +1 -0
- package/dist/src/memory/memory-kg-rollback.d.ts +47 -0
- package/dist/src/memory/memory-kg-rollback.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rollback.js +193 -0
- package/dist/src/memory/memory-kg-rollback.js.map +1 -0
- package/dist/src/memory/memory-kg-rules.d.ts +64 -0
- package/dist/src/memory/memory-kg-rules.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rules.js +251 -0
- package/dist/src/memory/memory-kg-rules.js.map +1 -0
- package/dist/src/memory/memory-kg-scan.d.ts +118 -0
- package/dist/src/memory/memory-kg-scan.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-scan.js +209 -0
- package/dist/src/memory/memory-kg-scan.js.map +1 -0
- package/dist/src/memory/memory-kg-search.d.ts +80 -0
- package/dist/src/memory/memory-kg-search.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-search.js +239 -0
- package/dist/src/memory/memory-kg-search.js.map +1 -0
- package/dist/src/memory/memory-kg.d.ts +19 -547
- package/dist/src/memory/memory-kg.d.ts.map +1 -1
- package/dist/src/memory/memory-kg.js +10 -2389
- package/dist/src/memory/memory-kg.js.map +1 -1
- package/dist/src/orgrt/daemon-types.d.ts +196 -0
- package/dist/src/orgrt/daemon-types.d.ts.map +1 -0
- package/dist/src/orgrt/daemon-types.js +38 -0
- package/dist/src/orgrt/daemon-types.js.map +1 -0
- package/dist/src/orgrt/daemon.d.ts +70 -300
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +41 -2536
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/idle-watchdog.d.ts +39 -0
- package/dist/src/orgrt/idle-watchdog.d.ts.map +1 -0
- package/dist/src/orgrt/idle-watchdog.js +289 -0
- package/dist/src/orgrt/idle-watchdog.js.map +1 -0
- package/dist/src/orgrt/org-reload.d.ts +7 -0
- package/dist/src/orgrt/org-reload.d.ts.map +1 -0
- package/dist/src/orgrt/org-reload.js +111 -0
- package/dist/src/orgrt/org-reload.js.map +1 -0
- package/dist/src/orgrt/org-start-steps.d.ts +21 -0
- package/dist/src/orgrt/org-start-steps.d.ts.map +1 -0
- package/dist/src/orgrt/org-start-steps.js +305 -0
- package/dist/src/orgrt/org-start-steps.js.map +1 -0
- package/dist/src/orgrt/org-start.d.ts +7 -0
- package/dist/src/orgrt/org-start.d.ts.map +1 -0
- package/dist/src/orgrt/org-start.js +435 -0
- package/dist/src/orgrt/org-start.js.map +1 -0
- package/dist/src/orgrt/org-state-file.d.ts +8 -0
- package/dist/src/orgrt/org-state-file.d.ts.map +1 -0
- package/dist/src/orgrt/org-state-file.js +116 -0
- package/dist/src/orgrt/org-state-file.js.map +1 -0
- package/dist/src/orgrt/org-stop.d.ts +6 -0
- package/dist/src/orgrt/org-stop.d.ts.map +1 -0
- package/dist/src/orgrt/org-stop.js +332 -0
- package/dist/src/orgrt/org-stop.js.map +1 -0
- package/dist/src/orgrt/org-tools.d.ts +11 -0
- package/dist/src/orgrt/org-tools.d.ts.map +1 -0
- package/dist/src/orgrt/org-tools.js +305 -0
- package/dist/src/orgrt/org-tools.js.map +1 -0
- package/dist/src/orgrt/role-incarnation.d.ts +18 -0
- package/dist/src/orgrt/role-incarnation.d.ts.map +1 -0
- package/dist/src/orgrt/role-incarnation.js +374 -0
- package/dist/src/orgrt/role-incarnation.js.map +1 -0
- package/dist/src/orgrt/role-respawn.d.ts +7 -0
- package/dist/src/orgrt/role-respawn.d.ts.map +1 -0
- package/dist/src/orgrt/role-respawn.js +318 -0
- package/dist/src/orgrt/role-respawn.js.map +1 -0
- package/dist/src/orgrt/role-session-opts.d.ts +389 -0
- package/dist/src/orgrt/role-session-opts.d.ts.map +1 -0
- package/dist/src/orgrt/role-session-opts.js +289 -0
- package/dist/src/orgrt/role-session-opts.js.map +1 -0
- package/dist/src/orgrt/runner-resolve.d.ts +43 -0
- package/dist/src/orgrt/runner-resolve.d.ts.map +1 -0
- package/dist/src/orgrt/runner-resolve.js +77 -0
- package/dist/src/orgrt/runner-resolve.js.map +1 -0
- package/dist/src/orgrt/session-gate.d.ts +34 -0
- package/dist/src/orgrt/session-gate.d.ts.map +1 -0
- package/dist/src/orgrt/session-gate.js +74 -0
- package/dist/src/orgrt/session-gate.js.map +1 -0
- package/dist/src/orgrt/session-prompt.d.ts +31 -0
- package/dist/src/orgrt/session-prompt.d.ts.map +1 -0
- package/dist/src/orgrt/session-prompt.js +115 -0
- package/dist/src/orgrt/session-prompt.js.map +1 -0
- package/dist/src/orgrt/session-run.d.ts +18 -0
- package/dist/src/orgrt/session-run.d.ts.map +1 -0
- package/dist/src/orgrt/session-run.js +429 -0
- package/dist/src/orgrt/session-run.js.map +1 -0
- package/dist/src/orgrt/session-stream.d.ts +21 -0
- package/dist/src/orgrt/session-stream.d.ts.map +1 -0
- package/dist/src/orgrt/session-stream.js +173 -0
- package/dist/src/orgrt/session-stream.js.map +1 -0
- package/dist/src/orgrt/session-types.d.ts +219 -0
- package/dist/src/orgrt/session-types.d.ts.map +1 -0
- package/dist/src/orgrt/session-types.js +2 -0
- package/dist/src/orgrt/session-types.js.map +1 -0
- package/dist/src/orgrt/session-usage.d.ts +29 -0
- package/dist/src/orgrt/session-usage.d.ts.map +1 -0
- package/dist/src/orgrt/session-usage.js +64 -0
- package/dist/src/orgrt/session-usage.js.map +1 -0
- package/dist/src/orgrt/session.d.ts +5 -281
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +8 -1117
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/ui/org-hil.mjs +1 -1
- package/dist/src/ui/routes-org-agents.mjs +318 -0
- package/dist/src/ui/routes-org-approvals.mjs +133 -0
- package/dist/src/ui/routes-org-config.mjs +459 -0
- package/dist/src/ui/routes-org-control.mjs +300 -0
- package/dist/src/ui/routes-org-files.mjs +172 -0
- package/dist/src/ui/routes-org-helpers.mjs +84 -0
- package/dist/src/ui/routes-org-knowledge.mjs +262 -0
- package/dist/src/ui/routes-org-lifecycle.mjs +224 -0
- package/dist/src/ui/routes-org-live.mjs +278 -0
- package/dist/src/ui/routes-org-mastermind.mjs +346 -0
- package/dist/src/ui/routes-org-planning.mjs +398 -0
- package/dist/src/ui/routes-org-runs.mjs +430 -0
- package/dist/src/ui/routes-org-status.mjs +342 -0
- package/dist/src/ui/routes-org-workflows.mjs +212 -0
- package/dist/src/ui/routes-org.mjs +26 -3847
- package/dist/src/ui/server.mjs +1 -1
- package/dist/src/utils/native-error.d.ts +1 -1
- package/dist/src/utils/native-error.js +1 -1
- package/dist/src/utils/paths.d.ts +2 -1
- package/dist/src/utils/paths.d.ts.map +1 -1
- package/dist/src/utils/paths.js +2 -1
- package/dist/src/utils/paths.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/org-skills/adversarial-reviewer/SKILL.md +1 -1
- package/org-skills/board-deck-builder/SKILL.md +1 -1
- package/org-skills/code-reviewer/SKILL.md +1 -1
- package/org-skills/debugging-wizard/SKILL.md +1 -1
- package/org-skills/scientific-thinking-literature-review/SKILL.md +1 -1
- package/org-skills/scientific-thinking-scholar-evaluation/SKILL.md +1 -1
- package/package.json +5 -5
|
@@ -4,1564 +4,24 @@
|
|
|
4
4
|
* Uses SQLiteBackend (better-sqlite3, sql.js WASM fallback) from @monoes/memory.
|
|
5
5
|
* LanceDB was replaced by this SQLite engine 2026-07; the on-disk data
|
|
6
6
|
* directory is still named `lancedb` for legacy/back-compat path resolution
|
|
7
|
-
* (see getDbPath
|
|
7
|
+
* (see getDbPath in memory-bridge-paths.ts) but no longer holds LanceDB data.
|
|
8
8
|
* All exported function signatures are unchanged.
|
|
9
9
|
*
|
|
10
10
|
* @module v1/cli/memory-bridge
|
|
11
11
|
*/
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* observability is added. Caller passes the bridge fn name + the thrown
|
|
26
|
-
* value so the log is greppable per call site.
|
|
27
|
-
*/
|
|
28
|
-
function logBridgeError(label, err) {
|
|
29
|
-
if (!(process.env.DEBUG || process.env.MONOMIND_DEBUG))
|
|
30
|
-
return;
|
|
31
|
-
const msg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
32
|
-
console.error(`[bridge:${label}] ${msg}`);
|
|
33
|
-
}
|
|
34
|
-
export function safeParseEmbedding(raw) {
|
|
35
|
-
if (typeof raw !== 'string' || raw.length === 0)
|
|
36
|
-
return null;
|
|
37
|
-
if (raw.length > MAX_EMBEDDING_JSON_BYTES)
|
|
38
|
-
return null;
|
|
39
|
-
let parsed;
|
|
40
|
-
try {
|
|
41
|
-
parsed = JSON.parse(raw);
|
|
42
|
-
}
|
|
43
|
-
catch (e) {
|
|
44
|
-
logBridgeError('safeParseEmbedding', e);
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
if (!Array.isArray(parsed))
|
|
48
|
-
return null;
|
|
49
|
-
if (parsed.length === 0 || parsed.length > MAX_EMBEDDING_DIMS)
|
|
50
|
-
return null;
|
|
51
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
52
|
-
const v = parsed[i];
|
|
53
|
-
if (typeof v !== 'number' || !Number.isFinite(v))
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
return parsed;
|
|
57
|
-
}
|
|
58
|
-
// ===== Constants =====
|
|
59
|
-
export const BRIDGE_EMBEDDING_MODEL = 'Alibaba-NLP/gte-modernbert-base';
|
|
60
|
-
export const BRIDGE_EMBEDDING_DIMS = 768;
|
|
61
|
-
const BRIDGE_MAX_KEY_LEN = 4 * 1024;
|
|
62
|
-
const BRIDGE_MAX_VALUE_LEN = 16 * 1024;
|
|
63
|
-
const MAX_TAGS = 32;
|
|
64
|
-
const MAX_TAG_LEN = 64;
|
|
65
|
-
// Search results serve the head of the stored content only — full values bloat
|
|
66
|
-
// every MCP payload. Entries needing the full text can read the entry by key.
|
|
67
|
-
const BRIDGE_RESULT_CONTENT_CAP = 500;
|
|
68
|
-
function capResultContent(content) {
|
|
69
|
-
return content.length > BRIDGE_RESULT_CONTENT_CAP
|
|
70
|
-
? `${content.slice(0, BRIDGE_RESULT_CONTENT_CAP)}…`
|
|
71
|
-
: content;
|
|
72
|
-
}
|
|
73
|
-
// ===== DB path resolution =====
|
|
74
|
-
// LanceDB commits via atomic rename, which exFAT/SMB project volumes don't support
|
|
75
|
-
// (ENOTSUP os error 45) — and non-APFS volumes grow ._ AppleDouble sidecars inside
|
|
76
|
-
// the .lance datasets that corrupt reads. So the store always lives on the home
|
|
77
|
-
// volume, namespaced per project directory.
|
|
78
|
-
//
|
|
79
|
-
// The slug is a hash of the full resolved path, not a character-substitution of
|
|
80
|
-
// it — flattening separators to '-' is not collision-safe ('/x/foo-bar' and
|
|
81
|
-
// '/x/foo/bar' would both flatten to 'x-foo-bar'). A short readable prefix is
|
|
82
|
-
// kept purely so the directory name is browsable; only the hash guarantees
|
|
83
|
-
// uniqueness.
|
|
84
|
-
// o-16: `.git` and `.monomind` are NOT equivalent evidence of a project.
|
|
85
|
-
// `.git` is created by the user, deliberately (`git init`/clone) — "this
|
|
86
|
-
// directory is a repository". `.monomind` is created by monomind ITSELF, as
|
|
87
|
-
// a side effect of running anywhere — "monomind once ran here", and nothing
|
|
88
|
-
// more. Treating a bare `.monomind` ancestor as adoptable is a feedback
|
|
89
|
-
// loop: a wrong resolution creates the very marker that captures every
|
|
90
|
-
// future descendant. Measured against the actual incident:
|
|
91
|
-
// `~/mdev-tmp/.monomind` and `~/.monomind` both exist,
|
|
92
|
-
// neither has a `.git` or manifest, and neither is a project — both are
|
|
93
|
-
// monomind's own state directories. `INDEPENDENT_PROJECT_MARKERS` (other
|
|
94
|
-
// than `.git`, handled unconditionally) is what makes a `.monomind`
|
|
95
|
-
// ancestor's claim independently checkable.
|
|
96
|
-
const INDEPENDENT_PROJECT_MARKERS = ['package.json', 'pyproject.toml', 'go.mod', 'Cargo.toml'];
|
|
97
|
-
function markerExists(p) {
|
|
98
|
-
try {
|
|
99
|
-
return fs.existsSync(p);
|
|
100
|
-
}
|
|
101
|
-
catch (e) {
|
|
102
|
-
logBridgeError('walkToProjectRoot', e); /* unreadable dir — treat as absent, keep walking */
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
/** o-16 revision 1 (reviewer MAJOR 1, measured not argued): the anchor used
|
|
107
|
-
* to be trusted unconditionally. Two proven harms: (a) a non-existent path
|
|
108
|
-
* (a typo) was adopted as-is, and `projectDataDir()` hashed it into a
|
|
109
|
-
* fresh, empty store directory — the exact split-store failure this item
|
|
110
|
-
* exists to eliminate, reintroduced by the escape hatch meant to fix it.
|
|
111
|
-
* (b) `MONOMIND_PROJECT_ROOT=/` disabled `getDbPath`'s MCP path-traversal
|
|
112
|
-
* guard entirely: the guard is `path.relative(getProjectRoot(), resolved)`
|
|
113
|
-
* not starting with `..`, and `path.relative('/', anything)` never does.
|
|
114
|
-
* `existsSync` + `isDirectory` alone catch (a) but NOT (b) — `/` exists and
|
|
115
|
-
* is a directory — so the filesystem-root check below is what closes (b)
|
|
116
|
-
* specifically; it is not redundant with the others. An invalid anchor is
|
|
117
|
-
* never silently accepted OR silently dropped: the caller falls through to
|
|
118
|
-
* the ordinary walk, and the fact that an anchor was set and rejected is
|
|
119
|
-
* preserved on the result (`invalidAnchor`) so `doctor` can say exactly
|
|
120
|
-
* what happened instead of the user just seeing an unexplained directory. */
|
|
121
|
-
function validateAnchor(raw) {
|
|
122
|
-
if (!path.isAbsolute(raw))
|
|
123
|
-
return { ok: false, problem: 'not an absolute path' };
|
|
124
|
-
const lexical = path.resolve(raw);
|
|
125
|
-
// o-16 revision 2 (reviewer MAJOR): resolve symlinks BEFORE validating,
|
|
126
|
-
// and return the REAL path, not the lexical one. Without this, a symlink
|
|
127
|
-
// whose real target is '/' (e.g. MONOMIND_PROJECT_ROOT=<tmp>/link-to-root)
|
|
128
|
-
// passes every check below on its lexical form — dirname(lexical) !==
|
|
129
|
-
// lexical, since the link itself sits inside a normal directory — while
|
|
130
|
-
// getDbPath's traversal guard (memory-bridge.ts's realOrResolved(), i.e.
|
|
131
|
-
// fs.realpathSync) resolves the SAME anchor to '/' downstream. Two
|
|
132
|
-
// notions of "the root" in one module: the store gets hashed from the
|
|
133
|
-
// lexical path this function returned, the guard's boundary is computed
|
|
134
|
-
// from the real one, and path.relative('/', anything) never starts with
|
|
135
|
-
// '..' — the exact guard-disabling hole this function exists to close,
|
|
136
|
-
// reopened one indirection away. Validating and returning the SAME (real)
|
|
137
|
-
// path is what makes validation and consumption agree, for any
|
|
138
|
-
// symlinked anchor, not just a literal '/'.
|
|
139
|
-
let real;
|
|
140
|
-
try {
|
|
141
|
-
real = fs.realpathSync(lexical);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return { ok: false, problem: 'does not exist' };
|
|
145
|
-
}
|
|
146
|
-
let stat;
|
|
147
|
-
try {
|
|
148
|
-
stat = fs.statSync(real);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
return { ok: false, problem: 'does not exist' };
|
|
152
|
-
}
|
|
153
|
-
if (!stat.isDirectory())
|
|
154
|
-
return { ok: false, problem: 'is not a directory' };
|
|
155
|
-
if (path.dirname(real) === real) {
|
|
156
|
-
return {
|
|
157
|
-
ok: false,
|
|
158
|
-
problem: 'is the filesystem root, which would disable the MCP path-traversal guard',
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
return { ok: true, resolved: real };
|
|
162
|
-
}
|
|
163
|
-
function walkToProjectRoot(start) {
|
|
164
|
-
// Explicit escape hatch: honored ahead of any walk, but only once valid —
|
|
165
|
-
// see validateAnchor's comment for why unconditional trust was wrong.
|
|
166
|
-
const anchorRaw = process.env.MONOMIND_PROJECT_ROOT;
|
|
167
|
-
let invalidAnchor;
|
|
168
|
-
if (anchorRaw) {
|
|
169
|
-
const check = validateAnchor(anchorRaw);
|
|
170
|
-
if (check.ok)
|
|
171
|
-
return { root: check.resolved, reason: 'explicit-anchor' };
|
|
172
|
-
invalidAnchor = { value: anchorRaw, problem: check.problem };
|
|
173
|
-
logBridgeError('walkToProjectRoot', new Error(`MONOMIND_PROJECT_ROOT ignored: "${anchorRaw}" ${check.problem} — falling back to the walk`));
|
|
174
|
-
}
|
|
175
|
-
// Retained from before o-16: protects the dotfiles-repo-at-$HOME case (a
|
|
176
|
-
// `.git` at $HOME must not swallow every loose project underneath it).
|
|
177
|
-
// Insufficient ALONE against the reported incident — that marker sat
|
|
178
|
-
// *inside* $HOME, so the walk reached it long before ever reaching home —
|
|
179
|
-
// which is why it is additive to, not a replacement for, the rule below.
|
|
180
|
-
const home = path.resolve(os.homedir());
|
|
181
|
-
let dir = start;
|
|
182
|
-
let ignoredBareMonomind;
|
|
183
|
-
let atStart = true;
|
|
184
|
-
for (;;) {
|
|
185
|
-
if (dir === home)
|
|
186
|
-
break;
|
|
187
|
-
if (markerExists(path.join(dir, '.git')))
|
|
188
|
-
return { root: dir, reason: 'git', invalidAnchor };
|
|
189
|
-
if (markerExists(path.join(dir, '.monomind'))) {
|
|
190
|
-
if (atStart)
|
|
191
|
-
return { root: dir, reason: 'monomind-at-start', invalidAnchor };
|
|
192
|
-
const corroborated = INDEPENDENT_PROJECT_MARKERS.some((m) => markerExists(path.join(dir, m)));
|
|
193
|
-
if (corroborated)
|
|
194
|
-
return { root: dir, reason: 'monomind-with-marker', invalidAnchor };
|
|
195
|
-
// The nearest marker found doesn't qualify — stop HERE. Continuing
|
|
196
|
-
// past it to adopt some more distant, unrelated ancestor's `.git`
|
|
197
|
-
// would break "nested projects keep their own brain" in a new way:
|
|
198
|
-
// a vendored sub-repo with only a bare `.monomind` would get silently
|
|
199
|
-
// merged into whatever repo happens to sit further up, which the
|
|
200
|
-
// original nearest-marker-wins design specifically existed to
|
|
201
|
-
// prevent. Not adopting is the fallback; walking past is not.
|
|
202
|
-
ignoredBareMonomind = dir;
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
atStart = false;
|
|
206
|
-
const parent = path.dirname(dir);
|
|
207
|
-
if (parent === dir)
|
|
208
|
-
break;
|
|
209
|
-
dir = parent;
|
|
210
|
-
}
|
|
211
|
-
if (ignoredBareMonomind !== undefined) {
|
|
212
|
-
logBridgeError('walkToProjectRoot', new Error(`bare .monomind ancestor ignored at ${ignoredBareMonomind} (no independent project marker) — using ${start}`));
|
|
213
|
-
}
|
|
214
|
-
return { root: start, reason: 'start-fallback', ignoredBareMonomind, invalidAnchor };
|
|
215
|
-
}
|
|
216
|
-
// getBackend() resolves the store path on every store/search, so a bulk ingest
|
|
217
|
-
// would otherwise repeat the stat walk thousands of times. A project does not
|
|
218
|
-
// gain or lose its markers mid-process; the key is the starting directory, so a
|
|
219
|
-
// chdir still re-resolves.
|
|
220
|
-
let _rootCacheKey;
|
|
221
|
-
let _rootCacheVal;
|
|
222
|
-
/** The full resolution — root, reason and (when the walk found nothing
|
|
223
|
-
* adoptable) which bare `.monomind` ancestor it ignored. `doctor` uses this
|
|
224
|
-
* to disclose which directory a user's brain is keyed to and why, instead
|
|
225
|
-
* of a plausible-but-silent path (o-16). See `getProjectRoot` for the
|
|
226
|
-
* string-only form the other ~28 call sites use. */
|
|
227
|
-
export function getProjectRootResolution(from = process.env.MONOMIND_CWD || process.cwd()) {
|
|
228
|
-
const start = path.resolve(from);
|
|
229
|
-
// Cache key must cover everything walkToProjectRoot's result depends on,
|
|
230
|
-
// not just `start` — MONOMIND_PROJECT_ROOT short-circuits the walk (o-16),
|
|
231
|
-
// so a cache keyed on `start` alone would replay a stale pre-anchor (or
|
|
232
|
-
// stale different-anchor) result for the same cwd once the env var changes
|
|
233
|
-
// mid-process, silently defeating the escape hatch it's supposed to be.
|
|
234
|
-
const cacheKey = `${start}\n${process.env.MONOMIND_PROJECT_ROOT ?? ''}`;
|
|
235
|
-
if (cacheKey === _rootCacheKey && _rootCacheVal !== undefined)
|
|
236
|
-
return _rootCacheVal;
|
|
237
|
-
const resolved = walkToProjectRoot(start);
|
|
238
|
-
_rootCacheKey = cacheKey;
|
|
239
|
-
_rootCacheVal = resolved;
|
|
240
|
-
return resolved;
|
|
241
|
-
}
|
|
242
|
-
/** The directory that identifies "this project" for every Second Brain store.
|
|
243
|
-
*
|
|
244
|
-
* Deliberately NOT the raw cwd: keying on cwd forked the brain per directory —
|
|
245
|
-
* `doc ingest ./docs` from a package subdir wrote to a different store, and a
|
|
246
|
-
* different metadata file, than the identical command at the repo root, and
|
|
247
|
-
* neither could see the other. We walk up to the nearest ancestor carrying a
|
|
248
|
-
* `.git` marker (any depth), or a `.monomind` marker corroborated by an
|
|
249
|
-
* independent project marker, so every directory inside one project resolves
|
|
250
|
-
* to one brain. Nested projects still win (the walk stops at the FIRST
|
|
251
|
-
* eligible marker), which keeps worktrees and vendored sub-repos independent.
|
|
252
|
-
*
|
|
253
|
-
* o-16: a BARE `.monomind` ancestor (no `.git`, no manifest) is NOT adopted.
|
|
254
|
-
* `.monomind` is created by monomind itself as a side effect of running
|
|
255
|
-
* anywhere — it is not independent evidence of a project, and a wrong
|
|
256
|
-
* resolution creates the very marker that would capture every future
|
|
257
|
-
* descendant if it were trusted alone. See `walkToProjectRoot`'s comment for
|
|
258
|
-
* the measured incident this fixes. The starting directory's OWN `.monomind`
|
|
259
|
-
* is still trusted unconditionally — that is the user pointing at their own
|
|
260
|
-
* project — and `.git` at any depth is untouched, so CI/devcontainer repos
|
|
261
|
-
* and subdirectory unification behave exactly as before.
|
|
262
|
-
*
|
|
263
|
-
* The walk never crosses the home directory: a dotfiles repo at `~` would
|
|
264
|
-
* otherwise swallow every loose project underneath it into one shared brain.
|
|
265
|
-
* `MONOMIND_PROJECT_ROOT`, if set AND VALID (absolute, exists, is a
|
|
266
|
-
* directory, is not itself the filesystem root — see `validateAnchor`), is
|
|
267
|
-
* an explicit anchor that skips the walk entirely — the escape hatch for
|
|
268
|
-
* the genuinely ambiguous case (design decision 4 / AC-3c: a real, non-git
|
|
269
|
-
* project whose root has a bare `.monomind` and no manifest has no other
|
|
270
|
-
* way to be found from a subdirectory). An invalid anchor falls through to
|
|
271
|
-
* the walk rather than being adopted OR silently dropped — see
|
|
272
|
-
* `ProjectRootResolution.invalidAnchor`.
|
|
273
|
-
*
|
|
274
|
-
* This is `MONOMIND_PROJECT_ROOT`'s SECOND consumer in this codebase —
|
|
275
|
-
* `mcp-tools/guidance-tools.ts:findProjectRoot()` already reads the same
|
|
276
|
-
* env var, for a DIFFERENT marker (`.claude`, not `.monomind`/`.git`), with
|
|
277
|
-
* a stricter rule: `if (envRoot && existsSync(join(envRoot, '.claude')))`,
|
|
278
|
-
* i.e. it only honors the anchor when `.claude` exists there, silently
|
|
279
|
-
* falling through to its own walk otherwise. After this revision the two
|
|
280
|
-
* converge on ANCHOR HANDLING for the cases that matter most — a typo'd or
|
|
281
|
-
* missing path is rejected by both (this one via `validateAnchor`,
|
|
282
|
-
* guidance-tools' because `.claude` is absent) — but "converge" describes
|
|
283
|
-
* only whether each accepts or rejects the anchor, not the final resolved
|
|
284
|
-
* root: a rejected anchor falls through to each resolver's OWN walk
|
|
285
|
-
* (`walkToProjectRoot` here; guidance-tools' own Strategy 1/2/3 chain
|
|
286
|
-
* there), which can still land on different directories for the same
|
|
287
|
-
* unanchored cwd. Two residual disagreements remain, both legitimate rather
|
|
288
|
-
* than oversights:
|
|
289
|
-
* 1. An anchor that exists and is a real directory but has no `.claude` in
|
|
290
|
-
* it: this function honors it (memory has no reason to require a
|
|
291
|
-
* `.claude` folder — a project's knowledge store isn't gated on whether
|
|
292
|
-
* an agent config lives there), guidance-tools does not (`.claude` is
|
|
293
|
-
* the one thing it's searching FOR, so its absence is a real signal, not
|
|
294
|
-
* noise, for that consumer specifically).
|
|
295
|
-
* 2. A RELATIVE anchor: `validateAnchor` rejects it outright (an anchor
|
|
296
|
-
* that silently depends on cwd defeats the point of an anchor).
|
|
297
|
-
* guidance-tools has no absolute-path check, so `existsSync(join(envRoot,
|
|
298
|
-
* '.claude'))` would resolve a relative `envRoot` against cwd and could
|
|
299
|
-
* accept it — narrower in practice (needs a `.claude` at that resolved
|
|
300
|
-
* location too) but a real gap in the two resolvers' shared assumptions.
|
|
301
|
-
* Reconciling the two resolvers' semantics into one shared rule is a
|
|
302
|
-
* separate, larger question (tracked as o-32), not something this fix
|
|
303
|
-
* should decide as a side effect.
|
|
304
|
-
*
|
|
305
|
-
* For anyone who already ran from the project root — the normal case — the
|
|
306
|
-
* resolved path is identical to before, so their store does not move.
|
|
307
|
-
*
|
|
308
|
-
* `MONOMIND_CWD` wins over the real cwd, matching `getProjectCwd()` in
|
|
309
|
-
* mcp-tools/types.ts — an MCP server is launched with whatever cwd the client
|
|
310
|
-
* chose, and that env var is already how monograph and swarm state learn which
|
|
311
|
-
* project they belong to. Inlined rather than imported to keep this module on
|
|
312
|
-
* node builtins only (see the static import in document-pipeline.ts).
|
|
313
|
-
*/
|
|
314
|
-
export function getProjectRoot(from = process.env.MONOMIND_CWD || process.cwd()) {
|
|
315
|
-
return getProjectRootResolution(from).root;
|
|
316
|
-
}
|
|
317
|
-
function projectDataDir() {
|
|
318
|
-
const resolved = path.resolve(getProjectRoot());
|
|
319
|
-
const hash = crypto.createHash('sha256').update(resolved).digest('hex').slice(0, 16);
|
|
320
|
-
const readable = path
|
|
321
|
-
.basename(resolved)
|
|
322
|
-
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
323
|
-
.slice(0, 40) || 'project';
|
|
324
|
-
return path.join(os.homedir(), '.monomind', 'projects', `${readable}-${hash}`);
|
|
325
|
-
}
|
|
326
|
-
/** Resolve symlinks so the traversal check below can't be bypassed by a link
|
|
327
|
-
* that lexically resolves inside the allowed trees but points outside them. */
|
|
328
|
-
function realOrResolved(p) {
|
|
329
|
-
try {
|
|
330
|
-
return fs.realpathSync(p);
|
|
331
|
-
}
|
|
332
|
-
catch (e) {
|
|
333
|
-
logBridgeError('realOrResolved', e);
|
|
334
|
-
return p;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
/** The personal, cross-project knowledge store. Deliberately a SIBLING of
|
|
338
|
-
* ~/.monomind/projects (never inside it) so per-project pruning heuristics
|
|
339
|
-
* (`cleanup --data`) can never touch it. Env-overridable for tests and for
|
|
340
|
-
* users who keep their brain on a synced/external location. Resolved lazily
|
|
341
|
-
* so the override works regardless of import order. */
|
|
342
|
-
export function getGlobalBrainDir() {
|
|
343
|
-
return (process.env.MONOMIND_GLOBAL_BRAIN_DIR || path.join(os.homedir(), '.monomind', 'global-brain'));
|
|
344
|
-
}
|
|
345
|
-
/** Sentinel callers pass as dbPath to address the global brain. */
|
|
346
|
-
export const GLOBAL_BRAIN = '@global';
|
|
347
|
-
function getDbPath(customPath) {
|
|
348
|
-
const defaultDir = path.join(projectDataDir(), 'lancedb');
|
|
349
|
-
if (!customPath || customPath === ':memory:')
|
|
350
|
-
return defaultDir;
|
|
351
|
-
if (customPath === GLOBAL_BRAIN)
|
|
352
|
-
return getGlobalBrainDir();
|
|
353
|
-
// Treat legacy .db paths (and the legacy .swarm dir) as a signal to use the default
|
|
354
|
-
if (customPath.endsWith('.db'))
|
|
355
|
-
return defaultDir;
|
|
356
|
-
const resolved = realOrResolved(path.resolve(customPath));
|
|
357
|
-
// Guard against path traversal from MCP inputs: only allow paths inside the
|
|
358
|
-
// project, the per-project home data dir, or the global brain.
|
|
359
|
-
//
|
|
360
|
-
// STATED LIMIT (o-16 revision 2, corrected in revision 3 — verifier found
|
|
361
|
-
// the first version of this comment named the wrong path): `validateAnchor`
|
|
362
|
-
// resolves a `MONOMIND_PROJECT_ROOT` anchor to its REAL path ONCE via
|
|
363
|
-
// `fs.realpathSync`, and caches THAT REAL PATH STRING in `_rootCacheVal`
|
|
364
|
-
// (see above) — `getProjectRoot()` returns this cached real-path string on
|
|
365
|
-
// every subsequent call in this process; it does not re-read the anchor
|
|
366
|
-
// or re-run `validateAnchor`. So swapping the filesystem entry AT THE
|
|
367
|
-
// ANCHOR PATH after validation does nothing — the anchor itself is never
|
|
368
|
-
// consulted again. The actual gap is one level further in: the line below
|
|
369
|
-
// calls `realOrResolved()` (a fresh `fs.realpathSync`) on that cached
|
|
370
|
-
// REAL-PATH STRING every time `getDbPath` runs. If the filesystem entry
|
|
371
|
-
// AT THAT RESOLVED TARGET (not the anchor) is swapped for a symlink to
|
|
372
|
-
// `/` after validation but before a later call here, this re-resolves to
|
|
373
|
-
// the new real target live, and the guard is bypassed for that call — a
|
|
374
|
-
// TOCTOU gap between a one-time resolution and a re-resolved-every-call
|
|
375
|
-
// consumption of the same path string. NOT defended against: closing it
|
|
376
|
-
// properly means validating and consuming a single resolved handle rather
|
|
377
|
-
// than a path string, which is a real design change and out of scope for
|
|
378
|
-
// this fix. Accepted because it needs filesystem write access at the
|
|
379
|
-
// resolved target's path, timed against a live process — an attacker
|
|
380
|
-
// with that capability already has easier routes than this guard.
|
|
381
|
-
const relCwd = path.relative(realOrResolved(getProjectRoot()), resolved);
|
|
382
|
-
const relHome = path.relative(realOrResolved(projectDataDir()), resolved);
|
|
383
|
-
const relGlobal = path.relative(realOrResolved(getGlobalBrainDir()), resolved);
|
|
384
|
-
if (!relCwd.startsWith('..') && !path.isAbsolute(relCwd))
|
|
385
|
-
return resolved;
|
|
386
|
-
if (!relHome.startsWith('..') && !path.isAbsolute(relHome))
|
|
387
|
-
return resolved;
|
|
388
|
-
if (!relGlobal.startsWith('..') && !path.isAbsolute(relGlobal))
|
|
389
|
-
return resolved;
|
|
390
|
-
return defaultDir;
|
|
391
|
-
}
|
|
392
|
-
/** Resolve the real on-disk SQLite data-dir path for a given custom path (or the
|
|
393
|
-
* default) — the dir is still named `lancedb` for legacy path back-compat. */
|
|
394
|
-
export function bridgeGetDbPath(customPath) {
|
|
395
|
-
return getDbPath(customPath);
|
|
396
|
-
}
|
|
397
|
-
function getAutomemConfig() {
|
|
398
|
-
const defaults = { dedupThreshold: 0.85, staleDays: 7, feedbackInfluence: 0.2 };
|
|
399
|
-
try {
|
|
400
|
-
const configPath = path.join(process.cwd(), '.monomind', 'automem-config.json');
|
|
401
|
-
if (!fs.existsSync(configPath))
|
|
402
|
-
return defaults;
|
|
403
|
-
const stat = fs.statSync(configPath);
|
|
404
|
-
if (stat.size > 64 * 1024)
|
|
405
|
-
return defaults;
|
|
406
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
407
|
-
return {
|
|
408
|
-
dedupThreshold: typeof config?.scaffold?.dedupThreshold === 'number'
|
|
409
|
-
? config.scaffold.dedupThreshold
|
|
410
|
-
: defaults.dedupThreshold,
|
|
411
|
-
staleDays: typeof config?.scaffold?.staleDays === 'number'
|
|
412
|
-
? config.scaffold.staleDays
|
|
413
|
-
: defaults.staleDays,
|
|
414
|
-
feedbackInfluence: typeof config?.scaffold?.feedbackInfluence === 'number'
|
|
415
|
-
? Math.max(0, Math.min(1, config.scaffold.feedbackInfluence))
|
|
416
|
-
: defaults.feedbackInfluence,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
catch (e) {
|
|
420
|
-
logBridgeError('loadBridgeConfig', e);
|
|
421
|
-
return defaults;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
// ===== Usage/feedback weights (cognee-style, stored in entry metadata) =====
|
|
425
|
-
//
|
|
426
|
-
// feedback_weight (0..1, default 0.5): EWMA of explicit/auto ratings applied to
|
|
427
|
-
// the entries actually used to produce an answer. frequency_weight (>=0): how
|
|
428
|
-
// often the entry was returned by a search. Both live in the entry's metadata
|
|
429
|
-
// JSON — deliberately NOT backend schema columns, so no @monoes/memory publish
|
|
430
|
-
// is needed and both backends work unchanged.
|
|
431
|
-
const DEFAULT_FEEDBACK_WEIGHT = 0.5;
|
|
432
|
-
const FEEDBACK_EWMA_ALPHA = 0.1;
|
|
433
|
-
/** frequency_weight normalization ceiling: 10+ uses counts as fully reinforced. */
|
|
434
|
-
const FREQUENCY_NORM_CAP = 10;
|
|
435
|
-
function entryWeights(metadata) {
|
|
436
|
-
const md = (metadata ?? {});
|
|
437
|
-
const fw = typeof md.feedback_weight === 'number' && Number.isFinite(md.feedback_weight)
|
|
438
|
-
? Math.max(0, Math.min(1, md.feedback_weight))
|
|
439
|
-
: DEFAULT_FEEDBACK_WEIGHT;
|
|
440
|
-
const freq = typeof md.frequency_weight === 'number' && Number.isFinite(md.frequency_weight)
|
|
441
|
-
? Math.max(0, md.frequency_weight)
|
|
442
|
-
: 0;
|
|
443
|
-
return { feedback: fw, frequency: freq };
|
|
444
|
-
}
|
|
445
|
-
/** Blend learned usefulness into a GENUINE embedding-similarity score.
|
|
446
|
-
* Cognee guard: never applied to keyword-fallback scores — those carry no
|
|
447
|
-
* real relevance signal, and blending there lets a high-feedback stale entry
|
|
448
|
-
* outrank relevant matches and self-reinforce (rich-get-richer). */
|
|
449
|
-
function blendScore(cosineSim, weights, influence) {
|
|
450
|
-
if (influence <= 0)
|
|
451
|
-
return cosineSim;
|
|
452
|
-
const usefulness = 0.7 * weights.feedback + 0.3 * Math.min(1, weights.frequency / FREQUENCY_NORM_CAP);
|
|
453
|
-
return (1 - influence) * cosineSim + influence * usefulness;
|
|
454
|
-
}
|
|
455
|
-
function generateId(prefix) {
|
|
456
|
-
return `${prefix}_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
|
|
457
|
-
}
|
|
458
|
-
const backendSlots = new Map();
|
|
459
|
-
const MAX_BACKEND_SLOTS = 5;
|
|
460
|
-
let _embedder = null;
|
|
461
|
-
let _embedderPromise = null;
|
|
462
|
-
const MAX_INIT_ATTEMPTS = 3;
|
|
463
|
-
// Process-local kill switch for both local ONNX models (embedder + reranker):
|
|
464
|
-
// the in-process equivalent of MONOMIND_NO_LOCAL_EMBEDDINGS=1 plus
|
|
465
|
-
// MONOMIND_RERANKER=0. Org runs flip this instead of setting those env vars,
|
|
466
|
-
// because process.env is inherited by every child — each role's CLI and every
|
|
467
|
-
// command a role runs through Bash — which silently degraded a role's own
|
|
468
|
-
// `monomind memory search` to keyword-only (#249).
|
|
469
|
-
let _localModelsDisabled = false;
|
|
470
|
-
/** Disable local model loads for THIS process only (see `org run`/`org serve`). */
|
|
471
|
-
export function disableLocalModels() {
|
|
472
|
-
_localModelsDisabled = true;
|
|
473
|
-
}
|
|
474
|
-
export function localEmbeddingsDisabled() {
|
|
475
|
-
return _localModelsDisabled || process.env.MONOMIND_NO_LOCAL_EMBEDDINGS === '1';
|
|
476
|
-
}
|
|
477
|
-
export function rerankerDisabled() {
|
|
478
|
-
return _localModelsDisabled || process.env.MONOMIND_RERANKER === '0';
|
|
479
|
-
}
|
|
480
|
-
// ===== Lazy cross-encoder reranker (ettin-32m) =====
|
|
481
|
-
//
|
|
482
|
-
// Same ORT constraints as the embedder (ADR-R001). Loaded only when the first
|
|
483
|
-
// search with >1 candidate completes — never on store, never on startup.
|
|
484
|
-
// Disabled with MONOMIND_RERANKER=0.
|
|
485
|
-
//
|
|
486
|
-
// The upstream HF ONNX file for ettin-reranker-32m-v1 only contains the base
|
|
487
|
-
// ModernBERT encoder (outputs last_hidden_state, no logits). The classifier
|
|
488
|
-
// head lives in separate sentence-transformers module safetensors files
|
|
489
|
-
// (2_Dense, 3_LayerNorm, 4_Dense). Two ways to score, in order of preference:
|
|
490
|
-
// - a self-exported ONNX with the head baked in (scripts/export-ettin-onnx.py,
|
|
491
|
-
// needs PyTorch) under ~/.monomind/models/ettin-reranker-32m-v1-onnx/
|
|
492
|
-
// - the upstream encoder plus the head applied in JS (reranker-head.ts), from
|
|
493
|
-
// head files fetched by `doc eval --provision-model` into
|
|
494
|
-
// ~/.monomind/models/ettin-reranker-32m-v1-head/
|
|
495
|
-
// With neither, the reranker does not load: the upstream encoder alone cannot
|
|
496
|
-
// score, and loading it anyway ran the model on every search for nothing.
|
|
497
|
-
export const BRIDGE_RERANKER_MODEL = 'cross-encoder/ettin-reranker-32m-v1';
|
|
498
|
-
/** Where reranker export / head files live. */
|
|
499
|
-
export function rerankerModelsDir() {
|
|
500
|
-
return path.join(os.homedir(), '.monomind', 'models');
|
|
501
|
-
}
|
|
502
|
-
/** How the reranker is scoring, or null when it is not loaded. */
|
|
503
|
-
let _rerankerKind = null;
|
|
504
|
-
export function rerankerKind() {
|
|
505
|
-
return _rerankerKind;
|
|
506
|
-
}
|
|
507
|
-
let _reranker = null;
|
|
508
|
-
let _rerankerPromise = null;
|
|
509
|
-
/** Pre-load the cross-encoder reranker model. Idempotent, no-op when
|
|
510
|
-
* MONOMIND_RERANKER=0. Exported so the eval harness can force-load before
|
|
511
|
-
* the network guard blocks model downloads. */
|
|
512
|
-
export async function loadReranker() {
|
|
513
|
-
if (_reranker)
|
|
514
|
-
return;
|
|
515
|
-
if (rerankerDisabled())
|
|
516
|
-
return;
|
|
517
|
-
if (!_rerankerPromise) {
|
|
518
|
-
_rerankerPromise = (async () => {
|
|
519
|
-
try {
|
|
520
|
-
const source = resolveRerankerSource(rerankerModelsDir());
|
|
521
|
-
if (!source)
|
|
522
|
-
return; // nothing that can score — stay unloaded (no per-search cost)
|
|
523
|
-
const hf = await import('@huggingface/transformers');
|
|
524
|
-
const opts = { local_files_only: true };
|
|
525
|
-
const sigmoid = (logit) => 1 / (1 + Math.exp(-logit));
|
|
526
|
-
if (source.kind === 'export') {
|
|
527
|
-
const tokenizer = await hf.AutoTokenizer.from_pretrained(source.dir, opts);
|
|
528
|
-
const model = await hf.AutoModelForSequenceClassification.from_pretrained(source.dir, opts);
|
|
529
|
-
_reranker = async (query, passage) => {
|
|
530
|
-
const inputs = await tokenizer(query, {
|
|
531
|
-
text_pair: passage,
|
|
532
|
-
padding: true,
|
|
533
|
-
truncation: true,
|
|
534
|
-
});
|
|
535
|
-
const output = await model(inputs);
|
|
536
|
-
// num_labels=1 → [1,1] regression score, apply sigmoid
|
|
537
|
-
return sigmoid(output.logits.data[0]);
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
const head = loadEttinHead(source.dir);
|
|
542
|
-
const tokenizer = await hf.AutoTokenizer.from_pretrained(BRIDGE_RERANKER_MODEL, opts);
|
|
543
|
-
const model = await hf.AutoModel.from_pretrained(BRIDGE_RERANKER_MODEL, {
|
|
544
|
-
...opts,
|
|
545
|
-
dtype: 'fp32',
|
|
546
|
-
});
|
|
547
|
-
_reranker = async (query, passage) => {
|
|
548
|
-
const inputs = await tokenizer(query, {
|
|
549
|
-
text_pair: passage,
|
|
550
|
-
padding: true,
|
|
551
|
-
truncation: true,
|
|
552
|
-
});
|
|
553
|
-
const output = await model(inputs);
|
|
554
|
-
return sigmoid(ettinHeadLogit(head, output.last_hidden_state.data));
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
_rerankerKind = source.kind;
|
|
558
|
-
}
|
|
559
|
-
catch (e) {
|
|
560
|
-
_rerankerPromise = null; // allow retry
|
|
561
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
562
|
-
console.error('[memory-bridge] reranker failed to load:', e);
|
|
563
|
-
}
|
|
564
|
-
})();
|
|
565
|
-
}
|
|
566
|
-
await _rerankerPromise;
|
|
567
|
-
}
|
|
568
|
-
/** Rerank an array of results using the cross-encoder. Mutates nothing; returns
|
|
569
|
-
* a new sorted array with reranker scores in provenance. */
|
|
570
|
-
async function rerankResults(query, results, limit) {
|
|
571
|
-
const reranker = _reranker;
|
|
572
|
-
if (!reranker || results.length <= 1)
|
|
573
|
-
return { reranked: results, applied: false };
|
|
574
|
-
try {
|
|
575
|
-
const scored = await Promise.all(results.map(async (r) => {
|
|
576
|
-
const rerankerScore = await reranker(query, r.content || '');
|
|
577
|
-
return {
|
|
578
|
-
...r,
|
|
579
|
-
score: rerankerScore,
|
|
580
|
-
provenance: `${r.provenance ?? ''}→rerank:${rerankerScore.toFixed(3)}`,
|
|
581
|
-
};
|
|
582
|
-
}));
|
|
583
|
-
scored.sort((a, b) => b.score - a.score);
|
|
584
|
-
return { reranked: scored.slice(0, limit), applied: true };
|
|
585
|
-
}
|
|
586
|
-
catch (e) {
|
|
587
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
588
|
-
console.error('[memory-bridge] reranking failed — returning original order:', e);
|
|
589
|
-
return { reranked: results, applied: false };
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
/** Flush after mutations: the sql.js fallback backend is in-memory WASM and
|
|
593
|
-
* only reaches disk via persist(); the CLI process is short-lived, so waiting
|
|
594
|
-
* for an auto-persist interval would lose writes. No-op on better-sqlite3. */
|
|
595
|
-
async function flushBackend(backend) {
|
|
596
|
-
try {
|
|
597
|
-
await backend?.persist?.();
|
|
598
|
-
}
|
|
599
|
-
catch (e) {
|
|
600
|
-
logBridgeError('flushBackend', e); /* best effort */
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
/** Loads the local embedding model.
|
|
604
|
-
*
|
|
605
|
-
* This is the single point where `onnxruntime-node` enters the CLI process
|
|
606
|
-
* (via @huggingface/transformers). Once it has run, the process is subject to
|
|
607
|
-
* docs/adrs/ADR-R001-onnxruntime-process-teardown.md: calling process.exit()
|
|
608
|
-
* will abort with SIGABRT ("mutex lock failed") instead of exiting cleanly,
|
|
609
|
-
* and disposing the pipeline first does not help.
|
|
610
|
-
*
|
|
611
|
-
* Anything that reaches this — `doctor`, `memory store`, `memory search`, the
|
|
612
|
-
* MCP memory tools — inherits that constraint, which is why it bit commands
|
|
613
|
-
* that look nothing like ML work. Adding a new caller is fine; adding a new
|
|
614
|
-
* process-exit path is not. */
|
|
615
|
-
async function loadEmbedder() {
|
|
616
|
-
if (_embedder)
|
|
617
|
-
return;
|
|
618
|
-
// MONOMIND_NO_LOCAL_EMBEDDINGS: skip the native pipeline() load entirely.
|
|
619
|
-
// On some machines @huggingface/transformers' ONNX runtime crashes the
|
|
620
|
-
// whole process with a native `libc++abi terminate` (a mutex failure
|
|
621
|
-
// inside the tokenizer backend) rather than throwing a catchable JS
|
|
622
|
-
// error — the try/catch below only protects against normal failures
|
|
623
|
-
// (missing cache, bad revision, offline), not that. Everything that
|
|
624
|
-
// reads _embedder already tolerates it being unset (keyword-only
|
|
625
|
-
// fallback), so skipping the call is the only way to actually avoid the
|
|
626
|
-
// crash rather than just failing to catch it. Org runs disable it
|
|
627
|
-
// in-process (disableLocalModels()) since a crashed org is much worse than
|
|
628
|
-
// degraded search; anyone else can opt in with the env var.
|
|
629
|
-
if (localEmbeddingsDisabled())
|
|
630
|
-
return;
|
|
631
|
-
if (!_embedderPromise) {
|
|
632
|
-
_embedderPromise = (async () => {
|
|
633
|
-
try {
|
|
634
|
-
const hf = await import('@huggingface/transformers');
|
|
635
|
-
// revision must be a git ref — 'main' is the HF default; 'default' 404s and
|
|
636
|
-
// silently killed embeddings (every search degraded to keyword matching)
|
|
637
|
-
// dtype pinned explicitly: transformers.js logs a "dtype not specified"
|
|
638
|
-
// warning to the console on every load otherwise (leaks into CLI output).
|
|
639
|
-
const extractor = await hf.pipeline('feature-extraction', BRIDGE_EMBEDDING_MODEL, {
|
|
640
|
-
revision: 'main',
|
|
641
|
-
dtype: 'q8',
|
|
642
|
-
local_files_only: true,
|
|
643
|
-
});
|
|
644
|
-
_embedder = async (text) => {
|
|
645
|
-
const output = await extractor(text, { pooling: 'cls', normalize: true });
|
|
646
|
-
return new Float32Array(output.data);
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
catch (e) {
|
|
650
|
-
_embedderPromise = null; // allow retry (e.g. first call offline)
|
|
651
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
652
|
-
console.error('[memory-bridge] embedding model failed to load — store and search without vectors:', e);
|
|
653
|
-
}
|
|
654
|
-
})();
|
|
655
|
-
}
|
|
656
|
-
await _embedderPromise;
|
|
657
|
-
}
|
|
658
|
-
/** Fetches the local embedding model into the transformers cache so later
|
|
659
|
-
* loadEmbedder() calls (local_files_only) find it. Same model, revision and
|
|
660
|
-
* dtype as loadEmbedder() — only the network is allowed here, which is why it
|
|
661
|
-
* is reserved for explicit opt-in steps (`init --with-embeddings`). Throws
|
|
662
|
-
* when the model cannot be fetched (offline, disabled, package missing);
|
|
663
|
-
* callers degrade. Loads onnxruntime, so ADR-R001 applies to the caller. */
|
|
664
|
-
export async function downloadEmbeddingModel() {
|
|
665
|
-
if (localEmbeddingsDisabled())
|
|
666
|
-
throw new Error('local embeddings are disabled (MONOMIND_NO_LOCAL_EMBEDDINGS=1)');
|
|
667
|
-
const hf = await import('@huggingface/transformers');
|
|
668
|
-
await hf.pipeline('feature-extraction', BRIDGE_EMBEDDING_MODEL, {
|
|
669
|
-
revision: 'main',
|
|
670
|
-
dtype: 'q8',
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
async function getBackend(dbPath) {
|
|
674
|
-
const dir = getDbPath(dbPath);
|
|
675
|
-
let slot = backendSlots.get(dir);
|
|
676
|
-
if (!slot) {
|
|
677
|
-
if (backendSlots.size >= MAX_BACKEND_SLOTS) {
|
|
678
|
-
const oldest = backendSlots.keys().next().value;
|
|
679
|
-
const evicted = backendSlots.get(oldest);
|
|
680
|
-
// shutdownBridge() below uses .shutdown() — that's the real method these
|
|
681
|
-
// backends expose. .close() doesn't exist on either backend class, so this
|
|
682
|
-
// resolved to undefined via the optional chain every time and never actually
|
|
683
|
-
// released the connection: every 6th distinct database path opened in this
|
|
684
|
-
// process leaked the oldest slot's connection for the process lifetime.
|
|
685
|
-
try {
|
|
686
|
-
await evicted?.instance?.shutdown?.();
|
|
687
|
-
}
|
|
688
|
-
catch (e) {
|
|
689
|
-
logBridgeError('getBackend.evictedShutdown', e); /* best effort */
|
|
690
|
-
}
|
|
691
|
-
backendSlots.delete(oldest);
|
|
692
|
-
}
|
|
693
|
-
slot = { promise: null, instance: null, available: null, attempts: 0 };
|
|
694
|
-
backendSlots.set(dir, slot);
|
|
695
|
-
}
|
|
696
|
-
if (slot.available === false)
|
|
697
|
-
return null;
|
|
698
|
-
if (slot.attempts >= MAX_INIT_ATTEMPTS) {
|
|
699
|
-
slot.available = false;
|
|
700
|
-
return null;
|
|
701
|
-
}
|
|
702
|
-
if (slot.instance)
|
|
703
|
-
return slot.instance;
|
|
704
|
-
if (!slot.promise) {
|
|
705
|
-
slot.promise = (async () => {
|
|
706
|
-
try {
|
|
707
|
-
const mod = await import('@monoes/memory');
|
|
708
|
-
await loadEmbedder();
|
|
709
|
-
// Local SQLite engine (LanceDB replaced 2026-07): better-sqlite3 when its
|
|
710
|
-
// native binding loads, sql.js (pure WASM) otherwise — both persist text
|
|
711
|
-
// AND embeddings, so vectors are always recomputable/derivable data.
|
|
712
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
713
|
-
// Origin marker: records which project this data dir belongs to, so
|
|
714
|
-
// `monomind cleanup --data` can verifiably prune dirs whose project
|
|
715
|
-
// no longer exists (the dir-name hash is one-way). Best-effort; never
|
|
716
|
-
// written for the global brain (it has no single origin project).
|
|
717
|
-
if (dir !== getGlobalBrainDir()) {
|
|
718
|
-
try {
|
|
719
|
-
// MUST be the same path projectDataDir() hashed into the slug. If
|
|
720
|
-
// this recorded the raw cwd, running any memory command from a
|
|
721
|
-
// package subdirectory would stamp that subdirectory into the
|
|
722
|
-
// project-root-keyed dir — and deleting the subdirectory later
|
|
723
|
-
// would make `cleanup --data` prune the WHOLE project's brain as
|
|
724
|
-
// orphaned.
|
|
725
|
-
const originFile = path.join(projectDataDir(), 'origin.json');
|
|
726
|
-
fs.writeFileSync(originFile, JSON.stringify({ path: getProjectRoot(), updatedAt: new Date().toISOString() }) +
|
|
727
|
-
'\n', 'utf-8');
|
|
728
|
-
}
|
|
729
|
-
catch (e) {
|
|
730
|
-
logBridgeError('getBackend.originWrite', e); /* non-fatal */
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
const cfg = {
|
|
734
|
-
databasePath: path.join(dir, 'memory.db'),
|
|
735
|
-
walMode: true,
|
|
736
|
-
optimize: true,
|
|
737
|
-
defaultNamespace: 'default',
|
|
738
|
-
embeddingGenerator: _embedder ?? undefined,
|
|
739
|
-
// R3: when the MCP server and a CLI hook subprocess hit the same
|
|
740
|
-
// memory.db at the same time, SQLite returns SQLITE_BUSY and the
|
|
741
|
-
// bridge silently no-ops. busy_timeout tells SQLite to wait up to
|
|
742
|
-
// 5s for a lock before giving up, which covers the normal handoff
|
|
743
|
-
// window. The native @monoes/memory backend reads this key.
|
|
744
|
-
busyTimeoutMs: 5000,
|
|
745
|
-
};
|
|
746
|
-
const origLog = console.log;
|
|
747
|
-
console.log = (...args) => {
|
|
748
|
-
const msg = String(args[0] ?? '');
|
|
749
|
-
if (msg.includes('Transformers.js') || msg.includes('Loading model'))
|
|
750
|
-
return;
|
|
751
|
-
origLog.apply(console, args);
|
|
752
|
-
};
|
|
753
|
-
let backend;
|
|
754
|
-
try {
|
|
755
|
-
try {
|
|
756
|
-
backend = new mod.SQLiteBackend(cfg);
|
|
757
|
-
await backend.initialize();
|
|
758
|
-
}
|
|
759
|
-
catch (e) {
|
|
760
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
761
|
-
console.error('[memory-bridge] better-sqlite3 unavailable — using sql.js backend:', e);
|
|
762
|
-
backend = new mod.SqlJsBackend(cfg);
|
|
763
|
-
await backend.initialize();
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
finally {
|
|
767
|
-
console.log = origLog;
|
|
768
|
-
}
|
|
769
|
-
slot.instance = backend;
|
|
770
|
-
slot.available = true;
|
|
771
|
-
return backend;
|
|
772
|
-
}
|
|
773
|
-
catch (e) {
|
|
774
|
-
slot.attempts++;
|
|
775
|
-
slot.promise = null;
|
|
776
|
-
if (slot.attempts >= MAX_INIT_ATTEMPTS)
|
|
777
|
-
slot.available = false;
|
|
778
|
-
logBridgeError('getBackend', e);
|
|
779
|
-
return null;
|
|
780
|
-
}
|
|
781
|
-
})();
|
|
782
|
-
}
|
|
783
|
-
return slot.promise;
|
|
784
|
-
}
|
|
785
|
-
// ===== Core CRUD =====
|
|
786
|
-
export async function bridgeStoreEntry(options) {
|
|
787
|
-
const backend = await getBackend(options.dbPath);
|
|
788
|
-
if (!backend)
|
|
789
|
-
return null;
|
|
790
|
-
try {
|
|
791
|
-
const key = typeof options.key === 'string' && options.key.length > BRIDGE_MAX_KEY_LEN
|
|
792
|
-
? options.key.slice(0, BRIDGE_MAX_KEY_LEN)
|
|
793
|
-
: options.key;
|
|
794
|
-
if (typeof options.value === 'string' && options.value.length > BRIDGE_MAX_VALUE_LEN) {
|
|
795
|
-
return {
|
|
796
|
-
success: false,
|
|
797
|
-
id: '',
|
|
798
|
-
error: `Value exceeds the ${BRIDGE_MAX_VALUE_LEN}-character cap (BRIDGE_MAX_VALUE_LEN = 16 KB); got ${options.value.length}. Split the content into smaller entries.`,
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
const value = options.value;
|
|
802
|
-
const namespace = options.namespace ?? 'default';
|
|
803
|
-
const tags = Array.isArray(options.tags)
|
|
804
|
-
? // src: tags carry the ingest source path for excerpt provenance — paths
|
|
805
|
-
// routinely exceed the general 64-char tag cap, so they get 512.
|
|
806
|
-
options.tags
|
|
807
|
-
.filter((t) => typeof t === 'string' &&
|
|
808
|
-
t.length > 0 &&
|
|
809
|
-
t.length <= (t.startsWith('src:') ? 512 : MAX_TAG_LEN))
|
|
810
|
-
.slice(0, MAX_TAGS)
|
|
811
|
-
: [];
|
|
812
|
-
const now = Date.now();
|
|
813
|
-
// Upsert resolves the EXISTING identity BEFORE minting anything: a
|
|
814
|
-
// re-ingest must update the row in place and keep the id the caller was
|
|
815
|
-
// already handed. The old store-new-then-delete-old order silently
|
|
816
|
-
// orphaned every outstanding reference — feedback against a
|
|
817
|
-
// previously-returned id then matched nothing and reported
|
|
818
|
-
// `success: true, applied: 0`, i.e. success while training nothing (K5).
|
|
819
|
-
let existing = null;
|
|
820
|
-
if (options.upsert) {
|
|
821
|
-
try {
|
|
822
|
-
existing = await backend.getByKey(namespace, key);
|
|
823
|
-
}
|
|
824
|
-
catch (e) {
|
|
825
|
-
logBridgeError('bridgeStoreEntry.upsertLookup', e); /* treat as no existing entry */
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
const id = existing?.id ?? generateId('entry');
|
|
829
|
-
// Generate embedding
|
|
830
|
-
let embedding;
|
|
831
|
-
let embeddingInfo;
|
|
832
|
-
if (options.generateEmbeddingFlag !== false && value.length > 0 && _embedder) {
|
|
833
|
-
try {
|
|
834
|
-
embedding = await _embedder(value);
|
|
835
|
-
embeddingInfo = { dimensions: embedding.length, model: BRIDGE_EMBEDDING_MODEL };
|
|
836
|
-
}
|
|
837
|
-
catch (e) {
|
|
838
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
839
|
-
console.error('[memory-bridge] embedding generation failed — storing entry without embedding:', e);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
const mod = await import('@monoes/memory');
|
|
843
|
-
const entry = mod.createDefaultEntry({
|
|
844
|
-
key,
|
|
845
|
-
content: value,
|
|
846
|
-
namespace,
|
|
847
|
-
tags,
|
|
848
|
-
// On a revision the stored metadata merges UNDER the caller's, so
|
|
849
|
-
// learned signal (feedback_weight, frequency_weight) survives while every
|
|
850
|
-
// field the caller states explicitly still wins. Same merge semantics the
|
|
851
|
-
// backend's own update() uses.
|
|
852
|
-
metadata: existing
|
|
853
|
-
? { ...(existing.metadata ?? {}), ...(options.metadata ?? {}) }
|
|
854
|
-
: options.metadata,
|
|
855
|
-
expiresAt: options.ttl ? now + options.ttl * 1000 : undefined,
|
|
856
|
-
});
|
|
857
|
-
// Override id and set embedding
|
|
858
|
-
entry.id = id;
|
|
859
|
-
if (embedding)
|
|
860
|
-
entry.embedding = embedding;
|
|
861
|
-
if (existing) {
|
|
862
|
-
// Carry the record's history forward. createdAt in particular anchors the
|
|
863
|
-
// entry in the backend's `created_at DESC` ordering: a revision keeps its
|
|
864
|
-
// position instead of jumping to the head of every scan.
|
|
865
|
-
entry.createdAt = existing.createdAt;
|
|
866
|
-
entry.updatedAt = now;
|
|
867
|
-
entry.version = (existing.version ?? 1) + 1;
|
|
868
|
-
entry.accessCount = existing.accessCount ?? 0;
|
|
869
|
-
entry.lastAccessedAt = existing.lastAccessedAt ?? now;
|
|
870
|
-
}
|
|
871
|
-
// Dedup gate: skip if a near-duplicate already exists IN THIS NAMESPACE —
|
|
872
|
-
// an unscoped search let a similar entry in some other namespace swallow
|
|
873
|
-
// the store entirely (returned duplicate:true, nothing written where asked).
|
|
874
|
-
const automemCfg = getAutomemConfig();
|
|
875
|
-
if (embedding && !options.upsert) {
|
|
876
|
-
try {
|
|
877
|
-
const similar = await backend.search(embedding, {
|
|
878
|
-
k: 1,
|
|
879
|
-
threshold: automemCfg.dedupThreshold,
|
|
880
|
-
filters: { type: 'exact', namespace },
|
|
881
|
-
});
|
|
882
|
-
if (similar.length > 0 && similar[0].score >= automemCfg.dedupThreshold) {
|
|
883
|
-
// Re-storing near-identical content is a usage signal: the fact keeps
|
|
884
|
-
// being worth remembering. Reinforce the surviving entry.
|
|
885
|
-
await recordUsageOnBackend(backend, [similar[0].entry.id]).catch(() => {
|
|
886
|
-
/* best effort */
|
|
887
|
-
});
|
|
888
|
-
return { success: true, id: similar[0].entry.id, duplicate: true };
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
catch (e) {
|
|
892
|
-
logBridgeError('bridgeStoreEntry.dedupSearch', e); /* non-fatal — store anyway */
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
// Compare-and-swap path (K5): a caller merging onto a row it read earlier
|
|
896
|
-
// (or asserting the row is still absent) asks for that check to be part of
|
|
897
|
-
// the write, atomically, instead of trusting its own stale read.
|
|
898
|
-
if (options.ifVersion !== undefined) {
|
|
899
|
-
if (options.ifVersion === 'absent') {
|
|
900
|
-
if (existing)
|
|
901
|
-
return {
|
|
902
|
-
success: false,
|
|
903
|
-
id: existing.id,
|
|
904
|
-
conflict: true,
|
|
905
|
-
error: 'ifVersion=absent but entry already exists',
|
|
906
|
-
};
|
|
907
|
-
const created = typeof backend.storeIfAbsent === 'function' ? await backend.storeIfAbsent(entry) : null;
|
|
908
|
-
if (created === null) {
|
|
909
|
-
// Backend predates storeIfAbsent (or is a test double) — fall back to
|
|
910
|
-
// the plain unconditional write rather than fail every caller.
|
|
911
|
-
await backend.store(entry);
|
|
912
|
-
}
|
|
913
|
-
else if (!created) {
|
|
914
|
-
return { success: false, id: '', conflict: true, error: 'entry created concurrently' };
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
else {
|
|
918
|
-
if (!existing)
|
|
919
|
-
return { success: false, id: '', conflict: true, error: 'entry no longer exists' };
|
|
920
|
-
const written = typeof backend.storeIfVersion === 'function'
|
|
921
|
-
? await backend.storeIfVersion(entry, options.ifVersion)
|
|
922
|
-
: null;
|
|
923
|
-
if (written === null) {
|
|
924
|
-
await backend.store(entry);
|
|
925
|
-
}
|
|
926
|
-
else if (!written) {
|
|
927
|
-
return {
|
|
928
|
-
success: false,
|
|
929
|
-
id,
|
|
930
|
-
conflict: true,
|
|
931
|
-
error: `version conflict: entry changed concurrently (expected version ${options.ifVersion})`,
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
await flushBackend(backend);
|
|
936
|
-
return { success: true, id, embedding: embeddingInfo };
|
|
937
|
-
}
|
|
938
|
-
// store() is INSERT OR REPLACE keyed on id, so reusing the existing id
|
|
939
|
-
// rewrites that row in place — there is no old row left to delete, and no
|
|
940
|
-
// window in which a failed store leaves the previous data destroyed.
|
|
941
|
-
await backend.store(entry);
|
|
942
|
-
await flushBackend(backend);
|
|
943
|
-
return { success: true, id, embedding: embeddingInfo };
|
|
944
|
-
}
|
|
945
|
-
catch (err) {
|
|
946
|
-
logBridgeError('bridgeStoreEntry', err);
|
|
947
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
948
|
-
return { success: false, id: '', error: message };
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
export async function bridgeSearchEntries(options) {
|
|
952
|
-
const backend = await getBackend(options.dbPath);
|
|
953
|
-
if (!backend)
|
|
954
|
-
return null;
|
|
955
|
-
try {
|
|
956
|
-
const { query: queryStr, limit = 10, threshold = 0.3 } = options;
|
|
957
|
-
// CLI callers pass 'all' as a no-filter sentinel — never treat it as a literal namespace
|
|
958
|
-
const namespace = options.namespace && options.namespace !== 'all' ? options.namespace : undefined;
|
|
959
|
-
const startTime = Date.now();
|
|
960
|
-
// ── Knowledge removal support (issue #106) ──────────────────────
|
|
961
|
-
// Pre-compute live document hashes for knowledge namespaces so we can
|
|
962
|
-
// (a) over-fetch to compensate for superseded entries being filtered,
|
|
963
|
-
// (b) filter them out after retrieval — ensuring removed documents
|
|
964
|
-
// are invisible to EVERY caller, not just searchKnowledge.
|
|
965
|
-
// Dynamic import breaks the circular dependency: document-pipeline
|
|
966
|
-
// imports getProjectRoot from this module.
|
|
967
|
-
let _knowledgeLive = null;
|
|
968
|
-
let _knowledgeHasMeta = false;
|
|
969
|
-
let _isSupersededKey = null;
|
|
970
|
-
const knowledgeFilterActive = namespace?.startsWith('knowledge:') && !options.includeSuperseded;
|
|
971
|
-
if (knowledgeFilterActive) {
|
|
972
|
-
try {
|
|
973
|
-
const dp = await import('../knowledge/document-pipeline.js');
|
|
974
|
-
const rootDir = options.dbPath === GLOBAL_BRAIN
|
|
975
|
-
? getGlobalBrainDir()
|
|
976
|
-
: (options.rootDir ?? getProjectRoot());
|
|
977
|
-
_knowledgeLive = dp.liveContentHashes(rootDir);
|
|
978
|
-
_knowledgeHasMeta = dp.hasKnowledgeMetadata(rootDir);
|
|
979
|
-
_isSupersededKey = dp.isSupersededKey;
|
|
980
|
-
}
|
|
981
|
-
catch (e) {
|
|
982
|
-
logBridgeError('bridgeSearchEntries.knowledgeFilter', e); /* non-fatal: skip filtering when pipeline is unavailable */
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
// Over-retrieve when the reranker is available: fetch more candidates so the
|
|
986
|
-
// cross-encoder can reshuffle them. The reranker trims back to `limit`.
|
|
987
|
-
// For knowledge namespaces, also over-fetch to compensate for superseded
|
|
988
|
-
// document versions that will be filtered out below.
|
|
989
|
-
const rerankerActive = !options.skipRerank && _reranker !== null && !rerankerDisabled();
|
|
990
|
-
const knowledgeLimit = _knowledgeLive && _knowledgeLive.size > 0
|
|
991
|
-
? Math.min(Math.max(limit * 20, limit), 300)
|
|
992
|
-
: limit;
|
|
993
|
-
const retrieveK = rerankerActive
|
|
994
|
-
? Math.min(knowledgeLimit * 3, Math.max(20, knowledgeLimit))
|
|
995
|
-
: knowledgeLimit;
|
|
996
|
-
let results = [];
|
|
997
|
-
let searchMethod = 'keyword';
|
|
998
|
-
// Reported to callers so "(semantic)" can never be printed over keyword hits.
|
|
999
|
-
// The two reasons for skipping the vector path are distinct and must not be
|
|
1000
|
-
// conflated: a healthy model given an empty query is not a missing model.
|
|
1001
|
-
let fallbackReason = !_embedder
|
|
1002
|
-
? 'no-embedding-model'
|
|
1003
|
-
: queryStr.length === 0
|
|
1004
|
-
? 'empty-query'
|
|
1005
|
-
: undefined;
|
|
1006
|
-
let semanticAttempted = false;
|
|
1007
|
-
if (_embedder && queryStr.length > 0) {
|
|
1008
|
-
semanticAttempted = true;
|
|
1009
|
-
try {
|
|
1010
|
-
const queryEmbedding = await _embedder(queryStr);
|
|
1011
|
-
const searchResults = await backend.search(queryEmbedding, {
|
|
1012
|
-
k: retrieveK,
|
|
1013
|
-
threshold,
|
|
1014
|
-
filters: namespace ? { type: 'exact', namespace } : undefined,
|
|
1015
|
-
});
|
|
1016
|
-
const { feedbackInfluence } = getAutomemConfig();
|
|
1017
|
-
results = searchResults
|
|
1018
|
-
.map((r) => {
|
|
1019
|
-
const weights = entryWeights(r.entry.metadata);
|
|
1020
|
-
// Blend only here (semantic path): r.score is a genuine cosine similarity.
|
|
1021
|
-
const blended = blendScore(r.score, weights, feedbackInfluence);
|
|
1022
|
-
return {
|
|
1023
|
-
id: r.entry.id,
|
|
1024
|
-
key: r.entry.key,
|
|
1025
|
-
content: capResultContent(r.entry.content || ''),
|
|
1026
|
-
score: blended,
|
|
1027
|
-
namespace: r.entry.namespace,
|
|
1028
|
-
provenance: `semantic:${r.score.toFixed(3)}${blended !== r.score ? `→${blended.toFixed(3)}` : ''}`,
|
|
1029
|
-
tags: r.entry.tags ?? [],
|
|
1030
|
-
_createdAt: r.entry.createdAt || 0,
|
|
1031
|
-
};
|
|
1032
|
-
})
|
|
1033
|
-
.sort((a, b) => b.score - a.score);
|
|
1034
|
-
searchMethod = 'semantic';
|
|
1035
|
-
fallbackReason = undefined;
|
|
1036
|
-
}
|
|
1037
|
-
catch (e) {
|
|
1038
|
-
// fall through to keyword search — but never claim this was semantic
|
|
1039
|
-
fallbackReason = 'embedding-failed';
|
|
1040
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
1041
|
-
console.error('[memory-bridge] semantic search failed — falling back to keyword matching:', e);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
// Keyword search — always runs (not just as a fallback).
|
|
1045
|
-
// Entries stored without embeddings are invisible to the vector path,
|
|
1046
|
-
// so keyword results are merged into semantic results (union, deduplicated
|
|
1047
|
-
// by key) to ensure every findable entry surfaces regardless of whether
|
|
1048
|
-
// it has an embedding. Semantic hits take priority on score.
|
|
1049
|
-
//
|
|
1050
|
-
// Issue #66: When the backend has FTS5, keyword matching runs inside
|
|
1051
|
-
// SQLite via MATCH — orders of magnitude faster than the old path that
|
|
1052
|
-
// loaded up to 50k rows and scanned them in JS. The JS fallback is
|
|
1053
|
-
// kept for sql.js WASM builds that lack the FTS5 extension.
|
|
1054
|
-
{
|
|
1055
|
-
const tokens = queryStr
|
|
1056
|
-
.toLowerCase()
|
|
1057
|
-
.split(/[^a-z0-9]+/)
|
|
1058
|
-
.filter((t) => t.length > 1);
|
|
1059
|
-
let keywordHits = [];
|
|
1060
|
-
if (tokens.length) {
|
|
1061
|
-
// ── FTS5 fast path ──────────────────────────────────────────
|
|
1062
|
-
const fts5Results = typeof backend.keywordSearch === 'function'
|
|
1063
|
-
? await backend.keywordSearch(queryStr, { namespace, limit }).catch(() => null)
|
|
1064
|
-
: null;
|
|
1065
|
-
if (fts5Results !== null && fts5Results.length > 0) {
|
|
1066
|
-
// FTS5 rank is negative (lower = better); normalise to 0–1 against
|
|
1067
|
-
// the BEST (largest-magnitude) result, not a hard floor of 1
|
|
1068
|
-
// (issue #224). BM25 IDF goes to zero/negative when a query term
|
|
1069
|
-
// appears in most or all of the matched rows — a small or
|
|
1070
|
-
// lexically-homogeneous result set (a duplicated FTS row for one
|
|
1071
|
-
// entry, per sql-schema.ts's `ensureFTS5Triggers`, was one way to
|
|
1072
|
-
// reach exactly this) — so a genuinely-best (or sole) match can
|
|
1073
|
-
// legitimately have |rank| < 1. A hard `Math.max(…,
|
|
1074
|
-
// 1)` floor then divides that down toward 0, displaying the
|
|
1075
|
-
// correct top match as ~0.00 instead of its best-available 1.0.
|
|
1076
|
-
// Mirrors the identical fix already applied to the JS BM25
|
|
1077
|
-
// fallback below (#126-review): only fall back to a floor of 1
|
|
1078
|
-
// when every rank is genuinely 0 (nothing to normalise against),
|
|
1079
|
-
// never merely because the raw magnitude is under 1.
|
|
1080
|
-
const rawMaxRank = Math.max(...fts5Results.map((r) => Math.abs(r.rank)));
|
|
1081
|
-
const maxRank = rawMaxRank > 0 ? rawMaxRank : 1;
|
|
1082
|
-
keywordHits = fts5Results.map((r) => {
|
|
1083
|
-
const score = rawMaxRank > 0 ? Math.abs(r.rank) / maxRank : 1;
|
|
1084
|
-
return {
|
|
1085
|
-
id: r.id,
|
|
1086
|
-
key: r.key,
|
|
1087
|
-
content: capResultContent(r.content || ''),
|
|
1088
|
-
score,
|
|
1089
|
-
namespace: r.namespace,
|
|
1090
|
-
provenance: `keyword-fts5:${score.toFixed(2)}`,
|
|
1091
|
-
tags: [],
|
|
1092
|
-
_createdAt: 0,
|
|
1093
|
-
};
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
else {
|
|
1097
|
-
// ── JS fallback (no FTS5 or empty FTS5 result) ────────────
|
|
1098
|
-
const entries = await backend.query({
|
|
1099
|
-
type: 'exact',
|
|
1100
|
-
...(namespace ? { namespace } : {}),
|
|
1101
|
-
limit: 50000,
|
|
1102
|
-
});
|
|
1103
|
-
// #126: Bm25Index.build() costs real time at scale (measured in
|
|
1104
|
-
// bm25-index.ts's own header: ~113ms/673 chunks, ~1.7s/12.5k
|
|
1105
|
-
// chunks) — this fallback can be handed up to 50,000 entries, so
|
|
1106
|
-
// building a fresh index on every call without a cap would make
|
|
1107
|
-
// large stores' searches slower, not better. Below the cap, BM25
|
|
1108
|
-
// (proper IDF weighting) replaces the naive token-overlap-fraction
|
|
1109
|
-
// scan; above it, the fast scan keeps running so latency never
|
|
1110
|
-
// regresses. MONOMIND_BM25=0 disables this arm entirely (mirrors
|
|
1111
|
-
// the MONOMIND_RERANKER kill-switch).
|
|
1112
|
-
const BM25_ENTRY_CAP = 1500;
|
|
1113
|
-
const bm25Enabled = (process.env.MONOMIND_BM25 ?? '1') !== '0';
|
|
1114
|
-
let bm25Ok = false;
|
|
1115
|
-
if (bm25Enabled && entries.length > 0 && entries.length <= BM25_ENTRY_CAP) {
|
|
1116
|
-
try {
|
|
1117
|
-
const { Bm25Index } = await import('./bm25-index.js');
|
|
1118
|
-
// #126-review: index by the entry's ARRAY POSITION, not e.key —
|
|
1119
|
-
// memory_entries only enforces UNIQUE(namespace, key), so a bare
|
|
1120
|
-
// key string can legitimately repeat across namespaces (e.g. two
|
|
1121
|
-
// agents each storing a 'summary' key in their own namespace).
|
|
1122
|
-
// Keying by e.key alone collided in that case: every BM25 hit
|
|
1123
|
-
// sharing that key string resolved to whichever entry happened
|
|
1124
|
-
// to be inserted last into the lookup map, silently returning
|
|
1125
|
-
// the wrong entry's id/content/namespace.
|
|
1126
|
-
const chunks = entries.map((e, i) => ({
|
|
1127
|
-
key: String(i),
|
|
1128
|
-
text: `${e.key || ''} ${e.content || ''}`,
|
|
1129
|
-
}));
|
|
1130
|
-
const idx = Bm25Index.build(chunks, () => false); // no superseded concept at this generic KV level — filtered later by callers that care
|
|
1131
|
-
const hits = idx.search(queryStr, limit);
|
|
1132
|
-
// #126-review: Math.max(..., 1) as a divide-by-zero guard also
|
|
1133
|
-
// silently floors the normalization divisor whenever every real
|
|
1134
|
-
// score is < 1 (common for small/sparse corpora — exactly the
|
|
1135
|
-
// regime this capped fallback runs in), so the top hit stopped
|
|
1136
|
-
// normalizing to 1.0 as the comment below claims. Only fall
|
|
1137
|
-
// back to 1 when there is no positive score to divide by.
|
|
1138
|
-
const rawMax = hits.length ? Math.max(...hits.map((h) => h.score)) : 0;
|
|
1139
|
-
const maxScore = rawMax > 0 ? rawMax : 1;
|
|
1140
|
-
keywordHits = hits.map((h) => {
|
|
1141
|
-
const e = entries[Number(h.key)];
|
|
1142
|
-
const normalized = h.score / maxScore; // BM25 scores aren't comparable across queries/corpora — normalise 0-1 like the FTS5 path does
|
|
1143
|
-
return {
|
|
1144
|
-
id: e.id,
|
|
1145
|
-
key: e.key,
|
|
1146
|
-
content: capResultContent(e.content || ''),
|
|
1147
|
-
score: normalized,
|
|
1148
|
-
namespace: e.namespace,
|
|
1149
|
-
provenance: `keyword-bm25:${normalized.toFixed(2)}`,
|
|
1150
|
-
tags: e.tags ?? [],
|
|
1151
|
-
_createdAt: e.createdAt || 0,
|
|
1152
|
-
};
|
|
1153
|
-
});
|
|
1154
|
-
bm25Ok = true;
|
|
1155
|
-
}
|
|
1156
|
-
catch (e) {
|
|
1157
|
-
// #126-review: BM25 build/search had no local try/catch, unlike
|
|
1158
|
-
// every other sub-path in this function — an exception here
|
|
1159
|
-
// used to propagate to the function's single outer catch,
|
|
1160
|
-
// discarding the whole call (including already-computed
|
|
1161
|
-
// semantic results) instead of degrading to the naive scan
|
|
1162
|
-
// below, which is what every other keyword-path failure does.
|
|
1163
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
1164
|
-
console.error('[memory-bridge] BM25 keyword search failed — falling back to token-overlap scan:', e);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
if (!bm25Ok) {
|
|
1168
|
-
keywordHits = entries
|
|
1169
|
-
.map((e) => {
|
|
1170
|
-
const haystack = `${e.key || ''} ${e.content || ''}`.toLowerCase();
|
|
1171
|
-
const hits = tokens.filter((t) => haystack.includes(t)).length;
|
|
1172
|
-
return { e, score: hits / tokens.length };
|
|
1173
|
-
})
|
|
1174
|
-
.filter((x) => x.score > 0)
|
|
1175
|
-
.sort((a, b) => b.score - a.score)
|
|
1176
|
-
.slice(0, limit)
|
|
1177
|
-
.map(({ e, score }) => ({
|
|
1178
|
-
id: e.id,
|
|
1179
|
-
key: e.key,
|
|
1180
|
-
content: capResultContent(e.content || ''),
|
|
1181
|
-
// Raw token-overlap fraction, NOT rescaled to look like a cosine.
|
|
1182
|
-
score,
|
|
1183
|
-
namespace: e.namespace,
|
|
1184
|
-
provenance: `keyword:${score.toFixed(2)}`,
|
|
1185
|
-
tags: e.tags ?? [],
|
|
1186
|
-
_createdAt: e.createdAt || 0,
|
|
1187
|
-
}));
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
// Issue #223/#224 follow-up: FTS5/BM25 ranks above are normalised
|
|
1191
|
-
// RELATIVE to the best result in this call's own small candidate set
|
|
1192
|
-
// (score = |rank| / maxRank), so the top — or sole — hit always
|
|
1193
|
-
// lands at ~1.0 by construction, no matter how weak the actual
|
|
1194
|
-
// match is. `options.threshold` compared against that already-
|
|
1195
|
-
// inflated score can never reject a top/sole hit, so a single
|
|
1196
|
-
// coincidental partial-token overlap looks exactly as confident as
|
|
1197
|
-
// a genuine strong match. Gate on independent evidence instead: how
|
|
1198
|
-
// much of the QUERY the candidate actually covers. This can only
|
|
1199
|
-
// narrow the result set (never rescue something already excluded),
|
|
1200
|
-
// and doesn't touch the rank-normalisation math other callers rely on.
|
|
1201
|
-
keywordHits = keywordHits.filter((h) => {
|
|
1202
|
-
if (h.score < threshold)
|
|
1203
|
-
return false;
|
|
1204
|
-
const haystack = `${h.key || ''} ${h.content || ''}`.toLowerCase();
|
|
1205
|
-
const matchedFraction = tokens.filter((t) => haystack.includes(t)).length / tokens.length;
|
|
1206
|
-
return matchedFraction >= threshold;
|
|
1207
|
-
});
|
|
1208
|
-
if (results.length === 0) {
|
|
1209
|
-
// No semantic results — keyword is all we have.
|
|
1210
|
-
results = keywordHits;
|
|
1211
|
-
searchMethod = semanticAttempted ? 'keyword-fallback' : 'keyword';
|
|
1212
|
-
if (semanticAttempted && !fallbackReason)
|
|
1213
|
-
fallbackReason = 'no-semantic-matches';
|
|
1214
|
-
}
|
|
1215
|
-
else {
|
|
1216
|
-
// Merge: union deduplicated by key, semantic wins on duplicates.
|
|
1217
|
-
// Extras are flagged _keywordOnly so the reranking step below can
|
|
1218
|
-
// skip them — the cross-encoder scores r.content, and an entry
|
|
1219
|
-
// findable ONLY by keyword (e.g. a placeholder/near-empty content
|
|
1220
|
-
// whose relevance lives in the key) reranks as noise and gets
|
|
1221
|
-
// sliced off by the final `limit`, silently defeating the whole
|
|
1222
|
-
// point of merging it in. Guaranteed inclusion has to survive
|
|
1223
|
-
// reranking, not just the merge.
|
|
1224
|
-
const seenKeys = new Set(results.map((r) => r.key));
|
|
1225
|
-
const extras = keywordHits.filter((kh) => !seenKeys.has(kh.key));
|
|
1226
|
-
if (extras.length) {
|
|
1227
|
-
results = [...results, ...extras.map((e) => ({ ...e, _keywordOnly: true }))];
|
|
1228
|
-
// searchMethod stays 'semantic' — the primary path succeeded;
|
|
1229
|
-
// keyword only supplemented entries that lacked embeddings.
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
else if (results.length === 0) {
|
|
1234
|
-
// Empty token list AND no semantic results — nothing to search.
|
|
1235
|
-
searchMethod = semanticAttempted ? 'keyword-fallback' : 'keyword';
|
|
1236
|
-
if (semanticAttempted && !fallbackReason)
|
|
1237
|
-
fallbackReason = 'no-semantic-matches';
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
// Filter stale entries based on automem config — skip for knowledge
|
|
1241
|
-
// namespaces (documents should remain searchable indefinitely)
|
|
1242
|
-
// Stale filtering is per-RESULT namespace (documents stay searchable
|
|
1243
|
-
// forever) — keying it on the query's namespace filter meant an
|
|
1244
|
-
// all-namespace search silently dropped knowledge:* results past the
|
|
1245
|
-
// stale cutoff.
|
|
1246
|
-
// org:* (cross-run org memory) and rules are durable learned state like
|
|
1247
|
-
// documents — the stale cliff silently erased org recall after a week.
|
|
1248
|
-
const durableNs = (ns) => ns.startsWith('knowledge:') ||
|
|
1249
|
-
ns.startsWith('org:') ||
|
|
1250
|
-
ns.startsWith('agent:') ||
|
|
1251
|
-
ns.startsWith('kg:') ||
|
|
1252
|
-
ns === 'rules';
|
|
1253
|
-
const isKnowledgeNs = namespace ? durableNs(namespace) : false;
|
|
1254
|
-
if (!isKnowledgeNs) {
|
|
1255
|
-
const { staleDays } = getAutomemConfig();
|
|
1256
|
-
const staleCutoff = Date.now() - staleDays * 86400000;
|
|
1257
|
-
results = results.filter((r) => durableNs(String(r.namespace ?? '')) || !r._createdAt || r._createdAt > staleCutoff);
|
|
1258
|
-
}
|
|
1259
|
-
results.forEach((r) => delete r._createdAt);
|
|
1260
|
-
// ── Knowledge superseded filtering (issue #106) ─────────────────
|
|
1261
|
-
// Remove document chunks whose content hash is no longer current
|
|
1262
|
-
// (i.e. the document was removed via `knowledge_remove` or replaced
|
|
1263
|
-
// by a newer ingest). This runs inside the bridge so every caller —
|
|
1264
|
-
// embeddings_search, CLI `memory search`, and searchKnowledge — gets
|
|
1265
|
-
// the same removal guarantee.
|
|
1266
|
-
if (_knowledgeLive && _isSupersededKey && results.length > 0) {
|
|
1267
|
-
results = results.filter((r) => !_isSupersededKey?.(String(r.key ?? ''), _knowledgeLive, _knowledgeHasMeta));
|
|
1268
|
-
// Trim back to the originally requested limit after overfetch — but
|
|
1269
|
-
// never let this blind size-based cut drop a _keywordOnly extra (see
|
|
1270
|
-
// the merge above): reserve its slot and trim the rest first.
|
|
1271
|
-
if (results.length > limit) {
|
|
1272
|
-
const extras = results.filter((r) => r._keywordOnly);
|
|
1273
|
-
const main = results.filter((r) => !r._keywordOnly);
|
|
1274
|
-
const keep = Math.max(0, limit - extras.length);
|
|
1275
|
-
results = [...main.slice(0, keep), ...extras.slice(0, limit)];
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
// Keyword-only extras are guaranteed to survive to the final result —
|
|
1279
|
-
// pull them out before reranking so the cross-encoder (which scores
|
|
1280
|
-
// r.content only) can't outrank them into oblivion, then reserve their
|
|
1281
|
-
// slots when re-merging below.
|
|
1282
|
-
const keywordOnlyResults = results.filter((r) => r._keywordOnly);
|
|
1283
|
-
let rerankPool = results.filter((r) => !r._keywordOnly);
|
|
1284
|
-
// ── Cross-encoder reranking ──────────────────────────────────────
|
|
1285
|
-
// Fires only when: reranker loaded, >1 result, not explicitly skipped.
|
|
1286
|
-
// Lazy-load on first qualifying search so startup stays fast.
|
|
1287
|
-
let reranked = false;
|
|
1288
|
-
if (!options.skipRerank && !rerankerDisabled() && rerankPool.length > 1) {
|
|
1289
|
-
if (!_reranker && !_rerankerPromise) {
|
|
1290
|
-
// First qualifying search — kick off the lazy load. This search
|
|
1291
|
-
// proceeds without reranking; the NEXT search will use it.
|
|
1292
|
-
loadReranker().catch(() => {
|
|
1293
|
-
/* swallowed — retry next time */
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
|
-
if (_reranker) {
|
|
1297
|
-
const rr = await rerankResults(queryStr, rerankPool, limit);
|
|
1298
|
-
rerankPool = rr.reranked;
|
|
1299
|
-
reranked = rr.applied;
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
if (keywordOnlyResults.length) {
|
|
1303
|
-
const keep = Math.max(0, limit - keywordOnlyResults.length);
|
|
1304
|
-
results = [...rerankPool.slice(0, keep), ...keywordOnlyResults.slice(0, limit)];
|
|
1305
|
-
}
|
|
1306
|
-
else {
|
|
1307
|
-
results = rerankPool;
|
|
1308
|
-
}
|
|
1309
|
-
results.forEach((r) => delete r._keywordOnly);
|
|
1310
|
-
return {
|
|
1311
|
-
success: true,
|
|
1312
|
-
results,
|
|
1313
|
-
searchTime: Date.now() - startTime,
|
|
1314
|
-
searchMethod,
|
|
1315
|
-
reranked,
|
|
1316
|
-
...(searchMethod === 'semantic' ? {} : { fallbackReason }),
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
catch (e) {
|
|
1320
|
-
logBridgeError('bridgeSearchEntries', e);
|
|
1321
|
-
return null;
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
export async function bridgeListEntries(options) {
|
|
1325
|
-
const backend = await getBackend(options.dbPath);
|
|
1326
|
-
if (!backend)
|
|
1327
|
-
return null;
|
|
1328
|
-
try {
|
|
1329
|
-
const entries = await backend.query({
|
|
1330
|
-
type: 'exact',
|
|
1331
|
-
// No namespace means "all namespaces" — the query builder (sql-backend.ts)
|
|
1332
|
-
// already skips its filter clause on a falsy namespace. Defaulting to the
|
|
1333
|
-
// literal string 'default' here (as this used to) overrode that legitimate
|
|
1334
|
-
// "no filter" signal and silently scoped every unfiltered list/search to a
|
|
1335
|
-
// namespace that's usually near-empty in practice.
|
|
1336
|
-
namespace: options.namespace,
|
|
1337
|
-
limit: options.limit ?? 100,
|
|
1338
|
-
offset: options.offset,
|
|
1339
|
-
});
|
|
1340
|
-
return {
|
|
1341
|
-
success: true,
|
|
1342
|
-
entries: entries.map((e) => ({
|
|
1343
|
-
id: e.id,
|
|
1344
|
-
key: e.key,
|
|
1345
|
-
namespace: e.namespace,
|
|
1346
|
-
content: e.content,
|
|
1347
|
-
accessCount: e.accessCount ?? 0,
|
|
1348
|
-
createdAt: new Date(e.createdAt).toISOString(),
|
|
1349
|
-
updatedAt: new Date(e.updatedAt).toISOString(),
|
|
1350
|
-
hasEmbedding: !!(e.embedding && e.embedding.length > 0),
|
|
1351
|
-
tags: e.tags ?? [],
|
|
1352
|
-
metadata: e.metadata ?? {},
|
|
1353
|
-
})),
|
|
1354
|
-
total: entries.length,
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
catch (e) {
|
|
1358
|
-
logBridgeError('bridgeListEntries', e);
|
|
1359
|
-
return null;
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
/** A real database count for one namespace — `SELECT COUNT(*) WHERE
|
|
1363
|
-
* namespace = ?` against the existing `idx_namespace` index — as opposed to
|
|
1364
|
-
* `bridgeListEntries.total`, which is only the returned page's length (K7:
|
|
1365
|
-
* "bridgeListEntries.total is the returned page length, not a database
|
|
1366
|
-
* count"). Every row in a KG namespace (`kg:nodes`, `kg:edges`, `rules`) is
|
|
1367
|
-
* exactly one node/edge/rule — the name/adjacency index namespaces are
|
|
1368
|
-
* separate — so this count needs no per-row filtering to be exact, and costs
|
|
1369
|
-
* one indexed query instead of paging the whole namespace.
|
|
1370
|
-
*
|
|
1371
|
-
* @returns null when the backend is unavailable, or when the loaded backend
|
|
1372
|
-
* predates `count()` — callers fall back to the paginated scan they already
|
|
1373
|
-
* had rather than fail. */
|
|
1374
|
-
export async function bridgeCountEntries(namespace, dbPath) {
|
|
1375
|
-
const backend = await getBackend(dbPath);
|
|
1376
|
-
if (!backend || typeof backend.count !== 'function')
|
|
1377
|
-
return null;
|
|
1378
|
-
try {
|
|
1379
|
-
return await backend.count(namespace);
|
|
1380
|
-
}
|
|
1381
|
-
catch (e) {
|
|
1382
|
-
logBridgeError('bridgeCountEntries', e);
|
|
1383
|
-
return null;
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
export async function bridgeGetEntry(options) {
|
|
1387
|
-
const backend = await getBackend(options.dbPath);
|
|
1388
|
-
if (!backend)
|
|
1389
|
-
return null;
|
|
1390
|
-
try {
|
|
1391
|
-
const { key, namespace = 'default' } = options;
|
|
1392
|
-
const entry = await backend.getByKey(namespace, key);
|
|
1393
|
-
if (!entry)
|
|
1394
|
-
return { success: true, found: false };
|
|
1395
|
-
return {
|
|
1396
|
-
success: true,
|
|
1397
|
-
found: true,
|
|
1398
|
-
entry: {
|
|
1399
|
-
id: entry.id,
|
|
1400
|
-
key: entry.key,
|
|
1401
|
-
namespace: entry.namespace,
|
|
1402
|
-
content: entry.content,
|
|
1403
|
-
accessCount: entry.accessCount ?? 0,
|
|
1404
|
-
createdAt: new Date(entry.createdAt).toISOString(),
|
|
1405
|
-
updatedAt: new Date(entry.updatedAt).toISOString(),
|
|
1406
|
-
hasEmbedding: !!(entry.embedding && entry.embedding.length > 0),
|
|
1407
|
-
tags: entry.tags ?? [],
|
|
1408
|
-
metadata: entry.metadata ?? {},
|
|
1409
|
-
...(typeof entry.version === 'number' ? { version: entry.version } : {}),
|
|
1410
|
-
},
|
|
1411
|
-
};
|
|
1412
|
-
}
|
|
1413
|
-
catch (e) {
|
|
1414
|
-
logBridgeError('bridgeGetEntry', e);
|
|
1415
|
-
return null;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
export async function bridgeDeleteEntry(options) {
|
|
1419
|
-
const backend = await getBackend(options.dbPath);
|
|
1420
|
-
if (!backend)
|
|
1421
|
-
return null;
|
|
1422
|
-
try {
|
|
1423
|
-
const namespace = options.namespace ?? 'default';
|
|
1424
|
-
let deleted = false;
|
|
1425
|
-
if (options.id) {
|
|
1426
|
-
deleted = await backend.delete(options.id);
|
|
1427
|
-
}
|
|
1428
|
-
else if (options.key) {
|
|
1429
|
-
const entry = await backend.getByKey(namespace, options.key);
|
|
1430
|
-
if (entry)
|
|
1431
|
-
deleted = await backend.delete(entry.id);
|
|
1432
|
-
}
|
|
1433
|
-
if (deleted)
|
|
1434
|
-
await flushBackend(backend);
|
|
1435
|
-
return { success: true, deleted };
|
|
1436
|
-
}
|
|
1437
|
-
catch (e) {
|
|
1438
|
-
logBridgeError('bridgeDeleteEntry', e);
|
|
1439
|
-
return { success: false, deleted: false };
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
// ===== Embeddings =====
|
|
1443
|
-
export async function bridgeGenerateEmbedding(text, dbPath) {
|
|
1444
|
-
await getBackend(dbPath); // ensure embedder is initialized
|
|
1445
|
-
if (!_embedder)
|
|
1446
|
-
return null;
|
|
1447
|
-
try {
|
|
1448
|
-
const emb = await _embedder(text);
|
|
1449
|
-
return { embedding: Array.from(emb), dimensions: emb.length, model: BRIDGE_EMBEDDING_MODEL };
|
|
1450
|
-
}
|
|
1451
|
-
catch (e) {
|
|
1452
|
-
logBridgeError('bridgeEmbedText', e);
|
|
1453
|
-
return null;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
export async function bridgeLoadEmbeddingModel(dbPath) {
|
|
1457
|
-
const startTime = Date.now();
|
|
1458
|
-
await getBackend(dbPath);
|
|
1459
|
-
if (!_embedder)
|
|
1460
|
-
return null;
|
|
1461
|
-
try {
|
|
1462
|
-
const test = await _embedder('test');
|
|
1463
|
-
if (!test)
|
|
1464
|
-
return null;
|
|
1465
|
-
return {
|
|
1466
|
-
success: true,
|
|
1467
|
-
dimensions: test.length,
|
|
1468
|
-
modelName: BRIDGE_EMBEDDING_MODEL,
|
|
1469
|
-
loadTime: Date.now() - startTime,
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
catch (e) {
|
|
1473
|
-
logBridgeError('bridgeLoadEmbeddingModel', e);
|
|
1474
|
-
return null;
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
export async function bridgeGetBackendStats(dbPath) {
|
|
1478
|
-
const backend = await getBackend(dbPath);
|
|
1479
|
-
if (!backend)
|
|
1480
|
-
return null;
|
|
1481
|
-
try {
|
|
1482
|
-
const stats = await backend.getStats();
|
|
1483
|
-
return {
|
|
1484
|
-
totalEntries: stats?.totalEntries ?? 0,
|
|
1485
|
-
entriesByNamespace: stats?.entriesByNamespace ?? {},
|
|
1486
|
-
memoryUsage: stats?.memoryUsage ?? 0,
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
catch (e) {
|
|
1490
|
-
logBridgeError('bridgeGetBackendStats', e);
|
|
1491
|
-
return null;
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
// ===== HNSW (real ANN status/build; search itself runs inside SqlBackend.search()) =====
|
|
1495
|
-
export async function bridgeAddToHNSW(options) {
|
|
1496
|
-
// The SQLite backend indexes entries automatically on store — this is a no-op
|
|
1497
|
-
const backend = await getBackend(options.dbPath);
|
|
1498
|
-
if (!backend)
|
|
1499
|
-
return null;
|
|
1500
|
-
try {
|
|
1501
|
-
const stats = await backend.getStats();
|
|
1502
|
-
return { success: true, indexSize: stats?.totalEntries ?? 0 };
|
|
1503
|
-
}
|
|
1504
|
-
catch (e) {
|
|
1505
|
-
logBridgeError('bridgeAddToHNSW', e);
|
|
1506
|
-
return { success: true };
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
/**
|
|
1510
|
-
* Real status for the ANN (HNSW) fast path inside SqlBackend.search() —
|
|
1511
|
-
* whether the corpus is big enough to use it, whether it's currently built,
|
|
1512
|
-
* and where its on-disk cache lives. Read-only; does not build anything.
|
|
1513
|
-
*/
|
|
1514
|
-
export async function bridgeGetHNSWStatus(dbPath) {
|
|
1515
|
-
const backend = await getBackend(dbPath);
|
|
1516
|
-
if (!backend || typeof backend.getAnnStatus !== 'function')
|
|
1517
|
-
return null;
|
|
1518
|
-
try {
|
|
1519
|
-
return { available: true, ...backend.getAnnStatus() };
|
|
1520
|
-
}
|
|
1521
|
-
catch (e) {
|
|
1522
|
-
logBridgeError('bridgeGetHNSWStatus', e);
|
|
1523
|
-
return null;
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
/**
|
|
1527
|
-
* Force-build (or reload from disk cache) the ANN index regardless of
|
|
1528
|
-
* MONOMIND_HNSW_THRESHOLD — the real implementation behind
|
|
1529
|
-
* `memory search --build-hnsw`.
|
|
1530
|
-
*/
|
|
1531
|
-
export async function bridgeForceBuildHNSW(dbPath) {
|
|
1532
|
-
const backend = await getBackend(dbPath);
|
|
1533
|
-
if (!backend || typeof backend.forceBuildAnnIndex !== 'function')
|
|
1534
|
-
return null;
|
|
1535
|
-
try {
|
|
1536
|
-
return await backend.forceBuildAnnIndex(BRIDGE_EMBEDDING_DIMS);
|
|
1537
|
-
}
|
|
1538
|
-
catch (e) {
|
|
1539
|
-
logBridgeError('bridgeForceBuildHNSW', e);
|
|
1540
|
-
return null;
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
// ===== Availability / lifecycle =====
|
|
1544
|
-
export async function isBridgeAvailable(dbPath) {
|
|
1545
|
-
const backend = await getBackend(dbPath);
|
|
1546
|
-
return !!backend;
|
|
1547
|
-
}
|
|
1548
|
-
export async function shutdownBridge() {
|
|
1549
|
-
for (const slot of backendSlots.values()) {
|
|
1550
|
-
if (slot.instance) {
|
|
1551
|
-
try {
|
|
1552
|
-
await slot.instance.shutdown();
|
|
1553
|
-
}
|
|
1554
|
-
catch (e) {
|
|
1555
|
-
logBridgeError('bridgeShutdown.slotShutdown', e); /* ignore */
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
backendSlots.clear();
|
|
1560
|
-
_embedder = null;
|
|
1561
|
-
_embedderPromise = null;
|
|
1562
|
-
_reranker = null;
|
|
1563
|
-
_rerankerPromise = null;
|
|
1564
|
-
}
|
|
12
|
+
import { flushBackend, getBackend } from './memory-bridge-backend.js';
|
|
13
|
+
import { capResultContent, entryWeights, generateId, logBridgeError, } from './memory-bridge-core.js';
|
|
14
|
+
import { bridgeDeleteEntry } from './memory-bridge-entries.js';
|
|
15
|
+
import { bridgeSearchEntries } from './memory-bridge-search.js';
|
|
16
|
+
import { bridgeStoreEntry } from './memory-bridge-store.js';
|
|
17
|
+
export { BRIDGE_RERANKER_MODEL, disableLocalModels, downloadEmbeddingModel, isBridgeAvailable, loadReranker, localEmbeddingsDisabled, rerankerDisabled, rerankerKind, rerankerModelsDir, shutdownBridge, } from './memory-bridge-backend.js';
|
|
18
|
+
export { BRIDGE_EMBEDDING_DIMS, BRIDGE_EMBEDDING_MODEL, safeParseEmbedding, } from './memory-bridge-core.js';
|
|
19
|
+
export { bridgeAddToHNSW, bridgeCountEntries, bridgeDeleteEntry, bridgeForceBuildHNSW, bridgeGenerateEmbedding, bridgeGetBackendStats, bridgeGetEntry, bridgeGetHNSWStatus, bridgeListEntries, bridgeLoadEmbeddingModel, } from './memory-bridge-entries.js';
|
|
20
|
+
export { bridgeApplyFeedback, bridgeRecordFeedback, bridgeRecordUsage, } from './memory-bridge-feedback.js';
|
|
21
|
+
export { bridgeGetDbPath, GLOBAL_BRAIN, getGlobalBrainDir, getProjectRoot, getProjectRootResolution, } from './memory-bridge-paths.js';
|
|
22
|
+
export { bridgeSearchEntries } from './memory-bridge-search.js';
|
|
23
|
+
export { bridgeLatestSession, bridgeSessionEnd, bridgeSessionStart, } from './memory-bridge-sessions.js';
|
|
24
|
+
export { bridgeStoreEntry } from './memory-bridge-store.js';
|
|
1565
25
|
// ===== Pattern store =====
|
|
1566
26
|
export async function bridgeStorePattern(options) {
|
|
1567
27
|
return bridgeStoreEntry({
|
|
@@ -1607,139 +67,6 @@ export async function bridgeSearchPatterns(options) {
|
|
|
1607
67
|
}),
|
|
1608
68
|
};
|
|
1609
69
|
}
|
|
1610
|
-
async function recordUsageOnBackend(backend, entryIds) {
|
|
1611
|
-
let updated = 0;
|
|
1612
|
-
const skipped = [];
|
|
1613
|
-
for (const id of entryIds) {
|
|
1614
|
-
if (typeof id !== 'string' || !id)
|
|
1615
|
-
continue;
|
|
1616
|
-
try {
|
|
1617
|
-
const entry = await backend.get(id);
|
|
1618
|
-
if (!entry) {
|
|
1619
|
-
skipped.push({ id, reason: 'not_found' });
|
|
1620
|
-
continue;
|
|
1621
|
-
}
|
|
1622
|
-
const { frequency } = entryWeights(entry.metadata);
|
|
1623
|
-
await backend.update(id, {
|
|
1624
|
-
metadata: { frequency_weight: frequency + 1 },
|
|
1625
|
-
lastAccessedAt: Date.now(),
|
|
1626
|
-
});
|
|
1627
|
-
updated++;
|
|
1628
|
-
}
|
|
1629
|
-
catch (e) {
|
|
1630
|
-
logBridgeError('recordUsageOnBackend.entryUpdate', e);
|
|
1631
|
-
skipped.push({ id, reason: 'error' });
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
return { updated, skipped };
|
|
1635
|
-
}
|
|
1636
|
-
/** Record that these entries were actually USED (returned to and consumed by a
|
|
1637
|
-
* caller) — increments frequency_weight, which feeds the ranking blend.
|
|
1638
|
-
*
|
|
1639
|
-
* Unresolvable ids are REPORTED, not dropped: this used to `continue` past
|
|
1640
|
-
* every id it could not read, so a caller handing it a page of stale ids got
|
|
1641
|
-
* `{success: true, updated: 0}` with nothing to say whether those entries had
|
|
1642
|
-
* been deleted or the writes had failed. Same contract as
|
|
1643
|
-
* `bridgeApplyFeedback`. */
|
|
1644
|
-
export async function bridgeRecordUsage(options) {
|
|
1645
|
-
const backend = await getBackend(options.dbPath);
|
|
1646
|
-
if (!backend)
|
|
1647
|
-
return null;
|
|
1648
|
-
try {
|
|
1649
|
-
const { updated, skipped } = await recordUsageOnBackend(backend, (options.entryIds ?? []).slice(0, 100));
|
|
1650
|
-
if (updated)
|
|
1651
|
-
await flushBackend(backend);
|
|
1652
|
-
return skipped.length ? { success: true, updated, skipped } : { success: true, updated };
|
|
1653
|
-
}
|
|
1654
|
-
catch (e) {
|
|
1655
|
-
logBridgeError('bridgeRecordUsage', e);
|
|
1656
|
-
return { success: false, updated: 0 };
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
/** Apply a usefulness rating to the entries that produced an answer:
|
|
1660
|
-
* EWMA feedback_weight' = w + alpha*(score - w), clipped [0,1] (cognee's
|
|
1661
|
-
* apply_feedback_weights). `ledgerKey` makes application idempotent — a
|
|
1662
|
-
* daemon retry or duplicate MCP call must never compound the update. */
|
|
1663
|
-
export async function bridgeApplyFeedback(options) {
|
|
1664
|
-
const backend = await getBackend(options.dbPath);
|
|
1665
|
-
if (!backend)
|
|
1666
|
-
return null;
|
|
1667
|
-
try {
|
|
1668
|
-
const score = Math.max(0, Math.min(1, options.score));
|
|
1669
|
-
const alpha = typeof options.alpha === 'number'
|
|
1670
|
-
? Math.max(0, Math.min(1, options.alpha))
|
|
1671
|
-
: FEEDBACK_EWMA_ALPHA;
|
|
1672
|
-
const ledgerEntryKey = options.ledgerKey ? `applied_${options.ledgerKey.slice(0, 500)}` : null;
|
|
1673
|
-
if (ledgerEntryKey) {
|
|
1674
|
-
const existing = await backend.getByKey('feedback', ledgerEntryKey).catch(() => null);
|
|
1675
|
-
if (existing)
|
|
1676
|
-
return { success: true, applied: 0, alreadyApplied: true };
|
|
1677
|
-
}
|
|
1678
|
-
let applied = 0;
|
|
1679
|
-
const skipped = [];
|
|
1680
|
-
for (const id of (options.entryIds ?? []).slice(0, 100)) {
|
|
1681
|
-
if (typeof id !== 'string' || !id)
|
|
1682
|
-
continue;
|
|
1683
|
-
try {
|
|
1684
|
-
const entry = await backend.get(id);
|
|
1685
|
-
if (!entry) {
|
|
1686
|
-
skipped.push({ id, reason: 'not_found' });
|
|
1687
|
-
continue;
|
|
1688
|
-
}
|
|
1689
|
-
const { feedback } = entryWeights(entry.metadata);
|
|
1690
|
-
const next = Math.max(0, Math.min(1, feedback + alpha * (score - feedback)));
|
|
1691
|
-
await backend.update(id, { metadata: { feedback_weight: next } });
|
|
1692
|
-
applied++;
|
|
1693
|
-
}
|
|
1694
|
-
catch (e) {
|
|
1695
|
-
logBridgeError('bridgeApplyFeedback.entryUpdate', e);
|
|
1696
|
-
skipped.push({ id, reason: 'error' });
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
if (ledgerEntryKey) {
|
|
1700
|
-
await bridgeStoreEntry({
|
|
1701
|
-
key: ledgerEntryKey,
|
|
1702
|
-
value: JSON.stringify({
|
|
1703
|
-
score,
|
|
1704
|
-
entryIds: options.entryIds.slice(0, 100),
|
|
1705
|
-
appliedAt: Date.now(),
|
|
1706
|
-
applied,
|
|
1707
|
-
skipped,
|
|
1708
|
-
}),
|
|
1709
|
-
namespace: 'feedback',
|
|
1710
|
-
generateEmbeddingFlag: false,
|
|
1711
|
-
dbPath: options.dbPath,
|
|
1712
|
-
upsert: true,
|
|
1713
|
-
});
|
|
1714
|
-
}
|
|
1715
|
-
if (applied)
|
|
1716
|
-
await flushBackend(backend);
|
|
1717
|
-
return skipped.length ? { success: true, applied, skipped } : { success: true, applied };
|
|
1718
|
-
}
|
|
1719
|
-
catch (err) {
|
|
1720
|
-
logBridgeError('bridgeApplyFeedback', err);
|
|
1721
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1722
|
-
return { success: false, applied: 0, error: message };
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
// ===== Feedback =====
|
|
1726
|
-
export async function bridgeRecordFeedback(options) {
|
|
1727
|
-
return bridgeStoreEntry({
|
|
1728
|
-
key: `feedback_${options.taskType}_${Date.now()}`,
|
|
1729
|
-
value: JSON.stringify({
|
|
1730
|
-
taskType: options.taskType,
|
|
1731
|
-
action: options.action,
|
|
1732
|
-
outcome: options.outcome,
|
|
1733
|
-
confidence: options.confidence ?? 0.5,
|
|
1734
|
-
metadata: options.metadata ?? {},
|
|
1735
|
-
recordedAt: Date.now(),
|
|
1736
|
-
}),
|
|
1737
|
-
namespace: 'feedback',
|
|
1738
|
-
tags: [options.taskType, options.outcome],
|
|
1739
|
-
generateEmbeddingFlag: true,
|
|
1740
|
-
dbPath: options.dbPath,
|
|
1741
|
-
});
|
|
1742
|
-
}
|
|
1743
70
|
// ===== Causal edges =====
|
|
1744
71
|
export async function bridgeRecordCausalEdge(options) {
|
|
1745
72
|
return bridgeStoreEntry({
|
|
@@ -1757,106 +84,6 @@ export async function bridgeRecordCausalEdge(options) {
|
|
|
1757
84
|
upsert: true,
|
|
1758
85
|
});
|
|
1759
86
|
}
|
|
1760
|
-
// ===== Session lifecycle =====
|
|
1761
|
-
export async function bridgeSessionStart(options) {
|
|
1762
|
-
return bridgeStoreEntry({
|
|
1763
|
-
key: `session_${options.sessionId}`,
|
|
1764
|
-
value: JSON.stringify({
|
|
1765
|
-
sessionId: options.sessionId,
|
|
1766
|
-
agentId: options.agentId,
|
|
1767
|
-
startedAt: Date.now(),
|
|
1768
|
-
status: 'active',
|
|
1769
|
-
metadata: options.metadata ?? {},
|
|
1770
|
-
}),
|
|
1771
|
-
namespace: 'sessions',
|
|
1772
|
-
tags: ['session', 'active'],
|
|
1773
|
-
generateEmbeddingFlag: false,
|
|
1774
|
-
dbPath: options.dbPath,
|
|
1775
|
-
upsert: true,
|
|
1776
|
-
});
|
|
1777
|
-
}
|
|
1778
|
-
export async function bridgeSessionEnd(options) {
|
|
1779
|
-
const backend = await getBackend(options.dbPath);
|
|
1780
|
-
if (!backend)
|
|
1781
|
-
return null;
|
|
1782
|
-
try {
|
|
1783
|
-
const existing = await backend.getByKey('sessions', `session_${options.sessionId}`);
|
|
1784
|
-
// Nothing is written for a session that was never started — do not report
|
|
1785
|
-
// that as a recorded session end.
|
|
1786
|
-
if (!existing)
|
|
1787
|
-
return { success: false, error: `no session ${options.sessionId} to end` };
|
|
1788
|
-
let data = {};
|
|
1789
|
-
try {
|
|
1790
|
-
data = JSON.parse(existing.content);
|
|
1791
|
-
}
|
|
1792
|
-
catch (e) {
|
|
1793
|
-
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
1794
|
-
console.error('[memory-bridge] session content failed to parse — ending session with empty prior state:', e);
|
|
1795
|
-
}
|
|
1796
|
-
const updated = await backend.update(existing.id, {
|
|
1797
|
-
content: JSON.stringify({
|
|
1798
|
-
...data,
|
|
1799
|
-
status: 'ended',
|
|
1800
|
-
endedAt: Date.now(),
|
|
1801
|
-
summary: options.summary,
|
|
1802
|
-
metrics: options.metrics ?? {},
|
|
1803
|
-
}),
|
|
1804
|
-
tags: ['session', 'ended'],
|
|
1805
|
-
});
|
|
1806
|
-
if (!updated)
|
|
1807
|
-
return { success: false, error: `session ${options.sessionId} vanished` };
|
|
1808
|
-
await flushBackend(backend);
|
|
1809
|
-
return { success: true };
|
|
1810
|
-
}
|
|
1811
|
-
catch (e) {
|
|
1812
|
-
logBridgeError('bridgeSessionEnd', e);
|
|
1813
|
-
return { success: false };
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
/** The most recently started session recorded by bridgeSessionStart, read
|
|
1817
|
-
* back from its row — or null when there is none (or no backend). */
|
|
1818
|
-
export async function bridgeLatestSession(options) {
|
|
1819
|
-
const backend = await getBackend(options.dbPath);
|
|
1820
|
-
if (!backend)
|
|
1821
|
-
return null;
|
|
1822
|
-
try {
|
|
1823
|
-
// Newest two rows: one of them may be the excluded (current) session.
|
|
1824
|
-
const rows = await backend.query({
|
|
1825
|
-
type: 'exact',
|
|
1826
|
-
namespace: 'sessions',
|
|
1827
|
-
keyPrefix: 'session_',
|
|
1828
|
-
sortField: 'createdAt',
|
|
1829
|
-
sortDirection: 'desc',
|
|
1830
|
-
limit: 2,
|
|
1831
|
-
});
|
|
1832
|
-
for (const row of rows) {
|
|
1833
|
-
let data;
|
|
1834
|
-
try {
|
|
1835
|
-
data = JSON.parse(row.content);
|
|
1836
|
-
}
|
|
1837
|
-
catch {
|
|
1838
|
-
continue;
|
|
1839
|
-
}
|
|
1840
|
-
if (typeof data?.sessionId !== 'string' || data.sessionId === options.excludeSessionId)
|
|
1841
|
-
continue;
|
|
1842
|
-
return {
|
|
1843
|
-
sessionId: data.sessionId,
|
|
1844
|
-
status: typeof data.status === 'string' ? data.status : 'unknown',
|
|
1845
|
-
startedAt: new Date(data.startedAt ?? row.createdAt).toISOString(),
|
|
1846
|
-
...(typeof data.endedAt === 'number'
|
|
1847
|
-
? { endedAt: new Date(data.endedAt).toISOString() }
|
|
1848
|
-
: {}),
|
|
1849
|
-
...(typeof data.summary === 'string' ? { summary: data.summary } : {}),
|
|
1850
|
-
...(data.metrics && typeof data.metrics === 'object' ? { metrics: data.metrics } : {}),
|
|
1851
|
-
};
|
|
1852
|
-
}
|
|
1853
|
-
return null;
|
|
1854
|
-
}
|
|
1855
|
-
catch (e) {
|
|
1856
|
-
logBridgeError('bridgeLatestSession', e);
|
|
1857
|
-
return null;
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
87
|
// ===== Task routing =====
|
|
1861
88
|
export async function bridgeRouteTask(options) {
|
|
1862
89
|
const result = await bridgeSearchEntries({
|