@panorama-ai/gateway 2.30.211 → 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
package/dist/index.js CHANGED
@@ -315,12 +315,12 @@ function isGatewayHealthCheckType(value) {
315
315
  // dist/index.js
316
316
  import dotenv from "dotenv";
317
317
  import { execFile as execFile6 } from "node:child_process";
318
- import { randomUUID as randomUUID6 } from "node:crypto";
318
+ import { randomUUID as randomUUID9 } from "node:crypto";
319
319
  import fsSync9 from "node:fs";
320
320
  import fs6 from "node:fs/promises";
321
321
  import os8 from "node:os";
322
- import path16 from "node:path";
323
- import process18 from "node:process";
322
+ import path20 from "node:path";
323
+ import process19 from "node:process";
324
324
  import { promisify as promisify6 } from "node:util";
325
325
 
326
326
  // dist/cli-args.js
@@ -712,8 +712,8 @@ async function showGatewayLogsCommand(options, dependencies) {
712
712
  return;
713
713
  let position = 0;
714
714
  try {
715
- const stat = await fs.stat(logPath);
716
- position = stat.size;
715
+ const stat2 = await fs.stat(logPath);
716
+ position = stat2.size;
717
717
  } catch {
718
718
  position = 0;
719
719
  }
@@ -721,16 +721,16 @@ async function showGatewayLogsCommand(options, dependencies) {
721
721
  if (event !== "change")
722
722
  return;
723
723
  try {
724
- const stat = await fs.stat(logPath);
725
- if (stat.size < position) {
724
+ const stat2 = await fs.stat(logPath);
725
+ if (stat2.size < position) {
726
726
  position = 0;
727
727
  }
728
728
  const handle = await fs.open(logPath, "r");
729
- const length = stat.size - position;
729
+ const length = stat2.size - position;
730
730
  if (length > 0) {
731
731
  const buffer = Buffer.alloc(length);
732
732
  await handle.read(buffer, 0, length, position);
733
- position = stat.size;
733
+ position = stat2.size;
734
734
  process2.stdout.write(buffer.toString("utf-8"));
735
735
  }
736
736
  await handle.close();
@@ -1515,11 +1515,11 @@ function isNotFoundError(error) {
1515
1515
  async function ensureOwnerOnlyDirectory(dirPath) {
1516
1516
  if (shouldEnforceOwnerOnlyPermissions()) {
1517
1517
  await fs2.mkdir(dirPath, { recursive: true, mode: OWNER_ONLY_DIRECTORY_MODE });
1518
- const stat = await fs2.stat(dirPath);
1519
- if (!stat.isDirectory()) {
1518
+ const stat2 = await fs2.stat(dirPath);
1519
+ if (!stat2.isDirectory()) {
1520
1520
  throw new Error(`Expected directory at ${dirPath}`);
1521
1521
  }
1522
- if (normalizeMode(stat.mode) !== OWNER_ONLY_DIRECTORY_MODE) {
1522
+ if (normalizeMode(stat2.mode) !== OWNER_ONLY_DIRECTORY_MODE) {
1523
1523
  await fs2.chmod(dirPath, OWNER_ONLY_DIRECTORY_MODE);
1524
1524
  }
1525
1525
  return;
@@ -1529,18 +1529,18 @@ async function ensureOwnerOnlyDirectory(dirPath) {
1529
1529
  async function ensureOwnerOnlyFile(filePath) {
1530
1530
  if (!shouldEnforceOwnerOnlyPermissions())
1531
1531
  return;
1532
- let stat;
1532
+ let stat2;
1533
1533
  try {
1534
- stat = await fs2.stat(filePath);
1534
+ stat2 = await fs2.stat(filePath);
1535
1535
  } catch (error) {
1536
1536
  if (isNotFoundError(error))
1537
1537
  return;
1538
1538
  throw error;
1539
1539
  }
1540
- if (!stat.isFile()) {
1540
+ if (!stat2.isFile()) {
1541
1541
  throw new Error(`Expected regular file at ${filePath}`);
1542
1542
  }
1543
- if (normalizeMode(stat.mode) !== OWNER_ONLY_FILE_MODE) {
1543
+ if (normalizeMode(stat2.mode) !== OWNER_ONLY_FILE_MODE) {
1544
1544
  await fs2.chmod(filePath, OWNER_ONLY_FILE_MODE);
1545
1545
  }
1546
1546
  }
@@ -2824,8 +2824,8 @@ function expandHomePath(value, homeDir = os4.homedir()) {
2824
2824
  function extractClaudeWrapperTarget(commandPath, options = {}) {
2825
2825
  const { homeDir, maxClaudeWrapperBytes } = resolveOptions(options);
2826
2826
  try {
2827
- const stat = fsSync4.statSync(commandPath);
2828
- if (!stat.isFile() || stat.size > maxClaudeWrapperBytes)
2827
+ const stat2 = fsSync4.statSync(commandPath);
2828
+ if (!stat2.isFile() || stat2.size > maxClaudeWrapperBytes)
2829
2829
  return null;
2830
2830
  const raw = fsSync4.readFileSync(commandPath, "utf8");
2831
2831
  const match = raw.match(/exec\s+(?:"([^"]+)"|'([^']+)'|([^\s]+))/);
@@ -2849,8 +2849,8 @@ function isBrokenClaudeWrapper(commandPath, options = {}) {
2849
2849
  }
2850
2850
  function isExecutableCandidate(candidate, platform = process.platform) {
2851
2851
  try {
2852
- const stat = fsSync4.statSync(candidate);
2853
- if (!stat.isFile())
2852
+ const stat2 = fsSync4.statSync(candidate);
2853
+ if (!stat2.isFile())
2854
2854
  return false;
2855
2855
  if (platform === "win32")
2856
2856
  return true;
@@ -9170,10 +9170,10 @@ function resolveGatewayRestartProbePath(entryPath, modulePath) {
9170
9170
  }
9171
9171
  function readRestartProbeFingerprint(filePath) {
9172
9172
  try {
9173
- const stat = fsSync8.statSync(filePath);
9174
- if (!stat.isFile())
9173
+ const stat2 = fsSync8.statSync(filePath);
9174
+ if (!stat2.isFile())
9175
9175
  return null;
9176
- return `${Math.round(stat.mtimeMs)}:${stat.size}`;
9176
+ return `${Math.round(stat2.mtimeMs)}:${stat2.size}`;
9177
9177
  } catch {
9178
9178
  return null;
9179
9179
  }
@@ -9861,9 +9861,27 @@ function createGatewayRuntimeState() {
9861
9861
  }
9862
9862
 
9863
9863
  // dist/managed-runtime.js
9864
- import { randomUUID as randomUUID5 } from "node:crypto";
9864
+ import { randomUUID as randomUUID8 } from "node:crypto";
9865
+ import { mkdir as mkdir4 } from "node:fs/promises";
9865
9866
  import { setTimeout as sleep3 } from "node:timers/promises";
9866
9867
 
9868
+ // ../shared/dist/drive-transfer-policy.js
9869
+ var DRIVE_TRANSFER_POLICY = {
9870
+ inlineContentMaxBytes: 10 * 1024 * 1024,
9871
+ objectFileMaxBytes: 1024 * 1024 * 1024,
9872
+ directUploadThresholdBytes: 8 * 1024 * 1024,
9873
+ localBatchMaxChanges: 1e3,
9874
+ localBatchInlineUploadMaxBytes: 50 * 1024 * 1024,
9875
+ localSyncBatchUploadMaxBytes: 1024 * 1024 * 1024,
9876
+ signedRequestValidMs: 10 * 60 * 1e3,
9877
+ preparedUploadExpiresMs: 60 * 60 * 1e3,
9878
+ networkRequestTimeoutMs: 6e4,
9879
+ multipartUploadThresholdBytes: null
9880
+ };
9881
+ function getDriveLocalSyncFileTooLargeMessage(sizeBytes) {
9882
+ return `Local drive sync file upload size ${sizeBytes} exceeds the ${DRIVE_TRANSFER_POLICY.objectFileMaxBytes} byte limit`;
9883
+ }
9884
+
9867
9885
  // ../shared/dist/linux-host-control/contract.js
9868
9886
  var LINUX_HOST_CONTROL_ENDPOINT_PATH = "/functions/v1/linux-host-control";
9869
9887
  function buildLinuxHostControlUrl(baseUrl) {
@@ -9887,6 +9905,14 @@ var DEFAULT_EXEC_TIMEOUT_MS = 3e5;
9887
9905
  var DEFAULT_HEARTBEAT_INTERVAL_MS2 = 15e3;
9888
9906
  var DEFAULT_MAX_CONSECUTIVE_HEARTBEAT_FAILURES = 3;
9889
9907
  var DEFAULT_OUTPUT_CAPTURE_BYTES = 5e6;
9908
+ var DEFAULT_DRIVE_SYNC_ROOT = "/panorama/drives";
9909
+ var DEFAULT_DRIVE_SYNC_STATE_DIR = "/var/lib/panorama/drive-sync";
9910
+ var DEFAULT_DRIVE_SYNC_CHANGE_LIMIT = 50;
9911
+ var DEFAULT_DRIVE_SYNC_MAX_BATCHES_PER_TICK = 25;
9912
+ var DEFAULT_DRIVE_SYNC_DEBOUNCE_MS = 1e3;
9913
+ var DEFAULT_DRIVE_SYNC_POLL_INTERVAL_MS = 5e3;
9914
+ var DEFAULT_DRIVE_SYNC_LOCAL_AUDIT_INTERVAL_MS = 3e5;
9915
+ var DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS = DRIVE_TRANSFER_POLICY.networkRequestTimeoutMs;
9890
9916
  function requireEnv(name, env = process16.env) {
9891
9917
  const value = env[name]?.trim();
9892
9918
  if (!value) {
@@ -9904,6 +9930,16 @@ function readPositiveInt(name, fallback, env = process16.env) {
9904
9930
  }
9905
9931
  return Math.floor(parsed);
9906
9932
  }
9933
+ function readBoolean(name, fallback, env = process16.env) {
9934
+ const raw = env[name]?.trim().toLowerCase();
9935
+ if (!raw)
9936
+ return fallback;
9937
+ if (["1", "true", "yes", "on"].includes(raw))
9938
+ return true;
9939
+ if (["0", "false", "no", "off"].includes(raw))
9940
+ return false;
9941
+ throw new Error(`${name} must be a boolean`);
9942
+ }
9907
9943
  function requirePositiveInt(name, env = process16.env) {
9908
9944
  const raw = requireEnv(name, env);
9909
9945
  const parsed = Number(raw);
@@ -9948,7 +9984,19 @@ function resolveManagedGatewayRuntimeConfig(env = process16.env) {
9948
9984
  execTimeoutMs: readPositiveInt("PANORAMA_AGENT_EXEC_TIMEOUT_MS", DEFAULT_EXEC_TIMEOUT_MS, env),
9949
9985
  outputCaptureBytes: readPositiveInt("PANORAMA_MANAGED_RUNTIME_OUTPUT_CAPTURE_BYTES", DEFAULT_OUTPUT_CAPTURE_BYTES, env),
9950
9986
  heartbeatIntervalMs: readPositiveInt("PANORAMA_MANAGED_RUNTIME_HEARTBEAT_INTERVAL_MS", DEFAULT_HEARTBEAT_INTERVAL_MS2, env),
9951
- maxConsecutiveHeartbeatFailures: readPositiveInt("PANORAMA_MANAGED_RUNTIME_MAX_CONSECUTIVE_HEARTBEAT_FAILURES", DEFAULT_MAX_CONSECUTIVE_HEARTBEAT_FAILURES, env)
9987
+ maxConsecutiveHeartbeatFailures: readPositiveInt("PANORAMA_MANAGED_RUNTIME_MAX_CONSECUTIVE_HEARTBEAT_FAILURES", DEFAULT_MAX_CONSECUTIVE_HEARTBEAT_FAILURES, env),
9988
+ driveSync: {
9989
+ enabled: readBoolean("PANORAMA_DRIVE_SYNC_ENABLED", false, env),
9990
+ rootDir: env.PANORAMA_DRIVE_SYNC_ROOT?.trim() || DEFAULT_DRIVE_SYNC_ROOT,
9991
+ stateDir: env.PANORAMA_DRIVE_SYNC_STATE_DIR?.trim() || DEFAULT_DRIVE_SYNC_STATE_DIR,
9992
+ clientKey: env.PANORAMA_DRIVE_SYNC_CLIENT_KEY?.trim() || null,
9993
+ changeLimit: readPositiveInt("PANORAMA_DRIVE_SYNC_CHANGE_LIMIT", DEFAULT_DRIVE_SYNC_CHANGE_LIMIT, env),
9994
+ maxBatchesPerTick: readPositiveInt("PANORAMA_DRIVE_SYNC_MAX_BATCHES_PER_TICK", DEFAULT_DRIVE_SYNC_MAX_BATCHES_PER_TICK, env),
9995
+ debounceMs: readPositiveInt("PANORAMA_DRIVE_SYNC_DEBOUNCE_MS", DEFAULT_DRIVE_SYNC_DEBOUNCE_MS, env),
9996
+ pollIntervalMs: readPositiveInt("PANORAMA_DRIVE_SYNC_POLL_INTERVAL_MS", DEFAULT_DRIVE_SYNC_POLL_INTERVAL_MS, env),
9997
+ localAuditIntervalMs: readPositiveInt("PANORAMA_DRIVE_SYNC_LOCAL_AUDIT_INTERVAL_MS", DEFAULT_DRIVE_SYNC_LOCAL_AUDIT_INTERVAL_MS, env),
9998
+ networkTimeoutMs: readPositiveInt("PANORAMA_DRIVE_SYNC_NETWORK_TIMEOUT_MS", DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS, env)
9999
+ }
9952
10000
  };
9953
10001
  }
9954
10002
 
@@ -10353,212 +10401,2977 @@ function startManagedRealtimeWakeLoop(params) {
10353
10401
  };
10354
10402
  }
10355
10403
 
10356
- // ../shared/dist/control-signals.js
10357
- var CONTROL_ACTIONS = /* @__PURE__ */ new Set(["continue", "pause", "stop"]);
10358
- function parseControlSignalCandidate(value) {
10359
- if (!value || typeof value !== "object" || Array.isArray(value)) {
10360
- return null;
10404
+ // dist/managed-runtime/drive-sync.js
10405
+ import { createHash as createHash3, randomUUID as randomUUID6 } from "node:crypto";
10406
+ import { mkdir as mkdir3, rename as rename3, rm as rm2 } from "node:fs/promises";
10407
+ import { hostname as hostname2 } from "node:os";
10408
+ import path18 from "node:path";
10409
+ import process17 from "node:process";
10410
+
10411
+ // dist/managed-runtime/drive-sync-filesystem.js
10412
+ import { createHash, randomUUID as randomUUID4 } from "node:crypto";
10413
+ import { createReadStream } from "node:fs";
10414
+ import { copyFile, lstat, mkdir, readdir, readlink, rename, stat } from "node:fs/promises";
10415
+ import path16 from "node:path";
10416
+ var DRIVE_SYNC_MAX_LOCAL_FILE_BYTES = DRIVE_TRANSFER_POLICY.objectFileMaxBytes;
10417
+ var UnstableLocalFileError = class extends Error {
10418
+ drivePath;
10419
+ constructor(drivePath) {
10420
+ super(`Local drive file changed while it was being read; deferring sync for ${drivePath}`);
10421
+ this.drivePath = drivePath;
10422
+ this.name = "UnstableLocalFileError";
10361
10423
  }
10362
- const rawNextAction = value.nextAction;
10363
- if (typeof rawNextAction !== "string" || !CONTROL_ACTIONS.has(rawNextAction)) {
10424
+ };
10425
+ function resolveDriveLocalRoot(rootDir, driveId) {
10426
+ if (!/^[0-9a-f-]{36}$/i.test(driveId)) {
10427
+ throw new Error(`Invalid drive id "${driveId}"`);
10428
+ }
10429
+ return path16.join(rootDir, driveId);
10430
+ }
10431
+ async function parkStaleDriveLocalFolder(rootDir, driveId) {
10432
+ const sourcePath = resolveDriveLocalRoot(rootDir, driveId);
10433
+ try {
10434
+ await lstat(sourcePath);
10435
+ } catch (error) {
10436
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
10437
+ return null;
10438
+ }
10439
+ throw error;
10440
+ }
10441
+ const staleRoot = path16.join(path16.resolve(rootDir), ".stale");
10442
+ await mkdir(staleRoot, { recursive: true });
10443
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10444
+ const targetPath = path16.join(staleRoot, `${driveId}-${timestamp}-${randomUUID4()}`);
10445
+ await rename(sourcePath, targetPath);
10446
+ return targetPath;
10447
+ }
10448
+ function resolveDriveLocalPath(driveRoot, drivePath) {
10449
+ const normalizedDrivePath = normalizeDrivePath(drivePath);
10450
+ const resolvedRoot = path16.resolve(driveRoot);
10451
+ if (normalizedDrivePath === "/") {
10452
+ return resolvedRoot;
10453
+ }
10454
+ const resolvedPath = path16.resolve(resolvedRoot, normalizedDrivePath.slice(1));
10455
+ if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path16.sep}`)) {
10456
+ throw new Error(`Drive path escapes drive root: ${drivePath}`);
10457
+ }
10458
+ return resolvedPath;
10459
+ }
10460
+ function resolveDirtyDrivePathsForDrive(driveRoot, dirtyLocalPaths) {
10461
+ if (!dirtyLocalPaths) {
10364
10462
  return null;
10365
10463
  }
10366
- const pauseDurationValue = value.pauseDuration ?? value.seconds;
10367
- const pauseDuration = typeof pauseDurationValue === "number" && Number.isFinite(pauseDurationValue) && pauseDurationValue > 0 ? pauseDurationValue : void 0;
10368
- return {
10369
- nextAction: rawNextAction,
10370
- pauseDuration
10371
- };
10464
+ const resolvedRoot = path16.resolve(driveRoot);
10465
+ const dirtyDrivePaths = /* @__PURE__ */ new Set();
10466
+ for (const dirtyLocalPath of dirtyLocalPaths) {
10467
+ const resolvedDirtyPath = path16.resolve(dirtyLocalPath);
10468
+ const relativePath = path16.relative(resolvedRoot, resolvedDirtyPath);
10469
+ if (!relativePath) {
10470
+ return null;
10471
+ }
10472
+ if (relativePath.startsWith("..") || path16.isAbsolute(relativePath)) {
10473
+ continue;
10474
+ }
10475
+ dirtyDrivePaths.add(normalizeDrivePath(`/${relativePath.split(path16.sep).join("/")}`));
10476
+ }
10477
+ return dirtyDrivePaths;
10372
10478
  }
10373
-
10374
- // dist/managed-runtime/shell-execution.js
10375
- import { spawn as spawn4 } from "node:child_process";
10376
- import { randomUUID as randomUUID4 } from "node:crypto";
10377
- import { readFile, unlink } from "node:fs/promises";
10378
- import { tmpdir } from "node:os";
10379
- import { join } from "node:path";
10380
- var DEFAULT_SIGTERM_GRACE_MS = 5e3;
10381
- var DEFAULT_OUTPUT_CAPTURE_BYTES2 = 5e6;
10382
- var CONTROL_SIGNAL_PATH_ENV = "PANORAMA_CONTROL_SIGNAL_PATH";
10383
- async function executeCommand(command, timeoutMs, options = {}) {
10384
- const startedAt = Date.now();
10385
- const outputCaptureBytes = options.outputCaptureBytes ?? DEFAULT_OUTPUT_CAPTURE_BYTES2;
10386
- return await new Promise((resolve) => {
10387
- const child = spawn4("bash", ["-lc", command], {
10388
- stdio: ["ignore", "pipe", "pipe"],
10389
- detached: true,
10390
- env: options.env
10479
+ async function readLocalDriveEntryAtPath(params) {
10480
+ const absolutePath = resolveDriveLocalPath(params.driveRoot, params.drivePath);
10481
+ let localStat;
10482
+ try {
10483
+ localStat = await lstat(absolutePath);
10484
+ } catch (error) {
10485
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
10486
+ return null;
10487
+ }
10488
+ throw error;
10489
+ }
10490
+ if (localStat.isDirectory()) {
10491
+ return {
10492
+ driveId: params.driveId,
10493
+ path: normalizeDrivePath(params.drivePath),
10494
+ entryType: "directory",
10495
+ contentSha256: null,
10496
+ sizeBytes: null,
10497
+ mimeType: null,
10498
+ absolutePath
10499
+ };
10500
+ }
10501
+ if (localStat.isFile()) {
10502
+ return await readStableLocalFileEntry({
10503
+ driveId: params.driveId,
10504
+ drivePath: normalizeDrivePath(params.drivePath),
10505
+ absolutePath,
10506
+ initialStat: localStat
10391
10507
  });
10392
- const stdoutOutput = new BoundedOutputCapture(outputCaptureBytes);
10393
- const stderrOutput = new BoundedOutputCapture(outputCaptureBytes);
10394
- let resolved = false;
10395
- let timedOut = false;
10396
- let spawnError = null;
10397
- let interrupted = false;
10398
- let forceKillHandle = null;
10399
- const finish = (result) => {
10400
- if (resolved)
10401
- return;
10402
- resolved = true;
10403
- clearTimeout(timeoutHandle);
10404
- if (forceKillHandle) {
10405
- clearTimeout(forceKillHandle);
10406
- forceKillHandle = null;
10407
- }
10408
- options.registerCancellation?.(null);
10409
- resolve(result);
10508
+ }
10509
+ if (localStat.isSymbolicLink()) {
10510
+ const linkTarget = await readlink(absolutePath).catch(() => "");
10511
+ return {
10512
+ driveId: params.driveId,
10513
+ path: normalizeDrivePath(params.drivePath),
10514
+ entryType: "symlink",
10515
+ contentSha256: sha256Hex2(Buffer.from(linkTarget)),
10516
+ sizeBytes: Buffer.byteLength(linkTarget),
10517
+ mimeType: null,
10518
+ absolutePath,
10519
+ unsupportedReason: "symlink_entry_unsupported"
10410
10520
  };
10411
- const killProcessGroup = (signal) => {
10412
- if (!child.pid)
10413
- return;
10521
+ }
10522
+ return {
10523
+ driveId: params.driveId,
10524
+ path: normalizeDrivePath(params.drivePath),
10525
+ entryType: "unsupported",
10526
+ contentSha256: null,
10527
+ sizeBytes: null,
10528
+ mimeType: null,
10529
+ absolutePath,
10530
+ unsupportedReason: describeUnsupportedLocalEntry(localStat)
10531
+ };
10532
+ }
10533
+ async function scanLocalDriveEntries(params) {
10534
+ const entries = /* @__PURE__ */ new Map();
10535
+ const deferredPaths = /* @__PURE__ */ new Set();
10536
+ const visitedDirectories = /* @__PURE__ */ new Set();
10537
+ let deferredFileCount = 0;
10538
+ async function addAncestorDirectories(drivePath) {
10539
+ const normalizedPath = normalizeDrivePath(drivePath);
10540
+ const segments = normalizedPath.split("/").filter(Boolean);
10541
+ for (let index = 1; index < segments.length; index += 1) {
10542
+ const ancestorPath = normalizeDrivePath(`/${segments.slice(0, index).join("/")}`);
10543
+ if (entries.has(ancestorPath)) {
10544
+ continue;
10545
+ }
10546
+ const absolutePath = resolveDriveLocalPath(params.driveRoot, ancestorPath);
10414
10547
  try {
10415
- process.kill(-child.pid, signal);
10416
- } catch {
10417
- try {
10418
- child.kill(signal);
10419
- } catch {
10548
+ const ancestorStat = await lstat(absolutePath);
10549
+ if (ancestorStat.isDirectory()) {
10550
+ entries.set(ancestorPath, {
10551
+ driveId: params.driveId,
10552
+ path: ancestorPath,
10553
+ entryType: "directory",
10554
+ contentSha256: null,
10555
+ sizeBytes: null,
10556
+ mimeType: null,
10557
+ absolutePath
10558
+ });
10420
10559
  }
10560
+ } catch (error) {
10561
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
10562
+ return;
10563
+ }
10564
+ throw error;
10421
10565
  }
10422
- };
10423
- const scheduleForceKill = () => {
10424
- if (forceKillHandle)
10425
- return;
10426
- forceKillHandle = setTimeout(() => {
10427
- forceKillHandle = null;
10428
- killProcessGroup("SIGKILL");
10429
- }, DEFAULT_SIGTERM_GRACE_MS);
10430
- forceKillHandle.unref();
10431
- };
10432
- const timeoutHandle = setTimeout(() => {
10433
- if (resolved)
10434
- return;
10435
- timedOut = true;
10436
- killProcessGroup("SIGTERM");
10437
- scheduleForceKill();
10438
- }, timeoutMs);
10439
- options.registerCancellation?.(() => {
10440
- if (resolved || interrupted)
10441
- return;
10442
- interrupted = true;
10443
- killProcessGroup("SIGTERM");
10444
- scheduleForceKill();
10445
- });
10446
- child.stdout?.on("data", (chunk) => {
10447
- stdoutOutput.append(chunk);
10448
- });
10449
- child.stderr?.on("data", (chunk) => {
10450
- stderrOutput.append(chunk);
10451
- });
10452
- child.on("error", (error) => {
10453
- spawnError = error.message;
10454
- });
10455
- child.on("close", (code) => {
10456
- const stdout = stdoutOutput.readText();
10457
- const stderr = stderrOutput.readText();
10458
- const durationMs = Date.now() - startedAt;
10459
- const commonResult = {
10460
- stdout,
10461
- stderr,
10462
- stdoutObservedBytes: stdoutOutput.observedBytes,
10463
- stderrObservedBytes: stderrOutput.observedBytes,
10464
- stdoutCapturedBytes: stdoutOutput.capturedBytes,
10465
- stderrCapturedBytes: stderrOutput.capturedBytes,
10466
- stdoutTruncated: stdoutOutput.truncated,
10467
- stderrTruncated: stderrOutput.truncated,
10468
- outputCaptureBytes,
10469
- exitCode: code,
10470
- durationMs
10471
- };
10472
- if (timedOut) {
10473
- finish({
10474
- ...commonResult,
10475
- status: "timed_out",
10476
- error: `Command timed out after ${timeoutMs}ms`
10477
- });
10478
- return;
10479
- }
10480
- if (interrupted) {
10481
- finish({
10482
- ...commonResult,
10483
- status: "cancelled",
10484
- error: "Command was interrupted because the managed gateway host was stopping."
10485
- });
10486
- return;
10487
- }
10488
- if (spawnError) {
10489
- finish({
10490
- ...commonResult,
10491
- status: "failed",
10492
- error: spawnError
10493
- });
10494
- return;
10566
+ }
10567
+ }
10568
+ async function addPathEntry(absolutePath, drivePath, childStat) {
10569
+ await addAncestorDirectories(drivePath);
10570
+ if (childStat.isDirectory()) {
10571
+ entries.set(drivePath, {
10572
+ driveId: params.driveId,
10573
+ path: drivePath,
10574
+ entryType: "directory",
10575
+ contentSha256: null,
10576
+ sizeBytes: null,
10577
+ mimeType: null,
10578
+ absolutePath
10579
+ });
10580
+ await visitDirectory(absolutePath, drivePath);
10581
+ return;
10582
+ }
10583
+ if (childStat.isFile()) {
10584
+ try {
10585
+ entries.set(drivePath, await readStableLocalFileEntry({
10586
+ driveId: params.driveId,
10587
+ drivePath,
10588
+ absolutePath,
10589
+ initialStat: childStat
10590
+ }));
10591
+ } catch (error) {
10592
+ if (error instanceof UnstableLocalFileError) {
10593
+ deferredPaths.add(drivePath);
10594
+ deferredFileCount += 1;
10595
+ return;
10596
+ }
10597
+ throw error;
10495
10598
  }
10496
- finish({
10497
- ...commonResult,
10498
- status: code === 0 ? "succeeded" : "failed",
10499
- error: code === 0 ? null : `Command exited with code ${code ?? "unknown"}`
10599
+ return;
10600
+ }
10601
+ if (childStat.isSymbolicLink()) {
10602
+ const linkTarget = await readlink(absolutePath).catch(() => "");
10603
+ entries.set(drivePath, {
10604
+ driveId: params.driveId,
10605
+ path: drivePath,
10606
+ entryType: "symlink",
10607
+ contentSha256: sha256Hex2(Buffer.from(linkTarget)),
10608
+ sizeBytes: Buffer.byteLength(linkTarget),
10609
+ mimeType: null,
10610
+ absolutePath,
10611
+ unsupportedReason: "symlink_entry_unsupported"
10500
10612
  });
10613
+ return;
10614
+ }
10615
+ entries.set(drivePath, {
10616
+ driveId: params.driveId,
10617
+ path: drivePath,
10618
+ entryType: "unsupported",
10619
+ contentSha256: null,
10620
+ sizeBytes: null,
10621
+ mimeType: null,
10622
+ absolutePath,
10623
+ unsupportedReason: describeUnsupportedLocalEntry(childStat)
10501
10624
  });
10502
- });
10503
- }
10504
- function buildManagedShellExecutionEnv(params) {
10505
- const safeEnv = {};
10506
- const passthroughKeys = [
10507
- "HOME",
10508
- "HOSTNAME",
10509
- "LANG",
10510
- "LC_ALL",
10511
- "LOGNAME",
10512
- "PATH",
10513
- "PWD",
10514
- "SHELL",
10515
- "SHLVL",
10516
- "TERM",
10517
- "TMPDIR",
10518
- "USER"
10519
- ];
10520
- for (const key of passthroughKeys) {
10521
- const value = params.baseEnv[key];
10522
- if (typeof value === "string" && value.length > 0) {
10523
- safeEnv[key] = value;
10625
+ }
10626
+ async function visitDirectory(absoluteDirectoryPath, driveDirectoryPath) {
10627
+ const resolvedDirectoryPath = path16.resolve(absoluteDirectoryPath);
10628
+ if (visitedDirectories.has(resolvedDirectoryPath)) {
10629
+ return;
10630
+ }
10631
+ visitedDirectories.add(resolvedDirectoryPath);
10632
+ const children = await readdir(absoluteDirectoryPath, { withFileTypes: true });
10633
+ for (const child of children) {
10634
+ const childAbsolutePath = path16.join(absoluteDirectoryPath, child.name);
10635
+ const childDrivePath = normalizeDrivePath(driveDirectoryPath === "/" ? `/${child.name}` : `${driveDirectoryPath}/${child.name}`);
10636
+ let childStat;
10637
+ try {
10638
+ childStat = await lstat(childAbsolutePath);
10639
+ } catch (error) {
10640
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
10641
+ continue;
10642
+ }
10643
+ throw error;
10644
+ }
10645
+ await addPathEntry(childAbsolutePath, childDrivePath, childStat);
10524
10646
  }
10525
10647
  }
10526
- safeEnv.PANORAMA_AGENT_ID = params.agentId;
10527
- safeEnv.PANORAMA_TOOL_EXECUTION_URL = params.toolExecutionUrl;
10528
- safeEnv.PANORAMA_TOOL_EXECUTION_TOKEN = params.toolExecutionToken;
10529
- safeEnv[CONTROL_SIGNAL_PATH_ENV] = params.controlSignalPath;
10530
- if (params.cycleId) {
10531
- safeEnv.PANORAMA_AGENT_CYCLE_ID = params.cycleId;
10648
+ const rootDrivePaths = params.rootDrivePaths;
10649
+ if (!rootDrivePaths) {
10650
+ await visitDirectory(path16.resolve(params.driveRoot), "/");
10651
+ return { entries, deferredPaths, deferredFileCount };
10532
10652
  }
10533
- return safeEnv;
10534
- }
10535
- function buildControlSignalPath(executionId) {
10536
- return join(tmpdir(), `panorama-control-${executionId}-${randomUUID4()}.json`);
10537
- }
10538
- async function readCommandControlSignal(path17) {
10539
- try {
10540
- const raw = await readFile(path17, "utf8");
10541
- return parseControlSignalCandidate(JSON.parse(raw));
10542
- } catch {
10543
- return null;
10544
- } finally {
10653
+ for (const dirtyPath of rootDrivePaths) {
10654
+ const normalizedDirtyPath = normalizeDrivePath(dirtyPath);
10655
+ if (normalizedDirtyPath === "/") {
10656
+ await visitDirectory(path16.resolve(params.driveRoot), "/");
10657
+ return { entries, deferredPaths, deferredFileCount };
10658
+ }
10659
+ const absolutePath = resolveDriveLocalPath(params.driveRoot, normalizedDirtyPath);
10545
10660
  try {
10546
- await unlink(path17);
10547
- } catch {
10661
+ const childStat = await lstat(absolutePath);
10662
+ await addPathEntry(absolutePath, normalizedDirtyPath, childStat);
10663
+ } catch (error) {
10664
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
10665
+ await addAncestorDirectories(normalizedDirtyPath);
10666
+ continue;
10667
+ }
10668
+ throw error;
10548
10669
  }
10549
10670
  }
10671
+ return { entries, deferredPaths, deferredFileCount };
10550
10672
  }
10551
-
10552
- // dist/managed-runtime/dependencies.js
10553
- var defaultManagedGatewayRuntimeDependencies = {
10554
- heartbeatLinuxHost,
10555
- dispatchLinuxExecution,
10556
- startLinuxExecution,
10673
+ async function writeLocalConflictCopy(params) {
10674
+ if (params.source.entryType !== "file") {
10675
+ throw new Error(`Cannot preserve non-file local conflict candidate: ${params.source.path}`);
10676
+ }
10677
+ const conflictDrivePath = await resolveAvailableConflictDrivePath({
10678
+ driveRoot: params.driveRoot,
10679
+ sourcePath: params.source.path,
10680
+ sourceRootPath: params.sourceRootPath,
10681
+ conflictRootPath: params.conflictRootPath
10682
+ });
10683
+ const conflictLocalPath = resolveDriveLocalPath(params.driveRoot, conflictDrivePath);
10684
+ await assertNoSymlinkInExistingPath(params.driveRoot, path16.dirname(conflictLocalPath));
10685
+ await mkdir(path16.dirname(conflictLocalPath), { recursive: true });
10686
+ await copyFile(params.source.absolutePath, conflictLocalPath);
10687
+ return conflictDrivePath;
10688
+ }
10689
+ async function resolveAvailableConflictDrivePath(params) {
10690
+ const parsed = path16.posix.parse(normalizeDrivePath(params.sourcePath));
10691
+ const conflictRoot = normalizeDrivePath(params.conflictRootPath);
10692
+ const sourceRoot = normalizeDrivePath(params.sourceRootPath);
10693
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
10694
+ const baseName = `${parsed.name} (Panorama conflict ${timestamp})`;
10695
+ const targetDirectory = sourceRoot === params.sourcePath ? parsed.dir || "/" : path16.posix.join(conflictRoot, path16.posix.relative(sourceRoot, parsed.dir || "/"));
10696
+ for (let attempt = 0; attempt < 100; attempt += 1) {
10697
+ const suffix = attempt === 0 ? "" : ` ${attempt + 1}`;
10698
+ const candidate = normalizeDrivePath(path16.posix.join(targetDirectory, `${baseName}${suffix}${parsed.ext}`));
10699
+ try {
10700
+ await lstat(resolveDriveLocalPath(params.driveRoot, candidate));
10701
+ } catch (error) {
10702
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
10703
+ return candidate;
10704
+ }
10705
+ throw error;
10706
+ }
10707
+ }
10708
+ throw new Error(`Could not allocate a local conflict path for ${params.sourcePath}`);
10709
+ }
10710
+ function buildSubtreeConflictRootPath(deletedSubtreePath) {
10711
+ const parsed = path16.posix.parse(normalizeDrivePath(deletedSubtreePath));
10712
+ return normalizeDrivePath(path16.posix.join(parsed.dir || "/", `${parsed.base} (Panorama conflicts)`));
10713
+ }
10714
+ async function assertPathExists(filePath, errorMessage) {
10715
+ try {
10716
+ await stat(filePath);
10717
+ } catch {
10718
+ throw new Error(errorMessage);
10719
+ }
10720
+ }
10721
+ async function assertLocalPathIsNotSymlink(filePath, errorMessage) {
10722
+ try {
10723
+ const current = await lstat(filePath);
10724
+ if (current.isSymbolicLink()) {
10725
+ throw new Error(errorMessage);
10726
+ }
10727
+ } catch (error) {
10728
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
10729
+ return;
10730
+ }
10731
+ throw error;
10732
+ }
10733
+ }
10734
+ async function assertNoSymlinkInExistingPath(driveRoot, targetPath) {
10735
+ const resolvedRoot = path16.resolve(driveRoot);
10736
+ const resolvedTarget = path16.resolve(targetPath);
10737
+ const relativePath = path16.relative(resolvedRoot, resolvedTarget);
10738
+ if (relativePath.startsWith("..") || path16.isAbsolute(relativePath)) {
10739
+ throw new Error(`Local drive path escapes drive root: ${targetPath}`);
10740
+ }
10741
+ if (!relativePath) {
10742
+ return;
10743
+ }
10744
+ let currentPath = resolvedRoot;
10745
+ for (const segment of relativePath.split(path16.sep).filter(Boolean)) {
10746
+ currentPath = path16.join(currentPath, segment);
10747
+ try {
10748
+ const current = await lstat(currentPath);
10749
+ if (current.isSymbolicLink()) {
10750
+ throw new Error(`Local drive path contains a symlink: ${currentPath}`);
10751
+ }
10752
+ } catch (error) {
10753
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
10754
+ return;
10755
+ }
10756
+ throw error;
10757
+ }
10758
+ }
10759
+ }
10760
+ function normalizeDrivePath(input) {
10761
+ if (!input.startsWith("/")) {
10762
+ throw new Error(`Drive path must be absolute: ${input}`);
10763
+ }
10764
+ const segments = input.split("/").filter(Boolean);
10765
+ if (segments.some((segment) => segment === "." || segment === "..")) {
10766
+ throw new Error(`Drive path must not contain traversal segments: ${input}`);
10767
+ }
10768
+ return input === "/" ? "/" : `/${segments.join("/")}`;
10769
+ }
10770
+ async function readStableLocalFileEntry(params) {
10771
+ const initialSizeBytes = Number(params.initialStat.size);
10772
+ const initialMtimeMs = Number(params.initialStat.mtimeMs);
10773
+ const initialCtimeMs = Number(params.initialStat.ctimeMs);
10774
+ if (initialSizeBytes > DRIVE_SYNC_MAX_LOCAL_FILE_BYTES) {
10775
+ return {
10776
+ driveId: params.driveId,
10777
+ path: params.drivePath,
10778
+ entryType: "unsupported",
10779
+ contentSha256: null,
10780
+ sizeBytes: initialSizeBytes,
10781
+ mimeType: null,
10782
+ absolutePath: params.absolutePath,
10783
+ mtimeMs: initialMtimeMs,
10784
+ ctimeMs: initialCtimeMs,
10785
+ unsupportedReason: `file_size_${initialSizeBytes}_exceeds_${DRIVE_SYNC_MAX_LOCAL_FILE_BYTES}_byte_limit`
10786
+ };
10787
+ }
10788
+ const contentSha256 = await sha256File(params.absolutePath);
10789
+ const finalStat = await lstat(params.absolutePath);
10790
+ if (!finalStat.isFile() || Number(finalStat.size) !== initialSizeBytes || Number(finalStat.mtimeMs) !== initialMtimeMs || Number(finalStat.ctimeMs) !== initialCtimeMs) {
10791
+ throw new UnstableLocalFileError(params.drivePath);
10792
+ }
10793
+ return {
10794
+ driveId: params.driveId,
10795
+ path: params.drivePath,
10796
+ entryType: "file",
10797
+ contentSha256,
10798
+ sizeBytes: Number(finalStat.size),
10799
+ mimeType: null,
10800
+ absolutePath: params.absolutePath,
10801
+ mtimeMs: Number(finalStat.mtimeMs),
10802
+ ctimeMs: Number(finalStat.ctimeMs)
10803
+ };
10804
+ }
10805
+ async function sha256File(filePath) {
10806
+ const hash = createHash("sha256");
10807
+ for await (const chunk of createReadStream(filePath)) {
10808
+ hash.update(chunk);
10809
+ }
10810
+ return hash.digest("hex");
10811
+ }
10812
+ function describeUnsupportedLocalEntry(entry) {
10813
+ if (entry.isBlockDevice())
10814
+ return "block_device_unsupported";
10815
+ if (entry.isCharacterDevice())
10816
+ return "character_device_unsupported";
10817
+ if (entry.isFIFO())
10818
+ return "fifo_unsupported";
10819
+ if (entry.isSocket())
10820
+ return "socket_unsupported";
10821
+ return "unsupported_entry_type";
10822
+ }
10823
+ function sha256Hex2(bytes) {
10824
+ return createHash("sha256").update(bytes).digest("hex");
10825
+ }
10826
+
10827
+ // dist/managed-runtime/drive-sync-planner.js
10828
+ function planLocalDriveSync(params) {
10829
+ const conflictPlan = planLocalConflictActions(params);
10830
+ return {
10831
+ mutations: buildLocalDriveMutationsFromPlanState({
10832
+ ...params,
10833
+ conflicts: conflictPlan.effectiveConflicts
10834
+ }),
10835
+ conflictActions: conflictPlan.actions
10836
+ };
10837
+ }
10838
+ function buildLocalDriveMutationsFromPlanState(params) {
10839
+ const mutations = [];
10840
+ const deletedDirectoryPaths = /* @__PURE__ */ new Set();
10841
+ const { moveByNewPath, movedBaselinePaths } = detectLocalFileMoves(params);
10842
+ const absentDeletedDirectoryRoots = /* @__PURE__ */ new Set();
10843
+ const localPaths = [...params.local.keys()].sort(compareDrivePathsForCreate);
10844
+ for (const drivePath of localPaths) {
10845
+ const localEntry = params.local.get(drivePath);
10846
+ const baselineEntry = params.baseline.get(drivePath) ?? null;
10847
+ const conflict = params.conflicts.get(drivePath) ?? null;
10848
+ if (baselineEntry && localEntryMatchesBaseline(localEntry, baselineEntry)) {
10849
+ continue;
10850
+ }
10851
+ if (conflict && localEntryMatchesConflict(localEntry, conflict)) {
10852
+ continue;
10853
+ }
10854
+ const movedFromEntry = moveByNewPath.get(drivePath) ?? null;
10855
+ if (!baselineEntry && movedFromEntry) {
10856
+ mutations.push(buildMoveMutation(movedFromEntry, localEntry));
10857
+ continue;
10858
+ }
10859
+ if (baselineEntry && baselineEntry.entryType !== localEntry.entryType) {
10860
+ mutations.push(buildDeleteMutation(baselineEntry));
10861
+ if (baselineEntry.entryType === "directory") {
10862
+ deletedDirectoryPaths.add(baselineEntry.path);
10863
+ }
10864
+ }
10865
+ mutations.push(buildUpsertMutation(localEntry, baselineEntry));
10866
+ }
10867
+ for (const [drivePath, baselineEntry] of [...params.baseline].sort(([left], [right]) => compareDrivePathsForCreate(left, right))) {
10868
+ if (baselineEntry.entryType !== "directory") {
10869
+ continue;
10870
+ }
10871
+ if (params.local.has(drivePath)) {
10872
+ continue;
10873
+ }
10874
+ if (params.deferredPaths.has(drivePath)) {
10875
+ continue;
10876
+ }
10877
+ if (params.conflicts.has(drivePath)) {
10878
+ continue;
10879
+ }
10880
+ if (hasConflictInSubtree(drivePath, params.conflicts)) {
10881
+ continue;
10882
+ }
10883
+ if (movedBaselinePaths.has(drivePath)) {
10884
+ continue;
10885
+ }
10886
+ if (hasDeletedAncestor(drivePath, absentDeletedDirectoryRoots)) {
10887
+ continue;
10888
+ }
10889
+ absentDeletedDirectoryRoots.add(drivePath);
10890
+ }
10891
+ const baselinePaths = [...params.baseline.keys()].sort(compareDrivePathsForDelete);
10892
+ for (const drivePath of baselinePaths) {
10893
+ if (params.local.has(drivePath)) {
10894
+ continue;
10895
+ }
10896
+ if (params.deferredPaths.has(drivePath)) {
10897
+ continue;
10898
+ }
10899
+ if (params.conflicts.has(drivePath)) {
10900
+ continue;
10901
+ }
10902
+ if (movedBaselinePaths.has(drivePath)) {
10903
+ continue;
10904
+ }
10905
+ if (hasDeletedAncestor(drivePath, absentDeletedDirectoryRoots)) {
10906
+ continue;
10907
+ }
10908
+ if (hasDeletedAncestor(drivePath, deletedDirectoryPaths)) {
10909
+ continue;
10910
+ }
10911
+ const baselineEntry = params.baseline.get(drivePath);
10912
+ if (baselineEntry.entryType === "directory" && hasConflictInSubtree(drivePath, params.conflicts)) {
10913
+ continue;
10914
+ }
10915
+ mutations.push(buildDeleteMutation(baselineEntry));
10916
+ if (baselineEntry.entryType === "directory") {
10917
+ deletedDirectoryPaths.add(baselineEntry.path);
10918
+ }
10919
+ }
10920
+ return mutations;
10921
+ }
10922
+ function planLocalConflictActions(params) {
10923
+ const actions = [];
10924
+ const effectiveConflicts = new Map(params.conflicts);
10925
+ for (const conflict of [...params.conflicts.values()].sort((left, right) => left.path.localeCompare(right.path))) {
10926
+ const baselineEntry = params.baseline.get(conflict.path) ?? null;
10927
+ const canonicalEntry = params.local.get(conflict.path) ?? null;
10928
+ const candidateEntry = conflict.conflictPath ? params.local.get(conflict.conflictPath) ?? null : null;
10929
+ const canonicalMatchesSynced = baselineEntry !== null && canonicalEntry !== null && localEntryMatchesBaseline(canonicalEntry, baselineEntry);
10930
+ if (canonicalMatchesSynced && candidateEntry !== null && conflict.conflictPath) {
10931
+ const candidateBaselineEntry = params.baseline.get(conflict.conflictPath) ?? null;
10932
+ if (candidateBaselineEntry !== null && localEntryMatchesBaseline(candidateEntry, candidateBaselineEntry)) {
10933
+ actions.push({
10934
+ type: "clear_conflict",
10935
+ driveId: conflict.driveId,
10936
+ path: conflict.path,
10937
+ reason: "candidate_preserved"
10938
+ });
10939
+ effectiveConflicts.delete(conflict.path);
10940
+ continue;
10941
+ }
10942
+ }
10943
+ if (candidateEntry !== null) {
10944
+ continue;
10945
+ }
10946
+ const movedCandidatePath = canonicalMatchesSynced ? findUniqueMovedConflictCandidatePath(params, conflict) : null;
10947
+ if (movedCandidatePath) {
10948
+ actions.push({
10949
+ type: "update_conflict_path",
10950
+ driveId: conflict.driveId,
10951
+ path: conflict.path,
10952
+ conflictPath: movedCandidatePath,
10953
+ reason: "candidate_moved"
10954
+ });
10955
+ effectiveConflicts.set(conflict.path, {
10956
+ ...conflict,
10957
+ conflictPath: movedCandidatePath
10958
+ });
10959
+ continue;
10960
+ }
10961
+ if (canonicalMatchesSynced || baselineEntry === null && canonicalEntry === null) {
10962
+ actions.push({
10963
+ type: "clear_conflict",
10964
+ driveId: conflict.driveId,
10965
+ path: conflict.path,
10966
+ reason: "candidate_removed"
10967
+ });
10968
+ effectiveConflicts.delete(conflict.path);
10969
+ }
10970
+ }
10971
+ return { actions, effectiveConflicts };
10972
+ }
10973
+ function findUniqueMovedConflictCandidatePath(params, conflict) {
10974
+ const key = conflictCandidateMoveKey(conflict);
10975
+ if (!key) {
10976
+ return null;
10977
+ }
10978
+ let matchedPath = null;
10979
+ for (const [drivePath, localEntry] of params.local) {
10980
+ if (drivePath === conflict.path || drivePath === conflict.conflictPath) {
10981
+ continue;
10982
+ }
10983
+ if (params.baseline.has(drivePath) || params.conflicts.has(drivePath)) {
10984
+ continue;
10985
+ }
10986
+ if (localFileMoveKey(localEntry) !== key) {
10987
+ continue;
10988
+ }
10989
+ if (matchedPath !== null) {
10990
+ return null;
10991
+ }
10992
+ matchedPath = drivePath;
10993
+ }
10994
+ return matchedPath;
10995
+ }
10996
+ function conflictCandidateMoveKey(conflict) {
10997
+ if (!conflict.localContentSha256 || conflict.localSizeBytes === null) {
10998
+ return null;
10999
+ }
11000
+ return `${conflict.localContentSha256}:${conflict.localSizeBytes}`;
11001
+ }
11002
+ function detectLocalFileMoves(params) {
11003
+ const deletedBaselineByKey = /* @__PURE__ */ new Map();
11004
+ const newLocalByKey = /* @__PURE__ */ new Map();
11005
+ for (const [drivePath, baselineEntry] of params.baseline) {
11006
+ if (params.local.has(drivePath) || params.deferredPaths.has(drivePath) || params.conflicts.has(drivePath)) {
11007
+ continue;
11008
+ }
11009
+ const key = localFileMoveKey(baselineEntry);
11010
+ if (!key) {
11011
+ continue;
11012
+ }
11013
+ deletedBaselineByKey.set(key, deletedBaselineByKey.has(key) ? null : baselineEntry);
11014
+ }
11015
+ for (const [drivePath, localEntry] of params.local) {
11016
+ if (params.baseline.has(drivePath) || params.conflicts.has(drivePath)) {
11017
+ continue;
11018
+ }
11019
+ const key = localFileMoveKey(localEntry);
11020
+ if (!key) {
11021
+ continue;
11022
+ }
11023
+ newLocalByKey.set(key, newLocalByKey.has(key) ? null : localEntry);
11024
+ }
11025
+ const moveByNewPath = /* @__PURE__ */ new Map();
11026
+ const movedBaselinePaths = /* @__PURE__ */ new Set();
11027
+ for (const [key, baselineEntry] of deletedBaselineByKey) {
11028
+ const localEntry = newLocalByKey.get(key) ?? null;
11029
+ if (!baselineEntry || !localEntry) {
11030
+ continue;
11031
+ }
11032
+ moveByNewPath.set(localEntry.path, baselineEntry);
11033
+ movedBaselinePaths.add(baselineEntry.path);
11034
+ }
11035
+ return { moveByNewPath, movedBaselinePaths };
11036
+ }
11037
+ function localFileMoveKey(entry) {
11038
+ if (entry.entryType !== "file" || !entry.contentSha256 || entry.sizeBytes === null) {
11039
+ return null;
11040
+ }
11041
+ return `${entry.contentSha256}:${entry.sizeBytes}`;
11042
+ }
11043
+ function buildUpsertMutation(localEntry, baselineEntry) {
11044
+ if (localEntry.entryType === "directory") {
11045
+ return {
11046
+ request: {
11047
+ type: "mkdir",
11048
+ path: localEntry.path
11049
+ },
11050
+ stateEntry: toStateEntry(localEntry, null),
11051
+ uploadBytes: 0
11052
+ };
11053
+ }
11054
+ if (localEntry.entryType === "file") {
11055
+ if (!localEntry.contentSha256 || localEntry.sizeBytes === null) {
11056
+ throw new Error(`Local drive file scan is missing content metadata: ${localEntry.path}`);
11057
+ }
11058
+ return {
11059
+ request: {
11060
+ type: "write_file",
11061
+ path: localEntry.path,
11062
+ content_sha256: localEntry.contentSha256,
11063
+ size_bytes: localEntry.sizeBytes,
11064
+ mime_type: localEntry.mimeType,
11065
+ base_version_id: baselineEntry?.entryType === "file" ? baselineEntry.versionId : null
11066
+ },
11067
+ stateEntry: toStateEntry(localEntry, baselineEntry?.entryType === "file" ? baselineEntry.versionId : null),
11068
+ uploadBytes: localEntry.sizeBytes ?? 0,
11069
+ localFilePath: localEntry.absolutePath,
11070
+ localFingerprint: localEntry.mtimeMs === void 0 || localEntry.ctimeMs === void 0 ? void 0 : {
11071
+ sizeBytes: localEntry.sizeBytes,
11072
+ mtimeMs: localEntry.mtimeMs,
11073
+ ctimeMs: localEntry.ctimeMs
11074
+ }
11075
+ };
11076
+ }
11077
+ return {
11078
+ request: {
11079
+ type: localEntry.entryType === "symlink" ? "symlink" : "unsupported",
11080
+ path: localEntry.path,
11081
+ entry_type: localEntry.entryType,
11082
+ reason: localEntry.unsupportedReason ?? "unsupported_entry_type"
11083
+ },
11084
+ stateEntry: toStateEntry(localEntry, baselineEntry?.versionId ?? null),
11085
+ uploadBytes: 0
11086
+ };
11087
+ }
11088
+ function buildMoveMutation(fromEntry, toEntry) {
11089
+ return {
11090
+ request: {
11091
+ type: "move",
11092
+ from_path: fromEntry.path,
11093
+ to_path: toEntry.path,
11094
+ base_version_id: fromEntry.entryType === "file" ? fromEntry.versionId : null
11095
+ },
11096
+ stateEntry: toStateEntry(toEntry, fromEntry.versionId),
11097
+ uploadBytes: 0
11098
+ };
11099
+ }
11100
+ function buildDeleteMutation(entry) {
11101
+ return {
11102
+ request: {
11103
+ type: "delete",
11104
+ path: entry.path,
11105
+ base_version_id: entry.entryType === "file" ? entry.versionId : null
11106
+ },
11107
+ stateEntry: entry,
11108
+ uploadBytes: 0
11109
+ };
11110
+ }
11111
+ function toStateEntry(localEntry, versionId) {
11112
+ return {
11113
+ driveId: localEntry.driveId,
11114
+ path: localEntry.path,
11115
+ entryType: localEntry.entryType,
11116
+ versionId,
11117
+ contentSha256: localEntry.contentSha256,
11118
+ sizeBytes: localEntry.sizeBytes,
11119
+ mimeType: localEntry.mimeType
11120
+ };
11121
+ }
11122
+ function localEntryMatchesBaseline(localEntry, baselineEntry) {
11123
+ return localEntry.entryType === baselineEntry.entryType && localEntry.contentSha256 === baselineEntry.contentSha256 && localEntry.sizeBytes === baselineEntry.sizeBytes;
11124
+ }
11125
+ function localEntryMatchesConflict(localEntry, conflict) {
11126
+ return localEntry.contentSha256 === conflict.localContentSha256 && localEntry.sizeBytes === conflict.localSizeBytes;
11127
+ }
11128
+ function compareDrivePathsForCreate(left, right) {
11129
+ const leftDepth = drivePathDepth(left);
11130
+ const rightDepth = drivePathDepth(right);
11131
+ if (leftDepth !== rightDepth) {
11132
+ return leftDepth - rightDepth;
11133
+ }
11134
+ return left.localeCompare(right);
11135
+ }
11136
+ function compareDrivePathsForDelete(left, right) {
11137
+ const leftDepth = drivePathDepth(left);
11138
+ const rightDepth = drivePathDepth(right);
11139
+ if (leftDepth !== rightDepth) {
11140
+ return rightDepth - leftDepth;
11141
+ }
11142
+ return right.localeCompare(left);
11143
+ }
11144
+ function drivePathDepth(drivePath) {
11145
+ return drivePath.split("/").filter(Boolean).length;
11146
+ }
11147
+ function hasDeletedAncestor(drivePath, deletedDirectoryPaths) {
11148
+ for (const deletedDirectoryPath of deletedDirectoryPaths) {
11149
+ if (deletedDirectoryPath === "/") {
11150
+ return true;
11151
+ }
11152
+ if (drivePath.startsWith(`${deletedDirectoryPath}/`)) {
11153
+ return true;
11154
+ }
11155
+ }
11156
+ return false;
11157
+ }
11158
+ function hasConflictInSubtree(drivePath, conflicts) {
11159
+ for (const conflictPath of conflicts.keys()) {
11160
+ if (drivePathIsInSubtree(conflictPath, drivePath)) {
11161
+ return true;
11162
+ }
11163
+ }
11164
+ return false;
11165
+ }
11166
+ function drivePathIsInSubtree(drivePath, subtreeRoot) {
11167
+ if (subtreeRoot === "/") {
11168
+ return true;
11169
+ }
11170
+ return drivePath === subtreeRoot || drivePath.startsWith(`${subtreeRoot}/`);
11171
+ }
11172
+
11173
+ // dist/managed-runtime/drive-sync-remote-planner.js
11174
+ function planRemoteDriveBatch(batch) {
11175
+ const operations = [];
11176
+ for (const change of batch.paths.filter((pathChange) => pathChange.change_type === "moved_to")) {
11177
+ if (!change.from_path || !change.to_path) {
11178
+ throw new Error(`Drive move batch ${batch.id} is missing from_path or to_path`);
11179
+ }
11180
+ operations.push({
11181
+ type: "move",
11182
+ change,
11183
+ fromPath: change.from_path,
11184
+ toPath: change.to_path,
11185
+ preserve: [
11186
+ {
11187
+ type: "path_conflict_candidate",
11188
+ path: change.from_path,
11189
+ reason: "remote_move_source_conflict"
11190
+ },
11191
+ {
11192
+ type: "overwrite_candidates",
11193
+ path: change.to_path,
11194
+ reason: "remote_move_target_conflict"
11195
+ }
11196
+ ]
11197
+ });
11198
+ }
11199
+ for (const change of batch.paths) {
11200
+ if (change.change_type === "moved_from" || change.change_type === "moved_to") {
11201
+ continue;
11202
+ }
11203
+ if (change.change_type === "unsupported" || change.change_type === "conflicted") {
11204
+ continue;
11205
+ }
11206
+ if (change.change_type === "deleted") {
11207
+ operations.push({
11208
+ type: "delete",
11209
+ change,
11210
+ path: change.path,
11211
+ preserve: [
11212
+ {
11213
+ type: "overwrite_candidates",
11214
+ path: change.path,
11215
+ reason: "remote_delete_conflict",
11216
+ subtreeReason: "remote_delete_subtree_conflict"
11217
+ }
11218
+ ]
11219
+ });
11220
+ continue;
11221
+ }
11222
+ if (change.entry_type === "directory") {
11223
+ operations.push({
11224
+ type: "mkdir",
11225
+ change,
11226
+ path: change.path
11227
+ });
11228
+ continue;
11229
+ }
11230
+ if (change.entry_type !== "file" || !change.version_id) {
11231
+ throw new Error(`Drive file change ${change.id} is missing a file version`);
11232
+ }
11233
+ operations.push({
11234
+ type: "write_file",
11235
+ change,
11236
+ path: change.path,
11237
+ versionId: change.version_id,
11238
+ preserve: [
11239
+ {
11240
+ type: "overwrite_candidates",
11241
+ path: change.path,
11242
+ reason: "remote_write_conflict",
11243
+ subtreeReason: "remote_write_directory_target_conflict"
11244
+ }
11245
+ ]
11246
+ });
11247
+ }
11248
+ return operations;
11249
+ }
11250
+
11251
+ // dist/managed-runtime/drive-sync-state-applier.js
11252
+ function applyLocalDriveConflictActions(params) {
11253
+ for (const action of params.actions) {
11254
+ if (action.type === "clear_conflict") {
11255
+ params.stateStore.deleteConflict?.(action.driveId, action.path);
11256
+ continue;
11257
+ }
11258
+ const existingConflict = params.stateStore.getConflict?.(action.driveId, action.path) ?? null;
11259
+ if (!existingConflict) {
11260
+ continue;
11261
+ }
11262
+ params.stateStore.upsertConflict?.({
11263
+ ...existingConflict,
11264
+ conflictPath: action.conflictPath
11265
+ });
11266
+ }
11267
+ }
11268
+ function applyAppliedLocalDriveMutationState(params) {
11269
+ if (params.mutation.request.type === "move") {
11270
+ const fromPath = readMutationPath(params.mutation.request, "from_path");
11271
+ const toPath = readMutationPath(params.mutation.request, "to_path");
11272
+ params.stateStore.moveEntryTree(params.driveId, fromPath, toPath);
11273
+ params.stateStore.deleteConflictTree?.(params.driveId, fromPath);
11274
+ params.stateStore.deleteConflictTree?.(params.driveId, toPath);
11275
+ return;
11276
+ }
11277
+ if (params.mutation.request.type === "delete") {
11278
+ const drivePath = readMutationPath(params.mutation.request, "path");
11279
+ params.stateStore.deleteEntryTree(params.driveId, drivePath);
11280
+ params.stateStore.deleteConflictTree?.(params.driveId, drivePath);
11281
+ return;
11282
+ }
11283
+ if (!params.mutation.stateEntry) {
11284
+ return;
11285
+ }
11286
+ params.stateStore.upsertEntry({
11287
+ ...params.mutation.stateEntry,
11288
+ versionId: typeof params.result.version_id === "string" ? params.result.version_id : params.mutation.stateEntry.versionId
11289
+ });
11290
+ params.stateStore.deleteConflict?.(params.driveId, params.mutation.stateEntry.path);
11291
+ }
11292
+ function applyUnsupportedLocalDriveMutationState(params) {
11293
+ if (!params.mutation.stateEntry) {
11294
+ return;
11295
+ }
11296
+ params.stateStore.upsertEntry(params.mutation.stateEntry);
11297
+ params.stateStore.deleteConflict?.(params.driveId, params.mutation.stateEntry.path);
11298
+ }
11299
+ function recordLocalUploadConflict(params) {
11300
+ params.stateStore.upsertConflict?.({
11301
+ driveId: params.driveId,
11302
+ path: params.conflictPath,
11303
+ conflictType: "local_upload",
11304
+ reason: params.result.reason ?? null,
11305
+ localContentSha256: params.mutation.stateEntry?.contentSha256 ?? null,
11306
+ localSizeBytes: params.mutation.stateEntry?.sizeBytes ?? null,
11307
+ localVersionId: typeof params.result.version_id === "string" ? params.result.version_id : params.mutation.stateEntry?.versionId ?? null,
11308
+ conflictId: typeof params.result.conflict_id === "string" ? params.result.conflict_id : null,
11309
+ conflictPath: params.localConflictPath,
11310
+ remoteBatchId: null,
11311
+ remoteSequence: null
11312
+ });
11313
+ }
11314
+ function applyRemoteDriveMoveState(params) {
11315
+ params.stateStore.moveEntryTree(params.driveId, normalizeDrivePath2(params.fromPath), normalizeDrivePath2(params.toPath));
11316
+ }
11317
+ function applyRemoteDriveDeleteState(params) {
11318
+ params.stateStore.deleteEntryTree(params.driveId, normalizeDrivePath2(params.path));
11319
+ }
11320
+ function applyRemoteDriveDirectoryState(params) {
11321
+ params.stateStore.upsertEntry({
11322
+ driveId: params.driveId,
11323
+ path: normalizeDrivePath2(params.path),
11324
+ entryType: "directory",
11325
+ versionId: null,
11326
+ contentSha256: null,
11327
+ sizeBytes: null,
11328
+ mimeType: null
11329
+ });
11330
+ }
11331
+ function applyRemoteDriveFileState(params) {
11332
+ params.stateStore.upsertEntry({
11333
+ driveId: params.driveId,
11334
+ path: normalizeDrivePath2(params.path),
11335
+ entryType: "file",
11336
+ versionId: params.versionId,
11337
+ contentSha256: params.version?.content_sha256 ?? null,
11338
+ sizeBytes: params.version?.size_bytes ?? params.downloadedSizeBytes,
11339
+ mimeType: params.version?.mime_type ?? null
11340
+ });
11341
+ }
11342
+ function recordRemoteApplyConflict(params) {
11343
+ const conflict = {
11344
+ driveId: params.driveId,
11345
+ path: normalizeDrivePath2(params.path),
11346
+ conflictType: "remote_apply",
11347
+ reason: params.reason,
11348
+ localContentSha256: params.localEntry?.contentSha256 ?? null,
11349
+ localSizeBytes: params.localEntry?.sizeBytes ?? null,
11350
+ localVersionId: params.baselineEntry?.versionId ?? null,
11351
+ conflictId: null,
11352
+ conflictPath: params.conflictPath,
11353
+ remoteBatchId: params.remoteBatchId,
11354
+ remoteSequence: params.remoteSequence
11355
+ };
11356
+ params.stateStore.upsertConflict?.(conflict);
11357
+ return conflict;
11358
+ }
11359
+ function resolveLocalMutationResultPath(result, mutation) {
11360
+ if (typeof result.path === "string") {
11361
+ return normalizeDrivePath2(result.path);
11362
+ }
11363
+ if (typeof result.to_path === "string") {
11364
+ return normalizeDrivePath2(result.to_path);
11365
+ }
11366
+ if (typeof result.from_path === "string") {
11367
+ return normalizeDrivePath2(result.from_path);
11368
+ }
11369
+ if (mutation.stateEntry) {
11370
+ return mutation.stateEntry.path;
11371
+ }
11372
+ if (typeof mutation.request.path === "string") {
11373
+ return normalizeDrivePath2(mutation.request.path);
11374
+ }
11375
+ if (typeof mutation.request.to_path === "string") {
11376
+ return normalizeDrivePath2(mutation.request.to_path);
11377
+ }
11378
+ if (typeof mutation.request.from_path === "string") {
11379
+ return normalizeDrivePath2(mutation.request.from_path);
11380
+ }
11381
+ throw new Error("Local drive sync mutation result did not include a path");
11382
+ }
11383
+ function readMutationPath(request, key) {
11384
+ const value = request[key];
11385
+ if (typeof value !== "string") {
11386
+ throw new Error(`Local drive sync mutation is missing ${key}`);
11387
+ }
11388
+ return normalizeDrivePath2(value);
11389
+ }
11390
+ function normalizeDrivePath2(input) {
11391
+ if (!input.startsWith("/")) {
11392
+ throw new Error(`Drive path must be absolute: ${input}`);
11393
+ }
11394
+ const segments = input.split("/").filter(Boolean);
11395
+ if (segments.some((segment) => segment === "." || segment === "..")) {
11396
+ throw new Error(`Drive path must not contain traversal segments: ${input}`);
11397
+ }
11398
+ return input === "/" ? "/" : `/${segments.join("/")}`;
11399
+ }
11400
+
11401
+ // dist/managed-runtime/drive-sync-transfer.js
11402
+ import { createHash as createHash2, randomUUID as randomUUID5 } from "node:crypto";
11403
+ import { createReadStream as createReadStream2, createWriteStream } from "node:fs";
11404
+ import { lstat as lstat2, mkdir as mkdir2, rename as rename2, rm, writeFile } from "node:fs/promises";
11405
+ import path17 from "node:path";
11406
+ import { Readable } from "node:stream";
11407
+ import { pipeline } from "node:stream/promises";
11408
+ var DEFAULT_DRIVE_SYNC_TRANSFER_TIMEOUT_MS = DRIVE_TRANSFER_POLICY.networkRequestTimeoutMs;
11409
+ var LocalDriveFileDeferredError = class extends Error {
11410
+ drivePath;
11411
+ constructor(drivePath, message) {
11412
+ super(message ?? `Local drive file changed while it was being read; deferring sync for ${drivePath}`);
11413
+ this.name = "LocalDriveFileDeferredError";
11414
+ this.drivePath = drivePath;
11415
+ }
11416
+ };
11417
+ function isLocalDriveFileDeferredError(error) {
11418
+ return error instanceof LocalDriveFileDeferredError;
11419
+ }
11420
+ async function prepareLocalMutationUploads(params) {
11421
+ const preparedMutations = [];
11422
+ const deferredPaths = [];
11423
+ const requiresUploadApi = params.mutations.some((mutation) => mutation.request.type === "write_file" && mutation.localFilePath && typeof mutation.request.storage_key !== "string");
11424
+ if (requiresUploadApi && !params.api.prepareFileUpload) {
11425
+ throw new Error("Drive sync direct upload API is not configured");
11426
+ }
11427
+ for (const mutation of params.mutations) {
11428
+ const localFilePath = mutation.localFilePath;
11429
+ if (mutation.request.type !== "write_file" || !localFilePath) {
11430
+ preparedMutations.push(mutation);
11431
+ continue;
11432
+ }
11433
+ if (typeof mutation.request.storage_key === "string") {
11434
+ preparedMutations.push(mutation);
11435
+ continue;
11436
+ }
11437
+ const drivePath = readMutationString(mutation.request, "path");
11438
+ try {
11439
+ const contentSha256 = readMutationString(mutation.request, "content_sha256");
11440
+ const sizeBytes = readMutationNumber(mutation.request, "size_bytes");
11441
+ const versionId = buildDeterministicDriveVersionId({
11442
+ driveId: params.driveId,
11443
+ path: drivePath,
11444
+ contentSha256,
11445
+ sizeBytes,
11446
+ baseVersionId: typeof mutation.request.base_version_id === "string" ? mutation.request.base_version_id : null
11447
+ });
11448
+ await assertLocalFileFingerprint(mutation);
11449
+ const prepared = await params.api.prepareFileUpload({
11450
+ sync_client_id: params.syncClientId,
11451
+ drive_id: params.driveId,
11452
+ path: drivePath,
11453
+ version_id: versionId,
11454
+ content_sha256: contentSha256,
11455
+ size_bytes: sizeBytes,
11456
+ mime_type: typeof mutation.request.mime_type === "string" ? mutation.request.mime_type : null
11457
+ });
11458
+ if (prepared.upload_request) {
11459
+ await uploadLocalFileToSignedRequest({
11460
+ filePath: localFilePath,
11461
+ sizeBytes,
11462
+ signedRequest: prepared.upload_request
11463
+ });
11464
+ } else if (prepared.upload_status !== "committed") {
11465
+ throw new Error("Prepared drive upload did not include an upload request");
11466
+ }
11467
+ await assertLocalFileFingerprint(mutation);
11468
+ mutation.request.version_id = prepared.version_id;
11469
+ mutation.request.storage_key = prepared.storage_key;
11470
+ preparedMutations.push(mutation);
11471
+ } catch (error) {
11472
+ if (!isLocalDriveFileDeferredError(error)) {
11473
+ throw error;
11474
+ }
11475
+ deferredPaths.push(error.drivePath);
11476
+ }
11477
+ }
11478
+ return { preparedMutations, deferredPaths };
11479
+ }
11480
+ async function downloadDriveVersionToLocalFile(params) {
11481
+ const response = await params.api.downloadFileVersion({
11482
+ sync_client_id: params.syncClientId,
11483
+ drive_id: params.driveId,
11484
+ version_id: params.versionId,
11485
+ format: "base64",
11486
+ transfer_mode: "direct"
11487
+ });
11488
+ if (response.file.transfer_mode === "direct" && response.file.download_request) {
11489
+ return await downloadSignedRequestToFile({
11490
+ signedRequest: response.file.download_request,
11491
+ targetPath: params.targetPath,
11492
+ expectedSizeBytes: params.expectedSizeBytes,
11493
+ expectedSha256: params.expectedSha256
11494
+ });
11495
+ }
11496
+ if (response.file.content_format !== "base64" || !response.file.content_base64) {
11497
+ throw new Error(`Drive file version ${params.versionId} was not returned as base64`);
11498
+ }
11499
+ const bytes = Buffer.from(response.file.content_base64, "base64");
11500
+ const contentSha256 = sha256Hex3(bytes);
11501
+ if (params.expectedSizeBytes !== null && bytes.byteLength !== params.expectedSizeBytes) {
11502
+ throw new Error(`Downloaded drive file size mismatch for ${params.versionId}`);
11503
+ }
11504
+ if (params.expectedSha256 && contentSha256 !== params.expectedSha256) {
11505
+ throw new Error(`Downloaded drive file hash mismatch for ${params.versionId}`);
11506
+ }
11507
+ await mkdir2(path17.dirname(params.targetPath), { recursive: true });
11508
+ await writeFile(params.targetPath, bytes);
11509
+ return { sizeBytes: bytes.byteLength, contentSha256 };
11510
+ }
11511
+ async function assertLocalFileFingerprint(mutation) {
11512
+ if (!mutation.localFilePath || !mutation.localFingerprint) {
11513
+ return;
11514
+ }
11515
+ const current = await lstat2(mutation.localFilePath);
11516
+ if (!current.isFile() || current.size !== mutation.localFingerprint.sizeBytes || current.mtimeMs !== mutation.localFingerprint.mtimeMs || current.ctimeMs !== mutation.localFingerprint.ctimeMs) {
11517
+ const drivePath = readMutationString(mutation.request, "path");
11518
+ throw new LocalDriveFileDeferredError(drivePath);
11519
+ }
11520
+ }
11521
+ async function uploadLocalFileToSignedRequest(params) {
11522
+ const headers = new Headers(params.signedRequest.headers);
11523
+ headers.set("content-length", String(params.sizeBytes));
11524
+ const response = await fetchWithTimeout(params.signedRequest.url, {
11525
+ method: params.signedRequest.method,
11526
+ headers,
11527
+ body: createReadStream2(params.filePath),
11528
+ duplex: "half"
11529
+ }, {
11530
+ timeoutMs: DEFAULT_DRIVE_SYNC_TRANSFER_TIMEOUT_MS,
11531
+ label: "Drive object upload"
11532
+ });
11533
+ if (!response.ok) {
11534
+ const body = await response.text().catch(() => "");
11535
+ throw new Error(`Drive object upload failed with status ${response.status}${body ? ` body=${body.slice(0, 500)}` : ""}`);
11536
+ }
11537
+ }
11538
+ async function downloadSignedRequestToFile(params) {
11539
+ const response = await fetchWithTimeout(params.signedRequest.url, {
11540
+ method: params.signedRequest.method,
11541
+ headers: params.signedRequest.headers
11542
+ }, {
11543
+ timeoutMs: DEFAULT_DRIVE_SYNC_TRANSFER_TIMEOUT_MS,
11544
+ label: "Drive object download"
11545
+ });
11546
+ if (!response.ok || !response.body) {
11547
+ const body = await response.text().catch(() => "");
11548
+ throw new Error(`Drive object download failed with status ${response.status}${body ? ` body=${body.slice(0, 500)}` : ""}`);
11549
+ }
11550
+ await mkdir2(path17.dirname(params.targetPath), { recursive: true });
11551
+ const tempPath = `${params.targetPath}.panorama-download-${randomUUID5()}.tmp`;
11552
+ const hash = createHash2("sha256");
11553
+ let sizeBytes = 0;
11554
+ try {
11555
+ const source = Readable.fromWeb(response.body);
11556
+ await pipeline(source, async function* hashChunks(chunks) {
11557
+ for await (const chunk of chunks) {
11558
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
11559
+ sizeBytes += buffer.byteLength;
11560
+ hash.update(buffer);
11561
+ yield buffer;
11562
+ }
11563
+ }, createWriteStream(tempPath));
11564
+ const contentSha256 = hash.digest("hex");
11565
+ if (params.expectedSizeBytes !== null && sizeBytes !== params.expectedSizeBytes) {
11566
+ throw new Error(`Downloaded drive file size mismatch for ${params.targetPath}`);
11567
+ }
11568
+ if (params.expectedSha256 && contentSha256 !== params.expectedSha256) {
11569
+ throw new Error(`Downloaded drive file hash mismatch for ${params.targetPath}`);
11570
+ }
11571
+ await rename2(tempPath, params.targetPath);
11572
+ return { sizeBytes, contentSha256 };
11573
+ } catch (error) {
11574
+ await rm(tempPath, { force: true }).catch(() => void 0);
11575
+ throw error;
11576
+ }
11577
+ }
11578
+ async function fetchWithTimeout(input, init, options) {
11579
+ const controller = new AbortController();
11580
+ const timer = setTimeout(() => {
11581
+ controller.abort();
11582
+ }, options.timeoutMs);
11583
+ timer.unref?.();
11584
+ try {
11585
+ return await fetch(input, {
11586
+ ...init,
11587
+ signal: init.signal ?? controller.signal
11588
+ });
11589
+ } catch (error) {
11590
+ if (controller.signal.aborted) {
11591
+ throw new Error(`${options.label} timed out after ${options.timeoutMs}ms`);
11592
+ }
11593
+ throw error;
11594
+ } finally {
11595
+ clearTimeout(timer);
11596
+ }
11597
+ }
11598
+ function buildDeterministicDriveVersionId(params) {
11599
+ const hash = createHash2("sha256").update([
11600
+ "panorama-drive-version-v1",
11601
+ params.driveId,
11602
+ normalizeDrivePath(params.path),
11603
+ params.contentSha256,
11604
+ String(params.sizeBytes),
11605
+ params.baseVersionId ?? ""
11606
+ ].join("\0")).digest();
11607
+ const bytes = Buffer.from(hash.subarray(0, 16));
11608
+ bytes[6] = bytes[6] & 15 | 80;
11609
+ bytes[8] = bytes[8] & 63 | 128;
11610
+ const hex = bytes.toString("hex");
11611
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
11612
+ }
11613
+ function readMutationString(request, key) {
11614
+ const value = request[key];
11615
+ if (typeof value !== "string") {
11616
+ throw new Error(`Local drive sync mutation is missing ${key}`);
11617
+ }
11618
+ return value;
11619
+ }
11620
+ function readMutationNumber(request, key) {
11621
+ const value = request[key];
11622
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
11623
+ throw new Error(`Local drive sync mutation is missing numeric ${key}`);
11624
+ }
11625
+ return value;
11626
+ }
11627
+ function sha256Hex3(bytes) {
11628
+ return createHash2("sha256").update(bytes).digest("hex");
11629
+ }
11630
+
11631
+ // dist/managed-runtime/drive-sync.js
11632
+ var MAX_LOCAL_BATCH_CHANGES = DRIVE_TRANSFER_POLICY.localBatchMaxChanges;
11633
+ var MAX_LOCAL_BATCH_UPLOAD_BYTES = DRIVE_TRANSFER_POLICY.localSyncBatchUploadMaxBytes;
11634
+ var MAX_LOCAL_FILE_UPLOAD_BYTES = DRIVE_TRANSFER_POLICY.objectFileMaxBytes;
11635
+ var DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS2 = DRIVE_TRANSFER_POLICY.networkRequestTimeoutMs;
11636
+ var DRIVE_SYNC_STATUS_CONFLICT_SAMPLE_LIMIT = 10;
11637
+ function isRecord2(value) {
11638
+ return !!value && typeof value === "object" && !Array.isArray(value);
11639
+ }
11640
+ function mapDriveSyncEntryRow(row) {
11641
+ return {
11642
+ driveId: row.drive_id,
11643
+ path: row.path,
11644
+ entryType: row.entry_type,
11645
+ versionId: row.version_id,
11646
+ contentSha256: row.content_sha256,
11647
+ sizeBytes: row.size_bytes,
11648
+ mimeType: row.mime_type
11649
+ };
11650
+ }
11651
+ function mapDriveSyncConflictRow(row) {
11652
+ return {
11653
+ driveId: row.drive_id,
11654
+ path: row.path,
11655
+ conflictType: row.conflict_type,
11656
+ reason: row.reason,
11657
+ localContentSha256: row.local_content_sha256,
11658
+ localSizeBytes: row.local_size_bytes,
11659
+ localVersionId: row.local_version_id,
11660
+ conflictId: row.conflict_id,
11661
+ conflictPath: row.conflict_path,
11662
+ remoteBatchId: row.remote_batch_id,
11663
+ remoteSequence: row.remote_sequence
11664
+ };
11665
+ }
11666
+ var DriveSyncStateStore = class _DriveSyncStateStore {
11667
+ db;
11668
+ constructor(db) {
11669
+ this.db = db;
11670
+ }
11671
+ static async open(stateDir) {
11672
+ await mkdir3(stateDir, { recursive: true });
11673
+ const sqlite = await import("node:sqlite");
11674
+ const db = new sqlite.DatabaseSync(path18.join(stateDir, "drive-sync.sqlite"));
11675
+ const store = new _DriveSyncStateStore(db);
11676
+ store.ensureSchema();
11677
+ return store;
11678
+ }
11679
+ close() {
11680
+ this.db.close();
11681
+ }
11682
+ getOrCreateClientKey(prefix) {
11683
+ const key = "client_instance_id";
11684
+ const rows = this.db.prepare("SELECT value FROM drive_sync_metadata WHERE key = ?").all(key);
11685
+ const existing = rows[0]?.value;
11686
+ if (existing) {
11687
+ return `${prefix}:${existing}`;
11688
+ }
11689
+ const clientInstanceId = randomUUID6();
11690
+ this.db.prepare(`
11691
+ INSERT INTO drive_sync_metadata (key, value, updated_at)
11692
+ VALUES (?, ?, datetime('now'))
11693
+ ON CONFLICT(key) DO UPDATE SET
11694
+ value = excluded.value,
11695
+ updated_at = excluded.updated_at
11696
+ `).run(key, clientInstanceId);
11697
+ return `${prefix}:${clientInstanceId}`;
11698
+ }
11699
+ getEntry(driveId, drivePath) {
11700
+ const normalizedPath = normalizeDrivePath(drivePath);
11701
+ const rows = this.db.prepare(`
11702
+ SELECT drive_id, path, entry_type, version_id, content_sha256, size_bytes, mime_type
11703
+ FROM drive_sync_entries
11704
+ WHERE drive_id = ? AND path = ?
11705
+ `).all(driveId, normalizedPath);
11706
+ return rows[0] ? mapDriveSyncEntryRow(rows[0]) : null;
11707
+ }
11708
+ listEntries(driveId) {
11709
+ const rows = this.db.prepare(`
11710
+ SELECT drive_id, path, entry_type, version_id, content_sha256, size_bytes, mime_type
11711
+ FROM drive_sync_entries
11712
+ WHERE drive_id = ?
11713
+ ORDER BY path ASC
11714
+ `).all(driveId);
11715
+ return rows.map(mapDriveSyncEntryRow);
11716
+ }
11717
+ listDrives() {
11718
+ const rows = this.db.prepare(`
11719
+ SELECT drive_id, title, root_path
11720
+ FROM drive_sync_drives
11721
+ ORDER BY drive_id ASC
11722
+ `).all();
11723
+ return rows.map((row) => ({
11724
+ id: row.drive_id,
11725
+ title: row.title,
11726
+ rootPath: row.root_path
11727
+ }));
11728
+ }
11729
+ getConflict(driveId, drivePath) {
11730
+ const normalizedPath = normalizeDrivePath(drivePath);
11731
+ const rows = this.db.prepare(`
11732
+ SELECT
11733
+ drive_id,
11734
+ path,
11735
+ conflict_type,
11736
+ reason,
11737
+ local_content_sha256,
11738
+ local_size_bytes,
11739
+ local_version_id,
11740
+ conflict_id,
11741
+ conflict_path,
11742
+ remote_batch_id,
11743
+ remote_sequence
11744
+ FROM drive_sync_conflicts
11745
+ WHERE drive_id = ? AND path = ?
11746
+ `).all(driveId, normalizedPath);
11747
+ return rows[0] ? mapDriveSyncConflictRow(rows[0]) : null;
11748
+ }
11749
+ listConflicts(driveId) {
11750
+ const rows = this.db.prepare(`
11751
+ SELECT
11752
+ drive_id,
11753
+ path,
11754
+ conflict_type,
11755
+ reason,
11756
+ local_content_sha256,
11757
+ local_size_bytes,
11758
+ local_version_id,
11759
+ conflict_id,
11760
+ conflict_path,
11761
+ remote_batch_id,
11762
+ remote_sequence
11763
+ FROM drive_sync_conflicts
11764
+ WHERE drive_id = ?
11765
+ ORDER BY path ASC
11766
+ `).all(driveId);
11767
+ return rows.map(mapDriveSyncConflictRow);
11768
+ }
11769
+ setSyncClient(client) {
11770
+ this.db.prepare(`
11771
+ INSERT INTO drive_sync_clients (id, team_id, agent_id, host_id, display_name, status, updated_at)
11772
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
11773
+ ON CONFLICT(id) DO UPDATE SET
11774
+ team_id = excluded.team_id,
11775
+ agent_id = excluded.agent_id,
11776
+ host_id = excluded.host_id,
11777
+ display_name = excluded.display_name,
11778
+ status = excluded.status,
11779
+ updated_at = excluded.updated_at
11780
+ `).run(client.id, client.team_id, client.agent_id, client.host_id, client.display_name, client.status);
11781
+ }
11782
+ upsertDrive(drive) {
11783
+ this.db.prepare(`
11784
+ INSERT INTO drive_sync_drives (drive_id, title, root_path, updated_at)
11785
+ VALUES (?, ?, ?, datetime('now'))
11786
+ ON CONFLICT(drive_id) DO UPDATE SET
11787
+ title = excluded.title,
11788
+ root_path = excluded.root_path,
11789
+ updated_at = excluded.updated_at
11790
+ `).run(drive.id, drive.title, drive.rootPath);
11791
+ }
11792
+ removeDrive(driveId) {
11793
+ this.db.prepare("DELETE FROM drive_sync_conflicts WHERE drive_id = ?").run(driveId);
11794
+ this.db.prepare("DELETE FROM drive_sync_entries WHERE drive_id = ?").run(driveId);
11795
+ this.db.prepare("DELETE FROM drive_sync_cursors WHERE drive_id = ?").run(driveId);
11796
+ this.db.prepare("DELETE FROM drive_sync_drives WHERE drive_id = ?").run(driveId);
11797
+ }
11798
+ setCursor(driveId, lastBatchSequence) {
11799
+ this.db.prepare(`
11800
+ INSERT INTO drive_sync_cursors (drive_id, last_batch_sequence, updated_at)
11801
+ VALUES (?, ?, datetime('now'))
11802
+ ON CONFLICT(drive_id) DO UPDATE SET
11803
+ last_batch_sequence = excluded.last_batch_sequence,
11804
+ updated_at = excluded.updated_at
11805
+ `).run(driveId, lastBatchSequence);
11806
+ }
11807
+ upsertEntry(entry) {
11808
+ this.db.prepare(`
11809
+ INSERT INTO drive_sync_entries (
11810
+ drive_id,
11811
+ path,
11812
+ entry_type,
11813
+ version_id,
11814
+ content_sha256,
11815
+ size_bytes,
11816
+ mime_type,
11817
+ updated_at
11818
+ )
11819
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
11820
+ ON CONFLICT(drive_id, path) DO UPDATE SET
11821
+ entry_type = excluded.entry_type,
11822
+ version_id = excluded.version_id,
11823
+ content_sha256 = excluded.content_sha256,
11824
+ size_bytes = excluded.size_bytes,
11825
+ mime_type = excluded.mime_type,
11826
+ updated_at = excluded.updated_at
11827
+ `).run(entry.driveId, entry.path, entry.entryType, entry.versionId, entry.contentSha256, entry.sizeBytes, entry.mimeType);
11828
+ }
11829
+ upsertConflict(conflict) {
11830
+ this.db.prepare(`
11831
+ INSERT INTO drive_sync_conflicts (
11832
+ drive_id,
11833
+ path,
11834
+ conflict_type,
11835
+ reason,
11836
+ local_content_sha256,
11837
+ local_size_bytes,
11838
+ local_version_id,
11839
+ conflict_id,
11840
+ conflict_path,
11841
+ remote_batch_id,
11842
+ remote_sequence,
11843
+ updated_at
11844
+ )
11845
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
11846
+ ON CONFLICT(drive_id, path) DO UPDATE SET
11847
+ conflict_type = excluded.conflict_type,
11848
+ reason = excluded.reason,
11849
+ local_content_sha256 = excluded.local_content_sha256,
11850
+ local_size_bytes = excluded.local_size_bytes,
11851
+ local_version_id = excluded.local_version_id,
11852
+ conflict_id = excluded.conflict_id,
11853
+ conflict_path = excluded.conflict_path,
11854
+ remote_batch_id = excluded.remote_batch_id,
11855
+ remote_sequence = excluded.remote_sequence,
11856
+ updated_at = excluded.updated_at
11857
+ `).run(conflict.driveId, conflict.path, conflict.conflictType, conflict.reason, conflict.localContentSha256, conflict.localSizeBytes, conflict.localVersionId, conflict.conflictId, conflict.conflictPath, conflict.remoteBatchId, conflict.remoteSequence);
11858
+ }
11859
+ deleteConflict(driveId, drivePath) {
11860
+ this.db.prepare("DELETE FROM drive_sync_conflicts WHERE drive_id = ? AND path = ?").run(driveId, normalizeDrivePath(drivePath));
11861
+ }
11862
+ deleteConflictTree(driveId, drivePath) {
11863
+ const normalizedPath = normalizeDrivePath(drivePath);
11864
+ if (normalizedPath === "/") {
11865
+ this.db.prepare("DELETE FROM drive_sync_conflicts WHERE drive_id = ?").run(driveId);
11866
+ return;
11867
+ }
11868
+ const prefix = `${normalizedPath}/`;
11869
+ this.db.prepare(`
11870
+ DELETE FROM drive_sync_conflicts
11871
+ WHERE drive_id = ?
11872
+ AND (path = ? OR substr(path, 1, ?) = ?)
11873
+ `).run(driveId, normalizedPath, prefix.length, prefix);
11874
+ }
11875
+ deleteEntryTree(driveId, drivePath) {
11876
+ const normalizedPath = normalizeDrivePath(drivePath);
11877
+ if (normalizedPath === "/") {
11878
+ this.db.prepare("DELETE FROM drive_sync_entries WHERE drive_id = ?").run(driveId);
11879
+ return;
11880
+ }
11881
+ const prefix = `${normalizedPath}/`;
11882
+ this.db.prepare(`
11883
+ DELETE FROM drive_sync_entries
11884
+ WHERE drive_id = ?
11885
+ AND (path = ? OR substr(path, 1, ?) = ?)
11886
+ `).run(driveId, normalizedPath, prefix.length, prefix);
11887
+ }
11888
+ moveEntryTree(driveId, fromPath, toPath) {
11889
+ const normalizedFromPath = normalizeDrivePath(fromPath);
11890
+ const normalizedToPath = normalizeDrivePath(toPath);
11891
+ const prefix = normalizedFromPath === "/" ? "/" : `${normalizedFromPath}/`;
11892
+ const rows = this.db.prepare(`
11893
+ SELECT path, entry_type, version_id, content_sha256, size_bytes, mime_type
11894
+ FROM drive_sync_entries
11895
+ WHERE drive_id = ?
11896
+ AND (path = ? OR substr(path, 1, ?) = ?)
11897
+ `).all(driveId, normalizedFromPath, prefix.length, prefix);
11898
+ this.deleteEntryTree(driveId, normalizedFromPath);
11899
+ for (const row of rows) {
11900
+ const suffix = row.path === normalizedFromPath ? "" : row.path.slice(normalizedFromPath.length);
11901
+ this.upsertEntry({
11902
+ driveId,
11903
+ path: `${normalizedToPath}${suffix}`,
11904
+ entryType: row.entry_type,
11905
+ versionId: row.version_id,
11906
+ contentSha256: row.content_sha256,
11907
+ sizeBytes: row.size_bytes,
11908
+ mimeType: row.mime_type
11909
+ });
11910
+ }
11911
+ }
11912
+ ensureSchema() {
11913
+ this.db.exec(`
11914
+ PRAGMA journal_mode = WAL;
11915
+ CREATE TABLE IF NOT EXISTS drive_sync_metadata (
11916
+ key TEXT PRIMARY KEY,
11917
+ value TEXT NOT NULL,
11918
+ updated_at TEXT NOT NULL
11919
+ );
11920
+ CREATE TABLE IF NOT EXISTS drive_sync_clients (
11921
+ id TEXT PRIMARY KEY,
11922
+ team_id TEXT NOT NULL,
11923
+ agent_id TEXT,
11924
+ host_id TEXT,
11925
+ display_name TEXT,
11926
+ status TEXT NOT NULL,
11927
+ updated_at TEXT NOT NULL
11928
+ );
11929
+ CREATE TABLE IF NOT EXISTS drive_sync_drives (
11930
+ drive_id TEXT PRIMARY KEY,
11931
+ title TEXT,
11932
+ root_path TEXT NOT NULL,
11933
+ updated_at TEXT NOT NULL
11934
+ );
11935
+ CREATE TABLE IF NOT EXISTS drive_sync_cursors (
11936
+ drive_id TEXT PRIMARY KEY,
11937
+ last_batch_sequence INTEGER NOT NULL,
11938
+ updated_at TEXT NOT NULL
11939
+ );
11940
+ CREATE TABLE IF NOT EXISTS drive_sync_entries (
11941
+ drive_id TEXT NOT NULL,
11942
+ path TEXT NOT NULL,
11943
+ entry_type TEXT NOT NULL,
11944
+ version_id TEXT,
11945
+ content_sha256 TEXT,
11946
+ size_bytes INTEGER,
11947
+ mime_type TEXT,
11948
+ updated_at TEXT NOT NULL,
11949
+ PRIMARY KEY (drive_id, path)
11950
+ );
11951
+ CREATE TABLE IF NOT EXISTS drive_sync_conflicts (
11952
+ drive_id TEXT NOT NULL,
11953
+ path TEXT NOT NULL,
11954
+ conflict_type TEXT NOT NULL,
11955
+ reason TEXT,
11956
+ local_content_sha256 TEXT,
11957
+ local_size_bytes INTEGER,
11958
+ local_version_id TEXT,
11959
+ conflict_id TEXT,
11960
+ conflict_path TEXT,
11961
+ remote_batch_id TEXT,
11962
+ remote_sequence INTEGER,
11963
+ updated_at TEXT NOT NULL,
11964
+ PRIMARY KEY (drive_id, path)
11965
+ );
11966
+ `);
11967
+ }
11968
+ };
11969
+ async function createManagedDriveSyncService(params) {
11970
+ const stateStore = params.stateStore ?? await DriveSyncStateStore.open(params.config.driveSync.stateDir);
11971
+ const api = params.api ?? createSupabaseDriveSyncApi(params.supabase, {
11972
+ timeoutMs: params.config.driveSync.networkTimeoutMs
11973
+ });
11974
+ const service = new ManagedDrivePullSyncService({
11975
+ config: params.config,
11976
+ api,
11977
+ stateStore
11978
+ });
11979
+ await mkdir3(params.config.driveSync.rootDir, { recursive: true });
11980
+ return service;
11981
+ }
11982
+ function createSupabaseDriveSyncApi(supabase, options = {}) {
11983
+ const timeoutMs = options.timeoutMs ?? DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS2;
11984
+ return {
11985
+ registerClient: (body) => invokeDriveSyncFunction(supabase, "register-drive-sync-client", body, timeoutMs),
11986
+ listDrives: (body) => invokeDriveSyncFunction(supabase, "list-sync-drives", body, timeoutMs),
11987
+ getChanges: (body) => invokeDriveSyncFunction(supabase, "get-drive-changes", body, timeoutMs),
11988
+ downloadFileVersion: (body) => invokeDriveSyncFunction(supabase, "download-drive-file-version", body, timeoutMs),
11989
+ prepareFileUpload: (body) => invokeDriveSyncFunction(supabase, "prepare-drive-file-upload", body, timeoutMs),
11990
+ ackChanges: (body) => invokeDriveSyncFunction(supabase, "ack-drive-changes", body, timeoutMs),
11991
+ applyLocalChangeBatch: (body) => invokeDriveSyncFunction(supabase, "apply-local-drive-change-batch", body, timeoutMs),
11992
+ applyConflictActions: (body) => invokeDriveSyncFunction(supabase, "apply-drive-conflict-actions", body, timeoutMs),
11993
+ reportStatus: (body) => invokeDriveSyncFunction(supabase, "report-drive-sync-status", body, timeoutMs)
11994
+ };
11995
+ }
11996
+ async function applyDriveChangeBatchToLocalFilesystem(params) {
11997
+ let appliedPathCount = 0;
11998
+ let remoteConflictCount = 0;
11999
+ const remoteConflicts = [];
12000
+ const operations = planRemoteDriveBatch(params.batch);
12001
+ for (const operation of operations) {
12002
+ if (operation.type === "move") {
12003
+ const sourcePath = resolveDriveLocalPath(params.driveRoot, operation.fromPath);
12004
+ const targetPath = resolveDriveLocalPath(params.driveRoot, operation.toPath);
12005
+ await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(sourcePath));
12006
+ const alreadyAppliedMove = await classifyRemoteMoveAlreadyApplied({
12007
+ driveId: params.batch.drive_id,
12008
+ driveRoot: params.driveRoot,
12009
+ stateStore: params.stateStore,
12010
+ fromPath: operation.fromPath,
12011
+ toPath: operation.toPath
12012
+ });
12013
+ if (alreadyAppliedMove) {
12014
+ if (alreadyAppliedMove === "advance_state") {
12015
+ applyRemoteDriveMoveState({
12016
+ stateStore: params.stateStore,
12017
+ driveId: params.batch.drive_id,
12018
+ fromPath: operation.fromPath,
12019
+ toPath: operation.toPath
12020
+ });
12021
+ }
12022
+ appliedPathCount += 1;
12023
+ continue;
12024
+ }
12025
+ const preserved2 = await preserveRemoteApplySteps({
12026
+ driveId: params.batch.drive_id,
12027
+ driveRoot: params.driveRoot,
12028
+ stateStore: params.stateStore,
12029
+ batch: params.batch,
12030
+ steps: operation.preserve
12031
+ });
12032
+ remoteConflictCount += preserved2.conflictCount;
12033
+ remoteConflicts.push(...preserved2.conflicts);
12034
+ await assertPathExists(sourcePath, `Drive move source is missing: ${operation.fromPath}`);
12035
+ await assertLocalPathIsNotSymlink(sourcePath, `Drive move source is a symlink: ${operation.fromPath}`);
12036
+ await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(targetPath));
12037
+ await mkdir3(path18.dirname(targetPath), { recursive: true });
12038
+ await rm2(targetPath, { recursive: true, force: true });
12039
+ await rename3(sourcePath, targetPath);
12040
+ applyRemoteDriveMoveState({
12041
+ stateStore: params.stateStore,
12042
+ driveId: params.batch.drive_id,
12043
+ fromPath: operation.fromPath,
12044
+ toPath: operation.toPath
12045
+ });
12046
+ appliedPathCount += 1;
12047
+ continue;
12048
+ }
12049
+ const localPath = resolveDriveLocalPath(params.driveRoot, operation.path);
12050
+ if (operation.type === "delete") {
12051
+ if (await remoteDeleteAlreadyApplied({
12052
+ driveId: params.batch.drive_id,
12053
+ driveRoot: params.driveRoot,
12054
+ path: operation.path
12055
+ })) {
12056
+ applyRemoteDriveDeleteState({
12057
+ stateStore: params.stateStore,
12058
+ driveId: params.batch.drive_id,
12059
+ path: operation.path
12060
+ });
12061
+ appliedPathCount += 1;
12062
+ continue;
12063
+ }
12064
+ const preserved2 = await preserveRemoteApplySteps({
12065
+ driveId: params.batch.drive_id,
12066
+ driveRoot: params.driveRoot,
12067
+ stateStore: params.stateStore,
12068
+ batch: params.batch,
12069
+ steps: operation.preserve
12070
+ });
12071
+ remoteConflictCount += preserved2.conflictCount;
12072
+ remoteConflicts.push(...preserved2.conflicts);
12073
+ await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(localPath));
12074
+ await rm2(localPath, { recursive: true, force: true });
12075
+ applyRemoteDriveDeleteState({
12076
+ stateStore: params.stateStore,
12077
+ driveId: params.batch.drive_id,
12078
+ path: operation.path
12079
+ });
12080
+ appliedPathCount += 1;
12081
+ continue;
12082
+ }
12083
+ if (operation.type === "mkdir") {
12084
+ await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(localPath));
12085
+ await assertLocalPathIsNotSymlink(localPath, `Drive directory target is a symlink: ${operation.path}`);
12086
+ await mkdir3(localPath, { recursive: true });
12087
+ applyRemoteDriveDirectoryState({
12088
+ stateStore: params.stateStore,
12089
+ driveId: params.batch.drive_id,
12090
+ path: operation.path
12091
+ });
12092
+ appliedPathCount += 1;
12093
+ continue;
12094
+ }
12095
+ const alreadyAppliedFile = await remoteFileAlreadyApplied({
12096
+ driveId: params.batch.drive_id,
12097
+ driveRoot: params.driveRoot,
12098
+ path: operation.path,
12099
+ version: operation.change.version
12100
+ });
12101
+ if (alreadyAppliedFile) {
12102
+ applyRemoteDriveFileState({
12103
+ stateStore: params.stateStore,
12104
+ driveId: params.batch.drive_id,
12105
+ path: operation.path,
12106
+ versionId: operation.versionId,
12107
+ version: operation.change.version,
12108
+ downloadedSizeBytes: alreadyAppliedFile.sizeBytes
12109
+ });
12110
+ appliedPathCount += 1;
12111
+ continue;
12112
+ }
12113
+ const preserved = await preserveRemoteApplySteps({
12114
+ driveId: params.batch.drive_id,
12115
+ driveRoot: params.driveRoot,
12116
+ stateStore: params.stateStore,
12117
+ batch: params.batch,
12118
+ steps: operation.preserve
12119
+ });
12120
+ remoteConflictCount += preserved.conflictCount;
12121
+ remoteConflicts.push(...preserved.conflicts);
12122
+ await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(localPath));
12123
+ await assertLocalPathIsNotSymlink(localPath, `Drive file target is a symlink: ${operation.path}`);
12124
+ if (preserved.overwriteResults.get(operation.path)?.localEntry?.entryType === "directory") {
12125
+ await rm2(localPath, { recursive: true, force: true });
12126
+ }
12127
+ const version = operation.change.version;
12128
+ const downloaded = await downloadDriveVersionToLocalFile({
12129
+ api: params.api,
12130
+ syncClientId: params.syncClientId,
12131
+ driveId: params.batch.drive_id,
12132
+ versionId: operation.versionId,
12133
+ targetPath: localPath,
12134
+ expectedSizeBytes: version?.size_bytes ?? null,
12135
+ expectedSha256: version?.content_sha256 ?? null
12136
+ });
12137
+ applyRemoteDriveFileState({
12138
+ stateStore: params.stateStore,
12139
+ driveId: params.batch.drive_id,
12140
+ path: operation.path,
12141
+ versionId: operation.versionId,
12142
+ version,
12143
+ downloadedSizeBytes: downloaded.sizeBytes
12144
+ });
12145
+ appliedPathCount += 1;
12146
+ }
12147
+ return { appliedPathCount, remoteConflictCount, remoteConflicts };
12148
+ }
12149
+ async function classifyRemoteMoveAlreadyApplied(params) {
12150
+ const normalizedFromPath = normalizeDrivePath(params.fromPath);
12151
+ const normalizedToPath = normalizeDrivePath(params.toPath);
12152
+ const localSource = await readLocalDriveEntryAtPath({
12153
+ driveId: params.driveId,
12154
+ driveRoot: params.driveRoot,
12155
+ drivePath: normalizedFromPath
12156
+ });
12157
+ if (localSource) {
12158
+ return null;
12159
+ }
12160
+ const baselineSource = params.stateStore.getEntry?.(params.driveId, normalizedFromPath) ?? null;
12161
+ const baselineTarget = params.stateStore.getEntry?.(params.driveId, normalizedToPath) ?? null;
12162
+ const localTarget = await readLocalDriveEntryAtPath({
12163
+ driveId: params.driveId,
12164
+ driveRoot: params.driveRoot,
12165
+ drivePath: normalizedToPath
12166
+ });
12167
+ if (!localTarget) {
12168
+ return null;
12169
+ }
12170
+ if (baselineTarget && localEntryMatchesBaseline(localTarget, baselineTarget)) {
12171
+ return "already_applied";
12172
+ }
12173
+ if (baselineSource?.entryType === "file" && localEntryMatchesBaseline(localTarget, baselineSource)) {
12174
+ return "advance_state";
12175
+ }
12176
+ return null;
12177
+ }
12178
+ async function remoteDeleteAlreadyApplied(params) {
12179
+ const localEntry = await readLocalDriveEntryAtPath({
12180
+ driveId: params.driveId,
12181
+ driveRoot: params.driveRoot,
12182
+ drivePath: params.path
12183
+ });
12184
+ return localEntry === null;
12185
+ }
12186
+ async function remoteFileAlreadyApplied(params) {
12187
+ if (!params.version) {
12188
+ return null;
12189
+ }
12190
+ const localEntry = await readLocalDriveEntryAtPath({
12191
+ driveId: params.driveId,
12192
+ driveRoot: params.driveRoot,
12193
+ drivePath: params.path
12194
+ });
12195
+ if (localEntry?.entryType !== "file" || localEntry.contentSha256 !== params.version.content_sha256 || localEntry.sizeBytes !== params.version.size_bytes) {
12196
+ return null;
12197
+ }
12198
+ return { sizeBytes: localEntry.sizeBytes ?? params.version.size_bytes };
12199
+ }
12200
+ async function preserveRemoteApplySteps(params) {
12201
+ let conflictCount = 0;
12202
+ const conflicts = [];
12203
+ const overwriteResults = /* @__PURE__ */ new Map();
12204
+ for (const step of params.steps) {
12205
+ if (step.type === "path_conflict_candidate") {
12206
+ const result2 = await preserveRemoteApplyConflictCandidate({
12207
+ driveId: params.driveId,
12208
+ driveRoot: params.driveRoot,
12209
+ drivePath: step.path,
12210
+ stateStore: params.stateStore,
12211
+ batch: params.batch,
12212
+ reason: step.reason
12213
+ });
12214
+ conflictCount += result2.conflictCount;
12215
+ conflicts.push(...result2.conflicts);
12216
+ continue;
12217
+ }
12218
+ const result = await preserveRemoteApplyOverwriteCandidates({
12219
+ driveId: params.driveId,
12220
+ driveRoot: params.driveRoot,
12221
+ drivePath: step.path,
12222
+ stateStore: params.stateStore,
12223
+ batch: params.batch,
12224
+ reason: step.reason,
12225
+ subtreeReason: step.subtreeReason
12226
+ });
12227
+ conflictCount += result.conflictCount;
12228
+ conflicts.push(...result.conflicts);
12229
+ overwriteResults.set(step.path, {
12230
+ localEntry: result.localEntry,
12231
+ baselineEntry: result.baselineEntry
12232
+ });
12233
+ }
12234
+ return { conflictCount, conflicts, overwriteResults };
12235
+ }
12236
+ async function preserveRemoteApplyConflictCandidate(params) {
12237
+ const normalizedPath = normalizeDrivePath(params.drivePath);
12238
+ const baselineEntry = params.stateStore.getEntry?.(params.driveId, normalizedPath) ?? null;
12239
+ const localEntry = await readLocalDriveEntryAtPath({
12240
+ driveId: params.driveId,
12241
+ driveRoot: params.driveRoot,
12242
+ drivePath: normalizedPath
12243
+ });
12244
+ if (!baselineEntry && !localEntry) {
12245
+ return { conflictCount: 0, conflicts: [] };
12246
+ }
12247
+ if (baselineEntry && localEntry && localEntryMatchesBaseline(localEntry, baselineEntry)) {
12248
+ return { conflictCount: 0, conflicts: [] };
12249
+ }
12250
+ if (!baselineEntry && localEntry?.entryType === "directory") {
12251
+ return { conflictCount: 0, conflicts: [] };
12252
+ }
12253
+ const conflictPath = localEntry?.entryType === "file" ? await writeLocalConflictCopy({
12254
+ driveRoot: params.driveRoot,
12255
+ source: localEntry,
12256
+ sourceRootPath: normalizedPath,
12257
+ conflictRootPath: normalizedPath
12258
+ }) : null;
12259
+ const conflict = recordRemoteApplyConflict({
12260
+ stateStore: params.stateStore,
12261
+ driveId: params.driveId,
12262
+ path: normalizedPath,
12263
+ reason: params.reason,
12264
+ localEntry,
12265
+ baselineEntry,
12266
+ conflictPath,
12267
+ remoteBatchId: params.batch.id,
12268
+ remoteSequence: params.batch.sequence
12269
+ });
12270
+ return { conflictCount: 1, conflicts: [conflict] };
12271
+ }
12272
+ async function preserveRemoteApplyOverwriteCandidates(params) {
12273
+ const normalizedPath = normalizeDrivePath(params.drivePath);
12274
+ const baselineEntry = params.stateStore.getEntry?.(params.driveId, normalizedPath) ?? null;
12275
+ const localEntry = await readLocalDriveEntryAtPath({
12276
+ driveId: params.driveId,
12277
+ driveRoot: params.driveRoot,
12278
+ drivePath: normalizedPath
12279
+ });
12280
+ if (baselineEntry?.entryType === "directory" || localEntry?.entryType === "directory") {
12281
+ const conflictCount = await preserveRemoteApplySubtreeConflictCandidates({
12282
+ driveId: params.driveId,
12283
+ driveRoot: params.driveRoot,
12284
+ drivePath: normalizedPath,
12285
+ stateStore: params.stateStore,
12286
+ batch: params.batch,
12287
+ reason: params.subtreeReason ?? params.reason
12288
+ });
12289
+ return { ...conflictCount, localEntry, baselineEntry };
12290
+ }
12291
+ const conflictResult = await preserveRemoteApplyConflictCandidate({
12292
+ driveId: params.driveId,
12293
+ driveRoot: params.driveRoot,
12294
+ drivePath: normalizedPath,
12295
+ stateStore: params.stateStore,
12296
+ batch: params.batch,
12297
+ reason: params.reason
12298
+ });
12299
+ return { ...conflictResult, localEntry, baselineEntry };
12300
+ }
12301
+ async function preserveRemoteApplySubtreeConflictCandidates(params) {
12302
+ const normalizedPath = normalizeDrivePath(params.drivePath);
12303
+ const baselineEntries = (params.stateStore.listEntries?.(params.driveId) ?? []).filter((entry) => drivePathIsInSubtree(entry.path, normalizedPath));
12304
+ const baselineByPath = new Map(baselineEntries.map((entry) => [entry.path, entry]));
12305
+ const localScan = await scanLocalDriveEntries({
12306
+ driveId: params.driveId,
12307
+ driveRoot: params.driveRoot
12308
+ });
12309
+ for (const deferredPath of localScan.deferredPaths) {
12310
+ if (drivePathIsInSubtree(deferredPath, normalizedPath)) {
12311
+ throw new Error(`Local drive subtree has an unstable file; refusing to delete ${params.drivePath}`);
12312
+ }
12313
+ }
12314
+ let conflictCount = 0;
12315
+ const conflicts = [];
12316
+ for (const [drivePath, localEntry] of localScan.entries) {
12317
+ if (!drivePathIsInSubtree(drivePath, normalizedPath)) {
12318
+ continue;
12319
+ }
12320
+ const baselineEntry = baselineByPath.get(drivePath) ?? null;
12321
+ if (baselineEntry && localEntryMatchesBaseline(localEntry, baselineEntry)) {
12322
+ continue;
12323
+ }
12324
+ if (localEntry.entryType === "directory") {
12325
+ continue;
12326
+ }
12327
+ const conflictPath = localEntry.entryType === "file" ? await writeLocalConflictCopy({
12328
+ driveRoot: params.driveRoot,
12329
+ source: localEntry,
12330
+ sourceRootPath: normalizedPath,
12331
+ conflictRootPath: buildSubtreeConflictRootPath(normalizedPath)
12332
+ }) : null;
12333
+ const conflict = recordRemoteApplyConflict({
12334
+ stateStore: params.stateStore,
12335
+ driveId: params.driveId,
12336
+ path: drivePath,
12337
+ reason: params.reason,
12338
+ localEntry,
12339
+ baselineEntry,
12340
+ conflictPath,
12341
+ remoteBatchId: params.batch.id,
12342
+ remoteSequence: params.batch.sequence
12343
+ });
12344
+ conflicts.push(conflict);
12345
+ conflictCount += 1;
12346
+ }
12347
+ return { conflictCount, conflicts };
12348
+ }
12349
+ async function reconcileLocalDriveToCloud(params) {
12350
+ const fullBaseline = new Map((params.stateStore.listEntries?.(params.driveId) ?? []).map((entry) => [entry.path, entry]));
12351
+ const baseline = params.dirtyDrivePaths ? filterBaselineForDirtyDrivePaths(fullBaseline, params.dirtyDrivePaths) : fullBaseline;
12352
+ const localScan = await scanLocalDriveEntries({
12353
+ driveId: params.driveId,
12354
+ driveRoot: params.driveRoot,
12355
+ rootDrivePaths: params.dirtyDrivePaths ?? void 0
12356
+ });
12357
+ const conflicts = new Map((params.stateStore.listConflicts?.(params.driveId) ?? []).map((conflict) => [
12358
+ conflict.path,
12359
+ conflict
12360
+ ]));
12361
+ const scopedConflicts = params.dirtyDrivePaths ? filterConflictsForDirtyDrivePaths(conflicts, params.dirtyDrivePaths) : conflicts;
12362
+ const localPlan = planLocalDriveSync({
12363
+ baseline,
12364
+ local: localScan.entries,
12365
+ deferredPaths: localScan.deferredPaths,
12366
+ conflicts: scopedConflicts
12367
+ });
12368
+ const confirmedConflictActions = await applyCloudDriveConflictLifecycleActions({
12369
+ syncClientId: params.syncClientId,
12370
+ driveId: params.driveId,
12371
+ api: params.api,
12372
+ conflicts: scopedConflicts,
12373
+ actions: localPlan.conflictActions
12374
+ });
12375
+ applyLocalDriveConflictActions({
12376
+ stateStore: params.stateStore,
12377
+ actions: confirmedConflictActions
12378
+ });
12379
+ const mutations = localPlan.mutations;
12380
+ if (mutations.length === 0) {
12381
+ return {
12382
+ ...emptyLocalReconcileSummary(),
12383
+ deferredFileCount: localScan.deferredFileCount
12384
+ };
12385
+ }
12386
+ let uploadedBatchCount = 0;
12387
+ let uploadedChangeCount = 0;
12388
+ let uploadedAppliedCount = 0;
12389
+ let uploadedConflictCount = 0;
12390
+ let uploadedUnsupportedCount = 0;
12391
+ let uploadedRejectedCount = 0;
12392
+ let deferredFileCount = localScan.deferredFileCount;
12393
+ const uploadResult = await prepareLocalMutationUploads({
12394
+ syncClientId: params.syncClientId,
12395
+ driveId: params.driveId,
12396
+ api: params.api,
12397
+ mutations
12398
+ });
12399
+ deferredFileCount += uploadResult.deferredPaths.length;
12400
+ const preparedMutations = uploadResult.preparedMutations;
12401
+ if (preparedMutations.length === 0) {
12402
+ return {
12403
+ ...emptyLocalReconcileSummary(),
12404
+ deferredFileCount
12405
+ };
12406
+ }
12407
+ const mutationChunks = chunkLocalDriveMutations(preparedMutations);
12408
+ const resourceOperationChunkFingerprints = mutationChunks.map((chunk, chunkIndex) => buildLocalResourceOperationChunkFingerprint({
12409
+ driveId: params.driveId,
12410
+ baseCursorSequence: params.baseCursorSequence,
12411
+ chunkIndex: chunkIndex + 1,
12412
+ chunkCount: mutationChunks.length,
12413
+ mutations: chunk
12414
+ }));
12415
+ const resourceOperationFingerprint = buildLocalResourceOperationFingerprint({
12416
+ driveId: params.driveId,
12417
+ baseCursorSequence: params.baseCursorSequence,
12418
+ chunkFingerprints: resourceOperationChunkFingerprints
12419
+ });
12420
+ const resourceOperationId = `local-reconcile:${resourceOperationFingerprint}`;
12421
+ let finalizedOperationResponse = null;
12422
+ for (const [chunkIndex, chunk] of mutationChunks.entries()) {
12423
+ const response = await params.api.applyLocalChangeBatch({
12424
+ sync_client_id: params.syncClientId,
12425
+ drive_id: params.driveId,
12426
+ resource_operation_id: resourceOperationId,
12427
+ resource_operation_fingerprint: resourceOperationFingerprint,
12428
+ resource_operation_chunk_fingerprint: resourceOperationChunkFingerprints[chunkIndex],
12429
+ resource_operation_chunk_fingerprints: resourceOperationChunkFingerprints,
12430
+ resource_operation_chunk_index: chunkIndex + 1,
12431
+ resource_operation_chunk_count: mutationChunks.length,
12432
+ base_cursor_sequence: params.baseCursorSequence,
12433
+ changes: chunk.map((mutation) => mutation.request)
12434
+ });
12435
+ uploadedBatchCount += 1;
12436
+ uploadedChangeCount += chunk.length;
12437
+ if (isStagedLocalOperationResponse(response)) {
12438
+ continue;
12439
+ }
12440
+ if (isFinalizedFullLocalOperationResponse(response, preparedMutations.length)) {
12441
+ finalizedOperationResponse = response;
12442
+ continue;
12443
+ }
12444
+ await applyAcceptedLocalChangeResults({
12445
+ driveId: params.driveId,
12446
+ driveRoot: params.driveRoot,
12447
+ mutations: chunk,
12448
+ results: response.results,
12449
+ stateStore: params.stateStore
12450
+ });
12451
+ uploadedAppliedCount += response.applied_count;
12452
+ uploadedConflictCount += response.conflict_count;
12453
+ uploadedUnsupportedCount += response.unsupported_count;
12454
+ uploadedRejectedCount += response.rejected_count;
12455
+ }
12456
+ if (finalizedOperationResponse) {
12457
+ await applyAcceptedLocalChangeResults({
12458
+ driveId: params.driveId,
12459
+ driveRoot: params.driveRoot,
12460
+ mutations: preparedMutations,
12461
+ results: finalizedOperationResponse.results,
12462
+ stateStore: params.stateStore
12463
+ });
12464
+ uploadedAppliedCount += finalizedOperationResponse.applied_count;
12465
+ uploadedConflictCount += finalizedOperationResponse.conflict_count;
12466
+ uploadedUnsupportedCount += finalizedOperationResponse.unsupported_count;
12467
+ uploadedRejectedCount += finalizedOperationResponse.rejected_count;
12468
+ }
12469
+ return {
12470
+ uploadedBatchCount,
12471
+ uploadedChangeCount,
12472
+ uploadedAppliedCount,
12473
+ uploadedConflictCount,
12474
+ uploadedUnsupportedCount,
12475
+ uploadedRejectedCount,
12476
+ deferredFileCount
12477
+ };
12478
+ }
12479
+ function emptyLocalReconcileSummary() {
12480
+ return {
12481
+ uploadedBatchCount: 0,
12482
+ uploadedChangeCount: 0,
12483
+ uploadedAppliedCount: 0,
12484
+ uploadedConflictCount: 0,
12485
+ uploadedUnsupportedCount: 0,
12486
+ uploadedRejectedCount: 0,
12487
+ deferredFileCount: 0
12488
+ };
12489
+ }
12490
+ function filterBaselineForDirtyDrivePaths(baseline, dirtyDrivePaths) {
12491
+ if (dirtyDrivePaths.size === 0) {
12492
+ return /* @__PURE__ */ new Map();
12493
+ }
12494
+ const normalizedDirtyPaths = [...dirtyDrivePaths].map(normalizeDrivePath);
12495
+ return new Map([...baseline].filter(([baselinePath]) => normalizedDirtyPaths.some((dirtyPath) => drivePathIsInSubtree(baselinePath, dirtyPath) || drivePathIsInSubtree(dirtyPath, baselinePath))));
12496
+ }
12497
+ function filterConflictsForDirtyDrivePaths(conflicts, dirtyDrivePaths) {
12498
+ if (dirtyDrivePaths.size === 0) {
12499
+ return /* @__PURE__ */ new Map();
12500
+ }
12501
+ const normalizedDirtyPaths = [...dirtyDrivePaths].map(normalizeDrivePath);
12502
+ return new Map([...conflicts].filter(([, conflict]) => normalizedDirtyPaths.some((dirtyPath) => drivePathsIntersect(conflict.path, dirtyPath) || (conflict.conflictPath ? drivePathsIntersect(conflict.conflictPath, dirtyPath) : false))));
12503
+ }
12504
+ function drivePathsIntersect(leftPath, rightPath) {
12505
+ return drivePathIsInSubtree(leftPath, rightPath) || drivePathIsInSubtree(rightPath, leftPath);
12506
+ }
12507
+ async function applyCloudDriveConflictLifecycleActions(params) {
12508
+ if (params.actions.length === 0 || !params.api.applyConflictActions) {
12509
+ return [];
12510
+ }
12511
+ const response = await params.api.applyConflictActions({
12512
+ sync_client_id: params.syncClientId,
12513
+ drive_id: params.driveId,
12514
+ operation_id: buildConflictLifecycleOperationId({
12515
+ driveId: params.driveId,
12516
+ actions: params.actions,
12517
+ conflicts: params.conflicts
12518
+ }),
12519
+ actions: params.actions.map((action) => {
12520
+ const conflict = params.conflicts.get(action.path) ?? null;
12521
+ const payload = {
12522
+ type: action.type,
12523
+ path: action.path,
12524
+ reason: action.reason
12525
+ };
12526
+ if (conflict?.conflictId) {
12527
+ payload.conflict_id = conflict.conflictId;
12528
+ }
12529
+ if (action.type === "update_conflict_path") {
12530
+ payload.conflict_path = action.conflictPath;
12531
+ }
12532
+ return payload;
12533
+ })
12534
+ });
12535
+ return collectConfirmedConflictActions(params.actions, response.results);
12536
+ }
12537
+ function collectConfirmedConflictActions(actions, results) {
12538
+ const confirmedActions = [];
12539
+ for (const result of results) {
12540
+ if (!isConfirmedConflictActionResult(result)) {
12541
+ continue;
12542
+ }
12543
+ const action = actions[result.index];
12544
+ if (!action || result.type !== action.type) {
12545
+ continue;
12546
+ }
12547
+ if (typeof result.path === "string" && normalizeDrivePath(result.path) !== action.path) {
12548
+ continue;
12549
+ }
12550
+ if (action.type === "clear_conflict") {
12551
+ if (result.status === "resolved" || result.status === "ignored") {
12552
+ confirmedActions.push(action);
12553
+ }
12554
+ continue;
12555
+ }
12556
+ if (result.status !== "updated") {
12557
+ continue;
12558
+ }
12559
+ confirmedActions.push({
12560
+ ...action,
12561
+ conflictPath: typeof result.conflict_path === "string" ? normalizeDrivePath(result.conflict_path) : action.conflictPath
12562
+ });
12563
+ }
12564
+ return confirmedActions;
12565
+ }
12566
+ function isConfirmedConflictActionResult(result) {
12567
+ return Number.isInteger(result.index) && typeof result.type === "string" && typeof result.status === "string";
12568
+ }
12569
+ function buildConflictLifecycleOperationId(params) {
12570
+ const stablePayload = params.actions.map((action) => {
12571
+ const conflict = params.conflicts.get(action.path) ?? null;
12572
+ return {
12573
+ type: action.type,
12574
+ path: action.path,
12575
+ reason: action.reason,
12576
+ conflict_id: conflict?.conflictId ?? null,
12577
+ conflict_path: action.type === "update_conflict_path" ? action.conflictPath : null
12578
+ };
12579
+ });
12580
+ const digest = sha256Hex4(Buffer.from(JSON.stringify({
12581
+ driveId: params.driveId,
12582
+ actions: stablePayload
12583
+ })));
12584
+ return `conflict-lifecycle:${digest}`;
12585
+ }
12586
+ function collectRemoteConflictsForAck(params) {
12587
+ const byPath = /* @__PURE__ */ new Map();
12588
+ for (const conflict of params.newConflicts) {
12589
+ if (conflict.conflictType === "remote_apply") {
12590
+ byPath.set(conflict.path, conflict);
12591
+ }
12592
+ }
12593
+ for (const conflict of params.stateStore.listConflicts?.(params.driveId) ?? []) {
12594
+ if (conflict.conflictType !== "remote_apply") {
12595
+ continue;
12596
+ }
12597
+ if (conflict.remoteBatchId === params.batch.id || conflict.remoteSequence === params.batch.sequence) {
12598
+ byPath.set(conflict.path, conflict);
12599
+ }
12600
+ }
12601
+ return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
12602
+ }
12603
+ function toRemoteConflictAckPayload(conflict) {
12604
+ return {
12605
+ path: conflict.path,
12606
+ reason: conflict.reason,
12607
+ conflict_path: conflict.conflictPath,
12608
+ local_content_sha256: conflict.localContentSha256,
12609
+ local_size_bytes: conflict.localSizeBytes,
12610
+ local_version_id: isUuid2(conflict.localVersionId) ? conflict.localVersionId : null,
12611
+ remote_batch_id: isUuid2(conflict.remoteBatchId) ? conflict.remoteBatchId : null,
12612
+ remote_sequence: conflict.remoteSequence
12613
+ };
12614
+ }
12615
+ function buildRemoteConflictAckOperationId(params) {
12616
+ const stablePayload = params.conflicts.map((conflict) => ({
12617
+ path: conflict.path,
12618
+ reason: conflict.reason,
12619
+ conflict_path: conflict.conflictPath,
12620
+ local_content_sha256: conflict.localContentSha256,
12621
+ local_size_bytes: conflict.localSizeBytes,
12622
+ local_version_id: isUuid2(conflict.localVersionId) ? conflict.localVersionId : null,
12623
+ remote_batch_id: isUuid2(conflict.remoteBatchId) ? conflict.remoteBatchId : null,
12624
+ remote_sequence: conflict.remoteSequence
12625
+ }));
12626
+ const digest = sha256Hex4(Buffer.from(JSON.stringify({
12627
+ driveId: params.driveId,
12628
+ batchId: params.batch.id,
12629
+ batchSequence: params.batch.sequence,
12630
+ conflicts: stablePayload
12631
+ })));
12632
+ return `remote-conflict-ack:${digest}`;
12633
+ }
12634
+ function applyRemoteConflictAckResults(params) {
12635
+ if (!params.results || !params.stateStore.getConflict || !params.stateStore.upsertConflict) {
12636
+ return;
12637
+ }
12638
+ for (const result of params.results) {
12639
+ if (typeof result.path !== "string" || typeof result.conflict_id !== "string") {
12640
+ continue;
12641
+ }
12642
+ const drivePath = normalizeDrivePath(result.path);
12643
+ const existing = params.stateStore.getConflict(params.driveId, drivePath);
12644
+ if (!existing) {
12645
+ continue;
12646
+ }
12647
+ params.stateStore.upsertConflict({
12648
+ ...existing,
12649
+ conflictId: result.conflict_id,
12650
+ conflictPath: typeof result.conflict_path === "string" ? result.conflict_path : existing.conflictPath
12651
+ });
12652
+ }
12653
+ }
12654
+ function isUuid2(value) {
12655
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
12656
+ }
12657
+ function isStagedLocalOperationResponse(response) {
12658
+ return response.resource_operation_status === "applying" && response.batch === null;
12659
+ }
12660
+ function isFinalizedFullLocalOperationResponse(response, preparedMutationCount) {
12661
+ return response.resource_operation_status === "finalized" && response.results.length >= preparedMutationCount;
12662
+ }
12663
+ function chunkLocalDriveMutations(mutations) {
12664
+ const chunks = [];
12665
+ let currentChunk = [];
12666
+ let currentUploadBytes = 0;
12667
+ for (const mutation of mutations) {
12668
+ if (mutation.uploadBytes > MAX_LOCAL_BATCH_UPLOAD_BYTES) {
12669
+ throw new Error(getDriveLocalSyncFileTooLargeMessage(mutation.uploadBytes));
12670
+ }
12671
+ const nextUploadBytes = currentUploadBytes + mutation.uploadBytes;
12672
+ if (currentChunk.length > 0 && (currentChunk.length >= MAX_LOCAL_BATCH_CHANGES || nextUploadBytes > MAX_LOCAL_BATCH_UPLOAD_BYTES)) {
12673
+ chunks.push(currentChunk);
12674
+ currentChunk = [];
12675
+ currentUploadBytes = 0;
12676
+ }
12677
+ currentChunk.push(mutation);
12678
+ currentUploadBytes += mutation.uploadBytes;
12679
+ }
12680
+ if (currentChunk.length > 0) {
12681
+ chunks.push(currentChunk);
12682
+ }
12683
+ return chunks;
12684
+ }
12685
+ function buildLocalResourceOperationChunkFingerprint(params) {
12686
+ return sha256Hex4(Buffer.from(stableJsonStringify({
12687
+ drive_id: params.driveId,
12688
+ base_cursor_sequence: params.baseCursorSequence,
12689
+ chunk_index: params.chunkIndex,
12690
+ chunk_count: params.chunkCount,
12691
+ changes: params.mutations.map((mutation) => mutation.request)
12692
+ })));
12693
+ }
12694
+ function buildLocalResourceOperationFingerprint(params) {
12695
+ return sha256Hex4(Buffer.from(stableJsonStringify({
12696
+ drive_id: params.driveId,
12697
+ base_cursor_sequence: params.baseCursorSequence,
12698
+ chunk_fingerprints: params.chunkFingerprints
12699
+ })));
12700
+ }
12701
+ async function applyAcceptedLocalChangeResults(params) {
12702
+ for (const result of params.results) {
12703
+ const mutation = params.mutations[result.index];
12704
+ if (!mutation) {
12705
+ continue;
12706
+ }
12707
+ if (result.status === "applied") {
12708
+ applyAppliedLocalDriveMutationState({
12709
+ stateStore: params.stateStore,
12710
+ driveId: params.driveId,
12711
+ mutation,
12712
+ result
12713
+ });
12714
+ continue;
12715
+ }
12716
+ if (result.status === "conflicted") {
12717
+ const conflictPath = resolveLocalMutationResultPath(result, mutation);
12718
+ const localConflictPath = await preserveLocalUploadConflictCandidate({
12719
+ driveRoot: params.driveRoot,
12720
+ mutation,
12721
+ conflictPath
12722
+ });
12723
+ recordLocalUploadConflict({
12724
+ stateStore: params.stateStore,
12725
+ driveId: params.driveId,
12726
+ mutation,
12727
+ result,
12728
+ conflictPath,
12729
+ localConflictPath
12730
+ });
12731
+ continue;
12732
+ }
12733
+ if (result.status === "unsupported" && mutation.stateEntry) {
12734
+ applyUnsupportedLocalDriveMutationState({
12735
+ stateStore: params.stateStore,
12736
+ driveId: params.driveId,
12737
+ mutation
12738
+ });
12739
+ }
12740
+ }
12741
+ }
12742
+ async function preserveLocalUploadConflictCandidate(params) {
12743
+ if (params.mutation.request.type !== "write_file" || !params.mutation.localFilePath || params.mutation.stateEntry?.entryType !== "file") {
12744
+ return null;
12745
+ }
12746
+ try {
12747
+ return await writeLocalConflictCopy({
12748
+ driveRoot: params.driveRoot,
12749
+ source: {
12750
+ driveId: params.mutation.stateEntry.driveId,
12751
+ path: params.mutation.stateEntry.path,
12752
+ entryType: "file",
12753
+ contentSha256: params.mutation.stateEntry.contentSha256,
12754
+ sizeBytes: params.mutation.stateEntry.sizeBytes,
12755
+ mimeType: params.mutation.stateEntry.mimeType,
12756
+ absolutePath: params.mutation.localFilePath
12757
+ },
12758
+ sourceRootPath: params.conflictPath,
12759
+ conflictRootPath: params.conflictPath
12760
+ });
12761
+ } catch (error) {
12762
+ console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} failed to preserve local upload conflict copy`, {
12763
+ path: params.conflictPath,
12764
+ error: error instanceof Error ? error.message : String(error)
12765
+ });
12766
+ return null;
12767
+ }
12768
+ }
12769
+ function emptyDriveSyncRunSummary(driveCount = 0) {
12770
+ return {
12771
+ driveCount,
12772
+ failedDriveCount: 0,
12773
+ appliedBatchCount: 0,
12774
+ appliedPathCount: 0,
12775
+ remoteConflictCount: 0,
12776
+ uploadedBatchCount: 0,
12777
+ uploadedChangeCount: 0,
12778
+ uploadedAppliedCount: 0,
12779
+ uploadedConflictCount: 0,
12780
+ uploadedUnsupportedCount: 0,
12781
+ uploadedRejectedCount: 0,
12782
+ deferredFileCount: 0
12783
+ };
12784
+ }
12785
+ function toDriveSyncStatusSummary(summary) {
12786
+ return {
12787
+ applied_batch_count: summary.appliedBatchCount,
12788
+ applied_path_count: summary.appliedPathCount,
12789
+ remote_conflict_count: summary.remoteConflictCount,
12790
+ uploaded_batch_count: summary.uploadedBatchCount,
12791
+ uploaded_change_count: summary.uploadedChangeCount,
12792
+ uploaded_applied_count: summary.uploadedAppliedCount,
12793
+ uploaded_conflict_count: summary.uploadedConflictCount,
12794
+ uploaded_unsupported_count: summary.uploadedUnsupportedCount,
12795
+ uploaded_rejected_count: summary.uploadedRejectedCount,
12796
+ deferred_file_count: summary.deferredFileCount
12797
+ };
12798
+ }
12799
+ function truncateDriveSyncStatusError(error) {
12800
+ if (!error || error.length <= 2e3) {
12801
+ return error;
12802
+ }
12803
+ return `${error.slice(0, 1997)}...`;
12804
+ }
12805
+ function deriveDriveSyncStatus(schedulerStatus) {
12806
+ if (!schedulerStatus) {
12807
+ return "active";
12808
+ }
12809
+ return schedulerStatus.watchStatus === "active" ? "active" : "degraded";
12810
+ }
12811
+ function driveSummaryRequiresDegradedStatus(summary) {
12812
+ return summary.remoteConflictCount > 0 || summary.uploadedConflictCount > 0 || summary.uploadedUnsupportedCount > 0 || summary.uploadedRejectedCount > 0 || summary.deferredFileCount > 0 || summary.failedDriveCount > 0;
12813
+ }
12814
+ function summarizeDriveSyncConflicts(conflicts) {
12815
+ return {
12816
+ unresolvedConflictCount: conflicts.length,
12817
+ unresolvedConflictSamples: conflicts.slice(0, DRIVE_SYNC_STATUS_CONFLICT_SAMPLE_LIMIT).map((conflict) => ({
12818
+ path: conflict.path,
12819
+ conflict_type: conflict.conflictType,
12820
+ reason: conflict.reason,
12821
+ conflict_path: conflict.conflictPath,
12822
+ conflict_id: conflict.conflictId,
12823
+ remote_sequence: conflict.remoteSequence
12824
+ }))
12825
+ };
12826
+ }
12827
+ var ManagedDrivePullSyncService = class {
12828
+ params;
12829
+ constructor(params) {
12830
+ this.params = params;
12831
+ }
12832
+ close() {
12833
+ this.params.stateStore.close?.();
12834
+ }
12835
+ async syncOnce(options = {}) {
12836
+ const includeLocalWrites = options.includeLocalWrites !== false;
12837
+ const syncStatus = deriveDriveSyncStatus(options.schedulerStatus);
12838
+ const syncClient = await this.ensureSyncClient();
12839
+ const drivesResponse = await this.params.api.listDrives({ sync_client_id: syncClient.id });
12840
+ this.params.stateStore.setSyncClient(drivesResponse.sync_client);
12841
+ await this.parkStaleDrives(new Set(drivesResponse.drives.map((drive) => drive.resource.id)));
12842
+ let appliedBatchCount = 0;
12843
+ let appliedPathCount = 0;
12844
+ let remoteConflictCount = 0;
12845
+ let uploadedBatchCount = 0;
12846
+ let uploadedChangeCount = 0;
12847
+ let uploadedAppliedCount = 0;
12848
+ let uploadedConflictCount = 0;
12849
+ let uploadedUnsupportedCount = 0;
12850
+ let uploadedRejectedCount = 0;
12851
+ let deferredFileCount = 0;
12852
+ let failedDriveCount = 0;
12853
+ for (const drive of drivesResponse.drives) {
12854
+ const driveRoot = resolveDriveLocalRoot(this.params.config.driveSync.rootDir, drive.resource.id);
12855
+ const dirtyDrivePaths = resolveDirtyDrivePathsForDrive(driveRoot, options.dirtyLocalPaths);
12856
+ const driveSummary = emptyDriveSyncRunSummary(1);
12857
+ let lastBatchSequence = drive.cursor?.last_batch_sequence ?? 0;
12858
+ try {
12859
+ await mkdir3(driveRoot, { recursive: true });
12860
+ this.params.stateStore.upsertDrive({
12861
+ id: drive.resource.id,
12862
+ title: drive.resource.title,
12863
+ rootPath: driveRoot
12864
+ });
12865
+ const result = await this.syncDrive({
12866
+ syncClientId: syncClient.id,
12867
+ driveId: drive.resource.id,
12868
+ driveRoot
12869
+ });
12870
+ lastBatchSequence = result.lastBatchSequence;
12871
+ driveSummary.appliedBatchCount += result.appliedBatchCount;
12872
+ driveSummary.appliedPathCount += result.appliedPathCount;
12873
+ driveSummary.remoteConflictCount += result.remoteConflictCount;
12874
+ if (includeLocalWrites && (!dirtyDrivePaths || dirtyDrivePaths.size > 0)) {
12875
+ const localResult = await reconcileLocalDriveToCloud({
12876
+ syncClientId: syncClient.id,
12877
+ driveId: drive.resource.id,
12878
+ driveRoot,
12879
+ baseCursorSequence: result.lastBatchSequence,
12880
+ api: this.params.api,
12881
+ stateStore: this.params.stateStore,
12882
+ dirtyDrivePaths
12883
+ });
12884
+ driveSummary.uploadedBatchCount += localResult.uploadedBatchCount;
12885
+ driveSummary.uploadedChangeCount += localResult.uploadedChangeCount;
12886
+ driveSummary.uploadedAppliedCount += localResult.uploadedAppliedCount;
12887
+ driveSummary.uploadedConflictCount += localResult.uploadedConflictCount;
12888
+ driveSummary.uploadedUnsupportedCount += localResult.uploadedUnsupportedCount;
12889
+ driveSummary.uploadedRejectedCount += localResult.uploadedRejectedCount;
12890
+ driveSummary.deferredFileCount += localResult.deferredFileCount;
12891
+ if (localResult.uploadedBatchCount > 0) {
12892
+ const followUpResult = await this.syncDrive({
12893
+ syncClientId: syncClient.id,
12894
+ driveId: drive.resource.id,
12895
+ driveRoot
12896
+ });
12897
+ lastBatchSequence = followUpResult.lastBatchSequence;
12898
+ driveSummary.appliedBatchCount += followUpResult.appliedBatchCount;
12899
+ driveSummary.appliedPathCount += followUpResult.appliedPathCount;
12900
+ driveSummary.remoteConflictCount += followUpResult.remoteConflictCount;
12901
+ }
12902
+ }
12903
+ appliedBatchCount += driveSummary.appliedBatchCount;
12904
+ appliedPathCount += driveSummary.appliedPathCount;
12905
+ remoteConflictCount += driveSummary.remoteConflictCount;
12906
+ uploadedBatchCount += driveSummary.uploadedBatchCount;
12907
+ uploadedChangeCount += driveSummary.uploadedChangeCount;
12908
+ uploadedAppliedCount += driveSummary.uploadedAppliedCount;
12909
+ uploadedConflictCount += driveSummary.uploadedConflictCount;
12910
+ uploadedUnsupportedCount += driveSummary.uploadedUnsupportedCount;
12911
+ uploadedRejectedCount += driveSummary.uploadedRejectedCount;
12912
+ deferredFileCount += driveSummary.deferredFileCount;
12913
+ await this.reportDriveSyncStatus({
12914
+ syncClientId: syncClient.id,
12915
+ driveId: drive.resource.id,
12916
+ driveRoot,
12917
+ status: syncStatus,
12918
+ lastBatchSequence,
12919
+ summary: driveSummary,
12920
+ lastReason: includeLocalWrites ? "sync_once" : "sync_once_remote_only",
12921
+ lastError: null,
12922
+ schedulerStatus: options.schedulerStatus
12923
+ });
12924
+ } catch (error) {
12925
+ const errorMessage = error instanceof Error ? error.message : String(error);
12926
+ failedDriveCount += 1;
12927
+ await this.reportDriveSyncStatus({
12928
+ syncClientId: syncClient.id,
12929
+ driveId: drive.resource.id,
12930
+ driveRoot,
12931
+ status: "error",
12932
+ lastBatchSequence,
12933
+ summary: driveSummary,
12934
+ lastReason: "sync_error",
12935
+ lastError: errorMessage,
12936
+ schedulerStatus: options.schedulerStatus
12937
+ });
12938
+ console.error(`${MANAGED_GATEWAY_LOG_PREFIX} failed to sync drive`, {
12939
+ driveId: drive.resource.id,
12940
+ error: errorMessage
12941
+ });
12942
+ }
12943
+ }
12944
+ return {
12945
+ driveCount: drivesResponse.drives.length,
12946
+ failedDriveCount,
12947
+ appliedBatchCount,
12948
+ appliedPathCount,
12949
+ remoteConflictCount,
12950
+ uploadedBatchCount,
12951
+ uploadedChangeCount,
12952
+ uploadedAppliedCount,
12953
+ uploadedConflictCount,
12954
+ uploadedUnsupportedCount,
12955
+ uploadedRejectedCount,
12956
+ deferredFileCount
12957
+ };
12958
+ }
12959
+ async reportDriveSyncStatus(params) {
12960
+ try {
12961
+ const unresolvedConflictSummary = summarizeDriveSyncConflicts(this.params.stateStore.listConflicts?.(params.driveId) ?? []);
12962
+ const status = params.status === "active" && (unresolvedConflictSummary.unresolvedConflictCount > 0 || driveSummaryRequiresDegradedStatus(params.summary)) ? "degraded" : params.status;
12963
+ await this.params.api.reportStatus({
12964
+ sync_client_id: params.syncClientId,
12965
+ drive_id: params.driveId,
12966
+ status,
12967
+ last_reason: params.lastReason,
12968
+ last_error: truncateDriveSyncStatusError(params.lastError),
12969
+ last_batch_sequence: params.lastBatchSequence,
12970
+ summary: toDriveSyncStatusSummary(params.summary),
12971
+ metadata: {
12972
+ root_path: params.driveRoot,
12973
+ runtime_id: this.params.config.runtimeId,
12974
+ host_id: this.params.config.hostId,
12975
+ agent_id: this.params.config.agentId,
12976
+ watcher_status: params.schedulerStatus?.watchStatus ?? null,
12977
+ watcher_degraded: params.schedulerStatus ? params.schedulerStatus.watchStatus !== "active" : null,
12978
+ watcher_error: truncateDriveSyncStatusError(params.schedulerStatus?.watchError ?? null),
12979
+ unresolved_conflict_count: unresolvedConflictSummary.unresolvedConflictCount,
12980
+ unresolved_conflicts: unresolvedConflictSummary.unresolvedConflictSamples
12981
+ }
12982
+ });
12983
+ } catch (error) {
12984
+ console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} failed to report drive sync status`, {
12985
+ driveId: params.driveId,
12986
+ error: error instanceof Error ? error.message : String(error)
12987
+ });
12988
+ }
12989
+ }
12990
+ async parkStaleDrives(accessibleDriveIds) {
12991
+ const knownDrives = this.params.stateStore.listDrives?.() ?? [];
12992
+ for (const knownDrive of knownDrives) {
12993
+ if (accessibleDriveIds.has(knownDrive.id)) {
12994
+ continue;
12995
+ }
12996
+ const parkedPath = await parkStaleDriveLocalFolder(this.params.config.driveSync.rootDir, knownDrive.id);
12997
+ this.params.stateStore.removeDrive?.(knownDrive.id);
12998
+ console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} parked stale drive folder`, {
12999
+ driveId: knownDrive.id,
13000
+ parkedPath
13001
+ });
13002
+ }
13003
+ }
13004
+ async ensureSyncClient() {
13005
+ const clientKey = this.params.config.driveSync.clientKey ?? this.params.stateStore.getOrCreateClientKey?.(`managed:${this.params.config.hostId}:${this.params.config.agentId}`) ?? `managed:${this.params.config.hostId}:${this.params.config.agentId}`;
13006
+ const response = await this.params.api.registerClient({
13007
+ agent_id: this.params.config.agentId,
13008
+ host_id: this.params.config.hostId,
13009
+ client_key: clientKey,
13010
+ display_name: `${hostname2()} managed drive sync`,
13011
+ metadata: {
13012
+ mode: "managed_gateway_pull",
13013
+ runtime_id: this.params.config.runtimeId,
13014
+ process_id: process17.pid
13015
+ }
13016
+ });
13017
+ this.params.stateStore.setSyncClient(response.sync_client);
13018
+ return response.sync_client;
13019
+ }
13020
+ async syncDrive(params) {
13021
+ let appliedBatchCount = 0;
13022
+ let appliedPathCount = 0;
13023
+ let remoteConflictCount = 0;
13024
+ let lastAckedSequence = 0;
13025
+ while (appliedBatchCount < this.params.config.driveSync.maxBatchesPerTick) {
13026
+ const remainingBatchCapacity = this.params.config.driveSync.maxBatchesPerTick - appliedBatchCount;
13027
+ const response = await this.params.api.getChanges({
13028
+ sync_client_id: params.syncClientId,
13029
+ drive_id: params.driveId,
13030
+ limit: Math.min(this.params.config.driveSync.changeLimit, remainingBatchCapacity)
13031
+ });
13032
+ lastAckedSequence = response.cursor.last_batch_sequence;
13033
+ if (response.changes.length === 0) {
13034
+ this.params.stateStore.setCursor(params.driveId, lastAckedSequence);
13035
+ break;
13036
+ }
13037
+ for (const batch of response.changes) {
13038
+ try {
13039
+ const result = await applyDriveChangeBatchToLocalFilesystem({
13040
+ syncClientId: params.syncClientId,
13041
+ driveRoot: params.driveRoot,
13042
+ batch,
13043
+ api: this.params.api,
13044
+ stateStore: this.params.stateStore
13045
+ });
13046
+ const remoteConflicts = collectRemoteConflictsForAck({
13047
+ driveId: params.driveId,
13048
+ batch,
13049
+ newConflicts: result.remoteConflicts,
13050
+ stateStore: this.params.stateStore
13051
+ });
13052
+ const ackResponse = await this.params.api.ackChanges({
13053
+ sync_client_id: params.syncClientId,
13054
+ drive_id: params.driveId,
13055
+ operation_id: remoteConflicts.length > 0 ? buildRemoteConflictAckOperationId({
13056
+ driveId: params.driveId,
13057
+ batch,
13058
+ conflicts: remoteConflicts
13059
+ }) : void 0,
13060
+ last_batch_sequence: batch.sequence,
13061
+ last_error: null,
13062
+ remote_conflicts: remoteConflicts.length > 0 ? remoteConflicts.map(toRemoteConflictAckPayload) : void 0
13063
+ });
13064
+ applyRemoteConflictAckResults({
13065
+ driveId: params.driveId,
13066
+ stateStore: this.params.stateStore,
13067
+ results: ackResponse.remote_conflicts
13068
+ });
13069
+ lastAckedSequence = ackResponse.cursor.last_batch_sequence;
13070
+ this.params.stateStore.setCursor(params.driveId, lastAckedSequence);
13071
+ appliedBatchCount += 1;
13072
+ appliedPathCount += result.appliedPathCount;
13073
+ remoteConflictCount += result.remoteConflictCount;
13074
+ } catch (error) {
13075
+ await this.params.api.ackChanges({
13076
+ sync_client_id: params.syncClientId,
13077
+ drive_id: params.driveId,
13078
+ last_batch_sequence: lastAckedSequence,
13079
+ last_error: error instanceof Error ? error.message : String(error)
13080
+ });
13081
+ throw error;
13082
+ }
13083
+ }
13084
+ if (!response.has_more) {
13085
+ break;
13086
+ }
13087
+ }
13088
+ return {
13089
+ appliedBatchCount,
13090
+ appliedPathCount,
13091
+ remoteConflictCount,
13092
+ lastBatchSequence: lastAckedSequence
13093
+ };
13094
+ }
13095
+ };
13096
+ async function invokeDriveSyncFunction(supabase, functionName, body, timeoutMs) {
13097
+ const { data, error } = await withTimeout(supabase.functions.invoke(functionName, { body }), timeoutMs, `${functionName} invocation`);
13098
+ if (error) {
13099
+ throw new Error(`${functionName} failed: ${await formatSupabaseFunctionError(error)}`);
13100
+ }
13101
+ if (!data?.success) {
13102
+ throw new Error(`${functionName} returned an invalid response`);
13103
+ }
13104
+ return data;
13105
+ }
13106
+ async function withTimeout(promise, timeoutMs, label) {
13107
+ let timer = null;
13108
+ const timeout = new Promise((_, reject) => {
13109
+ timer = setTimeout(() => {
13110
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
13111
+ }, timeoutMs);
13112
+ timer.unref?.();
13113
+ });
13114
+ try {
13115
+ return await Promise.race([promise, timeout]);
13116
+ } finally {
13117
+ if (timer) {
13118
+ clearTimeout(timer);
13119
+ }
13120
+ }
13121
+ }
13122
+ async function formatSupabaseFunctionError(error) {
13123
+ const message = error instanceof Error ? error.message : String(error);
13124
+ const context = isRecord2(error) ? error.context : null;
13125
+ if (!(context instanceof Response)) {
13126
+ return message;
13127
+ }
13128
+ const responseBody = await context.text().catch(() => null);
13129
+ const bodySuffix = responseBody && responseBody.trim().length > 0 ? ` body=${responseBody.trim().slice(0, 1e3)}` : "";
13130
+ return `${message} (status=${context.status}${bodySuffix})`;
13131
+ }
13132
+ function sha256Hex4(bytes) {
13133
+ return createHash3("sha256").update(bytes).digest("hex");
13134
+ }
13135
+ function stableJsonStringify(input) {
13136
+ if (input === void 0) {
13137
+ return "null";
13138
+ }
13139
+ if (input === null || typeof input !== "object") {
13140
+ return JSON.stringify(input);
13141
+ }
13142
+ if (Array.isArray(input)) {
13143
+ return `[${input.map((value) => stableJsonStringify(value)).join(",")}]`;
13144
+ }
13145
+ const record = input;
13146
+ return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stableJsonStringify(record[key])}`).join(",")}}`;
13147
+ }
13148
+ function logDriveSyncRunSummary(summary) {
13149
+ if (summary.driveCount === 0 || summary.appliedBatchCount === 0 && summary.uploadedBatchCount === 0 && summary.remoteConflictCount === 0 && summary.deferredFileCount === 0 && summary.failedDriveCount === 0) {
13150
+ return;
13151
+ }
13152
+ console.log(`${MANAGED_GATEWAY_LOG_PREFIX} applied drive sync changes`, {
13153
+ driveCount: summary.driveCount,
13154
+ failedDriveCount: summary.failedDriveCount,
13155
+ appliedBatchCount: summary.appliedBatchCount,
13156
+ appliedPathCount: summary.appliedPathCount,
13157
+ remoteConflictCount: summary.remoteConflictCount,
13158
+ uploadedBatchCount: summary.uploadedBatchCount,
13159
+ uploadedChangeCount: summary.uploadedChangeCount,
13160
+ uploadedAppliedCount: summary.uploadedAppliedCount,
13161
+ uploadedConflictCount: summary.uploadedConflictCount,
13162
+ uploadedUnsupportedCount: summary.uploadedUnsupportedCount,
13163
+ uploadedRejectedCount: summary.uploadedRejectedCount,
13164
+ deferredFileCount: summary.deferredFileCount
13165
+ });
13166
+ }
13167
+
13168
+ // ../shared/dist/control-signals.js
13169
+ var CONTROL_ACTIONS = /* @__PURE__ */ new Set(["continue", "pause", "stop"]);
13170
+ function parseControlSignalCandidate(value) {
13171
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
13172
+ return null;
13173
+ }
13174
+ const rawNextAction = value.nextAction;
13175
+ if (typeof rawNextAction !== "string" || !CONTROL_ACTIONS.has(rawNextAction)) {
13176
+ return null;
13177
+ }
13178
+ const pauseDurationValue = value.pauseDuration ?? value.seconds;
13179
+ const pauseDuration = typeof pauseDurationValue === "number" && Number.isFinite(pauseDurationValue) && pauseDurationValue > 0 ? pauseDurationValue : void 0;
13180
+ return {
13181
+ nextAction: rawNextAction,
13182
+ pauseDuration
13183
+ };
13184
+ }
13185
+
13186
+ // dist/managed-runtime/shell-execution.js
13187
+ import { spawn as spawn4 } from "node:child_process";
13188
+ import { randomUUID as randomUUID7 } from "node:crypto";
13189
+ import { readFile, unlink } from "node:fs/promises";
13190
+ import { tmpdir } from "node:os";
13191
+ import { join } from "node:path";
13192
+ var DEFAULT_SIGTERM_GRACE_MS = 5e3;
13193
+ var DEFAULT_OUTPUT_CAPTURE_BYTES2 = 5e6;
13194
+ var CONTROL_SIGNAL_PATH_ENV = "PANORAMA_CONTROL_SIGNAL_PATH";
13195
+ async function executeCommand(command, timeoutMs, options = {}) {
13196
+ const startedAt = Date.now();
13197
+ const outputCaptureBytes = options.outputCaptureBytes ?? DEFAULT_OUTPUT_CAPTURE_BYTES2;
13198
+ return await new Promise((resolve) => {
13199
+ const child = spawn4("bash", ["-lc", command], {
13200
+ stdio: ["ignore", "pipe", "pipe"],
13201
+ detached: true,
13202
+ env: options.env
13203
+ });
13204
+ const stdoutOutput = new BoundedOutputCapture(outputCaptureBytes);
13205
+ const stderrOutput = new BoundedOutputCapture(outputCaptureBytes);
13206
+ let resolved = false;
13207
+ let timedOut = false;
13208
+ let spawnError = null;
13209
+ let interrupted = false;
13210
+ let forceKillHandle = null;
13211
+ const finish = (result) => {
13212
+ if (resolved)
13213
+ return;
13214
+ resolved = true;
13215
+ clearTimeout(timeoutHandle);
13216
+ if (forceKillHandle) {
13217
+ clearTimeout(forceKillHandle);
13218
+ forceKillHandle = null;
13219
+ }
13220
+ options.registerCancellation?.(null);
13221
+ resolve(result);
13222
+ };
13223
+ const killProcessGroup = (signal) => {
13224
+ if (!child.pid)
13225
+ return;
13226
+ try {
13227
+ process.kill(-child.pid, signal);
13228
+ } catch {
13229
+ try {
13230
+ child.kill(signal);
13231
+ } catch {
13232
+ }
13233
+ }
13234
+ };
13235
+ const scheduleForceKill = () => {
13236
+ if (forceKillHandle)
13237
+ return;
13238
+ forceKillHandle = setTimeout(() => {
13239
+ forceKillHandle = null;
13240
+ killProcessGroup("SIGKILL");
13241
+ }, DEFAULT_SIGTERM_GRACE_MS);
13242
+ forceKillHandle.unref();
13243
+ };
13244
+ const timeoutHandle = setTimeout(() => {
13245
+ if (resolved)
13246
+ return;
13247
+ timedOut = true;
13248
+ killProcessGroup("SIGTERM");
13249
+ scheduleForceKill();
13250
+ }, timeoutMs);
13251
+ options.registerCancellation?.(() => {
13252
+ if (resolved || interrupted)
13253
+ return;
13254
+ interrupted = true;
13255
+ killProcessGroup("SIGTERM");
13256
+ scheduleForceKill();
13257
+ });
13258
+ child.stdout?.on("data", (chunk) => {
13259
+ stdoutOutput.append(chunk);
13260
+ });
13261
+ child.stderr?.on("data", (chunk) => {
13262
+ stderrOutput.append(chunk);
13263
+ });
13264
+ child.on("error", (error) => {
13265
+ spawnError = error.message;
13266
+ });
13267
+ child.on("close", (code) => {
13268
+ const stdout = stdoutOutput.readText();
13269
+ const stderr = stderrOutput.readText();
13270
+ const durationMs = Date.now() - startedAt;
13271
+ const commonResult = {
13272
+ stdout,
13273
+ stderr,
13274
+ stdoutObservedBytes: stdoutOutput.observedBytes,
13275
+ stderrObservedBytes: stderrOutput.observedBytes,
13276
+ stdoutCapturedBytes: stdoutOutput.capturedBytes,
13277
+ stderrCapturedBytes: stderrOutput.capturedBytes,
13278
+ stdoutTruncated: stdoutOutput.truncated,
13279
+ stderrTruncated: stderrOutput.truncated,
13280
+ outputCaptureBytes,
13281
+ exitCode: code,
13282
+ durationMs
13283
+ };
13284
+ if (timedOut) {
13285
+ finish({
13286
+ ...commonResult,
13287
+ status: "timed_out",
13288
+ error: `Command timed out after ${timeoutMs}ms`
13289
+ });
13290
+ return;
13291
+ }
13292
+ if (interrupted) {
13293
+ finish({
13294
+ ...commonResult,
13295
+ status: "cancelled",
13296
+ error: "Command was interrupted because the managed gateway host was stopping."
13297
+ });
13298
+ return;
13299
+ }
13300
+ if (spawnError) {
13301
+ finish({
13302
+ ...commonResult,
13303
+ status: "failed",
13304
+ error: spawnError
13305
+ });
13306
+ return;
13307
+ }
13308
+ finish({
13309
+ ...commonResult,
13310
+ status: code === 0 ? "succeeded" : "failed",
13311
+ error: code === 0 ? null : `Command exited with code ${code ?? "unknown"}`
13312
+ });
13313
+ });
13314
+ });
13315
+ }
13316
+ function buildManagedShellExecutionEnv(params) {
13317
+ const safeEnv = {};
13318
+ const passthroughKeys = [
13319
+ "HOME",
13320
+ "HOSTNAME",
13321
+ "LANG",
13322
+ "LC_ALL",
13323
+ "LOGNAME",
13324
+ "PATH",
13325
+ "PWD",
13326
+ "SHELL",
13327
+ "SHLVL",
13328
+ "TERM",
13329
+ "TMPDIR",
13330
+ "USER"
13331
+ ];
13332
+ for (const key of passthroughKeys) {
13333
+ const value = params.baseEnv[key];
13334
+ if (typeof value === "string" && value.length > 0) {
13335
+ safeEnv[key] = value;
13336
+ }
13337
+ }
13338
+ safeEnv.PANORAMA_AGENT_ID = params.agentId;
13339
+ safeEnv.PANORAMA_TOOL_EXECUTION_URL = params.toolExecutionUrl;
13340
+ safeEnv.PANORAMA_TOOL_EXECUTION_TOKEN = params.toolExecutionToken;
13341
+ safeEnv[CONTROL_SIGNAL_PATH_ENV] = params.controlSignalPath;
13342
+ if (params.cycleId) {
13343
+ safeEnv.PANORAMA_AGENT_CYCLE_ID = params.cycleId;
13344
+ }
13345
+ return safeEnv;
13346
+ }
13347
+ function buildControlSignalPath(executionId) {
13348
+ return join(tmpdir(), `panorama-control-${executionId}-${randomUUID7()}.json`);
13349
+ }
13350
+ async function readCommandControlSignal(path21) {
13351
+ try {
13352
+ const raw = await readFile(path21, "utf8");
13353
+ return parseControlSignalCandidate(JSON.parse(raw));
13354
+ } catch {
13355
+ return null;
13356
+ } finally {
13357
+ try {
13358
+ await unlink(path21);
13359
+ } catch {
13360
+ }
13361
+ }
13362
+ }
13363
+
13364
+ // dist/managed-runtime/dependencies.js
13365
+ var defaultManagedGatewayRuntimeDependencies = {
13366
+ heartbeatLinuxHost,
13367
+ dispatchLinuxExecution,
13368
+ startLinuxExecution,
10557
13369
  completeLinuxExecution,
10558
13370
  reconcileExecutionLifecycle,
10559
13371
  executeCommand,
10560
13372
  createRealtimeClient: createManagedRealtimeClient,
10561
13373
  startRealtimeWakeLoop: startManagedRealtimeWakeLoop,
13374
+ createDriveSyncService: createManagedDriveSyncService,
10562
13375
  registerSignalHandlers(onSignal) {
10563
13376
  process.once("SIGINT", onSignal);
10564
13377
  process.once("SIGTERM", onSignal);
@@ -10576,7 +13389,7 @@ function resolveManagedGatewayRuntimeDependencies(dependencies = {}) {
10576
13389
  }
10577
13390
 
10578
13391
  // dist/managed-runtime/execution-dispatcher.js
10579
- import process17 from "node:process";
13392
+ import process18 from "node:process";
10580
13393
 
10581
13394
  // ../shared/dist/cycle-step-artifacts.js
10582
13395
  var RESERVED_TOOL_RESULT_METADATA_KEYS = /* @__PURE__ */ new Set([
@@ -10967,7 +13780,7 @@ async function executeManagedLinuxExecution(params) {
10967
13780
  try {
10968
13781
  result = await dependencies.executeCommand(command, config.execTimeoutMs, {
10969
13782
  env: buildManagedShellExecutionEnv({
10970
- baseEnv: process17.env,
13783
+ baseEnv: process18.env,
10971
13784
  agentId: config.agentId,
10972
13785
  cycleId: startedExecution.cycle_id,
10973
13786
  toolExecutionUrl: config.toolExecutionUrl,
@@ -11055,6 +13868,295 @@ async function executeManagedLinuxExecution(params) {
11055
13868
  return { reconciliationRequired: false };
11056
13869
  }
11057
13870
 
13871
+ // dist/managed-runtime/drive-sync-scheduler.js
13872
+ import { lstatSync, readdirSync, watch } from "node:fs";
13873
+ import path19 from "node:path";
13874
+ function startManagedDriveSyncScheduler(params) {
13875
+ let stopped = false;
13876
+ let running = false;
13877
+ let pendingReason = null;
13878
+ let pendingRequiresLocalWrites = false;
13879
+ let pendingRequiresFullLocalScan = false;
13880
+ let pendingDirtyLocalPaths = /* @__PURE__ */ new Set();
13881
+ let pendingWaiters = [];
13882
+ let debounceTimer = null;
13883
+ let pollTimer = null;
13884
+ let currentRun = null;
13885
+ let watchHandle = null;
13886
+ let watchStatus = "unavailable";
13887
+ let lastReason = null;
13888
+ let lastStartedAt = null;
13889
+ let lastFinishedAt = null;
13890
+ let lastError = null;
13891
+ let watchError = null;
13892
+ let lastSummary = null;
13893
+ let lastSuccessfulLocalWriteSyncAt = Date.now();
13894
+ let hasCompletedLocalWriteSync = false;
13895
+ const localAuditIntervalMs = params.localAuditIntervalMs ?? 3e5;
13896
+ const completeWaiters = (waiters, result) => {
13897
+ for (const waiter of waiters) {
13898
+ waiter.resolve(result);
13899
+ }
13900
+ };
13901
+ const clearDebounceTimer = () => {
13902
+ if (!debounceTimer) {
13903
+ return;
13904
+ }
13905
+ clearTimeout(debounceTimer);
13906
+ debounceTimer = null;
13907
+ };
13908
+ const runPendingSync = async () => {
13909
+ if (stopped || running || !pendingReason) {
13910
+ return;
13911
+ }
13912
+ clearDebounceTimer();
13913
+ const reason = pendingReason;
13914
+ const includeLocalWrites = pendingRequiresLocalWrites;
13915
+ const dirtyLocalPaths = pendingRequiresFullLocalScan ? void 0 : [...pendingDirtyLocalPaths];
13916
+ const waiters = pendingWaiters;
13917
+ pendingReason = null;
13918
+ pendingRequiresLocalWrites = false;
13919
+ pendingRequiresFullLocalScan = false;
13920
+ pendingDirtyLocalPaths = /* @__PURE__ */ new Set();
13921
+ pendingWaiters = [];
13922
+ running = true;
13923
+ lastReason = reason;
13924
+ lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
13925
+ lastError = null;
13926
+ const run2 = (async () => {
13927
+ let result = {
13928
+ status: "failed",
13929
+ error: "Drive sync did not produce a result"
13930
+ };
13931
+ try {
13932
+ const syncOptions = {
13933
+ includeLocalWrites,
13934
+ dirtyLocalPaths,
13935
+ schedulerStatus: {
13936
+ watchStatus,
13937
+ watchError
13938
+ }
13939
+ };
13940
+ const summary = await params.service.syncOnce(syncOptions);
13941
+ lastSummary = summary;
13942
+ logDriveSyncRunSummary(summary);
13943
+ result = summary.failedDriveCount > 0 ? {
13944
+ status: "degraded",
13945
+ summary,
13946
+ error: `${summary.failedDriveCount} drive sync${summary.failedDriveCount === 1 ? "" : "s"} failed`
13947
+ } : { status: "success", summary };
13948
+ } catch (error) {
13949
+ const normalizedError = asError(error);
13950
+ lastError = normalizedError.message;
13951
+ result = { status: "failed", error: normalizedError.message };
13952
+ console.error(`${MANAGED_GATEWAY_LOG_PREFIX} failed to sync drives`, {
13953
+ reason,
13954
+ error: normalizedError
13955
+ });
13956
+ } finally {
13957
+ if (includeLocalWrites && (result.status === "success" || result.status === "degraded" && result.summary.failedDriveCount === 0)) {
13958
+ lastSuccessfulLocalWriteSyncAt = Date.now();
13959
+ hasCompletedLocalWriteSync = true;
13960
+ }
13961
+ lastFinishedAt = (/* @__PURE__ */ new Date()).toISOString();
13962
+ running = false;
13963
+ completeWaiters(waiters, result);
13964
+ if (pendingReason && !stopped) {
13965
+ schedulePendingSync(false);
13966
+ }
13967
+ }
13968
+ })();
13969
+ currentRun = run2;
13970
+ await run2;
13971
+ if (currentRun === run2) {
13972
+ currentRun = null;
13973
+ }
13974
+ };
13975
+ const schedulePendingSync = (immediate) => {
13976
+ if (stopped || running || !pendingReason) {
13977
+ return;
13978
+ }
13979
+ if (immediate) {
13980
+ clearDebounceTimer();
13981
+ void runPendingSync();
13982
+ return;
13983
+ }
13984
+ if (debounceTimer) {
13985
+ return;
13986
+ }
13987
+ debounceTimer = setTimeout(() => {
13988
+ debounceTimer = null;
13989
+ void runPendingSync();
13990
+ }, params.debounceMs);
13991
+ debounceTimer.unref?.();
13992
+ };
13993
+ const requestSync = (reason, options = {}) => {
13994
+ if (stopped) {
13995
+ return Promise.resolve({ status: "skipped", reason: "scheduler_stopped" });
13996
+ }
13997
+ pendingReason = reason;
13998
+ const requiresLocalWrites = requestRequiresLocalWrites({
13999
+ reason,
14000
+ watchStatus,
14001
+ lastSuccessfulLocalWriteSyncAt,
14002
+ hasCompletedLocalWriteSync,
14003
+ localAuditIntervalMs
14004
+ });
14005
+ pendingRequiresLocalWrites = pendingRequiresLocalWrites || requiresLocalWrites;
14006
+ if (options.dirtyLocalPaths && !pendingRequiresFullLocalScan) {
14007
+ for (const dirtyPath of options.dirtyLocalPaths) {
14008
+ pendingDirtyLocalPaths.add(dirtyPath);
14009
+ }
14010
+ } else if (requiresLocalWrites) {
14011
+ pendingRequiresFullLocalScan = true;
14012
+ pendingDirtyLocalPaths.clear();
14013
+ }
14014
+ const promise = new Promise((resolve) => {
14015
+ pendingWaiters.push({ resolve });
14016
+ });
14017
+ schedulePendingSync(options.immediate === true);
14018
+ return promise;
14019
+ };
14020
+ const startWatching = () => {
14021
+ try {
14022
+ watchHandle = (params.dependencies?.watchRoot ?? watchDriveRoot)(params.rootDir, (reason, dirtyPath) => {
14023
+ void requestSync(reason, {
14024
+ dirtyLocalPaths: dirtyPath ? [dirtyPath] : void 0
14025
+ });
14026
+ }, (error) => {
14027
+ const normalizedError = asError(error);
14028
+ watchStatus = "error";
14029
+ watchError = normalizedError.message;
14030
+ console.error(`${MANAGED_GATEWAY_LOG_PREFIX} drive sync filesystem watcher failed`, {
14031
+ rootDir: params.rootDir,
14032
+ error: normalizedError
14033
+ });
14034
+ });
14035
+ watchStatus = watchHandle ? "active" : "unavailable";
14036
+ watchError = watchHandle ? null : "filesystem watcher unavailable";
14037
+ } catch (error) {
14038
+ const normalizedError = asError(error);
14039
+ watchStatus = "unavailable";
14040
+ watchError = normalizedError.message;
14041
+ console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} drive sync filesystem watcher unavailable`, {
14042
+ rootDir: params.rootDir,
14043
+ error: normalizedError
14044
+ });
14045
+ }
14046
+ };
14047
+ startWatching();
14048
+ pollTimer = setInterval(() => {
14049
+ void requestSync("drive_sync_poll", { immediate: true });
14050
+ }, params.pollIntervalMs);
14051
+ pollTimer.unref?.();
14052
+ return {
14053
+ requestSync,
14054
+ getStatus() {
14055
+ return {
14056
+ running,
14057
+ pending: pendingReason !== null,
14058
+ watchStatus,
14059
+ lastReason,
14060
+ lastStartedAt,
14061
+ lastFinishedAt,
14062
+ lastError,
14063
+ watchError,
14064
+ lastSummary
14065
+ };
14066
+ },
14067
+ async stop() {
14068
+ stopped = true;
14069
+ clearDebounceTimer();
14070
+ if (pollTimer) {
14071
+ clearInterval(pollTimer);
14072
+ pollTimer = null;
14073
+ }
14074
+ watchHandle?.close();
14075
+ watchHandle = null;
14076
+ completeWaiters(pendingWaiters, { status: "skipped", reason: "scheduler_stopped" });
14077
+ pendingWaiters = [];
14078
+ pendingReason = null;
14079
+ pendingRequiresLocalWrites = false;
14080
+ pendingRequiresFullLocalScan = false;
14081
+ pendingDirtyLocalPaths.clear();
14082
+ await currentRun;
14083
+ }
14084
+ };
14085
+ }
14086
+ function requestRequiresLocalWrites(params) {
14087
+ if (params.reason !== "drive_sync_poll") {
14088
+ return true;
14089
+ }
14090
+ if (!params.hasCompletedLocalWriteSync && params.watchStatus !== "active") {
14091
+ return true;
14092
+ }
14093
+ return Date.now() - params.lastSuccessfulLocalWriteSyncAt >= params.localAuditIntervalMs;
14094
+ }
14095
+ function watchDriveRoot(rootDir, onChange, onError) {
14096
+ const watchers = /* @__PURE__ */ new Map();
14097
+ let closed = false;
14098
+ let refreshTimer = null;
14099
+ const addDirectoryTree = (directoryPath) => {
14100
+ if (closed || watchers.has(directoryPath)) {
14101
+ return;
14102
+ }
14103
+ const directoryStat = lstatSync(directoryPath);
14104
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
14105
+ return;
14106
+ }
14107
+ const watcher = watch(directoryPath, (_eventType, filename) => {
14108
+ const dirtyPath = typeof filename === "string" || Buffer.isBuffer(filename) ? path19.join(directoryPath, filename.toString()) : directoryPath;
14109
+ onChange("drive_sync_filesystem_change", dirtyPath);
14110
+ scheduleRefresh();
14111
+ });
14112
+ watcher.on("error", (error) => {
14113
+ watchers.delete(directoryPath);
14114
+ onError(asError(error));
14115
+ });
14116
+ watchers.set(directoryPath, watcher);
14117
+ for (const child of readdirSync(directoryPath, { withFileTypes: true })) {
14118
+ if (!child.isDirectory()) {
14119
+ continue;
14120
+ }
14121
+ addDirectoryTree(path19.join(directoryPath, child.name));
14122
+ }
14123
+ };
14124
+ const refreshDirectoryTree = () => {
14125
+ if (closed) {
14126
+ return;
14127
+ }
14128
+ try {
14129
+ addDirectoryTree(rootDir);
14130
+ } catch (error) {
14131
+ onError(asError(error));
14132
+ }
14133
+ };
14134
+ const scheduleRefresh = () => {
14135
+ if (closed || refreshTimer) {
14136
+ return;
14137
+ }
14138
+ refreshTimer = setTimeout(() => {
14139
+ refreshTimer = null;
14140
+ refreshDirectoryTree();
14141
+ }, 250);
14142
+ refreshTimer.unref?.();
14143
+ };
14144
+ addDirectoryTree(rootDir);
14145
+ return {
14146
+ close() {
14147
+ closed = true;
14148
+ if (refreshTimer) {
14149
+ clearTimeout(refreshTimer);
14150
+ refreshTimer = null;
14151
+ }
14152
+ for (const watcher of watchers.values()) {
14153
+ watcher.close();
14154
+ }
14155
+ watchers.clear();
14156
+ }
14157
+ };
14158
+ }
14159
+
11058
14160
  // dist/managed-runtime/heartbeat.js
11059
14161
  import { setTimeout as sleep2 } from "node:timers/promises";
11060
14162
  function startHostHeartbeatLoop(params) {
@@ -11201,6 +14303,8 @@ async function startManagedGatewayRuntime(config = resolveManagedGatewayRuntimeC
11201
14303
  let stopHeartbeat = null;
11202
14304
  let stopRealtimeWakeLoop = null;
11203
14305
  let unsubscribeAuthStateChange = null;
14306
+ let driveSyncService = null;
14307
+ let driveSyncScheduler = null;
11204
14308
  const removeSignalHandlers = runtimeDependencies.registerSignalHandlers(onSignal);
11205
14309
  try {
11206
14310
  await heartbeatHost();
@@ -11219,6 +14323,30 @@ async function startManagedGatewayRuntime(config = resolveManagedGatewayRuntimeC
11219
14323
  controlTimeoutMs: config.controlTimeoutMs
11220
14324
  });
11221
14325
  unsubscribeAuthStateChange = realtimeClient.unsubscribeAuthStateChange;
14326
+ if (config.driveSync.enabled) {
14327
+ driveSyncService = await runtimeDependencies.createDriveSyncService({
14328
+ config,
14329
+ supabase: realtimeClient.supabase
14330
+ });
14331
+ console.log(`${MANAGED_GATEWAY_LOG_PREFIX} enabled managed drive sync`, {
14332
+ rootDir: config.driveSync.rootDir,
14333
+ stateDir: config.driveSync.stateDir,
14334
+ clientKey: config.driveSync.clientKey ?? "<local-state-derived>",
14335
+ changeLimit: config.driveSync.changeLimit,
14336
+ maxBatchesPerTick: config.driveSync.maxBatchesPerTick,
14337
+ debounceMs: config.driveSync.debounceMs,
14338
+ pollIntervalMs: config.driveSync.pollIntervalMs,
14339
+ localAuditIntervalMs: config.driveSync.localAuditIntervalMs
14340
+ });
14341
+ await mkdir4(config.driveSync.rootDir, { recursive: true });
14342
+ driveSyncScheduler = startManagedDriveSyncScheduler({
14343
+ service: driveSyncService,
14344
+ rootDir: config.driveSync.rootDir,
14345
+ debounceMs: config.driveSync.debounceMs,
14346
+ pollIntervalMs: config.driveSync.pollIntervalMs,
14347
+ localAuditIntervalMs: config.driveSync.localAuditIntervalMs
14348
+ });
14349
+ }
11222
14350
  stopRealtimeWakeLoop = runtimeDependencies.startRealtimeWakeLoop({
11223
14351
  supabase: realtimeClient.supabase,
11224
14352
  hostId: config.hostId,
@@ -11271,9 +14399,22 @@ async function startManagedGatewayRuntime(config = resolveManagedGatewayRuntimeC
11271
14399
  if (draining) {
11272
14400
  continue;
11273
14401
  }
14402
+ if (driveSyncScheduler && !activeExecutionId) {
14403
+ const syncResult = await driveSyncScheduler.requestSync("runtime_wake", { immediate: true });
14404
+ if (syncResult.status === "failed") {
14405
+ console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} continuing managed execution after drive sync failure`, {
14406
+ error: syncResult.error
14407
+ });
14408
+ } else if (syncResult.status === "degraded") {
14409
+ console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} continuing managed execution with degraded drive sync`, {
14410
+ error: syncResult.error,
14411
+ failedDriveCount: syncResult.summary.failedDriveCount
14412
+ });
14413
+ }
14414
+ }
11274
14415
  while (!shutdownRequested && !draining) {
11275
14416
  try {
11276
- const claimToken = randomUUID5();
14417
+ const claimToken = randomUUID8();
11277
14418
  const execution = await runtimeDependencies.dispatchLinuxExecution({
11278
14419
  controlUrl: config.hostControlUrl,
11279
14420
  controlToken: config.hostControlToken,
@@ -11321,6 +14462,8 @@ async function startManagedGatewayRuntime(config = resolveManagedGatewayRuntimeC
11321
14462
  } finally {
11322
14463
  stopHeartbeat?.();
11323
14464
  await stopRealtimeWakeLoop?.();
14465
+ await driveSyncScheduler?.stop();
14466
+ driveSyncService?.close();
11324
14467
  unsubscribeAuthStateChange?.();
11325
14468
  removeSignalHandlers();
11326
14469
  console.log(`${MANAGED_GATEWAY_LOG_PREFIX} stopped managed gateway runtime`, {
@@ -11464,20 +14607,20 @@ function resolveGatewayPaths2(options) {
11464
14607
  return resolveGatewayPaths(getActiveOptions(options));
11465
14608
  }
11466
14609
  function resolveGatewayFallbackConfigDir() {
11467
- if (process18.platform === "darwin") {
11468
- return path16.join(os8.homedir(), "Library", "Application Support", "Panorama Gateway");
14610
+ if (process19.platform === "darwin") {
14611
+ return path20.join(os8.homedir(), "Library", "Application Support", "Panorama Gateway");
11469
14612
  }
11470
- if (process18.platform === "win32") {
11471
- const base = process18.env.APPDATA || path16.join(os8.homedir(), "AppData", "Roaming");
11472
- return path16.join(base, "Panorama Gateway");
14613
+ if (process19.platform === "win32") {
14614
+ const base = process19.env.APPDATA || path20.join(os8.homedir(), "AppData", "Roaming");
14615
+ return path20.join(base, "Panorama Gateway");
11473
14616
  }
11474
- const dataHome = process18.env.XDG_DATA_HOME || path16.join(os8.homedir(), ".local", "share");
11475
- return path16.join(dataHome, "panorama-gateway");
14617
+ const dataHome = process19.env.XDG_DATA_HOME || path20.join(os8.homedir(), ".local", "share");
14618
+ return path20.join(dataHome, "panorama-gateway");
11476
14619
  }
11477
14620
  async function ensureWritableDirectory(dir) {
11478
14621
  try {
11479
14622
  await ensureOwnerOnlyDirectory(dir);
11480
- const probe = path16.join(dir, `.panorama-write-${randomUUID6()}`);
14623
+ const probe = path20.join(dir, `.panorama-write-${randomUUID9()}`);
11481
14624
  await writeOwnerOnlyFile(probe, "ok");
11482
14625
  await fs6.unlink(probe);
11483
14626
  return { ok: true };
@@ -11487,16 +14630,16 @@ async function ensureWritableDirectory(dir) {
11487
14630
  }
11488
14631
  async function prepareGatewayConfigDir(options, allowFallback) {
11489
14632
  const resolvedOptions = getActiveOptions(options);
11490
- const overrideDir = getStringOption(resolvedOptions, "config-dir") || process18.env.PANORAMA_GATEWAY_CONFIG_DIR;
11491
- const overridePath = getStringOption(resolvedOptions, "config-path") || process18.env.PANORAMA_GATEWAY_CONFIG_PATH;
14633
+ const overrideDir = getStringOption(resolvedOptions, "config-dir") || process19.env.PANORAMA_GATEWAY_CONFIG_DIR;
14634
+ const overridePath = getStringOption(resolvedOptions, "config-path") || process19.env.PANORAMA_GATEWAY_CONFIG_PATH;
11492
14635
  const explicitOverride = Boolean(overrideDir || overridePath);
11493
14636
  const { configDir, configPath, logPath, pidPath } = resolveGatewayPaths2(resolvedOptions);
11494
14637
  const writable = await ensureWritableDirectory(configDir);
11495
14638
  if (writable.ok) {
11496
- process18.env.PANORAMA_GATEWAY_CONFIG_DIR = configDir;
11497
- process18.env.PANORAMA_GATEWAY_CONFIG_PATH = configPath;
11498
- process18.env.PANORAMA_GATEWAY_LOG_PATH = logPath;
11499
- process18.env.PANORAMA_GATEWAY_PID_PATH = pidPath;
14639
+ process19.env.PANORAMA_GATEWAY_CONFIG_DIR = configDir;
14640
+ process19.env.PANORAMA_GATEWAY_CONFIG_PATH = configPath;
14641
+ process19.env.PANORAMA_GATEWAY_LOG_PATH = logPath;
14642
+ process19.env.PANORAMA_GATEWAY_PID_PATH = pidPath;
11500
14643
  try {
11501
14644
  await ensureGatewayStatePermissions({ configDir, configPath, logPath, pidPath });
11502
14645
  } catch (error) {
@@ -11517,7 +14660,7 @@ async function prepareGatewayConfigDir(options, allowFallback) {
11517
14660
  const fallbackPaths = resolveGatewayPaths2(fallbackOptions);
11518
14661
  if (configPath !== fallbackPaths.configPath && fsSync9.existsSync(configPath)) {
11519
14662
  try {
11520
- await ensureOwnerOnlyDirectory(path16.dirname(fallbackPaths.configPath));
14663
+ await ensureOwnerOnlyDirectory(path20.dirname(fallbackPaths.configPath));
11521
14664
  await fs6.copyFile(configPath, fallbackPaths.configPath);
11522
14665
  await ensureOwnerOnlyFile(fallbackPaths.configPath);
11523
14666
  } catch (error) {
@@ -11529,10 +14672,10 @@ async function prepareGatewayConfigDir(options, allowFallback) {
11529
14672
  }
11530
14673
  }
11531
14674
  resolvedOptions["config-dir"] = fallbackDir;
11532
- process18.env.PANORAMA_GATEWAY_CONFIG_DIR = fallbackDir;
11533
- process18.env.PANORAMA_GATEWAY_CONFIG_PATH = fallbackPaths.configPath;
11534
- process18.env.PANORAMA_GATEWAY_LOG_PATH = fallbackPaths.logPath;
11535
- process18.env.PANORAMA_GATEWAY_PID_PATH = fallbackPaths.pidPath;
14675
+ process19.env.PANORAMA_GATEWAY_CONFIG_DIR = fallbackDir;
14676
+ process19.env.PANORAMA_GATEWAY_CONFIG_PATH = fallbackPaths.configPath;
14677
+ process19.env.PANORAMA_GATEWAY_LOG_PATH = fallbackPaths.logPath;
14678
+ process19.env.PANORAMA_GATEWAY_PID_PATH = fallbackPaths.pidPath;
11536
14679
  try {
11537
14680
  await ensureGatewayStatePermissions(fallbackPaths);
11538
14681
  } catch (error) {
@@ -11620,20 +14763,20 @@ async function getProcessUptime(pid) {
11620
14763
  function findRepoRoot(startDir) {
11621
14764
  let current = startDir;
11622
14765
  while (true) {
11623
- if (fsSync9.existsSync(path16.join(current, "pnpm-workspace.yaml"))) {
14766
+ if (fsSync9.existsSync(path20.join(current, "pnpm-workspace.yaml"))) {
11624
14767
  return current;
11625
14768
  }
11626
- const parent = path16.dirname(current);
14769
+ const parent = path20.dirname(current);
11627
14770
  if (parent === current)
11628
14771
  return null;
11629
14772
  current = parent;
11630
14773
  }
11631
14774
  }
11632
14775
  function loadEnvironment(options) {
11633
- const envFileOption = getStringOption(options, "env-file") || process18.env.PANORAMA_ENV_FILE || process18.env.PANORAMA_ENV_PATH;
11634
- const envNameOption = getStringOption(options, "env") || process18.env.PANORAMA_ENV || process18.env.ENVIRONMENT;
14776
+ const envFileOption = getStringOption(options, "env-file") || process19.env.PANORAMA_ENV_FILE || process19.env.PANORAMA_ENV_PATH;
14777
+ const envNameOption = getStringOption(options, "env") || process19.env.PANORAMA_ENV || process19.env.ENVIRONMENT;
11635
14778
  const envName = normalizeEnvName(envNameOption) ?? "local";
11636
- const envPath = envFileOption ? path16.resolve(envFileOption) : path16.join(findRepoRoot(process18.cwd()) ?? process18.cwd(), envName === "local" ? ".env" : `.env.${envName}`);
14779
+ const envPath = envFileOption ? path20.resolve(envFileOption) : path20.join(findRepoRoot(process19.cwd()) ?? process19.cwd(), envName === "local" ? ".env" : `.env.${envName}`);
11637
14780
  if (!fsSync9.existsSync(envPath)) {
11638
14781
  if (envFileOption || envNameOption) {
11639
14782
  throw new GatewayCliError("Environment file not found.", `Path: ${envPath}`);
@@ -11654,7 +14797,7 @@ function shouldEmitDebugOutput(stream) {
11654
14797
  return isVerbose();
11655
14798
  }
11656
14799
  function logInfo(message, data) {
11657
- if (!shouldEmitDebugOutput(process18.stdout))
14800
+ if (!shouldEmitDebugOutput(process19.stdout))
11658
14801
  return;
11659
14802
  if (data) {
11660
14803
  console.log(`[gateway] ${message}`, data);
@@ -11663,7 +14806,7 @@ function logInfo(message, data) {
11663
14806
  }
11664
14807
  }
11665
14808
  function logError(message, data) {
11666
- if (!shouldEmitDebugOutput(process18.stderr))
14809
+ if (!shouldEmitDebugOutput(process19.stderr))
11667
14810
  return;
11668
14811
  if (data) {
11669
14812
  console.error(`[gateway] ${message}`, data);
@@ -11672,7 +14815,7 @@ function logError(message, data) {
11672
14815
  }
11673
14816
  }
11674
14817
  function logWarn(message, data) {
11675
- if (!shouldEmitDebugOutput(process18.stderr))
14818
+ if (!shouldEmitDebugOutput(process19.stderr))
11676
14819
  return;
11677
14820
  if (data) {
11678
14821
  console.warn(`[gateway] ${message}`, data);
@@ -11809,7 +14952,7 @@ function buildGatewayDoctorDependencies() {
11809
14952
  };
11810
14953
  }
11811
14954
  async function run() {
11812
- const parsed = parseArgs(process18.argv.slice(2));
14955
+ const parsed = parseArgs(process19.argv.slice(2));
11813
14956
  ACTIVE_OPTIONS = parsed.options;
11814
14957
  applyOptionEnvOverrides(parsed.options);
11815
14958
  if (parsed.options.h || parsed.options.help || parsed.command === null) {
@@ -11825,7 +14968,7 @@ async function run() {
11825
14968
  throw new Error("Pairing code is required");
11826
14969
  }
11827
14970
  await pairGatewayCommand(code, parsed.options, configResolution, buildGatewayPairingDependencies());
11828
- const entryPath = process18.argv[1] || "";
14971
+ const entryPath = process19.argv[1] || "";
11829
14972
  if (entryPath.endsWith(".ts")) {
11830
14973
  cliInfo2('Auto-start skipped in dev mode; run "panorama-gateway start" when ready.', parsed.options);
11831
14974
  return;
@@ -11876,7 +15019,7 @@ async function run() {
11876
15019
  return;
11877
15020
  }
11878
15021
  printHelp();
11879
- process18.exitCode = 1;
15022
+ process19.exitCode = 1;
11880
15023
  }
11881
15024
  run().catch((error) => {
11882
15025
  const { message, details } = describeError(error);
@@ -11885,6 +15028,6 @@ run().catch((error) => {
11885
15028
  detailItems.push({ label: "Details", value: details, verboseOnly: true });
11886
15029
  }
11887
15030
  cliError2(message, ACTIVE_OPTIONS ?? void 0, detailItems);
11888
- process18.exitCode = 1;
15031
+ process19.exitCode = 1;
11889
15032
  });
11890
15033
  //# sourceMappingURL=index.js.map