@ricsam/r5d-worker 0.0.90 → 0.0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,6 +41,8 @@ Worker labels are mandatory and unique per user. Choose labels that describe hos
41
41
 
42
42
  The supervisor also reconnects automatically after server rollouts and transient network failures. A brief bounded delay prevents a tight retry loop while the server is unavailable. Pressing Ctrl+C still stops the worker, and agent-managed processes retain their existing in-process recovery path while disconnected.
43
43
 
44
+ The runtime opens its control connection before recovering interrupted project snapshots. During recovery and initial project configuration it remains connected but non-dispatchable, reports its current phase plus branch/byte progress to the server, and prints the same progress locally. Recovery runs in a credential-sanitized helper process so the control loop can continue answering heartbeats. A durable `restoring` transaction state permits same-filesystem snapshots to be restored with one atomic directory move; cross-filesystem layouts retain the copy-and-fsync fallback.
45
+
44
46
  The server registry is the sole heartbeat timeout authority. The worker answers server pings but does not run an independent connection watchdog, because synchronous bounded workspace operations can temporarily occupy its event loop. The server keeps those operations leased for their bounded lifetime and closes genuinely stale connections after the lease ends.
45
47
 
46
48
  Credential-generation restart fencing protects against stale or accidentally detached descendants. It is not a hostile same-UID sandbox: code running as the worker's OS user can read or tamper with that user's config and auth files, install a persistent user service, or return after reboot. Run untrusted agents in a disposable VM that is reset at the boundary, or use a separately privileged credential broker/service account. When rotating the worker's own local `r5dctl` credential, stop the worker and reboot/reset the host before writing the replacement credential so an old same-UID descendant cannot read the new config.
