@ricsam/r5d-worker 0.0.90 → 0.0.92
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 +3 -1
- package/dist/cjs/main.cjs +288 -46
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-snapshot-recovery-runner.cjs +171 -0
- package/dist/cjs/project-worktrees.cjs +183 -23
- package/dist/cjs/supervisor.cjs +15 -5
- package/dist/mjs/main.mjs +293 -49
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-snapshot-recovery-runner.mjs +135 -0
- package/dist/mjs/project-worktrees.mjs +183 -23
- package/dist/mjs/supervisor.mjs +12 -4
- package/dist/types/main.d.ts +24 -0
- package/dist/types/project-snapshot-recovery-runner.d.ts +10 -0
- package/dist/types/project-worktrees.d.ts +13 -1
- package/dist/types/supervisor.d.ts +4 -1
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -49,16 +49,15 @@ import {
|
|
|
49
49
|
isRetryableWorkerServerStatus,
|
|
50
50
|
superviseWorkerRuntime,
|
|
51
51
|
WORKER_CREDENTIAL_RESTART_EXIT_CODE,
|
|
52
|
-
WORKER_RECONNECT_DELAY_MS,
|
|
53
52
|
WORKER_RECONNECT_EXIT_CODE,
|
|
54
53
|
WORKER_RELOAD_EXIT_CODE,
|
|
55
|
-
WORKER_RUNTIME_ENV
|
|
54
|
+
WORKER_RUNTIME_ENV,
|
|
55
|
+
workerReconnectDelayMs
|
|
56
56
|
} from "./supervisor.mjs";
|
|
57
57
|
import { assertDisjointManagedRoots, managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
|
|
58
58
|
import {
|
|
59
59
|
applyObservedProjectMirrorHeads,
|
|
60
60
|
assertProjectMirrorHeadsPushed,
|
|
61
|
-
recoverStaleProjectWorktreeSnapshots,
|
|
62
61
|
createOrRetryLinkedProjectBranch,
|
|
63
62
|
deleteLinkedProjectBranch,
|
|
64
63
|
deleteProjectMirrorBranch,
|
|
@@ -71,6 +70,11 @@ import {
|
|
|
71
70
|
pushProjectMirrorHeads,
|
|
72
71
|
removeProjectWorktrees
|
|
73
72
|
} from "./project-worktrees.mjs";
|
|
73
|
+
import {
|
|
74
|
+
PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
|
|
75
|
+
recoverProjectSnapshotsInChild,
|
|
76
|
+
runProjectSnapshotRecoveryHelper
|
|
77
|
+
} from "./project-snapshot-recovery-runner.mjs";
|
|
74
78
|
import {
|
|
75
79
|
ProjectWorkspacePendingBranchPathChangeError,
|
|
76
80
|
ProjectWorkspaceStateStore,
|
|
@@ -87,6 +91,21 @@ import {
|
|
|
87
91
|
workspaceGitMountsForRemediation,
|
|
88
92
|
WorkspaceRemediationAncestryError
|
|
89
93
|
} from "./workspace-git-sync.mjs";
|
|
94
|
+
const WORKER_LIFECYCLE_DETAIL_MAX_LENGTH = 500;
|
|
95
|
+
const WORKER_SERVER_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
96
|
+
const WORKER_SERVER_HEARTBEAT_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
97
|
+
function boundedWorkerLifecycleProgress(progress) {
|
|
98
|
+
return progress.detail === void 0 ? progress : { ...progress, detail: progress.detail.slice(0, WORKER_LIFECYCLE_DETAIL_MAX_LENGTH) };
|
|
99
|
+
}
|
|
100
|
+
function workerServerHeartbeatAction(input) {
|
|
101
|
+
if (input.elapsedMs < input.timeoutMs) return "wait";
|
|
102
|
+
if (input.watchdogSweepDelayed || input.probeElapsedMs === void 0) return "start_probe";
|
|
103
|
+
return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
|
|
104
|
+
}
|
|
105
|
+
const workerLifecycleTestHarness = {
|
|
106
|
+
boundedWorkerLifecycleProgress,
|
|
107
|
+
workerServerHeartbeatAction
|
|
108
|
+
};
|
|
90
109
|
class ProjectWorkspaceConfigurationDeferredError extends Error {
|
|
91
110
|
}
|
|
92
111
|
class WorkerServerUnavailableError extends Error {
|
|
@@ -2600,6 +2619,13 @@ async function executeCommand(input) {
|
|
|
2600
2619
|
const startedAt = Date.now();
|
|
2601
2620
|
let timeout;
|
|
2602
2621
|
let spawnedProcess;
|
|
2622
|
+
let preparationSettled = false;
|
|
2623
|
+
const settlePreparation = () => {
|
|
2624
|
+
if (preparationSettled) return;
|
|
2625
|
+
preparationSettled = true;
|
|
2626
|
+
input.setPreparationHeartbeatLease(false);
|
|
2627
|
+
};
|
|
2628
|
+
input.setPreparationHeartbeatLease(true);
|
|
2603
2629
|
try {
|
|
2604
2630
|
const targetProcessEnv = await prepareShellEnvForTarget({
|
|
2605
2631
|
target: input.resolvedTarget.target,
|
|
@@ -2637,6 +2663,7 @@ async function executeCommand(input) {
|
|
|
2637
2663
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2638
2664
|
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2639
2665
|
});
|
|
2666
|
+
settlePreparation();
|
|
2640
2667
|
if (input.message.timeoutMs) {
|
|
2641
2668
|
timeout = setTimeout(() => {
|
|
2642
2669
|
void terminateProcessTree(subprocess).catch((error) => {
|
|
@@ -2672,6 +2699,7 @@ async function executeCommand(input) {
|
|
|
2672
2699
|
error: error instanceof Error ? error.message : String(error)
|
|
2673
2700
|
};
|
|
2674
2701
|
} finally {
|
|
2702
|
+
settlePreparation();
|
|
2675
2703
|
if (timeout) {
|
|
2676
2704
|
clearTimeout(timeout);
|
|
2677
2705
|
}
|
|
@@ -2687,6 +2715,13 @@ async function executeStreamingCommand(input) {
|
|
|
2687
2715
|
let timedOut = false;
|
|
2688
2716
|
let timeout;
|
|
2689
2717
|
let spawnedProcess;
|
|
2718
|
+
let preparationSettled = false;
|
|
2719
|
+
const settlePreparation = () => {
|
|
2720
|
+
if (preparationSettled) return;
|
|
2721
|
+
preparationSettled = true;
|
|
2722
|
+
input.setPreparationHeartbeatLease(false);
|
|
2723
|
+
};
|
|
2724
|
+
input.setPreparationHeartbeatLease(true);
|
|
2690
2725
|
try {
|
|
2691
2726
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
2692
2727
|
sendWorkerMessage(input.ws, {
|
|
@@ -2739,6 +2774,7 @@ async function executeStreamingCommand(input) {
|
|
|
2739
2774
|
...interactive ? { interactive: true, stdin: subprocess.stdin } : {},
|
|
2740
2775
|
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2741
2776
|
});
|
|
2777
|
+
settlePreparation();
|
|
2742
2778
|
started = true;
|
|
2743
2779
|
sendWorkerMessage(input.ws, {
|
|
2744
2780
|
type: "exec_started",
|
|
@@ -2815,6 +2851,7 @@ async function executeStreamingCommand(input) {
|
|
|
2815
2851
|
});
|
|
2816
2852
|
}
|
|
2817
2853
|
} finally {
|
|
2854
|
+
settlePreparation();
|
|
2818
2855
|
if (timeout) {
|
|
2819
2856
|
clearTimeout(timeout);
|
|
2820
2857
|
}
|
|
@@ -3156,7 +3193,8 @@ function createNodePtyBridge(options) {
|
|
|
3156
3193
|
};
|
|
3157
3194
|
}
|
|
3158
3195
|
const workerPtyBridgeTestHarness = {
|
|
3159
|
-
create: createNodePtyBridge
|
|
3196
|
+
create: createNodePtyBridge,
|
|
3197
|
+
closeAllBestEffort: closePtysBestEffort
|
|
3160
3198
|
};
|
|
3161
3199
|
function resolveHostShell(command, platform = process.platform) {
|
|
3162
3200
|
if (platform === "win32") {
|
|
@@ -3305,11 +3343,23 @@ function closePty(message) {
|
|
|
3305
3343
|
}
|
|
3306
3344
|
ptyProcess.kill();
|
|
3307
3345
|
}
|
|
3308
|
-
function
|
|
3309
|
-
for (const ptyProcess of
|
|
3310
|
-
|
|
3346
|
+
function closePtysBestEffort(ptys, reportFailure) {
|
|
3347
|
+
for (const [ptyId, ptyProcess] of ptys) {
|
|
3348
|
+
try {
|
|
3349
|
+
ptyProcess.kill();
|
|
3350
|
+
} catch (error) {
|
|
3351
|
+
reportFailure(ptyId, error);
|
|
3352
|
+
}
|
|
3311
3353
|
}
|
|
3312
3354
|
}
|
|
3355
|
+
function closeAllPtys() {
|
|
3356
|
+
closePtysBestEffort(activePtys, (ptyId, error) => {
|
|
3357
|
+
process.stderr.write(
|
|
3358
|
+
`[r5d-worker] failed to close PTY ${ptyId} after control disconnect: ${error instanceof Error ? error.message : String(error)}
|
|
3359
|
+
`
|
|
3360
|
+
);
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3313
3363
|
async function terminateCredentialBearingChildren(children) {
|
|
3314
3364
|
const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
|
|
3315
3365
|
const failures = results.flatMap(
|
|
@@ -3369,16 +3419,6 @@ function websocketUrl(baseUrl, label) {
|
|
|
3369
3419
|
}
|
|
3370
3420
|
async function startWorker(options) {
|
|
3371
3421
|
advanceWorkerAdmissionGeneration();
|
|
3372
|
-
process.on("uncaughtException", (error) => {
|
|
3373
|
-
process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
3374
|
-
`);
|
|
3375
|
-
});
|
|
3376
|
-
process.on("unhandledRejection", (reason) => {
|
|
3377
|
-
process.stderr.write(
|
|
3378
|
-
`[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
|
|
3379
|
-
`
|
|
3380
|
-
);
|
|
3381
|
-
});
|
|
3382
3422
|
const config = readConfig(options.configPath);
|
|
3383
3423
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
3384
3424
|
initializeCredentialStoreDirectoryOnce();
|
|
@@ -3411,28 +3451,6 @@ async function startWorker(options) {
|
|
|
3411
3451
|
runGit(["--version"]);
|
|
3412
3452
|
await verifyR5dctlAuth(baseUrl, token, label);
|
|
3413
3453
|
const initializedWorkspaceState = await workspaceSyncSingleFlight.runExclusive(() => {
|
|
3414
|
-
if (!startupProjectSnapshotRecoveryCompleted) {
|
|
3415
|
-
const snapshotRecovery = recoverStaleProjectWorktreeSnapshots({
|
|
3416
|
-
projectsRoot,
|
|
3417
|
-
temporaryRoot: projectWorktreeSnapshotsRoot
|
|
3418
|
-
});
|
|
3419
|
-
if (snapshotRecovery.restored.length > 0) {
|
|
3420
|
-
process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
|
|
3421
|
-
`);
|
|
3422
|
-
}
|
|
3423
|
-
if (snapshotRecovery.removed.length > 0) {
|
|
3424
|
-
process.stdout.write(`[r5d-worker] removed ${snapshotRecovery.removed.length} completed/incomplete project snapshot(s)
|
|
3425
|
-
`);
|
|
3426
|
-
}
|
|
3427
|
-
for (const failure of snapshotRecovery.failed) {
|
|
3428
|
-
process.stderr.write(`[r5d-worker] failed to recover stale project snapshot ${failure.path}: ${failure.error}
|
|
3429
|
-
`);
|
|
3430
|
-
}
|
|
3431
|
-
if (snapshotRecovery.failed.length > 0) {
|
|
3432
|
-
throw new Error("Project snapshot recovery is incomplete; refusing to mutate project checkouts");
|
|
3433
|
-
}
|
|
3434
|
-
startupProjectSnapshotRecoveryCompleted = true;
|
|
3435
|
-
}
|
|
3436
3454
|
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
3437
3455
|
fs.mkdirSync(syncRoot, { recursive: true });
|
|
3438
3456
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
@@ -3471,6 +3489,8 @@ async function startWorker(options) {
|
|
|
3471
3489
|
let cliUpdateInProgress = false;
|
|
3472
3490
|
let reloadAfterClose = false;
|
|
3473
3491
|
let shutdownAfterClose = false;
|
|
3492
|
+
let startupRecoveryComplete = startupProjectSnapshotRecoveryCompleted;
|
|
3493
|
+
let startupRecoveryPromise = Promise.resolve();
|
|
3474
3494
|
const automaticWorkspaceSyncIsCurrentlyAdmitted = () => workspaceAutomaticSyncIsAdmitted({
|
|
3475
3495
|
workspaceConfigured,
|
|
3476
3496
|
activeIncidentId: activeWorkspaceIncidentId,
|
|
@@ -3887,8 +3907,14 @@ async function startWorker(options) {
|
|
|
3887
3907
|
})
|
|
3888
3908
|
)
|
|
3889
3909
|
].sort();
|
|
3890
|
-
const ensureConfiguredProjects = async () => {
|
|
3891
|
-
|
|
3910
|
+
const ensureConfiguredProjects = async (onProgress) => {
|
|
3911
|
+
const projects = [...projectConfigById.values()];
|
|
3912
|
+
for (const [projectIndex, project] of projects.entries()) {
|
|
3913
|
+
onProgress?.({
|
|
3914
|
+
detail: `Configuring ${project.projectPath}`,
|
|
3915
|
+
completedItems: projectIndex,
|
|
3916
|
+
totalItems: projects.length
|
|
3917
|
+
});
|
|
3892
3918
|
validateProjectId(project.projectId);
|
|
3893
3919
|
assertRepositoryTransitionState(project);
|
|
3894
3920
|
for (const branch of project.branches) validateBranchName(branch.branchName);
|
|
@@ -3936,7 +3962,14 @@ async function startWorker(options) {
|
|
|
3936
3962
|
mirrorUrl: connection.mirrorUrl,
|
|
3937
3963
|
mirrorCredentialUsername: connection.mirrorCredentialUsername,
|
|
3938
3964
|
credentialHelper: connection.credentialHelper,
|
|
3939
|
-
gitIdentity: workspaceGitIdentity
|
|
3965
|
+
gitIdentity: workspaceGitIdentity,
|
|
3966
|
+
onSnapshotProgress: (progress) => onProgress?.({
|
|
3967
|
+
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}` : ""}`,
|
|
3968
|
+
completedBytes: progress.completedBytes,
|
|
3969
|
+
completedItems: progress.completedBranches,
|
|
3970
|
+
totalItems: progress.totalBranches,
|
|
3971
|
+
...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
|
|
3972
|
+
})
|
|
3940
3973
|
});
|
|
3941
3974
|
readyProjectIds.add(project.projectId);
|
|
3942
3975
|
reconciledProjectConfigFingerprints.set(project.projectId, configFingerprint);
|
|
@@ -3971,6 +4004,11 @@ async function startWorker(options) {
|
|
|
3971
4004
|
`
|
|
3972
4005
|
);
|
|
3973
4006
|
}
|
|
4007
|
+
onProgress?.({
|
|
4008
|
+
detail: `Configured ${project.projectPath}`,
|
|
4009
|
+
completedItems: projectIndex + 1,
|
|
4010
|
+
totalItems: projects.length
|
|
4011
|
+
});
|
|
3974
4012
|
}
|
|
3975
4013
|
};
|
|
3976
4014
|
const collectAheadOfOriginBranches = async () => {
|
|
@@ -4363,6 +4401,11 @@ async function startWorker(options) {
|
|
|
4363
4401
|
if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result2);
|
|
4364
4402
|
return result2;
|
|
4365
4403
|
}
|
|
4404
|
+
sendLifecycleProgress({
|
|
4405
|
+
phase: "syncing",
|
|
4406
|
+
operationId: attemptId,
|
|
4407
|
+
detail: input.trigger.detail ?? `Synchronizing workspace (${input.trigger.type})`
|
|
4408
|
+
});
|
|
4366
4409
|
if (!input.requestId && input.sendResult !== false) {
|
|
4367
4410
|
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
4368
4411
|
throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
|
|
@@ -4465,6 +4508,9 @@ async function startWorker(options) {
|
|
|
4465
4508
|
}
|
|
4466
4509
|
};
|
|
4467
4510
|
if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result);
|
|
4511
|
+
sendLifecycleProgress(
|
|
4512
|
+
result.outcome === "failed" ? { phase: "failed", operationId: attemptId, detail: result.error ?? "Workspace synchronization failed" } : { phase: "ready", operationId: attemptId, detail: "Workspace synchronized" }
|
|
4513
|
+
);
|
|
4468
4514
|
return result;
|
|
4469
4515
|
};
|
|
4470
4516
|
const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
|
|
@@ -4539,6 +4585,14 @@ async function startWorker(options) {
|
|
|
4539
4585
|
});
|
|
4540
4586
|
};
|
|
4541
4587
|
const configureWorkerWorkspace = async (message, receiptGeneration) => {
|
|
4588
|
+
const configurationOperationId = crypto.randomUUID();
|
|
4589
|
+
sendLifecycleProgress({
|
|
4590
|
+
phase: "configuring",
|
|
4591
|
+
operationId: configurationOperationId,
|
|
4592
|
+
detail: `Preparing ${message.projects.length} project(s)`,
|
|
4593
|
+
completedItems: 0,
|
|
4594
|
+
totalItems: message.projects.length
|
|
4595
|
+
});
|
|
4542
4596
|
const incidentDeferral = workspaceConfigurationIncidentDeferral({
|
|
4543
4597
|
requestedIncidentId: message.deferWorkspaceSyncForIncidentId,
|
|
4544
4598
|
activeIncidentId: activeWorkspaceIncidentId
|
|
@@ -4903,7 +4957,13 @@ async function startWorker(options) {
|
|
|
4903
4957
|
// checkout initialization from erasing synchronized dirt.
|
|
4904
4958
|
{ ignoreBusy: true }
|
|
4905
4959
|
);
|
|
4906
|
-
await ensureConfiguredProjects()
|
|
4960
|
+
await ensureConfiguredProjects((progress) => {
|
|
4961
|
+
sendLifecycleProgress({ phase: "configuring", operationId: configurationOperationId, ...progress });
|
|
4962
|
+
process.stdout.write(
|
|
4963
|
+
`[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)`}
|
|
4964
|
+
`
|
|
4965
|
+
);
|
|
4966
|
+
});
|
|
4907
4967
|
const configurationSyncAdmissionGeneration = workerAdmissionGeneration;
|
|
4908
4968
|
const assertConfigurationSyncStillAdmitted = () => {
|
|
4909
4969
|
if (receiptGeneration !== workspaceConfigurationReceiptGeneration || configurationSyncAdmissionGeneration !== workerAdmissionGeneration || !workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
@@ -5002,6 +5062,73 @@ async function startWorker(options) {
|
|
|
5002
5062
|
Authorization: `Bearer ${token}`
|
|
5003
5063
|
}
|
|
5004
5064
|
});
|
|
5065
|
+
let lastServerControlMessageAtMs = performance.now();
|
|
5066
|
+
let serverHeartbeatProbeStartedAtMs;
|
|
5067
|
+
let lastServerHeartbeatSweepAtMs = performance.now();
|
|
5068
|
+
let serverHeartbeatWatchdog;
|
|
5069
|
+
const noteServerControlActivity = () => {
|
|
5070
|
+
lastServerControlMessageAtMs = performance.now();
|
|
5071
|
+
serverHeartbeatProbeStartedAtMs = void 0;
|
|
5072
|
+
};
|
|
5073
|
+
const stopServerHeartbeatWatchdog = () => {
|
|
5074
|
+
if (!serverHeartbeatWatchdog) return;
|
|
5075
|
+
clearInterval(serverHeartbeatWatchdog);
|
|
5076
|
+
serverHeartbeatWatchdog = void 0;
|
|
5077
|
+
};
|
|
5078
|
+
const sendLifecycleProgress = (progress) => {
|
|
5079
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
|
|
5080
|
+
sendWorkerMessage(ws, { type: "worker_progress", progress: boundedWorkerLifecycleProgress(progress) });
|
|
5081
|
+
};
|
|
5082
|
+
const describeSnapshotProgress = (progress) => {
|
|
5083
|
+
const project = path.relative(projectsRoot, progress.projectRoot) || path.basename(progress.projectRoot);
|
|
5084
|
+
const branch = progress.branchName ? `/${progress.branchName}` : "";
|
|
5085
|
+
if (progress.phase === "scanning") return `Scanning ${project} recovery snapshot`;
|
|
5086
|
+
if (progress.phase === "syncing") return `Making restored ${project} files durable`;
|
|
5087
|
+
if (progress.phase === "cleaning") return `Cleaning recovered ${project} snapshot`;
|
|
5088
|
+
return `Restoring ${project}${branch}`;
|
|
5089
|
+
};
|
|
5090
|
+
const performStartupRecovery = async (operationId) => {
|
|
5091
|
+
if (startupProjectSnapshotRecoveryCompleted) return;
|
|
5092
|
+
sendLifecycleProgress({ phase: "recovering", operationId, detail: "Checking interrupted workspace transactions" });
|
|
5093
|
+
const snapshotRecovery = await workspaceSyncSingleFlight.runExclusive(
|
|
5094
|
+
async () => await recoverProjectSnapshotsInChild({
|
|
5095
|
+
projectsRoot,
|
|
5096
|
+
snapshotRoot: projectWorktreeSnapshotsRoot,
|
|
5097
|
+
onProgress: (progress) => {
|
|
5098
|
+
const lifecycleProgress = {
|
|
5099
|
+
phase: "recovering",
|
|
5100
|
+
operationId,
|
|
5101
|
+
detail: describeSnapshotProgress(progress),
|
|
5102
|
+
completedBytes: progress.completedBytes,
|
|
5103
|
+
completedItems: progress.completedBranches,
|
|
5104
|
+
totalItems: progress.totalBranches,
|
|
5105
|
+
...progress.totalBytes === void 0 ? {} : { totalBytes: progress.totalBytes }
|
|
5106
|
+
};
|
|
5107
|
+
sendLifecycleProgress(lifecycleProgress);
|
|
5108
|
+
process.stdout.write(
|
|
5109
|
+
`[r5d-worker] ${lifecycleProgress.detail}: ${progress.completedBranches}/${progress.totalBranches} branch(es), ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes
|
|
5110
|
+
`
|
|
5111
|
+
);
|
|
5112
|
+
}
|
|
5113
|
+
})
|
|
5114
|
+
);
|
|
5115
|
+
if (snapshotRecovery.restored.length > 0) {
|
|
5116
|
+
process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
|
|
5117
|
+
`);
|
|
5118
|
+
}
|
|
5119
|
+
if (snapshotRecovery.removed.length > 0) {
|
|
5120
|
+
process.stdout.write(`[r5d-worker] removed ${snapshotRecovery.removed.length} completed/incomplete project snapshot(s)
|
|
5121
|
+
`);
|
|
5122
|
+
}
|
|
5123
|
+
for (const failure of snapshotRecovery.failed) {
|
|
5124
|
+
process.stderr.write(`[r5d-worker] failed to recover stale project snapshot ${failure.path}: ${failure.error}
|
|
5125
|
+
`);
|
|
5126
|
+
}
|
|
5127
|
+
if (snapshotRecovery.failed.length > 0) {
|
|
5128
|
+
throw new Error("Project snapshot recovery is incomplete; refusing to mutate project checkouts");
|
|
5129
|
+
}
|
|
5130
|
+
startupProjectSnapshotRecoveryCompleted = true;
|
|
5131
|
+
};
|
|
5005
5132
|
const handleGracefulShutdown = () => {
|
|
5006
5133
|
if (shutdownAfterClose) return;
|
|
5007
5134
|
shutdownAfterClose = true;
|
|
@@ -5031,8 +5158,50 @@ async function startWorker(options) {
|
|
|
5031
5158
|
process.on("SIGINT", handleGracefulShutdown);
|
|
5032
5159
|
ws.addEventListener("open", () => {
|
|
5033
5160
|
currentWorkerSocket = ws;
|
|
5161
|
+
noteServerControlActivity();
|
|
5162
|
+
lastServerHeartbeatSweepAtMs = performance.now();
|
|
5163
|
+
serverHeartbeatWatchdog = setInterval(() => {
|
|
5164
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
|
|
5165
|
+
const nowMs = performance.now();
|
|
5166
|
+
const heartbeatAction = workerServerHeartbeatAction({
|
|
5167
|
+
elapsedMs: Math.max(0, nowMs - lastServerControlMessageAtMs),
|
|
5168
|
+
...serverHeartbeatProbeStartedAtMs === void 0 ? {} : { probeElapsedMs: Math.max(0, nowMs - serverHeartbeatProbeStartedAtMs) },
|
|
5169
|
+
timeoutMs: WORKER_SERVER_HEARTBEAT_TIMEOUT_MS,
|
|
5170
|
+
watchdogSweepDelayed: nowMs - lastServerHeartbeatSweepAtMs >= WORKER_SERVER_HEARTBEAT_INTERVAL_MS * 2
|
|
5171
|
+
});
|
|
5172
|
+
lastServerHeartbeatSweepAtMs = nowMs;
|
|
5173
|
+
if (heartbeatAction === "start_probe") {
|
|
5174
|
+
serverHeartbeatProbeStartedAtMs = nowMs;
|
|
5175
|
+
return;
|
|
5176
|
+
}
|
|
5177
|
+
if (heartbeatAction === "terminate") {
|
|
5178
|
+
stopServerHeartbeatWatchdog();
|
|
5179
|
+
process.stderr.write("[r5d-worker] server control heartbeat timed out; reconnecting\n");
|
|
5180
|
+
ws.terminate();
|
|
5181
|
+
}
|
|
5182
|
+
}, WORKER_SERVER_HEARTBEAT_INTERVAL_MS);
|
|
5183
|
+
serverHeartbeatWatchdog.unref();
|
|
5184
|
+
const startupOperationId = crypto.randomUUID();
|
|
5185
|
+
startupRecoveryPromise = Promise.resolve().then(async () => await performStartupRecovery(startupOperationId)).then(() => {
|
|
5186
|
+
startupRecoveryComplete = true;
|
|
5187
|
+
if (currentWorkerSocket === ws && ws.readyState === WebSocket.OPEN) {
|
|
5188
|
+
sendWorkerMessage(ws, { type: "startup_ready" });
|
|
5189
|
+
sendLifecycleProgress({
|
|
5190
|
+
phase: "configuring",
|
|
5191
|
+
operationId: crypto.randomUUID(),
|
|
5192
|
+
detail: "Waiting for workspace configuration"
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
5195
|
+
}).catch((error) => {
|
|
5196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5197
|
+
sendLifecycleProgress({ phase: "failed", operationId: startupOperationId, detail: message });
|
|
5198
|
+
process.stderr.write(`[r5d-worker] startup recovery failed: ${message}
|
|
5199
|
+
`);
|
|
5200
|
+
ws.close(1011, "Project snapshot recovery failed");
|
|
5201
|
+
});
|
|
5034
5202
|
const hello = {
|
|
5035
5203
|
type: "hello",
|
|
5204
|
+
startupReady: startupRecoveryComplete,
|
|
5036
5205
|
hostInfo: {
|
|
5037
5206
|
hostname: hostname(),
|
|
5038
5207
|
platform: process.platform,
|
|
@@ -5068,8 +5237,11 @@ async function startWorker(options) {
|
|
|
5068
5237
|
process.stdout.write("[r5d-worker] connected\n");
|
|
5069
5238
|
});
|
|
5070
5239
|
ws.addEventListener("message", (event) => {
|
|
5240
|
+
noteServerControlActivity();
|
|
5071
5241
|
void (async () => {
|
|
5072
5242
|
const message = JSON.parse(String(event.data));
|
|
5243
|
+
if (!startupRecoveryComplete && message.type !== "ping") await startupRecoveryPromise;
|
|
5244
|
+
if (!startupRecoveryComplete && message.type !== "ping") return;
|
|
5073
5245
|
const messageAdmissionGeneration = workerAdmissionGeneration;
|
|
5074
5246
|
const assertMessageAdmission = () => assertWorkerChildAdmission({
|
|
5075
5247
|
capturedGeneration: messageAdmissionGeneration,
|
|
@@ -5097,6 +5269,19 @@ async function startWorker(options) {
|
|
|
5097
5269
|
aheadOfOriginBranches: configured.aheadOfOriginBranches,
|
|
5098
5270
|
...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
|
|
5099
5271
|
});
|
|
5272
|
+
sendLifecycleProgress(
|
|
5273
|
+
configured.result.outcome === "failed" ? {
|
|
5274
|
+
phase: "failed",
|
|
5275
|
+
operationId: configured.result.attemptId,
|
|
5276
|
+
detail: configured.result.error ?? "Workspace configuration failed"
|
|
5277
|
+
} : {
|
|
5278
|
+
phase: "ready",
|
|
5279
|
+
operationId: configured.result.attemptId,
|
|
5280
|
+
detail: `${message.projects.length} project(s) configured`,
|
|
5281
|
+
completedItems: message.projects.length,
|
|
5282
|
+
totalItems: message.projects.length
|
|
5283
|
+
}
|
|
5284
|
+
);
|
|
5100
5285
|
process.stdout.write(
|
|
5101
5286
|
`[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
|
|
5102
5287
|
`
|
|
@@ -5660,6 +5845,13 @@ async function startWorker(options) {
|
|
|
5660
5845
|
token,
|
|
5661
5846
|
artifactRoot,
|
|
5662
5847
|
planRoot,
|
|
5848
|
+
setPreparationHeartbeatLease: (active) => {
|
|
5849
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
5850
|
+
type: "heartbeat_lease",
|
|
5851
|
+
operationId: `exec:${message.runId}`,
|
|
5852
|
+
active
|
|
5853
|
+
});
|
|
5854
|
+
},
|
|
5663
5855
|
assertAdmission: () => {
|
|
5664
5856
|
if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
|
|
5665
5857
|
throw new Error(
|
|
@@ -5723,6 +5915,13 @@ async function startWorker(options) {
|
|
|
5723
5915
|
token,
|
|
5724
5916
|
artifactRoot,
|
|
5725
5917
|
planRoot,
|
|
5918
|
+
setPreparationHeartbeatLease: (active) => {
|
|
5919
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
5920
|
+
type: "heartbeat_lease",
|
|
5921
|
+
operationId: `exec:${message.runId}`,
|
|
5922
|
+
active
|
|
5923
|
+
});
|
|
5924
|
+
},
|
|
5726
5925
|
assertAdmission: assertMessageAdmission
|
|
5727
5926
|
});
|
|
5728
5927
|
} finally {
|
|
@@ -5855,6 +6054,7 @@ async function startWorker(options) {
|
|
|
5855
6054
|
const processEvents = process;
|
|
5856
6055
|
processEvents.off("SIGTERM", handleGracefulShutdown);
|
|
5857
6056
|
processEvents.off("SIGINT", handleGracefulShutdown);
|
|
6057
|
+
stopServerHeartbeatWatchdog();
|
|
5858
6058
|
if (workspaceAutomaticTimer) {
|
|
5859
6059
|
clearTimeout(workspaceAutomaticTimer);
|
|
5860
6060
|
workspaceAutomaticTimer = void 0;
|
|
@@ -5885,23 +6085,47 @@ async function startWorker(options) {
|
|
|
5885
6085
|
if (shutdownAfterClose) {
|
|
5886
6086
|
process.exit(0);
|
|
5887
6087
|
}
|
|
6088
|
+
if (!startupRecoveryComplete) {
|
|
6089
|
+
process.stderr.write("[r5d-worker] control connection closed during workspace recovery; finishing recovery before reconnecting\n");
|
|
6090
|
+
void startupRecoveryPromise.then(() => {
|
|
6091
|
+
if (!startupRecoveryComplete) {
|
|
6092
|
+
process.exit(1);
|
|
6093
|
+
return;
|
|
6094
|
+
}
|
|
6095
|
+
const reconnectDelayMs = workerReconnectDelayMs();
|
|
6096
|
+
process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
|
|
6097
|
+
`);
|
|
6098
|
+
setTimeout(() => {
|
|
6099
|
+
void startWorker(options).catch((error) => {
|
|
6100
|
+
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6101
|
+
`);
|
|
6102
|
+
process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
|
|
6103
|
+
});
|
|
6104
|
+
}, reconnectDelayMs);
|
|
6105
|
+
});
|
|
6106
|
+
return;
|
|
6107
|
+
}
|
|
5888
6108
|
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5889
|
-
|
|
5890
|
-
|
|
6109
|
+
const scheduleReconnect = (reconnect2) => {
|
|
6110
|
+
const reconnectDelayMs = workerReconnectDelayMs();
|
|
6111
|
+
process.stderr.write(
|
|
6112
|
+
`[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 ${reconnectDelayMs}ms
|
|
5891
6113
|
`
|
|
5892
|
-
|
|
6114
|
+
);
|
|
6115
|
+
setTimeout(reconnect2, reconnectDelayMs);
|
|
6116
|
+
};
|
|
5893
6117
|
const reconnect = () => {
|
|
5894
6118
|
void startWorker(options).catch((error) => {
|
|
5895
6119
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
5896
6120
|
`);
|
|
5897
6121
|
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5898
|
-
|
|
6122
|
+
scheduleReconnect(reconnect);
|
|
5899
6123
|
return;
|
|
5900
6124
|
}
|
|
5901
6125
|
process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
|
|
5902
6126
|
});
|
|
5903
6127
|
};
|
|
5904
|
-
|
|
6128
|
+
scheduleReconnect(reconnect);
|
|
5905
6129
|
return;
|
|
5906
6130
|
}
|
|
5907
6131
|
process.exit(WORKER_RECONNECT_EXIT_CODE);
|
|
@@ -5911,10 +6135,28 @@ async function startWorker(options) {
|
|
|
5911
6135
|
process.stderr.write(`[r5d-worker] websocket error${message}
|
|
5912
6136
|
`);
|
|
5913
6137
|
});
|
|
5914
|
-
await new Promise(() => {
|
|
6138
|
+
await new Promise((resolve) => {
|
|
6139
|
+
ws.addEventListener("close", () => resolve(), { once: true });
|
|
6140
|
+
});
|
|
6141
|
+
}
|
|
6142
|
+
function installWorkerRuntimeFailureLogging() {
|
|
6143
|
+
process.on("uncaughtException", (error) => {
|
|
6144
|
+
process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
6145
|
+
`);
|
|
6146
|
+
});
|
|
6147
|
+
process.on("unhandledRejection", (reason) => {
|
|
6148
|
+
process.stderr.write(
|
|
6149
|
+
`[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
|
|
6150
|
+
`
|
|
6151
|
+
);
|
|
5915
6152
|
});
|
|
5916
6153
|
}
|
|
5917
6154
|
async function main() {
|
|
6155
|
+
if (process.argv[2] === PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND) {
|
|
6156
|
+
const exitCode2 = runProjectSnapshotRecoveryHelper(process.argv.slice(3));
|
|
6157
|
+
if (exitCode2 !== 0) process.exit(exitCode2);
|
|
6158
|
+
return;
|
|
6159
|
+
}
|
|
5918
6160
|
const parsed = parseArgs(process.argv.slice(2));
|
|
5919
6161
|
if (parsed.command === "help") {
|
|
5920
6162
|
printHelp();
|
|
@@ -5926,6 +6168,7 @@ async function main() {
|
|
|
5926
6168
|
}
|
|
5927
6169
|
if (process.env[WORKER_RUNTIME_ENV] === "1") {
|
|
5928
6170
|
delete process.env[WORKER_RUNTIME_ENV];
|
|
6171
|
+
installWorkerRuntimeFailureLogging();
|
|
5929
6172
|
try {
|
|
5930
6173
|
await startWorker(parsed.options);
|
|
5931
6174
|
} catch (error) {
|
|
@@ -5978,6 +6221,7 @@ export {
|
|
|
5978
6221
|
syncSessionArtifacts,
|
|
5979
6222
|
workerChildProcessEnvironment,
|
|
5980
6223
|
workerGitSecurityTestHarness,
|
|
6224
|
+
workerLifecycleTestHarness,
|
|
5981
6225
|
workerPtyBridgeTestHarness,
|
|
5982
6226
|
workerPtyTestHarness,
|
|
5983
6227
|
writeWorkerTextFile
|
package/dist/mjs/package.json
CHANGED