@panorama-ai/gateway 2.30.207 → 2.30.279

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 (57) hide show
  1. package/README.md +10 -0
  2. package/dist/cli-providers/types.d.ts +1 -1
  3. package/dist/cli-providers/types.d.ts.map +1 -1
  4. package/dist/database.types.d.ts +1723 -172
  5. package/dist/database.types.d.ts.map +1 -1
  6. package/dist/database.types.js.map +1 -1
  7. package/dist/finalize-subagent-run.d.ts +2 -1
  8. package/dist/finalize-subagent-run.d.ts.map +1 -1
  9. package/dist/finalize-subagent-run.js.map +1 -1
  10. package/dist/index.js +3384 -241
  11. package/dist/index.js.map +4 -4
  12. package/dist/managed-runtime/config.d.ts +13 -0
  13. package/dist/managed-runtime/config.d.ts.map +1 -1
  14. package/dist/managed-runtime/config.js +31 -0
  15. package/dist/managed-runtime/config.js.map +1 -1
  16. package/dist/managed-runtime/dependencies.d.ts +2 -0
  17. package/dist/managed-runtime/dependencies.d.ts.map +1 -1
  18. package/dist/managed-runtime/dependencies.js +2 -0
  19. package/dist/managed-runtime/dependencies.js.map +1 -1
  20. package/dist/managed-runtime/drive-sync-filesystem.d.ts +39 -0
  21. package/dist/managed-runtime/drive-sync-filesystem.d.ts.map +1 -0
  22. package/dist/managed-runtime/drive-sync-filesystem.js +434 -0
  23. package/dist/managed-runtime/drive-sync-filesystem.js.map +1 -0
  24. package/dist/managed-runtime/drive-sync-planner.d.ts +76 -0
  25. package/dist/managed-runtime/drive-sync-planner.d.ts.map +1 -0
  26. package/dist/managed-runtime/drive-sync-planner.js +363 -0
  27. package/dist/managed-runtime/drive-sync-planner.js.map +1 -0
  28. package/dist/managed-runtime/drive-sync-remote-planner.d.ts +52 -0
  29. package/dist/managed-runtime/drive-sync-remote-planner.d.ts.map +1 -0
  30. package/dist/managed-runtime/drive-sync-remote-planner.js +77 -0
  31. package/dist/managed-runtime/drive-sync-remote-planner.js.map +1 -0
  32. package/dist/managed-runtime/drive-sync-scheduler.d.ts +50 -0
  33. package/dist/managed-runtime/drive-sync-scheduler.d.ts.map +1 -0
  34. package/dist/managed-runtime/drive-sync-scheduler.js +302 -0
  35. package/dist/managed-runtime/drive-sync-scheduler.js.map +1 -0
  36. package/dist/managed-runtime/drive-sync-state-applier.d.ts +84 -0
  37. package/dist/managed-runtime/drive-sync-state-applier.d.ts.map +1 -0
  38. package/dist/managed-runtime/drive-sync-state-applier.js +153 -0
  39. package/dist/managed-runtime/drive-sync-state-applier.js.map +1 -0
  40. package/dist/managed-runtime/drive-sync-transfer.d.ts +86 -0
  41. package/dist/managed-runtime/drive-sync-transfer.d.ts.map +1 -0
  42. package/dist/managed-runtime/drive-sync-transfer.js +245 -0
  43. package/dist/managed-runtime/drive-sync-transfer.js.map +1 -0
  44. package/dist/managed-runtime/drive-sync.d.ts +416 -0
  45. package/dist/managed-runtime/drive-sync.d.ts.map +1 -0
  46. package/dist/managed-runtime/drive-sync.js +1641 -0
  47. package/dist/managed-runtime/drive-sync.js.map +1 -0
  48. package/dist/managed-runtime.d.ts.map +1 -1
  49. package/dist/managed-runtime.js +44 -0
  50. package/dist/managed-runtime.js.map +1 -1
  51. package/dist/subagent-adapters/types.d.ts +1 -1
  52. package/dist/subagent-adapters/types.d.ts.map +1 -1
  53. package/dist/subagent-output-persistence.d.ts +1 -1
  54. package/dist/subagent-output-persistence.d.ts.map +1 -1
  55. package/dist/subagent-output-persistence.js +1 -1
  56. package/dist/subagent-output-persistence.js.map +1 -1
  57. package/package.json +5 -5