package/dist/cjs/main.cjs CHANGED
@@ -80,6 +80,7 @@ var import_repository_transition_policy = require("./repository-transition-polic
80
80
  var import_supervisor = require("./supervisor.cjs");
81
81
  var import_managed_paths = require("./managed-paths.cjs");
82
82
  var import_project_worktrees = require("./project-worktrees.cjs");
83
+ var import_project_snapshot_recovery_runner = require("./project-snapshot-recovery-runner.cjs");
83
84
  var import_project_workspace_state = require("./project-workspace-state.cjs");
84
85
  var import_workspace_git_sync = require("./workspace-git-sync.cjs");
85
86
  const import_meta = {};
@@ -3407,28 +3408,6 @@ async function startWorker(options) {
3407
3408
  runGit(["--version"]);
3408
3409
  await verifyR5dctlAuth(baseUrl, token, label);
3409
3410
  const initializedWorkspaceState = await workspaceSyncSingleFlight.runExclusive(() => {
3410
- if (!startupProjectSnapshotRecoveryCompleted) {
3411
- const snapshotRecovery = (0, import_project_worktrees.recoverStaleProjectWorktreeSnapshots)({
3412
- projectsRoot,
3413
- temporaryRoot: projectWorktreeSnapshotsRoot
3414
- });
3415
- if (snapshotRecovery.restored.length > 0) {
3416
- process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
3417
- `);
3418
- }
3419
- if (snapshotRecovery.removed.length > 0) {
3420
- process.stdout.write(`[r5d-worker] removed ${snapshotRecovery.removed.length} completed/incomplete project snapshot(s)
3421
- `);
3422
- }
3423
- for (const failure of snapshotRecovery.failed) {
3424
- process.stderr.write(`[r5d-worker] failed to recover stale project snapshot ${failure.path}: ${failure.error}
3425
- `);
3426
- }
3427
- if (snapshotRecovery.failed.length > 0) {
3428
- throw new Error("Project snapshot recovery is incomplete; refusing to mutate project checkouts");
3429
- }
3430
- startupProjectSnapshotRecoveryCompleted = true;
3431
- }
3432
3411
  import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
3433
3412
  import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
3434
3413
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
@@ -3467,6 +3446,8 @@ async function startWorker(options) {
3467
3446
  let cliUpdateInProgress = false;
3468
3447
  let reloadAfterClose = false;
3469
3448
  let shutdownAfterClose = false;
3449
+ let startupRecoveryComplete = startupProjectSnapshotRecoveryCompleted;
3450
+ let startupRecoveryPromise = Promise.resolve();
3470
3451
  const automaticWorkspaceSyncIsCurrentlyAdmitted = () => workspaceAutomaticSyncIsAdmitted({
3471
3452
  workspaceConfigured,
3472
3453
  activeIncidentId: activeWorkspaceIncidentId,
@@ -3883,8 +3864,14 @@ async function startWorker(options) {
3883
3864
  })
3884
3865
  )
3885
3866
  ].sort();
3886
- const ensureConfiguredProjects = async () => {
3887
- for (const project of projectConfigById.values()) {
3867
+ const ensureConfiguredProjects = async (onProgress) => {
3868
+ const projects = [...projectConfigById.values()];
3869
+ for (const [projectIndex, project] of projects.entries()) {
3870
+ onProgress?.({
3871
+ detail: `Configuring ${project.projectPath}`,
3872
+ completedItems: projectIndex,
3873
+ totalItems: projects.length
3874
+ });
3888
3875
  validateProjectId(project.projectId);
3889
3876
  (0, import_repository_transition_policy.assertRepositoryTransitionState)(project);
3890
3877
  for (const branch of project.branches) validateBranchName(branch.branchName);
@@ -3932,7 +3919,14 @@ async function startWorker(options) {
3932
3919
  mirrorUrl: connection.mirrorUrl,
3933
3920
  mirrorCredentialUsername: connection.mirrorCredentialUsername,
3934
3921
  credentialHelper: connection.credentialHelper,
3935
- gitIdentity: workspaceGitIdentity
3922
+ gitIdentity: workspaceGitIdentity,
3923
+ onSnapshotProgress: (progress) => onProgress?.({
3924
+ detail: progress.phase === "scanning" ? `Measuring ${project.projectPath} workspace snapshot` : progress.phase === "syncing" ? `Making ${project.projectPath} workspace snapshot durable` : `${progress.operation === "restore" ? "Restoring" : "Snapshotting"} ${project.projectPath}${progress.branchName ? `/${progress.branchName}` : ""}`,
3925
+ completedBytes: progress.completedBytes,
3926
+ completedItems: progress.completedBranches,
3927
+ totalItems: progress.totalBranches,
3928
+ ...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
3929
+ })
3936
3930
  });
3937
3931
  readyProjectIds.add(project.projectId);
3938
3932
  reconciledProjectConfigFingerprints.set(project.projectId, configFingerprint);
@@ -3967,6 +3961,11 @@ async function startWorker(options) {
3967
3961
  `
3968
3962
  );
3969
3963
  }
3964
+ onProgress?.({
3965
+ detail: `Configured ${project.projectPath}`,
3966
+ completedItems: projectIndex + 1,
3967
+ totalItems: projects.length
3968
+ });
3970
3969
  }
3971
3970
  };
3972
3971
  const collectAheadOfOriginBranches = async () => {
@@ -4359,6 +4358,11 @@ async function startWorker(options) {
4359
4358
  if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result2);
4360
4359
  return result2;
4361
4360
  }
4361
+ sendLifecycleProgress({
4362
+ phase: "syncing",
4363
+ operationId: attemptId,
4364
+ detail: input.trigger.detail ?? `Synchronizing workspace (${input.trigger.type})`
4365
+ });
4362
4366
  if (!input.requestId && input.sendResult !== false) {
4363
4367
  if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4364
4368
  throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
@@ -4461,6 +4465,9 @@ async function startWorker(options) {
4461
4465
  }
4462
4466
  };
4463
4467
  if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result);
4468
+ sendLifecycleProgress(
4469
+ result.outcome === "failed" ? { phase: "failed", operationId: attemptId, detail: result.error ?? "Workspace synchronization failed" } : { phase: "ready", operationId: attemptId, detail: "Workspace synchronized" }
4470
+ );
4464
4471
  return result;
4465
4472
  };
4466
4473
  const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
@@ -4535,6 +4542,14 @@ async function startWorker(options) {
4535
4542
  });
4536
4543
  };
