@remnic/core 10.0.49 → 10.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +41 -40
- package/dist/access-authorization-probe.js +42 -41
- package/dist/access-boundary.js +41 -40
- package/dist/access-cli.js +107 -106
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +41 -40
- package/dist/access-http-briefing.d.ts +2 -0
- package/dist/access-http-briefing.d.ts.map +1 -1
- package/dist/access-http-briefing.js +42 -41
- package/dist/access-http-query.js +43 -42
- package/dist/access-http.d.ts.map +1 -1
- package/dist/access-http.js +59 -58
- package/dist/access-identity-continuity-surface.js +41 -40
- package/dist/access-lcm-surface.js +41 -40
- package/dist/access-mcp.js +51 -50
- package/dist/access-namespace-preflight.js +41 -40
- package/dist/access-observe-write-category.d.ts +1 -1
- package/dist/access-observe-write-category.d.ts.map +1 -1
- package/dist/access-observe-write-category.js +10 -10
- package/dist/access-observe-write-surface.d.ts.map +1 -1
- package/dist/access-observe-write-surface.js +41 -40
- package/dist/access-offline-manifest.js +4 -4
- package/dist/access-operations-batch.js +42 -41
- package/dist/access-operations.js +49 -48
- package/dist/access-recall-concurrency.js +41 -40
- package/dist/access-recall-response.js +41 -40
- package/dist/access-recall-surface.js +41 -40
- package/dist/access-schema.js +6 -6
- package/dist/access-service.d.ts +5 -0
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +41 -40
- package/dist/access-surface-catalog.js +43 -42
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +14 -14
- package/dist/{auto-sync-BNCBMU6H.js → auto-sync-UQQ6YJSB.js} +3 -3
- package/dist/briefing-deadline.d.ts +7 -0
- package/dist/briefing-deadline.d.ts.map +1 -0
- package/dist/briefing-deadline.js +13 -0
- package/dist/briefing-window.d.ts +12 -4
- package/dist/briefing-window.d.ts.map +1 -1
- package/dist/briefing-window.js +3 -1
- package/dist/briefing.d.ts +2 -0
- package/dist/briefing.d.ts.map +1 -1
- package/dist/briefing.js +12 -11
- package/dist/buffer.js +3 -3
- package/dist/calibration.js +2 -2
- package/dist/{capsule-merge-RNQUP2VL.js → capsule-merge-D4BTIJGK.js} +3 -3
- package/dist/causal-consolidation.js +11 -11
- package/dist/{chunk-ESDOQEJM.js → chunk-2AKIVSHP.js} +2 -2
- package/dist/{chunk-MCS4LIXP.js → chunk-43TUWQNR.js} +2 -2
- package/dist/{chunk-2NLCDNGA.js → chunk-564YP3DW.js} +2 -2
- package/dist/{chunk-PBWJVRKE.js → chunk-5FUDSK6Z.js} +2 -2
- package/dist/{chunk-V4GPFCJ7.js → chunk-5G53JSRO.js} +2 -2
- package/dist/{chunk-WJCAAVZX.js → chunk-5X2H3X4J.js} +2 -2
- package/dist/{chunk-CPBYKOTC.js → chunk-6ZQX3R66.js} +2 -2
- package/dist/{chunk-AF4UIHON.js → chunk-74AY3S7U.js} +9 -9
- package/dist/{chunk-XSJL62LE.js → chunk-7J6A5MV2.js} +2 -2
- package/dist/{chunk-A3WXV7YU.js → chunk-7SAE6T36.js} +2 -2
- package/dist/{chunk-BWF3KNSQ.js → chunk-7YVER7ZO.js} +2 -2
- package/dist/{chunk-5IDQVE4U.js → chunk-A3UFV4K3.js} +6 -6
- package/dist/{chunk-LWRIGKFM.js → chunk-A5L5KGDI.js} +19 -1
- package/dist/chunk-A5L5KGDI.js.map +1 -0
- package/dist/{chunk-6TIVVOVU.js → chunk-BNJAVRNJ.js} +9 -9
- package/dist/{chunk-CMOYD5CA.js → chunk-CCL2OXFO.js} +2 -2
- package/dist/{chunk-7XFQSQYS.js → chunk-CRN3EPE2.js} +4 -3
- package/dist/chunk-CRN3EPE2.js.map +1 -0
- package/dist/{chunk-3NYIXVCT.js → chunk-DMKTOCC7.js} +2 -2
- package/dist/chunk-DMKTOCC7.js.map +1 -0
- package/dist/{chunk-FRXS6SC7.js → chunk-EKUI7I2X.js} +20 -4
- package/dist/chunk-EKUI7I2X.js.map +1 -0
- package/dist/{chunk-N6EM7ESC.js → chunk-EKYWZYZJ.js} +22 -14
- package/dist/chunk-EKYWZYZJ.js.map +1 -0
- package/dist/{chunk-FOKTW6GY.js → chunk-EWBXMGXR.js} +2 -2
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-6W3QH3QW.js → chunk-GQIYL7OD.js} +388 -292
- package/dist/chunk-GQIYL7OD.js.map +1 -0
- package/dist/{chunk-E47UQBYW.js → chunk-GXVXBYRA.js} +3 -3
- package/dist/{chunk-RZDDNH63.js → chunk-HKOMHP6D.js} +27 -15
- package/dist/chunk-HKOMHP6D.js.map +1 -0
- package/dist/{chunk-AGCEJDG2.js → chunk-HPNXS6W6.js} +2 -2
- package/dist/{chunk-O4CA53UY.js → chunk-IMKOJVD4.js} +69 -63
- package/dist/chunk-IMKOJVD4.js.map +1 -0
- package/dist/{chunk-N5PISU26.js → chunk-IN6U2BS5.js} +2 -2
- package/dist/{chunk-HI53EK3Y.js → chunk-IUYXCTA7.js} +2 -2
- package/dist/{chunk-VGRBKEBH.js → chunk-JBWD3J5U.js} +3 -3
- package/dist/{chunk-YB3WN6XA.js → chunk-K5QQXVG6.js} +12 -12
- package/dist/{chunk-7LCGFUMB.js → chunk-KQAFEZQX.js} +4 -4
- package/dist/{chunk-HDT3DMUF.js → chunk-KW5YZELC.js} +2 -2
- package/dist/{chunk-DM6SOROF.js → chunk-LIBVANOF.js} +2 -2
- package/dist/{chunk-VKUMKVEU.js → chunk-LMMBV7QC.js} +4 -4
- package/dist/{chunk-NMOEOQV4.js → chunk-M2DOJBH7.js} +2 -2
- package/dist/{chunk-3INLTKTW.js → chunk-M4HRYLOA.js} +2 -2
- package/dist/{chunk-YY6PWFUC.js → chunk-M6TP6UYD.js} +3 -3
- package/dist/{chunk-BJTFLP3J.js → chunk-MLNXATFG.js} +2 -2
- package/dist/{chunk-C7WDWD35.js → chunk-MWURUSRE.js} +5 -5
- package/dist/{chunk-WIBWONL4.js → chunk-N74WJ7B2.js} +2 -2
- package/dist/{chunk-LCHFINL4.js → chunk-N7FNCE7F.js} +2 -2
- package/dist/{chunk-QZK6HG4J.js → chunk-NPLM7AFU.js} +2 -2
- package/dist/{chunk-CRJX4OMN.js → chunk-NSP5PIQH.js} +2 -2
- package/dist/{chunk-X52SXKVC.js → chunk-P7OVFGMU.js} +2 -2
- package/dist/{chunk-2J6BIOVN.js → chunk-PJCVI44H.js} +2 -2
- package/dist/{chunk-Q6PYCPHO.js → chunk-Q3PNQYAC.js} +2 -2
- package/dist/{chunk-D76AHITY.js → chunk-QS765SBL.js} +2 -2
- package/dist/{chunk-KNCR7APE.js → chunk-QTDZ2NYC.js} +7 -7
- package/dist/{chunk-YFXRLRKP.js → chunk-S4OZKPBS.js} +7 -11
- package/dist/{chunk-YFXRLRKP.js.map → chunk-S4OZKPBS.js.map} +1 -1
- package/dist/{chunk-HCPVL6SF.js → chunk-SJTSWG2Z.js} +3 -3
- package/dist/{chunk-SA6P2KS3.js → chunk-TEH5MDX5.js} +33 -17
- package/dist/chunk-TEH5MDX5.js.map +1 -0
- package/dist/{chunk-XFND63OB.js → chunk-TFWJXWL6.js} +2 -2
- package/dist/{chunk-SVJ5JVZO.js → chunk-TXKQBQYW.js} +3 -3
- package/dist/{chunk-WQFHHJWK.js → chunk-UALXWWGN.js} +2 -2
- package/dist/{chunk-3PVKV3XL.js → chunk-UNOAVHV4.js} +2 -2
- package/dist/{chunk-FYZXZUGU.js → chunk-UQBOPCDK.js} +2 -2
- package/dist/{chunk-SUKPM6DV.js → chunk-UWMZT2TY.js} +47 -4
- package/dist/chunk-UWMZT2TY.js.map +1 -0
- package/dist/{chunk-OD7ZXCUF.js → chunk-UXGXJUZ5.js} +7 -7
- package/dist/{chunk-NH37SP6V.js → chunk-UZVQFGAY.js} +81 -78
- package/dist/chunk-UZVQFGAY.js.map +1 -0
- package/dist/{chunk-2BY56HE6.js → chunk-VCOMACR6.js} +35 -35
- package/dist/{chunk-5FNZ3G6X.js → chunk-VKQI47DJ.js} +4 -4
- package/dist/{chunk-MPJDQC4G.js → chunk-WJCXUE2J.js} +2 -2
- package/dist/{chunk-C3HRYVGD.js → chunk-WV4UYDP6.js} +2 -2
- package/dist/{chunk-E3DL3CAL.js → chunk-WXJC2L66.js} +2 -2
- package/dist/chunk-X2KO4FK5.js +40 -0
- package/dist/chunk-X2KO4FK5.js.map +1 -0
- package/dist/{chunk-O7GNSHFF.js → chunk-XC3H7KON.js} +2 -2
- package/dist/{chunk-OOOX5C4X.js → chunk-XL2EE5W3.js} +4 -4
- package/dist/{chunk-NUK5XU3P.js → chunk-XTXWR6KQ.js} +25 -10
- package/dist/chunk-XTXWR6KQ.js.map +1 -0
- package/dist/{chunk-CZVNOTLR.js → chunk-XXR4A33N.js} +2 -2
- package/dist/{chunk-2AV2U2SZ.js → chunk-YDIOMDBW.js} +2 -2
- package/dist/{chunk-QFTNWDJ6.js → chunk-YECEEJUU.js} +2 -2
- package/dist/{chunk-BMG3SAXA.js → chunk-Z5277JWF.js} +3 -3
- package/dist/{chunk-SW3WHVKN.js → chunk-ZFNXJEUG.js} +2 -2
- package/dist/{chunk-VY6BD5KB.js → chunk-ZMO2S5RT.js} +12 -10
- package/dist/chunk-ZMO2S5RT.js.map +1 -0
- package/dist/{chunk-KMTMMBEB.js → chunk-ZNRTHYYW.js} +6 -6
- package/dist/{chunk-VCBS5GRQ.js → chunk-ZSMA67FV.js} +2 -2
- package/dist/cli.js +80 -79
- package/dist/coding/export-okf-codegraph.js +9 -9
- package/dist/compounding/engine.js +11 -11
- package/dist/config.js +14 -14
- package/dist/connectors/codex-materialize-runner.js +10 -10
- package/dist/connectors/index.js +10 -10
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.js +2 -2
- package/dist/convergence-refresh.js +12 -12
- package/dist/corpus-watermark.js +12 -12
- package/dist/correction/passive-correction-notifications.d.ts +1 -1
- package/dist/correction/passive-correction-notifications.d.ts.map +1 -1
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-http-glue.js +7 -7
- package/dist/{entity-canonical-id-migration-2YOLMOFC.js → entity-canonical-id-migration-ZBNRGUNV.js} +3 -3
- package/dist/entity-canonical-id-migration-ZBNRGUNV.js.map +1 -0
- package/dist/entity-retrieval.js +11 -11
- package/dist/explicit-capture.js +2 -2
- package/dist/external-wiki-access.js +42 -41
- package/dist/extraction.js +16 -16
- package/dist/{first-start-migration-QVD22WWS.js → first-start-migration-A6PHIBUQ.js} +4 -4
- package/dist/harmonic-retrieval.js +10 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +130 -122
- 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 +15 -15
- package/dist/location/tagging.d.ts +7 -5
- package/dist/location/tagging.d.ts.map +1 -1
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
- package/dist/maintenance/rebuild-memory-projection-runner.js +12 -12
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/memory-browse-operations.js +42 -41
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/storage.js +10 -10
- package/dist/offline-sync-file-io.d.ts +15 -0
- package/dist/offline-sync-file-io.d.ts.map +1 -1
- package/dist/offline-sync-file-io.js +9 -1
- package/dist/offline-sync-snapshot-stream.js +4 -4
- package/dist/offline-sync-upload-staging.js +2 -2
- package/dist/offline-sync.d.ts.map +1 -1
- package/dist/offline-sync.js +3 -3
- package/dist/operator-doctor-corpus.js +13 -13
- package/dist/operator-toolkit.js +24 -24
- package/dist/orchestration/maintenance.js +23 -23
- package/dist/orchestrator.d.ts +1 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +109 -108
- package/dist/page-versioning.js +2 -2
- package/dist/recall-navigation-operations.js +42 -41
- package/dist/recall-navigation.js +41 -40
- package/dist/resume-bundles.js +15 -15
- package/dist/semantic-consolidation.js +15 -15
- package/dist/semantic-rule-promotion.js +10 -10
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/source-agent-qualifier.js +13 -13
- package/dist/standup/build.js +10 -10
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts +1 -0
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts.map +1 -1
- package/dist/storage/entity-canonical-id-migration.d.ts +7 -0
- package/dist/storage/entity-canonical-id-migration.d.ts.map +1 -1
- package/dist/storage/entity-canonical-id-references.d.ts +2 -0
- package/dist/storage/entity-canonical-id-references.d.ts.map +1 -1
- package/dist/storage/entity-migration-completion-cache.d.ts +46 -0
- package/dist/storage/entity-migration-completion-cache.d.ts.map +1 -0
- package/dist/storage/entity-store.d.ts +1 -8
- package/dist/storage/entity-store.d.ts.map +1 -1
- package/dist/storage/memory-read-store.d.ts +3 -2
- package/dist/storage/memory-read-store.d.ts.map +1 -1
- package/dist/storage/windowed-memory-read.d.ts +6 -0
- package/dist/storage/windowed-memory-read.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +9 -9
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +44 -43
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +4 -4
- package/dist/transfer/export-okf.js +10 -10
- package/dist/verified-recall.js +10 -10
- package/dist/who-knows-cli.js +11 -11
- package/dist/who-knows.js +10 -10
- package/package.json +2 -2
- package/src/access-http-briefing.ts +4 -0
- package/src/access-http.test.ts +40 -11
- package/src/access-http.ts +6 -0
- package/src/access-observe-write-category.ts +1 -1
- package/src/access-observe-write-surface.ts +24 -19
- package/src/access-operations-batch.ts +18 -2
- package/src/access-service.ts +5 -0
- package/src/briefing-abort.test.ts +173 -0
- package/src/briefing-deadline.test.ts +84 -0
- package/src/briefing-deadline.ts +33 -0
- package/src/briefing-window-abort.test.ts +47 -0
- package/src/briefing-window.ts +33 -4
- package/src/briefing.ts +23 -16
- package/src/correction/passive-correction-notifications-abort.test.ts +42 -0
- package/src/correction/passive-correction-notifications.ts +4 -0
- package/src/entity-lazy-compatibility.test.ts +229 -0
- package/src/entity-migration-completion-notification.test.ts +148 -0
- package/src/index.ts +1 -1
- package/src/location/tagging.ts +14 -12
- package/src/offline-sync-file-io.ts +61 -2
- package/src/offline-sync.test.ts +313 -20
- package/src/offline-sync.ts +43 -21
- package/src/orchestrator-init.test.ts +49 -1
- package/src/orchestrator.ts +9 -8
- package/src/storage/entity-canonical-id-migration-adapter.ts +16 -1
- package/src/storage/entity-canonical-id-migration.ts +17 -8
- package/src/storage/entity-canonical-id-references.ts +24 -0
- package/src/storage/entity-migration-completion-cache.nofollow.test.ts +223 -0
- package/src/storage/entity-migration-completion-cache.ts +153 -0
- package/src/storage/entity-store.ts +58 -86
- package/src/storage/memory-read-store.ts +9 -3
- package/src/storage/windowed-memory-read.ts +8 -0
- package/src/storage-startup-nonblocking-migration.test.ts +183 -0
- package/src/storage.ts +7 -2
- package/src/testing/subjects/entity-migration-completion-cache.test.ts +90 -0
- package/dist/chunk-3NYIXVCT.js.map +0 -1
- package/dist/chunk-6W3QH3QW.js.map +0 -1
- package/dist/chunk-7XFQSQYS.js.map +0 -1
- package/dist/chunk-FRXS6SC7.js.map +0 -1
- package/dist/chunk-LWRIGKFM.js.map +0 -1
- package/dist/chunk-N6EM7ESC.js.map +0 -1
- package/dist/chunk-NH37SP6V.js.map +0 -1
- package/dist/chunk-NUK5XU3P.js.map +0 -1
- package/dist/chunk-O4CA53UY.js.map +0 -1
- package/dist/chunk-RZDDNH63.js.map +0 -1
- package/dist/chunk-SA6P2KS3.js.map +0 -1
- package/dist/chunk-SUKPM6DV.js.map +0 -1
- package/dist/chunk-VY6BD5KB.js.map +0 -1
- /package/dist/{auto-sync-BNCBMU6H.js.map → auto-sync-UQQ6YJSB.js.map} +0 -0
- /package/dist/{entity-canonical-id-migration-2YOLMOFC.js.map → briefing-deadline.js.map} +0 -0
- /package/dist/{capsule-merge-RNQUP2VL.js.map → capsule-merge-D4BTIJGK.js.map} +0 -0
- /package/dist/{chunk-ESDOQEJM.js.map → chunk-2AKIVSHP.js.map} +0 -0
- /package/dist/{chunk-MCS4LIXP.js.map → chunk-43TUWQNR.js.map} +0 -0
- /package/dist/{chunk-2NLCDNGA.js.map → chunk-564YP3DW.js.map} +0 -0
- /package/dist/{chunk-PBWJVRKE.js.map → chunk-5FUDSK6Z.js.map} +0 -0
- /package/dist/{chunk-V4GPFCJ7.js.map → chunk-5G53JSRO.js.map} +0 -0
- /package/dist/{chunk-WJCAAVZX.js.map → chunk-5X2H3X4J.js.map} +0 -0
- /package/dist/{chunk-CPBYKOTC.js.map → chunk-6ZQX3R66.js.map} +0 -0
- /package/dist/{chunk-AF4UIHON.js.map → chunk-74AY3S7U.js.map} +0 -0
- /package/dist/{chunk-XSJL62LE.js.map → chunk-7J6A5MV2.js.map} +0 -0
- /package/dist/{chunk-A3WXV7YU.js.map → chunk-7SAE6T36.js.map} +0 -0
- /package/dist/{chunk-BWF3KNSQ.js.map → chunk-7YVER7ZO.js.map} +0 -0
- /package/dist/{chunk-5IDQVE4U.js.map → chunk-A3UFV4K3.js.map} +0 -0
- /package/dist/{chunk-6TIVVOVU.js.map → chunk-BNJAVRNJ.js.map} +0 -0
- /package/dist/{chunk-CMOYD5CA.js.map → chunk-CCL2OXFO.js.map} +0 -0
- /package/dist/{chunk-FOKTW6GY.js.map → chunk-EWBXMGXR.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-E47UQBYW.js.map → chunk-GXVXBYRA.js.map} +0 -0
- /package/dist/{chunk-AGCEJDG2.js.map → chunk-HPNXS6W6.js.map} +0 -0
- /package/dist/{chunk-N5PISU26.js.map → chunk-IN6U2BS5.js.map} +0 -0
- /package/dist/{chunk-HI53EK3Y.js.map → chunk-IUYXCTA7.js.map} +0 -0
- /package/dist/{chunk-VGRBKEBH.js.map → chunk-JBWD3J5U.js.map} +0 -0
- /package/dist/{chunk-YB3WN6XA.js.map → chunk-K5QQXVG6.js.map} +0 -0
- /package/dist/{chunk-7LCGFUMB.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-HDT3DMUF.js.map → chunk-KW5YZELC.js.map} +0 -0
- /package/dist/{chunk-DM6SOROF.js.map → chunk-LIBVANOF.js.map} +0 -0
- /package/dist/{chunk-VKUMKVEU.js.map → chunk-LMMBV7QC.js.map} +0 -0
- /package/dist/{chunk-NMOEOQV4.js.map → chunk-M2DOJBH7.js.map} +0 -0
- /package/dist/{chunk-3INLTKTW.js.map → chunk-M4HRYLOA.js.map} +0 -0
- /package/dist/{chunk-YY6PWFUC.js.map → chunk-M6TP6UYD.js.map} +0 -0
- /package/dist/{chunk-BJTFLP3J.js.map → chunk-MLNXATFG.js.map} +0 -0
- /package/dist/{chunk-C7WDWD35.js.map → chunk-MWURUSRE.js.map} +0 -0
- /package/dist/{chunk-WIBWONL4.js.map → chunk-N74WJ7B2.js.map} +0 -0
- /package/dist/{chunk-LCHFINL4.js.map → chunk-N7FNCE7F.js.map} +0 -0
- /package/dist/{chunk-QZK6HG4J.js.map → chunk-NPLM7AFU.js.map} +0 -0
- /package/dist/{chunk-CRJX4OMN.js.map → chunk-NSP5PIQH.js.map} +0 -0
- /package/dist/{chunk-X52SXKVC.js.map → chunk-P7OVFGMU.js.map} +0 -0
- /package/dist/{chunk-2J6BIOVN.js.map → chunk-PJCVI44H.js.map} +0 -0
- /package/dist/{chunk-Q6PYCPHO.js.map → chunk-Q3PNQYAC.js.map} +0 -0
- /package/dist/{chunk-D76AHITY.js.map → chunk-QS765SBL.js.map} +0 -0
- /package/dist/{chunk-KNCR7APE.js.map → chunk-QTDZ2NYC.js.map} +0 -0
- /package/dist/{chunk-HCPVL6SF.js.map → chunk-SJTSWG2Z.js.map} +0 -0
- /package/dist/{chunk-XFND63OB.js.map → chunk-TFWJXWL6.js.map} +0 -0
- /package/dist/{chunk-SVJ5JVZO.js.map → chunk-TXKQBQYW.js.map} +0 -0
- /package/dist/{chunk-WQFHHJWK.js.map → chunk-UALXWWGN.js.map} +0 -0
- /package/dist/{chunk-3PVKV3XL.js.map → chunk-UNOAVHV4.js.map} +0 -0
- /package/dist/{chunk-FYZXZUGU.js.map → chunk-UQBOPCDK.js.map} +0 -0
- /package/dist/{chunk-OD7ZXCUF.js.map → chunk-UXGXJUZ5.js.map} +0 -0
- /package/dist/{chunk-2BY56HE6.js.map → chunk-VCOMACR6.js.map} +0 -0
- /package/dist/{chunk-5FNZ3G6X.js.map → chunk-VKQI47DJ.js.map} +0 -0
- /package/dist/{chunk-MPJDQC4G.js.map → chunk-WJCXUE2J.js.map} +0 -0
- /package/dist/{chunk-C3HRYVGD.js.map → chunk-WV4UYDP6.js.map} +0 -0
- /package/dist/{chunk-E3DL3CAL.js.map → chunk-WXJC2L66.js.map} +0 -0
- /package/dist/{chunk-O7GNSHFF.js.map → chunk-XC3H7KON.js.map} +0 -0
- /package/dist/{chunk-OOOX5C4X.js.map → chunk-XL2EE5W3.js.map} +0 -0
- /package/dist/{chunk-CZVNOTLR.js.map → chunk-XXR4A33N.js.map} +0 -0
- /package/dist/{chunk-2AV2U2SZ.js.map → chunk-YDIOMDBW.js.map} +0 -0
- /package/dist/{chunk-QFTNWDJ6.js.map → chunk-YECEEJUU.js.map} +0 -0
- /package/dist/{chunk-BMG3SAXA.js.map → chunk-Z5277JWF.js.map} +0 -0
- /package/dist/{chunk-SW3WHVKN.js.map → chunk-ZFNXJEUG.js.map} +0 -0
- /package/dist/{chunk-KMTMMBEB.js.map → chunk-ZNRTHYYW.js.map} +0 -0
- /package/dist/{chunk-VCBS5GRQ.js.map → chunk-ZSMA67FV.js.map} +0 -0
- /package/dist/{first-start-migration-QVD22WWS.js.map → first-start-migration-A6PHIBUQ.js.map} +0 -0
|
@@ -77,9 +77,16 @@ export async function plainFileDigest(filePath: string): Promise<string> {
|
|
|
77
77
|
return hash.digest("hex");
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
import { lstat, utimes } from "node:fs/promises";
|
|
80
|
+
import { lstat, realpath, utimes } from "node:fs/promises";
|
|
81
81
|
import { isCensusMtimeMs } from "./census-validation.js";
|
|
82
|
-
import
|
|
82
|
+
import path from "node:path";
|
|
83
|
+
import {
|
|
84
|
+
fromPosixRelPath,
|
|
85
|
+
isPathInsideRoot,
|
|
86
|
+
resolveSafeArchiveTarget,
|
|
87
|
+
type SafeArchiveRoot,
|
|
88
|
+
validateArchiveRelativePath,
|
|
89
|
+
} from "./transfer/fs-utils.js";
|
|
83
90
|
|
|
84
91
|
/** Sync-internal directory spooling partial uploads and coordination state. */
|
|
85
92
|
export const SYNC_INTERNAL_DIR = ".offline-sync";
|
|
@@ -124,3 +131,55 @@ export async function setSafeFileMtime(
|
|
|
124
131
|
await utimes(target, mtime, mtime);
|
|
125
132
|
return true;
|
|
126
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* True when a regular (non-symlink) file exists at the archive-relative path.
|
|
137
|
+
* A symlink is not a regular local file and reads as absent, so callers treat
|
|
138
|
+
* it like any other missing path instead of failing the whole push.
|
|
139
|
+
*/
|
|
140
|
+
export async function offlineSyncRegularFilePresent(
|
|
141
|
+
root: SafeArchiveRoot,
|
|
142
|
+
relPath: string,
|
|
143
|
+
): Promise<boolean> {
|
|
144
|
+
const target = path.resolve(root.abs, fromPosixRelPath(validateArchiveRelativePath(relPath, root.errorPrefix)));
|
|
145
|
+
if (!isPathInsideRoot(root.abs, target)) {
|
|
146
|
+
throw new Error(`${root.errorPrefix}: record path escapes target root: ${relPath}`);
|
|
147
|
+
}
|
|
148
|
+
const st = await lstat(target).catch((error: NodeJS.ErrnoException) => {
|
|
149
|
+
// SAFETY: Node fs errors carry `code`; only ENOENT means absent, all other
|
|
150
|
+
// errno codes must propagate so census uncertainty fails closed.
|
|
151
|
+
if (error.code === "ENOENT") return null;
|
|
152
|
+
throw error;
|
|
153
|
+
});
|
|
154
|
+
if (!st || st.isSymbolicLink() || !st.isFile()) return false;
|
|
155
|
+
// A regular file still has to resolve inside the root (a symlinked parent directory escapes it).
|
|
156
|
+
await resolveSafeArchiveTarget(root, relPath);
|
|
157
|
+
// The census skips a symlinked directory, so a file reached only through one is absent here too.
|
|
158
|
+
return (await realpath(target)) === path.join(root.real, path.relative(root.abs, target));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A path the census observed vanished before its digest was read: a retryable race, never a confirmed delete. */
|
|
162
|
+
export class OfflineSyncCensusRaceError extends Error {
|
|
163
|
+
constructor(readonly relPath: string, cause?: unknown) {
|
|
164
|
+
super(
|
|
165
|
+
`offline sync census race: ${relPath} vanished during digest; retry the snapshot`,
|
|
166
|
+
cause === undefined ? undefined : { cause },
|
|
167
|
+
);
|
|
168
|
+
this.name = "OfflineSyncCensusRaceError";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The message the CLI push retry loop matches: it defers the path and rebuilds the census. */
|
|
173
|
+
export function changedWhileBuildingChangesetError(relPath: string, cause?: unknown): Error {
|
|
174
|
+
return new Error(
|
|
175
|
+
`offline sync file changed while building changeset: ${relPath}`,
|
|
176
|
+
cause === undefined ? undefined : { cause },
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A `.catch` handler: a census race while building a changeset upsert stays retryable. */
|
|
181
|
+
export function censusRaceAsChangedWhileBuilding(relPath: string): (error: Error) => never {
|
|
182
|
+
return (error) => {
|
|
183
|
+
throw error instanceof OfflineSyncCensusRaceError ? changedWhileBuildingChangesetError(relPath, error) : error;
|
|
184
|
+
};
|
|
185
|
+
}
|
package/src/offline-sync.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
3
|
+
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, symlink, utimes, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -3714,33 +3714,37 @@ test("applyOfflineSyncSnapshot consumes readIncomingFile for records without inl
|
|
|
3714
3714
|
}
|
|
3715
3715
|
});
|
|
3716
3716
|
|
|
3717
|
-
test("buildOfflineSyncSnapshot
|
|
3717
|
+
test("buildOfflineSyncSnapshot fails closed when a file vanishes during digest", async () => {
|
|
3718
3718
|
const root = await tempDir("remnic-offline-vanish");
|
|
3719
3719
|
try {
|
|
3720
3720
|
await write(root, "facts/keep.md", "keep");
|
|
3721
3721
|
await write(root, "facts/gone.md", "gone");
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3722
|
+
await assert.rejects(
|
|
3723
|
+
() => buildOfflineSyncSnapshot({
|
|
3724
|
+
root,
|
|
3725
|
+
sourceId: "local",
|
|
3726
|
+
includeContent: false,
|
|
3727
|
+
readFileDigest: async ({ path: relPath, filePath }) => {
|
|
3728
|
+
if (relPath.endsWith("gone.md")) {
|
|
3729
|
+
await rm(filePath);
|
|
3730
|
+
throw Object.assign(new Error("ENOENT: vanished"), { code: "ENOENT" });
|
|
3731
|
+
}
|
|
3732
|
+
const content = await readFile(filePath);
|
|
3733
|
+
return {
|
|
3734
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
3735
|
+
bytes: content.length,
|
|
3736
|
+
};
|
|
3737
|
+
},
|
|
3738
|
+
}),
|
|
3739
|
+
/census race|vanished during digest|retry the snapshot/i,
|
|
3740
|
+
);
|
|
3741
|
+
assert.equal(await readUtf8(root, "facts/keep.md"), "keep");
|
|
3739
3742
|
} finally {
|
|
3740
3743
|
await rm(root, { recursive: true, force: true });
|
|
3741
3744
|
}
|
|
3742
3745
|
});
|
|
3743
3746
|
|
|
3747
|
+
|
|
3744
3748
|
test("base snapshot fails when a base-known file vanishes before the fast-base stat", async () => {
|
|
3745
3749
|
const root = await tempDir("remnic-offline-base-vanish");
|
|
3746
3750
|
try {
|
|
@@ -3770,7 +3774,7 @@ test("base snapshot fails when a base-known file vanishes before the fast-base s
|
|
|
3770
3774
|
}
|
|
3771
3775
|
});
|
|
3772
3776
|
|
|
3773
|
-
test("base snapshot omits a brand-new file that vanishes
|
|
3777
|
+
test("base snapshot omits a brand-new file that vanishes before the fast-base stat", async () => {
|
|
3774
3778
|
const root = await tempDir("remnic-offline-new-vanish");
|
|
3775
3779
|
try {
|
|
3776
3780
|
await write(root, "facts/keep.md", "keep");
|
|
@@ -3798,6 +3802,7 @@ test("base snapshot omits a brand-new file that vanishes during the walk", async
|
|
|
3798
3802
|
}
|
|
3799
3803
|
});
|
|
3800
3804
|
|
|
3805
|
+
|
|
3801
3806
|
test("snapshot fails when an embedding generation member vanishes during the walk", async () => {
|
|
3802
3807
|
const root = await tempDir("remnic-offline-shard-vanish");
|
|
3803
3808
|
try {
|
|
@@ -3822,3 +3827,291 @@ test("snapshot fails when an embedding generation member vanishes during the wal
|
|
|
3822
3827
|
await rm(root, { recursive: true, force: true });
|
|
3823
3828
|
}
|
|
3824
3829
|
});
|
|
3830
|
+
|
|
3831
|
+
test("a transient snapshot omission must not emit a delete for a shared-base file", async () => {
|
|
3832
|
+
const root = await tempDir("remnic-offline-uncertain-delete");
|
|
3833
|
+
try {
|
|
3834
|
+
await write(root, "facts/keep.md", "keep");
|
|
3835
|
+
await write(root, "facts/race.md", "race-v1");
|
|
3836
|
+
const base = await buildOfflineSyncSnapshot({
|
|
3837
|
+
root,
|
|
3838
|
+
sourceId: "local",
|
|
3839
|
+
includeContent: false,
|
|
3840
|
+
});
|
|
3841
|
+
// Live census fail-closes on digest ENOENT. A stale/incomplete currentFiles
|
|
3842
|
+
// list is still possible from an older client cache — that path must not
|
|
3843
|
+
// emit a confirmed delete when the file is back on disk.
|
|
3844
|
+
const incompleteFiles = base.files.filter((file) => file.path !== "facts/race.md");
|
|
3845
|
+
assert.deepEqual(
|
|
3846
|
+
incompleteFiles.map((file) => file.path),
|
|
3847
|
+
["facts/keep.md"],
|
|
3848
|
+
"fixture: the race path is absent from the incomplete currentFiles view",
|
|
3849
|
+
);
|
|
3850
|
+
await write(root, "facts/race.md", "race-v1");
|
|
3851
|
+
const changeset = await buildOfflineSyncChangesetFromSnapshot({
|
|
3852
|
+
root,
|
|
3853
|
+
sourceId: "local",
|
|
3854
|
+
currentFiles: incompleteFiles,
|
|
3855
|
+
baseFiles: base.files,
|
|
3856
|
+
});
|
|
3857
|
+
assert.equal(
|
|
3858
|
+
changeset.changes.filter((change) => change.path === "facts/race.md" && change.type === "delete").length,
|
|
3859
|
+
0,
|
|
3860
|
+
"an unreadable/recreated path must not become a confirmed delete against the shared base",
|
|
3861
|
+
);
|
|
3862
|
+
} finally {
|
|
3863
|
+
await rm(root, { recursive: true, force: true });
|
|
3864
|
+
}
|
|
3865
|
+
});
|
|
3866
|
+
|
|
3867
|
+
test("apply must not overwrite a recreated local file omitted from the local census", async () => {
|
|
3868
|
+
const root = await tempDir("remnic-offline-uncertain-overwrite");
|
|
3869
|
+
try {
|
|
3870
|
+
const recreated = "recreated local bytes";
|
|
3871
|
+
const incoming = "incoming remote bytes";
|
|
3872
|
+
await write(root, "facts/race.md", recreated);
|
|
3873
|
+
const incomingSha = createHash("sha256").update(incoming).digest("hex");
|
|
3874
|
+
// Local census omitted the recreated path (stale/racy currentFiles).
|
|
3875
|
+
// Incoming is base-less, so the apply path would treat it as a pure create.
|
|
3876
|
+
const result = await applyOfflineSyncSnapshot({
|
|
3877
|
+
root,
|
|
3878
|
+
currentFiles: [],
|
|
3879
|
+
baseFiles: [],
|
|
3880
|
+
snapshot: {
|
|
3881
|
+
format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
3882
|
+
schemaVersion: 1,
|
|
3883
|
+
createdAt: new Date().toISOString(),
|
|
3884
|
+
sourceId: "remote",
|
|
3885
|
+
includeTranscripts: false,
|
|
3886
|
+
files: [{
|
|
3887
|
+
path: "facts/race.md",
|
|
3888
|
+
sha256: incomingSha,
|
|
3889
|
+
bytes: Buffer.byteLength(incoming),
|
|
3890
|
+
mtimeMs: 2,
|
|
3891
|
+
contentBase64: Buffer.from(incoming).toString("base64"),
|
|
3892
|
+
}],
|
|
3893
|
+
},
|
|
3894
|
+
});
|
|
3895
|
+
assert.equal(
|
|
3896
|
+
await readUtf8(root, "facts/race.md"),
|
|
3897
|
+
recreated,
|
|
3898
|
+
"recreated local content must survive when the local census omitted it",
|
|
3899
|
+
);
|
|
3900
|
+
assert.equal(result.upserted, 0, "must not publish over an uncertain local path");
|
|
3901
|
+
} finally {
|
|
3902
|
+
await rm(root, { recursive: true, force: true });
|
|
3903
|
+
}
|
|
3904
|
+
});
|
|
3905
|
+
|
|
3906
|
+
test("changeset apply must not overwrite a recreated local file omitted by controlled digest census", async () => {
|
|
3907
|
+
// Public applyOfflineSyncChangeset builds local currentFiles via
|
|
3908
|
+
// buildOfflineSyncSnapshotForPaths when currentFiles is omitted. A controlled
|
|
3909
|
+
// digest that recreates local bytes then throws ENOENT must fail closed at
|
|
3910
|
+
// the shared census boundary — never export confirmed absence and overwrite.
|
|
3911
|
+
const root = await tempDir("remnic-offline-changeset-uncertain-overwrite");
|
|
3912
|
+
try {
|
|
3913
|
+
const recreated = "recreated local bytes";
|
|
3914
|
+
const incoming = "incoming remote bytes";
|
|
3915
|
+
const incomingSha = createHash("sha256").update(incoming).digest("hex");
|
|
3916
|
+
await write(root, "facts/race.md", recreated);
|
|
3917
|
+
|
|
3918
|
+
await assert.rejects(
|
|
3919
|
+
() => applyOfflineSyncChangeset({
|
|
3920
|
+
root,
|
|
3921
|
+
// No currentFiles: exercise the public snapshotForPaths census path.
|
|
3922
|
+
returnCurrentFiles: false,
|
|
3923
|
+
writeConflictCopies: false,
|
|
3924
|
+
readFileDigest: async ({ path: relPath, filePath }) => {
|
|
3925
|
+
if (relPath === "facts/race.md") {
|
|
3926
|
+
await writeFile(filePath, recreated);
|
|
3927
|
+
throw Object.assign(new Error("ENOENT: vanished during census"), { code: "ENOENT" });
|
|
3928
|
+
}
|
|
3929
|
+
const content = await readFile(filePath);
|
|
3930
|
+
return {
|
|
3931
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
3932
|
+
bytes: content.length,
|
|
3933
|
+
};
|
|
3934
|
+
},
|
|
3935
|
+
changeset: {
|
|
3936
|
+
format: "remnic.offline-sync.changeset.v1",
|
|
3937
|
+
schemaVersion: 1,
|
|
3938
|
+
createdAt: new Date().toISOString(),
|
|
3939
|
+
sourceId: "remote",
|
|
3940
|
+
includeTranscripts: false,
|
|
3941
|
+
changes: [{
|
|
3942
|
+
type: "upsert",
|
|
3943
|
+
path: "facts/race.md",
|
|
3944
|
+
file: {
|
|
3945
|
+
path: "facts/race.md",
|
|
3946
|
+
sha256: incomingSha,
|
|
3947
|
+
bytes: Buffer.byteLength(incoming),
|
|
3948
|
+
mtimeMs: 2,
|
|
3949
|
+
contentBase64: Buffer.from(incoming).toString("base64"),
|
|
3950
|
+
},
|
|
3951
|
+
}],
|
|
3952
|
+
},
|
|
3953
|
+
}),
|
|
3954
|
+
/census race|vanished during digest|retry the snapshot/i,
|
|
3955
|
+
);
|
|
3956
|
+
|
|
3957
|
+
assert.equal(
|
|
3958
|
+
await readUtf8(root, "facts/race.md"),
|
|
3959
|
+
recreated,
|
|
3960
|
+
"recreated local content must survive a fail-closed public changeset census race",
|
|
3961
|
+
);
|
|
3962
|
+
} finally {
|
|
3963
|
+
await rm(root, { recursive: true, force: true });
|
|
3964
|
+
}
|
|
3965
|
+
});
|
|
3966
|
+
|
|
3967
|
+
test("sender snapshot omission from digest race must not delete receiver shared-base file", async () => {
|
|
3968
|
+
// Root premise: sender digest race must not export an apparently-complete
|
|
3969
|
+
// snapshot that omits the path. Fail closed at the shared census boundary so
|
|
3970
|
+
// no consumer can assign confirmed absence. Sender and receiver bytes stay.
|
|
3971
|
+
const senderRoot = await tempDir("remnic-offline-sender-uncertain-omit");
|
|
3972
|
+
const receiverRoot = await tempDir("remnic-offline-receiver-shared-base");
|
|
3973
|
+
try {
|
|
3974
|
+
const good = "unchanged good fact bytes";
|
|
3975
|
+
const keep = "keep peer";
|
|
3976
|
+
await write(senderRoot, "facts/race.md", good);
|
|
3977
|
+
await write(senderRoot, "facts/keep.md", keep);
|
|
3978
|
+
await write(receiverRoot, "facts/race.md", good);
|
|
3979
|
+
await write(receiverRoot, "facts/keep.md", keep);
|
|
3980
|
+
|
|
3981
|
+
const sharedBase = await buildOfflineSyncSnapshot({
|
|
3982
|
+
root: receiverRoot,
|
|
3983
|
+
sourceId: "shared",
|
|
3984
|
+
includeContent: false,
|
|
3985
|
+
});
|
|
3986
|
+
assert.ok(
|
|
3987
|
+
sharedBase.files.some((file) => file.path === "facts/race.md"),
|
|
3988
|
+
"fixture: shared base includes the good race path",
|
|
3989
|
+
);
|
|
3990
|
+
|
|
3991
|
+
await assert.rejects(
|
|
3992
|
+
() => buildOfflineSyncSnapshot({
|
|
3993
|
+
root: senderRoot,
|
|
3994
|
+
sourceId: "sender",
|
|
3995
|
+
includeContent: false,
|
|
3996
|
+
readFileDigest: async ({ path: relPath, filePath }) => {
|
|
3997
|
+
if (relPath === "facts/race.md") {
|
|
3998
|
+
await writeFile(filePath, good);
|
|
3999
|
+
throw Object.assign(new Error("ENOENT: vanished during sender census"), { code: "ENOENT" });
|
|
4000
|
+
}
|
|
4001
|
+
const content = await readFile(filePath);
|
|
4002
|
+
return {
|
|
4003
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
4004
|
+
bytes: content.length,
|
|
4005
|
+
};
|
|
4006
|
+
},
|
|
4007
|
+
}),
|
|
4008
|
+
/census race|vanished during digest|retry the snapshot/i,
|
|
4009
|
+
);
|
|
4010
|
+
|
|
4011
|
+
assert.equal(await readUtf8(senderRoot, "facts/race.md"), good, "sender bytes preserved");
|
|
4012
|
+
assert.equal(await readUtf8(receiverRoot, "facts/race.md"), good, "receiver bytes preserved without a false snapshot");
|
|
4013
|
+
assert.equal(await readUtf8(receiverRoot, "facts/keep.md"), keep);
|
|
4014
|
+
} finally {
|
|
4015
|
+
await rm(senderRoot, { recursive: true, force: true });
|
|
4016
|
+
await rm(receiverRoot, { recursive: true, force: true });
|
|
4017
|
+
}
|
|
4018
|
+
});
|
|
4019
|
+
|
|
4020
|
+
|
|
4021
|
+
test("a file vanishing during the changeset upsert read keeps the retryable changed-while-building message", async () => {
|
|
4022
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-changeset-vanish-"));
|
|
4023
|
+
try {
|
|
4024
|
+
const rel = "facts/volatile.md";
|
|
4025
|
+
const bytes = Buffer.from("volatile bytes\n");
|
|
4026
|
+
await mkdir(path.join(root, "facts"), { recursive: true });
|
|
4027
|
+
await writeFile(path.join(root, rel), bytes);
|
|
4028
|
+
const currentFile = {
|
|
4029
|
+
path: rel,
|
|
4030
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
4031
|
+
bytes: bytes.byteLength,
|
|
4032
|
+
mtimeMs: 1,
|
|
4033
|
+
};
|
|
4034
|
+
await assert.rejects(
|
|
4035
|
+
buildOfflineSyncChangesetFromSnapshot({
|
|
4036
|
+
root,
|
|
4037
|
+
sourceId: "laptop",
|
|
4038
|
+
baseFiles: [],
|
|
4039
|
+
currentFiles: [currentFile],
|
|
4040
|
+
readFile: async () => {
|
|
4041
|
+
await rm(path.join(root, rel), { force: true });
|
|
4042
|
+
throw Object.assign(new Error("ENOENT: file vanished"), { code: "ENOENT" });
|
|
4043
|
+
},
|
|
4044
|
+
}),
|
|
4045
|
+
(error: Error) => {
|
|
4046
|
+
// The CLI retry loop matches this exact prefix to defer the path and rebuild the census.
|
|
4047
|
+
assert.equal(error.message, `offline sync file changed while building changeset: ${rel}`);
|
|
4048
|
+
return true;
|
|
4049
|
+
},
|
|
4050
|
+
);
|
|
4051
|
+
} finally {
|
|
4052
|
+
await rm(root, { recursive: true, force: true });
|
|
4053
|
+
}
|
|
4054
|
+
});
|
|
4055
|
+
|
|
4056
|
+
test("a tracked file replaced by a symlink is a local delete, not a push-blocking error", async () => {
|
|
4057
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-symlink-replaced-"));
|
|
4058
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-symlink-target-"));
|
|
4059
|
+
try {
|
|
4060
|
+
const rel = "facts/tracked.md";
|
|
4061
|
+
const bytes = Buffer.from("tracked bytes\n");
|
|
4062
|
+
await mkdir(path.join(root, "facts"), { recursive: true });
|
|
4063
|
+
await writeFile(path.join(outside, "target.md"), "outside\n");
|
|
4064
|
+
await symlink(path.join(outside, "target.md"), path.join(root, rel));
|
|
4065
|
+
const baseFile = {
|
|
4066
|
+
path: rel,
|
|
4067
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
4068
|
+
bytes: bytes.byteLength,
|
|
4069
|
+
mtimeMs: 1,
|
|
4070
|
+
};
|
|
4071
|
+
const changeset = await buildOfflineSyncChangesetFromSnapshot({
|
|
4072
|
+
root,
|
|
4073
|
+
sourceId: "laptop",
|
|
4074
|
+
baseFiles: [baseFile],
|
|
4075
|
+
currentFiles: [],
|
|
4076
|
+
});
|
|
4077
|
+
assert.deepEqual(
|
|
4078
|
+
changeset.changes.map((change) => [change.type, change.path]),
|
|
4079
|
+
[["delete", rel]],
|
|
4080
|
+
"a symlink is not a regular local file: it reads as a delete, as before the census change",
|
|
4081
|
+
);
|
|
4082
|
+
} finally {
|
|
4083
|
+
await rm(root, { recursive: true, force: true });
|
|
4084
|
+
await rm(outside, { recursive: true, force: true });
|
|
4085
|
+
}
|
|
4086
|
+
});
|
|
4087
|
+
|
|
4088
|
+
test("a tracked file under a directory replaced by an in-root symlink is a local delete", async () => {
|
|
4089
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-symlink-parent-"));
|
|
4090
|
+
try {
|
|
4091
|
+
const rel = "facts/tracked.md";
|
|
4092
|
+
const bytes = Buffer.from("tracked bytes\n");
|
|
4093
|
+
await mkdir(path.join(root, "elsewhere"), { recursive: true });
|
|
4094
|
+
await writeFile(path.join(root, "elsewhere", "tracked.md"), bytes);
|
|
4095
|
+
// The census skips a symlinked directory, so facts/tracked.md is not in it.
|
|
4096
|
+
await symlink(path.join(root, "elsewhere"), path.join(root, "facts"));
|
|
4097
|
+
const baseFile = {
|
|
4098
|
+
path: rel,
|
|
4099
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
4100
|
+
bytes: bytes.byteLength,
|
|
4101
|
+
mtimeMs: 1,
|
|
4102
|
+
};
|
|
4103
|
+
const changeset = await buildOfflineSyncChangesetFromSnapshot({
|
|
4104
|
+
root,
|
|
4105
|
+
sourceId: "laptop",
|
|
4106
|
+
baseFiles: [baseFile],
|
|
4107
|
+
currentFiles: [],
|
|
4108
|
+
});
|
|
4109
|
+
assert.deepEqual(
|
|
4110
|
+
changeset.changes.map((change) => [change.type, change.path]),
|
|
4111
|
+
[["delete", rel]],
|
|
4112
|
+
"a path reached only through a symlinked parent is absent, as the census sees it",
|
|
4113
|
+
);
|
|
4114
|
+
} finally {
|
|
4115
|
+
await rm(root, { recursive: true, force: true });
|
|
4116
|
+
}
|
|
4117
|
+
});
|
package/src/offline-sync.ts
CHANGED
|
@@ -70,6 +70,10 @@ import {
|
|
|
70
70
|
setSafeFileMtime,
|
|
71
71
|
assertNonNegativeFinite,
|
|
72
72
|
assertOfflineSyncMtimeMs,
|
|
73
|
+
offlineSyncRegularFilePresent,
|
|
74
|
+
OfflineSyncCensusRaceError,
|
|
75
|
+
censusRaceAsChangedWhileBuilding,
|
|
76
|
+
changedWhileBuildingChangesetError,
|
|
73
77
|
OFFLINE_SYNC_FAST_BASE_MTIME_TOLERANCE_MS,
|
|
74
78
|
} from "./offline-sync-file-io.js";
|
|
75
79
|
import { CENSUS_MAX_MTIME_MS, isSha256Hex } from "./census-validation.js";
|
|
@@ -793,19 +797,29 @@ async function canReuseFastBaseFileStateFromDisk(
|
|
|
793
797
|
|
|
794
798
|
async function readOfflineSyncFileRecord(
|
|
795
799
|
options: OfflineSyncFileRecordOptions,
|
|
796
|
-
): Promise<OfflineSyncFileRecord
|
|
800
|
+
): Promise<OfflineSyncFileRecord> {
|
|
797
801
|
try {
|
|
798
802
|
return await readPresentOfflineSyncFileRecord(options);
|
|
799
803
|
} catch (error) {
|
|
800
|
-
//
|
|
801
|
-
|
|
804
|
+
// Caller already observed the path (readdir/lstat). Digest ENOENT is an
|
|
805
|
+
// ambiguous race: exporting a silent omission assigns confirmed absence to
|
|
806
|
+
// every consumer (full snapshot apply + changeset apply). Fail closed.
|
|
807
|
+
if (isNotFoundError(error)) {
|
|
808
|
+
if (embeddingGenerationMembership(options.relPath) !== null) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
`offline sync embedding generation member vanished during snapshot walk: ${options.relPath}`,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
throw new OfflineSyncCensusRaceError(options.relPath, error);
|
|
814
|
+
}
|
|
802
815
|
throw error;
|
|
803
816
|
}
|
|
804
817
|
}
|
|
805
818
|
|
|
806
|
-
|
|
807
|
-
//
|
|
808
|
-
//
|
|
819
|
+
|
|
820
|
+
// Pre-stat vanish (readdir then lstat/stat ENOENT) may omit only when nothing
|
|
821
|
+
// can misread the gap: embedding generations travel as one unit (issue #3148),
|
|
822
|
+
// and a base-known file omitted here would fabricate a delete in
|
|
809
823
|
// buildOfflineSyncChangesetFromSnapshot.
|
|
810
824
|
function assertSnapshotVanishIsSafe(relPath: string, base: ReadonlyMap<string, unknown> | undefined): void {
|
|
811
825
|
if (embeddingGenerationMembership(relPath) !== null) {
|
|
@@ -816,6 +830,7 @@ function assertSnapshotVanishIsSafe(relPath: string, base: ReadonlyMap<string, u
|
|
|
816
830
|
}
|
|
817
831
|
}
|
|
818
832
|
|
|
833
|
+
|
|
819
834
|
async function readPresentOfflineSyncFileRecord(
|
|
820
835
|
options: OfflineSyncFileRecordOptions,
|
|
821
836
|
): Promise<OfflineSyncFileRecord> {
|
|
@@ -923,7 +938,7 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
923
938
|
path: relPosix,
|
|
924
939
|
filePath: abs,
|
|
925
940
|
})) continue;
|
|
926
|
-
|
|
941
|
+
yield await readOfflineSyncFileRecord({
|
|
927
942
|
root,
|
|
928
943
|
relPath: relPosix,
|
|
929
944
|
filePath: abs,
|
|
@@ -932,8 +947,7 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
|
|
|
932
947
|
readFileDigest: options.readFileDigest,
|
|
933
948
|
signal: options.signal,
|
|
934
949
|
});
|
|
935
|
-
|
|
936
|
-
else assertSnapshotVanishIsSafe(relPosix, undefined);
|
|
950
|
+
|
|
937
951
|
}
|
|
938
952
|
}
|
|
939
953
|
|
|
@@ -1094,8 +1108,7 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
1094
1108
|
try {
|
|
1095
1109
|
st = await stat(abs);
|
|
1096
1110
|
} catch (error) {
|
|
1097
|
-
//
|
|
1098
|
-
// path could fabricate a delete or downgrade a generation.
|
|
1111
|
+
// Pre-stat vanish after readdir: omit only when assertSnapshotVanishIsSafe allows.
|
|
1099
1112
|
if (isNotFoundError(error)) {
|
|
1100
1113
|
assertSnapshotVanishIsSafe(relPosix, base);
|
|
1101
1114
|
continue;
|
|
@@ -1112,7 +1125,7 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
1112
1125
|
files.push(baseEntry);
|
|
1113
1126
|
continue;
|
|
1114
1127
|
}
|
|
1115
|
-
|
|
1128
|
+
files.push(await readOfflineSyncFileRecord({
|
|
1116
1129
|
root,
|
|
1117
1130
|
relPath: relPosix,
|
|
1118
1131
|
filePath: abs,
|
|
@@ -1120,9 +1133,7 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
|
|
|
1120
1133
|
readFile: options.readFile,
|
|
1121
1134
|
readFileDigest: options.readFileDigest,
|
|
1122
1135
|
signal: options.signal,
|
|
1123
|
-
});
|
|
1124
|
-
if (record) files.push(record);
|
|
1125
|
-
else assertSnapshotVanishIsSafe(relPosix, base);
|
|
1136
|
+
}));
|
|
1126
1137
|
}
|
|
1127
1138
|
}
|
|
1128
1139
|
|
|
@@ -1208,7 +1219,7 @@ export async function buildOfflineSyncSnapshotForPaths(options: {
|
|
|
1208
1219
|
})) {
|
|
1209
1220
|
throw new Error(`offline sync snapshot path is excluded: ${relPath}`);
|
|
1210
1221
|
}
|
|
1211
|
-
|
|
1222
|
+
files.push(await readOfflineSyncFileRecord({
|
|
1212
1223
|
root,
|
|
1213
1224
|
relPath,
|
|
1214
1225
|
filePath,
|
|
@@ -1216,9 +1227,7 @@ export async function buildOfflineSyncSnapshotForPaths(options: {
|
|
|
1216
1227
|
readFile: options.readFile,
|
|
1217
1228
|
readFileDigest: options.readFileDigest,
|
|
1218
1229
|
signal: options.signal,
|
|
1219
|
-
});
|
|
1220
|
-
if (record) files.push(record);
|
|
1221
|
-
else assertSnapshotVanishIsSafe(relPath, undefined);
|
|
1230
|
+
}));
|
|
1222
1231
|
}
|
|
1223
1232
|
throwIfOfflineSyncAborted(options.signal);
|
|
1224
1233
|
|
|
@@ -1414,6 +1423,11 @@ export async function buildOfflineSyncChangesetFromSnapshot(options: {
|
|
|
1414
1423
|
excludeNodeLocalState?: boolean;
|
|
1415
1424
|
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
1416
1425
|
}): Promise<OfflineSyncChangeset> {
|
|
1426
|
+
const root = await prepareSafeArchiveRoot(
|
|
1427
|
+
path.resolve(options.root),
|
|
1428
|
+
"buildOfflineSyncChangesetFromSnapshot",
|
|
1429
|
+
"root",
|
|
1430
|
+
);
|
|
1417
1431
|
const includeTranscripts = options.includeTranscripts !== false;
|
|
1418
1432
|
const excludedPaths = new Set(
|
|
1419
1433
|
(options.excludePaths ?? []).map((relPath) => normalizeRelativePath(relPath, "excludePaths[]")),
|
|
@@ -1453,10 +1467,10 @@ export async function buildOfflineSyncChangesetFromSnapshot(options: {
|
|
|
1453
1467
|
now: options.now,
|
|
1454
1468
|
userExcludeRegexps: options.userExcludeRegexps,
|
|
1455
1469
|
readFile: options.readFile,
|
|
1456
|
-
});
|
|
1470
|
+
}).catch(censusRaceAsChangedWhileBuilding(relPath));
|
|
1457
1471
|
const record = file.files[0];
|
|
1458
1472
|
if (!record || typeof record.contentBase64 !== "string" || record.sha256 !== currentEntry.sha256) {
|
|
1459
|
-
throw
|
|
1473
|
+
throw changedWhileBuildingChangesetError(relPath);
|
|
1460
1474
|
}
|
|
1461
1475
|
changes.push({
|
|
1462
1476
|
type: "upsert",
|
|
@@ -1467,6 +1481,8 @@ export async function buildOfflineSyncChangesetFromSnapshot(options: {
|
|
|
1467
1481
|
continue;
|
|
1468
1482
|
}
|
|
1469
1483
|
if (!currentEntry && baseEntry) {
|
|
1484
|
+
// Census miss is not confirmed delete — recheck disk (transient ENOENT / recreate).
|
|
1485
|
+
if (await offlineSyncRegularFilePresent(root, relPath)) continue;
|
|
1470
1486
|
const mtimeMs = deletionMtimeByPath?.get(relPath);
|
|
1471
1487
|
changes.push({
|
|
1472
1488
|
type: "delete",
|
|
@@ -1808,6 +1824,12 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
1808
1824
|
continue;
|
|
1809
1825
|
}
|
|
1810
1826
|
if (!currentEntry && !base) {
|
|
1827
|
+
// Census miss with on-disk file: uncertain local presence, do not overwrite.
|
|
1828
|
+
if (await offlineSyncRegularFilePresent(root, relPath)) {
|
|
1829
|
+
pendingLocal += 1;
|
|
1830
|
+
skipped += 1;
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1811
1833
|
await writeSafeFile(root, relPath, requiredBuffer(incomingBuffers, relPath), options.writeFile, incoming.mtimeMs);
|
|
1812
1834
|
nextBase.set(relPath, toFileState(incoming));
|
|
1813
1835
|
upserted += 1;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import test from "node:test";
|
|
@@ -1547,3 +1548,50 @@ test("startupSearchSync composes its caller abort into the QMD probe", async ()
|
|
|
1547
1548
|
await rm(memoryDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
1548
1549
|
}
|
|
1549
1550
|
});
|
|
1551
|
+
|
|
1552
|
+
// Both startup call sites must open read readiness without migration replay.
|
|
1553
|
+
for (const namespace of ["default", "shared"]) {
|
|
1554
|
+
test(`startup defers entity migration for ${namespace} storage`, async () => {
|
|
1555
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-init-migration-defer-"));
|
|
1556
|
+
const orchestrator = new Orchestrator(
|
|
1557
|
+
parseConfig({
|
|
1558
|
+
memoryDir,
|
|
1559
|
+
workspaceDir: path.join(memoryDir, "workspace"),
|
|
1560
|
+
qmdEnabled: false,
|
|
1561
|
+
namespacesEnabled: namespace === "shared",
|
|
1562
|
+
namespaceCatalogEnabled: false,
|
|
1563
|
+
defaultNamespace: "default",
|
|
1564
|
+
sharedNamespace: "shared",
|
|
1565
|
+
namespacePolicies: [],
|
|
1566
|
+
embeddingFallbackEnabled: false,
|
|
1567
|
+
identityContinuityEnabled: false,
|
|
1568
|
+
transcriptEnabled: false,
|
|
1569
|
+
hourlySummariesEnabled: false,
|
|
1570
|
+
compoundingEnabled: false,
|
|
1571
|
+
knowledgeIndexEnabled: false,
|
|
1572
|
+
}),
|
|
1573
|
+
);
|
|
1574
|
+
const storage = await orchestrator.getStorage(namespace);
|
|
1575
|
+
const lockPath = path.join(storage.dir, "state", "entity-canonical-id-migration.lock");
|
|
1576
|
+
const journalPath = path.join(storage.dir, "state", "entity-canonical-id-migration-v1.json");
|
|
1577
|
+
try {
|
|
1578
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
1579
|
+
await writeFile(lockPath, `${process.pid} init-migration-test ${new Date().toISOString()}\n`);
|
|
1580
|
+
const outcome = await withTestDeadline(
|
|
1581
|
+
orchestrator.initialize().then(() => "settled" as const, () => "rejected" as const),
|
|
1582
|
+
);
|
|
1583
|
+
assert.equal(outcome, "settled", "startup must succeed while the migration lock is held");
|
|
1584
|
+
assert.equal(existsSync(lockPath), true);
|
|
1585
|
+
assert.equal(existsSync(journalPath), false, "startup must not run the migration");
|
|
1586
|
+
await rm(lockPath);
|
|
1587
|
+
await storage.ensureDirectories();
|
|
1588
|
+
// SAFETY: the journal is written by the migration under test and carries an optional boolean `complete`.
|
|
1589
|
+
const state = JSON.parse(await readFile(journalPath, "utf8")) as { complete?: boolean };
|
|
1590
|
+
assert.equal(state.complete, true, "default setup must still run migration");
|
|
1591
|
+
} finally {
|
|
1592
|
+
await rm(lockPath, { force: true });
|
|
1593
|
+
await orchestrator.destroy();
|
|
1594
|
+
await rm(memoryDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
}
|