nlm-memory 0.20.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/dist/cli/digest.js +21 -0
- package/dist/cli/digest.js.map +1 -1
- package/dist/cli/init.d.ts +12 -0
- package/dist/cli/init.js +49 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/nlm.d.ts +13 -1
- package/dist/cli/nlm.js +766 -55
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/actions/overlay.d.ts +20 -6
- package/dist/core/actions/overlay.js +32 -18
- package/dist/core/actions/overlay.js.map +1 -1
- package/dist/core/adapters/claude-code.js +6 -0
- package/dist/core/adapters/claude-code.js.map +1 -1
- package/dist/core/classifier/prompt.js +3 -3
- package/dist/core/digest/compose.d.ts +10 -0
- package/dist/core/digest/compose.js +7 -1
- package/dist/core/digest/compose.js.map +1 -1
- package/dist/core/embedding/embed-backfill.d.ts +13 -2
- package/dist/core/embedding/embed-backfill.js +181 -43
- package/dist/core/embedding/embed-backfill.js.map +1 -1
- package/dist/core/embedding/embedding-config.d.ts +17 -0
- package/dist/core/embedding/embedding-config.js +12 -0
- package/dist/core/embedding/embedding-config.js.map +1 -0
- package/dist/core/embedding/pg-embed-backfill.d.ts +31 -0
- package/dist/core/embedding/pg-embed-backfill.js +264 -0
- package/dist/core/embedding/pg-embed-backfill.js.map +1 -0
- package/dist/core/entities/dedup-suggest.d.ts +25 -0
- package/dist/core/entities/dedup-suggest.js +107 -0
- package/dist/core/entities/dedup-suggest.js.map +1 -0
- package/dist/core/eval/classifier-fixture-eval.d.ts +29 -0
- package/dist/core/eval/classifier-fixture-eval.js +217 -0
- package/dist/core/eval/classifier-fixture-eval.js.map +1 -0
- package/dist/core/eval/extraction-scoring.d.ts +63 -0
- package/dist/core/eval/extraction-scoring.js +79 -0
- package/dist/core/eval/extraction-scoring.js.map +1 -0
- package/dist/core/eval/run-eval.d.ts +40 -0
- package/dist/core/eval/run-eval.js +34 -0
- package/dist/core/eval/run-eval.js.map +1 -0
- package/dist/core/exemplars/capture-from-session.d.ts +1 -0
- package/dist/core/exemplars/capture-from-session.js +6 -2
- package/dist/core/exemplars/capture-from-session.js.map +1 -1
- package/dist/core/exemplars/diff-parse.d.ts +23 -0
- package/dist/core/exemplars/diff-parse.js +73 -0
- package/dist/core/exemplars/diff-parse.js.map +1 -0
- package/dist/core/exemplars/embed-backfill.d.ts +36 -0
- package/dist/core/exemplars/embed-backfill.js +85 -0
- package/dist/core/exemplars/embed-backfill.js.map +1 -0
- package/dist/core/exemplars/embed-text.d.ts +15 -0
- package/dist/core/exemplars/embed-text.js +18 -0
- package/dist/core/exemplars/embed-text.js.map +1 -0
- package/dist/core/exemplars/extract-exemplar.js +2 -65
- package/dist/core/exemplars/extract-exemplar.js.map +1 -1
- package/dist/core/exemplars/recall-code.js +4 -0
- package/dist/core/exemplars/recall-code.js.map +1 -1
- package/dist/core/exemplars/warm-embedder.d.ts +14 -0
- package/dist/core/exemplars/warm-embedder.js +18 -0
- package/dist/core/exemplars/warm-embedder.js.map +1 -0
- package/dist/core/health/corpus-state.d.ts +8 -0
- package/dist/core/health/corpus-state.js +11 -0
- package/dist/core/health/corpus-state.js.map +1 -0
- package/dist/core/health/embed-inflight.d.ts +9 -0
- package/dist/core/health/embed-inflight.js +30 -0
- package/dist/core/health/embed-inflight.js.map +1 -0
- package/dist/core/health/embedding-lane-state.d.ts +6 -0
- package/dist/core/health/embedding-lane-state.js +18 -0
- package/dist/core/health/embedding-lane-state.js.map +1 -0
- package/dist/core/health/warmup-state.d.ts +12 -0
- package/dist/core/health/warmup-state.js +12 -0
- package/dist/core/health/warmup-state.js.map +1 -0
- package/dist/core/hook/gate.d.ts +13 -6
- package/dist/core/hook/gate.js +47 -6
- package/dist/core/hook/gate.js.map +1 -1
- package/dist/core/hook/hook-log.d.ts +5 -0
- package/dist/core/hook/hook-log.js.map +1 -1
- package/dist/core/hook/memo.d.ts +12 -0
- package/dist/core/hook/memo.js +41 -1
- package/dist/core/hook/memo.js.map +1 -1
- package/dist/core/hook/pointer-block.d.ts +1 -0
- package/dist/core/hook/pointer-block.js +23 -2
- package/dist/core/hook/pointer-block.js.map +1 -1
- package/dist/core/hook/query-extract.d.ts +2 -1
- package/dist/core/hook/query-extract.js +10 -1
- package/dist/core/hook/query-extract.js.map +1 -1
- package/dist/core/hook/resolve-conversation-by-query.d.ts +28 -0
- package/dist/core/hook/resolve-conversation-by-query.js +98 -0
- package/dist/core/hook/resolve-conversation-by-query.js.map +1 -0
- package/dist/core/hook/select.d.ts +8 -0
- package/dist/core/hook/select.js +9 -2
- package/dist/core/hook/select.js.map +1 -1
- package/dist/core/hook/transcript.js +16 -2
- package/dist/core/hook/transcript.js.map +1 -1
- package/dist/core/ingest/ingest-session.d.ts +5 -0
- package/dist/core/ingest/ingest-session.js +3 -0
- package/dist/core/ingest/ingest-session.js.map +1 -1
- package/dist/core/ingest/reclassify-oversized.d.ts +5 -0
- package/dist/core/ingest/reclassify-oversized.js +3 -0
- package/dist/core/ingest/reclassify-oversized.js.map +1 -1
- package/dist/core/ingest/reprocess.d.ts +96 -0
- package/dist/core/ingest/reprocess.js +275 -0
- package/dist/core/ingest/reprocess.js.map +1 -0
- package/dist/core/integrity/check-invariants.d.ts +6 -1
- package/dist/core/integrity/check-invariants.js +209 -3
- package/dist/core/integrity/check-invariants.js.map +1 -1
- package/dist/core/metrics/corpus-stats.d.ts +49 -0
- package/dist/core/metrics/corpus-stats.js +111 -0
- package/dist/core/metrics/corpus-stats.js.map +1 -0
- package/dist/core/metrics/re-derivation.d.ts +48 -0
- package/dist/core/metrics/re-derivation.js +87 -0
- package/dist/core/metrics/re-derivation.js.map +1 -0
- package/dist/core/recall/citation-log.d.ts +8 -0
- package/dist/core/recall/citation-log.js +17 -0
- package/dist/core/recall/citation-log.js.map +1 -1
- package/dist/core/recall/query-intent.d.ts +7 -0
- package/dist/core/recall/query-intent.js +25 -0
- package/dist/core/recall/query-intent.js.map +1 -0
- package/dist/core/recall/query-log.d.ts +4 -0
- package/dist/core/recall/query-log.js +4 -0
- package/dist/core/recall/query-log.js.map +1 -1
- package/dist/core/recall/recall-service.d.ts +37 -1
- package/dist/core/recall/recall-service.js +106 -63
- package/dist/core/recall/recall-service.js.map +1 -1
- package/dist/core/recall/related-exemplars.d.ts +1 -0
- package/dist/core/recall/related-exemplars.js +4 -1
- package/dist/core/recall/related-exemplars.js.map +1 -1
- package/dist/core/recall/related-facts.js +2 -2
- package/dist/core/recall/related-facts.js.map +1 -1
- package/dist/core/recall/reranker.d.ts +2 -1
- package/dist/core/recall/reranker.js +3 -3
- package/dist/core/recall/reranker.js.map +1 -1
- package/dist/core/recall-facts/fact-query-log.d.ts +7 -0
- package/dist/core/recall-facts/fact-query-log.js +2 -0
- package/dist/core/recall-facts/fact-query-log.js.map +1 -1
- package/dist/core/recall-facts/fact-recall-service.d.ts +9 -1
- package/dist/core/recall-facts/fact-recall-service.js +21 -8
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
- package/dist/core/scheduler/scan-once.d.ts +6 -6
- package/dist/core/scheduler/scan-once.js +13 -49
- package/dist/core/scheduler/scan-once.js.map +1 -1
- package/dist/core/scheduler/scheduler.d.ts +10 -0
- package/dist/core/scheduler/scheduler.js +75 -10
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/signals/code-signal.d.ts +49 -0
- package/dist/core/signals/code-signal.js +53 -0
- package/dist/core/signals/code-signal.js.map +1 -0
- package/dist/core/storage/db-restore.js +3 -1
- package/dist/core/storage/db-restore.js.map +1 -1
- package/dist/core/storage/fact-batch.d.ts +9 -0
- package/dist/core/storage/fact-batch.js +14 -0
- package/dist/core/storage/fact-batch.js.map +1 -0
- package/dist/core/storage/pg-code-exemplar-store.d.ts +1 -0
- package/dist/core/storage/pg-code-exemplar-store.js +7 -0
- package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
- package/dist/core/storage/pg-embedding-config.d.ts +29 -0
- package/dist/core/storage/pg-embedding-config.js +53 -0
- package/dist/core/storage/pg-embedding-config.js.map +1 -0
- package/dist/core/storage/pg-entity-store.d.ts +12 -0
- package/dist/core/storage/pg-entity-store.js +79 -0
- package/dist/core/storage/pg-entity-store.js.map +1 -0
- package/dist/core/storage/pg-fact-ingest.d.ts +15 -0
- package/dist/core/storage/pg-fact-ingest.js +34 -0
- package/dist/core/storage/pg-fact-ingest.js.map +1 -0
- package/dist/core/storage/pg-fact-store.d.ts +3 -0
- package/dist/core/storage/pg-fact-store.js +26 -30
- package/dist/core/storage/pg-fact-store.js.map +1 -1
- package/dist/core/storage/pg-migrate.d.ts +18 -0
- package/dist/core/storage/pg-migrate.js +73 -0
- package/dist/core/storage/pg-migrate.js.map +1 -0
- package/dist/core/storage/pg-session-store.d.ts +9 -0
- package/dist/core/storage/pg-session-store.js +204 -76
- package/dist/core/storage/pg-session-store.js.map +1 -1
- package/dist/core/storage/pg-storage.d.ts +9 -7
- package/dist/core/storage/pg-storage.js +20 -50
- package/dist/core/storage/pg-storage.js.map +1 -1
- package/dist/core/storage/pg-workstream-store.d.ts +24 -0
- package/dist/core/storage/pg-workstream-store.js +96 -0
- package/dist/core/storage/pg-workstream-store.js.map +1 -0
- package/dist/core/storage/sqlite-code-exemplar-store.d.ts +1 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js +11 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
- package/dist/core/storage/sqlite-embedding-config.d.ts +10 -0
- package/dist/core/storage/sqlite-embedding-config.js +27 -0
- package/dist/core/storage/sqlite-embedding-config.js.map +1 -0
- package/dist/core/storage/sqlite-entity-store.d.ts +13 -0
- package/dist/core/storage/sqlite-entity-store.js +96 -0
- package/dist/core/storage/sqlite-entity-store.js.map +1 -0
- package/dist/core/storage/sqlite-fact-store.d.ts +15 -7
- package/dist/core/storage/sqlite-fact-store.js +76 -25
- package/dist/core/storage/sqlite-fact-store.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +20 -5
- package/dist/core/storage/sqlite-session-store.js +183 -68
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/core/storage/sqlite-storage.d.ts +8 -8
- package/dist/core/storage/sqlite-storage.js +21 -32
- package/dist/core/storage/sqlite-storage.js.map +1 -1
- package/dist/core/storage/sqlite-workstream-store.d.ts +24 -0
- package/dist/core/storage/sqlite-workstream-store.js +98 -0
- package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
- package/dist/core/work-digest/active-spans.d.ts +8 -0
- package/dist/core/work-digest/active-spans.js +29 -0
- package/dist/core/work-digest/active-spans.js.map +1 -0
- package/dist/core/work-digest/attribute.d.ts +15 -0
- package/dist/core/work-digest/attribute.js +76 -0
- package/dist/core/work-digest/attribute.js.map +1 -0
- package/dist/core/work-digest/build-work-digest.d.ts +12 -0
- package/dist/core/work-digest/build-work-digest.js +69 -0
- package/dist/core/work-digest/build-work-digest.js.map +1 -0
- package/dist/core/work-digest/compose-work-digest.d.ts +3 -0
- package/dist/core/work-digest/compose-work-digest.js +52 -0
- package/dist/core/work-digest/compose-work-digest.js.map +1 -0
- package/dist/core/work-digest/merge-active.d.ts +10 -0
- package/dist/core/work-digest/merge-active.js +22 -0
- package/dist/core/work-digest/merge-active.js.map +1 -0
- package/dist/core/work-digest/read-transcript-timestamps.d.ts +7 -0
- package/dist/core/work-digest/read-transcript-timestamps.js +40 -0
- package/dist/core/work-digest/read-transcript-timestamps.js.map +1 -0
- package/dist/core/work-digest/topics.d.ts +18 -0
- package/dist/core/work-digest/topics.js +26 -0
- package/dist/core/work-digest/topics.js.map +1 -0
- package/dist/core/work-digest/types.d.ts +45 -0
- package/dist/core/work-digest/types.js +2 -0
- package/dist/core/work-digest/types.js.map +1 -0
- package/dist/core/workstream/bind.d.ts +25 -0
- package/dist/core/workstream/bind.js +23 -0
- package/dist/core/workstream/bind.js.map +1 -0
- package/dist/core/workstream/compose-recall.d.ts +2 -0
- package/dist/core/workstream/compose-recall.js +39 -0
- package/dist/core/workstream/compose-recall.js.map +1 -0
- package/dist/core/workstream/merge-suggest.d.ts +19 -0
- package/dist/core/workstream/merge-suggest.js +61 -0
- package/dist/core/workstream/merge-suggest.js.map +1 -0
- package/dist/core/workstream/model.d.ts +20 -0
- package/dist/core/workstream/model.js +9 -0
- package/dist/core/workstream/model.js.map +1 -0
- package/dist/core/workstream/name-match.d.ts +8 -0
- package/dist/core/workstream/name-match.js +18 -0
- package/dist/core/workstream/name-match.js.map +1 -0
- package/dist/core/workstream/resolve.d.ts +10 -0
- package/dist/core/workstream/resolve.js +21 -0
- package/dist/core/workstream/resolve.js.map +1 -0
- package/dist/core/workstream/rollup.d.ts +12 -0
- package/dist/core/workstream/rollup.js +17 -0
- package/dist/core/workstream/rollup.js.map +1 -0
- package/dist/core/workstream/work-topics.d.ts +10 -0
- package/dist/core/workstream/work-topics.js +55 -0
- package/dist/core/workstream/work-topics.js.map +1 -0
- package/dist/hook/hook-helpers.d.ts +4 -0
- package/dist/hook/hook-helpers.js +36 -0
- package/dist/hook/hook-helpers.js.map +1 -0
- package/dist/hook/pre-compact-hook.d.ts +23 -0
- package/dist/hook/pre-compact-hook.js +66 -0
- package/dist/hook/pre-compact-hook.js.map +1 -0
- package/dist/hook/prompt-recall-hook.d.ts +37 -0
- package/dist/hook/prompt-recall-hook.js +120 -20
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/recall-gate.d.ts +29 -0
- package/dist/hook/recall-gate.js +50 -0
- package/dist/hook/recall-gate.js.map +1 -0
- package/dist/hook/recall-over-http.d.ts +1 -1
- package/dist/hook/recall-over-http.js +11 -11
- package/dist/hook/recall-over-http.js.map +1 -1
- package/dist/hook/recent-context.d.ts +25 -0
- package/dist/hook/recent-context.js +104 -0
- package/dist/hook/recent-context.js.map +1 -0
- package/dist/hook/score-floor.d.ts +27 -0
- package/dist/hook/score-floor.js +42 -0
- package/dist/hook/score-floor.js.map +1 -0
- package/dist/hook/session-end-hook.js +8 -29
- package/dist/hook/session-end-hook.js.map +1 -1
- package/dist/hook/session-start-hook.js +24 -59
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +22 -59
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/hook/subagent-start-hook.d.ts +28 -0
- package/dist/hook/subagent-start-hook.js +81 -0
- package/dist/hook/subagent-start-hook.js.map +1 -0
- package/dist/http/app.js +28 -8
- package/dist/http/app.js.map +1 -1
- package/dist/install/setup.js +2 -0
- package/dist/install/setup.js.map +1 -1
- package/dist/llm/build-classifier.d.ts +22 -0
- package/dist/llm/build-classifier.js +42 -0
- package/dist/llm/build-classifier.js.map +1 -0
- package/dist/llm/build-embedder.d.ts +11 -0
- package/dist/llm/build-embedder.js +42 -0
- package/dist/llm/build-embedder.js.map +1 -0
- package/dist/llm/bundled-embedder-client.d.ts +37 -0
- package/dist/llm/bundled-embedder-client.js +90 -0
- package/dist/llm/bundled-embedder-client.js.map +1 -0
- package/dist/llm/classifier-box.d.ts +13 -5
- package/dist/llm/classifier-box.js +28 -6
- package/dist/llm/classifier-box.js.map +1 -1
- package/dist/llm/classifier-egress.d.ts +18 -0
- package/dist/llm/classifier-egress.js +59 -0
- package/dist/llm/classifier-egress.js.map +1 -0
- package/dist/llm/client-shared.d.ts +4 -0
- package/dist/llm/client-shared.js +39 -0
- package/dist/llm/client-shared.js.map +1 -0
- package/dist/llm/deepseek-client.d.ts +20 -0
- package/dist/llm/deepseek-client.js +59 -45
- package/dist/llm/deepseek-client.js.map +1 -1
- package/dist/llm/embedder-info.d.ts +15 -0
- package/dist/llm/embedder-info.js +27 -0
- package/dist/llm/embedder-info.js.map +1 -0
- package/dist/llm/model-quirks.d.ts +4 -0
- package/dist/llm/model-quirks.js +7 -0
- package/dist/llm/model-quirks.js.map +1 -0
- package/dist/llm/naming.d.ts +5 -0
- package/dist/llm/naming.js +20 -0
- package/dist/llm/naming.js.map +1 -0
- package/dist/llm/ollama-client.d.ts +7 -8
- package/dist/llm/ollama-client.js +59 -51
- package/dist/llm/ollama-client.js.map +1 -1
- package/dist/llm/ollama-code-embedder.d.ts +8 -3
- package/dist/llm/ollama-code-embedder.js +14 -24
- package/dist/llm/ollama-code-embedder.js.map +1 -1
- package/dist/llm/openai-code-embedder-client.d.ts +25 -0
- package/dist/llm/openai-code-embedder-client.js +52 -0
- package/dist/llm/openai-code-embedder-client.js.map +1 -0
- package/dist/llm/openai-embedder-client.d.ts +41 -0
- package/dist/llm/openai-embedder-client.js +83 -0
- package/dist/llm/openai-embedder-client.js.map +1 -0
- package/dist/mcp/server.d.ts +47 -2
- package/dist/mcp/server.js +303 -18
- package/dist/mcp/server.js.map +1 -1
- package/dist/ports/code-embedder.d.ts +6 -1
- package/dist/ports/code-exemplar-store.d.ts +5 -0
- package/dist/ports/entity-store.d.ts +35 -0
- package/dist/ports/entity-store.js +7 -0
- package/dist/ports/entity-store.js.map +1 -0
- package/dist/ports/fact-store.d.ts +0 -0
- package/dist/ports/fact-store.js.map +1 -1
- package/dist/ports/llm-client.d.ts +9 -1
- package/dist/ports/session-store.d.ts +12 -0
- package/dist/ports/storage.d.ts +7 -30
- package/dist/ports/storage.js +3 -7
- package/dist/ports/storage.js.map +1 -1
- package/dist/ports/transcript-adapter.d.ts +2 -0
- package/dist/ports/workstream-store.d.ts +21 -0
- package/dist/ports/workstream-store.js +2 -0
- package/dist/ports/workstream-store.js.map +1 -0
- package/dist/shared/types.d.ts +23 -2
- package/fixtures/classifier-gold/README.md +94 -0
- package/fixtures/classifier-gold/reference.json +318 -0
- package/fixtures/classifier-gold/transcripts/01-bug-fix-null-pointer-api.txt +75 -0
- package/fixtures/classifier-gold/transcripts/02-bug-fix-race-condition-queue.txt +77 -0
- package/fixtures/classifier-gold/transcripts/03-bug-fix-jwt-clock-skew.txt +86 -0
- package/fixtures/classifier-gold/transcripts/04-feature-webhook-retry.txt +95 -0
- package/fixtures/classifier-gold/transcripts/05-feature-csv-export.txt +86 -0
- package/fixtures/classifier-gold/transcripts/06-feature-rate-limiting.txt +61 -0
- package/fixtures/classifier-gold/transcripts/07-refactor-db-abstraction.txt +72 -0
- package/fixtures/classifier-gold/transcripts/08-refactor-config-loading.txt +89 -0
- package/fixtures/classifier-gold/transcripts/09-ops-docker-staging.txt +94 -0
- package/fixtures/classifier-gold/transcripts/10-ops-nginx-ssl.txt +121 -0
- package/fixtures/classifier-gold/transcripts/11-ops-postgres-migration.txt +74 -0
- package/fixtures/classifier-gold/transcripts/12-ops-cron-debugging.txt +73 -0
- package/fixtures/classifier-gold/transcripts/13-research-vector-db-comparison.txt +59 -0
- package/fixtures/classifier-gold/transcripts/14-research-oauth-provider.txt +74 -0
- package/fixtures/classifier-gold/transcripts/15-writing-api-docs.txt +154 -0
- package/fixtures/classifier-gold/transcripts/16-meeting-sprint-planning.txt +69 -0
- package/fixtures/classifier-gold/transcripts/17-trivial-list-files.txt +14 -0
- package/fixtures/classifier-gold/transcripts/18-trivial-version-check.txt +21 -0
- package/fixtures/classifier-gold/transcripts/19-trivial-syntax-help.txt +29 -0
- package/fixtures/classifier-gold/transcripts/20-trivial-read-package-json.txt +41 -0
- package/migrations/000_initial_schema.sql +1 -1
- package/migrations/024_backfill_exemplar_repo_basename.sql +32 -0
- package/migrations/025_workstreams.sql +30 -0
- package/migrations/026_embedding_config.sql +12 -0
- package/migrations/027_drop_legacy_session_embeddings.sql +7 -0
- package/migrations/028_classifier_provenance.sql +5 -0
- package/migrations/pg/001_initial.sql +34 -0
- package/migrations/pg/019_split_replaces.sql +16 -7
- package/migrations/pg/025_workstreams.sql +34 -0
- package/migrations/pg/026_embedding_config.sql +12 -0
- package/migrations/pg/028_classifier_provenance.sql +5 -0
- package/migrations/pg/029_entity_variants.sql +9 -0
- package/nlm/index.js +395 -37
- package/package.json +12 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/prompt-recall-hook.mjs +402 -41
- package/plugin/scripts/session-start-hook.mjs +286 -60
- package/plugin/scripts/stop-hook.mjs +85 -65
- package/plugin-hermes-agent/__init__.py +2 -1
- package/templates/agent-contract/README.md +24 -0
- package/templates/agent-contract/claude-code.md +40 -0
- package/templates/agent-contract/generic.md +38 -0
- package/dist/core/embedding/embed-normalize.d.ts +0 -28
- package/dist/core/embedding/embed-normalize.js +0 -98
- package/dist/core/embedding/embed-normalize.js.map +0 -1
- package/dist/core/storage/pg-tx-context.d.ts +0 -52
- package/dist/core/storage/pg-tx-context.js +0 -149
- package/dist/core/storage/pg-tx-context.js.map +0 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- migrations/pg/025_workstreams.sql
|
|
2
|
+
-- PG parity for SQLite migration 025 (workstreams). Auto-applied by the
|
|
3
|
+
-- version-gated runner (runMigrationsPg). Idempotent: every DDL statement uses
|
|
4
|
+
-- IF NOT EXISTS or ADD COLUMN IF NOT EXISTS, so re-applying on an existing
|
|
5
|
+
-- schema is a no-op. Mirrors migrations/025_workstreams.sql.
|
|
6
|
+
|
|
7
|
+
BEGIN;
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS workstreams (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
label TEXT NOT NULL,
|
|
12
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','merged','retired')),
|
|
13
|
+
merged_into TEXT REFERENCES workstreams(id),
|
|
14
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
16
|
+
last_session_at TIMESTAMPTZ
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS workstream_entities (
|
|
20
|
+
workstream_id TEXT NOT NULL REFERENCES workstreams(id) ON DELETE CASCADE,
|
|
21
|
+
entity_canonical TEXT NOT NULL REFERENCES entities(canonical),
|
|
22
|
+
session_count INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
PRIMARY KEY (workstream_id, entity_canonical)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_workstream_entities_entity ON workstream_entities(entity_canonical);
|
|
27
|
+
|
|
28
|
+
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS workstream_id TEXT REFERENCES workstreams(id);
|
|
29
|
+
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS binding_source TEXT;
|
|
30
|
+
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS binding_confidence DOUBLE PRECISION;
|
|
31
|
+
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_workstream ON sessions(workstream_id);
|
|
33
|
+
|
|
34
|
+
COMMIT;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Records which embedder produced the stored vectors, per lane.
|
|
2
|
+
-- lane 'prose' covers session chunks and fact embeddings (same embedder);
|
|
3
|
+
-- lane 'code' covers code exemplar embeddings.
|
|
4
|
+
-- Absence of a row means pre-tracking corpus: reconcile treats the running
|
|
5
|
+
-- embedder as canonical and records it.
|
|
6
|
+
CREATE TABLE IF NOT EXISTS embedding_config (
|
|
7
|
+
lane TEXT PRIMARY KEY CHECK (lane IN ('prose', 'code')),
|
|
8
|
+
provider TEXT NOT NULL,
|
|
9
|
+
model TEXT NOT NULL,
|
|
10
|
+
dim INTEGER NOT NULL,
|
|
11
|
+
updated_at TEXT NOT NULL
|
|
12
|
+
);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Which classifier produced this session's classification. NULL = classified
|
|
2
|
+
-- before provenance tracking; nlm reprocess treats NULL as eligible.
|
|
3
|
+
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS classifier_provider TEXT;
|
|
4
|
+
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS classifier_model TEXT;
|
|
5
|
+
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS classifier_confidence REAL;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Entity variant alias table. Tracks merged surface forms so re-ingest of an
|
|
2
|
+
-- old spelling binds to the canonical entity instead of resurrecting the source.
|
|
3
|
+
CREATE TABLE IF NOT EXISTS entity_variants (
|
|
4
|
+
variant TEXT PRIMARY KEY,
|
|
5
|
+
canonical TEXT NOT NULL REFERENCES entities(canonical) ON DELETE CASCADE,
|
|
6
|
+
source_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_entity_variants_canonical ON entity_variants(canonical);
|
package/nlm/index.js
CHANGED
|
@@ -181,30 +181,55 @@ var STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
181
181
|
]);
|
|
182
182
|
var MIN_CONTENT_WORDS = 2;
|
|
183
183
|
var MIN_WORD_LEN = 3;
|
|
184
|
+
var SYSTEM_MESSAGE_PREFIX = /^<(task-notification|command-name|command-message|command-args|local-command-stdout|local-command-caveat|output-file|system-reminder)\b/;
|
|
184
185
|
function extractRecallQuery(prompt) {
|
|
186
|
+
if (SYSTEM_MESSAGE_PREFIX.test(prompt.trim())) return null;
|
|
185
187
|
const tokens = prompt.trim().split(/\s+/).map((t) => t.replace(/^[^\w-]+|[^\w-]+$/g, "")).filter((t) => t.length >= MIN_WORD_LEN);
|
|
186
188
|
const contentWords = tokens.filter((t) => !STOPWORDS.has(t.toLowerCase()));
|
|
187
189
|
if (contentWords.length < MIN_CONTENT_WORDS) return null;
|
|
188
190
|
return contentWords.join(" ");
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
// src/hook/hook-helpers.ts
|
|
194
|
+
function readStdin() {
|
|
195
|
+
return new Promise((resolve2) => {
|
|
196
|
+
let data = "";
|
|
197
|
+
process.stdin.setEncoding("utf8");
|
|
198
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
199
|
+
process.stdin.on("end", () => resolve2(data));
|
|
200
|
+
process.stdin.on("error", () => resolve2(data));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async function fetchWithTimeout(url, init, ms) {
|
|
204
|
+
const controller = new AbortController();
|
|
205
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
206
|
+
try {
|
|
207
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function hookModeFromEnv() {
|
|
213
|
+
return process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
|
|
214
|
+
}
|
|
215
|
+
|
|
191
216
|
// src/hook/recall-over-http.ts
|
|
192
217
|
var RECALL_LIMIT = 5;
|
|
193
218
|
var RECALL_TIMEOUT_MS = 2e3;
|
|
194
|
-
async function recallOverHttp(prompt, runtime, conversationId) {
|
|
219
|
+
async function recallOverHttp(prompt, runtime, conversationId, mode = "keyword") {
|
|
195
220
|
const query = extractRecallQuery(prompt);
|
|
196
221
|
if (query === null) return { hits: [], facts: [], exemplars: [] };
|
|
197
222
|
const portValue = process.env["NLM_PORT"] ?? "3940";
|
|
198
|
-
const url =
|
|
199
|
-
|
|
200
|
-
|
|
223
|
+
const url = (
|
|
224
|
+
// 127.0.0.1, not localhost: each hook is a fresh process with no connection
|
|
225
|
+
// reuse, and Node resolves localhost to IPv6 ::1 first — a measured ~50-300ms
|
|
226
|
+
// per-fire connect penalty vs ~3ms on the explicit IPv4 loopback.
|
|
227
|
+
`http://127.0.0.1:${portValue}/api/recall?q=${encodeURIComponent(query)}&mode=${mode}&limit=${RECALL_LIMIT}&withFacts=true&withExemplars=true` + (conversationId ? `&conversation_id=${encodeURIComponent(conversationId)}` : "")
|
|
228
|
+
);
|
|
201
229
|
try {
|
|
202
230
|
const extra = { "x-recall-source": "hook" };
|
|
203
231
|
if (runtime) extra["x-recall-runtime"] = runtime;
|
|
204
|
-
const res = await
|
|
205
|
-
headers: hookAuthHeaders(extra),
|
|
206
|
-
signal: controller.signal
|
|
207
|
-
});
|
|
232
|
+
const res = await fetchWithTimeout(url, { headers: hookAuthHeaders(extra) }, RECALL_TIMEOUT_MS);
|
|
208
233
|
if (!res.ok) return { hits: [], facts: [], exemplars: [] };
|
|
209
234
|
let body;
|
|
210
235
|
try {
|
|
@@ -232,8 +257,8 @@ async function recallOverHttp(prompt, runtime, conversationId) {
|
|
|
232
257
|
taskContext: e.taskContext
|
|
233
258
|
}));
|
|
234
259
|
return { hits, facts, exemplars };
|
|
235
|
-
}
|
|
236
|
-
|
|
260
|
+
} catch {
|
|
261
|
+
return { hits: [], facts: [], exemplars: [] };
|
|
237
262
|
}
|
|
238
263
|
}
|
|
239
264
|
|
|
@@ -243,14 +268,192 @@ import { pathToFileURL } from "node:url";
|
|
|
243
268
|
// src/core/hook/gate.ts
|
|
244
269
|
var LEADING_FILLER = /^(please|can you|could you|would you|will you|i need you to|i'd like you to|i want you to|i would like you to|help me|let's|lets|hey|ok|okay)\b[\s,]*/i;
|
|
245
270
|
var GENERATIVE_OPENER = /^(write|draft|create|compose|generate|brainstorm|design|outline|sketch|invent|rename|come up with)\b/i;
|
|
271
|
+
var HARNESS_TAGS = [
|
|
272
|
+
"ide_selection",
|
|
273
|
+
"ide_opened_file",
|
|
274
|
+
"ide_closed_file",
|
|
275
|
+
"ide_diagnostics",
|
|
276
|
+
"ide_recently_modified_file",
|
|
277
|
+
"task-notification",
|
|
278
|
+
"system-reminder",
|
|
279
|
+
"command-name",
|
|
280
|
+
"command-message",
|
|
281
|
+
"command-args",
|
|
282
|
+
"local-command-stdout",
|
|
283
|
+
"local-command-stderr"
|
|
284
|
+
];
|
|
285
|
+
var HARNESS_BLOCK = new RegExp(
|
|
286
|
+
`<(${HARNESS_TAGS.join("|")})\\b[^>]*>[\\s\\S]*?(?:</\\1>|$)`,
|
|
287
|
+
"gi"
|
|
288
|
+
);
|
|
289
|
+
var ACK_ONLY = /^(yes|yep|yeah|sure|ok|okay|k|thanks|thank you|ty|done|nice|cool|great|perfect|continue|next|go|go ahead|do it|yes please|sounds good|good|got it|right|correct)\W*$/i;
|
|
290
|
+
function contentWordCount(s) {
|
|
291
|
+
const m = s.match(/[A-Za-z0-9]{2,}/g);
|
|
292
|
+
return m ? m.length : 0;
|
|
293
|
+
}
|
|
246
294
|
function classifyPrompt(prompt) {
|
|
247
|
-
let p = prompt.trim();
|
|
295
|
+
let p = prompt.replace(HARNESS_BLOCK, " ").replace(/\s+/g, " ").trim();
|
|
296
|
+
if (contentWordCount(p) === 0) return "skip";
|
|
297
|
+
if (ACK_ONLY.test(p)) return "skip";
|
|
248
298
|
for (let i = 0; i < 3 && LEADING_FILLER.test(p); i++) {
|
|
249
299
|
p = p.replace(LEADING_FILLER, "");
|
|
250
300
|
}
|
|
301
|
+
p = p.trim();
|
|
302
|
+
if (contentWordCount(p) === 0) return "skip";
|
|
251
303
|
return GENERATIVE_OPENER.test(p) ? "generative" : "evaluate";
|
|
252
304
|
}
|
|
253
305
|
|
|
306
|
+
// src/hook/recent-context.ts
|
|
307
|
+
import { closeSync, existsSync as existsSync2, fstatSync, openSync, readSync } from "node:fs";
|
|
308
|
+
var DEFAULT_MAX_TURNS = 3;
|
|
309
|
+
var DEFAULT_MAX_BYTES = 64 * 1024;
|
|
310
|
+
var DEFAULT_PER_TURN_CHARS = 400;
|
|
311
|
+
var STOPWORDS2 = /* @__PURE__ */ new Set([
|
|
312
|
+
"the",
|
|
313
|
+
"a",
|
|
314
|
+
"an",
|
|
315
|
+
"and",
|
|
316
|
+
"or",
|
|
317
|
+
"but",
|
|
318
|
+
"is",
|
|
319
|
+
"are",
|
|
320
|
+
"was",
|
|
321
|
+
"were",
|
|
322
|
+
"be",
|
|
323
|
+
"been",
|
|
324
|
+
"being",
|
|
325
|
+
"do",
|
|
326
|
+
"does",
|
|
327
|
+
"did",
|
|
328
|
+
"doing",
|
|
329
|
+
"you",
|
|
330
|
+
"your",
|
|
331
|
+
"i",
|
|
332
|
+
"we",
|
|
333
|
+
"our",
|
|
334
|
+
"it",
|
|
335
|
+
"its",
|
|
336
|
+
"this",
|
|
337
|
+
"that",
|
|
338
|
+
"these",
|
|
339
|
+
"those",
|
|
340
|
+
"what",
|
|
341
|
+
"which",
|
|
342
|
+
"how",
|
|
343
|
+
"why",
|
|
344
|
+
"when",
|
|
345
|
+
"where",
|
|
346
|
+
"who",
|
|
347
|
+
"can",
|
|
348
|
+
"could",
|
|
349
|
+
"would",
|
|
350
|
+
"should",
|
|
351
|
+
"will",
|
|
352
|
+
"shall",
|
|
353
|
+
"may",
|
|
354
|
+
"might",
|
|
355
|
+
"must",
|
|
356
|
+
"please",
|
|
357
|
+
"to",
|
|
358
|
+
"of",
|
|
359
|
+
"in",
|
|
360
|
+
"on",
|
|
361
|
+
"for",
|
|
362
|
+
"with",
|
|
363
|
+
"so",
|
|
364
|
+
"now",
|
|
365
|
+
"then",
|
|
366
|
+
"here",
|
|
367
|
+
"there",
|
|
368
|
+
"just",
|
|
369
|
+
"also",
|
|
370
|
+
"as",
|
|
371
|
+
"at",
|
|
372
|
+
"by",
|
|
373
|
+
"if",
|
|
374
|
+
"my",
|
|
375
|
+
"me",
|
|
376
|
+
"us",
|
|
377
|
+
"ok",
|
|
378
|
+
"okay",
|
|
379
|
+
"yes",
|
|
380
|
+
"no",
|
|
381
|
+
"not",
|
|
382
|
+
"up",
|
|
383
|
+
"out",
|
|
384
|
+
"go",
|
|
385
|
+
"get",
|
|
386
|
+
"got",
|
|
387
|
+
"make",
|
|
388
|
+
"made",
|
|
389
|
+
"give",
|
|
390
|
+
"tell",
|
|
391
|
+
"want",
|
|
392
|
+
"need",
|
|
393
|
+
"think",
|
|
394
|
+
"know",
|
|
395
|
+
"let",
|
|
396
|
+
"lets",
|
|
397
|
+
"about",
|
|
398
|
+
"from",
|
|
399
|
+
"into"
|
|
400
|
+
]);
|
|
401
|
+
function topicalWordCount(s) {
|
|
402
|
+
const tokens = s.toLowerCase().match(/[a-z0-9]{2,}/g) ?? [];
|
|
403
|
+
return tokens.filter((t) => !STOPWORDS2.has(t)).length;
|
|
404
|
+
}
|
|
405
|
+
function textOf(message) {
|
|
406
|
+
if (typeof message !== "object" || message === null) return "";
|
|
407
|
+
const content = message.content;
|
|
408
|
+
if (typeof content === "string") return content;
|
|
409
|
+
if (Array.isArray(content)) {
|
|
410
|
+
return content.filter(
|
|
411
|
+
(b) => typeof b === "object" && b !== null && b.type === "text"
|
|
412
|
+
).map((b) => b.text).join(" ");
|
|
413
|
+
}
|
|
414
|
+
return "";
|
|
415
|
+
}
|
|
416
|
+
function tailRead(path, maxBytes) {
|
|
417
|
+
const fd = openSync(path, "r");
|
|
418
|
+
try {
|
|
419
|
+
const size = fstatSync(fd).size;
|
|
420
|
+
const start = size > maxBytes ? size - maxBytes : 0;
|
|
421
|
+
const len = size - start;
|
|
422
|
+
if (len <= 0) return "";
|
|
423
|
+
const buf = Buffer.alloc(len);
|
|
424
|
+
readSync(fd, buf, 0, len, start);
|
|
425
|
+
return buf.toString("utf8");
|
|
426
|
+
} finally {
|
|
427
|
+
closeSync(fd);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function recentConversationContext(transcriptPath, opts = {}) {
|
|
431
|
+
try {
|
|
432
|
+
if (!transcriptPath || !existsSync2(transcriptPath)) return "";
|
|
433
|
+
const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
434
|
+
const perTurnChars = opts.perTurnChars ?? DEFAULT_PER_TURN_CHARS;
|
|
435
|
+
const lines = tailRead(transcriptPath, opts.maxBytes ?? DEFAULT_MAX_BYTES).split("\n");
|
|
436
|
+
const turns = [];
|
|
437
|
+
for (let i = lines.length - 1; i >= 0 && turns.length < maxTurns; i--) {
|
|
438
|
+
const line = lines[i].trim();
|
|
439
|
+
if (!line) continue;
|
|
440
|
+
let evt;
|
|
441
|
+
try {
|
|
442
|
+
evt = JSON.parse(line);
|
|
443
|
+
} catch {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (evt.type !== "user" && evt.type !== "assistant") continue;
|
|
447
|
+
const text = textOf(evt.message).trim();
|
|
448
|
+
if (!text) continue;
|
|
449
|
+
turns.unshift(text.slice(0, perTurnChars));
|
|
450
|
+
}
|
|
451
|
+
return turns.join(" ").trim();
|
|
452
|
+
} catch {
|
|
453
|
+
return "";
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
254
457
|
// src/core/hook/hook-log.ts
|
|
255
458
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
256
459
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -269,7 +472,15 @@ function appendHookLog(entry) {
|
|
|
269
472
|
}
|
|
270
473
|
|
|
271
474
|
// src/core/hook/memo.ts
|
|
272
|
-
import {
|
|
475
|
+
import {
|
|
476
|
+
existsSync as existsSync3,
|
|
477
|
+
mkdirSync as mkdirSync2,
|
|
478
|
+
readdirSync,
|
|
479
|
+
readFileSync as readFileSync2,
|
|
480
|
+
rmSync,
|
|
481
|
+
statSync,
|
|
482
|
+
writeFileSync
|
|
483
|
+
} from "node:fs";
|
|
273
484
|
import { homedir as homedir3 } from "node:os";
|
|
274
485
|
import { join as join2 } from "node:path";
|
|
275
486
|
function stateDir() {
|
|
@@ -282,7 +493,7 @@ function memoPath(conversationId) {
|
|
|
282
493
|
function loadSurfaced(conversationId) {
|
|
283
494
|
try {
|
|
284
495
|
const path = memoPath(conversationId);
|
|
285
|
-
if (!
|
|
496
|
+
if (!existsSync3(path)) return /* @__PURE__ */ new Set();
|
|
286
497
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
287
498
|
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
288
499
|
return new Set(parsed.filter((x) => typeof x === "string"));
|
|
@@ -301,6 +512,26 @@ function recordSurfaced(conversationId, ids) {
|
|
|
301
512
|
}
|
|
302
513
|
|
|
303
514
|
// src/core/hook/pointer-block.ts
|
|
515
|
+
function truncateSummary(s, max = 200) {
|
|
516
|
+
if (s.length <= max) return s;
|
|
517
|
+
const window = s.slice(0, max);
|
|
518
|
+
const last = window[window.length - 1];
|
|
519
|
+
if ((last === "." || last === "!" || last === "?") && window.length - 1 >= 60) {
|
|
520
|
+
return window;
|
|
521
|
+
}
|
|
522
|
+
for (let i = window.length - 2; i >= 60; i--) {
|
|
523
|
+
const c = window[i];
|
|
524
|
+
if ((c === "." || c === "!" || c === "?") && window[i + 1] === " ") {
|
|
525
|
+
return s.slice(0, i + 1);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (let i = window.length - 1; i >= 60; i--) {
|
|
529
|
+
if (window[i] === " ") {
|
|
530
|
+
return s.slice(0, i) + " ...";
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return window + " ...";
|
|
534
|
+
}
|
|
304
535
|
function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
305
536
|
if (hits.length === 0 && facts.length === 0 && exemplars.length === 0) return "";
|
|
306
537
|
const out = [];
|
|
@@ -309,7 +540,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
|
309
540
|
for (const h of hits) {
|
|
310
541
|
const datePart = h.startedAt.slice(0, 10);
|
|
311
542
|
if (h.summary) {
|
|
312
|
-
out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${h.summary
|
|
543
|
+
out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${truncateSummary(h.summary)}`);
|
|
313
544
|
} else {
|
|
314
545
|
out.push(`- ${h.id} \xB7 ${h.label} (${datePart})`);
|
|
315
546
|
}
|
|
@@ -328,7 +559,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
|
328
559
|
out.push("## Related code exemplars (nlm-memory)");
|
|
329
560
|
for (const e of exemplars) {
|
|
330
561
|
const langPart = e.lang ? `${e.lang} \xB7 ` : "";
|
|
331
|
-
out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${e.taskContext
|
|
562
|
+
out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${truncateSummary(e.taskContext)}`);
|
|
332
563
|
}
|
|
333
564
|
}
|
|
334
565
|
const tools = exemplars.length > 0 ? "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved), recall_code (pull the full code for a related exemplar)." : "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved).";
|
|
@@ -337,25 +568,125 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
|
337
568
|
}
|
|
338
569
|
|
|
339
570
|
// src/core/hook/select.ts
|
|
571
|
+
function medianScore(hits) {
|
|
572
|
+
if (hits.length === 0) return 0;
|
|
573
|
+
const sorted = hits.map((h) => h.matchScore).sort((a, b) => a - b);
|
|
574
|
+
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
575
|
+
}
|
|
340
576
|
function selectHits(params) {
|
|
341
|
-
const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap } = params;
|
|
577
|
+
const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap, relativeFloor = 0 } = params;
|
|
578
|
+
const relCut = relativeFloor > 0 ? relativeFloor * medianScore(hits) : 0;
|
|
342
579
|
const eligible = hits.filter(
|
|
343
|
-
(h) => h.matchScore >= scoreThreshold && !surfaced.has(h.id)
|
|
580
|
+
(h) => h.matchScore >= scoreThreshold && h.matchScore >= relCut && !surfaced.has(h.id)
|
|
344
581
|
);
|
|
345
582
|
const budget = Math.max(0, perConversationCap - surfaced.size);
|
|
346
583
|
const limit = Math.min(perFireCap, budget);
|
|
347
584
|
return eligible.slice(0, limit);
|
|
348
585
|
}
|
|
349
586
|
|
|
587
|
+
// src/hook/score-floor.ts
|
|
588
|
+
function parseScoreFloor(raw) {
|
|
589
|
+
if (raw === void 0) return 0;
|
|
590
|
+
const parsed = Number(raw);
|
|
591
|
+
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
|
592
|
+
return parsed;
|
|
593
|
+
}
|
|
594
|
+
function parseRelativeFloor(raw, fallback) {
|
|
595
|
+
if (raw === void 0) return fallback;
|
|
596
|
+
const parsed = Number(raw);
|
|
597
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
598
|
+
return parsed;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/hook/recall-gate.ts
|
|
602
|
+
var RECALL_GATE_MODEL = "qwen3.5:4b";
|
|
603
|
+
var RECALL_GATE_SYSTEM = `You are a recall GATE protecting against off-topic memory injection. Given a USER PROMPT and a CANDIDATE prior-session context, answer irrelevant ONLY when the candidate is CLEARLY about a completely different topic, project, or task than the prompt (e.g. the prompt is about debugging a website and the candidate is about a trading pipeline). If there is ANY plausible topical connection, or you are at all unsure, answer relevant. Dropping a useful memory is worse than keeping a marginal one. You do NOT see the assistant's answer. Output {"gate":"relevant"|"irrelevant"}.`;
|
|
604
|
+
var GATE_FORMAT = { type: "object", properties: { gate: { type: "string", enum: ["relevant", "irrelevant"] } }, required: ["gate"] };
|
|
605
|
+
var GATE_OPTS = { temperature: 0, top_p: 1, top_k: 0, presence_penalty: 0, frequency_penalty: 0 };
|
|
606
|
+
function parseRecallGateMode(env = process.env) {
|
|
607
|
+
const v = env["NLM_HOOK_RECALL_GATE"]?.trim();
|
|
608
|
+
return v === "shadow" || v === "live" ? v : void 0;
|
|
609
|
+
}
|
|
610
|
+
var GATE_TIMEOUT_MS = 4e3;
|
|
611
|
+
function makeOllamaGate(url, model = RECALL_GATE_MODEL, timeoutMs = GATE_TIMEOUT_MS) {
|
|
612
|
+
return async (prompt, candidate) => {
|
|
613
|
+
try {
|
|
614
|
+
const r = await fetchWithTimeout(
|
|
615
|
+
`${url}/api/chat`,
|
|
616
|
+
{
|
|
617
|
+
method: "POST",
|
|
618
|
+
headers: { "Content-Type": "application/json" },
|
|
619
|
+
body: JSON.stringify({
|
|
620
|
+
model,
|
|
621
|
+
stream: false,
|
|
622
|
+
think: false,
|
|
623
|
+
// Keep the gate model resident between fires so only the first fire
|
|
624
|
+
// pays the cold-load; subsequent gates are ~1 judge call.
|
|
625
|
+
keep_alive: "15m",
|
|
626
|
+
format: GATE_FORMAT,
|
|
627
|
+
options: GATE_OPTS,
|
|
628
|
+
messages: [
|
|
629
|
+
{ role: "system", content: RECALL_GATE_SYSTEM },
|
|
630
|
+
{ role: "user", content: `USER PROMPT:
|
|
631
|
+
${prompt}
|
|
632
|
+
|
|
633
|
+
CANDIDATE CONTEXT:
|
|
634
|
+
${candidate}` }
|
|
635
|
+
]
|
|
636
|
+
})
|
|
637
|
+
},
|
|
638
|
+
timeoutMs
|
|
639
|
+
);
|
|
640
|
+
const d = await r.json();
|
|
641
|
+
const v = JSON.parse(d.message?.content ?? "{}").gate;
|
|
642
|
+
return v === "irrelevant" ? "irrelevant" : "relevant";
|
|
643
|
+
} catch {
|
|
644
|
+
return "relevant";
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
350
649
|
// src/hook/prompt-recall-hook.ts
|
|
351
|
-
var SCORE_THRESHOLD =
|
|
650
|
+
var SCORE_THRESHOLD = parseScoreFloor(process.env["NLM_RECALL_SCORE_FLOOR"]);
|
|
651
|
+
var RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
|
|
352
652
|
var PER_FIRE_CAP = 3;
|
|
353
653
|
var PER_CONVERSATION_CAP = 10;
|
|
354
654
|
var PROMPT_PREVIEW_CHARS = 200;
|
|
655
|
+
function parseHookDeadline(raw) {
|
|
656
|
+
if (raw === void 0) return 4e3;
|
|
657
|
+
const n = Number(raw);
|
|
658
|
+
return Number.isFinite(n) && n > 0 ? n : 4e3;
|
|
659
|
+
}
|
|
660
|
+
var HOOK_DEADLINE_MS = parseHookDeadline(process.env["NLM_HOOK_DEADLINE_MS"]);
|
|
661
|
+
async function withDeadline(p, ms, fallback) {
|
|
662
|
+
if (ms <= 0) return fallback;
|
|
663
|
+
let timer;
|
|
664
|
+
const timeout = new Promise((resolve2) => {
|
|
665
|
+
timer = setTimeout(() => resolve2(fallback), ms);
|
|
666
|
+
});
|
|
667
|
+
try {
|
|
668
|
+
return await Promise.race([p, timeout]);
|
|
669
|
+
} finally {
|
|
670
|
+
clearTimeout(timer);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
355
673
|
function hookRuntimeFromEnv(env = process.env) {
|
|
356
674
|
const raw = env["NLM_HOOK_RUNTIME"]?.trim();
|
|
357
675
|
return raw ? raw : "claude-code";
|
|
358
676
|
}
|
|
677
|
+
function promptRecallEnabled(env = process.env) {
|
|
678
|
+
const raw = env["NLM_HOOK_PROMPT_RECALL"]?.trim();
|
|
679
|
+
if (raw === void 0 || raw === "") return false;
|
|
680
|
+
return raw.toLowerCase() !== "off";
|
|
681
|
+
}
|
|
682
|
+
function buildRecallQuery(input, env = process.env) {
|
|
683
|
+
if (env["NLM_HOOK_CONTEXT_RECALL"] !== "1") return input.prompt;
|
|
684
|
+
if (!input.transcriptPath) return input.prompt;
|
|
685
|
+
const minWords = Number.parseInt(env["NLM_HOOK_CONTEXT_MIN_WORDS"] ?? "3", 10);
|
|
686
|
+
if (topicalWordCount(input.prompt) >= minWords) return input.prompt;
|
|
687
|
+
const context = recentConversationContext(input.transcriptPath);
|
|
688
|
+
return context ? `${context} ${input.prompt}` : input.prompt;
|
|
689
|
+
}
|
|
359
690
|
function normalizeRecall(raw) {
|
|
360
691
|
if (Array.isArray(raw)) return { hits: raw, facts: [] };
|
|
361
692
|
return raw;
|
|
@@ -363,7 +694,7 @@ function normalizeRecall(raw) {
|
|
|
363
694
|
async function runHook(input, deps) {
|
|
364
695
|
const gate = classifyPrompt(input.prompt);
|
|
365
696
|
const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
|
|
366
|
-
if (gate === "generative") {
|
|
697
|
+
if (gate === "generative" || gate === "skip") {
|
|
367
698
|
appendHookLog({
|
|
368
699
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
369
700
|
conversationId: input.conversationId,
|
|
@@ -376,9 +707,12 @@ async function runHook(input, deps) {
|
|
|
376
707
|
});
|
|
377
708
|
return "";
|
|
378
709
|
}
|
|
710
|
+
const deadline = Date.now() + (deps.deadlineMs ?? HOOK_DEADLINE_MS);
|
|
379
711
|
let fetched = { hits: [], facts: [] };
|
|
380
712
|
try {
|
|
381
|
-
fetched = normalizeRecall(
|
|
713
|
+
fetched = normalizeRecall(
|
|
714
|
+
await withDeadline(deps.recall(buildRecallQuery(input)), deadline - Date.now(), { hits: [], facts: [] })
|
|
715
|
+
);
|
|
382
716
|
} catch {
|
|
383
717
|
fetched = { hits: [], facts: [] };
|
|
384
718
|
}
|
|
@@ -388,10 +722,33 @@ async function runHook(input, deps) {
|
|
|
388
722
|
hits,
|
|
389
723
|
surfaced,
|
|
390
724
|
scoreThreshold: SCORE_THRESHOLD,
|
|
725
|
+
relativeFloor: RELATIVE_FLOOR,
|
|
391
726
|
perFireCap: PER_FIRE_CAP,
|
|
392
727
|
perConversationCap: PER_CONVERSATION_CAP
|
|
393
728
|
});
|
|
394
|
-
|
|
729
|
+
let gateDecisions;
|
|
730
|
+
let injected = selected;
|
|
731
|
+
if (deps.recallGate && selected.length > 0) {
|
|
732
|
+
const g = deps.recallGate;
|
|
733
|
+
const toGate = g.maxCandidates ? selected.slice(0, g.maxCandidates) : selected;
|
|
734
|
+
const remaining = deadline - Date.now();
|
|
735
|
+
const gateFallback = toGate.map((h) => ({ id: h.id, gate: "relevant" }));
|
|
736
|
+
if (remaining <= 0) {
|
|
737
|
+
gateDecisions = gateFallback;
|
|
738
|
+
} else {
|
|
739
|
+
gateDecisions = await withDeadline(
|
|
740
|
+
Promise.all(toGate.map(async (h) => ({ id: h.id, gate: await g.judge(input.prompt, `${h.label}
|
|
741
|
+
${h.summary ?? ""}`) }))),
|
|
742
|
+
remaining,
|
|
743
|
+
gateFallback
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
if (g.mode === "live") {
|
|
747
|
+
const drop = new Set(gateDecisions.filter((d) => d.gate === "irrelevant").map((d) => d.id));
|
|
748
|
+
injected = selected.filter((h) => !drop.has(h.id));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const block = formatPointerBlock(injected, fetched.facts, fetched.exemplars);
|
|
395
752
|
const estTokens = Math.ceil(block.length / 4);
|
|
396
753
|
appendHookLog({
|
|
397
754
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -399,25 +756,17 @@ async function runHook(input, deps) {
|
|
|
399
756
|
promptPreview: preview,
|
|
400
757
|
gate,
|
|
401
758
|
hits: hits.map((h) => ({ id: h.id, score: h.matchScore })),
|
|
402
|
-
wouldInject:
|
|
759
|
+
wouldInject: injected.map((h) => h.id),
|
|
403
760
|
estTokens,
|
|
404
|
-
mode: deps.mode
|
|
761
|
+
mode: deps.mode,
|
|
762
|
+
...gateDecisions ? { gateDecisions } : {}
|
|
405
763
|
});
|
|
406
|
-
if (deps.mode === "live" &&
|
|
407
|
-
recordSurfaced(input.conversationId,
|
|
764
|
+
if (deps.mode === "live" && injected.length > 0) {
|
|
765
|
+
recordSurfaced(input.conversationId, injected.map((h) => h.id));
|
|
408
766
|
return block;
|
|
409
767
|
}
|
|
410
768
|
return "";
|
|
411
769
|
}
|
|
412
|
-
function readStdin() {
|
|
413
|
-
return new Promise((resolve2) => {
|
|
414
|
-
let data = "";
|
|
415
|
-
process.stdin.setEncoding("utf8");
|
|
416
|
-
process.stdin.on("data", (chunk) => data += chunk);
|
|
417
|
-
process.stdin.on("end", () => resolve2(data));
|
|
418
|
-
process.stdin.on("error", () => resolve2(data));
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
770
|
async function main() {
|
|
422
771
|
try {
|
|
423
772
|
autoloadEnv();
|
|
@@ -425,12 +774,21 @@ async function main() {
|
|
|
425
774
|
const payload = JSON.parse(raw);
|
|
426
775
|
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
427
776
|
const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
777
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : void 0;
|
|
428
778
|
if (!prompt) return;
|
|
429
|
-
|
|
779
|
+
if (!promptRecallEnabled()) return;
|
|
780
|
+
const mode = hookModeFromEnv();
|
|
430
781
|
const runtime = hookRuntimeFromEnv();
|
|
782
|
+
const gateMode = parseRecallGateMode();
|
|
783
|
+
const gateUrl = process.env["OLLAMA_URL"] ?? "http://127.0.0.1:11434";
|
|
784
|
+
const gateTopN = Math.max(1, Number.parseInt(process.env["NLM_HOOK_RECALL_GATE_TOPN"] ?? "1", 10) || 1);
|
|
431
785
|
const out = await runHook(
|
|
432
|
-
{ prompt, conversationId },
|
|
433
|
-
{
|
|
786
|
+
{ prompt, conversationId, ...transcriptPath ? { transcriptPath } : {} },
|
|
787
|
+
{
|
|
788
|
+
mode,
|
|
789
|
+
recall: (q) => recallOverHttp(q, runtime, conversationId === "unknown" ? void 0 : conversationId),
|
|
790
|
+
...gateMode ? { recallGate: { mode: gateMode, judge: makeOllamaGate(gateUrl), maxCandidates: gateTopN } } : {}
|
|
791
|
+
}
|
|
434
792
|
);
|
|
435
793
|
if (out) process.stdout.write(out);
|
|
436
794
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nlm-memory",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Local-first non-linear memory operating system for AI operators.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
31
|
"migrations",
|
|
32
|
+
"fixtures",
|
|
33
|
+
"templates",
|
|
32
34
|
"plugin",
|
|
33
35
|
"plugin-hermes-agent",
|
|
34
36
|
"nlm",
|
|
@@ -52,16 +54,24 @@
|
|
|
52
54
|
"test:watch": "vitest",
|
|
53
55
|
"test:unit": "vitest run tests/unit",
|
|
54
56
|
"test:integration": "vitest run tests/integration",
|
|
57
|
+
"test:pg": "vitest run --no-file-parallelism .pg.test.ts",
|
|
55
58
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
56
59
|
"bench:longmemeval": "tsx scripts/longmemeval/run-harness.ts",
|
|
57
60
|
"bench:classifier": "tsx scripts/longmemeval/run-harness.ts --classifier",
|
|
58
61
|
"bench:compare": "tsx scripts/longmemeval/compare-classifiers.ts",
|
|
59
62
|
"eval:signals": "tsx scripts/eval/signals-eval.ts",
|
|
60
|
-
"eval:classifier": "tsx scripts/eval/classifier-eval.ts"
|
|
63
|
+
"eval:classifier": "tsx scripts/eval/classifier-eval.ts",
|
|
64
|
+
"eval:fact-recall": "tsx scripts/eval/fact-recall-eval.ts",
|
|
65
|
+
"eval:code-exemplar": "tsx scripts/eval/code-exemplar-eval.ts",
|
|
66
|
+
"eval:reranker": "tsx scripts/eval/reranker-ablation.ts",
|
|
67
|
+
"eval:floor": "tsx scripts/eval/floor-calibration.ts",
|
|
68
|
+
"eval:matcher": "tsx scripts/eval/tune-matcher.ts",
|
|
69
|
+
"eval:naming": "tsx scripts/eval/tune-naming.ts"
|
|
61
70
|
},
|
|
62
71
|
"dependencies": {
|
|
63
72
|
"@clack/prompts": "^1.4.0",
|
|
64
73
|
"@hono/node-server": "^2.0.3",
|
|
74
|
+
"@huggingface/transformers": "4.2.0",
|
|
65
75
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
76
|
"@toon-format/toon": "^2.3.0",
|
|
67
77
|
"better-sqlite3": "^12.10.0",
|