@@ -0,0 +1,1641 @@
1
+ import { DRIVE_TRANSFER_POLICY, getDriveLocalSyncFileTooLargeMessage, } from '@panorama/shared/drive-transfer-policy';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { mkdir, rename, rm } from 'node:fs/promises';
4
+ import { hostname } from 'node:os';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { assertLocalPathIsNotSymlink, assertNoSymlinkInExistingPath, assertPathExists, buildSubtreeConflictRootPath, normalizeDrivePath, parkStaleDriveLocalFolder, readLocalDriveEntryAtPath, resolveDirtyDrivePathsForDrive, resolveDriveLocalPath, resolveDriveLocalRoot, scanLocalDriveEntries, writeLocalConflictCopy, } from './drive-sync-filesystem.js';
8
+ import { drivePathIsInSubtree, localEntryMatchesBaseline, planLocalDriveSync, } from './drive-sync-planner.js';
9
+ import { planRemoteDriveBatch } from './drive-sync-remote-planner.js';
10
+ import { applyAppliedLocalDriveMutationState, applyLocalDriveConflictActions, applyRemoteDriveDeleteState, applyRemoteDriveDirectoryState, applyRemoteDriveFileState, applyRemoteDriveMoveState, applyUnsupportedLocalDriveMutationState, recordLocalUploadConflict, recordRemoteApplyConflict, resolveLocalMutationResultPath, } from './drive-sync-state-applier.js';
11
+ import { downloadDriveVersionToLocalFile, prepareLocalMutationUploads, } from './drive-sync-transfer.js';
12
+ import { MANAGED_GATEWAY_LOG_PREFIX } from './runtime-utils.js';
13
+ export { resolveDriveLocalPath, resolveDriveLocalRoot } from './drive-sync-filesystem.js';
14
+ const MAX_LOCAL_BATCH_CHANGES = DRIVE_TRANSFER_POLICY.localBatchMaxChanges;
15
+ const MAX_LOCAL_BATCH_UPLOAD_BYTES = DRIVE_TRANSFER_POLICY.localSyncBatchUploadMaxBytes;
16
+ const MAX_LOCAL_FILE_UPLOAD_BYTES = DRIVE_TRANSFER_POLICY.objectFileMaxBytes;
17
+ const DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS = DRIVE_TRANSFER_POLICY.networkRequestTimeoutMs;
18
+ const DRIVE_SYNC_STATUS_CONFLICT_SAMPLE_LIMIT = 10;
19
+ function isRecord(value) {
20
+ return !!value && typeof value === 'object' && !Array.isArray(value);
21
+ }
22
+ function mapDriveSyncEntryRow(row) {
23
+ return {
24
+ driveId: row.drive_id,
25
+ path: row.path,
26
+ entryType: row.entry_type,
27
+ versionId: row.version_id,
28
+ contentSha256: row.content_sha256,
29
+ sizeBytes: row.size_bytes,
30
+ mimeType: row.mime_type,
31
+ };
32
+ }
33
+ function mapDriveSyncConflictRow(row) {
34
+ return {
35
+ driveId: row.drive_id,
36
+ path: row.path,
37
+ conflictType: row.conflict_type,
38
+ reason: row.reason,
39
+ localContentSha256: row.local_content_sha256,
40
+ localSizeBytes: row.local_size_bytes,
41
+ localVersionId: row.local_version_id,
42
+ conflictId: row.conflict_id,
43
+ conflictPath: row.conflict_path,
44
+ remoteBatchId: row.remote_batch_id,
45
+ remoteSequence: row.remote_sequence,
46
+ };
47
+ }
48
+ export class DriveSyncStateStore {
49
+ db;
50
+ constructor(db) {
51
+ this.db = db;
52
+ }
53
+ static async open(stateDir) {
54
+ await mkdir(stateDir, { recursive: true });
55
+ const sqlite = await import('node:sqlite');
56
+ const db = new sqlite.DatabaseSync(path.join(stateDir, 'drive-sync.sqlite'));
57
+ const store = new DriveSyncStateStore(db);
58
+ store.ensureSchema();
59
+ return store;
60
+ }
61
+ close() {
62
+ this.db.close();
63
+ }
64
+ getOrCreateClientKey(prefix) {
65
+ const key = 'client_instance_id';
66
+ const rows = this.db
67
+ .prepare('SELECT value FROM drive_sync_metadata WHERE key = ?')
68
+ .all(key);
69
+ const existing = rows[0]?.value;
70
+ if (existing) {
71
+ return `${prefix}:${existing}`;
72
+ }
73
+ const clientInstanceId = randomUUID();
74
+ this.db
75
+ .prepare(`
76
+ INSERT INTO drive_sync_metadata (key, value, updated_at)
77
+ VALUES (?, ?, datetime('now'))
78
+ ON CONFLICT(key) DO UPDATE SET
79
+ value = excluded.value,
80
+ updated_at = excluded.updated_at
81
+ `)
82
+ .run(key, clientInstanceId);
83
+ return `${prefix}:${clientInstanceId}`;
84
+ }
85
+ getEntry(driveId, drivePath) {
86
+ const normalizedPath = normalizeDrivePath(drivePath);
87
+ const rows = this.db
88
+ .prepare(`
89
+ SELECT drive_id, path, entry_type, version_id, content_sha256, size_bytes, mime_type
90
+ FROM drive_sync_entries
91
+ WHERE drive_id = ? AND path = ?
92
+ `)
93
+ .all(driveId, normalizedPath);
94
+ return rows[0] ? mapDriveSyncEntryRow(rows[0]) : null;
95
+ }
96
+ listEntries(driveId) {
97
+ const rows = this.db
98
+ .prepare(`
99
+ SELECT drive_id, path, entry_type, version_id, content_sha256, size_bytes, mime_type
100
+ FROM drive_sync_entries
101
+ WHERE drive_id = ?
102
+ ORDER BY path ASC
103
+ `)
104
+ .all(driveId);
105
+ return rows.map(mapDriveSyncEntryRow);
106
+ }
107
+ listDrives() {
108
+ const rows = this.db
109
+ .prepare(`
110
+ SELECT drive_id, title, root_path
111
+ FROM drive_sync_drives
112
+ ORDER BY drive_id ASC
113
+ `)
114
+ .all();
115
+ return rows.map((row) => ({
116
+ id: row.drive_id,
117
+ title: row.title,
118
+ rootPath: row.root_path,
119
+ }));
120
+ }
121
+ getConflict(driveId, drivePath) {
122
+ const normalizedPath = normalizeDrivePath(drivePath);
123
+ const rows = this.db
124
+ .prepare(`
125
+ SELECT
126
+ drive_id,
127
+ path,
128
+ conflict_type,
129
+ reason,
130
+ local_content_sha256,
131
+ local_size_bytes,
132
+ local_version_id,
133
+ conflict_id,
134
+ conflict_path,
135
+ remote_batch_id,
136
+ remote_sequence
137
+ FROM drive_sync_conflicts
138
+ WHERE drive_id = ? AND path = ?
139
+ `)
140
+ .all(driveId, normalizedPath);
141
+ return rows[0] ? mapDriveSyncConflictRow(rows[0]) : null;
142
+ }
143
+ listConflicts(driveId) {
144
+ const rows = this.db
145
+ .prepare(`
146
+ SELECT
147
+ drive_id,
148
+ path,
149
+ conflict_type,
150
+ reason,
151
+ local_content_sha256,
152
+ local_size_bytes,
153
+ local_version_id,
154
+ conflict_id,
155
+ conflict_path,
156
+ remote_batch_id,
157
+ remote_sequence
158
+ FROM drive_sync_conflicts
159
+ WHERE drive_id = ?
160
+ ORDER BY path ASC
161
+ `)
162
+ .all(driveId);
163
+ return rows.map(mapDriveSyncConflictRow);
164
+ }
165
+ setSyncClient(client) {
166
+ this.db
167
+ .prepare(`
168
+ INSERT INTO drive_sync_clients (id, team_id, agent_id, host_id, display_name, status, updated_at)
169
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
170
+ ON CONFLICT(id) DO UPDATE SET
171
+ team_id = excluded.team_id,
172
+ agent_id = excluded.agent_id,
173
+ host_id = excluded.host_id,
174
+ display_name = excluded.display_name,
175
+ status = excluded.status,
176
+ updated_at = excluded.updated_at
177
+ `)
178
+ .run(client.id, client.team_id, client.agent_id, client.host_id, client.display_name, client.status);
179
+ }
180
+ upsertDrive(drive) {
181
+ this.db
182
+ .prepare(`
183
+ INSERT INTO drive_sync_drives (drive_id, title, root_path, updated_at)
184
+ VALUES (?, ?, ?, datetime('now'))
185
+ ON CONFLICT(drive_id) DO UPDATE SET
186
+ title = excluded.title,
187
+ root_path = excluded.root_path,
188
+ updated_at = excluded.updated_at
189
+ `)
190
+ .run(drive.id, drive.title, drive.rootPath);
191
+ }
192
+ removeDrive(driveId) {
193
+ this.db.prepare('DELETE FROM drive_sync_conflicts WHERE drive_id = ?').run(driveId);
194
+ this.db.prepare('DELETE FROM drive_sync_entries WHERE drive_id = ?').run(driveId);
195
+ this.db.prepare('DELETE FROM drive_sync_cursors WHERE drive_id = ?').run(driveId);
196
+ this.db.prepare('DELETE FROM drive_sync_drives WHERE drive_id = ?').run(driveId);
197
+ }
198
+ setCursor(driveId, lastBatchSequence) {
199
+ this.db
200
+ .prepare(`
201
+ INSERT INTO drive_sync_cursors (drive_id, last_batch_sequence, updated_at)
202
+ VALUES (?, ?, datetime('now'))
203
+ ON CONFLICT(drive_id) DO UPDATE SET
204
+ last_batch_sequence = excluded.last_batch_sequence,
205
+ updated_at = excluded.updated_at
206
+ `)
207
+ .run(driveId, lastBatchSequence);
208
+ }
209
+ upsertEntry(entry) {
210
+ this.db
211
+ .prepare(`
212
+ INSERT INTO drive_sync_entries (
213
+ drive_id,
214
+ path,
215
+ entry_type,
216
+ version_id,
217
+ content_sha256,
218
+ size_bytes,
219
+ mime_type,
220
+ updated_at
221
+ )
222
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
223
+ ON CONFLICT(drive_id, path) DO UPDATE SET
224
+ entry_type = excluded.entry_type,
225
+ version_id = excluded.version_id,
226
+ content_sha256 = excluded.content_sha256,
227
+ size_bytes = excluded.size_bytes,
228
+ mime_type = excluded.mime_type,
229
+ updated_at = excluded.updated_at
230
+ `)
231
+ .run(entry.driveId, entry.path, entry.entryType, entry.versionId, entry.contentSha256, entry.sizeBytes, entry.mimeType);
232
+ }
233
+ upsertConflict(conflict) {
234
+ this.db
235
+ .prepare(`
236
+ INSERT INTO drive_sync_conflicts (
237
+ drive_id,
238
+ path,
239
+ conflict_type,
240
+ reason,
241
+ local_content_sha256,
242
+ local_size_bytes,
243
+ local_version_id,
244
+ conflict_id,
245
+ conflict_path,
246
+ remote_batch_id,
247
+ remote_sequence,
248
+ updated_at
249
+ )
250
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
251
+ ON CONFLICT(drive_id, path) DO UPDATE SET
252
+ conflict_type = excluded.conflict_type,
253
+ reason = excluded.reason,
254
+ local_content_sha256 = excluded.local_content_sha256,
255
+ local_size_bytes = excluded.local_size_bytes,
256
+ local_version_id = excluded.local_version_id,
257
+ conflict_id = excluded.conflict_id,
258
+ conflict_path = excluded.conflict_path,
259
+ remote_batch_id = excluded.remote_batch_id,
260
+ remote_sequence = excluded.remote_sequence,
261
+ updated_at = excluded.updated_at
262
+ `)
263
+ .run(conflict.driveId, conflict.path, conflict.conflictType, conflict.reason, conflict.localContentSha256, conflict.localSizeBytes, conflict.localVersionId, conflict.conflictId, conflict.conflictPath, conflict.remoteBatchId, conflict.remoteSequence);
264
+ }
265
+ deleteConflict(driveId, drivePath) {
266
+ this.db
267
+ .prepare('DELETE FROM drive_sync_conflicts WHERE drive_id = ? AND path = ?')
268
+ .run(driveId, normalizeDrivePath(drivePath));
269
+ }
270
+ deleteConflictTree(driveId, drivePath) {
271
+ const normalizedPath = normalizeDrivePath(drivePath);
272
+ if (normalizedPath === '/') {
273
+ this.db.prepare('DELETE FROM drive_sync_conflicts WHERE drive_id = ?').run(driveId);
274
+ return;
275
+ }
276
+ const prefix = `${normalizedPath}/`;
277
+ this.db
278
+ .prepare(`
279
+ DELETE FROM drive_sync_conflicts
280
+ WHERE drive_id = ?
281
+ AND (path = ? OR substr(path, 1, ?) = ?)
282
+ `)
283
+ .run(driveId, normalizedPath, prefix.length, prefix);
284
+ }
285
+ deleteEntryTree(driveId, drivePath) {
286
+ const normalizedPath = normalizeDrivePath(drivePath);
287
+ if (normalizedPath === '/') {
288
+ this.db.prepare('DELETE FROM drive_sync_entries WHERE drive_id = ?').run(driveId);
289
+ return;
290
+ }
291
+ const prefix = `${normalizedPath}/`;
292
+ this.db
293
+ .prepare(`
294
+ DELETE FROM drive_sync_entries
295
+ WHERE drive_id = ?
296
+ AND (path = ? OR substr(path, 1, ?) = ?)
297
+ `)
298
+ .run(driveId, normalizedPath, prefix.length, prefix);
299
+ }
300
+ moveEntryTree(driveId, fromPath, toPath) {
301
+ const normalizedFromPath = normalizeDrivePath(fromPath);
302
+ const normalizedToPath = normalizeDrivePath(toPath);
303
+ const prefix = normalizedFromPath === '/' ? '/' : `${normalizedFromPath}/`;
304
+ const rows = this.db
305
+ .prepare(`
306
+ SELECT path, entry_type, version_id, content_sha256, size_bytes, mime_type
307
+ FROM drive_sync_entries
308
+ WHERE drive_id = ?
309
+ AND (path = ? OR substr(path, 1, ?) = ?)
310
+ `)
311
+ .all(driveId, normalizedFromPath, prefix.length, prefix);
312
+ this.deleteEntryTree(driveId, normalizedFromPath);
313
+ for (const row of rows) {
314
+ const suffix = row.path === normalizedFromPath ? '' : row.path.slice(normalizedFromPath.length);
315
+ this.upsertEntry({
316
+ driveId,
317
+ path: `${normalizedToPath}${suffix}`,
318
+ entryType: row.entry_type,
319
+ versionId: row.version_id,
320
+ contentSha256: row.content_sha256,
321
+ sizeBytes: row.size_bytes,
322
+ mimeType: row.mime_type,
323
+ });
324
+ }
325
+ }
326
+ ensureSchema() {
327
+ this.db.exec(`
328
+ PRAGMA journal_mode = WAL;
329
+ CREATE TABLE IF NOT EXISTS drive_sync_metadata (
330
+ key TEXT PRIMARY KEY,
331
+ value TEXT NOT NULL,
332
+ updated_at TEXT NOT NULL
333
+ );
334
+ CREATE TABLE IF NOT EXISTS drive_sync_clients (
335
+ id TEXT PRIMARY KEY,
336
+ team_id TEXT NOT NULL,
337
+ agent_id TEXT,
338
+ host_id TEXT,
339
+ display_name TEXT,
340
+ status TEXT NOT NULL,
341
+ updated_at TEXT NOT NULL
342
+ );
343
+ CREATE TABLE IF NOT EXISTS drive_sync_drives (
344
+ drive_id TEXT PRIMARY KEY,
345
+ title TEXT,
346
+ root_path TEXT NOT NULL,
347
+ updated_at TEXT NOT NULL
348
+ );
349
+ CREATE TABLE IF NOT EXISTS drive_sync_cursors (
350
+ drive_id TEXT PRIMARY KEY,
351
+ last_batch_sequence INTEGER NOT NULL,
352
+ updated_at TEXT NOT NULL
353
+ );
354
+ CREATE TABLE IF NOT EXISTS drive_sync_entries (
355
+ drive_id TEXT NOT NULL,
356
+ path TEXT NOT NULL,
357
+ entry_type TEXT NOT NULL,
358
+ version_id TEXT,
359
+ content_sha256 TEXT,
360
+ size_bytes INTEGER,
361
+ mime_type TEXT,
362
+ updated_at TEXT NOT NULL,
363
+ PRIMARY KEY (drive_id, path)
364
+ );
365
+ CREATE TABLE IF NOT EXISTS drive_sync_conflicts (
366
+ drive_id TEXT NOT NULL,
367
+ path TEXT NOT NULL,
368
+ conflict_type TEXT NOT NULL,
369
+ reason TEXT,
370
+ local_content_sha256 TEXT,
371
+ local_size_bytes INTEGER,
372
+ local_version_id TEXT,
373
+ conflict_id TEXT,
374
+ conflict_path TEXT,
375
+ remote_batch_id TEXT,
376
+ remote_sequence INTEGER,
377
+ updated_at TEXT NOT NULL,
378
+ PRIMARY KEY (drive_id, path)
379
+ );
380
+ `);
381
+ }
382
+ }
383
+ export async function createManagedDriveSyncService(params) {
384
+ const stateStore = params.stateStore ?? (await DriveSyncStateStore.open(params.config.driveSync.stateDir));
385
+ const api = params.api ??
386
+ createSupabaseDriveSyncApi(params.supabase, {
387
+ timeoutMs: params.config.driveSync.networkTimeoutMs,
388
+ });
389
+ const service = new ManagedDrivePullSyncService({
390
+ config: params.config,
391
+ api,
392
+ stateStore,
393
+ });
394
+ await mkdir(params.config.driveSync.rootDir, { recursive: true });
395
+ return service;
396
+ }
397
+ export function createSupabaseDriveSyncApi(supabase, options = {}) {
398
+ const timeoutMs = options.timeoutMs ?? DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS;
399
+ return {
400
+ registerClient: (body) => invokeDriveSyncFunction(supabase, 'register-drive-sync-client', body, timeoutMs),
401
+ listDrives: (body) => invokeDriveSyncFunction(supabase, 'list-sync-drives', body, timeoutMs),
402
+ getChanges: (body) => invokeDriveSyncFunction(supabase, 'get-drive-changes', body, timeoutMs),
403
+ downloadFileVersion: (body) => invokeDriveSyncFunction(supabase, 'download-drive-file-version', body, timeoutMs),
404
+ prepareFileUpload: (body) => invokeDriveSyncFunction(supabase, 'prepare-drive-file-upload', body, timeoutMs),
405
+ ackChanges: (body) => invokeDriveSyncFunction(supabase, 'ack-drive-changes', body, timeoutMs),
406
+ applyLocalChangeBatch: (body) => invokeDriveSyncFunction(supabase, 'apply-local-drive-change-batch', body, timeoutMs),
407
+ applyConflictActions: (body) => invokeDriveSyncFunction(supabase, 'apply-drive-conflict-actions', body, timeoutMs),
408
+ reportStatus: (body) => invokeDriveSyncFunction(supabase, 'report-drive-sync-status', body, timeoutMs),
409
+ };
410
+ }
411
+ export async function applyDriveChangeBatchToLocalFilesystem(params) {
412
+ let appliedPathCount = 0;
413
+ let remoteConflictCount = 0;
414
+ const remoteConflicts = [];
415
+ const operations = planRemoteDriveBatch(params.batch);
416
+ for (const operation of operations) {
417
+ if (operation.type === 'move') {
418
+ const sourcePath = resolveDriveLocalPath(params.driveRoot, operation.fromPath);
419
+ const targetPath = resolveDriveLocalPath(params.driveRoot, operation.toPath);
420
+ await assertNoSymlinkInExistingPath(params.driveRoot, path.dirname(sourcePath));
421
+ const alreadyAppliedMove = await classifyRemoteMoveAlreadyApplied({
422
+ driveId: params.batch.drive_id,
423
+ driveRoot: params.driveRoot,
424
+ stateStore: params.stateStore,
425
+ fromPath: operation.fromPath,
426
+ toPath: operation.toPath,
427
+ });
428
+ if (alreadyAppliedMove) {
429
+ if (alreadyAppliedMove === 'advance_state') {
430
+ applyRemoteDriveMoveState({
431
+ stateStore: params.stateStore,
432
+ driveId: params.batch.drive_id,
433
+ fromPath: operation.fromPath,
434
+ toPath: operation.toPath,
435
+ });
436
+ }
437
+ appliedPathCount += 1;
438
+ continue;
439
+ }
440
+ const preserved = await preserveRemoteApplySteps({
441
+ driveId: params.batch.drive_id,
442
+ driveRoot: params.driveRoot,
443
+ stateStore: params.stateStore,
444
+ batch: params.batch,
445
+ steps: operation.preserve,
446
+ });
447
+ remoteConflictCount += preserved.conflictCount;
448
+ remoteConflicts.push(...preserved.conflicts);
449
+ await assertPathExists(sourcePath, `Drive move source is missing: ${operation.fromPath}`);
450
+ await assertLocalPathIsNotSymlink(sourcePath, `Drive move source is a symlink: ${operation.fromPath}`);
451
+ await assertNoSymlinkInExistingPath(params.driveRoot, path.dirname(targetPath));
452
+ await mkdir(path.dirname(targetPath), { recursive: true });
453
+ await rm(targetPath, { recursive: true, force: true });
454
+ await rename(sourcePath, targetPath);
455
+ applyRemoteDriveMoveState({
456
+ stateStore: params.stateStore,
457
+ driveId: params.batch.drive_id,
458
+ fromPath: operation.fromPath,
459
+ toPath: operation.toPath,
460
+ });
461
+ appliedPathCount += 1;
462
+ continue;
463
+ }
464
+ const localPath = resolveDriveLocalPath(params.driveRoot, operation.path);
465
+ if (operation.type === 'delete') {
466
+ if (await remoteDeleteAlreadyApplied({
467
+ driveId: params.batch.drive_id,
468
+ driveRoot: params.driveRoot,
469
+ path: operation.path,
470
+ })) {
471
+ applyRemoteDriveDeleteState({
472
+ stateStore: params.stateStore,
473
+ driveId: params.batch.drive_id,
474
+ path: operation.path,
475
+ });
476
+ appliedPathCount += 1;
477
+ continue;
478
+ }
479
+ const preserved = await preserveRemoteApplySteps({
480
+ driveId: params.batch.drive_id,
481
+ driveRoot: params.driveRoot,
482
+ stateStore: params.stateStore,
483
+ batch: params.batch,
484
+ steps: operation.preserve,
485
+ });
486
+ remoteConflictCount += preserved.conflictCount;
487
+ remoteConflicts.push(...preserved.conflicts);
488
+ await assertNoSymlinkInExistingPath(params.driveRoot, path.dirname(localPath));
489
+ await rm(localPath, { recursive: true, force: true });
490
+ applyRemoteDriveDeleteState({
491
+ stateStore: params.stateStore,
492
+ driveId: params.batch.drive_id,
493
+ path: operation.path,
494
+ });
495
+ appliedPathCount += 1;
496
+ continue;
497
+ }
498
+ if (operation.type === 'mkdir') {
499
+ await assertNoSymlinkInExistingPath(params.driveRoot, path.dirname(localPath));
500
+ await assertLocalPathIsNotSymlink(localPath, `Drive directory target is a symlink: ${operation.path}`);
501
+ await mkdir(localPath, { recursive: true });
502
+ applyRemoteDriveDirectoryState({
503
+ stateStore: params.stateStore,
504
+ driveId: params.batch.drive_id,
505
+ path: operation.path,
506
+ });
507
+ appliedPathCount += 1;
508
+ continue;
509
+ }
510
+ const alreadyAppliedFile = await remoteFileAlreadyApplied({
511
+ driveId: params.batch.drive_id,
512
+ driveRoot: params.driveRoot,
513
+ path: operation.path,
514
+ version: operation.change.version,
515
+ });
516
+ if (alreadyAppliedFile) {
517
+ applyRemoteDriveFileState({
518
+ stateStore: params.stateStore,
519
+ driveId: params.batch.drive_id,
520
+ path: operation.path,
521
+ versionId: operation.versionId,
522
+ version: operation.change.version,
523
+ downloadedSizeBytes: alreadyAppliedFile.sizeBytes,
524
+ });
525
+ appliedPathCount += 1;
526
+ continue;
527
+ }
528
+ const preserved = await preserveRemoteApplySteps({
529
+ driveId: params.batch.drive_id,
530
+ driveRoot: params.driveRoot,
531
+ stateStore: params.stateStore,
532
+ batch: params.batch,
533
+ steps: operation.preserve,
534
+ });
535
+ remoteConflictCount += preserved.conflictCount;
536
+ remoteConflicts.push(...preserved.conflicts);
537
+ await assertNoSymlinkInExistingPath(params.driveRoot, path.dirname(localPath));
538
+ await assertLocalPathIsNotSymlink(localPath, `Drive file target is a symlink: ${operation.path}`);
539
+ if (preserved.overwriteResults.get(operation.path)?.localEntry?.entryType === 'directory') {
540
+ await rm(localPath, { recursive: true, force: true });
541
+ }
542
+ const version = operation.change.version;
543
+ const downloaded = await downloadDriveVersionToLocalFile({
544
+ api: params.api,
545
+ syncClientId: params.syncClientId,
546
+ driveId: params.batch.drive_id,
547
+ versionId: operation.versionId,
548
+ targetPath: localPath,
549
+ expectedSizeBytes: version?.size_bytes ?? null,
550
+ expectedSha256: version?.content_sha256 ?? null,
551
+ });
552
+ applyRemoteDriveFileState({
553
+ stateStore: params.stateStore,
554
+ driveId: params.batch.drive_id,
555
+ path: operation.path,
556
+ versionId: operation.versionId,
557
+ version,
558
+ downloadedSizeBytes: downloaded.sizeBytes,
559
+ });
560
+ appliedPathCount += 1;
561
+ }
562
+ return { appliedPathCount, remoteConflictCount, remoteConflicts };
563
+ }
564
+ async function classifyRemoteMoveAlreadyApplied(params) {
565
+ const normalizedFromPath = normalizeDrivePath(params.fromPath);
566
+ const normalizedToPath = normalizeDrivePath(params.toPath);
567
+ const localSource = await readLocalDriveEntryAtPath({
568
+ driveId: params.driveId,
569
+ driveRoot: params.driveRoot,
570
+ drivePath: normalizedFromPath,
571
+ });
572
+ if (localSource) {
573
+ return null;
574
+ }
575
+ const baselineSource = params.stateStore.getEntry?.(params.driveId, normalizedFromPath) ?? null;
576
+ const baselineTarget = params.stateStore.getEntry?.(params.driveId, normalizedToPath) ?? null;
577
+ const localTarget = await readLocalDriveEntryAtPath({
578
+ driveId: params.driveId,
579
+ driveRoot: params.driveRoot,
580
+ drivePath: normalizedToPath,
581
+ });
582
+ if (!localTarget) {
583
+ return null;
584
+ }
585
+ if (baselineTarget && localEntryMatchesBaseline(localTarget, baselineTarget)) {
586
+ return 'already_applied';
587
+ }
588
+ if (baselineSource?.entryType === 'file' &&
589
+ localEntryMatchesBaseline(localTarget, baselineSource)) {
590
+ return 'advance_state';
591
+ }
592
+ return null;
593
+ }
594
+ async function remoteDeleteAlreadyApplied(params) {
595
+ const localEntry = await readLocalDriveEntryAtPath({
596
+ driveId: params.driveId,
597
+ driveRoot: params.driveRoot,
598
+ drivePath: params.path,
599
+ });
600
+ return localEntry === null;
601
+ }
602
+ async function remoteFileAlreadyApplied(params) {
603
+ if (!params.version) {
604
+ return null;
605
+ }
606
+ const localEntry = await readLocalDriveEntryAtPath({
607
+ driveId: params.driveId,
608
+ driveRoot: params.driveRoot,
609
+ drivePath: params.path,
610
+ });
611
+ if (localEntry?.entryType !== 'file' ||
612
+ localEntry.contentSha256 !== params.version.content_sha256 ||
613
+ localEntry.sizeBytes !== params.version.size_bytes) {
614
+ return null;
615
+ }
616
+ return { sizeBytes: localEntry.sizeBytes ?? params.version.size_bytes };
617
+ }
618
+ async function preserveRemoteApplySteps(params) {
619
+ let conflictCount = 0;
620
+ const conflicts = [];
621
+ const overwriteResults = new Map();
622
+ for (const step of params.steps) {
623
+ if (step.type === 'path_conflict_candidate') {
624
+ const result = await preserveRemoteApplyConflictCandidate({
625
+ driveId: params.driveId,
626
+ driveRoot: params.driveRoot,
627
+ drivePath: step.path,
628
+ stateStore: params.stateStore,
629
+ batch: params.batch,
630
+ reason: step.reason,
631
+ });
632
+ conflictCount += result.conflictCount;
633
+ conflicts.push(...result.conflicts);
634
+ continue;
635
+ }
636
+ const result = await preserveRemoteApplyOverwriteCandidates({
637
+ driveId: params.driveId,
638
+ driveRoot: params.driveRoot,
639
+ drivePath: step.path,
640
+ stateStore: params.stateStore,
641
+ batch: params.batch,
642
+ reason: step.reason,
643
+ subtreeReason: step.subtreeReason,
644
+ });
645
+ conflictCount += result.conflictCount;
646
+ conflicts.push(...result.conflicts);
647
+ overwriteResults.set(step.path, {
648
+ localEntry: result.localEntry,
649
+ baselineEntry: result.baselineEntry,
650
+ });
651
+ }
652
+ return { conflictCount, conflicts, overwriteResults };
653
+ }
654
+ async function preserveRemoteApplyConflictCandidate(params) {
655
+ const normalizedPath = normalizeDrivePath(params.drivePath);
656
+ const baselineEntry = params.stateStore.getEntry?.(params.driveId, normalizedPath) ?? null;
657
+ const localEntry = await readLocalDriveEntryAtPath({
658
+ driveId: params.driveId,
659
+ driveRoot: params.driveRoot,
660
+ drivePath: normalizedPath,
661
+ });
662
+ if (!baselineEntry && !localEntry) {
663
+ return { conflictCount: 0, conflicts: [] };
664
+ }
665
+ if (baselineEntry && localEntry && localEntryMatchesBaseline(localEntry, baselineEntry)) {
666
+ return { conflictCount: 0, conflicts: [] };
667
+ }
668
+ if (!baselineEntry && localEntry?.entryType === 'directory') {
669
+ return { conflictCount: 0, conflicts: [] };
670
+ }
671
+ const conflictPath = localEntry?.entryType === 'file'
672
+ ? await writeLocalConflictCopy({
673
+ driveRoot: params.driveRoot,
674
+ source: localEntry,
675
+ sourceRootPath: normalizedPath,
676
+ conflictRootPath: normalizedPath,
677
+ })
678
+ : null;
679
+ const conflict = recordRemoteApplyConflict({
680
+ stateStore: params.stateStore,
681
+ driveId: params.driveId,
682
+ path: normalizedPath,
683
+ reason: params.reason,
684
+ localEntry,
685
+ baselineEntry,
686
+ conflictPath,
687
+ remoteBatchId: params.batch.id,
688
+ remoteSequence: params.batch.sequence,
689
+ });
690
+ return { conflictCount: 1, conflicts: [conflict] };
691
+ }
692
+ async function preserveRemoteApplyOverwriteCandidates(params) {
693
+ const normalizedPath = normalizeDrivePath(params.drivePath);
694
+ const baselineEntry = params.stateStore.getEntry?.(params.driveId, normalizedPath) ?? null;
695
+ const localEntry = await readLocalDriveEntryAtPath({
696
+ driveId: params.driveId,
697
+ driveRoot: params.driveRoot,
698
+ drivePath: normalizedPath,
699
+ });
700
+ if (baselineEntry?.entryType === 'directory' || localEntry?.entryType === 'directory') {
701
+ const conflictCount = await preserveRemoteApplySubtreeConflictCandidates({
702
+ driveId: params.driveId,
703
+ driveRoot: params.driveRoot,
704
+ drivePath: normalizedPath,
705
+ stateStore: params.stateStore,
706
+ batch: params.batch,
707
+ reason: params.subtreeReason ?? params.reason,
708
+ });
709
+ return { ...conflictCount, localEntry, baselineEntry };
710
+ }
711
+ const conflictResult = await preserveRemoteApplyConflictCandidate({
712
+ driveId: params.driveId,
713
+ driveRoot: params.driveRoot,
714
+ drivePath: normalizedPath,
715
+ stateStore: params.stateStore,
716
+ batch: params.batch,
717
+ reason: params.reason,
718
+ });
719
+ return { ...conflictResult, localEntry, baselineEntry };
720
+ }
721
+ async function preserveRemoteApplySubtreeConflictCandidates(params) {
722
+ const normalizedPath = normalizeDrivePath(params.drivePath);
723
+ const baselineEntries = (params.stateStore.listEntries?.(params.driveId) ?? []).filter((entry) => drivePathIsInSubtree(entry.path, normalizedPath));
724
+ const baselineByPath = new Map(baselineEntries.map((entry) => [entry.path, entry]));
725
+ const localScan = await scanLocalDriveEntries({
726
+ driveId: params.driveId,
727
+ driveRoot: params.driveRoot,
728
+ });
729
+ for (const deferredPath of localScan.deferredPaths) {
730
+ if (drivePathIsInSubtree(deferredPath, normalizedPath)) {
731
+ throw new Error(`Local drive subtree has an unstable file; refusing to delete ${params.drivePath}`);
732
+ }
733
+ }
734
+ let conflictCount = 0;
735
+ const conflicts = [];
736
+ for (const [drivePath, localEntry] of localScan.entries) {
737
+ if (!drivePathIsInSubtree(drivePath, normalizedPath)) {
738
+ continue;
739
+ }
740
+ const baselineEntry = baselineByPath.get(drivePath) ?? null;
741
+ if (baselineEntry && localEntryMatchesBaseline(localEntry, baselineEntry)) {
742
+ continue;
743
+ }
744
+ if (localEntry.entryType === 'directory') {
745
+ continue;
746
+ }
747
+ const conflictPath = localEntry.entryType === 'file'
748
+ ? await writeLocalConflictCopy({
749
+ driveRoot: params.driveRoot,
750
+ source: localEntry,
751
+ sourceRootPath: normalizedPath,
752
+ conflictRootPath: buildSubtreeConflictRootPath(normalizedPath),
753
+ })
754
+ : null;
755
+ const conflict = recordRemoteApplyConflict({
756
+ stateStore: params.stateStore,
757
+ driveId: params.driveId,
758
+ path: drivePath,
759
+ reason: params.reason,
760
+ localEntry,
761
+ baselineEntry,
762
+ conflictPath,
763
+ remoteBatchId: params.batch.id,
764
+ remoteSequence: params.batch.sequence,
765
+ });
766
+ conflicts.push(conflict);
767
+ conflictCount += 1;
768
+ }
769
+ return { conflictCount, conflicts };
770
+ }
771
+ export async function reconcileLocalDriveToCloud(params) {
772
+ const fullBaseline = new Map((params.stateStore.listEntries?.(params.driveId) ?? []).map((entry) => [entry.path, entry]));
773
+ const baseline = params.dirtyDrivePaths
774
+ ? filterBaselineForDirtyDrivePaths(fullBaseline, params.dirtyDrivePaths)
775
+ : fullBaseline;
776
+ const localScan = await scanLocalDriveEntries({
777
+ driveId: params.driveId,
778
+ driveRoot: params.driveRoot,
779
+ rootDrivePaths: params.dirtyDrivePaths ?? undefined,
780
+ });
781
+ const conflicts = new Map((params.stateStore.listConflicts?.(params.driveId) ?? []).map((conflict) => [
782
+ conflict.path,
783
+ conflict,
784
+ ]));
785
+ const scopedConflicts = params.dirtyDrivePaths
786
+ ? filterConflictsForDirtyDrivePaths(conflicts, params.dirtyDrivePaths)
787
+ : conflicts;
788
+ const localPlan = planLocalDriveSync({
789
+ baseline,
790
+ local: localScan.entries,
791
+ deferredPaths: localScan.deferredPaths,
792
+ conflicts: scopedConflicts,
793
+ });
794
+ const confirmedConflictActions = await applyCloudDriveConflictLifecycleActions({
795
+ syncClientId: params.syncClientId,
796
+ driveId: params.driveId,
797
+ api: params.api,
798
+ conflicts: scopedConflicts,
799
+ actions: localPlan.conflictActions,
800
+ });
801
+ applyLocalDriveConflictActions({
802
+ stateStore: params.stateStore,
803
+ actions: confirmedConflictActions,
804
+ });
805
+ const mutations = localPlan.mutations;
806
+ if (mutations.length === 0) {
807
+ return {
808
+ ...emptyLocalReconcileSummary(),
809
+ deferredFileCount: localScan.deferredFileCount,
810
+ };
811
+ }
812
+ let uploadedBatchCount = 0;
813
+ let uploadedChangeCount = 0;
814
+ let uploadedAppliedCount = 0;
815
+ let uploadedConflictCount = 0;
816
+ let uploadedUnsupportedCount = 0;
817
+ let uploadedRejectedCount = 0;
818
+ let deferredFileCount = localScan.deferredFileCount;
819
+ const uploadResult = await prepareLocalMutationUploads({
820
+ syncClientId: params.syncClientId,
821
+ driveId: params.driveId,
822
+ api: params.api,
823
+ mutations,
824
+ });
825
+ deferredFileCount += uploadResult.deferredPaths.length;
826
+ const preparedMutations = uploadResult.preparedMutations;
827
+ if (preparedMutations.length === 0) {
828
+ return {
829
+ ...emptyLocalReconcileSummary(),
830
+ deferredFileCount,
831
+ };
832
+ }
833
+ const mutationChunks = chunkLocalDriveMutations(preparedMutations);
834
+ const resourceOperationChunkFingerprints = mutationChunks.map((chunk, chunkIndex) => buildLocalResourceOperationChunkFingerprint({
835
+ driveId: params.driveId,
836
+ baseCursorSequence: params.baseCursorSequence,
837
+ chunkIndex: chunkIndex + 1,
838
+ chunkCount: mutationChunks.length,
839
+ mutations: chunk,
840
+ }));
841
+ const resourceOperationFingerprint = buildLocalResourceOperationFingerprint({
842
+ driveId: params.driveId,
843
+ baseCursorSequence: params.baseCursorSequence,
844
+ chunkFingerprints: resourceOperationChunkFingerprints,
845
+ });
846
+ const resourceOperationId = `local-reconcile:${resourceOperationFingerprint}`;
847
+ let finalizedOperationResponse = null;
848
+ for (const [chunkIndex, chunk] of mutationChunks.entries()) {
849
+ const response = await params.api.applyLocalChangeBatch({
850
+ sync_client_id: params.syncClientId,
851
+ drive_id: params.driveId,
852
+ resource_operation_id: resourceOperationId,
853
+ resource_operation_fingerprint: resourceOperationFingerprint,
854
+ resource_operation_chunk_fingerprint: resourceOperationChunkFingerprints[chunkIndex],
855
+ resource_operation_chunk_fingerprints: resourceOperationChunkFingerprints,
856
+ resource_operation_chunk_index: chunkIndex + 1,
857
+ resource_operation_chunk_count: mutationChunks.length,
858
+ base_cursor_sequence: params.baseCursorSequence,
859
+ changes: chunk.map((mutation) => mutation.request),
860
+ });
861
+ uploadedBatchCount += 1;
862
+ uploadedChangeCount += chunk.length;
863
+ if (isStagedLocalOperationResponse(response)) {
864
+ continue;
865
+ }
866
+ if (isFinalizedFullLocalOperationResponse(response, preparedMutations.length)) {
867
+ finalizedOperationResponse = response;
868
+ continue;
869
+ }
870
+ await applyAcceptedLocalChangeResults({
871
+ driveId: params.driveId,
872
+ driveRoot: params.driveRoot,
873
+ mutations: chunk,
874
+ results: response.results,
875
+ stateStore: params.stateStore,
876
+ });
877
+ uploadedAppliedCount += response.applied_count;
878
+ uploadedConflictCount += response.conflict_count;
879
+ uploadedUnsupportedCount += response.unsupported_count;
880
+ uploadedRejectedCount += response.rejected_count;
881
+ }
882
+ if (finalizedOperationResponse) {
883
+ await applyAcceptedLocalChangeResults({
884
+ driveId: params.driveId,
885
+ driveRoot: params.driveRoot,
886
+ mutations: preparedMutations,
887
+ results: finalizedOperationResponse.results,
888
+ stateStore: params.stateStore,
889
+ });
890
+ uploadedAppliedCount += finalizedOperationResponse.applied_count;
891
+ uploadedConflictCount += finalizedOperationResponse.conflict_count;
892
+ uploadedUnsupportedCount += finalizedOperationResponse.unsupported_count;
893
+ uploadedRejectedCount += finalizedOperationResponse.rejected_count;
894
+ }
895
+ return {
896
+ uploadedBatchCount,
897
+ uploadedChangeCount,
898
+ uploadedAppliedCount,
899
+ uploadedConflictCount,
900
+ uploadedUnsupportedCount,
901
+ uploadedRejectedCount,
902
+ deferredFileCount,
903
+ };
904
+ }
905
+ function emptyLocalReconcileSummary() {
906
+ return {
907
+ uploadedBatchCount: 0,
908
+ uploadedChangeCount: 0,
909
+ uploadedAppliedCount: 0,
910
+ uploadedConflictCount: 0,
911
+ uploadedUnsupportedCount: 0,
912
+ uploadedRejectedCount: 0,
913
+ deferredFileCount: 0,
914
+ };
915
+ }
916
+ function filterBaselineForDirtyDrivePaths(baseline, dirtyDrivePaths) {
917
+ if (dirtyDrivePaths.size === 0) {
918
+ return new Map();
919
+ }
920
+ const normalizedDirtyPaths = [...dirtyDrivePaths].map(normalizeDrivePath);
921
+ return new Map([...baseline].filter(([baselinePath]) => normalizedDirtyPaths.some((dirtyPath) => drivePathIsInSubtree(baselinePath, dirtyPath) ||
922
+ drivePathIsInSubtree(dirtyPath, baselinePath))));
923
+ }
924
+ function filterConflictsForDirtyDrivePaths(conflicts, dirtyDrivePaths) {
925
+ if (dirtyDrivePaths.size === 0) {
926
+ return new Map();
927
+ }
928
+ const normalizedDirtyPaths = [...dirtyDrivePaths].map(normalizeDrivePath);
929
+ return new Map([...conflicts].filter(([, conflict]) => normalizedDirtyPaths.some((dirtyPath) => drivePathsIntersect(conflict.path, dirtyPath) ||
930
+ (conflict.conflictPath ? drivePathsIntersect(conflict.conflictPath, dirtyPath) : false))));
931
+ }
932
+ function drivePathsIntersect(leftPath, rightPath) {
933
+ return drivePathIsInSubtree(leftPath, rightPath) || drivePathIsInSubtree(rightPath, leftPath);
934
+ }
935
+ async function applyCloudDriveConflictLifecycleActions(params) {
936
+ if (params.actions.length === 0 || !params.api.applyConflictActions) {
937
+ return [];
938
+ }
939
+ const response = await params.api.applyConflictActions({
940
+ sync_client_id: params.syncClientId,
941
+ drive_id: params.driveId,
942
+ operation_id: buildConflictLifecycleOperationId({
943
+ driveId: params.driveId,
944
+ actions: params.actions,
945
+ conflicts: params.conflicts,
946
+ }),
947
+ actions: params.actions.map((action) => {
948
+ const conflict = params.conflicts.get(action.path) ?? null;
949
+ const payload = {
950
+ type: action.type,
951
+ path: action.path,
952
+ reason: action.reason,
953
+ };
954
+ if (conflict?.conflictId) {
955
+ payload.conflict_id = conflict.conflictId;
956
+ }
957
+ if (action.type === 'update_conflict_path') {
958
+ payload.conflict_path = action.conflictPath;
959
+ }
960
+ return payload;
961
+ }),
962
+ });
963
+ return collectConfirmedConflictActions(params.actions, response.results);
964
+ }
965
+ function collectConfirmedConflictActions(actions, results) {
966
+ const confirmedActions = [];
967
+ for (const result of results) {
968
+ if (!isConfirmedConflictActionResult(result)) {
969
+ continue;
970
+ }
971
+ const action = actions[result.index];
972
+ if (!action || result.type !== action.type) {
973
+ continue;
974
+ }
975
+ if (typeof result.path === 'string' && normalizeDrivePath(result.path) !== action.path) {
976
+ continue;
977
+ }
978
+ if (action.type === 'clear_conflict') {
979
+ if (result.status === 'resolved' || result.status === 'ignored') {
980
+ confirmedActions.push(action);
981
+ }
982
+ continue;
983
+ }
984
+ if (result.status !== 'updated') {
985
+ continue;
986
+ }
987
+ confirmedActions.push({
988
+ ...action,
989
+ conflictPath: typeof result.conflict_path === 'string'
990
+ ? normalizeDrivePath(result.conflict_path)
991
+ : action.conflictPath,
992
+ });
993
+ }
994
+ return confirmedActions;
995
+ }
996
+ function isConfirmedConflictActionResult(result) {
997
+ return (Number.isInteger(result.index) &&
998
+ typeof result.type === 'string' &&
999
+ typeof result.status === 'string');
1000
+ }
1001
+ function buildConflictLifecycleOperationId(params) {
1002
+ const stablePayload = params.actions.map((action) => {
1003
+ const conflict = params.conflicts.get(action.path) ?? null;
1004
+ return {
1005
+ type: action.type,
1006
+ path: action.path,
1007
+ reason: action.reason,
1008
+ conflict_id: conflict?.conflictId ?? null,
1009
+ conflict_path: action.type === 'update_conflict_path' ? action.conflictPath : null,
1010
+ };
1011
+ });
1012
+ const digest = sha256Hex(Buffer.from(JSON.stringify({
1013
+ driveId: params.driveId,
1014
+ actions: stablePayload,
1015
+ })));
1016
+ return `conflict-lifecycle:${digest}`;
1017
+ }
1018
+ function collectRemoteConflictsForAck(params) {
1019
+ const byPath = new Map();
1020
+ for (const conflict of params.newConflicts) {
1021
+ if (conflict.conflictType === 'remote_apply') {
1022
+ byPath.set(conflict.path, conflict);
1023
+ }
1024
+ }
1025
+ for (const conflict of params.stateStore.listConflicts?.(params.driveId) ?? []) {
1026
+ if (conflict.conflictType !== 'remote_apply') {
1027
+ continue;
1028
+ }
1029
+ if (conflict.remoteBatchId === params.batch.id ||
1030
+ conflict.remoteSequence === params.batch.sequence) {
1031
+ byPath.set(conflict.path, conflict);
1032
+ }
1033
+ }
1034
+ return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
1035
+ }
1036
+ function toRemoteConflictAckPayload(conflict) {
1037
+ return {
1038
+ path: conflict.path,
1039
+ reason: conflict.reason,
1040
+ conflict_path: conflict.conflictPath,
1041
+ local_content_sha256: conflict.localContentSha256,
1042
+ local_size_bytes: conflict.localSizeBytes,
1043
+ local_version_id: isUuid(conflict.localVersionId) ? conflict.localVersionId : null,
1044
+ remote_batch_id: isUuid(conflict.remoteBatchId) ? conflict.remoteBatchId : null,
1045
+ remote_sequence: conflict.remoteSequence,
1046
+ };
1047
+ }
1048
+ function buildRemoteConflictAckOperationId(params) {
1049
+ const stablePayload = params.conflicts.map((conflict) => ({
1050
+ path: conflict.path,
1051
+ reason: conflict.reason,
1052
+ conflict_path: conflict.conflictPath,
1053
+ local_content_sha256: conflict.localContentSha256,
1054
+ local_size_bytes: conflict.localSizeBytes,
1055
+ local_version_id: isUuid(conflict.localVersionId) ? conflict.localVersionId : null,
1056
+ remote_batch_id: isUuid(conflict.remoteBatchId) ? conflict.remoteBatchId : null,
1057
+ remote_sequence: conflict.remoteSequence,
1058
+ }));
1059
+ const digest = sha256Hex(Buffer.from(JSON.stringify({
1060
+ driveId: params.driveId,
1061
+ batchId: params.batch.id,
1062
+ batchSequence: params.batch.sequence,
1063
+ conflicts: stablePayload,
1064
+ })));
1065
+ return `remote-conflict-ack:${digest}`;
1066
+ }
1067
+ function applyRemoteConflictAckResults(params) {
1068
+ if (!params.results || !params.stateStore.getConflict || !params.stateStore.upsertConflict) {
1069
+ return;
1070
+ }
1071
+ for (const result of params.results) {
1072
+ if (typeof result.path !== 'string' || typeof result.conflict_id !== 'string') {
1073
+ continue;
1074
+ }
1075
+ const drivePath = normalizeDrivePath(result.path);
1076
+ const existing = params.stateStore.getConflict(params.driveId, drivePath);
1077
+ if (!existing) {
1078
+ continue;
1079
+ }
1080
+ params.stateStore.upsertConflict({
1081
+ ...existing,
1082
+ conflictId: result.conflict_id,
1083
+ conflictPath: typeof result.conflict_path === 'string' ? result.conflict_path : existing.conflictPath,
1084
+ });
1085
+ }
1086
+ }
1087
+ function isUuid(value) {
1088
+ return (typeof value === 'string' &&
1089
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value));
1090
+ }
1091
+ function isStagedLocalOperationResponse(response) {
1092
+ return response.resource_operation_status === 'applying' && response.batch === null;
1093
+ }
1094
+ function isFinalizedFullLocalOperationResponse(response, preparedMutationCount) {
1095
+ return (response.resource_operation_status === 'finalized' &&
1096
+ response.results.length >= preparedMutationCount);
1097
+ }
1098
+ function chunkLocalDriveMutations(mutations) {
1099
+ const chunks = [];
1100
+ let currentChunk = [];
1101
+ let currentUploadBytes = 0;
1102
+ for (const mutation of mutations) {
1103
+ if (mutation.uploadBytes > MAX_LOCAL_BATCH_UPLOAD_BYTES) {
1104
+ throw new Error(getDriveLocalSyncFileTooLargeMessage(mutation.uploadBytes));
1105
+ }
1106
+ const nextUploadBytes = currentUploadBytes + mutation.uploadBytes;
1107
+ if (currentChunk.length > 0 &&
1108
+ (currentChunk.length >= MAX_LOCAL_BATCH_CHANGES ||
1109
+ nextUploadBytes > MAX_LOCAL_BATCH_UPLOAD_BYTES)) {
1110
+ chunks.push(currentChunk);
1111
+ currentChunk = [];
1112
+ currentUploadBytes = 0;
1113
+ }
1114
+ currentChunk.push(mutation);
1115
+ currentUploadBytes += mutation.uploadBytes;
1116
+ }
1117
+ if (currentChunk.length > 0) {
1118
+ chunks.push(currentChunk);
1119
+ }
1120
+ return chunks;
1121
+ }
1122
+ function buildLocalResourceOperationChunkFingerprint(params) {
1123
+ return sha256Hex(Buffer.from(stableJsonStringify({
1124
+ drive_id: params.driveId,
1125
+ base_cursor_sequence: params.baseCursorSequence,
1126
+ chunk_index: params.chunkIndex,
1127
+ chunk_count: params.chunkCount,
1128
+ changes: params.mutations.map((mutation) => mutation.request),
1129
+ })));
1130
+ }
1131
+ function buildLocalResourceOperationFingerprint(params) {
1132
+ return sha256Hex(Buffer.from(stableJsonStringify({
1133
+ drive_id: params.driveId,
1134
+ base_cursor_sequence: params.baseCursorSequence,
1135
+ chunk_fingerprints: params.chunkFingerprints,
1136
+ })));
1137
+ }
1138
+ async function applyAcceptedLocalChangeResults(params) {
1139
+ for (const result of params.results) {
1140
+ const mutation = params.mutations[result.index];
1141
+ if (!mutation) {
1142
+ continue;
1143
+ }
1144
+ if (result.status === 'applied') {
1145
+ applyAppliedLocalDriveMutationState({
1146
+ stateStore: params.stateStore,
1147
+ driveId: params.driveId,
1148
+ mutation,
1149
+ result,
1150
+ });
1151
+ continue;
1152
+ }
1153
+ if (result.status === 'conflicted') {
1154
+ const conflictPath = resolveLocalMutationResultPath(result, mutation);
1155
+ const localConflictPath = await preserveLocalUploadConflictCandidate({
1156
+ driveRoot: params.driveRoot,
1157
+ mutation,
1158
+ conflictPath,
1159
+ });
1160
+ recordLocalUploadConflict({
1161
+ stateStore: params.stateStore,
1162
+ driveId: params.driveId,
1163
+ mutation,
1164
+ result,
1165
+ conflictPath,
1166
+ localConflictPath,
1167
+ });
1168
+ continue;
1169
+ }
1170
+ if (result.status === 'unsupported' && mutation.stateEntry) {
1171
+ applyUnsupportedLocalDriveMutationState({
1172
+ stateStore: params.stateStore,
1173
+ driveId: params.driveId,
1174
+ mutation,
1175
+ });
1176
+ }
1177
+ }
1178
+ }
1179
+ async function preserveLocalUploadConflictCandidate(params) {
1180
+ if (params.mutation.request.type !== 'write_file' ||
1181
+ !params.mutation.localFilePath ||
1182
+ params.mutation.stateEntry?.entryType !== 'file') {
1183
+ return null;
1184
+ }
1185
+ try {
1186
+ return await writeLocalConflictCopy({
1187
+ driveRoot: params.driveRoot,
1188
+ source: {
1189
+ driveId: params.mutation.stateEntry.driveId,
1190
+ path: params.mutation.stateEntry.path,
1191
+ entryType: 'file',
1192
+ contentSha256: params.mutation.stateEntry.contentSha256,
1193
+ sizeBytes: params.mutation.stateEntry.sizeBytes,
1194
+ mimeType: params.mutation.stateEntry.mimeType,
1195
+ absolutePath: params.mutation.localFilePath,
1196
+ },
1197
+ sourceRootPath: params.conflictPath,
1198
+ conflictRootPath: params.conflictPath,
1199
+ });
1200
+ }
1201
+ catch (error) {
1202
+ console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} failed to preserve local upload conflict copy`, {
1203
+ path: params.conflictPath,
1204
+ error: error instanceof Error ? error.message : String(error),
1205
+ });
1206
+ return null;
1207
+ }
1208
+ }
1209
+ function emptyDriveSyncRunSummary(driveCount = 0) {
1210
+ return {
1211
+ driveCount,
1212
+ failedDriveCount: 0,
1213
+ appliedBatchCount: 0,
1214
+ appliedPathCount: 0,
1215
+ remoteConflictCount: 0,
1216
+ uploadedBatchCount: 0,
1217
+ uploadedChangeCount: 0,
1218
+ uploadedAppliedCount: 0,
1219
+ uploadedConflictCount: 0,
1220
+ uploadedUnsupportedCount: 0,
1221
+ uploadedRejectedCount: 0,
1222
+ deferredFileCount: 0,
1223
+ };
1224
+ }
1225
+ function toDriveSyncStatusSummary(summary) {
1226
+ return {
1227
+ applied_batch_count: summary.appliedBatchCount,
1228
+ applied_path_count: summary.appliedPathCount,
1229
+ remote_conflict_count: summary.remoteConflictCount,
1230
+ uploaded_batch_count: summary.uploadedBatchCount,
1231
+ uploaded_change_count: summary.uploadedChangeCount,
1232
+ uploaded_applied_count: summary.uploadedAppliedCount,
1233
+ uploaded_conflict_count: summary.uploadedConflictCount,
1234
+ uploaded_unsupported_count: summary.uploadedUnsupportedCount,
1235
+ uploaded_rejected_count: summary.uploadedRejectedCount,
1236
+ deferred_file_count: summary.deferredFileCount,
1237
+ };
1238
+ }
1239
+ function truncateDriveSyncStatusError(error) {
1240
+ if (!error || error.length <= 2000) {
1241
+ return error;
1242
+ }
1243
+ return `${error.slice(0, 1997)}...`;
1244
+ }
1245
+ function deriveDriveSyncStatus(schedulerStatus) {
1246
+ if (!schedulerStatus) {
1247
+ return 'active';
1248
+ }
1249
+ return schedulerStatus.watchStatus === 'active' ? 'active' : 'degraded';
1250
+ }
1251
+ function driveSummaryRequiresDegradedStatus(summary) {
1252
+ return (summary.remoteConflictCount > 0 ||
1253
+ summary.uploadedConflictCount > 0 ||
1254
+ summary.uploadedUnsupportedCount > 0 ||
1255
+ summary.uploadedRejectedCount > 0 ||
1256
+ summary.deferredFileCount > 0 ||
1257
+ summary.failedDriveCount > 0);
1258
+ }
1259
+ function summarizeDriveSyncConflicts(conflicts) {
1260
+ return {
1261
+ unresolvedConflictCount: conflicts.length,
1262
+ unresolvedConflictSamples: conflicts
1263
+ .slice(0, DRIVE_SYNC_STATUS_CONFLICT_SAMPLE_LIMIT)
1264
+ .map((conflict) => ({
1265
+ path: conflict.path,
1266
+ conflict_type: conflict.conflictType,
1267
+ reason: conflict.reason,
1268
+ conflict_path: conflict.conflictPath,
1269
+ conflict_id: conflict.conflictId,
1270
+ remote_sequence: conflict.remoteSequence,
1271
+ })),
1272
+ };
1273
+ }
1274
+ class ManagedDrivePullSyncService {
1275
+ params;
1276
+ constructor(params) {
1277
+ this.params = params;
1278
+ }
1279
+ close() {
1280
+ this.params.stateStore.close?.();
1281
+ }
1282
+ async syncOnce(options = {}) {
1283
+ const includeLocalWrites = options.includeLocalWrites !== false;
1284
+ const syncStatus = deriveDriveSyncStatus(options.schedulerStatus);
1285
+ const syncClient = await this.ensureSyncClient();
1286
+ const drivesResponse = await this.params.api.listDrives({ sync_client_id: syncClient.id });
1287
+ this.params.stateStore.setSyncClient(drivesResponse.sync_client);
1288
+ await this.parkStaleDrives(new Set(drivesResponse.drives.map((drive) => drive.resource.id)));
1289
+ let appliedBatchCount = 0;
1290
+ let appliedPathCount = 0;
1291
+ let remoteConflictCount = 0;
1292
+ let uploadedBatchCount = 0;
1293
+ let uploadedChangeCount = 0;
1294
+ let uploadedAppliedCount = 0;
1295
+ let uploadedConflictCount = 0;
1296
+ let uploadedUnsupportedCount = 0;
1297
+ let uploadedRejectedCount = 0;
1298
+ let deferredFileCount = 0;
1299
+ let failedDriveCount = 0;
1300
+ for (const drive of drivesResponse.drives) {
1301
+ const driveRoot = resolveDriveLocalRoot(this.params.config.driveSync.rootDir, drive.resource.id);
1302
+ const dirtyDrivePaths = resolveDirtyDrivePathsForDrive(driveRoot, options.dirtyLocalPaths);
1303
+ const driveSummary = emptyDriveSyncRunSummary(1);
1304
+ let lastBatchSequence = drive.cursor?.last_batch_sequence ?? 0;
1305
+ try {
1306
+ await mkdir(driveRoot, { recursive: true });
1307
+ this.params.stateStore.upsertDrive({
1308
+ id: drive.resource.id,
1309
+ title: drive.resource.title,
1310
+ rootPath: driveRoot,
1311
+ });
1312
+ const result = await this.syncDrive({
1313
+ syncClientId: syncClient.id,
1314
+ driveId: drive.resource.id,
1315
+ driveRoot,
1316
+ });
1317
+ lastBatchSequence = result.lastBatchSequence;
1318
+ driveSummary.appliedBatchCount += result.appliedBatchCount;
1319
+ driveSummary.appliedPathCount += result.appliedPathCount;
1320
+ driveSummary.remoteConflictCount += result.remoteConflictCount;
1321
+ if (includeLocalWrites && (!dirtyDrivePaths || dirtyDrivePaths.size > 0)) {
1322
+ const localResult = await reconcileLocalDriveToCloud({
1323
+ syncClientId: syncClient.id,
1324
+ driveId: drive.resource.id,
1325
+ driveRoot,
1326
+ baseCursorSequence: result.lastBatchSequence,
1327
+ api: this.params.api,
1328
+ stateStore: this.params.stateStore,
1329
+ dirtyDrivePaths,
1330
+ });
1331
+ driveSummary.uploadedBatchCount += localResult.uploadedBatchCount;
1332
+ driveSummary.uploadedChangeCount += localResult.uploadedChangeCount;
1333
+ driveSummary.uploadedAppliedCount += localResult.uploadedAppliedCount;
1334
+ driveSummary.uploadedConflictCount += localResult.uploadedConflictCount;
1335
+ driveSummary.uploadedUnsupportedCount += localResult.uploadedUnsupportedCount;
1336
+ driveSummary.uploadedRejectedCount += localResult.uploadedRejectedCount;
1337
+ driveSummary.deferredFileCount += localResult.deferredFileCount;
1338
+ if (localResult.uploadedBatchCount > 0) {
1339
+ const followUpResult = await this.syncDrive({
1340
+ syncClientId: syncClient.id,
1341
+ driveId: drive.resource.id,
1342
+ driveRoot,
1343
+ });
1344
+ lastBatchSequence = followUpResult.lastBatchSequence;
1345
+ driveSummary.appliedBatchCount += followUpResult.appliedBatchCount;
1346
+ driveSummary.appliedPathCount += followUpResult.appliedPathCount;
1347
+ driveSummary.remoteConflictCount += followUpResult.remoteConflictCount;
1348
+ }
1349
+ }
1350
+ appliedBatchCount += driveSummary.appliedBatchCount;
1351
+ appliedPathCount += driveSummary.appliedPathCount;
1352
+ remoteConflictCount += driveSummary.remoteConflictCount;
1353
+ uploadedBatchCount += driveSummary.uploadedBatchCount;
1354
+ uploadedChangeCount += driveSummary.uploadedChangeCount;
1355
+ uploadedAppliedCount += driveSummary.uploadedAppliedCount;
1356
+ uploadedConflictCount += driveSummary.uploadedConflictCount;
1357
+ uploadedUnsupportedCount += driveSummary.uploadedUnsupportedCount;
1358
+ uploadedRejectedCount += driveSummary.uploadedRejectedCount;
1359
+ deferredFileCount += driveSummary.deferredFileCount;
1360
+ await this.reportDriveSyncStatus({
1361
+ syncClientId: syncClient.id,
1362
+ driveId: drive.resource.id,
1363
+ driveRoot,
1364
+ status: syncStatus,
1365
+ lastBatchSequence,
1366
+ summary: driveSummary,
1367
+ lastReason: includeLocalWrites ? 'sync_once' : 'sync_once_remote_only',
1368
+ lastError: null,
1369
+ schedulerStatus: options.schedulerStatus,
1370
+ });
1371
+ }
1372
+ catch (error) {
1373
+ const errorMessage = error instanceof Error ? error.message : String(error);
1374
+ failedDriveCount += 1;
1375
+ await this.reportDriveSyncStatus({
1376
+ syncClientId: syncClient.id,
1377
+ driveId: drive.resource.id,
1378
+ driveRoot,
1379
+ status: 'error',
1380
+ lastBatchSequence,
1381
+ summary: driveSummary,
1382
+ lastReason: 'sync_error',
1383
+ lastError: errorMessage,
1384
+ schedulerStatus: options.schedulerStatus,
1385
+ });
1386
+ console.error(`${MANAGED_GATEWAY_LOG_PREFIX} failed to sync drive`, {
1387
+ driveId: drive.resource.id,
1388
+ error: errorMessage,
1389
+ });
1390
+ }
1391
+ }
1392
+ return {
1393
+ driveCount: drivesResponse.drives.length,
1394
+ failedDriveCount,
1395
+ appliedBatchCount,
1396
+ appliedPathCount,
1397
+ remoteConflictCount,
1398
+ uploadedBatchCount,
1399
+ uploadedChangeCount,
1400
+ uploadedAppliedCount,
1401
+ uploadedConflictCount,
1402
+ uploadedUnsupportedCount,
1403
+ uploadedRejectedCount,
1404
+ deferredFileCount,
1405
+ };
1406
+ }
1407
+ async reportDriveSyncStatus(params) {
1408
+ try {
1409
+ const unresolvedConflictSummary = summarizeDriveSyncConflicts(this.params.stateStore.listConflicts?.(params.driveId) ?? []);
1410
+ const status = params.status === 'active' &&
1411
+ (unresolvedConflictSummary.unresolvedConflictCount > 0 ||
1412
+ driveSummaryRequiresDegradedStatus(params.summary))
1413
+ ? 'degraded'
1414
+ : params.status;
1415
+ await this.params.api.reportStatus({
1416
+ sync_client_id: params.syncClientId,
1417
+ drive_id: params.driveId,
1418
+ status,
1419
+ last_reason: params.lastReason,
1420
+ last_error: truncateDriveSyncStatusError(params.lastError),
1421
+ last_batch_sequence: params.lastBatchSequence,
1422
+ summary: toDriveSyncStatusSummary(params.summary),
1423
+ metadata: {
1424
+ root_path: params.driveRoot,
1425
+ runtime_id: this.params.config.runtimeId,
1426
+ host_id: this.params.config.hostId,
1427
+ agent_id: this.params.config.agentId,
1428
+ watcher_status: params.schedulerStatus?.watchStatus ?? null,
1429
+ watcher_degraded: params.schedulerStatus
1430
+ ? params.schedulerStatus.watchStatus !== 'active'
1431
+ : null,
1432
+ watcher_error: truncateDriveSyncStatusError(params.schedulerStatus?.watchError ?? null),
1433
+ unresolved_conflict_count: unresolvedConflictSummary.unresolvedConflictCount,
1434
+ unresolved_conflicts: unresolvedConflictSummary.unresolvedConflictSamples,
1435
+ },
1436
+ });
1437
+ }
1438
+ catch (error) {
1439
+ console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} failed to report drive sync status`, {
1440
+ driveId: params.driveId,
1441
+ error: error instanceof Error ? error.message : String(error),
1442
+ });
1443
+ }
1444
+ }
1445
+ async parkStaleDrives(accessibleDriveIds) {
1446
+ const knownDrives = this.params.stateStore.listDrives?.() ?? [];
1447
+ for (const knownDrive of knownDrives) {
1448
+ if (accessibleDriveIds.has(knownDrive.id)) {
1449
+ continue;
1450
+ }
1451
+ const parkedPath = await parkStaleDriveLocalFolder(this.params.config.driveSync.rootDir, knownDrive.id);
1452
+ this.params.stateStore.removeDrive?.(knownDrive.id);
1453
+ console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} parked stale drive folder`, {
1454
+ driveId: knownDrive.id,
1455
+ parkedPath,
1456
+ });
1457
+ }
1458
+ }
1459
+ async ensureSyncClient() {
1460
+ const clientKey = this.params.config.driveSync.clientKey ??
1461
+ this.params.stateStore.getOrCreateClientKey?.(`managed:${this.params.config.hostId}:${this.params.config.agentId}`) ??
1462
+ `managed:${this.params.config.hostId}:${this.params.config.agentId}`;
1463
+ const response = await this.params.api.registerClient({
1464
+ agent_id: this.params.config.agentId,
1465
+ host_id: this.params.config.hostId,
1466
+ client_key: clientKey,
1467
+ display_name: `${hostname()} managed drive sync`,
1468
+ metadata: {
1469
+ mode: 'managed_gateway_pull',
1470
+ runtime_id: this.params.config.runtimeId,
1471
+ process_id: process.pid,
1472
+ },
1473
+ });
1474
+ this.params.stateStore.setSyncClient(response.sync_client);
1475
+ return response.sync_client;
1476
+ }
1477
+ async syncDrive(params) {
1478
+ let appliedBatchCount = 0;
1479
+ let appliedPathCount = 0;
1480
+ let remoteConflictCount = 0;
1481
+ let lastAckedSequence = 0;
1482
+ while (appliedBatchCount < this.params.config.driveSync.maxBatchesPerTick) {
1483
+ const remainingBatchCapacity = this.params.config.driveSync.maxBatchesPerTick - appliedBatchCount;
1484
+ const response = await this.params.api.getChanges({
1485
+ sync_client_id: params.syncClientId,
1486
+ drive_id: params.driveId,
1487
+ limit: Math.min(this.params.config.driveSync.changeLimit, remainingBatchCapacity),
1488
+ });
1489
+ lastAckedSequence = response.cursor.last_batch_sequence;
1490
+ if (response.changes.length === 0) {
1491
+ this.params.stateStore.setCursor(params.driveId, lastAckedSequence);
1492
+ break;
1493
+ }
1494
+ for (const batch of response.changes) {
1495
+ try {
1496
+ const result = await applyDriveChangeBatchToLocalFilesystem({
1497
+ syncClientId: params.syncClientId,
1498
+ driveRoot: params.driveRoot,
1499
+ batch,
1500
+ api: this.params.api,
1501
+ stateStore: this.params.stateStore,
1502
+ });
1503
+ const remoteConflicts = collectRemoteConflictsForAck({
1504
+ driveId: params.driveId,
1505
+ batch,
1506
+ newConflicts: result.remoteConflicts,
1507
+ stateStore: this.params.stateStore,
1508
+ });
1509
+ const ackResponse = await this.params.api.ackChanges({
1510
+ sync_client_id: params.syncClientId,
1511
+ drive_id: params.driveId,
1512
+ operation_id: remoteConflicts.length > 0
1513
+ ? buildRemoteConflictAckOperationId({
1514
+ driveId: params.driveId,
1515
+ batch,
1516
+ conflicts: remoteConflicts,
1517
+ })
1518
+ : undefined,
1519
+ last_batch_sequence: batch.sequence,
1520
+ last_error: null,
1521
+ remote_conflicts: remoteConflicts.length > 0
1522
+ ? remoteConflicts.map(toRemoteConflictAckPayload)
1523
+ : undefined,
1524
+ });
1525
+ applyRemoteConflictAckResults({
1526
+ driveId: params.driveId,
1527
+ stateStore: this.params.stateStore,
1528
+ results: ackResponse.remote_conflicts,
1529
+ });
1530
+ lastAckedSequence = ackResponse.cursor.last_batch_sequence;
1531
+ this.params.stateStore.setCursor(params.driveId, lastAckedSequence);
1532
+ appliedBatchCount += 1;
1533
+ appliedPathCount += result.appliedPathCount;
1534
+ remoteConflictCount += result.remoteConflictCount;
1535
+ }
1536
+ catch (error) {
1537
+ await this.params.api.ackChanges({
1538
+ sync_client_id: params.syncClientId,
1539
+ drive_id: params.driveId,
1540
+ last_batch_sequence: lastAckedSequence,
1541
+ last_error: error instanceof Error ? error.message : String(error),
1542
+ });
1543
+ throw error;
1544
+ }
1545
+ }
1546
+ if (!response.has_more) {
1547
+ break;
1548
+ }
1549
+ }
1550
+ return {
1551
+ appliedBatchCount,
1552
+ appliedPathCount,
1553
+ remoteConflictCount,
1554
+ lastBatchSequence: lastAckedSequence,
1555
+ };
1556
+ }
1557
+ }
1558
+ async function invokeDriveSyncFunction(supabase, functionName, body, timeoutMs) {
1559
+ const { data, error } = await withTimeout(supabase.functions.invoke(functionName, { body }), timeoutMs, `${functionName} invocation`);
1560
+ if (error) {
1561
+ throw new Error(`${functionName} failed: ${await formatSupabaseFunctionError(error)}`);
1562
+ }
1563
+ if (!data?.success) {
1564
+ throw new Error(`${functionName} returned an invalid response`);
1565
+ }
1566
+ return data;
1567
+ }
1568
+ async function withTimeout(promise, timeoutMs, label) {
1569
+ let timer = null;
1570
+ const timeout = new Promise((_, reject) => {
1571
+ timer = setTimeout(() => {
1572
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
1573
+ }, timeoutMs);
1574
+ timer.unref?.();
1575
+ });
1576
+ try {
1577
+ return await Promise.race([promise, timeout]);
1578
+ }
1579
+ finally {
1580
+ if (timer) {
1581
+ clearTimeout(timer);
1582
+ }
1583
+ }
1584
+ }
1585
+ async function formatSupabaseFunctionError(error) {
1586
+ const message = error instanceof Error ? error.message : String(error);
1587
+ const context = isRecord(error) ? error.context : null;
1588
+ if (!(context instanceof Response)) {
1589
+ return message;
1590
+ }
1591
+ const responseBody = await context.text().catch(() => null);
1592
+ const bodySuffix = responseBody && responseBody.trim().length > 0
1593
+ ? ` body=${responseBody.trim().slice(0, 1000)}`
1594
+ : '';
1595
+ return `${message} (status=${context.status}${bodySuffix})`;
1596
+ }
1597
+ function sha256Hex(bytes) {
1598
+ return createHash('sha256').update(bytes).digest('hex');
1599
+ }
1600
+ function stableJsonStringify(input) {
1601
+ if (input === undefined) {
1602
+ return 'null';
1603
+ }
1604
+ if (input === null || typeof input !== 'object') {
1605
+ return JSON.stringify(input);
1606
+ }
1607
+ if (Array.isArray(input)) {
1608
+ return `[${input.map((value) => stableJsonStringify(value)).join(',')}]`;
1609
+ }
1610
+ const record = input;
1611
+ return `{${Object.keys(record)
1612
+ .filter((key) => record[key] !== undefined)
1613
+ .sort()
1614
+ .map((key) => `${JSON.stringify(key)}:${stableJsonStringify(record[key])}`)
1615
+ .join(',')}}`;
1616
+ }
1617
+ export function logDriveSyncRunSummary(summary) {
1618
+ if (summary.driveCount === 0 ||
1619
+ (summary.appliedBatchCount === 0 &&
1620
+ summary.uploadedBatchCount === 0 &&
1621
+ summary.remoteConflictCount === 0 &&
1622
+ summary.deferredFileCount === 0 &&
1623
+ summary.failedDriveCount === 0)) {
1624
+ return;
1625
+ }
1626
+ console.log(`${MANAGED_GATEWAY_LOG_PREFIX} applied drive sync changes`, {
1627
+ driveCount: summary.driveCount,
1628
+ failedDriveCount: summary.failedDriveCount,
1629
+ appliedBatchCount: summary.appliedBatchCount,
1630
+ appliedPathCount: summary.appliedPathCount,
1631
+ remoteConflictCount: summary.remoteConflictCount,
1632
+ uploadedBatchCount: summary.uploadedBatchCount,
1633
+ uploadedChangeCount: summary.uploadedChangeCount,
1634
+ uploadedAppliedCount: summary.uploadedAppliedCount,
1635
+ uploadedConflictCount: summary.uploadedConflictCount,
1636
+ uploadedUnsupportedCount: summary.uploadedUnsupportedCount,
1637
+ uploadedRejectedCount: summary.uploadedRejectedCount,
1638
+ deferredFileCount: summary.deferredFileCount,
1639
+ });
1640
+ }
1641
+ //# sourceMappingURL=drive-sync.js.map