4537
4544
  const configureWorkerWorkspace = async (message, receiptGeneration) => {
4545
+ const configurationOperationId = crypto.randomUUID();
4546
+ sendLifecycleProgress({
4547
+ phase: "configuring",
4548
+ operationId: configurationOperationId,
4549
+ detail: `Preparing ${message.projects.length} project(s)`,
4550
+ completedItems: 0,
4551
+ totalItems: message.projects.length
4552
+ });
4538
4553
  const incidentDeferral = workspaceConfigurationIncidentDeferral({
4539
4554
  requestedIncidentId: message.deferWorkspaceSyncForIncidentId,
4540
4555
  activeIncidentId: activeWorkspaceIncidentId
@@ -4899,7 +4914,13 @@ async function startWorker(options) {
4899
4914
  // checkout initialization from erasing synchronized dirt.
4900
4915
  { ignoreBusy: true }
4901
4916
  );
4902
- await ensureConfiguredProjects();
4917
+ await ensureConfiguredProjects((progress) => {
4918
+ sendLifecycleProgress({ phase: "configuring", operationId: configurationOperationId, ...progress });
4919
+ process.stdout.write(
4920
+ `[r5d-worker] ${progress.detail ?? "Configuring workspace"}${progress.completedBytes === void 0 ? "" : `: ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes`}${progress.completedItems === void 0 ? "" : `, ${progress.completedItems}/${progress.totalItems ?? "?"} item(s)`}
4921
+ `
4922
+ );
4923
+ });
4903
4924
  const configurationSyncAdmissionGeneration = workerAdmissionGeneration;
4904
4925
  const assertConfigurationSyncStillAdmitted = () => {
4905
4926
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration || configurationSyncAdmissionGeneration !== workerAdmissionGeneration || !workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
@@ -4998,6 +5019,60 @@ async function startWorker(options) {
4998
5019
  Authorization: `Bearer ${token}`
4999
5020
  }
5000
5021
  });
5022
+ const sendLifecycleProgress = (progress) => {
5023
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
5024
+ sendWorkerMessage(ws, { type: "worker_progress", progress });
5025
+ };
5026
+ const describeSnapshotProgress = (progress) => {
5027
+ const project = import_node_path.default.relative(projectsRoot, progress.projectRoot) || import_node_path.default.basename(progress.projectRoot);
5028
+ const branch = progress.branchName ? `/${progress.branchName}` : "";
5029
+ if (progress.phase === "scanning") return `Scanning ${project} recovery snapshot`;
5030
+ if (progress.phase === "syncing") return `Making restored ${project} files durable`;
5031
+ if (progress.phase === "cleaning") return `Cleaning recovered ${project} snapshot`;
5032
+ return `Restoring ${project}${branch}`;
5033
+ };
5034
+ const performStartupRecovery = async (operationId) => {
5035
+ if (startupProjectSnapshotRecoveryCompleted) return;
5036
+ sendLifecycleProgress({ phase: "recovering", operationId, detail: "Checking interrupted workspace transactions" });
5037
+ const snapshotRecovery = await workspaceSyncSingleFlight.runExclusive(
5038
+ async () => await (0, import_project_snapshot_recovery_runner.recoverProjectSnapshotsInChild)({
5039
+ projectsRoot,
5040
+ snapshotRoot: projectWorktreeSnapshotsRoot,
5041
+ onProgress: (progress) => {
5042
+ const lifecycleProgress = {
5043
+ phase: "recovering",
5044
+ operationId,
5045
+ detail: describeSnapshotProgress(progress),
5046
+ completedBytes: progress.completedBytes,
5047
+ completedItems: progress.completedBranches,
5048
+ totalItems: progress.totalBranches,
5049
+ ...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
5050
+ };
5051
+ sendLifecycleProgress(lifecycleProgress);
5052
+ process.stdout.write(
5053
+ `[r5d-worker] ${lifecycleProgress.detail}: ${progress.completedBranches}/${progress.totalBranches} branch(es), ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes
5054
+ `
5055
+ );
5056
+ }
5057
+ })
5058
+ );
5059
+ if (snapshotRecovery.restored.length > 0) {
5060
+ process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
5061
+ `);
5062
+ }
5063
+ if (snapshotRecovery.removed.length > 0) {
5064
+ process.stdout.write(`[r5d-worker] removed ${snapshotRecovery.removed.length} completed/incomplete project snapshot(s)
5065
+ `);
5066
+ }
5067
+ for (const failure of snapshotRecovery.failed) {
5068
+ process.stderr.write(`[r5d-worker] failed to recover stale project snapshot ${failure.path}: ${failure.error}
5069
+ `);
5070
+ }
5071
+ if (snapshotRecovery.failed.length > 0) {
5072
+ throw new Error("Project snapshot recovery is incomplete; refusing to mutate project checkouts");
5073
+ }
5074
+ startupProjectSnapshotRecoveryCompleted = true;
5075
+ };
5001
5076
  const handleGracefulShutdown = () => {
5002
5077
  if (shutdownAfterClose) return;
5003
5078
  shutdownAfterClose = true;
@@ -5027,8 +5102,27 @@ async function startWorker(options) {
5027
5102
  process.on("SIGINT", handleGracefulShutdown);
5028
5103
  ws.addEventListener("open", () => {
5029
5104
  currentWorkerSocket = ws;
5105
+ const startupOperationId = crypto.randomUUID();
5106
+ startupRecoveryPromise = Promise.resolve().then(async () => await performStartupRecovery(startupOperationId)).then(() => {
5107
+ startupRecoveryComplete = true;
5108
+ if (currentWorkerSocket === ws && ws.readyState === WebSocket.OPEN) {
5109
+ sendWorkerMessage(ws, { type: "startup_ready" });
5110
+ sendLifecycleProgress({
5111
+ phase: "configuring",
5112
+ operationId: crypto.randomUUID(),
5113
+ detail: "Waiting for workspace configuration"
5114
+ });
5115
+ }
5116
+ }).catch((error) => {
5117
+ const message = error instanceof Error ? error.message : String(error);
5118
+ sendLifecycleProgress({ phase: "failed", operationId: startupOperationId, detail: message });
5119
+ process.stderr.write(`[r5d-worker] startup recovery failed: ${message}
5120
+ `);
5121
+ ws.close(1011, "Project snapshot recovery failed");
5122
+ });
5030
5123
  const hello = {
5031
5124
  type: "hello",
5125
+ startupReady: startupRecoveryComplete,
5032
5126
  hostInfo: {
5033
5127
  hostname: (0, import_node_os.hostname)(),
5034
5128
  platform: process.platform,
@@ -5066,6 +5160,8 @@ async function startWorker(options) {
5066
5160
  ws.addEventListener("message", (event) => {
5067
5161
  void (async () => {
5068
5162
  const message = JSON.parse(String(event.data));
5163
+ if (!startupRecoveryComplete && message.type !== "ping") await startupRecoveryPromise;
5164
+ if (!startupRecoveryComplete && message.type !== "ping") return;
5069
5165
  const messageAdmissionGeneration = workerAdmissionGeneration;
5070
5166
  const assertMessageAdmission = () => assertWorkerChildAdmission({
5071
5167
  capturedGeneration: messageAdmissionGeneration,
@@ -5093,6 +5189,19 @@ async function startWorker(options) {
5093
5189
  aheadOfOriginBranches: configured.aheadOfOriginBranches,
5094
5190
  ...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
5095
5191
  });
5192
+ sendLifecycleProgress(
5193
+ configured.result.outcome === "failed" ? {
5194
+ phase: "failed",
5195
+ operationId: configured.result.attemptId,
5196
+ detail: configured.result.error ?? "Workspace configuration failed"
5197
+ } : {
5198
+ phase: "ready",
5199
+ operationId: configured.result.attemptId,
5200
+ detail: `${message.projects.length} project(s) configured`,
5201
+ completedItems: message.projects.length,
5202
+ totalItems: message.projects.length
5203
+ }
5204
+ );
5096
5205
  process.stdout.write(
5097
5206
  `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
5098
5207
  `
@@ -5881,6 +5990,21 @@ async function startWorker(options) {
5881
5990
  if (shutdownAfterClose) {
5882
5991
  process.exit(0);
5883
5992
  }
5993
+ if (!startupRecoveryComplete) {
5994
+ process.stderr.write("[r5d-worker] control connection closed during workspace recovery; finishing recovery before reconnecting\n");
5995
+ void startupRecoveryPromise.then(() => {
5996
+ if (!startupRecoveryComplete) {
5997
+ process.exit(1);
5998
+ return;
5999
+ }
6000
+ void startWorker(options).catch((error) => {
6001
+ process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
6002
+ `);
6003
+ process.exit(error instanceof WorkerServerUnavailableError ? import_supervisor.WORKER_RECONNECT_EXIT_CODE : 1);
6004
+ });
6005
+ });
6006
+ return;
6007
+ }
5884
6008
  if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
