@remnic/core 10.0.83 → 10.0.85
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 +67 -66
- package/dist/access-authorization-probe.js +68 -67
- package/dist/access-boundary.js +67 -66
- package/dist/access-cli.js +148 -146
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +67 -66
- package/dist/access-http-briefing.js +68 -67
- package/dist/access-http-query.js +69 -68
- package/dist/access-http.js +91 -89
- package/dist/access-identity-continuity-surface.js +67 -66
- package/dist/access-lcm-surface.js +67 -66
- package/dist/access-mcp-frames.d.ts +29 -0
- package/dist/access-mcp-frames.d.ts.map +1 -0
- package/dist/access-mcp-frames.js +10 -0
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.d.ts.map +1 -1
- package/dist/access-mcp.js +82 -80
- package/dist/access-namespace-preflight.js +67 -66
- package/dist/access-observe-write-category.js +9 -9
- package/dist/access-observe-write-surface.d.ts.map +1 -1
- package/dist/access-observe-write-surface.js +67 -66
- package/dist/access-offline-manifest.js +3 -3
- package/dist/access-operations-batch.js +71 -70
- package/dist/access-operations.d.ts +1 -1
- package/dist/access-operations.js +78 -77
- package/dist/access-recall-concurrency.js +67 -66
- package/dist/access-recall-response.js +67 -66
- package/dist/access-recall-surface.js +67 -66
- package/dist/access-schema.js +4 -4
- package/dist/access-service.js +67 -66
- package/dist/access-surface-catalog.js +69 -68
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +31 -31
- package/dist/{auto-sync-C7E6G7Y2.js → auto-sync-3AZW7MXI.js} +6 -6
- package/dist/briefing.js +25 -25
- package/dist/buffer-persistence.d.ts +8 -4
- package/dist/buffer-persistence.d.ts.map +1 -1
- package/dist/buffer-persistence.js +1 -1
- package/dist/buffer-turn-helpers.d.ts +5 -0
- package/dist/buffer-turn-helpers.d.ts.map +1 -1
- package/dist/buffer-turn-helpers.js +1 -1
- package/dist/buffer.d.ts +4 -1
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +9 -9
- package/dist/calibration.js +2 -2
- package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/capsule-gunzip-KGBDGF7P.js +11 -0
- package/dist/{capsule-merge-XVTQJJ6R.js → capsule-merge-2IIXFXN5.js} +5 -11
- package/dist/capsule-merge-2IIXFXN5.js.map +1 -0
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +32 -32
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-T6T2RMWW.js → chunk-23B4HLWG.js} +5 -5
- package/dist/{chunk-LECSS7EE.js → chunk-2ASEDFOA.js} +2 -2
- package/dist/{chunk-TPX7XXY4.js → chunk-2JAT4S7N.js} +2 -2
- package/dist/{chunk-XRBHIR7Q.js → chunk-2URKYLMK.js} +2 -2
- package/dist/{chunk-IN4OFAHX.js → chunk-2XYDL2FG.js} +2 -2
- package/dist/{chunk-HQGATKG2.js → chunk-3KGCHTTK.js} +2 -2
- package/dist/{chunk-YAUHSNPB.js → chunk-3LHPTBWG.js} +2 -2
- package/dist/{chunk-7PMOCQOL.js → chunk-3ODZLCHC.js} +192 -63
- package/dist/chunk-3ODZLCHC.js.map +1 -0
- package/dist/{chunk-XT45WCKX.js → chunk-3VU2FDTU.js} +8 -8
- package/dist/{chunk-CIIHIAJ2.js → chunk-433EXTC4.js} +3 -3
- package/dist/{chunk-QZSIDSB2.js → chunk-44FQCC5D.js} +2 -2
- package/dist/{chunk-YUTT4ZTI.js → chunk-45VUHDV2.js} +2 -2
- package/dist/{chunk-54FDG7K7.js → chunk-4DPLCYEK.js} +2 -2
- package/dist/{chunk-KO3YBHOV.js → chunk-4IWEPQEV.js} +2 -2
- package/dist/{chunk-H7BZBSP6.js → chunk-52II7PB6.js} +4 -4
- package/dist/{chunk-K6RTQZA4.js → chunk-6DANGCMO.js} +2 -2
- package/dist/{chunk-2IW5PT7U.js → chunk-6KFFRMPQ.js} +2 -2
- package/dist/{chunk-MSIRVXQ3.js → chunk-73YVMZ7D.js} +4 -4
- package/dist/{chunk-A7BM2JTH.js → chunk-AJHNXIQU.js} +2 -2
- package/dist/{chunk-2D7BTSDW.js → chunk-ANZZLRUX.js} +2 -2
- package/dist/{chunk-YOEYL6CH.js → chunk-AP3TSV6S.js} +2 -2
- package/dist/{chunk-GQZSGXR6.js → chunk-BATPQNRG.js} +2 -2
- package/dist/{chunk-D5SSBCYY.js → chunk-BLGQUZFB.js} +2 -2
- package/dist/{chunk-BDN3TDGV.js → chunk-BLMTOIBK.js} +2 -2
- package/dist/{chunk-B74NPUHT.js → chunk-BMIM6ESC.js} +43 -43
- package/dist/chunk-BMIM6ESC.js.map +1 -0
- package/dist/{chunk-R45L4TV6.js → chunk-BUKK5SWA.js} +6 -6
- package/dist/{chunk-TFLPDNQV.js → chunk-CCU7URRP.js} +2 -2
- package/dist/{chunk-2XJZLSXE.js → chunk-CMRHXPLQ.js} +20 -20
- package/dist/{chunk-BK4FBHZ3.js → chunk-D6THW7PA.js} +3 -3
- package/dist/{chunk-BFBS7OFQ.js → chunk-DC55RIEZ.js} +4 -4
- package/dist/{chunk-VKDAJJQP.js → chunk-DI5ET2IE.js} +5 -5
- package/dist/{chunk-QZBTIYKD.js → chunk-DM4RRTN7.js} +3 -3
- package/dist/{chunk-S57QBPTA.js → chunk-DPPLNZ3A.js} +4 -6
- package/dist/chunk-DPPLNZ3A.js.map +1 -0
- package/dist/{chunk-EBOHFZI4.js → chunk-E5QEF3JO.js} +2 -2
- package/dist/{chunk-DD6OIFDG.js → chunk-E7V5AK7I.js} +3 -3
- package/dist/{chunk-WAD32CZG.js → chunk-ELWI2R6H.js} +26 -13
- package/dist/chunk-ELWI2R6H.js.map +1 -0
- package/dist/{chunk-BV2BRHD4.js → chunk-FHGWKODN.js} +2 -2
- package/dist/{chunk-HPFVZPRH.js → chunk-FK4WC6VK.js} +3 -3
- package/dist/{chunk-VLMBSGQF.js → chunk-FRM4UTDR.js} +6 -6
- package/dist/{chunk-LTDLJCMQ.js → chunk-FZPDRCOB.js} +2 -2
- package/dist/{chunk-ZJJFUEPO.js → chunk-GBOYOL5L.js} +2 -2
- package/dist/{chunk-YUTG35M4.js → chunk-GJVGFKOU.js} +2 -2
- package/dist/{chunk-77R7BQZ2.js → chunk-GKYIRTNX.js} +33 -11
- package/dist/{chunk-77R7BQZ2.js.map → chunk-GKYIRTNX.js.map} +1 -1
- package/dist/{chunk-K36RMINQ.js → chunk-GOZXQNIM.js} +3 -3
- package/dist/{chunk-HWHNQVRV.js → chunk-GYNRNKMW.js} +2 -2
- package/dist/{chunk-LU66I56Q.js → chunk-H34INI3P.js} +29 -3
- package/dist/chunk-H34INI3P.js.map +1 -0
- package/dist/{chunk-AYTDCMEL.js → chunk-H4D5YURJ.js} +80 -94
- package/dist/chunk-H4D5YURJ.js.map +1 -0
- package/dist/{chunk-6LCWKS62.js → chunk-HVFX2N2G.js} +60 -59
- package/dist/chunk-HVFX2N2G.js.map +1 -0
- package/dist/chunk-IGLJNHOX.js +62 -0
- package/dist/chunk-IGLJNHOX.js.map +1 -0
- package/dist/{chunk-KFLD7ZZG.js → chunk-IUKYFTGW.js} +3 -3
- package/dist/{chunk-YK7GOPIE.js → chunk-JFH6JAGY.js} +6 -4
- package/dist/chunk-JFH6JAGY.js.map +1 -0
- package/dist/{chunk-BNQJKWWZ.js → chunk-K3FH3LAQ.js} +4 -4
- package/dist/{chunk-FJ2VJTGA.js → chunk-KHZVAISH.js} +2 -2
- package/dist/{chunk-WHQXWFH7.js → chunk-LT3CMO65.js} +2 -2
- package/dist/{chunk-NUSAAN75.js → chunk-MHSBOEH7.js} +2 -2
- package/dist/{chunk-KJCL2FZO.js → chunk-MNYQVYJ4.js} +2 -2
- package/dist/{chunk-RZH5RVJC.js → chunk-MTTYIXSU.js} +2 -2
- package/dist/{chunk-FVRTCYZ3.js → chunk-MVOJDLXS.js} +2 -2
- package/dist/{chunk-KESJ5XX3.js → chunk-MYCCO6AA.js} +2 -2
- package/dist/{chunk-66JOLMWS.js → chunk-MZCGHPIJ.js} +2 -2
- package/dist/{chunk-B4ZU34JQ.js → chunk-N6II66E5.js} +3 -3
- package/dist/{chunk-BK7AJUWB.js → chunk-NIV7RPWQ.js} +6 -6
- package/dist/{chunk-TLHF75OR.js → chunk-O55AGDTI.js} +10 -10
- package/dist/{chunk-GGJFRVRD.js → chunk-OMYRPMQC.js} +2 -2
- package/dist/{chunk-SVCHO337.js → chunk-ONN2E5QD.js} +32 -23
- package/dist/chunk-ONN2E5QD.js.map +1 -0
- package/dist/{chunk-XHGYZRVW.js → chunk-OSTFDWZJ.js} +4 -4
- package/dist/{chunk-BXLOS5AJ.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-WC475JOG.js → chunk-P225W4RQ.js} +6 -6
- package/dist/{chunk-TFA6HPOA.js → chunk-QCP75JGO.js} +5 -5
- package/dist/{chunk-JRVMCZE3.js → chunk-QEYTRFR3.js} +2 -2
- package/dist/chunk-QV2VZKD5.js +31 -0
- package/dist/chunk-QV2VZKD5.js.map +1 -0
- package/dist/{chunk-IZJQNDGI.js → chunk-R6HMAIKM.js} +2 -2
- package/dist/{chunk-VUROWXFL.js → chunk-RNGDKFY2.js} +2 -2
- package/dist/{chunk-FF6N7BXC.js → chunk-S45INSV3.js} +4 -4
- package/dist/{chunk-2RUTN7PR.js → chunk-S7GJNA3K.js} +2 -2
- package/dist/{chunk-XYP5WBDO.js → chunk-SGHV6LZ7.js} +13 -13
- package/dist/chunk-SGHV6LZ7.js.map +1 -0
- package/dist/{chunk-RJWL3RHB.js → chunk-TFEHSPGQ.js} +2 -2
- package/dist/{chunk-T77AX6DG.js → chunk-TQVECGUQ.js} +4 -4
- package/dist/{chunk-OQSSQOKQ.js → chunk-TV65NR7L.js} +2 -2
- package/dist/{chunk-KQUSP6LB.js → chunk-UDKWFDV2.js} +1 -1
- package/dist/chunk-UDKWFDV2.js.map +1 -0
- package/dist/{chunk-HQPFACXO.js → chunk-UH3FPX3L.js} +2 -2
- package/dist/{chunk-4EACCIPO.js → chunk-UHPHH74F.js} +2 -2
- package/dist/{chunk-DQEMWVMT.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-6QYPL6NI.js → chunk-V45TEVDA.js} +3 -3
- package/dist/{chunk-GCAQ3VUJ.js → chunk-VCVEMQJK.js} +2 -2
- package/dist/{chunk-6L63IN3M.js → chunk-VEFGHRKV.js} +1 -1
- package/dist/{chunk-E33UJHY3.js → chunk-WFDWLWDK.js} +2 -2
- package/dist/{chunk-4KLQI3L2.js → chunk-WLNWFHX3.js} +1 -1
- package/dist/chunk-WLNWFHX3.js.map +1 -0
- package/dist/{chunk-KQLBXUUQ.js → chunk-WYG5UNDY.js} +2 -2
- package/dist/{chunk-MPGLM2VU.js → chunk-X7REO4CH.js} +2 -2
- package/dist/{chunk-MPDLLLOG.js → chunk-XO23OKMK.js} +9 -5
- package/dist/chunk-XO23OKMK.js.map +1 -0
- package/dist/{chunk-Z3DDQXM4.js → chunk-XSFQN7TP.js} +35 -36
- package/dist/chunk-XSFQN7TP.js.map +1 -0
- package/dist/{chunk-FZVROZKC.js → chunk-XVZA6VXC.js} +4 -4
- package/dist/{chunk-WV254W4V.js → chunk-Y3IYKNBY.js} +2 -2
- package/dist/{chunk-FUOAIJKQ.js → chunk-Y43X3E24.js} +2 -2
- package/dist/{chunk-3T3VHXUH.js → chunk-YAAKVTZA.js} +2 -2
- package/dist/{chunk-VPEIWMUT.js → chunk-YLTU2DC4.js} +2 -2
- package/dist/{chunk-65SKM4BF.js → chunk-YNBMBL4C.js} +2 -2
- package/dist/{chunk-PJYK7HH2.js → chunk-ZWTHVUT5.js} +2 -2
- package/dist/cli.js +110 -108
- package/dist/coding/export-okf-codegraph.js +25 -25
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +27 -27
- package/dist/config.js +28 -28
- package/dist/connectors/codex-materialize-runner.js +25 -25
- package/dist/connectors/index.js +25 -25
- package/dist/contradiction/index.js +4 -4
- package/dist/convergence-refresh.js +27 -27
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +27 -27
- package/dist/deep-recall-http-glue.js +5 -5
- package/dist/entity-mention-index-cache.d.ts.map +1 -1
- package/dist/entity-mention-index-cache.js +2 -1
- package/dist/entity-retrieval.js +27 -26
- package/dist/event-order-recall.js +2 -2
- package/dist/explicit-capture.js +5 -5
- package/dist/external-wiki-access.js +68 -67
- package/dist/extraction-evidence-class.js +2 -2
- package/dist/extraction-quote-grounding.d.ts.map +1 -1
- package/dist/extraction-quote-grounding.js +1 -1
- package/dist/extraction-source-grounding-rules.js +2 -2
- package/dist/extraction-source-grounding.js +5 -5
- package/dist/extraction-span-materialize.js +2 -2
- package/dist/extraction.js +40 -40
- package/dist/{first-start-migration-QVD22WWS.js → first-start-migration-A6PHIBUQ.js} +4 -4
- package/dist/{graph-edge-decay-CXTNJ7IQ.js → graph-edge-decay-EKWXRBWF.js} +5 -5
- package/dist/graph-edge-decay-EKWXRBWF.js.map +1 -0
- package/dist/graph-jsonl.js +3 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/harmonic-retrieval.js +25 -25
- package/dist/index.js +205 -203
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +2 -2
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +28 -28
- package/dist/maintenance/memory-governance.js +25 -25
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +25 -25
- package/dist/maintenance/rebuild-memory-projection-runner.js +27 -27
- package/dist/maintenance/rebuild-memory-projection.js +26 -26
- package/dist/memory-browse-operations.js +68 -67
- package/dist/memory-subject.js +2 -2
- package/dist/namespaces/migrate.js +26 -26
- package/dist/namespaces/search.d.ts +8 -0
- package/dist/namespaces/search.d.ts.map +1 -1
- package/dist/namespaces/search.js +10 -6
- package/dist/namespaces/storage.js +25 -25
- package/dist/operator-doctor-corpus.js +28 -28
- package/dist/operator-toolkit.js +39 -39
- package/dist/orchestration/backlog-scheduler.d.ts +3 -2
- package/dist/orchestration/backlog-scheduler.d.ts.map +1 -1
- package/dist/orchestration/backlog-scheduler.js +159 -157
- package/dist/orchestration/backlog-scheduler.js.map +1 -1
- package/dist/orchestration/buffer-drain-runner.d.ts +6 -1
- package/dist/orchestration/buffer-drain-runner.d.ts.map +1 -1
- package/dist/orchestration/buffer-drain-runner.js +149 -147
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestration/fact-dedup-window.d.ts +1 -4
- package/dist/orchestration/fact-dedup-window.d.ts.map +1 -1
- package/dist/orchestration/maintenance.js +39 -39
- package/dist/orchestrator.js +148 -146
- package/dist/provenance.js +2 -2
- package/dist/recall-navigation-operations.js +68 -67
- package/dist/recall-navigation.js +67 -66
- package/dist/reconcile/manifest.js +2 -2
- package/dist/{resolution-BNWE5Y25.js → resolution-23ZBDM6A.js} +5 -5
- package/dist/resolution-23ZBDM6A.js.map +1 -0
- package/dist/resume-bundles.js +29 -29
- package/dist/salvage-envelope.js +4 -4
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +9 -9
- package/dist/semantic-consolidation.js +26 -26
- package/dist/semantic-rule-promotion.js +25 -25
- package/dist/semantic-rule-verifier.js +25 -25
- package/dist/source-agent-qualifier.js +31 -31
- package/dist/standup/build.js +25 -25
- package/dist/storage/buffer-drain-claims.d.ts +18 -6
- package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
- package/dist/storage/buffer-drain-claims.js +2 -2
- package/dist/storage/buffer-session-files.js +2 -2
- package/dist/storage/content-hash-index.d.ts +8 -0
- package/dist/storage/content-hash-index.d.ts.map +1 -1
- package/dist/storage.d.ts +10 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +24 -24
- package/dist/subject-config.js +3 -3
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +70 -69
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-gunzip.d.ts +14 -0
- package/dist/transfer/capsule-gunzip.d.ts.map +1 -0
- package/dist/transfer/capsule-import.d.ts.map +1 -1
- package/dist/transfer/capsule-import.js +3 -2
- package/dist/transfer/capsule-merge.d.ts.map +1 -1
- package/dist/transfer/export-okf.js +25 -25
- package/dist/verified-recall.js +25 -25
- package/dist/who-knows-cli.js +26 -26
- package/dist/who-knows.js +25 -25
- package/dist/write-envelope.js +3 -3
- package/package.json +2 -2
- package/src/access-mcp-correction-tools.test.ts +45 -0
- package/src/access-mcp-frames.test.ts +47 -0
- package/src/access-mcp-frames.ts +74 -0
- package/src/access-mcp.test.ts +146 -0
- package/src/access-mcp.ts +30 -30
- package/src/access-observe-write-surface.ts +4 -4
- package/src/backlog-scheduler.test.ts +443 -3
- package/src/buffer-persistence.ts +15 -6
- package/src/buffer-turn-helpers.ts +6 -0
- package/src/buffer.ts +6 -6
- package/src/cli.ts +4 -4
- package/src/compounding/weekly-evidence.test.ts +237 -0
- package/src/entity-mention-index-cache.ts +1 -6
- package/src/extraction-quote-grounding.test.ts +49 -0
- package/src/extraction-quote-grounding.ts +50 -12
- package/src/fact-dedup-window.test.ts +103 -20
- package/src/maintenance/rebuild-memory-projection-runner.test.ts +90 -0
- package/src/namespaces/search.test.ts +339 -0
- package/src/namespaces/search.ts +54 -1
- package/src/orchestration/backlog-scheduler.ts +13 -12
- package/src/orchestration/buffer-drain-runner.ts +40 -16
- package/src/orchestration/extraction-persist.ts +2 -10
- package/src/orchestration/fact-dedup-window.ts +5 -12
- package/src/orchestration/full-corpus-heap-guard.test.ts +52 -0
- package/src/projection-rebuild-config.test.ts +79 -0
- package/src/storage/buffer-drain-claims.test.ts +330 -6
- package/src/storage/buffer-drain-claims.ts +226 -80
- package/src/storage/buffer-session-files.ts +2 -2
- package/src/storage/content-hash-index.ts +9 -0
- package/src/storage/entity-store-symlink.test.ts +44 -0
- package/src/storage.ts +15 -0
- package/src/transfer/capsule-gunzip.ts +39 -0
- package/src/transfer/capsule-import.test.ts +73 -0
- package/src/transfer/capsule-import.ts +2 -2
- package/src/transfer/capsule-merge.test.ts +34 -0
- package/src/transfer/capsule-merge.ts +2 -10
- package/src/utils/malformed-entity-record.test.ts +69 -0
- package/dist/capsule-merge-XVTQJJ6R.js.map +0 -1
- package/dist/chunk-4KLQI3L2.js.map +0 -1
- package/dist/chunk-6LCWKS62.js.map +0 -1
- package/dist/chunk-7PMOCQOL.js.map +0 -1
- package/dist/chunk-AYTDCMEL.js.map +0 -1
- package/dist/chunk-B74NPUHT.js.map +0 -1
- package/dist/chunk-KQUSP6LB.js.map +0 -1
- package/dist/chunk-LU66I56Q.js.map +0 -1
- package/dist/chunk-MPDLLLOG.js.map +0 -1
- package/dist/chunk-S57QBPTA.js.map +0 -1
- package/dist/chunk-SVCHO337.js.map +0 -1
- package/dist/chunk-WAD32CZG.js.map +0 -1
- package/dist/chunk-XYP5WBDO.js.map +0 -1
- package/dist/chunk-YK7GOPIE.js.map +0 -1
- package/dist/chunk-Z3DDQXM4.js.map +0 -1
- /package/dist/{capsule-crypto-YO5QJ6L3.js.map → access-mcp-frames.js.map} +0 -0
- /package/dist/{auto-sync-C7E6G7Y2.js.map → auto-sync-3AZW7MXI.js.map} +0 -0
- /package/dist/{graph-edge-decay-CXTNJ7IQ.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
- /package/dist/{resolution-BNWE5Y25.js.map → capsule-gunzip-KGBDGF7P.js.map} +0 -0
- /package/dist/{chunk-T6T2RMWW.js.map → chunk-23B4HLWG.js.map} +0 -0
- /package/dist/{chunk-LECSS7EE.js.map → chunk-2ASEDFOA.js.map} +0 -0
- /package/dist/{chunk-TPX7XXY4.js.map → chunk-2JAT4S7N.js.map} +0 -0
- /package/dist/{chunk-XRBHIR7Q.js.map → chunk-2URKYLMK.js.map} +0 -0
- /package/dist/{chunk-IN4OFAHX.js.map → chunk-2XYDL2FG.js.map} +0 -0
- /package/dist/{chunk-HQGATKG2.js.map → chunk-3KGCHTTK.js.map} +0 -0
- /package/dist/{chunk-YAUHSNPB.js.map → chunk-3LHPTBWG.js.map} +0 -0
- /package/dist/{chunk-XT45WCKX.js.map → chunk-3VU2FDTU.js.map} +0 -0
- /package/dist/{chunk-CIIHIAJ2.js.map → chunk-433EXTC4.js.map} +0 -0
- /package/dist/{chunk-QZSIDSB2.js.map → chunk-44FQCC5D.js.map} +0 -0
- /package/dist/{chunk-YUTT4ZTI.js.map → chunk-45VUHDV2.js.map} +0 -0
- /package/dist/{chunk-54FDG7K7.js.map → chunk-4DPLCYEK.js.map} +0 -0
- /package/dist/{chunk-KO3YBHOV.js.map → chunk-4IWEPQEV.js.map} +0 -0
- /package/dist/{chunk-H7BZBSP6.js.map → chunk-52II7PB6.js.map} +0 -0
- /package/dist/{chunk-K6RTQZA4.js.map → chunk-6DANGCMO.js.map} +0 -0
- /package/dist/{chunk-2IW5PT7U.js.map → chunk-6KFFRMPQ.js.map} +0 -0
- /package/dist/{chunk-MSIRVXQ3.js.map → chunk-73YVMZ7D.js.map} +0 -0
- /package/dist/{chunk-A7BM2JTH.js.map → chunk-AJHNXIQU.js.map} +0 -0
- /package/dist/{chunk-2D7BTSDW.js.map → chunk-ANZZLRUX.js.map} +0 -0
- /package/dist/{chunk-YOEYL6CH.js.map → chunk-AP3TSV6S.js.map} +0 -0
- /package/dist/{chunk-GQZSGXR6.js.map → chunk-BATPQNRG.js.map} +0 -0
- /package/dist/{chunk-D5SSBCYY.js.map → chunk-BLGQUZFB.js.map} +0 -0
- /package/dist/{chunk-BDN3TDGV.js.map → chunk-BLMTOIBK.js.map} +0 -0
- /package/dist/{chunk-R45L4TV6.js.map → chunk-BUKK5SWA.js.map} +0 -0
- /package/dist/{chunk-TFLPDNQV.js.map → chunk-CCU7URRP.js.map} +0 -0
- /package/dist/{chunk-2XJZLSXE.js.map → chunk-CMRHXPLQ.js.map} +0 -0
- /package/dist/{chunk-BK4FBHZ3.js.map → chunk-D6THW7PA.js.map} +0 -0
- /package/dist/{chunk-BFBS7OFQ.js.map → chunk-DC55RIEZ.js.map} +0 -0
- /package/dist/{chunk-VKDAJJQP.js.map → chunk-DI5ET2IE.js.map} +0 -0
- /package/dist/{chunk-QZBTIYKD.js.map → chunk-DM4RRTN7.js.map} +0 -0
- /package/dist/{chunk-EBOHFZI4.js.map → chunk-E5QEF3JO.js.map} +0 -0
- /package/dist/{chunk-DD6OIFDG.js.map → chunk-E7V5AK7I.js.map} +0 -0
- /package/dist/{chunk-BV2BRHD4.js.map → chunk-FHGWKODN.js.map} +0 -0
- /package/dist/{chunk-HPFVZPRH.js.map → chunk-FK4WC6VK.js.map} +0 -0
- /package/dist/{chunk-VLMBSGQF.js.map → chunk-FRM4UTDR.js.map} +0 -0
- /package/dist/{chunk-LTDLJCMQ.js.map → chunk-FZPDRCOB.js.map} +0 -0
- /package/dist/{chunk-ZJJFUEPO.js.map → chunk-GBOYOL5L.js.map} +0 -0
- /package/dist/{chunk-YUTG35M4.js.map → chunk-GJVGFKOU.js.map} +0 -0
- /package/dist/{chunk-K36RMINQ.js.map → chunk-GOZXQNIM.js.map} +0 -0
- /package/dist/{chunk-HWHNQVRV.js.map → chunk-GYNRNKMW.js.map} +0 -0
- /package/dist/{chunk-KFLD7ZZG.js.map → chunk-IUKYFTGW.js.map} +0 -0
- /package/dist/{chunk-BNQJKWWZ.js.map → chunk-K3FH3LAQ.js.map} +0 -0
- /package/dist/{chunk-FJ2VJTGA.js.map → chunk-KHZVAISH.js.map} +0 -0
- /package/dist/{chunk-WHQXWFH7.js.map → chunk-LT3CMO65.js.map} +0 -0
- /package/dist/{chunk-NUSAAN75.js.map → chunk-MHSBOEH7.js.map} +0 -0
- /package/dist/{chunk-KJCL2FZO.js.map → chunk-MNYQVYJ4.js.map} +0 -0
- /package/dist/{chunk-RZH5RVJC.js.map → chunk-MTTYIXSU.js.map} +0 -0
- /package/dist/{chunk-FVRTCYZ3.js.map → chunk-MVOJDLXS.js.map} +0 -0
- /package/dist/{chunk-KESJ5XX3.js.map → chunk-MYCCO6AA.js.map} +0 -0
- /package/dist/{chunk-66JOLMWS.js.map → chunk-MZCGHPIJ.js.map} +0 -0
- /package/dist/{chunk-B4ZU34JQ.js.map → chunk-N6II66E5.js.map} +0 -0
- /package/dist/{chunk-BK7AJUWB.js.map → chunk-NIV7RPWQ.js.map} +0 -0
- /package/dist/{chunk-TLHF75OR.js.map → chunk-O55AGDTI.js.map} +0 -0
- /package/dist/{chunk-GGJFRVRD.js.map → chunk-OMYRPMQC.js.map} +0 -0
- /package/dist/{chunk-XHGYZRVW.js.map → chunk-OSTFDWZJ.js.map} +0 -0
- /package/dist/{chunk-BXLOS5AJ.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-WC475JOG.js.map → chunk-P225W4RQ.js.map} +0 -0
- /package/dist/{chunk-TFA6HPOA.js.map → chunk-QCP75JGO.js.map} +0 -0
- /package/dist/{chunk-JRVMCZE3.js.map → chunk-QEYTRFR3.js.map} +0 -0
- /package/dist/{chunk-IZJQNDGI.js.map → chunk-R6HMAIKM.js.map} +0 -0
- /package/dist/{chunk-VUROWXFL.js.map → chunk-RNGDKFY2.js.map} +0 -0
- /package/dist/{chunk-FF6N7BXC.js.map → chunk-S45INSV3.js.map} +0 -0
- /package/dist/{chunk-2RUTN7PR.js.map → chunk-S7GJNA3K.js.map} +0 -0
- /package/dist/{chunk-RJWL3RHB.js.map → chunk-TFEHSPGQ.js.map} +0 -0
- /package/dist/{chunk-T77AX6DG.js.map → chunk-TQVECGUQ.js.map} +0 -0
- /package/dist/{chunk-OQSSQOKQ.js.map → chunk-TV65NR7L.js.map} +0 -0
- /package/dist/{chunk-HQPFACXO.js.map → chunk-UH3FPX3L.js.map} +0 -0
- /package/dist/{chunk-4EACCIPO.js.map → chunk-UHPHH74F.js.map} +0 -0
- /package/dist/{chunk-DQEMWVMT.js.map → chunk-UVYI6VIX.js.map} +0 -0
- /package/dist/{chunk-6QYPL6NI.js.map → chunk-V45TEVDA.js.map} +0 -0
- /package/dist/{chunk-GCAQ3VUJ.js.map → chunk-VCVEMQJK.js.map} +0 -0
- /package/dist/{chunk-6L63IN3M.js.map → chunk-VEFGHRKV.js.map} +0 -0
- /package/dist/{chunk-E33UJHY3.js.map → chunk-WFDWLWDK.js.map} +0 -0
- /package/dist/{chunk-KQLBXUUQ.js.map → chunk-WYG5UNDY.js.map} +0 -0
- /package/dist/{chunk-MPGLM2VU.js.map → chunk-X7REO4CH.js.map} +0 -0
- /package/dist/{chunk-FZVROZKC.js.map → chunk-XVZA6VXC.js.map} +0 -0
- /package/dist/{chunk-WV254W4V.js.map → chunk-Y3IYKNBY.js.map} +0 -0
- /package/dist/{chunk-FUOAIJKQ.js.map → chunk-Y43X3E24.js.map} +0 -0
- /package/dist/{chunk-3T3VHXUH.js.map → chunk-YAAKVTZA.js.map} +0 -0
- /package/dist/{chunk-VPEIWMUT.js.map → chunk-YLTU2DC4.js.map} +0 -0
- /package/dist/{chunk-65SKM4BF.js.map → chunk-YNBMBL4C.js.map} +0 -0
- /package/dist/{chunk-PJYK7HH2.js.map → chunk-ZWTHVUT5.js.map} +0 -0
- /package/dist/{first-start-migration-QVD22WWS.js.map → first-start-migration-A6PHIBUQ.js.map} +0 -0
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
* extraction is not durably finished, so a kill at any point loses
|
|
21
21
|
* nothing and the next run resumes.
|
|
22
22
|
* - A live buffer owner that must persist a claimed session hands its
|
|
23
|
-
* in-memory entry INTO the claim (
|
|
24
|
-
* writing a session file — it can never resurrect
|
|
23
|
+
* in-memory entry INTO the claim (identity-aware, occurrence-preserving
|
|
24
|
+
* merge) instead of writing a session file — it can never resurrect
|
|
25
|
+
* turns the drain already finished.
|
|
25
26
|
* - Claims are idempotent: re-claiming, double-draining, and two
|
|
26
27
|
* concurrent drainers are all safe (per-key mutation lock + exclusive
|
|
27
28
|
* lease; the lock also fences lease takeover and progress writes).
|
|
@@ -38,7 +39,7 @@ import { mkdir, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
|
38
39
|
import path from "node:path";
|
|
39
40
|
|
|
40
41
|
import { bufferSessionFileName, type BufferFileIo } from "./buffer-session-files.js";
|
|
41
|
-
import {
|
|
42
|
+
import { bufferTurnIdentityKey, subtractBufferTurns, type DrainedTurnSets } from "../buffer-turn-helpers.js";
|
|
42
43
|
import { log } from "../logger.js";
|
|
43
44
|
import { withHeldFileLock } from "../utils/serialize-mutations.js";
|
|
44
45
|
import type { BufferEntryState, BufferTurn } from "../types.js";
|
|
@@ -47,11 +48,12 @@ import type { BufferEntryState, BufferTurn } from "../types.js";
|
|
|
47
48
|
export interface BufferSessionSaveOutcome {
|
|
48
49
|
/** "saved": the session file holds the entry. "handed_over": the entry moved into a drain claim. */
|
|
49
50
|
status: "saved" | "handed_over";
|
|
50
|
-
/**
|
|
51
|
-
drainedTurns?:
|
|
51
|
+
/** Tombstoned turn identities by their original active/retained list. */
|
|
52
|
+
drainedTurns?: DrainedTurnSets;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export interface DrainLease {
|
|
56
|
+
/** Unique fencing token for one scheduler/process identity; never reuse it after restart. */
|
|
55
57
|
ownerId: string;
|
|
56
58
|
pid: number;
|
|
57
59
|
/** Host the owner runs on: a takeover may only judge liveness on that host. */
|
|
@@ -74,6 +76,13 @@ export interface DrainClaimFile {
|
|
|
74
76
|
drained?: BufferTurn[];
|
|
75
77
|
/** Same tombstone for turns the drain extracted from `retainedTurns`. */
|
|
76
78
|
drainedRetained?: BufferTurn[];
|
|
79
|
+
/**
|
|
80
|
+
* Pending turns whose only copy sits in the other list than the one they
|
|
81
|
+
* were claimed into (active to retained or back). When such a turn is
|
|
82
|
+
* extracted, its tombstone goes to BOTH lists: a live owner still holds
|
|
83
|
+
* the stale copy in the list the turn started in.
|
|
84
|
+
*/
|
|
85
|
+
movedTurns?: BufferTurn[];
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
export type ClaimFromSessionOutcome = "claimed" | "empty" | "already_claimed";
|
|
@@ -95,29 +104,80 @@ function emptyEntry(): BufferEntryState {
|
|
|
95
104
|
return { turns: [], lastExtractionAt: null, extractionCount: 0 };
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
function turnCounts(turns: readonly BufferTurn[]): Map<string, number> {
|
|
108
|
+
const counts = new Map<string, number>();
|
|
109
|
+
for (const turn of turns) {
|
|
110
|
+
const key = bufferTurnIdentityKey(turn);
|
|
111
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
112
|
+
}
|
|
113
|
+
return counts;
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
function mergeTurns(existing: BufferTurn[], incoming: readonly BufferTurn[]): BufferTurn[] {
|
|
101
|
-
|
|
117
|
+
const merged = [...existing];
|
|
118
|
+
const existingCounts = turnCounts(existing);
|
|
119
|
+
const incomingCounts = new Map<string, number>();
|
|
120
|
+
for (const turn of incoming) {
|
|
121
|
+
const key = bufferTurnIdentityKey(turn);
|
|
122
|
+
const count = (incomingCounts.get(key) ?? 0) + 1;
|
|
123
|
+
incomingCounts.set(key, count);
|
|
124
|
+
if (count > (existingCounts.get(key) ?? 0)) merged.push(turn);
|
|
125
|
+
}
|
|
126
|
+
return merged;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function preserveSourceOccurrences(
|
|
130
|
+
target: BufferTurn[],
|
|
131
|
+
source: readonly BufferTurn[],
|
|
132
|
+
identity: string,
|
|
133
|
+
): void {
|
|
134
|
+
const targetCount = turnCounts(target).get(identity) ?? 0;
|
|
135
|
+
let sourceCount = 0;
|
|
136
|
+
for (const turn of source) {
|
|
137
|
+
if (bufferTurnIdentityKey(turn) !== identity) continue;
|
|
138
|
+
sourceCount += 1;
|
|
139
|
+
if (sourceCount > targetCount) target.push(turn);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function appendUnmatched(
|
|
144
|
+
snapshot: readonly BufferTurn[],
|
|
145
|
+
matchedCounts: ReadonlyMap<string, number>,
|
|
146
|
+
unmatched: BufferTurn[],
|
|
147
|
+
): void {
|
|
148
|
+
const remaining = new Map(matchedCounts);
|
|
149
|
+
for (const turn of snapshot) {
|
|
150
|
+
const key = bufferTurnIdentityKey(turn);
|
|
151
|
+
const matched = remaining.get(key) ?? 0;
|
|
152
|
+
if (matched > 0) remaining.set(key, matched - 1);
|
|
153
|
+
else unmatched.push(turn);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function removeOneByIdentity(turns: BufferTurn[], identity: string): boolean {
|
|
158
|
+
const index = turns.findIndex((turn) => bufferTurnIdentityKey(turn) === identity);
|
|
159
|
+
if (index < 0) return false;
|
|
160
|
+
turns.splice(index, 1);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function takeByIdentity(source: readonly BufferTurn[], identity: string, count: number): BufferTurn[] {
|
|
165
|
+
return source.filter((turn) => bufferTurnIdentityKey(turn) === identity).slice(0, count);
|
|
102
166
|
}
|
|
103
167
|
|
|
104
168
|
function mergeEntries(claim: DrainClaimFile, incoming: BufferEntryState): DrainClaimFile {
|
|
105
169
|
const entry = emptyEntry();
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// list counts — a turn in either one left the claim for good).
|
|
109
|
-
const gone = [...(claim.drained ?? []), ...(claim.drainedRetained ?? [])];
|
|
170
|
+
// The claim's own lists already exclude what was extracted; each tombstone
|
|
171
|
+
// applies only to the incoming copy in the list the turn was extracted from.
|
|
110
172
|
entry.turns = mergeTurns(
|
|
111
|
-
|
|
112
|
-
subtractBufferTurns(incoming.turns,
|
|
173
|
+
claim.entry.turns,
|
|
174
|
+
subtractBufferTurns(incoming.turns, claim.drained ?? []),
|
|
113
175
|
);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const dedupedRetained = mergeTurns([], retained);
|
|
120
|
-
if (dedupedRetained.length > 0) entry.retainedTurns = dedupedRetained;
|
|
176
|
+
entry.retainedTurns = mergeTurns(
|
|
177
|
+
claim.entry.retainedTurns ?? [],
|
|
178
|
+
subtractBufferTurns(incoming.retainedTurns ?? [], claim.drainedRetained ?? []),
|
|
179
|
+
);
|
|
180
|
+
if (entry.retainedTurns.length === 0) delete entry.retainedTurns;
|
|
121
181
|
const stamps = [claim.entry.lastExtractionAt, incoming.lastExtractionAt].filter(
|
|
122
182
|
(value): value is string => value !== null && value !== undefined,
|
|
123
183
|
);
|
|
@@ -163,6 +223,7 @@ const claimBodySchema = z.object({
|
|
|
163
223
|
claimedAt: z.string().optional(),
|
|
164
224
|
drained: z.array(z.unknown()).optional(),
|
|
165
225
|
drainedRetained: z.array(z.unknown()).optional(),
|
|
226
|
+
movedTurns: z.array(z.unknown()).optional(),
|
|
166
227
|
});
|
|
167
228
|
|
|
168
229
|
function parseBody(raw: string): DrainClaimFile | null {
|
|
@@ -204,6 +265,10 @@ function parseBody(raw: string): DrainClaimFile | null {
|
|
|
204
265
|
// SAFETY: same boundary as the turns above (this module writes the file).
|
|
205
266
|
claim.drainedRetained = body.drainedRetained as BufferTurn[];
|
|
206
267
|
}
|
|
268
|
+
if (Array.isArray(body.movedTurns)) {
|
|
269
|
+
// SAFETY: same boundary as the turns above (this module writes the file).
|
|
270
|
+
claim.movedTurns = body.movedTurns as BufferTurn[];
|
|
271
|
+
}
|
|
207
272
|
return claim;
|
|
208
273
|
}
|
|
209
274
|
|
|
@@ -232,6 +297,10 @@ function parseLease(raw: string): DrainLease | null {
|
|
|
232
297
|
leaseMs: lease.leaseMs,
|
|
233
298
|
};
|
|
234
299
|
}
|
|
300
|
+
function leaseBelongsToProcess(lease: DrainLease | null, ownerId: string): boolean {
|
|
301
|
+
return lease !== null && lease.ownerId === ownerId && lease.pid === process.pid && lease.host === hostname();
|
|
302
|
+
}
|
|
303
|
+
|
|
235
304
|
|
|
236
305
|
/** Claim bodies `listPage` may read per requested entry: bounds one scan's
|
|
237
306
|
* I/O even when the directory is mostly empty tombstones. */
|
|
@@ -375,20 +444,19 @@ export class BufferDrainClaims {
|
|
|
375
444
|
async (acquired) => {
|
|
376
445
|
if (!acquired) return "busy";
|
|
377
446
|
const existing = await this.readLease(key);
|
|
447
|
+
if (existing?.ownerId === lease.ownerId && existing.pid === lease.pid && existing.host === lease.host) {
|
|
448
|
+
// Reuse the token only from the same process identity.
|
|
449
|
+
await this.writeLease(key, { ...existing, heartbeatAt: new Date().toISOString(), leaseMs: lease.leaseMs });
|
|
450
|
+
return "adopted";
|
|
451
|
+
}
|
|
378
452
|
if (existing && !isLeaseExpired(existing)) {
|
|
379
|
-
if (existing.ownerId === lease.ownerId) {
|
|
380
|
-
// The same drainer already holds it: renew the lease.
|
|
381
|
-
await this.writeLease(key, { ...existing, heartbeatAt: new Date().toISOString(), leaseMs: lease.leaseMs });
|
|
382
|
-
return "adopted";
|
|
383
|
-
}
|
|
384
453
|
if (isOwnerAlive(existing)) return "busy";
|
|
385
454
|
// The owner died without releasing the claim (killed worker):
|
|
386
455
|
// take it over now instead of waiting for the lease to expire.
|
|
387
456
|
await unlink(this.leaseFileFor(key)).catch(() => {});
|
|
388
457
|
} else if (existing) {
|
|
389
458
|
// Expired lease: clear it before the exclusive create. We hold the
|
|
390
|
-
// lock, so no concurrent adopter can interleave its own
|
|
391
|
-
// unlink/create between these steps.
|
|
459
|
+
// lock, so no concurrent adopter can interleave its own unlink/create.
|
|
392
460
|
await unlink(this.leaseFileFor(key)).catch(() => {});
|
|
393
461
|
}
|
|
394
462
|
try {
|
|
@@ -444,7 +512,7 @@ export class BufferDrainClaims {
|
|
|
444
512
|
await withHeldFileLock(this.lockFileFor(key), { staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS }, async (acquired) => {
|
|
445
513
|
if (!acquired) throw new Error(`buffer drain claim for ${key} is busy; progress write skipped`);
|
|
446
514
|
const lease = await this.readLease(key);
|
|
447
|
-
if (!lease
|
|
515
|
+
if (!leaseBelongsToProcess(lease, ownerId)) {
|
|
448
516
|
throw new Error(`buffer drain claim for ${key} is no longer owned by ${ownerId}; progress write skipped`);
|
|
449
517
|
}
|
|
450
518
|
const claim = await this.readClaimBody(key);
|
|
@@ -452,39 +520,87 @@ export class BufferDrainClaims {
|
|
|
452
520
|
const seenTurns = seen?.turns ?? claim.entry.turns;
|
|
453
521
|
const seenRetained = seen?.retainedTurns ?? claim.entry.retainedTurns ?? [];
|
|
454
522
|
const entryRetained = entry.retainedTurns ?? [];
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
523
|
+
const seenActiveCounts = turnCounts(seenTurns);
|
|
524
|
+
const seenRetainedCounts = turnCounts(seenRetained);
|
|
525
|
+
const activeOutputCounts = turnCounts(entry.turns);
|
|
526
|
+
const retainedOutputCounts = turnCounts(entryRetained);
|
|
527
|
+
const matchedActiveCounts = new Map<string, number>();
|
|
528
|
+
const matchedRetainedCounts = new Map<string, number>();
|
|
529
|
+
const ambiguousIdentities = new Set<string>();
|
|
530
|
+
const identityKeys = new Set([
|
|
531
|
+
...seenActiveCounts.keys(),
|
|
532
|
+
...seenRetainedCounts.keys(),
|
|
533
|
+
]);
|
|
534
|
+
const crossedCounts = new Map<string, number>();
|
|
535
|
+
for (const identity of identityKeys) {
|
|
536
|
+
const seenActive = seenActiveCounts.get(identity) ?? 0;
|
|
537
|
+
const seenRetainedCount = seenRetainedCounts.get(identity) ?? 0;
|
|
538
|
+
const activeOutput = activeOutputCounts.get(identity) ?? 0;
|
|
539
|
+
const retainedOutput = retainedOutputCounts.get(identity) ?? 0;
|
|
540
|
+
if (seenActive > 0 && seenRetainedCount > 0 && activeOutput + retainedOutput > 0) {
|
|
541
|
+
// Equal identities across source lists cannot reveal which original
|
|
542
|
+
// occurrence survived a cross-list move. Keep both source lists
|
|
543
|
+
// pending until a later progress write removes every copy.
|
|
544
|
+
matchedActiveCounts.set(identity, seenActive);
|
|
545
|
+
matchedRetainedCounts.set(identity, seenRetainedCount);
|
|
546
|
+
ambiguousIdentities.add(identity);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const matchedActive = Math.min(seenActive, activeOutput);
|
|
550
|
+
const matchedRetained = Math.min(seenRetainedCount, retainedOutput);
|
|
551
|
+
const activeMovedToRetained = Math.min(
|
|
552
|
+
seenActive - matchedActive,
|
|
553
|
+
retainedOutput - matchedRetained,
|
|
554
|
+
);
|
|
555
|
+
const retainedMovedToActive = Math.min(
|
|
556
|
+
seenRetainedCount - matchedRetained,
|
|
557
|
+
activeOutput - matchedActive,
|
|
558
|
+
);
|
|
559
|
+
matchedActiveCounts.set(identity, matchedActive + activeMovedToRetained);
|
|
560
|
+
matchedRetainedCounts.set(identity, matchedRetained + retainedMovedToActive);
|
|
561
|
+
const crossed = activeMovedToRetained + retainedMovedToActive;
|
|
562
|
+
if (crossed > 0) crossedCounts.set(identity, crossed);
|
|
563
|
+
}
|
|
564
|
+
// A pending copy that crossed lists stays tombstoned in its ORIGINAL
|
|
565
|
+
// list too once it is extracted. A copy that crosses back is home again.
|
|
566
|
+
const moved = [...(claim.movedTurns ?? [])];
|
|
567
|
+
for (const [identity, crossed] of crossedCounts) {
|
|
568
|
+
let returned = 0;
|
|
569
|
+
while (returned < crossed && removeOneByIdentity(moved, identity)) returned += 1;
|
|
570
|
+
moved.push(...takeByIdentity([...seenTurns, ...seenRetained], identity, crossed - returned));
|
|
466
571
|
}
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
572
|
+
const extractedActive: BufferTurn[] = [];
|
|
573
|
+
appendUnmatched(seenTurns, matchedActiveCounts, extractedActive);
|
|
574
|
+
const extractedRetained: BufferTurn[] = [];
|
|
575
|
+
appendUnmatched(seenRetained, matchedRetainedCounts, extractedRetained);
|
|
576
|
+
const drained = [...(claim.drained ?? []), ...extractedActive];
|
|
577
|
+
const drainedRetained = [...(claim.drainedRetained ?? []), ...extractedRetained];
|
|
578
|
+
for (const turn of extractedActive) {
|
|
579
|
+
if (removeOneByIdentity(moved, bufferTurnIdentityKey(turn))) drainedRetained.push(turn);
|
|
474
580
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
const
|
|
483
|
-
(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
581
|
+
for (const turn of extractedRetained) {
|
|
582
|
+
if (removeOneByIdentity(moved, bufferTurnIdentityKey(turn))) drained.push(turn);
|
|
583
|
+
}
|
|
584
|
+
// Source occurrences of an ambiguous identity are restored BEFORE late
|
|
585
|
+
// arrivals are appended, so a late equal turn cannot stand in for them.
|
|
586
|
+
const keptTurns = [...entry.turns];
|
|
587
|
+
const keptRetained = [...entryRetained];
|
|
588
|
+
for (const identity of ambiguousIdentities) {
|
|
589
|
+
preserveSourceOccurrences(keptTurns, seenTurns, identity);
|
|
590
|
+
preserveSourceOccurrences(keptRetained, seenRetained, identity);
|
|
591
|
+
}
|
|
592
|
+
const late = subtractBufferTurns(claim.entry.turns, seenTurns);
|
|
593
|
+
const lateRetained = subtractBufferTurns(claim.entry.retainedTurns ?? [], seenRetained);
|
|
594
|
+
const next: BufferEntryState = { ...entry, turns: [...keptTurns, ...late] };
|
|
595
|
+
const nextRetained = [...keptRetained, ...lateRetained];
|
|
596
|
+
if (nextRetained.length > 0) next.retainedTurns = nextRetained;
|
|
597
|
+
await this.writeClaimBody(key, {
|
|
598
|
+
...claim,
|
|
599
|
+
entry: next,
|
|
600
|
+
drained,
|
|
601
|
+
drainedRetained,
|
|
602
|
+
movedTurns: moved.length > 0 ? moved : undefined,
|
|
603
|
+
});
|
|
488
604
|
});
|
|
489
605
|
}
|
|
490
606
|
|
|
@@ -496,7 +612,7 @@ export class BufferDrainClaims {
|
|
|
496
612
|
return await withHeldFileLock(this.lockFileFor(key), { staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS }, async (acquired) => {
|
|
497
613
|
if (!acquired) return "ok";
|
|
498
614
|
const lease = await this.readLease(key);
|
|
499
|
-
if (!lease || lease
|
|
615
|
+
if (!lease || !leaseBelongsToProcess(lease, ownerId)) return "lost";
|
|
500
616
|
await this.writeLease(key, { ...lease, heartbeatAt: new Date().toISOString(), leaseMs });
|
|
501
617
|
return "ok";
|
|
502
618
|
});
|
|
@@ -504,9 +620,16 @@ export class BufferDrainClaims {
|
|
|
504
620
|
|
|
505
621
|
/** Give the key back (content stays; the next run resumes it). */
|
|
506
622
|
async releaseLease(key: string, ownerId: string): Promise<void> {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
623
|
+
await withHeldFileLock(
|
|
624
|
+
this.lockFileFor(key),
|
|
625
|
+
{ staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS },
|
|
626
|
+
async (acquired) => {
|
|
627
|
+
if (!acquired) return;
|
|
628
|
+
const lease = await this.readLease(key);
|
|
629
|
+
if (!leaseBelongsToProcess(lease, ownerId)) return;
|
|
630
|
+
await unlink(this.leaseFileFor(key)).catch(() => {});
|
|
631
|
+
},
|
|
632
|
+
);
|
|
510
633
|
}
|
|
511
634
|
|
|
512
635
|
/**
|
|
@@ -514,25 +637,48 @@ export class BufferDrainClaims {
|
|
|
514
637
|
* extract. Non-empty claims stay (deferred content is never dropped here).
|
|
515
638
|
*/
|
|
516
639
|
async ack(key: string, ownerId: string): Promise<AckOutcome> {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
640
|
+
return await withHeldFileLock(
|
|
641
|
+
this.lockFileFor(key),
|
|
642
|
+
{ staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS },
|
|
643
|
+
async (acquired) => {
|
|
644
|
+
if (!acquired) return "busy";
|
|
645
|
+
const lease = await this.readLease(key);
|
|
646
|
+
if (!leaseBelongsToProcess(lease, ownerId)) return "busy";
|
|
647
|
+
const claim = await this.readClaimBody(key);
|
|
648
|
+
if (!claim) {
|
|
649
|
+
await unlink(this.leaseFileFor(key)).catch(() => {});
|
|
650
|
+
return "acked";
|
|
651
|
+
}
|
|
652
|
+
if (claim.entry.turns.length > 0 || (claim.entry.retainedTurns?.length ?? 0) > 0) return "not_empty";
|
|
653
|
+
// Keep the empty claim as a tombstone until its live owner consumes it.
|
|
654
|
+
await unlink(this.leaseFileFor(key)).catch(() => {});
|
|
655
|
+
return "acked";
|
|
656
|
+
},
|
|
657
|
+
);
|
|
530
658
|
}
|
|
531
659
|
|
|
532
|
-
/**
|
|
533
|
-
async drainedTurns(key: string): Promise<
|
|
660
|
+
/** Return extracted identities with their active/retained provenance intact. */
|
|
661
|
+
async drainedTurns(key: string): Promise<DrainedTurnSets> {
|
|
534
662
|
const claim = await this.readClaimBody(key);
|
|
535
|
-
return
|
|
663
|
+
return {
|
|
664
|
+
turns: claim?.drained ?? [],
|
|
665
|
+
retainedTurns: claim?.drainedRetained ?? [],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Return this lease owner's progress, fenced against claim takeover. */
|
|
670
|
+
async drainedTurnsForOwner(key: string, ownerId: string): Promise<DrainedTurnSets | null> {
|
|
671
|
+
return await withHeldFileLock(
|
|
672
|
+
this.lockFileFor(key),
|
|
673
|
+
{ staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS },
|
|
674
|
+
async (acquired) => {
|
|
675
|
+
if (!acquired) throw new Error(`buffer drain claim for ${key} is busy; cannot reconcile progress`);
|
|
676
|
+
const lease = await this.readLease(key);
|
|
677
|
+
if (!leaseBelongsToProcess(lease, ownerId)) return null;
|
|
678
|
+
const claim = await this.readClaimBody(key);
|
|
679
|
+
return claim ? { turns: claim.drained ?? [], retainedTurns: claim.drainedRetained ?? [] } : null;
|
|
680
|
+
}
|
|
681
|
+
);
|
|
536
682
|
}
|
|
537
683
|
|
|
538
684
|
/**
|
|
@@ -148,9 +148,9 @@ export class BufferSessionFiles {
|
|
|
148
148
|
// the owner's fresh remainder back as a session file (the drained
|
|
149
149
|
// turns are subtracted here too, so a caller that did not subtract
|
|
150
150
|
// them cannot resurrect them) and release the tombstone.
|
|
151
|
-
const fresh: BufferEntryState = { ...entry, turns: subtractBufferTurns(entry.turns, drained) };
|
|
151
|
+
const fresh: BufferEntryState = { ...entry, turns: subtractBufferTurns(entry.turns, drained.turns) };
|
|
152
152
|
if (entry.retainedTurns && entry.retainedTurns.length > 0) {
|
|
153
|
-
fresh.retainedTurns = subtractBufferTurns(entry.retainedTurns, drained);
|
|
153
|
+
fresh.retainedTurns = subtractBufferTurns(entry.retainedTurns, drained.retainedTurns);
|
|
154
154
|
if (fresh.retainedTurns.length === 0) delete fresh.retainedTurns;
|
|
155
155
|
}
|
|
156
156
|
await this.assertDirSafe();
|
|
@@ -99,6 +99,15 @@ export class FactHashIndexNotAuthoritativeError extends Error {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* One dedup lookup: whether the shared index holds the hash, and whether that
|
|
104
|
+
* answer can be trusted as a clean MISS. Both fields describe the same instant.
|
|
105
|
+
*/
|
|
106
|
+
export interface ContentHashLookup {
|
|
107
|
+
readonly present: boolean;
|
|
108
|
+
readonly authoritative: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
type DiskFingerprint = {
|
|
103
112
|
mtimeMs: number;
|
|
104
113
|
size: number;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { StorageManager } from "../storage.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* COS-388c Finding C2: ordinary entity reads must not follow a symlink at
|
|
11
|
+
* `entities/<id>.md` to content outside the memory dir. List skips the
|
|
12
|
+
* same entry. A normal entity file still reads.
|
|
13
|
+
*/
|
|
14
|
+
test("readEntity refuses entities/evil.md symlink outside the memory dir (C2)", async () => {
|
|
15
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-entity-c2-"));
|
|
16
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-entity-c2-out-"));
|
|
17
|
+
try {
|
|
18
|
+
const storage = new StorageManager(dir);
|
|
19
|
+
await storage.ensureDirectories({ waitForEntityMigration: false });
|
|
20
|
+
const id = await storage.writeEntity("Alice", "person", ["Normal entity inside the store."]);
|
|
21
|
+
|
|
22
|
+
await writeFile(path.join(outside, "secret.md"), "OUTSIDE_SECRET_CONTENT\n");
|
|
23
|
+
await symlink(path.join(outside, "secret.md"), path.join(dir, "entities", "evil.md"));
|
|
24
|
+
|
|
25
|
+
const evil = await storage.readEntity("evil");
|
|
26
|
+
assert.equal(
|
|
27
|
+
evil.includes("OUTSIDE_SECRET_CONTENT"),
|
|
28
|
+
false,
|
|
29
|
+
"entity read must not return the outside symlink target",
|
|
30
|
+
);
|
|
31
|
+
assert.equal(evil, "", "symlinked entity id is treated as absent, not as escaped content");
|
|
32
|
+
|
|
33
|
+
const alice = await storage.readEntity(id);
|
|
34
|
+
assert.match(alice, /Normal entity inside the store/);
|
|
35
|
+
|
|
36
|
+
const names = await storage.listEntityNames();
|
|
37
|
+
assert.equal(names.includes("evil"), false, "list must skip the symlinked entity entry");
|
|
38
|
+
assert.ok(names.includes(id), "normal entity remains listable");
|
|
39
|
+
} finally {
|
|
40
|
+
StorageManager.clearAllStaticCaches();
|
|
41
|
+
await rm(dir, { recursive: true, force: true });
|
|
42
|
+
await rm(outside, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
package/src/storage.ts
CHANGED
|
@@ -215,6 +215,7 @@ import {
|
|
|
215
215
|
FactHashIndexNotAuthoritativeError,
|
|
216
216
|
CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS,
|
|
217
217
|
type ContentHashIndexLockOptions,
|
|
218
|
+
type ContentHashLookup,
|
|
218
219
|
} from "./storage/content-hash-index.js";
|
|
219
220
|
import {
|
|
220
221
|
TombstoneBlockedCaptureIndexHost,
|
|
@@ -4172,6 +4173,20 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
4172
4173
|
return await this.ensureFactHashIndexAuthoritative();
|
|
4173
4174
|
}
|
|
4174
4175
|
|
|
4176
|
+
/**
|
|
4177
|
+
* Shared-index membership of `content` plus the authority of that answer,
|
|
4178
|
+
* read in one synchronous step after the freshness check or locked rebuild.
|
|
4179
|
+
* A separate probe and `isFactContentHashAuthoritative()` call can pair a
|
|
4180
|
+
* stale MISS with authority from another moment, because each one may rebuild
|
|
4181
|
+
* the index (#3312). Trust a MISS only when `authoritative`; otherwise
|
|
4182
|
+
* confirm against the corpus.
|
|
4183
|
+
*/
|
|
4184
|
+
async lookupContentHash(content: string): Promise<ContentHashLookup> {
|
|
4185
|
+
const index = await this.getFactHashIndex();
|
|
4186
|
+
await this.ensureFactHashIndexAuthoritative();
|
|
4187
|
+
return { present: index.has(content), authoritative: this.factHashIndexAuthoritative === true };
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4175
4190
|
async writeArtifact(
|
|
4176
4191
|
quote: string,
|
|
4177
4192
|
options: {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { gunzipSync } from "node:zlib";
|
|
2
|
+
|
|
3
|
+
import { isErrnoCode } from "../utils/errno.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hard cap on decompressed capsule JSON for import, merge and inspect. `gunzipSync` without
|
|
7
|
+
* `maxOutputLength` allocates whatever the stream claims, so a small
|
|
8
|
+
* highly-compressible gzip can OOM the process. 64 MiB is enough for ordinary
|
|
9
|
+
* capsules and stops classic bomb shapes at the zlib edge.
|
|
10
|
+
*/
|
|
11
|
+
export const CAPSULE_IMPORT_MAX_GUNZIP_BYTES = 64 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gunzip a capsule archive buffer with a hard decompressed-size cap.
|
|
15
|
+
* `caller` is the public API name used in error messages (`importCapsule`,
|
|
16
|
+
* `mergeCapsule`, `capsule inspect`).
|
|
17
|
+
*/
|
|
18
|
+
export function gunzipCapsuleArchive(
|
|
19
|
+
raw: Buffer,
|
|
20
|
+
caller: string,
|
|
21
|
+
archiveAbs: string,
|
|
22
|
+
): string {
|
|
23
|
+
try {
|
|
24
|
+
return gunzipSync(raw, {
|
|
25
|
+
maxOutputLength: CAPSULE_IMPORT_MAX_GUNZIP_BYTES,
|
|
26
|
+
}).toString("utf-8");
|
|
27
|
+
} catch (cause) {
|
|
28
|
+
if (isErrnoCode(cause, "ERR_BUFFER_TOO_LARGE")) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`${caller}: archive decompressed size exceeds limit of ${CAPSULE_IMPORT_MAX_GUNZIP_BYTES} bytes`,
|
|
31
|
+
cause instanceof Error ? { cause } : undefined,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(
|
|
35
|
+
`${caller}: archive is not a valid gzip file: ${archiveAbs}`,
|
|
36
|
+
cause instanceof Error ? { cause } : undefined,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* capsule gunzip bounds — Finding C3 (gzip bomb / unbounded gunzip).
|
|
3
|
+
*
|
|
4
|
+
* importCapsule/mergeCapsule previously called gunzipSync with no
|
|
5
|
+
* maxOutputLength, so a small highly-compressible archive could expand into
|
|
6
|
+
* multi-gigabyte buffers. These tests pin the refusal of a real gzip-bomb
|
|
7
|
+
* shape and keep a normal capsule import green.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { gzipSync } from "node:zlib";
|
|
15
|
+
|
|
16
|
+
import { makeTempDir } from "../testing/tmp-dir.js";
|
|
17
|
+
import { exportCapsule } from "./capsule-export.js";
|
|
18
|
+
import { CAPSULE_IMPORT_MAX_GUNZIP_BYTES } from "./capsule-gunzip.js";
|
|
19
|
+
import { importCapsule } from "./capsule-import.js";
|
|
20
|
+
|
|
21
|
+
async function writeFact(dir: string, id: string, body: string): Promise<void> {
|
|
22
|
+
await mkdir(path.join(dir, "facts"), { recursive: true });
|
|
23
|
+
await writeFile(
|
|
24
|
+
path.join(dir, "facts", `${id}.md`),
|
|
25
|
+
`---\nid: ${id}\n---\n${body}\n`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("importCapsule refuses a small gzip that expands past the output bound", async () => {
|
|
30
|
+
const dir = await makeTempDir("cap-gunzip-bomb-");
|
|
31
|
+
// Highly compressible payload one byte over the decompressed cap. gzip of
|
|
32
|
+
// 64 MiB of identical bytes stays ~65 KiB — a classic bomb shape.
|
|
33
|
+
const bombPayload = Buffer.alloc(CAPSULE_IMPORT_MAX_GUNZIP_BYTES + 1, 0x41);
|
|
34
|
+
const archivePath = path.join(dir, "bomb.capsule.json.gz");
|
|
35
|
+
await writeFile(archivePath, gzipSync(bombPayload));
|
|
36
|
+
|
|
37
|
+
const root = await makeTempDir("cap-gunzip-bomb-root-");
|
|
38
|
+
await assert.rejects(
|
|
39
|
+
() =>
|
|
40
|
+
importCapsule({
|
|
41
|
+
archivePath,
|
|
42
|
+
root,
|
|
43
|
+
mode: "skip",
|
|
44
|
+
}),
|
|
45
|
+
/importCapsule: archive decompressed size exceeds limit of \d+ bytes/,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("importCapsule still imports a normal capsule archive", async () => {
|
|
50
|
+
const srcDir = await makeTempDir("cap-gunzip-ok-src-");
|
|
51
|
+
const dstDir = await makeTempDir("cap-gunzip-ok-dst-");
|
|
52
|
+
const outDir = await makeTempDir("cap-gunzip-ok-out-");
|
|
53
|
+
|
|
54
|
+
await writeFact(srcDir, "fact-ok", "Normal capsule fact.");
|
|
55
|
+
const exported = await exportCapsule({
|
|
56
|
+
name: "gunzip-ok-capsule",
|
|
57
|
+
root: srcDir,
|
|
58
|
+
outDir,
|
|
59
|
+
pluginVersion: "0.0.0-test",
|
|
60
|
+
now: 1_700_000_000_000,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = await importCapsule({
|
|
64
|
+
archivePath: exported.archivePath,
|
|
65
|
+
root: dstDir,
|
|
66
|
+
mode: "skip",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.equal(result.imported.length, 1);
|
|
70
|
+
assert.equal(result.skipped.length, 0);
|
|
71
|
+
const content = await readFile(path.join(dstDir, "facts", "fact-ok.md"), "utf-8");
|
|
72
|
+
assert.match(content, /Normal capsule fact\./);
|
|
73
|
+
});
|