@remnic/core 9.69.87 → 9.69.89
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 +56 -49
- package/dist/access-authorization-probe.js +57 -50
- package/dist/access-boundary.js +56 -49
- package/dist/access-citation-namespace.js +2 -2
- package/dist/access-cli.js +129 -122
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +56 -49
- package/dist/access-http-briefing.js +57 -50
- package/dist/access-http-offline-stream.d.ts.map +1 -1
- package/dist/access-http-offline-stream.js +1 -1
- package/dist/access-http-query.js +58 -51
- package/dist/access-http.js +78 -71
- package/dist/access-identity-continuity-surface.js +56 -49
- package/dist/access-lcm-surface.js +56 -49
- package/dist/access-mcp-output-schemas.d.ts.map +1 -1
- package/dist/access-mcp-output-schemas.js +1 -1
- package/dist/access-mcp.js +69 -62
- package/dist/access-namespace-preflight.js +56 -49
- package/dist/access-observe-write-category.js +20 -13
- package/dist/access-observe-write-surface.js +56 -49
- package/dist/access-offline-manifest.js +16 -10
- package/dist/access-operations-batch.js +60 -53
- package/dist/access-operations.js +67 -60
- package/dist/access-recall-concurrency.js +56 -49
- package/dist/access-recall-response.js +56 -49
- package/dist/access-recall-surface.js +56 -49
- package/dist/access-schema.js +16 -7
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +56 -49
- package/dist/access-surface-catalog.js +58 -51
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +24 -18
- package/dist/active-recall.js.map +1 -1
- package/dist/{auto-sync-6IPX2BAC.js → auto-sync-U5N3CB5Y.js} +7 -7
- package/dist/briefing.js +11 -11
- package/dist/buffer.js +7 -7
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/{capsule-merge-3MNURAQ3.js → capsule-merge-ZCXUPLNJ.js} +2 -2
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +14 -14
- 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-JE334SIA.js → chunk-22DYJLFN.js} +2 -2
- package/dist/{chunk-YTQGGO3K.js → chunk-22LMCQ5B.js} +85 -82
- package/dist/{chunk-YTQGGO3K.js.map → chunk-22LMCQ5B.js.map} +1 -1
- package/dist/{chunk-JGSQ6P4D.js → chunk-2FX374UI.js} +2 -2
- package/dist/{chunk-SY5KDWGU.js → chunk-2WLMWP2T.js} +2 -2
- package/dist/{chunk-6TT6HRE4.js → chunk-3AGHSQPM.js} +2 -2
- package/dist/{chunk-P6WFK4ZJ.js → chunk-3O5BB45T.js} +4 -4
- package/dist/{chunk-3C3J243X.js → chunk-3QKO4QN7.js} +3 -3
- package/dist/{chunk-KFCMMDHH.js → chunk-3YD44CH2.js} +2 -2
- package/dist/{chunk-FRCESVV6.js → chunk-4HUCP4R4.js} +18 -18
- package/dist/chunk-4HUCP4R4.js.map +1 -0
- package/dist/chunk-4NSLQ7EU.js +137 -0
- package/dist/chunk-4NSLQ7EU.js.map +1 -0
- package/dist/{chunk-7DE2NOON.js → chunk-4O4XCBOS.js} +3 -3
- package/dist/{chunk-GDYU6VF4.js → chunk-5AYUHSAU.js} +2 -2
- package/dist/{chunk-ICZQG4XE.js → chunk-5FAGCOZD.js} +4 -4
- package/dist/{chunk-VKQI47DJ.js → chunk-5FNZ3G6X.js} +4 -4
- package/dist/{chunk-CK5KOVHW.js → chunk-5R627MYY.js} +3 -2
- package/dist/{chunk-CK5KOVHW.js.map → chunk-5R627MYY.js.map} +1 -1
- package/dist/{chunk-VQNONC4E.js → chunk-5STPRPE7.js} +2 -2
- package/dist/{chunk-52II7PB6.js → chunk-645WOU5I.js} +4 -4
- package/dist/{chunk-SRZW7FNU.js → chunk-64ZUALXK.js} +2 -2
- package/dist/{chunk-4BZP2B4U.js → chunk-6MNKDTDI.js} +2 -2
- package/dist/{chunk-DUJYRVUD.js → chunk-6O2VUWV6.js} +3 -3
- package/dist/chunk-75PT4N7Q.js +75 -0
- package/dist/chunk-75PT4N7Q.js.map +1 -0
- package/dist/{chunk-HESDNYRB.js → chunk-76M5OK7V.js} +2 -2
- package/dist/{chunk-FAN73COB.js → chunk-7D7OUO5W.js} +2 -2
- package/dist/{chunk-DZ525SNQ.js → chunk-7QE46QXI.js} +1 -1
- package/dist/{chunk-VX6GCKBS.js → chunk-7WBSMXGC.js} +4 -4
- package/dist/{chunk-VVDWZRZG.js → chunk-A6RVIHHK.js} +3 -3
- package/dist/{chunk-M536WNTQ.js → chunk-AEJ3TE5H.js} +2 -2
- package/dist/{chunk-72JNNJJJ.js → chunk-AXC4WSF3.js} +3 -3
- package/dist/chunk-AXGJMDBS.js +684 -0
- package/dist/chunk-AXGJMDBS.js.map +1 -0
- package/dist/{chunk-UA5DX5C3.js → chunk-AZIOEOK4.js} +2 -2
- package/dist/{chunk-N4S4NUX5.js → chunk-B3O3IRZN.js} +1 -1
- package/dist/{chunk-XTYX6V52.js → chunk-BJVO4H5C.js} +2 -2
- package/dist/{chunk-7LCGFUMB.js → chunk-BUKK5SWA.js} +4 -4
- package/dist/{chunk-TM3RXL4G.js → chunk-BUQKSQXL.js} +2 -2
- package/dist/chunk-C6HPNPNK.js +64 -0
- package/dist/chunk-C6HPNPNK.js.map +1 -0
- package/dist/{chunk-RB776UH3.js → chunk-C7V3KPKJ.js} +235 -93
- package/dist/chunk-C7V3KPKJ.js.map +1 -0
- package/dist/{chunk-FOH7AMMI.js → chunk-CALBQ2NG.js} +71 -72
- package/dist/chunk-CALBQ2NG.js.map +1 -0
- package/dist/{chunk-37S3IMYB.js → chunk-DGS34QSY.js} +2 -2
- package/dist/{chunk-4IEOGIPB.js → chunk-DO4C4AXD.js} +2 -2
- package/dist/{chunk-SPSRGEDQ.js → chunk-EAQQ2FD3.js} +9 -9
- package/dist/{chunk-3BTFBQLO.js → chunk-EFHNFTWB.js} +49 -3
- package/dist/chunk-EFHNFTWB.js.map +1 -0
- package/dist/{chunk-C3QXITIP.js → chunk-EQUD7FP4.js} +2 -2
- package/dist/chunk-ETUZFFPU.js +218 -0
- package/dist/chunk-ETUZFFPU.js.map +1 -0
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-YITN5WXI.js → chunk-FC52YWAO.js} +2 -2
- package/dist/{chunk-ZI7QA4MK.js → chunk-FEIXG5FA.js} +2 -2
- package/dist/{chunk-CLXTDOKV.js → chunk-FUNBJQYR.js} +2 -2
- package/dist/{chunk-CLSYE5OZ.js → chunk-G2STTRYB.js} +2 -2
- package/dist/{chunk-TDYAIXHK.js → chunk-H36X4NRH.js} +5 -5
- package/dist/{chunk-3GQC3NMB.js → chunk-H56VY4BY.js} +5 -5
- package/dist/{chunk-PUKTBULQ.js → chunk-HWLIMQE2.js} +2 -2
- package/dist/{chunk-V2ES4WCZ.js → chunk-IIARQTZ2.js} +2 -2
- package/dist/{chunk-QEV26L2F.js → chunk-IKDRGVVJ.js} +2 -2
- package/dist/{chunk-WQZZDN5M.js → chunk-IVEHY2VT.js} +254 -377
- package/dist/chunk-IVEHY2VT.js.map +1 -0
- package/dist/{chunk-LS3XH3UE.js → chunk-IYIBGSHJ.js} +2 -2
- package/dist/{chunk-YSJTCOHR.js → chunk-J27JML5O.js} +2 -2
- package/dist/{chunk-AQK54Q7O.js → chunk-JB56XYHP.js} +2 -2
- package/dist/{chunk-NE6PHPLG.js → chunk-JBH26P62.js} +10 -10
- package/dist/{chunk-CWHV574S.js → chunk-JUT3OR2L.js} +6 -6
- package/dist/{chunk-6ET2VII7.js → chunk-JYG2334B.js} +2 -2
- package/dist/{chunk-LF5IGOOD.js → chunk-JYM4Y6SO.js} +16 -16
- package/dist/{chunk-FB64XE3R.js → chunk-JZ4TJHR2.js} +2 -2
- package/dist/{chunk-EHS5MJEF.js → chunk-KTKHO3UI.js} +3 -3
- package/dist/{chunk-WIKO4SCR.js → chunk-LHRQBMJN.js} +2 -2
- package/dist/{chunk-K4J4E7WV.js → chunk-LO4UKPKX.js} +2 -2
- package/dist/{chunk-JN72EPOE.js → chunk-MAAL6OZV.js} +7 -7
- package/dist/chunk-MAAL6OZV.js.map +1 -0
- package/dist/{chunk-MY2PLXSM.js → chunk-MCTM4YPL.js} +2 -1
- package/dist/{chunk-MY2PLXSM.js.map → chunk-MCTM4YPL.js.map} +1 -1
- package/dist/{chunk-6NQ4LSMS.js → chunk-MGZMC6WN.js} +47 -47
- package/dist/chunk-MSAI3Q7D.js +372 -0
- package/dist/chunk-MSAI3Q7D.js.map +1 -0
- package/dist/{chunk-BMYZTZ5D.js → chunk-MY7B3HYC.js} +2 -2
- package/dist/{chunk-NB3LRY4T.js → chunk-N4PYB235.js} +2 -2
- package/dist/{chunk-S3YM4SZQ.js → chunk-N7QV3CMX.js} +2 -2
- package/dist/{chunk-L6UZEAID.js → chunk-NJVR724O.js} +4 -4
- package/dist/{chunk-VLLD4TOB.js → chunk-NMZVDRVP.js} +2 -2
- package/dist/{chunk-P2WZHLUF.js → chunk-NOL7SEWN.js} +2 -2
- package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-6L3PBSAR.js → chunk-PMNLL2TL.js} +2 -2
- package/dist/{chunk-VYCOJKSX.js → chunk-PO7NXNGC.js} +2 -2
- package/dist/{chunk-4IIMMZZK.js → chunk-PULDSMCJ.js} +10 -10
- package/dist/{chunk-G6SVZWPB.js → chunk-PZX3AQLF.js} +44 -1
- package/dist/chunk-PZX3AQLF.js.map +1 -0
- package/dist/{chunk-HBBHL4YF.js → chunk-QOXNTKXB.js} +2 -2
- package/dist/{chunk-CUCWX3MO.js → chunk-QZLCVO2V.js} +2 -2
- package/dist/{chunk-NE7I2RCT.js → chunk-R3X2T2LP.js} +2 -2
- package/dist/{chunk-S7GCT7NU.js → chunk-RBITPCMK.js} +8 -8
- package/dist/{chunk-EVBI7S4J.js → chunk-RMDGAAZF.js} +4 -4
- package/dist/{chunk-BJRAELDH.js → chunk-SCXURCVT.js} +3 -3
- package/dist/{chunk-EQ6YOIKO.js → chunk-ST2RQUXC.js} +3 -3
- package/dist/{chunk-CS52MFDV.js → chunk-SZG4WFAG.js} +2 -2
- package/dist/{chunk-BFO3IR56.js → chunk-TPEZ2SHV.js} +2 -2
- package/dist/chunk-TYT4TEQV.js +45 -0
- package/dist/chunk-TYT4TEQV.js.map +1 -0
- package/dist/{chunk-YRLHEEQT.js → chunk-U6VC6RP3.js} +2 -2
- package/dist/chunk-UCPGOUDZ.js +726 -0
- package/dist/chunk-UCPGOUDZ.js.map +1 -0
- package/dist/{chunk-MOQPEFLN.js → chunk-UP75KL3L.js} +2 -2
- package/dist/{chunk-W4AOR52H.js → chunk-UQDC3W77.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-D5F7TKMH.js → chunk-V4JZCTMK.js} +2 -2
- package/dist/{chunk-MQJL6CDZ.js → chunk-VA4O3U7U.js} +4 -4
- package/dist/{chunk-4CDYONWO.js → chunk-VDO32NLK.js} +3 -3
- package/dist/{chunk-SOZUJCME.js → chunk-VHDKPGMG.js} +6 -6
- package/dist/{chunk-QWW2JDDX.js → chunk-WNRTXEWW.js} +6 -6
- package/dist/{chunk-NR2ASHBL.js → chunk-XOV7NRY5.js} +2 -2
- package/dist/{chunk-KP7SANCF.js → chunk-YPI4RQWV.js} +2 -2
- package/dist/{chunk-JKMHZF3Q.js → chunk-YSWJXOOL.js} +2 -2
- package/dist/{chunk-LQG5O5EP.js → chunk-Z3O53WZR.js} +2 -2
- package/dist/{chunk-QVS22WTO.js → chunk-ZG5DWWKK.js} +2 -2
- package/dist/cli.js +99 -92
- package/dist/coding/export-okf-codegraph.js +12 -12
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +12 -12
- package/dist/config.js +24 -18
- package/dist/connectors/codex-materialize-runner.js +11 -11
- package/dist/connectors/index.js +11 -11
- package/dist/console/state.d.ts +30 -0
- package/dist/console/state.d.ts.map +1 -1
- package/dist/contradiction/index.js +5 -5
- package/dist/convergence-refresh.js +13 -13
- package/dist/corpus-watermark.js +13 -13
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-http-glue.js +16 -8
- package/dist/embedding-fallback.d.ts +41 -31
- package/dist/embedding-fallback.d.ts.map +1 -1
- package/dist/embedding-fallback.js +9 -1
- package/dist/embedding-generation-lock.d.ts +49 -0
- package/dist/embedding-generation-lock.d.ts.map +1 -0
- package/dist/embedding-generation-lock.js +19 -0
- package/dist/embedding-index-storage.d.ts +276 -0
- package/dist/embedding-index-storage.d.ts.map +1 -0
- package/dist/embedding-index-storage.js +28 -0
- package/dist/entity-retrieval.js +12 -12
- package/dist/event-order-recall.js +3 -3
- package/dist/explicit-capture.js +6 -6
- package/dist/external-wiki-access.js +57 -50
- package/dist/extraction.js +17 -17
- package/dist/{first-start-migration-7OATHJKB.js → first-start-migration-GAT7G733.js} +4 -4
- package/dist/{graph-edge-decay-KQGS3L3B.js → graph-edge-decay-MKIRZ762.js} +5 -5
- 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 +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +195 -164
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +14 -14
- package/dist/maintenance/memory-governance.js +11 -11
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +11 -11
- package/dist/maintenance/rebuild-memory-projection.js +12 -12
- package/dist/memory-browse-operations.js +57 -50
- package/dist/memory-subject.js +3 -3
- package/dist/namespaces/migrate.js +12 -12
- package/dist/namespaces/storage.js +11 -11
- package/dist/offline-sync-changeset-generations.d.ts +82 -0
- package/dist/offline-sync-changeset-generations.d.ts.map +1 -0
- package/dist/offline-sync-changeset-generations.js +21 -0
- package/dist/offline-sync-embedding-generation.d.ts +257 -0
- package/dist/offline-sync-embedding-generation.d.ts.map +1 -0
- package/dist/offline-sync-embedding-generation.js +44 -0
- package/dist/offline-sync-embedding-generation.js.map +1 -0
- package/dist/offline-sync-exclude-globs.d.ts +15 -0
- package/dist/offline-sync-exclude-globs.d.ts.map +1 -1
- package/dist/offline-sync-exclude-globs.js +11 -3
- package/dist/offline-sync-file-io.d.ts +7 -0
- package/dist/offline-sync-file-io.d.ts.map +1 -1
- package/dist/offline-sync-file-io.js +14 -2
- package/dist/offline-sync-impression-drain.js +3 -3
- package/dist/offline-sync-runtime-state.d.ts +5 -0
- package/dist/offline-sync-runtime-state.d.ts.map +1 -0
- package/dist/offline-sync-runtime-state.js +14 -0
- package/dist/offline-sync-runtime-state.js.map +1 -0
- package/dist/offline-sync-snapshot-stream.d.ts +26 -0
- package/dist/offline-sync-snapshot-stream.d.ts.map +1 -0
- package/dist/offline-sync-snapshot-stream.js +29 -0
- package/dist/offline-sync-snapshot-stream.js.map +1 -0
- package/dist/offline-sync-upload-staging.d.ts +34 -0
- package/dist/offline-sync-upload-staging.d.ts.map +1 -0
- package/dist/offline-sync-upload-staging.js +26 -0
- package/dist/offline-sync-upload-staging.js.map +1 -0
- package/dist/offline-sync.d.ts +58 -28
- package/dist/offline-sync.d.ts.map +1 -1
- package/dist/offline-sync.js +22 -8
- package/dist/operator-doctor-corpus.js +14 -14
- package/dist/operator-toolkit.js +35 -29
- package/dist/orchestration/maintenance.js +22 -22
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +129 -122
- package/dist/recall-navigation-operations.js +57 -50
- package/dist/recall-navigation.js +56 -49
- package/dist/reconcile/manifest.js +4 -4
- package/dist/{resolution-DNX7FO7A.js → resolution-K4A3VPKZ.js} +6 -6
- package/dist/resolution-K4A3VPKZ.js.map +1 -0
- package/dist/resume-bundles.js +25 -19
- package/dist/salvage-envelope.js +5 -5
- package/dist/secure-store/index.js +2 -2
- package/dist/secure-store/secure-fs.d.ts +14 -1
- package/dist/secure-store/secure-fs.d.ts.map +1 -1
- package/dist/semantic-consolidation.js +12 -12
- package/dist/semantic-rule-promotion.js +11 -11
- package/dist/semantic-rule-verifier.js +11 -11
- package/dist/source-agent-qualifier.js +14 -14
- package/dist/standup/build.js +11 -11
- package/dist/{state-ITXJKXRE.js → state-JB47KHYV.js} +2 -2
- package/dist/state-JB47KHYV.js.map +1 -0
- package/dist/storage.d.ts +9 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +10 -10
- package/dist/subject-config.js +4 -4
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +59 -52
- package/dist/support-passport/offline-sync-guard.d.ts +6 -2
- package/dist/support-passport/offline-sync-guard.d.ts.map +1 -1
- package/dist/{trace-ZFVRH4GO.js → trace-LXXCMZJA.js} +3 -3
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/export-okf.js +12 -12
- package/dist/{tui-55UJMJX7.js → tui-2Z5WOV7L.js} +3 -3
- package/dist/tui-2Z5WOV7L.js.map +1 -0
- package/dist/verified-recall.js +11 -11
- package/dist/who-knows-cli.js +12 -12
- package/dist/who-knows.js +11 -11
- package/dist/write-envelope.js +4 -4
- package/package.json +2 -2
- package/src/access-http-offline-stream.ts +3 -0
- package/src/access-mcp-output-schemas.ts +1 -0
- package/src/access-service.ts +7 -18
- package/src/console/state.test.ts +56 -0
- package/src/console/state.ts +83 -0
- package/src/embedding-fallback.ts +325 -141
- package/src/embedding-generation-lock.ts +141 -0
- package/src/embedding-index-storage.test.ts +245 -0
- package/src/embedding-index-storage.ts +1068 -0
- package/src/index.ts +3 -3
- package/src/offline-sync-changeset-generations.test.ts +962 -0
- package/src/offline-sync-changeset-generations.ts +549 -0
- package/src/offline-sync-embedding-generation.test.ts +2173 -0
- package/src/offline-sync-embedding-generation.ts +1153 -0
- package/src/offline-sync-exclude-globs.ts +112 -0
- package/src/offline-sync-file-io.ts +48 -0
- package/src/offline-sync-runtime-state.ts +48 -0
- package/src/offline-sync-snapshot-stream.ts +99 -0
- package/src/offline-sync-upload-staging.ts +291 -0
- package/src/offline-sync.test.ts +164 -1
- package/src/offline-sync.ts +372 -454
- package/src/orchestrator.ts +2 -1
- package/src/secure-store/secure-fs.ts +26 -6
- package/src/storage.ts +11 -6
- package/src/support-passport/offline-sync-guard.ts +16 -2
- package/dist/chunk-3BTFBQLO.js.map +0 -1
- package/dist/chunk-AREGMX2J.js +0 -50
- package/dist/chunk-AREGMX2J.js.map +0 -1
- package/dist/chunk-FOH7AMMI.js.map +0 -1
- package/dist/chunk-FRCESVV6.js.map +0 -1
- package/dist/chunk-G6SVZWPB.js.map +0 -1
- package/dist/chunk-JN72EPOE.js.map +0 -1
- package/dist/chunk-RB776UH3.js.map +0 -1
- package/dist/chunk-WQZZDN5M.js.map +0 -1
- /package/dist/{auto-sync-6IPX2BAC.js.map → auto-sync-U5N3CB5Y.js.map} +0 -0
- /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
- /package/dist/{capsule-merge-3MNURAQ3.js.map → capsule-merge-ZCXUPLNJ.js.map} +0 -0
- /package/dist/{chunk-JE334SIA.js.map → chunk-22DYJLFN.js.map} +0 -0
- /package/dist/{chunk-JGSQ6P4D.js.map → chunk-2FX374UI.js.map} +0 -0
- /package/dist/{chunk-SY5KDWGU.js.map → chunk-2WLMWP2T.js.map} +0 -0
- /package/dist/{chunk-6TT6HRE4.js.map → chunk-3AGHSQPM.js.map} +0 -0
- /package/dist/{chunk-P6WFK4ZJ.js.map → chunk-3O5BB45T.js.map} +0 -0
- /package/dist/{chunk-3C3J243X.js.map → chunk-3QKO4QN7.js.map} +0 -0
- /package/dist/{chunk-KFCMMDHH.js.map → chunk-3YD44CH2.js.map} +0 -0
- /package/dist/{chunk-7DE2NOON.js.map → chunk-4O4XCBOS.js.map} +0 -0
- /package/dist/{chunk-GDYU6VF4.js.map → chunk-5AYUHSAU.js.map} +0 -0
- /package/dist/{chunk-ICZQG4XE.js.map → chunk-5FAGCOZD.js.map} +0 -0
- /package/dist/{chunk-VKQI47DJ.js.map → chunk-5FNZ3G6X.js.map} +0 -0
- /package/dist/{chunk-VQNONC4E.js.map → chunk-5STPRPE7.js.map} +0 -0
- /package/dist/{chunk-52II7PB6.js.map → chunk-645WOU5I.js.map} +0 -0
- /package/dist/{chunk-SRZW7FNU.js.map → chunk-64ZUALXK.js.map} +0 -0
- /package/dist/{chunk-4BZP2B4U.js.map → chunk-6MNKDTDI.js.map} +0 -0
- /package/dist/{chunk-DUJYRVUD.js.map → chunk-6O2VUWV6.js.map} +0 -0
- /package/dist/{chunk-HESDNYRB.js.map → chunk-76M5OK7V.js.map} +0 -0
- /package/dist/{chunk-FAN73COB.js.map → chunk-7D7OUO5W.js.map} +0 -0
- /package/dist/{chunk-DZ525SNQ.js.map → chunk-7QE46QXI.js.map} +0 -0
- /package/dist/{chunk-VX6GCKBS.js.map → chunk-7WBSMXGC.js.map} +0 -0
- /package/dist/{chunk-VVDWZRZG.js.map → chunk-A6RVIHHK.js.map} +0 -0
- /package/dist/{chunk-M536WNTQ.js.map → chunk-AEJ3TE5H.js.map} +0 -0
- /package/dist/{chunk-72JNNJJJ.js.map → chunk-AXC4WSF3.js.map} +0 -0
- /package/dist/{chunk-UA5DX5C3.js.map → chunk-AZIOEOK4.js.map} +0 -0
- /package/dist/{chunk-N4S4NUX5.js.map → chunk-B3O3IRZN.js.map} +0 -0
- /package/dist/{chunk-XTYX6V52.js.map → chunk-BJVO4H5C.js.map} +0 -0
- /package/dist/{chunk-7LCGFUMB.js.map → chunk-BUKK5SWA.js.map} +0 -0
- /package/dist/{chunk-TM3RXL4G.js.map → chunk-BUQKSQXL.js.map} +0 -0
- /package/dist/{chunk-37S3IMYB.js.map → chunk-DGS34QSY.js.map} +0 -0
- /package/dist/{chunk-4IEOGIPB.js.map → chunk-DO4C4AXD.js.map} +0 -0
- /package/dist/{chunk-SPSRGEDQ.js.map → chunk-EAQQ2FD3.js.map} +0 -0
- /package/dist/{chunk-C3QXITIP.js.map → chunk-EQUD7FP4.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-YITN5WXI.js.map → chunk-FC52YWAO.js.map} +0 -0
- /package/dist/{chunk-ZI7QA4MK.js.map → chunk-FEIXG5FA.js.map} +0 -0
- /package/dist/{chunk-CLXTDOKV.js.map → chunk-FUNBJQYR.js.map} +0 -0
- /package/dist/{chunk-CLSYE5OZ.js.map → chunk-G2STTRYB.js.map} +0 -0
- /package/dist/{chunk-TDYAIXHK.js.map → chunk-H36X4NRH.js.map} +0 -0
- /package/dist/{chunk-3GQC3NMB.js.map → chunk-H56VY4BY.js.map} +0 -0
- /package/dist/{chunk-PUKTBULQ.js.map → chunk-HWLIMQE2.js.map} +0 -0
- /package/dist/{chunk-V2ES4WCZ.js.map → chunk-IIARQTZ2.js.map} +0 -0
- /package/dist/{chunk-QEV26L2F.js.map → chunk-IKDRGVVJ.js.map} +0 -0
- /package/dist/{chunk-LS3XH3UE.js.map → chunk-IYIBGSHJ.js.map} +0 -0
- /package/dist/{chunk-YSJTCOHR.js.map → chunk-J27JML5O.js.map} +0 -0
- /package/dist/{chunk-AQK54Q7O.js.map → chunk-JB56XYHP.js.map} +0 -0
- /package/dist/{chunk-NE6PHPLG.js.map → chunk-JBH26P62.js.map} +0 -0
- /package/dist/{chunk-CWHV574S.js.map → chunk-JUT3OR2L.js.map} +0 -0
- /package/dist/{chunk-6ET2VII7.js.map → chunk-JYG2334B.js.map} +0 -0
- /package/dist/{chunk-LF5IGOOD.js.map → chunk-JYM4Y6SO.js.map} +0 -0
- /package/dist/{chunk-FB64XE3R.js.map → chunk-JZ4TJHR2.js.map} +0 -0
- /package/dist/{chunk-EHS5MJEF.js.map → chunk-KTKHO3UI.js.map} +0 -0
- /package/dist/{chunk-WIKO4SCR.js.map → chunk-LHRQBMJN.js.map} +0 -0
- /package/dist/{chunk-K4J4E7WV.js.map → chunk-LO4UKPKX.js.map} +0 -0
- /package/dist/{chunk-6NQ4LSMS.js.map → chunk-MGZMC6WN.js.map} +0 -0
- /package/dist/{chunk-BMYZTZ5D.js.map → chunk-MY7B3HYC.js.map} +0 -0
- /package/dist/{chunk-NB3LRY4T.js.map → chunk-N4PYB235.js.map} +0 -0
- /package/dist/{chunk-S3YM4SZQ.js.map → chunk-N7QV3CMX.js.map} +0 -0
- /package/dist/{chunk-L6UZEAID.js.map → chunk-NJVR724O.js.map} +0 -0
- /package/dist/{chunk-VLLD4TOB.js.map → chunk-NMZVDRVP.js.map} +0 -0
- /package/dist/{chunk-P2WZHLUF.js.map → chunk-NOL7SEWN.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-6L3PBSAR.js.map → chunk-PMNLL2TL.js.map} +0 -0
- /package/dist/{chunk-VYCOJKSX.js.map → chunk-PO7NXNGC.js.map} +0 -0
- /package/dist/{chunk-4IIMMZZK.js.map → chunk-PULDSMCJ.js.map} +0 -0
- /package/dist/{chunk-HBBHL4YF.js.map → chunk-QOXNTKXB.js.map} +0 -0
- /package/dist/{chunk-CUCWX3MO.js.map → chunk-QZLCVO2V.js.map} +0 -0
- /package/dist/{chunk-NE7I2RCT.js.map → chunk-R3X2T2LP.js.map} +0 -0
- /package/dist/{chunk-S7GCT7NU.js.map → chunk-RBITPCMK.js.map} +0 -0
- /package/dist/{chunk-EVBI7S4J.js.map → chunk-RMDGAAZF.js.map} +0 -0
- /package/dist/{chunk-BJRAELDH.js.map → chunk-SCXURCVT.js.map} +0 -0
- /package/dist/{chunk-EQ6YOIKO.js.map → chunk-ST2RQUXC.js.map} +0 -0
- /package/dist/{chunk-CS52MFDV.js.map → chunk-SZG4WFAG.js.map} +0 -0
- /package/dist/{chunk-BFO3IR56.js.map → chunk-TPEZ2SHV.js.map} +0 -0
- /package/dist/{chunk-YRLHEEQT.js.map → chunk-U6VC6RP3.js.map} +0 -0
- /package/dist/{chunk-MOQPEFLN.js.map → chunk-UP75KL3L.js.map} +0 -0
- /package/dist/{chunk-W4AOR52H.js.map → chunk-UQDC3W77.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
- /package/dist/{chunk-D5F7TKMH.js.map → chunk-V4JZCTMK.js.map} +0 -0
- /package/dist/{chunk-MQJL6CDZ.js.map → chunk-VA4O3U7U.js.map} +0 -0
- /package/dist/{chunk-4CDYONWO.js.map → chunk-VDO32NLK.js.map} +0 -0
- /package/dist/{chunk-SOZUJCME.js.map → chunk-VHDKPGMG.js.map} +0 -0
- /package/dist/{chunk-QWW2JDDX.js.map → chunk-WNRTXEWW.js.map} +0 -0
- /package/dist/{chunk-NR2ASHBL.js.map → chunk-XOV7NRY5.js.map} +0 -0
- /package/dist/{chunk-KP7SANCF.js.map → chunk-YPI4RQWV.js.map} +0 -0
- /package/dist/{chunk-JKMHZF3Q.js.map → chunk-YSWJXOOL.js.map} +0 -0
- /package/dist/{chunk-LQG5O5EP.js.map → chunk-Z3O53WZR.js.map} +0 -0
- /package/dist/{chunk-QVS22WTO.js.map → chunk-ZG5DWWKK.js.map} +0 -0
- /package/dist/{graph-edge-decay-KQGS3L3B.js.map → embedding-generation-lock.js.map} +0 -0
- /package/dist/{resolution-DNX7FO7A.js.map → embedding-index-storage.js.map} +0 -0
- /package/dist/{first-start-migration-7OATHJKB.js.map → first-start-migration-GAT7G733.js.map} +0 -0
- /package/dist/{state-ITXJKXRE.js.map → graph-edge-decay-MKIRZ762.js.map} +0 -0
- /package/dist/{tui-55UJMJX7.js.map → offline-sync-changeset-generations.js.map} +0 -0
- /package/dist/{trace-ZFVRH4GO.js.map → trace-LXXCMZJA.js.map} +0 -0
package/src/offline-sync.ts
CHANGED
|
@@ -24,7 +24,36 @@ import {
|
|
|
24
24
|
type SafeArchiveRoot,
|
|
25
25
|
} from "./transfer/fs-utils.js";
|
|
26
26
|
import { parseFlexibleIsoTimestamp } from "./utils/iso-timestamp.js";
|
|
27
|
-
import {
|
|
27
|
+
import { EmbeddingIndexStorageError } from "./embedding-index-storage.js";
|
|
28
|
+
import { matchesOfflineSyncDefaultExclude } from "./offline-sync-exclude-globs.js";
|
|
29
|
+
import {
|
|
30
|
+
applyIncomingEmbeddingGenerations,
|
|
31
|
+
assertEmbeddingGenerationStillIncluded,
|
|
32
|
+
computeOmittedEmbeddingGenerationPaths,
|
|
33
|
+
divergedEmbeddingGenerationDeferrals,
|
|
34
|
+
embeddingGenerationMembership,
|
|
35
|
+
isEmbeddingGenerationDirPath,
|
|
36
|
+
isPathInOmittedEmbeddingGeneration,
|
|
37
|
+
} from "./offline-sync-embedding-generation.js";
|
|
38
|
+
import {
|
|
39
|
+
applyChangesetEmbeddingGenerations,
|
|
40
|
+
normalizeChangesetGenerations,
|
|
41
|
+
type OfflineSyncChangesetGeneration,
|
|
42
|
+
} from "./offline-sync-changeset-generations.js";
|
|
43
|
+
import { withEmbeddingGenerationLock, withEmbeddingGenerationLockIter } from "./embedding-generation-lock.js";
|
|
44
|
+
import {
|
|
45
|
+
isCanonicalRuntimeStatePath,
|
|
46
|
+
shouldDeleteAbsentIncomingOfflineRuntimeFile,
|
|
47
|
+
shouldPreferIncomingOfflineRuntimeFile,
|
|
48
|
+
} from "./offline-sync-runtime-state.js";
|
|
49
|
+
export { shouldPreferIncomingOfflineRuntimeFile } from "./offline-sync-runtime-state.js";
|
|
50
|
+
export { computeOmittedEmbeddingGenerationPaths } from "./offline-sync-embedding-generation.js";
|
|
51
|
+
export type { OfflineSyncChangesetGeneration } from "./offline-sync-changeset-generations.js";
|
|
52
|
+
export {
|
|
53
|
+
compileOfflineSyncExcludeGlobs,
|
|
54
|
+
globToRegExp,
|
|
55
|
+
parseOfflineSyncExcludes,
|
|
56
|
+
} from "./offline-sync-exclude-globs.js";
|
|
28
57
|
import {
|
|
29
58
|
isEncryptedOfflineSyncFile,
|
|
30
59
|
readPlainOfflineSyncFileChunk,
|
|
@@ -33,8 +62,21 @@ import {
|
|
|
33
62
|
type OfflineSyncExcludeFile,
|
|
34
63
|
type OfflineSyncFileTarget,
|
|
35
64
|
plainFileDigest,
|
|
65
|
+
SYNC_INTERNAL_DIR,
|
|
66
|
+
setSafeFileMtime,
|
|
67
|
+
assertNonNegativeFinite,
|
|
68
|
+
assertOfflineSyncMtimeMs,
|
|
69
|
+
OFFLINE_SYNC_FAST_BASE_MTIME_TOLERANCE_MS,
|
|
36
70
|
} from "./offline-sync-file-io.js";
|
|
37
|
-
import { CENSUS_MAX_MTIME_MS,
|
|
71
|
+
import { CENSUS_MAX_MTIME_MS, isSha256Hex } from "./census-validation.js";
|
|
72
|
+
import {
|
|
73
|
+
cleanupOfflineUpload,
|
|
74
|
+
digestOfflineUploadStagingContent,
|
|
75
|
+
pruneOfflineUploadStaging,
|
|
76
|
+
writeOfflineUploadChunk,
|
|
77
|
+
writeSafeFileFromUpload,
|
|
78
|
+
hashText,
|
|
79
|
+
} from "./offline-sync-upload-staging.js";
|
|
38
80
|
export type { OfflineSyncExcludeFile, OfflineSyncFileTarget } from "./offline-sync-file-io.js";
|
|
39
81
|
|
|
40
82
|
export const OFFLINE_SYNC_SNAPSHOT_FORMAT = "remnic.offline-sync.snapshot.v1";
|
|
@@ -76,6 +118,14 @@ export interface OfflineSyncSnapshot {
|
|
|
76
118
|
includeTranscripts: boolean;
|
|
77
119
|
files: OfflineSyncFileRecord[];
|
|
78
120
|
deletions?: OfflineSyncDeletionRevision[];
|
|
121
|
+
/**
|
|
122
|
+
* Embedding generation dirs whose on-disk members were ALL omitted from
|
|
123
|
+
* `files` because a push filter excluded at least one member (PR #3148
|
|
124
|
+
* round 4). Their absence here is NOT a delete instruction: a receiver
|
|
125
|
+
* keeps its local copy and its base entries until the generation is
|
|
126
|
+
* pushed unfiltered or genuinely deleted with deletion metadata.
|
|
127
|
+
*/
|
|
128
|
+
omittedEmbeddingGenerationDirs?: string[];
|
|
79
129
|
}
|
|
80
130
|
|
|
81
131
|
export type OfflineSyncChange =
|
|
@@ -99,6 +149,13 @@ export interface OfflineSyncChangeset {
|
|
|
99
149
|
sourceId: string;
|
|
100
150
|
includeTranscripts: boolean;
|
|
101
151
|
changes: OfflineSyncChange[];
|
|
152
|
+
/**
|
|
153
|
+
* Complete post-change membership of every embedding generation the
|
|
154
|
+
* changeset touches (issue #3150). Absent on legacy senders: receivers
|
|
155
|
+
* defer manifest-less shard upserts as conflicts instead of publishing a
|
|
156
|
+
* mixed generation.
|
|
157
|
+
*/
|
|
158
|
+
embeddingGenerations?: OfflineSyncChangesetGeneration[];
|
|
102
159
|
}
|
|
103
160
|
|
|
104
161
|
export interface OfflineSyncState {
|
|
@@ -119,7 +176,12 @@ export interface OfflineSyncConflict {
|
|
|
119
176
|
| "remote_exists_for_local_create"
|
|
120
177
|
| "remote_changed_for_local_update"
|
|
121
178
|
| "remote_deleted_for_local_update"
|
|
122
|
-
| "remote_changed_for_local_delete"
|
|
179
|
+
| "remote_changed_for_local_delete"
|
|
180
|
+
// Changeset generation deferrals (#3150): the receiver refused a whole
|
|
181
|
+
// embedding generation; surfaced as conflicts so the push side never
|
|
182
|
+
// checkpoints it as applied.
|
|
183
|
+
| "embedding_generation_diverged"
|
|
184
|
+
| "embedding_generation_manifest_required";
|
|
123
185
|
baseSha256?: string;
|
|
124
186
|
localSha256?: string;
|
|
125
187
|
incomingSha256?: string;
|
|
@@ -166,7 +228,21 @@ export interface OfflineSyncFileWriteChunksTarget extends OfflineSyncFileTarget
|
|
|
166
228
|
chunks: AsyncIterable<Buffer>;
|
|
167
229
|
}
|
|
168
230
|
|
|
169
|
-
export interface OfflineSyncFileStagingWriteTarget extends OfflineSyncFileWriteTarget {
|
|
231
|
+
export interface OfflineSyncFileStagingWriteTarget extends OfflineSyncFileWriteTarget {
|
|
232
|
+
/**
|
|
233
|
+
* ABSOLUTE final path whose canonical AAD the staged ciphertext binds
|
|
234
|
+
* (secure-store deployments): the offline-sync embedding generation
|
|
235
|
+
* transaction stages under a staging dir and publishes by rename, so the
|
|
236
|
+
* ciphertext must decrypt at the FINAL path. The AAD derives through the
|
|
237
|
+
* same filePathAad logic as ordinary reads (platform-consistent).
|
|
238
|
+
*/
|
|
239
|
+
finalAadFilePath?: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface OfflineSyncFileStagingReadTarget extends OfflineSyncFileTarget {
|
|
243
|
+
/** See {@link OfflineSyncFileStagingWriteTarget.finalAadFilePath}. */
|
|
244
|
+
finalAadFilePath?: string;
|
|
245
|
+
}
|
|
170
246
|
|
|
171
247
|
export interface OfflineSyncFileContentChunk extends Omit<OfflineSyncFileState, "sha256"> {
|
|
172
248
|
sha256?: string;
|
|
@@ -205,9 +281,6 @@ interface OfflineSyncFileRecordOptions {
|
|
|
205
281
|
signal?: AbortSignal;
|
|
206
282
|
}
|
|
207
283
|
|
|
208
|
-
const SYNC_INTERNAL_DIR = ".offline-sync";
|
|
209
|
-
const OFFLINE_SYNC_UPLOAD_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
210
|
-
const OFFLINE_SYNC_FAST_BASE_MTIME_TOLERANCE_MS = 1_000;
|
|
211
284
|
const OFFLINE_SYNC_FAST_BASE_CTIME_TOLERANCE_MS = 1;
|
|
212
285
|
const EXCLUDED_FILE_NAMES = new Set([
|
|
213
286
|
".sync-state.json",
|
|
@@ -228,52 +301,7 @@ const EXCLUDED_FILE_PREFIXES = [
|
|
|
228
301
|
* matches zero or more whole segments, and a trailing `dir/star-star`
|
|
229
302
|
* matches everything under `dir/` at any depth.
|
|
230
303
|
*/
|
|
231
|
-
export function globToRegExp(glob: string): RegExp {
|
|
232
|
-
if (typeof glob !== "string" || glob.length === 0) {
|
|
233
|
-
throw new Error("offlineSyncExcludes entry must be a non-empty string");
|
|
234
|
-
}
|
|
235
|
-
if (glob.includes("\0")) {
|
|
236
|
-
throw new Error("offlineSyncExcludes entry must not contain NUL bytes");
|
|
237
|
-
}
|
|
238
|
-
let source = "";
|
|
239
|
-
for (let i = 0; i < glob.length; i += 1) {
|
|
240
|
-
const ch = glob[i];
|
|
241
|
-
if (ch === "*") {
|
|
242
|
-
if (glob[i + 1] === "*") {
|
|
243
|
-
// `**` is cross-segment wherever it appears:
|
|
244
|
-
// leading `**/` -> zero or more whole segments
|
|
245
|
-
// `/**` at end -> everything under the directory
|
|
246
|
-
// `a/**/b` -> any depth between segments
|
|
247
|
-
// (Cursor review on PR #1793: trailing `scratch/**` must match
|
|
248
|
-
// nested `scratch/a/b.md`, matching the offline-mode guide.)
|
|
249
|
-
if (glob[i + 2] === "/") {
|
|
250
|
-
source += "(?:.*/)?";
|
|
251
|
-
i += 2;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
source += ".*";
|
|
255
|
-
i += 1;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
source += "[^/]*";
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
if (ch === "?") {
|
|
262
|
-
source += "[^/]";
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
if (ch === "/") {
|
|
266
|
-
source += "/";
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
source += ch.replace(/[\\^$.+()|{}\[\]]/g, "\\$&");
|
|
270
|
-
}
|
|
271
|
-
return new RegExp(`^${source}$`);
|
|
272
|
-
}
|
|
273
304
|
|
|
274
|
-
function hashText(value: string): string {
|
|
275
|
-
return createHash("sha256").update(value).digest("hex");
|
|
276
|
-
}
|
|
277
305
|
|
|
278
306
|
function sha256Buffer(buffer: Buffer): { sha256: string; bytes: number } {
|
|
279
307
|
return sha256Bytes(buffer);
|
|
@@ -307,20 +335,7 @@ function assertNonNegativeInteger(value: unknown, field: string): number {
|
|
|
307
335
|
return value;
|
|
308
336
|
}
|
|
309
337
|
|
|
310
|
-
function assertNonNegativeFinite(value: unknown, field: string): number {
|
|
311
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
312
|
-
throw new Error(`${field} must be a non-negative finite number`);
|
|
313
|
-
}
|
|
314
|
-
return value;
|
|
315
|
-
}
|
|
316
338
|
|
|
317
|
-
function assertOfflineSyncMtimeMs(value: unknown, field: string): number {
|
|
318
|
-
const mtimeMs = assertNonNegativeFinite(value, field);
|
|
319
|
-
if (!isCensusMtimeMs(mtimeMs)) {
|
|
320
|
-
throw new Error(`${field} must be within JavaScript Date range`);
|
|
321
|
-
}
|
|
322
|
-
return mtimeMs;
|
|
323
|
-
}
|
|
324
339
|
|
|
325
340
|
function assertBoolean(value: unknown, field: string): boolean {
|
|
326
341
|
if (typeof value !== "boolean") {
|
|
@@ -407,6 +422,10 @@ function snapshotBuilderDeletions(options: {
|
|
|
407
422
|
includeTranscripts: boolean;
|
|
408
423
|
excludeNodeLocalState?: boolean;
|
|
409
424
|
userExcludeRegexps?: readonly RegExp[];
|
|
425
|
+
/** Generation dirs omitted by the push enumeration (PR #3148): revisions
|
|
426
|
+
* for ANY member — including already-absent ones — are suppressed, so a
|
|
427
|
+
* filtered generation can never be announced as deleted. */
|
|
428
|
+
omittedGenerationDirs?: ReadonlySet<string>;
|
|
410
429
|
}): OfflineSyncDeletionRevision[] | undefined {
|
|
411
430
|
const deletions = normalizeDeletionRevisions(options.deletions, "deletions");
|
|
412
431
|
if (deletions === undefined) return undefined;
|
|
@@ -418,6 +437,7 @@ function snapshotBuilderDeletions(options: {
|
|
|
418
437
|
return deletions.filter((deletion) =>
|
|
419
438
|
(scopedPaths === undefined || scopedPaths.has(deletion.path.toLowerCase())) &&
|
|
420
439
|
!presentPaths.has(deletion.path.toLowerCase()) &&
|
|
440
|
+
!isDeletionInOmittedGeneration(deletion.path, options.omittedGenerationDirs) &&
|
|
421
441
|
!(options.excludeNodeLocalState === false
|
|
422
442
|
? shouldExcludeRelPath(deletion.path, options.includeTranscripts)
|
|
423
443
|
: shouldExcludePushRelPath(
|
|
@@ -427,11 +447,21 @@ function snapshotBuilderDeletions(options: {
|
|
|
427
447
|
)));
|
|
428
448
|
}
|
|
429
449
|
|
|
450
|
+
function isDeletionInOmittedGeneration(
|
|
451
|
+
relPath: string,
|
|
452
|
+
omittedGenerationDirs: ReadonlySet<string> | undefined,
|
|
453
|
+
): boolean {
|
|
454
|
+
if (!omittedGenerationDirs || omittedGenerationDirs.size === 0) return false;
|
|
455
|
+
const membership = embeddingGenerationMembership(relPath);
|
|
456
|
+
return membership !== null && omittedGenerationDirs.has(membership.shardDir);
|
|
457
|
+
}
|
|
458
|
+
|
|
430
459
|
export async function filterOfflineSyncDeletionRevisions(options: {
|
|
431
460
|
root: string;
|
|
432
461
|
deletions: readonly OfflineSyncDeletionRevision[];
|
|
433
462
|
includeTranscripts?: boolean;
|
|
434
463
|
userExcludeRegexps?: readonly RegExp[];
|
|
464
|
+
excludeFile?: OfflineSyncExcludeFile;
|
|
435
465
|
}): Promise<OfflineSyncDeletionRevision[]> {
|
|
436
466
|
const deletions = normalizeDeletionRevisions(options.deletions, "deletions");
|
|
437
467
|
if (deletions === undefined) throw new Error("deletions must be an array");
|
|
@@ -442,9 +472,20 @@ export async function filterOfflineSyncDeletionRevisions(options: {
|
|
|
442
472
|
"root",
|
|
443
473
|
);
|
|
444
474
|
const includeTranscripts = options.includeTranscripts !== false;
|
|
475
|
+
const omission = await computeOmittedEmbeddingGenerationPaths({
|
|
476
|
+
rootAbs: root.abs,
|
|
477
|
+
userExcludeRegexps: options.userExcludeRegexps,
|
|
478
|
+
excludeFile: options.excludeFile,
|
|
479
|
+
isExcludedRelPath: (relPath) =>
|
|
480
|
+
shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps),
|
|
481
|
+
});
|
|
482
|
+
const omittedDirs = new Set(omission.omittedDirs);
|
|
445
483
|
const filtered: OfflineSyncDeletionRevision[] = [];
|
|
446
484
|
for (const deletion of deletions) {
|
|
447
|
-
if (
|
|
485
|
+
if (
|
|
486
|
+
shouldExcludePushRelPath(deletion.path, includeTranscripts, options.userExcludeRegexps) ||
|
|
487
|
+
isDeletionInOmittedGeneration(deletion.path, omittedDirs)
|
|
488
|
+
) {
|
|
448
489
|
continue;
|
|
449
490
|
}
|
|
450
491
|
const filePath = path.join(root.abs, ...deletion.path.split("/"));
|
|
@@ -503,6 +544,26 @@ export function normalizeOfflineSyncSnapshot(
|
|
|
503
544
|
if (excludedPath) {
|
|
504
545
|
throw new Error(`offline sync snapshot contains excluded path: ${excludedPath}`);
|
|
505
546
|
}
|
|
547
|
+
let omittedEmbeddingGenerationDirs: string[] | undefined;
|
|
548
|
+
if (obj.omittedEmbeddingGenerationDirs !== undefined) {
|
|
549
|
+
if (!Array.isArray(obj.omittedEmbeddingGenerationDirs)) {
|
|
550
|
+
throw new Error("offline sync snapshot omittedEmbeddingGenerationDirs must be an array");
|
|
551
|
+
}
|
|
552
|
+
const seenDirs = new Set<string>();
|
|
553
|
+
for (const [index, entry] of obj.omittedEmbeddingGenerationDirs.entries()) {
|
|
554
|
+
if (
|
|
555
|
+
typeof entry !== "string" ||
|
|
556
|
+
!isEmbeddingGenerationDirPath(entry) ||
|
|
557
|
+
seenDirs.has(entry)
|
|
558
|
+
) {
|
|
559
|
+
throw new Error(
|
|
560
|
+
`omittedEmbeddingGenerationDirs[${index}] must be a unique canonical embeddings generation dir`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
seenDirs.add(entry);
|
|
564
|
+
}
|
|
565
|
+
omittedEmbeddingGenerationDirs = [...seenDirs].sort((left, right) => left.localeCompare(right));
|
|
566
|
+
}
|
|
506
567
|
return {
|
|
507
568
|
format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
508
569
|
schemaVersion: 1,
|
|
@@ -511,6 +572,7 @@ export function normalizeOfflineSyncSnapshot(
|
|
|
511
572
|
includeTranscripts,
|
|
512
573
|
files,
|
|
513
574
|
...(deletions === undefined ? {} : { deletions }),
|
|
575
|
+
...(omittedEmbeddingGenerationDirs === undefined ? {} : { omittedEmbeddingGenerationDirs }),
|
|
514
576
|
};
|
|
515
577
|
}
|
|
516
578
|
|
|
@@ -586,6 +648,7 @@ export function normalizeOfflineSyncChangeset(
|
|
|
586
648
|
if (excludedPath) {
|
|
587
649
|
throw new Error(`offline sync changeset contains excluded path: ${excludedPath}`);
|
|
588
650
|
}
|
|
651
|
+
const embeddingGenerations = normalizeChangesetGenerations(obj.embeddingGenerations, changes);
|
|
589
652
|
return {
|
|
590
653
|
format: OFFLINE_SYNC_CHANGESET_FORMAT,
|
|
591
654
|
schemaVersion: 1,
|
|
@@ -593,6 +656,7 @@ export function normalizeOfflineSyncChangeset(
|
|
|
593
656
|
sourceId,
|
|
594
657
|
includeTranscripts,
|
|
595
658
|
changes: changes.sort(compareByPath),
|
|
659
|
+
...(embeddingGenerations === undefined ? {} : { embeddingGenerations }),
|
|
596
660
|
};
|
|
597
661
|
}
|
|
598
662
|
|
|
@@ -675,15 +739,7 @@ function shouldExcludePushRelPath(
|
|
|
675
739
|
|
|
676
740
|
// Precompiled once at module load — this check sits on the hot
|
|
677
741
|
// enumeration path for every walked file (Kilo review, PR #1793).
|
|
678
|
-
const DEFAULT_OFFLINE_SYNC_EXCLUDE_REGEXPS: readonly RegExp[] =
|
|
679
|
-
DEFAULT_OFFLINE_SYNC_EXCLUDE_GLOBS.map((glob) => globToRegExp(glob));
|
|
680
742
|
|
|
681
|
-
function matchesOfflineSyncDefaultExclude(relPosix: string): boolean {
|
|
682
|
-
for (const regexp of DEFAULT_OFFLINE_SYNC_EXCLUDE_REGEXPS) {
|
|
683
|
-
if (regexp.test(relPosix)) return true;
|
|
684
|
-
}
|
|
685
|
-
return false;
|
|
686
|
-
}
|
|
687
743
|
|
|
688
744
|
/**
|
|
689
745
|
* Compile operator-supplied `offlineSyncExcludes` glob strings into a
|
|
@@ -692,46 +748,6 @@ function matchesOfflineSyncDefaultExclude(relPosix: string): boolean {
|
|
|
692
748
|
* a thrown error as a fatal configuration mistake and refuse to start the
|
|
693
749
|
* sync run rather than silently dropping the bad entry.
|
|
694
750
|
*/
|
|
695
|
-
export function compileOfflineSyncExcludeGlobs(
|
|
696
|
-
globs: readonly unknown[],
|
|
697
|
-
): RegExp[] {
|
|
698
|
-
const out: RegExp[] = [];
|
|
699
|
-
for (const entry of globs) {
|
|
700
|
-
if (typeof entry !== "string" || entry.length === 0) {
|
|
701
|
-
throw new Error("offlineSyncExcludes must contain only non-empty strings");
|
|
702
|
-
}
|
|
703
|
-
out.push(globToRegExp(entry));
|
|
704
|
-
}
|
|
705
|
-
return out;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
* Validate the operator-supplied offline-sync exclude list (#1786).
|
|
710
|
-
* Rejects loudly instead of silently defaulting (CLAUDE.md rule 39):
|
|
711
|
-
* a misspelled key value must fail config parse, not be ignored.
|
|
712
|
-
* Lives next to the glob compiler so config.ts only carries the call.
|
|
713
|
-
*/
|
|
714
|
-
export function parseOfflineSyncExcludes(raw: unknown): string[] {
|
|
715
|
-
if (raw === undefined || raw === null) return [];
|
|
716
|
-
if (!Array.isArray(raw)) {
|
|
717
|
-
throw new Error(
|
|
718
|
-
`offlineSyncExcludes must be an array of non-empty glob strings; got ${typeof raw}`,
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
for (const entry of raw) {
|
|
722
|
-
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
723
|
-
throw new Error(
|
|
724
|
-
"offlineSyncExcludes must contain only non-empty glob strings",
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
const globs = raw.map((entry) => (entry as string).trim());
|
|
729
|
-
// Compile-check every glob now so a bad pattern fails at parse time
|
|
730
|
-
// rather than mid-sync. compileOfflineSyncExcludeGlobs throws with a
|
|
731
|
-
// per-entry message.
|
|
732
|
-
compileOfflineSyncExcludeGlobs(globs);
|
|
733
|
-
return globs;
|
|
734
|
-
}
|
|
735
751
|
|
|
736
752
|
function shouldIgnoreIncomingRuntimePath(relPosix: string): boolean {
|
|
737
753
|
if (isInternalRemnicStatePath(relPosix)) return true;
|
|
@@ -740,43 +756,6 @@ function shouldIgnoreIncomingRuntimePath(relPosix: string): boolean {
|
|
|
740
756
|
return isCanonicalRuntimeStatePath(parts) && basename.includes(".tmp-");
|
|
741
757
|
}
|
|
742
758
|
|
|
743
|
-
function isCanonicalRuntimeStatePath(parts: string[]): boolean {
|
|
744
|
-
if (parts[0] === "state") return true;
|
|
745
|
-
return parts[0] === "namespaces" && parts.length >= 4 && parts[2] === "state";
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const REMOTE_AUTHORITATIVE_RUNTIME_STATE_FILES = new Set([
|
|
749
|
-
".artifact-write-version.log",
|
|
750
|
-
".memory-status-version.log",
|
|
751
|
-
"buffer.json",
|
|
752
|
-
"embeddings.json",
|
|
753
|
-
"index_time.json",
|
|
754
|
-
"last_intent.json",
|
|
755
|
-
"last_qmd_recall.json",
|
|
756
|
-
"last_recall.json",
|
|
757
|
-
"lcm.sqlite-shm",
|
|
758
|
-
"lcm.sqlite-wal",
|
|
759
|
-
"memory-lifecycle-ledger.jsonl",
|
|
760
|
-
"recall_impressions.jsonl",
|
|
761
|
-
]);
|
|
762
|
-
|
|
763
|
-
const ABSENT_INCOMING_RUNTIME_DELETE_FILES = new Set([
|
|
764
|
-
"lcm.sqlite-shm",
|
|
765
|
-
"lcm.sqlite-wal",
|
|
766
|
-
]);
|
|
767
|
-
|
|
768
|
-
export function shouldPreferIncomingOfflineRuntimeFile(relPosix: string): boolean {
|
|
769
|
-
const parts = relPosix.split("/");
|
|
770
|
-
const basename = parts[parts.length - 1] ?? "";
|
|
771
|
-
return isCanonicalRuntimeStatePath(parts) && REMOTE_AUTHORITATIVE_RUNTIME_STATE_FILES.has(basename);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function shouldDeleteAbsentIncomingOfflineRuntimeFile(relPosix: string): boolean {
|
|
775
|
-
const parts = relPosix.split("/");
|
|
776
|
-
const basename = parts[parts.length - 1] ?? "";
|
|
777
|
-
return isCanonicalRuntimeStatePath(parts) && ABSENT_INCOMING_RUNTIME_DELETE_FILES.has(basename);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
759
|
function filterBaseFilesForMode(
|
|
781
760
|
files: readonly OfflineSyncFileState[],
|
|
782
761
|
includeTranscripts: boolean,
|
|
@@ -850,6 +829,13 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
850
829
|
readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
|
|
851
830
|
excludeFile?: OfflineSyncExcludeFile;
|
|
852
831
|
userExcludeRegexps?: readonly RegExp[];
|
|
832
|
+
/**
|
|
833
|
+
* Precomputed whole-generation omission set (PR #3148 round 4): paths the
|
|
834
|
+
* walk must skip because their embedding generation lost ANY member to a
|
|
835
|
+
* push filter. When omitted and push excludes are active, the set is
|
|
836
|
+
* computed here so streaming census consumers get the same semantics.
|
|
837
|
+
*/
|
|
838
|
+
skipEmbeddingGenerationPaths?: ReadonlySet<string>;
|
|
853
839
|
/**
|
|
854
840
|
* When false, enumeration uses only the legacy structural excludes —
|
|
855
841
|
* the apply/pull-side view of local files. Push-side callers keep the
|
|
@@ -864,8 +850,19 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
864
850
|
const rootAbs = path.resolve(options.root);
|
|
865
851
|
const root = await prepareSafeArchiveRoot(rootAbs, "iterateOfflineSyncSnapshotFileRecords", "root");
|
|
866
852
|
const includeTranscripts = options.includeTranscripts !== false;
|
|
867
|
-
|
|
868
|
-
|
|
853
|
+
const pushExcludes = options.excludeNodeLocalState !== false;
|
|
854
|
+
const skipGenerationPaths = options.skipEmbeddingGenerationPaths ??
|
|
855
|
+
(pushExcludes
|
|
856
|
+
? (await computeOmittedEmbeddingGenerationPaths({
|
|
857
|
+
rootAbs: root.abs,
|
|
858
|
+
userExcludeRegexps: options.userExcludeRegexps,
|
|
859
|
+
excludeFile: options.excludeFile,
|
|
860
|
+
isExcludedRelPath: (relPath) =>
|
|
861
|
+
shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps),
|
|
862
|
+
})).omittedPaths
|
|
863
|
+
: undefined);
|
|
864
|
+
|
|
865
|
+
async function* walk(dirAbs: string, includeContent = options.includeContent === true): AsyncIterable<OfflineSyncFileRecord> {
|
|
869
866
|
throwIfOfflineSyncAborted(options.signal);
|
|
870
867
|
let entries = await readdir(dirAbs, { withFileTypes: true });
|
|
871
868
|
entries = entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
@@ -877,9 +874,17 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
877
874
|
? shouldExcludeRelPath(relPosix, includeTranscripts)
|
|
878
875
|
: shouldExcludePushRelPath(relPosix, includeTranscripts, options.userExcludeRegexps)
|
|
879
876
|
) continue;
|
|
877
|
+
if (skipGenerationPaths?.has(relPosix)) continue;
|
|
880
878
|
if (entry.isSymbolicLink()) continue;
|
|
881
879
|
if (entry.isDirectory()) {
|
|
882
|
-
|
|
880
|
+
if (isEmbeddingGenerationDirPath(relPosix)) {
|
|
881
|
+
yield* withEmbeddingGenerationLockIter(path.dirname(abs), (content) => walk(abs, content), includeContent,
|
|
882
|
+
() => assertEmbeddingGenerationStillIncluded(root.abs, relPosix, options.excludeFile,
|
|
883
|
+
(rel) => shouldExcludePushRelPath(rel, includeTranscripts, options.userExcludeRegexps),
|
|
884
|
+
[]));
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
yield* walk(abs, includeContent);
|
|
883
888
|
continue;
|
|
884
889
|
}
|
|
885
890
|
if (!entry.isFile()) continue;
|
|
@@ -892,7 +897,7 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
892
897
|
root,
|
|
893
898
|
relPath: relPosix,
|
|
894
899
|
filePath: abs,
|
|
895
|
-
includeContent
|
|
900
|
+
includeContent,
|
|
896
901
|
readFile: options.readFile,
|
|
897
902
|
readFileDigest: options.readFileDigest,
|
|
898
903
|
signal: options.signal,
|
|
@@ -926,8 +931,23 @@ export async function buildOfflineSyncSnapshot(options: {
|
|
|
926
931
|
}): Promise<OfflineSyncSnapshot> {
|
|
927
932
|
throwIfOfflineSyncAborted(options.signal);
|
|
928
933
|
const includeTranscripts = options.includeTranscripts !== false;
|
|
934
|
+
const pushExcludes = options.excludeNodeLocalState !== false;
|
|
935
|
+
const normalizedDeletions = normalizeDeletionRevisions(options.deletions, "deletions");
|
|
936
|
+
const omission = pushExcludes
|
|
937
|
+
? await computeOmittedEmbeddingGenerationPaths({
|
|
938
|
+
rootAbs: path.resolve(options.root),
|
|
939
|
+
userExcludeRegexps: options.userExcludeRegexps,
|
|
940
|
+
excludeFile: options.excludeFile,
|
|
941
|
+
tombstonedPaths: (normalizedDeletions ?? []).map((deletion) => deletion.path),
|
|
942
|
+
isExcludedRelPath: (relPath) =>
|
|
943
|
+
shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps),
|
|
944
|
+
})
|
|
945
|
+
: undefined;
|
|
929
946
|
const files: OfflineSyncFileRecord[] = [];
|
|
930
|
-
for await (const file of iterateOfflineSyncSnapshotFileRecords(
|
|
947
|
+
for await (const file of iterateOfflineSyncSnapshotFileRecords({
|
|
948
|
+
...options,
|
|
949
|
+
skipEmbeddingGenerationPaths: omission?.omittedPaths,
|
|
950
|
+
})) files.push(file);
|
|
931
951
|
throwIfOfflineSyncAborted(options.signal);
|
|
932
952
|
|
|
933
953
|
const sortedFiles = files.sort(compareByPath);
|
|
@@ -937,6 +957,9 @@ export async function buildOfflineSyncSnapshot(options: {
|
|
|
937
957
|
includeTranscripts,
|
|
938
958
|
excludeNodeLocalState: options.excludeNodeLocalState,
|
|
939
959
|
userExcludeRegexps: options.userExcludeRegexps,
|
|
960
|
+
omittedGenerationDirs: omission && omission.omittedDirs.length > 0
|
|
961
|
+
? new Set(omission.omittedDirs)
|
|
962
|
+
: undefined,
|
|
940
963
|
});
|
|
941
964
|
return {
|
|
942
965
|
format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
@@ -946,6 +969,9 @@ export async function buildOfflineSyncSnapshot(options: {
|
|
|
946
969
|
includeTranscripts,
|
|
947
970
|
files: sortedFiles,
|
|
948
971
|
...(deletions === undefined ? {} : { deletions }),
|
|
972
|
+
...(omission && omission.omittedDirs.length > 0
|
|
973
|
+
? { omittedEmbeddingGenerationDirs: omission.omittedDirs }
|
|
974
|
+
: {}),
|
|
949
975
|
};
|
|
950
976
|
}
|
|
951
977
|
|
|
@@ -984,6 +1010,18 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
984
1010
|
const baseCapturedAtMs = rawBaseCapturedAtMs !== undefined && Number.isFinite(rawBaseCapturedAtMs)
|
|
985
1011
|
? rawBaseCapturedAtMs
|
|
986
1012
|
: null;
|
|
1013
|
+
const pushExcludes = options.excludeNodeLocalState !== false;
|
|
1014
|
+
const normalizedDeletions = normalizeDeletionRevisions(options.deletions, "deletions");
|
|
1015
|
+
const omission = pushExcludes
|
|
1016
|
+
? await computeOmittedEmbeddingGenerationPaths({
|
|
1017
|
+
rootAbs: root.abs,
|
|
1018
|
+
userExcludeRegexps: options.userExcludeRegexps,
|
|
1019
|
+
excludeFile: options.excludeFile,
|
|
1020
|
+
tombstonedPaths: (normalizedDeletions ?? []).map((deletion) => deletion.path),
|
|
1021
|
+
isExcludedRelPath: (relPath) =>
|
|
1022
|
+
shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps),
|
|
1023
|
+
})
|
|
1024
|
+
: undefined;
|
|
987
1025
|
const files: OfflineSyncFileRecord[] = [];
|
|
988
1026
|
|
|
989
1027
|
async function walk(dirAbs: string): Promise<void> {
|
|
@@ -999,8 +1037,18 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
999
1037
|
? shouldExcludeRelPath(relPosix, includeTranscripts)
|
|
1000
1038
|
: shouldExcludePushRelPath(relPosix, includeTranscripts, options.userExcludeRegexps)
|
|
1001
1039
|
) continue;
|
|
1040
|
+
if (omission?.omittedPaths.has(relPosix)) continue;
|
|
1002
1041
|
if (entry.isSymbolicLink()) continue;
|
|
1003
1042
|
if (entry.isDirectory()) {
|
|
1043
|
+
if (isEmbeddingGenerationDirPath(relPosix)) {
|
|
1044
|
+
await withEmbeddingGenerationLock(path.dirname(abs), async () => {
|
|
1045
|
+
await assertEmbeddingGenerationStillIncluded(root.abs, relPosix, options.excludeFile,
|
|
1046
|
+
(rel) => shouldExcludePushRelPath(rel, includeTranscripts, options.userExcludeRegexps),
|
|
1047
|
+
(normalizedDeletions ?? []).map((deletion) => deletion.path));
|
|
1048
|
+
await walk(abs);
|
|
1049
|
+
});
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1004
1052
|
await walk(abs);
|
|
1005
1053
|
continue;
|
|
1006
1054
|
}
|
|
@@ -1043,6 +1091,9 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
1043
1091
|
includeTranscripts,
|
|
1044
1092
|
excludeNodeLocalState: options.excludeNodeLocalState,
|
|
1045
1093
|
userExcludeRegexps: options.userExcludeRegexps,
|
|
1094
|
+
omittedGenerationDirs: omission && omission.omittedDirs.length > 0
|
|
1095
|
+
? new Set(omission.omittedDirs)
|
|
1096
|
+
: undefined,
|
|
1046
1097
|
});
|
|
1047
1098
|
return {
|
|
1048
1099
|
format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
@@ -1052,6 +1103,9 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
1052
1103
|
includeTranscripts,
|
|
1053
1104
|
files: sortedFiles,
|
|
1054
1105
|
...(deletions === undefined ? {} : { deletions }),
|
|
1106
|
+
...(omission && omission.omittedDirs.length > 0
|
|
1107
|
+
? { omittedEmbeddingGenerationDirs: omission.omittedDirs }
|
|
1108
|
+
: {}),
|
|
1055
1109
|
};
|
|
1056
1110
|
}
|
|
1057
1111
|
|
|
@@ -1491,10 +1545,21 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1491
1545
|
currentFiles?: readonly OfflineSyncFileState[];
|
|
1492
1546
|
deferredPaths?: readonly string[];
|
|
1493
1547
|
allowMissingConflictContent?: boolean;
|
|
1548
|
+
/**
|
|
1549
|
+
* Explicit INCOMING content source for snapshot records without inline
|
|
1550
|
+
* `contentBase64` (oversized generation members the client fetched
|
|
1551
|
+
* chunk-wise into a private staging root). Consulted only for such
|
|
1552
|
+
* records; return null to keep the metadata-only behavior (the local
|
|
1553
|
+
* file must hash-match the record). Returned buffers are digest-verified
|
|
1554
|
+
* against the incoming record before anything is published.
|
|
1555
|
+
*/
|
|
1556
|
+
readIncomingFile?: (target: { path: string; sha256: string; bytes: number }) => Promise<Buffer | null>;
|
|
1494
1557
|
writeConflictCopies?: boolean;
|
|
1495
1558
|
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
1496
1559
|
readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
|
|
1497
1560
|
writeFile?: (target: OfflineSyncFileWriteTarget) => Promise<void>;
|
|
1561
|
+
writeStagingFile?: (target: OfflineSyncFileStagingWriteTarget) => Promise<void>;
|
|
1562
|
+
readStagingFile?: (target: OfflineSyncFileStagingReadTarget) => Promise<Buffer>;
|
|
1498
1563
|
deleteFile?: (target: OfflineSyncFileDeleteTarget) => Promise<void>;
|
|
1499
1564
|
recordDeletionRevision?: OfflineSyncRecordDeletionRevision;
|
|
1500
1565
|
}): Promise<OfflineSyncApplySnapshotResult> {
|
|
@@ -1507,8 +1572,9 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1507
1572
|
const deletionMtimeByPath = snapshot.deletions === undefined
|
|
1508
1573
|
? undefined
|
|
1509
1574
|
: new Map(snapshot.deletions.map((deletion) => [deletion.path, deletion.mtimeMs] as const));
|
|
1510
|
-
const incomingBuffers = verifyRecordContents(snapshot.files, "offline sync snapshot", {
|
|
1575
|
+
const incomingBuffers = await verifyRecordContents(snapshot.files, "offline sync snapshot", {
|
|
1511
1576
|
requireContent: false,
|
|
1577
|
+
readIncomingFile: options.readIncomingFile,
|
|
1512
1578
|
});
|
|
1513
1579
|
const root = await ensureSyncRoot(options.root, "applyOfflineSyncSnapshot");
|
|
1514
1580
|
const currentFiles = options.currentFiles
|
|
@@ -1523,12 +1589,19 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1523
1589
|
excludeNodeLocalState: false,
|
|
1524
1590
|
})).files;
|
|
1525
1591
|
const currentMap = byPath(currentFiles);
|
|
1526
|
-
const deferredPaths = new Set(options.deferredPaths ?? [])
|
|
1592
|
+
const deferredPaths = new Set([...(options.deferredPaths ?? []),
|
|
1593
|
+
...divergedEmbeddingGenerationDeferrals({
|
|
1594
|
+
incomingFiles: snapshot.files, baseFiles: [...baseMap.values()], currentFiles,
|
|
1595
|
+
}),
|
|
1596
|
+
]);
|
|
1597
|
+
const omittedGenerationDirs = new Set(snapshot.omittedEmbeddingGenerationDirs ?? []);
|
|
1527
1598
|
if (deletionMtimeByPath && options.recordDeletionRevision) {
|
|
1528
1599
|
for (const [relPath, mtimeMs] of deletionMtimeByPath) {
|
|
1529
1600
|
if (
|
|
1530
1601
|
currentMap.has(relPath) ||
|
|
1531
1602
|
deferredPaths.has(relPath) ||
|
|
1603
|
+
(omittedGenerationDirs.size > 0 &&
|
|
1604
|
+
isPathInOmittedEmbeddingGeneration(relPath, omittedGenerationDirs)) ||
|
|
1532
1605
|
matchesOfflineSyncDefaultExclude(relPath)
|
|
1533
1606
|
) {
|
|
1534
1607
|
continue;
|
|
@@ -1554,6 +1627,52 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1554
1627
|
return requiredBuffer(incomingBuffers, relPath);
|
|
1555
1628
|
};
|
|
1556
1629
|
|
|
1630
|
+
// Embedding generations travel and replace as ONE unit (issue #3148):
|
|
1631
|
+
// atomic replacement, cross-layout marker conversion, whole-generation
|
|
1632
|
+
// removal on tombstones — all extracted to the generation module (issue
|
|
1633
|
+
// #3150) so the changeset path can share it without growing this file.
|
|
1634
|
+
const { transactionResults, deferredGenerationDirs } = await applyIncomingEmbeddingGenerations({
|
|
1635
|
+
root,
|
|
1636
|
+
incomingMap,
|
|
1637
|
+
incomingBuffers,
|
|
1638
|
+
baseMap,
|
|
1639
|
+
currentMap,
|
|
1640
|
+
deferredPaths,
|
|
1641
|
+
omittedGenerationDirs,
|
|
1642
|
+
deletionMtimeByPath,
|
|
1643
|
+
io: {
|
|
1644
|
+
readFile: options.readFile,
|
|
1645
|
+
writeStagingFile: options.writeStagingFile,
|
|
1646
|
+
readStagingFile: options.readStagingFile,
|
|
1647
|
+
deleteFile: options.deleteFile,
|
|
1648
|
+
},
|
|
1649
|
+
customIoPresent: Boolean(options.writeFile || options.readFile || options.readFileDigest || options.deleteFile),
|
|
1650
|
+
now: Date.now(),
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
const transactionHandled = new Set<string>();
|
|
1654
|
+
let generationUpserted = 0;
|
|
1655
|
+
let generationDeleted = 0;
|
|
1656
|
+
for (const result of transactionResults) {
|
|
1657
|
+
if (result.deferredLocalDivergence) {
|
|
1658
|
+
for (const relPath of result.handledPaths) deferredPaths.add(relPath);
|
|
1659
|
+
deferredGenerationDirs.add(result.shardDirRel);
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
generationUpserted += result.upserted;
|
|
1663
|
+
generationDeleted += result.deleted;
|
|
1664
|
+
for (const relPath of result.handledPaths) transactionHandled.add(relPath);
|
|
1665
|
+
// Record converted shard paths directly; they may not be in any input map.
|
|
1666
|
+
for (const [relPath, state] of result.writtenStates) {
|
|
1667
|
+
nextBase.set(relPath, state);
|
|
1668
|
+
transactionHandled.add(relPath);
|
|
1669
|
+
}
|
|
1670
|
+
for (const relPath of result.removedPaths) {
|
|
1671
|
+
nextBase.delete(relPath);
|
|
1672
|
+
transactionHandled.add(relPath);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1557
1676
|
for (const relPath of unionPaths(baseMap, incomingMap, currentMap)) {
|
|
1558
1677
|
const base = baseMap.get(relPath);
|
|
1559
1678
|
const incoming = incomingMap.get(relPath);
|
|
@@ -1566,6 +1685,21 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1566
1685
|
continue;
|
|
1567
1686
|
}
|
|
1568
1687
|
|
|
1688
|
+
// The generation transaction already published or removed this path and
|
|
1689
|
+
// applied its nextBase bookkeeping above; nothing left to do.
|
|
1690
|
+
if (transactionHandled.has(relPath)) continue;
|
|
1691
|
+
|
|
1692
|
+
// A deferral anywhere in a generation defers the WHOLE generation: no
|
|
1693
|
+
// partial replacement in either direction, local files and base entries
|
|
1694
|
+
// stay exactly as they are.
|
|
1695
|
+
const membership = embeddingGenerationMembership(relPath);
|
|
1696
|
+
if (membership && deferredGenerationDirs.has(membership.shardDir)) {
|
|
1697
|
+
if (base) nextBase.set(relPath, base);
|
|
1698
|
+
else nextBase.delete(relPath);
|
|
1699
|
+
skipped += 1;
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1569
1703
|
if (incoming) {
|
|
1570
1704
|
if (currentEntry?.sha256 === incoming.sha256) {
|
|
1571
1705
|
if (await setSafeFileMtime(root, relPath, incoming.mtimeMs)) {
|
|
@@ -1578,7 +1712,14 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1578
1712
|
skipped += 1;
|
|
1579
1713
|
continue;
|
|
1580
1714
|
}
|
|
1581
|
-
|
|
1715
|
+
// Remote unchanged from the shared base while the local file drifted:
|
|
1716
|
+
// keep the local copy (no content is needed to make that decision).
|
|
1717
|
+
// Members of a replaced generation never reach this branch — the
|
|
1718
|
+
// generation transaction or the deferral guard handled them above.
|
|
1719
|
+
if (
|
|
1720
|
+
shouldPreferIncomingOfflineRuntimeFile(relPath) &&
|
|
1721
|
+
currentEntry && base && incoming.sha256 === base.sha256
|
|
1722
|
+
) {
|
|
1582
1723
|
nextBase.set(relPath, base);
|
|
1583
1724
|
skipped += 1;
|
|
1584
1725
|
continue;
|
|
@@ -1659,6 +1800,18 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1659
1800
|
skipped += 1;
|
|
1660
1801
|
continue;
|
|
1661
1802
|
}
|
|
1803
|
+
if (
|
|
1804
|
+
shouldPreferIncomingOfflineRuntimeFile(relPath) &&
|
|
1805
|
+
membership !== null && omittedGenerationDirs.has(membership.shardDir)
|
|
1806
|
+
) {
|
|
1807
|
+
// The push omitted this whole generation because a filter excluded at
|
|
1808
|
+
// least one member (snapshot.omittedEmbeddingGenerationDirs): absence
|
|
1809
|
+
// is NOT a deletion — keep the local generation and its base entries
|
|
1810
|
+
// until it is pushed unfiltered or deleted with deletion metadata.
|
|
1811
|
+
pendingLocal += 1;
|
|
1812
|
+
skipped += 1;
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1662
1815
|
if (
|
|
1663
1816
|
shouldPreferIncomingOfflineRuntimeFile(relPath) &&
|
|
1664
1817
|
(base || shouldDeleteAbsentIncomingOfflineRuntimeFile(relPath))
|
|
@@ -1704,8 +1857,8 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1704
1857
|
}
|
|
1705
1858
|
|
|
1706
1859
|
return {
|
|
1707
|
-
upserted,
|
|
1708
|
-
deleted,
|
|
1860
|
+
upserted: upserted + generationUpserted,
|
|
1861
|
+
deleted: deleted + generationDeleted,
|
|
1709
1862
|
skipped,
|
|
1710
1863
|
pendingLocal,
|
|
1711
1864
|
conflicts,
|
|
@@ -1722,6 +1875,8 @@ export async function applyOfflineSyncChangeset(options: {
|
|
|
1722
1875
|
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
1723
1876
|
readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
|
|
1724
1877
|
writeFile?: (target: OfflineSyncFileWriteTarget) => Promise<void>;
|
|
1878
|
+
writeStagingFile?: (target: OfflineSyncFileStagingWriteTarget) => Promise<void>;
|
|
1879
|
+
readStagingFile?: (target: OfflineSyncFileStagingReadTarget) => Promise<Buffer>;
|
|
1725
1880
|
deleteFile?: (target: OfflineSyncFileDeleteTarget) => Promise<void>;
|
|
1726
1881
|
recordDeletionRevision?: OfflineSyncRecordDeletionRevision;
|
|
1727
1882
|
}): Promise<OfflineSyncApplyChangesetResult> {
|
|
@@ -1740,7 +1895,7 @@ export async function applyOfflineSyncChangeset(options: {
|
|
|
1740
1895
|
const records = changeset.changes
|
|
1741
1896
|
.filter((change): change is Extract<OfflineSyncChange, { type: "upsert" }> => change.type === "upsert")
|
|
1742
1897
|
.map((change) => change.file);
|
|
1743
|
-
const incomingBuffers = verifyRecordContents(records, "offline sync changeset");
|
|
1898
|
+
const incomingBuffers = await verifyRecordContents(records, "offline sync changeset");
|
|
1744
1899
|
const currentFiles = options.currentFiles
|
|
1745
1900
|
? filterBaseFilesForMode(normalizeFileStates(options.currentFiles), changeset.includeTranscripts).sort(compareByPath)
|
|
1746
1901
|
: (await buildOfflineSyncSnapshotForPaths({
|
|
@@ -1759,7 +1914,30 @@ export async function applyOfflineSyncChangeset(options: {
|
|
|
1759
1914
|
let appliedDeletes = 0;
|
|
1760
1915
|
let skipped = 0;
|
|
1761
1916
|
|
|
1917
|
+
// Embedding generations apply as ONE unit through the shared locked
|
|
1918
|
+
// transaction (issue #3150). Deferred generations surface as conflicts so
|
|
1919
|
+
// the push side never checkpoints them as applied.
|
|
1920
|
+
const generationRouting = await applyChangesetEmbeddingGenerations({
|
|
1921
|
+
root,
|
|
1922
|
+
changeset,
|
|
1923
|
+
incomingBuffers,
|
|
1924
|
+
currentMap,
|
|
1925
|
+
io: {
|
|
1926
|
+
readFile: options.readFile,
|
|
1927
|
+
writeStagingFile: options.writeStagingFile,
|
|
1928
|
+
readStagingFile: options.readStagingFile,
|
|
1929
|
+
deleteFile: options.deleteFile,
|
|
1930
|
+
},
|
|
1931
|
+
customIoPresent: Boolean(options.writeFile || options.readFile || options.readFileDigest || options.deleteFile),
|
|
1932
|
+
now: Date.now(),
|
|
1933
|
+
});
|
|
1934
|
+
appliedUpserts += generationRouting.appliedUpserts;
|
|
1935
|
+
appliedDeletes += generationRouting.appliedDeletes;
|
|
1936
|
+
conflicts.push(...generationRouting.conflicts);
|
|
1937
|
+
|
|
1762
1938
|
for (const change of changeset.changes) {
|
|
1939
|
+
if (generationRouting.conflictedPaths.has(change.path)) continue;
|
|
1940
|
+
if (generationRouting.transactionHandled.has(change.path)) continue;
|
|
1763
1941
|
const currentEntry = currentMap.get(change.path);
|
|
1764
1942
|
if (change.type === "upsert") {
|
|
1765
1943
|
if (currentEntry?.sha256 === change.file.sha256) {
|
|
@@ -1839,6 +2017,12 @@ export async function applyOfflineSyncChangeset(options: {
|
|
|
1839
2017
|
});
|
|
1840
2018
|
}
|
|
1841
2019
|
|
|
2020
|
+
// Generation transactions bypassed the per-file loop; fold their published
|
|
2021
|
+
// and removed members into the partial result map (returnCurrentFiles ===
|
|
2022
|
+
// false consumers) so it reflects the post-apply generation exactly.
|
|
2023
|
+
for (const [relPath, state] of generationRouting.writtenStates) currentMap.set(relPath, state);
|
|
2024
|
+
for (const relPath of generationRouting.removedPaths) currentMap.delete(relPath);
|
|
2025
|
+
|
|
1842
2026
|
return {
|
|
1843
2027
|
appliedUpserts,
|
|
1844
2028
|
appliedDeletes,
|
|
@@ -1859,14 +2043,37 @@ export async function applyOfflineSyncChangeset(options: {
|
|
|
1859
2043
|
};
|
|
1860
2044
|
}
|
|
1861
2045
|
|
|
1862
|
-
function verifyRecordContents(
|
|
2046
|
+
async function verifyRecordContents(
|
|
1863
2047
|
records: readonly OfflineSyncFileRecord[],
|
|
1864
2048
|
context: string,
|
|
1865
|
-
options: {
|
|
1866
|
-
|
|
2049
|
+
options: {
|
|
2050
|
+
requireContent?: boolean;
|
|
2051
|
+
readIncomingFile?: (target: { path: string; sha256: string; bytes: number }) => Promise<Buffer | null>;
|
|
2052
|
+
} = {},
|
|
2053
|
+
): Promise<Map<string, Buffer>> {
|
|
1867
2054
|
const buffers = new Map<string, Buffer>();
|
|
1868
2055
|
for (const record of records) {
|
|
1869
2056
|
if (typeof record.contentBase64 !== "string") {
|
|
2057
|
+
if (options.readIncomingFile) {
|
|
2058
|
+
// Explicit INCOMING content source for records whose bytes travel
|
|
2059
|
+
// out-of-band (chunked fetch into the client's private staging
|
|
2060
|
+
// root). Never the local current-state file: the buffer is
|
|
2061
|
+
// digest-verified against the incoming record before anything is
|
|
2062
|
+
// published. null keeps the metadata-only behavior below.
|
|
2063
|
+
const incoming = await options.readIncomingFile({
|
|
2064
|
+
path: record.path,
|
|
2065
|
+
sha256: record.sha256,
|
|
2066
|
+
bytes: record.bytes,
|
|
2067
|
+
});
|
|
2068
|
+
if (incoming) {
|
|
2069
|
+
const digest = sha256Buffer(incoming);
|
|
2070
|
+
if (digest.sha256 !== record.sha256 || digest.bytes !== record.bytes) {
|
|
2071
|
+
throw new Error(`${context}: incoming content checksum mismatch for ${record.path}`);
|
|
2072
|
+
}
|
|
2073
|
+
buffers.set(record.path, incoming);
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
1870
2077
|
if (options.requireContent === false) continue;
|
|
1871
2078
|
throw new Error(`${context}: contentBase64 is required for ${record.path}`);
|
|
1872
2079
|
}
|
|
@@ -1956,29 +2163,6 @@ async function writeSafeFile(
|
|
|
1956
2163
|
}
|
|
1957
2164
|
}
|
|
1958
2165
|
|
|
1959
|
-
async function setSafeFileMtime(
|
|
1960
|
-
root: SafeArchiveRoot,
|
|
1961
|
-
relPath: string,
|
|
1962
|
-
mtimeMs: number | undefined,
|
|
1963
|
-
): Promise<boolean> {
|
|
1964
|
-
if (mtimeMs === undefined) return true;
|
|
1965
|
-
const target = await resolveSafeArchiveTarget(root, relPath);
|
|
1966
|
-
const targetStat = await lstat(target).catch((error: unknown) => {
|
|
1967
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
1968
|
-
throw error;
|
|
1969
|
-
});
|
|
1970
|
-
if (!targetStat) return false;
|
|
1971
|
-
if (targetStat.isSymbolicLink()) {
|
|
1972
|
-
throw new Error(`offline sync target is a symlink: ${relPath}`);
|
|
1973
|
-
}
|
|
1974
|
-
const targetMtimeMs = assertOfflineSyncMtimeMs(mtimeMs, "mtimeMs");
|
|
1975
|
-
if (Math.abs(targetStat.mtimeMs - targetMtimeMs) <= OFFLINE_SYNC_FAST_BASE_MTIME_TOLERANCE_MS) {
|
|
1976
|
-
return true;
|
|
1977
|
-
}
|
|
1978
|
-
const mtime = new Date(targetMtimeMs);
|
|
1979
|
-
await utimes(target, mtime, mtime);
|
|
1980
|
-
return true;
|
|
1981
|
-
}
|
|
1982
2166
|
|
|
1983
2167
|
/**
|
|
1984
2168
|
* Read the local file digest for a single relative path without applying
|
|
@@ -2232,272 +2416,6 @@ export async function applyOfflineSyncFileContentChunk(options: {
|
|
|
2232
2416
|
}
|
|
2233
2417
|
}
|
|
2234
2418
|
|
|
2235
|
-
function offlineUploadRelPath(options: {
|
|
2236
|
-
sourceId: string;
|
|
2237
|
-
relPath: string;
|
|
2238
|
-
sha256: string;
|
|
2239
|
-
bytes: number;
|
|
2240
|
-
}): string {
|
|
2241
|
-
const key = hashText([
|
|
2242
|
-
options.sourceId,
|
|
2243
|
-
options.relPath,
|
|
2244
|
-
options.sha256,
|
|
2245
|
-
String(options.bytes),
|
|
2246
|
-
].join("\0"));
|
|
2247
|
-
return `${SYNC_INTERNAL_DIR}/uploads/${key}.part`;
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
async function offlineUploadPath(root: SafeArchiveRoot, options: {
|
|
2251
|
-
sourceId: string;
|
|
2252
|
-
relPath: string;
|
|
2253
|
-
sha256: string;
|
|
2254
|
-
bytes: number;
|
|
2255
|
-
}): Promise<OfflineUploadStaging> {
|
|
2256
|
-
const relPath = offlineUploadRelPath(options);
|
|
2257
|
-
return {
|
|
2258
|
-
kind: "single",
|
|
2259
|
-
relPath,
|
|
2260
|
-
filePath: await resolveSafeArchiveTarget(root, relPath),
|
|
2261
|
-
};
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
async function offlineUploadChunkPath(root: SafeArchiveRoot, options: {
|
|
2265
|
-
sourceId: string;
|
|
2266
|
-
relPath: string;
|
|
2267
|
-
sha256: string;
|
|
2268
|
-
bytes: number;
|
|
2269
|
-
offset: number;
|
|
2270
|
-
}): Promise<OfflineUploadStaging> {
|
|
2271
|
-
const uploadRelPath = offlineUploadRelPath(options);
|
|
2272
|
-
const relPath = `${uploadRelPath}/${String(options.offset).padStart(20, "0")}.part`;
|
|
2273
|
-
return {
|
|
2274
|
-
kind: "chunks",
|
|
2275
|
-
relPath,
|
|
2276
|
-
filePath: await resolveSafeArchiveTarget(root, relPath),
|
|
2277
|
-
};
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
async function writeOfflineUploadChunk(options: {
|
|
2281
|
-
root: SafeArchiveRoot;
|
|
2282
|
-
sourceId: string;
|
|
2283
|
-
relPath: string;
|
|
2284
|
-
sha256: string;
|
|
2285
|
-
bytes: number;
|
|
2286
|
-
offset: number;
|
|
2287
|
-
content: Buffer;
|
|
2288
|
-
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
2289
|
-
writeFile?: (target: OfflineSyncFileWriteTarget) => Promise<void>;
|
|
2290
|
-
writeStagingFile?: (target: OfflineSyncFileStagingWriteTarget) => Promise<void>;
|
|
2291
|
-
}): Promise<OfflineUploadStaging> {
|
|
2292
|
-
if ((options.writeFile || options.writeStagingFile) && !options.readFile) {
|
|
2293
|
-
throw new Error("offline sync upload chunk storage hooks require readFile");
|
|
2294
|
-
}
|
|
2295
|
-
const uploadRoot = {
|
|
2296
|
-
...(await offlineUploadPath(options.root, options)),
|
|
2297
|
-
kind: "chunks" as const,
|
|
2298
|
-
};
|
|
2299
|
-
if (options.offset === 0) {
|
|
2300
|
-
await rm(uploadRoot.filePath, { recursive: true, force: true }).catch(() => {});
|
|
2301
|
-
} else {
|
|
2302
|
-
const existing = await stat(uploadRoot.filePath).catch((error: unknown) => {
|
|
2303
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
2304
|
-
throw error;
|
|
2305
|
-
});
|
|
2306
|
-
if (!existing || !existing.isDirectory()) {
|
|
2307
|
-
throw new Error(`offline sync upload is missing initial chunk for ${options.relPath}`);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
const chunk = await offlineUploadChunkPath(options.root, { ...options, offset: options.offset });
|
|
2311
|
-
|
|
2312
|
-
const writeStagingFile = options.writeStagingFile ?? options.writeFile;
|
|
2313
|
-
if (writeStagingFile) {
|
|
2314
|
-
// Storage-backed services provide these hooks so secure-store deployments
|
|
2315
|
-
// keep staged partial uploads encrypted at rest without mutating indexes.
|
|
2316
|
-
await writeOfflineUploadContent({
|
|
2317
|
-
root: options.root,
|
|
2318
|
-
relPath: chunk.relPath,
|
|
2319
|
-
filePath: chunk.filePath,
|
|
2320
|
-
content: options.content,
|
|
2321
|
-
writeFile: writeStagingFile,
|
|
2322
|
-
});
|
|
2323
|
-
return uploadRoot;
|
|
2324
|
-
}
|
|
2325
|
-
|
|
2326
|
-
await mkdir(path.dirname(chunk.filePath), { recursive: true });
|
|
2327
|
-
const existingChunk = await lstat(chunk.filePath).catch((error: unknown) => {
|
|
2328
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
2329
|
-
throw error;
|
|
2330
|
-
});
|
|
2331
|
-
if (existingChunk?.isSymbolicLink()) {
|
|
2332
|
-
throw new Error(`offline sync upload chunk is a symlink: ${chunk.relPath}`);
|
|
2333
|
-
}
|
|
2334
|
-
await writeFile(chunk.filePath, options.content, { mode: 0o600 });
|
|
2335
|
-
return uploadRoot;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
async function pruneOfflineUploadStaging(root: SafeArchiveRoot): Promise<void> {
|
|
2339
|
-
const uploadsRelPath = `${SYNC_INTERNAL_DIR}/uploads`;
|
|
2340
|
-
const uploadsPath = await resolveSafeArchiveTarget(root, uploadsRelPath);
|
|
2341
|
-
const entries = await readdir(uploadsPath, { withFileTypes: true }).catch((error: unknown) => {
|
|
2342
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
2343
|
-
throw error;
|
|
2344
|
-
});
|
|
2345
|
-
const now = Date.now();
|
|
2346
|
-
await Promise.all(entries.map(async (entry) => {
|
|
2347
|
-
if (!/^[a-f0-9]{64}\.part$/i.test(entry.name)) return;
|
|
2348
|
-
const relPath = `${uploadsRelPath}/${entry.name}`;
|
|
2349
|
-
const filePath = await resolveSafeArchiveTarget(root, relPath);
|
|
2350
|
-
const info = await lstat(filePath).catch((error: unknown) => {
|
|
2351
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
2352
|
-
throw error;
|
|
2353
|
-
});
|
|
2354
|
-
if (!info) return;
|
|
2355
|
-
if (now - info.mtimeMs <= OFFLINE_SYNC_UPLOAD_STAGING_MAX_AGE_MS) return;
|
|
2356
|
-
await rm(filePath, { recursive: true, force: true });
|
|
2357
|
-
}));
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
async function* readOfflineUploadStagingChunks(options: {
|
|
2361
|
-
root: SafeArchiveRoot;
|
|
2362
|
-
upload: OfflineUploadStaging;
|
|
2363
|
-
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
2364
|
-
}): AsyncGenerator<Buffer> {
|
|
2365
|
-
if (options.upload.kind === "single") {
|
|
2366
|
-
yield await readOfflineUploadContent({
|
|
2367
|
-
root: options.root,
|
|
2368
|
-
relPath: options.upload.relPath,
|
|
2369
|
-
filePath: options.upload.filePath,
|
|
2370
|
-
readFile: options.readFile,
|
|
2371
|
-
});
|
|
2372
|
-
return;
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
const entries = await readdir(options.upload.filePath);
|
|
2376
|
-
const chunkNames = entries
|
|
2377
|
-
.filter((entry) => /^\d{20}\.part$/.test(entry))
|
|
2378
|
-
.sort();
|
|
2379
|
-
if (chunkNames.length === 0) {
|
|
2380
|
-
throw new Error(`offline sync upload is missing chunks for ${options.upload.relPath}`);
|
|
2381
|
-
}
|
|
2382
|
-
let expectedOffset = 0;
|
|
2383
|
-
for (const chunkName of chunkNames) {
|
|
2384
|
-
const offset = Number(chunkName.slice(0, 20));
|
|
2385
|
-
if (!Number.isSafeInteger(offset) || offset !== expectedOffset) {
|
|
2386
|
-
throw new Error(
|
|
2387
|
-
`offline sync upload offset mismatch for ${options.upload.relPath}: expected ${expectedOffset}, got ${offset}`,
|
|
2388
|
-
);
|
|
2389
|
-
}
|
|
2390
|
-
const relPath = `${options.upload.relPath}/${chunkName}`;
|
|
2391
|
-
const filePath = await resolveSafeArchiveTarget(options.root, relPath);
|
|
2392
|
-
const content = await readOfflineUploadContent({
|
|
2393
|
-
root: options.root,
|
|
2394
|
-
relPath,
|
|
2395
|
-
filePath,
|
|
2396
|
-
readFile: options.readFile,
|
|
2397
|
-
});
|
|
2398
|
-
expectedOffset += content.length;
|
|
2399
|
-
yield content;
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
async function digestOfflineUploadStagingContent(options: {
|
|
2404
|
-
root: SafeArchiveRoot;
|
|
2405
|
-
upload: OfflineUploadStaging;
|
|
2406
|
-
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
2407
|
-
}): Promise<{ sha256: string; bytes: number }> {
|
|
2408
|
-
const hash = createHash("sha256");
|
|
2409
|
-
let bytes = 0;
|
|
2410
|
-
for await (const chunk of readOfflineUploadStagingChunks(options)) {
|
|
2411
|
-
hash.update(chunk);
|
|
2412
|
-
bytes += chunk.length;
|
|
2413
|
-
}
|
|
2414
|
-
return { sha256: hash.digest("hex"), bytes };
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
async function writeSafeFileFromUpload(
|
|
2418
|
-
root: SafeArchiveRoot,
|
|
2419
|
-
relPath: string,
|
|
2420
|
-
upload: OfflineUploadStaging,
|
|
2421
|
-
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>,
|
|
2422
|
-
writeFileChunks?: (target: OfflineSyncFileWriteChunksTarget) => Promise<void>,
|
|
2423
|
-
mtimeMs?: number,
|
|
2424
|
-
): Promise<void> {
|
|
2425
|
-
const target = await resolveSafeArchiveTarget(root, relPath);
|
|
2426
|
-
const chunks = readOfflineUploadStagingChunks({ root, upload, readFile });
|
|
2427
|
-
if (writeFileChunks) {
|
|
2428
|
-
await writeFileChunks({ root: root.abs, path: relPath, filePath: target, chunks });
|
|
2429
|
-
await setSafeFileMtime(root, relPath, mtimeMs);
|
|
2430
|
-
return;
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
2434
|
-
const tmp = path.join(
|
|
2435
|
-
path.dirname(target),
|
|
2436
|
-
`.remnic-sync.${process.pid}.${randomUUID()}.tmp`,
|
|
2437
|
-
);
|
|
2438
|
-
const handle = await open(tmp, "w", 0o600);
|
|
2439
|
-
try {
|
|
2440
|
-
for await (const chunk of chunks) {
|
|
2441
|
-
if (chunk.length > 0) await handle.write(chunk);
|
|
2442
|
-
}
|
|
2443
|
-
await handle.close();
|
|
2444
|
-
const targetStat = await lstat(target).catch((error: unknown) => {
|
|
2445
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
2446
|
-
throw error;
|
|
2447
|
-
});
|
|
2448
|
-
if (targetStat?.isSymbolicLink()) {
|
|
2449
|
-
throw new Error(`offline sync target is a symlink: ${relPath}`);
|
|
2450
|
-
}
|
|
2451
|
-
await rename(tmp, target);
|
|
2452
|
-
await setSafeFileMtime(root, relPath, mtimeMs);
|
|
2453
|
-
} catch (error) {
|
|
2454
|
-
await handle.close().catch(() => {});
|
|
2455
|
-
await unlink(tmp).catch(() => {});
|
|
2456
|
-
throw error;
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
async function cleanupOfflineUpload(upload: OfflineUploadStaging): Promise<void> {
|
|
2461
|
-
if (upload.kind === "chunks") {
|
|
2462
|
-
await rm(upload.filePath, { recursive: true, force: true });
|
|
2463
|
-
return;
|
|
2464
|
-
}
|
|
2465
|
-
await unlink(upload.filePath).catch((error: unknown) => {
|
|
2466
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
2467
|
-
throw error;
|
|
2468
|
-
});
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
async function readOfflineUploadContent(options: {
|
|
2472
|
-
root: SafeArchiveRoot;
|
|
2473
|
-
relPath: string;
|
|
2474
|
-
filePath: string;
|
|
2475
|
-
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
2476
|
-
}): Promise<Buffer> {
|
|
2477
|
-
if (options.readFile) {
|
|
2478
|
-
return options.readFile({
|
|
2479
|
-
root: options.root.abs,
|
|
2480
|
-
path: options.relPath,
|
|
2481
|
-
filePath: options.filePath,
|
|
2482
|
-
});
|
|
2483
|
-
}
|
|
2484
|
-
return readFile(options.filePath);
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
async function writeOfflineUploadContent(options: {
|
|
2488
|
-
root: SafeArchiveRoot;
|
|
2489
|
-
relPath: string;
|
|
2490
|
-
filePath: string;
|
|
2491
|
-
content: Buffer;
|
|
2492
|
-
writeFile: (target: OfflineSyncFileWriteTarget) => Promise<void>;
|
|
2493
|
-
}): Promise<void> {
|
|
2494
|
-
await options.writeFile({
|
|
2495
|
-
root: options.root.abs,
|
|
2496
|
-
path: options.relPath,
|
|
2497
|
-
filePath: options.filePath,
|
|
2498
|
-
content: options.content,
|
|
2499
|
-
});
|
|
2500
|
-
}
|
|
2501
2419
|
|
|
2502
2420
|
async function deleteSafeFile(
|
|
2503
2421
|
root: SafeArchiveRoot,
|