@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-service.ts
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
type CorrectionPlan,
|
|
58
58
|
type CorrectionRequest,
|
|
59
59
|
} from "./correction/index.js";
|
|
60
|
+
import { isHandleToken } from "./recall-handles.js";
|
|
60
61
|
import { createVersion } from "./page-versioning.js";
|
|
61
62
|
import { WorkStorage } from "./work/storage.js";
|
|
62
63
|
import {
|
|
@@ -1008,6 +1009,8 @@ export interface EngramAccessObserveRequest {
|
|
|
1008
1009
|
namespace?: string;
|
|
1009
1010
|
authenticatedPrincipal?: string;
|
|
1010
1011
|
skipExtraction?: boolean;
|
|
1012
|
+
/** Optional idempotency key (issue #1649): a retried POST with the same key is deduplicated server-side; divergent reuse is a conflict. */
|
|
1013
|
+
idempotencyKey?: string;
|
|
1011
1014
|
/**
|
|
1012
1015
|
* Working directory of the calling agent session (issue #569 wiring).
|
|
1013
1016
|
* When provided and no `codingContext` is attached for this session,
|
|
@@ -1067,6 +1070,8 @@ export interface EngramAccessObserveResponse {
|
|
|
1067
1070
|
scopeDebug?: EngramAccessScopeDebug;
|
|
1068
1071
|
lcmArchived: boolean;
|
|
1069
1072
|
extractionQueued: boolean;
|
|
1073
|
+
/** True when replayed from the idempotency cache (issue #1649); lets the HTTP surface skip the write-quota slot, matching memory_store replay semantics. */
|
|
1074
|
+
idempotencyReplay?: boolean;
|
|
1070
1075
|
}
|
|
1071
1076
|
|
|
1072
1077
|
export interface EngramAccessLcmSearchRequest {
|
|
@@ -2523,8 +2528,8 @@ export class EngramAccessService {
|
|
|
2523
2528
|
}
|
|
2524
2529
|
}
|
|
2525
2530
|
|
|
2526
|
-
private async handleIdempotentWrite<T extends
|
|
2527
|
-
operation:
|
|
2531
|
+
private async handleIdempotentWrite<T extends { idempotencyReplay?: boolean }>(options: {
|
|
2532
|
+
operation: string;
|
|
2528
2533
|
idempotencyKey?: string;
|
|
2529
2534
|
requestFingerprint: unknown;
|
|
2530
2535
|
skip?: boolean;
|
|
@@ -4487,10 +4492,19 @@ export class EngramAccessService {
|
|
|
4487
4492
|
}
|
|
4488
4493
|
}
|
|
4489
4494
|
|
|
4490
|
-
async memoryGet(
|
|
4495
|
+
async memoryGet(
|
|
4496
|
+
memoryId: string,
|
|
4497
|
+
namespace?: string,
|
|
4498
|
+
principal?: string,
|
|
4499
|
+
sessionKey?: string,
|
|
4500
|
+
): Promise<EngramAccessMemoryResponse> {
|
|
4501
|
+
// Issue #1582 — accept a `[m:xxxx]` handle in place of a memory id; resolve
|
|
4502
|
+
// it against the caller's session via the shared helper (rule 22). A raw id
|
|
4503
|
+
// passes through unchanged, so callers unaware of handles are unaffected.
|
|
4504
|
+
const resolvedId = this.resolveMemoryIdOrHandleInput(memoryId, sessionKey);
|
|
4491
4505
|
const resolvedNamespace = this.resolveReadableNamespace(namespace, principal);
|
|
4492
4506
|
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
4493
|
-
const memory = await storage.getMemoryById(
|
|
4507
|
+
const memory = await storage.getMemoryById(resolvedId);
|
|
4494
4508
|
if (!memory) {
|
|
4495
4509
|
return { found: false, namespace: resolvedNamespace };
|
|
4496
4510
|
}
|
|
@@ -4781,7 +4795,38 @@ export class EngramAccessService {
|
|
|
4781
4795
|
return service;
|
|
4782
4796
|
}
|
|
4783
4797
|
|
|
4798
|
+
/**
|
|
4799
|
+
* Issue #1582 — resolve a single memoryId-or-handle reference to a concrete
|
|
4800
|
+
* memory id. Handles resolve against the session's recall history via the
|
|
4801
|
+
* orchestrator's shared helper (rule 22); raw ids pass through unchanged. A
|
|
4802
|
+
* handle that misses or is ambiguous becomes an EngramAccessInputError so the
|
|
4803
|
+
* boundary maps it to a 400 (rule 34/51 — list candidates, never guess).
|
|
4804
|
+
*/
|
|
4805
|
+
private resolveMemoryIdOrHandleInput(
|
|
4806
|
+
ref: string,
|
|
4807
|
+
sessionKey: string | undefined,
|
|
4808
|
+
): string {
|
|
4809
|
+
if (!isHandleToken(ref)) return ref;
|
|
4810
|
+
try {
|
|
4811
|
+
return this.orchestrator.resolveMemoryIdOrHandle(ref, sessionKey);
|
|
4812
|
+
} catch (err) {
|
|
4813
|
+
throw new EngramAccessInputError(
|
|
4814
|
+
err instanceof Error ? err.message : String(err),
|
|
4815
|
+
);
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
|
|
4784
4819
|
async correctionPlan(request: CorrectionRequest): Promise<CorrectionPlan> {
|
|
4820
|
+
// Issue #1582 — resolve any `[m:xxxx]` handle in targetIds to its memory
|
|
4821
|
+
// id against the caller's session BEFORE planning, so the planner only ever
|
|
4822
|
+
// sees concrete ids. One shared resolve path (rule 22); a handle that
|
|
4823
|
+
// misses or collides is an explicit input error, never silent (rule 34/51).
|
|
4824
|
+
if (request.targetIds?.some((id) => isHandleToken(id))) {
|
|
4825
|
+
const targetIds = request.targetIds.map((id) =>
|
|
4826
|
+
this.resolveMemoryIdOrHandleInput(id, request.sessionKey),
|
|
4827
|
+
);
|
|
4828
|
+
return this.correctionService().plan({ ...request, targetIds });
|
|
4829
|
+
}
|
|
4785
4830
|
return this.correctionService().plan(request);
|
|
4786
4831
|
}
|
|
4787
4832
|
|
|
@@ -5700,7 +5745,63 @@ export class EngramAccessService {
|
|
|
5700
5745
|
return shapeMemorySummary(memory, baseDir, disclosure, rawExcerpts);
|
|
5701
5746
|
}
|
|
5702
5747
|
|
|
5703
|
-
async observe(
|
|
5748
|
+
async observe(
|
|
5749
|
+
request: EngramAccessObserveRequest,
|
|
5750
|
+
hooks?: { enforceWriteQuota?: () => void | Promise<void> },
|
|
5751
|
+
): Promise<EngramAccessObserveResponse> {
|
|
5752
|
+
// Issue #1649: dedup retried observe POSTs server-side. A retry with the
|
|
5753
|
+
// same `idempotencyKey` replays the cached response and skips every side
|
|
5754
|
+
// effect (LCM/extraction/objective-state); divergent payload OR principal
|
|
5755
|
+
// reuse is a conflict (fingerprint folds in authenticatedPrincipal, so a
|
|
5756
|
+
// cross-identity replay can never be silent). `enforceWriteQuota` runs as
|
|
5757
|
+
// `beforeExecute` inside the lock — only on a real miss — so a response-lost
|
|
5758
|
+
// retry never 429s even when the first attempt filled the window (#1434).
|
|
5759
|
+
//
|
|
5760
|
+
// The fingerprint folds in the EFFECTIVE coding context (#1649 codex P2):
|
|
5761
|
+
// runObserve's scope can be derived from the session's ATTACHED coding
|
|
5762
|
+
// context (which takes PRECEDENCE over per-call cwd/projectTag per
|
|
5763
|
+
// resolveMemoryScopePlan), so neither cwd/projectTag NOR the raw ambient
|
|
5764
|
+
// context alone fully captures the effective scope. The effective context
|
|
5765
|
+
// is computed the SAME way resolveMemoryScopePlan does — session-attached
|
|
5766
|
+
// first, per-call cwd/projectTag fallback — but READ-ONLY: it does NOT seed
|
|
5767
|
+
// the session, so a conflict/quota-rejection path leaves no orphaned binding.
|
|
5768
|
+
// The value is stable across replays because resolveMemoryScopePlan's
|
|
5769
|
+
// seeding writes the IDENTICAL context that resolveCodingContextFromOptions
|
|
5770
|
+
// derives — so getCodingContextForSession returns the same object on the
|
|
5771
|
+
// replay as resolveCodingContextFromOptions returned on the first call.
|
|
5772
|
+
// Only computed when a key is present; non-keyed observes skip the work.
|
|
5773
|
+
const idempotencyKey = request.idempotencyKey?.trim();
|
|
5774
|
+
let effectiveCodingContext: CodingContext | null | undefined;
|
|
5775
|
+
if (idempotencyKey && !(typeof request.namespace === "string" && request.namespace.trim().length > 0)) {
|
|
5776
|
+
// Explicit namespace pins the scope (resolveMemoryScopePlan returns early),
|
|
5777
|
+
// so the coding context is irrelevant — skip it to avoid false conflicts
|
|
5778
|
+
// when the session context changes under a namespace-pinned observe.
|
|
5779
|
+
// resolveCodingContextFromOptions self-gates on projectScope (no scattered
|
|
5780
|
+
// config read here).
|
|
5781
|
+
effectiveCodingContext =
|
|
5782
|
+
(typeof this.orchestrator.getCodingContextForSession === "function"
|
|
5783
|
+
? this.orchestrator.getCodingContextForSession(request.sessionKey)
|
|
5784
|
+
: null) ?? (await this.resolveCodingContextFromOptions(request));
|
|
5785
|
+
}
|
|
5786
|
+
return this.handleIdempotentWrite<EngramAccessObserveResponse>({
|
|
5787
|
+
operation: "observe",
|
|
5788
|
+
idempotencyKey: request.idempotencyKey,
|
|
5789
|
+
requestFingerprint: {
|
|
5790
|
+
sessionKey: request.sessionKey,
|
|
5791
|
+
messages: request.messages,
|
|
5792
|
+
namespace: request.namespace,
|
|
5793
|
+
skipExtraction: request.skipExtraction,
|
|
5794
|
+
authenticatedPrincipal: request.authenticatedPrincipal,
|
|
5795
|
+
cwd: request.cwd,
|
|
5796
|
+
projectTag: request.projectTag,
|
|
5797
|
+
effectiveCodingContext: effectiveCodingContext ?? null,
|
|
5798
|
+
},
|
|
5799
|
+
beforeExecute: hooks?.enforceWriteQuota,
|
|
5800
|
+
execute: () => this.runObserve(request),
|
|
5801
|
+
});
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
private async runObserve(request: EngramAccessObserveRequest): Promise<EngramAccessObserveResponse> {
|
|
5704
5805
|
if (!request.sessionKey || typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
|
|
5705
5806
|
throw new EngramAccessInputError("sessionKey is required and must be a non-empty string");
|
|
5706
5807
|
}
|
|
@@ -49,7 +49,9 @@ import {
|
|
|
49
49
|
// alongside the other GET list routes (baseline 80 from #1525 + 1 = 81). Also a
|
|
50
50
|
// catalog-completeness correction: POST /engram/v1/memories (operation memory_store)
|
|
51
51
|
// was always live but omitted from HTTP_ROUTES — it is migrated, so no count change.
|
|
52
|
-
|
|
52
|
+
// #1583 remnic chat adds memory_chat — an unmigrated handler dispatched to
|
|
53
|
+
// processChatMessage (carries user input; not a boundary operation) → 81 + 1 = 82.
|
|
54
|
+
const UNMIGRATED_HANDLER_BASELINE = 82;
|
|
53
55
|
|
|
54
56
|
// Keep the import live — `getOperation` is the call surfaces use at dispatch
|
|
55
57
|
// time; referencing it here pins the registry's lookup contract.
|
|
@@ -72,7 +74,7 @@ function shortToolName(advertised: string): string {
|
|
|
72
74
|
/** Spin up a server with emitLegacyTools=true and read the deduped short names. */
|
|
73
75
|
async function liveMcpToolShortNames(): Promise<ReadonlySet<string>> {
|
|
74
76
|
const stub = { briefingEnabled: true } as unknown as EngramAccessService;
|
|
75
|
-
const server = new EngramMcpServer(stub, { emitLegacyTools: true, codingDecisionVisible: true, architectureCardVisible: true, codegraphVisible: true, sessionDeltaVisible: true });
|
|
77
|
+
const server = new EngramMcpServer(stub, { emitLegacyTools: true, codingDecisionVisible: true, architectureCardVisible: true, codegraphVisible: true, sessionDeltaVisible: true, chatVisible: true });
|
|
76
78
|
const response = await server.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
77
79
|
const result = (response as { result?: { tools?: Array<{ name: string }> } }).result;
|
|
78
80
|
const names = new Set<string>();
|
|
@@ -75,6 +75,9 @@ export const MCP_TOOLS: readonly McpToolEntry[] = [
|
|
|
75
75
|
// Correction Contract (issue #1580) — one plan/apply pipeline.
|
|
76
76
|
{ tool: "memory_correct_plan", operation: "memory_correct_plan" },
|
|
77
77
|
{ tool: "memory_correct_apply", operation: "memory_correct_apply" },
|
|
78
|
+
// Conversational memory inspection + correction (issue #1583) — unmigrated
|
|
79
|
+
// handler dispatched to processChatMessage (not a boundary operation).
|
|
80
|
+
{ tool: "memory_chat", operation: null },
|
|
78
81
|
{ tool: "coding_decision", operation: "coding_decision" },
|
|
79
82
|
{ tool: "coding_architecture", operation: "coding_architecture" },
|
|
80
83
|
// codegraph parity tools (issue #1554)
|
|
@@ -229,6 +232,8 @@ export const HTTP_ROUTES: readonly HttpRouteEntry[] = [
|
|
|
229
232
|
// streaming lifecycle is owned by the HTTP transport (handleGraphEventsSSE).
|
|
230
233
|
{ method: "GET", pathname: "/engram/v1/offline-sync/snapshot-stream", operation: "offline_sync_snapshot_stream" },
|
|
231
234
|
{ method: "GET", pathname: "/engram/v1/graph/events", operation: "graph_events" },
|
|
235
|
+
{ method: "POST", pathname: "/engram/v1/chat/message", operation: "chat_message" },
|
|
236
|
+
{ method: "GET", pathname: "/engram/v1/chat/events/:id", operation: "chat_events" },
|
|
232
237
|
];
|
|
233
238
|
|
|
234
239
|
/**
|
|
@@ -311,3 +311,75 @@ test("getMemoryForActiveMemory ignores explicit namespace overrides when namespa
|
|
|
311
311
|
assert.equal(result.id, "shared-memory");
|
|
312
312
|
assert.equal(result.text, "shared text");
|
|
313
313
|
});
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
test("getMemoryForActiveMemory resolves a [m:xxxx] handle via the orchestrator resolver (#1582)", async () => {
|
|
317
|
+
const resolved: string[] = [];
|
|
318
|
+
const orchestrator = {
|
|
319
|
+
config: { namespacesEnabled: false },
|
|
320
|
+
resolveSelfNamespace: () => "readable-session",
|
|
321
|
+
getStorageForNamespace: async () => ({
|
|
322
|
+
getMemoryById: async (id: string) =>
|
|
323
|
+
id === "fact-1" ? ({ content: "text", frontmatter: {} } as never) : null,
|
|
324
|
+
}),
|
|
325
|
+
resolveMemoryIdOrHandle: (ref: string) => {
|
|
326
|
+
resolved.push(ref);
|
|
327
|
+
return "fact-1";
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const result = await getMemoryForActiveMemory(
|
|
332
|
+
orchestrator as never,
|
|
333
|
+
"[m:4f2a]",
|
|
334
|
+
{ sessionKey: "session-x" },
|
|
335
|
+
);
|
|
336
|
+
assert.deepEqual(resolved, ["[m:4f2a]"]);
|
|
337
|
+
assert.equal(result.error, undefined);
|
|
338
|
+
assert.equal(result.id, "fact-1");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("getMemoryForActiveMemory passes a raw id through unchanged (no resolver call) (#1582)", async () => {
|
|
342
|
+
const orchestrator = {
|
|
343
|
+
config: { namespacesEnabled: false },
|
|
344
|
+
resolveSelfNamespace: () => "readable-session",
|
|
345
|
+
getStorageForNamespace: async () => ({
|
|
346
|
+
getMemoryById: async (id: string) =>
|
|
347
|
+
id === "fact-1" ? ({ content: "text", frontmatter: {} } as never) : null,
|
|
348
|
+
}),
|
|
349
|
+
resolveMemoryIdOrHandle: () => {
|
|
350
|
+
throw new Error("resolver must not be called for a raw id");
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const result = await getMemoryForActiveMemory(
|
|
355
|
+
orchestrator as never,
|
|
356
|
+
"fact-1",
|
|
357
|
+
{ sessionKey: "session-x" },
|
|
358
|
+
);
|
|
359
|
+
assert.equal(result.id, "fact-1");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("getMemoryForActiveMemory returns not_found when a handle cannot be resolved (#1582, cursor review)", async () => {
|
|
363
|
+
// A missing session key, unknown handle, or ambiguous handle makes the
|
|
364
|
+
// orchestrator resolver THROW. The active-memory get path must yield the SAME
|
|
365
|
+
// not_found contract a bad raw id gets — not propagate the throw and crash an
|
|
366
|
+
// OpenClaw active-memory caller.
|
|
367
|
+
const orchestrator = {
|
|
368
|
+
config: { namespacesEnabled: false },
|
|
369
|
+
resolveSelfNamespace: () => "readable-session",
|
|
370
|
+
getStorageForNamespace: async () => ({
|
|
371
|
+
getMemoryById: async () => null,
|
|
372
|
+
}),
|
|
373
|
+
resolveMemoryIdOrHandle: () => {
|
|
374
|
+
throw new Error("Memory handle [m:dead] cannot be resolved without a session key.");
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const result = await getMemoryForActiveMemory(
|
|
379
|
+
orchestrator as never,
|
|
380
|
+
"[m:dead]",
|
|
381
|
+
// No sessionKey → resolver throws "without a session key".
|
|
382
|
+
{},
|
|
383
|
+
);
|
|
384
|
+
assert.equal(result.error, "not_found");
|
|
385
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { canReadNamespace, defaultNamespaceForPrincipal, resolvePrincipal } from "./namespaces/principal.js";
|
|
2
2
|
import type { MemoryFile, PluginConfig } from "./types.js";
|
|
3
3
|
import { collapseWhitespace, truncateCodePointSafe } from "./whitespace.js";
|
|
4
|
+
import { isHandleToken } from "./recall-handles.js";
|
|
4
5
|
|
|
5
6
|
export interface ActiveMemoryMetadata {
|
|
6
7
|
type?: "fact" | "preference";
|
|
@@ -192,6 +193,8 @@ export async function getMemoryForActiveMemory(
|
|
|
192
193
|
storage?: {
|
|
193
194
|
getMemoryById?: (id: string) => Promise<MemoryFile | null>;
|
|
194
195
|
};
|
|
196
|
+
/** Issue #1582 — resolves a `[m:xxxx]` handle to a memory id (orchestrator). */
|
|
197
|
+
resolveMemoryIdOrHandle?: (ref: string, sessionKey?: string) => string;
|
|
195
198
|
},
|
|
196
199
|
id: string,
|
|
197
200
|
options: {
|
|
@@ -210,10 +213,26 @@ export async function getMemoryForActiveMemory(
|
|
|
210
213
|
? await orchestrator.getStorageForNamespace(namespace)
|
|
211
214
|
: orchestrator.storage;
|
|
212
215
|
|
|
213
|
-
|
|
216
|
+
// Issue #1582 — resolve a `[m:xxxx]` handle to its memory id against the
|
|
217
|
+
// caller's session before the storage read, so OpenClaw active-memory agents
|
|
218
|
+
// can cite injected handles through the SAME shared path as memory_get /
|
|
219
|
+
// correction (cursor review). Raw ids pass through unchanged.
|
|
220
|
+
//
|
|
221
|
+
// A handle that misses, collides, or has no session key becomes not_found —
|
|
222
|
+
// the SAME behavior a bad raw id yields below — instead of throwing, so an
|
|
223
|
+
// active-memory caller gets a uniform not-found contract (cursor review).
|
|
224
|
+
let resolvedId = id;
|
|
225
|
+
if (isHandleToken(id) && typeof orchestrator.resolveMemoryIdOrHandle === "function") {
|
|
226
|
+
try {
|
|
227
|
+
resolvedId = orchestrator.resolveMemoryIdOrHandle(id, options.sessionKey);
|
|
228
|
+
} catch {
|
|
229
|
+
return { error: "not_found" };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const memory = await storage?.getMemoryById?.(resolvedId);
|
|
214
233
|
if (!memory) return { error: "not_found" };
|
|
215
234
|
return {
|
|
216
|
-
id,
|
|
235
|
+
id: resolvedId,
|
|
217
236
|
text: collapseWhitespace(memory.content),
|
|
218
237
|
metadata: buildActiveMemoryMetadataFromMemory(memory),
|
|
219
238
|
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat CLI surface tests (issue #1583 PR 2).
|
|
3
|
+
*
|
|
4
|
+
* Tests the non-TTY `--once` scripting mode and the no-LLM error path.
|
|
5
|
+
* Interactive readline mode is exercised via the golden-transcript pattern
|
|
6
|
+
* in chat-engine.test.ts (the engine loop is identical); here we cover the
|
|
7
|
+
* CLI's own input/output contract.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { strict as assert } from "node:assert";
|
|
11
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
|
|
16
|
+
import type { EngramAccessService } from "../access-service.js";
|
|
17
|
+
import { parseConfig } from "../config.js";
|
|
18
|
+
import { runChatCli } from "./chat-cli.js";
|
|
19
|
+
import { DEFAULT_CHAT_CONFIG } from "./chat-config.js";
|
|
20
|
+
|
|
21
|
+
/** Capture stdout.write for the duration of `fn`. */
|
|
22
|
+
async function captureStdout(fn: () => Promise<void>): Promise<string> {
|
|
23
|
+
const chunks: string[] = [];
|
|
24
|
+
const original = process.stdout.write.bind(process.stdout);
|
|
25
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
26
|
+
chunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
27
|
+
return true;
|
|
28
|
+
}) as typeof process.stdout.write;
|
|
29
|
+
try {
|
|
30
|
+
await fn();
|
|
31
|
+
} finally {
|
|
32
|
+
process.stdout.write = original;
|
|
33
|
+
}
|
|
34
|
+
return chunks.join("");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeService(overrides: Partial<EngramAccessService> = {}): EngramAccessService {
|
|
38
|
+
return {
|
|
39
|
+
fallbackLlmRef: {
|
|
40
|
+
chatCompletion: async () => ({ content: "Based on memory_search, your database uses PostgreSQL [id: abc]." }),
|
|
41
|
+
},
|
|
42
|
+
configRef: parseConfig({ memoryDir: "/tmp/chat-cli-test", chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
43
|
+
memoryDir: "/tmp/chat-cli-test",
|
|
44
|
+
memoryGet: () => Promise.resolve(null),
|
|
45
|
+
memoryTimeline: () => Promise.resolve([]),
|
|
46
|
+
memorySearch: () => Promise.resolve({ results: [{ path: "mem/abc.md", score: 0.9, snippet: "PostgreSQL" }], count: 1 }),
|
|
47
|
+
recallExplain: () => Promise.resolve(null),
|
|
48
|
+
entityGet: () => Promise.resolve(null),
|
|
49
|
+
memoryProfile: () => Promise.resolve({}),
|
|
50
|
+
memoryEntitiesList: () => Promise.resolve({ items: [] }),
|
|
51
|
+
memoryQuestions: () => Promise.resolve({ items: [] }),
|
|
52
|
+
reviewQueue: () => Promise.resolve({ items: [] }),
|
|
53
|
+
...overrides,
|
|
54
|
+
} as unknown as EngramAccessService;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
58
|
+
const dir = await mkdtemp(join(tmpdir(), "chat-cli-"));
|
|
59
|
+
try { return await fn(dir); } finally { await rm(dir, { recursive: true, force: true }); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test("CLI --once mode: prints the assistant reply and session id", async () => {
|
|
63
|
+
await withTempDir(async (memoryDir) => {
|
|
64
|
+
const service = makeService({
|
|
65
|
+
memoryDir,
|
|
66
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
67
|
+
});
|
|
68
|
+
const out = await captureStdout(() =>
|
|
69
|
+
runChatCli({
|
|
70
|
+
service,
|
|
71
|
+
config: service.configRef!.chat,
|
|
72
|
+
memoryDir,
|
|
73
|
+
once: true,
|
|
74
|
+
input: "what database do I use?",
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
assert.ok(out.includes("PostgreSQL"), `expected reply in stdout, got: ${out}`);
|
|
78
|
+
assert.ok(out.includes("[session:"), "session id should be printed for --once mode");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("CLI --once mode: no input → tagged error", async () => {
|
|
83
|
+
await withTempDir(async (memoryDir) => {
|
|
84
|
+
const service = makeService({
|
|
85
|
+
memoryDir,
|
|
86
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
87
|
+
});
|
|
88
|
+
const out = await captureStdout(() =>
|
|
89
|
+
runChatCli({
|
|
90
|
+
service,
|
|
91
|
+
config: service.configRef!.chat,
|
|
92
|
+
memoryDir,
|
|
93
|
+
once: true,
|
|
94
|
+
input: " ",
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
assert.ok(out.includes("[error]"), `expected error tag, got: ${out}`);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("CLI: no LLM available → tagged error reply", async () => {
|
|
102
|
+
await withTempDir(async (memoryDir) => {
|
|
103
|
+
const service = makeService({
|
|
104
|
+
fallbackLlmRef: null,
|
|
105
|
+
localLlmRef: null,
|
|
106
|
+
memoryDir,
|
|
107
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
108
|
+
});
|
|
109
|
+
const out = await captureStdout(() =>
|
|
110
|
+
runChatCli({
|
|
111
|
+
service,
|
|
112
|
+
config: service.configRef!.chat,
|
|
113
|
+
memoryDir,
|
|
114
|
+
once: true,
|
|
115
|
+
input: "hi",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
assert.ok(out.includes("[error]"), `expected error tag, got: ${out}`);
|
|
119
|
+
assert.ok(out.toLowerCase().includes("llm") || out.toLowerCase().includes("model"), "should mention model availability");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("CLI: resumes an existing session by id", async () => {
|
|
124
|
+
await withTempDir(async (memoryDir) => {
|
|
125
|
+
const service = makeService({
|
|
126
|
+
memoryDir,
|
|
127
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
128
|
+
});
|
|
129
|
+
// First message creates a session.
|
|
130
|
+
const first = await captureStdout(() =>
|
|
131
|
+
runChatCli({ service, config: service.configRef!.chat, memoryDir, once: true, input: "hello" }),
|
|
132
|
+
);
|
|
133
|
+
const sessionMatch = /\[session: ([^\]]+)\]/.exec(first);
|
|
134
|
+
assert.ok(sessionMatch, "first run should print a session id");
|
|
135
|
+
const sessionId = sessionMatch[1]!;
|
|
136
|
+
// Second message resumes it.
|
|
137
|
+
const second = await captureStdout(() =>
|
|
138
|
+
runChatCli({ service, config: service.configRef!.chat, memoryDir, once: true, input: "again", sessionId }),
|
|
139
|
+
);
|
|
140
|
+
assert.ok(second.includes("[session:"), "resumed run should also print a session id");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remnic chat` CLI implementation (issue #1583 PR 2).
|
|
3
|
+
*
|
|
4
|
+
* Interactive readline loop that streams assistant text and renders diffs.
|
|
5
|
+
* Supports `--daemon-url` for remote daemon over HTTP (Tailscale fleet case)
|
|
6
|
+
* and `--once` / non-TTY mode for scripting/tests
|
|
7
|
+
* (`echo "question" | remnic chat --once`).
|
|
8
|
+
*
|
|
9
|
+
* This module is imported by cli.ts with a single thin registration line —
|
|
10
|
+
* the god-file ratchet (#1520) tracks cli.ts LOC.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import readline from "node:readline";
|
|
14
|
+
import { createInterface } from "node:readline";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
|
|
17
|
+
import type { EngramAccessService } from "../access-service.js";
|
|
18
|
+
import type { PluginConfig } from "../types.js";
|
|
19
|
+
import type { ChatConfig } from "./chat-types.js";
|
|
20
|
+
import type { ChatTurnResult } from "./chat-types.js";
|
|
21
|
+
import { ChatEngine } from "./chat-engine.js";
|
|
22
|
+
import { createProductionChatLlmAdapter } from "./chat-llm.js";
|
|
23
|
+
import { createChatExecutor } from "./chat-executor.js";
|
|
24
|
+
import {
|
|
25
|
+
createChatSession,
|
|
26
|
+
loadChatSession,
|
|
27
|
+
appendTranscriptEntry,
|
|
28
|
+
sessionBelongsToPrincipal,
|
|
29
|
+
} from "./chat-session.js";
|
|
30
|
+
import { isConfirmationMessage } from "./chat-engine.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read all of stdin to a UTF-8 string (non-TTY / scripting mode).
|
|
34
|
+
*/
|
|
35
|
+
async function readAllStdin(): Promise<string> {
|
|
36
|
+
const chunks: Buffer[] = [];
|
|
37
|
+
for await (const chunk of process.stdin) {
|
|
38
|
+
chunks.push(chunk as Buffer);
|
|
39
|
+
}
|
|
40
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ChatCliOptions {
|
|
44
|
+
service: EngramAccessService;
|
|
45
|
+
config: ChatConfig | undefined;
|
|
46
|
+
memoryDir: string;
|
|
47
|
+
principal?: string;
|
|
48
|
+
namespace?: string;
|
|
49
|
+
sessionKey?: string;
|
|
50
|
+
/** Existing chat session id to resume. */
|
|
51
|
+
sessionId?: string;
|
|
52
|
+
/** Run a single message and exit (non-TTY / scripting mode). */
|
|
53
|
+
once?: boolean;
|
|
54
|
+
/** Input text for --once mode. */
|
|
55
|
+
input?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run the chat CLI. In interactive mode, enters a readline loop. In
|
|
60
|
+
* `--once` mode, processes a single message and prints the reply to stdout.
|
|
61
|
+
*/
|
|
62
|
+
export async function runChatCli(opts: ChatCliOptions): Promise<void> {
|
|
63
|
+
if (!opts.config?.enabled) {
|
|
64
|
+
const msg = "[error] Remnic Chat is disabled. Set \"chat.enabled\": true in your config to enable it.";
|
|
65
|
+
if (opts.once) { process.stdout.write(msg + "\n"); return; }
|
|
66
|
+
console.error(msg);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const config = opts.config;
|
|
70
|
+
const llm = opts.service.fallbackLlmRef ?? opts.service.localLlmRef;
|
|
71
|
+
if (!llm) {
|
|
72
|
+
const msg = "[error] No LLM model is available. Configure a local or cloud model to use Remnic Chat.";
|
|
73
|
+
if (opts.once) {
|
|
74
|
+
process.stdout.write(msg + "\n");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.error(msg);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const adapter = createProductionChatLlmAdapter(llm as {
|
|
82
|
+
chatCompletion(
|
|
83
|
+
messages: Array<{ role: string; content: string }>,
|
|
84
|
+
options?: { model?: string; signal?: AbortSignal },
|
|
85
|
+
): Promise<{ content: string } | null>;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Load or create session FIRST so the executor inherits the session's
|
|
89
|
+
// stored namespace/sessionKey scope — a resumed session created via MCP
|
|
90
|
+
// must not lose its scope binding (codex review, Thread 19).
|
|
91
|
+
let session;
|
|
92
|
+
if (opts.sessionId) {
|
|
93
|
+
session = await loadChatSession(opts.memoryDir, opts.sessionId);
|
|
94
|
+
if (!session) {
|
|
95
|
+
console.error(`Chat session not found: ${opts.sessionId}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!sessionBelongsToPrincipal(session, opts.principal)) {
|
|
99
|
+
console.error("Access denied: this chat session belongs to a different principal.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
session = await createChatSession(opts.memoryDir, {
|
|
104
|
+
principal: opts.principal,
|
|
105
|
+
sessionKey: opts.sessionKey,
|
|
106
|
+
namespace: opts.namespace,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const executor = createChatExecutor({
|
|
111
|
+
service: opts.service,
|
|
112
|
+
principal: opts.principal,
|
|
113
|
+
...(session.namespace ? { namespace: session.namespace } : {}),
|
|
114
|
+
...(session.sessionKey ? { sessionKey: session.sessionKey } : {}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const engine = new ChatEngine({
|
|
118
|
+
llm: adapter,
|
|
119
|
+
executor,
|
|
120
|
+
maxToolCallsPerTurn: config.maxToolCallsPerTurn,
|
|
121
|
+
...(config.model ? { model: config.model } : {}),
|
|
122
|
+
correctionAvailable: false,
|
|
123
|
+
scopeInspectAvailable: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── Non-TTY / --once mode ───────────────────────────────────────────
|
|
127
|
+
if (opts.once) {
|
|
128
|
+
// Read stdin here (not in cli.ts) so the god-file wiring stays thin.
|
|
129
|
+
const message = opts.input ?? await readAllStdin();
|
|
130
|
+
if (!message.trim()) {
|
|
131
|
+
process.stdout.write("[error] No input provided.\n");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const userEntry = await appendTranscriptEntry(opts.memoryDir, session.id, {
|
|
135
|
+
role: "user",
|
|
136
|
+
content: message,
|
|
137
|
+
});
|
|
138
|
+
session.transcript.push(userEntry);
|
|
139
|
+
const result = await engine.processMessage(message, session);
|
|
140
|
+
const assistantEntry = await appendTranscriptEntry(opts.memoryDir, session.id, {
|
|
141
|
+
role: "assistant",
|
|
142
|
+
content: result.reply,
|
|
143
|
+
});
|
|
144
|
+
session.transcript.push(assistantEntry);
|
|
145
|
+
process.stdout.write(result.reply + "\n");
|
|
146
|
+
process.stdout.write(`[session: ${session.id}]\n`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Interactive mode ────────────────────────────────────────────────
|
|
151
|
+
console.log(`Remnic Chat — session ${session.id}`);
|
|
152
|
+
console.log("Type your message. Type 'exit' or Ctrl-C to quit.\n");
|
|
153
|
+
|
|
154
|
+
const rl = createInterface({
|
|
155
|
+
input: process.stdin,
|
|
156
|
+
output: process.stdout,
|
|
157
|
+
prompt: "you> ",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
rl.prompt();
|
|
161
|
+
|
|
162
|
+
let inFlight: Promise<void> = Promise.resolve();
|
|
163
|
+
rl.on("line", (line: string) => {
|
|
164
|
+
const message = line.trim();
|
|
165
|
+
if (!message) {
|
|
166
|
+
rl.prompt();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (message === "exit" || message === "quit") {
|
|
170
|
+
rl.close();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Serialize turns: a second line waits for the first to finish so
|
|
174
|
+
// transcript updates and engine state don't race on the same session.
|
|
175
|
+
inFlight = inFlight.then(async () => {
|
|
176
|
+
const userEntry = await appendTranscriptEntry(opts.memoryDir, session!.id, {
|
|
177
|
+
role: "user",
|
|
178
|
+
content: message,
|
|
179
|
+
});
|
|
180
|
+
session!.transcript.push(userEntry);
|
|
181
|
+
|
|
182
|
+
const result = await engine.processMessage(message, session!);
|
|
183
|
+
const assistantEntry = await appendTranscriptEntry(opts.memoryDir, session!.id, {
|
|
184
|
+
role: "assistant",
|
|
185
|
+
content: result.reply,
|
|
186
|
+
});
|
|
187
|
+
session!.transcript.push(assistantEntry);
|
|
188
|
+
|
|
189
|
+
console.log(`\nassistant> ${result.reply}\n`);
|
|
190
|
+
if (result.pendingPlan) {
|
|
191
|
+
console.log(`[pending plan: ${result.pendingPlan.planId} — reply 'apply' to confirm]\n`);
|
|
192
|
+
}
|
|
193
|
+
if (result.skippedTools && result.skippedTools.length > 0) {
|
|
194
|
+
console.log(`[note: ${result.skippedTools.length} tool(s) skipped due to budget]\n`);
|
|
195
|
+
}
|
|
196
|
+
rl.prompt();
|
|
197
|
+
}).catch(() => {
|
|
198
|
+
console.error("[error] An unexpected error occurred during this turn. Please try again.");
|
|
199
|
+
rl.prompt();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await new Promise<void>((resolve) => {
|
|
204
|
+
rl.on("close", resolve);
|
|
205
|
+
});
|
|
206
|
+
}
|