@remnic/cli 1.0.23 → 1.0.25

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.
Files changed (2) hide show
  1. package/dist/index.js +979 -241
  2. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import path11 from "path";
5
5
  import { createDecipheriv, createHash } from "crypto";
6
6
  import * as childProcess2 from "child_process";
7
7
  import { fileURLToPath as fileURLToPath4 } from "url";
8
+ import { gzipSync } from "zlib";
8
9
  import {
9
10
  parseConfig,
10
11
  isOpenaiApiKeyDisabled,
@@ -91,15 +92,19 @@ import {
91
92
  OFFLINE_SYNC_APPLY_MAX_BODY_BYTES,
92
93
  OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
93
94
  OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES,
95
+ OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
96
+ applyOfflineSyncFileContentChunk,
94
97
  applyOfflineSyncSnapshot,
95
- buildOfflineSyncChangeset,
96
- buildOfflineSyncSnapshot,
98
+ buildOfflineSyncChangesetFromSnapshot,
99
+ buildOfflineSyncSnapshotFromBase,
97
100
  defaultOfflineSyncStatePath,
98
101
  normalizeOfflineSyncSnapshot,
99
102
  offlineSyncStateFromSnapshot,
100
103
  readOfflineSyncFileContentChunk,
101
104
  readOfflineSyncState,
105
+ shouldPreferIncomingOfflineRuntimeFile,
102
106
  summarizeOfflineSyncPendingChanges,
107
+ summarizeOfflineSyncPendingFiles,
103
108
  writeOfflineSyncState,
104
109
  buildActionConfidenceInputFromOptions,
105
110
  evaluateActionConfidence,
@@ -6643,29 +6648,230 @@ function offlineEndpoint(remoteUrl, pathname, params = {}) {
6643
6648
  }
6644
6649
  return url.toString();
6645
6650
  }
6651
+ var OFFLINE_SYNC_REQUEST_TIMEOUT_DEFAULT_MS = 15 * 6e4;
6652
+ var OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES = 16 * 1024 * 1024;
6653
+ function parseOfflineSyncRequestTimeoutMs(raw, fallback = OFFLINE_SYNC_REQUEST_TIMEOUT_DEFAULT_MS) {
6654
+ if (raw === void 0 || raw.trim().length === 0) return fallback;
6655
+ const parsed = Number(raw);
6656
+ if (!Number.isInteger(parsed) || parsed < 1e3) {
6657
+ throw new Error("REMNIC_OFFLINE_REQUEST_TIMEOUT_MS must be an integer >= 1000");
6658
+ }
6659
+ return parsed;
6660
+ }
6661
+ function formatOfflineRequestForError(url, init = {}) {
6662
+ const method = (init.method ?? "GET").toString().toUpperCase();
6663
+ try {
6664
+ const parsed = new URL(url);
6665
+ return `${method} ${parsed.pathname}${parsed.search}`;
6666
+ } catch {
6667
+ return `${method} ${url}`;
6668
+ }
6669
+ }
6670
+ function offlineRequestTimeoutMs() {
6671
+ return parseOfflineSyncRequestTimeoutMs(
6672
+ process.env.REMNIC_OFFLINE_REQUEST_TIMEOUT_MS ?? process.env.ENGRAM_OFFLINE_REQUEST_TIMEOUT_MS
6673
+ );
6674
+ }
6675
+ function offlineFetchHeaders(token, initHeaders, defaultContentType) {
6676
+ const headers = new Headers(initHeaders);
6677
+ headers.set("authorization", `Bearer ${token}`);
6678
+ if (defaultContentType && !headers.has("content-type")) {
6679
+ headers.set("content-type", defaultContentType);
6680
+ }
6681
+ return headers;
6682
+ }
6683
+ async function fetchOfflineWithResponse(url, token, init = {}, options = {}, consume) {
6684
+ const timeoutMs = offlineRequestTimeoutMs();
6685
+ const controller = new AbortController();
6686
+ const upstreamSignal = init.signal;
6687
+ const requestContext = formatOfflineRequestForError(url, init);
6688
+ let didTimeout = false;
6689
+ const timeout = setTimeout(() => {
6690
+ didTimeout = true;
6691
+ controller.abort();
6692
+ }, timeoutMs);
6693
+ const abortFromUpstream = () => controller.abort(upstreamSignal?.reason);
6694
+ if (upstreamSignal) {
6695
+ if (upstreamSignal.aborted) controller.abort(upstreamSignal.reason);
6696
+ else upstreamSignal.addEventListener("abort", abortFromUpstream, { once: true });
6697
+ }
6698
+ let response;
6699
+ try {
6700
+ response = await fetch(url, {
6701
+ ...init,
6702
+ signal: controller.signal,
6703
+ headers: offlineFetchHeaders(token, init.headers, options.defaultContentType)
6704
+ });
6705
+ } catch (error) {
6706
+ clearTimeout(timeout);
6707
+ upstreamSignal?.removeEventListener("abort", abortFromUpstream);
6708
+ if (didTimeout) {
6709
+ throw new Error(`offline sync request timed out after ${timeoutMs}ms: ${requestContext}`);
6710
+ }
6711
+ const message = error instanceof Error ? error.message : String(error);
6712
+ throw new Error(`offline sync request failed before response: ${requestContext} - ${message}`);
6713
+ }
6714
+ try {
6715
+ return await consume(response);
6716
+ } catch (error) {
6717
+ if (didTimeout) {
6718
+ throw new Error(`offline sync request timed out after ${timeoutMs}ms: ${requestContext}`);
6719
+ }
6720
+ throw error;
6721
+ } finally {
6722
+ clearTimeout(timeout);
6723
+ upstreamSignal?.removeEventListener("abort", abortFromUpstream);
6724
+ }
6725
+ }
6726
+ async function throwOfflineResponseError(response, url, init, label = "offline sync request") {
6727
+ let detail = "";
6728
+ try {
6729
+ detail = await response.text();
6730
+ } catch {
6731
+ detail = "";
6732
+ }
6733
+ throw new Error(
6734
+ `${label} failed: ${formatOfflineRequestForError(url, init)} returned ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
6735
+ );
6736
+ }
6646
6737
  async function fetchOfflineJson(url, token, init = {}) {
6647
- const response = await fetch(url, {
6648
- ...init,
6649
- headers: {
6650
- authorization: `Bearer ${token}`,
6651
- ...init.body !== void 0 ? { "content-type": "application/json" } : {},
6652
- ...init.headers ?? {}
6738
+ return fetchOfflineWithResponse(
6739
+ url,
6740
+ token,
6741
+ init,
6742
+ { defaultContentType: init.body !== void 0 ? "application/json" : void 0 },
6743
+ async (response) => {
6744
+ if (!response.ok) {
6745
+ await throwOfflineResponseError(response, url, init);
6746
+ }
6747
+ return await response.json();
6748
+ }
6749
+ );
6750
+ }
6751
+ async function parseOfflineSnapshotStreamResponse(response) {
6752
+ if (!response.body) {
6753
+ throw new Error("offline sync snapshot stream response omitted body");
6754
+ }
6755
+ const reader = response.body.getReader();
6756
+ const decoder = new TextDecoder();
6757
+ let buffered = "";
6758
+ let header = null;
6759
+ const files = [];
6760
+ const handleLine = (line) => {
6761
+ if (line.trim().length === 0) return;
6762
+ const parsed = JSON.parse(line);
6763
+ if (parsed.type === "snapshot") {
6764
+ if (header) throw new Error("offline sync snapshot stream repeated header");
6765
+ header = {
6766
+ ...typeof parsed.namespace === "string" && parsed.namespace.length > 0 ? { namespace: parsed.namespace } : {},
6767
+ format: parsed.format,
6768
+ schemaVersion: parsed.schemaVersion,
6769
+ createdAt: parsed.createdAt,
6770
+ sourceId: parsed.sourceId,
6771
+ includeTranscripts: parsed.includeTranscripts
6772
+ };
6773
+ return;
6653
6774
  }
6775
+ if (parsed.type === "file") {
6776
+ if (!header) throw new Error("offline sync snapshot stream file arrived before header");
6777
+ files.push(parsed.file);
6778
+ return;
6779
+ }
6780
+ throw new Error("offline sync snapshot stream contained unknown event");
6781
+ };
6782
+ for (; ; ) {
6783
+ const { value, done } = await reader.read();
6784
+ if (done) break;
6785
+ buffered += decoder.decode(value, { stream: true });
6786
+ for (; ; ) {
6787
+ const newline = buffered.indexOf("\n");
6788
+ if (newline < 0) break;
6789
+ const line = buffered.slice(0, newline);
6790
+ buffered = buffered.slice(newline + 1);
6791
+ handleLine(line);
6792
+ }
6793
+ }
6794
+ buffered += decoder.decode();
6795
+ handleLine(buffered);
6796
+ const finalHeader = header;
6797
+ if (!finalHeader) throw new Error("offline sync snapshot stream omitted header");
6798
+ const snapshot = normalizeOfflineSyncSnapshot({
6799
+ format: finalHeader.format,
6800
+ schemaVersion: finalHeader.schemaVersion,
6801
+ createdAt: finalHeader.createdAt,
6802
+ sourceId: finalHeader.sourceId,
6803
+ includeTranscripts: finalHeader.includeTranscripts,
6804
+ files
6654
6805
  });
6655
- if (!response.ok) {
6656
- let detail = "";
6657
- try {
6658
- detail = await response.text();
6659
- } catch {
6660
- detail = "";
6806
+ return {
6807
+ ...finalHeader.namespace ? { namespace: finalHeader.namespace } : {},
6808
+ ...snapshot
6809
+ };
6810
+ }
6811
+ async function fetchOfflineSnapshotStream(args) {
6812
+ const url = offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/snapshot-stream", {
6813
+ namespace: args.namespace,
6814
+ include_transcripts: args.includeTranscripts ? "true" : "false",
6815
+ content: "false"
6816
+ });
6817
+ return fetchOfflineWithResponse(
6818
+ url,
6819
+ args.token,
6820
+ {},
6821
+ {},
6822
+ async (response) => {
6823
+ if (!response.ok) {
6824
+ await throwOfflineResponseError(response, url, {}, "offline sync snapshot-stream request");
6825
+ }
6826
+ return parseOfflineSnapshotStreamResponse(response);
6661
6827
  }
6662
- throw new Error(
6663
- `offline sync request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
6664
- );
6665
- }
6666
- return await response.json();
6828
+ );
6667
6829
  }
