@ricsam/r5d-worker 0.0.58 → 0.0.60
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 +1 -1
- package/dist/cjs/main.cjs +569 -191
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-convergence.cjs +256 -0
- package/dist/cjs/workspace-incident-state.cjs +32 -3
- package/dist/cjs/workspace-manifest-admission.cjs +64 -0
- package/dist/cjs/workspace-sync.cjs +129 -16
- package/dist/mjs/main.mjs +579 -197
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-convergence.mjs +213 -0
- package/dist/mjs/workspace-incident-state.mjs +31 -3
- package/dist/mjs/workspace-manifest-admission.mjs +30 -0
- package/dist/mjs/workspace-sync.mjs +126 -15
- package/dist/types/workspace-convergence.d.ts +82 -0
- package/dist/types/workspace-incident-state.d.ts +15 -1
- package/dist/types/workspace-manifest-admission.d.ts +24 -0
- package/dist/types/workspace-sync.d.ts +23 -6
- package/package.json +1 -1
- package/dist/cjs/workspace-sync-fast-path.cjs +0 -108
- package/dist/mjs/workspace-sync-fast-path.mjs +0 -81
- package/dist/types/workspace-sync-fast-path.d.ts +0 -69
package/dist/mjs/main.mjs
CHANGED
|
@@ -13,13 +13,19 @@ import {
|
|
|
13
13
|
} from "./heartbeat.mjs";
|
|
14
14
|
import { terminateProcessTree } from "./process-tree.mjs";
|
|
15
15
|
import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
|
|
16
|
-
import { applyWorkspaceIncidentUpdate, releasePendingWorkspaceHead } from "./workspace-incident-state.mjs";
|
|
16
|
+
import { applyWorkspaceIncidentUpdate, releasePendingWorkspaceHead, WorkspaceIncidentOrderingFence } from "./workspace-incident-state.mjs";
|
|
17
17
|
import { acquireWorkspaceCommandMutation, runWorkspaceCommand } from "./workspace-command-sync-policy.mjs";
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
WORKSPACE_PUBLICATION_QUIET_MS,
|
|
20
|
+
WORKSPACE_IDLE_SAFETY_SCAN_MS,
|
|
21
|
+
ExplicitWorkspacePublicationQueue,
|
|
22
|
+
WorkspaceManifestRevisionFence,
|
|
23
|
+
processWorkspaceConvergenceState,
|
|
24
|
+
waitForWorkspacePublicationOnShutdown,
|
|
25
|
+
workspacePublicationIncidentAction,
|
|
26
|
+
workspacePublicationTelemetry
|
|
27
|
+
} from "./workspace-convergence.mjs";
|
|
28
|
+
import { inspectWorkspaceManifestFilesystem } from "./workspace-manifest-admission.mjs";
|
|
23
29
|
import {
|
|
24
30
|
isRetryableWorkerServerStatus,
|
|
25
31
|
superviseWorkerRuntime,
|
|
@@ -30,15 +36,11 @@ import {
|
|
|
30
36
|
} from "./supervisor.mjs";
|
|
31
37
|
import { managedProjectRoot, migrateLegacyProjectRoots, validateManagedBranchName } from "./managed-paths.mjs";
|
|
32
38
|
import {
|
|
33
|
-
WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
|
|
34
39
|
WorkspaceSyncSingleFlight,
|
|
35
|
-
|
|
40
|
+
convergeWorkspaceHead,
|
|
36
41
|
synchronizeWorkspace,
|
|
37
42
|
workspaceProjectsForSync
|
|
38
43
|
} from "./workspace-sync.mjs";
|
|
39
|
-
function workspaceSyncFastPathTelemetry(decision) {
|
|
40
|
-
return decision.hit ? { hit: true, reason: decision.reason, proofAgeMs: decision.proofAgeMs } : { hit: false, reason: decision.reason };
|
|
41
|
-
}
|
|
42
44
|
class WorkerServerUnavailableError extends Error {
|
|
43
45
|
name = "WorkerServerUnavailableError";
|
|
44
46
|
}
|
|
@@ -775,25 +777,13 @@ function fallbackInternalRemoteUrl(baseUrl, projectId) {
|
|
|
775
777
|
}
|
|
776
778
|
function visibleCheckoutRemoteFor(input) {
|
|
777
779
|
const canonicalCheckout = input.manifest?.canonicalCheckouts?.find((checkout) => checkout.branchName === input.branchName);
|
|
778
|
-
const isCanonicalManifestBranch = Boolean(canonicalCheckout);
|
|
779
|
-
const usesCanonicalRemote = isCanonicalManifestBranch || !input.manifest?.repoHttpUrl;
|
|
780
|
-
if (usesCanonicalRemote) {
|
|
781
|
-
return {
|
|
782
|
-
remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
|
|
783
|
-
authHeader: `Authorization: Bearer ${input.token}`,
|
|
784
|
-
persistAuth: false,
|
|
785
|
-
reconcileExistingOrigin: isCanonicalManifestBranch,
|
|
786
|
-
requireRemoteBranch: isCanonicalManifestBranch,
|
|
787
|
-
requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
780
|
return {
|
|
791
|
-
remoteUrl:
|
|
792
|
-
authHeader: input.
|
|
793
|
-
persistAuth:
|
|
794
|
-
reconcileExistingOrigin:
|
|
795
|
-
requireRemoteBranch:
|
|
796
|
-
requiredBaseCommit: null
|
|
781
|
+
remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
|
|
782
|
+
authHeader: `Authorization: Bearer ${input.token}`,
|
|
783
|
+
persistAuth: false,
|
|
784
|
+
reconcileExistingOrigin: true,
|
|
785
|
+
requireRemoteBranch: true,
|
|
786
|
+
requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
|
|
797
787
|
};
|
|
798
788
|
}
|
|
799
789
|
function gitExtraHeaderUrlForRemote(remoteUrl) {
|
|
@@ -1014,9 +1004,11 @@ function checkoutVisibleBranch(input) {
|
|
|
1014
1004
|
}
|
|
1015
1005
|
function migrateExistingCheckoutToRequiredRemote(input) {
|
|
1016
1006
|
if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
|
|
1017
|
-
|
|
1007
|
+
const dirtyWorkingTree = hasWorktreeChanges(input.branchPath);
|
|
1008
|
+
if (dirtyWorkingTree && input.requiredBaseCommit) {
|
|
1018
1009
|
throw new Error(`Cannot migrate dirty checkout for ${input.branchName} to its canonical remote`);
|
|
1019
1010
|
}
|
|
1011
|
+
const preserveWorkingTree = dirtyWorkingTree;
|
|
1020
1012
|
runGit(
|
|
1021
1013
|
visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
|
|
1022
1014
|
{
|
|
@@ -1031,11 +1023,12 @@ function migrateExistingCheckoutToRequiredRemote(input) {
|
|
|
1031
1023
|
if (input.requiredBaseCommit && (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, canonicalRemoteRef], { cwd: input.branchPath }))) {
|
|
1032
1024
|
throw new Error(`Canonical branch ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
|
|
1033
1025
|
}
|
|
1026
|
+
ensureOriginRemote(input.branchPath, input.remoteUrl);
|
|
1027
|
+
if (preserveWorkingTree) return;
|
|
1034
1028
|
if (tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) {
|
|
1035
1029
|
const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
|
|
1036
1030
|
runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
|
|
1037
1031
|
}
|
|
1038
|
-
ensureOriginRemote(input.branchPath, input.remoteUrl);
|
|
1039
1032
|
runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
|
|
1040
1033
|
cwd: input.branchPath
|
|
1041
1034
|
});
|
|
@@ -1128,7 +1121,7 @@ function ensureVisibleGitCheckout(input) {
|
|
|
1128
1121
|
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
1129
1122
|
return branchPath;
|
|
1130
1123
|
}
|
|
1131
|
-
function
|
|
1124
|
+
function ensureBranchWorktree(input) {
|
|
1132
1125
|
const defaultBranch = input.manifest?.defaultBranch || "main";
|
|
1133
1126
|
const remote = visibleCheckoutRemoteFor(input);
|
|
1134
1127
|
return {
|
|
@@ -1161,7 +1154,7 @@ function resolveWorkerSessionTarget(input) {
|
|
|
1161
1154
|
}
|
|
1162
1155
|
const manifest = input.manifestByProjectId.get(input.target.projectId);
|
|
1163
1156
|
const projectRoot = projectRootFor(input.projectsRoot, input.target.projectId, input.manifestByProjectId);
|
|
1164
|
-
const
|
|
1157
|
+
const worktree = ensureBranchWorktree({
|
|
1165
1158
|
projectId: input.target.projectId,
|
|
1166
1159
|
baseUrl: input.baseUrl,
|
|
1167
1160
|
token: input.token,
|
|
@@ -1170,7 +1163,7 @@ function resolveWorkerSessionTarget(input) {
|
|
|
1170
1163
|
branchName: input.target.branchName,
|
|
1171
1164
|
manifest
|
|
1172
1165
|
});
|
|
1173
|
-
return { target: input.target, rootPath:
|
|
1166
|
+
return { target: input.target, rootPath: worktree.branchPath, manifest };
|
|
1174
1167
|
}
|
|
1175
1168
|
function workspaceSyncCheckoutTargets(input) {
|
|
1176
1169
|
const selected = /* @__PURE__ */ new Map();
|
|
@@ -2268,6 +2261,7 @@ async function openPty(input) {
|
|
|
2268
2261
|
onExit: (event) => {
|
|
2269
2262
|
input.releaseWorkspaceMutation?.();
|
|
2270
2263
|
activePtys.delete(input.message.ptyId);
|
|
2264
|
+
input.onTerminal?.();
|
|
2271
2265
|
sendWorkerMessage(input.ws, {
|
|
2272
2266
|
type: "pty_exit",
|
|
2273
2267
|
ptyId: input.message.ptyId,
|
|
@@ -2278,6 +2272,7 @@ async function openPty(input) {
|
|
|
2278
2272
|
onError: (error) => {
|
|
2279
2273
|
input.releaseWorkspaceMutation?.();
|
|
2280
2274
|
activePtys.delete(input.message.ptyId);
|
|
2275
|
+
input.onTerminal?.();
|
|
2281
2276
|
sendWorkerMessage(input.ws, {
|
|
2282
2277
|
type: "pty_error",
|
|
2283
2278
|
requestId: input.message.requestId,
|
|
@@ -2369,15 +2364,25 @@ async function startWorker(options) {
|
|
|
2369
2364
|
const pendingManifestCheckouts = /* @__PURE__ */ new Map();
|
|
2370
2365
|
let workspaceRemoteUrl = null;
|
|
2371
2366
|
let activeWorkspaceIncidentId = null;
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2367
|
+
const workspaceIncidentOrdering = new WorkspaceIncidentOrderingFence();
|
|
2368
|
+
const workspaceConvergence = processWorkspaceConvergenceState(path.join(syncRoot, "workspace-convergence.json"));
|
|
2369
|
+
const workspaceManifestRevision = new WorkspaceManifestRevisionFence();
|
|
2370
|
+
let workspaceReady = false;
|
|
2371
|
+
let workspacePublicationTimer;
|
|
2372
|
+
let workspaceSafetyScan;
|
|
2373
|
+
let workspaceManifestHydrationTimer;
|
|
2374
|
+
let workspaceManifestHydrationInFlight = false;
|
|
2375
|
+
let workspaceSafetyScanInFlight = false;
|
|
2376
|
+
let workspaceHeadConvergenceInFlight = false;
|
|
2377
|
+
let pendingWorkspaceHead;
|
|
2378
|
+
let pendingPublication;
|
|
2379
|
+
const explicitWorkspacePublications = new ExplicitWorkspacePublicationQueue();
|
|
2375
2380
|
let terminalReplayTimer;
|
|
2376
2381
|
let workspaceSyncRequestsInFlight = 0;
|
|
2377
2382
|
let sessionArtifactSyncRequestsInFlight = 0;
|
|
2378
2383
|
let cliUpdateInProgress = false;
|
|
2379
2384
|
let reloadAfterClose = false;
|
|
2380
|
-
|
|
2385
|
+
let shutdownAfterClose = false;
|
|
2381
2386
|
const workspaceSyncInput = (trigger, overrides = {}) => {
|
|
2382
2387
|
if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
|
|
2383
2388
|
const projects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
|
|
@@ -2403,21 +2408,160 @@ async function startWorker(options) {
|
|
|
2403
2408
|
);
|
|
2404
2409
|
}
|
|
2405
2410
|
};
|
|
2406
|
-
const
|
|
2411
|
+
const sendWorkspaceReady = () => {
|
|
2412
|
+
const manifestRevision = workspaceManifestRevision.readyRevision();
|
|
2413
|
+
if (!workspaceReady || !manifestRevision) return;
|
|
2407
2414
|
try {
|
|
2408
2415
|
ws.send(
|
|
2409
2416
|
JSON.stringify({
|
|
2410
|
-
type: "
|
|
2411
|
-
|
|
2417
|
+
type: "workspace_ready",
|
|
2418
|
+
manifestRevision,
|
|
2419
|
+
pendingCheckouts: [...pendingManifestCheckouts.values()].sort(
|
|
2420
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
2421
|
+
),
|
|
2422
|
+
...workspaceConvergence.snapshot()
|
|
2423
|
+
})
|
|
2424
|
+
);
|
|
2425
|
+
} catch (error) {
|
|
2426
|
+
process.stderr.write(`[r5d-worker] failed to send workspace readiness: ${error instanceof Error ? error.message : String(error)}
|
|
2427
|
+
`);
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
2431
|
+
const armWorkspacePublication = (trigger, options2 = { delayMs: WORKSPACE_PUBLICATION_QUIET_MS }) => {
|
|
2432
|
+
workspaceConvergence.schedule();
|
|
2433
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
|
|
2434
|
+
sendWorkspaceReady();
|
|
2435
|
+
if (options2.requestId && !explicitWorkspacePublications.schedule({ requestId: options2.requestId, trigger }, pendingPublication?.requestId)) {
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
if (!options2.requestId && explicitWorkspacePublications.hasScheduled()) return;
|
|
2439
|
+
if (workspacePublicationTimer) clearTimeout(workspacePublicationTimer);
|
|
2440
|
+
workspacePublicationTimer = setTimeout(
|
|
2441
|
+
() => {
|
|
2442
|
+
workspacePublicationTimer = void 0;
|
|
2443
|
+
if (options2.requestId) explicitWorkspacePublications.consumeScheduled(options2.requestId);
|
|
2444
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
|
|
2445
|
+
if (!workspaceReady || !workspaceRemoteUrl) {
|
|
2446
|
+
armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
if (activeWorkspaceIncidentId) {
|
|
2450
|
+
workspaceConvergence.block();
|
|
2451
|
+
sendWorkspaceReady();
|
|
2452
|
+
if (workspacePublicationIncidentAction(options2.requestId) === "defer_automatic") return;
|
|
2453
|
+
if (pendingPublication) {
|
|
2454
|
+
explicitWorkspacePublications.enqueue({ requestId: options2.requestId, trigger }, pendingPublication.requestId);
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
const snapshot2 = workspaceConvergence.snapshot();
|
|
2458
|
+
pendingPublication = {
|
|
2459
|
+
requestId: options2.requestId,
|
|
2460
|
+
trigger,
|
|
2461
|
+
dirtyGeneration: snapshot2.dirtyGeneration,
|
|
2462
|
+
explicit: true,
|
|
2463
|
+
workInFlight: false,
|
|
2464
|
+
requestedAt: Date.now()
|
|
2465
|
+
};
|
|
2466
|
+
try {
|
|
2467
|
+
ws.send(
|
|
2468
|
+
JSON.stringify({
|
|
2469
|
+
type: "workspace_publication_request",
|
|
2470
|
+
requestId: options2.requestId,
|
|
2471
|
+
trigger,
|
|
2472
|
+
dirtyGeneration: snapshot2.dirtyGeneration,
|
|
2473
|
+
localHead: snapshot2.localHead,
|
|
2474
|
+
desiredCanonicalHead: snapshot2.desiredCanonicalHead
|
|
2475
|
+
})
|
|
2476
|
+
);
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
pendingPublication = void 0;
|
|
2479
|
+
workspaceConvergence.defer();
|
|
2480
|
+
process.stderr.write(
|
|
2481
|
+
`[r5d-worker] failed to request blocked workspace publication authority: ${error instanceof Error ? error.message : String(error)}
|
|
2482
|
+
`
|
|
2483
|
+
);
|
|
2484
|
+
armWorkspacePublication(trigger, { delayMs: 1e3, requestId: options2.requestId });
|
|
2485
|
+
}
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
if (pendingPublication) {
|
|
2489
|
+
armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
const snapshot = workspaceConvergence.snapshot();
|
|
2493
|
+
const requestId = options2.requestId ?? crypto.randomUUID();
|
|
2494
|
+
pendingPublication = {
|
|
2495
|
+
requestId,
|
|
2412
2496
|
trigger,
|
|
2413
|
-
|
|
2497
|
+
dirtyGeneration: snapshot.dirtyGeneration,
|
|
2498
|
+
explicit: options2.requestId !== void 0,
|
|
2499
|
+
workInFlight: false,
|
|
2500
|
+
requestedAt: Date.now()
|
|
2501
|
+
};
|
|
2502
|
+
try {
|
|
2503
|
+
ws.send(
|
|
2504
|
+
JSON.stringify({
|
|
2505
|
+
type: "workspace_publication_request",
|
|
2506
|
+
requestId,
|
|
2507
|
+
trigger,
|
|
2508
|
+
dirtyGeneration: snapshot.dirtyGeneration,
|
|
2509
|
+
localHead: snapshot.localHead,
|
|
2510
|
+
desiredCanonicalHead: snapshot.desiredCanonicalHead
|
|
2511
|
+
})
|
|
2512
|
+
);
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
pendingPublication = void 0;
|
|
2515
|
+
workspaceConvergence.defer();
|
|
2516
|
+
process.stderr.write(
|
|
2517
|
+
`[r5d-worker] failed to request workspace publication: ${error instanceof Error ? error.message : String(error)}
|
|
2518
|
+
`
|
|
2519
|
+
);
|
|
2520
|
+
armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
|
|
2521
|
+
}
|
|
2522
|
+
},
|
|
2523
|
+
Math.max(0, options2.delayMs)
|
|
2524
|
+
);
|
|
2525
|
+
workspacePublicationTimer.unref();
|
|
2526
|
+
};
|
|
2527
|
+
const markWorkspaceDirty = (trigger, force = false) => {
|
|
2528
|
+
workspaceConvergence.markDirty();
|
|
2529
|
+
armWorkspacePublication(trigger, { delayMs: force ? 0 : WORKSPACE_PUBLICATION_QUIET_MS });
|
|
2530
|
+
};
|
|
2531
|
+
const convergeAvailableWorkspaceHead = async () => {
|
|
2532
|
+
if (pendingWorkspaceHead === void 0 || !workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || workspaceHeadConvergenceInFlight) {
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
const advertisedHead = pendingWorkspaceHead;
|
|
2536
|
+
pendingWorkspaceHead = void 0;
|
|
2537
|
+
workspaceConvergence.observeCanonicalHead(advertisedHead);
|
|
2538
|
+
workspaceHeadConvergenceInFlight = true;
|
|
2539
|
+
try {
|
|
2540
|
+
const snapshot = workspaceConvergence.snapshot();
|
|
2541
|
+
const hydrateVisible = !pendingPublication && snapshot.dirtyGeneration === snapshot.publishedGeneration;
|
|
2542
|
+
const converged = await workspaceSyncSingleFlight.runExclusive(
|
|
2543
|
+
() => convergeWorkspaceHead(workspaceSyncInput({ type: "inbound_head", detail: advertisedHead ?? "unborn" }), {
|
|
2544
|
+
hydrateVisible
|
|
2414
2545
|
})
|
|
2415
2546
|
);
|
|
2547
|
+
workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
|
|
2548
|
+
if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
|
|
2549
|
+
if (hydrateVisible && converged.localChanges) {
|
|
2550
|
+
markWorkspaceDirty({ type: "periodic", detail: "inbound overlap retained for next publication" });
|
|
2551
|
+
}
|
|
2552
|
+
sendWorkspaceReady();
|
|
2416
2553
|
} catch (error) {
|
|
2554
|
+
pendingWorkspaceHead = advertisedHead;
|
|
2417
2555
|
process.stderr.write(
|
|
2418
|
-
`[r5d-worker] failed to
|
|
2556
|
+
`[r5d-worker] failed to fetch available workspace head: ${error instanceof Error ? error.message : String(error)}
|
|
2419
2557
|
`
|
|
2420
2558
|
);
|
|
2559
|
+
} finally {
|
|
2560
|
+
workspaceHeadConvergenceInFlight = false;
|
|
2561
|
+
heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
|
|
2562
|
+
if (pendingWorkspaceHead !== void 0) {
|
|
2563
|
+
setTimeout(() => void convergeAvailableWorkspaceHead(), 1e3).unref();
|
|
2564
|
+
}
|
|
2421
2565
|
}
|
|
2422
2566
|
};
|
|
2423
2567
|
const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
|
|
@@ -2433,7 +2577,7 @@ async function startWorker(options) {
|
|
|
2433
2577
|
workspaceShadowRoot,
|
|
2434
2578
|
manifestByProjectId
|
|
2435
2579
|
});
|
|
2436
|
-
const
|
|
2580
|
+
const ensureVisibleWorktrees = (targets, options2 = {}) => {
|
|
2437
2581
|
const created = [];
|
|
2438
2582
|
for (const target of targets) {
|
|
2439
2583
|
const manifest = manifestByProjectId.get(target.projectId);
|
|
@@ -2462,20 +2606,62 @@ async function startWorker(options) {
|
|
|
2462
2606
|
reconcileExistingOrigin: remote.reconcileExistingOrigin,
|
|
2463
2607
|
requireRemoteBranch: remote.requireRemoteBranch,
|
|
2464
2608
|
requiredBaseCommit: remote.requiredBaseCommit,
|
|
2465
|
-
allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.
|
|
2609
|
+
allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requiredBaseCommit),
|
|
2466
2610
|
clearPersistentAuth: !remote.persistAuth
|
|
2467
2611
|
});
|
|
2468
2612
|
if (!existed) created.push(target);
|
|
2469
2613
|
}
|
|
2470
2614
|
return created;
|
|
2471
2615
|
};
|
|
2472
|
-
const
|
|
2616
|
+
const schedulePendingManifestCheckoutHydration = (delayMs = 0) => {
|
|
2617
|
+
if (workspaceManifestHydrationTimer || workspaceManifestHydrationInFlight || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
workspaceManifestHydrationTimer = setTimeout(
|
|
2621
|
+
() => {
|
|
2622
|
+
workspaceManifestHydrationTimer = void 0;
|
|
2623
|
+
if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
2624
|
+
if (pendingManifestCheckouts.size > 0 && currentWorkerSocket === ws && ws.readyState === WebSocket.OPEN) {
|
|
2625
|
+
schedulePendingManifestCheckoutHydration(1e3);
|
|
2626
|
+
}
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
workspaceManifestHydrationInFlight = true;
|
|
2630
|
+
void workspaceSyncSingleFlight.runExclusive(async () => {
|
|
2631
|
+
if (!workspaceReady || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const targets = [...pendingManifestCheckouts.values()];
|
|
2635
|
+
ensureVisibleWorktrees(targets, { strictCanonicalRevalidation: true });
|
|
2636
|
+
for (const target of targets) {
|
|
2637
|
+
const manifest = manifestByProjectId.get(target.projectId);
|
|
2638
|
+
if (!manifest || !manifest.branches.includes(target.branchName)) continue;
|
|
2639
|
+
const branchPath = path.join(projectRootFor(projectsRoot, target.projectId, manifestByProjectId), target.branchName);
|
|
2640
|
+
if (hasNormalVisibleGitDir(branchPath)) {
|
|
2641
|
+
pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}).then(() => sendWorkspaceReady()).catch((error) => {
|
|
2645
|
+
process.stderr.write(
|
|
2646
|
+
`[r5d-worker] pending manifest checkout hydration failed: ${error instanceof Error ? error.message : String(error)}
|
|
2647
|
+
`
|
|
2648
|
+
);
|
|
2649
|
+
}).finally(() => {
|
|
2650
|
+
workspaceManifestHydrationInFlight = false;
|
|
2651
|
+
heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
|
|
2652
|
+
if (pendingManifestCheckouts.size > 0) schedulePendingManifestCheckoutHydration(1e3);
|
|
2653
|
+
});
|
|
2654
|
+
},
|
|
2655
|
+
Math.max(0, delayMs)
|
|
2656
|
+
);
|
|
2657
|
+
workspaceManifestHydrationTimer.unref();
|
|
2658
|
+
};
|
|
2659
|
+
const runRequestedWorkspaceSync = async (authority, trigger, overrides = {}) => {
|
|
2473
2660
|
const requestedAt = Date.now();
|
|
2474
2661
|
let queueEnteredAt = requestedAt;
|
|
2475
2662
|
let prepareStartedAt = requestedAt;
|
|
2476
2663
|
let synchronizeStartedAt = requestedAt;
|
|
2477
2664
|
let synchronizeFinishedAt = requestedAt;
|
|
2478
|
-
let fastPathDecision = { hit: false, reason: "no_recent_proof" };
|
|
2479
2665
|
workspaceSyncRequestsInFlight += 1;
|
|
2480
2666
|
try {
|
|
2481
2667
|
let pendingTargets = [];
|
|
@@ -2484,48 +2670,12 @@ async function startWorker(options) {
|
|
|
2484
2670
|
const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
|
|
2485
2671
|
const syncProjects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
|
|
2486
2672
|
if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
|
|
2487
|
-
const scopeKey = workspaceSyncScopeKey(workspaceRemoteUrl, syncProjects);
|
|
2488
2673
|
const allowedCheckoutKeys = new Set(
|
|
2489
2674
|
syncProjects.flatMap((project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName)))
|
|
2490
2675
|
);
|
|
2491
2676
|
const scopedPendingTargets = [...pendingManifestCheckouts.values()].filter(
|
|
2492
2677
|
(target) => allowedCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
|
|
2493
2678
|
);
|
|
2494
|
-
const workerBusy = activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress;
|
|
2495
|
-
fastPathDecision = recentWorkspaceSyncProof.evaluate({
|
|
2496
|
-
trigger,
|
|
2497
|
-
allowRecentNoChangeFastPath,
|
|
2498
|
-
canonicalHead: authoritativeCanonicalHead,
|
|
2499
|
-
scopeKey,
|
|
2500
|
-
nowMs: Date.now(),
|
|
2501
|
-
skipVisibleMirror: overrides.skipVisibleMirror,
|
|
2502
|
-
resetToCanonical: overrides.resetToCanonical,
|
|
2503
|
-
incidentActive: activeWorkspaceIncidentId !== null,
|
|
2504
|
-
workerBusy,
|
|
2505
|
-
pendingCheckouts: scopedPendingTargets.length > 0 || Boolean(overrides.newVisibleCheckouts?.length)
|
|
2506
|
-
});
|
|
2507
|
-
if (fastPathDecision.hit) {
|
|
2508
|
-
prepareStartedAt = queueEnteredAt;
|
|
2509
|
-
synchronizeStartedAt = queueEnteredAt;
|
|
2510
|
-
synchronizeFinishedAt = queueEnteredAt;
|
|
2511
|
-
return {
|
|
2512
|
-
type: "workspace_sync",
|
|
2513
|
-
attemptId: overrides.attemptId ?? crypto.randomUUID(),
|
|
2514
|
-
workerLabel: label,
|
|
2515
|
-
trigger,
|
|
2516
|
-
outcome: "no_change",
|
|
2517
|
-
startingHead: fastPathDecision.canonicalHead,
|
|
2518
|
-
expectedHead: fastPathDecision.canonicalHead,
|
|
2519
|
-
publishedHead: fastPathDecision.canonicalHead,
|
|
2520
|
-
rebaseCount: 0,
|
|
2521
|
-
diffSizeBytes: 0,
|
|
2522
|
-
gitStatus: "",
|
|
2523
|
-
affectedProjects: [],
|
|
2524
|
-
affectedPaths: [],
|
|
2525
|
-
discardedPaths: [],
|
|
2526
|
-
localChangesDiscarded: false
|
|
2527
|
-
};
|
|
2528
|
-
}
|
|
2529
2679
|
prepareStartedAt = Date.now();
|
|
2530
2680
|
let checkoutTargets = [];
|
|
2531
2681
|
if (shouldEnsureCheckouts) {
|
|
@@ -2537,7 +2687,7 @@ async function startWorker(options) {
|
|
|
2537
2687
|
});
|
|
2538
2688
|
}
|
|
2539
2689
|
pendingTargets = shouldEnsureCheckouts ? scopedPendingTargets : [];
|
|
2540
|
-
const createdCheckouts =
|
|
2690
|
+
const createdCheckouts = ensureVisibleWorktrees(checkoutTargets, {
|
|
2541
2691
|
strictCanonicalRevalidation: true
|
|
2542
2692
|
});
|
|
2543
2693
|
const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
|
|
@@ -2551,24 +2701,12 @@ async function startWorker(options) {
|
|
|
2551
2701
|
...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
|
|
2552
2702
|
});
|
|
2553
2703
|
synchronizeStartedAt = Date.now();
|
|
2554
|
-
recentWorkspaceSyncProof.invalidate();
|
|
2555
|
-
const observationGeneration = recentWorkspaceSyncProof.beginObservation();
|
|
2556
|
-
const workerWasBusyDuringObservation = workerBusy;
|
|
2557
2704
|
let synchronized;
|
|
2558
2705
|
try {
|
|
2559
2706
|
synchronized = await synchronizeWorkspace(input);
|
|
2560
2707
|
} finally {
|
|
2561
2708
|
synchronizeFinishedAt = Date.now();
|
|
2562
2709
|
}
|
|
2563
|
-
if ((synchronized.outcome === "no_change" || synchronized.outcome === "updated") && pendingTargets.length === 0) {
|
|
2564
|
-
recentWorkspaceSyncProof.recordStable({
|
|
2565
|
-
observationGeneration,
|
|
2566
|
-
canonicalHead: synchronized.publishedHead ?? null,
|
|
2567
|
-
scopeKey,
|
|
2568
|
-
observedAtMs: synchronizeFinishedAt,
|
|
2569
|
-
workerBusy: workerWasBusyDuringObservation || activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress || activeWorkspaceIncidentId !== null
|
|
2570
|
-
});
|
|
2571
|
-
}
|
|
2572
2710
|
return synchronized;
|
|
2573
2711
|
});
|
|
2574
2712
|
const completedAt = Date.now();
|
|
@@ -2578,8 +2716,7 @@ async function startWorker(options) {
|
|
|
2578
2716
|
totalMs: completedAt - requestedAt,
|
|
2579
2717
|
queueMs: queueEnteredAt - requestedAt,
|
|
2580
2718
|
prepareMs: synchronizeStartedAt - prepareStartedAt,
|
|
2581
|
-
synchronizeMs: synchronizeFinishedAt - synchronizeStartedAt
|
|
2582
|
-
fastPath: workspaceSyncFastPathTelemetry(fastPathDecision)
|
|
2719
|
+
synchronizeMs: synchronizeFinishedAt - synchronizeStartedAt
|
|
2583
2720
|
}
|
|
2584
2721
|
};
|
|
2585
2722
|
if (resultWithTelemetry.outcome === "no_change" || resultWithTelemetry.outcome === "published" || resultWithTelemetry.outcome === "updated" || resultWithTelemetry.outcome === "conflict_reset") {
|
|
@@ -2588,7 +2725,6 @@ async function startWorker(options) {
|
|
|
2588
2725
|
if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
|
|
2589
2726
|
}
|
|
2590
2727
|
}
|
|
2591
|
-
previousPeriodicFingerprint = null;
|
|
2592
2728
|
sendWorkspaceSyncResult(authority, resultWithTelemetry);
|
|
2593
2729
|
return resultWithTelemetry;
|
|
2594
2730
|
} catch (error) {
|
|
@@ -2611,12 +2747,10 @@ async function startWorker(options) {
|
|
|
2611
2747
|
totalMs: completedAt - requestedAt,
|
|
2612
2748
|
queueMs: queueEnteredAt - requestedAt,
|
|
2613
2749
|
prepareMs: Math.max(0, synchronizeStartedAt - prepareStartedAt),
|
|
2614
|
-
synchronizeMs: Math.max(0, synchronizeFinishedAt - synchronizeStartedAt)
|
|
2615
|
-
fastPath: workspaceSyncFastPathTelemetry(fastPathDecision)
|
|
2750
|
+
synchronizeMs: Math.max(0, synchronizeFinishedAt - synchronizeStartedAt)
|
|
2616
2751
|
},
|
|
2617
2752
|
error: error instanceof Error ? error.message : String(error)
|
|
2618
2753
|
};
|
|
2619
|
-
previousPeriodicFingerprint = null;
|
|
2620
2754
|
sendWorkspaceSyncResult(authority, result);
|
|
2621
2755
|
return result;
|
|
2622
2756
|
} finally {
|
|
@@ -2632,6 +2766,24 @@ async function startWorker(options) {
|
|
|
2632
2766
|
let heartbeatBusyGrace = null;
|
|
2633
2767
|
let heartbeatTimedOut = false;
|
|
2634
2768
|
let heartbeatWatchdog;
|
|
2769
|
+
const handleGracefulShutdown = () => {
|
|
2770
|
+
if (shutdownAfterClose) return;
|
|
2771
|
+
shutdownAfterClose = true;
|
|
2772
|
+
void waitForWorkspacePublicationOnShutdown({
|
|
2773
|
+
snapshot: () => workspaceConvergence.snapshot(),
|
|
2774
|
+
forcePublication: () => armWorkspacePublication({ type: "manual", detail: "graceful worker shutdown" }, { delayMs: 0 })
|
|
2775
|
+
}).then((outcome) => {
|
|
2776
|
+
process.stdout.write(`[r5d-worker] graceful workspace publication ${outcome}
|
|
2777
|
+
`);
|
|
2778
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
2779
|
+
ws.close(1e3, "Worker shutting down");
|
|
2780
|
+
} else {
|
|
2781
|
+
process.exit(0);
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
};
|
|
2785
|
+
process.on("SIGTERM", handleGracefulShutdown);
|
|
2786
|
+
process.on("SIGINT", handleGracefulShutdown);
|
|
2635
2787
|
const stopHeartbeatWatchdog = () => {
|
|
2636
2788
|
if (heartbeatWatchdog) {
|
|
2637
2789
|
clearInterval(heartbeatWatchdog);
|
|
@@ -2662,7 +2814,8 @@ async function startWorker(options) {
|
|
|
2662
2814
|
updateClis: true,
|
|
2663
2815
|
canonicalResolverCheckout: true,
|
|
2664
2816
|
browserPortForwarding: true,
|
|
2665
|
-
globalWorkspaceSyncLeaseV1: true
|
|
2817
|
+
globalWorkspaceSyncLeaseV1: true,
|
|
2818
|
+
eventualWorkspaceSyncV1: true
|
|
2666
2819
|
},
|
|
2667
2820
|
projectRoot: projectsRoot,
|
|
2668
2821
|
artifactRoot,
|
|
@@ -2689,14 +2842,19 @@ async function startWorker(options) {
|
|
|
2689
2842
|
return;
|
|
2690
2843
|
}
|
|
2691
2844
|
if (message.type === "workspace_manifest") {
|
|
2692
|
-
|
|
2693
|
-
|
|
2845
|
+
const revisionToken = workspaceManifestRevision.begin(message.revision);
|
|
2846
|
+
workspaceReady = false;
|
|
2847
|
+
const manifestFilesystem = inspectWorkspaceManifestFilesystem({
|
|
2848
|
+
projectsRoot,
|
|
2849
|
+
workspaceShadowRoot,
|
|
2850
|
+
projects: message.projects
|
|
2851
|
+
});
|
|
2852
|
+
const completed = await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
2853
|
+
if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
|
|
2854
|
+
const firstBootstrap = manifestFilesystem.firstBootstrap;
|
|
2694
2855
|
configureGitHubAuth(message.githubCredential);
|
|
2695
2856
|
visibleGitIdentity = message.gitIdentity;
|
|
2696
2857
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
2697
|
-
const previousCheckoutKeys = new Set(
|
|
2698
|
-
allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
|
|
2699
|
-
);
|
|
2700
2858
|
const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
|
|
2701
2859
|
manifestByProjectId.clear();
|
|
2702
2860
|
for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
|
|
@@ -2709,80 +2867,77 @@ async function startWorker(options) {
|
|
|
2709
2867
|
}
|
|
2710
2868
|
for (const checkout of currentCheckouts) {
|
|
2711
2869
|
const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
|
|
2712
|
-
if (previousCheckoutKeys.has(key)) continue;
|
|
2713
2870
|
const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
|
|
2714
2871
|
if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
|
|
2715
2872
|
}
|
|
2716
|
-
|
|
2873
|
+
if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
|
|
2874
|
+
const created = firstBootstrap || migratedProjectIds.length > 0 ? ensureVisibleWorktrees(currentCheckouts, { strictCanonicalRevalidation: true }) : [];
|
|
2875
|
+
if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
|
|
2876
|
+
const bootstrapped = await convergeWorkspaceHead(workspaceSyncInput({ type: "connect" }, { newVisibleCheckouts: created }), {
|
|
2877
|
+
// An existing checkout becomes routable after a fetch. Its
|
|
2878
|
+
// visible tree converges asynchronously; first bootstrap alone
|
|
2879
|
+
// must materialize the canonical snapshot before readiness.
|
|
2880
|
+
// Re-check the incident immediately before convergence so a
|
|
2881
|
+
// concurrently delivered fence forces fetch-only behavior.
|
|
2882
|
+
hydrateVisible: firstBootstrap && !activeWorkspaceIncidentId
|
|
2883
|
+
});
|
|
2884
|
+
if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
|
|
2885
|
+
workspaceConvergence.setLocalHead(bootstrapped.localHead);
|
|
2886
|
+
workspaceConvergence.observeCanonicalHead(bootstrapped.canonicalHead);
|
|
2887
|
+
for (const checkout of currentCheckouts) {
|
|
2888
|
+
const manifest = manifestByProjectId.get(checkout.projectId);
|
|
2889
|
+
if (!manifest) continue;
|
|
2890
|
+
const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
|
|
2891
|
+
if (hasNormalVisibleGitDir(branchPath)) {
|
|
2892
|
+
pendingManifestCheckouts.delete(manifestCheckoutKey(checkout.projectId, checkout.branchName));
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2717
2895
|
process.stdout.write(
|
|
2718
2896
|
`[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
|
|
2719
2897
|
`
|
|
2720
2898
|
);
|
|
2899
|
+
return true;
|
|
2721
2900
|
});
|
|
2722
|
-
if (!
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2901
|
+
if (!completed || !workspaceManifestRevision.complete(revisionToken)) return;
|
|
2902
|
+
workspaceReady = true;
|
|
2903
|
+
sendWorkspaceReady();
|
|
2904
|
+
schedulePendingManifestCheckoutHydration();
|
|
2905
|
+
if (!workspaceSafetyScan) {
|
|
2906
|
+
workspaceSafetyScan = setInterval(() => {
|
|
2907
|
+
if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || workspaceSafetyScanInFlight) {
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
workspaceSafetyScanInFlight = true;
|
|
2727
2911
|
void (async () => {
|
|
2728
|
-
const
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
)
|
|
2912
|
+
const before = workspaceConvergence.snapshot();
|
|
2913
|
+
const hydrateVisible = before.dirtyGeneration === before.publishedGeneration;
|
|
2914
|
+
const converged = await workspaceSyncSingleFlight.runExclusive(
|
|
2915
|
+
() => convergeWorkspaceHead(workspaceSyncInput({ type: "periodic", detail: "idle convergence scan" }), {
|
|
2916
|
+
hydrateVisible
|
|
2917
|
+
})
|
|
2732
2918
|
);
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
)
|
|
2736
|
-
|
|
2737
|
-
recentWorkspaceSyncProof.invalidate();
|
|
2738
|
-
previousPeriodicFingerprint = null;
|
|
2739
|
-
requestWorkspaceSync({
|
|
2740
|
-
type: "periodic",
|
|
2741
|
-
detail: "workspace manifest additions"
|
|
2742
|
-
});
|
|
2743
|
-
return;
|
|
2919
|
+
workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
|
|
2920
|
+
if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
|
|
2921
|
+
if (converged.localChanges && hydrateVisible) {
|
|
2922
|
+
markWorkspaceDirty({ type: "periodic", detail: "idle safety scan" });
|
|
2744
2923
|
}
|
|
2745
|
-
|
|
2746
|
-
const observationGeneration = recentWorkspaceSyncProof.beginObservation();
|
|
2747
|
-
const input = workspaceSyncInput({ type: "periodic" });
|
|
2748
|
-
const workerBusy = activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress;
|
|
2749
|
-
return {
|
|
2750
|
-
observationGeneration,
|
|
2751
|
-
scopeKey: workspaceSyncScopeKey(input.remoteUrl, input.projects),
|
|
2752
|
-
fingerprint: calculateWorkspaceDiffFingerprint(input),
|
|
2753
|
-
canonicalHead: readShadowWorkspaceCanonicalHead(workspaceShadowRoot),
|
|
2754
|
-
workerBusy
|
|
2755
|
-
};
|
|
2756
|
-
});
|
|
2757
|
-
if (observation.fingerprint === emptyFingerprint) {
|
|
2758
|
-
recentWorkspaceSyncProof.recordStable({
|
|
2759
|
-
observationGeneration: observation.observationGeneration,
|
|
2760
|
-
canonicalHead: observation.canonicalHead,
|
|
2761
|
-
scopeKey: observation.scopeKey,
|
|
2762
|
-
observedAtMs: Date.now(),
|
|
2763
|
-
workerBusy: observation.workerBusy || activeProcesses.size > 0 || activePtys.size > 0 || cliUpdateInProgress
|
|
2764
|
-
});
|
|
2765
|
-
previousPeriodicFingerprint = null;
|
|
2766
|
-
return;
|
|
2767
|
-
}
|
|
2768
|
-
recentWorkspaceSyncProof.invalidate();
|
|
2769
|
-
if (previousPeriodicFingerprint !== observation.fingerprint) {
|
|
2770
|
-
previousPeriodicFingerprint = observation.fingerprint;
|
|
2771
|
-
return;
|
|
2772
|
-
}
|
|
2773
|
-
previousPeriodicFingerprint = null;
|
|
2774
|
-
requestWorkspaceSync({ type: "periodic" }, observation.fingerprint);
|
|
2924
|
+
sendWorkspaceReady();
|
|
2775
2925
|
})().catch((error) => {
|
|
2776
2926
|
process.stderr.write(
|
|
2777
|
-
`[r5d-worker]
|
|
2927
|
+
`[r5d-worker] idle workspace safety scan failed: ${error instanceof Error ? error.message : String(error)}
|
|
2778
2928
|
`
|
|
2779
2929
|
);
|
|
2780
2930
|
}).finally(() => {
|
|
2781
|
-
|
|
2931
|
+
workspaceSafetyScanInFlight = false;
|
|
2782
2932
|
heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
|
|
2783
2933
|
});
|
|
2784
|
-
},
|
|
2785
|
-
|
|
2934
|
+
}, WORKSPACE_IDLE_SAFETY_SCAN_MS);
|
|
2935
|
+
workspaceSafetyScan.unref();
|
|
2936
|
+
}
|
|
2937
|
+
pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
|
|
2938
|
+
void convergeAvailableWorkspaceHead();
|
|
2939
|
+
if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
|
|
2940
|
+
armWorkspacePublication({ type: "periodic", detail: "resume dirty generation after reconnect" }, { delayMs: 0 });
|
|
2786
2941
|
}
|
|
2787
2942
|
return;
|
|
2788
2943
|
}
|
|
@@ -2802,12 +2957,181 @@ async function startWorker(options) {
|
|
|
2802
2957
|
confirmationReason: message.confirmationReason,
|
|
2803
2958
|
resetToCanonical: message.resetToCanonical,
|
|
2804
2959
|
skipVisibleMirror: message.skipVisibleMirror
|
|
2805
|
-
}
|
|
2806
|
-
message.canonicalHead,
|
|
2807
|
-
message.allowRecentNoChangeFastPath
|
|
2960
|
+
}
|
|
2808
2961
|
);
|
|
2809
2962
|
return;
|
|
2810
2963
|
}
|
|
2964
|
+
if (message.type === "workspace_head_available") {
|
|
2965
|
+
workspaceConvergence.observeCanonicalHead(message.canonicalHead);
|
|
2966
|
+
pendingWorkspaceHead = message.canonicalHead;
|
|
2967
|
+
sendWorkspaceReady();
|
|
2968
|
+
void convergeAvailableWorkspaceHead();
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
if (message.type === "workspace_publication_now") {
|
|
2972
|
+
armWorkspacePublication(message.trigger, { delayMs: 0, requestId: message.requestId });
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
if (message.type === "workspace_publication_authority") {
|
|
2976
|
+
const publication = pendingPublication;
|
|
2977
|
+
if (!publication || publication.requestId !== message.requestId) {
|
|
2978
|
+
process.stderr.write(`[r5d-worker] ignoring publication authority for unknown request ${message.requestId}
|
|
2979
|
+
`);
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
if (message.status !== "granted") {
|
|
2983
|
+
pendingPublication = void 0;
|
|
2984
|
+
workspaceConvergence.observeCanonicalHead(message.canonicalHead);
|
|
2985
|
+
const applyBlockedResponse = message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId);
|
|
2986
|
+
if (applyBlockedResponse) {
|
|
2987
|
+
workspaceConvergence.block();
|
|
2988
|
+
if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
|
|
2989
|
+
} else {
|
|
2990
|
+
workspaceConvergence.resetTransientPublicationState();
|
|
2991
|
+
workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
|
|
2992
|
+
if (message.status === "busy") {
|
|
2993
|
+
armWorkspacePublication(publication.trigger, {
|
|
2994
|
+
delayMs: message.retryAfterMs ?? 1e3,
|
|
2995
|
+
requestId: publication.requestId
|
|
2996
|
+
});
|
|
2997
|
+
} else if (!activeWorkspaceIncidentId) {
|
|
2998
|
+
const explicit = explicitWorkspacePublications.next();
|
|
2999
|
+
if (explicit) {
|
|
3000
|
+
armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
|
|
3001
|
+
} else if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
|
|
3002
|
+
armWorkspacePublication(publication.trigger, { delayMs: 0 });
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
sendWorkspaceReady();
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
publication.attemptId = message.attemptId;
|
|
3010
|
+
publication.authorityToken = message.authorityToken;
|
|
3011
|
+
publication.workInFlight = true;
|
|
3012
|
+
pendingPublication = publication;
|
|
3013
|
+
if (currentWorkerSocket !== ws) {
|
|
3014
|
+
publication.workInFlight = false;
|
|
3015
|
+
workspaceConvergence.resetTransientPublicationState();
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
workspaceConvergence.beginPreparing();
|
|
3019
|
+
sendWorkspaceReady();
|
|
3020
|
+
let prepareStartedAt = Date.now();
|
|
3021
|
+
let synchronizeStartedAt = prepareStartedAt;
|
|
3022
|
+
let synchronizeStarted = false;
|
|
3023
|
+
let result;
|
|
3024
|
+
try {
|
|
3025
|
+
result = await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
3026
|
+
prepareStartedAt = Date.now();
|
|
3027
|
+
const pendingTargets = [...pendingManifestCheckouts.values()];
|
|
3028
|
+
const createdCheckouts = ensureVisibleWorktrees(pendingTargets, { strictCanonicalRevalidation: true });
|
|
3029
|
+
synchronizeStartedAt = Date.now();
|
|
3030
|
+
synchronizeStarted = true;
|
|
3031
|
+
const prepared = await synchronizeWorkspace(
|
|
3032
|
+
workspaceSyncInput(publication.trigger, {
|
|
3033
|
+
attemptId: message.attemptId,
|
|
3034
|
+
quarantineRef: message.quarantineRef,
|
|
3035
|
+
expectedCanonicalHead: message.expectedHead,
|
|
3036
|
+
newVisibleCheckouts: [...createdCheckouts, ...pendingTargets]
|
|
3037
|
+
})
|
|
3038
|
+
);
|
|
3039
|
+
for (const target of pendingTargets) {
|
|
3040
|
+
pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
|
|
3041
|
+
}
|
|
3042
|
+
return prepared.outcome === "updated" ? { ...prepared, outcome: "no_change" } : prepared;
|
|
3043
|
+
});
|
|
3044
|
+
} catch (error) {
|
|
3045
|
+
if (!synchronizeStarted) synchronizeStartedAt = Date.now();
|
|
3046
|
+
result = {
|
|
3047
|
+
type: "workspace_sync",
|
|
3048
|
+
attemptId: message.attemptId,
|
|
3049
|
+
workerLabel: label,
|
|
3050
|
+
trigger: publication.trigger,
|
|
3051
|
+
outcome: "failed",
|
|
3052
|
+
startingHead: message.expectedHead,
|
|
3053
|
+
expectedHead: message.expectedHead,
|
|
3054
|
+
rebaseCount: 0,
|
|
3055
|
+
diffSizeBytes: 0,
|
|
3056
|
+
gitStatus: "",
|
|
3057
|
+
affectedProjects: [],
|
|
3058
|
+
affectedPaths: [],
|
|
3059
|
+
discardedPaths: [],
|
|
3060
|
+
localChangesDiscarded: false,
|
|
3061
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
const finishedAt = Date.now();
|
|
3065
|
+
result = {
|
|
3066
|
+
...result,
|
|
3067
|
+
dirtyGeneration: publication.dirtyGeneration,
|
|
3068
|
+
telemetry: workspacePublicationTelemetry({
|
|
3069
|
+
requestedAt: publication.requestedAt,
|
|
3070
|
+
prepareStartedAt,
|
|
3071
|
+
synchronizeStartedAt,
|
|
3072
|
+
finishedAt
|
|
3073
|
+
})
|
|
3074
|
+
};
|
|
3075
|
+
workspaceConvergence.beginSubmitting();
|
|
3076
|
+
sendWorkspaceReady();
|
|
3077
|
+
try {
|
|
3078
|
+
ws.send(
|
|
3079
|
+
JSON.stringify({
|
|
3080
|
+
type: "workspace_publication_result",
|
|
3081
|
+
requestId: publication.requestId,
|
|
3082
|
+
attemptId: message.attemptId,
|
|
3083
|
+
authorityToken: message.authorityToken,
|
|
3084
|
+
dirtyGeneration: publication.dirtyGeneration,
|
|
3085
|
+
result
|
|
3086
|
+
})
|
|
3087
|
+
);
|
|
3088
|
+
} catch (error) {
|
|
3089
|
+
workspaceConvergence.resetTransientPublicationState();
|
|
3090
|
+
throw error;
|
|
3091
|
+
} finally {
|
|
3092
|
+
publication.workInFlight = false;
|
|
3093
|
+
if (currentWorkerSocket !== ws) workspaceConvergence.resetTransientPublicationState();
|
|
3094
|
+
}
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
if (message.type === "workspace_publication_completed") {
|
|
3098
|
+
const publication = pendingPublication;
|
|
3099
|
+
if (!publication || publication.requestId !== message.requestId || publication.attemptId !== message.attemptId) {
|
|
3100
|
+
process.stderr.write(`[r5d-worker] ignoring completion for unknown publication ${message.requestId}
|
|
3101
|
+
`);
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
pendingPublication = void 0;
|
|
3105
|
+
workspaceConvergence.observeCanonicalHead(message.canonicalHead);
|
|
3106
|
+
if (message.status === "published" || message.status === "no_change") {
|
|
3107
|
+
workspaceConvergence.complete({
|
|
3108
|
+
generation: Math.min(message.publishedGeneration ?? publication.dirtyGeneration, publication.dirtyGeneration),
|
|
3109
|
+
canonicalHead: message.canonicalHead,
|
|
3110
|
+
published: true
|
|
3111
|
+
});
|
|
3112
|
+
} else if (message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId)) {
|
|
3113
|
+
workspaceConvergence.block();
|
|
3114
|
+
if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
|
|
3115
|
+
} else {
|
|
3116
|
+
workspaceConvergence.resetTransientPublicationState();
|
|
3117
|
+
workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
|
|
3118
|
+
}
|
|
3119
|
+
sendWorkspaceReady();
|
|
3120
|
+
const explicit = message.status === "retry" && publication.explicit ? { requestId: publication.requestId, trigger: publication.trigger } : explicitWorkspacePublications.next();
|
|
3121
|
+
if (explicit) {
|
|
3122
|
+
armWorkspacePublication(explicit.trigger, {
|
|
3123
|
+
delayMs: message.retryAfterMs ?? 0,
|
|
3124
|
+
requestId: explicit.requestId
|
|
3125
|
+
});
|
|
3126
|
+
} else if (message.status === "retry" || message.status === "failed" || workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
|
|
3127
|
+
armWorkspacePublication(publication.trigger, { delayMs: message.retryAfterMs ?? 1e3 });
|
|
3128
|
+
}
|
|
3129
|
+
if (message.canonicalHead !== workspaceConvergence.snapshot().localHead) {
|
|
3130
|
+
pendingWorkspaceHead = message.canonicalHead;
|
|
3131
|
+
void convergeAvailableWorkspaceHead();
|
|
3132
|
+
}
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
2811
3135
|
if (message.type === "sync_session_artifacts") {
|
|
2812
3136
|
sessionArtifactSyncRequestsInFlight += 1;
|
|
2813
3137
|
try {
|
|
@@ -2835,18 +3159,27 @@ async function startWorker(options) {
|
|
|
2835
3159
|
return;
|
|
2836
3160
|
}
|
|
2837
3161
|
if (message.type === "workspace_incident_updated") {
|
|
2838
|
-
|
|
3162
|
+
workspaceIncidentOrdering.observe(message);
|
|
2839
3163
|
const previousIncidentId = activeWorkspaceIncidentId;
|
|
2840
3164
|
activeWorkspaceIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
|
|
2841
|
-
previousPeriodicFingerprint = null;
|
|
2842
3165
|
const releasedHead = releasePendingWorkspaceHead(
|
|
2843
3166
|
previousIncidentId,
|
|
2844
3167
|
activeWorkspaceIncidentId,
|
|
2845
3168
|
message.status === "resolved" || message.status === "confirmed" || message.status === "reset" ? message.incidentId : null
|
|
2846
3169
|
);
|
|
2847
|
-
|
|
2848
|
-
|
|
3170
|
+
workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
|
|
3171
|
+
if (releasedHead.fetchAuthoritativeHead) {
|
|
3172
|
+
pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
|
|
3173
|
+
void convergeAvailableWorkspaceHead();
|
|
2849
3174
|
}
|
|
3175
|
+
const explicit = !activeWorkspaceIncidentId ? explicitWorkspacePublications.next() : void 0;
|
|
3176
|
+
if (explicit) {
|
|
3177
|
+
armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
|
|
3178
|
+
} else if (!activeWorkspaceIncidentId && workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
|
|
3179
|
+
armWorkspacePublication({ type: "periodic", detail: "resume after workspace incident" }, { delayMs: 0 });
|
|
3180
|
+
}
|
|
3181
|
+
if (!activeWorkspaceIncidentId) schedulePendingManifestCheckoutHydration();
|
|
3182
|
+
sendWorkspaceReady();
|
|
2850
3183
|
return;
|
|
2851
3184
|
}
|
|
2852
3185
|
if (message.type === "exec_terminal_ack") {
|
|
@@ -2882,7 +3215,6 @@ async function startWorker(options) {
|
|
|
2882
3215
|
return;
|
|
2883
3216
|
}
|
|
2884
3217
|
cliUpdateInProgress = true;
|
|
2885
|
-
recentWorkspaceSyncProof.invalidate();
|
|
2886
3218
|
try {
|
|
2887
3219
|
const result = await installCliUpdate(message);
|
|
2888
3220
|
sendWorkerMessage(ws, {
|
|
@@ -2945,9 +3277,8 @@ async function startWorker(options) {
|
|
|
2945
3277
|
});
|
|
2946
3278
|
return;
|
|
2947
3279
|
}
|
|
2948
|
-
recentWorkspaceSyncProof.invalidate();
|
|
2949
3280
|
const releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
|
|
2950
|
-
let
|
|
3281
|
+
let mutationLeaseTransferred = false;
|
|
2951
3282
|
try {
|
|
2952
3283
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
2953
3284
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
|
|
@@ -2957,9 +3288,14 @@ async function startWorker(options) {
|
|
|
2957
3288
|
message,
|
|
2958
3289
|
resolvedTarget,
|
|
2959
3290
|
planRoot,
|
|
2960
|
-
...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {}
|
|
3291
|
+
...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
|
|
3292
|
+
...targetMayMutateVisibleWorkspace(message.target) ? {
|
|
3293
|
+
onTerminal: () => {
|
|
3294
|
+
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true);
|
|
3295
|
+
}
|
|
3296
|
+
} : {}
|
|
2961
3297
|
});
|
|
2962
|
-
|
|
3298
|
+
mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
|
|
2963
3299
|
} catch (error) {
|
|
2964
3300
|
sendWorkerMessage(ws, {
|
|
2965
3301
|
type: "pty_error",
|
|
@@ -2968,12 +3304,15 @@ async function startWorker(options) {
|
|
|
2968
3304
|
error: error instanceof Error ? error.message : String(error)
|
|
2969
3305
|
});
|
|
2970
3306
|
} finally {
|
|
2971
|
-
if (!
|
|
3307
|
+
if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
|
|
2972
3308
|
}
|
|
2973
3309
|
return;
|
|
2974
3310
|
}
|
|
2975
3311
|
if (message.type === "pty_input") {
|
|
2976
|
-
|
|
3312
|
+
const activePty = activePtys.get(message.ptyId);
|
|
3313
|
+
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
3314
|
+
markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` });
|
|
3315
|
+
}
|
|
2977
3316
|
writePty(ws, message);
|
|
2978
3317
|
return;
|
|
2979
3318
|
}
|
|
@@ -2982,8 +3321,11 @@ async function startWorker(options) {
|
|
|
2982
3321
|
return;
|
|
2983
3322
|
}
|
|
2984
3323
|
if (message.type === "pty_close") {
|
|
2985
|
-
|
|
3324
|
+
const activePty = activePtys.get(message.ptyId);
|
|
2986
3325
|
closePty(message);
|
|
3326
|
+
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
3327
|
+
armWorkspacePublication({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, { delayMs: 0 });
|
|
3328
|
+
}
|
|
2987
3329
|
return;
|
|
2988
3330
|
}
|
|
2989
3331
|
if (message.type === "exec") {
|
|
@@ -2997,7 +3339,6 @@ async function startWorker(options) {
|
|
|
2997
3339
|
});
|
|
2998
3340
|
return;
|
|
2999
3341
|
}
|
|
3000
|
-
recentWorkspaceSyncProof.invalidate();
|
|
3001
3342
|
let result;
|
|
3002
3343
|
try {
|
|
3003
3344
|
const runCommand = async () => {
|
|
@@ -3018,6 +3359,12 @@ async function startWorker(options) {
|
|
|
3018
3359
|
};
|
|
3019
3360
|
}
|
|
3020
3361
|
ws.send(JSON.stringify(result));
|
|
3362
|
+
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
3363
|
+
markWorkspaceDirty(
|
|
3364
|
+
{ type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
|
|
3365
|
+
true
|
|
3366
|
+
);
|
|
3367
|
+
}
|
|
3021
3368
|
return;
|
|
3022
3369
|
}
|
|
3023
3370
|
if (message.type === "exec_start") {
|
|
@@ -3030,25 +3377,38 @@ async function startWorker(options) {
|
|
|
3030
3377
|
});
|
|
3031
3378
|
return;
|
|
3032
3379
|
}
|
|
3033
|
-
recentWorkspaceSyncProof.invalidate();
|
|
3034
3380
|
sendWorkerMessage(ws, {
|
|
3035
3381
|
type: "exec_accepted",
|
|
3036
3382
|
requestId: message.requestId,
|
|
3037
3383
|
runId: message.runId
|
|
3038
3384
|
});
|
|
3039
3385
|
const runCommand = async () => {
|
|
3040
|
-
|
|
3041
|
-
|
|
3386
|
+
try {
|
|
3387
|
+
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3388
|
+
process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
|
|
3042
3389
|
`);
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3390
|
+
await executeStreamingCommand({
|
|
3391
|
+
ws,
|
|
3392
|
+
message,
|
|
3393
|
+
resolvedTarget,
|
|
3394
|
+
baseUrl,
|
|
3395
|
+
token,
|
|
3396
|
+
artifactRoot,
|
|
3397
|
+
planRoot
|
|
3398
|
+
});
|
|
3399
|
+
} finally {
|
|
3400
|
+
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
3401
|
+
markWorkspaceDirty(
|
|
3402
|
+
{
|
|
3403
|
+
type: "process_terminal",
|
|
3404
|
+
sessionId: message.sessionId,
|
|
3405
|
+
processRunId: message.runId,
|
|
3406
|
+
detail: "process completed"
|
|
3407
|
+
},
|
|
3408
|
+
true
|
|
3409
|
+
);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3052
3412
|
};
|
|
3053
3413
|
const execution = runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
|
|
3054
3414
|
void execution.catch((error) => {
|
|
@@ -3062,9 +3422,8 @@ async function startWorker(options) {
|
|
|
3062
3422
|
return;
|
|
3063
3423
|
}
|
|
3064
3424
|
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
|
|
3065
|
-
if (message.type === "write" || message.type === "edit") recentWorkspaceSyncProof.invalidate();
|
|
3066
3425
|
try {
|
|
3067
|
-
const result = await workspaceSyncSingleFlight
|
|
3426
|
+
const result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
|
|
3068
3427
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3069
3428
|
return await executeOperation({
|
|
3070
3429
|
message,
|
|
@@ -3082,6 +3441,14 @@ async function startWorker(options) {
|
|
|
3082
3441
|
result
|
|
3083
3442
|
})
|
|
3084
3443
|
);
|
|
3444
|
+
if ((message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target)) {
|
|
3445
|
+
markWorkspaceDirty({
|
|
3446
|
+
type: message.type,
|
|
3447
|
+
sessionId: message.sessionId,
|
|
3448
|
+
toolCallId: message.requestId,
|
|
3449
|
+
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3085
3452
|
} catch (error) {
|
|
3086
3453
|
ws.send(
|
|
3087
3454
|
JSON.stringify({
|
|
@@ -3111,11 +3478,23 @@ async function startWorker(options) {
|
|
|
3111
3478
|
});
|
|
3112
3479
|
});
|
|
3113
3480
|
ws.addEventListener("close", (event) => {
|
|
3114
|
-
|
|
3481
|
+
process.off("SIGTERM", handleGracefulShutdown);
|
|
3482
|
+
process.off("SIGINT", handleGracefulShutdown);
|
|
3115
3483
|
stopHeartbeatWatchdog();
|
|
3116
|
-
if (
|
|
3117
|
-
|
|
3118
|
-
|
|
3484
|
+
if (workspacePublicationTimer) {
|
|
3485
|
+
clearTimeout(workspacePublicationTimer);
|
|
3486
|
+
workspacePublicationTimer = void 0;
|
|
3487
|
+
}
|
|
3488
|
+
if (!pendingPublication?.workInFlight) {
|
|
3489
|
+
workspaceConvergence.resetTransientPublicationState();
|
|
3490
|
+
}
|
|
3491
|
+
if (workspaceSafetyScan) {
|
|
3492
|
+
clearInterval(workspaceSafetyScan);
|
|
3493
|
+
workspaceSafetyScan = void 0;
|
|
3494
|
+
}
|
|
3495
|
+
if (workspaceManifestHydrationTimer) {
|
|
3496
|
+
clearTimeout(workspaceManifestHydrationTimer);
|
|
3497
|
+
workspaceManifestHydrationTimer = void 0;
|
|
3119
3498
|
}
|
|
3120
3499
|
if (terminalReplayTimer) {
|
|
3121
3500
|
clearInterval(terminalReplayTimer);
|
|
@@ -3131,6 +3510,9 @@ async function startWorker(options) {
|
|
|
3131
3510
|
if (reloadAfterClose) {
|
|
3132
3511
|
process.exit(WORKER_RELOAD_EXIT_CODE);
|
|
3133
3512
|
}
|
|
3513
|
+
if (shutdownAfterClose) {
|
|
3514
|
+
process.exit(0);
|
|
3515
|
+
}
|
|
3134
3516
|
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
3135
3517
|
process.stderr.write(
|
|
3136
3518
|
`[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms
|