@remnic/core 10.0.48 → 10.0.50
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-admin-ops-surface.js +73 -70
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.js +74 -71
- package/dist/access-boundary.js +73 -70
- package/dist/access-cli.js +170 -167
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +73 -70
- package/dist/access-http-briefing.d.ts +2 -0
- package/dist/access-http-briefing.d.ts.map +1 -1
- package/dist/access-http-briefing.js +74 -71
- package/dist/access-http-query.js +75 -72
- package/dist/access-http.d.ts.map +1 -1
- package/dist/access-http.js +96 -93
- package/dist/access-identity-continuity-surface.js +73 -70
- package/dist/access-lcm-surface.js +73 -70
- package/dist/access-mcp.js +85 -82
- package/dist/access-namespace-preflight.js +73 -70
- package/dist/access-observe-write-category.d.ts +1 -1
- package/dist/access-observe-write-category.d.ts.map +1 -1
- package/dist/access-observe-write-category.js +15 -14
- package/dist/access-observe-write-surface.d.ts.map +1 -1
- package/dist/access-observe-write-surface.js +73 -70
- package/dist/access-offline-manifest.js +9 -8
- package/dist/access-operations-batch.js +76 -73
- package/dist/access-operations.js +83 -80
- package/dist/access-recall-concurrency.js +73 -70
- package/dist/access-recall-response.js +73 -70
- package/dist/access-recall-surface.js +73 -70
- package/dist/access-schema.js +11 -10
- package/dist/access-service.d.ts +5 -0
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +73 -70
- package/dist/access-surface-catalog.js +75 -72
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +32 -30
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-BNCBMU6H.js → auto-sync-UQQ6YJSB.js} +3 -3
- package/dist/briefing-deadline.d.ts +7 -0
- package/dist/briefing-deadline.d.ts.map +1 -0
- package/dist/briefing-deadline.js +13 -0
- package/dist/briefing-window.d.ts +12 -4
- package/dist/briefing-window.d.ts.map +1 -1
- package/dist/briefing-window.js +3 -1
- package/dist/briefing.d.ts +2 -0
- package/dist/briefing.d.ts.map +1 -1
- package/dist/briefing.js +22 -20
- package/dist/buffer.js +6 -6
- package/dist/calibration.js +2 -2
- package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/{capsule-merge-RNQUP2VL.js → capsule-merge-D4BTIJGK.js} +3 -3
- package/dist/capture-write-lock-scope.d.ts +7 -0
- package/dist/capture-write-lock-scope.d.ts.map +1 -0
- package/dist/capture-write-lock-scope.js +10 -0
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +23 -22
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +3 -3
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-FAKRTS3Y.js → chunk-2AKIVSHP.js} +2 -2
- package/dist/{chunk-EIYJK7OD.js → chunk-2M36PR7R.js} +2 -2
- package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-6P6MZDBL.js → chunk-3NL4ALUP.js} +2 -2
- package/dist/{chunk-6CC2YCKG.js → chunk-43TUWQNR.js} +3 -3
- package/dist/{chunk-H7BZBSP6.js → chunk-52II7PB6.js} +4 -4
- package/dist/{chunk-2NLCDNGA.js → chunk-564YP3DW.js} +2 -2
- package/dist/{chunk-5KGYR5VN.js → chunk-5DVOBBTV.js} +2 -2
- package/dist/{chunk-RT2GN6KQ.js → chunk-5FUDSK6Z.js} +2 -2
- package/dist/{chunk-B4XSXFKI.js → chunk-5G53JSRO.js} +2 -2
- package/dist/{chunk-BGK3SFCC.js → chunk-5X2H3X4J.js} +2 -2
- package/dist/{chunk-OXSTKCYK.js → chunk-6AEKSOWV.js} +33 -5
- package/dist/chunk-6AEKSOWV.js.map +1 -0
- package/dist/{chunk-4J7BNL7B.js → chunk-6ZQX3R66.js} +2 -2
- package/dist/{chunk-JNLYHJG7.js → chunk-74AY3S7U.js} +4 -4
- package/dist/{chunk-RUAIO5UA.js → chunk-7J6A5MV2.js} +2 -2
- package/dist/{chunk-A45QXZMU.js → chunk-7SAE6T36.js} +2 -2
- package/dist/chunk-7VLBZKNB.js +26 -0
- package/dist/chunk-7VLBZKNB.js.map +1 -0
- package/dist/{chunk-QE2XQM3S.js → chunk-7YVER7ZO.js} +2 -2
- package/dist/{chunk-IJH6METJ.js → chunk-A3UFV4K3.js} +4 -4
- package/dist/{chunk-LWRIGKFM.js → chunk-A5L5KGDI.js} +19 -1
- package/dist/chunk-A5L5KGDI.js.map +1 -0
- package/dist/{chunk-LXN737FX.js → chunk-AAANTXCQ.js} +2 -2
- package/dist/{chunk-I3IPXGFQ.js → chunk-AJJPXUE6.js} +4 -4
- package/dist/{chunk-2DPJFJFH.js → chunk-BNJAVRNJ.js} +11 -11
- package/dist/{chunk-YL564QS2.js → chunk-CCL2OXFO.js} +3 -3
- package/dist/{chunk-CHSRA76M.js → chunk-CRN3EPE2.js} +4 -3
- package/dist/chunk-CRN3EPE2.js.map +1 -0
- package/dist/{chunk-ZX5N5D6M.js → chunk-DAOMWGO2.js} +4 -4
- package/dist/{chunk-YUE7J2HZ.js → chunk-DMKTOCC7.js} +2 -2
- package/dist/chunk-DMKTOCC7.js.map +1 -0
- package/dist/{chunk-FRXS6SC7.js → chunk-EKUI7I2X.js} +20 -4
- package/dist/chunk-EKUI7I2X.js.map +1 -0
- package/dist/{chunk-PKPOO6X5.js → chunk-EKYWZYZJ.js} +25 -17
- package/dist/chunk-EKYWZYZJ.js.map +1 -0
- package/dist/{chunk-23ZMQONL.js → chunk-ERMT5WQM.js} +2 -2
- package/dist/{chunk-VTCNVGM6.js → chunk-EWBXMGXR.js} +2 -2
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-XTN54NML.js → chunk-G3T6YMBO.js} +4 -4
- package/dist/{chunk-2JNBPOZ2.js → chunk-GDYLZ6FR.js} +3 -3
- package/dist/{chunk-DZPTR2XQ.js → chunk-GFAEKSFR.js} +2 -2
- package/dist/{chunk-L46MG2GE.js → chunk-GQIYL7OD.js} +400 -301
- package/dist/chunk-GQIYL7OD.js.map +1 -0
- package/dist/{chunk-M7LA4QBY.js → chunk-GXVXBYRA.js} +3 -3
- package/dist/{chunk-UXAF4RQM.js → chunk-HKOMHP6D.js} +27 -15
- package/dist/chunk-HKOMHP6D.js.map +1 -0
- package/dist/{chunk-JGMJZBFV.js → chunk-HPNXS6W6.js} +2 -2
- package/dist/{chunk-BFPF5N3H.js → chunk-HXFIILGF.js} +4 -4
- package/dist/{chunk-YBKQAVPZ.js → chunk-IMKOJVD4.js} +101 -81
- package/dist/chunk-IMKOJVD4.js.map +1 -0
- package/dist/{chunk-CMNL3QSL.js → chunk-IN6U2BS5.js} +2 -2
- package/dist/{chunk-BWRX322G.js → chunk-IUYXCTA7.js} +2 -2
- package/dist/{chunk-LU57EEJW.js → chunk-JBWD3J5U.js} +3 -3
- package/dist/chunk-JNA7LFMQ.js +41 -0
- package/dist/chunk-JNA7LFMQ.js.map +1 -0
- package/dist/{chunk-2V664H6O.js → chunk-JS44PBCM.js} +5 -5
- package/dist/{chunk-XKTHJHSQ.js → chunk-JZCBMKR5.js} +3 -3
- package/dist/{chunk-MXPVNDGO.js → chunk-K5QQXVG6.js} +12 -12
- package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
- package/dist/{chunk-WUBGMV3G.js → chunk-KRAMCBIW.js} +9 -9
- package/dist/{chunk-NDMPRT3Z.js → chunk-KW5YZELC.js} +2 -2
- package/dist/{chunk-2RQR4TXV.js → chunk-LIBVANOF.js} +2 -2
- package/dist/{chunk-M7L5A2AT.js → chunk-LMMBV7QC.js} +4 -4
- package/dist/{chunk-PJBYVJKO.js → chunk-LXXIV7Y2.js} +2 -2
- package/dist/{chunk-MZCTS3ZG.js → chunk-M2DOJBH7.js} +2 -2
- package/dist/{chunk-WOPZ2P5Z.js → chunk-M4HRYLOA.js} +2 -2
- package/dist/{chunk-JCYEDMOY.js → chunk-M6TP6UYD.js} +3 -3
- package/dist/{chunk-KAUCVTAW.js → chunk-MLNXATFG.js} +2 -2
- package/dist/{chunk-2NX7JTST.js → chunk-MPWOAIIH.js} +2 -2
- package/dist/{chunk-RQLTCHUL.js → chunk-MWURUSRE.js} +13 -13
- package/dist/{chunk-3EGL7XZO.js → chunk-N74WJ7B2.js} +2 -2
- package/dist/{chunk-DRXTMDAG.js → chunk-N7FNCE7F.js} +2 -2
- package/dist/{chunk-6CWAY4R7.js → chunk-NPLM7AFU.js} +2 -2
- package/dist/{chunk-VECRSYQG.js → chunk-NRGQ5HJO.js} +1 -1
- package/dist/{chunk-PPDNG7WC.js → chunk-NSP5PIQH.js} +2 -2
- package/dist/{chunk-6HVLHIAB.js → chunk-P7OVFGMU.js} +2 -2
- package/dist/{chunk-EX27VKU6.js → chunk-PDTNQLPC.js} +2 -2
- package/dist/{chunk-ME5LVISI.js → chunk-PJCVI44H.js} +2 -2
- package/dist/{chunk-LJCHDFLP.js → chunk-Q3PNQYAC.js} +2 -2
- package/dist/{chunk-YFJHFEET.js → chunk-QS765SBL.js} +2 -2
- package/dist/{chunk-FJ264QBP.js → chunk-QTDZ2NYC.js} +11 -11
- package/dist/{chunk-Q6UZREJK.js → chunk-RKRX4C4H.js} +2 -2
- package/dist/{chunk-YFXRLRKP.js → chunk-S4OZKPBS.js} +7 -11
- package/dist/{chunk-YFXRLRKP.js.map → chunk-S4OZKPBS.js.map} +1 -1
- package/dist/{chunk-GLZWOIU3.js → chunk-SJTSWG2Z.js} +3 -3
- package/dist/{chunk-Y57GJDG5.js → chunk-SWRRZNHM.js} +14 -17
- package/dist/chunk-SWRRZNHM.js.map +1 -0
- package/dist/{chunk-CCTLRRV6.js → chunk-TEH5MDX5.js} +55 -34
- package/dist/chunk-TEH5MDX5.js.map +1 -0
- package/dist/{chunk-XM747YJF.js → chunk-TFWJXWL6.js} +2 -2
- package/dist/{chunk-QFQ4UG3I.js → chunk-TXKQBQYW.js} +4 -4
- package/dist/{chunk-BMOJFGVX.js → chunk-UALXWWGN.js} +2 -2
- package/dist/{chunk-B44NFHWI.js → chunk-UNOAVHV4.js} +2 -2
- package/dist/{chunk-TF5NZGPS.js → chunk-UQBOPCDK.js} +2 -2
- package/dist/{chunk-SUKPM6DV.js → chunk-UWMZT2TY.js} +47 -4
- package/dist/chunk-UWMZT2TY.js.map +1 -0
- package/dist/{chunk-5R2IF2KI.js → chunk-UXGXJUZ5.js} +7 -7
- package/dist/{chunk-FWV2SXSL.js → chunk-UZVQFGAY.js} +101 -98
- package/dist/chunk-UZVQFGAY.js.map +1 -0
- package/dist/{chunk-J6B6FK2X.js → chunk-VCOMACR6.js} +53 -53
- package/dist/{chunk-WCGJK4NQ.js → chunk-VKQI47DJ.js} +7 -7
- package/dist/{chunk-MPJDQC4G.js → chunk-WJCXUE2J.js} +2 -2
- package/dist/{chunk-BCDECPVO.js → chunk-WV4UYDP6.js} +2 -2
- package/dist/{chunk-VWJ56T63.js → chunk-WXJC2L66.js} +2 -2
- package/dist/chunk-X2KO4FK5.js +40 -0
- package/dist/chunk-X2KO4FK5.js.map +1 -0
- package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-FPANDKMQ.js → chunk-XC3H7KON.js} +2 -2
- package/dist/{chunk-OOOX5C4X.js → chunk-XL2EE5W3.js} +4 -4
- package/dist/{chunk-3DKKZVXS.js → chunk-XTXWR6KQ.js} +26 -11
- package/dist/chunk-XTXWR6KQ.js.map +1 -0
- package/dist/{chunk-3SL52JKD.js → chunk-XVZA6VXC.js} +4 -4
- package/dist/{chunk-CZVNOTLR.js → chunk-XXR4A33N.js} +2 -2
- package/dist/{chunk-2AV2U2SZ.js → chunk-YDIOMDBW.js} +2 -2
- package/dist/{chunk-XTVH2NAW.js → chunk-YECEEJUU.js} +3 -3
- package/dist/{chunk-QR6W3FSW.js → chunk-Z5277JWF.js} +3 -3
- package/dist/{chunk-CTHYR4QK.js → chunk-ZFNXJEUG.js} +2 -2
- package/dist/{chunk-6Q56WDHG.js → chunk-ZLCBLDJO.js} +2 -2
- package/dist/{chunk-VY6BD5KB.js → chunk-ZMO2S5RT.js} +12 -10
- package/dist/chunk-ZMO2S5RT.js.map +1 -0
- package/dist/{chunk-KRACAE73.js → chunk-ZNRTHYYW.js} +6 -6
- package/dist/{chunk-7AIPWNM7.js → chunk-ZSMA67FV.js} +2 -2
- package/dist/cli.js +123 -120
- package/dist/coding/export-okf-codegraph.js +19 -18
- package/dist/coding/export-okf-codegraph.js.map +1 -1
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +21 -20
- package/dist/config.js +32 -30
- package/dist/connectors/codex-materialize-runner.js +20 -19
- package/dist/connectors/index.js +20 -19
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.js +2 -2
- package/dist/convergence-refresh.js +22 -21
- package/dist/corpus-watermark.js +22 -21
- package/dist/correction/passive-correction-notifications.d.ts +1 -1
- package/dist/correction/passive-correction-notifications.d.ts.map +1 -1
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-http-glue.js +12 -11
- package/dist/deep-recall.js +2 -2
- package/dist/embedding-fallback.js +3 -2
- package/dist/embedding-generation-marker.d.ts +16 -0
- package/dist/embedding-generation-marker.d.ts.map +1 -0
- package/dist/embedding-generation-marker.js +19 -0
- package/dist/embedding-generation-marker.js.map +1 -0
- package/dist/embedding-index-storage.d.ts +2 -4
- package/dist/embedding-index-storage.d.ts.map +1 -1
- package/dist/embedding-index-storage.js +2 -1
- package/dist/{entity-canonical-id-migration-2YOLMOFC.js → entity-canonical-id-migration-ZBNRGUNV.js} +3 -3
- package/dist/entity-canonical-id-migration-ZBNRGUNV.js.map +1 -0
- package/dist/entity-retrieval.js +22 -21
- package/dist/event-order-recall.js +2 -2
- package/dist/explicit-capture.js +3 -3
- package/dist/external-wiki-access.js +74 -71
- package/dist/extraction-span-materialize.js +2 -2
- package/dist/extraction.js +30 -29
- package/dist/{graph-edge-decay-3HVLMT6U.js → graph-edge-decay-IU75NV42.js} +6 -6
- package/dist/graph-edge-decay-IU75NV42.js.map +1 -0
- package/dist/graph-jsonl.js +4 -4
- package/dist/graph-snapshot.js +4 -4
- package/dist/graph.js +5 -5
- package/dist/harmonic-retrieval.js +20 -19
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -218
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +5 -5
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +25 -24
- package/dist/location/tagging.d.ts +7 -5
- package/dist/location/tagging.d.ts.map +1 -1
- package/dist/maintenance/memory-governance.js +20 -19
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +20 -19
- package/dist/maintenance/rebuild-memory-projection-runner.js +22 -21
- package/dist/maintenance/rebuild-memory-projection.js +21 -20
- package/dist/memory-browse-operations.js +74 -71
- package/dist/memory-browse.js +5 -5
- package/dist/namespaces/migrate.js +21 -20
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.js +20 -19
- package/dist/offline-sync-changeset-generations.js +5 -4
- package/dist/offline-sync-embedding-generation.d.ts +12 -2
- package/dist/offline-sync-embedding-generation.d.ts.map +1 -1
- package/dist/offline-sync-embedding-generation.js +6 -3
- package/dist/offline-sync-file-io.d.ts +15 -0
- package/dist/offline-sync-file-io.d.ts.map +1 -1
- package/dist/offline-sync-file-io.js +9 -1
- package/dist/offline-sync-snapshot-stream.js +9 -8
- package/dist/offline-sync-upload-staging.js +2 -2
- package/dist/offline-sync.d.ts.map +1 -1
- package/dist/offline-sync.js +10 -9
- package/dist/operator-doctor-corpus.js +23 -22
- package/dist/operator-toolkit.js +45 -43
- package/dist/orchestration/maintenance.js +33 -32
- package/dist/orchestrator.d.ts +1 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +170 -167
- package/dist/page-versioning.js +2 -2
- package/dist/qmd.js +2 -2
- package/dist/recall-navigation-operations.js +74 -71
- package/dist/recall-navigation.js +73 -70
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.js +5 -5
- package/dist/resume-bundles.js +33 -31
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +9 -9
- package/dist/semantic-consolidation.js +25 -24
- package/dist/semantic-rule-promotion.js +20 -19
- package/dist/semantic-rule-verifier.js +20 -19
- package/dist/source-agent-qualifier.js +24 -23
- package/dist/standup/build.js +20 -19
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts +1 -0
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts.map +1 -1
- package/dist/storage/entity-canonical-id-migration.d.ts +7 -0
- package/dist/storage/entity-canonical-id-migration.d.ts.map +1 -1
- package/dist/storage/entity-canonical-id-references.d.ts +2 -0
- package/dist/storage/entity-canonical-id-references.d.ts.map +1 -1
- package/dist/storage/entity-migration-completion-cache.d.ts +46 -0
- package/dist/storage/entity-migration-completion-cache.d.ts.map +1 -0
- package/dist/storage/entity-store.d.ts +1 -8
- package/dist/storage/entity-store.d.ts.map +1 -1
- package/dist/storage/memory-read-store.d.ts +3 -2
- package/dist/storage/memory-read-store.d.ts.map +1 -1
- package/dist/storage/windowed-memory-read.d.ts +6 -0
- package/dist/storage/windowed-memory-read.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +19 -18
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +76 -73
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +4 -4
- package/dist/transfer/export-okf.js +20 -19
- package/dist/verified-recall.js +20 -19
- package/dist/who-knows-cli.js +21 -20
- package/dist/who-knows.js +20 -19
- package/package.json +2 -2
- package/src/access-http-briefing.ts +4 -0
- package/src/access-http.test.ts +40 -11
- package/src/access-http.ts +6 -0
- package/src/access-observe-write-category.ts +1 -1
- package/src/access-observe-write-surface.ts +24 -19
- package/src/access-operations-batch.ts +18 -2
- package/src/access-service-peer-capture-backoff.test.ts +236 -0
- package/src/access-service.ts +11 -2
- package/src/briefing-abort.test.ts +173 -0
- package/src/briefing-deadline.test.ts +84 -0
- package/src/briefing-deadline.ts +33 -0
- package/src/briefing-window-abort.test.ts +47 -0
- package/src/briefing-window.ts +33 -4
- package/src/briefing.ts +23 -16
- package/src/capture-write-lock-scope.test.ts +32 -0
- package/src/capture-write-lock-scope.ts +30 -0
- package/src/correction/passive-correction-notifications-abort.test.ts +42 -0
- package/src/correction/passive-correction-notifications.ts +4 -0
- package/src/embedding-generation-marker.ts +51 -0
- package/src/embedding-index-storage.ts +13 -29
- package/src/entity-lazy-compatibility.test.ts +229 -0
- package/src/entity-migration-completion-notification.test.ts +148 -0
- package/src/index.ts +2 -2
- package/src/location/tagging.ts +14 -12
- package/src/offline-sync-embedding-delete-digest.test.ts +58 -0
- package/src/offline-sync-embedding-generation-marker.test.ts +245 -0
- package/src/offline-sync-embedding-generation.ts +47 -2
- package/src/offline-sync-embedding-lock-loss.test.ts +42 -0
- package/src/offline-sync-file-io.ts +61 -2
- package/src/offline-sync.test.ts +313 -20
- package/src/offline-sync.ts +47 -22
- package/src/orchestrator-init.test.ts +49 -1
- package/src/orchestrator.ts +9 -8
- package/src/storage/entity-canonical-id-migration-adapter.ts +16 -1
- package/src/storage/entity-canonical-id-migration.ts +17 -8
- package/src/storage/entity-canonical-id-references.ts +24 -0
- package/src/storage/entity-migration-completion-cache.nofollow.test.ts +223 -0
- package/src/storage/entity-migration-completion-cache.ts +153 -0
- package/src/storage/entity-store.ts +58 -86
- package/src/storage/memory-read-store.ts +9 -3
- package/src/storage/windowed-memory-read.ts +8 -0
- package/src/storage-startup-nonblocking-migration.test.ts +183 -0
- package/src/storage.ts +9 -3
- package/src/testing/subjects/entity-migration-completion-cache.test.ts +90 -0
- package/dist/chunk-3DKKZVXS.js.map +0 -1
- package/dist/chunk-CCTLRRV6.js.map +0 -1
- package/dist/chunk-CHSRA76M.js.map +0 -1
- package/dist/chunk-FRXS6SC7.js.map +0 -1
- package/dist/chunk-FWV2SXSL.js.map +0 -1
- package/dist/chunk-L46MG2GE.js.map +0 -1
- package/dist/chunk-LWRIGKFM.js.map +0 -1
- package/dist/chunk-OXSTKCYK.js.map +0 -1
- package/dist/chunk-PKPOO6X5.js.map +0 -1
- package/dist/chunk-SUKPM6DV.js.map +0 -1
- package/dist/chunk-UXAF4RQM.js.map +0 -1
- package/dist/chunk-VY6BD5KB.js.map +0 -1
- package/dist/chunk-Y57GJDG5.js.map +0 -1
- package/dist/chunk-YBKQAVPZ.js.map +0 -1
- package/dist/chunk-YUE7J2HZ.js.map +0 -1
- /package/dist/{auto-sync-BNCBMU6H.js.map → auto-sync-UQQ6YJSB.js.map} +0 -0
- /package/dist/{capsule-crypto-GWVG7LGC.js.map → briefing-deadline.js.map} +0 -0
- /package/dist/{entity-canonical-id-migration-2YOLMOFC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
- /package/dist/{capsule-merge-RNQUP2VL.js.map → capsule-merge-D4BTIJGK.js.map} +0 -0
- /package/dist/{graph-edge-decay-3HVLMT6U.js.map → capture-write-lock-scope.js.map} +0 -0
- /package/dist/{chunk-FAKRTS3Y.js.map → chunk-2AKIVSHP.js.map} +0 -0
- /package/dist/{chunk-EIYJK7OD.js.map → chunk-2M36PR7R.js.map} +0 -0
- /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-6P6MZDBL.js.map → chunk-3NL4ALUP.js.map} +0 -0
- /package/dist/{chunk-6CC2YCKG.js.map → chunk-43TUWQNR.js.map} +0 -0
- /package/dist/{chunk-H7BZBSP6.js.map → chunk-52II7PB6.js.map} +0 -0
- /package/dist/{chunk-2NLCDNGA.js.map → chunk-564YP3DW.js.map} +0 -0
- /package/dist/{chunk-5KGYR5VN.js.map → chunk-5DVOBBTV.js.map} +0 -0
- /package/dist/{chunk-RT2GN6KQ.js.map → chunk-5FUDSK6Z.js.map} +0 -0
- /package/dist/{chunk-B4XSXFKI.js.map → chunk-5G53JSRO.js.map} +0 -0
- /package/dist/{chunk-BGK3SFCC.js.map → chunk-5X2H3X4J.js.map} +0 -0
- /package/dist/{chunk-4J7BNL7B.js.map → chunk-6ZQX3R66.js.map} +0 -0
- /package/dist/{chunk-JNLYHJG7.js.map → chunk-74AY3S7U.js.map} +0 -0
- /package/dist/{chunk-RUAIO5UA.js.map → chunk-7J6A5MV2.js.map} +0 -0
- /package/dist/{chunk-A45QXZMU.js.map → chunk-7SAE6T36.js.map} +0 -0
- /package/dist/{chunk-QE2XQM3S.js.map → chunk-7YVER7ZO.js.map} +0 -0
- /package/dist/{chunk-IJH6METJ.js.map → chunk-A3UFV4K3.js.map} +0 -0
- /package/dist/{chunk-LXN737FX.js.map → chunk-AAANTXCQ.js.map} +0 -0
- /package/dist/{chunk-I3IPXGFQ.js.map → chunk-AJJPXUE6.js.map} +0 -0
- /package/dist/{chunk-2DPJFJFH.js.map → chunk-BNJAVRNJ.js.map} +0 -0
- /package/dist/{chunk-YL564QS2.js.map → chunk-CCL2OXFO.js.map} +0 -0
- /package/dist/{chunk-ZX5N5D6M.js.map → chunk-DAOMWGO2.js.map} +0 -0
- /package/dist/{chunk-23ZMQONL.js.map → chunk-ERMT5WQM.js.map} +0 -0
- /package/dist/{chunk-VTCNVGM6.js.map → chunk-EWBXMGXR.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-XTN54NML.js.map → chunk-G3T6YMBO.js.map} +0 -0
- /package/dist/{chunk-2JNBPOZ2.js.map → chunk-GDYLZ6FR.js.map} +0 -0
- /package/dist/{chunk-DZPTR2XQ.js.map → chunk-GFAEKSFR.js.map} +0 -0
- /package/dist/{chunk-M7LA4QBY.js.map → chunk-GXVXBYRA.js.map} +0 -0
- /package/dist/{chunk-JGMJZBFV.js.map → chunk-HPNXS6W6.js.map} +0 -0
- /package/dist/{chunk-BFPF5N3H.js.map → chunk-HXFIILGF.js.map} +0 -0
- /package/dist/{chunk-CMNL3QSL.js.map → chunk-IN6U2BS5.js.map} +0 -0
- /package/dist/{chunk-BWRX322G.js.map → chunk-IUYXCTA7.js.map} +0 -0
- /package/dist/{chunk-LU57EEJW.js.map → chunk-JBWD3J5U.js.map} +0 -0
- /package/dist/{chunk-2V664H6O.js.map → chunk-JS44PBCM.js.map} +0 -0
- /package/dist/{chunk-XKTHJHSQ.js.map → chunk-JZCBMKR5.js.map} +0 -0
- /package/dist/{chunk-MXPVNDGO.js.map → chunk-K5QQXVG6.js.map} +0 -0
- /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-WUBGMV3G.js.map → chunk-KRAMCBIW.js.map} +0 -0
- /package/dist/{chunk-NDMPRT3Z.js.map → chunk-KW5YZELC.js.map} +0 -0
- /package/dist/{chunk-2RQR4TXV.js.map → chunk-LIBVANOF.js.map} +0 -0
- /package/dist/{chunk-M7L5A2AT.js.map → chunk-LMMBV7QC.js.map} +0 -0
- /package/dist/{chunk-PJBYVJKO.js.map → chunk-LXXIV7Y2.js.map} +0 -0
- /package/dist/{chunk-MZCTS3ZG.js.map → chunk-M2DOJBH7.js.map} +0 -0
- /package/dist/{chunk-WOPZ2P5Z.js.map → chunk-M4HRYLOA.js.map} +0 -0
- /package/dist/{chunk-JCYEDMOY.js.map → chunk-M6TP6UYD.js.map} +0 -0
- /package/dist/{chunk-KAUCVTAW.js.map → chunk-MLNXATFG.js.map} +0 -0
- /package/dist/{chunk-2NX7JTST.js.map → chunk-MPWOAIIH.js.map} +0 -0
- /package/dist/{chunk-RQLTCHUL.js.map → chunk-MWURUSRE.js.map} +0 -0
- /package/dist/{chunk-3EGL7XZO.js.map → chunk-N74WJ7B2.js.map} +0 -0
- /package/dist/{chunk-DRXTMDAG.js.map → chunk-N7FNCE7F.js.map} +0 -0
- /package/dist/{chunk-6CWAY4R7.js.map → chunk-NPLM7AFU.js.map} +0 -0
- /package/dist/{chunk-VECRSYQG.js.map → chunk-NRGQ5HJO.js.map} +0 -0
- /package/dist/{chunk-PPDNG7WC.js.map → chunk-NSP5PIQH.js.map} +0 -0
- /package/dist/{chunk-6HVLHIAB.js.map → chunk-P7OVFGMU.js.map} +0 -0
- /package/dist/{chunk-EX27VKU6.js.map → chunk-PDTNQLPC.js.map} +0 -0
- /package/dist/{chunk-ME5LVISI.js.map → chunk-PJCVI44H.js.map} +0 -0
- /package/dist/{chunk-LJCHDFLP.js.map → chunk-Q3PNQYAC.js.map} +0 -0
- /package/dist/{chunk-YFJHFEET.js.map → chunk-QS765SBL.js.map} +0 -0
- /package/dist/{chunk-FJ264QBP.js.map → chunk-QTDZ2NYC.js.map} +0 -0
- /package/dist/{chunk-Q6UZREJK.js.map → chunk-RKRX4C4H.js.map} +0 -0
- /package/dist/{chunk-GLZWOIU3.js.map → chunk-SJTSWG2Z.js.map} +0 -0
- /package/dist/{chunk-XM747YJF.js.map → chunk-TFWJXWL6.js.map} +0 -0
- /package/dist/{chunk-QFQ4UG3I.js.map → chunk-TXKQBQYW.js.map} +0 -0
- /package/dist/{chunk-BMOJFGVX.js.map → chunk-UALXWWGN.js.map} +0 -0
- /package/dist/{chunk-B44NFHWI.js.map → chunk-UNOAVHV4.js.map} +0 -0
- /package/dist/{chunk-TF5NZGPS.js.map → chunk-UQBOPCDK.js.map} +0 -0
- /package/dist/{chunk-5R2IF2KI.js.map → chunk-UXGXJUZ5.js.map} +0 -0
- /package/dist/{chunk-J6B6FK2X.js.map → chunk-VCOMACR6.js.map} +0 -0
- /package/dist/{chunk-WCGJK4NQ.js.map → chunk-VKQI47DJ.js.map} +0 -0
- /package/dist/{chunk-MPJDQC4G.js.map → chunk-WJCXUE2J.js.map} +0 -0
- /package/dist/{chunk-BCDECPVO.js.map → chunk-WV4UYDP6.js.map} +0 -0
- /package/dist/{chunk-VWJ56T63.js.map → chunk-WXJC2L66.js.map} +0 -0
- /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-FPANDKMQ.js.map → chunk-XC3H7KON.js.map} +0 -0
- /package/dist/{chunk-OOOX5C4X.js.map → chunk-XL2EE5W3.js.map} +0 -0
- /package/dist/{chunk-3SL52JKD.js.map → chunk-XVZA6VXC.js.map} +0 -0
- /package/dist/{chunk-CZVNOTLR.js.map → chunk-XXR4A33N.js.map} +0 -0
- /package/dist/{chunk-2AV2U2SZ.js.map → chunk-YDIOMDBW.js.map} +0 -0
- /package/dist/{chunk-XTVH2NAW.js.map → chunk-YECEEJUU.js.map} +0 -0
- /package/dist/{chunk-QR6W3FSW.js.map → chunk-Z5277JWF.js.map} +0 -0
- /package/dist/{chunk-CTHYR4QK.js.map → chunk-ZFNXJEUG.js.map} +0 -0
- /package/dist/{chunk-6Q56WDHG.js.map → chunk-ZLCBLDJO.js.map} +0 -0
- /package/dist/{chunk-KRACAE73.js.map → chunk-ZNRTHYYW.js.map} +0 -0
- /package/dist/{chunk-7AIPWNM7.js.map → chunk-ZSMA67FV.js.map} +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test, { type TestContext } from "node:test";
|
|
6
|
+
import { abortError } from "./abort-error.js";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
9
|
+
import { AccessObserveWriteSurface } from "./access-observe-write-surface.js";
|
|
10
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
11
|
+
import { buildBriefing, type ParsedBriefingWindow } from "./briefing.js";
|
|
12
|
+
import type { CorpusReadOptions } from "./corpus-read-cancellation.js";
|
|
13
|
+
import { StorageManager } from "./storage.js";
|
|
14
|
+
import type { WindowedMemoryReadOptions, WindowedMemoryReadResult } from "./storage/windowed-memory-read.js";
|
|
15
|
+
import type { EntityFile, MemoryFile } from "./types.js";
|
|
16
|
+
|
|
17
|
+
const WINDOW: ParsedBriefingWindow = {
|
|
18
|
+
from: new Date("2026-04-10T00:00:00.000Z"),
|
|
19
|
+
to: new Date("2026-04-11T00:00:00.000Z"),
|
|
20
|
+
label: "test-window",
|
|
21
|
+
};
|
|
22
|
+
const MEMORY: MemoryFile = {
|
|
23
|
+
path: "/synthetic/mem.md",
|
|
24
|
+
frontmatter: {
|
|
25
|
+
id: "synthetic-mem",
|
|
26
|
+
category: "fact",
|
|
27
|
+
created: "2026-04-10T01:00:00.000Z",
|
|
28
|
+
updated: "2026-04-10T02:00:00.000Z",
|
|
29
|
+
source: "test",
|
|
30
|
+
confidence: 0.9,
|
|
31
|
+
confidenceTier: "explicit",
|
|
32
|
+
tags: [],
|
|
33
|
+
},
|
|
34
|
+
content: "synthetic briefing memory",
|
|
35
|
+
};
|
|
36
|
+
const ENTITY: EntityFile = {
|
|
37
|
+
name: "SyntheticEntity",
|
|
38
|
+
type: "project",
|
|
39
|
+
updated: "2026-04-10T03:00:00.000Z",
|
|
40
|
+
facts: ["synthetic fact"],
|
|
41
|
+
timeline: [],
|
|
42
|
+
relationships: [],
|
|
43
|
+
activity: [],
|
|
44
|
+
aliases: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function makeStorage(t: TestContext): StorageManager {
|
|
48
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "remnic-briefing-abort-"));
|
|
49
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
50
|
+
const storage = new StorageManager(dir);
|
|
51
|
+
t.mock.method(storage, "readMemoriesWindow", async (_options: WindowedMemoryReadOptions) => ({
|
|
52
|
+
memories: [MEMORY],
|
|
53
|
+
filePaths: [MEMORY.path],
|
|
54
|
+
}));
|
|
55
|
+
t.mock.method(storage, "readAllEntityFiles", async (_options?: CorpusReadOptions) => [ENTITY]);
|
|
56
|
+
return storage;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function makeStallingStorage(t: TestContext) {
|
|
60
|
+
const storage = makeStorage(t);
|
|
61
|
+
const fallback = Promise.withResolvers<void>();
|
|
62
|
+
// The fallback releases broken signal plumbing too, so the regression cannot hang.
|
|
63
|
+
t.mock.method(
|
|
64
|
+
storage,
|
|
65
|
+
"readMemoriesWindow",
|
|
66
|
+
(options: WindowedMemoryReadOptions): Promise<WindowedMemoryReadResult> => {
|
|
67
|
+
const signal = options.abortSignal;
|
|
68
|
+
if (!signal) return fallback.promise.then(() => ({ memories: [], filePaths: [] }));
|
|
69
|
+
const read = Promise.withResolvers<WindowedMemoryReadResult>();
|
|
70
|
+
const onAbort = () => read.reject(abortError("corpus read aborted"));
|
|
71
|
+
if (signal.aborted) {
|
|
72
|
+
onAbort();
|
|
73
|
+
return read.promise;
|
|
74
|
+
}
|
|
75
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
76
|
+
void fallback.promise.then(() => {
|
|
77
|
+
signal.removeEventListener("abort", onAbort);
|
|
78
|
+
read.resolve({ memories: [], filePaths: [] });
|
|
79
|
+
});
|
|
80
|
+
return read.promise;
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
return { storage, releaseFallback: fallback.resolve };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test("buildBriefing rejects with an AbortError when the caller's cancel signal fires mid-read", async (t) => {
|
|
87
|
+
const { storage, releaseFallback } = makeStallingStorage(t);
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const call = buildBriefing({ storage, window: WINDOW, allowLlm: false, abortSignal: controller.signal });
|
|
90
|
+
controller.abort();
|
|
91
|
+
releaseFallback();
|
|
92
|
+
await assert.rejects(call, { name: "AbortError", message: /abort|cancel/i });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("buildBriefing rejects when the caller's cancel signal is already aborted", async (t) => {
|
|
96
|
+
const { storage, releaseFallback } = makeStallingStorage(t);
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
controller.abort();
|
|
99
|
+
const call = buildBriefing({ storage, window: WINDOW, allowLlm: false, abortSignal: controller.signal });
|
|
100
|
+
releaseFallback();
|
|
101
|
+
await assert.rejects(call, { name: "AbortError" });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a cancelled briefing does not drain the correction-notification queue", async (t) => {
|
|
105
|
+
const storage = makeStorage(t);
|
|
106
|
+
const queueDir = path.join(storage.dir, "state", "corrections");
|
|
107
|
+
mkdirSync(queueDir, { recursive: true });
|
|
108
|
+
const queueFile = path.join(queueDir, "notifications.jsonl");
|
|
109
|
+
writeFileSync(
|
|
110
|
+
queueFile,
|
|
111
|
+
`${JSON.stringify({ planId: "synthetic-plan", summary: "synthetic correction", undoCommand: "remnic correct plan synthetic-plan", createdAt: "2026-04-10T04:00:00.000Z" })}\n`
|
|
112
|
+
);
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const call = buildBriefing({
|
|
115
|
+
storage,
|
|
116
|
+
window: WINDOW,
|
|
117
|
+
abortSignal: controller.signal,
|
|
118
|
+
// Cancel after reads, just before the queue-draining phase.
|
|
119
|
+
followupGenerator: async () => {
|
|
120
|
+
controller.abort();
|
|
121
|
+
return [];
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
await assert.rejects(call, { name: "AbortError" });
|
|
125
|
+
assert.ok(existsSync(queueFile), "a cancelled briefing must not consume correction notifications");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("buildBriefing returns a non-empty briefing when nothing is cancelled", async (t) => {
|
|
129
|
+
const result = await buildBriefing({ storage: makeStorage(t), window: WINDOW, allowLlm: false });
|
|
130
|
+
assert.equal(result.sections.activeThreads.length, 1);
|
|
131
|
+
assert.equal(result.sections.recentEntities.length, 1);
|
|
132
|
+
assert.ok(result.markdown.length > 0);
|
|
133
|
+
assert.deepEqual(result.window, { from: "2026-04-10T00:00:00.000Z", to: "2026-04-11T00:00:00.000Z" });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("an HTTP/MCP briefing cancelled during the location read keeps the notification queue", async (t) => {
|
|
137
|
+
const storage = makeStorage(t);
|
|
138
|
+
const queueDir = path.join(storage.dir, "state", "corrections");
|
|
139
|
+
mkdirSync(queueDir, { recursive: true });
|
|
140
|
+
const queueFile = path.join(queueDir, "notifications.jsonl");
|
|
141
|
+
writeFileSync(
|
|
142
|
+
queueFile,
|
|
143
|
+
`${JSON.stringify({ planId: "synthetic-plan", summary: "synthetic correction", undoCommand: "remnic correct plan synthetic-plan", createdAt: "2026-04-10T04:00:00.000Z" })}\n`
|
|
144
|
+
);
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const read: (file: string, encoding: "utf-8") => Promise<string> = fs.readFile;
|
|
147
|
+
t.mock.method(fs, "readFile", async (file: string) => {
|
|
148
|
+
if (file.endsWith(path.join("state", "locations", "sync.json"))) controller.abort(abortError("deadline"));
|
|
149
|
+
return read(file, "utf-8");
|
|
150
|
+
});
|
|
151
|
+
syncBuiltinESMExports();
|
|
152
|
+
t.after(() => {
|
|
153
|
+
t.mock.restoreAll();
|
|
154
|
+
syncBuiltinESMExports();
|
|
155
|
+
});
|
|
156
|
+
// Object.create yields untyped values, so no assertion is needed: briefing() reads only these members.
|
|
157
|
+
const orchestrator: Orchestrator = Object.assign(Object.create(Orchestrator.prototype), {
|
|
158
|
+
config: {
|
|
159
|
+
briefing: { enabled: true, defaultWindow: "24h", defaultFormat: "markdown", maxFollowups: 0, llmFollowups: false },
|
|
160
|
+
location: { enabled: true, tagging: { enabled: true }, timezone: "UTC" },
|
|
161
|
+
},
|
|
162
|
+
getStorage: async () => storage,
|
|
163
|
+
});
|
|
164
|
+
const surface = new AccessObserveWriteSurface(
|
|
165
|
+
Object.assign(Object.create(null), { orchestrator, resolveReadableNamespace: () => "default" }),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
await assert.rejects(
|
|
169
|
+
surface.briefing({ includeLocation: true, abortSignal: controller.signal }),
|
|
170
|
+
{ name: "AbortError" },
|
|
171
|
+
);
|
|
172
|
+
assert.ok(existsSync(queueFile), "a briefing cancelled during the location read must not consume notifications");
|
|
173
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #3139 — the briefing's default deadline.
|
|
3
|
+
*
|
|
4
|
+
* The budget is a server-side limit, not a client cancellation: exhausting it
|
|
5
|
+
* must surface as an explicit TimeoutError, never as an empty result.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
BRIEFING_DEADLINE_MS,
|
|
13
|
+
BriefingTimeoutError,
|
|
14
|
+
withBriefingDeadline,
|
|
15
|
+
} from "./briefing-deadline.js";
|
|
16
|
+
|
|
17
|
+
test("withBriefingDeadline rejects with a TimeoutError when the default budget is exhausted", async (t) => {
|
|
18
|
+
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 0 });
|
|
19
|
+
|
|
20
|
+
const pending = withBriefingDeadline(
|
|
21
|
+
() => Promise.withResolvers<never>().promise,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
t.mock.timers.tick(BRIEFING_DEADLINE_MS);
|
|
25
|
+
|
|
26
|
+
await assert.rejects(pending, (err) => {
|
|
27
|
+
assert.ok(
|
|
28
|
+
err instanceof BriefingTimeoutError,
|
|
29
|
+
"budget exhaustion must surface as a timeout",
|
|
30
|
+
);
|
|
31
|
+
assert.equal(err.name, "TimeoutError");
|
|
32
|
+
assert.equal(err.deadlineMs, BRIEFING_DEADLINE_MS);
|
|
33
|
+
return true;
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("withBriefingDeadline clears its timer once the run settles", async (t) => {
|
|
38
|
+
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 0 });
|
|
39
|
+
let observed: AbortSignal | undefined;
|
|
40
|
+
|
|
41
|
+
const result = await withBriefingDeadline(async (signal) => {
|
|
42
|
+
observed = signal;
|
|
43
|
+
return "briefing";
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
assert.equal(result, "briefing");
|
|
47
|
+
t.mock.timers.tick(BRIEFING_DEADLINE_MS);
|
|
48
|
+
assert.equal(
|
|
49
|
+
observed?.aborted,
|
|
50
|
+
false,
|
|
51
|
+
"the budget timer must be cleared when the run settles, not left to fire afterwards",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("withBriefingDeadline preserves work failures and clears their timer", async (t) => {
|
|
56
|
+
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 0 });
|
|
57
|
+
const failure = new Error("synthetic work failure");
|
|
58
|
+
const signals: AbortSignal[] = [];
|
|
59
|
+
|
|
60
|
+
const pending = withBriefingDeadline(async (signal) => {
|
|
61
|
+
signals.push(signal);
|
|
62
|
+
throw failure;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await assert.rejects(pending, (err) => err === failure);
|
|
66
|
+
assert.equal(signals.length, 1);
|
|
67
|
+
t.mock.timers.tick(BRIEFING_DEADLINE_MS);
|
|
68
|
+
assert.equal(signals[0].aborted, false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("withBriefingDeadline normalizes arbitrary caller cancellation to AbortError", async () => {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const pending = withBriefingDeadline(
|
|
74
|
+
() => Promise.withResolvers<never>().promise,
|
|
75
|
+
controller.signal,
|
|
76
|
+
);
|
|
77
|
+
controller.abort(new Error("synthetic caller cancellation"));
|
|
78
|
+
|
|
79
|
+
await assert.rejects(pending, (err) => {
|
|
80
|
+
assert.ok(err instanceof Error);
|
|
81
|
+
assert.equal(err.name, "AbortError");
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isAbortError, raceAbort } from "./abort-error.js";
|
|
2
|
+
|
|
3
|
+
export const BRIEFING_DEADLINE_MS = 30_000;
|
|
4
|
+
|
|
5
|
+
export class BriefingTimeoutError extends Error {
|
|
6
|
+
constructor(readonly deadlineMs: number) {
|
|
7
|
+
super(`briefing timed out after ${deadlineMs}ms`);
|
|
8
|
+
this.name = "TimeoutError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function withBriefingDeadline<T>(
|
|
13
|
+
run: (signal: AbortSignal) => Promise<T>,
|
|
14
|
+
abortSignal?: AbortSignal
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const signal = abortSignal ? AbortSignal.any([abortSignal, controller.signal]) : controller.signal;
|
|
18
|
+
const timer = setTimeout(
|
|
19
|
+
() => controller.abort(new BriefingTimeoutError(BRIEFING_DEADLINE_MS)),
|
|
20
|
+
BRIEFING_DEADLINE_MS
|
|
21
|
+
);
|
|
22
|
+
try {
|
|
23
|
+
signal.throwIfAborted();
|
|
24
|
+
return await raceAbort(run(signal), signal, "briefing aborted");
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// raceAbort normalizes cancellation; retain the distinct deadline reason.
|
|
27
|
+
const reason: unknown = signal.reason;
|
|
28
|
+
if (signal.aborted && (reason instanceof BriefingTimeoutError || isAbortError(reason))) throw reason;
|
|
29
|
+
throw err;
|
|
30
|
+
} finally {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { safeReadMemories } from "./briefing-window.js";
|
|
4
|
+
import type { CorpusReadOptions } from "./corpus-read-cancellation.js";
|
|
5
|
+
import type { MemoryFile } from "./types.js";
|
|
6
|
+
|
|
7
|
+
test("caller cancellation reaches the signal-aware legacy briefing scan", async () => {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const started = Promise.withResolvers<AbortSignal>();
|
|
10
|
+
const release = Promise.withResolvers<void>();
|
|
11
|
+
const reader = {
|
|
12
|
+
supportsAbortSignal: true,
|
|
13
|
+
async readAllMemories(options?: CorpusReadOptions): Promise<MemoryFile[]> {
|
|
14
|
+
assert.ok(options?.abortSignal);
|
|
15
|
+
started.resolve(options.abortSignal);
|
|
16
|
+
await release.promise;
|
|
17
|
+
options.abortSignal.throwIfAborted();
|
|
18
|
+
return [];
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const result = safeReadMemories(
|
|
22
|
+
reader,
|
|
23
|
+
{
|
|
24
|
+
from: new Date("2026-04-10T00:00:00.000Z"),
|
|
25
|
+
to: new Date("2026-04-11T00:00:00.000Z"),
|
|
26
|
+
label: "yesterday",
|
|
27
|
+
},
|
|
28
|
+
{ signal: controller.signal }
|
|
29
|
+
).then(
|
|
30
|
+
() => ({ ok: true as const }),
|
|
31
|
+
(error) => ({ ok: false as const, error })
|
|
32
|
+
);
|
|
33
|
+
try {
|
|
34
|
+
const scanSignal = await started.promise;
|
|
35
|
+
assert.equal(scanSignal.aborted, false);
|
|
36
|
+
controller.abort();
|
|
37
|
+
release.resolve();
|
|
38
|
+
const outcome = await result;
|
|
39
|
+
assert.equal(outcome.ok, false, "a cancelled scan must reject, not return an empty corpus");
|
|
40
|
+
assert.ok(outcome.error instanceof Error);
|
|
41
|
+
assert.equal(outcome.error.name, "AbortError");
|
|
42
|
+
assert.equal(scanSignal.aborted, true, "the underlying scan must stop too");
|
|
43
|
+
} finally {
|
|
44
|
+
release.resolve();
|
|
45
|
+
await result;
|
|
46
|
+
}
|
|
47
|
+
});
|
package/src/briefing-window.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { log } from "./logger.js";
|
|
2
|
-
import type { MemoryFile } from "./types.js";
|
|
2
|
+
import type { EntityFile, MemoryFile } from "./types.js";
|
|
3
3
|
import { isAbortError } from "./abort-error.js";
|
|
4
4
|
import type { CorpusReadOptions } from "./corpus-read-cancellation.js";
|
|
5
5
|
import type { ParsedBriefingWindow } from "./briefing.js";
|
|
6
|
+
import type { WindowedMemoryReadOptions } from "./storage/windowed-memory-read.js";
|
|
6
7
|
|
|
7
8
|
type BriefingMemoryReader = {
|
|
8
|
-
readMemoriesWindow?: (options:
|
|
9
|
+
readMemoriesWindow?: (options: WindowedMemoryReadOptions) => Promise<{ memories: MemoryFile[] }>;
|
|
9
10
|
readAllMemories: ((options?: CorpusReadOptions) => Promise<MemoryFile[]>) & {
|
|
10
11
|
supportsAbortSignal?: boolean;
|
|
11
12
|
};
|
|
@@ -30,6 +31,12 @@ export const BRIEFING_FULL_READ_FALLBACK_MS = 30_000;
|
|
|
30
31
|
export interface SafeReadMemoriesOptions {
|
|
31
32
|
/** Test seam overriding {@link BRIEFING_FULL_READ_FALLBACK_MS}. */
|
|
32
33
|
fallbackDeadlineMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Caller cancellation. The signal reaches the storage read so it stops at
|
|
36
|
+
* its next boundary, and a cancelled read is rethrown rather than degraded
|
|
37
|
+
* into an empty corpus (issue #3139).
|
|
38
|
+
*/
|
|
39
|
+
signal?: AbortSignal;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
type BriefingReadMode = "windowed" | "full-read-fallback";
|
|
@@ -138,6 +145,12 @@ export async function safeReadMemories(
|
|
|
138
145
|
resultCount = memories.length;
|
|
139
146
|
return memories;
|
|
140
147
|
} catch (err) {
|
|
148
|
+
if (isAbortError(err) && options.signal?.aborted) {
|
|
149
|
+
// Caller cancellation is not a read failure: rethrow it so the briefing
|
|
150
|
+
// rejects instead of degrading into an empty corpus (issue #3139). The
|
|
151
|
+
// own fallback deadline is reported as BriefingReadTimedOut below.
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
141
154
|
if (err instanceof BriefingReadTimedOut) {
|
|
142
155
|
outcome = "timeout";
|
|
143
156
|
bestEffortLog(
|
|
@@ -174,7 +187,9 @@ async function readBriefingMemories(
|
|
|
174
187
|
// the full corpus on cache misses; keep the full-read fallback for custom
|
|
175
188
|
// StorageManager-compatible callers that predate readMemoriesWindow().
|
|
176
189
|
if (mode === "windowed") {
|
|
177
|
-
const
|
|
190
|
+
const readOptions: WindowedMemoryReadOptions = { updatedAfter: window.from };
|
|
191
|
+
if (options.signal) readOptions.abortSignal = options.signal;
|
|
192
|
+
const result = await storage.readMemoriesWindow!(readOptions);
|
|
178
193
|
return result.memories;
|
|
179
194
|
}
|
|
180
195
|
// Refuse legacy adapters that do not explicitly declare cancellation support (#2827).
|
|
@@ -195,12 +210,13 @@ async function readBriefingMemories(
|
|
|
195
210
|
deadlineFired = true;
|
|
196
211
|
controller.abort();
|
|
197
212
|
}, deadlineMs);
|
|
213
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
198
214
|
try {
|
|
199
215
|
// Direct await, no Promise.race: the signal IS the deadline. A
|
|
200
216
|
// signal-aware read settles on abort (AbortError at its next scan
|
|
201
217
|
// boundary, #2307), so nothing is left scanning in the background once
|
|
202
218
|
// this function returns.
|
|
203
|
-
const memories = await storage.readAllMemories({ abortSignal:
|
|
219
|
+
const memories = await storage.readAllMemories({ abortSignal: signal });
|
|
204
220
|
if (deadlineFired) {
|
|
205
221
|
// A double that resolves partial rows instead of rejecting on abort
|
|
206
222
|
// must not have them served as a complete corpus.
|
|
@@ -214,3 +230,16 @@ async function readBriefingMemories(
|
|
|
214
230
|
clearTimeout(timer);
|
|
215
231
|
}
|
|
216
232
|
}
|
|
233
|
+
|
|
234
|
+
export async function safeReadEntities(
|
|
235
|
+
storage: { readAllEntityFiles(options?: CorpusReadOptions): Promise<EntityFile[]> },
|
|
236
|
+
signal: AbortSignal,
|
|
237
|
+
): Promise<EntityFile[]> {
|
|
238
|
+
try {
|
|
239
|
+
return await storage.readAllEntityFiles({ abortSignal: signal });
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (isAbortError(err) && signal.aborted) throw err;
|
|
242
|
+
log.warn(`briefing: readAllEntityFiles failed: ${err}`);
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
package/src/briefing.ts
CHANGED
|
@@ -19,12 +19,14 @@ import { readFile } from "node:fs/promises";
|
|
|
19
19
|
import os from "node:os";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { log } from "./logger.js";
|
|
22
|
+
import { isAbortError, throwIfAborted } from "./abort-error.js";
|
|
23
|
+
import { withBriefingDeadline } from "./briefing-deadline.js";
|
|
22
24
|
import { withBriefingFollowupTimeout } from "./briefing-followup-timeout.js";
|
|
23
25
|
import { extractJsonCandidates } from "./json-extract.js";
|
|
24
26
|
import { drainPassiveCorrectionNotifications } from "./correction/passive-correction-notifications.js";
|
|
25
27
|
import { excludeSupportPassportPrivateMemories } from "./support-passport/card-projection.js";
|
|
26
28
|
import { normalizeEntityName, StorageManager } from "./storage.js";
|
|
27
|
-
import { safeReadMemories } from "./briefing-window.js";
|
|
29
|
+
import { safeReadEntities, safeReadMemories } from "./briefing-window.js";
|
|
28
30
|
import { readEnvVar, resolveHomeDir } from "./runtime/env.js";
|
|
29
31
|
import type {
|
|
30
32
|
BriefingActiveThread,
|
|
@@ -817,6 +819,8 @@ export interface BuildBriefingOptions {
|
|
|
817
819
|
followupGenerator?: BriefingFollowupGenerator;
|
|
818
820
|
/** Injected "now" — makes tests deterministic. */
|
|
819
821
|
now?: Date;
|
|
822
|
+
/** Caller cancellation must not consume correction notifications. */
|
|
823
|
+
abortSignal?: AbortSignal;
|
|
820
824
|
}
|
|
821
825
|
|
|
822
826
|
const MAX_ACTIVE_THREADS = 8;
|
|
@@ -830,15 +834,23 @@ const MAX_OPEN_COMMITMENTS = 8;
|
|
|
830
834
|
* omitted and `followupsUnavailableReason` is set.
|
|
831
835
|
*/
|
|
832
836
|
export async function buildBriefing(options: BuildBriefingOptions): Promise<BriefingResult> {
|
|
837
|
+
return withBriefingDeadline(
|
|
838
|
+
(signal) => buildBriefingWithSignal(options, signal),
|
|
839
|
+
options.abortSignal,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function buildBriefingWithSignal(options: BuildBriefingOptions, signal: AbortSignal): Promise<BriefingResult> {
|
|
833
844
|
const now = options.now ?? new Date();
|
|
834
845
|
const window = options.window ?? defaultWindow(now);
|
|
835
846
|
const maxFollowups = clampFollowups(options.maxFollowups);
|
|
836
847
|
const focus = options.focus ?? null;
|
|
837
848
|
|
|
838
849
|
const [allMemories, allEntities] = await Promise.all([
|
|
839
|
-
safeReadMemories(options.storage, window).then(excludeSupportPassportPrivateMemories),
|
|
840
|
-
safeReadEntities(options.storage),
|
|
850
|
+
safeReadMemories(options.storage, window, { signal }).then(excludeSupportPassportPrivateMemories),
|
|
851
|
+
safeReadEntities(options.storage, signal),
|
|
841
852
|
]);
|
|
853
|
+
throwIfAborted(signal, "briefing aborted");
|
|
842
854
|
|
|
843
855
|
const memoriesInWindow = filterMemoriesByWindow(allMemories, window);
|
|
844
856
|
const focusedMemories = focus
|
|
@@ -852,6 +864,7 @@ export async function buildBriefing(options: BuildBriefingOptions): Promise<Brie
|
|
|
852
864
|
const calendarLoadResult = options.calendarSource
|
|
853
865
|
? await loadTodayCalendar(options.calendarSource, now)
|
|
854
866
|
: undefined;
|
|
867
|
+
throwIfAborted(signal, "briefing aborted");
|
|
855
868
|
|
|
856
869
|
const calendarSourceErrors: BriefingCalendarSourceError[] =
|
|
857
870
|
calendarLoadResult?.error ? [calendarLoadResult.error] : [];
|
|
@@ -904,18 +917,20 @@ export async function buildBriefing(options: BuildBriefingOptions): Promise<Brie
|
|
|
904
917
|
log.warn(`briefing: ${followupsUnavailableReason}`);
|
|
905
918
|
}
|
|
906
919
|
}
|
|
920
|
+
throwIfAborted(signal, "briefing aborted");
|
|
907
921
|
|
|
908
922
|
const sections: BriefingSections = {
|
|
909
923
|
...sectionsBase,
|
|
910
924
|
suggestedFollowups: followups,
|
|
911
925
|
};
|
|
912
926
|
|
|
913
|
-
// Drain auto-applied passive-correction notifications (issue #1581)
|
|
914
|
-
//
|
|
915
|
-
//
|
|
927
|
+
// Drain auto-applied passive-correction notifications (issue #1581): cleared on read,
|
|
928
|
+
// so each surfaces in exactly one briefing. Fail-open except cancellation, which must
|
|
929
|
+
// not consume notifications nobody receives.
|
|
930
|
+
throwIfAborted(signal, "briefing aborted");
|
|
916
931
|
let correctionNotifications: BriefingResult["correctionNotifications"];
|
|
917
932
|
try {
|
|
918
|
-
const drained = await drainPassiveCorrectionNotifications(options.storage.dir);
|
|
933
|
+
const drained = await drainPassiveCorrectionNotifications(options.storage.dir, signal);
|
|
919
934
|
if (drained.length > 0) {
|
|
920
935
|
correctionNotifications = drained.map((n) => ({
|
|
921
936
|
planId: n.planId,
|
|
@@ -925,6 +940,7 @@ export async function buildBriefing(options: BuildBriefingOptions): Promise<Brie
|
|
|
925
940
|
}));
|
|
926
941
|
}
|
|
927
942
|
} catch (err) {
|
|
943
|
+
if (isAbortError(err)) throw err;
|
|
928
944
|
log.debug(`briefing: correction notification drain failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
929
945
|
}
|
|
930
946
|
|
|
@@ -977,15 +993,6 @@ function defaultWindow(now: Date): ParsedBriefingWindow {
|
|
|
977
993
|
return { from: new Date(now.getTime() - DAY_MS), to: now, label: "yesterday" };
|
|
978
994
|
}
|
|
979
995
|
|
|
980
|
-
async function safeReadEntities(storage: StorageManager): Promise<EntityFile[]> {
|
|
981
|
-
try {
|
|
982
|
-
return await storage.readAllEntityFiles();
|
|
983
|
-
} catch (err) {
|
|
984
|
-
log.warn(`briefing: readAllEntityFiles failed: ${err}`);
|
|
985
|
-
return [];
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
996
|
function memoryTimestamp(memory: MemoryFile): number {
|
|
990
997
|
const raw = memory.frontmatter.updated || memory.frontmatter.created;
|
|
991
998
|
if (!raw) return 0;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { scopedCaptureWriteLockLimits, withCaptureWriteLockBackoff } from "./capture-write-lock-scope.js";
|
|
4
|
+
|
|
5
|
+
const limits = { initialDelayMs: 20, maxDelayMs: 50, deadlineMs: 8_000 };
|
|
6
|
+
const expected = {
|
|
7
|
+
captureWriteLockInitialDelayMs: 20,
|
|
8
|
+
captureWriteLockMaxDelayMs: 50,
|
|
9
|
+
captureWriteLockDeadlineMs: 8_000,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
test("capture backoff scope belongs only to its storage and restores nested state after rejection", async () => {
|
|
13
|
+
const storage = { dir: "synthetic-shared-root" };
|
|
14
|
+
const other = { dir: storage.dir };
|
|
15
|
+
const result = await withCaptureWriteLockBackoff(storage, limits, async () => {
|
|
16
|
+
await Promise.resolve();
|
|
17
|
+
assert.deepEqual(scopedCaptureWriteLockLimits(storage), expected);
|
|
18
|
+
assert.equal(scopedCaptureWriteLockLimits(other), undefined);
|
|
19
|
+
await assert.rejects(
|
|
20
|
+
withCaptureWriteLockBackoff(other, { ...limits, deadlineMs: 4_000 }, async () => {
|
|
21
|
+
assert.equal(scopedCaptureWriteLockLimits(other)?.captureWriteLockDeadlineMs, 4_000);
|
|
22
|
+
assert.equal(scopedCaptureWriteLockLimits(storage), undefined);
|
|
23
|
+
throw new Error("peer apply failed");
|
|
24
|
+
}),
|
|
25
|
+
/peer apply failed/
|
|
26
|
+
);
|
|
27
|
+
return scopedCaptureWriteLockLimits(storage);
|
|
28
|
+
});
|
|
29
|
+
assert.deepEqual(result, expected);
|
|
30
|
+
assert.equal(scopedCaptureWriteLockLimits(storage), undefined);
|
|
31
|
+
assert.equal(scopedCaptureWriteLockLimits(other), undefined);
|
|
32
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { CaptureWriteLockBackoffLimits, CaptureWriteLockLimitSource } from "./capture-write-lock-backoff.js";
|
|
3
|
+
import type { StorageManager } from "./storage.js";
|
|
4
|
+
|
|
5
|
+
type CaptureLockOwner = Pick<StorageManager, "dir">;
|
|
6
|
+
|
|
7
|
+
const captureScope = new AsyncLocalStorage<{ storage: CaptureLockOwner; limits: CaptureWriteLockLimitSource }>();
|
|
8
|
+
|
|
9
|
+
export function withCaptureWriteLockBackoff<T>(
|
|
10
|
+
storage: CaptureLockOwner,
|
|
11
|
+
limits: CaptureWriteLockBackoffLimits,
|
|
12
|
+
run: () => Promise<T>
|
|
13
|
+
): Promise<T> {
|
|
14
|
+
return captureScope.run(
|
|
15
|
+
{
|
|
16
|
+
storage,
|
|
17
|
+
limits: {
|
|
18
|
+
captureWriteLockInitialDelayMs: limits.initialDelayMs,
|
|
19
|
+
captureWriteLockMaxDelayMs: limits.maxDelayMs,
|
|
20
|
+
captureWriteLockDeadlineMs: limits.deadlineMs,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
run
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function scopedCaptureWriteLockLimits(storage: CaptureLockOwner): CaptureWriteLockLimitSource | undefined {
|
|
28
|
+
const scope = captureScope.getStore();
|
|
29
|
+
return scope?.storage === storage ? scope.limits : undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import {
|
|
8
|
+
drainPassiveCorrectionNotifications,
|
|
9
|
+
enqueuePassiveCorrectionNotification,
|
|
10
|
+
} from "./passive-correction-notifications.js";
|
|
11
|
+
|
|
12
|
+
// Issue #3139 review: a briefing cancelled mid-drain must leave the queue
|
|
13
|
+
// intact, because the caller never receives what the drain consumed.
|
|
14
|
+
test("a drain cancelled after reading keeps the notification queue", async (t) => {
|
|
15
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "remnic-3139-drain-"));
|
|
16
|
+
t.after(() => fs.rm(dir, { recursive: true, force: true }));
|
|
17
|
+
await enqueuePassiveCorrectionNotification(dir, {
|
|
18
|
+
planId: "plan-1",
|
|
19
|
+
summary: "kept",
|
|
20
|
+
undoCommand: "undo",
|
|
21
|
+
createdAt: new Date(0).toISOString(),
|
|
22
|
+
});
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const read: (file: string, encoding: "utf-8") => Promise<string> = fs.readFile;
|
|
25
|
+
t.mock.method(fs, "readFile", async (file: string) => {
|
|
26
|
+
const content = await read(file, "utf-8");
|
|
27
|
+
controller.abort(new Error("deadline"));
|
|
28
|
+
return content;
|
|
29
|
+
});
|
|
30
|
+
syncBuiltinESMExports();
|
|
31
|
+
t.after(() => {
|
|
32
|
+
t.mock.restoreAll();
|
|
33
|
+
syncBuiltinESMExports();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await assert.rejects(drainPassiveCorrectionNotifications(dir, controller.signal), { name: "AbortError" });
|
|
37
|
+
|
|
38
|
+
t.mock.restoreAll();
|
|
39
|
+
syncBuiltinESMExports();
|
|
40
|
+
const kept = await drainPassiveCorrectionNotifications(dir);
|
|
41
|
+
assert.equal(kept.length, 1, "the cancelled drain must not consume the queue");
|
|
42
|
+
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { mkdir, readFile, rename, writeFile, unlink } from "node:fs/promises";
|
|
16
16
|
import path from "node:path";
|
|
17
|
+
import { throwIfAborted } from "../abort-error.js";
|
|
17
18
|
import { serializeMutations } from "../utils/serialize-mutations.js";
|
|
18
19
|
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
@@ -84,6 +85,7 @@ export async function enqueuePassiveCorrectionNotification(
|
|
|
84
85
|
*/
|
|
85
86
|
export async function drainPassiveCorrectionNotifications(
|
|
86
87
|
storageDir: string,
|
|
88
|
+
signal?: AbortSignal,
|
|
87
89
|
): Promise<PassiveCorrectionNotification[]> {
|
|
88
90
|
const filePath = notificationsPath(storageDir);
|
|
89
91
|
return serializeMutations(`passive-notification:${filePath}`, async () => {
|
|
@@ -96,6 +98,8 @@ export async function drainPassiveCorrectionNotifications(
|
|
|
96
98
|
throw err;
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
// Cancellation after the read must keep the queue: the caller never receives these.
|
|
102
|
+
throwIfAborted(signal, "briefing aborted");
|
|
99
103
|
const notifications: PassiveCorrectionNotification[] = [];
|
|
100
104
|
for (const line of raw.split("\n")) {
|
|
101
105
|
const trimmed = line.trim();
|