@remnic/core 9.3.712 → 9.3.714
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/dist/access-boundary.d.ts +6 -6
- package/dist/access-boundary.js +17 -16
- package/dist/access-cli.js +40 -39
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +24 -23
- package/dist/access-mcp.d.ts +18 -5
- package/dist/access-mcp.js +21 -20
- package/dist/access-operations-batch.js +18 -17
- package/dist/access-operations.d.ts +6 -5
- package/dist/access-operations.js +20 -19
- package/dist/access-schema.d.ts +10 -0
- package/dist/access-schema.js +1 -1
- package/dist/{access-service-Dx2rSJjH.d.ts → access-service-lddjZoRh.d.ts} +20 -5
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +16 -15
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-surface-catalog.js +6 -1
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +3 -1
- package/dist/active-memory-bridge.js +2 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +4 -3
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +8 -7
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-CadPvH29.d.ts → catalog-CbR0CsUw.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +9 -8
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-OMSTU233.js → chunk-2KAYTPPT.js} +4 -4
- package/dist/{chunk-VYZNWH4J.js → chunk-2SXMCQEQ.js} +3 -3
- package/dist/{chunk-NXL5CVE7.js → chunk-2Y6LMOQQ.js} +2 -2
- package/dist/{chunk-AMXOCZHE.js → chunk-3VUPWDSE.js} +15 -3
- package/dist/{chunk-AMXOCZHE.js.map → chunk-3VUPWDSE.js.map} +1 -1
- package/dist/{chunk-FVQJYWH7.js → chunk-5GKPFL3S.js} +1 -1
- package/dist/{chunk-FVQJYWH7.js.map → chunk-5GKPFL3S.js.map} +1 -1
- package/dist/{chunk-LQ4J7ELC.js → chunk-67HG7KFE.js} +2 -2
- package/dist/{chunk-ZBG4K36I.js → chunk-6FUJ2VYY.js} +3 -3
- package/dist/{chunk-GMRNKPWO.js → chunk-6QUPUTT7.js} +2 -2
- package/dist/{chunk-Z56IHRVV.js → chunk-6UU43ZGD.js} +2 -2
- package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
- package/dist/{chunk-OWOKOFXK.js → chunk-AJOIM6UR.js} +2 -2
- package/dist/{chunk-JBPKEARU.js → chunk-AU7Q3LSC.js} +4 -4
- package/dist/{chunk-YVWZW37D.js → chunk-BNOAB5HH.js} +62 -12
- package/dist/{chunk-YVWZW37D.js.map → chunk-BNOAB5HH.js.map} +1 -1
- package/dist/{chunk-A2R5NV6O.js → chunk-CTAHKIG6.js} +205 -29
- package/dist/chunk-CTAHKIG6.js.map +1 -0
- package/dist/{chunk-K22IJO4R.js → chunk-CTOQEZSN.js} +2 -2
- package/dist/{chunk-SINGJCUR.js → chunk-CX77GNMB.js} +14 -3
- package/dist/chunk-CX77GNMB.js.map +1 -0
- package/dist/{chunk-HSDJCT3V.js → chunk-DXGS5YOQ.js} +3 -3
- package/dist/{chunk-HV57RHMD.js → chunk-EH5ED3C6.js} +2 -2
- package/dist/{chunk-NUWZFSKC.js → chunk-FVZ2ISIX.js} +9 -1
- package/dist/chunk-FVZ2ISIX.js.map +1 -0
- package/dist/{chunk-GKPIY7EQ.js → chunk-FXZKXW7C.js} +117 -46
- package/dist/chunk-FXZKXW7C.js.map +1 -0
- package/dist/{chunk-7NDYFAJS.js → chunk-GM3BMWKR.js} +2 -2
- package/dist/{chunk-ZVDRL6SK.js → chunk-HHLWSWWF.js} +2 -2
- package/dist/chunk-HHLWSWWF.js.map +1 -0
- package/dist/{chunk-5QUS24F7.js → chunk-I56LKMF7.js} +2 -2
- package/dist/{chunk-Z3B2YW56.js → chunk-IHMT6XTY.js} +9 -5
- package/dist/chunk-IHMT6XTY.js.map +1 -0
- package/dist/chunk-JAKIDQB2.js +173 -0
- package/dist/chunk-JAKIDQB2.js.map +1 -0
- package/dist/{chunk-35YJ6KCV.js → chunk-JEIROMLX.js} +2 -2
- package/dist/{chunk-SKQCFAYU.js → chunk-JVEMUX6L.js} +2 -2
- package/dist/{chunk-CXMXAC5R.js → chunk-K44MIN6D.js} +3 -3
- package/dist/{chunk-5KQCOIPW.js → chunk-MECU6WSH.js} +188 -31
- package/dist/chunk-MECU6WSH.js.map +1 -0
- package/dist/{chunk-BQCFXAMN.js → chunk-MPHDVHIB.js} +2 -2
- package/dist/{chunk-BOGENF7P.js → chunk-MSVSP5VO.js} +1 -1
- package/dist/chunk-MSVSP5VO.js.map +1 -0
- package/dist/{chunk-QXNFQKWU.js → chunk-MTHNJCOJ.js} +2 -2
- package/dist/{chunk-3TCRU4JA.js → chunk-O62P2LFL.js} +2 -2
- package/dist/{chunk-JSDZMOT7.js → chunk-OC56UWV5.js} +11 -11
- package/dist/{chunk-7IBEWQLG.js → chunk-QOIONSOI.js} +2 -2
- package/dist/{chunk-4P5UAL3C.js → chunk-RBHANGVX.js} +6 -6
- package/dist/{chunk-SK2CR6MW.js → chunk-RVLB4E6N.js} +2 -2
- package/dist/{chunk-I3BT2IDW.js → chunk-SYDHFFAW.js} +1110 -28
- package/dist/chunk-SYDHFFAW.js.map +1 -0
- package/dist/{chunk-VILEUJXC.js → chunk-UC4YOAC2.js} +74 -2
- package/dist/{chunk-VILEUJXC.js.map → chunk-UC4YOAC2.js.map} +1 -1
- package/dist/{chunk-CNVIWMQI.js → chunk-UY4JMEAK.js} +2 -2
- package/dist/{chunk-S6FQLQGH.js → chunk-V7GDTQ6I.js} +2 -2
- package/dist/{chunk-JNOYYWCA.js → chunk-VO4UICII.js} +3 -3
- package/dist/{chunk-54TI5GLV.js → chunk-WI4QX7GH.js} +3 -3
- package/dist/{chunk-3XHD3XGK.js → chunk-XCTDHF7U.js} +30 -2
- package/dist/chunk-XCTDHF7U.js.map +1 -0
- package/dist/{chunk-XKU4YE6Z.js → chunk-XZTUMWKQ.js} +2 -2
- package/dist/{cli-DTWRoJEI.d.ts → cli-iAiVkbYJ.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +43 -42
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +8 -7
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -3
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +8 -7
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +9 -8
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +8 -7
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/explicit-capture.js +3 -2
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +4 -3
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +62 -61
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +3 -3
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +8 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -7
- package/dist/maintenance/rebuild-memory-projection.js +9 -8
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +12 -11
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +8 -7
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +17 -16
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +10 -9
- package/dist/{orchestrator-B1iVjEyc.d.ts → orchestrator-iVZWi8aW.d.ts} +21 -4
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +32 -31
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/provenance.js +3 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-handles.d.ts +228 -0
- package/dist/recall-handles.js +42 -0
- package/dist/recall-handles.js.map +1 -0
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +40 -2
- package/dist/recall-state.js +3 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +5 -4
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +6 -2
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +2 -2
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +4 -4
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-CY8uVRNJ.d.ts → semantic-consolidation-DSjMDZ1u.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +9 -8
- package/dist/semantic-rule-promotion.js +8 -7
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +8 -7
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +3 -3
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +7 -6
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/{types-Dr0Cw3L9.d.ts → types-D9dwzU_x.d.ts} +25 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +8 -7
- package/package.json +2 -2
- package/src/access-boundary.ts +4 -1
- package/src/access-http.ts +78 -20
- package/src/access-mcp.test.ts +29 -0
- package/src/access-mcp.ts +99 -17
- package/src/access-operations-batch.ts +14 -0
- package/src/access-operations.ts +5 -0
- package/src/access-schema.ts +8 -0
- package/src/access-service-observe-idempotency.test.ts +308 -0
- package/src/access-service.ts +106 -5
- package/src/access-surface-catalog.test.ts +4 -2
- package/src/access-surface-catalog.ts +5 -0
- package/src/active-memory-bridge.test.ts +72 -0
- package/src/active-memory-bridge.ts +21 -2
- package/src/chat/chat-cli.test.ts +142 -0
- package/src/chat/chat-cli.ts +206 -0
- package/src/chat/chat-config.ts +57 -0
- package/src/chat/chat-engine.test.ts +272 -0
- package/src/chat/chat-engine.ts +518 -0
- package/src/chat/chat-executor.ts +133 -0
- package/src/chat/chat-factory.ts +161 -0
- package/src/chat/chat-http.test.ts +197 -0
- package/src/chat/chat-http.ts +222 -0
- package/src/chat/chat-llm.ts +229 -0
- package/src/chat/chat-mcp.test.ts +135 -0
- package/src/chat/chat-security.test.ts +491 -0
- package/src/chat/chat-session.ts +296 -0
- package/src/chat/chat-system-prompt.ts +76 -0
- package/src/chat/chat-tools.ts +255 -0
- package/src/chat/chat-types.ts +167 -0
- package/src/cli.ts +20 -0
- package/src/config.ts +9 -0
- package/src/orchestrator.ts +106 -5
- package/src/recall-handles-wiring.test.ts +477 -0
- package/src/recall-handles.test.ts +283 -0
- package/src/recall-handles.ts +414 -0
- package/src/recall-state.ts +99 -0
- package/src/sanitize.ts +6 -0
- package/src/types.ts +29 -0
- package/dist/chunk-3XHD3XGK.js.map +0 -1
- package/dist/chunk-5KQCOIPW.js.map +0 -1
- package/dist/chunk-A2R5NV6O.js.map +0 -1
- package/dist/chunk-BOGENF7P.js.map +0 -1
- package/dist/chunk-GKPIY7EQ.js.map +0 -1
- package/dist/chunk-I3BT2IDW.js.map +0 -1
- package/dist/chunk-NUWZFSKC.js.map +0 -1
- package/dist/chunk-SINGJCUR.js.map +0 -1
- package/dist/chunk-Z3B2YW56.js.map +0 -1
- package/dist/chunk-ZVDRL6SK.js.map +0 -1
- /package/dist/{chunk-OMSTU233.js.map → chunk-2KAYTPPT.js.map} +0 -0
- /package/dist/{chunk-VYZNWH4J.js.map → chunk-2SXMCQEQ.js.map} +0 -0
- /package/dist/{chunk-NXL5CVE7.js.map → chunk-2Y6LMOQQ.js.map} +0 -0
- /package/dist/{chunk-LQ4J7ELC.js.map → chunk-67HG7KFE.js.map} +0 -0
- /package/dist/{chunk-ZBG4K36I.js.map → chunk-6FUJ2VYY.js.map} +0 -0
- /package/dist/{chunk-GMRNKPWO.js.map → chunk-6QUPUTT7.js.map} +0 -0
- /package/dist/{chunk-Z56IHRVV.js.map → chunk-6UU43ZGD.js.map} +0 -0
- /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
- /package/dist/{chunk-OWOKOFXK.js.map → chunk-AJOIM6UR.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-AU7Q3LSC.js.map} +0 -0
- /package/dist/{chunk-K22IJO4R.js.map → chunk-CTOQEZSN.js.map} +0 -0
- /package/dist/{chunk-HSDJCT3V.js.map → chunk-DXGS5YOQ.js.map} +0 -0
- /package/dist/{chunk-HV57RHMD.js.map → chunk-EH5ED3C6.js.map} +0 -0
- /package/dist/{chunk-7NDYFAJS.js.map → chunk-GM3BMWKR.js.map} +0 -0
- /package/dist/{chunk-5QUS24F7.js.map → chunk-I56LKMF7.js.map} +0 -0
- /package/dist/{chunk-35YJ6KCV.js.map → chunk-JEIROMLX.js.map} +0 -0
- /package/dist/{chunk-SKQCFAYU.js.map → chunk-JVEMUX6L.js.map} +0 -0
- /package/dist/{chunk-CXMXAC5R.js.map → chunk-K44MIN6D.js.map} +0 -0
- /package/dist/{chunk-BQCFXAMN.js.map → chunk-MPHDVHIB.js.map} +0 -0
- /package/dist/{chunk-QXNFQKWU.js.map → chunk-MTHNJCOJ.js.map} +0 -0
- /package/dist/{chunk-3TCRU4JA.js.map → chunk-O62P2LFL.js.map} +0 -0
- /package/dist/{chunk-JSDZMOT7.js.map → chunk-OC56UWV5.js.map} +0 -0
- /package/dist/{chunk-7IBEWQLG.js.map → chunk-QOIONSOI.js.map} +0 -0
- /package/dist/{chunk-4P5UAL3C.js.map → chunk-RBHANGVX.js.map} +0 -0
- /package/dist/{chunk-SK2CR6MW.js.map → chunk-RVLB4E6N.js.map} +0 -0
- /package/dist/{chunk-CNVIWMQI.js.map → chunk-UY4JMEAK.js.map} +0 -0
- /package/dist/{chunk-S6FQLQGH.js.map → chunk-V7GDTQ6I.js.map} +0 -0
- /package/dist/{chunk-JNOYYWCA.js.map → chunk-VO4UICII.js.map} +0 -0
- /package/dist/{chunk-54TI5GLV.js.map → chunk-WI4QX7GH.js.map} +0 -0
- /package/dist/{chunk-XKU4YE6Z.js.map → chunk-XZTUMWKQ.js.map} +0 -0
package/src/access-mcp.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
buildChatGptMemoryInspectorResult,
|
|
35
35
|
type RemnicChatGptMemoryInspectorInput,
|
|
36
36
|
} from "./mcp-memory-inspector-app.js";
|
|
37
|
+
import { processChatMessage } from "./chat/chat-factory.js";
|
|
37
38
|
|
|
38
39
|
type JsonRpcId = string | number | null;
|
|
39
40
|
|
|
@@ -50,6 +51,12 @@ type McpRequestOptions = {
|
|
|
50
51
|
sessionKeyOverride?: string;
|
|
51
52
|
sessionId?: string;
|
|
52
53
|
correlationId?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Write-quota enforcer forwarded into observe's idempotency lock
|
|
56
|
+
* (issue #1649). Only the MCP-over-HTTP transport supplies this (it owns
|
|
57
|
+
* the write-rate-limit window); the standalone MCP server has no quota.
|
|
58
|
+
*/
|
|
59
|
+
enforceWriteQuota?: () => void | Promise<void>;
|
|
53
60
|
};
|
|
54
61
|
|
|
55
62
|
type McpTool = {
|
|
@@ -365,6 +372,13 @@ export class EngramMcpServer {
|
|
|
365
372
|
*/
|
|
366
373
|
private readonly correctionVisible: boolean;
|
|
367
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Whether the `memory_chat` tool should appear in `tools/list` (issue
|
|
377
|
+
* #1583). Gated on `chat.enabled`. When false the tools array is
|
|
378
|
+
* byte-identical to pre-feature (rule 39).
|
|
379
|
+
*/
|
|
380
|
+
private readonly chatVisible: boolean;
|
|
381
|
+
|
|
368
382
|
constructor(
|
|
369
383
|
private readonly service: EngramAccessService,
|
|
370
384
|
options: {
|
|
@@ -377,6 +391,7 @@ export class EngramMcpServer {
|
|
|
377
391
|
codegraphVisible?: boolean;
|
|
378
392
|
sessionDeltaVisible?: boolean;
|
|
379
393
|
correctionVisible?: boolean;
|
|
394
|
+
chatVisible?: boolean;
|
|
380
395
|
} = {},
|
|
381
396
|
) {
|
|
382
397
|
this.citationsEnabled = options.citationsEnabled === true;
|
|
@@ -388,6 +403,7 @@ export class EngramMcpServer {
|
|
|
388
403
|
this.sessionDeltaVisible = options.sessionDeltaVisible === true;
|
|
389
404
|
// correction defaults to visible (enabled by default — plan is read-only).
|
|
390
405
|
this.correctionVisible = options.correctionVisible !== false;
|
|
406
|
+
this.chatVisible = options.chatVisible === true;
|
|
391
407
|
this.authenticatedPrincipal =
|
|
392
408
|
options.principal?.trim() ||
|
|
393
409
|
readEnvVar("OPENCLAW_ENGRAM_ACCESS_PRINCIPAL")?.trim() ||
|
|
@@ -1003,6 +1019,11 @@ export class EngramMcpServer {
|
|
|
1003
1019
|
properties: {
|
|
1004
1020
|
memoryId: { type: "string" },
|
|
1005
1021
|
namespace: { type: "string" },
|
|
1022
|
+
// Issue #1582 — when memoryId is a `[m:xxxx]` handle, sessionKey
|
|
1023
|
+
// scopes resolution to this session's recent recall history.
|
|
1024
|
+
// Advertising it here lets the transport inject its session key
|
|
1025
|
+
// (toolAcceptsArgument) so MCP callers can cite handles (codex review).
|
|
1026
|
+
sessionKey: { type: "string" },
|
|
1006
1027
|
},
|
|
1007
1028
|
required: ["memoryId"],
|
|
1008
1029
|
additionalProperties: false,
|
|
@@ -1156,6 +1177,11 @@ export class EngramMcpServer {
|
|
|
1156
1177
|
},
|
|
1157
1178
|
description: "Conversation messages to observe",
|
|
1158
1179
|
},
|
|
1180
|
+
idempotencyKey: {
|
|
1181
|
+
type: "string",
|
|
1182
|
+
description:
|
|
1183
|
+
"Optional idempotency key (issue #1649). Deduplicates a retried observe POST server-side so the batch is ingested once even when the HTTP response is lost. Reusing the key with a different payload is rejected.",
|
|
1184
|
+
},
|
|
1159
1185
|
namespace: { type: "string" },
|
|
1160
1186
|
skipExtraction: { type: "boolean" },
|
|
1161
1187
|
cwd: { type: "string", description: "Working directory for auto git-context resolution." },
|
|
@@ -2214,6 +2240,26 @@ export class EngramMcpServer {
|
|
|
2214
2240
|
);
|
|
2215
2241
|
this.tools = [...this.tools, ...planTool, ...applyTool];
|
|
2216
2242
|
}
|
|
2243
|
+
if (this.chatVisible) {
|
|
2244
|
+
const chatTools = withToolAliases(
|
|
2245
|
+
{
|
|
2246
|
+
name: "engram.memory_chat",
|
|
2247
|
+
description:
|
|
2248
|
+
"Conversational memory inspection and correction (issue #1583). Send a message to chat about what Remnic remembers, inspect memories, or request corrections. Returns {reply, chatSessionId, pendingPlan?}.",
|
|
2249
|
+
inputSchema: {
|
|
2250
|
+
type: "object",
|
|
2251
|
+
properties: {
|
|
2252
|
+
message: { type: "string", description: "The user's message — a question about memories, a correction request, or a confirmation (yes/apply) to proceed with a pending plan." },
|
|
2253
|
+
chatSessionId: { type: "string", description: "Optional existing chat session id to resume. Omit to start a new session." },
|
|
2254
|
+
},
|
|
2255
|
+
required: ["message"],
|
|
2256
|
+
additionalProperties: false,
|
|
2257
|
+
},
|
|
2258
|
+
},
|
|
2259
|
+
this.emitLegacyTools,
|
|
2260
|
+
);
|
|
2261
|
+
this.tools = [...this.tools, ...chatTools];
|
|
2262
|
+
}
|
|
2217
2263
|
}
|
|
2218
2264
|
|
|
2219
2265
|
/** Get clientInfo for a specific MCP session. Returns undefined for non-MCP requests. */
|
|
@@ -2374,7 +2420,13 @@ export class EngramMcpServer {
|
|
|
2374
2420
|
argumentsObject = { ...argumentsObject, sessionKey: options.sessionKeyOverride };
|
|
2375
2421
|
}
|
|
2376
2422
|
const effectivePrincipal = options?.principalOverride ?? this.authenticatedPrincipal;
|
|
2377
|
-
|
|
2423
|
+
// Forward the MCP session scope (namespace/sessionKey overrides) so
|
|
2424
|
+
// tools like memory_chat bind the caller active scope (Thread 17).
|
|
2425
|
+
const mcpScope = {
|
|
2426
|
+
...(options?.namespaceOverride ? { namespace: options.namespaceOverride } : {}),
|
|
2427
|
+
...(options?.sessionKeyOverride ? { sessionKey: options.sessionKeyOverride } : {}),
|
|
2428
|
+
};
|
|
2429
|
+
const result = await this.callTool(name, argumentsObject, effectivePrincipal, options?.sessionId, mcpScope, options?.enforceWriteQuota);
|
|
2378
2430
|
return {
|
|
2379
2431
|
jsonrpc: "2.0",
|
|
2380
2432
|
id,
|
|
@@ -2562,7 +2614,7 @@ export class EngramMcpServer {
|
|
|
2562
2614
|
}));
|
|
2563
2615
|
}
|
|
2564
2616
|
|
|
2565
|
-
private async callTool(name: string, args: Record<string, unknown>, effectivePrincipal?: string, mcpSessionId?: string): Promise<unknown> {
|
|
2617
|
+
private async callTool(name: string, args: Record<string, unknown>, effectivePrincipal?: string, mcpSessionId?: string, scope?: { namespace?: string; sessionKey?: string }, enforceWriteQuota?: () => void | Promise<void>): Promise<unknown> {
|
|
2566
2618
|
// Migrated operations dispatch through the access boundary (issue #1525):
|
|
2567
2619
|
// one registry entry owns schema validation, normalization (rules
|
|
2568
2620
|
// 17/28/36/48/51), and error mapping for every surface. The switch
|
|
@@ -3104,21 +3156,27 @@ export class EngramMcpServer {
|
|
|
3104
3156
|
);
|
|
3105
3157
|
case "engram.observe": {
|
|
3106
3158
|
const body = parseMcpRequest("observe", args);
|
|
3107
|
-
return this.service.observe(
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3159
|
+
return this.service.observe(
|
|
3160
|
+
{
|
|
3161
|
+
sessionKey: body.sessionKey,
|
|
3162
|
+
messages: body.messages.map((message) => ({
|
|
3163
|
+
role: message.role,
|
|
3164
|
+
content: message.content,
|
|
3165
|
+
parts: message.parts ?? undefined,
|
|
3166
|
+
rawContent: message.rawContent ?? undefined,
|
|
3167
|
+
sourceFormat: message.sourceFormat ?? undefined,
|
|
3168
|
+
})),
|
|
3169
|
+
skipExtraction: body.skipExtraction === true,
|
|
3170
|
+
idempotencyKey: body.idempotencyKey,
|
|
3171
|
+
namespace: body.namespace,
|
|
3172
|
+
authenticatedPrincipal: effectivePrincipal,
|
|
3173
|
+
cwd: body.cwd,
|
|
3174
|
+
projectTag: body.projectTag,
|
|
3175
|
+
},
|
|
3176
|
+
// Forward the transport's write-quota enforcer into observe's
|
|
3177
|
+
// idempotency lock so a replay is never 429'd (issue #1649).
|
|
3178
|
+
enforceWriteQuota ? { enforceWriteQuota } : undefined,
|
|
3179
|
+
);
|
|
3122
3180
|
}
|
|
3123
3181
|
case "engram.lcm_search":
|
|
3124
3182
|
return this.service.lcmSearch({
|
|
@@ -3725,6 +3783,30 @@ export class EngramMcpServer {
|
|
|
3725
3783
|
authenticatedPrincipal: effectivePrincipal,
|
|
3726
3784
|
});
|
|
3727
3785
|
}
|
|
3786
|
+
case "engram.memory_chat": {
|
|
3787
|
+
const message = typeof args.message === "string" ? args.message : "";
|
|
3788
|
+
if (!message) {
|
|
3789
|
+
throw new EngramAccessInputError("message is required");
|
|
3790
|
+
}
|
|
3791
|
+
const chatSessionId = typeof args.chatSessionId === "string" ? args.chatSessionId : undefined;
|
|
3792
|
+
const chatResult = await processChatMessage({
|
|
3793
|
+
service: this.service,
|
|
3794
|
+
config: this.service.configRef?.chat,
|
|
3795
|
+
memoryDir: this.service.memoryDir,
|
|
3796
|
+
message,
|
|
3797
|
+
...(chatSessionId ? { chatSessionId } : {}),
|
|
3798
|
+
...(effectivePrincipal ? { principal: effectivePrincipal } : {}),
|
|
3799
|
+
...(scope?.namespace ? { namespace: scope.namespace } : {}),
|
|
3800
|
+
...(scope?.sessionKey ? { sessionKey: scope.sessionKey } : {}),
|
|
3801
|
+
});
|
|
3802
|
+
// Strip internal error details — MCP clients get the tagged reply only,
|
|
3803
|
+
// matching the HTTP handler (cursor security review, Thread 17).
|
|
3804
|
+
if (chatResult && typeof chatResult === "object" && "error" in chatResult) {
|
|
3805
|
+
const { error: _stripped, ...wireResult } = chatResult as unknown as Record<string, unknown>;
|
|
3806
|
+
return wireResult;
|
|
3807
|
+
}
|
|
3808
|
+
return chatResult;
|
|
3809
|
+
}
|
|
3728
3810
|
default:
|
|
3729
3811
|
throw new Error(`unknown tool: ${name}`);
|
|
3730
3812
|
}
|
|
@@ -1466,3 +1466,17 @@ defineOperation({
|
|
|
1466
1466
|
schema: looseSchema,
|
|
1467
1467
|
handler: async (_input, _ctx) => ({ result: { stream: "sse" } }),
|
|
1468
1468
|
});
|
|
1469
|
+
|
|
1470
|
+
defineOperation({
|
|
1471
|
+
name: "chat_message",
|
|
1472
|
+
description: "Send a message to Remnic Chat (issue #1583). Endpoint registration only — the handler enforces the chat_disabled gate, message validation, and the confirmation protocol.",
|
|
1473
|
+
schema: looseSchema,
|
|
1474
|
+
handler: async (_input, _ctx) => ({ result: { ok: true } }),
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
defineOperation({
|
|
1478
|
+
name: "chat_events",
|
|
1479
|
+
description: "Stream chat session transcript events (SSE, issue #1583). Endpoint registration only.",
|
|
1480
|
+
schema: looseSchema,
|
|
1481
|
+
handler: async (_input, _ctx) => ({ result: { stream: "sse" } }),
|
|
1482
|
+
});
|
package/src/access-operations.ts
CHANGED
|
@@ -58,11 +58,15 @@ import {
|
|
|
58
58
|
*/
|
|
59
59
|
const memoryGetSchema = z.object({
|
|
60
60
|
memoryId: z.string().trim().min(1, "memoryId is required").max(512),
|
|
61
|
+
// Issue #1582 — when memoryId is a `[m:xxxx]` handle, sessionKey scopes the
|
|
62
|
+
// resolution to this session's recent recall history.
|
|
63
|
+
sessionKey: z.string().trim().min(1).max(512).nullable().optional(),
|
|
61
64
|
namespace: z.string().trim().max(256).nullable().optional(),
|
|
62
65
|
});
|
|
63
66
|
|
|
64
67
|
export interface MemoryGetInput {
|
|
65
68
|
readonly memoryId: string;
|
|
69
|
+
readonly sessionKey?: string | null;
|
|
66
70
|
readonly namespace?: string | null;
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -79,6 +83,7 @@ export const memoryGetOperation = defineOperation<MemoryGetInput, MemoryGetOutpu
|
|
|
79
83
|
input.memoryId,
|
|
80
84
|
input.namespace ?? undefined,
|
|
81
85
|
ctx.authenticatedPrincipal,
|
|
86
|
+
input.sessionKey ?? undefined,
|
|
82
87
|
);
|
|
83
88
|
return { result };
|
|
84
89
|
},
|
package/src/access-schema.ts
CHANGED
|
@@ -216,6 +216,14 @@ export const observeRequestSchema = z.object({
|
|
|
216
216
|
messages: z.array(messageSchema).min(1, "messages must be a non-empty array"),
|
|
217
217
|
namespace: namespaceSchema,
|
|
218
218
|
skipExtraction: z.boolean().optional(),
|
|
219
|
+
/**
|
|
220
|
+
* Optional idempotency key for server-side dedup of retried observe POSTs
|
|
221
|
+
* (issue #1649). A retry that reaches the daemon after the first attempt
|
|
222
|
+
* already processed is replayed from cache instead of re-ingested. When the
|
|
223
|
+
* key is reused with a DIFFERENT payload the request is rejected as a
|
|
224
|
+
* conflict (same contract as memory_store/suggestion_submit).
|
|
225
|
+
*/
|
|
226
|
+
idempotencyKey: idempotencyKeySchema,
|
|
219
227
|
/** Working directory for auto git-context resolution (issue #569). */
|
|
220
228
|
cwd: z.string().trim().min(1, "cwd must be non-empty when provided").max(2048).optional(),
|
|
221
229
|
/**
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1649: a retried observe POST (same `idempotencyKey`) must be deduplicated
|
|
3
|
+
* server-side — the batch is ingested exactly once even when the HTTP response
|
|
4
|
+
* is lost and the client replays the request. Without the key the daemon
|
|
5
|
+
* re-runs every side effect under `skipDedupeCheck: true` and the turn is
|
|
6
|
+
* queued for extraction twice.
|
|
7
|
+
*
|
|
8
|
+
* Verified here against the `EngramAccessService.observe` path with a stub
|
|
9
|
+
* orchestrator that records every namespace-bearing side effect (LCM enqueue +
|
|
10
|
+
* extraction replay), the same probe shape used by the #1495 observe-scope
|
|
11
|
+
* tests. The idempotency store is filesystem-backed, so `memoryDir` is a real
|
|
12
|
+
* temp directory (not the synthetic path the scope tests use, which never touch
|
|
13
|
+
* the store because they omit the key).
|
|
14
|
+
*/
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import test from "node:test";
|
|
20
|
+
|
|
21
|
+
import { EngramAccessService } from "./access-service.js";
|
|
22
|
+
import { EngramAccessInputError } from "./access-service.js";
|
|
23
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
24
|
+
import type { EngramAccessObserveRequest } from "./access-service.js";
|
|
25
|
+
import type { CodingContext, PluginConfig } from "./types.js";
|
|
26
|
+
|
|
27
|
+
interface ObserveProbe {
|
|
28
|
+
orch: Orchestrator;
|
|
29
|
+
contexts: Map<string, CodingContext>;
|
|
30
|
+
lcmCalls: Array<{ sessionKey: string }>;
|
|
31
|
+
extractionCalls: Array<{
|
|
32
|
+
sessionKeys: string[];
|
|
33
|
+
writeNamespaceOverride?: string;
|
|
34
|
+
principalOverride?: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeObserveProbe(memoryDir: string): ObserveProbe {
|
|
39
|
+
const contexts = new Map<string, CodingContext>();
|
|
40
|
+
const lcmCalls: ObserveProbe["lcmCalls"] = [];
|
|
41
|
+
const extractionCalls: ObserveProbe["extractionCalls"] = [];
|
|
42
|
+
|
|
43
|
+
const config = {
|
|
44
|
+
namespacesEnabled: true,
|
|
45
|
+
defaultNamespace: "default",
|
|
46
|
+
sharedNamespace: "shared",
|
|
47
|
+
namespacePolicies: [
|
|
48
|
+
{ name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
|
|
49
|
+
],
|
|
50
|
+
codingMode: { projectScope: true },
|
|
51
|
+
memoryDir,
|
|
52
|
+
// Disable objective-state snapshots so the probe doesn't need a writable
|
|
53
|
+
// storage backend — the idempotency contract under test is LCM + extraction.
|
|
54
|
+
objectiveStateMemoryEnabled: false,
|
|
55
|
+
objectiveStateSnapshotWritesEnabled: false,
|
|
56
|
+
principalFromSessionKeyMode: "prefix",
|
|
57
|
+
principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
|
|
58
|
+
recallCrossNamespaceBudgetEnabled: false,
|
|
59
|
+
recallCrossNamespaceBudgetWindowMs: 60_000,
|
|
60
|
+
recallCrossNamespaceBudgetSoftLimit: 10,
|
|
61
|
+
recallCrossNamespaceBudgetHardLimit: 30,
|
|
62
|
+
} as unknown as PluginConfig;
|
|
63
|
+
|
|
64
|
+
const orch = {
|
|
65
|
+
config,
|
|
66
|
+
getCodingContextForSession: (sk: string | undefined) =>
|
|
67
|
+
(sk ? contexts.get(sk) : null) ?? null,
|
|
68
|
+
setCodingContextForSession: (sk: string, ctx: CodingContext | null) => {
|
|
69
|
+
if (ctx === null) contexts.delete(sk);
|
|
70
|
+
else contexts.set(sk, ctx);
|
|
71
|
+
},
|
|
72
|
+
applyCodingNamespaceOverlay: (sk: string | undefined, base: string) =>
|
|
73
|
+
Orchestrator.prototype.applyCodingNamespaceOverlay.call(orch, sk, base),
|
|
74
|
+
getStorage: async (ns: string) => ({ dir: join(memoryDir, "storage", ns) }),
|
|
75
|
+
lcmEngine: {
|
|
76
|
+
enabled: true,
|
|
77
|
+
enqueueObserveMessages: (sessionKey: string) => {
|
|
78
|
+
lcmCalls.push({ sessionKey });
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
ingestReplayBatch: async (
|
|
82
|
+
turns: Array<{ sessionKey: string }>,
|
|
83
|
+
options: { writeNamespaceOverride?: string; principalOverride?: string } = {},
|
|
84
|
+
) => {
|
|
85
|
+
extractionCalls.push({
|
|
86
|
+
sessionKeys: turns.map((t) => t.sessionKey),
|
|
87
|
+
writeNamespaceOverride: options.writeNamespaceOverride,
|
|
88
|
+
principalOverride: options.principalOverride,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
} as unknown as Orchestrator;
|
|
92
|
+
|
|
93
|
+
return { orch, contexts, lcmCalls, extractionCalls };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function observeRequest(
|
|
97
|
+
overrides: Partial<EngramAccessObserveRequest>,
|
|
98
|
+
): EngramAccessObserveRequest {
|
|
99
|
+
return {
|
|
100
|
+
sessionKey: "pi-geek:abc123",
|
|
101
|
+
messages: [
|
|
102
|
+
{ role: "user", content: "what database are we using?" },
|
|
103
|
+
{ role: "assistant", content: "we use postgres for the primary store" },
|
|
104
|
+
],
|
|
105
|
+
...overrides,
|
|
106
|
+
} as EngramAccessObserveRequest;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test("#1649 a retried observe with the same idempotencyKey is deduplicated to a single ingest", async () => {
|
|
110
|
+
const memoryDir = mkdtempSync(join(tmpdir(), "remnic-observe-idem-"));
|
|
111
|
+
try {
|
|
112
|
+
const probe = makeObserveProbe(memoryDir);
|
|
113
|
+
const service = new EngramAccessService(probe.orch);
|
|
114
|
+
const key = "observe-batch-pi-geek-abc123-#1";
|
|
115
|
+
|
|
116
|
+
const first = await service.observe(observeRequest({ idempotencyKey: key }));
|
|
117
|
+
// First attempt is a real ingest.
|
|
118
|
+
assert.equal(first.idempotencyReplay, undefined, "first attempt is not a replay");
|
|
119
|
+
assert.equal(probe.extractionCalls.length, 1, "first attempt ingests once");
|
|
120
|
+
assert.equal(probe.lcmCalls.length, 1, "first attempt archives LCM once");
|
|
121
|
+
|
|
122
|
+
// Retry: same key, same payload — the daemon already processed the body, so
|
|
123
|
+
// this is the response-lost-after-process case the issue describes.
|
|
124
|
+
const second = await service.observe(observeRequest({ idempotencyKey: key }));
|
|
125
|
+
assert.equal(second.idempotencyReplay, true, "retry is served from the cache");
|
|
126
|
+
assert.equal(second.accepted, first.accepted, "retry reports the same accepted count");
|
|
127
|
+
assert.equal(
|
|
128
|
+
second.effectiveNamespace,
|
|
129
|
+
first.effectiveNamespace,
|
|
130
|
+
"retry reports the same effective namespace",
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// The defining contract: NO second ingest, NO second LCM archive.
|
|
134
|
+
assert.equal(probe.extractionCalls.length, 1, "retry must not queue extraction a second time");
|
|
135
|
+
assert.equal(probe.lcmCalls.length, 1, "retry must not archive LCM a second time");
|
|
136
|
+
} finally {
|
|
137
|
+
rmSync(memoryDir, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("#1649 observe without an idempotencyKey is never deduplicated (backward compatible)", async () => {
|
|
142
|
+
const memoryDir = mkdtempSync(join(tmpdir(), "remnic-observe-nokey-"));
|
|
143
|
+
try {
|
|
144
|
+
const probe = makeObserveProbe(memoryDir);
|
|
145
|
+
const service = new EngramAccessService(probe.orch);
|
|
146
|
+
|
|
147
|
+
await service.observe(observeRequest({}));
|
|
148
|
+
await service.observe(observeRequest({}));
|
|
149
|
+
|
|
150
|
+
assert.equal(probe.extractionCalls.length, 2, "no key ⇒ both calls ingest");
|
|
151
|
+
assert.equal(probe.lcmCalls.length, 2, "no key ⇒ both calls archive LCM");
|
|
152
|
+
} finally {
|
|
153
|
+
rmSync(memoryDir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("#1649 reusing an idempotencyKey with a divergent payload is rejected as a conflict", async () => {
|
|
158
|
+
const memoryDir = mkdtempSync(join(tmpdir(), "remnic-observe-conflict-"));
|
|
159
|
+
try {
|
|
160
|
+
const probe = makeObserveProbe(memoryDir);
|
|
161
|
+
const service = new EngramAccessService(probe.orch);
|
|
162
|
+
const key = "observe-batch-pi-geek-abc123-#2";
|
|
163
|
+
|
|
164
|
+
await service.observe(observeRequest({ idempotencyKey: key }));
|
|
165
|
+
|
|
166
|
+
// Same key, DIFFERENT messages — a stale key must never silently mask a
|
|
167
|
+
// unrelated batch. Same contract as memory_store/suggestion_submit.
|
|
168
|
+
await assert.rejects(
|
|
169
|
+
service.observe(
|
|
170
|
+
observeRequest({
|
|
171
|
+
idempotencyKey: key,
|
|
172
|
+
messages: [
|
|
173
|
+
{ role: "user", content: "completely different question" },
|
|
174
|
+
{ role: "assistant", content: "completely different answer" },
|
|
175
|
+
],
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
178
|
+
(err: unknown) =>
|
|
179
|
+
err instanceof EngramAccessInputError &&
|
|
180
|
+
/idempotencyKey reuse conflict/.test(err.message),
|
|
181
|
+
"divergent payload under a reused key must throw a conflict",
|
|
182
|
+
);
|
|
183
|
+
} finally {
|
|
184
|
+
rmSync(memoryDir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("#1649 review fix: enforceWriteQuota runs only on a cache miss, never on a replay", async () => {
|
|
189
|
+
const memoryDir = mkdtempSync(join(tmpdir(), "remnic-observe-quota-hook-"));
|
|
190
|
+
try {
|
|
191
|
+
const probe = makeObserveProbe(memoryDir);
|
|
192
|
+
const service = new EngramAccessService(probe.orch);
|
|
193
|
+
const key = "observe-batch-pi-geek-abc123-#3";
|
|
194
|
+
let quotaChecks = 0;
|
|
195
|
+
const enforceWriteQuota = () => {
|
|
196
|
+
quotaChecks += 1;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await service.observe(observeRequest({ idempotencyKey: key }), { enforceWriteQuota });
|
|
200
|
+
assert.equal(quotaChecks, 1, "quota is enforced exactly once on the real ingest");
|
|
201
|
+
|
|
202
|
+
const replay = await service.observe(observeRequest({ idempotencyKey: key }), { enforceWriteQuota });
|
|
203
|
+
assert.equal(replay.idempotencyReplay, true, "second call is a replay");
|
|
204
|
+
assert.equal(quotaChecks, 1, "quota is NOT re-checked on a replay (response-lost retry must not 429)");
|
|
205
|
+
assert.equal(probe.extractionCalls.length, 1, "replay did not re-ingest");
|
|
206
|
+
} finally {
|
|
207
|
+
rmSync(memoryDir, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("#1649 review fix: the same idempotencyKey under a different principal is a conflict, not a silent replay", async () => {
|
|
212
|
+
const memoryDir = mkdtempSync(join(tmpdir(), "remnic-observe-principal-conflict-"));
|
|
213
|
+
try {
|
|
214
|
+
const probe = makeObserveProbe(memoryDir);
|
|
215
|
+
const service = new EngramAccessService(probe.orch);
|
|
216
|
+
const key = "observe-batch-shared-key-#4";
|
|
217
|
+
|
|
218
|
+
// Principal alice ingests under the key.
|
|
219
|
+
await service.observe(
|
|
220
|
+
observeRequest({ idempotencyKey: key, authenticatedPrincipal: "alice" }),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// A different principal reusing the same key + same body must NOT replay
|
|
224
|
+
// alice's cached response — the fingerprint folds in authenticatedPrincipal,
|
|
225
|
+
// so this is a conflict (defends against cross-identity replay when the
|
|
226
|
+
// principal is supplied out-of-band via HTTP/MCP auth).
|
|
227
|
+
await assert.rejects(
|
|
228
|
+
service.observe(
|
|
229
|
+
observeRequest({ idempotencyKey: key, authenticatedPrincipal: "bob" }),
|
|
230
|
+
),
|
|
231
|
+
(err: unknown) =>
|
|
232
|
+
err instanceof EngramAccessInputError &&
|
|
233
|
+
/idempotencyKey reuse conflict/.test(err.message),
|
|
234
|
+
"cross-principal key reuse must throw a conflict",
|
|
235
|
+
);
|
|
236
|
+
} finally {
|
|
237
|
+
rmSync(memoryDir, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("#1649 review fix: rebinding the session's ambient coding context makes the same key a conflict", async () => {
|
|
242
|
+
const memoryDir = mkdtempSync(join(tmpdir(), "remnic-observe-ambient-scope-"));
|
|
243
|
+
try {
|
|
244
|
+
const probe = makeObserveProbe(memoryDir);
|
|
245
|
+
const service = new EngramAccessService(probe.orch);
|
|
246
|
+
const key = "observe-batch-ambient-scope-#5";
|
|
247
|
+
const req = observeRequest({ idempotencyKey: key });
|
|
248
|
+
|
|
249
|
+
// No explicit namespace/cwd/projectTag and no ambient context yet — the
|
|
250
|
+
// scope resolves to the default store. This caches the response.
|
|
251
|
+
await service.observe(req);
|
|
252
|
+
assert.equal(probe.extractionCalls.length, 1, "first observe ingests once");
|
|
253
|
+
|
|
254
|
+
// The session is now rebound to a coding project by an external caller
|
|
255
|
+
// (e.g. a memory_store with cwd, or a direct setCodingContextForSession).
|
|
256
|
+
// The resolved writeNamespace changed, so reusing the same key + same body
|
|
257
|
+
// must NOT silently replay — it is a conflict.
|
|
258
|
+
probe.contexts.set(req.sessionKey, {
|
|
259
|
+
projectId: "tag:remnic",
|
|
260
|
+
branch: null,
|
|
261
|
+
rootPath: "/projects/remnic",
|
|
262
|
+
defaultBranch: null,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await assert.rejects(
|
|
266
|
+
service.observe(req),
|
|
267
|
+
(err: unknown) =>
|
|
268
|
+
err instanceof EngramAccessInputError &&
|
|
269
|
+
/idempotencyKey reuse conflict/.test(err.message),
|
|
270
|
+
"key reuse after ambient scope rebind must throw a conflict, not silently replay",
|
|
271
|
+
);
|
|
272
|
+
} finally {
|
|
273
|
+
rmSync(memoryDir, { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("#1649 review fix: projectTag observe replays correctly despite scope resolution side effects", async () => {
|
|
278
|
+
// The fingerprint folds in the RESOLVED writeNamespace (not the raw ambient
|
|
279
|
+
// context). resolveMemoryScopePlan seeds the session context from projectTag
|
|
280
|
+
// on the first call; the replay reads the SAME seeded context and resolves the
|
|
281
|
+
// SAME writeNamespace. This proves the fingerprint is stable across replays
|
|
282
|
+
// even though the scope resolution mutates session state on the first call.
|
|
283
|
+
const memoryDir = mkdtempSync(join(tmpdir(), "remnic-observe-tag-replay-"));
|
|
284
|
+
try {
|
|
285
|
+
const probe = makeObserveProbe(memoryDir);
|
|
286
|
+
const service = new EngramAccessService(probe.orch);
|
|
287
|
+
const key = "observe-batch-tag-replay-#6";
|
|
288
|
+
const req = observeRequest({ idempotencyKey: key, projectTag: "remnic" });
|
|
289
|
+
|
|
290
|
+
const first = await service.observe(req);
|
|
291
|
+
assert.equal(first.idempotencyReplay, undefined, "first attempt is a real ingest");
|
|
292
|
+
|
|
293
|
+
// The first observe's resolveMemoryScopePlan seeded a coding context for
|
|
294
|
+
// this session. Verify it was actually set (the side effect we depend on).
|
|
295
|
+
assert.ok(
|
|
296
|
+
probe.contexts.get(req.sessionKey),
|
|
297
|
+
"first observe seeded a coding context via resolveMemoryScopePlan",
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Retry with the same key + same body + same projectTag — the pre-resolved
|
|
301
|
+
// writeNamespace matches because the seeded context produces the same scope.
|
|
302
|
+
const replay = await service.observe(req);
|
|
303
|
+
assert.equal(replay.idempotencyReplay, true, "projectTag observe replays despite scope-resolution side effects");
|
|
304
|
+
assert.equal(probe.extractionCalls.length, 1, "replay did not re-ingest");
|
|
305
|
+
} finally {
|
|
306
|
+
rmSync(memoryDir, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
});
|