@remnic/core 9.3.726 → 9.3.727
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 +5 -5
- package/dist/access-boundary.js +20 -20
- package/dist/access-cli.js +42 -42
- package/dist/access-http.d.ts +20 -5
- package/dist/access-http.js +25 -25
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +24 -24
- package/dist/access-operations-batch.js +21 -21
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +23 -23
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-B2PlPtxh.d.ts → access-service-Bgpjxo4f.d.ts} +36 -4
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +19 -19
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +3 -3
- 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 +7 -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/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/{catalog-CzUb-GyD.d.ts → catalog-bMczlO2G.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 +8 -8
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-Q6D75LVH.js → chunk-27QQC7ZX.js} +75 -14
- package/dist/{chunk-Q6D75LVH.js.map → chunk-27QQC7ZX.js.map} +1 -1
- package/dist/{chunk-53EEQYE5.js → chunk-2KAYTPPT.js} +7 -7
- package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-BIA7ELIB.js → chunk-36SEDFAU.js} +3 -3
- package/dist/{chunk-UQSVUEN2.js → chunk-37436GML.js} +2 -2
- package/dist/{chunk-RRHHQYJS.js → chunk-3TSTGI6F.js} +2 -2
- package/dist/{chunk-FO3HO52S.js → chunk-4H4F3BFL.js} +2 -2
- package/dist/{chunk-BEUCACTV.js → chunk-4NOI577P.js} +2 -2
- package/dist/{chunk-BBIE7W52.js → chunk-4NR2PDZ5.js} +2 -2
- package/dist/{chunk-TQQH2FTX.js → chunk-6PY5YJAV.js} +101 -15
- package/dist/{chunk-TQQH2FTX.js.map → chunk-6PY5YJAV.js.map} +1 -1
- package/dist/{chunk-PKMQK2WY.js → chunk-773DPI6T.js} +4 -4
- package/dist/{chunk-MQVKHFIU.js → chunk-7RLCCNHU.js} +8 -8
- package/dist/{chunk-XGTBNVIU.js → chunk-AAD2ZS6P.js} +11 -1
- package/dist/chunk-AAD2ZS6P.js.map +1 -0
- package/dist/{chunk-EZNDNMRH.js → chunk-AQLOP5F4.js} +2 -2
- package/dist/{chunk-UNNDFYUX.js → chunk-ARV3AUOM.js} +5 -5
- package/dist/{chunk-NPL7VDM6.js → chunk-ASKXIAHD.js} +2 -2
- package/dist/{chunk-K4ZNARDO.js → chunk-CBWI22JN.js} +2 -2
- package/dist/{chunk-ZOHMPFOB.js → chunk-CKFLLAIS.js} +3 -3
- package/dist/{chunk-DCKNMLEQ.js → chunk-CSRXQ6NB.js} +2 -2
- package/dist/{chunk-6AFVIOVZ.js → chunk-CTOQEZSN.js} +2 -2
- package/dist/{chunk-7NS7AWMZ.js → chunk-DKJCRGWM.js} +3 -3
- package/dist/{chunk-JPUT5NGH.js → chunk-DMQWV6SQ.js} +2 -2
- package/dist/{chunk-F3XZN24Z.js → chunk-DRUDC2UX.js} +2 -2
- package/dist/{chunk-KJR66XOM.js → chunk-EY6MDMHK.js} +2 -2
- package/dist/{chunk-3TUT4PWB.js → chunk-FVZ2ISIX.js} +2 -2
- package/dist/{chunk-C4X4QGIO.js → chunk-G6NKALRN.js} +5 -2
- package/dist/chunk-G6NKALRN.js.map +1 -0
- package/dist/{chunk-6SQQOJ4U.js → chunk-GKRLPHPU.js} +2 -2
- package/dist/{chunk-5NWYQUQC.js → chunk-HOHVIVDR.js} +2 -2
- package/dist/{chunk-6WAJQKBS.js → chunk-JYLFZWUX.js} +2 -2
- package/dist/{chunk-5W7CTZ2E.js → chunk-KQAFEZQX.js} +4 -4
- package/dist/{chunk-YTZ3VGC7.js → chunk-M4V2JVSZ.js} +2 -2
- package/dist/{chunk-PLG6JHQS.js → chunk-MJIHJ7OG.js} +38 -38
- package/dist/{chunk-TA3Y6GEX.js → chunk-MLKWJOAJ.js} +2 -2
- package/dist/{chunk-2NBP25BG.js → chunk-N3VNUM4Z.js} +171 -79
- package/dist/chunk-N3VNUM4Z.js.map +1 -0
- package/dist/{chunk-UKTDXTWX.js → chunk-NSA5ZRUL.js} +6 -6
- package/dist/{chunk-DQXPJ6HD.js → chunk-OEEW4KJA.js} +4 -4
- package/dist/{chunk-7AG54AF3.js → chunk-PUR2CKX2.js} +1 -1
- package/dist/chunk-PUR2CKX2.js.map +1 -0
- package/dist/{chunk-5OOA7SGX.js → chunk-R7IYU5DZ.js} +2 -2
- package/dist/{chunk-WGTWGDR5.js → chunk-SNKPUJHR.js} +5 -5
- package/dist/{chunk-BB57OZWC.js → chunk-UVPL565C.js} +2 -2
- package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-OAHOL3ZC.js → chunk-XPHVAOBT.js} +111 -30
- package/dist/chunk-XPHVAOBT.js.map +1 -0
- package/dist/{chunk-V7CMKMHM.js → chunk-Y4YD5TYW.js} +3 -3
- package/dist/{chunk-MDBZTH7N.js → chunk-YKBHOBJK.js} +44 -10
- package/dist/chunk-YKBHOBJK.js.map +1 -0
- package/dist/{chunk-3MS3QVX6.js → chunk-YOLZS7NY.js} +2 -2
- package/dist/{chunk-6OIDDKAA.js → chunk-YTU7WKYN.js} +35 -9
- package/dist/chunk-YTU7WKYN.js.map +1 -0
- package/dist/{cli-IOlDHSBT.d.ts → cli-BLME420h.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +47 -47
- package/dist/compat/codex-cli-fallback.d.ts +1 -0
- package/dist/compat/codex-cli-fallback.js +22 -0
- package/dist/compat/codex-cli-fallback.js.map +1 -0
- package/dist/compat/codex-thread-key.d.ts +1 -0
- package/dist/compat/codex-thread-key.js +18 -0
- package/dist/compat/codex-thread-key.js.map +1 -0
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +7 -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 +3 -3
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +7 -7
- 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 +7 -7
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-faithfulness.js +2 -2
- 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 +2 -2
- package/dist/faithfulness-config.d.ts +1 -0
- package/dist/faithfulness-config.js +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.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 +61 -61
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +7 -7
- 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/local-model-endpoint.d.ts +9 -4
- package/dist/local-model-endpoint.js +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +7 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.js +8 -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 -12
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +7 -7
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +18 -18
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +9 -9
- package/dist/{orchestrator-C_OMSIMZ.d.ts → orchestrator-DqPAz8ot.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +30 -30
- 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 +1 -1
- 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-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -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 +4 -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/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +3 -3
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +7 -7
- 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-B8LiYtvx.d.ts → semantic-consolidation-Cl15ksZy.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +8 -8
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +7 -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 +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +6 -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/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-DmTaqob2.d.ts → types-ChQb9D6s.d.ts} +8 -0
- 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 +7 -7
- package/package.json +26 -2
- package/src/access-http.ts +68 -1
- package/src/admin/admin-surfaces.test.ts +289 -0
- package/src/admin/admin-surfaces.ts +138 -7
- package/src/bitemporal-dedup-backfill.test.ts +294 -0
- package/src/chat/chat-engine.test.ts +66 -1
- package/src/chat/chat-http.test.ts +119 -1
- package/src/chat/chat-http.ts +252 -97
- package/src/chat/chat-session.ts +143 -7
- package/src/compat/codex-cli-fallback.ts +23 -0
- package/src/compat/codex-subpath-aliases.test.ts +93 -0
- package/src/compat/codex-thread-key.ts +23 -0
- package/src/extraction-faithfulness.test.ts +240 -0
- package/src/extraction-faithfulness.ts +58 -6
- package/src/faithfulness-config.ts +16 -1
- package/src/local-model-endpoint.ts +24 -3
- package/src/orchestrator.ts +200 -8
- package/src/provenance-extraction.test.ts +266 -0
- package/src/provenance.ts +66 -16
- package/src/types.ts +8 -0
- package/dist/chunk-2NBP25BG.js.map +0 -1
- package/dist/chunk-6OIDDKAA.js.map +0 -1
- package/dist/chunk-7AG54AF3.js.map +0 -1
- package/dist/chunk-C4X4QGIO.js.map +0 -1
- package/dist/chunk-MDBZTH7N.js.map +0 -1
- package/dist/chunk-OAHOL3ZC.js.map +0 -1
- package/dist/chunk-XGTBNVIU.js.map +0 -1
- /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
- /package/dist/{chunk-53EEQYE5.js.map → chunk-2KAYTPPT.js.map} +0 -0
- /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-BIA7ELIB.js.map → chunk-36SEDFAU.js.map} +0 -0
- /package/dist/{chunk-UQSVUEN2.js.map → chunk-37436GML.js.map} +0 -0
- /package/dist/{chunk-RRHHQYJS.js.map → chunk-3TSTGI6F.js.map} +0 -0
- /package/dist/{chunk-FO3HO52S.js.map → chunk-4H4F3BFL.js.map} +0 -0
- /package/dist/{chunk-BEUCACTV.js.map → chunk-4NOI577P.js.map} +0 -0
- /package/dist/{chunk-BBIE7W52.js.map → chunk-4NR2PDZ5.js.map} +0 -0
- /package/dist/{chunk-PKMQK2WY.js.map → chunk-773DPI6T.js.map} +0 -0
- /package/dist/{chunk-MQVKHFIU.js.map → chunk-7RLCCNHU.js.map} +0 -0
- /package/dist/{chunk-EZNDNMRH.js.map → chunk-AQLOP5F4.js.map} +0 -0
- /package/dist/{chunk-UNNDFYUX.js.map → chunk-ARV3AUOM.js.map} +0 -0
- /package/dist/{chunk-NPL7VDM6.js.map → chunk-ASKXIAHD.js.map} +0 -0
- /package/dist/{chunk-K4ZNARDO.js.map → chunk-CBWI22JN.js.map} +0 -0
- /package/dist/{chunk-ZOHMPFOB.js.map → chunk-CKFLLAIS.js.map} +0 -0
- /package/dist/{chunk-DCKNMLEQ.js.map → chunk-CSRXQ6NB.js.map} +0 -0
- /package/dist/{chunk-6AFVIOVZ.js.map → chunk-CTOQEZSN.js.map} +0 -0
- /package/dist/{chunk-7NS7AWMZ.js.map → chunk-DKJCRGWM.js.map} +0 -0
- /package/dist/{chunk-JPUT5NGH.js.map → chunk-DMQWV6SQ.js.map} +0 -0
- /package/dist/{chunk-F3XZN24Z.js.map → chunk-DRUDC2UX.js.map} +0 -0
- /package/dist/{chunk-KJR66XOM.js.map → chunk-EY6MDMHK.js.map} +0 -0
- /package/dist/{chunk-3TUT4PWB.js.map → chunk-FVZ2ISIX.js.map} +0 -0
- /package/dist/{chunk-6SQQOJ4U.js.map → chunk-GKRLPHPU.js.map} +0 -0
- /package/dist/{chunk-5NWYQUQC.js.map → chunk-HOHVIVDR.js.map} +0 -0
- /package/dist/{chunk-6WAJQKBS.js.map → chunk-JYLFZWUX.js.map} +0 -0
- /package/dist/{chunk-5W7CTZ2E.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-YTZ3VGC7.js.map → chunk-M4V2JVSZ.js.map} +0 -0
- /package/dist/{chunk-PLG6JHQS.js.map → chunk-MJIHJ7OG.js.map} +0 -0
- /package/dist/{chunk-TA3Y6GEX.js.map → chunk-MLKWJOAJ.js.map} +0 -0
- /package/dist/{chunk-UKTDXTWX.js.map → chunk-NSA5ZRUL.js.map} +0 -0
- /package/dist/{chunk-DQXPJ6HD.js.map → chunk-OEEW4KJA.js.map} +0 -0
- /package/dist/{chunk-5OOA7SGX.js.map → chunk-R7IYU5DZ.js.map} +0 -0
- /package/dist/{chunk-WGTWGDR5.js.map → chunk-SNKPUJHR.js.map} +0 -0
- /package/dist/{chunk-BB57OZWC.js.map → chunk-UVPL565C.js.map} +0 -0
- /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-V7CMKMHM.js.map → chunk-Y4YD5TYW.js.map} +0 -0
- /package/dist/{chunk-3MS3QVX6.js.map → chunk-YOLZS7NY.js.map} +0 -0
package/src/access-http.ts
CHANGED
|
@@ -34,6 +34,9 @@ import { getOperation } from "./access-boundary.js";
|
|
|
34
34
|
// dispatch the migrated routes through the registry (issue #1525).
|
|
35
35
|
import "./access-operations.js";
|
|
36
36
|
import { handleChatMessage, handleChatEventsSSE } from "./chat/chat-http.js";
|
|
37
|
+
// cleanupExpiredChatSessions wires the chat session TTL sweep into the
|
|
38
|
+
// server lifecycle (issue #1685 item 1 / #1687 Thread 21).
|
|
39
|
+
import { cleanupExpiredChatSessions } from "./chat/chat-session.js";
|
|
37
40
|
|
|
38
41
|
export interface EngramAccessHttpServerOptions {
|
|
39
42
|
service: EngramAccessService;
|
|
@@ -293,6 +296,13 @@ export class EngramAccessHttpServer {
|
|
|
293
296
|
*/
|
|
294
297
|
private readonly sseCleanupFns = new Set<() => void>();
|
|
295
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Periodic chat-session TTL sweep handle (issue #1685 item 1 /
|
|
301
|
+
* #1687 Thread 21). unref'd so it never blocks process exit
|
|
302
|
+
* (rule 47); cleared in `stop()`.
|
|
303
|
+
*/
|
|
304
|
+
private chatTtlTimer: NodeJS.Timeout | null = null;
|
|
305
|
+
|
|
296
306
|
constructor(options: EngramAccessHttpServerOptions) {
|
|
297
307
|
this.service = options.service;
|
|
298
308
|
this.host = options.host?.trim() || "127.0.0.1";
|
|
@@ -386,11 +396,25 @@ export class EngramAccessHttpServer {
|
|
|
386
396
|
this.server = server;
|
|
387
397
|
const address = server.address();
|
|
388
398
|
this.boundPort = typeof address === "object" && address ? address.port : this.requestedPort;
|
|
399
|
+
|
|
400
|
+
// ── Chat session TTL sweep (issue #1685 item 1 / #1687 Thread 21) ──
|
|
401
|
+
// Expire idle chat sessions so the JSONL store cannot grow unbounded.
|
|
402
|
+
// A one-shot sweep runs on startup; a periodic re-sweep follows on an
|
|
403
|
+
// unref'd 1-hour cadence (rule 47 — never blocks process exit). Both
|
|
404
|
+
// are no-ops when chat is disabled (no TTL configured).
|
|
405
|
+
this.scheduleChatSessionTtlSweep();
|
|
406
|
+
|
|
389
407
|
return this.status();
|
|
390
408
|
}
|
|
391
409
|
|
|
392
410
|
async stop(): Promise<void> {
|
|
393
411
|
if (!this.server) return;
|
|
412
|
+
// Release the chat-session TTL sweep timer (issue #1685) alongside
|
|
413
|
+
// the SSE cleanups below.
|
|
414
|
+
if (this.chatTtlTimer) {
|
|
415
|
+
clearInterval(this.chatTtlTimer);
|
|
416
|
+
this.chatTtlTimer = null;
|
|
417
|
+
}
|
|
394
418
|
const server = this.server;
|
|
395
419
|
this.server = null;
|
|
396
420
|
this.boundPort = 0;
|
|
@@ -418,6 +442,36 @@ export class EngramAccessHttpServer {
|
|
|
418
442
|
});
|
|
419
443
|
}
|
|
420
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Schedule the chat-session TTL sweep: one immediate pass plus a
|
|
447
|
+
* recurring hourly pass (issue #1685 item 1 / #1687 Thread 21). Both
|
|
448
|
+
* are skipped when chat is disabled. The recurring timer is unref'd
|
|
449
|
+
* so it never keeps the event loop alive on its own (rule 47). The
|
|
450
|
+
* sweep itself is best-effort — cleanup failures are swallowed inside
|
|
451
|
+
* `cleanupExpiredChatSessions`.
|
|
452
|
+
*/
|
|
453
|
+
private scheduleChatSessionTtlSweep(): void {
|
|
454
|
+
// Clear any prior handle so a second start() without an intervening
|
|
455
|
+
// stop() cannot leak a dangling setInterval (Kilo review thread).
|
|
456
|
+
if (this.chatTtlTimer) {
|
|
457
|
+
clearInterval(this.chatTtlTimer);
|
|
458
|
+
this.chatTtlTimer = null;
|
|
459
|
+
}
|
|
460
|
+
const chat = this.service.configRef?.chat;
|
|
461
|
+
if (!chat?.enabled) return;
|
|
462
|
+
const ttlHours = typeof chat.sessionTtlHours === "number" && chat.sessionTtlHours > 0
|
|
463
|
+
? chat.sessionTtlHours
|
|
464
|
+
: 72;
|
|
465
|
+
// Startup sweep — fire and forget; failures never block the server.
|
|
466
|
+
void cleanupExpiredChatSessions(this.service.memoryDir, ttlHours).catch(() => { /* best-effort */ });
|
|
467
|
+
// Hourly re-sweep (unref'd — rule 47).
|
|
468
|
+
const SWEEP_INTERVAL_MS = 60 * 60 * 1000;
|
|
469
|
+
this.chatTtlTimer = setInterval(() => {
|
|
470
|
+
void cleanupExpiredChatSessions(this.service.memoryDir, ttlHours).catch(() => { /* best-effort */ });
|
|
471
|
+
}, SWEEP_INTERVAL_MS);
|
|
472
|
+
this.chatTtlTimer.unref?.();
|
|
473
|
+
}
|
|
474
|
+
|
|
421
475
|
status(): EngramAccessHttpServerStatus {
|
|
422
476
|
return {
|
|
423
477
|
running: this.server !== null,
|
|
@@ -2083,7 +2137,20 @@ export class EngramAccessHttpServer {
|
|
|
2083
2137
|
void getOperation("chat_events"); // boundary dispatch (issue #1525)
|
|
2084
2138
|
await handleChatEventsSSE(
|
|
2085
2139
|
req, res, chatEventsMatch[1] ?? "",
|
|
2086
|
-
{
|
|
2140
|
+
{
|
|
2141
|
+
service: this.service,
|
|
2142
|
+
config: this.service.configRef?.chat,
|
|
2143
|
+
memoryDir: this.service.memoryDir,
|
|
2144
|
+
// Register the chat-SSE disconnect cleanup with this server's
|
|
2145
|
+
// stop() set so an HTTP shutdown forcibly releases the heartbeat
|
|
2146
|
+
// and transcript subscription even when a lingering client never
|
|
2147
|
+
// emits 'close' (cursor Medium review; mirrors handleGraphEventsSSE
|
|
2148
|
+
// which registers in sseCleanupFns at access-http.ts:2604).
|
|
2149
|
+
registerSseCleanup: (cleanup) => {
|
|
2150
|
+
this.sseCleanupFns.add(cleanup);
|
|
2151
|
+
return () => { this.sseCleanupFns.delete(cleanup); };
|
|
2152
|
+
},
|
|
2153
|
+
},
|
|
2087
2154
|
this.resolveRequestPrincipal(req),
|
|
2088
2155
|
);
|
|
2089
2156
|
return;
|
|
@@ -13,6 +13,7 @@ import path from "node:path";
|
|
|
13
13
|
import test from "node:test";
|
|
14
14
|
|
|
15
15
|
import { NamespaceCatalog } from "../namespaces/catalog.js";
|
|
16
|
+
import { resolveScopeProfilePlan } from "../namespaces/scope-profiles.js";
|
|
16
17
|
import { parseConfig } from "../config.js";
|
|
17
18
|
import {
|
|
18
19
|
AdminPromotionError,
|
|
@@ -456,3 +457,291 @@ test("promoteMemory throws source_not_found when the source memory is absent", a
|
|
|
456
457
|
(err: unknown) => err instanceof AdminPromotionError && err.code === "source_not_found",
|
|
457
458
|
);
|
|
458
459
|
});
|
|
460
|
+
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// gatherMaintenanceHealth — honest QMD state classification (issue #1658 thread 1)
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
|
|
465
|
+
/** Build a QMD health snapshot fixture for the provider. */
|
|
466
|
+
function qmdFixture(namespace: string, overrides: Partial<Record<string, unknown>> = {}) {
|
|
467
|
+
return {
|
|
468
|
+
namespace,
|
|
469
|
+
collection: `col-${namespace}`,
|
|
470
|
+
available: true,
|
|
471
|
+
collectionState: "present",
|
|
472
|
+
debugStatus: "ok",
|
|
473
|
+
installedVersion: "1.0.0",
|
|
474
|
+
supportedVersion: "1.0.0",
|
|
475
|
+
supported: true,
|
|
476
|
+
upgradeAvailable: false,
|
|
477
|
+
daemonMode: false,
|
|
478
|
+
...overrides,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
test("gatherMaintenanceHealth classifies a present QMD collection as healthy (issue #1658 thread 1)", async () => {
|
|
483
|
+
const memoryDir = await makeTempDir();
|
|
484
|
+
try {
|
|
485
|
+
const config = makeConfig({ memoryDir });
|
|
486
|
+
const catalog = new NamespaceCatalog(config);
|
|
487
|
+
await catalog.registerConfiguredNamespaces();
|
|
488
|
+
const report = await gatherMaintenanceHealth({
|
|
489
|
+
catalog,
|
|
490
|
+
qmdHealthProvider: async (ns) => qmdFixture(ns, { collectionState: "present" }),
|
|
491
|
+
});
|
|
492
|
+
assert.equal(report.degradedMode, false);
|
|
493
|
+
const entry = report.perNamespace.find((e) => e.namespace === "default");
|
|
494
|
+
assert.ok(entry, "default namespace entry must exist");
|
|
495
|
+
assert.equal(entry!.qmdState, "healthy");
|
|
496
|
+
assert.equal(entry!.qmdDegraded, false);
|
|
497
|
+
assert.match(entry!.qmdStateReason, /ready/i);
|
|
498
|
+
} finally {
|
|
499
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("gatherMaintenanceHealth classifies an unknown collection state distinctly from missing (issue #1658 thread 1)", async () => {
|
|
504
|
+
const memoryDir = await makeTempDir();
|
|
505
|
+
try {
|
|
506
|
+
const config = makeConfig({ memoryDir });
|
|
507
|
+
const catalog = new NamespaceCatalog(config);
|
|
508
|
+
await catalog.registerConfiguredNamespaces();
|
|
509
|
+
const report = await gatherMaintenanceHealth({
|
|
510
|
+
catalog,
|
|
511
|
+
qmdHealthProvider: async (ns) => qmdFixture(ns, { collectionState: "unknown" }),
|
|
512
|
+
});
|
|
513
|
+
// "unknown" is still counted degraded (matches the router health path) so
|
|
514
|
+
// aggregate alerting is unchanged, but it is now RENDERED honestly as a
|
|
515
|
+
// distinct state rather than collapsed into "missing".
|
|
516
|
+
assert.equal(report.degradedMode, true);
|
|
517
|
+
const entry = report.perNamespace.find((e) => e.namespace === "default");
|
|
518
|
+
assert.ok(entry);
|
|
519
|
+
assert.equal(entry!.qmdState, "unknown");
|
|
520
|
+
assert.equal(entry!.qmdDegraded, true);
|
|
521
|
+
assert.match(entry!.qmdStateReason, /transient/i);
|
|
522
|
+
} finally {
|
|
523
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("gatherMaintenanceHealth classifies a missing collection as an actionable state (issue #1658 thread 1)", async () => {
|
|
528
|
+
const memoryDir = await makeTempDir();
|
|
529
|
+
try {
|
|
530
|
+
const config = makeConfig({ memoryDir });
|
|
531
|
+
const catalog = new NamespaceCatalog(config);
|
|
532
|
+
await catalog.registerConfiguredNamespaces();
|
|
533
|
+
const report = await gatherMaintenanceHealth({
|
|
534
|
+
catalog,
|
|
535
|
+
qmdHealthProvider: async (ns) => qmdFixture(ns, { collectionState: "missing" }),
|
|
536
|
+
});
|
|
537
|
+
const entry = report.perNamespace.find((e) => e.namespace === "default");
|
|
538
|
+
assert.ok(entry);
|
|
539
|
+
assert.equal(entry!.qmdState, "missing");
|
|
540
|
+
assert.equal(entry!.qmdDegraded, true);
|
|
541
|
+
assert.match(entry!.qmdStateReason, /not built|index/i);
|
|
542
|
+
} finally {
|
|
543
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("gatherMaintenanceHealth classifies an unavailable backend distinctly (issue #1658 thread 1)", async () => {
|
|
548
|
+
const memoryDir = await makeTempDir();
|
|
549
|
+
try {
|
|
550
|
+
const config = makeConfig({ memoryDir });
|
|
551
|
+
const catalog = new NamespaceCatalog(config);
|
|
552
|
+
await catalog.registerConfiguredNamespaces();
|
|
553
|
+
const report = await gatherMaintenanceHealth({
|
|
554
|
+
catalog,
|
|
555
|
+
qmdHealthProvider: async (ns) => qmdFixture(ns, { available: false, collectionState: "unknown" }),
|
|
556
|
+
});
|
|
557
|
+
const entry = report.perNamespace.find((e) => e.namespace === "default");
|
|
558
|
+
assert.ok(entry);
|
|
559
|
+
assert.equal(entry!.qmdState, "unavailable");
|
|
560
|
+
assert.equal(entry!.qmdDegraded, true);
|
|
561
|
+
} finally {
|
|
562
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("gatherMaintenanceHealth reports not_probed when no provider is supplied (issue #1658 thread 1)", async () => {
|
|
567
|
+
const memoryDir = await makeTempDir();
|
|
568
|
+
try {
|
|
569
|
+
const config = makeConfig({ memoryDir });
|
|
570
|
+
const catalog = new NamespaceCatalog(config);
|
|
571
|
+
await catalog.registerConfiguredNamespaces();
|
|
572
|
+
const report = await gatherMaintenanceHealth({ catalog });
|
|
573
|
+
const entry = report.perNamespace.find((e) => e.namespace === "default");
|
|
574
|
+
assert.ok(entry);
|
|
575
|
+
assert.equal(entry!.qmdState, "not_probed");
|
|
576
|
+
assert.equal(entry!.qmdDegraded, false);
|
|
577
|
+
} finally {
|
|
578
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
// promoteMemory — honest project-context promotion reasons (issue #1658 thread 2)
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
test("promoteMemory reports an honest 'no active scope profile' reason for a project target without a profile (issue #1658 thread 2)", async () => {
|
|
587
|
+
const config = makeConfig();
|
|
588
|
+
const storage = makeMemoryStorageProvider();
|
|
589
|
+
const result = await promoteMemory({
|
|
590
|
+
config,
|
|
591
|
+
sourceMemoryId: "mem-1",
|
|
592
|
+
sourceNamespace: "default",
|
|
593
|
+
principal: "alice",
|
|
594
|
+
targets: [{ kind: "userProject" }],
|
|
595
|
+
reason: "promote project fact",
|
|
596
|
+
actor: "alice",
|
|
597
|
+
storage,
|
|
598
|
+
// scopeProfilePlan omitted → no active profile.
|
|
599
|
+
});
|
|
600
|
+
assert.equal(result.ok, false);
|
|
601
|
+
const target = result.targets[0]!;
|
|
602
|
+
assert.equal(target.authorized, false);
|
|
603
|
+
assert.equal(target.namespace, "");
|
|
604
|
+
assert.match(target.reason, /no active scope profile/);
|
|
605
|
+
assert.match(target.reason, /coding context/i);
|
|
606
|
+
assert.equal(storage.written.length, 0);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("promoteMemory reports an honest 'no active scope profile' reason for a non-project target without a profile (issue #1658 thread 2)", async () => {
|
|
610
|
+
const config = makeConfig();
|
|
611
|
+
const storage = makeMemoryStorageProvider();
|
|
612
|
+
const result = await promoteMemory({
|
|
613
|
+
config,
|
|
614
|
+
sourceMemoryId: "mem-1",
|
|
615
|
+
sourceNamespace: "default",
|
|
616
|
+
principal: "alice",
|
|
617
|
+
targets: [{ kind: "serverShared" }],
|
|
618
|
+
reason: "promote shared fact",
|
|
619
|
+
actor: "alice",
|
|
620
|
+
storage,
|
|
621
|
+
});
|
|
622
|
+
assert.equal(result.ok, false);
|
|
623
|
+
const target = result.targets[0]!;
|
|
624
|
+
assert.match(target.reason, /no active scope profile/);
|
|
625
|
+
// Non-project targets must NOT mention coding context.
|
|
626
|
+
assert.doesNotMatch(target.reason, /coding context/i);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test("promoteMemory surfaces the profile layer's 'missing project context' reason when a project target is configured but unresolvable (issue #1658 thread 2)", async () => {
|
|
630
|
+
const config = makeConfig({
|
|
631
|
+
defaultScopeProfile: "team",
|
|
632
|
+
scopeProfiles: {
|
|
633
|
+
team: {
|
|
634
|
+
readOrder: ["userGlobal"],
|
|
635
|
+
writeDefault: "userGlobal",
|
|
636
|
+
promotionTargets: ["userProject"],
|
|
637
|
+
autoPromote: { enabled: false, targets: [], categories: ["fact"], minConfidenceTier: "speculative" },
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
principalFromSessionKeyMode: "prefix",
|
|
641
|
+
principalFromSessionKeyRules: [{ match: "alice:", principal: "alice" }],
|
|
642
|
+
namespacePolicies: [{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] }],
|
|
643
|
+
});
|
|
644
|
+
// No coding context → the userProject layer resolves "missing project context".
|
|
645
|
+
const scopeProfilePlan = resolveScopeProfilePlan({
|
|
646
|
+
config,
|
|
647
|
+
principal: "alice",
|
|
648
|
+
codingContext: null,
|
|
649
|
+
codingOverlay: null,
|
|
650
|
+
});
|
|
651
|
+
assert.ok(scopeProfilePlan, "scope profile must be active for this config");
|
|
652
|
+
const storage = makeMemoryStorageProvider();
|
|
653
|
+
const result = await promoteMemory({
|
|
654
|
+
config,
|
|
655
|
+
sourceMemoryId: "mem-1",
|
|
656
|
+
sourceNamespace: "alice",
|
|
657
|
+
principal: "alice",
|
|
658
|
+
targets: [{ kind: "userProject" }],
|
|
659
|
+
reason: "promote project fact",
|
|
660
|
+
actor: "alice",
|
|
661
|
+
storage,
|
|
662
|
+
scopeProfilePlan,
|
|
663
|
+
});
|
|
664
|
+
assert.equal(result.ok, false);
|
|
665
|
+
const target = result.targets[0]!;
|
|
666
|
+
assert.equal(target.authorized, false);
|
|
667
|
+
assert.match(target.reason, /missing project context/i);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// inspectScope — read-only override honest warning (issue #1658 thread 6)
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
|
|
674
|
+
test("inspectScope warns when an explicit override is readable but not writable (issue #1658 thread 6)", () => {
|
|
675
|
+
const config = makeConfig({
|
|
676
|
+
principalFromSessionKeyMode: "prefix",
|
|
677
|
+
principalFromSessionKeyRules: [{ match: "alice:", principal: "alice" }],
|
|
678
|
+
namespacePolicies: [
|
|
679
|
+
// alice can READ "audit-log" but cannot WRITE it.
|
|
680
|
+
{ name: "audit-log", readPrincipals: ["alice", "bob"], writePrincipals: ["bob"] },
|
|
681
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
682
|
+
],
|
|
683
|
+
});
|
|
684
|
+
const inspection = inspectScope({
|
|
685
|
+
config,
|
|
686
|
+
sessionKey: "alice:bot:x:1",
|
|
687
|
+
namespace: "audit-log",
|
|
688
|
+
});
|
|
689
|
+
// The override is readable → it becomes the effective write namespace...
|
|
690
|
+
assert.equal(inspection.namespaceOverride, "audit-log");
|
|
691
|
+
assert.equal(inspection.writeNamespace, "audit-log");
|
|
692
|
+
// ...but alice cannot write it → an honest warning must surface the mismatch.
|
|
693
|
+
assert.ok(
|
|
694
|
+
inspection.warnings.some(
|
|
695
|
+
(w) => w.includes("audit-log") && w.includes("not writable") && w.includes("writes will be rejected"),
|
|
696
|
+
),
|
|
697
|
+
"expected a read-only-override warning; got: " + JSON.stringify(inspection.warnings),
|
|
698
|
+
);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test("inspectScope does not warn when an explicit override is writable (issue #1658 thread 6)", () => {
|
|
702
|
+
const config = makeConfig({
|
|
703
|
+
principalFromSessionKeyMode: "prefix",
|
|
704
|
+
principalFromSessionKeyRules: [{ match: "alice:", principal: "alice" }],
|
|
705
|
+
namespacePolicies: [
|
|
706
|
+
{ name: "shared", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
707
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
708
|
+
],
|
|
709
|
+
});
|
|
710
|
+
const inspection = inspectScope({
|
|
711
|
+
config,
|
|
712
|
+
sessionKey: "alice:bot:x:1",
|
|
713
|
+
namespace: "shared",
|
|
714
|
+
});
|
|
715
|
+
assert.equal(inspection.namespaceOverride, "shared");
|
|
716
|
+
assert.ok(
|
|
717
|
+
!inspection.warnings.some((w) => w.includes("not writable")),
|
|
718
|
+
"no read-only-override warning expected for a writable override",
|
|
719
|
+
);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test("gatherMaintenanceHealth preserves the original qmdDegraded invariant for an unrecognized collectionState (issue #1658 thread 1)", async () => {
|
|
723
|
+
// The original degraded formula was: !available || "missing" || "unknown".
|
|
724
|
+
// An available backend with any OTHER collectionState (present/skipped/and any
|
|
725
|
+
// unrecognized value) was NOT degraded. The classified qmdState surfaces the
|
|
726
|
+
// unrecognized value honestly, but the degraded bit must stay false so the
|
|
727
|
+
// aggregate alerting invariant is byte-for-byte unchanged.
|
|
728
|
+
const memoryDir = await makeTempDir();
|
|
729
|
+
try {
|
|
730
|
+
const config = makeConfig({ memoryDir });
|
|
731
|
+
const catalog = new NamespaceCatalog(config);
|
|
732
|
+
await catalog.registerConfiguredNamespaces();
|
|
733
|
+
const report = await gatherMaintenanceHealth({
|
|
734
|
+
catalog,
|
|
735
|
+
qmdHealthProvider: async (ns) =>
|
|
736
|
+
qmdFixture(ns, { collectionState: "future-state" }),
|
|
737
|
+
});
|
|
738
|
+
const entry = report.perNamespace.find((e) => e.namespace === "default");
|
|
739
|
+
assert.ok(entry);
|
|
740
|
+
assert.equal(entry!.qmdDegraded, false, "unrecognized collectionState must not flip degraded (unchanged semantics)");
|
|
741
|
+
assert.equal(entry!.qmdState, "unknown");
|
|
742
|
+
assert.match(entry!.qmdStateReason, /unrecognized collection state 'future-state'/);
|
|
743
|
+
assert.equal(report.degradedMode, false);
|
|
744
|
+
} finally {
|
|
745
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
746
|
+
}
|
|
747
|
+
});
|
|
@@ -213,6 +213,17 @@ export function inspectScope(options: InspectScopeOptions): ScopeInspection {
|
|
|
213
213
|
`requested namespace override '${options.namespace}' is not readable by this principal and was ignored`
|
|
214
214
|
);
|
|
215
215
|
}
|
|
216
|
+
// Issue #1658 thread 6: when an explicit namespace override is readable but
|
|
217
|
+
// NOT writable, it still becomes the effective write namespace (baseNamespace
|
|
218
|
+
// in the scope plan). Surface the mismatch honestly so the operator knows
|
|
219
|
+
// writes will be rejected rather than reading writeNamespace as a usable
|
|
220
|
+
// target. (The per-layer writable flag already records this, but the
|
|
221
|
+
// top-level writeNamespace field does not.)
|
|
222
|
+
if (plan.namespaceOverride && !canWriteNamespace(plan.principal, plan.namespaceOverride, config)) {
|
|
223
|
+
warnings.push(
|
|
224
|
+
`explicit namespace override '${plan.namespaceOverride}' is readable but not writable by this principal; it is the effective write namespace but writes will be rejected`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
216
227
|
|
|
217
228
|
const layers: ScopeInspectionLayer[] = [];
|
|
218
229
|
const scopeProfilePlan = plan.scopeProfilePlan;
|
|
@@ -417,6 +428,27 @@ function toAdminEntry(record: NamespaceRecord, staleBefore: Date): AdminNamespac
|
|
|
417
428
|
// Maintenance and QMD health
|
|
418
429
|
// ---------------------------------------------------------------------------
|
|
419
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Honest, distinguishable classification of a namespace's QMD/search health
|
|
433
|
+
* (issue #1658 thread 1). A single `qmdDegraded` bit collapses the legitimate
|
|
434
|
+
* transient "unknown" (QMD reachable but collection status not yet populated,
|
|
435
|
+
* normal during startup/reindex) together with the actionable "missing"
|
|
436
|
+
* (collection not built — operator must act) and "unavailable" (backend down).
|
|
437
|
+
* Surfacing a classified state lets the dashboard render each case honestly
|
|
438
|
+
* instead of a flat degraded flag or a silent blank.
|
|
439
|
+
*/
|
|
440
|
+
export type AdminNamespaceQmdState =
|
|
441
|
+
/** Backend reachable and collection present (or intentionally skipped). */
|
|
442
|
+
| "healthy"
|
|
443
|
+
/** Backend reachable but collection status not yet determined (transient). */
|
|
444
|
+
| "unknown"
|
|
445
|
+
/** Backend reachable but the collection is not built for this namespace. */
|
|
446
|
+
| "missing"
|
|
447
|
+
/** Backend unreachable / probe threw. */
|
|
448
|
+
| "unavailable"
|
|
449
|
+
/** No QMD probe was run for this report (provider omitted or returned null). */
|
|
450
|
+
| "not_probed";
|
|
451
|
+
|
|
420
452
|
/** QMD/search health snapshot for one namespace (delegated to NamespaceSearchRouter). */
|
|
421
453
|
export interface AdminNamespaceQmdHealth {
|
|
422
454
|
namespace: string;
|
|
@@ -438,8 +470,20 @@ export interface AdminNamespaceHealth {
|
|
|
438
470
|
lastMaintenanceAt?: Record<string, string>;
|
|
439
471
|
/** True when no maintenance has ever been recorded. */
|
|
440
472
|
maintenanceMissing: boolean;
|
|
441
|
-
/**
|
|
473
|
+
/**
|
|
474
|
+
* True when QMD is unavailable OR its collection is missing/unknown for this
|
|
475
|
+
* namespace. Matches the NamespaceSearchRouter health path so the aggregate
|
|
476
|
+
* `degradedMode` flag stays consistent with runtime alerting.
|
|
477
|
+
*/
|
|
442
478
|
qmdDegraded: boolean;
|
|
479
|
+
/**
|
|
480
|
+
* Honest, distinguishable QMD state (issue #1658 thread 1). Complements
|
|
481
|
+
* `qmdDegraded` so the dashboard can render "unknown" (transient) separately
|
|
482
|
+
* from "missing"/"unavailable" (action needed) instead of a flat bit.
|
|
483
|
+
*/
|
|
484
|
+
qmdState: AdminNamespaceQmdState;
|
|
485
|
+
/** Human-readable explanation of {@link qmdState}, safe to show operators. */
|
|
486
|
+
qmdStateReason: string;
|
|
443
487
|
qmd?: AdminNamespaceQmdHealth;
|
|
444
488
|
/** Reason the QMD probe failed, when it did. */
|
|
445
489
|
qmdError?: string;
|
|
@@ -473,6 +517,66 @@ export interface MaintenanceHealthOptions {
|
|
|
473
517
|
* timestamps from the catalog and (optionally) QMD diagnostics from the
|
|
474
518
|
* injected provider.
|
|
475
519
|
*/
|
|
520
|
+
/**
|
|
521
|
+
* Classify a QMD health snapshot into an honest, distinguishable state bucket
|
|
522
|
+
* (issue #1658 thread 1). The `degraded` bit mirrors the NamespaceSearchRouter
|
|
523
|
+
* health path (unavailable / missing / unknown) so the aggregate `degradedMode`
|
|
524
|
+
* flag and any downstream alerting stay consistent; the classified `state` +
|
|
525
|
+
* `reason` let the dashboard render each case distinctly instead of a flat bit.
|
|
526
|
+
*/
|
|
527
|
+
function classifyQmdState(qmd: AdminNamespaceQmdHealth): {
|
|
528
|
+
state: AdminNamespaceQmdState;
|
|
529
|
+
reason: string;
|
|
530
|
+
degraded: boolean;
|
|
531
|
+
} {
|
|
532
|
+
if (!qmd.available) {
|
|
533
|
+
return {
|
|
534
|
+
state: "unavailable",
|
|
535
|
+
reason: "QMD backend is unavailable for this namespace",
|
|
536
|
+
degraded: true,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
// collectionState is the router's string ("present" | "missing" | "unknown"
|
|
540
|
+
// | "skipped"); classify defensively — an unrecognized value is reported as
|
|
541
|
+
// "unknown" rather than silently treated as healthy.
|
|
542
|
+
switch (qmd.collectionState) {
|
|
543
|
+
case "present":
|
|
544
|
+
return { state: "healthy", reason: "QMD collection is ready", degraded: false };
|
|
545
|
+
case "skipped":
|
|
546
|
+
// "skipped" means QMD is intentionally disabled for this namespace; it is
|
|
547
|
+
// neither degraded nor a blank — surface it honestly as healthy/idle.
|
|
548
|
+
return {
|
|
549
|
+
state: "healthy",
|
|
550
|
+
reason: "QMD is skipped for this namespace (intentionally disabled)",
|
|
551
|
+
degraded: false,
|
|
552
|
+
};
|
|
553
|
+
case "missing":
|
|
554
|
+
return {
|
|
555
|
+
state: "missing",
|
|
556
|
+
reason: "QMD collection is not built for this namespace — add the collection to the QMD index",
|
|
557
|
+
degraded: true,
|
|
558
|
+
};
|
|
559
|
+
case "unknown":
|
|
560
|
+
return {
|
|
561
|
+
state: "unknown",
|
|
562
|
+
reason: "QMD collection status could not be determined (transient during startup/reindex)",
|
|
563
|
+
degraded: true,
|
|
564
|
+
};
|
|
565
|
+
default:
|
|
566
|
+
// Preserve the ORIGINAL qmdDegraded invariant exactly: an available
|
|
567
|
+
// backend with an unrecognized collectionState was NOT degraded before
|
|
568
|
+
// this PR (the pre-image only flagged missing/unknown/unavailable). We
|
|
569
|
+
// still surface the unrecognized value HONESTLY via state/reason so the
|
|
570
|
+
// dashboard can render it, but we do not flip the degraded bit — that
|
|
571
|
+
// would change alerting and contradict the "unchanged semantics" claim.
|
|
572
|
+
return {
|
|
573
|
+
state: "unknown",
|
|
574
|
+
reason: `QMD reported an unrecognized collection state '${qmd.collectionState}'`,
|
|
575
|
+
degraded: false,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
476
580
|
export async function gatherMaintenanceHealth(options: MaintenanceHealthOptions): Promise<MaintenanceHealthReport> {
|
|
477
581
|
const { catalog, qmdHealthProvider } = options;
|
|
478
582
|
if (!catalog.enabled) {
|
|
@@ -488,22 +592,32 @@ export async function gatherMaintenanceHealth(options: MaintenanceHealthOptions)
|
|
|
488
592
|
lastMaintenanceAt: record.lastMaintenanceAt,
|
|
489
593
|
maintenanceMissing: !record.lastMaintenanceAt || Object.keys(record.lastMaintenanceAt).length === 0,
|
|
490
594
|
qmdDegraded: false,
|
|
595
|
+
qmdState: "not_probed",
|
|
596
|
+
qmdStateReason: "QMD health probe not run for this report",
|
|
491
597
|
};
|
|
492
598
|
if (qmdHealthProvider) {
|
|
493
599
|
try {
|
|
494
600
|
const qmd = await qmdHealthProvider(record.namespace);
|
|
495
601
|
if (qmd) {
|
|
496
602
|
entry.qmd = qmd;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
603
|
+
// Classify the collection state into an honest, distinguishable
|
|
604
|
+
// bucket (issue #1658 thread 1) and derive the degraded bit from
|
|
605
|
+
// the same inputs the NamespaceSearchRouter health path uses.
|
|
606
|
+
const classified = classifyQmdState(qmd);
|
|
607
|
+
entry.qmdState = classified.state;
|
|
608
|
+
entry.qmdStateReason = classified.reason;
|
|
609
|
+
entry.qmdDegraded = classified.degraded;
|
|
610
|
+
} else {
|
|
611
|
+
entry.qmdState = "not_probed";
|
|
612
|
+
entry.qmdStateReason = "QMD health not available for this namespace";
|
|
501
613
|
}
|
|
502
614
|
} catch (err) {
|
|
503
|
-
entry.qmdDegraded = true;
|
|
504
615
|
// Sanitize: never echo raw error messages to the dashboard. The
|
|
505
616
|
// generic diagnostic is sufficient for operators; the full error
|
|
506
617
|
// is logged by the qmdHealthProvider's caller (access-service).
|
|
618
|
+
entry.qmdDegraded = true;
|
|
619
|
+
entry.qmdState = "unavailable";
|
|
620
|
+
entry.qmdStateReason = "QMD health probe failed";
|
|
507
621
|
entry.qmdError = "QMD health probe failed";
|
|
508
622
|
}
|
|
509
623
|
}
|
|
@@ -735,7 +849,24 @@ export async function promoteMemory(options: PromoteMemoryOptions): Promise<Memo
|
|
|
735
849
|
: "explicit namespace is not writable by this principal",
|
|
736
850
|
};
|
|
737
851
|
}
|
|
738
|
-
|
|
852
|
+
if (!scopeProfilePlan) {
|
|
853
|
+
// Issue #1658 thread 2: distinguish "no active scope profile" from
|
|
854
|
+
// "profile does not configure this target". A project-scoped target
|
|
855
|
+
// (userProject/teamProject) cannot resolve without an active profile +
|
|
856
|
+
// coding context; report that honestly instead of a generic "not
|
|
857
|
+
// configured" message that implies a profile is active.
|
|
858
|
+
return {
|
|
859
|
+
target: requested.kind,
|
|
860
|
+
namespace: "",
|
|
861
|
+
authorized: false,
|
|
862
|
+
promoted: false,
|
|
863
|
+
reason:
|
|
864
|
+
requested.kind === "userProject" || requested.kind === "teamProject"
|
|
865
|
+
? `no active scope profile; '${requested.kind}' promotion requires an active scope profile with a coding context (pass a sessionKey)`
|
|
866
|
+
: `no active scope profile; '${requested.kind}' promotion requires an active scope profile`,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
const layer = scopeProfilePlan.promotionTargets.find((t) => t.target === requested.kind);
|
|
739
870
|
if (!layer) {
|
|
740
871
|
return {
|
|
741
872
|
target: requested.kind,
|