@jungjaehoon/mama-core 2.4.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -239
- package/db/migrations/030-case-first-memory-substrate.sql +13 -98
- package/db/migrations/034-add-connector-event-scope-columns.sql +9 -13
- package/db/migrations/039-add-connector-event-operator-ingest-seq.sql +9 -95
- package/db/migrations/062-refresh-connector-event-sequences.sql +9 -113
- package/db/migrations/063-refresh-legacy-connector-event-updates.sql +9 -32
- package/db/migrations/067-add-connector-event-source-entity-id.sql +9 -13
- package/db/migrations/069-create-registry-nodes.sql +52 -0
- package/db/migrations/070-record-identity.sql +35 -0
- package/db/migrations/071-service-operation-origins.sql +187 -0
- package/db/migrations/072-observation-versions.sql +29 -0
- package/db/migrations/073-registry-corrections.sql +45 -0
- package/db/migrations/074-work-graph-ref-kinds.sql +39 -0
- package/db/migrations/075-agent-judgments.sql +58 -0
- package/db/migrations/076-scoped-checkpoint-bindings.sql +15 -0
- package/db/migrations/077-legacy-record-kind.sql +6 -0
- package/db/migrations/078-source-ingest-commands.sql +18 -0
- package/db/migrations/079-twin-edge-relations.sql +43 -0
- package/db/migrations/080-restore-duration-days.sql +7 -0
- package/db/migrations/081-mailbox.sql +61 -0
- package/db/migrations/082-commitments-row-id.sql +12 -0
- package/db/migrations/083-observation-is-the-evidence.sql +49 -0
- package/db/migrations/084-scope-kind-is-not-the-cores-to-list.sql +34 -0
- package/db/migrations/085-a-migration-has-a-source.sql +37 -0
- package/db/migrations/086-the-mailbox-is-generic.sql +110 -0
- package/db/migrations/087-mailbox-payload.sql +5 -0
- package/db/migrations/088-native-input-journal.sql +14 -0
- package/db/migrations/089-native-turn-receipt-lookup.sql +3 -0
- package/db/migrations/090-model-run-native-inputs.sql +29 -0
- package/db/migrations/091-native-turn-results.sql +11 -0
- package/db/migrations/092-restore-decision-fts.sql +34 -0
- package/db/migrations/093-native-turn-primary-kind.sql +14 -0
- package/db/migrations/094-commitment-run-provenance.sql +9 -0
- package/db/migrations/095-model-run-native-input-identity.sql +39 -0
- package/db/migrations/096-drop-connector-event-index-fts.sql +9 -0
- package/db/migrations/097-native-input-json-validity.sql +37 -0
- package/db/migrations/098-workflow-memory-kind.sql +105 -0
- package/dist/action-contracts.d.ts +212 -0
- package/dist/action-contracts.js +10 -0
- package/dist/api/catalog.d.ts +118 -0
- package/dist/api/catalog.js +1756 -0
- package/dist/api/dispatch.d.ts +76 -0
- package/dist/api/dispatch.js +372 -0
- package/dist/canonicalize.d.ts +2 -2
- package/dist/canonicalize.js +3 -3
- package/dist/client/client.d.ts +33 -0
- package/dist/client/client.js +123 -0
- package/dist/client/ipc.d.ts +81 -0
- package/dist/client/ipc.js +314 -0
- package/dist/db-adapter/base-adapter.d.ts +6 -66
- package/dist/db-adapter/base-adapter.js +6 -6
- package/dist/db-adapter/index.d.ts +5 -5
- package/dist/db-adapter/index.js +2 -5
- package/dist/db-adapter/node-sqlite-adapter.d.ts +85 -15
- package/dist/db-adapter/node-sqlite-adapter.js +1968 -570
- package/dist/db-manager.d.ts +53 -97
- package/dist/db-manager.js +121 -603
- package/dist/embedding/embedder.d.ts +132 -0
- package/dist/embedding/embedder.js +299 -0
- package/dist/identity/principal-repository.d.ts +2 -2
- package/dist/index.d.ts +43 -58
- package/dist/index.js +97 -145
- package/dist/{context-compile/channel-grant.d.ts → knowledge/access.d.ts} +12 -17
- package/dist/knowledge/access.js +663 -0
- package/dist/knowledge/case-errors.d.ts +31 -0
- package/dist/knowledge/case-errors.js +40 -0
- package/dist/{cases/search-rollup.d.ts → knowledge/case-search-rollup.d.ts} +5 -4
- package/dist/{cases/search-rollup.js → knowledge/case-search-rollup.js} +6 -8
- package/dist/{cases/store.d.ts → knowledge/case-store.d.ts} +3 -6
- package/dist/{cases/store.js → knowledge/case-store.js} +7 -42
- package/dist/{cases/timeline-range.d.ts → knowledge/case-timeline-range.d.ts} +3 -3
- package/dist/{cases/timeline-range.js → knowledge/case-timeline-range.js} +47 -25
- package/dist/{cases/types.d.ts → knowledge/case-types.d.ts} +8 -2
- package/dist/{cases/types.js → knowledge/case-types.js} +1 -1
- package/dist/knowledge/commitments.d.ts +188 -0
- package/dist/knowledge/commitments.js +370 -0
- package/dist/knowledge/decision-edges.d.ts +70 -0
- package/dist/knowledge/decision-edges.js +123 -0
- package/dist/{agent-graph/types.d.ts → knowledge/graph-query.d.ts} +80 -70
- package/dist/knowledge/graph-query.js +1585 -0
- package/dist/knowledge/identity.d.ts +16 -0
- package/dist/knowledge/identity.js +22 -0
- package/dist/knowledge/index.d.ts +47 -0
- package/dist/knowledge/index.js +90 -0
- package/dist/knowledge/judgments.d.ts +118 -0
- package/dist/knowledge/judgments.js +808 -0
- package/dist/knowledge/observations.d.ts +160 -0
- package/dist/knowledge/observations.js +449 -0
- package/dist/knowledge/question-type.d.ts +34 -0
- package/dist/knowledge/question-type.js +70 -0
- package/dist/{search → knowledge}/ranker-features.js +1 -1
- package/dist/{search → knowledge}/search-quality.d.ts +0 -1
- package/dist/knowledge/search.d.ts +38 -0
- package/dist/knowledge/search.js +91 -0
- package/dist/knowledge/source-ingest.d.ts +65 -0
- package/dist/knowledge/source-ingest.js +174 -0
- package/dist/{edges/types.d.ts → knowledge/twin-edge-types.d.ts} +31 -4
- package/dist/{edges/types.js → knowledge/twin-edge-types.js} +13 -2
- package/dist/knowledge/work-dates.d.ts +49 -0
- package/dist/knowledge/work-dates.js +120 -0
- package/dist/mama-api.d.ts +114 -778
- package/dist/mama-api.js +313 -2461
- package/dist/memory/api.d.ts +264 -24
- package/dist/memory/api.js +1633 -843
- package/dist/memory/bootstrap-builder.d.ts +2 -1
- package/dist/memory/bootstrap-builder.js +5 -5
- package/dist/memory/channel-summary-state-store.d.ts +3 -2
- package/dist/memory/channel-summary-state-store.js +53 -51
- package/dist/memory/channel-summary-store.d.ts +5 -3
- package/dist/memory/channel-summary-store.js +3 -8
- package/dist/memory/dashboard-read.d.ts +46 -0
- package/dist/memory/dashboard-read.js +94 -0
- package/dist/memory/event-store.d.ts +3 -3
- package/dist/memory/event-store.js +3 -10
- package/dist/memory/finding-store.d.ts +3 -2
- package/dist/memory/finding-store.js +2 -7
- package/dist/memory/graph-read.d.ts +52 -0
- package/dist/memory/graph-read.js +130 -0
- package/dist/memory/judgment-types.d.ts +394 -0
- package/dist/memory/judgment-types.js +16 -0
- package/dist/memory/provenance-audit.d.ts +5 -2
- package/dist/memory/provenance-audit.js +8 -14
- package/dist/memory/provenance-live.d.ts +141 -0
- package/dist/memory/provenance-live.js +409 -0
- package/dist/memory/provenance-query.d.ts +5 -4
- package/dist/memory/provenance-query.js +21 -29
- package/dist/memory/provenance-resolver.d.ts +191 -0
- package/dist/memory/provenance-resolver.js +155 -0
- package/dist/memory/provenance.d.ts +1 -11
- package/dist/memory/provenance.js +2 -33
- package/dist/memory/recall-sanitize.d.ts +55 -0
- package/dist/memory/recall-sanitize.js +144 -0
- package/dist/memory/secret-filter.d.ts +32 -0
- package/dist/memory/secret-filter.js +126 -0
- package/dist/memory/truth-store.d.ts +2 -1
- package/dist/memory/truth-store.js +1 -4
- package/dist/memory/types.d.ts +74 -24
- package/dist/memory/types.js +75 -5
- package/dist/memory/write-adapters.d.ts +98 -0
- package/dist/memory/write-adapters.js +242 -0
- package/dist/operations/owner-action-effects.d.ts +53 -0
- package/dist/operations/owner-action-effects.js +96 -0
- package/dist/provenance/source-ref.d.ts +1 -1
- package/dist/provenance/source-ref.js +0 -2
- package/dist/registry/corrections.d.ts +31 -0
- package/dist/registry/corrections.js +332 -0
- package/dist/registry/record-identity.d.ts +49 -0
- package/dist/registry/record-identity.js +133 -0
- package/dist/registry/store.d.ts +101 -0
- package/dist/registry/store.js +395 -0
- package/dist/registry/types.d.ts +28 -0
- package/dist/registry/types.js +5 -0
- package/dist/relevance-scorer.js +2 -2
- package/dist/runtime/agent-event-bus.d.ts +50 -0
- package/dist/runtime/agent-event-bus.js +101 -0
- package/dist/runtime/drivers/claude-cli-wrapper.d.ts +155 -0
- package/dist/runtime/drivers/claude-cli-wrapper.js +392 -0
- package/dist/runtime/drivers/cli-arg-redaction.d.ts +2 -0
- package/dist/runtime/drivers/cli-arg-redaction.js +18 -0
- package/dist/runtime/drivers/cli-secret-redaction.d.ts +17 -0
- package/dist/runtime/drivers/cli-secret-redaction.js +126 -0
- package/dist/runtime/drivers/codex-app-server-process.d.ts +277 -0
- package/dist/runtime/drivers/codex-app-server-process.js +2342 -0
- package/dist/runtime/drivers/codex-auxiliary-tools.d.ts +17 -0
- package/dist/runtime/drivers/codex-auxiliary-tools.js +267 -0
- package/dist/runtime/drivers/codex-home.d.ts +20 -0
- package/dist/runtime/drivers/codex-home.js +674 -0
- package/dist/runtime/drivers/codex-thread-registry.d.ts +63 -0
- package/dist/runtime/drivers/codex-thread-registry.js +433 -0
- package/dist/runtime/drivers/persistent-cli-adapter.d.ts +160 -0
- package/dist/runtime/drivers/persistent-cli-adapter.js +396 -0
- package/dist/runtime/drivers/persistent-cli-process.d.ts +424 -0
- package/dist/runtime/drivers/persistent-cli-process.js +1543 -0
- package/dist/runtime/drivers/types.d.ts +456 -0
- package/dist/runtime/drivers/types.js +110 -0
- package/dist/runtime/mailbox.d.ts +223 -0
- package/dist/runtime/mailbox.js +582 -0
- package/dist/runtime/model-run-store.d.ts +28 -0
- package/dist/{model-runs/store.js → runtime/model-run-store.js} +81 -41
- package/dist/{model-runs/types.d.ts → runtime/model-run-types.d.ts} +33 -4
- package/dist/{model-runs/types.js → runtime/model-run-types.js} +1 -1
- package/dist/runtime/native-effect-observer.d.ts +22 -0
- package/dist/runtime/native-effect-observer.js +103 -0
- package/dist/runtime/native-input-journal.d.ts +46 -0
- package/dist/runtime/native-input-journal.js +308 -0
- package/dist/runtime/native-prompt.d.ts +118 -0
- package/dist/runtime/native-prompt.js +496 -0
- package/dist/runtime/native-session.d.ts +10 -0
- package/dist/{agent-graph/index.js → runtime/native-session.js} +9 -6
- package/dist/runtime/native-tool-trace-observer.d.ts +5 -0
- package/dist/runtime/native-tool-trace-observer.js +65 -0
- package/dist/runtime/native-turn.d.ts +272 -0
- package/dist/runtime/native-turn.js +1041 -0
- package/dist/runtime/operations.d.ts +236 -0
- package/dist/runtime/operations.js +565 -0
- package/dist/runtime/post-tool-handler.d.ts +74 -0
- package/dist/runtime/post-tool-handler.js +137 -0
- package/dist/runtime/prompt-layers.d.ts +126 -0
- package/dist/runtime/prompt-layers.js +239 -0
- package/dist/runtime/runtime-process.d.ts +102 -0
- package/dist/runtime/runtime-process.js +206 -0
- package/dist/runtime/runtime.d.ts +278 -0
- package/dist/runtime/runtime.js +563 -0
- package/dist/runtime/session-pool.d.ts +176 -0
- package/dist/runtime/session-pool.js +369 -0
- package/dist/runtime/stimulus-payload.d.ts +5 -0
- package/dist/runtime/stimulus-payload.js +50 -0
- package/dist/runtime/subagent-bridge.d.ts +24 -0
- package/dist/{agent-graph/types.js → runtime/subagent-bridge.js} +1 -1
- package/dist/runtime/text-completion.d.ts +22 -0
- package/dist/{agent-situation/types.js → runtime/text-completion.js} +1 -1
- package/dist/runtime/token-estimator.d.ts +25 -0
- package/dist/runtime/token-estimator.js +75 -0
- package/dist/runtime/tool-trace-store.d.ts +22 -0
- package/dist/{model-runs → runtime}/tool-trace-store.js +46 -27
- package/dist/runtime/trace-summary.d.ts +5 -0
- package/dist/runtime/trace-summary.js +74 -0
- package/dist/runtime/turn-text.d.ts +23 -0
- package/dist/runtime/turn-text.js +41 -0
- package/dist/storage/database.d.ts +67 -0
- package/dist/storage/database.js +158 -0
- package/package.json +20 -34
- package/dist/agent-graph/alias-write.d.ts +0 -3
- package/dist/agent-graph/alias-write.js +0 -234
- package/dist/agent-graph/entity-resolve.d.ts +0 -3
- package/dist/agent-graph/entity-resolve.js +0 -271
- package/dist/agent-graph/errors.d.ts +0 -4
- package/dist/agent-graph/errors.js +0 -11
- package/dist/agent-graph/graph-query.d.ts +0 -5
- package/dist/agent-graph/graph-query.js +0 -360
- package/dist/agent-graph/index.d.ts +0 -6
- package/dist/agent-situation/builder.d.ts +0 -8
- package/dist/agent-situation/builder.js +0 -239
- package/dist/agent-situation/cache-key.d.ts +0 -4
- package/dist/agent-situation/cache-key.js +0 -64
- package/dist/agent-situation/index.d.ts +0 -7
- package/dist/agent-situation/index.js +0 -28
- package/dist/agent-situation/packet-store.d.ts +0 -27
- package/dist/agent-situation/packet-store.js +0 -228
- package/dist/agent-situation/ranking-policy.d.ts +0 -18
- package/dist/agent-situation/ranking-policy.js +0 -75
- package/dist/agent-situation/source-readers.d.ts +0 -52
- package/dist/agent-situation/source-readers.js +0 -300
- package/dist/agent-situation/types.d.ts +0 -195
- package/dist/cases/wiki-page-index.d.ts +0 -37
- package/dist/cases/wiki-page-index.js +0 -201
- package/dist/config-loader.d.ts +0 -68
- package/dist/config-loader.js +0 -239
- package/dist/connectors/event-index.d.ts +0 -24
- package/dist/connectors/event-index.js +0 -245
- package/dist/connectors/raw-query.d.ts +0 -40
- package/dist/connectors/raw-query.js +0 -412
- package/dist/connectors/types.d.ts +0 -117
- package/dist/connectors/types.js +0 -3
- package/dist/context-compile/boundary-defaults.d.ts +0 -12
- package/dist/context-compile/boundary-defaults.js +0 -105
- package/dist/context-compile/channel-grant.js +0 -66
- package/dist/context-compile/compiler-policy.d.ts +0 -39
- package/dist/context-compile/compiler-policy.js +0 -147
- package/dist/context-compile/compiler.d.ts +0 -18
- package/dist/context-compile/compiler.js +0 -404
- package/dist/context-compile/index.d.ts +0 -10
- package/dist/context-compile/index.js +0 -26
- package/dist/context-compile/packet-store.d.ts +0 -8
- package/dist/context-compile/packet-store.js +0 -239
- package/dist/context-compile/ref.d.ts +0 -7
- package/dist/context-compile/ref.js +0 -102
- package/dist/context-compile/source-readers.d.ts +0 -70
- package/dist/context-compile/source-readers.js +0 -840
- package/dist/context-compile/types.d.ts +0 -155
- package/dist/context-compile/types.js +0 -11
- package/dist/context-compile/visibility.d.ts +0 -21
- package/dist/context-compile/visibility.js +0 -224
- package/dist/decision-tracker.d.ts +0 -206
- package/dist/decision-tracker.js +0 -481
- package/dist/edges/ref-validation.d.ts +0 -7
- package/dist/edges/ref-validation.js +0 -321
- package/dist/edges/store.d.ts +0 -8
- package/dist/edges/store.js +0 -129
- package/dist/embedding-client.d.ts +0 -28
- package/dist/embedding-client.js +0 -53
- package/dist/embedding-server/index.d.ts +0 -67
- package/dist/embedding-server/index.js +0 -405
- package/dist/embedding-server/mobile/auth.d.ts +0 -44
- package/dist/embedding-server/mobile/auth.js +0 -150
- package/dist/embedding-server/mobile/daemon.d.ts +0 -129
- package/dist/embedding-server/mobile/daemon.js +0 -313
- package/dist/embedding-server/mobile/output-parser.d.ts +0 -115
- package/dist/embedding-server/mobile/output-parser.js +0 -241
- package/dist/embedding-server/mobile/session-api.d.ts +0 -57
- package/dist/embedding-server/mobile/session-api.js +0 -261
- package/dist/embedding-server/mobile/session-manager.d.ts +0 -138
- package/dist/embedding-server/mobile/session-manager.js +0 -378
- package/dist/embedding-server/mobile/websocket-handler.d.ts +0 -137
- package/dist/embedding-server/mobile/websocket-handler.js +0 -486
- package/dist/embeddings.d.ts +0 -68
- package/dist/embeddings.js +0 -244
- package/dist/entities/audit-metrics.d.ts +0 -68
- package/dist/entities/audit-metrics.js +0 -124
- package/dist/entities/candidate-generator.d.ts +0 -8
- package/dist/entities/candidate-generator.js +0 -24
- package/dist/entities/entity-linked-decision-counts.d.ts +0 -2
- package/dist/entities/entity-linked-decision-counts.js +0 -29
- package/dist/entities/entity-list.d.ts +0 -22
- package/dist/entities/entity-list.js +0 -3
- package/dist/entities/entity-orphan-list.d.ts +0 -17
- package/dist/entities/entity-orphan-list.js +0 -3
- package/dist/entities/entity-search.d.ts +0 -20
- package/dist/entities/entity-search.js +0 -3
- package/dist/entities/errors.d.ts +0 -55
- package/dist/entities/errors.js +0 -100
- package/dist/entities/lineage-store.d.ts +0 -28
- package/dist/entities/lineage-store.js +0 -204
- package/dist/entities/normalization.d.ts +0 -14
- package/dist/entities/normalization.js +0 -63
- package/dist/entities/policy-store.d.ts +0 -8
- package/dist/entities/policy-store.js +0 -75
- package/dist/entities/policy-types.d.ts +0 -72
- package/dist/entities/policy-types.js +0 -59
- package/dist/entities/projection.d.ts +0 -8
- package/dist/entities/projection.js +0 -82
- package/dist/entities/provenance-query.d.ts +0 -32
- package/dist/entities/provenance-query.js +0 -3
- package/dist/entities/read-identity.d.ts +0 -31
- package/dist/entities/read-identity.js +0 -264
- package/dist/entities/recall-bridge.d.ts +0 -5
- package/dist/entities/recall-bridge.js +0 -171
- package/dist/entities/resolution-engine.d.ts +0 -8
- package/dist/entities/resolution-engine.js +0 -3
- package/dist/entities/rollback-preview.d.ts +0 -35
- package/dist/entities/rollback-preview.js +0 -3
- package/dist/entities/source-locator.d.ts +0 -3
- package/dist/entities/source-locator.js +0 -32
- package/dist/entities/store.d.ts +0 -69
- package/dist/entities/store.js +0 -544
- package/dist/entities/types.d.ts +0 -166
- package/dist/entities/types.js +0 -30
- package/dist/memory/extraction-prompt.d.ts +0 -4
- package/dist/memory/extraction-prompt.js +0 -89
- package/dist/memory/scope-backfill.d.ts +0 -14
- package/dist/memory/scope-backfill.js +0 -3
- package/dist/memory/scope-store.d.ts +0 -4
- package/dist/memory/scope-store.js +0 -9
- package/dist/memory-store.d.ts +0 -87
- package/dist/memory-store.js +0 -78
- package/dist/model-runs/store.d.ts +0 -13
- package/dist/model-runs/tool-trace-store.d.ts +0 -8
- package/dist/notification-manager.d.ts +0 -7
- package/dist/notification-manager.js +0 -12
- package/dist/outcome-tracker.d.ts +0 -151
- package/dist/outcome-tracker.js +0 -271
- package/dist/query-intent.d.ts +0 -23
- package/dist/query-intent.js +0 -114
- package/dist/runtime/trusted-provenance.d.ts +0 -3
- package/dist/runtime/trusted-provenance.js +0 -6
- package/dist/search/question-type.d.ts +0 -5
- package/dist/search/question-type.js +0 -41
- package/dist/test-utils.d.ts +0 -80
- package/dist/test-utils.js +0 -218
- package/dist/tier-validator.d.ts +0 -43
- package/dist/tier-validator.js +0 -159
- package/dist/time-formatter.d.ts +0 -25
- package/dist/time-formatter.js +0 -93
- /package/dist/{search → knowledge}/feedback-store.d.ts +0 -0
- /package/dist/{search → knowledge}/feedback-store.js +0 -0
- /package/dist/{search → knowledge}/ranker-features.d.ts +0 -0
- /package/dist/{search → knowledge}/ranker-rescore.d.ts +0 -0
- /package/dist/{search → knowledge}/ranker-rescore.js +0 -0
- /package/dist/{search → knowledge}/ranker-trainer.d.ts +0 -0
- /package/dist/{search → knowledge}/ranker-trainer.js +0 -0
- /package/dist/{search → knowledge}/search-quality.js +0 -0
package/dist/memory/api.js
CHANGED
|
@@ -5,42 +5,61 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getChannelSummary = exports.upsertChannelSummary = exports.LEXICAL_STOPWORDS = void 0;
|
|
7
7
|
exports.buildDecisionId = buildDecisionId;
|
|
8
|
+
exports.readMemoryRecordsInScopes = readMemoryRecordsInScopes;
|
|
9
|
+
exports.readMemoryRecordById = readMemoryRecordById;
|
|
8
10
|
exports.getLexicalQueryTokens = getLexicalQueryTokens;
|
|
9
11
|
exports.topicAffinityBoost = topicAffinityBoost;
|
|
10
12
|
exports.loadEdgesForIds = loadEdgesForIds;
|
|
11
13
|
exports.saveMemory = saveMemory;
|
|
12
|
-
exports.
|
|
14
|
+
exports.saveJudgmentRecord = saveJudgmentRecord;
|
|
15
|
+
exports.boundReadScopesFor = boundReadScopesFor;
|
|
16
|
+
exports.saveLegacyMemory = saveLegacyMemory;
|
|
13
17
|
exports.promoteMemoryStatus = promoteMemoryStatus;
|
|
18
|
+
exports.retireMemoryRecord = retireMemoryRecord;
|
|
14
19
|
exports.buildProfile = buildProfile;
|
|
15
20
|
exports.recallMemory = recallMemory;
|
|
16
21
|
exports.ingestMemory = ingestMemory;
|
|
17
|
-
exports.ingestWithTrustedProvenance = ingestWithTrustedProvenance;
|
|
18
22
|
exports.evolveMemory = evolveMemory;
|
|
19
23
|
exports.buildMemoryBootstrap = buildMemoryBootstrap;
|
|
20
24
|
exports.createAuditAck = createAuditAck;
|
|
21
25
|
exports.recordMemoryAudit = recordMemoryAudit;
|
|
22
|
-
exports.setExtractionFn = setExtractionFn;
|
|
23
26
|
exports.ingestConversation = ingestConversation;
|
|
24
|
-
exports.
|
|
27
|
+
exports.expandWithGraphInAdapter = expandWithGraphInAdapter;
|
|
28
|
+
exports.suggestInAdapter = suggestInAdapter;
|
|
29
|
+
exports.listDecisionsInAdapter = listDecisionsInAdapter;
|
|
30
|
+
exports.updateOutcomeInAdapter = updateOutcomeInAdapter;
|
|
31
|
+
exports.readMemoryStatsInAdapter = readMemoryStatsInAdapter;
|
|
32
|
+
exports.saveCheckpointInAdapter = saveCheckpointInAdapter;
|
|
33
|
+
exports.loadCheckpointInAdapter = loadCheckpointInAdapter;
|
|
34
|
+
exports.listCheckpointsInAdapter = listCheckpointsInAdapter;
|
|
25
35
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
36
|
+
const canonicalize_js_1 = require("../canonicalize.js");
|
|
26
37
|
const db_manager_js_1 = require("../db-manager.js");
|
|
27
|
-
const
|
|
28
|
-
const
|
|
38
|
+
const write_adapters_js_1 = require("./write-adapters.js");
|
|
39
|
+
const decision_formatter_js_1 = require("../decision-formatter.js");
|
|
40
|
+
const debug_logger_js_1 = require("../debug-logger.js");
|
|
41
|
+
const graph_query_js_1 = require("../knowledge/graph-query.js");
|
|
42
|
+
const case_search_rollup_js_1 = require("../knowledge/case-search-rollup.js");
|
|
43
|
+
const ranker_rescore_js_1 = require("../knowledge/ranker-rescore.js");
|
|
44
|
+
const ranker_features_js_1 = require("../knowledge/ranker-features.js");
|
|
45
|
+
const search_js_1 = require("../knowledge/search.js");
|
|
46
|
+
const embedder_js_1 = require("../embedding/embedder.js");
|
|
47
|
+
const index_js_1 = require("../knowledge/index.js");
|
|
48
|
+
const write_adapters_js_2 = require("./write-adapters.js");
|
|
29
49
|
const profile_builder_js_1 = require("./profile-builder.js");
|
|
30
50
|
const bootstrap_builder_js_1 = require("./bootstrap-builder.js");
|
|
31
51
|
const evolution_engine_js_1 = require("./evolution-engine.js");
|
|
32
52
|
const channel_summary_state_store_js_1 = require("./channel-summary-state-store.js");
|
|
33
|
-
const
|
|
34
|
-
const
|
|
53
|
+
const debug_logger_js_2 = require("../debug-logger.js");
|
|
54
|
+
const secret_filter_js_1 = require("./secret-filter.js");
|
|
35
55
|
const types_js_1 = require("./types.js");
|
|
36
56
|
const channel_summary_store_js_1 = require("./channel-summary-store.js");
|
|
37
57
|
Object.defineProperty(exports, "getChannelSummary", { enumerable: true, get: function () { return channel_summary_store_js_1.getChannelSummary; } });
|
|
38
58
|
Object.defineProperty(exports, "upsertChannelSummary", { enumerable: true, get: function () { return channel_summary_store_js_1.upsertChannelSummary; } });
|
|
39
|
-
const
|
|
40
|
-
const read_identity_js_1 = require("../entities/read-identity.js");
|
|
41
|
-
const search_quality_js_1 = require("../search/search-quality.js");
|
|
42
|
-
const event_store_js_1 = require("./event-store.js");
|
|
59
|
+
const search_quality_js_1 = require("../knowledge/search-quality.js");
|
|
43
60
|
const provenance_js_1 = require("./provenance.js");
|
|
61
|
+
const judgments_js_1 = require("../knowledge/judgments.js");
|
|
62
|
+
const record_identity_js_1 = require("../registry/record-identity.js");
|
|
44
63
|
function buildDecisionId(topic) {
|
|
45
64
|
// Non-ASCII topics (Korean etc.) used to collapse into bare underscore runs
|
|
46
65
|
// ("decision_______<ts>") - unreadable and near-colliding. Collapse the runs
|
|
@@ -76,12 +95,35 @@ function toMemoryRecord(row, scopes, fallbackSource) {
|
|
|
76
95
|
}
|
|
77
96
|
}
|
|
78
97
|
const savedSource = trustContext?.source;
|
|
98
|
+
let guidance = {};
|
|
99
|
+
if (typeof row.payload_json === 'string') {
|
|
100
|
+
try {
|
|
101
|
+
const payload = JSON.parse(row.payload_json);
|
|
102
|
+
if (payload.guidance &&
|
|
103
|
+
typeof payload.guidance === 'object' &&
|
|
104
|
+
!Array.isArray(payload.guidance)) {
|
|
105
|
+
guidance = payload.guidance;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* malformed payloads do not become guidance */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const steps = Array.isArray(guidance.steps)
|
|
113
|
+
? guidance.steps.filter((step) => typeof step === 'string')
|
|
114
|
+
: undefined;
|
|
115
|
+
const evidenceChecks = Array.isArray(guidance.evidence_checks)
|
|
116
|
+
? guidance.evidence_checks.filter((check) => typeof check === 'string')
|
|
117
|
+
: undefined;
|
|
79
118
|
return {
|
|
80
119
|
id: String(row.id),
|
|
81
120
|
topic: String(row.topic),
|
|
82
121
|
kind: row.kind ?? 'decision',
|
|
83
122
|
summary: String(row.summary ?? row.decision ?? ''),
|
|
84
123
|
details: String(row.reasoning ?? row.decision ?? ''),
|
|
124
|
+
...(typeof guidance.applies_when === 'string' ? { applies_when: guidance.applies_when } : {}),
|
|
125
|
+
...(steps === undefined ? {} : { steps }),
|
|
126
|
+
...(evidenceChecks === undefined ? {} : { evidence_checks: evidenceChecks }),
|
|
85
127
|
confidence: Number(row.confidence ?? 0.5),
|
|
86
128
|
status: row.status ?? 'active',
|
|
87
129
|
scopes,
|
|
@@ -92,124 +134,71 @@ function toMemoryRecord(row, scopes, fallbackSource) {
|
|
|
92
134
|
event_datetime: typeof row.event_datetime === 'number' && Number.isFinite(row.event_datetime)
|
|
93
135
|
? row.event_datetime
|
|
94
136
|
: null,
|
|
137
|
+
outcome: row.outcome ?? null,
|
|
95
138
|
};
|
|
96
139
|
}
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.get(...uniqueIds);
|
|
110
|
-
return typeof row?.event_datetime === 'number' && Number.isFinite(row.event_datetime)
|
|
111
|
-
? row.event_datetime
|
|
112
|
-
: null;
|
|
140
|
+
function scopeBoundRowsClause(scopes, alias = 'd') {
|
|
141
|
+
if (scopes.length === 0)
|
|
142
|
+
return { sql: '0', params: [] };
|
|
143
|
+
const alternatives = scopes.map(() => '(s.kind = ? AND s.external_id = ?)').join(' OR ');
|
|
144
|
+
return {
|
|
145
|
+
sql: `EXISTS (
|
|
146
|
+
SELECT 1 FROM memory_scope_bindings b
|
|
147
|
+
JOIN memory_scopes s ON s.id = b.scope_id
|
|
148
|
+
WHERE b.memory_id = ${alias}.id AND (${alternatives})
|
|
149
|
+
)`,
|
|
150
|
+
params: scopes.flatMap((scope) => [scope.kind, scope.id]),
|
|
151
|
+
};
|
|
113
152
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return 3;
|
|
124
|
-
default:
|
|
125
|
-
return 99;
|
|
153
|
+
/** Read complete memory records whose stored scopes intersect the admitted scopes. */
|
|
154
|
+
async function readMemoryRecordsInScopes(adapter, scopes, options = {}) {
|
|
155
|
+
const scopeClause = scopeBoundRowsClause(scopes);
|
|
156
|
+
const conditions = [scopeClause.sql];
|
|
157
|
+
const params = [...scopeClause.params];
|
|
158
|
+
const kinds = options.kind === undefined ? [] : Array.isArray(options.kind) ? options.kind : [options.kind];
|
|
159
|
+
if (kinds.length > 0) {
|
|
160
|
+
conditions.push(`d.kind IN (${kinds.map(() => '?').join(', ')})`);
|
|
161
|
+
params.push(...kinds);
|
|
126
162
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
163
|
+
const statuses = options.status === undefined
|
|
164
|
+
? []
|
|
165
|
+
: Array.isArray(options.status)
|
|
166
|
+
? options.status
|
|
167
|
+
: [options.status];
|
|
168
|
+
if (statuses.length > 0) {
|
|
169
|
+
conditions.push(`COALESCE(d.status, 'active') IN (${statuses.map(() => '?').join(', ')})`);
|
|
170
|
+
params.push(...statuses);
|
|
132
171
|
}
|
|
133
|
-
const placeholders = uniqueIds.map(() => '?').join(', ');
|
|
134
172
|
const rows = adapter
|
|
135
|
-
.prepare(`
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
AND n.status = 'active'
|
|
146
|
-
AND n.merged_into IS NULL
|
|
147
|
-
GROUP BY n.id, n.kind
|
|
148
|
-
`)
|
|
149
|
-
.all(...uniqueIds);
|
|
150
|
-
rows.sort((left, right) => {
|
|
151
|
-
const kindDelta = getTimelineEntityKindPriority(left.entity_kind) -
|
|
152
|
-
getTimelineEntityKindPriority(right.entity_kind);
|
|
153
|
-
if (kindDelta !== 0) {
|
|
154
|
-
return kindDelta;
|
|
155
|
-
}
|
|
156
|
-
if (right.matched_observations !== left.matched_observations) {
|
|
157
|
-
return right.matched_observations - left.matched_observations;
|
|
158
|
-
}
|
|
159
|
-
return left.entity_id.localeCompare(right.entity_id);
|
|
160
|
-
});
|
|
161
|
-
return rows[0]?.entity_id ?? null;
|
|
173
|
+
.prepare(`SELECT d.id, d.topic, d.decision, d.reasoning, d.confidence, d.created_at, d.updated_at,
|
|
174
|
+
d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime,
|
|
175
|
+
d.outcome, d.payload_json
|
|
176
|
+
FROM decisions d
|
|
177
|
+
WHERE ${conditions.join(' AND ')}
|
|
178
|
+
ORDER BY d.created_at ASC, d.id ASC`)
|
|
179
|
+
.all(...params);
|
|
180
|
+
const scopesById = batchLoadScopes(adapter, rows.map((row) => String(row.id)));
|
|
181
|
+
const fallbackSource = { package: 'mama-core', source_type: 'db' };
|
|
182
|
+
return rows.map((row) => toMemoryRecord(row, scopesById.get(String(row.id)) ?? [], fallbackSource));
|
|
162
183
|
}
|
|
163
|
-
|
|
164
|
-
|
|
184
|
+
/** Read one complete record only when at least one stored scope is admitted. */
|
|
185
|
+
async function readMemoryRecordById(adapter, memoryId, scopes) {
|
|
186
|
+
const id = memoryId.trim();
|
|
187
|
+
const scopeClause = scopeBoundRowsClause(scopes);
|
|
188
|
+
if (!id || scopes.length === 0)
|
|
165
189
|
return null;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
190
|
+
const row = adapter
|
|
191
|
+
.prepare(`SELECT d.id, d.topic, d.decision, d.reasoning, d.confidence, d.created_at, d.updated_at,
|
|
192
|
+
d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime,
|
|
193
|
+
d.outcome, d.payload_json
|
|
194
|
+
FROM decisions d
|
|
195
|
+
WHERE d.id = ? AND ${scopeClause.sql}
|
|
196
|
+
LIMIT 1`)
|
|
197
|
+
.get(id, ...scopeClause.params);
|
|
198
|
+
if (!row)
|
|
170
199
|
return null;
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
id: timelineEvent.id ?? `et_${node_crypto_1.default.randomUUID()}`,
|
|
174
|
-
entity_id: resolvedEntityId,
|
|
175
|
-
event_type: timelineEvent.event_type,
|
|
176
|
-
role: timelineEvent.role ?? null,
|
|
177
|
-
valid_from: timelineEvent.valid_from ?? null,
|
|
178
|
-
valid_to: timelineEvent.valid_to ?? null,
|
|
179
|
-
observed_at: timelineEvent.observed_at ?? null,
|
|
180
|
-
source_ref: timelineEvent.source_ref ?? `decision:${memoryId}`,
|
|
181
|
-
summary: timelineEvent.summary,
|
|
182
|
-
details: timelineEvent.details ??
|
|
183
|
-
JSON.stringify({
|
|
184
|
-
memory_id: memoryId,
|
|
185
|
-
topic,
|
|
186
|
-
}),
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function insertTimelineEventForSave(adapter, event, createdAt) {
|
|
190
|
-
adapter
|
|
191
|
-
.prepare(`
|
|
192
|
-
INSERT INTO entity_timeline_events (
|
|
193
|
-
id, entity_id, event_type, role, valid_from, valid_to, observed_at,
|
|
194
|
-
source_ref, summary, details, created_at
|
|
195
|
-
)
|
|
196
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
197
|
-
`)
|
|
198
|
-
.run(event.id, event.entity_id, event.event_type, event.role, event.valid_from, event.valid_to, event.observed_at, event.source_ref, event.summary, event.details, createdAt);
|
|
199
|
-
}
|
|
200
|
-
function cleanupFailedMemorySave(adapter, memoryId) {
|
|
201
|
-
const row = adapter.prepare(`SELECT rowid FROM decisions WHERE id = ?`).get(memoryId);
|
|
202
|
-
if (!row) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const cleanup = () => {
|
|
206
|
-
adapter.prepare(`DELETE FROM embeddings WHERE rowid = ?`).run(row.rowid);
|
|
207
|
-
adapter.prepare(`DELETE FROM decisions WHERE id = ?`).run(memoryId);
|
|
208
|
-
};
|
|
209
|
-
const txResult = adapter.transaction(cleanup);
|
|
210
|
-
if (typeof txResult === 'function') {
|
|
211
|
-
txResult();
|
|
212
|
-
}
|
|
200
|
+
const recordScopes = batchLoadScopes(adapter, [id]).get(id) ?? [];
|
|
201
|
+
return toMemoryRecord(row, recordScopes, { package: 'mama-core', source_type: 'db' });
|
|
213
202
|
}
|
|
214
203
|
function batchLoadScopes(adapter, memoryIds) {
|
|
215
204
|
const scopeMap = new Map();
|
|
@@ -232,28 +221,27 @@ function batchLoadScopes(adapter, memoryIds) {
|
|
|
232
221
|
}
|
|
233
222
|
return scopeMap;
|
|
234
223
|
}
|
|
235
|
-
async function loadScopedMemories(scopes) {
|
|
236
|
-
await (0, db_manager_js_1.initDB)();
|
|
237
|
-
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
224
|
+
async function loadScopedMemories(adapter, scopes) {
|
|
238
225
|
const fallbackSource = { package: 'mama-core', source_type: 'db' };
|
|
239
226
|
let rows;
|
|
240
227
|
if (scopes.length === 0) {
|
|
241
228
|
rows = adapter
|
|
242
229
|
.prepare(`
|
|
243
230
|
SELECT id, topic, decision, reasoning, confidence, created_at, updated_at, trust_context,
|
|
244
|
-
kind, status, summary, event_date, event_datetime
|
|
231
|
+
kind, status, summary, event_date, event_datetime, outcome, payload_json
|
|
245
232
|
FROM decisions
|
|
246
233
|
ORDER BY COALESCE(event_datetime, created_at) DESC, created_at DESC
|
|
247
234
|
`)
|
|
248
235
|
.all();
|
|
249
236
|
}
|
|
250
237
|
else {
|
|
251
|
-
const scopeIds = await Promise.all(scopes.map((scope) => (0, db_manager_js_1.ensureMemoryScope)(scope.kind, scope.id)));
|
|
238
|
+
const scopeIds = await Promise.all(scopes.map((scope) => (0, db_manager_js_1.ensureMemoryScope)(adapter, scope.kind, scope.id)));
|
|
252
239
|
const placeholders = scopeIds.map(() => '?').join(', ');
|
|
253
240
|
rows = adapter
|
|
254
241
|
.prepare(`
|
|
255
242
|
SELECT DISTINCT d.id, d.topic, d.decision, d.reasoning, d.confidence, d.created_at,
|
|
256
|
-
d.updated_at, d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime
|
|
243
|
+
d.updated_at, d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime,
|
|
244
|
+
d.outcome, d.payload_json
|
|
257
245
|
FROM decisions d
|
|
258
246
|
JOIN memory_scope_bindings msb ON msb.memory_id = d.id
|
|
259
247
|
WHERE msb.scope_id IN (${placeholders})
|
|
@@ -400,12 +388,25 @@ function buildLexicalCandidates(records, query) {
|
|
|
400
388
|
return { memory: record, score };
|
|
401
389
|
})
|
|
402
390
|
.filter((candidate) => candidate.score > 0)
|
|
403
|
-
.sort(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return
|
|
408
|
-
}
|
|
391
|
+
.sort(compareLexicalCandidates);
|
|
392
|
+
}
|
|
393
|
+
function lexicalEventTime(record) {
|
|
394
|
+
if (typeof record.event_datetime === 'number' && Number.isFinite(record.event_datetime)) {
|
|
395
|
+
return record.event_datetime;
|
|
396
|
+
}
|
|
397
|
+
const numericCreated = Number(record.created_at);
|
|
398
|
+
if (Number.isFinite(numericCreated)) {
|
|
399
|
+
return numericCreated;
|
|
400
|
+
}
|
|
401
|
+
const parsedCreated = Date.parse(String(record.created_at));
|
|
402
|
+
return Number.isFinite(parsedCreated) ? parsedCreated : 0;
|
|
403
|
+
}
|
|
404
|
+
function compareLexicalCandidates(left, right) {
|
|
405
|
+
// Relevance wins; equally relevant history follows event time, not import or
|
|
406
|
+
// commit order. FTS rowid order is not an event-time judgment.
|
|
407
|
+
return (right.score - left.score ||
|
|
408
|
+
lexicalEventTime(right.memory) - lexicalEventTime(left.memory) ||
|
|
409
|
+
left.memory.id.localeCompare(right.memory.id));
|
|
409
410
|
}
|
|
410
411
|
function lexicalScoreToConfidence(score) {
|
|
411
412
|
return Math.min(0.95, 0.45 + score * 0.05);
|
|
@@ -460,11 +461,9 @@ function _mergeRecallCandidates(primary, lexical) {
|
|
|
460
461
|
})
|
|
461
462
|
.map((candidate) => candidate.memory);
|
|
462
463
|
}
|
|
463
|
-
async function loadEdgesForIds(ids) {
|
|
464
|
+
async function loadEdgesForIds(adapter, ids) {
|
|
464
465
|
if (ids.length === 0)
|
|
465
466
|
return [];
|
|
466
|
-
await (0, db_manager_js_1.initDB)();
|
|
467
|
-
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
468
467
|
const placeholders = ids.map(() => '?').join(', ');
|
|
469
468
|
const rows = adapter
|
|
470
469
|
.prepare(`SELECT from_id, to_id, relationship AS type, reason
|
|
@@ -479,223 +478,221 @@ async function loadEdgesForIds(ids) {
|
|
|
479
478
|
reason: row.reason ?? undefined,
|
|
480
479
|
}));
|
|
481
480
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
else if (primaryScope) {
|
|
498
|
-
const scopeId = await (0, db_manager_js_1.ensureMemoryScope)(primaryScope.kind, primaryScope.id);
|
|
499
|
-
existingCandidates = adapter
|
|
500
|
-
.prepare(`
|
|
501
|
-
SELECT d.id, d.topic, d.summary, d.kind
|
|
502
|
-
FROM decisions d
|
|
503
|
-
JOIN memory_scope_bindings msb ON msb.memory_id = d.id
|
|
504
|
-
WHERE d.topic = ? AND msb.scope_id = ?
|
|
505
|
-
AND (d.status = 'active' OR d.status IS NULL)
|
|
506
|
-
AND d.superseded_by IS NULL
|
|
507
|
-
ORDER BY d.created_at DESC
|
|
508
|
-
LIMIT 5
|
|
509
|
-
`)
|
|
510
|
-
.all(input.topic, scopeId).filter((c) => !excludeIds.has(c.id));
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
existingCandidates = adapter
|
|
514
|
-
.prepare(`
|
|
515
|
-
SELECT id, topic, summary, kind
|
|
516
|
-
FROM decisions
|
|
517
|
-
WHERE topic = ?
|
|
518
|
-
AND (status = 'active' OR status IS NULL)
|
|
519
|
-
AND superseded_by IS NULL
|
|
520
|
-
ORDER BY created_at DESC
|
|
521
|
-
LIMIT 5
|
|
522
|
-
`)
|
|
523
|
-
.all(input.topic).filter((c) => !excludeIds.has(c.id));
|
|
481
|
+
const GUIDANCE_MEMORY_KINDS = new Set([
|
|
482
|
+
'lesson',
|
|
483
|
+
'preference',
|
|
484
|
+
'constraint',
|
|
485
|
+
'workflow',
|
|
486
|
+
]);
|
|
487
|
+
function oneLineGuidanceText(value) {
|
|
488
|
+
return value.trim().replace(/\s+/g, ' ');
|
|
489
|
+
}
|
|
490
|
+
function guidancePayloadFor(input) {
|
|
491
|
+
if (!GUIDANCE_MEMORY_KINDS.has(input.kind))
|
|
492
|
+
return undefined;
|
|
493
|
+
const appliesWhen = typeof input.appliesWhen === 'string' ? oneLineGuidanceText(input.appliesWhen) : '';
|
|
494
|
+
if (!appliesWhen) {
|
|
495
|
+
throw new judgments_js_1.JudgmentError('INVALID_INPUT', `memory.save kind '${input.kind}' requires an appliesWhen line`);
|
|
524
496
|
}
|
|
525
|
-
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
let scopeFiltered = semanticResults;
|
|
536
|
-
if (primaryScope) {
|
|
537
|
-
const semIds = semanticResults.map((r) => String(r.id));
|
|
538
|
-
const semScopeMap = batchLoadScopes(adapter, semIds);
|
|
539
|
-
const scopeKey = `${primaryScope.kind}:${primaryScope.id}`;
|
|
540
|
-
scopeFiltered = semanticResults.filter((r) => {
|
|
541
|
-
const scopes = semScopeMap.get(String(r.id)) ?? [];
|
|
542
|
-
return scopes.length === 0 || scopes.some((s) => `${s.kind}:${s.id}` === scopeKey);
|
|
543
|
-
});
|
|
497
|
+
const guidance = { applies_when: appliesWhen };
|
|
498
|
+
if (input.kind === 'workflow') {
|
|
499
|
+
const steps = Array.isArray(input.steps) ? input.steps.map(oneLineGuidanceText) : [];
|
|
500
|
+
if (steps.length === 0 || steps.some((step) => step.length === 0)) {
|
|
501
|
+
throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.save kind workflow requires one or more ordered steps');
|
|
502
|
+
}
|
|
503
|
+
guidance.steps = steps;
|
|
504
|
+
if (input.evidenceChecks !== undefined) {
|
|
505
|
+
if (!Array.isArray(input.evidenceChecks)) {
|
|
506
|
+
throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.save evidenceChecks must be a list');
|
|
544
507
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
.filter((c) => !excludeIds.has(String(c.id)))
|
|
551
|
-
.map((r) => ({
|
|
552
|
-
id: String(r.id),
|
|
553
|
-
topic: String(r.topic || ''),
|
|
554
|
-
summary: String(r.decision || ''),
|
|
555
|
-
kind: 'fact',
|
|
556
|
-
_semanticMatch: true,
|
|
557
|
-
}));
|
|
558
|
-
}
|
|
559
|
-
catch {
|
|
560
|
-
// Semantic search unavailable — proceed with empty candidates
|
|
508
|
+
const checks = input.evidenceChecks.map(oneLineGuidanceText);
|
|
509
|
+
if (checks.some((check) => check.length === 0)) {
|
|
510
|
+
throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.save evidenceChecks cannot contain an empty line');
|
|
511
|
+
}
|
|
512
|
+
guidance.evidence_checks = checks;
|
|
561
513
|
}
|
|
562
514
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
515
|
+
return { guidance };
|
|
516
|
+
}
|
|
517
|
+
async function saveMemoryInternal(adapter, input, provenance, access, trustedEnvelope, legacy, commandIdOverride) {
|
|
518
|
+
const targetStatus = input.status ?? 'active';
|
|
519
|
+
const requestedScopes = input.scopes ?? [];
|
|
520
|
+
const payload = guidancePayloadFor(input);
|
|
521
|
+
const eventDateTime = typeof input.eventDateTime === 'number' ? input.eventDateTime : null;
|
|
522
|
+
// Fail before any write: identity references and relationship targets are
|
|
523
|
+
// checked against the same admitted scopes the command will carry.
|
|
524
|
+
(0, record_identity_js_1.validateRecordIdentityReferences)(adapter, {
|
|
525
|
+
itemId: input.itemId,
|
|
526
|
+
actors: input.actors,
|
|
527
|
+
scopes: input.scopes,
|
|
528
|
+
});
|
|
529
|
+
const commandId = commandIdOverride ?? `save:${buildDecisionId(input.topic)}`;
|
|
530
|
+
const recordId = (0, index_js_1.judgmentRecordId)(commandId);
|
|
531
|
+
// Relationships are persisted only when the caller names their target ids
|
|
532
|
+
// explicitly. Matching topic text or vector similarity is evidence for
|
|
533
|
+
// retrieval, not identity.
|
|
534
|
+
const explicitRelationships = Array.from(new Map((legacy?.relationships ?? []).flatMap((relationship) => relationship.targetIds.map((targetId) => [
|
|
535
|
+
`${relationship.type}:${targetId}`,
|
|
536
|
+
{ type: relationship.type, targetId },
|
|
537
|
+
]))).values());
|
|
538
|
+
(0, write_adapters_js_2.assertRelationshipTargetsVisible)(adapter, explicitRelationships.map((relationship) => relationship.targetId), access.scopes, trustedEnvelope);
|
|
539
|
+
const { links, replaces, decisionEdges, supersedeTargets } = (0, write_adapters_js_2.relationshipsToCommandFields)(explicitRelationships, { trusted: trustedEnvelope });
|
|
540
|
+
const authoredLinks = [...links, ...(input.links ?? [])];
|
|
541
|
+
const authoredReplacements = [...replaces, ...(input.replaces ?? [])];
|
|
542
|
+
const allSupersedeTargets = [
|
|
543
|
+
...new Set([
|
|
544
|
+
...supersedeTargets,
|
|
545
|
+
...(input.replaces ?? []).map((replacement) => replacement.id),
|
|
546
|
+
]),
|
|
547
|
+
];
|
|
548
|
+
const embeddingDecision = {
|
|
549
|
+
id: recordId,
|
|
580
550
|
topic: input.topic,
|
|
581
551
|
decision: input.summary,
|
|
582
552
|
reasoning: input.details,
|
|
583
553
|
confidence: input.confidence ?? 0.5,
|
|
584
|
-
supersedes: supersedesTarget,
|
|
585
|
-
created_at: now,
|
|
586
|
-
updated_at: now,
|
|
587
|
-
trust_context: JSON.stringify({ source: input.source }),
|
|
588
554
|
event_date: input.eventDate ?? null,
|
|
589
555
|
event_datetime: eventDateTime,
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
adapter
|
|
634
|
-
.prepare(`
|
|
635
|
-
INSERT OR IGNORE INTO decision_entity_sources (
|
|
636
|
-
decision_id, entity_observation_id, relation_type, created_at
|
|
637
|
-
) VALUES (?, ?, ?, ?)
|
|
638
|
-
`)
|
|
639
|
-
.run(id, entityObservationId, 'support', now);
|
|
640
|
-
}
|
|
641
|
-
if (timelineEvent) {
|
|
642
|
-
insertTimelineEventForSave(adapter, timelineEvent, now);
|
|
643
|
-
}
|
|
644
|
-
for (const edge of evolution.edges) {
|
|
645
|
-
if (edge.type === 'supersedes') {
|
|
646
|
-
adapter
|
|
647
|
-
.prepare(`UPDATE decisions SET superseded_by = ?, status = 'superseded', updated_at = ? WHERE id = ?`)
|
|
648
|
-
.run(id, now, edge.to_id);
|
|
556
|
+
};
|
|
557
|
+
const command = {
|
|
558
|
+
commandId,
|
|
559
|
+
topic: input.topic,
|
|
560
|
+
summary: input.summary,
|
|
561
|
+
reasoning: input.details,
|
|
562
|
+
recordKind: 'judgment',
|
|
563
|
+
confidence: input.confidence ?? 0.5,
|
|
564
|
+
eventDate: input.eventDate ?? null,
|
|
565
|
+
eventDatetime: eventDateTime,
|
|
566
|
+
outcome: legacy?.outcome ?? null,
|
|
567
|
+
failureReason: legacy?.failureReason ?? null,
|
|
568
|
+
limitation: legacy?.limitation ?? null,
|
|
569
|
+
sourceRefs: provenance.source_refs,
|
|
570
|
+
provenance: provenance.provenance,
|
|
571
|
+
...(payload === undefined ? {} : { payload }),
|
|
572
|
+
agentId: provenance.agent_id,
|
|
573
|
+
modelRunId: provenance.model_run_id,
|
|
574
|
+
envelopeHash: provenance.envelope_hash,
|
|
575
|
+
gatewayCallId: provenance.gateway_call_id,
|
|
576
|
+
scopes: [...requestedScopes],
|
|
577
|
+
links: authoredLinks,
|
|
578
|
+
replaces: authoredReplacements,
|
|
579
|
+
record: {
|
|
580
|
+
kind: input.kind,
|
|
581
|
+
status: targetStatus,
|
|
582
|
+
summary: input.summary,
|
|
583
|
+
isStatic: legacy?.isStatic ?? (input.kind === 'preference' || input.kind === 'constraint' ? 1 : 0),
|
|
584
|
+
userInvolvement: legacy?.userInvolvement ?? null,
|
|
585
|
+
trustContext: JSON.stringify({ source: input.source }),
|
|
586
|
+
},
|
|
587
|
+
event: {
|
|
588
|
+
actor: provenance.actor,
|
|
589
|
+
...(provenance.source_turn_id ? { sourceTurnId: provenance.source_turn_id } : {}),
|
|
590
|
+
evidenceRefs: provenance.source_refs,
|
|
591
|
+
reason: buildSaveEventReason(provenance.tool_name, provenance.gateway_call_id),
|
|
592
|
+
},
|
|
593
|
+
projections: {
|
|
594
|
+
decisionEdges,
|
|
595
|
+
...(allSupersedeTargets.length > 0 ? { supersedeTargets: allSupersedeTargets } : {}),
|
|
596
|
+
...(input.itemId !== undefined || input.actors !== undefined
|
|
597
|
+
? {
|
|
598
|
+
recordIdentity: { itemId: input.itemId ?? null, actors: input.actors ?? [] },
|
|
649
599
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
600
|
+
: {}),
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
let receipt;
|
|
604
|
+
try {
|
|
605
|
+
receipt = await (0, index_js_1.appendJudgment)(command, access, {
|
|
606
|
+
adapter,
|
|
607
|
+
embedder: (0, write_adapters_js_2.commandEmbedder)(embeddingDecision),
|
|
657
608
|
});
|
|
658
609
|
}
|
|
659
610
|
catch (error) {
|
|
660
|
-
cleanupFailedMemorySave(adapter, id);
|
|
661
611
|
const rollbackError = (error instanceof Error ? error : new Error(String(error)));
|
|
662
|
-
rollbackError.memoryId =
|
|
612
|
+
rollbackError.memoryId = recordId;
|
|
663
613
|
throw rollbackError;
|
|
664
614
|
}
|
|
665
|
-
// Sync the adapter's status cache for every row whose status changed in the
|
|
666
|
-
// post-insert transaction: the saved row itself (status is written AFTER
|
|
667
|
-
// insertEmbedding populated the cache) and any rows just superseded. This makes
|
|
668
|
-
// the vectorSearch pre-filter correct within this session, not only after reload.
|
|
669
|
-
if (adapter.refreshDecisionStatusCache) {
|
|
670
|
-
const changedIds = [
|
|
671
|
-
id,
|
|
672
|
-
...evolution.edges.filter((e) => e.type === 'supersedes').map((e) => e.to_id),
|
|
673
|
-
];
|
|
674
|
-
const ridStmt = adapter.prepare('SELECT rowid FROM decisions WHERE id = ?');
|
|
675
|
-
for (const changedId of changedIds) {
|
|
676
|
-
const ridRow = ridStmt.get(changedId);
|
|
677
|
-
if (ridRow) {
|
|
678
|
-
adapter.refreshDecisionStatusCache(ridRow.rowid);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
615
|
return {
|
|
683
616
|
success: true,
|
|
684
|
-
id,
|
|
685
|
-
saved_decision_id:
|
|
686
|
-
timeline_event_id: timelineEvent?.id ?? null,
|
|
687
|
-
timeline_event_ids: timelineEvent ? [timelineEvent.id] : [],
|
|
617
|
+
id: receipt.recordId,
|
|
618
|
+
saved_decision_id: receipt.recordId,
|
|
688
619
|
};
|
|
689
620
|
}
|
|
690
|
-
async function saveMemory(input) {
|
|
691
|
-
|
|
621
|
+
async function saveMemory(adapter, input) {
|
|
622
|
+
const clean = (0, provenance_js_1.sanitizePublicSaveMemoryInput)(input);
|
|
623
|
+
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
|
|
624
|
+
const access = (0, write_adapters_js_2.writeAccessForProvenance)(provenance, clean.scopes ?? []);
|
|
625
|
+
return saveMemoryInternal(adapter, clean, provenance, access, false);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* The unified `memory.save` action path. Access IS the call authority — the
|
|
629
|
+
* command's scopes are checked against it inside `appendJudgment`, and the
|
|
630
|
+
* record's provenance is composed from it plus the session facts the host
|
|
631
|
+
* attests (never from caller input). `commandId` is the caller's operationId,
|
|
632
|
+
* so a retried call replays the original receipt.
|
|
633
|
+
*/
|
|
634
|
+
async function saveJudgmentRecord(adapter, input, access, commandId, session) {
|
|
635
|
+
// An omitted scope request writes under the full admitted scopes — the same
|
|
636
|
+
// default the gateway's trusted path applied via the context packet.
|
|
637
|
+
const clean = (0, provenance_js_1.sanitizePublicSaveMemoryInput)({
|
|
638
|
+
...input,
|
|
639
|
+
scopes: input.scopes ?? access.scopes,
|
|
640
|
+
});
|
|
641
|
+
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)({
|
|
642
|
+
actor: session?.actor ?? 'main_agent',
|
|
643
|
+
agent_id: access.agentId,
|
|
644
|
+
model_run_id: session?.modelRunId,
|
|
645
|
+
envelope_hash: session?.envelopeHash,
|
|
646
|
+
tool_name: session?.toolName,
|
|
647
|
+
gateway_call_id: session?.gatewayCallId,
|
|
648
|
+
context_packet_id: session?.contextPacketId,
|
|
649
|
+
source_turn_id: session?.sourceTurnId,
|
|
650
|
+
source_message_ref: session?.sourceMessageRef,
|
|
651
|
+
source_refs: session?.sourceRefs ? [...session.sourceRefs] : undefined,
|
|
652
|
+
});
|
|
653
|
+
return saveMemoryInternal(adapter, clean, provenance, access, true, undefined, commandId);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* The read-side scope rule for memory actions — the same bound `appendJudgment`
|
|
657
|
+
* applies to writes. An omitted request reads under the full admitted scopes;
|
|
658
|
+
* an explicit request must be a subset of them. An empty admitted set admits an
|
|
659
|
+
* empty corpus: a caller with no scopes can see no scoped record (reads never
|
|
660
|
+
* fall back to an unbounded scan — the legacy `scopes: []` = read-all behavior
|
|
661
|
+
* is exactly what this bound removes).
|
|
662
|
+
*/
|
|
663
|
+
function boundReadScopesFor(access, requested) {
|
|
664
|
+
const readable = [...access.scopes, ...(access.readScopes ?? [])];
|
|
665
|
+
const scopeKey = (scope) => JSON.stringify([scope.kind, scope.id]);
|
|
666
|
+
const admitted = new Map(readable.map((scope) => [scopeKey(scope), scope]));
|
|
667
|
+
const effective = requested ?? [...admitted.values()];
|
|
668
|
+
const seen = new Set();
|
|
669
|
+
return effective.map((scope) => {
|
|
670
|
+
if (typeof scope !== 'object' ||
|
|
671
|
+
scope === null ||
|
|
672
|
+
typeof scope.kind !== 'string' ||
|
|
673
|
+
scope.kind.trim().length === 0 ||
|
|
674
|
+
typeof scope.id !== 'string' ||
|
|
675
|
+
scope.id.trim().length === 0) {
|
|
676
|
+
throw new judgments_js_1.JudgmentError('INVALID_SCOPE', 'scope kind is invalid');
|
|
677
|
+
}
|
|
678
|
+
const key = scopeKey(scope);
|
|
679
|
+
if (seen.has(key)) {
|
|
680
|
+
throw new judgments_js_1.JudgmentError('INVALID_SCOPE', 'Read scopes must be unique');
|
|
681
|
+
}
|
|
682
|
+
seen.add(key);
|
|
683
|
+
if (!admitted.has(key)) {
|
|
684
|
+
throw new judgments_js_1.JudgmentError('SCOPE_DENIED', 'Read scope is outside the admitted access');
|
|
685
|
+
}
|
|
686
|
+
return scope;
|
|
687
|
+
});
|
|
692
688
|
}
|
|
693
|
-
async function
|
|
694
|
-
|
|
689
|
+
async function saveLegacyMemory(adapter, input, legacy, access) {
|
|
690
|
+
const clean = (0, provenance_js_1.sanitizePublicSaveMemoryInput)(input);
|
|
691
|
+
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
|
|
692
|
+
const effectiveAccess = access ?? (0, write_adapters_js_2.writeAccessForProvenance)(provenance, clean.scopes ?? []);
|
|
693
|
+
return saveMemoryInternal(adapter, clean, provenance, effectiveAccess, access !== undefined, legacy);
|
|
695
694
|
}
|
|
696
|
-
async function promoteMemoryStatus(input) {
|
|
697
|
-
await (0, db_manager_js_1.initDB)();
|
|
698
|
-
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
695
|
+
async function promoteMemoryStatus(adapter, input) {
|
|
699
696
|
const memoryId = input.memoryId;
|
|
700
697
|
const now = input.nowMs ?? Date.now();
|
|
701
698
|
const targetStatus = input.status;
|
|
@@ -718,7 +715,7 @@ async function promoteMemoryStatus(input) {
|
|
|
718
715
|
const primaryScope = scopes[0] ?? null;
|
|
719
716
|
let existingCandidates;
|
|
720
717
|
if (primaryScope) {
|
|
721
|
-
const scopeId =
|
|
718
|
+
const scopeId = (0, db_manager_js_1.ensureMemoryScope)(adapter, primaryScope.kind, primaryScope.id);
|
|
722
719
|
existingCandidates = adapter
|
|
723
720
|
.prepare(`
|
|
724
721
|
SELECT d.id, d.topic, d.summary, d.kind
|
|
@@ -748,10 +745,10 @@ async function promoteMemoryStatus(input) {
|
|
|
748
745
|
if (existingCandidates.length === 0) {
|
|
749
746
|
try {
|
|
750
747
|
const queryText = `${topic} ${summary}`;
|
|
751
|
-
const embedding = await (0,
|
|
748
|
+
const embedding = await (0, embedder_js_1.generateEmbedding)(queryText, 'query');
|
|
752
749
|
// Same exclusion as saveMemoryInternal's fallback: superseded history must
|
|
753
750
|
// not crowd out the prior ACTIVE decision from the 3 candidate slots.
|
|
754
|
-
const semanticResults = await (0,
|
|
751
|
+
const semanticResults = await (0, search_js_1.vectorSearch)(adapter, embedding, 3, 0.82, undefined, Array.from(EXCLUDED_STATUSES));
|
|
755
752
|
let scopeFiltered = semanticResults;
|
|
756
753
|
if (primaryScope) {
|
|
757
754
|
const semIds = semanticResults.map((result) => String(result.id));
|
|
@@ -792,46 +789,104 @@ async function promoteMemoryStatus(input) {
|
|
|
792
789
|
const existingSupersedesTarget = typeof row.supersedes === 'string' && row.supersedes.length > 0 ? row.supersedes : null;
|
|
793
790
|
const supersedesTarget = evolution.edges.find((edge) => edge.type === 'supersedes')?.to_id ??
|
|
794
791
|
(targetStatus === 'active' ? existingSupersedesTarget : null);
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
792
|
+
// The status change is an authored amendment: one append-only judgment record
|
|
793
|
+
// carries it, and the target row's projection columns move in the same
|
|
794
|
+
// transaction. A replayed command (same id, same payload) returns the stored
|
|
795
|
+
// receipt instead of rewriting.
|
|
796
|
+
const commandId = `promote:${memoryId}:${node_crypto_1.default
|
|
797
|
+
.createHash('sha256')
|
|
798
|
+
.update((0, canonicalize_js_1.canonicalizeJSON)({
|
|
799
|
+
status: targetStatus,
|
|
800
|
+
supersedes: supersedesTarget,
|
|
801
|
+
edges: evolution.edges.map((edge) => `${edge.type}:${edge.to_id}`),
|
|
802
|
+
now,
|
|
803
|
+
}))
|
|
804
|
+
.digest('hex')
|
|
805
|
+
.slice(0, 16)}`;
|
|
806
|
+
const supersedesEdges = evolution.edges.filter((edge) => edge.type === 'supersedes');
|
|
807
|
+
const command = {
|
|
808
|
+
commandId,
|
|
809
|
+
// Audit records link to the amended memory instead of sharing its topic,
|
|
810
|
+
// keeping them out of the topic's evolution candidate pool.
|
|
811
|
+
topic: `judgment/${memoryId}`,
|
|
812
|
+
summary: `Status '${targetStatus}' applied to ${memoryId}`,
|
|
813
|
+
recordKind: 'judgment',
|
|
814
|
+
payload: {
|
|
815
|
+
amended: memoryId,
|
|
816
|
+
status: targetStatus,
|
|
817
|
+
supersedes: supersedesTarget,
|
|
818
|
+
edges: evolution.edges.map((edge) => ({ type: edge.type, to_id: edge.to_id })),
|
|
819
|
+
},
|
|
820
|
+
scopes,
|
|
821
|
+
agentId: null,
|
|
822
|
+
links: [{ relation: 'amends', target: { kind: 'memory', id: memoryId } }],
|
|
823
|
+
amends: [
|
|
824
|
+
// supersedes is included only when a target resolved: applyAmendment
|
|
825
|
+
// writes a column for every key present, so a null here would clear the
|
|
826
|
+
// target's predecessor pointer on non-active promotions.
|
|
827
|
+
{
|
|
828
|
+
target: { kind: 'memory', id: memoryId },
|
|
829
|
+
status: targetStatus,
|
|
830
|
+
...(supersedesTarget !== null ? { supersedes: supersedesTarget } : {}),
|
|
831
|
+
},
|
|
832
|
+
...supersedesEdges.map((edge) => ({
|
|
833
|
+
target: { kind: 'memory', id: edge.to_id },
|
|
834
|
+
supersededBy: memoryId,
|
|
835
|
+
status: 'superseded',
|
|
836
|
+
})),
|
|
837
|
+
],
|
|
838
|
+
projections: {
|
|
839
|
+
decisionEdges: evolution.edges.map((edge) => ({
|
|
840
|
+
fromId: memoryId,
|
|
841
|
+
targetId: edge.to_id,
|
|
842
|
+
relationship: edge.type,
|
|
843
|
+
reason: edge.reason ?? null,
|
|
844
|
+
weight: 1,
|
|
845
|
+
})),
|
|
846
|
+
},
|
|
847
|
+
record: {
|
|
848
|
+
kind: 'fact',
|
|
849
|
+
status: 'active',
|
|
850
|
+
summary: `Status '${targetStatus}' applied to ${memoryId}`,
|
|
851
|
+
},
|
|
852
|
+
recordedAt: now,
|
|
853
|
+
event: { reason: `promote ${memoryId} to '${targetStatus}'` },
|
|
854
|
+
};
|
|
855
|
+
await (0, index_js_1.appendJudgment)(command, (0, write_adapters_js_2.unsignedWriteAccess)(scopes), { adapter });
|
|
856
|
+
}
|
|
857
|
+
/** Append an access-checked status amendment for one stored memory record. */
|
|
858
|
+
async function retireMemoryRecord(adapter, input, access, commandId) {
|
|
859
|
+
const id = input.memoryId.trim();
|
|
860
|
+
const reason = input.reason.trim();
|
|
861
|
+
if (!id)
|
|
862
|
+
throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.retire requires memory_id');
|
|
863
|
+
if (!reason)
|
|
864
|
+
throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.retire requires a reason');
|
|
865
|
+
if (input.status !== 'stale' && input.status !== 'superseded') {
|
|
866
|
+
throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.retire status must be stale or superseded');
|
|
831
867
|
}
|
|
868
|
+
const record = await readMemoryRecordById(adapter, id, access.scopes);
|
|
869
|
+
if (!record) {
|
|
870
|
+
throw new judgments_js_1.JudgmentError('REFERENCE_NOT_FOUND', 'Memory record is unavailable in the admitted scopes');
|
|
871
|
+
}
|
|
872
|
+
const admittedScopes = record.scopes.filter((recordScope) => access.scopes.some((scope) => scope.kind === recordScope.kind && scope.id === recordScope.id));
|
|
873
|
+
const summary = `Status '${input.status}' applied to ${id}: ${reason}`;
|
|
874
|
+
const receipt = await (0, index_js_1.appendJudgment)({
|
|
875
|
+
commandId,
|
|
876
|
+
topic: `judgment/${id}`,
|
|
877
|
+
summary,
|
|
878
|
+
recordKind: 'judgment',
|
|
879
|
+
payload: { amended: id, status: input.status, reason },
|
|
880
|
+
scopes: admittedScopes,
|
|
881
|
+
links: [{ relation: 'amends', target: { kind: 'memory', id } }],
|
|
882
|
+
amends: [{ target: { kind: 'memory', id }, status: input.status }],
|
|
883
|
+
record: { kind: 'fact', status: 'active', summary },
|
|
884
|
+
event: { reason: `Retired ${id} as ${input.status}: ${reason}` },
|
|
885
|
+
}, access, { adapter });
|
|
886
|
+
return { success: true, id, status: input.status, reason, receiptId: receipt.recordId };
|
|
832
887
|
}
|
|
833
|
-
async function buildProfile(scopes) {
|
|
834
|
-
const records = await loadScopedMemories(scopes);
|
|
888
|
+
async function buildProfile(adapter, scopes) {
|
|
889
|
+
const records = await loadScopedMemories(adapter, scopes);
|
|
835
890
|
return (0, profile_builder_js_1.classifyProfileEntries)(records);
|
|
836
891
|
}
|
|
837
892
|
const EXCLUDED_STATUSES = new Set([
|
|
@@ -840,14 +895,17 @@ const EXCLUDED_STATUSES = new Set([
|
|
|
840
895
|
'contradicted',
|
|
841
896
|
'stale',
|
|
842
897
|
]);
|
|
843
|
-
async function recallMemory(query, options = {}) {
|
|
898
|
+
async function recallMemory(adapter, query, options = {}) {
|
|
844
899
|
const bundle = (0, types_js_1.createEmptyRecallBundle)(query);
|
|
845
900
|
const searchOptions = (0, search_quality_js_1.normalizeSearchQualityOptions)(options);
|
|
901
|
+
const matchesKind = (kind) => options.kind === undefined ||
|
|
902
|
+
(Array.isArray(options.kind)
|
|
903
|
+
? options.kind.some((value) => value === kind)
|
|
904
|
+
: kind === options.kind);
|
|
846
905
|
const diagnostics = {
|
|
847
906
|
candidate_counts: {
|
|
848
907
|
vector: 0,
|
|
849
908
|
lexical: 0,
|
|
850
|
-
entity: 0,
|
|
851
909
|
graph_expanded: 0,
|
|
852
910
|
vector_only: 0,
|
|
853
911
|
rejected_by_strictness: 0,
|
|
@@ -858,14 +916,12 @@ async function recallMemory(query, options = {}) {
|
|
|
858
916
|
let matched = [];
|
|
859
917
|
let fusedHits = [];
|
|
860
918
|
let retrievalSource = 'none';
|
|
861
|
-
const projectionMode = process.env.MAMA_ENTITY_PROJECTION_MODE ?? 'shadow';
|
|
862
919
|
const vectorSimilarityById = new Map();
|
|
863
920
|
const lexicalScoreById = new Map();
|
|
864
|
-
const entitySupportIds = new Set();
|
|
865
921
|
let _lexicalRecords = null;
|
|
866
922
|
const loadLexical = async () => {
|
|
867
923
|
if (_lexicalRecords === null) {
|
|
868
|
-
_lexicalRecords = await loadScopedMemories(options.scopes ?? []);
|
|
924
|
+
_lexicalRecords = await loadScopedMemories(adapter, options.scopes ?? []);
|
|
869
925
|
}
|
|
870
926
|
return _lexicalRecords;
|
|
871
927
|
};
|
|
@@ -888,25 +944,27 @@ async function recallMemory(query, options = {}) {
|
|
|
888
944
|
}
|
|
889
945
|
}
|
|
890
946
|
// Hybrid search: vector + BM25/lexical in parallel, fused with RRF
|
|
891
|
-
|
|
947
|
+
// One adapter for the whole retrieval: generateEmbedding yields, and a reset
|
|
948
|
+
// between two adapter lookups would fuse candidates from two databases.
|
|
949
|
+
const searchAdapter = adapter;
|
|
892
950
|
// Channel 1: Vector search (semantic similarity) — run all sub-queries
|
|
893
951
|
const vectorMatched = [];
|
|
894
952
|
let primaryQueryEmbedding = null;
|
|
895
953
|
try {
|
|
896
954
|
for (const sq of subQueries) {
|
|
897
|
-
const queryEmbedding = await (0,
|
|
955
|
+
const queryEmbedding = await (0, embedder_js_1.generateEmbedding)(sq, 'query');
|
|
898
956
|
if (sq === query && primaryQueryEmbedding === null) {
|
|
899
957
|
primaryQueryEmbedding = queryEmbedding;
|
|
900
958
|
}
|
|
901
|
-
const vectorResults = await (0,
|
|
959
|
+
const vectorResults = await (0, search_js_1.vectorSearch)(searchAdapter, queryEmbedding, vectorLimit, searchOptions.threshold, searchOptions.topicPrefix,
|
|
902
960
|
// Keep superseded history out of the candidate top-K at search time; the
|
|
903
961
|
// post-filter below stays the authority (and includeHistory restores it).
|
|
904
|
-
options.includeHistory ? undefined : Array.from(EXCLUDED_STATUSES));
|
|
962
|
+
options.includeHistory ? undefined : Array.from(EXCLUDED_STATUSES), options.kind);
|
|
905
963
|
let filtered = vectorResults;
|
|
906
964
|
let vectorScopeMap = new Map();
|
|
907
965
|
if (options.scopes && options.scopes.length > 0) {
|
|
908
966
|
const vectorIds = vectorResults.map((r) => String(r.id));
|
|
909
|
-
vectorScopeMap = batchLoadScopes(
|
|
967
|
+
vectorScopeMap = batchLoadScopes(adapter, vectorIds);
|
|
910
968
|
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
911
969
|
filtered = vectorResults.filter((r) => {
|
|
912
970
|
const scopes = vectorScopeMap.get(String(r.id)) ?? [];
|
|
@@ -924,7 +982,7 @@ async function recallMemory(query, options = {}) {
|
|
|
924
982
|
vectorMatched.push({
|
|
925
983
|
id: String(result.id),
|
|
926
984
|
topic: String(result.topic || ''),
|
|
927
|
-
kind: 'decision',
|
|
985
|
+
kind: (result.kind ?? 'decision'),
|
|
928
986
|
summary: String(result.decision || ''),
|
|
929
987
|
details: String(result.reasoning || ''),
|
|
930
988
|
confidence: result.similarity ?? 0.5,
|
|
@@ -935,6 +993,7 @@ async function recallMemory(query, options = {}) {
|
|
|
935
993
|
updated_at: result.created_at ?? Date.now(),
|
|
936
994
|
event_date: result.event_date ?? null,
|
|
937
995
|
event_datetime: result.event_datetime ?? null,
|
|
996
|
+
outcome: result.outcome ?? null,
|
|
938
997
|
});
|
|
939
998
|
diagnostics.candidate_counts.vector += 1;
|
|
940
999
|
if (typeof result.similarity === 'number' && Number.isFinite(result.similarity)) {
|
|
@@ -944,7 +1003,7 @@ async function recallMemory(query, options = {}) {
|
|
|
944
1003
|
} // end sub-query loop
|
|
945
1004
|
}
|
|
946
1005
|
catch (vectorErr) {
|
|
947
|
-
(0,
|
|
1006
|
+
(0, debug_logger_js_2.warn)(`[recallMemory] Vector search failed: ${vectorErr instanceof Error ? vectorErr.message : String(vectorErr)}`);
|
|
948
1007
|
}
|
|
949
1008
|
// Deduplicate vector results from multiple sub-queries
|
|
950
1009
|
const seenIds = new Set();
|
|
@@ -1013,9 +1072,9 @@ async function recallMemory(query, options = {}) {
|
|
|
1013
1072
|
.map((t) => stemToken(t))
|
|
1014
1073
|
.filter((t) => !FTS5_NOISE_WORDS.has(t));
|
|
1015
1074
|
const ftsQuery = ftsTokens.length > 0 ? ftsTokens.join(' OR ') : query;
|
|
1016
|
-
const ftsResults = await (0,
|
|
1075
|
+
const ftsResults = await (0, search_js_1.fts5Search)(searchAdapter, ftsQuery, lexicalLimit, options.kind);
|
|
1017
1076
|
if (ftsResults.length > 0) {
|
|
1018
|
-
const adapter =
|
|
1077
|
+
const adapter = searchAdapter;
|
|
1019
1078
|
const fallbackSource = {
|
|
1020
1079
|
package: 'mama-core',
|
|
1021
1080
|
source_type: 'fts5',
|
|
@@ -1025,7 +1084,7 @@ async function recallMemory(query, options = {}) {
|
|
|
1025
1084
|
for (const ftsRow of ftsResults) {
|
|
1026
1085
|
const row = adapter
|
|
1027
1086
|
.prepare(`SELECT id, topic, decision, reasoning, confidence, created_at, updated_at,
|
|
1028
|
-
trust_context, kind, status, summary, event_date, event_datetime
|
|
1087
|
+
trust_context, kind, status, summary, event_date, event_datetime, outcome
|
|
1029
1088
|
FROM decisions WHERE id = ?`)
|
|
1030
1089
|
.get(ftsRow.id);
|
|
1031
1090
|
if (!row)
|
|
@@ -1042,6 +1101,8 @@ async function recallMemory(query, options = {}) {
|
|
|
1042
1101
|
// Topic prefix filtering (matches vectorSearch behavior)
|
|
1043
1102
|
if (searchOptions.topicPrefix && !record.topic.startsWith(searchOptions.topicPrefix))
|
|
1044
1103
|
continue;
|
|
1104
|
+
if (!matchesKind(record.kind))
|
|
1105
|
+
continue;
|
|
1045
1106
|
// Scope filtering
|
|
1046
1107
|
if (options.scopes && options.scopes.length > 0) {
|
|
1047
1108
|
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
@@ -1070,14 +1131,14 @@ async function recallMemory(query, options = {}) {
|
|
|
1070
1131
|
for (const candidate of lexicalCandidates) {
|
|
1071
1132
|
candidate.score += topicAffinityBoost(candidate.memory.topic, boostTokens, boostQuery);
|
|
1072
1133
|
}
|
|
1073
|
-
lexicalCandidates.sort(
|
|
1134
|
+
lexicalCandidates.sort(compareLexicalCandidates);
|
|
1074
1135
|
}
|
|
1075
1136
|
// Fallback: in-memory lexical if FTS5 returned nothing
|
|
1076
1137
|
if (lexicalCandidates.length === 0) {
|
|
1077
1138
|
let lexicalRecords = await loadLexical();
|
|
1078
1139
|
if (options.scopes && options.scopes.length > 0) {
|
|
1079
1140
|
const lexicalIds = lexicalRecords.map((r) => r.id);
|
|
1080
|
-
const scopeMap = batchLoadScopes(
|
|
1141
|
+
const scopeMap = batchLoadScopes(adapter, lexicalIds);
|
|
1081
1142
|
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
1082
1143
|
lexicalRecords = lexicalRecords.filter((r) => {
|
|
1083
1144
|
const scopes = scopeMap.get(r.id) ?? [];
|
|
@@ -1090,6 +1151,9 @@ async function recallMemory(query, options = {}) {
|
|
|
1090
1151
|
if (searchOptions.topicPrefix) {
|
|
1091
1152
|
lexicalRecords = lexicalRecords.filter((r) => r.topic.startsWith(searchOptions.topicPrefix));
|
|
1092
1153
|
}
|
|
1154
|
+
if (options.kind !== undefined) {
|
|
1155
|
+
lexicalRecords = lexicalRecords.filter((r) => matchesKind(r.kind));
|
|
1156
|
+
}
|
|
1093
1157
|
lexicalCandidates = buildLexicalCandidates(lexicalRecords, query);
|
|
1094
1158
|
if (subQueries.length > 1) {
|
|
1095
1159
|
for (const sq of subQueries.slice(1)) {
|
|
@@ -1100,7 +1164,7 @@ async function recallMemory(query, options = {}) {
|
|
|
1100
1164
|
}
|
|
1101
1165
|
}
|
|
1102
1166
|
}
|
|
1103
|
-
lexicalCandidates.sort(
|
|
1167
|
+
lexicalCandidates.sort(compareLexicalCandidates);
|
|
1104
1168
|
}
|
|
1105
1169
|
}
|
|
1106
1170
|
} // end needsLexical
|
|
@@ -1149,56 +1213,6 @@ async function recallMemory(query, options = {}) {
|
|
|
1149
1213
|
record: entry.record,
|
|
1150
1214
|
fused_rank_score: entry.score,
|
|
1151
1215
|
}));
|
|
1152
|
-
let hasWikiHits = false;
|
|
1153
|
-
const wikiScores = new Map();
|
|
1154
|
-
const wikiVectorRankMap = new Map();
|
|
1155
|
-
const wikiVectorScoreById = new Map();
|
|
1156
|
-
try {
|
|
1157
|
-
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
1158
|
-
const wikiFtsHits = (0, wiki_page_index_js_1.ftsSearchWikiPages)(adapter, query, requestedLimit * 2);
|
|
1159
|
-
let wikiVectorHits = [];
|
|
1160
|
-
if (primaryQueryEmbedding) {
|
|
1161
|
-
try {
|
|
1162
|
-
wikiVectorHits = (0, wiki_page_index_js_1.vectorSearchWikiPages)(adapter, primaryQueryEmbedding, requestedLimit * 2);
|
|
1163
|
-
}
|
|
1164
|
-
catch (wikiVectorErr) {
|
|
1165
|
-
(0, debug_logger_js_1.warn)(`[recallMemory] Wiki vector search failed: ${wikiVectorErr instanceof Error ? wikiVectorErr.message : String(wikiVectorErr)}`);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
for (let i = 0; i < wikiVectorHits.length; i++) {
|
|
1169
|
-
wikiVectorRankMap.set(wikiVectorHits[i].record.id, i);
|
|
1170
|
-
wikiVectorScoreById.set(wikiVectorHits[i].record.id, wikiVectorHits[i].raw_score);
|
|
1171
|
-
}
|
|
1172
|
-
diagnostics.candidate_counts.lexical += wikiFtsHits.length;
|
|
1173
|
-
diagnostics.candidate_counts.vector += wikiVectorHits.length;
|
|
1174
|
-
for (let i = 0; i < wikiFtsHits.length; i++) {
|
|
1175
|
-
const hit = wikiFtsHits[i];
|
|
1176
|
-
const lexScore = 1 / (RRF_K + i + 1);
|
|
1177
|
-
const vecRank = wikiVectorRankMap.get(hit.record.id);
|
|
1178
|
-
const vecBoost = vecRank !== undefined ? 0.2 * (1 / (RRF_K + vecRank + 1)) : 0;
|
|
1179
|
-
wikiScores.set(hit.record.id, {
|
|
1180
|
-
record: hit.record,
|
|
1181
|
-
score: lexScore + vecBoost,
|
|
1182
|
-
lexicalSupport: true,
|
|
1183
|
-
vectorSimilarity: wikiVectorScoreById.get(hit.record.id) ?? null,
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
for (let i = 0; i < wikiVectorHits.length; i++) {
|
|
1187
|
-
const hit = wikiVectorHits[i];
|
|
1188
|
-
if (wikiScores.has(hit.record.id)) {
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
wikiScores.set(hit.record.id, {
|
|
1192
|
-
record: hit.record,
|
|
1193
|
-
score: 0.15 * (1 / (RRF_K + i + 1)),
|
|
1194
|
-
lexicalSupport: false,
|
|
1195
|
-
vectorSimilarity: hit.raw_score,
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
catch (wikiFtsErr) {
|
|
1200
|
-
(0, debug_logger_js_1.warn)(`[recallMemory] Wiki FTS search failed: ${wikiFtsErr instanceof Error ? wikiFtsErr.message : String(wikiFtsErr)}`);
|
|
1201
|
-
}
|
|
1202
1216
|
fusedHits = decisionFusedHits;
|
|
1203
1217
|
// Normalize RRF scores to 0-1 range so downstream consumers (threshold filters,
|
|
1204
1218
|
// similarity displays) get meaningful values. The raw RRF score for K=60 tops out
|
|
@@ -1217,34 +1231,6 @@ async function recallMemory(query, options = {}) {
|
|
|
1217
1231
|
else if (lexicalCandidates.length > 0) {
|
|
1218
1232
|
retrievalSource = 'lexical_search';
|
|
1219
1233
|
}
|
|
1220
|
-
let canonicalMatched = [];
|
|
1221
|
-
if (projectionMode !== 'off') {
|
|
1222
|
-
try {
|
|
1223
|
-
canonicalMatched = await (0, recall_bridge_js_1.queryCanonicalEntities)(query, options.scopes ?? [], { limit: 10 });
|
|
1224
|
-
diagnostics.candidate_counts.entity = canonicalMatched.length;
|
|
1225
|
-
for (const canonical of canonicalMatched) {
|
|
1226
|
-
entitySupportIds.add(canonical.id);
|
|
1227
|
-
}
|
|
1228
|
-
if (projectionMode === 'dual-write' && canonicalMatched.length > 0) {
|
|
1229
|
-
const seenIds = new Set(matched.map((item) => item.id));
|
|
1230
|
-
for (const canonical of canonicalMatched) {
|
|
1231
|
-
if (!seenIds.has(canonical.id)) {
|
|
1232
|
-
matched.push(canonical);
|
|
1233
|
-
seenIds.add(canonical.id);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
retrievalSource =
|
|
1237
|
-
retrievalSource === 'none' ? 'entity_canonical' : `${retrievalSource}+entity_canonical`;
|
|
1238
|
-
}
|
|
1239
|
-
else if (projectionMode === 'shadow' && canonicalMatched.length > 0) {
|
|
1240
|
-
retrievalSource =
|
|
1241
|
-
retrievalSource === 'none' ? 'shadow_entity_probe' : `${retrievalSource}+shadow_probe`;
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
catch (canonicalErr) {
|
|
1245
|
-
(0, debug_logger_js_1.warn)(`[recallMemory] Canonical entity recall failed: ${canonicalErr instanceof Error ? canonicalErr.message : String(canonicalErr)}`);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
1234
|
const requestedScopeKeys = new Set((options.scopes ?? []).map((scope) => `${scope.kind}:${scope.id}`));
|
|
1249
1235
|
const hasRequestedScopeSupport = (record) => {
|
|
1250
1236
|
if (requestedScopeKeys.size === 0) {
|
|
@@ -1276,160 +1262,19 @@ async function recallMemory(query, options = {}) {
|
|
|
1276
1262
|
// (when itself a single token) appears as a complete word in the query.
|
|
1277
1263
|
return topicTokens.includes(normalizedQuery) || queryTokens.includes(normalizedTopic);
|
|
1278
1264
|
};
|
|
1279
|
-
const hasExactWikiSupport = (record) => {
|
|
1280
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
1281
|
-
if (!normalizedQuery || normalizedQuery.length < EXACT_TOPIC_MIN_QUERY_LENGTH) {
|
|
1282
|
-
return false;
|
|
1283
|
-
}
|
|
1284
|
-
const normalizedTitle = record.title.trim().toLowerCase();
|
|
1285
|
-
if (!normalizedTitle) {
|
|
1286
|
-
return false;
|
|
1287
|
-
}
|
|
1288
|
-
if (normalizedQuery === normalizedTitle) {
|
|
1289
|
-
return true;
|
|
1290
|
-
}
|
|
1291
|
-
const queryTokens = tokenizeForExactTopic(normalizedQuery);
|
|
1292
|
-
const titleTokens = tokenizeForExactTopic(normalizedTitle);
|
|
1293
|
-
// Wiki exact_topic uses TITLE only — page body matches are too lenient
|
|
1294
|
-
// for an "exact" signal and remain available through other diagnostics.
|
|
1295
|
-
return titleTokens.includes(normalizedQuery) || queryTokens.includes(normalizedTitle);
|
|
1296
|
-
};
|
|
1297
|
-
const wikiDecisionIdCache = new Map();
|
|
1298
|
-
const normalizeWikiDecisionSourceId = (sourceId) => {
|
|
1299
|
-
const trimmed = sourceId.trim();
|
|
1300
|
-
if (!trimmed) {
|
|
1301
|
-
return null;
|
|
1302
|
-
}
|
|
1303
|
-
if (trimmed.startsWith('decision://')) {
|
|
1304
|
-
const id = trimmed.slice('decision://'.length).trim();
|
|
1305
|
-
return id || null;
|
|
1306
|
-
}
|
|
1307
|
-
if (trimmed.startsWith('decision:')) {
|
|
1308
|
-
const id = trimmed.slice('decision:'.length).trim();
|
|
1309
|
-
return id || null;
|
|
1310
|
-
}
|
|
1311
|
-
if (/^[A-Za-z][A-Za-z0-9_-]*:/.test(trimmed)) {
|
|
1312
|
-
return null;
|
|
1313
|
-
}
|
|
1314
|
-
return trimmed;
|
|
1315
|
-
};
|
|
1316
|
-
const loadWikiDecisionIds = (record) => {
|
|
1317
|
-
const cached = wikiDecisionIdCache.get(record.id);
|
|
1318
|
-
if (cached) {
|
|
1319
|
-
return cached;
|
|
1320
|
-
}
|
|
1321
|
-
const ids = new Set();
|
|
1322
|
-
for (const sourceId of record.source_ids) {
|
|
1323
|
-
const decisionId = normalizeWikiDecisionSourceId(sourceId);
|
|
1324
|
-
if (decisionId) {
|
|
1325
|
-
ids.add(decisionId);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
if (record.case_id) {
|
|
1329
|
-
const rows = (0, db_manager_js_1.getAdapter)()
|
|
1330
|
-
.prepare(`
|
|
1331
|
-
SELECT source_id
|
|
1332
|
-
FROM case_memberships
|
|
1333
|
-
WHERE case_id = ?
|
|
1334
|
-
AND source_type = 'decision'
|
|
1335
|
-
AND status = 'active'
|
|
1336
|
-
`)
|
|
1337
|
-
.all(record.case_id);
|
|
1338
|
-
for (const row of rows) {
|
|
1339
|
-
if (row.source_id) {
|
|
1340
|
-
ids.add(row.source_id);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
const decisionIds = Array.from(ids);
|
|
1345
|
-
wikiDecisionIdCache.set(record.id, decisionIds);
|
|
1346
|
-
return decisionIds;
|
|
1347
|
-
};
|
|
1348
|
-
const wikiScopeSupportCache = new Map();
|
|
1349
|
-
const hasRequestedWikiScopeSupport = (record) => {
|
|
1350
|
-
if (requestedScopeKeys.size === 0) {
|
|
1351
|
-
return true;
|
|
1352
|
-
}
|
|
1353
|
-
const cached = wikiScopeSupportCache.get(record.id);
|
|
1354
|
-
if (cached !== undefined) {
|
|
1355
|
-
return cached;
|
|
1356
|
-
}
|
|
1357
|
-
const decisionIds = loadWikiDecisionIds(record);
|
|
1358
|
-
if (decisionIds.length === 0) {
|
|
1359
|
-
wikiScopeSupportCache.set(record.id, false);
|
|
1360
|
-
return false;
|
|
1361
|
-
}
|
|
1362
|
-
const scopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), decisionIds);
|
|
1363
|
-
const supported = decisionIds.some((id) => (scopeMap.get(id) ?? []).some((scope) => requestedScopeKeys.has(`${scope.kind}:${scope.id}`)));
|
|
1364
|
-
wikiScopeSupportCache.set(record.id, supported);
|
|
1365
|
-
return supported;
|
|
1366
|
-
};
|
|
1367
1265
|
// Combined check used for the wiki *filter* path: a wiki entry must have at
|
|
1368
1266
|
// least one linked decision that matches BOTH the requested scope AND the
|
|
1369
1267
|
// requested topicPrefix. Without this, the page passes when scope matches
|
|
1370
1268
|
// decision A and topic matches decision B — i.e., neither single decision
|
|
1371
1269
|
// satisfies the caller's filter. Diagnostics reporting (which only signals
|
|
1372
1270
|
// scope confirmation) keeps using hasRequestedWikiScopeSupport.
|
|
1373
|
-
const wikiCombinedSupportCache = new Map();
|
|
1374
|
-
const hasWikiDecisionWithScopeAndTopic = (record) => {
|
|
1375
|
-
const requiresScope = requestedScopeKeys.size > 0;
|
|
1376
|
-
const requiresTopic = !!searchOptions.topicPrefix;
|
|
1377
|
-
if (!requiresScope && !requiresTopic) {
|
|
1378
|
-
return true;
|
|
1379
|
-
}
|
|
1380
|
-
const cached = wikiCombinedSupportCache.get(record.id);
|
|
1381
|
-
if (cached !== undefined) {
|
|
1382
|
-
return cached;
|
|
1383
|
-
}
|
|
1384
|
-
const decisionIds = loadWikiDecisionIds(record);
|
|
1385
|
-
if (decisionIds.length === 0) {
|
|
1386
|
-
wikiCombinedSupportCache.set(record.id, false);
|
|
1387
|
-
return false;
|
|
1388
|
-
}
|
|
1389
|
-
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
1390
|
-
const scopeMap = requiresScope ? batchLoadScopes(adapter, decisionIds) : null;
|
|
1391
|
-
const topicByDecisionId = new Map();
|
|
1392
|
-
if (requiresTopic) {
|
|
1393
|
-
const IN_CHUNK_SIZE = 900;
|
|
1394
|
-
for (let i = 0; i < decisionIds.length; i += IN_CHUNK_SIZE) {
|
|
1395
|
-
const chunk = decisionIds.slice(i, i + IN_CHUNK_SIZE);
|
|
1396
|
-
const placeholders = chunk.map(() => '?').join(', ');
|
|
1397
|
-
const rows = adapter
|
|
1398
|
-
.prepare(`SELECT id, topic FROM decisions WHERE id IN (${placeholders})`)
|
|
1399
|
-
.all(...chunk);
|
|
1400
|
-
for (const row of rows) {
|
|
1401
|
-
topicByDecisionId.set(row.id, String(row.topic ?? ''));
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
const supported = decisionIds.some((id) => {
|
|
1406
|
-
if (requiresScope) {
|
|
1407
|
-
const scopes = scopeMap?.get(id) ?? [];
|
|
1408
|
-
const scopeOk = scopes.some((scope) => requestedScopeKeys.has(`${scope.kind}:${scope.id}`));
|
|
1409
|
-
if (!scopeOk) {
|
|
1410
|
-
return false;
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
if (requiresTopic) {
|
|
1414
|
-
const topic = topicByDecisionId.get(id) ?? '';
|
|
1415
|
-
if (!topic.startsWith(searchOptions.topicPrefix)) {
|
|
1416
|
-
return false;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
return true;
|
|
1420
|
-
});
|
|
1421
|
-
wikiCombinedSupportCache.set(record.id, supported);
|
|
1422
|
-
return supported;
|
|
1423
|
-
};
|
|
1424
1271
|
const buildDiagnostics = (record, graphSource) => {
|
|
1425
1272
|
const vectorSimilarity = vectorSimilarityById.get(record.id) ?? null;
|
|
1426
1273
|
const lexicalSupport = lexicalScoreById.has(record.id);
|
|
1427
|
-
const entitySupport = entitySupportIds.has(record.id);
|
|
1428
1274
|
const exactTopicSupport = hasExactTopicSupport(record);
|
|
1429
1275
|
const scopeSupport = hasRequestedScopeSupport(record);
|
|
1430
1276
|
const confirmationSignals = [
|
|
1431
1277
|
lexicalSupport ? 'lexical' : null,
|
|
1432
|
-
entitySupport ? 'entity' : null,
|
|
1433
1278
|
exactTopicSupport ? 'exact_topic' : null,
|
|
1434
1279
|
].filter((signal) => signal !== null);
|
|
1435
1280
|
const metadataSignals = [
|
|
@@ -1443,14 +1288,11 @@ async function recallMemory(query, options = {}) {
|
|
|
1443
1288
|
? 'vector_search'
|
|
1444
1289
|
: lexicalSupport
|
|
1445
1290
|
? 'lexical_search'
|
|
1446
|
-
:
|
|
1447
|
-
? 'entity_canonical'
|
|
1448
|
-
: String(record.source.source_type || 'unknown');
|
|
1291
|
+
: String(record.source.source_type || 'unknown');
|
|
1449
1292
|
return {
|
|
1450
1293
|
retrieval_source: retrievalSourceForRecord,
|
|
1451
1294
|
vector_similarity: vectorSimilarity,
|
|
1452
1295
|
lexical_support: lexicalSupport,
|
|
1453
|
-
entity_support: entitySupport,
|
|
1454
1296
|
scope_support: scopeSupport,
|
|
1455
1297
|
graph_source: graphSource,
|
|
1456
1298
|
is_vector_only: vectorSimilarity !== null && confirmationSignals.length === 0,
|
|
@@ -1459,41 +1301,6 @@ async function recallMemory(query, options = {}) {
|
|
|
1459
1301
|
candidate_threshold_used: searchOptions.threshold,
|
|
1460
1302
|
};
|
|
1461
1303
|
};
|
|
1462
|
-
const buildWikiDiagnostics = (entry) => {
|
|
1463
|
-
const lexicalSupport = entry.lexicalSupport;
|
|
1464
|
-
const exactTopicSupport = hasExactWikiSupport(entry.record);
|
|
1465
|
-
const scopeSupport = hasRequestedWikiScopeSupport(entry.record);
|
|
1466
|
-
const confirmationSignals = [
|
|
1467
|
-
lexicalSupport ? 'lexical' : null,
|
|
1468
|
-
exactTopicSupport ? 'exact_topic' : null,
|
|
1469
|
-
].filter((signal) => signal !== null);
|
|
1470
|
-
// Only attribute 'scope' when the caller actually requested a scope filter.
|
|
1471
|
-
// hasRequestedWikiScopeSupport() returns true by default for unscoped searches,
|
|
1472
|
-
// so without this guard the diagnostics would overstate why the wiki hit passed.
|
|
1473
|
-
const metadataSignals = [
|
|
1474
|
-
requestedScopeKeys.size > 0 && scopeSupport ? 'scope' : null,
|
|
1475
|
-
'graph_primary',
|
|
1476
|
-
].filter((signal) => signal !== null);
|
|
1477
|
-
const retrievalSourceForRecord = entry.vectorSimilarity !== null && lexicalSupport
|
|
1478
|
-
? 'wiki_hybrid_rrf'
|
|
1479
|
-
: entry.vectorSimilarity !== null
|
|
1480
|
-
? 'wiki_vector_search'
|
|
1481
|
-
: lexicalSupport
|
|
1482
|
-
? 'wiki_lexical_search'
|
|
1483
|
-
: 'wiki_page';
|
|
1484
|
-
return {
|
|
1485
|
-
retrieval_source: retrievalSourceForRecord,
|
|
1486
|
-
vector_similarity: entry.vectorSimilarity,
|
|
1487
|
-
lexical_support: lexicalSupport,
|
|
1488
|
-
entity_support: false,
|
|
1489
|
-
scope_support: scopeSupport,
|
|
1490
|
-
graph_source: 'primary',
|
|
1491
|
-
is_vector_only: entry.vectorSimilarity !== null && confirmationSignals.length === 0,
|
|
1492
|
-
confirmation_signals: confirmationSignals,
|
|
1493
|
-
metadata_signals: metadataSignals,
|
|
1494
|
-
candidate_threshold_used: searchOptions.threshold,
|
|
1495
|
-
};
|
|
1496
|
-
};
|
|
1497
1304
|
const passesStrictness = (hitDiagnostics) => {
|
|
1498
1305
|
if (searchOptions.strictness === 'recall') {
|
|
1499
1306
|
return true;
|
|
@@ -1504,9 +1311,7 @@ async function recallMemory(query, options = {}) {
|
|
|
1504
1311
|
if (!searchOptions.minLexicalSupport) {
|
|
1505
1312
|
return true;
|
|
1506
1313
|
}
|
|
1507
|
-
return (hitDiagnostics.lexical_support ||
|
|
1508
|
-
hitDiagnostics.entity_support ||
|
|
1509
|
-
hitDiagnostics.confirmation_signals.includes('exact_topic'));
|
|
1314
|
+
return (hitDiagnostics.lexical_support || hitDiagnostics.confirmation_signals.includes('exact_topic'));
|
|
1510
1315
|
};
|
|
1511
1316
|
const acceptedPrimaryIds = new Set();
|
|
1512
1317
|
matched = matched.flatMap((record) => {
|
|
@@ -1535,54 +1340,11 @@ async function recallMemory(query, options = {}) {
|
|
|
1535
1340
|
const cappedIds = new Set(matched.map((record) => record.id));
|
|
1536
1341
|
fusedHits = fusedHits.filter((hit) => hit.source_type !== 'decision' || cappedIds.has(hit.source_id));
|
|
1537
1342
|
}
|
|
1538
|
-
const acceptedWikiFusedHits = Array.from(wikiScores.values())
|
|
1539
|
-
.sort((a, b) => b.score - a.score)
|
|
1540
|
-
.flatMap((entry) => {
|
|
1541
|
-
if (!hasWikiDecisionWithScopeAndTopic(entry.record)) {
|
|
1542
|
-
return [];
|
|
1543
|
-
}
|
|
1544
|
-
const wikiDiagnostics = buildWikiDiagnostics(entry);
|
|
1545
|
-
if (wikiDiagnostics.is_vector_only) {
|
|
1546
|
-
diagnostics.candidate_counts.vector_only += 1;
|
|
1547
|
-
}
|
|
1548
|
-
if (!passesStrictness(wikiDiagnostics)) {
|
|
1549
|
-
diagnostics.candidate_counts.rejected_by_strictness += 1;
|
|
1550
|
-
return [];
|
|
1551
|
-
}
|
|
1552
|
-
return [
|
|
1553
|
-
{
|
|
1554
|
-
source_type: 'wiki_page',
|
|
1555
|
-
source_id: entry.record.source_locator,
|
|
1556
|
-
record: searchOptions.diagnostics
|
|
1557
|
-
? { ...entry.record, retrieval_diagnostics: wikiDiagnostics }
|
|
1558
|
-
: entry.record,
|
|
1559
|
-
fused_rank_score: entry.score,
|
|
1560
|
-
...(searchOptions.diagnostics ? { retrieval_diagnostics: wikiDiagnostics } : {}),
|
|
1561
|
-
...(entry.record.page_type === 'case' ? { page_type: 'case' } : {}),
|
|
1562
|
-
...(entry.record.case_id ? { case_id: entry.record.case_id } : {}),
|
|
1563
|
-
},
|
|
1564
|
-
];
|
|
1565
|
-
});
|
|
1566
|
-
hasWikiHits = acceptedWikiFusedHits.length > 0;
|
|
1567
|
-
fusedHits = [...fusedHits, ...acceptedWikiFusedHits].sort((left, right) => right.fused_rank_score - left.fused_rank_score);
|
|
1568
|
-
if (hasWikiHits) {
|
|
1569
|
-
retrievalSource = retrievalSource === 'none' ? 'wiki_page' : `${retrievalSource}+wiki_page`;
|
|
1570
|
-
}
|
|
1571
|
-
if (matched.length > 0) {
|
|
1572
|
-
const readIdentityIndex = await (0, read_identity_js_1.loadDecisionReadIdentityIndex)(matched.filter((record) => !record.read_identity).map((record) => record.id));
|
|
1573
|
-
for (const record of matched) {
|
|
1574
|
-
if (record.read_identity) {
|
|
1575
|
-
continue;
|
|
1576
|
-
}
|
|
1577
|
-
record.read_identity = (0, read_identity_js_1.resolveReadIdentity)(record, readIdentityIndex.get(record.id) ?? []);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
1343
|
// Enrich active records with summaries from their superseded predecessors.
|
|
1581
1344
|
// When ingestConversation extracts multiple facts under the same topic, only
|
|
1582
1345
|
// the last survives as "active" — the earlier ones become superseded and are
|
|
1583
1346
|
// excluded from search. This recovers their key information so it is not lost.
|
|
1584
1347
|
if (matched.length > 0) {
|
|
1585
|
-
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
1586
1348
|
const stmtChain = adapter.prepare(`SELECT id, summary, decision FROM decisions WHERE superseded_by = ?`);
|
|
1587
1349
|
for (const record of matched) {
|
|
1588
1350
|
const predecessors = stmtChain.all(record.id);
|
|
@@ -1613,24 +1375,29 @@ async function recallMemory(query, options = {}) {
|
|
|
1613
1375
|
created_at: m.created_at,
|
|
1614
1376
|
similarity: m.confidence ?? 0.5,
|
|
1615
1377
|
}));
|
|
1616
|
-
const
|
|
1617
|
-
const mamaDefault = mamaApiModule.default;
|
|
1618
|
-
const expanded = await mamaDefault.expandWithGraph(candidates);
|
|
1378
|
+
const expanded = await expandWithGraphInAdapter(adapter, candidates);
|
|
1619
1379
|
const primaryIds = new Set(matched.map((m) => m.id));
|
|
1620
1380
|
let expandedOnly = expanded.filter((e) => !primaryIds.has(e.id));
|
|
1621
1381
|
let expandedScopeMap = new Map();
|
|
1622
1382
|
// Re-filter expanded results: apply status and scope checks
|
|
1623
1383
|
if (!options.includeHistory) {
|
|
1624
1384
|
expandedOnly = expandedOnly.filter((e) => {
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1385
|
+
const row = adapter
|
|
1386
|
+
.prepare(`SELECT kind, status FROM decisions WHERE id = ?`)
|
|
1387
|
+
.get(e.id);
|
|
1627
1388
|
const status = row?.status || '';
|
|
1628
|
-
return !status || !EXCLUDED_STATUSES.has(status);
|
|
1389
|
+
return matchesKind(row?.kind) && (!status || !EXCLUDED_STATUSES.has(status));
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
else if (options.kind !== undefined) {
|
|
1393
|
+
expandedOnly = expandedOnly.filter((e) => {
|
|
1394
|
+
const row = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
|
|
1395
|
+
return matchesKind(row?.kind);
|
|
1629
1396
|
});
|
|
1630
1397
|
}
|
|
1631
1398
|
if (options.scopes && options.scopes.length > 0) {
|
|
1632
1399
|
const expandedIds = expandedOnly.map((e) => e.id);
|
|
1633
|
-
expandedScopeMap = batchLoadScopes(
|
|
1400
|
+
expandedScopeMap = batchLoadScopes(adapter, expandedIds);
|
|
1634
1401
|
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
1635
1402
|
expandedOnly = expandedOnly.filter((e) => {
|
|
1636
1403
|
const scopes = expandedScopeMap.get(e.id) ?? [];
|
|
@@ -1640,10 +1407,11 @@ async function recallMemory(query, options = {}) {
|
|
|
1640
1407
|
});
|
|
1641
1408
|
}
|
|
1642
1409
|
bundle.graph_context.expanded = expandedOnly.flatMap((e) => {
|
|
1410
|
+
const kindRow = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
|
|
1643
1411
|
const expandedRecord = {
|
|
1644
1412
|
id: String(e.id),
|
|
1645
1413
|
topic: String(e.topic || ''),
|
|
1646
|
-
kind: 'decision',
|
|
1414
|
+
kind: (kindRow?.kind ?? 'decision'),
|
|
1647
1415
|
summary: String(e.decision || ''),
|
|
1648
1416
|
details: '',
|
|
1649
1417
|
confidence: e.graph_rank ?? 0.5,
|
|
@@ -1695,12 +1463,11 @@ async function recallMemory(query, options = {}) {
|
|
|
1695
1463
|
// never point at nodes that never made it into the graph payload.
|
|
1696
1464
|
const acceptedExpandedIds = bundle.graph_context.expanded.map((record) => record.id);
|
|
1697
1465
|
const allIds = [...matched.map((m) => m.id), ...acceptedExpandedIds];
|
|
1698
|
-
const allEdges = await loadEdgesForIds(allIds);
|
|
1466
|
+
const allEdges = await loadEdgesForIds(adapter, allIds);
|
|
1699
1467
|
// Filter out edges pointing to decisions with excluded statuses
|
|
1700
1468
|
const activeIds = new Set(allIds);
|
|
1701
1469
|
const edgesToCheck = allEdges.filter((e) => !activeIds.has(e.to_id) || !activeIds.has(e.from_id));
|
|
1702
1470
|
if (edgesToCheck.length > 0) {
|
|
1703
|
-
const adapter = (0, db_manager_js_1.getAdapter)();
|
|
1704
1471
|
const checkIds = [
|
|
1705
1472
|
...new Set(edgesToCheck.flatMap((e) => [e.from_id, e.to_id]).filter((id) => !activeIds.has(id))),
|
|
1706
1473
|
];
|
|
@@ -1726,176 +1493,1199 @@ async function recallMemory(query, options = {}) {
|
|
|
1726
1493
|
bundle.search_meta.diagnostics = diagnostics;
|
|
1727
1494
|
}
|
|
1728
1495
|
if (options.includeProfile) {
|
|
1729
|
-
bundle.profile = await buildProfile(options.scopes ?? []);
|
|
1496
|
+
bundle.profile = await buildProfile(adapter, options.scopes ?? []);
|
|
1730
1497
|
}
|
|
1731
1498
|
return bundle;
|
|
1732
1499
|
}
|
|
1733
|
-
async function ingestMemoryInternal(
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
scopes:
|
|
1500
|
+
async function ingestMemoryInternal(adapter, input) {
|
|
1501
|
+
// Raw evidence goes through source.ingest: exactly one immutable observation
|
|
1502
|
+
// per request, no judgment row, no extraction.
|
|
1503
|
+
const normalized = input.content;
|
|
1504
|
+
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
|
|
1505
|
+
const requestedScopes = input.scopes ?? [];
|
|
1506
|
+
const access = (0, write_adapters_js_2.writeAccessForProvenance)(provenance, requestedScopes);
|
|
1507
|
+
const commandId = `source:${node_crypto_1.default
|
|
1508
|
+
.createHash('sha256')
|
|
1509
|
+
.update((0, canonicalize_js_1.canonicalizeJSON)({
|
|
1510
|
+
content: normalized,
|
|
1511
|
+
scopes: requestedScopes,
|
|
1745
1512
|
source: input.source,
|
|
1746
|
-
eventDate: input.eventDate,
|
|
1747
|
-
eventDateTime: input.eventDateTime,
|
|
1748
|
-
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1513
|
+
eventDate: input.eventDate ?? null,
|
|
1514
|
+
eventDateTime: input.eventDateTime ?? null,
|
|
1515
|
+
provenance: provenance.provenance,
|
|
1516
|
+
}))
|
|
1517
|
+
.digest('hex')
|
|
1518
|
+
.slice(0, 24)}`;
|
|
1519
|
+
const receipt = await (0, index_js_1.ingestSource)({
|
|
1520
|
+
commandId,
|
|
1521
|
+
source: {
|
|
1522
|
+
connector: input.source.source_type ?? 'ingest',
|
|
1523
|
+
id: commandId,
|
|
1524
|
+
},
|
|
1525
|
+
body: normalized,
|
|
1526
|
+
sourceAt: typeof input.eventDateTime === 'number'
|
|
1527
|
+
? input.eventDateTime
|
|
1528
|
+
: input.eventDate
|
|
1529
|
+
? Date.parse(input.eventDate)
|
|
1530
|
+
: null,
|
|
1531
|
+
metadata: {
|
|
1532
|
+
source: input.source,
|
|
1533
|
+
eventDate: input.eventDate ?? null,
|
|
1534
|
+
eventDateTime: input.eventDateTime ?? null,
|
|
1535
|
+
},
|
|
1536
|
+
scopes: [...requestedScopes],
|
|
1537
|
+
event: {
|
|
1538
|
+
actor: provenance.actor,
|
|
1539
|
+
...(provenance.source_turn_id ? { sourceTurnId: provenance.source_turn_id } : {}),
|
|
1540
|
+
...(provenance.source_refs.length > 0 ? { evidenceRefs: provenance.source_refs } : {}),
|
|
1541
|
+
reason: 'ingest memory',
|
|
1542
|
+
},
|
|
1543
|
+
}, access, { adapter });
|
|
1544
|
+
return { success: true, id: receipt.observationId };
|
|
1752
1545
|
}
|
|
1753
|
-
async function
|
|
1754
|
-
return ingestMemoryInternal((0, provenance_js_1.sanitizePublicIngestMemoryInput)(input)
|
|
1546
|
+
async function ingestMemory(adapter, input) {
|
|
1547
|
+
return ingestMemoryInternal(adapter, (0, provenance_js_1.sanitizePublicIngestMemoryInput)(input));
|
|
1755
1548
|
}
|
|
1756
1549
|
async function evolveMemory(input) {
|
|
1757
1550
|
return (0, evolution_engine_js_1.resolveMemoryEvolution)(input);
|
|
1758
1551
|
}
|
|
1759
|
-
async function buildMemoryBootstrap(params) {
|
|
1760
|
-
return (0, bootstrap_builder_js_1.buildMemoryAgentBootstrap)(params);
|
|
1552
|
+
async function buildMemoryBootstrap(adapter, params) {
|
|
1553
|
+
return (0, bootstrap_builder_js_1.buildMemoryAgentBootstrap)(adapter, params);
|
|
1761
1554
|
}
|
|
1762
1555
|
function createAuditAck(input) {
|
|
1763
1556
|
return (0, types_js_1.createMemoryAuditAck)(input);
|
|
1764
1557
|
}
|
|
1765
|
-
async function recordMemoryAudit(input) {
|
|
1766
|
-
return (0, channel_summary_state_store_js_1.recordChannelAudit)(input);
|
|
1767
|
-
}
|
|
1768
|
-
async function callExtractionLLM(prompt, options) {
|
|
1769
|
-
const model = options.model ?? 'claude-sonnet-4-6';
|
|
1770
|
-
const baseUrl = options.baseUrl ?? 'https://api.anthropic.com';
|
|
1771
|
-
// Security: only send ANTHROPIC_API_KEY to Anthropic's own domain
|
|
1772
|
-
const isAnthropicDomain = /^https?:\/\/([^/]*\.)?anthropic\.com(\/|$)/i.test(baseUrl);
|
|
1773
|
-
const apiKey = isAnthropicDomain
|
|
1774
|
-
? (options.apiKey ?? process.env.ANTHROPIC_API_KEY)
|
|
1775
|
-
: options.apiKey; // custom baseUrl must supply its own key explicitly
|
|
1776
|
-
if (!apiKey) {
|
|
1777
|
-
throw new Error(isAnthropicDomain
|
|
1778
|
-
? 'ANTHROPIC_API_KEY is required for extraction'
|
|
1779
|
-
: 'apiKey must be provided explicitly when using a custom baseUrl');
|
|
1780
|
-
}
|
|
1781
|
-
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
1782
|
-
method: 'POST',
|
|
1783
|
-
headers: {
|
|
1784
|
-
'Content-Type': 'application/json',
|
|
1785
|
-
'x-api-key': apiKey,
|
|
1786
|
-
'anthropic-version': '2023-06-01',
|
|
1787
|
-
},
|
|
1788
|
-
body: JSON.stringify({
|
|
1789
|
-
model,
|
|
1790
|
-
max_tokens: 4096,
|
|
1791
|
-
messages: [{ role: 'user', content: prompt }],
|
|
1792
|
-
}),
|
|
1793
|
-
});
|
|
1794
|
-
if (!res.ok) {
|
|
1795
|
-
const errorBody = await res.text().catch(() => 'unknown');
|
|
1796
|
-
throw new Error(`Anthropic API error ${res.status}: ${errorBody}`);
|
|
1797
|
-
}
|
|
1798
|
-
const data = (await res.json());
|
|
1799
|
-
const text = data.content?.find((c) => c.type === 'text')?.text ?? '';
|
|
1800
|
-
return (0, extraction_prompt_js_1.parseExtractionResponse)(text);
|
|
1801
|
-
}
|
|
1802
|
-
let extractionFn = callExtractionLLM;
|
|
1803
|
-
function setExtractionFn(fn) {
|
|
1804
|
-
extractionFn = fn ?? callExtractionLLM;
|
|
1558
|
+
async function recordMemoryAudit(adapter, input) {
|
|
1559
|
+
return (0, channel_summary_state_store_js_1.recordChannelAudit)(adapter, input);
|
|
1805
1560
|
}
|
|
1806
|
-
async function ingestConversationInternal(
|
|
1561
|
+
async function ingestConversationInternal(adapter, input) {
|
|
1807
1562
|
if (!input.messages || input.messages.length === 0) {
|
|
1808
1563
|
throw new Error('messages array must not be empty');
|
|
1809
1564
|
}
|
|
1565
|
+
// Extraction was removed from the write boundary: conversations are stored as
|
|
1566
|
+
// raw evidence only. The option is rejected before any write so a caller can
|
|
1567
|
+
// never mistake a raw observation for an extracted judgment.
|
|
1568
|
+
if (input.extract !== undefined) {
|
|
1569
|
+
throw new Error('ingestConversation() no longer supports the extract option; ' +
|
|
1570
|
+
'conversations are stored as raw source observations without judgment writes');
|
|
1571
|
+
}
|
|
1810
1572
|
const conversationText = input.messages.map((m) => `${m.role}: ${m.content}`).join('\n');
|
|
1811
1573
|
const topicPrefix = input.topicPrefix || '';
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
|
|
1574
|
+
const body = topicPrefix ? `${topicPrefix}${conversationText}` : conversationText;
|
|
1575
|
+
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
|
|
1576
|
+
const requestedScopes = input.scopes ?? [];
|
|
1577
|
+
const access = (0, write_adapters_js_2.writeAccessForProvenance)(provenance, requestedScopes);
|
|
1578
|
+
// No observedAt in the command: it must hash identically on a retry so the
|
|
1579
|
+
// same commandId replays its stored receipt instead of conflicting.
|
|
1580
|
+
const commandId = `source-conv:${node_crypto_1.default
|
|
1581
|
+
.createHash('sha256')
|
|
1582
|
+
.update((0, canonicalize_js_1.canonicalizeJSON)({
|
|
1583
|
+
body,
|
|
1584
|
+
scopes: requestedScopes,
|
|
1815
1585
|
source: input.source,
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1586
|
+
sessionDate: input.sessionDate ?? null,
|
|
1587
|
+
provenance: provenance.provenance,
|
|
1588
|
+
}))
|
|
1589
|
+
.digest('hex')
|
|
1590
|
+
.slice(0, 24)}`;
|
|
1591
|
+
const receipt = await (0, index_js_1.ingestSource)({
|
|
1592
|
+
commandId,
|
|
1593
|
+
source: {
|
|
1594
|
+
connector: `conversation:${input.source.source_type ?? 'unknown'}`,
|
|
1595
|
+
id: commandId,
|
|
1596
|
+
},
|
|
1597
|
+
body,
|
|
1598
|
+
sourceAt: input.sessionDate ? Date.parse(input.sessionDate) : null,
|
|
1599
|
+
metadata: {
|
|
1600
|
+
message_count: input.messages.length,
|
|
1601
|
+
roles: input.messages.map((m) => m.role),
|
|
1602
|
+
session_date: input.sessionDate ?? null,
|
|
1603
|
+
topic_prefix: topicPrefix || null,
|
|
1604
|
+
source: input.source,
|
|
1605
|
+
},
|
|
1606
|
+
scopes: [...requestedScopes],
|
|
1607
|
+
event: {
|
|
1608
|
+
actor: provenance.actor,
|
|
1609
|
+
...(provenance.source_turn_id ? { sourceTurnId: provenance.source_turn_id } : {}),
|
|
1610
|
+
...(provenance.source_refs.length > 0 ? { evidenceRefs: provenance.source_refs } : {}),
|
|
1611
|
+
reason: 'ingest conversation',
|
|
1612
|
+
},
|
|
1613
|
+
}, access, { adapter });
|
|
1614
|
+
return { rawId: receipt.observationId, extractedMemories: [] };
|
|
1615
|
+
}
|
|
1616
|
+
async function ingestConversation(adapter, input) {
|
|
1617
|
+
return ingestConversationInternal(adapter, (0, provenance_js_1.sanitizePublicIngestConversationInput)(input));
|
|
1618
|
+
}
|
|
1619
|
+
// ── Adapter-bound reads and writes the catalog actions call ─────────────────
|
|
1620
|
+
//
|
|
1621
|
+
// These were `mama-api.ts`'s, beside an ambient-handle facade for callers who
|
|
1622
|
+
// did not hold a database. The catalog holds one and calls these directly, so
|
|
1623
|
+
// they live with the rest of the memory API. What stays in `mama-api.ts` is the
|
|
1624
|
+
// facade and the formatting the CLI still uses (W22).
|
|
1625
|
+
function numberOrNull(value) {
|
|
1626
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
return value;
|
|
1630
|
+
}
|
|
1631
|
+
function resultRecord(result) {
|
|
1632
|
+
if (typeof result.record === 'object' &&
|
|
1633
|
+
result.record !== null &&
|
|
1634
|
+
!Array.isArray(result.record)) {
|
|
1635
|
+
return result.record;
|
|
1636
|
+
}
|
|
1637
|
+
return {};
|
|
1638
|
+
}
|
|
1639
|
+
function stringOrNull(value) {
|
|
1640
|
+
if (value === null || value === undefined) {
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
return String(value);
|
|
1644
|
+
}
|
|
1645
|
+
function confidenceValue(record, fallback) {
|
|
1646
|
+
const numeric = numberOrNull(record.confidence);
|
|
1647
|
+
if (numeric !== null) {
|
|
1648
|
+
return numeric;
|
|
1649
|
+
}
|
|
1650
|
+
switch (record.confidence) {
|
|
1651
|
+
case 'high':
|
|
1652
|
+
return 0.9;
|
|
1653
|
+
case 'medium':
|
|
1654
|
+
return 0.6;
|
|
1655
|
+
case 'low':
|
|
1656
|
+
return 0.3;
|
|
1657
|
+
default:
|
|
1658
|
+
return fallback;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
async function expandWithGraphInAdapter(adapter, candidates) {
|
|
1662
|
+
const graphEnhanced = new Map(); // Use Map for deduplication by ID
|
|
1663
|
+
const primaryIds = new Set(candidates.map((c) => c.id)); // Track primary candidates
|
|
1664
|
+
// Process each candidate
|
|
1665
|
+
for (const candidate of candidates) {
|
|
1666
|
+
// Add primary candidate with higher rank
|
|
1667
|
+
if (!graphEnhanced.has(candidate.id)) {
|
|
1668
|
+
graphEnhanced.set(candidate.id, {
|
|
1669
|
+
...candidate,
|
|
1670
|
+
graph_source: 'primary', // Mark as primary result
|
|
1671
|
+
graph_rank: 1.0, // Highest rank
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
// 1. Add supersedes chain (evolution history)
|
|
1675
|
+
try {
|
|
1676
|
+
const chain = await (0, graph_query_js_1.queryDecisionGraph)(adapter, candidate.topic, candidate.id);
|
|
1677
|
+
for (const decision of chain) {
|
|
1678
|
+
if (!graphEnhanced.has(decision.id)) {
|
|
1679
|
+
graphEnhanced.set(decision.id, {
|
|
1680
|
+
...decision,
|
|
1681
|
+
graph_source: 'supersedes_chain',
|
|
1682
|
+
graph_rank: 0.8, // Lower rank than primary
|
|
1683
|
+
similarity: (candidate.similarity ?? 0) * 0.9, // Inherit similarity, slightly reduced
|
|
1684
|
+
related_to: candidate.id, // Track relationship
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
catch (error) {
|
|
1690
|
+
(0, debug_logger_js_1.warn)(`Failed to get supersedes chain for ${candidate.topic}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1691
|
+
}
|
|
1692
|
+
// 2. Add semantic edges (refines, contradicts, builds_on, debates, synthesizes)
|
|
1693
|
+
try {
|
|
1694
|
+
const rawEdges = (await (0, graph_query_js_1.querySemanticEdges)(adapter, [candidate.id])) || {};
|
|
1695
|
+
const edges = {
|
|
1696
|
+
refines: rawEdges.refines || [],
|
|
1697
|
+
refined_by: rawEdges.refined_by || [],
|
|
1698
|
+
contradicts: rawEdges.contradicts || [],
|
|
1699
|
+
contradicted_by: rawEdges.contradicted_by || [],
|
|
1700
|
+
builds_on: rawEdges.builds_on || [],
|
|
1701
|
+
built_on_by: rawEdges.built_on_by || [],
|
|
1702
|
+
debates: rawEdges.debates || [],
|
|
1703
|
+
debated_by: rawEdges.debated_by || [],
|
|
1704
|
+
synthesizes: rawEdges.synthesizes || [],
|
|
1705
|
+
synthesized_by: rawEdges.synthesized_by || [],
|
|
1706
|
+
};
|
|
1707
|
+
// Helper to add edge to graph
|
|
1708
|
+
const addEdge = (edge, idField, source, rank, simFactor) => {
|
|
1709
|
+
const id = edge[idField];
|
|
1710
|
+
if (!graphEnhanced.has(id)) {
|
|
1711
|
+
graphEnhanced.set(id, {
|
|
1712
|
+
id: id,
|
|
1713
|
+
topic: edge.topic,
|
|
1714
|
+
decision: edge.decision,
|
|
1715
|
+
confidence: edge.confidence,
|
|
1716
|
+
created_at: edge.created_at,
|
|
1717
|
+
graph_source: source,
|
|
1718
|
+
graph_rank: rank,
|
|
1719
|
+
similarity: (candidate.similarity ?? 0) * simFactor,
|
|
1720
|
+
related_to: candidate.id,
|
|
1721
|
+
edge_reason: edge.reason,
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
// Add refines edges
|
|
1726
|
+
for (const edge of edges.refines) {
|
|
1727
|
+
addEdge(edge, 'to_id', 'refines', 0.7, 0.85);
|
|
1728
|
+
}
|
|
1729
|
+
// Add refined_by edges
|
|
1730
|
+
for (const edge of edges.refined_by) {
|
|
1731
|
+
addEdge(edge, 'from_id', 'refined_by', 0.7, 0.85);
|
|
1732
|
+
}
|
|
1733
|
+
// Add contradicts edges (lower rank, but still relevant)
|
|
1734
|
+
for (const edge of edges.contradicts) {
|
|
1735
|
+
addEdge(edge, 'to_id', 'contradicts', 0.6, 0.8);
|
|
1736
|
+
}
|
|
1737
|
+
// Story 2.1: Add builds_on edges (high relevance - extending prior work)
|
|
1738
|
+
for (const edge of edges.builds_on) {
|
|
1739
|
+
addEdge(edge, 'to_id', 'builds_on', 0.75, 0.9);
|
|
1740
|
+
}
|
|
1741
|
+
// Add built_on_by edges (someone built on this decision)
|
|
1742
|
+
for (const edge of edges.built_on_by) {
|
|
1743
|
+
addEdge(edge, 'from_id', 'built_on_by', 0.75, 0.9);
|
|
1744
|
+
}
|
|
1745
|
+
// Add debates edges (alternative view)
|
|
1746
|
+
for (const edge of edges.debates) {
|
|
1747
|
+
addEdge(edge, 'to_id', 'debates', 0.65, 0.85);
|
|
1748
|
+
}
|
|
1749
|
+
// Add debated_by edges
|
|
1750
|
+
for (const edge of edges.debated_by) {
|
|
1751
|
+
addEdge(edge, 'from_id', 'debated_by', 0.65, 0.85);
|
|
1752
|
+
}
|
|
1753
|
+
// Add synthesizes edges (unified approach)
|
|
1754
|
+
for (const edge of edges.synthesizes) {
|
|
1755
|
+
addEdge(edge, 'to_id', 'synthesizes', 0.7, 0.88);
|
|
1756
|
+
}
|
|
1757
|
+
// Add synthesized_by edges
|
|
1758
|
+
for (const edge of edges.synthesized_by) {
|
|
1759
|
+
addEdge(edge, 'from_id', 'synthesized_by', 0.7, 0.88);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
catch (error) {
|
|
1763
|
+
(0, debug_logger_js_1.warn)(`Failed to get semantic edges for ${candidate.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
// 3. Convert Map to Array
|
|
1767
|
+
const allResults = Array.from(graphEnhanced.values());
|
|
1768
|
+
// 4. Sort: Interleave expanded results after their related primary
|
|
1769
|
+
// This ensures edge-connected decisions appear near their source
|
|
1770
|
+
const primaryResults = allResults
|
|
1771
|
+
.filter((r) => primaryIds.has(r.id))
|
|
1772
|
+
.sort((a, b) => {
|
|
1773
|
+
const scoreA = a.final_score || a.similarity || 0;
|
|
1774
|
+
const scoreB = b.final_score || b.similarity || 0;
|
|
1775
|
+
return scoreB - scoreA;
|
|
1776
|
+
});
|
|
1777
|
+
const expandedResults = allResults.filter((r) => !primaryIds.has(r.id));
|
|
1778
|
+
// Build final results: each primary followed by its related expanded results
|
|
1779
|
+
const results = [];
|
|
1780
|
+
for (const primary of primaryResults) {
|
|
1781
|
+
results.push(primary);
|
|
1782
|
+
// Find expanded results related to this primary
|
|
1783
|
+
const relatedExpanded = expandedResults.filter((e) => e.related_to === primary.id);
|
|
1784
|
+
// Sort related by graph_rank (higher first)
|
|
1785
|
+
relatedExpanded.sort((a, b) => (b.graph_rank || 0) - (a.graph_rank || 0));
|
|
1786
|
+
// Add related expanded results right after their primary
|
|
1787
|
+
results.push(...relatedExpanded);
|
|
1788
|
+
}
|
|
1789
|
+
// Add any orphaned expanded results (shouldn't happen, but safety net)
|
|
1790
|
+
const includedIds = new Set(results.map((r) => r.id));
|
|
1791
|
+
const orphaned = expandedResults.filter((e) => !includedIds.has(e.id));
|
|
1792
|
+
results.push(...orphaned);
|
|
1793
|
+
return results;
|
|
1794
|
+
}
|
|
1795
|
+
function applyRecencyBoost(results, options = {}) {
|
|
1796
|
+
const { recencyWeight = 0.3, recencyScale = 7, recencyDecay = 0.5, disableRecency = false, } = options;
|
|
1797
|
+
if (disableRecency || recencyWeight === 0) {
|
|
1798
|
+
return results;
|
|
1799
|
+
}
|
|
1800
|
+
const now = Date.now(); // Current timestamp in milliseconds
|
|
1801
|
+
return results
|
|
1802
|
+
.map((r) => {
|
|
1803
|
+
// created_at is stored in milliseconds in the database
|
|
1804
|
+
const createdAt = typeof r.created_at === 'number' ? r.created_at : Date.parse(r.created_at || '0');
|
|
1805
|
+
const ageInDays = (now - createdAt) / (86400 * 1000);
|
|
1806
|
+
// Gaussian Decay: exp(-((age / scale)^2) / (2 * ln(1 / decay)))
|
|
1807
|
+
// At scale days: score = decay (e.g., 7 days = 50%)
|
|
1808
|
+
const gaussianDecay = Math.exp(-Math.pow(ageInDays / recencyScale, 2) / (2 * Math.log(1 / recencyDecay)));
|
|
1809
|
+
// Combine semantic similarity with recency
|
|
1810
|
+
const similarity = r.similarity ?? 0;
|
|
1811
|
+
const finalScore = similarity * (1 - recencyWeight) + gaussianDecay * recencyWeight;
|
|
1812
|
+
return {
|
|
1813
|
+
...r,
|
|
1814
|
+
recency_score: gaussianDecay,
|
|
1815
|
+
recency_age_days: Math.round(ageInDays * 10) / 10,
|
|
1816
|
+
final_score: finalScore,
|
|
1817
|
+
};
|
|
1818
|
+
})
|
|
1819
|
+
.sort((a, b) => (b.final_score ?? 0) - (a.final_score ?? 0));
|
|
1820
|
+
}
|
|
1821
|
+
/** Phase 3 Task 33: learned-ranker meta attached to mama.suggest response. */
|
|
1822
|
+
function buildRankerMeta(applied, modelId, skippedReason) {
|
|
1823
|
+
const meta = {
|
|
1824
|
+
model_id: modelId,
|
|
1825
|
+
feature_set_version: ranker_features_js_1.SEARCH_RANKER_FEATURE_SET_VERSION,
|
|
1826
|
+
applied,
|
|
1827
|
+
mode: 'offline',
|
|
1828
|
+
};
|
|
1829
|
+
if (skippedReason) {
|
|
1830
|
+
meta.skipped_reason = skippedReason;
|
|
1831
|
+
}
|
|
1832
|
+
return meta;
|
|
1833
|
+
}
|
|
1834
|
+
function mapRolledUpResult(result) {
|
|
1835
|
+
const record = resultRecord(result);
|
|
1836
|
+
const retrievalDiagnostics = result.retrieval_diagnostics;
|
|
1837
|
+
const topic = stringOrNull(record.topic ?? record.title) ?? result.source_id;
|
|
1838
|
+
// For wiki_page leaves, prefer the markdown body (`content`) in `decision` so
|
|
1839
|
+
// downstream consumers see the meaningful body rather than the short title.
|
|
1840
|
+
// Decision/checkpoint records use their own fields (summary/decision).
|
|
1841
|
+
const isWikiPageLeaf = result.source_type === 'wiki_page';
|
|
1842
|
+
const decision = isWikiPageLeaf
|
|
1843
|
+
? (stringOrNull(record.content ?? record.summary ?? record.decision ?? record.title) ??
|
|
1844
|
+
result.source_id)
|
|
1845
|
+
: (stringOrNull(record.summary ?? record.decision ?? record.title ?? record.content) ??
|
|
1846
|
+
result.source_id);
|
|
1847
|
+
const reasoning = stringOrNull(record.details ?? record.reasoning ?? record.status_reason ?? record.content) ??
|
|
1848
|
+
'';
|
|
1849
|
+
return {
|
|
1850
|
+
id: result.source_id,
|
|
1851
|
+
topic,
|
|
1852
|
+
decision,
|
|
1853
|
+
reasoning,
|
|
1854
|
+
confidence: confidenceValue(record, result.score),
|
|
1855
|
+
// NOT similarity. `result.score` is normalized Reciprocal Rank Fusion
|
|
1856
|
+
// (RRF_K = 60, divided by the top hit), so the second result scores 61/62
|
|
1857
|
+
// and the third 61/63 whatever they say -- rank, not likeness. Mirroring it
|
|
1858
|
+
// into `similarity` made every save warn "High similarity (98%)" against
|
|
1859
|
+
// something unrelated, which teaches the reader to ignore the check. The
|
|
1860
|
+
// rollup path already says this and returns null; this one now agrees.
|
|
1861
|
+
similarity: null,
|
|
1862
|
+
retrieval_score: result.score,
|
|
1863
|
+
created_at: record.created_at ?? null,
|
|
1864
|
+
event_date: record.event_date ?? null,
|
|
1865
|
+
event_datetime: record.event_datetime ?? null,
|
|
1866
|
+
graph_source: retrievalDiagnostics?.graph_source ?? 'primary',
|
|
1867
|
+
graph_rank: 1,
|
|
1868
|
+
related_to: null,
|
|
1869
|
+
edge_reason: null,
|
|
1870
|
+
case_id: result.case_id,
|
|
1871
|
+
source_type: result.source_type,
|
|
1872
|
+
kind: stringOrNull(record.kind) ?? 'decision',
|
|
1873
|
+
contributing_leaves: result.contributing_leaves ?? null,
|
|
1874
|
+
...(result.contributing_leaf_diagnostics
|
|
1875
|
+
? { contributing_leaf_diagnostics: result.contributing_leaf_diagnostics }
|
|
1876
|
+
: {}),
|
|
1877
|
+
...(retrievalDiagnostics ? { retrieval_diagnostics: retrievalDiagnostics } : {}),
|
|
1821
1878
|
};
|
|
1822
|
-
|
|
1823
|
-
|
|
1879
|
+
}
|
|
1880
|
+
async function suggestInAdapter(adapter, userQuestion, options = {}
|
|
1881
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1882
|
+
) {
|
|
1883
|
+
if (!userQuestion || typeof userQuestion !== 'string') {
|
|
1884
|
+
throw new Error('mama.suggest() requires userQuestion (string)');
|
|
1824
1885
|
}
|
|
1825
|
-
|
|
1886
|
+
const { format = 'json', limit = 5, threshold, useReranking = false, rerankWithLearned = false,
|
|
1887
|
+
// Recency boosting parameters (Gaussian Decay - Elasticsearch style)
|
|
1888
|
+
recencyWeight = 0.3, // 0-1: How much to weight recency (0.3 = 70% semantic, 30% recency)
|
|
1889
|
+
recencyScale = 7, // Days until recency score drops to 50%
|
|
1890
|
+
recencyDecay = 0.5, // Score at scale point (0.5 = 50%)
|
|
1891
|
+
disableRecency = false, // Set true to disable recency boosting entirely
|
|
1892
|
+
strict, strictness, includeRelated, minLexicalSupport, diagnostics: includeDiagnostics, kind, } = options;
|
|
1893
|
+
const normalizedSearchOptions = (0, search_quality_js_1.normalizeSearchQualityOptions)({
|
|
1894
|
+
threshold,
|
|
1895
|
+
strict,
|
|
1896
|
+
strictness,
|
|
1897
|
+
disableRecency,
|
|
1898
|
+
includeRelated,
|
|
1899
|
+
topicPrefix: options.topicPrefix,
|
|
1900
|
+
minLexicalSupport,
|
|
1901
|
+
diagnostics: includeDiagnostics,
|
|
1902
|
+
});
|
|
1903
|
+
const memoryV2QualityContractRequested = threshold !== undefined ||
|
|
1904
|
+
strict !== undefined ||
|
|
1905
|
+
strictness !== undefined ||
|
|
1906
|
+
includeRelated !== undefined ||
|
|
1907
|
+
minLexicalSupport !== undefined ||
|
|
1908
|
+
includeDiagnostics === true ||
|
|
1909
|
+
options.topicPrefix !== undefined ||
|
|
1910
|
+
options.scopes !== undefined ||
|
|
1911
|
+
options.kind !== undefined;
|
|
1912
|
+
const rerankPoolLimit = rerankWithLearned ? Math.max(limit * 4, limit + 5) : limit;
|
|
1826
1913
|
try {
|
|
1827
|
-
//
|
|
1828
|
-
|
|
1829
|
-
const
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1914
|
+
// `recallMemoryInAdapter` was mama-api's alias for this file's own
|
|
1915
|
+
// `recallMemory`. Inside it, the function has its name.
|
|
1916
|
+
const bundle = await recallMemory(adapter, userQuestion, {
|
|
1917
|
+
includeProfile: false,
|
|
1918
|
+
topicPrefix: options.topicPrefix,
|
|
1919
|
+
limit: rerankPoolLimit,
|
|
1920
|
+
threshold,
|
|
1921
|
+
strict,
|
|
1922
|
+
strictness,
|
|
1923
|
+
disableRecency,
|
|
1924
|
+
includeRelated,
|
|
1925
|
+
minLexicalSupport,
|
|
1926
|
+
diagnostics: includeDiagnostics,
|
|
1927
|
+
kind,
|
|
1928
|
+
...(options.scopes && { scopes: options.scopes }),
|
|
1929
|
+
});
|
|
1930
|
+
const diagnosticsByMemoryId = new Map(bundle.memories
|
|
1931
|
+
.filter((memory) => memory.retrieval_diagnostics)
|
|
1932
|
+
.map((memory) => [memory.id, memory.retrieval_diagnostics]));
|
|
1933
|
+
const rawFusedHits = bundle.fused_hits ?? [];
|
|
1934
|
+
const fusedHits = rawFusedHits.map((hit) => {
|
|
1935
|
+
if (hit.source_type !== 'decision') {
|
|
1936
|
+
return hit;
|
|
1937
|
+
}
|
|
1938
|
+
const recordDiagnostics = typeof hit.record === 'object' && hit.record !== null && !Array.isArray(hit.record)
|
|
1939
|
+
? hit.record.retrieval_diagnostics
|
|
1940
|
+
: undefined;
|
|
1941
|
+
const retrievalDiagnostics = diagnosticsByMemoryId.get(hit.source_id) ?? recordDiagnostics;
|
|
1942
|
+
if (!retrievalDiagnostics) {
|
|
1943
|
+
return hit;
|
|
1944
|
+
}
|
|
1945
|
+
const record = typeof hit.record === 'object' && hit.record !== null && !Array.isArray(hit.record)
|
|
1946
|
+
? { ...hit.record, retrieval_diagnostics: retrievalDiagnostics }
|
|
1947
|
+
: hit.record;
|
|
1948
|
+
return {
|
|
1949
|
+
...hit,
|
|
1950
|
+
record,
|
|
1951
|
+
retrieval_diagnostics: retrievalDiagnostics,
|
|
1952
|
+
};
|
|
1953
|
+
});
|
|
1954
|
+
const rolledUp = fusedHits.length > 0 ? (0, case_search_rollup_js_1.rollUpSearchHits)({ fusedHits, adapter }) : [];
|
|
1955
|
+
const diagnosticsResponse = includeDiagnostics === true ? { diagnostics: bundle.search_meta.diagnostics ?? null } : {};
|
|
1956
|
+
// Phase 3 Task 33: compute base ranker meta once so every return path
|
|
1957
|
+
// (rolledUp, memories fallback, vector-search fallback) can attach it.
|
|
1958
|
+
// rerankWithLearned-driven rescoring still only applies to result arrays
|
|
1959
|
+
// that match the ranker's expected shape (id + source_type + case_id).
|
|
1960
|
+
const baseRankerMeta = useReranking
|
|
1961
|
+
? buildRankerMeta(false, null, 'llm_reranking_requested')
|
|
1962
|
+
: rerankWithLearned
|
|
1963
|
+
? null // marker: rescoring requested, actual meta set per-path after rescore
|
|
1964
|
+
: buildRankerMeta(false, null, 'feature_disabled');
|
|
1965
|
+
const applyLearnedRanker = (results) => {
|
|
1966
|
+
if (baseRankerMeta !== null) {
|
|
1967
|
+
return { results, meta: baseRankerMeta };
|
|
1968
|
+
}
|
|
1969
|
+
// Phase 3 Task 33: the caller opted in via rerankWithLearned, but the
|
|
1970
|
+
// runtime `search_ranker_enabled` gate still has final say. This lets
|
|
1971
|
+
// operators disable the learned ranker globally during rollback without
|
|
1972
|
+
// touching any caller code.
|
|
1973
|
+
let runtimeEnabled = true;
|
|
1974
|
+
try {
|
|
1975
|
+
runtimeEnabled = (0, ranker_rescore_js_1.isSearchRankerEnabled)(adapter);
|
|
1976
|
+
}
|
|
1977
|
+
catch (err) {
|
|
1978
|
+
(0, debug_logger_js_1.warn)(`[mama.suggest] isSearchRankerEnabled check failed: ${String(err)}`);
|
|
1979
|
+
}
|
|
1980
|
+
if (!runtimeEnabled) {
|
|
1981
|
+
return { results, meta: buildRankerMeta(false, null, 'feature_disabled') };
|
|
1982
|
+
}
|
|
1983
|
+
try {
|
|
1984
|
+
const rescored = (0, ranker_rescore_js_1.rescoreSearchResults)(adapter, {
|
|
1985
|
+
query: userQuestion,
|
|
1986
|
+
results,
|
|
1987
|
+
});
|
|
1988
|
+
return {
|
|
1989
|
+
results: rescored.results,
|
|
1990
|
+
meta: buildRankerMeta(rescored.skipped_reason === undefined, rescored.model_id, rescored.skipped_reason),
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
catch (err) {
|
|
1994
|
+
(0, debug_logger_js_1.warn)(`[mama.suggest] learned-ranker rescore failed: ${String(err)}`);
|
|
1995
|
+
return { results, meta: buildRankerMeta(false, null, 'rescore_error') };
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
const summarizeGraphExpansion = (rows) => {
|
|
1999
|
+
const sources = {
|
|
2000
|
+
primary: 0,
|
|
2001
|
+
supersedes_chain: 0,
|
|
2002
|
+
refines: 0,
|
|
2003
|
+
refined_by: 0,
|
|
2004
|
+
contradicts: 0,
|
|
2005
|
+
};
|
|
2006
|
+
let expandedCount = 0;
|
|
2007
|
+
for (const row of rows) {
|
|
2008
|
+
const graphSource = row.graph_source ?? 'primary';
|
|
2009
|
+
if (graphSource === 'primary') {
|
|
2010
|
+
sources.primary += 1;
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
expandedCount += 1;
|
|
2014
|
+
if (graphSource in sources) {
|
|
2015
|
+
const key = graphSource;
|
|
2016
|
+
sources[key] += 1;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
return {
|
|
2020
|
+
total_results: rows.length,
|
|
2021
|
+
primary_count: sources.primary,
|
|
2022
|
+
expanded_count: expandedCount,
|
|
2023
|
+
sources,
|
|
2024
|
+
};
|
|
2025
|
+
};
|
|
2026
|
+
if (rolledUp.length > 0) {
|
|
2027
|
+
const filteredResults = rolledUp.slice(0, rerankPoolLimit);
|
|
2028
|
+
const { results: mappedResults, meta: rankerMeta } = applyLearnedRanker(filteredResults.map(mapRolledUpResult));
|
|
2029
|
+
const limitedResults = mappedResults.slice(0, limit);
|
|
2030
|
+
if (format === 'markdown') {
|
|
2031
|
+
const context = limitedResults
|
|
2032
|
+
.map((result, index) => `${index + 1}. [${result.topic}] ${result.decision}\n ${result.reasoning}`)
|
|
2033
|
+
.join('\n');
|
|
2034
|
+
return `🔍 Search method: memory_v2\n${context}`;
|
|
2035
|
+
}
|
|
2036
|
+
return {
|
|
2037
|
+
query: userQuestion,
|
|
2038
|
+
results: limitedResults,
|
|
2039
|
+
...diagnosticsResponse,
|
|
2040
|
+
meta: {
|
|
2041
|
+
count: limitedResults.length,
|
|
2042
|
+
search_method: 'memory_v2',
|
|
2043
|
+
threshold: normalizedSearchOptions.threshold,
|
|
2044
|
+
recency_boost: disableRecency
|
|
2045
|
+
? null
|
|
2046
|
+
: {
|
|
2047
|
+
weight: recencyWeight,
|
|
2048
|
+
scale: recencyScale,
|
|
2049
|
+
decay: recencyDecay,
|
|
2050
|
+
},
|
|
2051
|
+
graph_expansion: summarizeGraphExpansion(limitedResults),
|
|
2052
|
+
ranker: rankerMeta,
|
|
2053
|
+
},
|
|
2054
|
+
};
|
|
1841
2055
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2056
|
+
if (bundle.memories.length > 0) {
|
|
2057
|
+
// recallMemory uses RRF fusion — confidence is overwritten with the normalized
|
|
2058
|
+
// retrieval score (0-1 range, where 1.0 = best match in this result set).
|
|
2059
|
+
// The original stored confidence is lost after RRF normalization.
|
|
2060
|
+
// We capture the retrieval score separately so `similarity` reflects search
|
|
2061
|
+
// relevance while `confidence` is passed through as-is from the bundle.
|
|
2062
|
+
const filteredMemories = bundle.memories.slice(0, rerankPoolLimit);
|
|
2063
|
+
const baseRows = filteredMemories.map((memory) => ({
|
|
2064
|
+
id: memory.id,
|
|
2065
|
+
topic: memory.topic,
|
|
2066
|
+
decision: memory.summary,
|
|
2067
|
+
reasoning: memory.details,
|
|
2068
|
+
confidence: memory.confidence,
|
|
2069
|
+
// recallMemory currently normalizes fused retrieval rank into `confidence`.
|
|
2070
|
+
// Keep that value visible as retrieval_score, but do not pretend it is
|
|
2071
|
+
// semantic similarity; save-time warning logic keys off `similarity`.
|
|
2072
|
+
similarity: null,
|
|
2073
|
+
retrieval_score: memory.confidence ?? null,
|
|
2074
|
+
final_score: memory.confidence ?? null,
|
|
2075
|
+
created_at: memory.created_at,
|
|
2076
|
+
event_date: memory.event_date ?? null,
|
|
2077
|
+
event_datetime: memory.event_datetime ?? null,
|
|
2078
|
+
graph_source: memory.retrieval_diagnostics?.graph_source ?? 'primary',
|
|
2079
|
+
graph_rank: 1,
|
|
2080
|
+
related_to: null,
|
|
2081
|
+
edge_reason: null,
|
|
2082
|
+
case_id: null,
|
|
2083
|
+
source_type: memory.kind ??
|
|
2084
|
+
memory.source?.source_type ??
|
|
2085
|
+
memory.source_type ??
|
|
2086
|
+
memory.type ??
|
|
2087
|
+
'decision',
|
|
2088
|
+
kind: memory.kind,
|
|
2089
|
+
...(memory.retrieval_diagnostics
|
|
2090
|
+
? { retrieval_diagnostics: memory.retrieval_diagnostics }
|
|
2091
|
+
: {}),
|
|
2092
|
+
}));
|
|
2093
|
+
const { results: rankedRows, meta: rankerMeta } = applyLearnedRanker(baseRows);
|
|
2094
|
+
const limitedRows = rankedRows.slice(0, limit);
|
|
2095
|
+
if (format === 'markdown') {
|
|
2096
|
+
const context = limitedRows
|
|
2097
|
+
.map((row, index) => `${index + 1}. [${row.topic}] ${row.decision}\n ${row.reasoning}`)
|
|
2098
|
+
.join('\n');
|
|
2099
|
+
return `🔍 Search method: memory_v2\n${context}`;
|
|
2100
|
+
}
|
|
2101
|
+
return {
|
|
2102
|
+
query: userQuestion,
|
|
2103
|
+
results: limitedRows,
|
|
2104
|
+
...diagnosticsResponse,
|
|
2105
|
+
meta: {
|
|
2106
|
+
count: limitedRows.length,
|
|
2107
|
+
search_method: 'memory_v2',
|
|
2108
|
+
threshold: normalizedSearchOptions.threshold,
|
|
2109
|
+
recency_boost: disableRecency
|
|
2110
|
+
? null
|
|
2111
|
+
: {
|
|
2112
|
+
weight: recencyWeight,
|
|
2113
|
+
scale: recencyScale,
|
|
2114
|
+
decay: recencyDecay,
|
|
2115
|
+
},
|
|
2116
|
+
graph_expansion: summarizeGraphExpansion(limitedRows),
|
|
2117
|
+
ranker: rankerMeta,
|
|
2118
|
+
},
|
|
2119
|
+
};
|
|
1848
2120
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2121
|
+
if (memoryV2QualityContractRequested) {
|
|
2122
|
+
const emptyRows = [];
|
|
2123
|
+
const { meta: rankerMeta } = applyLearnedRanker(emptyRows);
|
|
2124
|
+
if (format === 'markdown') {
|
|
2125
|
+
return '🔍 Search method: memory_v2\n';
|
|
2126
|
+
}
|
|
2127
|
+
return {
|
|
2128
|
+
query: userQuestion,
|
|
2129
|
+
results: emptyRows,
|
|
2130
|
+
...diagnosticsResponse,
|
|
2131
|
+
meta: {
|
|
2132
|
+
count: 0,
|
|
2133
|
+
search_method: 'memory_v2',
|
|
2134
|
+
threshold: normalizedSearchOptions.threshold,
|
|
2135
|
+
recency_boost: disableRecency
|
|
2136
|
+
? null
|
|
2137
|
+
: {
|
|
2138
|
+
weight: recencyWeight,
|
|
2139
|
+
scale: recencyScale,
|
|
2140
|
+
decay: recencyDecay,
|
|
2141
|
+
},
|
|
2142
|
+
graph_expansion: summarizeGraphExpansion(emptyRows),
|
|
2143
|
+
ranker: rankerMeta,
|
|
2144
|
+
},
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
// 1. Try vector search first (if sqlite-vss is available)
|
|
2148
|
+
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-explicit-any
|
|
2149
|
+
let results = [];
|
|
2150
|
+
let searchMethod = 'vector';
|
|
1865
2151
|
try {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
2152
|
+
// Generate query embedding
|
|
2153
|
+
const queryEmbedding = await (0, embedder_js_1.generateEmbedding)(userQuestion, 'query');
|
|
2154
|
+
// Adaptive threshold (shorter queries need higher confidence)
|
|
2155
|
+
const wordCount = userQuestion.split(/\s+/).length;
|
|
2156
|
+
const adaptiveThreshold = threshold !== undefined ? threshold : wordCount < 3 ? 0.7 : 0.6;
|
|
2157
|
+
// Vector search
|
|
2158
|
+
results = await (0, search_js_1.vectorSearch)(adapter, queryEmbedding, rerankPoolLimit * 2, 0.5, undefined, undefined, kind); // Get more candidates
|
|
2159
|
+
// Filter by adaptive threshold
|
|
2160
|
+
results = results.filter((r) => r.similarity >= adaptiveThreshold);
|
|
2161
|
+
// Stage 1.4: Temporal boost — detect time-related queries and boost matching results
|
|
2162
|
+
{
|
|
2163
|
+
const temporalPatterns = [
|
|
2164
|
+
// English
|
|
2165
|
+
/\b(yesterday|today|last\s+(?:week|month|year)|(\d+)\s+(?:days?|weeks?|months?)\s+ago)\b/i,
|
|
2166
|
+
/\b(before|after|since|until|during)\s+\w+/i,
|
|
2167
|
+
/\b(how\s+long|when\s+did|what\s+date|what\s+day)\b/i,
|
|
2168
|
+
// Korean
|
|
2169
|
+
/(?:어제|오늘|그제|지난\s*(?:주|달|해)|(\d+)\s*(?:일|주|달|개월)\s*(?:전|후|뒤))/, // Korean: temporal query detection
|
|
2170
|
+
/(?:언제|얼마나|며칠|몇\s*(?:일|주|달|개월))/, // Korean: temporal query detection
|
|
2171
|
+
];
|
|
2172
|
+
const isTemporalQuery = temporalPatterns.some((p) => p.test(userQuestion));
|
|
2173
|
+
if (isTemporalQuery && results.length > 0) {
|
|
2174
|
+
// Boost results that contain date/time references in their content
|
|
2175
|
+
const datePatterns = [
|
|
2176
|
+
/\d{4}[-/]\d{1,2}[-/]\d{1,2}/,
|
|
2177
|
+
/(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d/i,
|
|
2178
|
+
/\d+\s*(?:일|월|년|주|시간|분)/, // Korean: date reference in content
|
|
2179
|
+
/(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i,
|
|
2180
|
+
/(?:월요일|화요일|수요일|목요일|금요일|토요일|일요일)/, // Korean: date reference in content
|
|
2181
|
+
];
|
|
2182
|
+
for (const result of results) {
|
|
2183
|
+
const content = `${result.decision || ''} ${result.reasoning || ''}`;
|
|
2184
|
+
const hasDateRef = datePatterns.some((p) => p.test(content));
|
|
2185
|
+
if (hasDateRef) {
|
|
2186
|
+
result.similarity = Math.min(1.0, (result.similarity || 0) + 0.1);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
// Stage 1.5: Apply recency boosting (Gaussian Decay)
|
|
2193
|
+
// Allows Claude to adjust search strategy (recent vs historical)
|
|
2194
|
+
if (results.length > 0 && !disableRecency) {
|
|
2195
|
+
results = applyRecencyBoost(results, {
|
|
2196
|
+
recencyWeight,
|
|
2197
|
+
recencyScale,
|
|
2198
|
+
recencyDecay,
|
|
2199
|
+
disableRecency,
|
|
2200
|
+
});
|
|
2201
|
+
searchMethod = 'vector+recency';
|
|
2202
|
+
}
|
|
2203
|
+
// Stage 1.7: FTS5 hybrid merge (Haiku Memory Layer)
|
|
2204
|
+
{
|
|
2205
|
+
try {
|
|
2206
|
+
const ftsResults = await (0, search_js_1.fts5Search)(adapter, userQuestion, rerankPoolLimit * 2, kind);
|
|
2207
|
+
if (ftsResults.length > 0) {
|
|
2208
|
+
// Normalize FTS5 ranks (BM25 returns negative values, closer to 0 = better)
|
|
2209
|
+
const maxRank = Math.max(...ftsResults.map((r) => Math.abs(r.rank)));
|
|
2210
|
+
const ftsMap = new Map(ftsResults.map((r) => [r.id, maxRank > 0 ? 1 - Math.abs(r.rank) / maxRank : 0.5]));
|
|
2211
|
+
// Tunable hybrid weights (env: MAMA_VECTOR_WEIGHT, MAMA_FTS5_WEIGHT)
|
|
2212
|
+
const vectorWeight = parseFloat(process.env.MAMA_VECTOR_WEIGHT || '0.6');
|
|
2213
|
+
const fts5Weight = parseFloat(process.env.MAMA_FTS5_WEIGHT || '0.4');
|
|
2214
|
+
// Merge: boost existing results that also matched FTS5
|
|
2215
|
+
for (const result of results) {
|
|
2216
|
+
const ftsScore = ftsMap.get(result.id);
|
|
2217
|
+
if (ftsScore !== undefined) {
|
|
2218
|
+
result.similarity = vectorWeight * result.similarity + fts5Weight * ftsScore;
|
|
2219
|
+
ftsMap.delete(result.id);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
// Add FTS5-only results (not in embedding results)
|
|
2223
|
+
for (const [id, ftsScore] of ftsMap) {
|
|
2224
|
+
const ftsResult = ftsResults.find((r) => r.id === id);
|
|
2225
|
+
if (ftsResult) {
|
|
2226
|
+
// Need to get full decision record
|
|
2227
|
+
const stmt = adapter.prepare('SELECT * FROM decisions WHERE id = ? AND superseded_by IS NULL');
|
|
2228
|
+
const decision = stmt.get(id);
|
|
2229
|
+
if (decision && (kind === undefined || decision.kind === kind)) {
|
|
2230
|
+
results.push({
|
|
2231
|
+
...decision,
|
|
2232
|
+
similarity: fts5Weight * ftsScore, // Only FTS5 score component
|
|
2233
|
+
graph_source: 'fts5',
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
// Re-sort by similarity
|
|
2239
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2240
|
+
results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
|
|
2241
|
+
searchMethod = disableRecency ? 'vector+fts5' : 'vector+recency+fts5';
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
catch {
|
|
2245
|
+
// FTS5 not available, continue with embedding-only results
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
// Stage 2: Graph expansion (NEW - Phase 1)
|
|
2249
|
+
// Expand candidates with supersedes chain and semantic edges
|
|
2250
|
+
if (results.length > 0) {
|
|
2251
|
+
const graphEnhanced = await expandWithGraphInAdapter(adapter, results);
|
|
2252
|
+
results = graphEnhanced;
|
|
2253
|
+
searchMethod = disableRecency ? 'vector+graph' : 'vector+recency+graph';
|
|
2254
|
+
}
|
|
2255
|
+
// Stage 2.5: is_static boost (after graph expansion to preserve sort order)
|
|
2256
|
+
for (const result of results) {
|
|
2257
|
+
if (result.is_static === 1) {
|
|
2258
|
+
result.final_score = Math.min(1.0, (result.final_score ?? result.similarity ?? 0) + 0.2);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
// Re-sort by final_score after is_static boost
|
|
2262
|
+
results.sort((a, b) => (b.final_score ?? b.similarity ?? 0) - (a.final_score ?? a.similarity ?? 0));
|
|
2263
|
+
}
|
|
2264
|
+
catch (vectorError) {
|
|
2265
|
+
// Fallback to keyword search if vector search unavailable
|
|
2266
|
+
(0, debug_logger_js_1.warn)(`Vector search failed: ${vectorError instanceof Error ? vectorError.message : String(vectorError)}, falling back to keyword search`);
|
|
2267
|
+
searchMethod = 'keyword';
|
|
2268
|
+
// Keyword search fallback
|
|
2269
|
+
const keywords = userQuestion
|
|
2270
|
+
.toLowerCase()
|
|
2271
|
+
.split(/\s+/)
|
|
2272
|
+
.filter((w) => w.length > 2); // Filter short words
|
|
2273
|
+
if (keywords.length === 0) {
|
|
2274
|
+
if (format === 'markdown') {
|
|
2275
|
+
return `💡 Hint: Please be more specific.\nExample: "Railway Volume settings" or "mesh parameter optimization"`;
|
|
2276
|
+
}
|
|
2277
|
+
return null; // JSON mode returns null for empty/invalid queries
|
|
2278
|
+
}
|
|
2279
|
+
// Build LIKE query for each keyword
|
|
2280
|
+
const likeConditions = keywords.map(() => '(topic LIKE ? OR decision LIKE ?)').join(' OR ');
|
|
2281
|
+
const likeParams = keywords.flatMap((k) => [`%${k}%`, `%${k}%`]);
|
|
2282
|
+
const kindClause = kind === undefined ? '' : 'AND kind = ?';
|
|
2283
|
+
const stmt = adapter.prepare(`
|
|
2284
|
+
SELECT * FROM decisions
|
|
2285
|
+
WHERE ${likeConditions}
|
|
2286
|
+
AND superseded_by IS NULL
|
|
2287
|
+
${kindClause}
|
|
2288
|
+
ORDER BY created_at DESC
|
|
2289
|
+
LIMIT ?
|
|
2290
|
+
`);
|
|
2291
|
+
const rows = (await stmt.all(...likeParams, ...(kind === undefined ? [] : [kind]), rerankPoolLimit));
|
|
2292
|
+
results = rows.map((row) => ({
|
|
2293
|
+
...row,
|
|
2294
|
+
similarity: 0.75, // Assign moderate similarity for keyword matches
|
|
2295
|
+
}));
|
|
2296
|
+
// Stage 2: Graph expansion for keyword results (Phase 1)
|
|
2297
|
+
if (results.length > 0) {
|
|
2298
|
+
const graphEnhanced = await expandWithGraphInAdapter(adapter, results);
|
|
2299
|
+
results = graphEnhanced;
|
|
2300
|
+
searchMethod = 'keyword+graph';
|
|
2301
|
+
}
|
|
1888
2302
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
2303
|
+
if (results.length === 0) {
|
|
2304
|
+
if (format === 'markdown') {
|
|
2305
|
+
const wordCount = userQuestion.split(/\s+/).length;
|
|
2306
|
+
if (wordCount < 3) {
|
|
2307
|
+
return `💡 Hint: Please be more specific.\nExample: "Why did we choose COMPLEX mesh structure?" or "What parameters are used for large layers?"`;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
return null;
|
|
1891
2311
|
}
|
|
2312
|
+
// 5. Optional: LLM re-ranking (only if requested)
|
|
2313
|
+
if (useReranking) {
|
|
2314
|
+
results = await rerankWithLLM(options.runner, userQuestion, results);
|
|
2315
|
+
}
|
|
2316
|
+
const rerankCandidateResults = results.slice(0, rerankPoolLimit);
|
|
2317
|
+
const vectorRows = rerankCandidateResults.map((r) => ({
|
|
2318
|
+
id: r.id,
|
|
2319
|
+
topic: r.topic,
|
|
2320
|
+
decision: r.decision,
|
|
2321
|
+
reasoning: r.reasoning,
|
|
2322
|
+
confidence: r.confidence,
|
|
2323
|
+
similarity: r.similarity,
|
|
2324
|
+
created_at: r.created_at,
|
|
2325
|
+
event_date: r.event_date ?? null,
|
|
2326
|
+
event_datetime: r.event_datetime ?? null,
|
|
2327
|
+
kind: r.kind ?? 'decision',
|
|
2328
|
+
// Recency metadata (NEW - Gaussian Decay)
|
|
2329
|
+
recency_score: r.recency_score,
|
|
2330
|
+
recency_age_days: r.recency_age_days,
|
|
2331
|
+
final_score: r.final_score || r.similarity, // Falls back to similarity if no recency
|
|
2332
|
+
retrieval_score: r.similarity ?? null,
|
|
2333
|
+
// Graph metadata (NEW - Phase 1)
|
|
2334
|
+
graph_source: r.graph_source || 'primary',
|
|
2335
|
+
graph_rank: r.graph_rank || 1.0,
|
|
2336
|
+
related_to: r.related_to || null,
|
|
2337
|
+
edge_reason: r.edge_reason || null,
|
|
2338
|
+
case_id: null,
|
|
2339
|
+
source_type: 'decision',
|
|
2340
|
+
}));
|
|
2341
|
+
const { results: rankedVectorRows, meta: rankerMeta } = applyLearnedRanker(vectorRows);
|
|
2342
|
+
const finalResults = rankedVectorRows.slice(0, limit);
|
|
2343
|
+
// Markdown format (for human display)
|
|
2344
|
+
if (format === 'markdown') {
|
|
2345
|
+
const context = (0, decision_formatter_js_1.formatContext)(finalResults, { maxTokens: 500 });
|
|
2346
|
+
// Add graph expansion summary if applicable
|
|
2347
|
+
let graphSummary = '';
|
|
2348
|
+
if (searchMethod.includes('graph')) {
|
|
2349
|
+
const primaryCount = finalResults.filter((r) => r.graph_source === 'primary').length;
|
|
2350
|
+
const expandedCount = finalResults.filter((r) => r.graph_source !== 'primary').length;
|
|
2351
|
+
graphSummary = `\n📊 Graph expansion: ${primaryCount} primary + ${expandedCount} related (supersedes/refines/contradicts)\n`;
|
|
2352
|
+
}
|
|
2353
|
+
return `🔍 Search method: ${searchMethod}${graphSummary}\n${context}`;
|
|
2354
|
+
}
|
|
2355
|
+
// Calculate graph expansion stats
|
|
2356
|
+
const graphStats = {
|
|
2357
|
+
total_results: finalResults.length,
|
|
2358
|
+
primary_count: finalResults.filter((r) => r.graph_source === 'primary').length,
|
|
2359
|
+
expanded_count: finalResults.filter((r) => r.graph_source !== 'primary').length,
|
|
2360
|
+
sources: {
|
|
2361
|
+
primary: finalResults.filter((r) => r.graph_source === 'primary').length,
|
|
2362
|
+
supersedes_chain: finalResults.filter((r) => r.graph_source === 'supersedes_chain').length,
|
|
2363
|
+
refines: finalResults.filter((r) => r.graph_source === 'refines').length,
|
|
2364
|
+
refined_by: finalResults.filter((r) => r.graph_source === 'refined_by').length,
|
|
2365
|
+
contradicts: finalResults.filter((r) => r.graph_source === 'contradicts').length,
|
|
2366
|
+
},
|
|
2367
|
+
};
|
|
2368
|
+
return {
|
|
2369
|
+
query: userQuestion,
|
|
2370
|
+
results: finalResults,
|
|
2371
|
+
meta: {
|
|
2372
|
+
count: finalResults.length,
|
|
2373
|
+
search_method: searchMethod,
|
|
2374
|
+
threshold: threshold || 'adaptive',
|
|
2375
|
+
// Recency boosting config (NEW - Gaussian Decay)
|
|
2376
|
+
recency_boost: disableRecency
|
|
2377
|
+
? null
|
|
2378
|
+
: {
|
|
2379
|
+
weight: recencyWeight,
|
|
2380
|
+
scale: recencyScale,
|
|
2381
|
+
decay: recencyDecay,
|
|
2382
|
+
},
|
|
2383
|
+
// Graph expansion stats (NEW - Phase 1)
|
|
2384
|
+
graph_expansion: searchMethod.includes('graph') ? graphStats : null,
|
|
2385
|
+
ranker: rankerMeta,
|
|
2386
|
+
},
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
catch (error) {
|
|
2390
|
+
// Graceful degradation
|
|
2391
|
+
(0, debug_logger_js_1.warn)(`mama.suggest() failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2392
|
+
return null;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Re-rank search results using local LLM (optional enhancement)
|
|
2397
|
+
*
|
|
2398
|
+
* @param {string} userQuestion - User's question
|
|
2399
|
+
* @param {Array} results - Vector search results
|
|
2400
|
+
* @returns {Promise<Array>} Re-ranked results
|
|
2401
|
+
*/
|
|
2402
|
+
async function rerankWithLLM(runner, userQuestion,
|
|
2403
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2404
|
+
results
|
|
2405
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2406
|
+
) {
|
|
2407
|
+
if (!runner) {
|
|
2408
|
+
// No model was stated for this runtime, so there is nothing to ask. The
|
|
2409
|
+
// ranking already computed stands, which is what an unreachable model
|
|
2410
|
+
// produced before — said once, here, rather than discovered as a failure.
|
|
2411
|
+
(0, debug_logger_js_1.warn)('Re-ranking skipped: this runtime states no model runner; using vector ranking');
|
|
2412
|
+
return results;
|
|
2413
|
+
}
|
|
2414
|
+
try {
|
|
2415
|
+
const prompt = `User asked: "${userQuestion}"
|
|
2416
|
+
|
|
2417
|
+
Found decisions (ranked by vector similarity):
|
|
2418
|
+
${results.map((r, i) => `${i + 1}. [${(r.similarity ?? 0).toFixed(3)}] ${r.topic}: ${r.decision.substring(0, 60)}...`).join('\n')}
|
|
2419
|
+
|
|
2420
|
+
Re-rank these by actual relevance to the user's intent (not just keyword similarity).
|
|
2421
|
+
Return JSON: { "ranking": [index1, index2, ...] } (0-based indices)
|
|
2422
|
+
|
|
2423
|
+
Example: { "ranking": [2, 0, 4, 1, 3] } means 3rd is most relevant, then 1st, then 5th...`;
|
|
2424
|
+
const response = await runner(prompt, {
|
|
2425
|
+
format: 'json',
|
|
2426
|
+
temperature: 0.3,
|
|
2427
|
+
maxTokens: 100,
|
|
2428
|
+
timeoutMs: 3000,
|
|
2429
|
+
});
|
|
2430
|
+
const parsed = typeof response === 'string' ? JSON.parse(response) : response;
|
|
2431
|
+
// Reorder results based on LLM ranking
|
|
2432
|
+
return parsed.ranking.map((idx) => results[idx]).filter(Boolean);
|
|
1892
2433
|
}
|
|
1893
|
-
|
|
2434
|
+
catch (error) {
|
|
2435
|
+
(0, debug_logger_js_1.warn)(`Re-ranking failed: ${error instanceof Error ? error.message : String(error)}, using vector ranking`);
|
|
2436
|
+
return results; // Fallback to vector ranking
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
/** `LIKE ? ESCAPE '\\'` pattern that matches topics starting with `prefix`, metacharacters literal. */
|
|
2440
|
+
function topicPrefixLikePattern(prefix) {
|
|
2441
|
+
return `${prefix.replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
|
|
1894
2442
|
}
|
|
1895
|
-
async function
|
|
1896
|
-
|
|
2443
|
+
async function listDecisionsInAdapter(adapter, options = {}) {
|
|
2444
|
+
const { limit = 10, format = 'json' } = options;
|
|
2445
|
+
try {
|
|
2446
|
+
let decisions;
|
|
2447
|
+
const topicPrefix = typeof options.topicPrefix === 'string' ? options.topicPrefix.trim() : '';
|
|
2448
|
+
// A prefix read keeps superseded rows: they are the item's earlier rounds.
|
|
2449
|
+
const currency = topicPrefix ? '' : 'AND d.superseded_by IS NULL';
|
|
2450
|
+
const kindClause = options.kind === undefined ? '' : 'AND d.kind = ?';
|
|
2451
|
+
const prefixClause = topicPrefix ? "AND d.topic LIKE ? ESCAPE '\\'" : '';
|
|
2452
|
+
const prefixParams = topicPrefix ? [topicPrefixLikePattern(topicPrefix)] : [];
|
|
2453
|
+
const kindParams = options.kind === undefined ? [] : [options.kind];
|
|
2454
|
+
if (options.scopes && options.scopes.length > 0) {
|
|
2455
|
+
// Scope-filtered query: JOIN memory_scope_bindings + memory_scopes
|
|
2456
|
+
const scopeIds = await Promise.all(options.scopes.map((s) => (0, db_manager_js_1.ensureMemoryScope)(adapter, s.kind, s.id)));
|
|
2457
|
+
const placeholders = scopeIds.map(() => '?').join(', ');
|
|
2458
|
+
const stmt = adapter.prepare(`
|
|
2459
|
+
SELECT DISTINCT d.* FROM decisions d
|
|
2460
|
+
JOIN memory_scope_bindings msb ON msb.memory_id = d.id
|
|
2461
|
+
WHERE msb.scope_id IN (${placeholders})
|
|
2462
|
+
${currency}
|
|
2463
|
+
${kindClause}
|
|
2464
|
+
${prefixClause}
|
|
2465
|
+
ORDER BY COALESCE(d.event_datetime, d.created_at) DESC, d.created_at DESC
|
|
2466
|
+
LIMIT ?
|
|
2467
|
+
`);
|
|
2468
|
+
decisions = await stmt.all(...scopeIds, ...kindParams, ...prefixParams, limit);
|
|
2469
|
+
}
|
|
2470
|
+
else {
|
|
2471
|
+
const stmt = adapter.prepare(`
|
|
2472
|
+
SELECT d.* FROM decisions d
|
|
2473
|
+
WHERE 1 = 1
|
|
2474
|
+
${currency}
|
|
2475
|
+
${kindClause}
|
|
2476
|
+
${prefixClause}
|
|
2477
|
+
ORDER BY COALESCE(d.event_datetime, d.created_at) DESC, d.created_at DESC
|
|
2478
|
+
LIMIT ?
|
|
2479
|
+
`);
|
|
2480
|
+
decisions = await stmt.all(...kindParams, ...prefixParams, limit);
|
|
2481
|
+
}
|
|
2482
|
+
if (format === 'markdown') {
|
|
2483
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2484
|
+
return (0, decision_formatter_js_1.formatList)(decisions);
|
|
2485
|
+
}
|
|
2486
|
+
return decisions;
|
|
2487
|
+
}
|
|
2488
|
+
catch (error) {
|
|
2489
|
+
throw new Error(`mama.listDecisions() failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2490
|
+
}
|
|
1897
2491
|
}
|
|
1898
|
-
async function
|
|
1899
|
-
|
|
2492
|
+
async function updateOutcomeInAdapter(adapter, decisionId, { outcome, failure_reason, limitation }) {
|
|
2493
|
+
if (!decisionId || typeof decisionId !== 'string') {
|
|
2494
|
+
throw new Error('mama.updateOutcome() requires decisionId (string)');
|
|
2495
|
+
}
|
|
2496
|
+
// AX Improvement: Be forgiving with case sensitivity
|
|
2497
|
+
const normalizedOutcome = outcome ? outcome.toUpperCase() : null;
|
|
2498
|
+
if (!normalizedOutcome || !['SUCCESS', 'FAILED', 'PARTIAL'].includes(normalizedOutcome)) {
|
|
2499
|
+
throw new Error('mama.updateOutcome() outcome must be "SUCCESS", "FAILED", or "PARTIAL"');
|
|
2500
|
+
}
|
|
2501
|
+
try {
|
|
2502
|
+
// Append-only: one judgment record carries the outcome change; the
|
|
2503
|
+
// maintained decisions projection columns move in the same transaction.
|
|
2504
|
+
await (0, write_adapters_js_1.appendOutcomeAmendment)(decisionId, {
|
|
2505
|
+
outcome: normalizedOutcome,
|
|
2506
|
+
failureReason: failure_reason || null,
|
|
2507
|
+
limitation: limitation || null,
|
|
2508
|
+
eventReason: `mama.updateOutcome(${decisionId})`,
|
|
2509
|
+
}, { adapter });
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
catch (error) {
|
|
2513
|
+
throw new Error(`mama.updateOutcome() failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
/**
|
|
2517
|
+
* Counts over the admitted corpus.
|
|
2518
|
+
*
|
|
2519
|
+
* Every count answers the same question the scope filter answers for a listing:
|
|
2520
|
+
* a decision this caller may not read is not a decision it may count. An
|
|
2521
|
+
* admitted-empty caller counts zero rather than counting everything — the same
|
|
2522
|
+
* fail-closed reading the listing gives.
|
|
2523
|
+
*/
|
|
2524
|
+
async function readMemoryStatsInAdapter(adapter, scopes) {
|
|
2525
|
+
const empty = {
|
|
2526
|
+
total: 0,
|
|
2527
|
+
thisWeek: 0,
|
|
2528
|
+
thisMonth: 0,
|
|
2529
|
+
checkpoints: 0,
|
|
2530
|
+
outcomes: {},
|
|
2531
|
+
topTopics: [],
|
|
2532
|
+
};
|
|
2533
|
+
if (scopes.length === 0) {
|
|
2534
|
+
return empty;
|
|
2535
|
+
}
|
|
2536
|
+
const scopeIds = await Promise.all(scopes.map((scope) => (0, db_manager_js_1.ensureMemoryScope)(adapter, scope.kind, scope.id)));
|
|
2537
|
+
const placeholders = scopeIds.map(() => '?').join(', ');
|
|
2538
|
+
const admitted = `
|
|
2539
|
+
JOIN memory_scope_bindings msb ON msb.memory_id = d.id
|
|
2540
|
+
WHERE msb.scope_id IN (${placeholders})
|
|
2541
|
+
`;
|
|
2542
|
+
const now = Date.now();
|
|
2543
|
+
const countSince = async (since) => {
|
|
2544
|
+
const stmt = adapter.prepare(`
|
|
2545
|
+
SELECT COUNT(DISTINCT d.id) as count FROM decisions d
|
|
2546
|
+
${admitted}
|
|
2547
|
+
${since === undefined ? '' : 'AND d.created_at > ?'}
|
|
2548
|
+
`);
|
|
2549
|
+
const row = (await stmt.get(...scopeIds, ...(since === undefined ? [] : [since])));
|
|
2550
|
+
return row?.count ?? 0;
|
|
2551
|
+
};
|
|
2552
|
+
const day = 24 * 60 * 60 * 1000;
|
|
2553
|
+
const [total, thisWeek, thisMonth] = await Promise.all([
|
|
2554
|
+
countSince(),
|
|
2555
|
+
countSince(now - 7 * day),
|
|
2556
|
+
countSince(now - 30 * day),
|
|
2557
|
+
]);
|
|
2558
|
+
const outcomeRows = (await adapter
|
|
2559
|
+
.prepare(`SELECT d.outcome as outcome, COUNT(DISTINCT d.id) as count FROM decisions d
|
|
2560
|
+
${admitted} AND d.outcome IS NOT NULL
|
|
2561
|
+
GROUP BY d.outcome`)
|
|
2562
|
+
.all(...scopeIds));
|
|
2563
|
+
const outcomes = {};
|
|
2564
|
+
for (const row of outcomeRows) {
|
|
2565
|
+
outcomes[row.outcome?.toLowerCase() ?? 'unknown'] = row.count;
|
|
2566
|
+
}
|
|
2567
|
+
const topTopics = (await adapter
|
|
2568
|
+
.prepare(`SELECT d.topic as topic, COUNT(DISTINCT d.id) as count FROM decisions d
|
|
2569
|
+
${admitted} AND d.topic IS NOT NULL
|
|
2570
|
+
GROUP BY d.topic
|
|
2571
|
+
ORDER BY count DESC
|
|
2572
|
+
LIMIT 5`)
|
|
2573
|
+
.all(...scopeIds));
|
|
2574
|
+
// Checkpoints carry no scope binding, and `memory.checkpoint.load` reads them
|
|
2575
|
+
// under the same authority: the count is of what that read can reach.
|
|
2576
|
+
const checkpointRow = (await adapter
|
|
2577
|
+
.prepare('SELECT COUNT(*) as count FROM checkpoints')
|
|
2578
|
+
.get());
|
|
2579
|
+
return {
|
|
2580
|
+
total,
|
|
2581
|
+
thisWeek,
|
|
2582
|
+
thisMonth,
|
|
2583
|
+
checkpoints: checkpointRow?.count ?? 0,
|
|
2584
|
+
outcomes,
|
|
2585
|
+
topTopics,
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Save current session checkpoint (New Feature: Session Continuity)
|
|
2590
|
+
*
|
|
2591
|
+
* @param {string} summary - Summary of current session state
|
|
2592
|
+
* @param {Array<string>} openFiles - List of currently open files
|
|
2593
|
+
* @param {string} nextSteps - Next steps to be taken
|
|
2594
|
+
* @returns {Promise<number>} Checkpoint ID
|
|
2595
|
+
*/
|
|
2596
|
+
async function saveCheckpointInAdapter(adapter, summary, openFiles = [], nextSteps = '',
|
|
2597
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2598
|
+
recentConversation = []) {
|
|
2599
|
+
if (!summary) {
|
|
2600
|
+
throw new Error('Summary is required for checkpoint');
|
|
2601
|
+
}
|
|
2602
|
+
const scan = (0, secret_filter_js_1.scanMemoryWriteInput)({
|
|
2603
|
+
summary,
|
|
2604
|
+
open_files: openFiles,
|
|
2605
|
+
next_steps: nextSteps,
|
|
2606
|
+
recent_conversation: recentConversation,
|
|
2607
|
+
});
|
|
2608
|
+
if (!scan.clean)
|
|
2609
|
+
throw new secret_filter_js_1.SecretMaterialRefusedError(scan.matches);
|
|
2610
|
+
if (scan.warnings.length > 0)
|
|
2611
|
+
(0, debug_logger_js_2.warn)(`Checkpoint content warnings: ${scan.warnings.join(', ')}`);
|
|
2612
|
+
try {
|
|
2613
|
+
const stmt = adapter.prepare(`
|
|
2614
|
+
INSERT INTO checkpoints (timestamp, summary, open_files, next_steps, recent_conversation, status)
|
|
2615
|
+
VALUES (?, ?, ?, ?, ?, 'active')
|
|
2616
|
+
`);
|
|
2617
|
+
const result = stmt.run(Date.now(), summary, JSON.stringify(openFiles), nextSteps, JSON.stringify(recentConversation || []));
|
|
2618
|
+
return result.lastInsertRowid;
|
|
2619
|
+
}
|
|
2620
|
+
catch (error) {
|
|
2621
|
+
throw new Error(`Failed to save checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
async function loadCheckpointInAdapter(adapter) {
|
|
2625
|
+
try {
|
|
2626
|
+
const stmt = adapter.prepare(`
|
|
2627
|
+
SELECT * FROM checkpoints
|
|
2628
|
+
WHERE status = 'active'
|
|
2629
|
+
ORDER BY timestamp DESC
|
|
2630
|
+
LIMIT 1
|
|
2631
|
+
`);
|
|
2632
|
+
const checkpoint = stmt.get();
|
|
2633
|
+
if (checkpoint) {
|
|
2634
|
+
try {
|
|
2635
|
+
checkpoint.open_files =
|
|
2636
|
+
typeof checkpoint.open_files === 'string'
|
|
2637
|
+
? JSON.parse(checkpoint.open_files)
|
|
2638
|
+
: checkpoint.open_files || [];
|
|
2639
|
+
}
|
|
2640
|
+
catch {
|
|
2641
|
+
checkpoint.open_files = [];
|
|
2642
|
+
}
|
|
2643
|
+
try {
|
|
2644
|
+
checkpoint.recent_conversation =
|
|
2645
|
+
typeof checkpoint.recent_conversation === 'string'
|
|
2646
|
+
? JSON.parse(checkpoint.recent_conversation || '[]')
|
|
2647
|
+
: checkpoint.recent_conversation || [];
|
|
2648
|
+
}
|
|
2649
|
+
catch {
|
|
2650
|
+
checkpoint.recent_conversation = [];
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
return checkpoint || null;
|
|
2654
|
+
}
|
|
2655
|
+
catch (error) {
|
|
2656
|
+
throw new Error(`Failed to load checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
async function listCheckpointsInAdapter(adapter, limit = 10) {
|
|
2660
|
+
try {
|
|
2661
|
+
const stmt = adapter.prepare(`
|
|
2662
|
+
SELECT * FROM checkpoints
|
|
2663
|
+
ORDER BY timestamp DESC
|
|
2664
|
+
LIMIT ?
|
|
2665
|
+
`);
|
|
2666
|
+
const checkpoints = stmt.all(limit);
|
|
2667
|
+
return checkpoints.map((c) => {
|
|
2668
|
+
try {
|
|
2669
|
+
c.open_files =
|
|
2670
|
+
typeof c.open_files === 'string' ? JSON.parse(c.open_files) : c.open_files || [];
|
|
2671
|
+
}
|
|
2672
|
+
catch {
|
|
2673
|
+
c.open_files = [];
|
|
2674
|
+
}
|
|
2675
|
+
try {
|
|
2676
|
+
c.recent_conversation =
|
|
2677
|
+
typeof c.recent_conversation === 'string'
|
|
2678
|
+
? JSON.parse(c.recent_conversation)
|
|
2679
|
+
: c.recent_conversation || [];
|
|
2680
|
+
}
|
|
2681
|
+
catch {
|
|
2682
|
+
c.recent_conversation = [];
|
|
2683
|
+
}
|
|
2684
|
+
return c;
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
catch (error) {
|
|
2688
|
+
throw new Error(`Failed to list checkpoints: ${error instanceof Error ? error.message : String(error)}`);
|
|
2689
|
+
}
|
|
1900
2690
|
}
|
|
1901
2691
|
//# sourceMappingURL=api.js.map
|