6668
6830
  async function fetchOfflineSnapshot(args) {
6831
+ let tryStreamSnapshot = false;
6832
+ if (args.includeContent === false && args.baseFiles && args.baseFiles.length > 0) {
6833
+ if (args.baseFiles.length > OFFLINE_SYNC_SNAPSHOT_BASE_POST_MAX_FILES) {
6834
+ tryStreamSnapshot = true;
6835
+ } else {
6836
+ const postBody = offlineSnapshotBasePostBody({
6837
+ namespace: args.namespace,
6838
+ includeTranscripts: args.includeTranscripts,
6839
+ baseFiles: args.baseFiles,
6840
+ baseCapturedAt: args.baseCapturedAt
6841
+ });
6842
+ const postRequest = offlineSnapshotBasePostRequest(postBody);
6843
+ if (postRequest) {
6844
+ const postRequestUsesGzip = new Headers(postRequest.headers).get("content-encoding")?.toLowerCase() === "gzip";
6845
+ try {
6846
+ return await fetchOfflineJson(
6847
+ offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/snapshot"),
6848
+ args.token,
6849
+ {
6850
+ method: "POST",
6851
+ ...postRequest
6852
+ }
6853
+ );
6854
+ } catch (error) {
6855
+ if (!isOfflineSnapshotPostFallbackError(error, { compressed: postRequestUsesGzip })) throw error;
6856
+ tryStreamSnapshot = true;
6857
+ }
6858
+ } else {
6859
+ tryStreamSnapshot = true;
6860
+ }
6861
+ }
6862
+ }
6863
+ if (tryStreamSnapshot) {
6864
+ try {
6865
+ return await fetchOfflineSnapshotStream({
6866
+ remoteUrl: args.remoteUrl,
6867
+ token: args.token,
6868
+ namespace: args.namespace,
6869
+ includeTranscripts: args.includeTranscripts
6870
+ });
6871
+ } catch (error) {
6872
+ if (!isOfflineSnapshotStreamFallbackError(error)) throw error;
6873
+ }
6874
+ }
6669
6875
  return fetchOfflineJson(
6670
6876
  offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/snapshot", {
6671
6877
  namespace: args.namespace,
@@ -6675,6 +6881,47 @@ async function fetchOfflineSnapshot(args) {
6675
6881
  args.token
6676
6882
  );
6677
6883
  }
6884
+ function offlineSnapshotBasePostBody(args) {
6885
+ return JSON.stringify({
6886
+ namespace: args.namespace,
6887
+ includeTranscripts: args.includeTranscripts,
6888
+ includeContent: false,
6889
+ baseFiles: args.baseFiles,
6890
+ ...args.baseCapturedAt ? { baseCapturedAt: args.baseCapturedAt.toISOString() } : {}
6891
+ });
6892
+ }
6893
+ function offlineSnapshotBasePostBodyFits(body) {
6894
+ const bytes = Buffer.byteLength(body, "utf-8");
6895
+ return bytes <= OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES && bytes <= OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES;
6896
+ }
6897
+ var OFFLINE_SYNC_SNAPSHOT_BASE_POST_MAX_FILES = 5e4;
6898
+ function offlineSnapshotBasePostRequest(body) {
6899
+ const bytes = Buffer.byteLength(body, "utf-8");
6900
+ if (bytes > OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES) return null;
6901
+ if (bytes <= OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES) {
6902
+ return { body };
6903
+ }
6904
+ const compressed = gzipSync(body);
6905
+ if (compressed.byteLength > OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES) {
6906
+ return null;
6907
+ }
6908
+ return {
6909
+ body: compressed,
6910
+ headers: {
6911
+ "content-encoding": "gzip"
6912
+ }
6913
+ };
6914
+ }
6915
+ function isOfflineSnapshotPostFallbackError(error, options = {}) {
6916
+ const message = error instanceof Error ? error.message : String(error);
6917
+ if (/offline-sync\/snapshot\b.* returned (404|405|413)\b/.test(message)) return true;
6918
+ if (!options.compressed) return false;
6919
+ return /offline-sync\/snapshot\b.* returned (400|415)\b/.test(message) && /\b(unsupported_content_encoding|invalid_gzip_body|invalid_json)\b/.test(message);
6920
+ }
6921
+ function isOfflineSnapshotStreamFallbackError(error) {
6922
+ const message = error instanceof Error ? error.message : String(error);
6923
+ return /offline-sync\/snapshot-stream\b.* returned (404|405)\b/.test(message);
6924
+ }
6678
6925
  async function fetchOfflineFiles(args) {
6679
6926
  return fetchOfflineJson(
6680
6927
  offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/files"),
@@ -6702,6 +6949,28 @@ var OFFLINE_SYNC_DIRECT_PUSH_MIN_BYTES = Math.min(
6702
6949
  OFFLINE_SYNC_INLINE_CONTENT_MAX_BYTES
6703
6950
  );
6704
6951
  var OFFLINE_SYNC_DIRECT_HYDRATE_MIN_BYTES = OFFLINE_SYNC_DIRECT_PUSH_MIN_BYTES;
6952
+ var OFFLINE_SYNC_CHANGESET_RETRY_MAX = 1024;
6953
+ var OfflineRemoteFileChangedError = class extends Error {
6954
+ path;
6955
+ constructor(path12) {
6956
+ super(`remote file changed while fetching offline content: ${path12}`);
6957
+ this.name = "OfflineRemoteFileChangedError";
6958
+ this.path = path12;
6959
+ }
6960
+ };
6961
+ function isOfflineRemoteFileChangedError(error) {
6962
+ return error instanceof OfflineRemoteFileChangedError || error instanceof Error && error.message.startsWith("remote file changed while fetching offline content: ");
6963
+ }
6964
+ function isOfflineLocalFileChangedError(error) {
6965
+ return error instanceof Error && error.message.startsWith("local file changed while pushing offline content: ");
6966
+ }
6967
+ function offlineChangesetFileChangedPath(error) {
6968
+ if (!(error instanceof Error)) return null;
6969
+ const prefix = "offline sync file changed while building changeset: ";
6970
+ if (!error.message.startsWith(prefix)) return null;
6971
+ const relPath = error.message.slice(prefix.length).trim();
6972
+ return relPath.length > 0 ? relPath : null;
6973
+ }
6705
6974
  function parseOfflineHeaderNumber(headers, name) {
6706
6975
  const raw = headers.get(name);
6707
6976
  if (raw === null) throw new Error(`offline file content response omitted ${name}`);
@@ -6712,86 +6981,77 @@ function parseOfflineHeaderNumber(headers, name) {
6712
6981
  return parsed;
6713
6982
  }
6714
6983
  async function fetchOfflineFileContentChunk(args) {
6715
- const response = await fetch(
6716
- offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/file-content"),
6717
- {
6718
- method: "POST",
6719
- headers: {
6720
- authorization: `Bearer ${args.token}`,
6721
- "content-type": "application/json"
6722
- },
6723
- body: JSON.stringify({
6724
- namespace: args.namespace,
6725
- includeTranscripts: args.includeTranscripts,
6726
- path: args.path,
6727
- offset: args.offset,
6728
- length: args.length
6729
- })
6984
+ const url = offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/file-content");
6985
+ const init = {
6986
+ method: "POST",
6987
+ body: JSON.stringify({
6988
+ namespace: args.namespace,
6989
+ includeTranscripts: args.includeTranscripts,
6990
+ path: args.path,
6991
+ offset: args.offset,
6992
+ length: args.length
6993
+ })
6994
+ };
6995
+ return fetchOfflineWithResponse(
6996
+ url,
6997
+ args.token,
6998
+ init,
6999
+ { defaultContentType: "application/json" },
7000
+ async (response) => {
7001
+ if (!response.ok) {
7002
+ await throwOfflineResponseError(response, url, init, "offline sync file-content request");
7003
+ }
7004
+ const encodedPath = response.headers.get("x-remnic-file-path");
7005
+ const relPath = encodedPath ? decodeURIComponent(encodedPath) : args.path;
7006
+ const content = Buffer.from(await response.arrayBuffer());
7007
+ const chunkBytes = parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-bytes");
7008
+ const sha256 = response.headers.get("x-remnic-file-sha256") ?? void 0;
7009
+ if (content.length !== chunkBytes) {
7010
+ throw new Error(`offline file content response length mismatch for ${relPath}`);
7011
+ }
7012
+ return {
7013
+ path: relPath,
7014
+ ...sha256 ? { sha256 } : {},
7015
+ bytes: parseOfflineHeaderNumber(response.headers, "x-remnic-file-bytes"),
7016
+ mtimeMs: parseOfflineHeaderNumber(response.headers, "x-remnic-file-mtime-ms"),
7017
+ offset: parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-offset"),
7018
+ chunkBytes,
7019
+ content
7020
+ };
6730
7021
  }
6731
7022
  );
6732
- if (!response.ok) {
6733
- let detail = "";
6734
- try {
6735
- detail = await response.text();
6736
- } catch {
6737
- detail = "";
6738
- }
6739
- throw new Error(
6740
- `offline sync file-content request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
6741
- );
6742
- }
6743
- const encodedPath = response.headers.get("x-remnic-file-path");
6744
- const relPath = encodedPath ? decodeURIComponent(encodedPath) : args.path;
6745
- const content = Buffer.from(await response.arrayBuffer());
6746
- const chunkBytes = parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-bytes");
6747
- const sha256 = response.headers.get("x-remnic-file-sha256") ?? void 0;
6748
- if (content.length !== chunkBytes) {
6749
- throw new Error(`offline file content response length mismatch for ${relPath}`);
6750
- }
6751
- return {
6752
- path: relPath,
6753
- ...sha256 ? { sha256 } : {},
6754
- bytes: parseOfflineHeaderNumber(response.headers, "x-remnic-file-bytes"),
6755
- mtimeMs: parseOfflineHeaderNumber(response.headers, "x-remnic-file-mtime-ms"),
6756
- offset: parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-offset"),
6757
- chunkBytes,
6758
- content
6759
- };
6760
7023
  }
6761
7024
  async function postOfflineFileContentChunk(args) {
6762
- const response = await fetch(
6763
- offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/apply-file-content", {
6764
- namespace: args.namespace
6765
- }),
6766
- {
6767
- method: "POST",
6768
- headers: {
6769
- authorization: `Bearer ${args.token}`,
6770
- "content-type": "application/octet-stream",
6771
- "x-remnic-include-transcripts": args.includeTranscripts ? "true" : "false",
6772
- "x-remnic-source-id": encodeURIComponent(args.sourceId),
6773
- "x-remnic-file-path": encodeURIComponent(args.file.path),
6774
- "x-remnic-file-sha256": args.file.sha256,
6775
- "x-remnic-file-bytes": String(args.file.bytes),
6776
- "x-remnic-file-mtime-ms": String(args.file.mtimeMs),
6777
- "x-remnic-chunk-offset": String(args.offset),
6778
- ...args.baseSha256 ? { "x-remnic-base-sha256": args.baseSha256 } : {}
6779
- },
6780
- body: new Blob([new Uint8Array(args.content)])
7025
+ const url = offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/apply-file-content", {
7026
+ namespace: args.namespace
7027
+ });
7028
+ const init = {
7029
+ method: "POST",
7030
+ headers: {
7031
+ "content-type": "application/octet-stream",
7032
+ "x-remnic-include-transcripts": args.includeTranscripts ? "true" : "false",
7033
+ "x-remnic-source-id": encodeURIComponent(args.sourceId),
7034
+ "x-remnic-file-path": encodeURIComponent(args.file.path),
7035
+ "x-remnic-file-sha256": args.file.sha256,
7036
+ "x-remnic-file-bytes": String(args.file.bytes),
7037
+ "x-remnic-file-mtime-ms": String(args.file.mtimeMs),
7038
+ "x-remnic-chunk-offset": String(args.offset),
7039
+ ...args.baseSha256 ? { "x-remnic-base-sha256": args.baseSha256 } : {}
7040
+ },
7041
+ body: new Blob([new Uint8Array(args.content)])
7042
+ };
7043
+ return fetchOfflineWithResponse(
7044
+ url,
7045
+ args.token,
7046
+ init,
7047
+ {},
7048
+ async (response) => {
7049
+ if (!response.ok) {
7050
+ await throwOfflineResponseError(response, url, init, "offline sync apply-file-content request");
7051
+ }
7052
+ return await response.json();
6781
7053
  }
6782
7054
  );
6783
- if (!response.ok) {
6784
- let detail = "";
6785
- try {
6786
- detail = await response.text();
6787
- } catch {
6788
- detail = "";
6789
- }
6790
- throw new Error(
6791
- `offline sync apply-file-content request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
6792
- );
6793
- }
6794
- return await response.json();
6795
7055
  }
6796
7056
  function resolvedOfflineSnapshotNamespace(snapshot, requestedNamespace) {
6797
7057
  const resolved = typeof snapshot.namespace === "string" && snapshot.namespace.trim().length > 0 ? snapshot.namespace.trim() : void 0;
@@ -6831,10 +7091,29 @@ function offlineFileStateMap(files) {
6831
7091
  function offlineSnapshotContentFilesForApply(options) {
6832
7092
  const base = offlineFileStateMap(options.baseFiles);
6833
7093
  const current = options.currentFiles ? offlineFileStateMap(options.currentFiles) : null;
7094
+ const conflictContentMaxBytes = options.conflictContentMaxBytes ?? Number.POSITIVE_INFINITY;
7095
+ const deferredPaths = new Set(options.deferredPaths ?? []);
6834
7096
  const files = [];
6835
7097
  for (const incoming of options.snapshot.files) {
6836
- if (current?.get(incoming.path)?.sha256 === incoming.sha256) continue;
6837
- if (base.get(incoming.path)?.sha256 === incoming.sha256) continue;
7098
+ if (deferredPaths.has(incoming.path)) continue;
7099
+ const baseEntry = base.get(incoming.path);
7100
+ const currentEntry = current?.get(incoming.path);
7101
+ if (currentEntry?.sha256 === incoming.sha256) continue;
7102
+ if (currentEntry && baseEntry && incoming.sha256 === baseEntry.sha256) continue;
7103
+ if (shouldPreferIncomingOfflineRuntimeFile(incoming.path)) {
7104
+ files.push(incoming);
7105
+ continue;
7106
+ }
7107
+ if (!currentEntry && baseEntry && incoming.sha256 === baseEntry.sha256) continue;
7108
+ if (!currentEntry && !baseEntry) {
7109
+ files.push(incoming);
7110
+ continue;
7111
+ }
7112
+ if (baseEntry && currentEntry && currentEntry.sha256 === baseEntry.sha256) {
7113
+ files.push(incoming);
7114
+ continue;
7115
+ }
7116
+ if (incoming.bytes > conflictContentMaxBytes) continue;
6838
7117
  files.push(incoming);
6839
7118
  }
6840
7119
  return files.sort((left, right) => left.path.localeCompare(right.path));
@@ -6842,15 +7121,24 @@ function offlineSnapshotContentFilesForApply(options) {
6842
7121
  function shouldDirectHydrateOfflineFile(options) {
6843
7122
  if (options.incoming.bytes < OFFLINE_SYNC_DIRECT_HYDRATE_MIN_BYTES) return false;
6844
7123
  if (options.current?.sha256 === options.incoming.sha256) return false;
7124
+ if (shouldPreferIncomingOfflineRuntimeFile(options.incoming.path)) return true;
6845
7125
  if (options.current && options.base && options.current.sha256 === options.base.sha256) {
6846
7126
  return true;
6847
7127
  }
6848
7128
  return !options.current && !options.base;
6849
7129
  }
7130
+ function offlinePartialHydrationForPaths(options) {
7131
+ const hydratedPaths = new Set(options.hydratedPaths);
7132
+ return {
7133
+ hydratedFiles: options.files.filter((file) => hydratedPaths.has(file.path)),
7134
+ remoteDeferredPaths: [...options.deferredPaths]
7135
+ };
7136
+ }
6850
7137
  function offlineDirectPushFiles(options) {
6851
7138
  const base = offlineFileStateMap(options.baseFiles);
6852
7139
  return options.currentFiles.filter((current) => {
6853
7140
  if (current.bytes < OFFLINE_SYNC_DIRECT_PUSH_MIN_BYTES) return false;
7141
+ if (shouldPreferIncomingOfflineRuntimeFile(current.path)) return false;
6854
7142
  return current.sha256 !== base.get(current.path)?.sha256;
6855
7143
  }).sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path));
6856
7144
  }
@@ -6870,6 +7158,9 @@ async function pushOfflineFileContent(args) {
6870
7158
  }
6871
7159
  let offset = 0;
6872
7160
  let finalResult = null;
7161
+ let remoteSatisfiedResult = null;
7162
+ const hash = createHash("sha256");
7163
+ let bytes = 0;
6873
7164
  while (offset < args.file.bytes || args.file.bytes === 0 && offset === 0) {
6874
7165
  const chunk = await readOfflineSyncFileContentChunk({
6875
7166
  root: args.memoryDir,
@@ -6888,23 +7179,36 @@ async function pushOfflineFileContent(args) {
6888
7179
  if (chunk.chunkBytes === 0 && args.file.bytes > 0) {
6889
7180
  throw new Error(`local offline content chunk was empty before EOF: ${args.file.path}`);
6890
7181
  }
6891
- finalResult = await postOfflineFileContentChunk({
6892
- remoteUrl: args.remoteUrl,
6893
- token: args.token,
6894
- namespace: args.namespace,
6895
- includeTranscripts: args.includeTranscripts,
6896
- sourceId: args.sourceId,
6897
- file: args.file,
6898
- baseSha256: args.baseSha256,
6899
- offset,
6900
- content: chunk.content
6901
- });
6902
- if (finalResult.conflict) {
6903
- return finalResult;
7182
+ hash.update(chunk.content);
7183
+ bytes += chunk.chunkBytes;
7184
+ if (!remoteSatisfiedResult) {
7185
+ finalResult = await postOfflineFileContentChunk({
7186
+ remoteUrl: args.remoteUrl,
7187
+ token: args.token,
7188
+ namespace: args.namespace,
7189
+ includeTranscripts: args.includeTranscripts,
7190
+ sourceId: args.sourceId,
7191
+ file: args.file,
7192
+ baseSha256: args.baseSha256,
7193
+ offset,
7194
+ content: chunk.content
7195
+ });
7196
+ if (finalResult.conflict) {
7197
+ return finalResult;
7198
+ }
7199
+ if (finalResult.done && finalResult.skipped) {
7200
+ remoteSatisfiedResult = finalResult;
7201
+ }
6904
7202
  }
6905
7203
  offset += chunk.chunkBytes;
6906
7204
  if (args.file.bytes === 0) break;
6907
7205
  }
7206
+ if (hash.digest("hex") !== args.file.sha256 || bytes !== args.file.bytes) {
7207
+ throw new Error(`local file changed while pushing offline content: ${args.file.path}`);
7208
+ }
7209
+ if (remoteSatisfiedResult) {
7210
+ return remoteSatisfiedResult;
7211
+ }
6908
7212
  if (!finalResult?.done) {
6909
7213
  throw new Error(`offline sync large-file push did not finish for ${args.file.path}`);
6910
7214
  }
@@ -6926,6 +7230,7 @@ async function pushOfflineFileContentFromChunkReader(args) {
6926
7230
  let offset = 0;
6927
7231
  let pending = null;
6928
7232
  let finalResult = null;
7233
+ let remoteSatisfiedResult = null;
6929
7234
  for await (const rawChunk of chunks) {
6930
7235
  const chunk = Buffer.from(rawChunk);
6931
7236
  if (chunk.length === 0) continue;
@@ -6934,19 +7239,24 @@ async function pushOfflineFileContentFromChunkReader(args) {
6934
7239
  }
6935
7240
  if (pending) {
6936
7241
  hash.update(pending);
6937
- finalResult = await postOfflineFileContentChunk({
6938
- remoteUrl: args.remoteUrl,
6939
- token: args.token,
6940
- namespace: args.namespace,
6941
- includeTranscripts: args.includeTranscripts,
6942
- sourceId: args.sourceId,
6943
- file: args.file,
6944
- baseSha256: args.baseSha256,
6945
- offset,
6946
- content: pending
6947
- });
6948
- if (finalResult.conflict) {
6949
- return finalResult;
7242
+ if (!remoteSatisfiedResult) {
7243
+ finalResult = await postOfflineFileContentChunk({
7244
+ remoteUrl: args.remoteUrl,
7245
+ token: args.token,
7246
+ namespace: args.namespace,
7247
+ includeTranscripts: args.includeTranscripts,
7248
+ sourceId: args.sourceId,
7249
+ file: args.file,
7250
+ baseSha256: args.baseSha256,
7251
+ offset,
7252
+ content: pending
7253
+ });
7254
+ if (finalResult.conflict) {
7255
+ return finalResult;
7256
+ }
7257
+ if (finalResult.done && finalResult.skipped) {
7258
+ remoteSatisfiedResult = finalResult;
7259
+ }
6950
7260
  }
6951
7261
  offset += pending.length;
6952
7262
  }
@@ -6958,6 +7268,9 @@ async function pushOfflineFileContentFromChunkReader(args) {
6958
7268
  if (digest !== args.file.sha256 || finalBytes !== args.file.bytes) {
6959
7269
  throw new Error(`local file changed while pushing offline content: ${args.file.path}`);
6960
7270
  }
7271
+ if (remoteSatisfiedResult) {
7272
+ return remoteSatisfiedResult;
7273
+ }
6961
7274
  if (pending) {
6962
7275
  finalResult = await postOfflineFileContentChunk({
6963
7276
  remoteUrl: args.remoteUrl,
@@ -6973,6 +7286,9 @@ async function pushOfflineFileContentFromChunkReader(args) {
6973
7286
  if (finalResult.conflict) {
6974
7287
  return finalResult;
6975
7288
  }
7289
+ if (finalResult.done && finalResult.skipped) {
7290
+ return finalResult;
7291
+ }
6976
7292
  } else if (args.file.bytes === 0) {
6977
7293
  finalResult = await postOfflineFileContentChunk({
6978
7294
  remoteUrl: args.remoteUrl,
@@ -6991,11 +7307,10 @@ async function pushOfflineFileContentFromChunkReader(args) {
6991
7307
  }
6992
7308
  return finalResult;
6993
7309
  }
6994
- async function fetchOfflineFileContent(args) {
6995
- const chunks = [];
6996
- const hash = createHash("sha256");
7310
+ async function hydrateOfflineFileContent(args) {
6997
7311
  let offset = 0;
6998
- while (offset < args.expected.bytes) {
7312
+ let finalResult = null;
7313
+ while (offset < args.expected.bytes || args.expected.bytes === 0 && offset === 0) {
6999
7314
  const chunk = await fetchOfflineFileContentChunk({
7000
7315
  remoteUrl: args.remoteUrl,
7001
7316
  token: args.token,
@@ -7005,54 +7320,92 @@ async function fetchOfflineFileContent(args) {
7005
7320
  offset,
7006
7321
  length: Math.min(
7007
7322
  OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
7008
- args.expected.bytes - offset
7323
+ Math.max(1, args.expected.bytes - offset)
7009
7324
  )
7010
7325
  });
7011
7326
  if (chunk.path !== args.expected.path || chunk.sha256 !== void 0 && chunk.sha256 !== args.expected.sha256 || chunk.bytes !== args.expected.bytes || chunk.mtimeMs !== args.expected.mtimeMs || chunk.offset !== offset || chunk.chunkBytes !== chunk.content.length) {
7012
- throw new Error(`remote file changed while fetching offline content: ${args.expected.path}`);
7327
+ throw new OfflineRemoteFileChangedError(args.expected.path);
7013
7328
  }
7014
- if (chunk.chunkBytes === 0) {
7329
+ if (chunk.chunkBytes === 0 && args.expected.bytes > 0) {
7015
7330
  throw new Error(`remote offline content chunk was empty before EOF: ${args.expected.path}`);
7016
7331
  }
7017
- chunks.push(chunk.content);
7018
- hash.update(chunk.content);
7332
+ finalResult = await applyOfflineSyncFileContentChunk({
7333
+ root: args.memoryDir,
7334
+ sourceId: args.sourceId,
7335
+ path: args.expected.path,
7336
+ sha256: args.expected.sha256,
7337
+ bytes: args.expected.bytes,
7338
+ mtimeMs: args.expected.mtimeMs,
7339
+ offset,
7340
+ content: chunk.content,
7341
+ ...args.baseSha256 ? { baseSha256: args.baseSha256 } : {},
7342
+ includeTranscripts: args.includeTranscripts,
7343
+ readFile: args.readFile,
7344
+ readFileDigest: args.readFileDigest,
7345
+ writeFile: args.writeFile,
7346
+ writeStagingFile: args.writeStagingFile,
7347
+ writeFileChunks: args.writeFileChunks
7348
+ });
7349
+ if (finalResult.conflict) {
7350
+ return finalResult;
7351
+ }
7352
+ if (finalResult.done && finalResult.skipped) {
7353
+ return finalResult;
7354
+ }
7019
7355
  offset += chunk.chunkBytes;
7356
+ if (args.expected.bytes === 0) break;
7020
7357
  }
7021
- const content = Buffer.concat(chunks, offset);
7022
- const digest = hash.digest("hex");
7023
- if (digest !== args.expected.sha256 || content.length !== args.expected.bytes) {
7024
- throw new Error(`remote offline content checksum mismatch for ${args.expected.path}`);
7358
+ if (!finalResult?.done) {
7359
+ throw new Error(`offline sync large-file hydrate did not finish for ${args.expected.path}`);
7025
7360
  }
7026
- return content;
7361
+ return finalResult;
7027
7362
  }
7028
7363
  async function directHydrateLargeOfflineFiles(args) {
7029
- if (!args.writeFile) return /* @__PURE__ */ new Set();
7364
+ if (!args.readFile || !args.writeFile || !args.writeStagingFile || !args.writeFileChunks) {
7365
+ return { hydratedPaths: /* @__PURE__ */ new Set(), deferredPaths: /* @__PURE__ */ new Set() };
7366
+ }
7030
7367
  const snapshot = normalizeOfflineSyncSnapshot(args.snapshot);
7031
7368
  const base = offlineFileStateMap(args.baseFiles);
7032
7369
  const current = offlineFileStateMap(args.currentFiles);
7033
- const hydrated = /* @__PURE__ */ new Set();
7370
+ const hydratedPaths = args.hydrationProgress?.hydratedPaths ?? /* @__PURE__ */ new Set();
7371
+ const deferredPaths = args.hydrationProgress?.deferredPaths ?? /* @__PURE__ */ new Set();
7034
7372
  const candidates = snapshot.files.filter((incoming) => shouldDirectHydrateOfflineFile({
7035
7373
  incoming,
7036
7374
  base: base.get(incoming.path),
7037
7375
  current: current.get(incoming.path)
7038
7376
  })).sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path));
7039
7377
  for (const incoming of candidates) {
7040
- const content = await fetchOfflineFileContent({
7041
- remoteUrl: args.remoteUrl,
7042
- token: args.token,
7043
- namespace: args.namespace,
7044
- includeTranscripts: args.includeTranscripts,
7045
- expected: incoming
7046
- });
7047
- await args.writeFile({
7048
- root: args.memoryDir,
7049
- path: incoming.path,
7050
- filePath: resolveOfflineDirectHydrationPath(args.memoryDir, incoming.path),
7051
- content
7052
- });
7053
- hydrated.add(incoming.path);
7378
+ let result;
7379
+ try {
7380
+ result = await hydrateOfflineFileContent({
7381
+ remoteUrl: args.remoteUrl,
7382
+ token: args.token,
7383
+ namespace: args.namespace,
7384
+ includeTranscripts: args.includeTranscripts,
7385
+ memoryDir: args.memoryDir,
7386
+ sourceId: "remote",
7387
+ expected: incoming,
7388
+ baseSha256: base.get(incoming.path)?.sha256,
7389
+ readFile: args.readFile,
7390
+ readFileDigest: args.readFileDigest,
7391
+ writeFile: args.writeFile,
7392
+ writeStagingFile: args.writeStagingFile,
7393
+ writeFileChunks: args.writeFileChunks
7394
+ });
7395
+ } catch (error) {
7396
+ if (!isOfflineRemoteFileChangedError(error)) throw error;
7397
+ deferredPaths.add(incoming.path);
7398
+ continue;
7399
+ }
7400
+ if (result.conflict) {
7401
+ deferredPaths.add(result.conflict.path);
7402
+ continue;
7403
+ }
7404
+ if (result.applied || result.skipped) {
7405
+ hydratedPaths.add(incoming.path);
7406
+ }
7054
7407
  }
7055
- return hydrated;
7408
+ return { hydratedPaths, deferredPaths };
7056
7409
  }
7057
7410
  function chunkOfflineFileContentBatches(files) {
7058
7411
  const chunks = [];
@@ -7076,7 +7429,7 @@ function chunkOfflineFileContentBatches(files) {
7076
7429
  }
7077
7430
  function isOfflineFilesUnsupportedError(error) {
7078
7431
  const message = error instanceof Error ? error.message : String(error);
7079
- return /offline sync request failed: 404\b/.test(message);
7432
+ return /offline sync request failed: .* returned 404\b/.test(message);
7080
7433
  }
7081
7434
  function isMissingOfflineContentError(error) {
7082
7435
  const message = error instanceof Error ? error.message : String(error);
@@ -7087,7 +7440,9 @@ async function hydrateOfflineSnapshotContent(args) {
7087
7440
  const neededFiles = offlineSnapshotContentFilesForApply({
7088
7441
  snapshot,
7089
7442
  baseFiles: args.baseFiles,
7090
- currentFiles: args.currentFiles
7443
+ currentFiles: args.currentFiles,
7444
+ conflictContentMaxBytes: OFFLINE_SYNC_FILES_CONTENT_MAX_BATCH_BYTES,
7445
+ deferredPaths: args.deferredPaths
7091
7446
  });
7092
7447
  if (neededFiles.length === 0) return { ...args.snapshot, files: snapshot.files };
7093
7448
  const expectedByPath = new Map(snapshot.files.map((file) => [file.path, file]));
@@ -7180,7 +7535,8 @@ async function postOfflineChangesBatch(args) {
7180
7535
  method: "POST",
7181
7536
  body: JSON.stringify({
7182
7537
  namespace: args.namespace,
7183
- changeset: args.changeset
7538
+ changeset: args.changeset,
7539
+ returnCurrentFiles: false
7184
7540
  })
7185
7541
  }
7186
7542
  );
@@ -7201,6 +7557,7 @@ async function pushOfflineChanges(args) {
7201
7557
  appliedDeletes += result.appliedDeletes;
7202
7558
  skipped += result.skipped;
7203
7559
  conflicts.push(...result.conflicts);
7560
+ args.onBatchApplied?.({ changeset, result });
7204
7561
  }
7205
7562
  return {
7206
7563
  namespace,
@@ -7246,6 +7603,24 @@ async function createOfflineStorageIo(memoryDir) {
7246
7603
  }
7247
7604
  return {
7248
7605
  readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
7606
+ readFileDigest: async ({ filePath }) => {
7607
+ const hash = createHash("sha256");
7608
+ let bytes = 0;
7609
+ for await (const rawChunk of readOfflineSyncFileChunks({
7610
+ filePath,
7611
+ memoryDir,
7612
+ secureStoreKey,
7613
+ chunkSize: OFFLINE_SYNC_FILE_CONTENT_UPLOAD_CHUNK_BYTES
7614
+ })) {
7615
+ const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
7616
+ hash.update(chunk);
7617
+ bytes += chunk.length;
7618
+ }
7619
+ return {
7620
+ sha256: hash.digest("hex"),
7621
+ bytes
7622
+ };
7623
+ },
7249
7624
  readFileChunks: ({ filePath, chunkSize }) => readOfflineSyncFileChunks({
7250
7625
  filePath,
7251
7626
  memoryDir,
@@ -7253,6 +7628,8 @@ async function createOfflineStorageIo(memoryDir) {
7253
7628
  chunkSize
7254
7629
  }),
7255
7630
  writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
7631
+ writeStagingFile: async ({ filePath, content }) => storage.writeOfflineSyncStagingFile(filePath, content),
7632
+ writeFileChunks: async ({ filePath, chunks }) => storage.writeOfflineSyncFileChunks(filePath, chunks),
7256
7633
  deleteFile: async ({ filePath }) => storage.deleteOfflineSyncFile(filePath)
7257
7634
  };
7258
7635
  }
@@ -7366,6 +7743,39 @@ var OfflineLargeFilePushError = class extends Error {
7366
7743
  this.failures = failures;
7367
7744
  }
7368
7745
  };
7746
+ function advanceOfflineBaseFilesForSuccessfulPush(options) {
7747
+ const next = offlineFileStateMap(options.baseFiles);
7748
+ const current = offlineFileStateMap(options.currentFiles);
7749
+ const conflictPaths = new Set((options.conflicts ?? []).map((conflict) => conflict.path));
7750
+ for (const relPath of options.directPushedPaths ?? []) {
7751
+ if (conflictPaths.has(relPath)) continue;
7752
+ const file = current.get(relPath);
7753
+ if (file) next.set(relPath, file);
7754
+ }
7755
+ for (const change of options.changeset.changes) {
7756
+ if (conflictPaths.has(change.path)) continue;
7757
+ if (change.type === "delete") {
7758
+ next.delete(change.path);
7759
+ } else {
7760
+ next.set(change.path, {
7761
+ path: change.file.path,
7762
+ sha256: change.file.sha256,
7763
+ bytes: change.file.bytes,
7764
+ mtimeMs: change.file.mtimeMs
7765
+ });
7766
+ }
7767
+ }
7768
+ for (const file of options.hydratedFiles ?? []) {
7769
+ if (conflictPaths.has(file.path)) continue;
7770
+ next.set(file.path, {
7771
+ path: file.path,
7772
+ sha256: file.sha256,
7773
+ bytes: file.bytes,
7774
+ mtimeMs: file.mtimeMs
7775
+ });
7776
+ }
7777
+ return [...next.values()].sort((left, right) => left.path.localeCompare(right.path));
7778
+ }
7369
7779
  async function runOfflineSyncOnce(options) {
7370
7780
  fs7.mkdirSync(options.memoryDir, { recursive: true });
7371
7781
  let activeStatePath = options.statePath;
@@ -7404,21 +7814,23 @@ async function runOfflineSyncOnce(options) {
7404
7814
  });
7405
7815
  }
7406
7816
  const baseFiles = priorState?.baseFiles ?? [];
7817
+ const baseCapturedAt = priorState ? new Date(priorState.lastSyncedAt) : void 0;
7407
7818
  const storageIo = await createOfflineStorageIo(options.memoryDir);
7408
7819
  const localSourceId = localOfflineSourceId(options.memoryDir);
7409
- const pendingSummary = await summarizeOfflineSyncPendingChanges({
7820
+ const currentSnapshotForPush = await buildOfflineSyncSnapshotFromBase({
7410
7821
  root: options.memoryDir,
7411
7822
  sourceId: localSourceId,
7412
7823
  baseFiles,
7413
- includeTranscripts: options.includeTranscripts,
7414
- readFile: storageIo.readFile
7415
- });
7416
- const currentSnapshotForPush = await buildOfflineSyncSnapshot({
7417
- root: options.memoryDir,
7418
- sourceId: localSourceId,
7824
+ baseCapturedAt,
7419
7825
  includeContent: false,
7420
7826
  includeTranscripts: options.includeTranscripts,
7421
- readFile: storageIo.readFile
7827
+ readFile: storageIo.readFile,
7828
+ readFileDigest: storageIo.readFileDigest
7829
+ });
7830
+ const pendingSummary = summarizeOfflineSyncPendingFiles({
7831
+ baseFiles,
7832
+ currentFiles: currentSnapshotForPush.files,
7833
+ includeTranscripts: options.includeTranscripts
7422
7834
  });
7423
7835
  const baseByPath = offlineFileStateMap(baseFiles);
7424
7836
  let directPushAppliedUpserts = 0;
@@ -7426,6 +7838,7 @@ async function runOfflineSyncOnce(options) {
7426
7838
  let directPushNamespace;
7427
7839
  const directPushConflicts = [];
7428
7840
  const directPushedPaths = /* @__PURE__ */ new Set();
7841
+ const directPushDeferredPaths = /* @__PURE__ */ new Set();
7429
7842
  const directPushFailures = [];
7430
7843
  for (const file of offlineDirectPushFiles({
7431
7844
  currentFiles: currentSnapshotForPush.files,
@@ -7446,6 +7859,10 @@ async function runOfflineSyncOnce(options) {
7446
7859
  readFileChunks: storageIo.readFileChunks
7447
7860
  });
7448
7861
  } catch (error) {
7862
+ if (isOfflineLocalFileChangedError(error)) {
7863
+ directPushDeferredPaths.add(file.path);
7864
+ continue;
7865
+ }
7449
7866
  directPushFailures.push({
7450
7867
  path: file.path,
7451
7868
  error: error instanceof Error ? error.message : String(error)
@@ -7465,100 +7882,343 @@ async function runOfflineSyncOnce(options) {
7465
7882
  if (result.skipped) directPushSkipped += 1;
7466
7883
  }
7467
7884
  }
7468
- if (directPushFailures.length > 0) {
7469
- throw new OfflineLargeFilePushError(directPushFailures);
7470
- }
7471
- const changeset = await buildOfflineSyncChangeset({
7472
- root: options.memoryDir,
7885
+ let changeset = {
7886
+ format: "remnic.offline-sync.changeset.v1",
7887
+ schemaVersion: 1,
7888
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7473
7889
  sourceId: localSourceId,
7474
- baseFiles,
7475
- excludePaths: [...directPushedPaths],
7476
7890
  includeTranscripts: options.includeTranscripts,
7477
- readFile: storageIo.readFile
7478
- });
7479
- const pushedInline = changeset.changes.length > 0 ? await pushOfflineChanges({
7480
- remoteUrl: options.remoteUrl,
7481
- token: options.token,
7482
- namespace: syncNamespace,
7483
- changeset
7484
- }) : null;
7485
- const pushed = directPushedPaths.size > 0 || pushedInline ? {
7486
- namespace: pushedInline?.namespace ?? directPushNamespace ?? syncNamespace ?? "",
7487
- appliedUpserts: (pushedInline?.appliedUpserts ?? 0) + directPushAppliedUpserts,
7488
- appliedDeletes: pushedInline?.appliedDeletes ?? 0,
7489
- skipped: (pushedInline?.skipped ?? 0) + directPushSkipped,
7490
- conflicts: [...directPushConflicts, ...pushedInline?.conflicts ?? []]
7891
+ changes: []
7892
+ };
7893
+ let pushed = null;
7894
+ const buildPushedSummary = (pushedInline2) => directPushedPaths.size > 0 || pushedInline2 ? {
7895
+ namespace: pushedInline2?.namespace ?? directPushNamespace ?? syncNamespace ?? "",
7896
+ appliedUpserts: (pushedInline2?.appliedUpserts ?? 0) + directPushAppliedUpserts,
7897
+ appliedDeletes: pushedInline2?.appliedDeletes ?? 0,
7898
+ skipped: (pushedInline2?.skipped ?? 0) + directPushSkipped,
7899
+ conflicts: [...directPushConflicts, ...pushedInline2?.conflicts ?? []]
7491
7900
  } : null;
7492
- const remoteSnapshotMetadata = await fetchOfflineSnapshot({
7493
- remoteUrl: options.remoteUrl,
7494
- token: options.token,
7495
- namespace: syncNamespace,
7496
- includeTranscripts: options.includeTranscripts,
7497
- includeContent: false
7498
- });
7499
- const currentSnapshot = await buildOfflineSyncSnapshot({
7500
- root: options.memoryDir,
7501
- sourceId: localSourceId,
7502
- includeContent: false,
7503
- includeTranscripts: options.includeTranscripts,
7504
- readFile: storageIo.readFile
7901
+ const mergeInlinePushSummary = (prior, result) => ({
7902
+ namespace: result.namespace || prior?.namespace || syncNamespace || "",
7903
+ appliedUpserts: (prior?.appliedUpserts ?? 0) + result.appliedUpserts,
7904
+ appliedDeletes: (prior?.appliedDeletes ?? 0) + result.appliedDeletes,
7905
+ skipped: (prior?.skipped ?? 0) + result.skipped,
7906
+ conflicts: [...prior?.conflicts ?? [], ...result.conflicts]
7505
7907
  });
7506
- const directHydratedPaths = await directHydrateLargeOfflineFiles({
7507
- remoteUrl: options.remoteUrl,
7508
- token: options.token,
7509
- namespace: syncNamespace,
7510
- includeTranscripts: options.includeTranscripts,
7511
- snapshot: remoteSnapshotMetadata,
7512
- baseFiles,
7513
- currentFiles: currentSnapshot.files,
7908
+ pushed = buildPushedSummary(null);
7909
+ const stateWritePathsFor = (resolvedNamespace2) => offlineStatePathsForNamespace({
7514
7910
  memoryDir: options.memoryDir,
7515
- writeFile: storageIo.writeFile
7911
+ remoteUrl: options.remoteUrl,
7912
+ requestedNamespace: options.namespace,
7913
+ resolvedNamespace: resolvedNamespace2,
7914
+ explicitStatePath: options.statePathExplicit ? activeStatePath : void 0
7516
7915
  });
7517
- const applyCurrentSnapshot = directHydratedPaths.size > 0 ? await buildOfflineSyncSnapshot({
7916
+ const writePartialPushState = async (error, partial, checkpointChangeset = changeset) => {
7917
+ const resolvedNamespace2 = partial?.resolvedNamespace ?? resolvedOfflineSnapshotNamespace({ namespace: pushed?.namespace ?? "" }, syncNamespace);
7918
+ const stateWritePaths2 = stateWritePathsFor(resolvedNamespace2);
7919
+ const nextBaseFiles = advanceOfflineBaseFilesForSuccessfulPush({
7920
+ baseFiles,
7921
+ currentFiles: currentSnapshotForPush.files,
7922
+ directPushedPaths: [...directPushedPaths],
7923
+ hydratedFiles: partial?.hydratedFiles,
7924
+ changeset: checkpointChangeset,
7925
+ conflicts: pushed?.conflicts ?? directPushConflicts
7926
+ });
7927
+ const state2 = {
7928
+ version: 1,
7929
+ remoteId: options.remoteUrl,
7930
+ ...resolvedNamespace2 ? { namespace: resolvedNamespace2 } : {},
7931
+ includeTranscripts: options.includeTranscripts,
7932
+ // Partial checkpoints do not recapture conflicted/deferred paths, so keep
7933
+ // the original capture time for safe fast-base reuse on the next run.
7934
+ lastSyncedAt: priorState?.lastSyncedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7935
+ baseFiles: nextBaseFiles
7936
+ };
7937
+ for (const statePath of stateWritePaths2) {
7938
+ await writeOfflineSyncState(statePath, state2);
7939
+ }
7940
+ const message = error instanceof Error ? error.message : String(error);
7941
+ return {
7942
+ statePath: stateWritePaths2[0] ?? activeStatePath,
7943
+ namespace: resolvedNamespace2,
7944
+ prepared: priorState === null,
7945
+ pushed,
7946
+ pull: null,
7947
+ pullError: message,
7948
+ partial: true,
7949
+ pendingSummary,
7950
+ remoteFileCount: partial?.remoteFileCount ?? null,
7951
+ deferred: {
7952
+ localChangedDuringPush: [...directPushDeferredPaths].sort(),
7953
+ remoteChangedDuringHydrate: [...partial?.remoteDeferredPaths ?? []].sort(),
7954
+ total: directPushDeferredPaths.size + (partial?.remoteDeferredPaths?.length ?? 0)
7955
+ }
7956
+ };
7957
+ };
7958
+ if (directPushFailures.length > 0) {
7959
+ const error = new OfflineLargeFilePushError(directPushFailures);
7960
+ if (pushed) return writePartialPushState(error);
7961
+ throw error;
7962
+ }
7963
+ let currentSnapshotForChangeset = directPushedPaths.size > 0 ? await buildOfflineSyncSnapshotFromBase({
7518
7964
  root: options.memoryDir,
7519
7965
  sourceId: localSourceId,
7966
+ baseFiles,
7967
+ baseCapturedAt,
7520
7968
  includeContent: false,
7521
7969
  includeTranscripts: options.includeTranscripts,
7522
- readFile: storageIo.readFile
7523
- }) : currentSnapshot;
7524
- const remoteSnapshot = await hydrateOfflineSnapshotContent({
7525
- remoteUrl: options.remoteUrl,
7526
- token: options.token,
7527
- namespace: syncNamespace,
7528
- includeTranscripts: options.includeTranscripts,
7529
- snapshot: remoteSnapshotMetadata,
7530
- baseFiles,
7531
- currentFiles: applyCurrentSnapshot.files
7532
- });
7533
- const resolvedNamespace = resolvedOfflineSnapshotNamespace(remoteSnapshot, syncNamespace);
7534
- let pull;
7970
+ readFile: storageIo.readFile,
7971
+ readFileDigest: storageIo.readFileDigest
7972
+ }) : currentSnapshotForPush;
7973
+ let changesetRetryCount = 0;
7974
+ for (; ; ) {
7975
+ try {
7976
+ changeset = await buildOfflineSyncChangesetFromSnapshot({
7977
+ root: options.memoryDir,
7978
+ sourceId: localSourceId,
7979
+ currentFiles: currentSnapshotForChangeset.files,
7980
+ baseFiles,
7981
+ excludePaths: [...directPushedPaths, ...directPushDeferredPaths],
7982
+ includeTranscripts: options.includeTranscripts,
7983
+ readFile: storageIo.readFile
7984
+ });
7985
+ break;
7986
+ } catch (error) {
7987
+ const changedPath = offlineChangesetFileChangedPath(error);
7988
+ if (!changedPath) {
7989
+ if (pushed) return writePartialPushState(error);
7990
+ throw error;
7991
+ }
7992
+ if (directPushDeferredPaths.has(changedPath)) {
7993
+ const stalledError = new Error(`offline sync changeset retry stalled on already-deferred path: ${changedPath}`);
7994
+ if (pushed) return writePartialPushState(stalledError);
7995
+ throw stalledError;
7996
+ }
7997
+ if (changesetRetryCount >= OFFLINE_SYNC_CHANGESET_RETRY_MAX) {
7998
+ const retryError = new Error(
7999
+ `offline sync changeset retry limit exceeded after ${OFFLINE_SYNC_CHANGESET_RETRY_MAX} volatile files; last changed path: ${changedPath}`
8000
+ );
8001
+ if (pushed) return writePartialPushState(retryError);
8002
+ throw retryError;
8003
+ }
8004
+ changesetRetryCount += 1;
8005
+ directPushDeferredPaths.add(changedPath);
8006
+ currentSnapshotForChangeset = await buildOfflineSyncSnapshotFromBase({
8007
+ root: options.memoryDir,
8008
+ sourceId: localSourceId,
8009
+ baseFiles,
8010
+ baseCapturedAt,
8011
+ includeContent: false,
8012
+ includeTranscripts: options.includeTranscripts,
8013
+ readFile: storageIo.readFile,
8014
+ readFileDigest: storageIo.readFileDigest
8015
+ });
8016
+ }
8017
+ }
8018
+ const inlineAppliedChanges = [];
8019
+ let pushedInlineProgress = null;
8020
+ let pushedInline = null;
7535
8021
  try {
7536
- pull = await applyOfflineSyncSnapshot({
8022
+ pushedInline = changeset.changes.length > 0 ? await pushOfflineChanges({
8023
+ remoteUrl: options.remoteUrl,
8024
+ token: options.token,
8025
+ namespace: syncNamespace,
8026
+ changeset,
8027
+ onBatchApplied: (batch) => {
8028
+ inlineAppliedChanges.push(...batch.changeset.changes);
8029
+ pushedInlineProgress = mergeInlinePushSummary(pushedInlineProgress, batch.result);
8030
+ pushed = buildPushedSummary(pushedInlineProgress);
8031
+ }
8032
+ }) : null;
8033
+ } catch (error) {
8034
+ if (pushed || inlineAppliedChanges.length > 0) {
8035
+ return writePartialPushState(error, void 0, {
8036
+ ...changeset,
8037
+ changes: inlineAppliedChanges
8038
+ });
8039
+ }
8040
+ throw error;
8041
+ }
8042
+ pushed = buildPushedSummary(pushedInline);
8043
+ let remoteSnapshotMetadata;
8044
+ try {
8045
+ remoteSnapshotMetadata = await fetchOfflineSnapshot({
8046
+ remoteUrl: options.remoteUrl,
8047
+ token: options.token,
8048
+ namespace: syncNamespace,
8049
+ includeTranscripts: options.includeTranscripts,
8050
+ includeContent: false,
8051
+ baseFiles,
8052
+ baseCapturedAt
8053
+ });
8054
+ } catch (error) {
8055
+ if (pushed) return writePartialPushState(error);
8056
+ throw error;
8057
+ }
8058
+ let currentSnapshot;
8059
+ try {
8060
+ currentSnapshot = await buildOfflineSyncSnapshotFromBase({
7537
8061
  root: options.memoryDir,
7538
- snapshot: remoteSnapshot,
8062
+ sourceId: localSourceId,
8063
+ baseFiles,
8064
+ baseCapturedAt,
8065
+ includeContent: false,
8066
+ includeTranscripts: options.includeTranscripts,
8067
+ readFile: storageIo.readFile,
8068
+ readFileDigest: storageIo.readFileDigest
8069
+ });
8070
+ } catch (error) {
8071
+ if (pushed) return writePartialPushState(error);
8072
+ throw error;
8073
+ }
8074
+ let directHydration;
8075
+ const directHydrationProgress = {
8076
+ hydratedPaths: /* @__PURE__ */ new Set(),
8077
+ deferredPaths: /* @__PURE__ */ new Set()
8078
+ };
8079
+ try {
8080
+ directHydration = await directHydrateLargeOfflineFiles({
8081
+ remoteUrl: options.remoteUrl,
8082
+ token: options.token,
8083
+ namespace: syncNamespace,
8084
+ includeTranscripts: options.includeTranscripts,
8085
+ snapshot: remoteSnapshotMetadata,
7539
8086
  baseFiles,
8087
+ currentFiles: currentSnapshot.files,
8088
+ memoryDir: options.memoryDir,
7540
8089
  readFile: storageIo.readFile,
8090
+ readFileDigest: storageIo.readFileDigest,
7541
8091
  writeFile: storageIo.writeFile,
7542
- deleteFile: storageIo.deleteFile
8092
+ writeStagingFile: storageIo.writeStagingFile,
8093
+ writeFileChunks: storageIo.writeFileChunks,
8094
+ hydrationProgress: directHydrationProgress
7543
8095
  });
7544
8096
  } catch (error) {
7545
- if (!isMissingOfflineContentError(error)) throw error;
7546
- const retrySnapshot = await hydrateOfflineSnapshotContent({
8097
+ const partial = offlinePartialHydrationForPaths({
8098
+ files: remoteSnapshotMetadata.files,
8099
+ hydratedPaths: directHydrationProgress.hydratedPaths,
8100
+ deferredPaths: directHydrationProgress.deferredPaths
8101
+ });
8102
+ if (pushed || partial.hydratedFiles.length > 0) {
8103
+ return writePartialPushState(error, {
8104
+ ...partial,
8105
+ resolvedNamespace: resolvedOfflineSnapshotNamespace(remoteSnapshotMetadata, syncNamespace),
8106
+ remoteFileCount: remoteSnapshotMetadata.files.length
8107
+ });
8108
+ }
8109
+ throw error;
8110
+ }
8111
+ const directHydratedPaths = directHydration.hydratedPaths;
8112
+ const remoteDeferredPaths = directHydration.deferredPaths;
8113
+ const partialHydration = offlinePartialHydrationForPaths({
8114
+ files: remoteSnapshotMetadata.files,
8115
+ hydratedPaths: directHydratedPaths,
8116
+ deferredPaths: remoteDeferredPaths
8117
+ });
8118
+ const partialHydrationWithContext = {
8119
+ ...partialHydration,
8120
+ resolvedNamespace: resolvedOfflineSnapshotNamespace(remoteSnapshotMetadata, syncNamespace),
8121
+ remoteFileCount: remoteSnapshotMetadata.files.length
8122
+ };
8123
+ const buildCurrentSnapshotForApply = async () => buildOfflineSyncSnapshotFromBase({
8124
+ root: options.memoryDir,
8125
+ sourceId: localSourceId,
8126
+ baseFiles,
8127
+ baseCapturedAt,
8128
+ includeContent: false,
8129
+ includeTranscripts: options.includeTranscripts,
8130
+ readFile: storageIo.readFile,
8131
+ readFileDigest: storageIo.readFileDigest
8132
+ });
8133
+ const applyCurrentSnapshot = directHydratedPaths.size > 0 ? await buildCurrentSnapshotForApply() : currentSnapshot;
8134
+ let remoteSnapshot;
8135
+ try {
8136
+ remoteSnapshot = await hydrateOfflineSnapshotContent({
7547
8137
  remoteUrl: options.remoteUrl,
7548
8138
  token: options.token,
7549
8139
  namespace: syncNamespace,
7550
8140
  includeTranscripts: options.includeTranscripts,
7551
8141
  snapshot: remoteSnapshotMetadata,
7552
- baseFiles
8142
+ baseFiles,
8143
+ currentFiles: applyCurrentSnapshot.files,
8144
+ deferredPaths: [...remoteDeferredPaths]
7553
8145
  });
8146
+ } catch (error) {
8147
+ if (pushed || partialHydration.hydratedFiles.length > 0) {
8148
+ return writePartialPushState(error, partialHydrationWithContext);
8149
+ }
8150
+ throw error;
8151
+ }
8152
+ const resolvedNamespace = resolvedOfflineSnapshotNamespace(remoteSnapshot, syncNamespace);
8153
+ let pull;
8154
+ try {
8155
+ const latestApplySnapshot = await buildCurrentSnapshotForApply();
7554
8156
  pull = await applyOfflineSyncSnapshot({
7555
8157
  root: options.memoryDir,
7556
- snapshot: retrySnapshot,
8158
+ snapshot: remoteSnapshot,
7557
8159
  baseFiles,
8160
+ currentFiles: latestApplySnapshot.files,
8161
+ deferredPaths: [...remoteDeferredPaths],
8162
+ allowMissingConflictContent: true,
7558
8163
  readFile: storageIo.readFile,
8164
+ readFileDigest: storageIo.readFileDigest,
7559
8165
  writeFile: storageIo.writeFile,
7560
8166
  deleteFile: storageIo.deleteFile
7561
8167
  });
8168
+ } catch (error) {
8169
+ if (!isMissingOfflineContentError(error)) {
8170
+ if (pushed || partialHydration.hydratedFiles.length > 0) {
8171
+ return writePartialPushState(error, {
8172
+ ...partialHydrationWithContext,
8173
+ resolvedNamespace
8174
+ });
8175
+ }
8176
+ throw error;
8177
+ }
8178
+ let retrySnapshot;
8179
+ try {
8180
+ retrySnapshot = await hydrateOfflineSnapshotContent({
8181
+ remoteUrl: options.remoteUrl,
8182
+ token: options.token,
8183
+ namespace: syncNamespace,
8184
+ includeTranscripts: options.includeTranscripts,
8185
+ snapshot: remoteSnapshotMetadata,
8186
+ baseFiles,
8187
+ currentFiles: applyCurrentSnapshot.files,
8188
+ deferredPaths: [...remoteDeferredPaths]
8189
+ });
8190
+ } catch (retryError) {
8191
+ if (pushed || partialHydration.hydratedFiles.length > 0) {
8192
+ return writePartialPushState(retryError, {
8193
+ ...partialHydrationWithContext,
8194
+ resolvedNamespace
8195
+ });
8196
+ }
8197
+ throw retryError;
8198
+ }
8199
+ try {
8200
+ const latestRetryApplySnapshot = await buildCurrentSnapshotForApply();
8201
+ pull = await applyOfflineSyncSnapshot({
8202
+ root: options.memoryDir,
8203
+ snapshot: retrySnapshot,
8204
+ baseFiles,
8205
+ currentFiles: latestRetryApplySnapshot.files,
8206
+ deferredPaths: [...remoteDeferredPaths],
8207
+ allowMissingConflictContent: true,
8208
+ readFile: storageIo.readFile,
8209
+ readFileDigest: storageIo.readFileDigest,
8210
+ writeFile: storageIo.writeFile,
8211
+ deleteFile: storageIo.deleteFile
8212
+ });
8213
+ } catch (retryApplyError) {
8214
+ if (pushed || partialHydration.hydratedFiles.length > 0) {
8215
+ return writePartialPushState(retryApplyError, {
8216
+ ...partialHydrationWithContext,
8217
+ resolvedNamespace
8218
+ });
8219
+ }
8220
+ throw retryApplyError;
8221
+ }
7562
8222
  }
7563
8223
  const state = offlineSyncStateFromSnapshot({
7564
8224
  remoteId: options.remoteUrl,
@@ -7566,13 +8226,7 @@ async function runOfflineSyncOnce(options) {
7566
8226
  snapshot: remoteSnapshot,
7567
8227
  baseFiles: pull.nextBaseFiles
7568
8228
  });
7569
- const stateWritePaths = offlineStatePathsForNamespace({
7570
- memoryDir: options.memoryDir,
7571
- remoteUrl: options.remoteUrl,
7572
- requestedNamespace: options.namespace,
7573
- resolvedNamespace,
7574
- explicitStatePath: options.statePathExplicit ? activeStatePath : void 0
7575
- });
8229
+ const stateWritePaths = stateWritePathsFor(resolvedNamespace);
7576
8230
  for (const statePath of stateWritePaths) {
7577
8231
  await writeOfflineSyncState(statePath, state);
7578
8232
  }
@@ -7582,8 +8236,53 @@ async function runOfflineSyncOnce(options) {
7582
8236
  prepared: priorState === null,
7583
8237
  pushed,
7584
8238
  pull,
8239
+ partial: false,
7585
8240
  pendingSummary,
7586
- remoteFileCount: remoteSnapshot.files.length
8241
+ remoteFileCount: remoteSnapshot.files.length,
8242
+ deferred: {
8243
+ localChangedDuringPush: [...directPushDeferredPaths].sort(),
8244
+ remoteChangedDuringHydrate: [...remoteDeferredPaths].sort(),
8245
+ total: directPushDeferredPaths.size + remoteDeferredPaths.size
8246
+ }
8247
+ };
8248
+ }
8249
+ function sumOfflineFileBytes(files) {
8250
+ return files.reduce((total, file) => total + file.bytes, 0);
8251
+ }
8252
+ function offlineStateJsonSummary(state) {
8253
+ if (!state) return null;
8254
+ return {
8255
+ remoteId: state.remoteId,
8256
+ namespace: state.namespace ?? null,
8257
+ includeTranscripts: state.includeTranscripts,
8258
+ lastSyncedAt: state.lastSyncedAt,
8259
+ baseFileCount: state.baseFiles.length,
8260
+ baseBytes: sumOfflineFileBytes(state.baseFiles)
8261
+ };
8262
+ }
8263
+ function offlinePullJsonSummary(pull) {
8264
+ return {
8265
+ upserted: pull.upserted,
8266
+ deleted: pull.deleted,
8267
+ skipped: pull.skipped,
8268
+ pendingLocal: pull.pendingLocal,
8269
+ conflicts: pull.conflicts,
8270
+ nextBaseFileCount: pull.nextBaseFiles.length,
8271
+ nextBaseBytes: sumOfflineFileBytes(pull.nextBaseFiles)
8272
+ };
8273
+ }
8274
+ function offlineSyncResultJsonSummary(result) {
8275
+ return {
8276
+ statePath: result.statePath,
8277
+ namespace: result.namespace ?? null,
8278
+ prepared: result.prepared,
8279
+ partial: result.partial,
8280
+ pushed: result.pushed,
8281
+ pull: result.pull ? offlinePullJsonSummary(result.pull) : null,
8282
+ pullError: result.pullError ?? null,
8283
+ pendingSummary: result.pendingSummary,
8284
+ remoteFileCount: result.remoteFileCount,
8285
+ deferred: result.deferred
7587
8286
  };
7588
8287
  }
7589
8288
  function assertOfflineStateMatches(options) {
@@ -7664,6 +8363,7 @@ Environment fallbacks:
7664
8363
  snapshot: remoteSnapshot,
7665
8364
  baseFiles: existingState?.state.baseFiles ?? [],
7666
8365
  readFile: storageIo.readFile,
8366
+ readFileDigest: storageIo.readFileDigest,
7667
8367
  writeFile: storageIo.writeFile,
7668
8368
  deleteFile: storageIo.deleteFile
7669
8369
  });
@@ -7677,7 +8377,12 @@ Environment fallbacks:
7677
8377
  await writeOfflineSyncState(pathToWrite, state);
7678
8378
  }
7679
8379
  if (json) {
7680
- console.log(JSON.stringify({ statePath: activeStatePath, namespace: resolvedNamespace, remoteFiles: remoteSnapshot.files.length, pull }, null, 2));
8380
+ console.log(JSON.stringify({
8381
+ statePath: activeStatePath,
8382
+ namespace: resolvedNamespace,
8383
+ remoteFiles: remoteSnapshot.files.length,
8384
+ pull: offlinePullJsonSummary(pull)
8385
+ }, null, 2));
7681
8386
  } else {
7682
8387
  console.log(`Offline cache prepared: ${memoryDir}`);
7683
8388
  console.log(`Namespace: ${resolvedNamespace ?? "(default)"}`);
@@ -7699,12 +8404,19 @@ Environment fallbacks:
7699
8404
  statePathExplicit
7700
8405
  });
7701
8406
  if (json) {
7702
- console.log(JSON.stringify(result, null, 2));
8407
+ console.log(JSON.stringify(offlineSyncResultJsonSummary(result), null, 2));
7703
8408
  } else {
7704
8409
  console.log(`Offline sync complete${result.prepared ? " (initialized state)" : ""}.`);
7705
8410
  console.log(`Pushed: ${result.pushed ? `${result.pushed.appliedUpserts} upserts, ${result.pushed.appliedDeletes} deletes, ${result.pushed.conflicts.length} conflicts` : "nothing pending"}`);
7706
- console.log(`Pulled: ${result.pull.upserted} upserts, ${result.pull.deleted} deletes, ${result.pull.conflicts.length} conflicts`);
8411
+ if (result.pull) {
8412
+ console.log(`Pulled: ${result.pull.upserted} upserts, ${result.pull.deleted} deletes, ${result.pull.conflicts.length} conflicts`);
8413
+ } else {
8414
+ console.log(`Pulled: deferred (${result.pullError ?? "pull unavailable"})`);
8415
+ }
7707
8416
  console.log(`Pending local before push: ${result.pendingSummary.total}`);
8417
+ if (result.deferred.total > 0) {
8418
+ console.log(`Deferred volatile files: ${result.deferred.total}`);
8419
+ }
7708
8420
  console.log(`Namespace: ${result.namespace ?? "(default)"}`);
7709
8421
  console.log(`State: ${result.statePath}`);
7710
8422
  }
@@ -7727,11 +8439,17 @@ Environment fallbacks:
7727
8439
  root: memoryDir,
7728
8440
  sourceId: localOfflineSourceId(memoryDir),
7729
8441
  baseFiles: state?.baseFiles ?? [],
8442
+ baseCapturedAt: state ? new Date(state.lastSyncedAt) : void 0,
7730
8443
  includeTranscripts,
7731
- readFile: storageIo.readFile
8444
+ readFile: storageIo.readFile,
8445
+ readFileDigest: storageIo.readFileDigest
7732
8446
  });
7733
8447
  if (json) {
7734
- console.log(JSON.stringify({ statePath: statePath ?? null, state, pending: summary }, null, 2));
8448
+ console.log(JSON.stringify({
8449
+ statePath: statePath ?? null,
8450
+ state: offlineStateJsonSummary(state),
8451
+ pending: summary
8452
+ }, null, 2));
7735
8453
  } else {
7736
8454
  console.log(`Offline state: ${state ? "ready" : "not prepared"}`);
7737
8455
  console.log(`State: ${statePath ?? "(not selected; pass --state or --remote-url to inspect a prepared remote state)"}`);
@@ -7762,8 +8480,10 @@ Environment fallbacks:
7762
8480
  statePath,
7763
8481
  statePathExplicit
7764
8482
  });
8483
+ const pulled = result.pull ? result.pull.upserted + result.pull.deleted : 0;
8484
+ const conflicts = (result.pushed?.conflicts.length ?? 0) + (result.pull?.conflicts.length ?? 0);
7765
8485
  console.log(
7766
- `[${(/* @__PURE__ */ new Date()).toISOString()}] sync ok: pushed=${result.pushed ? result.pushed.appliedUpserts + result.pushed.appliedDeletes : 0}, pulled=${result.pull.upserted + result.pull.deleted}, conflicts=${(result.pushed?.conflicts.length ?? 0) + result.pull.conflicts.length}`
8486
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] sync ${result.partial ? "partial" : "ok"}: pushed=${result.pushed ? result.pushed.appliedUpserts + result.pushed.appliedDeletes : 0}, pulled=${pulled}, conflicts=${conflicts}, deferred=${result.deferred.total}${result.pullError ? `, pullError=${result.pullError}` : ""}`
7767
8487
  );
7768
8488
  } catch (error) {
7769
8489
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] sync waiting: ${error instanceof Error ? error.message : String(error)}`);
@@ -10676,32 +11396,50 @@ if (argv1Base.endsWith("remnic.ts") || argv1Base.endsWith("remnic.js") || argv1B
10676
11396
  export {
10677
11397
  BENCHMARK_CATALOG,
10678
11398
  OFFLINE_SYNC_APPLY_MAX_REQUEST_BYTES,
11399
+ OFFLINE_SYNC_CHANGESET_RETRY_MAX,
10679
11400
  OFFLINE_SYNC_DIRECT_HYDRATE_MIN_BYTES,
10680
11401
  OFFLINE_SYNC_DIRECT_PUSH_MIN_BYTES,
10681
11402
  OFFLINE_SYNC_FILE_CONTENT_UPLOAD_CHUNK_BYTES,
11403
+ OFFLINE_SYNC_REQUEST_TIMEOUT_DEFAULT_MS,
11404
+ OFFLINE_SYNC_SNAPSHOT_BASE_POST_MAX_FILES,
11405
+ OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES,
10682
11406
  TAXONOMY_RESOLVE_BOOLEAN_FLAGS,
10683
11407
  TAXONOMY_RESOLVE_VALUE_FLAGS,
10684
11408
  __benchDatasetTestHooks,
11409
+ advanceOfflineBaseFilesForSuccessfulPush,
10685
11410
  buildBenchRuntimeProfileRequest,
10686
11411
  buildPackageBenchExecutionPlans,
10687
11412
  buildQueryRecallRequest,
10688
11413
  chunkOfflineChangesetApplyBatches,
10689
11414
  chunkOfflineFileContentBatches,
11415
+ directHydrateLargeOfflineFiles,
10690
11416
  extractXrayRawArgs,
11417
+ fetchOfflineSnapshot,
10691
11418
  formatOfflineLargeFilePushFailureMessage,
11419
+ formatOfflineRequestForError,
10692
11420
  getBenchUsageText,
10693
11421
  hasFlag,
11422
+ isOfflineSnapshotPostFallbackError,
10694
11423
  main,
11424
+ offlinePartialHydrationForPaths,
11425
+ offlineSnapshotBasePostBody,
11426
+ offlineSnapshotBasePostBodyFits,
11427
+ offlineSnapshotBasePostRequest,
11428
+ offlineSnapshotContentFilesForApply,
10695
11429
  parseBenchArgs,
10696
11430
  parseCapsuleForkArgs,
10697
11431
  parseConnectorConfig,
11432
+ parseOfflineSyncRequestTimeoutMs,
10698
11433
  parseTaxonomyResolveArgs,
10699
11434
  parseTrainingExportArgs,
11435
+ pushOfflineFileContent,
11436
+ pushOfflineFileContentFromChunkReader,
10700
11437
  renderQueryTextLines,
10701
11438
  resolveConfigPath,
10702
11439
  resolveFlag,
10703
11440
  resolveMemoryDir,
10704
11441
  resolveSyncSourceDir,
11442
+ runOfflineSyncOnce,
10705
11443
  runTrainingExport,
10706
11444
  runXrayCommand,
10707
11445
  shouldDirectHydrateOfflineFile,