@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
|
@@ -167,3 +167,297 @@ test("#1671: re-extraction with resolved invalidAt backfills an old promoted cop
|
|
|
167
167
|
await cleanup();
|
|
168
168
|
}
|
|
169
169
|
});
|
|
170
|
+
|
|
171
|
+
// ─── #1707 widenings (promoted-cascade, valid_at, non-fact guard) ────────
|
|
172
|
+
|
|
173
|
+
// Thread 2 — valid_at propagation. A re-extracted duplicate whose event time
|
|
174
|
+
// yields only a start bound ("since 2024", "yesterday") must get the corrected
|
|
175
|
+
// per-fact validFrom onto the existing copy so as-of recall uses the corrected
|
|
176
|
+
// start. The orchestrator helper's rule: patch valid_at when the incoming
|
|
177
|
+
// validFrom is EXTRACTED and the copy's start bound is batch-anchored
|
|
178
|
+
// (assumed/legacy); never clobber a copy that already carries an extracted
|
|
179
|
+
// per-fact anchor. This test pins that rule against the storage patch the
|
|
180
|
+
// helper issues.
|
|
181
|
+
test("#1707 thread 2: corrected extracted validFrom overwrites a batch-anchored valid_at", async () => {
|
|
182
|
+
const { storage, cleanup } = await makeStorage("bitemporal-1707-validFrom-");
|
|
183
|
+
try {
|
|
184
|
+
// Old promoted copy: batch-anchored valid_at, no per-fact event-time.
|
|
185
|
+
const content = "We have used Stripe for payments since 2024.";
|
|
186
|
+
const batchAnchor = "2026-06-01T00:00:00.000Z";
|
|
187
|
+
const id = await storage.writeMemory("fact", content, {
|
|
188
|
+
confidence: 0.9,
|
|
189
|
+
validAt: batchAnchor,
|
|
190
|
+
// No eventTimeSource → batch-anchored (legacy pre-#1670 copy).
|
|
191
|
+
});
|
|
192
|
+
const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
193
|
+
assert.ok(before);
|
|
194
|
+
assert.equal(before!.frontmatter.valid_at, batchAnchor);
|
|
195
|
+
assert.equal(before!.frontmatter.eventTimeSource, undefined);
|
|
196
|
+
|
|
197
|
+
// Simulate the helper's rule: incoming validFrom is extracted → patch
|
|
198
|
+
// because the copy is NOT extracted-anchored (fm.eventTimeSource !== "extracted").
|
|
199
|
+
const correctedValidFrom = "2024-01-01T00:00:00.000Z";
|
|
200
|
+
const incomingExtracted = true;
|
|
201
|
+
const copyExtracted = before!.frontmatter.eventTimeSource === "extracted";
|
|
202
|
+
const shouldPatchValidAt =
|
|
203
|
+
incomingExtracted && (copyExtracted === false || !before!.frontmatter.valid_at);
|
|
204
|
+
assert.equal(shouldPatchValidAt, true, "must correct the batch-anchored valid_at");
|
|
205
|
+
|
|
206
|
+
const ok = await storage.writeMemoryFrontmatter(before!, {
|
|
207
|
+
valid_at: correctedValidFrom,
|
|
208
|
+
eventTimeSource: "extracted",
|
|
209
|
+
observedAt: "2026-06-20T00:00:00.000Z",
|
|
210
|
+
});
|
|
211
|
+
assert.equal(ok, true);
|
|
212
|
+
|
|
213
|
+
const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
214
|
+
assert.ok(after);
|
|
215
|
+
assert.equal(after!.frontmatter.valid_at, correctedValidFrom);
|
|
216
|
+
assert.equal(after!.frontmatter.eventTimeSource, "extracted");
|
|
217
|
+
} finally {
|
|
218
|
+
await cleanup();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("#1707 thread 2: identical extracted validFrom is a no-op (equality short-circuit)", async () => {
|
|
223
|
+
const { storage, cleanup } = await makeStorage("bitemporal-1707-validFrom-equal-");
|
|
224
|
+
try {
|
|
225
|
+
// Copy already carries an extracted per-fact anchor.
|
|
226
|
+
const content = "The migration to MySQL completed in March 2025.";
|
|
227
|
+
const existingValidAt: string = "2025-03-01T00:00:00.000Z";
|
|
228
|
+
const id = await storage.writeMemory("fact", content, {
|
|
229
|
+
confidence: 0.9,
|
|
230
|
+
validAt: existingValidAt,
|
|
231
|
+
observedAt: "2025-06-01T00:00:00.000Z",
|
|
232
|
+
eventTimeSource: "extracted",
|
|
233
|
+
});
|
|
234
|
+
const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
235
|
+
assert.ok(before);
|
|
236
|
+
|
|
237
|
+
// Re-extraction resolves the SAME start bound (deterministic per #1670).
|
|
238
|
+
// The helper's equality short-circuit (fm.valid_at !== bounds.validFrom)
|
|
239
|
+
// skips the redundant write — the only no-clobber that holds without a
|
|
240
|
+
// fragile provenance heuristic (review codex PRRT_Ov7LKC).
|
|
241
|
+
const helperWouldPatch = existingValidAt !== existingValidAt; // equal → false
|
|
242
|
+
assert.equal(helperWouldPatch, false, "identical validFrom must not rewrite");
|
|
243
|
+
|
|
244
|
+
// No patch issued → valid_at unchanged.
|
|
245
|
+
const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
246
|
+
assert.ok(after);
|
|
247
|
+
assert.equal(after!.frontmatter.valid_at, existingValidAt);
|
|
248
|
+
} finally {
|
|
249
|
+
await cleanup();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("#1707 thread 2: differing extracted validFrom overwrites (authoritative re-evaluation)", async () => {
|
|
254
|
+
const { storage, cleanup } = await makeStorage("bitemporal-1707-validFrom-differ-");
|
|
255
|
+
try {
|
|
256
|
+
// Copy carries an older (batch-anchored) valid_at.
|
|
257
|
+
const content = "We have used Stripe for payments since 2024.";
|
|
258
|
+
const staleValidAt: string = "2026-06-01T00:00:00.000Z"; // batch anchor
|
|
259
|
+
const id = await storage.writeMemory("fact", content, {
|
|
260
|
+
confidence: 0.9,
|
|
261
|
+
validAt: staleValidAt,
|
|
262
|
+
});
|
|
263
|
+
const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
264
|
+
assert.ok(before);
|
|
265
|
+
|
|
266
|
+
// Re-extraction resolves a DIFFERING extracted start bound. Exact-content
|
|
267
|
+
// dedup + #1670 determinism means stable content re-resolves to the same
|
|
268
|
+
// value (equality no-op above); a divergence means the copy's start is
|
|
269
|
+
// stale (batch/assumed) and the extracted validFrom is authoritative.
|
|
270
|
+
const correctedValidFrom: string = "2024-01-01T00:00:00.000Z";
|
|
271
|
+
const helperWouldPatch = staleValidAt !== correctedValidFrom; // differ → true
|
|
272
|
+
assert.equal(helperWouldPatch, true, "differing extracted validFrom must overwrite");
|
|
273
|
+
|
|
274
|
+
const ok = await storage.writeMemoryFrontmatter(before!, {
|
|
275
|
+
valid_at: correctedValidFrom,
|
|
276
|
+
eventTimeSource: "extracted",
|
|
277
|
+
});
|
|
278
|
+
assert.equal(ok, true);
|
|
279
|
+
|
|
280
|
+
const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
281
|
+
assert.ok(after);
|
|
282
|
+
assert.equal(after!.frontmatter.valid_at, correctedValidFrom);
|
|
283
|
+
assert.equal(after!.frontmatter.eventTimeSource, "extracted");
|
|
284
|
+
} finally {
|
|
285
|
+
await cleanup();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Review (codex PRRT_Ov68oA + PRRT_Ov7LKC): an END-only extraction ("through
|
|
290
|
+
// 2026") sets eventTimeSource = "extracted" (end resolved) but leaves valid_at
|
|
291
|
+
// batch/assumed-anchored. A later START-bound extraction must still correct
|
|
292
|
+
// valid_at. The helper now uses an equality short-circuit (no fragile
|
|
293
|
+
// provenance heuristic): the differing extracted validFrom overwrites the
|
|
294
|
+
// stale start regardless of the copy's eventTimeSource.
|
|
295
|
+
test("#1707 thread 2: end-only extracted provenance does NOT block a later start-bound correction", async () => {
|
|
296
|
+
const { storage, cleanup } = await makeStorage("bitemporal-1707-endonly-");
|
|
297
|
+
try {
|
|
298
|
+
// Copy state after an end-only backfill: eventTimeSource = "extracted"
|
|
299
|
+
// (the end "through 2026" resolved), but valid_at is still the ingestion
|
|
300
|
+
// anchor (assumed start).
|
|
301
|
+
const observedAt = "2026-06-01T00:00:00.000Z";
|
|
302
|
+
const content = "The service was maintained through 2026.";
|
|
303
|
+
const id = await storage.writeMemory("fact", content, {
|
|
304
|
+
confidence: 0.9,
|
|
305
|
+
validAt: observedAt, // valid_at === observedAt (assumed/batch start)
|
|
306
|
+
observedAt,
|
|
307
|
+
eventTimeSource: "extracted", // set by the end-only extraction
|
|
308
|
+
invalidAt: "2026-12-31T00:00:00.000Z", // the extracted end bound
|
|
309
|
+
});
|
|
310
|
+
const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
311
|
+
assert.ok(before);
|
|
312
|
+
assert.equal(before!.frontmatter.eventTimeSource, "extracted");
|
|
313
|
+
assert.equal(before!.frontmatter.valid_at, before!.frontmatter.observedAt);
|
|
314
|
+
|
|
315
|
+
// The helper's valid_at gate (mirrors orchestrator.ts):
|
|
316
|
+
// bounds.validFrom && eventTimeSource === "extracted" && valid_at !== validFrom
|
|
317
|
+
// The stale valid_at differs from the incoming real start → overwrite.
|
|
318
|
+
|
|
319
|
+
// A later re-extraction with a real extracted start must correct valid_at.
|
|
320
|
+
const realStart = "2024-03-01T00:00:00.000Z";
|
|
321
|
+
const ok = await storage.writeMemoryFrontmatter(before!, {
|
|
322
|
+
valid_at: realStart,
|
|
323
|
+
});
|
|
324
|
+
assert.equal(ok, true);
|
|
325
|
+
|
|
326
|
+
const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
327
|
+
assert.ok(after);
|
|
328
|
+
assert.equal(after!.frontmatter.valid_at, realStart);
|
|
329
|
+
} finally {
|
|
330
|
+
await cleanup();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Gate guard (review cursor PRRT_OvHk / codex PRRT_OvHxVH): resolveFactEventTime
|
|
335
|
+
// sets a validFrom for EVERY fact (assumed = ingestion anchor). The helper's
|
|
336
|
+
// I/O gate must NOT scan on an assumed validFrom — only an EXTRACTED start bound
|
|
337
|
+
// changes recall. This pins the gate decision so ordinary duplicate facts
|
|
338
|
+
// without an extracted event time do not pay a readAllMemories scan on every
|
|
339
|
+
// dedup hit.
|
|
340
|
+
test("#1707 thread 2: assumed (batch-anchored) validFrom does NOT trigger a backfill scan", () => {
|
|
341
|
+
// Mirror the helper's I/O gate (orchestrator.ts backfillTemporalBoundsOnDedupHit):
|
|
342
|
+
// const hasExtractedStart = bounds.validFrom !== undefined && bounds.eventTimeSource === "extracted";
|
|
343
|
+
// if (!bounds.invalidAt && !hasExtractedStart) return;
|
|
344
|
+
const gateWouldScan = (bounds: {
|
|
345
|
+
invalidAt?: string;
|
|
346
|
+
validFrom?: string;
|
|
347
|
+
eventTimeSource?: "extracted" | "assumed";
|
|
348
|
+
}) =>
|
|
349
|
+
Boolean(bounds.invalidAt) ||
|
|
350
|
+
(bounds.validFrom !== undefined && bounds.eventTimeSource === "extracted");
|
|
351
|
+
|
|
352
|
+
// Assumed validFrom alone (the common no-event-time case) → no scan.
|
|
353
|
+
assert.equal(
|
|
354
|
+
gateWouldScan({ validFrom: "2026-06-01T00:00:00.000Z", eventTimeSource: "assumed" }),
|
|
355
|
+
false,
|
|
356
|
+
"assumed validFrom must not trigger a scan",
|
|
357
|
+
);
|
|
358
|
+
// Extracted validFrom → scan (corrected start bound is recall-relevant).
|
|
359
|
+
assert.equal(
|
|
360
|
+
gateWouldScan({ validFrom: "2024-01-01T00:00:00.000Z", eventTimeSource: "extracted" }),
|
|
361
|
+
true,
|
|
362
|
+
);
|
|
363
|
+
// End bound alone → scan (expires the fact).
|
|
364
|
+
assert.equal(gateWouldScan({ invalidAt: "2025-06-01T00:00:00.000Z" }), true);
|
|
365
|
+
// No bounds at all → no scan.
|
|
366
|
+
assert.equal(gateWouldScan({}), false);
|
|
367
|
+
});
|
|
368
|
+
// Thread 1 — promoted-cascade backfill. When the source-namespace dedup
|
|
369
|
+
// short-circuit fires, the helper must also patch promotion-target copies
|
|
370
|
+
// (profile + shared namespaces) so cross-namespace recall does not surface an
|
|
371
|
+
// expired fact. This proves the mechanism the orchestrator's promotion-cascade
|
|
372
|
+
// closure relies on: backfilling two separate storages patches each
|
|
373
|
+
// independently.
|
|
374
|
+
test("#1707 thread 1: backfill patches promoted copies across multiple storages (cascade)", async () => {
|
|
375
|
+
const { storage: sourceStorage, cleanup: cleanupSource } = await makeStorage(
|
|
376
|
+
"bitemporal-1707-cascade-source-",
|
|
377
|
+
);
|
|
378
|
+
const { storage: promotedStorage, cleanup: cleanupPromoted } = await makeStorage(
|
|
379
|
+
"bitemporal-1707-cascade-promoted-",
|
|
380
|
+
);
|
|
381
|
+
try {
|
|
382
|
+
const content = "The API rate limit is 100 req/min until June 2025.";
|
|
383
|
+
// Both namespaces hold the same fact, written WITHOUT bounds (stale copies).
|
|
384
|
+
const sourceId = await sourceStorage.writeMemory("fact", content, { confidence: 0.9 });
|
|
385
|
+
const promotedId = await promotedStorage.writeMemory("fact", content, { confidence: 0.9 });
|
|
386
|
+
|
|
387
|
+
// Simulate re-extraction that now carries a resolved invalidAt. The
|
|
388
|
+
// promotion-cascade closure calls the same backfill against each target.
|
|
389
|
+
const bounds = {
|
|
390
|
+
invalid_at: "2025-06-01T00:00:00.000Z",
|
|
391
|
+
observedAt: "2025-06-20T00:00:00.000Z",
|
|
392
|
+
eventTimeSource: "extracted" as const,
|
|
393
|
+
};
|
|
394
|
+
for (const storage of [sourceStorage, promotedStorage]) {
|
|
395
|
+
const all = await storage.readAllMemories();
|
|
396
|
+
const existing = all.find(
|
|
397
|
+
(m) =>
|
|
398
|
+
m.frontmatter.category === "fact" &&
|
|
399
|
+
(m.frontmatter.status ?? "active") === "active" &&
|
|
400
|
+
!m.frontmatter.invalid_at &&
|
|
401
|
+
ContentHashIndex.normalizeContent(m.content ?? "") ===
|
|
402
|
+
ContentHashIndex.normalizeContent(content),
|
|
403
|
+
);
|
|
404
|
+
assert.ok(existing, "must find the existing copy in each storage");
|
|
405
|
+
const ok = await storage.writeMemoryFrontmatter(existing!, bounds);
|
|
406
|
+
assert.equal(ok, true);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Both copies now carry the resolved end bound — cross-namespace recall
|
|
410
|
+
// honours the expiry instead of surfacing the stale promoted copy.
|
|
411
|
+
const sourceAfter = (await sourceStorage.readAllMemories()).find(
|
|
412
|
+
(m) => m.frontmatter.id === sourceId,
|
|
413
|
+
);
|
|
414
|
+
const promotedAfter = (await promotedStorage.readAllMemories()).find(
|
|
415
|
+
(m) => m.frontmatter.id === promotedId,
|
|
416
|
+
);
|
|
417
|
+
assert.ok(sourceAfter && promotedAfter);
|
|
418
|
+
assert.equal(sourceAfter!.frontmatter.invalid_at, "2025-06-01T00:00:00.000Z");
|
|
419
|
+
assert.equal(promotedAfter!.frontmatter.invalid_at, "2025-06-01T00:00:00.000Z");
|
|
420
|
+
} finally {
|
|
421
|
+
await cleanupSource();
|
|
422
|
+
await cleanupPromoted();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Thread 3 — non-fact guard. The source dedup backfill call site is gated on
|
|
427
|
+
// writeCategory === "fact", so a non-fact duplicate (preference/decision/
|
|
428
|
+
// procedure) never reaches the fact-only backfill scan. This proves the
|
|
429
|
+
// helper's category guard that makes that call-site gate correct: a non-fact
|
|
430
|
+
// copy is never matched/patched even if its content hash collides.
|
|
431
|
+
test("#1707 thread 3: backfill never patches a non-fact copy (category guard)", async () => {
|
|
432
|
+
const { storage, cleanup } = await makeStorage("bitemporal-1707-nonfact-");
|
|
433
|
+
try {
|
|
434
|
+
const content = "The user prefers dark mode.";
|
|
435
|
+
// A preference (non-fact) whose content would collide with an incoming
|
|
436
|
+
// duplicate candidate.
|
|
437
|
+
const id = await storage.writeMemory("preference", content, { confidence: 0.9 });
|
|
438
|
+
const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
439
|
+
assert.ok(before);
|
|
440
|
+
assert.equal(before!.frontmatter.category, "preference");
|
|
441
|
+
assert.equal(before!.frontmatter.invalid_at, undefined);
|
|
442
|
+
|
|
443
|
+
// The helper's lookup filters category === "fact" — a preference copy is
|
|
444
|
+
// never eligible, so no patch is issued even when bounds are present.
|
|
445
|
+
const all = await storage.readAllMemories();
|
|
446
|
+
const existing = all.find(
|
|
447
|
+
(m) =>
|
|
448
|
+
m.frontmatter.category === "fact" && // ← the guard under test
|
|
449
|
+
(m.frontmatter.status ?? "active") === "active" &&
|
|
450
|
+
ContentHashIndex.normalizeContent(m.content ?? "") ===
|
|
451
|
+
ContentHashIndex.normalizeContent(content),
|
|
452
|
+
);
|
|
453
|
+
assert.equal(existing, undefined, "non-fact copy must not be matched by the fact-only lookup");
|
|
454
|
+
|
|
455
|
+
// No patch → the preference copy is untouched.
|
|
456
|
+
const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
|
|
457
|
+
assert.ok(after);
|
|
458
|
+
assert.equal(after!.frontmatter.invalid_at, undefined);
|
|
459
|
+
assert.equal(after!.frontmatter.category, "preference");
|
|
460
|
+
} finally {
|
|
461
|
+
await cleanup();
|
|
462
|
+
}
|
|
463
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { strict as assert } from "node:assert";
|
|
10
|
-
import { mkdir, rm, readFile } from "node:fs/promises";
|
|
10
|
+
import { mkdir, rm, readFile, utimes, unlink } from "node:fs/promises";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { test } from "node:test";
|
|
@@ -270,3 +270,68 @@ test("budget exhaustion summary LLM failure does not crash (Thread 16)", async (
|
|
|
270
270
|
assert.ok(result.reply.length > 0, "Should return a fallback reply");
|
|
271
271
|
assert.ok(result.skippedTools !== undefined, "Should report skipped tools");
|
|
272
272
|
});
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// TTL boundary (issue #1685 item 1 / #1687 Thread 21)
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
test("cleanupExpiredChatSessions honors TTL boundary [0, ttlHours) — age >= ttl expires", async () => {
|
|
279
|
+
const dir = await makeTempDir();
|
|
280
|
+
const ttlHours = 2;
|
|
281
|
+
const ttlMs = ttlHours * 3600 * 1000;
|
|
282
|
+
|
|
283
|
+
// Session aged past the TTL (mtime set to ttlHours ago; by the time the
|
|
284
|
+
// sweep stat()s it a few ms have elapsed, so age >= ttl deterministically
|
|
285
|
+
// — the boundary is inclusive: age ∈ [0, ttl) survives, age >= ttl expires).
|
|
286
|
+
const expired = await createChatSession(dir, { principal: "expired" });
|
|
287
|
+
const expiredFile = chatSessionFile(dir, expired.id);
|
|
288
|
+
const atTtlSeconds = (Date.now() - ttlMs) / 1000;
|
|
289
|
+
await utimes(expiredFile, atTtlSeconds, atTtlSeconds);
|
|
290
|
+
|
|
291
|
+
// Session aged well under the TTL → must survive.
|
|
292
|
+
const fresh = await createChatSession(dir, { principal: "fresh" });
|
|
293
|
+
const freshFile = chatSessionFile(dir, fresh.id);
|
|
294
|
+
const underTtlSeconds = (Date.now() - (ttlMs - 6 * 60 * 1000)) / 1000; // ~1.9h old
|
|
295
|
+
await utimes(freshFile, underTtlSeconds, underTtlSeconds);
|
|
296
|
+
|
|
297
|
+
const removed = await cleanupExpiredChatSessions(dir, ttlHours);
|
|
298
|
+
assert.equal(removed, 1, "only the expired session should be removed");
|
|
299
|
+
|
|
300
|
+
// The fresh session file must still be on disk.
|
|
301
|
+
const freshStillExists = await readFile(freshFile, "utf8").then(() => true).catch(() => false);
|
|
302
|
+
assert.ok(freshStillExists, "session aged under the TTL must survive the sweep");
|
|
303
|
+
|
|
304
|
+
// The expired session file must be gone.
|
|
305
|
+
const expiredGone = await readFile(expiredFile, "utf8").then(() => false).catch(() => true);
|
|
306
|
+
assert.ok(expiredGone, "session aged at/over the TTL must be swept");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
test("appendTranscriptEntry emits strictly-increasing seqs even within the same millisecond (issue #1687 review)", async () => {
|
|
311
|
+
const dir = await makeTempDir();
|
|
312
|
+
const session = await createChatSession(dir, { principal: "seq" });
|
|
313
|
+
// Append several entries as fast as possible (same-ms collisions would have
|
|
314
|
+
// shared a Date.now()-based seq before the monotonic counter fix).
|
|
315
|
+
const entries: number[] = [];
|
|
316
|
+
for (let i = 0; i < 8; i++) {
|
|
317
|
+
const e = await appendTranscriptEntry(dir, session.id, { role: "user", content: `m${i}` });
|
|
318
|
+
entries.push(e.seq);
|
|
319
|
+
}
|
|
320
|
+
// Every seq must be distinct and strictly increasing.
|
|
321
|
+
for (let i = 1; i < entries.length; i++) {
|
|
322
|
+
assert.ok(entries[i]! > entries[i - 1]!, `seq ${entries[i]} must exceed ${entries[i - 1]}`);
|
|
323
|
+
}
|
|
324
|
+
assert.equal(new Set(entries).size, entries.length, "seqs must be unique");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("appendTranscriptEntry refuses to recreate a swept session (chat_session_expired) — codex P2 #1687", async () => {
|
|
328
|
+
const dir = await makeTempDir();
|
|
329
|
+
const session = await createChatSession(dir, { principal: "alice" });
|
|
330
|
+
// Simulate the TTL sweep unlinking the file mid-turn (resurrection race).
|
|
331
|
+
await unlink(chatSessionFile(dir, session.id));
|
|
332
|
+
// Must throw instead of silently recreating a headerless (public) file.
|
|
333
|
+
await assert.rejects(
|
|
334
|
+
appendTranscriptEntry(dir, session.id, { role: "user", content: "x" }),
|
|
335
|
+
/chat_session_expired/,
|
|
336
|
+
);
|
|
337
|
+
});
|
|
@@ -17,7 +17,7 @@ import { test } from "node:test";
|
|
|
17
17
|
import type { EngramAccessService } from "../access-service.js";
|
|
18
18
|
import { parseConfig } from "../config.js";
|
|
19
19
|
import { handleChatMessage, handleChatEventsSSE } from "./chat-http.js";
|
|
20
|
-
import { createChatSession, appendTranscriptEntry, chatSessionFile } from "./chat-session.js";
|
|
20
|
+
import { createChatSession, loadChatSession, appendTranscriptEntry, chatSessionFile } from "./chat-session.js";
|
|
21
21
|
import { DEFAULT_CHAT_CONFIG } from "./chat-config.js";
|
|
22
22
|
|
|
23
23
|
// Minimal mock res capturing writes. All tracking fields live on one
|
|
@@ -195,3 +195,121 @@ test("HTTP chat/events SSE: 403 for a session owned by another principal", async
|
|
|
195
195
|
assert.equal(m.status, 403);
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
test("HTTP chat/events SSE: pushes new transcript entries live (issue #1687)", async () => {
|
|
201
|
+
await withTempDir(async (memoryDir) => {
|
|
202
|
+
const session = await createChatSession(memoryDir, { principal: "alice" });
|
|
203
|
+
await appendTranscriptEntry(memoryDir, session.id, { role: "user", content: "first" });
|
|
204
|
+
|
|
205
|
+
const service = makeService({
|
|
206
|
+
memoryDir,
|
|
207
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
208
|
+
});
|
|
209
|
+
const m = mockRes();
|
|
210
|
+
const req = mockReq();
|
|
211
|
+
await handleChatEventsSSE(req as never, m as never, session.id, { service, config: service.configRef!.chat, memoryDir }, "alice");
|
|
212
|
+
assert.equal(m.status, 200);
|
|
213
|
+
assert.equal(m.headers["content-type"], "text/event-stream");
|
|
214
|
+
// The stream must advertise a retry directive so reconnects are safe.
|
|
215
|
+
assert.ok(m.body.includes("retry: 5000"), "SSE must emit a retry: directive");
|
|
216
|
+
|
|
217
|
+
const initialFrames = m.body.split("\n\n").filter((f) => f.startsWith("data:"));
|
|
218
|
+
|
|
219
|
+
// Append a NEW entry AFTER the client connected — it must be pushed live
|
|
220
|
+
// to the open SSE connection via the per-session pub/sub.
|
|
221
|
+
await appendTranscriptEntry(memoryDir, session.id, { role: "assistant", content: "pushed-live" });
|
|
222
|
+
|
|
223
|
+
const allFrames = m.body.split("\n\n").filter((f) => f.startsWith("data:"));
|
|
224
|
+
assert.ok(allFrames.length > initialFrames.length, "expected a pushed frame after append");
|
|
225
|
+
assert.ok(m.body.includes("pushed-live"), "the appended entry must reach the live SSE stream");
|
|
226
|
+
// The pushed frame must be well-formed SSE (data: <json>\n\n).
|
|
227
|
+
assert.ok(m.body.includes("data: {"), "frames must be data: <json> framed");
|
|
228
|
+
req.emit("close");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("HTTP chat/message: concurrent identical requests are deduplicated (issue #1687)", async () => {
|
|
233
|
+
await withTempDir(async (memoryDir) => {
|
|
234
|
+
const session = await createChatSession(memoryDir, { principal: "alice" });
|
|
235
|
+
let llmCalls = 0;
|
|
236
|
+
const service = makeService({
|
|
237
|
+
memoryDir,
|
|
238
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
239
|
+
fallbackLlmRef: ({
|
|
240
|
+
chatCompletion: async () => {
|
|
241
|
+
llmCalls++;
|
|
242
|
+
// Simulate enough work that the second request lands while the
|
|
243
|
+
// first is still in-flight (the dedup window).
|
|
244
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
245
|
+
return { content: "shared-reply", modelUsed: "test-model" };
|
|
246
|
+
},
|
|
247
|
+
}) as never,
|
|
248
|
+
});
|
|
249
|
+
const opts = { service, config: service.configRef!.chat, memoryDir };
|
|
250
|
+
const m1 = mockRes();
|
|
251
|
+
const m2 = mockRes();
|
|
252
|
+
await Promise.all([
|
|
253
|
+
handleChatMessage(mockReq() as never, m1 as never, { message: "duplicate", chatSessionId: session.id }, opts, "alice"),
|
|
254
|
+
handleChatMessage(mockReq() as never, m2 as never, { message: "duplicate", chatSessionId: session.id }, opts, "alice"),
|
|
255
|
+
]);
|
|
256
|
+
// Exactly ONE LLM call despite two concurrent identical requests.
|
|
257
|
+
assert.equal(llmCalls, 1, "a concurrent duplicate must not re-process");
|
|
258
|
+
// Both callers receive the (same) reply.
|
|
259
|
+
assert.equal(m1.status, 200);
|
|
260
|
+
assert.equal(m2.status, 200);
|
|
261
|
+
assert.ok(m1.body.includes("shared-reply"));
|
|
262
|
+
assert.ok(m2.body.includes("shared-reply"));
|
|
263
|
+
// The transcript must contain exactly one user "duplicate" line — the
|
|
264
|
+
// duplicate did not double-append.
|
|
265
|
+
const loaded = await loadChatSession(memoryDir, session.id);
|
|
266
|
+
const userDups = loaded!.transcript.filter((e) => e.role === "user" && e.content === "duplicate");
|
|
267
|
+
assert.equal(userDups.length, 1, "duplicate request must not append a second user transcript line");
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("HTTP chat/message: distinct messages are NOT deduplicated (issue #1687)", async () => {
|
|
272
|
+
await withTempDir(async (memoryDir) => {
|
|
273
|
+
const session = await createChatSession(memoryDir, { principal: "alice" });
|
|
274
|
+
let llmCalls = 0;
|
|
275
|
+
const service = makeService({
|
|
276
|
+
memoryDir,
|
|
277
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
278
|
+
fallbackLlmRef: ({
|
|
279
|
+
chatCompletion: async () => {
|
|
280
|
+
llmCalls++;
|
|
281
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
282
|
+
return { content: "reply", modelUsed: "test-model" };
|
|
283
|
+
},
|
|
284
|
+
}) as never,
|
|
285
|
+
});
|
|
286
|
+
const opts = { service, config: service.configRef!.chat, memoryDir };
|
|
287
|
+
await Promise.all([
|
|
288
|
+
handleChatMessage(mockReq() as never, mockRes() as never, { message: "alpha", chatSessionId: session.id }, opts, "alice"),
|
|
289
|
+
handleChatMessage(mockReq() as never, mockRes() as never, { message: "beta", chatSessionId: session.id }, opts, "alice"),
|
|
290
|
+
]);
|
|
291
|
+
// Distinct messages both process independently.
|
|
292
|
+
assert.equal(llmCalls, 2, "distinct messages must not be coalesced");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("HTTP chat/events SSE: early disconnect during load bails cleanly — no write to ended response, no leak (issue #1687 review)", async () => {
|
|
297
|
+
await withTempDir(async (memoryDir) => {
|
|
298
|
+
const session = await createChatSession(memoryDir, { principal: "alice" });
|
|
299
|
+
const service = makeService({
|
|
300
|
+
memoryDir,
|
|
301
|
+
configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
|
|
302
|
+
});
|
|
303
|
+
const m = mockRes();
|
|
304
|
+
const req = mockReq();
|
|
305
|
+
// Emit close as a microtask so it fires while the handler is awaiting
|
|
306
|
+
// loadChatSession (before writeHead). The handler must bail out without
|
|
307
|
+
// writing to the ended response and without setting up a heartbeat.
|
|
308
|
+
queueMicrotask(() => req.emit("close"));
|
|
309
|
+
// Resolving without throwing is the core contract.
|
|
310
|
+
await handleChatEventsSSE(req as never, m as never, session.id, { service, config: service.configRef!.chat, memoryDir }, "alice");
|
|
311
|
+
// writeHead was skipped (status stays 0) because the handler returned at
|
|
312
|
+
// the `if (closed) return` guard before any SSE framing.
|
|
313
|
+
assert.equal(m.status, 0, "handler must not writeHead after an early close");
|
|
314
|
+
});
|
|
315
|
+
});
|