@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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat engine factory + message processor (issue #1583).
|
|
3
|
+
*
|
|
4
|
+
* Wires the LLM adapter, executor, and engine from config + access-service.
|
|
5
|
+
* Used by all three surfaces (CLI, HTTP, MCP) so they share identical
|
|
6
|
+
* engine construction and session lifecycle.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { EngramAccessService } from "../access-service.js";
|
|
10
|
+
import type { FallbackLlmClient } from "../fallback-llm.js";
|
|
11
|
+
import type { LocalLlmClient } from "../local-llm.js";
|
|
12
|
+
import type { ChatConfig, ChatTurnResult } from "./chat-types.js";
|
|
13
|
+
import { ChatEngine } from "./chat-engine.js";
|
|
14
|
+
import { createProductionChatLlmAdapter } from "./chat-llm.js";
|
|
15
|
+
import { createChatExecutor } from "./chat-executor.js";
|
|
16
|
+
import {
|
|
17
|
+
createChatSession,
|
|
18
|
+
loadChatSession,
|
|
19
|
+
appendTranscriptEntry,
|
|
20
|
+
sessionBelongsToPrincipal,
|
|
21
|
+
markPendingPlan,
|
|
22
|
+
markPlanResolved,
|
|
23
|
+
markPendingPromotion,
|
|
24
|
+
markPromotionResolved,
|
|
25
|
+
} from "./chat-session.js";
|
|
26
|
+
|
|
27
|
+
export interface ChatEngineFactoryOptions {
|
|
28
|
+
service: EngramAccessService;
|
|
29
|
+
config: ChatConfig;
|
|
30
|
+
fallbackLlm: FallbackLlmClient | null;
|
|
31
|
+
localLlm: LocalLlmClient | null;
|
|
32
|
+
principal?: string;
|
|
33
|
+
namespace?: string;
|
|
34
|
+
sessionKey?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a {@link ChatEngine} from the factory options. Returns `null` when
|
|
39
|
+
* no LLM client is available. The engine uses the fallback LLM chain first
|
|
40
|
+
* (cloud/gateway), then the local LLM chain — both fully supported (gotcha 1).
|
|
41
|
+
*/
|
|
42
|
+
export function createChatEngine(opts: ChatEngineFactoryOptions): ChatEngine | null {
|
|
43
|
+
const llmClient = opts.fallbackLlm ?? opts.localLlm;
|
|
44
|
+
if (!llmClient) return null;
|
|
45
|
+
|
|
46
|
+
const adapter = createProductionChatLlmAdapter(llmClient as {
|
|
47
|
+
chatCompletion(
|
|
48
|
+
messages: Array<{ role: string; content: string }>,
|
|
49
|
+
options?: { model?: string; signal?: AbortSignal },
|
|
50
|
+
): Promise<{ content: string } | null>;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const executor = createChatExecutor({
|
|
54
|
+
service: opts.service,
|
|
55
|
+
principal: opts.principal,
|
|
56
|
+
namespace: opts.namespace,
|
|
57
|
+
sessionKey: opts.sessionKey,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return new ChatEngine({
|
|
61
|
+
llm: adapter,
|
|
62
|
+
executor,
|
|
63
|
+
maxToolCallsPerTurn: opts.config.maxToolCallsPerTurn,
|
|
64
|
+
...(opts.config.model ? { model: opts.config.model } : {}),
|
|
65
|
+
correctionAvailable: false,
|
|
66
|
+
scopeInspectAvailable: false,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Process a single chat message end-to-end. Shared by MCP and HTTP surfaces
|
|
72
|
+
* so session lifecycle, transcript persistence, and error handling are
|
|
73
|
+
* identical across transports.
|
|
74
|
+
*
|
|
75
|
+
* Throws when chat is disabled or no LLM is available.
|
|
76
|
+
*/
|
|
77
|
+
export async function processChatMessage(opts: {
|
|
78
|
+
service: EngramAccessService;
|
|
79
|
+
config: ChatConfig | undefined;
|
|
80
|
+
memoryDir: string;
|
|
81
|
+
message: string;
|
|
82
|
+
chatSessionId?: string;
|
|
83
|
+
principal?: string;
|
|
84
|
+
/** Namespace/sessionKey forwarded from the MCP request scope (Thread 17). */
|
|
85
|
+
namespace?: string;
|
|
86
|
+
sessionKey?: string;
|
|
87
|
+
}): Promise<ChatTurnResult> {
|
|
88
|
+
if (!opts.config?.enabled) {
|
|
89
|
+
throw new Error("chat_disabled");
|
|
90
|
+
}
|
|
91
|
+
if (!opts.service.fallbackLlmRef && !opts.service.localLlmRef) {
|
|
92
|
+
throw new Error("no_llm_available");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Load or create the session FIRST so the engine inherits the session's
|
|
96
|
+
// namespace/sessionKey scope (rule 42 — every tool call flows with that
|
|
97
|
+
// identity; the executor must not use a different scope than the session).
|
|
98
|
+
let session;
|
|
99
|
+
if (opts.chatSessionId) {
|
|
100
|
+
session = await loadChatSession(opts.memoryDir, opts.chatSessionId);
|
|
101
|
+
if (!session) throw new Error("chat_session_not_found");
|
|
102
|
+
if (!sessionBelongsToPrincipal(session, opts.principal)) {
|
|
103
|
+
throw new Error("access_denied");
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
session = await createChatSession(opts.memoryDir, {
|
|
107
|
+
principal: opts.principal,
|
|
108
|
+
...(opts.namespace ? { namespace: opts.namespace } : {}),
|
|
109
|
+
...(opts.sessionKey ? { sessionKey: opts.sessionKey } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const engine = createChatEngine({
|
|
114
|
+
service: opts.service,
|
|
115
|
+
config: opts.config as ChatConfig,
|
|
116
|
+
fallbackLlm: opts.service.fallbackLlmRef,
|
|
117
|
+
localLlm: opts.service.localLlmRef,
|
|
118
|
+
principal: opts.principal,
|
|
119
|
+
...(session.namespace ? { namespace: session.namespace } : {}),
|
|
120
|
+
...(session.sessionKey ? { sessionKey: session.sessionKey } : {}),
|
|
121
|
+
});
|
|
122
|
+
if (!engine) {
|
|
123
|
+
throw new Error("engine_unavailable");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await appendTranscriptEntry(opts.memoryDir, session.id, {
|
|
127
|
+
role: "user",
|
|
128
|
+
content: opts.message,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Snapshot pending state BEFORE the turn so we only persist a marker when
|
|
132
|
+
// state actually changed (Thread 15 — a normal turn must not clear an
|
|
133
|
+
// active pending plan/promotion from an earlier turn).
|
|
134
|
+
const priorPendingPlanId = session.pendingPlanId;
|
|
135
|
+
const priorPendingPromotionId = session.pendingPromotionId;
|
|
136
|
+
|
|
137
|
+
const result = await engine.processMessage(opts.message, session);
|
|
138
|
+
|
|
139
|
+
await appendTranscriptEntry(opts.memoryDir, session.id, {
|
|
140
|
+
role: "assistant",
|
|
141
|
+
content: result.reply,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Persist pending-plan/promotion state so a later turn can confirm it
|
|
145
|
+
// (append-only — loadChatSession scans for the latest unresolved marker).
|
|
146
|
+
if (result.pendingPlan?.planId) {
|
|
147
|
+
await markPendingPlan(opts.memoryDir, session.id, result.pendingPlan.planId);
|
|
148
|
+
} else if (session.pendingPromotionId && !priorPendingPromotionId) {
|
|
149
|
+
// A promotion just became pending this turn.
|
|
150
|
+
await markPendingPromotion(opts.memoryDir, session.id, session.pendingPromotionId);
|
|
151
|
+
} else if (priorPendingPlanId && !session.pendingPlanId) {
|
|
152
|
+
// A pending plan was consumed (applied) this turn.
|
|
153
|
+
await markPlanResolved(opts.memoryDir, session.id, priorPendingPlanId);
|
|
154
|
+
} else if (priorPendingPromotionId && !session.pendingPromotionId) {
|
|
155
|
+
// A pending promotion was consumed (applied) this turn.
|
|
156
|
+
await markPromotionResolved(opts.memoryDir, session.id, priorPendingPromotionId);
|
|
157
|
+
}
|
|
158
|
+
// else: no pending-state change this turn — do NOT append a marker.
|
|
159
|
+
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat HTTP surface tests (issue #1583 PR 3).
|
|
3
|
+
*
|
|
4
|
+
* Contract tests for handleChatMessage (POST /engram/v1/chat/message) and
|
|
5
|
+
* handleChatEventsSSE (GET /engram/v1/chat/events/:id). Auth/isolation,
|
|
6
|
+
* chat-disabled gate, input validation, and the happy path are all covered
|
|
7
|
+
* against in-process mock req/res objects — no port binding.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { strict as assert } from "node:assert";
|
|
11
|
+
import { EventEmitter } from "node:events";
|
|
12
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { test } from "node:test";
|
|
16
|
+
|
|
17
|
+
import type { EngramAccessService } from "../access-service.js";
|
|
18
|
+
import { parseConfig } from "../config.js";
|
|
19
|
+
import { handleChatMessage, handleChatEventsSSE } from "./chat-http.js";
|
|
20
|
+
import { createChatSession, appendTranscriptEntry, chatSessionFile } from "./chat-session.js";
|
|
21
|
+
import { DEFAULT_CHAT_CONFIG } from "./chat-config.js";
|
|
22
|
+
|
|
23
|
+
// Minimal mock res capturing writes. All tracking fields live on one
|
|
24
|
+
// object the methods mutate (no spread copy — primitives would freeze at 0).
|
|
25
|
+
interface MockRes {
|
|
26
|
+
emitter: EventEmitter;
|
|
27
|
+
status: number;
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
body: string;
|
|
30
|
+
writeHead: (s: number, h?: Record<string, string>) => void;
|
|
31
|
+
write: (c: string) => void;
|
|
32
|
+
end: (c?: string) => void;
|
|
33
|
+
}
|
|
34
|
+
function mockRes(): MockRes {
|
|
35
|
+
const m: MockRes = {
|
|
36
|
+
emitter: new EventEmitter(),
|
|
37
|
+
status: 0,
|
|
38
|
+
headers: {},
|
|
39
|
+
body: "",
|
|
40
|
+
writeHead(status: number, headers?: Record<string, string>) { m.status = status; if (headers) m.headers = headers; },
|
|
41
|
+
write(chunk: string) { m.body += chunk; },
|
|
42
|
+
end(chunk?: string) { if (chunk) m.body += chunk; m.emitter.emit("done"); },
|
|
43
|
+
};
|
|
44
|
+
return m;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mockReq(): EventEmitter {
|
|
48
|
+
return new EventEmitter();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeService(overrides: Partial<EngramAccessService> = {}): EngramAccessService {
|
|
52
|
+
return {
|
|
53
|
+
fallbackLlmRef: { chatCompletion: async () => ({ content: "stub reply" }) },
|
|
54
|
+
configRef: parseConfig({ memoryDir: "/tmp/chat-http-test", chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
55
|
+
memoryDir: "/tmp/chat-http-test",
|
|
56
|
+
memoryGet: () => Promise.resolve(null),
|
|
57
|
+
memoryTimeline: () => Promise.resolve([]),
|
|
58
|
+
memorySearch: () => Promise.resolve({ results: [], count: 0 }),
|
|
59
|
+
recallExplain: () => Promise.resolve(null),
|
|
60
|
+
entityGet: () => Promise.resolve(null),
|
|
61
|
+
memoryProfile: () => Promise.resolve({}),
|
|
62
|
+
memoryEntitiesList: () => Promise.resolve({ items: [] }),
|
|
63
|
+
memoryQuestions: () => Promise.resolve({ items: [] }),
|
|
64
|
+
reviewQueue: () => Promise.resolve({ items: [] }),
|
|
65
|
+
...overrides,
|
|
66
|
+
} as unknown as EngramAccessService;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
70
|
+
const dir = await mkdtemp(join(tmpdir(), "chat-http-"));
|
|
71
|
+
try { return await fn(dir); } finally { await rm(dir, { recursive: true, force: true }); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test("HTTP chat/message: 404 when chat is disabled", async () => {
|
|
75
|
+
await withTempDir(async (memoryDir) => {
|
|
76
|
+
const service = makeService({
|
|
77
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: false } }),
|
|
78
|
+
memoryDir,
|
|
79
|
+
});
|
|
80
|
+
const m = mockRes();
|
|
81
|
+
await handleChatMessage(mockReq() as never, m as never, { message: "hi" }, { service, config: service.configRef!.chat, memoryDir });
|
|
82
|
+
assert.equal(m.status, 404);
|
|
83
|
+
assert.ok(m.body.includes("chat_disabled"));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("HTTP chat/message: 400 when message is missing", async () => {
|
|
88
|
+
await withTempDir(async (memoryDir) => {
|
|
89
|
+
const service = makeService({ memoryDir, configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }) });
|
|
90
|
+
const m = mockRes();
|
|
91
|
+
await handleChatMessage(mockReq() as never, m as never, {}, { service, config: service.configRef!.chat, memoryDir });
|
|
92
|
+
assert.equal(m.status, 400);
|
|
93
|
+
assert.ok(m.body.includes("required"));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("HTTP chat/message: 503 when no LLM is available", async () => {
|
|
98
|
+
await withTempDir(async (memoryDir) => {
|
|
99
|
+
const service = makeService({
|
|
100
|
+
fallbackLlmRef: null,
|
|
101
|
+
localLlmRef: null,
|
|
102
|
+
memoryDir,
|
|
103
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
104
|
+
});
|
|
105
|
+
const m = mockRes();
|
|
106
|
+
await handleChatMessage(mockReq() as never, m as never, { message: "hi" }, { service, config: service.configRef!.chat, memoryDir });
|
|
107
|
+
assert.equal(m.status, 503);
|
|
108
|
+
assert.ok(m.body.includes("no_llm"));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("HTTP chat/message: 200 happy path returns reply + chatSessionId", async () => {
|
|
113
|
+
await withTempDir(async (memoryDir) => {
|
|
114
|
+
const service = makeService({
|
|
115
|
+
memoryDir,
|
|
116
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
117
|
+
});
|
|
118
|
+
const m = mockRes();
|
|
119
|
+
await handleChatMessage(mockReq() as never, m as never, { message: "hello" }, { service, config: service.configRef!.chat, memoryDir });
|
|
120
|
+
assert.equal(m.status, 200);
|
|
121
|
+
const parsed = JSON.parse(m.body) as { reply?: string; chatSessionId?: string };
|
|
122
|
+
assert.ok(parsed.reply && parsed.reply.length > 0);
|
|
123
|
+
assert.ok(parsed.chatSessionId);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("HTTP chat/message: 403 when resuming a session owned by another principal", async () => {
|
|
128
|
+
await withTempDir(async (memoryDir) => {
|
|
129
|
+
const session = await createChatSession(memoryDir, { principal: "alice" });
|
|
130
|
+
const service = makeService({
|
|
131
|
+
memoryDir,
|
|
132
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
133
|
+
});
|
|
134
|
+
const m = mockRes();
|
|
135
|
+
await handleChatMessage(
|
|
136
|
+
mockReq() as never, m as never,
|
|
137
|
+
{ message: "hi", chatSessionId: session.id },
|
|
138
|
+
{ service, config: service.configRef!.chat, memoryDir },
|
|
139
|
+
"bob",
|
|
140
|
+
);
|
|
141
|
+
assert.equal(m.status, 403);
|
|
142
|
+
assert.ok(m.body.includes("access_denied"));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("HTTP chat/message: 404 when resuming a non-existent session", async () => {
|
|
147
|
+
await withTempDir(async (memoryDir) => {
|
|
148
|
+
const service = makeService({
|
|
149
|
+
memoryDir,
|
|
150
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
151
|
+
});
|
|
152
|
+
const m = mockRes();
|
|
153
|
+
await handleChatMessage(
|
|
154
|
+
mockReq() as never, m as never,
|
|
155
|
+
{ message: "hi", chatSessionId: "does-not-exist" },
|
|
156
|
+
{ service, config: service.configRef!.chat, memoryDir },
|
|
157
|
+
);
|
|
158
|
+
assert.equal(m.status, 404);
|
|
159
|
+
assert.ok(m.body.includes("chat_session_not_found"));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("HTTP chat/events SSE: streams transcript entries", async () => {
|
|
164
|
+
await withTempDir(async (memoryDir) => {
|
|
165
|
+
const session = await createChatSession(memoryDir, { principal: "alice" });
|
|
166
|
+
await appendTranscriptEntry(memoryDir, session.id, { role: "user", content: "hello" });
|
|
167
|
+
await appendTranscriptEntry(memoryDir, session.id, { role: "assistant", content: "world" });
|
|
168
|
+
|
|
169
|
+
const service = makeService({
|
|
170
|
+
memoryDir,
|
|
171
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
172
|
+
});
|
|
173
|
+
const m = mockRes();
|
|
174
|
+
const req = mockReq();
|
|
175
|
+
await handleChatEventsSSE(req as never, m as never, session.id, { service, config: service.configRef!.chat, memoryDir }, "alice");
|
|
176
|
+
assert.equal(m.status, 200);
|
|
177
|
+
assert.equal(m.headers["content-type"], "text/event-stream");
|
|
178
|
+
// The system header + user + assistant = 3 data frames.
|
|
179
|
+
const frames = m.body.split("\n\n").filter((f) => f.startsWith("data:"));
|
|
180
|
+
assert.ok(frames.length >= 3, `expected >=3 SSE frames, got ${frames.length}`);
|
|
181
|
+
// Close the request to stop the heartbeat.
|
|
182
|
+
req.emit("close");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("HTTP chat/events SSE: 403 for a session owned by another principal", async () => {
|
|
187
|
+
await withTempDir(async (memoryDir) => {
|
|
188
|
+
const session = await createChatSession(memoryDir, { principal: "alice" });
|
|
189
|
+
const service = makeService({
|
|
190
|
+
memoryDir,
|
|
191
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
192
|
+
});
|
|
193
|
+
const m = mockRes();
|
|
194
|
+
await handleChatEventsSSE(mockReq() as never, m as never, session.id, { service, config: service.configRef!.chat, memoryDir }, "bob");
|
|
195
|
+
assert.equal(m.status, 403);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat HTTP handler (issue #1583 PR 3).
|
|
3
|
+
*
|
|
4
|
+
* Implements the HTTP backend for the admin console Chat pane:
|
|
5
|
+
* - POST /engram/v1/chat/message — send a message, get a reply
|
|
6
|
+
* - GET /engram/v1/chat/events/:chatSessionId — SSE stream for replies
|
|
7
|
+
*
|
|
8
|
+
* Auth: same Bearer token as every other admin endpoint (loopback bearer).
|
|
9
|
+
* Per-session isolation: every request verifies the session's principal
|
|
10
|
+
* matches the caller (rule 42/47).
|
|
11
|
+
*
|
|
12
|
+
* This module is imported by access-http.ts with thin route registration —
|
|
13
|
+
* the god-file ratchet (#1520) tracks access-http.ts LOC.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
17
|
+
|
|
18
|
+
import type { EngramAccessService } from "../access-service.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 { isConfirmationMessage } from "./chat-engine.js";
|
|
23
|
+
import { createProductionChatLlmAdapter } from "./chat-llm.js";
|
|
24
|
+
import { createChatExecutor } from "./chat-executor.js";
|
|
25
|
+
import {
|
|
26
|
+
createChatSession,
|
|
27
|
+
loadChatSession,
|
|
28
|
+
appendTranscriptEntry,
|
|
29
|
+
sessionBelongsToPrincipal,
|
|
30
|
+
markPendingPlan,
|
|
31
|
+
markPlanResolved,
|
|
32
|
+
markPendingPromotion,
|
|
33
|
+
markPromotionResolved,
|
|
34
|
+
} from "./chat-session.js";
|
|
35
|
+
|
|
36
|
+
export interface ChatHttpHandlerOptions {
|
|
37
|
+
service: EngramAccessService;
|
|
38
|
+
config: ChatConfig | undefined;
|
|
39
|
+
memoryDir: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Handle POST /engram/v1/chat/message.
|
|
44
|
+
*
|
|
45
|
+
* Body: `{ message: string, chatSessionId?: string }`
|
|
46
|
+
* Response: `{ reply: string, chatSessionId: string, pendingPlan?: {...} }`
|
|
47
|
+
*/
|
|
48
|
+
export async function handleChatMessage(
|
|
49
|
+
req: IncomingMessage,
|
|
50
|
+
res: ServerResponse,
|
|
51
|
+
body: Record<string, unknown>,
|
|
52
|
+
opts: ChatHttpHandlerOptions,
|
|
53
|
+
principal?: string,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
if (!opts.config?.enabled) {
|
|
56
|
+
respondJson(res, 404, { error: "chat_disabled", code: "chat_disabled" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const message = typeof body.message === "string" ? body.message : "";
|
|
61
|
+
if (!message) {
|
|
62
|
+
respondJson(res, 400, { error: "message is required", code: "input_error" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const llm = opts.service.fallbackLlmRef ?? opts.service.localLlmRef;
|
|
67
|
+
if (!llm) {
|
|
68
|
+
respondJson(res, 503, {
|
|
69
|
+
error: "No LLM model is available. Configure a local or cloud model.",
|
|
70
|
+
code: "no_llm",
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const adapter = createProductionChatLlmAdapter(llm as {
|
|
76
|
+
chatCompletion(
|
|
77
|
+
messages: Array<{ role: string; content: string }>,
|
|
78
|
+
options?: { model?: string; signal?: AbortSignal },
|
|
79
|
+
): Promise<{ content: string } | null>;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Load or create session.
|
|
83
|
+
const requestedSessionId = typeof body.chatSessionId === "string" ? body.chatSessionId : undefined;
|
|
84
|
+
let session;
|
|
85
|
+
if (requestedSessionId) {
|
|
86
|
+
session = await loadChatSession(opts.memoryDir, requestedSessionId);
|
|
87
|
+
if (!session) {
|
|
88
|
+
respondJson(res, 404, { error: "chat_session_not_found", code: "chat_session_not_found" });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!sessionBelongsToPrincipal(session, principal)) {
|
|
92
|
+
respondJson(res, 403, { error: "access_denied", code: "access_denied" });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
session = await createChatSession(opts.memoryDir, { principal });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const executor = createChatExecutor({
|
|
100
|
+
service: opts.service,
|
|
101
|
+
principal,
|
|
102
|
+
...(session.namespace ? { namespace: session.namespace } : {}),
|
|
103
|
+
...(session.sessionKey ? { sessionKey: session.sessionKey } : {}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const engine = new ChatEngine({
|
|
107
|
+
llm: adapter,
|
|
108
|
+
executor,
|
|
109
|
+
maxToolCallsPerTurn: opts.config.maxToolCallsPerTurn,
|
|
110
|
+
...(opts.config.model ? { model: opts.config.model } : {}),
|
|
111
|
+
correctionAvailable: false,
|
|
112
|
+
scopeInspectAvailable: false,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await appendTranscriptEntry(opts.memoryDir, session.id, {
|
|
116
|
+
role: "user",
|
|
117
|
+
content: message,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Snapshot pending state BEFORE the turn so we only persist a marker when
|
|
121
|
+
// state actually changed (Thread 15 — a normal turn must not clear an
|
|
122
|
+
// active pending plan/promotion from an earlier turn).
|
|
123
|
+
const priorPendingPlanId = session.pendingPlanId;
|
|
124
|
+
const priorPendingPromotionId = session.pendingPromotionId;
|
|
125
|
+
|
|
126
|
+
const result: ChatTurnResult = await engine.processMessage(message, session);
|
|
127
|
+
|
|
128
|
+
await appendTranscriptEntry(opts.memoryDir, session.id, {
|
|
129
|
+
role: "assistant",
|
|
130
|
+
content: result.reply,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Persist pending-plan/promotion state (append-only markers). Only append
|
|
134
|
+
// when state actually changed.
|
|
135
|
+
if (result.pendingPlan?.planId) {
|
|
136
|
+
await markPendingPlan(opts.memoryDir, session.id, result.pendingPlan.planId);
|
|
137
|
+
} else if (session.pendingPromotionId && !priorPendingPromotionId) {
|
|
138
|
+
await markPendingPromotion(opts.memoryDir, session.id, session.pendingPromotionId);
|
|
139
|
+
} else if (priorPendingPlanId && !session.pendingPlanId && !result.error) {
|
|
140
|
+
await markPlanResolved(opts.memoryDir, session.id, priorPendingPlanId);
|
|
141
|
+
} else if (priorPendingPromotionId && !session.pendingPromotionId && !result.error) {
|
|
142
|
+
await markPromotionResolved(opts.memoryDir, session.id, priorPendingPromotionId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Strip internal error details from the wire response (CodeQL — no stack
|
|
146
|
+
// traces or internal messages leak to the client; keep only the tagged reply).
|
|
147
|
+
const wireResult = result.error
|
|
148
|
+
? { reply: result.reply, chatSessionId: result.chatSessionId, ...(result.pendingPlan ? { pendingPlan: result.pendingPlan } : {}) }
|
|
149
|
+
: result;
|
|
150
|
+
respondJson(res, 200, wireResult);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handle GET /engram/v1/chat/events/:chatSessionId.
|
|
155
|
+
*
|
|
156
|
+
* SSE stream. Sends a heartbeat every 25s so proxies don't time out.
|
|
157
|
+
* Currently streams the session transcript as an initial burst, then keeps
|
|
158
|
+
* the connection open for future updates.
|
|
159
|
+
*/
|
|
160
|
+
export async function handleChatEventsSSE(
|
|
161
|
+
req: IncomingMessage,
|
|
162
|
+
res: ServerResponse,
|
|
163
|
+
chatSessionId: string,
|
|
164
|
+
opts: ChatHttpHandlerOptions,
|
|
165
|
+
principal?: string,
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
if (!opts.config?.enabled) {
|
|
168
|
+
respondJson(res, 404, { error: "chat_disabled", code: "chat_disabled" });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const session = await loadChatSession(opts.memoryDir, chatSessionId);
|
|
173
|
+
if (!session) {
|
|
174
|
+
respondJson(res, 404, { error: "chat_session_not_found", code: "chat_session_not_found" });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!sessionBelongsToPrincipal(session, principal)) {
|
|
178
|
+
respondJson(res, 403, { error: "access_denied", code: "access_denied" });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// SSE headers.
|
|
183
|
+
res.writeHead(200, {
|
|
184
|
+
"content-type": "text/event-stream",
|
|
185
|
+
"cache-control": "no-cache",
|
|
186
|
+
connection: "keep-alive",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Send the transcript as an initial burst.
|
|
190
|
+
for (const entry of session.transcript) {
|
|
191
|
+
res.write(`data: ${JSON.stringify(entry)}\n\n`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Heartbeat. Unref'd so a lingering connection never blocks process
|
|
195
|
+
// exit (rule 47 — no shared mutable objects keep the loop alive).
|
|
196
|
+
const heartbeat = setInterval(() => {
|
|
197
|
+
try {
|
|
198
|
+
res.write(`data: ${JSON.stringify({ type: "heartbeat" })}\n\n`);
|
|
199
|
+
} catch {
|
|
200
|
+
// Connection closed — clearInterval below.
|
|
201
|
+
}
|
|
202
|
+
}, 25_000);
|
|
203
|
+
heartbeat.unref();
|
|
204
|
+
|
|
205
|
+
req.on("close", () => {
|
|
206
|
+
clearInterval(heartbeat);
|
|
207
|
+
try { res.end(); } catch { /* already ended */ }
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Helpers
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
function respondJson(res: ServerResponse, status: number, body: unknown): void {
|
|
216
|
+
const json = JSON.stringify(body);
|
|
217
|
+
res.writeHead(status, {
|
|
218
|
+
"content-type": "application/json; charset=utf-8",
|
|
219
|
+
"content-length": Buffer.byteLength(json, "utf8"),
|
|
220
|
+
});
|
|
221
|
+
res.end(json);
|
|
222
|
+
}
|