@ricsam/r5d-worker 0.0.101 → 0.0.103
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 +16 -3
- package/dist/cjs/main.cjs +390 -322
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-mirror-fetch-policy.cjs +48 -7
- package/dist/cjs/project-mirror-refs-token.cjs +36 -2
- package/dist/cjs/project-worktrees.cjs +49 -32
- package/dist/cjs/workspace-git-sync.cjs +45 -6
- package/dist/cjs/workspace-mutation-gate.cjs +35 -0
- package/dist/cjs/workspace-projection-ledger.cjs +81 -0
- package/dist/cjs/workspace-sync-coalescer.cjs +58 -0
- package/dist/mjs/main.mjs +395 -324
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-mirror-fetch-policy.mjs +46 -6
- package/dist/mjs/project-mirror-refs-token.mjs +30 -1
- package/dist/mjs/project-worktrees.mjs +52 -33
- package/dist/mjs/workspace-git-sync.mjs +45 -6
- package/dist/mjs/workspace-mutation-gate.mjs +35 -0
- package/dist/mjs/workspace-projection-ledger.mjs +57 -0
- package/dist/mjs/workspace-sync-coalescer.mjs +34 -0
- package/dist/types/main.d.ts +29 -17
- package/dist/types/project-mirror-fetch-policy.d.ts +70 -15
- package/dist/types/project-mirror-refs-token.d.ts +34 -0
- package/dist/types/project-worktrees.d.ts +26 -8
- package/dist/types/workspace-git-sync.d.ts +24 -2
- package/dist/types/workspace-mutation-gate.d.ts +24 -3
- package/dist/types/workspace-projection-ledger.d.ts +59 -0
- package/dist/types/workspace-sync-coalescer.d.ts +28 -0
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
runWorkspaceCommand
|
|
35
35
|
} from "./workspace-command-sync-policy.mjs";
|
|
36
36
|
import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
|
|
37
|
+
import { WorkspaceProjectionLedger } from "./workspace-projection-ledger.mjs";
|
|
38
|
+
import { WorkspaceSyncCoalescer } from "./workspace-sync-coalescer.mjs";
|
|
37
39
|
import { checkoutPathMovePublicationComplete, preserveProjectCheckoutPathMove } from "./workspace-path-move.mjs";
|
|
38
40
|
import { busyProjectConfigurationChangeIds, deferredProjectConfigurationPendingBranches } from "./workspace-project-config-policy.mjs";
|
|
39
41
|
import { creatorLocalProjectBranchIsAuthorized, workerProjectBranchDisposition } from "./workspace-preserve-only-policy.mjs";
|
|
@@ -58,7 +60,7 @@ import { assertDisjointManagedRoots, managedProjectRoot, validateManagedBranchNa
|
|
|
58
60
|
import {
|
|
59
61
|
applyObservedProjectMirrorHeads,
|
|
60
62
|
assertProjectMirrorHeadsPushed,
|
|
61
|
-
|
|
63
|
+
computeLocalProjectMirrorRefsTokens,
|
|
62
64
|
createOrRetryLinkedProjectBranch,
|
|
63
65
|
deleteLinkedProjectBranch,
|
|
64
66
|
deleteProjectMirrorBranch,
|
|
@@ -71,7 +73,10 @@ import {
|
|
|
71
73
|
pushProjectMirrorHeads,
|
|
72
74
|
removeProjectWorktrees
|
|
73
75
|
} from "./project-worktrees.mjs";
|
|
74
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
parseProjectMirrorRefsTokensMessage,
|
|
78
|
+
planProjectMirrorObservationFetch
|
|
79
|
+
} from "./project-mirror-fetch-policy.mjs";
|
|
75
80
|
import {
|
|
76
81
|
PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
|
|
77
82
|
recoverProjectSnapshotsInChild,
|
|
@@ -182,6 +187,12 @@ const workspaceSyncSingleFlight = {
|
|
|
182
187
|
acquireMutation() {
|
|
183
188
|
return workspaceMutationGate.acquireMutation();
|
|
184
189
|
},
|
|
190
|
+
runBranchMutation(operation) {
|
|
191
|
+
return workspaceMutationGate.runBranchMutation(operation);
|
|
192
|
+
},
|
|
193
|
+
branchMutationQueuePosition() {
|
|
194
|
+
return workspaceMutationGate.branchMutationQueuePosition();
|
|
195
|
+
},
|
|
185
196
|
afterCurrent() {
|
|
186
197
|
return workspaceSyncQueue;
|
|
187
198
|
}
|
|
@@ -312,6 +323,7 @@ const workerPtyTestHarness = {
|
|
|
312
323
|
removeEnvFiles: removePtyEnvFiles,
|
|
313
324
|
resolveEnvFileReferences: resolvePtyEnvFileReferences,
|
|
314
325
|
commandHasWorkspaceEffect: workerCommandHasWorkspaceEffect,
|
|
326
|
+
targetProcessEnv,
|
|
315
327
|
canonicalSyncTerminalHead,
|
|
316
328
|
ptyIsWorkspaceBusy: workerPtyIsWorkspaceBusy,
|
|
317
329
|
parseLinuxForegroundBusy: parseLinuxPtyForegroundBusy
|
|
@@ -319,21 +331,12 @@ const workerPtyTestHarness = {
|
|
|
319
331
|
function defaultConfigPath() {
|
|
320
332
|
return path.join(os.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
321
333
|
}
|
|
322
|
-
function
|
|
323
|
-
return path.join(os.homedir(), ".r5d"
|
|
324
|
-
}
|
|
325
|
-
function defaultSyncRoot() {
|
|
326
|
-
return path.join(os.homedir(), ".r5d", "sync");
|
|
327
|
-
}
|
|
328
|
-
function defaultArtifactRoot() {
|
|
329
|
-
return path.join(os.homedir(), ".r5d", "artifacts");
|
|
330
|
-
}
|
|
331
|
-
function defaultPlanRoot() {
|
|
332
|
-
return path.join(os.homedir(), ".r5d", "plans");
|
|
334
|
+
function defaultRootDir() {
|
|
335
|
+
return path.join(os.homedir(), ".r5d");
|
|
333
336
|
}
|
|
334
337
|
function printHelp() {
|
|
335
338
|
process.stdout.write(`Usage:
|
|
336
|
-
r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
|
|
339
|
+
r5d-worker start --label <label> [--root <dir>] [--base-url <url>] [--token <token>] [--api-key <key>]
|
|
337
340
|
r5d-worker --version
|
|
338
341
|
|
|
339
342
|
Options:
|
|
@@ -342,10 +345,7 @@ Options:
|
|
|
342
345
|
--token <token> r5d worker token
|
|
343
346
|
--api-key <key> r5d API key
|
|
344
347
|
--config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
|
|
345
|
-
--
|
|
346
|
-
--sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
|
|
347
|
-
--artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
|
|
348
|
-
--plan-root <dir> Override synced plan root (default from R5D_PLANS_ROOT or ~/.r5d/plans)
|
|
348
|
+
--root <dir> Override worker data root (default from R5D_ROOT or ~/.r5d)
|
|
349
349
|
-v, --version Show r5d-worker version
|
|
350
350
|
-h, --help Show this help
|
|
351
351
|
`);
|
|
@@ -460,24 +460,9 @@ function parseArgs(argv) {
|
|
|
460
460
|
options.label = label;
|
|
461
461
|
continue;
|
|
462
462
|
}
|
|
463
|
-
const
|
|
464
|
-
if (
|
|
465
|
-
options.
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
const syncRoot = readOption("--sync-root");
|
|
469
|
-
if (syncRoot !== void 0) {
|
|
470
|
-
options.syncRoot = syncRoot;
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
const artifactRoot = readOption("--artifact-root");
|
|
474
|
-
if (artifactRoot !== void 0) {
|
|
475
|
-
options.artifactRoot = artifactRoot;
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
const planRoot = readOption("--plan-root");
|
|
479
|
-
if (planRoot !== void 0) {
|
|
480
|
-
options.planRoot = planRoot;
|
|
463
|
+
const rootDir = readOption("--root");
|
|
464
|
+
if (rootDir !== void 0) {
|
|
465
|
+
options.rootDir = rootDir;
|
|
481
466
|
continue;
|
|
482
467
|
}
|
|
483
468
|
rest.push(arg);
|
|
@@ -522,11 +507,9 @@ async function readResponseText(response) {
|
|
|
522
507
|
return "";
|
|
523
508
|
}
|
|
524
509
|
}
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
|
|
529
|
-
const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
|
|
510
|
+
const R5D_ROOT_REF = "$R5D_ROOT";
|
|
511
|
+
const R5D_ROOT_ENV = "R5D_ROOT";
|
|
512
|
+
const BUILT_IN_TOOL_PATH_REFS = [R5D_ROOT_REF];
|
|
530
513
|
function parseBuiltInToolPath(inputPath) {
|
|
531
514
|
const normalized = inputPath.replace(/\\/g, "/");
|
|
532
515
|
const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
|
|
@@ -542,6 +525,26 @@ function parseBuiltInToolPath(inputPath) {
|
|
|
542
525
|
relativePath: match[3] ?? ""
|
|
543
526
|
};
|
|
544
527
|
}
|
|
528
|
+
function expandRootPathSegments(relativePath, builtInPaths) {
|
|
529
|
+
const values = {
|
|
530
|
+
R5D_SESSION_ID: builtInPaths.sessionId,
|
|
531
|
+
R5D_PROJECT_ID: builtInPaths.projectId,
|
|
532
|
+
R5D_BRANCH_NAME: builtInPaths.branchName,
|
|
533
|
+
R5D_ACTIVE_PLAN_ID: builtInPaths.activePlanId
|
|
534
|
+
};
|
|
535
|
+
let expandedPath = relativePath;
|
|
536
|
+
for (const [variableName, value] of Object.entries(values)) {
|
|
537
|
+
const variablePattern = new RegExp(`\\$(?:\\{${variableName}\\}|${variableName})(?=\\/|\\.|$)`, "g");
|
|
538
|
+
if (!variablePattern.test(expandedPath)) continue;
|
|
539
|
+
if (!value) throw new Error(`$${variableName} is unavailable for this worker path`);
|
|
540
|
+
expandedPath = expandedPath.replace(variablePattern, value);
|
|
541
|
+
}
|
|
542
|
+
const unsupportedVariable = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/.exec(expandedPath);
|
|
543
|
+
if (unsupportedVariable) {
|
|
544
|
+
throw new Error(`Unsupported ${R5D_ROOT_REF} path variable "$${unsupportedVariable[1] ?? unsupportedVariable[2]}"`);
|
|
545
|
+
}
|
|
546
|
+
return expandedPath.split("/").filter(Boolean);
|
|
547
|
+
}
|
|
545
548
|
function validateArtifactSessionId(sessionId) {
|
|
546
549
|
if (!/^(migration-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
547
550
|
throw new Error(`Invalid artifact session id: ${sessionId}`);
|
|
@@ -557,14 +560,6 @@ function sessionArtifactDir(artifactRoot, sessionId) {
|
|
|
557
560
|
validateArtifactSessionId(sessionId);
|
|
558
561
|
return path.join(artifactRoot, sessionId);
|
|
559
562
|
}
|
|
560
|
-
function artifactEnv(artifactRoot, sessionId) {
|
|
561
|
-
if (!sessionId) {
|
|
562
|
-
return {};
|
|
563
|
-
}
|
|
564
|
-
return {
|
|
565
|
-
R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
563
|
function projectPlanDir(planRoot, projectId, branchName) {
|
|
569
564
|
validateProjectId(projectId);
|
|
570
565
|
validateBranchName(branchName);
|
|
@@ -577,21 +572,18 @@ function targetPlanDir(planRoot, target) {
|
|
|
577
572
|
}
|
|
578
573
|
return path.join(planRoot, "workspace");
|
|
579
574
|
}
|
|
580
|
-
function planEnv(
|
|
581
|
-
const
|
|
582
|
-
const env = {
|
|
583
|
-
[R5D_PLANS_DIR_ENV]: plansDir
|
|
584
|
-
};
|
|
575
|
+
function planEnv(activePlanId) {
|
|
576
|
+
const env = {};
|
|
585
577
|
if (activePlanId) {
|
|
586
578
|
validatePlanId(activePlanId);
|
|
587
579
|
env.R5D_ACTIVE_PLAN_ID = activePlanId;
|
|
588
|
-
env.R5D_ACTIVE_PLAN_FILE = path.join(plansDir, `${activePlanId}.plan.md`);
|
|
589
580
|
}
|
|
590
581
|
return env;
|
|
591
582
|
}
|
|
592
583
|
function isArtifactEnvPath(filePath) {
|
|
593
584
|
try {
|
|
594
|
-
|
|
585
|
+
const parsed = parseBuiltInToolPath(filePath);
|
|
586
|
+
return parsed?.ref === R5D_ROOT_REF && parsed.relativePath.split("/").filter(Boolean)[0] === "artifacts";
|
|
595
587
|
} catch {
|
|
596
588
|
return false;
|
|
597
589
|
}
|
|
@@ -733,48 +725,46 @@ async function prepareBuiltInToolPaths(input) {
|
|
|
733
725
|
if (!parsed) {
|
|
734
726
|
return void 0;
|
|
735
727
|
}
|
|
736
|
-
|
|
728
|
+
const builtInPaths = {
|
|
729
|
+
rootDir: input.rootDir,
|
|
730
|
+
sessionId: input.sessionId,
|
|
731
|
+
activePlanId: input.activePlanId,
|
|
732
|
+
...input.target.type === "project" ? { projectId: input.target.projectId, branchName: input.target.branchName } : {}
|
|
733
|
+
};
|
|
734
|
+
const rootPathSegments = expandRootPathSegments(parsed.relativePath, builtInPaths);
|
|
735
|
+
if (rootPathSegments[0] === "artifacts") {
|
|
737
736
|
if (input.access === "write") {
|
|
738
737
|
throw new Error(
|
|
739
|
-
`${
|
|
738
|
+
`${R5D_ROOT_REF}/artifacts attachments are read-only. Copy them into a project path before editing or creating files.`
|
|
740
739
|
);
|
|
741
740
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
})
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
const plansDir = targetPlanDir(input.planRoot, input.target);
|
|
755
|
-
fs.mkdirSync(plansDir, { recursive: true });
|
|
756
|
-
if (parsed.ref === R5D_PLANS_DIR_REF) {
|
|
757
|
-
return { plansDir };
|
|
741
|
+
const referencedSessionId = rootPathSegments[1];
|
|
742
|
+
if (!referencedSessionId) return builtInPaths;
|
|
743
|
+
validateArtifactSessionId(referencedSessionId);
|
|
744
|
+
await syncSessionArtifacts({
|
|
745
|
+
baseUrl: input.baseUrl,
|
|
746
|
+
token: input.token,
|
|
747
|
+
sessionId: referencedSessionId,
|
|
748
|
+
artifactRoot: path.join(input.rootDir, "artifacts")
|
|
749
|
+
});
|
|
758
750
|
}
|
|
759
|
-
if (
|
|
760
|
-
|
|
751
|
+
if (rootPathSegments[0] === "plans") {
|
|
752
|
+
fs.mkdirSync(targetPlanDir(path.join(input.rootDir, "plans"), input.target), { recursive: true });
|
|
761
753
|
}
|
|
762
|
-
|
|
763
|
-
return {
|
|
764
|
-
plansDir,
|
|
765
|
-
activePlanFile: path.join(plansDir, `${input.activePlanId}.plan.md`)
|
|
766
|
-
};
|
|
754
|
+
return builtInPaths;
|
|
767
755
|
}
|
|
768
756
|
async function prepareArtifactEnvForShell(input) {
|
|
757
|
+
const env = { [R5D_ROOT_ENV]: input.rootDir };
|
|
769
758
|
if (!input.sessionId) {
|
|
770
|
-
return
|
|
759
|
+
return env;
|
|
771
760
|
}
|
|
761
|
+
const artifactRoot = path.join(input.rootDir, "artifacts");
|
|
772
762
|
try {
|
|
773
763
|
await syncSessionArtifacts({
|
|
774
764
|
baseUrl: input.baseUrl,
|
|
775
765
|
token: input.token,
|
|
776
766
|
sessionId: input.sessionId,
|
|
777
|
-
artifactRoot
|
|
767
|
+
artifactRoot
|
|
778
768
|
});
|
|
779
769
|
} catch (error) {
|
|
780
770
|
process.stderr.write(
|
|
@@ -782,16 +772,14 @@ async function prepareArtifactEnvForShell(input) {
|
|
|
782
772
|
`
|
|
783
773
|
);
|
|
784
774
|
}
|
|
785
|
-
const artifactsDir = sessionArtifactDir(
|
|
775
|
+
const artifactsDir = sessionArtifactDir(artifactRoot, input.sessionId);
|
|
786
776
|
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
787
|
-
return
|
|
788
|
-
R5D_ARTIFACTS_DIR: artifactsDir
|
|
789
|
-
};
|
|
777
|
+
return env;
|
|
790
778
|
}
|
|
791
779
|
function preparePlanEnvForShell(input) {
|
|
792
|
-
const plansDir = targetPlanDir(input.
|
|
780
|
+
const plansDir = targetPlanDir(path.join(input.rootDir, "plans"), input.target);
|
|
793
781
|
fs.mkdirSync(plansDir, { recursive: true });
|
|
794
|
-
return planEnv(input.
|
|
782
|
+
return planEnv(input.activePlanId);
|
|
795
783
|
}
|
|
796
784
|
async function verifyR5dctlAuth(baseUrl, token, workerLabel) {
|
|
797
785
|
const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
|
|
@@ -959,30 +947,17 @@ function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
|
|
|
959
947
|
if (parsed.relativePath.split("/").includes("..")) {
|
|
960
948
|
throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
|
|
961
949
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
rootPath = builtInPaths?.plansDir;
|
|
968
|
-
} else {
|
|
969
|
-
if (parsed.relativePath) {
|
|
970
|
-
throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
|
|
971
|
-
}
|
|
972
|
-
rootPath = builtInPaths?.plansDir;
|
|
973
|
-
absolutePath = builtInPaths?.activePlanFile;
|
|
974
|
-
}
|
|
975
|
-
if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
|
|
976
|
-
const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
|
|
977
|
-
throw new Error(`${parsed.ref} paths require ${requirement}`);
|
|
978
|
-
}
|
|
979
|
-
const normalizedPath = parsed.relativePath ? path.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
|
|
950
|
+
const rootPath = builtInPaths?.rootDir;
|
|
951
|
+
if (!rootPath) throw new Error(`${R5D_ROOT_REF} paths require an available worker root`);
|
|
952
|
+
const expandedPath = expandRootPathSegments(parsed.relativePath, builtInPaths);
|
|
953
|
+
if (expandedPath.includes("..")) throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
|
|
954
|
+
const normalizedPath = expandedPath.length > 0 ? path.posix.normalize(expandedPath.join("/")).replace(/^\/+|\/+$/g, "") : "";
|
|
980
955
|
const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
|
|
981
|
-
absolutePath
|
|
956
|
+
const absolutePath = normalizedRelativePath ? path.resolve(rootPath, ...normalizedRelativePath.split("/")) : path.resolve(rootPath);
|
|
982
957
|
assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
|
|
983
958
|
return {
|
|
984
959
|
absolutePath,
|
|
985
|
-
displayPath:
|
|
960
|
+
displayPath: parsed.relativePath ? `${parsed.ref}/${parsed.relativePath}` : parsed.ref,
|
|
986
961
|
repoRelativePath: null,
|
|
987
962
|
scope: "virtual",
|
|
988
963
|
virtualRootPath: path.resolve(rootPath)
|
|
@@ -2226,8 +2201,7 @@ async function executeReadFileOperation(input) {
|
|
|
2226
2201
|
token: input.token,
|
|
2227
2202
|
sessionId: input.message.sessionId,
|
|
2228
2203
|
activePlanId: input.message.activePlanId,
|
|
2229
|
-
|
|
2230
|
-
planRoot: input.planRoot,
|
|
2204
|
+
rootDir: input.rootDir,
|
|
2231
2205
|
access: "read"
|
|
2232
2206
|
});
|
|
2233
2207
|
return readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
|
|
@@ -2240,8 +2214,7 @@ async function executeWriteFileOperation(input) {
|
|
|
2240
2214
|
token: input.token,
|
|
2241
2215
|
sessionId: input.message.sessionId,
|
|
2242
2216
|
activePlanId: input.message.activePlanId,
|
|
2243
|
-
|
|
2244
|
-
planRoot: input.planRoot,
|
|
2217
|
+
rootDir: input.rootDir,
|
|
2245
2218
|
access: "write"
|
|
2246
2219
|
});
|
|
2247
2220
|
input.assertAdmission();
|
|
@@ -2259,8 +2232,7 @@ async function executeEditFileOperation(input) {
|
|
|
2259
2232
|
token: input.token,
|
|
2260
2233
|
sessionId: input.message.sessionId,
|
|
2261
2234
|
activePlanId: input.message.activePlanId,
|
|
2262
|
-
|
|
2263
|
-
planRoot: input.planRoot,
|
|
2235
|
+
rootDir: input.rootDir,
|
|
2264
2236
|
access: "write"
|
|
2265
2237
|
});
|
|
2266
2238
|
input.assertAdmission();
|
|
@@ -2387,8 +2359,7 @@ async function executeGrepOperation(input) {
|
|
|
2387
2359
|
token: input.token,
|
|
2388
2360
|
sessionId: input.message.sessionId,
|
|
2389
2361
|
activePlanId: input.message.activePlanId,
|
|
2390
|
-
|
|
2391
|
-
planRoot: input.planRoot,
|
|
2362
|
+
rootDir: input.rootDir,
|
|
2392
2363
|
access: "read"
|
|
2393
2364
|
});
|
|
2394
2365
|
return grepWorkerFiles(
|
|
@@ -2446,8 +2417,7 @@ async function executeFindOperation(input) {
|
|
|
2446
2417
|
token: input.token,
|
|
2447
2418
|
sessionId: input.message.sessionId,
|
|
2448
2419
|
activePlanId: input.message.activePlanId,
|
|
2449
|
-
|
|
2450
|
-
planRoot: input.planRoot,
|
|
2420
|
+
rootDir: input.rootDir,
|
|
2451
2421
|
access: "read"
|
|
2452
2422
|
});
|
|
2453
2423
|
return findWorkerFiles(
|
|
@@ -2488,8 +2458,7 @@ async function executeLsOperation(input) {
|
|
|
2488
2458
|
token: input.token,
|
|
2489
2459
|
sessionId: input.message.sessionId,
|
|
2490
2460
|
activePlanId: input.message.activePlanId,
|
|
2491
|
-
|
|
2492
|
-
planRoot: input.planRoot,
|
|
2461
|
+
rootDir: input.rootDir,
|
|
2493
2462
|
access: "read"
|
|
2494
2463
|
});
|
|
2495
2464
|
return listWorkerDirectory(input.resolvedTarget.rootPath, input.message.path, input.message.limit, builtInPaths);
|
|
@@ -2595,8 +2564,7 @@ async function executeViewFileBytesOperation(input) {
|
|
|
2595
2564
|
token: input.token,
|
|
2596
2565
|
sessionId: input.message.sessionId,
|
|
2597
2566
|
activePlanId: input.message.activePlanId,
|
|
2598
|
-
|
|
2599
|
-
planRoot: input.planRoot,
|
|
2567
|
+
rootDir: input.rootDir,
|
|
2600
2568
|
access: "read"
|
|
2601
2569
|
});
|
|
2602
2570
|
return readWorkerImageFile(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
|
|
@@ -2636,27 +2604,30 @@ function targetIdentityEnv(target) {
|
|
|
2636
2604
|
R5D_BRANCH_NAME: ""
|
|
2637
2605
|
};
|
|
2638
2606
|
}
|
|
2607
|
+
function targetProcessEnv(rootDir, target) {
|
|
2608
|
+
return { [R5D_ROOT_ENV]: rootDir, ...targetIdentityEnv(target) };
|
|
2609
|
+
}
|
|
2639
2610
|
async function prepareShellEnvForTarget(input) {
|
|
2640
2611
|
const artifactProcessEnv = await prepareArtifactEnvForShell({
|
|
2641
2612
|
baseUrl: input.baseUrl,
|
|
2642
2613
|
token: input.token,
|
|
2643
2614
|
sessionId: input.sessionId,
|
|
2644
|
-
|
|
2615
|
+
rootDir: input.rootDir
|
|
2645
2616
|
});
|
|
2646
2617
|
if (input.target.type === "workspace" && input.target.rootProfile === "canonical_sync") {
|
|
2647
2618
|
return {
|
|
2648
2619
|
...artifactProcessEnv,
|
|
2649
|
-
...
|
|
2620
|
+
...targetProcessEnv(input.rootDir, input.target)
|
|
2650
2621
|
};
|
|
2651
2622
|
}
|
|
2652
2623
|
return {
|
|
2653
2624
|
...artifactProcessEnv,
|
|
2654
2625
|
...preparePlanEnvForShell({
|
|
2655
2626
|
target: input.target,
|
|
2656
|
-
|
|
2627
|
+
rootDir: input.rootDir,
|
|
2657
2628
|
activePlanId: input.activePlanId
|
|
2658
2629
|
}),
|
|
2659
|
-
...
|
|
2630
|
+
...targetProcessEnv(input.rootDir, input.target)
|
|
2660
2631
|
};
|
|
2661
2632
|
}
|
|
2662
2633
|
async function executeCommand(input) {
|
|
@@ -2671,13 +2642,12 @@ async function executeCommand(input) {
|
|
|
2671
2642
|
};
|
|
2672
2643
|
input.setPreparationHeartbeatLease(true);
|
|
2673
2644
|
try {
|
|
2674
|
-
const
|
|
2645
|
+
const targetProcessEnv2 = await prepareShellEnvForTarget({
|
|
2675
2646
|
target: input.resolvedTarget.target,
|
|
2676
2647
|
baseUrl: input.baseUrl,
|
|
2677
2648
|
token: input.token,
|
|
2678
2649
|
sessionId: input.message.sessionId,
|
|
2679
|
-
|
|
2680
|
-
planRoot: input.planRoot,
|
|
2650
|
+
rootDir: input.rootDir,
|
|
2681
2651
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
2682
2652
|
});
|
|
2683
2653
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
@@ -2687,7 +2657,7 @@ async function executeCommand(input) {
|
|
|
2687
2657
|
stdout: "pipe",
|
|
2688
2658
|
stderr: "pipe",
|
|
2689
2659
|
detached: true,
|
|
2690
|
-
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {},
|
|
2660
|
+
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2])
|
|
2691
2661
|
});
|
|
2692
2662
|
spawnedProcess = subprocess;
|
|
2693
2663
|
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
@@ -2776,13 +2746,12 @@ async function executeStreamingCommand(input) {
|
|
|
2776
2746
|
});
|
|
2777
2747
|
return;
|
|
2778
2748
|
}
|
|
2779
|
-
const
|
|
2749
|
+
const targetProcessEnv2 = await prepareShellEnvForTarget({
|
|
2780
2750
|
target: input.resolvedTarget.target,
|
|
2781
2751
|
baseUrl: input.baseUrl,
|
|
2782
2752
|
token: input.token,
|
|
2783
2753
|
sessionId: input.message.sessionId,
|
|
2784
|
-
|
|
2785
|
-
planRoot: input.planRoot,
|
|
2754
|
+
rootDir: input.rootDir,
|
|
2786
2755
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
2787
2756
|
});
|
|
2788
2757
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
@@ -2796,7 +2765,7 @@ async function executeStreamingCommand(input) {
|
|
|
2796
2765
|
stdout: "pipe",
|
|
2797
2766
|
stderr: "pipe",
|
|
2798
2767
|
detached: true,
|
|
2799
|
-
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {},
|
|
2768
|
+
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2])
|
|
2800
2769
|
});
|
|
2801
2770
|
spawnedProcess = subprocess;
|
|
2802
2771
|
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
@@ -3260,10 +3229,10 @@ async function openPty(input) {
|
|
|
3260
3229
|
}
|
|
3261
3230
|
const planProcessEnv = !(input.resolvedTarget.target.type === "workspace" && input.resolvedTarget.target.rootProfile === "canonical_sync") ? preparePlanEnvForShell({
|
|
3262
3231
|
target: input.resolvedTarget.target,
|
|
3263
|
-
|
|
3232
|
+
rootDir: input.rootDir,
|
|
3264
3233
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
3265
3234
|
}) : {};
|
|
3266
|
-
const
|
|
3235
|
+
const targetEnv = targetProcessEnv(input.rootDir, input.resolvedTarget.target);
|
|
3267
3236
|
const shell = resolveHostShell(input.message.command);
|
|
3268
3237
|
const outputCoalescer = createPtyOutputCoalescer({
|
|
3269
3238
|
send: (data) => {
|
|
@@ -3295,7 +3264,7 @@ async function openPty(input) {
|
|
|
3295
3264
|
githubProcessEnv(),
|
|
3296
3265
|
resolvePtyEnvFileReferences(input.message.env ?? {}, stagedEnvFiles),
|
|
3297
3266
|
planProcessEnv,
|
|
3298
|
-
|
|
3267
|
+
targetEnv,
|
|
3299
3268
|
shell.env ?? {}
|
|
3300
3269
|
])
|
|
3301
3270
|
},
|
|
@@ -3468,12 +3437,13 @@ async function startWorker(options) {
|
|
|
3468
3437
|
initializeCredentialStoreDirectoryOnce();
|
|
3469
3438
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
3470
3439
|
validateLabel(label);
|
|
3471
|
-
const
|
|
3472
|
-
const
|
|
3440
|
+
const rootDir = path.resolve(options.rootDir ?? process.env.R5D_ROOT ?? defaultRootDir());
|
|
3441
|
+
const projectsRoot = path.join(rootDir, "projects");
|
|
3442
|
+
const syncRoot = path.join(rootDir, "sync");
|
|
3473
3443
|
const workspaceShadowRoot = path.join(syncRoot, "workspace");
|
|
3474
3444
|
const projectWorktreeSnapshotsRoot = path.join(syncRoot, "project-worktree-snapshots");
|
|
3475
|
-
const artifactRoot = path.
|
|
3476
|
-
const planRoot = path.
|
|
3445
|
+
const artifactRoot = path.join(rootDir, "artifacts");
|
|
3446
|
+
const planRoot = path.join(rootDir, "plans");
|
|
3477
3447
|
assertDisjointManagedRoots([
|
|
3478
3448
|
{ label: "projects root", rootPath: projectsRoot },
|
|
3479
3449
|
{ label: "sync root", rootPath: syncRoot },
|
|
@@ -3482,13 +3452,7 @@ async function startWorker(options) {
|
|
|
3482
3452
|
]);
|
|
3483
3453
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
3484
3454
|
`);
|
|
3485
|
-
process.stdout.write(`[r5d-worker]
|
|
3486
|
-
`);
|
|
3487
|
-
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
3488
|
-
`);
|
|
3489
|
-
process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
|
|
3490
|
-
`);
|
|
3491
|
-
process.stdout.write(`[r5d-worker] plan root: ${planRoot}
|
|
3455
|
+
process.stdout.write(`[r5d-worker] root: ${rootDir}
|
|
3492
3456
|
`);
|
|
3493
3457
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
3494
3458
|
`);
|
|
@@ -3513,9 +3477,29 @@ async function startWorker(options) {
|
|
|
3513
3477
|
const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
|
|
3514
3478
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
3515
3479
|
const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
|
|
3480
|
+
const advertisedProjectMirrorMountTokens = /* @__PURE__ */ new Map();
|
|
3481
|
+
let advertisedWorkspaceHead;
|
|
3516
3482
|
const fetchedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
|
|
3517
|
-
const
|
|
3483
|
+
const fetchedProjectMirrorMountTokens = /* @__PURE__ */ new Map();
|
|
3518
3484
|
const projectMirrorFetchSkippingEnabled = process.env.R5D_PROJECT_MIRROR_FETCH_SKIP !== "0";
|
|
3485
|
+
const forgetLocalProjectMirrorRefsTokens = (projectId, branchName) => {
|
|
3486
|
+
fetchedProjectMirrorRefsTokens.delete(projectId);
|
|
3487
|
+
if (branchName === void 0) fetchedProjectMirrorMountTokens.delete(projectId);
|
|
3488
|
+
else fetchedProjectMirrorMountTokens.get(projectId)?.delete(branchName);
|
|
3489
|
+
};
|
|
3490
|
+
const recordLocalProjectMirrorRefsTokens = async (project) => {
|
|
3491
|
+
try {
|
|
3492
|
+
const tokens = await computeLocalProjectMirrorRefsTokens({
|
|
3493
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
3494
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
3495
|
+
mounts: project.branches.map(({ branchId, branchName }) => ({ branchId, branchName }))
|
|
3496
|
+
});
|
|
3497
|
+
fetchedProjectMirrorRefsTokens.set(project.projectId, tokens.project);
|
|
3498
|
+
fetchedProjectMirrorMountTokens.set(project.projectId, tokens.mounts);
|
|
3499
|
+
} catch {
|
|
3500
|
+
forgetLocalProjectMirrorRefsTokens(project.projectId);
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3519
3503
|
const pendingMirrorDeletes = /* @__PURE__ */ new Map();
|
|
3520
3504
|
let workspaceRemoteUrl = null;
|
|
3521
3505
|
let workspaceCredentialHelper = null;
|
|
@@ -3530,6 +3514,7 @@ async function startWorker(options) {
|
|
|
3530
3514
|
let workspacePeriodicTimer;
|
|
3531
3515
|
let pendingAutomaticTrigger;
|
|
3532
3516
|
const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
|
|
3517
|
+
const workspaceProjectionLedger = new WorkspaceProjectionLedger();
|
|
3533
3518
|
let automaticSyncInFlight = false;
|
|
3534
3519
|
let terminalReplayTimer;
|
|
3535
3520
|
let workspaceSyncRequestsInFlight = 0;
|
|
@@ -3899,7 +3884,7 @@ async function startWorker(options) {
|
|
|
3899
3884
|
workspaceRelativePath: "workspace-plans",
|
|
3900
3885
|
sourceMode: "all",
|
|
3901
3886
|
hydrateDeletionMode: "all",
|
|
3902
|
-
// Workspace-level agents
|
|
3887
|
+
// Workspace-level agents reach this directory through $R5D_ROOT/plans.
|
|
3903
3888
|
// Keep both projection and hydration away from it while any visible-
|
|
3904
3889
|
// projects process, PTY, or built-in write/edit is active or reserved.
|
|
3905
3890
|
busy: () => {
|
|
@@ -3937,7 +3922,10 @@ async function startWorker(options) {
|
|
|
3937
3922
|
deleteWhenSourceMissing: true
|
|
3938
3923
|
});
|
|
3939
3924
|
}
|
|
3940
|
-
return mounts
|
|
3925
|
+
return mounts.map((mount) => {
|
|
3926
|
+
const lastProjectedMutationToken = workspaceProjectionLedger.lastProjectedToken(mount);
|
|
3927
|
+
return lastProjectedMutationToken === void 0 ? mount : { ...mount, lastProjectedMutationToken };
|
|
3928
|
+
});
|
|
3941
3929
|
};
|
|
3942
3930
|
const workspaceLocalHead = async () => {
|
|
3943
3931
|
if (!fs.existsSync(path.join(workspaceShadowRoot, ".git"))) return null;
|
|
@@ -4162,17 +4150,7 @@ async function startWorker(options) {
|
|
|
4162
4150
|
for (const result of results) {
|
|
4163
4151
|
lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
|
|
4164
4152
|
}
|
|
4165
|
-
|
|
4166
|
-
fetchedProjectMirrorRefsTokens.set(
|
|
4167
|
-
project.projectId,
|
|
4168
|
-
await computeLocalProjectMirrorRefsToken({
|
|
4169
|
-
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
4170
|
-
primaryBranchName: primaryProjectBranch(project)
|
|
4171
|
-
})
|
|
4172
|
-
);
|
|
4173
|
-
} catch {
|
|
4174
|
-
fetchedProjectMirrorRefsTokens.delete(project.projectId);
|
|
4175
|
-
}
|
|
4153
|
+
await recordLocalProjectMirrorRefsTokens(project);
|
|
4176
4154
|
}
|
|
4177
4155
|
const publishedTombstoneIds = [
|
|
4178
4156
|
...new Set(
|
|
@@ -4198,7 +4176,7 @@ async function startWorker(options) {
|
|
|
4198
4176
|
credentialHelper: deletion.credentialHelper,
|
|
4199
4177
|
credentialUsername: deletion.credentialUsername
|
|
4200
4178
|
});
|
|
4201
|
-
|
|
4179
|
+
forgetLocalProjectMirrorRefsTokens(deletion.projectId, deletion.branchName);
|
|
4202
4180
|
}
|
|
4203
4181
|
if (deletion.tombstoneId && !deletion.projectDeleted) {
|
|
4204
4182
|
projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
@@ -4210,8 +4188,8 @@ async function startWorker(options) {
|
|
|
4210
4188
|
if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
|
|
4211
4189
|
removeProjectCredentialStore(deletion.projectId);
|
|
4212
4190
|
advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4213
|
-
|
|
4214
|
-
|
|
4191
|
+
advertisedProjectMirrorMountTokens.delete(deletion.projectId);
|
|
4192
|
+
forgetLocalProjectMirrorRefsTokens(deletion.projectId);
|
|
4215
4193
|
}
|
|
4216
4194
|
lastObservedProjectHeads.delete(key);
|
|
4217
4195
|
}
|
|
@@ -4220,39 +4198,20 @@ async function startWorker(options) {
|
|
|
4220
4198
|
const connection = projectConnection(project);
|
|
4221
4199
|
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
4222
4200
|
const primaryBranchName = primaryProjectBranch(project);
|
|
4223
|
-
|
|
4224
|
-
const advertisedBeforeFetch = advertisedProjectMirrorRefsTokens.get(project.projectId);
|
|
4225
|
-
const observations = await observeProjectMirrorHeads({
|
|
4226
|
-
projectRoot,
|
|
4227
|
-
primaryBranchName,
|
|
4228
|
-
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
4229
|
-
mirrorUrl: connection.mirrorUrl,
|
|
4230
|
-
credentialHelper: connection.credentialHelper,
|
|
4231
|
-
credentialUsername: connection.mirrorCredentialUsername,
|
|
4232
|
-
allowNonFastForward: options2.allowNonFastForward,
|
|
4233
|
-
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
|
|
4234
|
-
});
|
|
4235
|
-
if (advertisedBeforeFetch === void 0) fetchedProjectMirrorRefsTokens.delete(project.projectId);
|
|
4236
|
-
else fetchedProjectMirrorRefsTokens.set(project.projectId, advertisedBeforeFetch);
|
|
4237
|
-
lastProjectMirrorFetchAtMs.set(project.projectId, Date.now());
|
|
4238
|
-
return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
|
|
4239
|
-
}
|
|
4240
|
-
return {
|
|
4241
|
-
projectId: project.projectId,
|
|
4201
|
+
const observations = await observeProjectMirrorHeads({
|
|
4242
4202
|
projectRoot,
|
|
4243
4203
|
primaryBranchName,
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
};
|
|
4204
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
4205
|
+
mirrorUrl: connection.mirrorUrl,
|
|
4206
|
+
credentialHelper: connection.credentialHelper,
|
|
4207
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
4208
|
+
allowNonFastForward: options2.allowNonFastForward,
|
|
4209
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
|
|
4210
|
+
refreshRemote: options2.fetch.scope !== "none",
|
|
4211
|
+
...options2.fetch.scope === "mounts" ? { fetchOnlyBranches: new Set(options2.fetch.mounts.map(({ branchName }) => branchName)) } : {}
|
|
4212
|
+
});
|
|
4213
|
+
if (options2.fetch.scope !== "none") await recordLocalProjectMirrorRefsTokens(project);
|
|
4214
|
+
return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
|
|
4256
4215
|
};
|
|
4257
4216
|
const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
|
|
4258
4217
|
const observationStartedAt = Date.now();
|
|
@@ -4263,28 +4222,39 @@ async function startWorker(options) {
|
|
|
4263
4222
|
if (input.failClosed) {
|
|
4264
4223
|
const bundles2 = [];
|
|
4265
4224
|
for (const project of eligibleProjects) {
|
|
4266
|
-
bundles2.push(
|
|
4225
|
+
bundles2.push(
|
|
4226
|
+
await observeSingleProjectMirrorBundle(project, {
|
|
4227
|
+
allowNonFastForward: input.allowNonFastForward,
|
|
4228
|
+
fetch: { scope: "project", reason: "fail_closed" }
|
|
4229
|
+
})
|
|
4230
|
+
);
|
|
4267
4231
|
}
|
|
4268
4232
|
return bundles2;
|
|
4269
4233
|
}
|
|
4270
|
-
let
|
|
4234
|
+
let wholeMirrorFetches = 0;
|
|
4235
|
+
let scopedMirrorFetches = 0;
|
|
4236
|
+
let fetchedMounts = 0;
|
|
4237
|
+
let eligibleMounts = 0;
|
|
4271
4238
|
const bundles = await mapWithConcurrencyLimit(
|
|
4272
4239
|
eligibleProjects,
|
|
4273
4240
|
PROJECT_MIRROR_OBSERVE_CONCURRENCY,
|
|
4274
4241
|
async (project) => {
|
|
4275
4242
|
try {
|
|
4276
|
-
const
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4243
|
+
const fetch2 = planProjectMirrorObservationFetch({
|
|
4244
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
4245
|
+
advertisedProjectToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
|
|
4246
|
+
advertisedMountTokens: advertisedProjectMirrorMountTokens.get(project.projectId),
|
|
4247
|
+
fetchedProjectToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
|
|
4248
|
+
fetchedMountTokens: fetchedProjectMirrorMountTokens.get(project.projectId) ?? /* @__PURE__ */ new Map(),
|
|
4281
4249
|
skippingEnabled: projectMirrorFetchSkippingEnabled
|
|
4282
4250
|
});
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4251
|
+
eligibleMounts += project.branches.length;
|
|
4252
|
+
if (fetch2.scope === "project") wholeMirrorFetches += 1;
|
|
4253
|
+
if (fetch2.scope === "mounts") {
|
|
4254
|
+
scopedMirrorFetches += 1;
|
|
4255
|
+
fetchedMounts += fetch2.mounts.length;
|
|
4256
|
+
}
|
|
4257
|
+
return await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, fetch: fetch2 });
|
|
4288
4258
|
} catch (error) {
|
|
4289
4259
|
process.stderr.write(
|
|
4290
4260
|
`[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -4294,9 +4264,9 @@ async function startWorker(options) {
|
|
|
4294
4264
|
}
|
|
4295
4265
|
}
|
|
4296
4266
|
);
|
|
4297
|
-
if (
|
|
4267
|
+
if (wholeMirrorFetches + scopedMirrorFetches > 0) {
|
|
4298
4268
|
process.stderr.write(
|
|
4299
|
-
`[r5d-worker] project mirror observation fetched ${
|
|
4269
|
+
`[r5d-worker] project mirror observation fetched ${wholeMirrorFetches + scopedMirrorFetches} of ${eligibleProjects.length} mirror(s) (${wholeMirrorFetches} whole, ${scopedMirrorFetches} scoped to ${fetchedMounts} of ${eligibleMounts} mount(s))
|
|
4300
4270
|
`
|
|
4301
4271
|
);
|
|
4302
4272
|
}
|
|
@@ -4410,6 +4380,7 @@ async function startWorker(options) {
|
|
|
4410
4380
|
}
|
|
4411
4381
|
const configuredMounts = buildWorkspaceMounts();
|
|
4412
4382
|
if (input.resetToCanonical) {
|
|
4383
|
+
workspaceProjectionLedger.reset();
|
|
4413
4384
|
const resetHasBusyLiveMount = configuredMounts.some(
|
|
4414
4385
|
(mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
|
|
4415
4386
|
);
|
|
@@ -4452,6 +4423,7 @@ async function startWorker(options) {
|
|
|
4452
4423
|
}
|
|
4453
4424
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4454
4425
|
const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
|
|
4426
|
+
if (outerRemediation) workspaceProjectionLedger.reset();
|
|
4455
4427
|
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
4456
4428
|
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4457
4429
|
for (const bundle of observedProjectHeads) {
|
|
@@ -4478,6 +4450,10 @@ async function startWorker(options) {
|
|
|
4478
4450
|
allowLargeDiff: input.confirmedLargeDiff,
|
|
4479
4451
|
skipMountMirror: outerRemediation,
|
|
4480
4452
|
requiredAncestorHeads: input.requiredAncestorHeads,
|
|
4453
|
+
// Same gate as the mirror fetches: while the server's read of the hidden
|
|
4454
|
+
// workspace head equals the head this clone last fetched or pushed, the
|
|
4455
|
+
// cycle's workspace head fetches would transfer nothing.
|
|
4456
|
+
advertisedRemoteHead: projectMirrorFetchSkippingEnabled && !outerRemediation ? advertisedWorkspaceHead : void 0,
|
|
4481
4457
|
assertStillAdmitted: input.assertStillAdmitted,
|
|
4482
4458
|
afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
|
|
4483
4459
|
await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
|
|
@@ -4499,7 +4475,11 @@ async function startWorker(options) {
|
|
|
4499
4475
|
result.localHead ?? result.remoteHead
|
|
4500
4476
|
);
|
|
4501
4477
|
}
|
|
4502
|
-
|
|
4478
|
+
const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
4479
|
+
if (!outerRemediation) {
|
|
4480
|
+
workspaceProjectionLedger.recordCycle({ result: mapped, mounts, projectedMutationTokens: result.projectedMutationTokens });
|
|
4481
|
+
}
|
|
4482
|
+
return mapped;
|
|
4503
4483
|
};
|
|
4504
4484
|
const sendWorkspaceSyncResult = (requestId, result) => {
|
|
4505
4485
|
try {
|
|
@@ -4537,17 +4517,143 @@ async function startWorker(options) {
|
|
|
4537
4517
|
}, unsafeWorkspaceSyncRecoveryBounceDelayMs(attempt));
|
|
4538
4518
|
timer.unref();
|
|
4539
4519
|
};
|
|
4520
|
+
const captureWorkspaceSyncAdmission = () => ({
|
|
4521
|
+
generation: workerAdmissionGeneration,
|
|
4522
|
+
activeIncidentId: activeWorkspaceIncidentId,
|
|
4523
|
+
deferredIncidentId: deferredWorkspaceConfiguration?.incidentId ?? null
|
|
4524
|
+
});
|
|
4525
|
+
const workspaceSyncAdmissionSnapshotsMatch = (left, right) => left.generation === right.generation && left.activeIncidentId === right.activeIncidentId && left.deferredIncidentId === right.deferredIncidentId;
|
|
4526
|
+
const assertWorkspaceSyncStillAdmitted = (admitted, trigger) => {
|
|
4527
|
+
if (!workspaceSyncAdmissionSnapshotsMatch(admitted, captureWorkspaceSyncAdmission()) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, trigger)) {
|
|
4528
|
+
throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
|
|
4529
|
+
}
|
|
4530
|
+
};
|
|
4531
|
+
const executeWorkspaceSyncCycle = async (input) => {
|
|
4532
|
+
const attemptId = input.attemptId;
|
|
4533
|
+
lastWorkspaceSyncMirrorObservationMs = 0;
|
|
4534
|
+
if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
|
|
4535
|
+
return await failedWorkspaceSyncResult(
|
|
4536
|
+
attemptId,
|
|
4537
|
+
input.trigger,
|
|
4538
|
+
new Error(
|
|
4539
|
+
`Workspace synchronization was fenced while queued by incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
|
|
4540
|
+
)
|
|
4541
|
+
);
|
|
4542
|
+
}
|
|
4543
|
+
try {
|
|
4544
|
+
input.assertStillAdmitted();
|
|
4545
|
+
const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
|
|
4546
|
+
return await performWorkspaceSync({
|
|
4547
|
+
attemptId,
|
|
4548
|
+
trigger: input.trigger,
|
|
4549
|
+
confirmedLargeDiff: input.confirmedLargeDiff === true || hasDurableBranchDeletion,
|
|
4550
|
+
confirmationReason: input.confirmationReason ?? (hasDurableBranchDeletion ? "durable project branch deletion" : void 0),
|
|
4551
|
+
resetToCanonical: input.resetToCanonical,
|
|
4552
|
+
requiredAncestorHeads: input.requiredAncestorHeads,
|
|
4553
|
+
assertStillAdmitted: input.assertStillAdmitted
|
|
4554
|
+
});
|
|
4555
|
+
} catch (error) {
|
|
4556
|
+
workspaceProjectionLedger.reset();
|
|
4557
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4558
|
+
const hydrationRecoverable = workspaceSyncFailureHydrationIsSafe({
|
|
4559
|
+
error,
|
|
4560
|
+
resetToCanonical: input.resetToCanonical === true,
|
|
4561
|
+
inspectRecoverableHydration: () => workspaceGitHydrationIsRecoverable(workspaceShadowRoot, buildWorkspaceMounts())
|
|
4562
|
+
});
|
|
4563
|
+
if (hydrationRecoverable) {
|
|
4564
|
+
if (error instanceof WorkspaceSyncAdmissionChangedError) {
|
|
4565
|
+
process.stderr.write(
|
|
4566
|
+
`[r5d-worker] workspace synchronization was cancelled with every mount at a recorded basis; the next cycle converges it: ${errorMessage}
|
|
4567
|
+
`
|
|
4568
|
+
);
|
|
4569
|
+
}
|
|
4570
|
+
return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
|
|
4571
|
+
}
|
|
4572
|
+
const fenced = fenceUnsafeWorkspaceSyncFailure({
|
|
4573
|
+
hydrationRecoverable,
|
|
4574
|
+
resetToCanonical: input.resetToCanonical === true,
|
|
4575
|
+
recoveryAttempts: unsafeWorkspaceSyncRecoveryAttempts,
|
|
4576
|
+
maxRecoveryAttempts: WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
|
|
4577
|
+
hasRetainableWork: activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0,
|
|
4578
|
+
bouncePending: unsafeWorkspaceSyncBouncePending,
|
|
4579
|
+
invalidateExecution: () => {
|
|
4580
|
+
workspaceConfigured = false;
|
|
4581
|
+
advanceWorkerAdmissionGeneration();
|
|
4582
|
+
},
|
|
4583
|
+
scheduleRecoveryBounce: () => {
|
|
4584
|
+
unsafeWorkspaceSyncRecoveryAttempts += 1;
|
|
4585
|
+
scheduleUnsafeWorkspaceSyncRecoveryBounce(errorMessage);
|
|
4586
|
+
},
|
|
4587
|
+
fatalExit: (exitCode) => {
|
|
4588
|
+
process.stderr.write(
|
|
4589
|
+
`[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
|
|
4590
|
+
`
|
|
4591
|
+
);
|
|
4592
|
+
process.exit(exitCode);
|
|
4593
|
+
},
|
|
4594
|
+
exitCode: 1
|
|
4595
|
+
});
|
|
4596
|
+
if (fenced === "recovery_pending") {
|
|
4597
|
+
process.stderr.write(
|
|
4598
|
+
`[r5d-worker] workspace synchronization failed without a recoverable hydration basis while in-process recovery is already pending: ${errorMessage}
|
|
4599
|
+
`
|
|
4600
|
+
);
|
|
4601
|
+
}
|
|
4602
|
+
return {
|
|
4603
|
+
...await failedWorkspaceSyncResult(attemptId, input.trigger, error),
|
|
4604
|
+
failureReason: "unsafe_hydration",
|
|
4605
|
+
recoveryAttempt: unsafeWorkspaceSyncRecoveryAttempts
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4608
|
+
};
|
|
4609
|
+
const REMEDIATION_TRIGGER_TYPES = /* @__PURE__ */ new Set(["remediation", "remediation_confirm", "remediation_reset"]);
|
|
4610
|
+
const isOrdinaryWorkspaceSyncRequest = (input) => input.confirmedLargeDiff !== true && input.resetToCanonical !== true && !(input.requiredAncestorHeads && input.requiredAncestorHeads.length > 0) && !REMEDIATION_TRIGGER_TYPES.has(input.trigger.type);
|
|
4611
|
+
const LOCAL_ACTIVITY_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4612
|
+
"write",
|
|
4613
|
+
"edit",
|
|
4614
|
+
"shell_inline",
|
|
4615
|
+
"process_terminal",
|
|
4616
|
+
"process_cancel"
|
|
4617
|
+
]);
|
|
4618
|
+
const answerOrdinaryWorkspaceSyncFromLastCycle = (requests) => {
|
|
4619
|
+
if (!workspaceConfigured) return null;
|
|
4620
|
+
return workspaceProjectionLedger.answerFromLastCycle({
|
|
4621
|
+
// A workspace-rooted command carries no project target because it may
|
|
4622
|
+
// have touched any checkout.
|
|
4623
|
+
targetMountIds: requests.map(
|
|
4624
|
+
({ trigger }) => LOCAL_ACTIVITY_TRIGGER_TYPES.has(trigger.type) && trigger.projectId && trigger.branchName ? projectMountId(trigger.projectId, trigger.branchName) : null
|
|
4625
|
+
),
|
|
4626
|
+
mounts: buildWorkspaceMounts(),
|
|
4627
|
+
pendingDeletions: pendingMirrorDeletes.size > 0 || projectWorkspaceState.pendingTreeDeletions.length > 0 || projectWorkspaceState.pendingMirrorRefDeletions.length > 0
|
|
4628
|
+
});
|
|
4629
|
+
};
|
|
4630
|
+
const ordinaryWorkspaceSyncCoalescer = new WorkspaceSyncCoalescer({
|
|
4631
|
+
runExclusive: (operation) => workspaceSyncSingleFlight.runExclusive(operation),
|
|
4632
|
+
runBatch: async (requests) => {
|
|
4633
|
+
const startedAtMs = Date.now();
|
|
4634
|
+
const admission = captureWorkspaceSyncAdmission();
|
|
4635
|
+
const staleAttemptIds = /* @__PURE__ */ new Set();
|
|
4636
|
+
const admitted = [];
|
|
4637
|
+
for (const request of requests) {
|
|
4638
|
+
if (workspaceSyncAdmissionSnapshotsMatch(request.admission, admission)) admitted.push(request);
|
|
4639
|
+
else staleAttemptIds.add(request.attemptId);
|
|
4640
|
+
}
|
|
4641
|
+
const leader = admitted[0];
|
|
4642
|
+
if (!leader) return { result: null, ranCycle: false, startedAtMs, finishedAtMs: Date.now(), prepareMs: 0, staleAttemptIds };
|
|
4643
|
+
const answered = answerOrdinaryWorkspaceSyncFromLastCycle(admitted);
|
|
4644
|
+
if (answered) return { result: answered, ranCycle: false, startedAtMs, finishedAtMs: Date.now(), prepareMs: 0, staleAttemptIds };
|
|
4645
|
+
const result = await executeWorkspaceSyncCycle({
|
|
4646
|
+
attemptId: leader.attemptId,
|
|
4647
|
+
trigger: leader.trigger,
|
|
4648
|
+
assertStillAdmitted: () => assertWorkspaceSyncStillAdmitted(admission, leader.trigger)
|
|
4649
|
+
});
|
|
4650
|
+
return { result, ranCycle: true, startedAtMs, finishedAtMs: Date.now(), prepareMs: lastWorkspaceSyncMirrorObservationMs, staleAttemptIds };
|
|
4651
|
+
}
|
|
4652
|
+
});
|
|
4540
4653
|
const runWorkspaceSync = async (input) => {
|
|
4541
4654
|
const attemptId = input.attemptId ?? crypto.randomUUID();
|
|
4542
4655
|
const requestedAt = Date.now();
|
|
4543
|
-
const
|
|
4544
|
-
const admittedActiveIncidentId = activeWorkspaceIncidentId;
|
|
4545
|
-
const admittedDeferredIncidentId = deferredWorkspaceConfiguration?.incidentId ?? null;
|
|
4546
|
-
const assertStillAdmitted = () => {
|
|
4547
|
-
if (admittedGeneration !== workerAdmissionGeneration || admittedActiveIncidentId !== activeWorkspaceIncidentId || admittedDeferredIncidentId !== (deferredWorkspaceConfiguration?.incidentId ?? null) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
|
|
4548
|
-
throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
|
|
4549
|
-
}
|
|
4550
|
-
};
|
|
4656
|
+
const admission = captureWorkspaceSyncAdmission();
|
|
4551
4657
|
if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
|
|
4552
4658
|
const result2 = {
|
|
4553
4659
|
...await failedWorkspaceSyncResult(
|
|
@@ -4573,104 +4679,47 @@ async function startWorker(options) {
|
|
|
4573
4679
|
}
|
|
4574
4680
|
ws.send(JSON.stringify({ type: "workspace_sync_started", attemptId }));
|
|
4575
4681
|
}
|
|
4576
|
-
let
|
|
4577
|
-
let
|
|
4682
|
+
let queueMs = 0;
|
|
4683
|
+
let prepareMs = 0;
|
|
4684
|
+
let synchronizeMs = 0;
|
|
4578
4685
|
workspaceSyncRequestsInFlight += 1;
|
|
4579
4686
|
let result;
|
|
4580
4687
|
try {
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
|
|
4597
|
-
return await performWorkspaceSync({
|
|
4688
|
+
if (isOrdinaryWorkspaceSyncRequest(input)) {
|
|
4689
|
+
const settlement = await ordinaryWorkspaceSyncCoalescer.request({ attemptId, trigger: input.trigger, admission });
|
|
4690
|
+
queueMs = settlement.startedAtMs - requestedAt;
|
|
4691
|
+
prepareMs = settlement.prepareMs;
|
|
4692
|
+
synchronizeMs = settlement.finishedAtMs - settlement.startedAtMs;
|
|
4693
|
+
result = settlement.staleAttemptIds.has(attemptId) || settlement.result === null ? await failedWorkspaceSyncResult(
|
|
4694
|
+
attemptId,
|
|
4695
|
+
input.trigger,
|
|
4696
|
+
new Error("Workspace synchronization admission changed while the request was queued")
|
|
4697
|
+
) : { ...settlement.result, attemptId, trigger: input.trigger };
|
|
4698
|
+
} else {
|
|
4699
|
+
result = await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
4700
|
+
const startedAt = Date.now();
|
|
4701
|
+
queueMs = startedAt - requestedAt;
|
|
4702
|
+
const cycleResult = await executeWorkspaceSyncCycle({
|
|
4598
4703
|
attemptId,
|
|
4599
4704
|
trigger: input.trigger,
|
|
4600
|
-
confirmedLargeDiff: input.confirmedLargeDiff
|
|
4601
|
-
confirmationReason: input.confirmationReason
|
|
4705
|
+
confirmedLargeDiff: input.confirmedLargeDiff,
|
|
4706
|
+
confirmationReason: input.confirmationReason,
|
|
4602
4707
|
resetToCanonical: input.resetToCanonical,
|
|
4603
4708
|
requiredAncestorHeads: input.requiredAncestorHeads,
|
|
4604
|
-
assertStillAdmitted
|
|
4605
|
-
});
|
|
4606
|
-
} catch (error) {
|
|
4607
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4608
|
-
const hydrationRecoverable = workspaceSyncFailureHydrationIsSafe({
|
|
4609
|
-
error,
|
|
4610
|
-
resetToCanonical: input.resetToCanonical === true,
|
|
4611
|
-
inspectRecoverableHydration: () => workspaceGitHydrationIsRecoverable(workspaceShadowRoot, buildWorkspaceMounts())
|
|
4612
|
-
});
|
|
4613
|
-
if (hydrationRecoverable) {
|
|
4614
|
-
if (error instanceof WorkspaceSyncAdmissionChangedError) {
|
|
4615
|
-
process.stderr.write(
|
|
4616
|
-
`[r5d-worker] workspace synchronization was cancelled with every mount at a recorded basis; the next cycle converges it: ${errorMessage}
|
|
4617
|
-
`
|
|
4618
|
-
);
|
|
4619
|
-
}
|
|
4620
|
-
return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
|
|
4621
|
-
}
|
|
4622
|
-
const fenced = fenceUnsafeWorkspaceSyncFailure({
|
|
4623
|
-
hydrationRecoverable,
|
|
4624
|
-
resetToCanonical: input.resetToCanonical === true,
|
|
4625
|
-
recoveryAttempts: unsafeWorkspaceSyncRecoveryAttempts,
|
|
4626
|
-
maxRecoveryAttempts: WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
|
|
4627
|
-
hasRetainableWork: activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0,
|
|
4628
|
-
bouncePending: unsafeWorkspaceSyncBouncePending,
|
|
4629
|
-
invalidateExecution: () => {
|
|
4630
|
-
workspaceConfigured = false;
|
|
4631
|
-
advanceWorkerAdmissionGeneration();
|
|
4632
|
-
},
|
|
4633
|
-
scheduleRecoveryBounce: () => {
|
|
4634
|
-
unsafeWorkspaceSyncRecoveryAttempts += 1;
|
|
4635
|
-
scheduleUnsafeWorkspaceSyncRecoveryBounce(errorMessage);
|
|
4636
|
-
},
|
|
4637
|
-
fatalExit: (exitCode) => {
|
|
4638
|
-
process.stderr.write(
|
|
4639
|
-
`[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
|
|
4640
|
-
`
|
|
4641
|
-
);
|
|
4642
|
-
process.exit(exitCode);
|
|
4643
|
-
},
|
|
4644
|
-
exitCode: 1
|
|
4709
|
+
assertStillAdmitted: () => assertWorkspaceSyncStillAdmitted(admission, input.trigger)
|
|
4645
4710
|
});
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
}
|
|
4652
|
-
return {
|
|
4653
|
-
...await failedWorkspaceSyncResult(attemptId, input.trigger, error),
|
|
4654
|
-
failureReason: "unsafe_hydration",
|
|
4655
|
-
recoveryAttempt: unsafeWorkspaceSyncRecoveryAttempts
|
|
4656
|
-
};
|
|
4657
|
-
}
|
|
4658
|
-
});
|
|
4711
|
+
prepareMs = lastWorkspaceSyncMirrorObservationMs;
|
|
4712
|
+
synchronizeMs = Date.now() - startedAt;
|
|
4713
|
+
return cycleResult;
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4659
4716
|
} finally {
|
|
4660
4717
|
workspaceSyncRequestsInFlight -= 1;
|
|
4661
4718
|
}
|
|
4662
4719
|
if (result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
|
|
4663
|
-
const finishedAt = Date.now();
|
|
4664
4720
|
result = {
|
|
4665
4721
|
...result,
|
|
4666
|
-
telemetry: {
|
|
4667
|
-
totalMs: finishedAt - requestedAt,
|
|
4668
|
-
queueMs: queueEnteredAt - requestedAt,
|
|
4669
|
-
// Duration of the pre-outer mirror-head observation phase; near zero
|
|
4670
|
-
// on a cycle whose token-gated observation skipped every fetch.
|
|
4671
|
-
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
4672
|
-
synchronizeMs: finishedAt - syncStartedAt
|
|
4673
|
-
}
|
|
4722
|
+
telemetry: { totalMs: Date.now() - requestedAt, queueMs, prepareMs, synchronizeMs }
|
|
4674
4723
|
};
|
|
4675
4724
|
if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result);
|
|
4676
4725
|
sendLifecycleProgress(
|
|
@@ -4940,6 +4989,7 @@ async function startWorker(options) {
|
|
|
4940
4989
|
);
|
|
4941
4990
|
}
|
|
4942
4991
|
workspaceConfigured = false;
|
|
4992
|
+
workspaceProjectionLedger.reset();
|
|
4943
4993
|
const preserveOnlyBranches = message.projects.flatMap(
|
|
4944
4994
|
(project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
|
|
4945
4995
|
);
|
|
@@ -4949,6 +4999,16 @@ async function startWorker(options) {
|
|
|
4949
4999
|
preserveOnlyBranches
|
|
4950
5000
|
});
|
|
4951
5001
|
}
|
|
5002
|
+
for (const project of message.projects) {
|
|
5003
|
+
for (const branch of project.branches) {
|
|
5004
|
+
const pending2 = projectWorkspaceState.locallyPendingCreatedBranches.find(
|
|
5005
|
+
(candidate) => candidate.projectId === project.projectId && candidate.branchName === branch.branchName
|
|
5006
|
+
);
|
|
5007
|
+
if (pending2 && pending2.branchId === branch.branchId) {
|
|
5008
|
+
projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pending2);
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
4952
5012
|
const stillPendingCreatedBranches = new Map(
|
|
4953
5013
|
projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
|
|
4954
5014
|
pendingCreatedBranchKey(branch.projectId, branch.branchName),
|
|
@@ -5390,11 +5450,10 @@ async function startWorker(options) {
|
|
|
5390
5450
|
workspaceConfigResetToCanonicalV1: true,
|
|
5391
5451
|
projectBranchDeletionFastAckV1: true,
|
|
5392
5452
|
projectMirrorLeaseV1: true,
|
|
5393
|
-
projectMirrorRefsTokensV1: true
|
|
5453
|
+
projectMirrorRefsTokensV1: true,
|
|
5454
|
+
projectBranchWorkingTreeModeV1: true
|
|
5394
5455
|
},
|
|
5395
|
-
|
|
5396
|
-
artifactRoot,
|
|
5397
|
-
planRoot
|
|
5456
|
+
root: rootDir
|
|
5398
5457
|
}
|
|
5399
5458
|
};
|
|
5400
5459
|
ws.send(JSON.stringify(hello));
|
|
@@ -5429,14 +5488,12 @@ async function startWorker(options) {
|
|
|
5429
5488
|
return;
|
|
5430
5489
|
}
|
|
5431
5490
|
if (message.type === "project_mirror_refs_tokens") {
|
|
5491
|
+
const advertised = parseProjectMirrorRefsTokensMessage(message);
|
|
5432
5492
|
advertisedProjectMirrorRefsTokens.clear();
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
}
|
|
5438
|
-
}
|
|
5439
|
-
}
|
|
5493
|
+
for (const [projectId, token2] of advertised.projectTokens) advertisedProjectMirrorRefsTokens.set(projectId, token2);
|
|
5494
|
+
advertisedProjectMirrorMountTokens.clear();
|
|
5495
|
+
for (const [projectId, tokens] of advertised.mountTokens) advertisedProjectMirrorMountTokens.set(projectId, tokens);
|
|
5496
|
+
advertisedWorkspaceHead = advertised.workspaceHead;
|
|
5440
5497
|
return;
|
|
5441
5498
|
}
|
|
5442
5499
|
if (message.type === "workspace_config") {
|
|
@@ -5509,13 +5566,21 @@ async function startWorker(options) {
|
|
|
5509
5566
|
});
|
|
5510
5567
|
return;
|
|
5511
5568
|
}
|
|
5569
|
+
sendWorkerMessage(ws, {
|
|
5570
|
+
type: "operation_queued",
|
|
5571
|
+
requestId: message.requestId,
|
|
5572
|
+
queuePosition: workspaceSyncSingleFlight.branchMutationQueuePosition()
|
|
5573
|
+
});
|
|
5512
5574
|
try {
|
|
5575
|
+
if (message.workingTree !== "carry" && message.workingTree !== "clean") {
|
|
5576
|
+
throw new Error('Project branch creation requires workingTree "carry" or "clean"');
|
|
5577
|
+
}
|
|
5513
5578
|
const pendingBranch = {
|
|
5514
5579
|
branchId: message.branchId,
|
|
5515
5580
|
projectId: message.projectId,
|
|
5516
5581
|
branchName: message.targetBranch
|
|
5517
5582
|
};
|
|
5518
|
-
const created = await workspaceSyncSingleFlight.
|
|
5583
|
+
const created = await workspaceSyncSingleFlight.runBranchMutation(() => {
|
|
5519
5584
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
5520
5585
|
throw new Error(
|
|
5521
5586
|
`Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
|
|
@@ -5556,6 +5621,7 @@ async function startWorker(options) {
|
|
|
5556
5621
|
primaryBranchName: primaryProjectBranch(project),
|
|
5557
5622
|
sourceBranchName: message.sourceBranch,
|
|
5558
5623
|
branchName: message.targetBranch,
|
|
5624
|
+
workingTree: message.workingTree,
|
|
5559
5625
|
pendingRetry: pendingWasRecorded
|
|
5560
5626
|
});
|
|
5561
5627
|
gitBranchCreated = true;
|
|
@@ -5612,9 +5678,14 @@ async function startWorker(options) {
|
|
|
5612
5678
|
});
|
|
5613
5679
|
return;
|
|
5614
5680
|
}
|
|
5681
|
+
sendWorkerMessage(ws, {
|
|
5682
|
+
type: "operation_queued",
|
|
5683
|
+
requestId: message.requestId,
|
|
5684
|
+
queuePosition: workspaceSyncSingleFlight.branchMutationQueuePosition()
|
|
5685
|
+
});
|
|
5615
5686
|
try {
|
|
5616
5687
|
const deletionKey = projectBranchKey(message.projectId, message.branchName);
|
|
5617
|
-
const deletionNeedsSync = await workspaceSyncSingleFlight.
|
|
5688
|
+
const deletionNeedsSync = await workspaceSyncSingleFlight.runBranchMutation(() => {
|
|
5618
5689
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
5619
5690
|
throw new Error(
|
|
5620
5691
|
`Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
|
|
@@ -5645,6 +5716,9 @@ async function startWorker(options) {
|
|
|
5645
5716
|
projectId: message.projectId,
|
|
5646
5717
|
branchName: message.branchName
|
|
5647
5718
|
});
|
|
5719
|
+
if (pendingLocalBranch) {
|
|
5720
|
+
projectWorkspaceState = projectWorkspaceStateStore.clearPendingCreatedBranch(pendingLocalBranch);
|
|
5721
|
+
}
|
|
5648
5722
|
pendingCreatedBranchPublicationNotBefore.delete(pendingCreatedBranchKey(message.projectId, message.branchName));
|
|
5649
5723
|
if (pendingMirrorDeletes.has(deletionKey)) return true;
|
|
5650
5724
|
stageProjectBranchDeletion(project, message.branchName, true);
|
|
@@ -5922,7 +5996,7 @@ async function startWorker(options) {
|
|
|
5922
5996
|
ws,
|
|
5923
5997
|
message,
|
|
5924
5998
|
resolvedTarget,
|
|
5925
|
-
|
|
5999
|
+
rootDir,
|
|
5926
6000
|
assertAdmission: assertMessageAdmission,
|
|
5927
6001
|
...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
|
|
5928
6002
|
...targetMayMutateVisibleWorkspace(message.target) ? {
|
|
@@ -6020,8 +6094,7 @@ async function startWorker(options) {
|
|
|
6020
6094
|
resolvedTarget,
|
|
6021
6095
|
baseUrl,
|
|
6022
6096
|
token,
|
|
6023
|
-
|
|
6024
|
-
planRoot,
|
|
6097
|
+
rootDir,
|
|
6025
6098
|
setPreparationHeartbeatLease: (active) => {
|
|
6026
6099
|
sendWorkerMessageFromCurrentSource(ws, {
|
|
6027
6100
|
type: "heartbeat_lease",
|
|
@@ -6090,8 +6163,7 @@ async function startWorker(options) {
|
|
|
6090
6163
|
resolvedTarget,
|
|
6091
6164
|
baseUrl,
|
|
6092
6165
|
token,
|
|
6093
|
-
|
|
6094
|
-
planRoot,
|
|
6166
|
+
rootDir,
|
|
6095
6167
|
setPreparationHeartbeatLease: (active) => {
|
|
6096
6168
|
sendWorkerMessageFromCurrentSource(ws, {
|
|
6097
6169
|
type: "heartbeat_lease",
|
|
@@ -6173,8 +6245,7 @@ async function startWorker(options) {
|
|
|
6173
6245
|
resolvedTarget,
|
|
6174
6246
|
baseUrl,
|
|
6175
6247
|
token,
|
|
6176
|
-
|
|
6177
|
-
planRoot,
|
|
6248
|
+
rootDir,
|
|
6178
6249
|
assertAdmission: assertMessageAdmission
|
|
6179
6250
|
});
|
|
6180
6251
|
});
|