5885
6009
  process.stderr.write(
5886
6010
  `[r5d-worker] ${activeProcesses.size} process(es), ${credentialBearingProcessGroups.size} owned process group(s), and ${activePtys.size} PTY(s) active with ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${import_supervisor.WORKER_RECONNECT_DELAY_MS}ms
@@ -5911,6 +6035,11 @@ async function startWorker(options) {
5911
6035
  });
5912
6036
  }
5913
6037
  async function main() {
6038
+ if (process.argv[2] === import_project_snapshot_recovery_runner.PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND) {
6039
+ const exitCode2 = (0, import_project_snapshot_recovery_runner.runProjectSnapshotRecoveryHelper)(process.argv.slice(3));
6040
+ if (exitCode2 !== 0) process.exit(exitCode2);
6041
+ return;
6042
+ }
5914
6043
  const parsed = parseArgs(process.argv.slice(2));
5915
6044
  if (parsed.command === "help") {
5916
6045
  printHelp();
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.90",
3
+ "version": "0.0.91",
4
4
  "type": "commonjs"
5
5
  }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var project_snapshot_recovery_runner_exports = {};
30
+ __export(project_snapshot_recovery_runner_exports, {
31
+ PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND: () => PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
32
+ recoverProjectSnapshotsInChild: () => recoverProjectSnapshotsInChild,
33
+ runProjectSnapshotRecoveryHelper: () => runProjectSnapshotRecoveryHelper
34
+ });
35
+ module.exports = __toCommonJS(project_snapshot_recovery_runner_exports);
36
+ var import_node_path = __toESM(require("node:path"), 1);
37
+ var import_node_child_process = require("node:child_process");
38
+ var import_project_worktrees = require("./project-worktrees.cjs");
39
+ const PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND = "__recover-project-snapshots";
40
+ function requireAbsolutePath(value, label) {
41
+ if (!value) throw new Error(`Missing ${label}`);
42
+ const resolved = import_node_path.default.resolve(value);
43
+ if (resolved !== value) throw new Error(`${label} must be an absolute path`);
44
+ return resolved;
45
+ }
46
+ function parseHelperArguments(args) {
47
+ let projectsRoot;
48
+ let snapshotRoot;
49
+ for (let index = 0; index < args.length; index += 1) {
50
+ const argument = args[index];
51
+ if (argument === "--projects-root") {
52
+ projectsRoot = args[index + 1];
53
+ index += 1;
54
+ continue;
55
+ }
56
+ if (argument === "--snapshot-root") {
57
+ snapshotRoot = args[index + 1];
58
+ index += 1;
59
+ continue;
60
+ }
61
+ throw new Error(`Unknown project snapshot recovery helper argument: ${argument ?? "(missing)"}`);
62
+ }
63
+ return {
64
+ projectsRoot: requireAbsolutePath(projectsRoot, "projects root"),
65
+ snapshotRoot: requireAbsolutePath(snapshotRoot, "snapshot root")
66
+ };
67
+ }
68
+ function writeHelperMessage(message) {
69
+ process.stdout.write(`${JSON.stringify(message)}
70
+ `);
71
+ }
72
+ function runProjectSnapshotRecoveryHelper(args) {
73
+ const { projectsRoot, snapshotRoot } = parseHelperArguments(args);
74
+ const result = (0, import_project_worktrees.recoverStaleProjectWorktreeSnapshots)({
75
+ projectsRoot,
76
+ temporaryRoot: snapshotRoot,
77
+ onProgress: (progress) => writeHelperMessage({ type: "progress", progress })
78
+ });
79
+ writeHelperMessage({ type: "result", result });
80
+ return result.failed.length === 0 ? 0 : 1;
81
+ }
82
+ function isRecoveryProgress(value) {
83
+ if (!value || typeof value !== "object") return false;
84
+ const progress = value;
85
+ return progress.operation === "restore" && (progress.phase === "scanning" || progress.phase === "copying" || progress.phase === "moving" || progress.phase === "syncing" || progress.phase === "cleaning") && typeof progress.projectRoot === "string" && Number.isSafeInteger(progress.completedBranches) && Number.isSafeInteger(progress.totalBranches) && typeof progress.completedBytes === "number" && Number.isFinite(progress.completedBytes) && (progress.totalBytes === void 0 || typeof progress.totalBytes === "number" && Number.isFinite(progress.totalBytes));
86
+ }
87
+ function isRecoveryResult(value) {
88
+ if (!value || typeof value !== "object") return false;
89
+ const result = value;
90
+ return Array.isArray(result.restored) && Array.isArray(result.removed) && Array.isArray(result.failed);
91
+ }
92
+ function recoveryChildEnvironment() {
93
+ const environment = {};
94
+ for (const name of ["PATH", "TMPDIR", "TMP", "TEMP", "SystemRoot", "WINDIR"]) {
95
+ const value = process.env[name];
96
+ if (value !== void 0) environment[name] = value;
97
+ }
98
+ return environment;
99
+ }
100
+ async function recoverProjectSnapshotsInChild(input) {
101
+ const entrypoint = process.argv[1];
102
+ if (!entrypoint) throw new Error("Cannot locate the r5d-worker runtime entrypoint for snapshot recovery");
103
+ const child = (0, import_node_child_process.spawn)(
104
+ process.execPath,
105
+ [
106
+ entrypoint,
107
+ PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
108
+ "--projects-root",
109
+ import_node_path.default.resolve(input.projectsRoot),
110
+ "--snapshot-root",
111
+ import_node_path.default.resolve(input.snapshotRoot)
112
+ ],
113
+ {
114
+ env: recoveryChildEnvironment(),
115
+ stdio: ["ignore", "pipe", "pipe"]
116
+ }
117
+ );
118
+ const terminateOnParentExit = () => {
119
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
120
+ };
121
+ const parentProcessEvents = process;
122
+ parentProcessEvents.once("exit", terminateOnParentExit);
123
+ let stdout = "";
124
+ let stderr = "";
125
+ let result;
126
+ child.stdout.setEncoding("utf8");
127
+ child.stdout.on("data", (chunk) => {
128
+ stdout += chunk;
129
+ while (true) {
130
+ const newline = stdout.indexOf("\n");
131
+ if (newline < 0) break;
132
+ const line = stdout.slice(0, newline);
133
+ stdout = stdout.slice(newline + 1);
134
+ if (!line.trim()) continue;
135
+ let message;
136
+ try {
137
+ message = JSON.parse(line);
138
+ } catch {
139
+ stderr += `Invalid recovery helper output: ${line}
140
+ `;
141
+ continue;
142
+ }
143
+ if (!message || typeof message !== "object") continue;
144
+ const record = message;
145
+ if (record.type === "progress" && isRecoveryProgress(record.progress)) input.onProgress?.(record.progress);
146
+ if (record.type === "result" && isRecoveryResult(record.result)) result = record.result;
147
+ }
148
+ });
149
+ child.stderr.setEncoding("utf8");
150
+ child.stderr.on("data", (chunk) => {
151
+ stderr = `${stderr}${chunk}`.slice(-32768);
152
+ });
153
+ const exitCode = await new Promise((resolve, reject) => {
154
+ child.once("error", reject);
155
+ child.once("close", (code, signal) => {
156
+ if (signal) reject(new Error(`Project snapshot recovery helper exited on signal ${signal}`));
157
+ else resolve(code ?? 1);
158
+ });
159
+ }).finally(() => parentProcessEvents.off("exit", terminateOnParentExit));
160
+ if (exitCode !== 0 || !result) {
161
+ const detail = stderr.trim();
162
+ throw new Error(detail || `Project snapshot recovery helper exited with status ${exitCode}`);
163
+ }
164
+ return result;
165
+ }
166
+ // Annotate the CommonJS export names for ESM import in node:
167
+ 0 && (module.exports = {
168
+ PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
169
+ recoverProjectSnapshotsInChild,
170
+ runProjectSnapshotRecoveryHelper
171
+ });