@remnic/core 9.69.87 → 9.69.88
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 +61 -55
- package/dist/access-authorization-probe.js +62 -56
- package/dist/access-boundary.js +61 -55
- package/dist/access-citation-namespace.js +2 -2
- package/dist/access-cli.js +140 -134
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +61 -55
- package/dist/access-http-briefing.js +62 -56
- 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 +63 -57
- package/dist/access-http.js +85 -79
- package/dist/access-identity-continuity-surface.js +61 -55
- package/dist/access-lcm-surface.js +61 -55
- 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 +74 -68
- package/dist/access-namespace-preflight.js +61 -55
- package/dist/access-observe-write-category.js +19 -13
- package/dist/access-observe-write-surface.js +61 -55
- package/dist/access-offline-manifest.js +15 -10
- package/dist/access-operations-batch.js +65 -59
- package/dist/access-operations.js +72 -66
- package/dist/access-recall-concurrency.js +61 -55
- package/dist/access-recall-response.js +61 -55
- package/dist/access-recall-surface.js +61 -55
- package/dist/access-schema.js +15 -7
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +61 -55
- package/dist/access-surface-catalog.js +63 -57
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +27 -22
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-6IPX2BAC.js → auto-sync-U5N3CB5Y.js} +7 -7
- package/dist/briefing.js +15 -15
- package/dist/buffer.js +7 -7
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.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 +18 -18
- 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-JGSQ6P4D.js → chunk-2DLWLNO4.js} +3 -3
- package/dist/{chunk-LS3XH3UE.js → chunk-2NAD4YCI.js} +2 -2
- package/dist/{chunk-SY5KDWGU.js → chunk-2WLMWP2T.js} +2 -2
- package/dist/{chunk-P6WFK4ZJ.js → chunk-3O5BB45T.js} +4 -4
- package/dist/{chunk-SOZUJCME.js → chunk-3SGTV7RD.js} +6 -6
- package/dist/{chunk-YITN5WXI.js → chunk-4KN2JLQV.js} +2 -2
- package/dist/{chunk-JKMHZF3Q.js → chunk-4MP7XR5J.js} +2 -2
- 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-SLEAJ67D.js → chunk-4OQET73E.js} +4 -4
- package/dist/{chunk-GDYU6VF4.js → chunk-5AYUHSAU.js} +2 -2
- package/dist/{chunk-QWW2JDDX.js → chunk-5E6RYRYW.js} +6 -6
- package/dist/{chunk-ICZQG4XE.js → chunk-5FAGCOZD.js} +4 -4
- package/dist/{chunk-4IEOGIPB.js → chunk-5MNUBT7T.js} +2 -2
- 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-5X4MXNEU.js +64 -0
- package/dist/chunk-5X4MXNEU.js.map +1 -0
- package/dist/{chunk-52II7PB6.js → chunk-645WOU5I.js} +4 -4
- package/dist/{chunk-DUJYRVUD.js → chunk-6O2VUWV6.js} +3 -3
- package/dist/{chunk-CS52MFDV.js → chunk-6UQFJULG.js} +2 -2
- 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-4CDYONWO.js → chunk-A4VSOUCB.js} +3 -3
- package/dist/{chunk-HBBHL4YF.js → chunk-AE5U5NMS.js} +2 -2
- package/dist/{chunk-CUCWX3MO.js → chunk-AEHVZY3X.js} +2 -2
- package/dist/{chunk-DZ525SNQ.js → chunk-AEU4EQGV.js} +1 -1
- package/dist/{chunk-VLLD4TOB.js → chunk-AWG3TIAY.js} +2 -2
- package/dist/{chunk-72JNNJJJ.js → chunk-AXC4WSF3.js} +3 -3
- package/dist/{chunk-N4S4NUX5.js → chunk-B3O3IRZN.js} +1 -1
- package/dist/{chunk-FAN73COB.js → chunk-BGI4NUAO.js} +2 -2
- package/dist/{chunk-XTYX6V52.js → chunk-BJVO4H5C.js} +2 -2
- package/dist/{chunk-EHS5MJEF.js → chunk-BLVR4WYU.js} +3 -3
- package/dist/{chunk-KFCMMDHH.js → chunk-BOZJD5PK.js} +2 -2
- package/dist/{chunk-TM3RXL4G.js → chunk-BUQKSQXL.js} +2 -2
- package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
- package/dist/{chunk-EVBI7S4J.js → chunk-C6U57KG4.js} +4 -4
- package/dist/{chunk-RB776UH3.js → chunk-C7V3KPKJ.js} +235 -93
- package/dist/chunk-C7V3KPKJ.js.map +1 -0
- package/dist/{chunk-VX6GCKBS.js → chunk-C7ZDKWPS.js} +4 -4
- package/dist/{chunk-ZI7QA4MK.js → chunk-CRXRZA3G.js} +2 -2
- package/dist/{chunk-VQNONC4E.js → chunk-CZJA2EQE.js} +2 -2
- package/dist/{chunk-M536WNTQ.js → chunk-D5BSQR63.js} +2 -2
- package/dist/{chunk-37S3IMYB.js → chunk-DGS34QSY.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
- 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-EQ6YOIKO.js → chunk-FJLQXMTU.js} +3 -3
- package/dist/{chunk-SRZW7FNU.js → chunk-FKD656D2.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-KP7SANCF.js → chunk-GWM6T5EQ.js} +2 -2
- package/dist/{chunk-UA5DX5C3.js → chunk-GZDTVYK2.js} +2 -2
- package/dist/{chunk-3GQC3NMB.js → chunk-H56VY4BY.js} +5 -5
- package/dist/{chunk-L6UZEAID.js → chunk-H5BJWPUI.js} +4 -4
- package/dist/{chunk-YSJTCOHR.js → chunk-HLB7GKGG.js} +2 -2
- package/dist/{chunk-PUKTBULQ.js → chunk-HWLIMQE2.js} +2 -2
- package/dist/{chunk-CWHV574S.js → chunk-IOSZHHJC.js} +6 -6
- package/dist/{chunk-P2WZHLUF.js → chunk-JVPWRNHA.js} +2 -2
- package/dist/{chunk-6ET2VII7.js → chunk-JYG2334B.js} +2 -2
- package/dist/chunk-K6T33R4Q.js +535 -0
- package/dist/chunk-K6T33R4Q.js.map +1 -0
- package/dist/{chunk-WIKO4SCR.js → chunk-LHRQBMJN.js} +2 -2
- package/dist/{chunk-K4J4E7WV.js → chunk-LO4UKPKX.js} +2 -2
- package/dist/{chunk-LF5IGOOD.js → chunk-LU7E2MXE.js} +17 -17
- package/dist/{chunk-RJLV4BKR.js → chunk-LV5H6FTN.js} +2 -2
- package/dist/{chunk-3C3J243X.js → chunk-LW3HKRL7.js} +3 -3
- package/dist/{chunk-4BZP2B4U.js → chunk-M6C7SOR6.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-SPSRGEDQ.js → chunk-MXCAJKMA.js} +9 -9
- 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-AQK54Q7O.js → chunk-NYKIUQL6.js} +2 -2
- package/dist/{chunk-WQZZDN5M.js → chunk-OCFXVDBZ.js} +322 -376
- package/dist/chunk-OCFXVDBZ.js.map +1 -0
- package/dist/{chunk-NE6PHPLG.js → chunk-OK52IY3W.js} +10 -10
- package/dist/{chunk-6TT6HRE4.js → chunk-P2ODTYOF.js} +2 -2
- package/dist/{chunk-W4AOR52H.js → chunk-PEBHPCX2.js} +2 -2
- package/dist/{chunk-VYCOJKSX.js → chunk-PO7NXNGC.js} +2 -2
- package/dist/{chunk-G6SVZWPB.js → chunk-PZX3AQLF.js} +44 -1
- package/dist/chunk-PZX3AQLF.js.map +1 -0
- package/dist/{chunk-XAITGGX5.js → chunk-Q4AUIFUA.js} +4 -4
- package/dist/{chunk-MQJL6CDZ.js → chunk-QAW55ZHJ.js} +4 -4
- package/dist/{chunk-FOH7AMMI.js → chunk-QB57WIS6.js} +73 -82
- package/dist/chunk-QB57WIS6.js.map +1 -0
- package/dist/{chunk-QVS22WTO.js → chunk-QI7K7BZ5.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-YTQGGO3K.js → chunk-S2GE2K36.js} +81 -78
- package/dist/{chunk-YTQGGO3K.js.map → chunk-S2GE2K36.js.map} +1 -1
- package/dist/{chunk-BJRAELDH.js → chunk-SCXURCVT.js} +3 -3
- package/dist/{chunk-FRCESVV6.js → chunk-STEOT4DI.js} +19 -19
- package/dist/chunk-STEOT4DI.js.map +1 -0
- package/dist/{chunk-BFO3IR56.js → chunk-SWBC3UER.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-D5F7TKMH.js → chunk-V4JZCTMK.js} +2 -2
- package/dist/{chunk-QEV26L2F.js → chunk-VALKBYBN.js} +2 -2
- package/dist/{chunk-7LCGFUMB.js → chunk-VDX2J7OX.js} +4 -4
- package/dist/{chunk-TDYAIXHK.js → chunk-VVX5PZFS.js} +5 -5
- package/dist/{chunk-6NQ4LSMS.js → chunk-W3XXFPRI.js} +52 -52
- package/dist/{chunk-4IIMMZZK.js → chunk-WKY7N3L7.js} +10 -10
- package/dist/{chunk-VVDWZRZG.js → chunk-X5SHE2O3.js} +3 -3
- package/dist/{chunk-NR2ASHBL.js → chunk-XOV7NRY5.js} +2 -2
- package/dist/{chunk-6L3PBSAR.js → chunk-YJ7MW627.js} +2 -2
- package/dist/{chunk-LQG5O5EP.js → chunk-Z3O53WZR.js} +2 -2
- package/dist/{chunk-FB64XE3R.js → chunk-Z5LP6ILP.js} +2 -2
- package/dist/{chunk-VKQI47DJ.js → chunk-Z6EZCN6Z.js} +8 -8
- package/dist/{chunk-V2ES4WCZ.js → chunk-ZZZ4I455.js} +2 -2
- package/dist/cli.js +106 -100
- package/dist/coding/export-okf-codegraph.js +16 -16
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +16 -16
- package/dist/config.js +27 -22
- package/dist/connectors/codex-materialize-runner.js +15 -15
- package/dist/connectors/index.js +15 -15
- 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 +17 -17
- package/dist/corpus-watermark.js +17 -17
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-http-glue.js +15 -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 +16 -16
- package/dist/event-order-recall.js +3 -3
- package/dist/explicit-capture.js +6 -6
- package/dist/external-wiki-access.js +62 -56
- package/dist/extraction.js +21 -21
- 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 +207 -177
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +9 -9
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +18 -18
- package/dist/maintenance/memory-governance.js +15 -15
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
- package/dist/maintenance/rebuild-memory-projection.js +16 -16
- package/dist/memory-browse-operations.js +62 -56
- package/dist/memory-subject.js +3 -3
- package/dist/namespaces/migrate.js +16 -16
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.js +15 -15
- package/dist/offline-sync-embedding-generation.d.ts +207 -0
- package/dist/offline-sync-embedding-generation.d.ts.map +1 -0
- package/dist/offline-sync-embedding-generation.js +42 -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 +28 -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 +46 -27
- package/dist/offline-sync.d.ts.map +1 -1
- package/dist/offline-sync.js +21 -8
- package/dist/operator-doctor-corpus.js +18 -18
- package/dist/operator-toolkit.js +38 -33
- package/dist/orchestration/maintenance.js +26 -26
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +142 -136
- package/dist/recall-navigation-operations.js +62 -56
- package/dist/recall-navigation.js +61 -55
- 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 +28 -23
- package/dist/salvage-envelope.js +5 -5
- package/dist/search/factory.js +3 -3
- package/dist/search/index.js +7 -7
- 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 +16 -16
- package/dist/semantic-rule-promotion.js +15 -15
- package/dist/semantic-rule-verifier.js +15 -15
- package/dist/source-agent-qualifier.js +18 -18
- package/dist/standup/build.js +15 -15
- 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 +14 -14
- package/dist/subject-config.js +4 -4
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +64 -58
- 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 +16 -16
- package/dist/{tui-55UJMJX7.js → tui-2Z5WOV7L.js} +3 -3
- package/dist/tui-2Z5WOV7L.js.map +1 -0
- package/dist/verified-recall.js +15 -15
- package/dist/who-knows-cli.js +16 -16
- package/dist/who-knows.js +15 -15
- 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-embedding-generation.test.ts +2173 -0
- package/src/offline-sync-embedding-generation.ts +885 -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 +458 -453
- package/src/orchestrator.ts +2 -1
- package/src/secure-store/secure-fs.ts +26 -6
- package/src/storage.ts +11 -6
- 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-YO5QJ6L3.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-2DLWLNO4.js.map} +0 -0
- /package/dist/{chunk-LS3XH3UE.js.map → chunk-2NAD4YCI.js.map} +0 -0
- /package/dist/{chunk-SY5KDWGU.js.map → chunk-2WLMWP2T.js.map} +0 -0
- /package/dist/{chunk-P6WFK4ZJ.js.map → chunk-3O5BB45T.js.map} +0 -0
- /package/dist/{chunk-SOZUJCME.js.map → chunk-3SGTV7RD.js.map} +0 -0
- /package/dist/{chunk-YITN5WXI.js.map → chunk-4KN2JLQV.js.map} +0 -0
- /package/dist/{chunk-JKMHZF3Q.js.map → chunk-4MP7XR5J.js.map} +0 -0
- /package/dist/{chunk-7DE2NOON.js.map → chunk-4O4XCBOS.js.map} +0 -0
- /package/dist/{chunk-SLEAJ67D.js.map → chunk-4OQET73E.js.map} +0 -0
- /package/dist/{chunk-GDYU6VF4.js.map → chunk-5AYUHSAU.js.map} +0 -0
- /package/dist/{chunk-QWW2JDDX.js.map → chunk-5E6RYRYW.js.map} +0 -0
- /package/dist/{chunk-ICZQG4XE.js.map → chunk-5FAGCOZD.js.map} +0 -0
- /package/dist/{chunk-4IEOGIPB.js.map → chunk-5MNUBT7T.js.map} +0 -0
- /package/dist/{chunk-52II7PB6.js.map → chunk-645WOU5I.js.map} +0 -0
- /package/dist/{chunk-DUJYRVUD.js.map → chunk-6O2VUWV6.js.map} +0 -0
- /package/dist/{chunk-CS52MFDV.js.map → chunk-6UQFJULG.js.map} +0 -0
- /package/dist/{chunk-HESDNYRB.js.map → chunk-76M5OK7V.js.map} +0 -0
- /package/dist/{chunk-4CDYONWO.js.map → chunk-A4VSOUCB.js.map} +0 -0
- /package/dist/{chunk-HBBHL4YF.js.map → chunk-AE5U5NMS.js.map} +0 -0
- /package/dist/{chunk-CUCWX3MO.js.map → chunk-AEHVZY3X.js.map} +0 -0
- /package/dist/{chunk-DZ525SNQ.js.map → chunk-AEU4EQGV.js.map} +0 -0
- /package/dist/{chunk-VLLD4TOB.js.map → chunk-AWG3TIAY.js.map} +0 -0
- /package/dist/{chunk-72JNNJJJ.js.map → chunk-AXC4WSF3.js.map} +0 -0
- /package/dist/{chunk-N4S4NUX5.js.map → chunk-B3O3IRZN.js.map} +0 -0
- /package/dist/{chunk-FAN73COB.js.map → chunk-BGI4NUAO.js.map} +0 -0
- /package/dist/{chunk-XTYX6V52.js.map → chunk-BJVO4H5C.js.map} +0 -0
- /package/dist/{chunk-EHS5MJEF.js.map → chunk-BLVR4WYU.js.map} +0 -0
- /package/dist/{chunk-KFCMMDHH.js.map → chunk-BOZJD5PK.js.map} +0 -0
- /package/dist/{chunk-TM3RXL4G.js.map → chunk-BUQKSQXL.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
- /package/dist/{chunk-EVBI7S4J.js.map → chunk-C6U57KG4.js.map} +0 -0
- /package/dist/{chunk-VX6GCKBS.js.map → chunk-C7ZDKWPS.js.map} +0 -0
- /package/dist/{chunk-ZI7QA4MK.js.map → chunk-CRXRZA3G.js.map} +0 -0
- /package/dist/{chunk-VQNONC4E.js.map → chunk-CZJA2EQE.js.map} +0 -0
- /package/dist/{chunk-M536WNTQ.js.map → chunk-D5BSQR63.js.map} +0 -0
- /package/dist/{chunk-37S3IMYB.js.map → chunk-DGS34QSY.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.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-EQ6YOIKO.js.map → chunk-FJLQXMTU.js.map} +0 -0
- /package/dist/{chunk-SRZW7FNU.js.map → chunk-FKD656D2.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-KP7SANCF.js.map → chunk-GWM6T5EQ.js.map} +0 -0
- /package/dist/{chunk-UA5DX5C3.js.map → chunk-GZDTVYK2.js.map} +0 -0
- /package/dist/{chunk-3GQC3NMB.js.map → chunk-H56VY4BY.js.map} +0 -0
- /package/dist/{chunk-L6UZEAID.js.map → chunk-H5BJWPUI.js.map} +0 -0
- /package/dist/{chunk-YSJTCOHR.js.map → chunk-HLB7GKGG.js.map} +0 -0
- /package/dist/{chunk-PUKTBULQ.js.map → chunk-HWLIMQE2.js.map} +0 -0
- /package/dist/{chunk-CWHV574S.js.map → chunk-IOSZHHJC.js.map} +0 -0
- /package/dist/{chunk-P2WZHLUF.js.map → chunk-JVPWRNHA.js.map} +0 -0
- /package/dist/{chunk-6ET2VII7.js.map → chunk-JYG2334B.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-LF5IGOOD.js.map → chunk-LU7E2MXE.js.map} +0 -0
- /package/dist/{chunk-RJLV4BKR.js.map → chunk-LV5H6FTN.js.map} +0 -0
- /package/dist/{chunk-3C3J243X.js.map → chunk-LW3HKRL7.js.map} +0 -0
- /package/dist/{chunk-4BZP2B4U.js.map → chunk-M6C7SOR6.js.map} +0 -0
- /package/dist/{chunk-SPSRGEDQ.js.map → chunk-MXCAJKMA.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-AQK54Q7O.js.map → chunk-NYKIUQL6.js.map} +0 -0
- /package/dist/{chunk-NE6PHPLG.js.map → chunk-OK52IY3W.js.map} +0 -0
- /package/dist/{chunk-6TT6HRE4.js.map → chunk-P2ODTYOF.js.map} +0 -0
- /package/dist/{chunk-W4AOR52H.js.map → chunk-PEBHPCX2.js.map} +0 -0
- /package/dist/{chunk-VYCOJKSX.js.map → chunk-PO7NXNGC.js.map} +0 -0
- /package/dist/{chunk-XAITGGX5.js.map → chunk-Q4AUIFUA.js.map} +0 -0
- /package/dist/{chunk-MQJL6CDZ.js.map → chunk-QAW55ZHJ.js.map} +0 -0
- /package/dist/{chunk-QVS22WTO.js.map → chunk-QI7K7BZ5.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-BJRAELDH.js.map → chunk-SCXURCVT.js.map} +0 -0
- /package/dist/{chunk-BFO3IR56.js.map → chunk-SWBC3UER.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-D5F7TKMH.js.map → chunk-V4JZCTMK.js.map} +0 -0
- /package/dist/{chunk-QEV26L2F.js.map → chunk-VALKBYBN.js.map} +0 -0
- /package/dist/{chunk-7LCGFUMB.js.map → chunk-VDX2J7OX.js.map} +0 -0
- /package/dist/{chunk-TDYAIXHK.js.map → chunk-VVX5PZFS.js.map} +0 -0
- /package/dist/{chunk-6NQ4LSMS.js.map → chunk-W3XXFPRI.js.map} +0 -0
- /package/dist/{chunk-4IIMMZZK.js.map → chunk-WKY7N3L7.js.map} +0 -0
- /package/dist/{chunk-VVDWZRZG.js.map → chunk-X5SHE2O3.js.map} +0 -0
- /package/dist/{chunk-NR2ASHBL.js.map → chunk-XOV7NRY5.js.map} +0 -0
- /package/dist/{chunk-6L3PBSAR.js.map → chunk-YJ7MW627.js.map} +0 -0
- /package/dist/{chunk-LQG5O5EP.js.map → chunk-Z3O53WZR.js.map} +0 -0
- /package/dist/{chunk-FB64XE3R.js.map → chunk-Z5LP6ILP.js.map} +0 -0
- /package/dist/{chunk-VKQI47DJ.js.map → chunk-Z6EZCN6Z.js.map} +0 -0
- /package/dist/{chunk-V2ES4WCZ.js.map → chunk-ZZZ4I455.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/{state-ITXJKXRE.js.map → graph-edge-decay-MKIRZ762.js.map} +0 -0
- /package/dist/{tui-55UJMJX7.js.map → offline-sync-embedding-generation.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,37 @@ 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
|
+
EMBEDDING_SHARD_DIR_BASENAME,
|
|
31
|
+
applyEmbeddingGenerationRemoval,
|
|
32
|
+
applyEmbeddingGenerationTransaction,
|
|
33
|
+
assertEmbeddingGenerationStillIncluded,
|
|
34
|
+
computeOmittedEmbeddingGenerationPaths,
|
|
35
|
+
detectIncomingEmbeddingGenerations,
|
|
36
|
+
divergedEmbeddingGenerationDeferrals,
|
|
37
|
+
embeddingGenerationMembership,
|
|
38
|
+
embeddingMarkerDirOf,
|
|
39
|
+
embeddingShardDirOf,
|
|
40
|
+
isEmbeddingGenerationDirPath,
|
|
41
|
+
isPathInOmittedEmbeddingGeneration,
|
|
42
|
+
type EmbeddingGenerationTransactionIo,
|
|
43
|
+
type EmbeddingGenerationTransactionResult,
|
|
44
|
+
} from "./offline-sync-embedding-generation.js";
|
|
45
|
+
import { withEmbeddingGenerationLock, withEmbeddingGenerationLockIter } from "./embedding-generation-lock.js";
|
|
46
|
+
import {
|
|
47
|
+
isCanonicalRuntimeStatePath,
|
|
48
|
+
shouldDeleteAbsentIncomingOfflineRuntimeFile,
|
|
49
|
+
shouldPreferIncomingOfflineRuntimeFile,
|
|
50
|
+
} from "./offline-sync-runtime-state.js";
|
|
51
|
+
export { shouldPreferIncomingOfflineRuntimeFile } from "./offline-sync-runtime-state.js";
|
|
52
|
+
export { computeOmittedEmbeddingGenerationPaths } from "./offline-sync-embedding-generation.js";
|
|
53
|
+
export {
|
|
54
|
+
compileOfflineSyncExcludeGlobs,
|
|
55
|
+
globToRegExp,
|
|
56
|
+
parseOfflineSyncExcludes,
|
|
57
|
+
} from "./offline-sync-exclude-globs.js";
|
|
28
58
|
import {
|
|
29
59
|
isEncryptedOfflineSyncFile,
|
|
30
60
|
readPlainOfflineSyncFileChunk,
|
|
@@ -33,8 +63,21 @@ import {
|
|
|
33
63
|
type OfflineSyncExcludeFile,
|
|
34
64
|
type OfflineSyncFileTarget,
|
|
35
65
|
plainFileDigest,
|
|
66
|
+
SYNC_INTERNAL_DIR,
|
|
67
|
+
setSafeFileMtime,
|
|
68
|
+
assertNonNegativeFinite,
|
|
69
|
+
assertOfflineSyncMtimeMs,
|
|
70
|
+
OFFLINE_SYNC_FAST_BASE_MTIME_TOLERANCE_MS,
|
|
36
71
|
} from "./offline-sync-file-io.js";
|
|
37
|
-
import { CENSUS_MAX_MTIME_MS,
|
|
72
|
+
import { CENSUS_MAX_MTIME_MS, isSha256Hex } from "./census-validation.js";
|
|
73
|
+
import {
|
|
74
|
+
cleanupOfflineUpload,
|
|
75
|
+
digestOfflineUploadStagingContent,
|
|
76
|
+
pruneOfflineUploadStaging,
|
|
77
|
+
writeOfflineUploadChunk,
|
|
78
|
+
writeSafeFileFromUpload,
|
|
79
|
+
hashText,
|
|
80
|
+
} from "./offline-sync-upload-staging.js";
|
|
38
81
|
export type { OfflineSyncExcludeFile, OfflineSyncFileTarget } from "./offline-sync-file-io.js";
|
|
39
82
|
|
|
40
83
|
export const OFFLINE_SYNC_SNAPSHOT_FORMAT = "remnic.offline-sync.snapshot.v1";
|
|
@@ -76,6 +119,14 @@ export interface OfflineSyncSnapshot {
|
|
|
76
119
|
includeTranscripts: boolean;
|
|
77
120
|
files: OfflineSyncFileRecord[];
|
|
78
121
|
deletions?: OfflineSyncDeletionRevision[];
|
|
122
|
+
/**
|
|
123
|
+
* Embedding generation dirs whose on-disk members were ALL omitted from
|
|
124
|
+
* `files` because a push filter excluded at least one member (PR #3148
|
|
125
|
+
* round 4). Their absence here is NOT a delete instruction: a receiver
|
|
126
|
+
* keeps its local copy and its base entries until the generation is
|
|
127
|
+
* pushed unfiltered or genuinely deleted with deletion metadata.
|
|
128
|
+
*/
|
|
129
|
+
omittedEmbeddingGenerationDirs?: string[];
|
|
79
130
|
}
|
|
80
131
|
|
|
81
132
|
export type OfflineSyncChange =
|
|
@@ -166,7 +217,21 @@ export interface OfflineSyncFileWriteChunksTarget extends OfflineSyncFileTarget
|
|
|
166
217
|
chunks: AsyncIterable<Buffer>;
|
|
167
218
|
}
|
|
168
219
|
|
|
169
|
-
export interface OfflineSyncFileStagingWriteTarget extends OfflineSyncFileWriteTarget {
|
|
220
|
+
export interface OfflineSyncFileStagingWriteTarget extends OfflineSyncFileWriteTarget {
|
|
221
|
+
/**
|
|
222
|
+
* ABSOLUTE final path whose canonical AAD the staged ciphertext binds
|
|
223
|
+
* (secure-store deployments): the offline-sync embedding generation
|
|
224
|
+
* transaction stages under a staging dir and publishes by rename, so the
|
|
225
|
+
* ciphertext must decrypt at the FINAL path. The AAD derives through the
|
|
226
|
+
* same filePathAad logic as ordinary reads (platform-consistent).
|
|
227
|
+
*/
|
|
228
|
+
finalAadFilePath?: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface OfflineSyncFileStagingReadTarget extends OfflineSyncFileTarget {
|
|
232
|
+
/** See {@link OfflineSyncFileStagingWriteTarget.finalAadFilePath}. */
|
|
233
|
+
finalAadFilePath?: string;
|
|
234
|
+
}
|
|
170
235
|
|
|
171
236
|
export interface OfflineSyncFileContentChunk extends Omit<OfflineSyncFileState, "sha256"> {
|
|
172
237
|
sha256?: string;
|
|
@@ -205,9 +270,6 @@ interface OfflineSyncFileRecordOptions {
|
|
|
205
270
|
signal?: AbortSignal;
|
|
206
271
|
}
|
|
207
272
|
|
|
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
273
|
const OFFLINE_SYNC_FAST_BASE_CTIME_TOLERANCE_MS = 1;
|
|
212
274
|
const EXCLUDED_FILE_NAMES = new Set([
|
|
213
275
|
".sync-state.json",
|
|
@@ -228,52 +290,7 @@ const EXCLUDED_FILE_PREFIXES = [
|
|
|
228
290
|
* matches zero or more whole segments, and a trailing `dir/star-star`
|
|
229
291
|
* matches everything under `dir/` at any depth.
|
|
230
292
|
*/
|
|
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
293
|
|
|
274
|
-
function hashText(value: string): string {
|
|
275
|
-
return createHash("sha256").update(value).digest("hex");
|
|
276
|
-
}
|
|
277
294
|
|
|
278
295
|
function sha256Buffer(buffer: Buffer): { sha256: string; bytes: number } {
|
|
279
296
|
return sha256Bytes(buffer);
|
|
@@ -307,20 +324,7 @@ function assertNonNegativeInteger(value: unknown, field: string): number {
|
|
|
307
324
|
return value;
|
|
308
325
|
}
|
|
309
326
|
|
|
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
327
|
|
|
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
328
|
|
|
325
329
|
function assertBoolean(value: unknown, field: string): boolean {
|
|
326
330
|
if (typeof value !== "boolean") {
|
|
@@ -407,6 +411,10 @@ function snapshotBuilderDeletions(options: {
|
|
|
407
411
|
includeTranscripts: boolean;
|
|
408
412
|
excludeNodeLocalState?: boolean;
|
|
409
413
|
userExcludeRegexps?: readonly RegExp[];
|
|
414
|
+
/** Generation dirs omitted by the push enumeration (PR #3148): revisions
|
|
415
|
+
* for ANY member — including already-absent ones — are suppressed, so a
|
|
416
|
+
* filtered generation can never be announced as deleted. */
|
|
417
|
+
omittedGenerationDirs?: ReadonlySet<string>;
|
|
410
418
|
}): OfflineSyncDeletionRevision[] | undefined {
|
|
411
419
|
const deletions = normalizeDeletionRevisions(options.deletions, "deletions");
|
|
412
420
|
if (deletions === undefined) return undefined;
|
|
@@ -418,6 +426,7 @@ function snapshotBuilderDeletions(options: {
|
|
|
418
426
|
return deletions.filter((deletion) =>
|
|
419
427
|
(scopedPaths === undefined || scopedPaths.has(deletion.path.toLowerCase())) &&
|
|
420
428
|
!presentPaths.has(deletion.path.toLowerCase()) &&
|
|
429
|
+
!isDeletionInOmittedGeneration(deletion.path, options.omittedGenerationDirs) &&
|
|
421
430
|
!(options.excludeNodeLocalState === false
|
|
422
431
|
? shouldExcludeRelPath(deletion.path, options.includeTranscripts)
|
|
423
432
|
: shouldExcludePushRelPath(
|
|
@@ -427,11 +436,21 @@ function snapshotBuilderDeletions(options: {
|
|
|
427
436
|
)));
|
|
428
437
|
}
|
|
429
438
|
|
|
439
|
+
function isDeletionInOmittedGeneration(
|
|
440
|
+
relPath: string,
|
|
441
|
+
omittedGenerationDirs: ReadonlySet<string> | undefined,
|
|
442
|
+
): boolean {
|
|
443
|
+
if (!omittedGenerationDirs || omittedGenerationDirs.size === 0) return false;
|
|
444
|
+
const membership = embeddingGenerationMembership(relPath);
|
|
445
|
+
return membership !== null && omittedGenerationDirs.has(membership.shardDir);
|
|
446
|
+
}
|
|
447
|
+
|
|
430
448
|
export async function filterOfflineSyncDeletionRevisions(options: {
|
|
431
449
|
root: string;
|
|
432
450
|
deletions: readonly OfflineSyncDeletionRevision[];
|
|
433
451
|
includeTranscripts?: boolean;
|
|
434
452
|
userExcludeRegexps?: readonly RegExp[];
|
|
453
|
+
excludeFile?: OfflineSyncExcludeFile;
|
|
435
454
|
}): Promise<OfflineSyncDeletionRevision[]> {
|
|
436
455
|
const deletions = normalizeDeletionRevisions(options.deletions, "deletions");
|
|
437
456
|
if (deletions === undefined) throw new Error("deletions must be an array");
|
|
@@ -442,9 +461,20 @@ export async function filterOfflineSyncDeletionRevisions(options: {
|
|
|
442
461
|
"root",
|
|
443
462
|
);
|
|
444
463
|
const includeTranscripts = options.includeTranscripts !== false;
|
|
464
|
+
const omission = await computeOmittedEmbeddingGenerationPaths({
|
|
465
|
+
rootAbs: root.abs,
|
|
466
|
+
userExcludeRegexps: options.userExcludeRegexps,
|
|
467
|
+
excludeFile: options.excludeFile,
|
|
468
|
+
isExcludedRelPath: (relPath) =>
|
|
469
|
+
shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps),
|
|
470
|
+
});
|
|
471
|
+
const omittedDirs = new Set(omission.omittedDirs);
|
|
445
472
|
const filtered: OfflineSyncDeletionRevision[] = [];
|
|
446
473
|
for (const deletion of deletions) {
|
|
447
|
-
if (
|
|
474
|
+
if (
|
|
475
|
+
shouldExcludePushRelPath(deletion.path, includeTranscripts, options.userExcludeRegexps) ||
|
|
476
|
+
isDeletionInOmittedGeneration(deletion.path, omittedDirs)
|
|
477
|
+
) {
|
|
448
478
|
continue;
|
|
449
479
|
}
|
|
450
480
|
const filePath = path.join(root.abs, ...deletion.path.split("/"));
|
|
@@ -503,6 +533,26 @@ export function normalizeOfflineSyncSnapshot(
|
|
|
503
533
|
if (excludedPath) {
|
|
504
534
|
throw new Error(`offline sync snapshot contains excluded path: ${excludedPath}`);
|
|
505
535
|
}
|
|
536
|
+
let omittedEmbeddingGenerationDirs: string[] | undefined;
|
|
537
|
+
if (obj.omittedEmbeddingGenerationDirs !== undefined) {
|
|
538
|
+
if (!Array.isArray(obj.omittedEmbeddingGenerationDirs)) {
|
|
539
|
+
throw new Error("offline sync snapshot omittedEmbeddingGenerationDirs must be an array");
|
|
540
|
+
}
|
|
541
|
+
const seenDirs = new Set<string>();
|
|
542
|
+
for (const [index, entry] of obj.omittedEmbeddingGenerationDirs.entries()) {
|
|
543
|
+
if (
|
|
544
|
+
typeof entry !== "string" ||
|
|
545
|
+
!isEmbeddingGenerationDirPath(entry) ||
|
|
546
|
+
seenDirs.has(entry)
|
|
547
|
+
) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
`omittedEmbeddingGenerationDirs[${index}] must be a unique canonical embeddings generation dir`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
seenDirs.add(entry);
|
|
553
|
+
}
|
|
554
|
+
omittedEmbeddingGenerationDirs = [...seenDirs].sort((left, right) => left.localeCompare(right));
|
|
555
|
+
}
|
|
506
556
|
return {
|
|
507
557
|
format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
508
558
|
schemaVersion: 1,
|
|
@@ -511,6 +561,7 @@ export function normalizeOfflineSyncSnapshot(
|
|
|
511
561
|
includeTranscripts,
|
|
512
562
|
files,
|
|
513
563
|
...(deletions === undefined ? {} : { deletions }),
|
|
564
|
+
...(omittedEmbeddingGenerationDirs === undefined ? {} : { omittedEmbeddingGenerationDirs }),
|
|
514
565
|
};
|
|
515
566
|
}
|
|
516
567
|
|
|
@@ -675,15 +726,7 @@ function shouldExcludePushRelPath(
|
|
|
675
726
|
|
|
676
727
|
// Precompiled once at module load — this check sits on the hot
|
|
677
728
|
// 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
729
|
|
|
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
730
|
|
|
688
731
|
/**
|
|
689
732
|
* Compile operator-supplied `offlineSyncExcludes` glob strings into a
|
|
@@ -692,46 +735,6 @@ function matchesOfflineSyncDefaultExclude(relPosix: string): boolean {
|
|
|
692
735
|
* a thrown error as a fatal configuration mistake and refuse to start the
|
|
693
736
|
* sync run rather than silently dropping the bad entry.
|
|
694
737
|
*/
|
|
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
738
|
|
|
736
739
|
function shouldIgnoreIncomingRuntimePath(relPosix: string): boolean {
|
|
737
740
|
if (isInternalRemnicStatePath(relPosix)) return true;
|
|
@@ -740,43 +743,6 @@ function shouldIgnoreIncomingRuntimePath(relPosix: string): boolean {
|
|
|
740
743
|
return isCanonicalRuntimeStatePath(parts) && basename.includes(".tmp-");
|
|
741
744
|
}
|
|
742
745
|
|
|
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
746
|
function filterBaseFilesForMode(
|
|
781
747
|
files: readonly OfflineSyncFileState[],
|
|
782
748
|
includeTranscripts: boolean,
|
|
@@ -850,6 +816,13 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
850
816
|
readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
|
|
851
817
|
excludeFile?: OfflineSyncExcludeFile;
|
|
852
818
|
userExcludeRegexps?: readonly RegExp[];
|
|
819
|
+
/**
|
|
820
|
+
* Precomputed whole-generation omission set (PR #3148 round 4): paths the
|
|
821
|
+
* walk must skip because their embedding generation lost ANY member to a
|
|
822
|
+
* push filter. When omitted and push excludes are active, the set is
|
|
823
|
+
* computed here so streaming census consumers get the same semantics.
|
|
824
|
+
*/
|
|
825
|
+
skipEmbeddingGenerationPaths?: ReadonlySet<string>;
|
|
853
826
|
/**
|
|
854
827
|
* When false, enumeration uses only the legacy structural excludes —
|
|
855
828
|
* the apply/pull-side view of local files. Push-side callers keep the
|
|
@@ -864,8 +837,19 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
864
837
|
const rootAbs = path.resolve(options.root);
|
|
865
838
|
const root = await prepareSafeArchiveRoot(rootAbs, "iterateOfflineSyncSnapshotFileRecords", "root");
|
|
866
839
|
const includeTranscripts = options.includeTranscripts !== false;
|
|
867
|
-
|
|
868
|
-
|
|
840
|
+
const pushExcludes = options.excludeNodeLocalState !== false;
|
|
841
|
+
const skipGenerationPaths = options.skipEmbeddingGenerationPaths ??
|
|
842
|
+
(pushExcludes
|
|
843
|
+
? (await computeOmittedEmbeddingGenerationPaths({
|
|
844
|
+
rootAbs: root.abs,
|
|
845
|
+
userExcludeRegexps: options.userExcludeRegexps,
|
|
846
|
+
excludeFile: options.excludeFile,
|
|
847
|
+
isExcludedRelPath: (relPath) =>
|
|
848
|
+
shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps),
|
|
849
|
+
})).omittedPaths
|
|
850
|
+
: undefined);
|
|
851
|
+
|
|
852
|
+
async function* walk(dirAbs: string, includeContent = options.includeContent === true): AsyncIterable<OfflineSyncFileRecord> {
|
|
869
853
|
throwIfOfflineSyncAborted(options.signal);
|
|
870
854
|
let entries = await readdir(dirAbs, { withFileTypes: true });
|
|
871
855
|
entries = entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
@@ -877,9 +861,17 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
877
861
|
? shouldExcludeRelPath(relPosix, includeTranscripts)
|
|
878
862
|
: shouldExcludePushRelPath(relPosix, includeTranscripts, options.userExcludeRegexps)
|
|
879
863
|
) continue;
|
|
864
|
+
if (skipGenerationPaths?.has(relPosix)) continue;
|
|
880
865
|
if (entry.isSymbolicLink()) continue;
|
|
881
866
|
if (entry.isDirectory()) {
|
|
882
|
-
|
|
867
|
+
if (isEmbeddingGenerationDirPath(relPosix)) {
|
|
868
|
+
yield* withEmbeddingGenerationLockIter(path.dirname(abs), (content) => walk(abs, content), includeContent,
|
|
869
|
+
() => assertEmbeddingGenerationStillIncluded(root.abs, relPosix, options.excludeFile,
|
|
870
|
+
(rel) => shouldExcludePushRelPath(rel, includeTranscripts, options.userExcludeRegexps),
|
|
871
|
+
[]));
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
yield* walk(abs, includeContent);
|
|
883
875
|
continue;
|
|
884
876
|
}
|
|
885
877
|
if (!entry.isFile()) continue;
|
|
@@ -892,7 +884,7 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
892
884
|
root,
|
|
893
885
|
relPath: relPosix,
|
|
894
886
|
filePath: abs,
|
|
895
|
-
includeContent
|
|
887
|
+
includeContent,
|
|
896
888
|
readFile: options.readFile,
|
|
897
889
|
readFileDigest: options.readFileDigest,
|
|
898
890
|
signal: options.signal,
|
|
@@ -926,8 +918,23 @@ export async function buildOfflineSyncSnapshot(options: {
|
|
|
926
918
|
}): Promise<OfflineSyncSnapshot> {
|
|
927
919
|
throwIfOfflineSyncAborted(options.signal);
|
|
928
920
|
const includeTranscripts = options.includeTranscripts !== false;
|
|
921
|
+
const pushExcludes = options.excludeNodeLocalState !== false;
|
|
922
|
+
const normalizedDeletions = normalizeDeletionRevisions(options.deletions, "deletions");
|
|
923
|
+
const omission = pushExcludes
|
|
924
|
+
? await computeOmittedEmbeddingGenerationPaths({
|
|
925
|
+
rootAbs: path.resolve(options.root),
|
|
926
|
+
userExcludeRegexps: options.userExcludeRegexps,
|
|
927
|
+
excludeFile: options.excludeFile,
|
|
928
|
+
tombstonedPaths: (normalizedDeletions ?? []).map((deletion) => deletion.path),
|
|
929
|
+
isExcludedRelPath: (relPath) =>
|
|
930
|
+
shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps),
|
|
931
|
+
})
|
|
932
|
+
: undefined;
|
|
929
933
|
const files: OfflineSyncFileRecord[] = [];
|
|
930
|
-
for await (const file of iterateOfflineSyncSnapshotFileRecords(
|
|
934
|
+
for await (const file of iterateOfflineSyncSnapshotFileRecords({
|
|
935
|
+
...options,
|
|
936
|
+
skipEmbeddingGenerationPaths: omission?.omittedPaths,
|
|
937
|
+
})) files.push(file);
|
|
931
938
|
throwIfOfflineSyncAborted(options.signal);
|
|
932
939
|
|
|
933
940
|
const sortedFiles = files.sort(compareByPath);
|
|
@@ -937,6 +944,9 @@ export async function buildOfflineSyncSnapshot(options: {
|
|
|
937
944
|
includeTranscripts,
|
|
938
945
|
excludeNodeLocalState: options.excludeNodeLocalState,
|
|
939
946
|
userExcludeRegexps: options.userExcludeRegexps,
|
|
947
|
+
omittedGenerationDirs: omission && omission.omittedDirs.length > 0
|
|
948
|
+
? new Set(omission.omittedDirs)
|
|
949
|
+
: undefined,
|
|
940
950
|
});
|
|
941
951
|
return {
|
|
942
952
|
format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
@@ -946,6 +956,9 @@ export async function buildOfflineSyncSnapshot(options: {
|
|
|
946
956
|
includeTranscripts,
|
|
947
957
|
files: sortedFiles,
|
|
948
958
|
...(deletions === undefined ? {} : { deletions }),
|
|
959
|
+
...(omission && omission.omittedDirs.length > 0
|
|
960
|
+
? { omittedEmbeddingGenerationDirs: omission.omittedDirs }
|
|
961
|
+
: {}),
|
|
949
962
|
};
|
|
950
963
|
}
|
|
951
964
|
|
|
@@ -984,6 +997,18 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
984
997
|
const baseCapturedAtMs = rawBaseCapturedAtMs !== undefined && Number.isFinite(rawBaseCapturedAtMs)
|
|
985
998
|
? rawBaseCapturedAtMs
|
|
986
999
|
: null;
|
|
1000
|
+
const pushExcludes = options.excludeNodeLocalState !== false;
|
|
1001
|
+
const normalizedDeletions = normalizeDeletionRevisions(options.deletions, "deletions");
|
|
1002
|
+
const omission = pushExcludes
|
|
1003
|
+
? await computeOmittedEmbeddingGenerationPaths({
|
|
1004
|
+
rootAbs: root.abs,
|
|
1005
|
+
userExcludeRegexps: options.userExcludeRegexps,
|
|
1006
|
+
excludeFile: options.excludeFile,
|
|
1007
|
+
tombstonedPaths: (normalizedDeletions ?? []).map((deletion) => deletion.path),
|
|
1008
|
+
isExcludedRelPath: (relPath) =>
|
|
1009
|
+
shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps),
|
|
1010
|
+
})
|
|
1011
|
+
: undefined;
|
|
987
1012
|
const files: OfflineSyncFileRecord[] = [];
|
|
988
1013
|
|
|
989
1014
|
async function walk(dirAbs: string): Promise<void> {
|
|
@@ -999,8 +1024,18 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
999
1024
|
? shouldExcludeRelPath(relPosix, includeTranscripts)
|
|
1000
1025
|
: shouldExcludePushRelPath(relPosix, includeTranscripts, options.userExcludeRegexps)
|
|
1001
1026
|
) continue;
|
|
1027
|
+
if (omission?.omittedPaths.has(relPosix)) continue;
|
|
1002
1028
|
if (entry.isSymbolicLink()) continue;
|
|
1003
1029
|
if (entry.isDirectory()) {
|
|
1030
|
+
if (isEmbeddingGenerationDirPath(relPosix)) {
|
|
1031
|
+
await withEmbeddingGenerationLock(path.dirname(abs), async () => {
|
|
1032
|
+
await assertEmbeddingGenerationStillIncluded(root.abs, relPosix, options.excludeFile,
|
|
1033
|
+
(rel) => shouldExcludePushRelPath(rel, includeTranscripts, options.userExcludeRegexps),
|
|
1034
|
+
(normalizedDeletions ?? []).map((deletion) => deletion.path));
|
|
1035
|
+
await walk(abs);
|
|
1036
|
+
});
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1004
1039
|
await walk(abs);
|
|
1005
1040
|
continue;
|
|
1006
1041
|
}
|
|
@@ -1043,6 +1078,9 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
1043
1078
|
includeTranscripts,
|
|
1044
1079
|
excludeNodeLocalState: options.excludeNodeLocalState,
|
|
1045
1080
|
userExcludeRegexps: options.userExcludeRegexps,
|
|
1081
|
+
omittedGenerationDirs: omission && omission.omittedDirs.length > 0
|
|
1082
|
+
? new Set(omission.omittedDirs)
|
|
1083
|
+
: undefined,
|
|
1046
1084
|
});
|
|
1047
1085
|
return {
|
|
1048
1086
|
format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
@@ -1052,6 +1090,9 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
1052
1090
|
includeTranscripts,
|
|
1053
1091
|
files: sortedFiles,
|
|
1054
1092
|
...(deletions === undefined ? {} : { deletions }),
|
|
1093
|
+
...(omission && omission.omittedDirs.length > 0
|
|
1094
|
+
? { omittedEmbeddingGenerationDirs: omission.omittedDirs }
|
|
1095
|
+
: {}),
|
|
1055
1096
|
};
|
|
1056
1097
|
}
|
|
1057
1098
|
|
|
@@ -1491,10 +1532,21 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1491
1532
|
currentFiles?: readonly OfflineSyncFileState[];
|
|
1492
1533
|
deferredPaths?: readonly string[];
|
|
1493
1534
|
allowMissingConflictContent?: boolean;
|
|
1535
|
+
/**
|
|
1536
|
+
* Explicit INCOMING content source for snapshot records without inline
|
|
1537
|
+
* `contentBase64` (oversized generation members the client fetched
|
|
1538
|
+
* chunk-wise into a private staging root). Consulted only for such
|
|
1539
|
+
* records; return null to keep the metadata-only behavior (the local
|
|
1540
|
+
* file must hash-match the record). Returned buffers are digest-verified
|
|
1541
|
+
* against the incoming record before anything is published.
|
|
1542
|
+
*/
|
|
1543
|
+
readIncomingFile?: (target: { path: string; sha256: string; bytes: number }) => Promise<Buffer | null>;
|
|
1494
1544
|
writeConflictCopies?: boolean;
|
|
1495
1545
|
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
1496
1546
|
readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
|
|
1497
1547
|
writeFile?: (target: OfflineSyncFileWriteTarget) => Promise<void>;
|
|
1548
|
+
writeStagingFile?: (target: OfflineSyncFileStagingWriteTarget) => Promise<void>;
|
|
1549
|
+
readStagingFile?: (target: OfflineSyncFileStagingReadTarget) => Promise<Buffer>;
|
|
1498
1550
|
deleteFile?: (target: OfflineSyncFileDeleteTarget) => Promise<void>;
|
|
1499
1551
|
recordDeletionRevision?: OfflineSyncRecordDeletionRevision;
|
|
1500
1552
|
}): Promise<OfflineSyncApplySnapshotResult> {
|
|
@@ -1507,8 +1559,9 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1507
1559
|
const deletionMtimeByPath = snapshot.deletions === undefined
|
|
1508
1560
|
? undefined
|
|
1509
1561
|
: new Map(snapshot.deletions.map((deletion) => [deletion.path, deletion.mtimeMs] as const));
|
|
1510
|
-
const incomingBuffers = verifyRecordContents(snapshot.files, "offline sync snapshot", {
|
|
1562
|
+
const incomingBuffers = await verifyRecordContents(snapshot.files, "offline sync snapshot", {
|
|
1511
1563
|
requireContent: false,
|
|
1564
|
+
readIncomingFile: options.readIncomingFile,
|
|
1512
1565
|
});
|
|
1513
1566
|
const root = await ensureSyncRoot(options.root, "applyOfflineSyncSnapshot");
|
|
1514
1567
|
const currentFiles = options.currentFiles
|
|
@@ -1523,12 +1576,19 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1523
1576
|
excludeNodeLocalState: false,
|
|
1524
1577
|
})).files;
|
|
1525
1578
|
const currentMap = byPath(currentFiles);
|
|
1526
|
-
const deferredPaths = new Set(options.deferredPaths ?? [])
|
|
1579
|
+
const deferredPaths = new Set([...(options.deferredPaths ?? []),
|
|
1580
|
+
...divergedEmbeddingGenerationDeferrals({
|
|
1581
|
+
incomingFiles: snapshot.files, baseFiles: [...baseMap.values()], currentFiles,
|
|
1582
|
+
}),
|
|
1583
|
+
]);
|
|
1584
|
+
const omittedGenerationDirs = new Set(snapshot.omittedEmbeddingGenerationDirs ?? []);
|
|
1527
1585
|
if (deletionMtimeByPath && options.recordDeletionRevision) {
|
|
1528
1586
|
for (const [relPath, mtimeMs] of deletionMtimeByPath) {
|
|
1529
1587
|
if (
|
|
1530
1588
|
currentMap.has(relPath) ||
|
|
1531
1589
|
deferredPaths.has(relPath) ||
|
|
1590
|
+
(omittedGenerationDirs.size > 0 &&
|
|
1591
|
+
isPathInOmittedEmbeddingGeneration(relPath, omittedGenerationDirs)) ||
|
|
1532
1592
|
matchesOfflineSyncDefaultExclude(relPath)
|
|
1533
1593
|
) {
|
|
1534
1594
|
continue;
|
|
@@ -1554,6 +1614,183 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1554
1614
|
return requiredBuffer(incomingBuffers, relPath);
|
|
1555
1615
|
};
|
|
1556
1616
|
|
|
1617
|
+
// Embedding generations travel and replace as ONE unit (issue #3148).
|
|
1618
|
+
// Incoming shards form the generation; an incoming legacy marker is a
|
|
1619
|
+
// complete generation too (converted to the sharded layout, preserving the
|
|
1620
|
+
// one-way migration). A deferral anywhere in a generation defers the WHOLE
|
|
1621
|
+
// generation; a generation the push omitted by filter is announced by the
|
|
1622
|
+
// snapshot and its absence is never a delete instruction.
|
|
1623
|
+
const incomingGenerations = detectIncomingEmbeddingGenerations(incomingMap.keys());
|
|
1624
|
+
const generationDirs = new Set([
|
|
1625
|
+
...incomingGenerations.shardDirs,
|
|
1626
|
+
...incomingGenerations.legacyMarkerDirs,
|
|
1627
|
+
]);
|
|
1628
|
+
const deferredGenerationDirs = new Set<string>();
|
|
1629
|
+
for (const deferredPath of deferredPaths) {
|
|
1630
|
+
const membership = embeddingGenerationMembership(deferredPath);
|
|
1631
|
+
if (membership) {
|
|
1632
|
+
deferredGenerationDirs.add(membership.shardDir);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const transactionResults: EmbeddingGenerationTransactionResult[] = [];
|
|
1636
|
+
// Included and omitted cannot describe the same generation.
|
|
1637
|
+
for (const shardDir of generationDirs) {
|
|
1638
|
+
if (omittedGenerationDirs.has(shardDir)) {
|
|
1639
|
+
throw new EmbeddingIndexStorageError(
|
|
1640
|
+
`offline sync snapshot both includes and omits embedding generation ${shardDir}; refusing to apply`,
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
// Fail closed BEFORE any publication: custom storage IO (encrypted or
|
|
1645
|
+
// otherwise) without the staging pair would make the generation transaction
|
|
1646
|
+
// fall back to unencrypted raw writes — a plaintext downgrade of the staged
|
|
1647
|
+
// generation. Callers must wire both staging hooks (the CLI does).
|
|
1648
|
+
if (
|
|
1649
|
+
generationDirs.size > 0 &&
|
|
1650
|
+
(options.writeFile || options.readFile || options.readFileDigest || options.deleteFile) &&
|
|
1651
|
+
!(options.writeStagingFile && options.readStagingFile)
|
|
1652
|
+
) {
|
|
1653
|
+
throw new EmbeddingIndexStorageError(
|
|
1654
|
+
"refusing to replace embedding generation: custom storage IO requires both writeStagingFile and readStagingFile hooks; unencrypted raw staging would plaintext-downgrade the index",
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
for (const shardDir of generationDirs) {
|
|
1658
|
+
if (deferredGenerationDirs.has(shardDir)) continue;
|
|
1659
|
+
if (
|
|
1660
|
+
incomingGenerations.legacyMarkerDirs.has(shardDir) &&
|
|
1661
|
+
!incomingGenerations.shardDirs.has(shardDir)
|
|
1662
|
+
) {
|
|
1663
|
+
// Cross-layout: an incoming legacy marker replaces a local sharded
|
|
1664
|
+
// generation as a whole (codex round 4). When shards are ALSO
|
|
1665
|
+
// incoming, the sharded set is the generation and the marker is
|
|
1666
|
+
// written as an inert artifact by the per-file loop below.
|
|
1667
|
+
const markerRel = [...incomingMap.keys()].find(
|
|
1668
|
+
(relPath) => embeddingMarkerDirOf(relPath) === shardDir.slice(0, -`/${EMBEDDING_SHARD_DIR_BASENAME}`.length),
|
|
1669
|
+
);
|
|
1670
|
+
if (!markerRel) continue;
|
|
1671
|
+
transactionResults.push(await applyEmbeddingGenerationTransaction({
|
|
1672
|
+
root,
|
|
1673
|
+
shardDirRel: shardDir,
|
|
1674
|
+
incomingShardPaths: [],
|
|
1675
|
+
incomingMarker: {
|
|
1676
|
+
path: markerRel,
|
|
1677
|
+
sha256: incomingMap.get(markerRel)!.sha256,
|
|
1678
|
+
buffer: incomingBuffers.get(markerRel) ?? null,
|
|
1679
|
+
},
|
|
1680
|
+
incomingMarkerPresent: false,
|
|
1681
|
+
incomingShardStates: new Map(),
|
|
1682
|
+
incomingBuffers,
|
|
1683
|
+
baseStates: baseMap,
|
|
1684
|
+
io: {
|
|
1685
|
+
readFile: options.readFile,
|
|
1686
|
+
writeStagingFile: options.writeStagingFile,
|
|
1687
|
+
readStagingFile: options.readStagingFile,
|
|
1688
|
+
deleteFile: options.deleteFile,
|
|
1689
|
+
},
|
|
1690
|
+
now: Date.now(),
|
|
1691
|
+
}));
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
transactionResults.push(await applyEmbeddingGenerationTransaction({
|
|
1695
|
+
root,
|
|
1696
|
+
shardDirRel: shardDir,
|
|
1697
|
+
incomingShardPaths: [...incomingMap.keys()]
|
|
1698
|
+
.filter((relPath) => embeddingShardDirOf(relPath) === shardDir)
|
|
1699
|
+
.sort(),
|
|
1700
|
+
incomingMarker: null,
|
|
1701
|
+
incomingMarkerPresent: Boolean(
|
|
1702
|
+
incomingGenerations.legacyMarkerDirs.has(shardDir),
|
|
1703
|
+
),
|
|
1704
|
+
incomingShardStates: new Map(
|
|
1705
|
+
[...incomingMap.values()]
|
|
1706
|
+
.filter((relPath) => embeddingShardDirOf(relPath.path) === shardDir)
|
|
1707
|
+
.map((state) => [state.path, state]),
|
|
1708
|
+
),
|
|
1709
|
+
incomingBuffers,
|
|
1710
|
+
baseStates: baseMap,
|
|
1711
|
+
io: {
|
|
1712
|
+
readFile: options.readFile,
|
|
1713
|
+
writeStagingFile: options.writeStagingFile,
|
|
1714
|
+
readStagingFile: options.readStagingFile,
|
|
1715
|
+
deleteFile: options.deleteFile,
|
|
1716
|
+
},
|
|
1717
|
+
now: Date.now(),
|
|
1718
|
+
}));
|
|
1719
|
+
}
|
|
1720
|
+
// Whole-generation removal (codex round 6 P1): when the remote deleted a
|
|
1721
|
+
// previously shared sharded index, the deletion revisions cover EVERY
|
|
1722
|
+
// local member. Remove that generation as ONE atomic swap instead of
|
|
1723
|
+
// deleting shards one-by-one, so a crash or delete failure can never
|
|
1724
|
+
// expose a partial same-identity generation serving missing memories.
|
|
1725
|
+
const localGenerationDirs = new Set<string>();
|
|
1726
|
+
for (const relPath of currentMap.keys()) {
|
|
1727
|
+
const membership = embeddingGenerationMembership(relPath);
|
|
1728
|
+
if (membership?.kind === "shard") localGenerationDirs.add(membership.shardDir);
|
|
1729
|
+
}
|
|
1730
|
+
for (const shardDir of localGenerationDirs) {
|
|
1731
|
+
if (
|
|
1732
|
+
generationDirs.has(shardDir) ||
|
|
1733
|
+
deferredGenerationDirs.has(shardDir) ||
|
|
1734
|
+
omittedGenerationDirs.has(shardDir)
|
|
1735
|
+
) {
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
const localMembers = [...currentMap.keys()].filter(
|
|
1739
|
+
(relPath) => embeddingGenerationMembership(relPath)?.shardDir === shardDir,
|
|
1740
|
+
);
|
|
1741
|
+
if (localMembers.length === 0) continue;
|
|
1742
|
+
// Tombstone or shared-base absence authorizes whole-generation removal.
|
|
1743
|
+
const baseMembers = localMembers.filter((relPath) => baseMap.has(relPath));
|
|
1744
|
+
// Tombstone evidence is satisfied by the members the REMOTE knew: the
|
|
1745
|
+
// remote cannot tombstone local-only extras or an inert legacy marker,
|
|
1746
|
+
// and the removal sweeps those once the remote deletion is established.
|
|
1747
|
+
// Non-empty same-generation tombstones establish remote absence on
|
|
1748
|
+
// their own: they may name shards already absent locally (removed in an
|
|
1749
|
+
// earlier round), so the set is NOT required to intersect localMembers
|
|
1750
|
+
// (codex round 6, XiOb).
|
|
1751
|
+
const tombstonedMembers =
|
|
1752
|
+
deletionMtimeByPath === undefined
|
|
1753
|
+
? []
|
|
1754
|
+
: [...deletionMtimeByPath.keys()].filter(
|
|
1755
|
+
(relPath) => embeddingGenerationMembership(relPath)?.shardDir === shardDir,
|
|
1756
|
+
);
|
|
1757
|
+
const tombstoneCovered = tombstonedMembers.length > 0;
|
|
1758
|
+
const baseEvidenced =
|
|
1759
|
+
baseMembers.length > 0 && baseMembers.every((relPath) => !incomingMap.has(relPath));
|
|
1760
|
+
if (!tombstoneCovered && !baseEvidenced) continue;
|
|
1761
|
+
transactionResults.push(await applyEmbeddingGenerationRemoval({
|
|
1762
|
+
root,
|
|
1763
|
+
shardDirRel: shardDir,
|
|
1764
|
+
io: { deleteFile: options.deleteFile, readFile: options.readFile },
|
|
1765
|
+
deletionMtimeByPath,
|
|
1766
|
+
baseStates: baseMap,
|
|
1767
|
+
now: Date.now(),
|
|
1768
|
+
}));
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const transactionHandled = new Set<string>();
|
|
1772
|
+
let generationUpserted = 0;
|
|
1773
|
+
let generationDeleted = 0;
|
|
1774
|
+
for (const result of transactionResults) {
|
|
1775
|
+
if (result.deferredLocalDivergence) {
|
|
1776
|
+
for (const relPath of result.handledPaths) deferredPaths.add(relPath);
|
|
1777
|
+
deferredGenerationDirs.add(result.shardDirRel);
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
generationUpserted += result.upserted;
|
|
1781
|
+
generationDeleted += result.deleted;
|
|
1782
|
+
for (const relPath of result.handledPaths) transactionHandled.add(relPath);
|
|
1783
|
+
// Record converted shard paths directly; they may not be in any input map.
|
|
1784
|
+
for (const [relPath, state] of result.writtenStates) {
|
|
1785
|
+
nextBase.set(relPath, state);
|
|
1786
|
+
transactionHandled.add(relPath);
|
|
1787
|
+
}
|
|
1788
|
+
for (const relPath of result.removedPaths) {
|
|
1789
|
+
nextBase.delete(relPath);
|
|
1790
|
+
transactionHandled.add(relPath);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1557
1794
|
for (const relPath of unionPaths(baseMap, incomingMap, currentMap)) {
|
|
1558
1795
|
const base = baseMap.get(relPath);
|
|
1559
1796
|
const incoming = incomingMap.get(relPath);
|
|
@@ -1566,6 +1803,21 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1566
1803
|
continue;
|
|
1567
1804
|
}
|
|
1568
1805
|
|
|
1806
|
+
// The generation transaction already published or removed this path and
|
|
1807
|
+
// applied its nextBase bookkeeping above; nothing left to do.
|
|
1808
|
+
if (transactionHandled.has(relPath)) continue;
|
|
1809
|
+
|
|
1810
|
+
// A deferral anywhere in a generation defers the WHOLE generation: no
|
|
1811
|
+
// partial replacement in either direction, local files and base entries
|
|
1812
|
+
// stay exactly as they are.
|
|
1813
|
+
const membership = embeddingGenerationMembership(relPath);
|
|
1814
|
+
if (membership && deferredGenerationDirs.has(membership.shardDir)) {
|
|
1815
|
+
if (base) nextBase.set(relPath, base);
|
|
1816
|
+
else nextBase.delete(relPath);
|
|
1817
|
+
skipped += 1;
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1569
1821
|
if (incoming) {
|
|
1570
1822
|
if (currentEntry?.sha256 === incoming.sha256) {
|
|
1571
1823
|
if (await setSafeFileMtime(root, relPath, incoming.mtimeMs)) {
|
|
@@ -1578,7 +1830,14 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1578
1830
|
skipped += 1;
|
|
1579
1831
|
continue;
|
|
1580
1832
|
}
|
|
1581
|
-
|
|
1833
|
+
// Remote unchanged from the shared base while the local file drifted:
|
|
1834
|
+
// keep the local copy (no content is needed to make that decision).
|
|
1835
|
+
// Members of a replaced generation never reach this branch — the
|
|
1836
|
+
// generation transaction or the deferral guard handled them above.
|
|
1837
|
+
if (
|
|
1838
|
+
shouldPreferIncomingOfflineRuntimeFile(relPath) &&
|
|
1839
|
+
currentEntry && base && incoming.sha256 === base.sha256
|
|
1840
|
+
) {
|
|
1582
1841
|
nextBase.set(relPath, base);
|
|
1583
1842
|
skipped += 1;
|
|
1584
1843
|
continue;
|
|
@@ -1659,6 +1918,18 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1659
1918
|
skipped += 1;
|
|
1660
1919
|
continue;
|
|
1661
1920
|
}
|
|
1921
|
+
if (
|
|
1922
|
+
shouldPreferIncomingOfflineRuntimeFile(relPath) &&
|
|
1923
|
+
membership !== null && omittedGenerationDirs.has(membership.shardDir)
|
|
1924
|
+
) {
|
|
1925
|
+
// The push omitted this whole generation because a filter excluded at
|
|
1926
|
+
// least one member (snapshot.omittedEmbeddingGenerationDirs): absence
|
|
1927
|
+
// is NOT a deletion — keep the local generation and its base entries
|
|
1928
|
+
// until it is pushed unfiltered or deleted with deletion metadata.
|
|
1929
|
+
pendingLocal += 1;
|
|
1930
|
+
skipped += 1;
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1662
1933
|
if (
|
|
1663
1934
|
shouldPreferIncomingOfflineRuntimeFile(relPath) &&
|
|
1664
1935
|
(base || shouldDeleteAbsentIncomingOfflineRuntimeFile(relPath))
|
|
@@ -1704,8 +1975,8 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1704
1975
|
}
|
|
1705
1976
|
|
|
1706
1977
|
return {
|
|
1707
|
-
upserted,
|
|
1708
|
-
deleted,
|
|
1978
|
+
upserted: upserted + generationUpserted,
|
|
1979
|
+
deleted: deleted + generationDeleted,
|
|
1709
1980
|
skipped,
|
|
1710
1981
|
pendingLocal,
|
|
1711
1982
|
conflicts,
|
|
@@ -1740,7 +2011,7 @@ export async function applyOfflineSyncChangeset(options: {
|
|
|
1740
2011
|
const records = changeset.changes
|
|
1741
2012
|
.filter((change): change is Extract<OfflineSyncChange, { type: "upsert" }> => change.type === "upsert")
|
|
1742
2013
|
.map((change) => change.file);
|
|
1743
|
-
const incomingBuffers = verifyRecordContents(records, "offline sync changeset");
|
|
2014
|
+
const incomingBuffers = await verifyRecordContents(records, "offline sync changeset");
|
|
1744
2015
|
const currentFiles = options.currentFiles
|
|
1745
2016
|
? filterBaseFilesForMode(normalizeFileStates(options.currentFiles), changeset.includeTranscripts).sort(compareByPath)
|
|
1746
2017
|
: (await buildOfflineSyncSnapshotForPaths({
|
|
@@ -1859,14 +2130,37 @@ export async function applyOfflineSyncChangeset(options: {
|
|
|
1859
2130
|
};
|
|
1860
2131
|
}
|
|
1861
2132
|
|
|
1862
|
-
function verifyRecordContents(
|
|
2133
|
+
async function verifyRecordContents(
|
|
1863
2134
|
records: readonly OfflineSyncFileRecord[],
|
|
1864
2135
|
context: string,
|
|
1865
|
-
options: {
|
|
1866
|
-
|
|
2136
|
+
options: {
|
|
2137
|
+
requireContent?: boolean;
|
|
2138
|
+
readIncomingFile?: (target: { path: string; sha256: string; bytes: number }) => Promise<Buffer | null>;
|
|
2139
|
+
} = {},
|
|
2140
|
+
): Promise<Map<string, Buffer>> {
|
|
1867
2141
|
const buffers = new Map<string, Buffer>();
|
|
1868
2142
|
for (const record of records) {
|
|
1869
2143
|
if (typeof record.contentBase64 !== "string") {
|
|
2144
|
+
if (options.readIncomingFile) {
|
|
2145
|
+
// Explicit INCOMING content source for records whose bytes travel
|
|
2146
|
+
// out-of-band (chunked fetch into the client's private staging
|
|
2147
|
+
// root). Never the local current-state file: the buffer is
|
|
2148
|
+
// digest-verified against the incoming record before anything is
|
|
2149
|
+
// published. null keeps the metadata-only behavior below.
|
|
2150
|
+
const incoming = await options.readIncomingFile({
|
|
2151
|
+
path: record.path,
|
|
2152
|
+
sha256: record.sha256,
|
|
2153
|
+
bytes: record.bytes,
|
|
2154
|
+
});
|
|
2155
|
+
if (incoming) {
|
|
2156
|
+
const digest = sha256Buffer(incoming);
|
|
2157
|
+
if (digest.sha256 !== record.sha256 || digest.bytes !== record.bytes) {
|
|
2158
|
+
throw new Error(`${context}: incoming content checksum mismatch for ${record.path}`);
|
|
2159
|
+
}
|
|
2160
|
+
buffers.set(record.path, incoming);
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
1870
2164
|
if (options.requireContent === false) continue;
|
|
1871
2165
|
throw new Error(`${context}: contentBase64 is required for ${record.path}`);
|
|
1872
2166
|
}
|
|
@@ -1956,29 +2250,6 @@ async function writeSafeFile(
|
|
|
1956
2250
|
}
|
|
1957
2251
|
}
|
|
1958
2252
|
|
|
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
2253
|
|
|
1983
2254
|
/**
|
|
1984
2255
|
* Read the local file digest for a single relative path without applying
|
|
@@ -2232,272 +2503,6 @@ export async function applyOfflineSyncFileContentChunk(options: {
|
|
|
2232
2503
|
}
|
|
2233
2504
|
}
|
|
2234
2505
|
|
|
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
2506
|
|
|
2502
2507
|
async function deleteSafeFile(
|
|
2503
2508
|
root: SafeArchiveRoot,
|