@remnic/core 9.63.0 → 9.63.2
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.d.ts +1 -1
- package/dist/access-admin-ops-surface.js +34 -33
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.d.ts +1 -1
- package/dist/access-authorization-probe.js +35 -34
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +34 -33
- package/dist/access-cli.js +99 -97
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +1 -1
- package/dist/access-extraction-force-flush.js +34 -33
- package/dist/access-http-lcm-compaction.d.ts +1 -1
- package/dist/access-http-lifecycle-flush.d.ts +1 -1
- package/dist/access-http-offline-stream.d.ts +1 -1
- package/dist/access-http-query.js +34 -33
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +47 -46
- package/dist/access-identity-continuity-surface.js +34 -33
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +34 -33
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +40 -39
- package/dist/access-namespace-preflight.js +34 -33
- package/dist/access-observe-write-surface.d.ts +1 -1
- package/dist/access-observe-write-surface.js +34 -33
- package/dist/access-operations-batch.js +35 -34
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-operations.js +39 -38
- package/dist/access-recall-concurrency.d.ts +1 -1
- package/dist/access-recall-concurrency.js +34 -33
- package/dist/access-recall-response.d.ts +1 -1
- package/dist/access-recall-response.js +34 -33
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +34 -33
- package/dist/access-schema.d.ts +88 -88
- package/dist/access-schema.js +3 -3
- package/dist/access-service-helpers.d.ts +1 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +34 -33
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/access-surface-catalog.js +36 -35
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/{auto-sync-Z52VWMNC.js → auto-sync-BKRJDMKX.js} +3 -3
- package/dist/behavior-learner.js +2 -2
- package/dist/briefing.js +15 -15
- package/dist/buffer.js +5 -4
- package/dist/causal-behavior.js +3 -3
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.js +18 -18
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory.js +2 -2
- package/dist/{chunk-5F6HMNMZ.js → chunk-2LA3H54D.js} +2 -2
- package/dist/{chunk-IRLH4NM2.js → chunk-2PSX5BJU.js} +3 -3
- package/dist/{chunk-JBPKEARU.js → chunk-2QSZNTDO.js} +7 -7
- package/dist/{chunk-IZRNVNPV.js → chunk-3LJ2L3T3.js} +2 -2
- package/dist/{chunk-CLJ5QS6J.js → chunk-44KHFMCM.js} +72 -64
- package/dist/{chunk-CLJ5QS6J.js.map → chunk-44KHFMCM.js.map} +1 -1
- package/dist/{chunk-JDI7G6EA.js → chunk-4NMVX5U7.js} +2 -2
- package/dist/{chunk-WFLBP5GC.js → chunk-4NZHHGVL.js} +25 -6
- package/dist/chunk-4NZHHGVL.js.map +1 -0
- package/dist/{chunk-ANMIOQPU.js → chunk-4XN35YQD.js} +478 -164
- package/dist/chunk-4XN35YQD.js.map +1 -0
- package/dist/{chunk-LJOIZPJH.js → chunk-5QZR2RA3.js} +2 -2
- package/dist/{chunk-FF6E6HOT.js → chunk-5UFQCU6M.js} +32 -32
- package/dist/{chunk-4HO3542O.js → chunk-5W5N7XJZ.js} +2 -2
- package/dist/{chunk-4Y2RHVCV.js → chunk-5W62VEFH.js} +4 -4
- package/dist/{chunk-DEOYTFA6.js → chunk-645WOU5I.js} +4 -52
- package/dist/chunk-645WOU5I.js.map +1 -0
- package/dist/{chunk-LTJTP5YQ.js → chunk-6D7PZKCJ.js} +10 -10
- package/dist/{chunk-BBHFAVMP.js → chunk-6F2UMP4H.js} +7 -7
- package/dist/{chunk-IIPJKCZB.js → chunk-6NAPOLRO.js} +3 -3
- package/dist/{chunk-CK3NQUSQ.js → chunk-6UZR7KEK.js} +2 -2
- package/dist/{chunk-2TYIAKU5.js → chunk-6YMEU6OQ.js} +2 -2
- package/dist/{chunk-H23GIXJC.js → chunk-A73QHOZP.js} +2 -2
- package/dist/{chunk-LLGDVG52.js → chunk-AJSZ2EK6.js} +3 -3
- package/dist/{chunk-HVKMJAEF.js → chunk-B2OOJCUX.js} +2 -2
- package/dist/chunk-BUYVPUEM.js +66 -0
- package/dist/chunk-BUYVPUEM.js.map +1 -0
- package/dist/{chunk-YTHQHH5U.js → chunk-C6WMSXCJ.js} +2 -2
- package/dist/{chunk-JKW663ZN.js → chunk-CIAI24TQ.js} +2 -2
- package/dist/{chunk-PD6O7AXF.js → chunk-CSAKJVXW.js} +14 -17
- package/dist/chunk-CSAKJVXW.js.map +1 -0
- package/dist/{chunk-EFWHCTYJ.js → chunk-DNRTKO2U.js} +2 -2
- package/dist/{chunk-QCECHYZL.js → chunk-E2GYBTSP.js} +24 -4
- package/dist/chunk-E2GYBTSP.js.map +1 -0
- package/dist/{chunk-4IZ2IT7D.js → chunk-EJLTNPRY.js} +3 -3
- package/dist/{chunk-JCJGYNIS.js → chunk-EJQXDKYK.js} +4 -4
- package/dist/{chunk-347ZR6D4.js → chunk-FKJVEW6O.js} +2 -2
- package/dist/{chunk-DROAPP6N.js → chunk-GFNS3BHV.js} +3 -3
- package/dist/{chunk-ALNZEWGN.js → chunk-GPFZFCRN.js} +2 -2
- package/dist/{chunk-KPML4Q3M.js → chunk-GTRGH4ZT.js} +3 -3
- package/dist/{chunk-TIRJV2LW.js → chunk-I6OQCT3Y.js} +2 -2
- package/dist/{chunk-CH627RRV.js → chunk-IKXUG6KC.js} +8 -8
- package/dist/{chunk-HSBQJRV2.js → chunk-IM56VPKO.js} +3 -3
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-BP6WMYLP.js → chunk-ITYAXU4S.js} +3 -3
- package/dist/{chunk-4FFROSFG.js → chunk-JJ7JW62R.js} +23 -23
- package/dist/{chunk-RFH7U5LT.js → chunk-JJX5XEIG.js} +2 -2
- package/dist/{chunk-CALURYWR.js → chunk-JWPG65LH.js} +14 -14
- package/dist/chunk-K2XUWZBM.js +417 -0
- package/dist/chunk-K2XUWZBM.js.map +1 -0
- package/dist/{chunk-G2FCSILA.js → chunk-KWIGDKM2.js} +2 -2
- package/dist/{chunk-WR2YKUIW.js → chunk-KWN6NOHQ.js} +2 -2
- package/dist/{chunk-227UFSEQ.js → chunk-LEPOP46W.js} +4 -4
- package/dist/{chunk-WMOJIOIG.js → chunk-LZCSOUJ7.js} +2 -2
- package/dist/{chunk-256FBCVX.js → chunk-M4AHBM72.js} +7 -7
- package/dist/{chunk-5PFGMO33.js → chunk-MEW53VQJ.js} +2 -2
- package/dist/{chunk-DCUMFQYM.js → chunk-ML3U4KPR.js} +2 -2
- package/dist/{chunk-X6E7TT3A.js → chunk-MR5HWQHS.js} +2 -2
- package/dist/{chunk-H7PRHUBD.js → chunk-NRLELHAY.js} +2 -2
- package/dist/{chunk-ZS4JADYY.js → chunk-OW7NVMGQ.js} +2 -2
- package/dist/{chunk-TGSMU3F2.js → chunk-PADG72LJ.js} +3 -3
- package/dist/{chunk-6GGTATZF.js → chunk-PYIR4SSB.js} +4 -4
- package/dist/chunk-PYIR4SSB.js.map +1 -0
- package/dist/{chunk-UDTME32W.js → chunk-Q35XGIEM.js} +2 -2
- package/dist/{chunk-XPRJSHNI.js → chunk-QJ622LTI.js} +6 -3
- package/dist/chunk-QJ622LTI.js.map +1 -0
- package/dist/{chunk-AYIHDC5S.js → chunk-QSIQVC32.js} +2 -2
- package/dist/{chunk-3MA7JVXN.js → chunk-R74NCKEY.js} +4 -4
- package/dist/{chunk-QCI73Z7C.js → chunk-RYGJGXL6.js} +2 -2
- package/dist/{chunk-4OW2T32G.js → chunk-SQ3EGGII.js} +2 -2
- package/dist/{chunk-ZPGHA7EO.js → chunk-TRPX6F2G.js} +10 -10
- package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
- package/dist/{chunk-7BJYTX6Q.js → chunk-VAMQU4W7.js} +2 -2
- package/dist/{chunk-YSR3DXGC.js → chunk-VN7IIQGS.js} +2 -2
- package/dist/{chunk-DTLF2SLZ.js → chunk-WDUG2CH3.js} +2 -2
- package/dist/{chunk-YIIC5IDU.js → chunk-X4MGNVOO.js} +2 -2
- package/dist/{chunk-BHO6RHE5.js → chunk-XFCQIQQU.js} +3 -3
- package/dist/{chunk-PSNN25DW.js → chunk-XZPSBGHA.js} +2 -2
- package/dist/{chunk-PMRUMGK6.js → chunk-Y7COCXY2.js} +2 -2
- package/dist/{chunk-DJ7HMNSL.js → chunk-YCVB7X56.js} +4 -2
- package/dist/chunk-YCVB7X56.js.map +1 -0
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-JFLKSD5V.js → chunk-Z334TJRB.js} +2 -2
- package/dist/{cli-Bw9z-KqX.d.ts → cli-DJwNNDL1.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +68 -67
- package/dist/coding/pre-action-gate.js +3 -3
- package/dist/compounding/engine.js +15 -15
- package/dist/connectors/codex-materialize-runner.js +15 -15
- package/dist/connectors/index.d.ts +45 -1
- package/dist/connectors/index.js +24 -16
- package/dist/convergence-refresh.js +17 -17
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +17 -17
- package/dist/dashboard-runtime.js +2 -2
- package/dist/direct-answer-wiring.js +3 -3
- package/dist/direct-answer.js +2 -2
- package/dist/entity-retrieval.js +15 -15
- package/dist/event-order-query.d.ts +3 -0
- package/dist/event-order-query.js +9 -0
- package/dist/event-order-query.js.map +1 -0
- package/dist/event-order-recall.d.ts +2 -2
- package/dist/event-order-recall.js +5 -2
- package/dist/external-wiki-access.d.ts +1 -1
- package/dist/external-wiki-access.js +35 -34
- package/dist/external-wiki-mcp-tools.d.ts +1 -1
- package/dist/extraction-normalization.js +2 -2
- package/dist/extraction.js +25 -24
- package/dist/harmonic-construction.js +2 -2
- package/dist/harmonic-retrieval.js +2 -2
- package/dist/index.d.ts +683 -682
- package/dist/index.js +108 -105
- package/dist/intent.js +2 -1
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +6 -6
- package/dist/maintenance/memory-governance.js +15 -15
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
- package/dist/maintenance/rebuild-memory-projection.js +16 -16
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +16 -16
- package/dist/namespaces/search.js +10 -10
- package/dist/namespaces/storage.js +15 -15
- package/dist/operator-doctor-corpus.js +18 -18
- package/dist/operator-toolkit.js +22 -22
- package/dist/orchestration/maintenance.js +20 -20
- package/dist/orchestrator.js +101 -99
- package/dist/policy-runtime.js +3 -3
- package/dist/{public-http-DK-QwaBu.d.ts → public-http-43z1YT-k.d.ts} +1 -1
- package/dist/{qmd-D_62bYNl.d.ts → qmd-CBSxevyb.d.ts} +12 -1
- package/dist/qmd-preflight.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/qmd.js +3 -1
- package/dist/recall-planner-i18n.d.ts +31 -0
- package/dist/recall-planner-i18n.js +30 -0
- package/dist/recall-planner-i18n.js.map +1 -0
- package/dist/recall-planner-llm.js +2 -1
- package/dist/recall-planner-llm.js.map +1 -1
- package/dist/recall-query-policy.js +2 -2
- package/dist/recall-tokenization.js +1 -1
- package/dist/resume-bundles.js +3 -3
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-agents.js +2 -1
- package/dist/schemas.d.ts +117 -434
- package/dist/schemas.js +1 -1
- package/dist/search/factory.js +9 -9
- package/dist/search/index.js +15 -15
- package/dist/search/orama-backend.js +2 -2
- package/dist/semantic-consolidation.js +16 -16
- package/dist/semantic-rule-promotion.js +15 -15
- package/dist/semantic-rule-verifier.js +15 -15
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +21 -20
- package/dist/storage.js +14 -14
- package/dist/support-passport/index.d.ts +4 -4
- package/dist/support-passport/index.js +37 -36
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/trust-zones.js +2 -2
- package/dist/verified-recall.js +15 -15
- package/dist/work-product-ledger.js +2 -2
- package/package.json +2 -2
- package/src/connectors/droid-mcp.ts +434 -0
- package/src/connectors/index.ts +70 -0
- package/src/direct-answer.ts +6 -3
- package/src/event-order-query.ts +78 -0
- package/src/event-order-recall.ts +4 -90
- package/src/intent.ts +19 -19
- package/src/orchestration/turn-ingestion.ts +24 -1
- package/src/orchestrator-flush.test.ts +142 -1
- package/src/qmd-process-group.test.ts +125 -0
- package/src/qmd.ts +30 -2
- package/src/recall-planner-i18n.ts +193 -0
- package/src/recall-tokenization.ts +3 -1
- package/src/retrieval-agents.ts +6 -10
- package/src/schemas.test.ts +158 -0
- package/src/schemas.ts +45 -22
- package/dist/chunk-6GGTATZF.js.map +0 -1
- package/dist/chunk-ANMIOQPU.js.map +0 -1
- package/dist/chunk-DEOYTFA6.js.map +0 -1
- package/dist/chunk-DJ7HMNSL.js.map +0 -1
- package/dist/chunk-PD6O7AXF.js.map +0 -1
- package/dist/chunk-QCECHYZL.js.map +0 -1
- package/dist/chunk-WFLBP5GC.js.map +0 -1
- package/dist/chunk-XPRJSHNI.js.map +0 -1
- package/dist/{access-service-M4LgO141.d.ts → access-service-DBpEnlzA.d.ts} +64 -64
- /package/dist/{auto-sync-Z52VWMNC.js.map → auto-sync-BKRJDMKX.js.map} +0 -0
- /package/dist/{chunk-5F6HMNMZ.js.map → chunk-2LA3H54D.js.map} +0 -0
- /package/dist/{chunk-IRLH4NM2.js.map → chunk-2PSX5BJU.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-2QSZNTDO.js.map} +0 -0
- /package/dist/{chunk-IZRNVNPV.js.map → chunk-3LJ2L3T3.js.map} +0 -0
- /package/dist/{chunk-JDI7G6EA.js.map → chunk-4NMVX5U7.js.map} +0 -0
- /package/dist/{chunk-LJOIZPJH.js.map → chunk-5QZR2RA3.js.map} +0 -0
- /package/dist/{chunk-FF6E6HOT.js.map → chunk-5UFQCU6M.js.map} +0 -0
- /package/dist/{chunk-4HO3542O.js.map → chunk-5W5N7XJZ.js.map} +0 -0
- /package/dist/{chunk-4Y2RHVCV.js.map → chunk-5W62VEFH.js.map} +0 -0
- /package/dist/{chunk-LTJTP5YQ.js.map → chunk-6D7PZKCJ.js.map} +0 -0
- /package/dist/{chunk-BBHFAVMP.js.map → chunk-6F2UMP4H.js.map} +0 -0
- /package/dist/{chunk-IIPJKCZB.js.map → chunk-6NAPOLRO.js.map} +0 -0
- /package/dist/{chunk-CK3NQUSQ.js.map → chunk-6UZR7KEK.js.map} +0 -0
- /package/dist/{chunk-2TYIAKU5.js.map → chunk-6YMEU6OQ.js.map} +0 -0
- /package/dist/{chunk-H23GIXJC.js.map → chunk-A73QHOZP.js.map} +0 -0
- /package/dist/{chunk-LLGDVG52.js.map → chunk-AJSZ2EK6.js.map} +0 -0
- /package/dist/{chunk-HVKMJAEF.js.map → chunk-B2OOJCUX.js.map} +0 -0
- /package/dist/{chunk-YTHQHH5U.js.map → chunk-C6WMSXCJ.js.map} +0 -0
- /package/dist/{chunk-JKW663ZN.js.map → chunk-CIAI24TQ.js.map} +0 -0
- /package/dist/{chunk-EFWHCTYJ.js.map → chunk-DNRTKO2U.js.map} +0 -0
- /package/dist/{chunk-4IZ2IT7D.js.map → chunk-EJLTNPRY.js.map} +0 -0
- /package/dist/{chunk-JCJGYNIS.js.map → chunk-EJQXDKYK.js.map} +0 -0
- /package/dist/{chunk-347ZR6D4.js.map → chunk-FKJVEW6O.js.map} +0 -0
- /package/dist/{chunk-DROAPP6N.js.map → chunk-GFNS3BHV.js.map} +0 -0
- /package/dist/{chunk-ALNZEWGN.js.map → chunk-GPFZFCRN.js.map} +0 -0
- /package/dist/{chunk-KPML4Q3M.js.map → chunk-GTRGH4ZT.js.map} +0 -0
- /package/dist/{chunk-TIRJV2LW.js.map → chunk-I6OQCT3Y.js.map} +0 -0
- /package/dist/{chunk-CH627RRV.js.map → chunk-IKXUG6KC.js.map} +0 -0
- /package/dist/{chunk-HSBQJRV2.js.map → chunk-IM56VPKO.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-BP6WMYLP.js.map → chunk-ITYAXU4S.js.map} +0 -0
- /package/dist/{chunk-4FFROSFG.js.map → chunk-JJ7JW62R.js.map} +0 -0
- /package/dist/{chunk-RFH7U5LT.js.map → chunk-JJX5XEIG.js.map} +0 -0
- /package/dist/{chunk-CALURYWR.js.map → chunk-JWPG65LH.js.map} +0 -0
- /package/dist/{chunk-G2FCSILA.js.map → chunk-KWIGDKM2.js.map} +0 -0
- /package/dist/{chunk-WR2YKUIW.js.map → chunk-KWN6NOHQ.js.map} +0 -0
- /package/dist/{chunk-227UFSEQ.js.map → chunk-LEPOP46W.js.map} +0 -0
- /package/dist/{chunk-WMOJIOIG.js.map → chunk-LZCSOUJ7.js.map} +0 -0
- /package/dist/{chunk-256FBCVX.js.map → chunk-M4AHBM72.js.map} +0 -0
- /package/dist/{chunk-5PFGMO33.js.map → chunk-MEW53VQJ.js.map} +0 -0
- /package/dist/{chunk-DCUMFQYM.js.map → chunk-ML3U4KPR.js.map} +0 -0
- /package/dist/{chunk-X6E7TT3A.js.map → chunk-MR5HWQHS.js.map} +0 -0
- /package/dist/{chunk-H7PRHUBD.js.map → chunk-NRLELHAY.js.map} +0 -0
- /package/dist/{chunk-ZS4JADYY.js.map → chunk-OW7NVMGQ.js.map} +0 -0
- /package/dist/{chunk-TGSMU3F2.js.map → chunk-PADG72LJ.js.map} +0 -0
- /package/dist/{chunk-UDTME32W.js.map → chunk-Q35XGIEM.js.map} +0 -0
- /package/dist/{chunk-AYIHDC5S.js.map → chunk-QSIQVC32.js.map} +0 -0
- /package/dist/{chunk-3MA7JVXN.js.map → chunk-R74NCKEY.js.map} +0 -0
- /package/dist/{chunk-QCI73Z7C.js.map → chunk-RYGJGXL6.js.map} +0 -0
- /package/dist/{chunk-4OW2T32G.js.map → chunk-SQ3EGGII.js.map} +0 -0
- /package/dist/{chunk-ZPGHA7EO.js.map → chunk-TRPX6F2G.js.map} +0 -0
- /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
- /package/dist/{chunk-7BJYTX6Q.js.map → chunk-VAMQU4W7.js.map} +0 -0
- /package/dist/{chunk-YSR3DXGC.js.map → chunk-VN7IIQGS.js.map} +0 -0
- /package/dist/{chunk-DTLF2SLZ.js.map → chunk-WDUG2CH3.js.map} +0 -0
- /package/dist/{chunk-YIIC5IDU.js.map → chunk-X4MGNVOO.js.map} +0 -0
- /package/dist/{chunk-BHO6RHE5.js.map → chunk-XFCQIQQU.js.map} +0 -0
- /package/dist/{chunk-PSNN25DW.js.map → chunk-XZPSBGHA.js.map} +0 -0
- /package/dist/{chunk-PMRUMGK6.js.map → chunk-Y7COCXY2.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-JFLKSD5V.js.map → chunk-Z334TJRB.js.map} +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { EVENT_ORDER_CUES, containsRecallCue } from "./recall-planner-i18n.js";
|
|
2
|
+
|
|
3
|
+
function isTimelineSummaryQuery(normalized: string): boolean {
|
|
4
|
+
return (
|
|
5
|
+
/\b(?:summarize|summary|major progress|what happened|develop(?:ed|ment)?|approached)\b/.test(
|
|
6
|
+
normalized,
|
|
7
|
+
) &&
|
|
8
|
+
(/\b(?:over time|throughout|between|project|progress|timeline|journey|sessions?|along the way)\b/.test(
|
|
9
|
+
normalized,
|
|
10
|
+
) ||
|
|
11
|
+
/\bfrom\b.+\bthrough\b/.test(normalized))
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function shouldRecallEventOrderEvidence(query: string): boolean {
|
|
16
|
+
const normalized = query.toLowerCase();
|
|
17
|
+
|
|
18
|
+
if (containsRecallCue(normalized, EVENT_ORDER_CUES)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
/\border in which\b/.test(normalized) ||
|
|
24
|
+
/\bsequence in which\b/.test(normalized) ||
|
|
25
|
+
/\breconstruct\b.*\btimeline\b/.test(normalized) ||
|
|
26
|
+
/\btimeline\b.*\bin order\b/.test(normalized) ||
|
|
27
|
+
/\bsequence\b.*\bin order\b/.test(normalized) ||
|
|
28
|
+
/\bintroduced\b.*\bin order\b/.test(normalized) ||
|
|
29
|
+
(/\bwalk me through\b/.test(normalized) && /\bin order\b/.test(normalized)) ||
|
|
30
|
+
(/\bin order\b/.test(normalized) &&
|
|
31
|
+
/\b(?:develop(?:ed|ment)?|evolv(?:e|ed|ing)?|progress|throughout|conversations?|sessions?)\b/.test(
|
|
32
|
+
normalized,
|
|
33
|
+
)) ||
|
|
34
|
+
/\bprogress\b.*\bin order\b/.test(normalized) ||
|
|
35
|
+
/\bchronological(?:ly| order)?\b/.test(normalized) ||
|
|
36
|
+
isTimelineSummaryQuery(normalized)
|
|
37
|
+
) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (/\bwhich\b.*\bfirst\b/.test(normalized) || /\bwhat was the first\b/.test(normalized)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (/\bhappened first\b/.test(normalized) || /\bwhich\b.*\bhappened\b/.test(normalized)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (/\bhow many days\b/.test(normalized)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (/\bwhen did\b/.test(normalized)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (/\bwhich\b.*\blast\b/.test(normalized) || /\bthe last\b.*\bi\b/.test(normalized)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (/\bwho\b.*\bfirst\b/.test(normalized)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (/\bmost recent(?:ly)?\b/.test(normalized)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (/\border of\b/.test(normalized) || /\bfrom earliest to latest\b/.test(normalized)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (/\bhow long\b/.test(normalized)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (/\bhow many (months|weeks|years)\b/.test(normalized)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (/\bhow many\b.*\bbefore\b/.test(normalized)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (/\bhow old\b.*\bwhen\b/.test(normalized)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
type RankedEvidenceItem,
|
|
8
8
|
} from "./recall-pipeline-stages.js";
|
|
9
9
|
|
|
10
|
+
import { shouldRecallEventOrderEvidence } from "./event-order-query.js";
|
|
11
|
+
export { shouldRecallEventOrderEvidence };
|
|
12
|
+
|
|
13
|
+
|
|
10
14
|
export interface EventOrderRecallOptions {
|
|
11
15
|
engine: ExplicitCueRecallEngine | null | undefined;
|
|
12
16
|
// event-order reads a SINGLE LCM session key. Unlike the relevance-ranked
|
|
@@ -28,87 +32,6 @@ const DEFAULT_SCAN_WINDOW_TURNS = 12;
|
|
|
28
32
|
const DEFAULT_SCAN_WINDOW_TOKENS = 24_000;
|
|
29
33
|
const DEFAULT_MAX_ITEMS = 24;
|
|
30
34
|
|
|
31
|
-
export function shouldRecallEventOrderEvidence(query: string): boolean {
|
|
32
|
-
const normalized = query.toLowerCase();
|
|
33
|
-
|
|
34
|
-
// --- Existing event-order patterns (LongMemEval order-reconstruction phrasing) ---
|
|
35
|
-
if (
|
|
36
|
-
/\border in which\b/.test(normalized) ||
|
|
37
|
-
/\bsequence in which\b/.test(normalized) ||
|
|
38
|
-
/\breconstruct\b.*\btimeline\b/.test(normalized) ||
|
|
39
|
-
/\btimeline\b.*\bin order\b/.test(normalized) ||
|
|
40
|
-
/\bsequence\b.*\bin order\b/.test(normalized) ||
|
|
41
|
-
/\bintroduced\b.*\bin order\b/.test(normalized) ||
|
|
42
|
-
(/\bwalk me through\b/.test(normalized) && /\bin order\b/.test(normalized)) ||
|
|
43
|
-
(/\bin order\b/.test(normalized) &&
|
|
44
|
-
/\b(?:develop(?:ed|ment)?|evolv(?:e|ed|ing)?|progress|throughout|conversations?|sessions?)\b/.test(normalized)) ||
|
|
45
|
-
/\bprogress\b.*\bin order\b/.test(normalized) ||
|
|
46
|
-
/\bchronological(?:ly| order)?\b/.test(normalized) ||
|
|
47
|
-
isTimelineSummaryQuery(normalized)
|
|
48
|
-
) {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// --- Temporal-reasoning patterns (LongMemEval temporal-reasoning question type) ---
|
|
53
|
-
// These question shapes need chronological evidence to answer correctly:
|
|
54
|
-
// ordering ("which happened first"), relative time ("how many days
|
|
55
|
-
// between"), and recency ("which did I [verb] last"). The event-order tier
|
|
56
|
-
// surfaces a chronologically sorted evidence pack so the responder can
|
|
57
|
-
// reconstruct the answer from an ordered timeline rather than fragments.
|
|
58
|
-
// Verified against the LongMemEval-oracle dataset: 0% of the 133
|
|
59
|
-
// temporal-reasoning questions matched the original heuristic above.
|
|
60
|
-
|
|
61
|
-
// "Which X did I Y first, A or B?" / "What was the first X ...?"
|
|
62
|
-
if (/\bwhich\b.*\bfirst\b/.test(normalized) || /\bwhat was the first\b/.test(normalized)) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
// "Which event happened first, A or B?" / "Which happened first"
|
|
66
|
-
if (/\bhappened first\b/.test(normalized) || /\bwhich\b.*\bhappened\b/.test(normalized)) {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
// "How many days before/between/after/did it take"
|
|
70
|
-
if (/\bhow many days\b/.test(normalized)) {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
// "When did I ..." (date-shaped)
|
|
74
|
-
if (/\bwhen did\b/.test(normalized)) {
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
// "Which X did I Y last?" / "the last X I ..."
|
|
78
|
-
if (/\bwhich\b.*\blast\b/.test(normalized) || /\bthe last\b.*\bi\b/.test(normalized)) {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
// "Who did I meet/become first, X or Y?" (person ordering)
|
|
82
|
-
if (/\bwho\b.*\bfirst\b/.test(normalized)) {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
// "most recently" / "most recent" (recency)
|
|
86
|
-
if (/\bmost recent(?:ly)?\b/.test(normalized)) {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
// "What is the order of..." / "...from earliest to latest"
|
|
90
|
-
if (/\border of\b/.test(normalized) || /\bfrom earliest to latest\b/.test(normalized)) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// "How long have/did/had I been..." (duration)
|
|
95
|
-
if (/\bhow long\b/.test(normalized)) {
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
// "How many months/weeks/years ago/before" (relative time)
|
|
99
|
-
if (/\bhow many (months|weeks|years)\b/.test(normalized)) {
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
// "How many X did I [verb] before Y" (temporal comparison)
|
|
103
|
-
if (/\bhow many\b.*\bbefore\b/.test(normalized)) {
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
// "How old was I when..." (age-at-time)
|
|
107
|
-
if (/\bhow old\b.*\bwhen\b/.test(normalized)) {
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
35
|
|
|
113
36
|
export async function buildEventOrderRecallSection(
|
|
114
37
|
options: EventOrderRecallOptions,
|
|
@@ -1693,15 +1616,6 @@ function deriveChronologicalCueLabels(content: string, query: string): string[]
|
|
|
1693
1616
|
return [...labels].slice(0, 5);
|
|
1694
1617
|
}
|
|
1695
1618
|
|
|
1696
|
-
function isTimelineSummaryQuery(normalized: string): boolean {
|
|
1697
|
-
return /\b(?:summarize|summary|major progress|what happened|develop(?:ed|ment)?|approached)\b/.test(
|
|
1698
|
-
normalized,
|
|
1699
|
-
) &&
|
|
1700
|
-
(/\b(?:over time|throughout|between|project|progress|timeline|journey|sessions?|along the way)\b/.test(
|
|
1701
|
-
normalized,
|
|
1702
|
-
) ||
|
|
1703
|
-
/\bfrom\b.+\bthrough\b/.test(normalized));
|
|
1704
|
-
}
|
|
1705
1619
|
|
|
1706
1620
|
function addLabelIf(
|
|
1707
1621
|
labels: Set<string>,
|
package/src/intent.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CAUSAL_CHAIN_CUES,
|
|
3
|
+
GRAPH_MODE_CUES,
|
|
4
|
+
containsRecallCue,
|
|
5
|
+
endsWithQuestionMark,
|
|
6
|
+
isMultilingualAck,
|
|
7
|
+
stripTrailingRecallNoise,
|
|
8
|
+
} from "./recall-planner-i18n.js";
|
|
1
9
|
import type { MemoryIntent, RecallPlanMode } from "./types.js";
|
|
2
10
|
|
|
3
11
|
const GOAL_PATTERNS: Array<{ re: RegExp; goal: string }> = [
|
|
@@ -100,35 +108,26 @@ export function intentCompatibilityScore(queryIntent: MemoryIntent, memoryIntent
|
|
|
100
108
|
|
|
101
109
|
export function planRecallMode(prompt: string): RecallPlanMode {
|
|
102
110
|
const p = normalizeTextInput(prompt).trim();
|
|
103
|
-
let ackCandidate = p;
|
|
104
|
-
while (ackCandidate.length > 0) {
|
|
105
|
-
const ch = ackCandidate.charCodeAt(ackCandidate.length - 1);
|
|
106
|
-
const isDigit = ch >= 48 && ch <= 57;
|
|
107
|
-
const isUpper = ch >= 65 && ch <= 90;
|
|
108
|
-
const isLower = ch >= 97 && ch <= 122;
|
|
109
|
-
// Strip any trailing non-alphanumeric noise (punctuation/emojis/symbols).
|
|
110
|
-
if (isDigit || isUpper || isLower) break;
|
|
111
|
-
ackCandidate = ackCandidate.slice(0, -1);
|
|
112
|
-
}
|
|
113
|
-
ackCandidate = ackCandidate.trim();
|
|
114
111
|
if (p.length === 0) return "no_recall";
|
|
115
112
|
|
|
116
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
/\b(timeline|sequence|history|what happened|chain of events|root cause)\b/i.test(p) ||
|
|
115
|
+
containsRecallCue(p, GRAPH_MODE_CUES)
|
|
116
|
+
) {
|
|
117
117
|
return "graph_mode";
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// Reserve no_recall for low-information acknowledgements; avoid broad regressions.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
) {
|
|
121
|
+
// Multilingual table (issue #2191): Japanese/Chinese/Korean/Russian/etc acks
|
|
122
|
+
// route the same way; the exact-match set cannot match longer English prompts.
|
|
123
|
+
if (p.length <= 18 && isMultilingualAck(stripTrailingRecallNoise(p))) {
|
|
125
124
|
return "no_recall";
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
// Full recall for prompts that are explicitly memory-seeking or analytical questions.
|
|
129
128
|
if (
|
|
130
129
|
/\b(previous|earlier|remember|last time|did we|what did we decide|context|summarize|summary|recap|key points|decision)\b/i.test(p) ||
|
|
131
|
-
|
|
130
|
+
endsWithQuestionMark(p) ||
|
|
132
131
|
/^(what|why|how|when|where|who|which)\b/i.test(p.toLowerCase())
|
|
133
132
|
) {
|
|
134
133
|
return "full";
|
|
@@ -148,7 +147,8 @@ export function planRecallMode(prompt: string): RecallPlanMode {
|
|
|
148
147
|
export function hasBroadGraphIntent(prompt: string): boolean {
|
|
149
148
|
const p = normalizeTextInput(prompt).trim().toLowerCase();
|
|
150
149
|
if (!p) return false;
|
|
151
|
-
return
|
|
152
|
-
p
|
|
150
|
+
return (
|
|
151
|
+
/\b(what changed|how did we get here|why did this happen|what led to|cause chain|dependency chain|regression chain|failure chain)\b/i.test(p) ||
|
|
152
|
+
containsRecallCue(p, CAUSAL_CHAIN_CUES)
|
|
153
153
|
);
|
|
154
154
|
}
|
|
@@ -123,6 +123,25 @@ export interface TurnIngestionOptions extends SourceConnectorProvenance {
|
|
|
123
123
|
turnFingerprint?: string;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Flush-plan recovery imports are compacted material the host already
|
|
128
|
+
* summarized before appending it to the flush-plan file. Feeding those
|
|
129
|
+
* turns through LCM observation again re-summarizes the same content
|
|
130
|
+
* (issue #2457). The producer stamps both a dedicated `sourceLabel` and a
|
|
131
|
+
* `:flush-plan[:chunk]`-shaped `sourceId`, so either marker identifies a
|
|
132
|
+
* recovery turn; ordinary bulk-import turns never carry either.
|
|
133
|
+
*/
|
|
134
|
+
function isFlushPlanRecoveryTurn(
|
|
135
|
+
turn: Pick<BufferTurn, "importProvenance">,
|
|
136
|
+
): boolean {
|
|
137
|
+
const provenance = turn.importProvenance;
|
|
138
|
+
if (provenance?.sourceLabel === "OpenClaw flush plan") return true;
|
|
139
|
+
return (
|
|
140
|
+
typeof provenance?.sourceId === "string" &&
|
|
141
|
+
/:flush-plan(:\d+\/\d+)?$/.test(provenance.sourceId)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
126
145
|
export class TurnIngestionCoordinator {
|
|
127
146
|
constructor(
|
|
128
147
|
private readonly deps: TurnIngestionDeps,
|
|
@@ -456,7 +475,11 @@ export class TurnIngestionCoordinator {
|
|
|
456
475
|
};
|
|
457
476
|
}
|
|
458
477
|
|
|
459
|
-
|
|
478
|
+
// Skip LCM only when every turn is flush-plan recovery material; a mixed
|
|
479
|
+
// batch keeps the ordinary observe path (issue #2457). Extraction and
|
|
480
|
+
// persistence below are unaffected either way.
|
|
481
|
+
const skipLcmObservation = sessionTurns.every(isFlushPlanRecoveryTurn);
|
|
482
|
+
if (!skipLcmObservation && this.deps.lcmEngine?.enabled) {
|
|
460
483
|
await this.deps.lcmEngine.observeMessages(
|
|
461
484
|
sessionKey,
|
|
462
485
|
sessionTurns.map((turn) => ({
|
|
@@ -12,8 +12,10 @@ import { TurnIngestionCoordinator } from "./orchestration/turn-ingestion.js";
|
|
|
12
12
|
import { SmartBuffer } from "./buffer.js";
|
|
13
13
|
import { parseConfig } from "./config.js";
|
|
14
14
|
import { stableHash } from "./coding/git-context.js";
|
|
15
|
-
import type { BufferState, BufferTurn } from "./types.js";
|
|
15
|
+
import type { BufferState, BufferTurn, PluginConfig } from "./types.js";
|
|
16
16
|
import type { ImportTurn } from "./bulk-import/types.js";
|
|
17
|
+
import type { LcmEngine } from "./lcm/index.js";
|
|
18
|
+
import type { ExtractionRunResult } from "./orchestration/extraction-run.js";
|
|
17
19
|
import { namespaceIdentityToken } from "./namespaces/identity.js";
|
|
18
20
|
import { readNamespaceMaintenanceStatuses } from "./maintenance/namespace-planner.js";
|
|
19
21
|
import { stubPersistExtraction } from "./testing/orchestrator-lite.js";
|
|
@@ -28,6 +30,22 @@ function makeTurn(sessionKey: string, content: string, sessionOwnerPrincipal?: s
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Writable orchestrator double for `ingestBulkImportBatch` slice tests: the
|
|
35
|
+
* real prototype methods run, but the per-test stub surface must stay
|
|
36
|
+
* assignable (the real class marks these fields readonly).
|
|
37
|
+
*/
|
|
38
|
+
interface BulkImportTestDouble {
|
|
39
|
+
config: PluginConfig;
|
|
40
|
+
extractionQueueCoordinator: ExtractionQueueCoordinator;
|
|
41
|
+
lcmEngine: LcmEngine | null;
|
|
42
|
+
runExtraction: (
|
|
43
|
+
turns: BufferTurn[],
|
|
44
|
+
options?: Record<string, unknown>,
|
|
45
|
+
) => Promise<ExtractionRunResult>;
|
|
46
|
+
ingestBulkImportBatch: Orchestrator["ingestBulkImportBatch"];
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
interface ScopedFlushTestDouble {
|
|
32
50
|
buffer: {
|
|
33
51
|
getTurns(bufferKey: string): BufferTurn[];
|
|
@@ -804,6 +822,129 @@ test("ingestBulkImportBatch can disable source-valid-at replay context", async (
|
|
|
804
822
|
);
|
|
805
823
|
});
|
|
806
824
|
|
|
825
|
+
test("ingestBulkImportBatch skips LCM observation for flush-plan recovery batches (issue #2457)", async () => {
|
|
826
|
+
const orchestrator = Object.create(Orchestrator.prototype) as unknown as BulkImportTestDouble;
|
|
827
|
+
orchestrator.config = parseConfig({});
|
|
828
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
829
|
+
const observedSessionKeys: string[] = [];
|
|
830
|
+
orchestrator.lcmEngine = {
|
|
831
|
+
enabled: true,
|
|
832
|
+
observeMessages: async (sessionKey: string) => {
|
|
833
|
+
observedSessionKeys.push(sessionKey);
|
|
834
|
+
},
|
|
835
|
+
// Partial engine double for the observe seam.
|
|
836
|
+
} as unknown as LcmEngine;
|
|
837
|
+
const extractedSlices: BufferTurn[][] = [];
|
|
838
|
+
orchestrator.runExtraction = async (turns: BufferTurn[]) => {
|
|
839
|
+
extractedSlices.push(turns);
|
|
840
|
+
return { status: "completed", persistedCount: 1, durableOutputCount: 1 };
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
const result = await orchestrator.ingestBulkImportBatch([
|
|
844
|
+
{
|
|
845
|
+
role: "user",
|
|
846
|
+
timestamp: "2026-06-24T12:00:00.000Z",
|
|
847
|
+
content: "Recovery material the host already compacted once.",
|
|
848
|
+
importProvenance: {
|
|
849
|
+
sourceLabel: "OpenClaw flush plan",
|
|
850
|
+
sourceId: "openclaw-remnic:flush-plan",
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
role: "user",
|
|
855
|
+
timestamp: "2026-06-24T12:00:01.000Z",
|
|
856
|
+
content: "Chunked recovery material.",
|
|
857
|
+
importProvenance: {
|
|
858
|
+
sourceLabel: "OpenClaw flush plan",
|
|
859
|
+
sourceId: "openclaw-remnic:flush-plan:2/3",
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
]);
|
|
863
|
+
|
|
864
|
+
assert.deepEqual(observedSessionKeys, []);
|
|
865
|
+
assert.equal(extractedSlices.length, 2);
|
|
866
|
+
assert.equal(result.extractionCount, 2);
|
|
867
|
+
assert.equal(result.persistedCount, 2);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test("ingestBulkImportBatch still observes ordinary bulk imports through LCM (issue #2457)", async () => {
|
|
871
|
+
const orchestrator = Object.create(Orchestrator.prototype) as unknown as BulkImportTestDouble;
|
|
872
|
+
orchestrator.config = parseConfig({});
|
|
873
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
874
|
+
const observed: { sessionKey: string; turnCount: number }[] = [];
|
|
875
|
+
orchestrator.lcmEngine = {
|
|
876
|
+
enabled: true,
|
|
877
|
+
observeMessages: async (sessionKey: string, messages: unknown[]) => {
|
|
878
|
+
observed.push({ sessionKey, turnCount: messages.length });
|
|
879
|
+
},
|
|
880
|
+
// Partial engine double for the observe seam.
|
|
881
|
+
} as unknown as LcmEngine;
|
|
882
|
+
orchestrator.runExtraction = async () => ({
|
|
883
|
+
status: "completed",
|
|
884
|
+
persistedCount: 1,
|
|
885
|
+
durableOutputCount: 1,
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
await orchestrator.ingestBulkImportBatch([
|
|
889
|
+
{
|
|
890
|
+
role: "user",
|
|
891
|
+
timestamp: "2026-06-24T12:00:00.000Z",
|
|
892
|
+
content: "Ordinary chatgpt import turn.",
|
|
893
|
+
importProvenance: { sourceLabel: "chatgpt", sourceId: "cg-1" },
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
role: "user",
|
|
897
|
+
timestamp: "2026-06-24T12:00:01.000Z",
|
|
898
|
+
content: "Ordinary turn without provenance.",
|
|
899
|
+
},
|
|
900
|
+
]);
|
|
901
|
+
|
|
902
|
+
assert.deepEqual(
|
|
903
|
+
observed.map((entry) => entry.turnCount),
|
|
904
|
+
[2],
|
|
905
|
+
);
|
|
906
|
+
assert.ok(observed[0]?.sessionKey.startsWith("bulk-import:batch:"));
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test("ingestBulkImportBatch observes mixed batches that contain flush-plan turns (issue #2457)", async () => {
|
|
910
|
+
const orchestrator = Object.create(Orchestrator.prototype) as unknown as BulkImportTestDouble;
|
|
911
|
+
orchestrator.config = parseConfig({});
|
|
912
|
+
orchestrator.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
913
|
+
const observed: number[] = [];
|
|
914
|
+
orchestrator.lcmEngine = {
|
|
915
|
+
enabled: true,
|
|
916
|
+
observeMessages: async (_sessionKey: string, messages: unknown[]) => {
|
|
917
|
+
observed.push(messages.length);
|
|
918
|
+
},
|
|
919
|
+
// Partial engine double for the observe seam.
|
|
920
|
+
} as unknown as LcmEngine;
|
|
921
|
+
orchestrator.runExtraction = async () => ({
|
|
922
|
+
status: "completed",
|
|
923
|
+
persistedCount: 1,
|
|
924
|
+
durableOutputCount: 1,
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
await orchestrator.ingestBulkImportBatch([
|
|
928
|
+
{
|
|
929
|
+
role: "user",
|
|
930
|
+
timestamp: "2026-06-24T12:00:00.000Z",
|
|
931
|
+
content: "Recovery material mixed into an ordinary batch.",
|
|
932
|
+
importProvenance: {
|
|
933
|
+
sourceLabel: "OpenClaw flush plan",
|
|
934
|
+
sourceId: "openclaw-remnic:flush-plan",
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
role: "user",
|
|
939
|
+
timestamp: "2026-06-24T12:00:01.000Z",
|
|
940
|
+
content: "Ordinary chatgpt import turn.",
|
|
941
|
+
importProvenance: { sourceLabel: "chatgpt", sourceId: "cg-2" },
|
|
942
|
+
},
|
|
943
|
+
]);
|
|
944
|
+
|
|
945
|
+
assert.deepEqual(observed, [2]);
|
|
946
|
+
});
|
|
947
|
+
|
|
807
948
|
test("ingestBulkImportBatch preserves partial metadata failure before a later slice rejects", async () => {
|
|
808
949
|
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
809
950
|
orchestrator.config = parseConfig({});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { runCommandWithTimeoutForTest } from "./qmd.js";
|
|
8
|
+
|
|
9
|
+
// Regression for issue #2456: on timeout/abort the bounded qmd subprocess
|
|
10
|
+
// launcher must be killed together with its descendants. The launcher runs
|
|
11
|
+
// detached (own process group) so a negative-pid signal reaches grandchildren
|
|
12
|
+
// the old child.kill("SIGKILL") left behind. POSIX only — Windows falls back
|
|
13
|
+
// to a direct child kill by design.
|
|
14
|
+
//
|
|
15
|
+
// Real timers by design: this suite kills real OS processes, so the timeout
|
|
16
|
+
// trigger and the exit polls run against the platform clock. Fake timers
|
|
17
|
+
// cannot deliver signals to a spawned `sleep`. Every poll awaits a condition
|
|
18
|
+
// (pid file exists, process gone), never a fixed sleep.
|
|
19
|
+
const isPosix = process.platform !== "win32";
|
|
20
|
+
|
|
21
|
+
function isAlive(pid: number): boolean {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return (err as NodeJS.ErrnoException).code === "EPERM";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function waitForExit(pid: number, deadlineMs = 10_000): Promise<boolean> {
|
|
31
|
+
const deadline = Date.now() + deadlineMs;
|
|
32
|
+
while (Date.now() < deadline) {
|
|
33
|
+
if (!isAlive(pid)) return true;
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
35
|
+
}
|
|
36
|
+
return !isAlive(pid);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function setupFakeLauncher(): Promise<{ script: string; pidFile: string; dir: string }> {
|
|
40
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "remnic-qmd-kill-"));
|
|
41
|
+
const script = path.join(dir, "fake-qmd");
|
|
42
|
+
const pidFile = path.join(dir, "grandchild.pid");
|
|
43
|
+
// Launcher spawns a grandchild that outlives it, then hangs until killed.
|
|
44
|
+
await fs.promises.writeFile(
|
|
45
|
+
script,
|
|
46
|
+
`#!/bin/sh\nsleep 30 &\necho $! > "${pidFile}"\nsleep 30\n`,
|
|
47
|
+
{ mode: 0o755 },
|
|
48
|
+
);
|
|
49
|
+
return { script, pidFile, dir };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readPidFile(pidFile: string, deadlineMs = 5_000): Promise<number> {
|
|
53
|
+
const deadline = Date.now() + deadlineMs;
|
|
54
|
+
for (;;) {
|
|
55
|
+
try {
|
|
56
|
+
const text = await fs.promises.readFile(pidFile, "utf8");
|
|
57
|
+
const pid = Number.parseInt(text.trim(), 10);
|
|
58
|
+
assert.ok(Number.isInteger(pid) && pid > 0, `bad grandchild pid: ${text}`);
|
|
59
|
+
return pid;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (Date.now() >= deadline) throw err;
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function cleanupSurvivors(pids: Array<number | undefined>, dir?: string): Promise<void> {
|
|
68
|
+
for (const pid of pids) {
|
|
69
|
+
if (pid === undefined || !isAlive(pid)) continue;
|
|
70
|
+
try {
|
|
71
|
+
process.kill(pid, "SIGKILL");
|
|
72
|
+
} catch {
|
|
73
|
+
// Already gone.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (dir) await fs.promises.rm(dir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
test(
|
|
80
|
+
"timeout kills qmd launcher process group including grandchildren",
|
|
81
|
+
{ skip: isPosix ? false : "POSIX process groups do not exist on Windows" },
|
|
82
|
+
async () => {
|
|
83
|
+
const { script, pidFile, dir } = await setupFakeLauncher();
|
|
84
|
+
let grandchildPid: number | undefined;
|
|
85
|
+
try {
|
|
86
|
+
await assert.rejects(
|
|
87
|
+
runCommandWithTimeoutForTest(script, [], { timeoutMs: 500, label: "qmd fake" }),
|
|
88
|
+
(err: Error & { timedOut?: boolean }) => err.timedOut === true,
|
|
89
|
+
);
|
|
90
|
+
grandchildPid = await readPidFile(pidFile);
|
|
91
|
+
assert.ok(
|
|
92
|
+
await waitForExit(grandchildPid),
|
|
93
|
+
`grandchild ${grandchildPid} survived the group kill`,
|
|
94
|
+
);
|
|
95
|
+
} finally {
|
|
96
|
+
await cleanupSurvivors([grandchildPid], dir);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
test(
|
|
102
|
+
"abort kills qmd launcher process group including grandchildren",
|
|
103
|
+
{ skip: isPosix ? false : "POSIX process groups do not exist on Windows" },
|
|
104
|
+
async () => {
|
|
105
|
+
const { script, pidFile, dir } = await setupFakeLauncher();
|
|
106
|
+
let grandchildPid: number | undefined;
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
try {
|
|
109
|
+
const run = runCommandWithTimeoutForTest(script, [], {
|
|
110
|
+
timeoutMs: 60_000,
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
label: "qmd fake",
|
|
113
|
+
});
|
|
114
|
+
setTimeout(() => controller.abort(), 300);
|
|
115
|
+
await assert.rejects(run, /aborted/);
|
|
116
|
+
grandchildPid = await readPidFile(pidFile);
|
|
117
|
+
assert.ok(
|
|
118
|
+
await waitForExit(grandchildPid),
|
|
119
|
+
`grandchild ${grandchildPid} survived the group kill`,
|
|
120
|
+
);
|
|
121
|
+
} finally {
|
|
122
|
+
await cleanupSurvivors([grandchildPid], dir);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
);
|
package/src/qmd.ts
CHANGED
|
@@ -562,6 +562,11 @@ function runCommandWithTimeout(
|
|
|
562
562
|
const child = launchProcess(command, args, {
|
|
563
563
|
env: mergeEnv({ NO_COLOR: "1", ...options.env }),
|
|
564
564
|
stdio: ["ignore", "pipe", "pipe"],
|
|
565
|
+
// POSIX: own process group so a timeout/abort kill can take down qmd's
|
|
566
|
+
// grandchildren too (launcher scripts spawn workers that outlive the
|
|
567
|
+
// launcher itself). Issue #2456. Windows ignores this option here —
|
|
568
|
+
// kill falls back to a direct child.kill.
|
|
569
|
+
detached: process.platform !== "win32",
|
|
565
570
|
});
|
|
566
571
|
if (!child.stdout || !child.stderr) {
|
|
567
572
|
reject(new Error(`${label} failed to open stdio pipes`));
|
|
@@ -575,7 +580,7 @@ function runCommandWithTimeout(
|
|
|
575
580
|
const timer = setTimeout(() => {
|
|
576
581
|
settled = true;
|
|
577
582
|
cleanup();
|
|
578
|
-
child
|
|
583
|
+
killBoundedChild(child);
|
|
579
584
|
// Flag the deadline breach so classifyProbeFailure can tell a genuine
|
|
580
585
|
// timeout from a non-zero-exit error whose message merely embeds stderr.
|
|
581
586
|
reject(
|
|
@@ -589,7 +594,7 @@ function runCommandWithTimeout(
|
|
|
589
594
|
settled = true;
|
|
590
595
|
clearTimeout(timer);
|
|
591
596
|
cleanup();
|
|
592
|
-
child
|
|
597
|
+
killBoundedChild(child);
|
|
593
598
|
reject(abortError(`${label} aborted`));
|
|
594
599
|
};
|
|
595
600
|
const cleanup = () => {
|
|
@@ -636,6 +641,29 @@ function runProcessCommand(
|
|
|
636
641
|
});
|
|
637
642
|
}
|
|
638
643
|
|
|
644
|
+
// Kill a bounded subprocess and its descendants. On POSIX the child runs
|
|
645
|
+
// detached (own process group), so a negative-pid signal reaches the whole
|
|
646
|
+
// tree. Fall back to a direct kill on Windows, missing pid, or when the
|
|
647
|
+
// group is already gone. Never throws — callers use it on best-effort paths.
|
|
648
|
+
function killBoundedChild(child: CommandChildProcess): void {
|
|
649
|
+
const pid = child.pid;
|
|
650
|
+
if (pid !== undefined && process.platform !== "win32") {
|
|
651
|
+
try {
|
|
652
|
+
process.kill(-pid, "SIGKILL");
|
|
653
|
+
return;
|
|
654
|
+
} catch {
|
|
655
|
+
// Group already reaped or never became a leader — fall through.
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
child.kill("SIGKILL");
|
|
660
|
+
} catch {
|
|
661
|
+
// Already exited between the check and the kill.
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export { runCommandWithTimeout as runCommandWithTimeoutForTest };
|
|
666
|
+
|
|
639
667
|
// ---------------------------------------------------------------------------
|
|
640
668
|
// QMD Stdio Daemon Session (MCP over stdio child process)
|
|
641
669
|
// ---------------------------------------------------------------------------
|