@ricsam/r5d-worker 0.0.36 → 0.0.38
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 +5 -1
- package/dist/cjs/main.cjs +234 -525
- package/dist/cjs/managed-paths.cjs +158 -0
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-sync.cjs +653 -0
- package/dist/mjs/main.mjs +239 -523
- package/dist/mjs/managed-paths.mjs +116 -0
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-sync.mjs +608 -0
- package/dist/types/main.d.ts +0 -50
- package/dist/types/managed-paths.d.ts +15 -0
- package/dist/types/workspace-sync.d.ts +74 -0
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -9,17 +9,21 @@ import { configureVisibleGitIdentity } from "./git-identity.mjs";
|
|
|
9
9
|
import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
|
|
10
10
|
import { terminateProcessTree } from "./process-tree.mjs";
|
|
11
11
|
import { superviseWorkerRuntime, WORKER_RELOAD_EXIT_CODE, WORKER_RUNTIME_ENV } from "./supervisor.mjs";
|
|
12
|
+
import { managedProjectRoot, migrateLegacyProjectRoots, validateManagedBranchName } from "./managed-paths.mjs";
|
|
13
|
+
import {
|
|
14
|
+
WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
|
|
15
|
+
WorkspaceSyncSingleFlight,
|
|
16
|
+
calculateWorkspaceDiffFingerprint,
|
|
17
|
+
synchronizeWorkspace
|
|
18
|
+
} from "./workspace-sync.mjs";
|
|
12
19
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
13
20
|
const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
14
21
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
15
|
-
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
|
|
16
22
|
const DEFAULT_READ_LIMIT = 2e3;
|
|
17
23
|
const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
18
24
|
const MAX_LINE_LENGTH = 2e3;
|
|
19
|
-
const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
20
|
-
const R5D_REMOTE_NAME = "r5d";
|
|
21
|
-
const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
|
|
22
25
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
26
|
+
const pendingProcessTerminals = /* @__PURE__ */ new Map();
|
|
23
27
|
const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
24
28
|
const activePtys = /* @__PURE__ */ new Map();
|
|
25
29
|
let currentWorkerSocket = null;
|
|
@@ -478,8 +482,10 @@ async function prepareArtifactEnvForShell(input) {
|
|
|
478
482
|
artifactRoot: input.artifactRoot
|
|
479
483
|
});
|
|
480
484
|
} catch (error) {
|
|
481
|
-
process.stderr.write(
|
|
482
|
-
`)
|
|
485
|
+
process.stderr.write(
|
|
486
|
+
`[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
|
|
487
|
+
`
|
|
488
|
+
);
|
|
483
489
|
}
|
|
484
490
|
const artifactsDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
|
|
485
491
|
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
@@ -535,7 +541,9 @@ function resolveCredentials(options, config) {
|
|
|
535
541
|
throw new Error("Authentication required. Run `r5dctl auth login` or set R5D_WORKER_TOKEN/R5D_API_KEY.");
|
|
536
542
|
}
|
|
537
543
|
return {
|
|
538
|
-
baseUrl: normalizeBaseUrl(
|
|
544
|
+
baseUrl: normalizeBaseUrl(
|
|
545
|
+
options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL
|
|
546
|
+
),
|
|
539
547
|
token
|
|
540
548
|
};
|
|
541
549
|
}
|
|
@@ -553,9 +561,7 @@ function validateProjectId(projectId) {
|
|
|
553
561
|
}
|
|
554
562
|
}
|
|
555
563
|
function validateBranchName(branchName) {
|
|
556
|
-
|
|
557
|
-
throw new Error(`Invalid branch name from server: ${branchName}`);
|
|
558
|
-
}
|
|
564
|
+
validateManagedBranchName(branchName);
|
|
559
565
|
}
|
|
560
566
|
function validatePlanId(planId) {
|
|
561
567
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
|
|
@@ -589,27 +595,6 @@ function runGit(args, options = {}) {
|
|
|
589
595
|
}
|
|
590
596
|
return result.stdout.toString().trim();
|
|
591
597
|
}
|
|
592
|
-
async function runGitAsync(args, options = {}) {
|
|
593
|
-
const command = ["git", ...gitAuthArgs(options.auth), ...args];
|
|
594
|
-
const subprocess = Bun.spawn(command, {
|
|
595
|
-
cwd: options.cwd,
|
|
596
|
-
stdout: "pipe",
|
|
597
|
-
stderr: "pipe",
|
|
598
|
-
env: {
|
|
599
|
-
...process.env,
|
|
600
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
604
|
-
collectStream(subprocess.stdout),
|
|
605
|
-
collectStream(subprocess.stderr),
|
|
606
|
-
subprocess.exited
|
|
607
|
-
]);
|
|
608
|
-
if (exitCode !== 0) {
|
|
609
|
-
throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
|
|
610
|
-
}
|
|
611
|
-
return stdout.trim();
|
|
612
|
-
}
|
|
613
598
|
function tryGit(args, options = {}) {
|
|
614
599
|
try {
|
|
615
600
|
runGit(args, options);
|
|
@@ -618,34 +603,6 @@ function tryGit(args, options = {}) {
|
|
|
618
603
|
return false;
|
|
619
604
|
}
|
|
620
605
|
}
|
|
621
|
-
async function tryGitAsync(args, options = {}) {
|
|
622
|
-
try {
|
|
623
|
-
await runGitAsync(args, options);
|
|
624
|
-
return true;
|
|
625
|
-
} catch {
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
function runInternalGit(context, args) {
|
|
630
|
-
return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
|
|
631
|
-
}
|
|
632
|
-
function runInternalGitAsync(context, args) {
|
|
633
|
-
return runGitAsync(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
|
|
634
|
-
}
|
|
635
|
-
function runInternalGitDir(context, args) {
|
|
636
|
-
return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
|
|
637
|
-
}
|
|
638
|
-
function tryInternalGit(context, args) {
|
|
639
|
-
try {
|
|
640
|
-
runInternalGit(context, args);
|
|
641
|
-
return true;
|
|
642
|
-
} catch {
|
|
643
|
-
return false;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
function getInternalCommitHash(context) {
|
|
647
|
-
return runInternalGit(context, ["rev-parse", "HEAD"]);
|
|
648
|
-
}
|
|
649
606
|
function getGitBlobHashForContent(content) {
|
|
650
607
|
const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
651
608
|
return createHash("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
|
|
@@ -653,12 +610,6 @@ function getGitBlobHashForContent(content) {
|
|
|
653
610
|
function hasWorktreeChanges(cwd) {
|
|
654
611
|
return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
|
|
655
612
|
}
|
|
656
|
-
function getInternalStatus(context) {
|
|
657
|
-
return runInternalGit(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
|
|
658
|
-
}
|
|
659
|
-
function hasInternalWorktreeChanges(context) {
|
|
660
|
-
return getInternalStatus(context).trim().length > 0;
|
|
661
|
-
}
|
|
662
613
|
function isInsideBranchPath(branchPath, filePath) {
|
|
663
614
|
const relative = path.relative(path.resolve(branchPath), path.resolve(filePath));
|
|
664
615
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
@@ -721,32 +672,15 @@ function resolveWorkerFilePath(branchPath, inputPath) {
|
|
|
721
672
|
function resolveProjectFilePath(branchPath, inputPath) {
|
|
722
673
|
const resolved = resolveWorkerFilePath(branchPath, inputPath);
|
|
723
674
|
if (resolved.scope === "host") {
|
|
724
|
-
throw new Error(
|
|
725
|
-
`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`
|
|
726
|
-
);
|
|
675
|
+
throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
|
|
727
676
|
}
|
|
728
677
|
return resolved;
|
|
729
678
|
}
|
|
730
|
-
function resolveRemoteUrl(baseUrl, remoteUrl) {
|
|
731
|
-
if (/^https?:\/\//i.test(remoteUrl)) {
|
|
732
|
-
return remoteUrl;
|
|
733
|
-
}
|
|
734
|
-
return new URL(remoteUrl, baseUrl).toString();
|
|
735
|
-
}
|
|
736
679
|
function fallbackInternalRemoteUrl(baseUrl, projectId) {
|
|
737
680
|
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
738
681
|
}
|
|
739
|
-
function internalGitAuth(baseUrl, token) {
|
|
740
|
-
return {
|
|
741
|
-
extraHeaderUrl: baseUrl,
|
|
742
|
-
header: `Authorization: Bearer ${token}`
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
function internalRemoteUrlFor(baseUrl, projectId, manifest) {
|
|
746
|
-
return resolveRemoteUrl(baseUrl, manifest?.internalRemoteUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId));
|
|
747
|
-
}
|
|
748
682
|
function githubRemoteUrlFor(baseUrl, projectId, manifest) {
|
|
749
|
-
return manifest?.repoHttpUrl ??
|
|
683
|
+
return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
|
|
750
684
|
}
|
|
751
685
|
function gitExtraHeaderUrlForRemote(remoteUrl) {
|
|
752
686
|
try {
|
|
@@ -764,12 +698,6 @@ function gitAuthForRemote(remoteUrl, authHeader) {
|
|
|
764
698
|
header: authHeader
|
|
765
699
|
};
|
|
766
700
|
}
|
|
767
|
-
function branchGitDirName(branchName) {
|
|
768
|
-
return `${encodeURIComponent(branchName)}.git`;
|
|
769
|
-
}
|
|
770
|
-
function internalGitDirFor(syncRoot, projectId, branchName) {
|
|
771
|
-
return path.join(syncRoot, projectId, branchGitDirName(branchName));
|
|
772
|
-
}
|
|
773
701
|
function hasNormalVisibleGitDir(branchPath) {
|
|
774
702
|
const gitPath = path.join(branchPath, ".git");
|
|
775
703
|
return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
|
|
@@ -781,13 +709,6 @@ function ensureOriginRemote(branchPath, remoteUrl) {
|
|
|
781
709
|
runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
|
|
782
710
|
}
|
|
783
711
|
}
|
|
784
|
-
async function ensureOriginRemoteAsync(branchPath, remoteUrl) {
|
|
785
|
-
if (await tryGitAsync(["remote", "get-url", "origin"], { cwd: branchPath })) {
|
|
786
|
-
await runGitAsync(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
|
|
787
|
-
} else {
|
|
788
|
-
await runGitAsync(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
712
|
function shellQuote(value) {
|
|
792
713
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
793
714
|
}
|
|
@@ -983,194 +904,20 @@ function ensureVisibleGitCheckout(input) {
|
|
|
983
904
|
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
984
905
|
return branchPath;
|
|
985
906
|
}
|
|
986
|
-
function configureInternalRepo(context, remoteUrl) {
|
|
987
|
-
if (tryInternalGit(context, ["remote", "get-url", R5D_REMOTE_NAME])) {
|
|
988
|
-
runInternalGit(context, ["remote", "set-url", R5D_REMOTE_NAME, remoteUrl]);
|
|
989
|
-
} else {
|
|
990
|
-
runInternalGit(context, ["remote", "add", R5D_REMOTE_NAME, remoteUrl]);
|
|
991
|
-
}
|
|
992
|
-
if (tryInternalGit(context, ["remote", "get-url", LEGACY_BUILD_IT_NOW_REMOTE_NAME])) {
|
|
993
|
-
runInternalGit(context, ["remote", "remove", LEGACY_BUILD_IT_NOW_REMOTE_NAME]);
|
|
994
|
-
}
|
|
995
|
-
runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
|
|
996
|
-
runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
|
|
997
|
-
}
|
|
998
|
-
function ensureInternalSyncGit(input) {
|
|
999
|
-
validateBranchName(input.branchName);
|
|
1000
|
-
const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
|
|
1001
|
-
fs.mkdirSync(path.dirname(gitDir), { recursive: true });
|
|
1002
|
-
if (!fs.existsSync(gitDir)) {
|
|
1003
|
-
runGit(["init", "--bare", gitDir]);
|
|
1004
|
-
}
|
|
1005
|
-
const auth = internalGitAuth(input.baseUrl, input.token);
|
|
1006
|
-
const context = { gitDir, workTree: input.branchPath, auth };
|
|
1007
|
-
const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
1008
|
-
configureInternalRepo(context, remoteUrl);
|
|
1009
|
-
runInternalGit(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
|
|
1010
|
-
const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`]);
|
|
1011
|
-
const target = hasRemoteBranch ? `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}` : `refs/remotes/${R5D_REMOTE_NAME}/main`;
|
|
1012
|
-
const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
|
|
1013
|
-
if (!hasHead || !hasInternalWorktreeChanges(context)) {
|
|
1014
|
-
runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
|
|
1015
|
-
}
|
|
1016
|
-
return context;
|
|
1017
|
-
}
|
|
1018
907
|
function ensureBranchWorkspace(input) {
|
|
1019
908
|
const defaultBranch = input.manifest?.defaultBranch || "main";
|
|
1020
909
|
const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
1021
910
|
const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
|
|
1022
|
-
const branchPath = ensureVisibleGitCheckout({
|
|
1023
|
-
projectRoot: input.projectRoot,
|
|
1024
|
-
branchName: input.branchName,
|
|
1025
|
-
githubRemoteUrl,
|
|
1026
|
-
githubAuth,
|
|
1027
|
-
githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
|
|
1028
|
-
defaultBranch,
|
|
1029
|
-
reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
|
|
1030
|
-
});
|
|
1031
|
-
const internalGit = ensureInternalSyncGit({
|
|
1032
|
-
projectId: input.projectId,
|
|
1033
|
-
baseUrl: input.baseUrl,
|
|
1034
|
-
token: input.token,
|
|
1035
|
-
syncRoot: input.syncRoot,
|
|
1036
|
-
branchPath,
|
|
1037
|
-
branchName: input.branchName,
|
|
1038
|
-
manifest: input.manifest
|
|
1039
|
-
});
|
|
1040
|
-
return { branchPath, internalGit };
|
|
1041
|
-
}
|
|
1042
|
-
async function ensureVisibleGitCheckoutForRepositorySync(input) {
|
|
1043
|
-
validateBranchName(input.branchName);
|
|
1044
|
-
fs.mkdirSync(input.projectRoot, { recursive: true });
|
|
1045
|
-
const branchPath = path.join(input.projectRoot, input.branchName);
|
|
1046
|
-
const createdCheckout = !hasNormalVisibleGitDir(branchPath);
|
|
1047
|
-
if (createdCheckout) {
|
|
1048
|
-
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
1049
|
-
fs.mkdirSync(path.dirname(branchPath), { recursive: true });
|
|
1050
|
-
if (!await tryGitAsync(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
|
|
1051
|
-
fs.mkdirSync(branchPath, { recursive: true });
|
|
1052
|
-
await runGitAsync(["init"], { cwd: branchPath });
|
|
1053
|
-
}
|
|
1054
|
-
await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
|
|
1055
|
-
configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
|
|
1056
|
-
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
1057
|
-
await tryGitAsync(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
|
|
1058
|
-
checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
|
|
1059
|
-
return branchPath;
|
|
1060
|
-
}
|
|
1061
|
-
if (input.reconcileExistingOrigin) {
|
|
1062
|
-
await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
|
|
1063
|
-
}
|
|
1064
|
-
configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
|
|
1065
|
-
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
1066
|
-
return branchPath;
|
|
1067
|
-
}
|
|
1068
|
-
async function forceSyncManifestBranchFromInternal(input) {
|
|
1069
|
-
const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
1070
|
-
const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest.repoAuthHeader);
|
|
1071
|
-
const branchPath = await ensureVisibleGitCheckoutForRepositorySync({
|
|
1072
|
-
projectRoot: input.projectRoot,
|
|
1073
|
-
branchName: input.branchName,
|
|
1074
|
-
githubRemoteUrl,
|
|
1075
|
-
githubAuth,
|
|
1076
|
-
githubAuthHeader: input.manifest.repoAuthHeader,
|
|
1077
|
-
defaultBranch: input.manifest.defaultBranch || "main",
|
|
1078
|
-
reconcileExistingOrigin: Boolean(input.manifest.repoHttpUrl)
|
|
1079
|
-
});
|
|
1080
|
-
const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
|
|
1081
|
-
fs.mkdirSync(path.dirname(gitDir), { recursive: true });
|
|
1082
|
-
if (!fs.existsSync(gitDir)) {
|
|
1083
|
-
await runGitAsync(["init", "--bare", gitDir]);
|
|
1084
|
-
}
|
|
1085
|
-
const context = {
|
|
1086
|
-
gitDir,
|
|
1087
|
-
workTree: branchPath,
|
|
1088
|
-
auth: internalGitAuth(input.baseUrl, input.token)
|
|
1089
|
-
};
|
|
1090
|
-
configureInternalRepo(context, internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest));
|
|
1091
|
-
await runInternalGitAsync(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
|
|
1092
|
-
const branchTarget = `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`;
|
|
1093
|
-
const mainTarget = `refs/remotes/${R5D_REMOTE_NAME}/main`;
|
|
1094
|
-
const target = tryInternalGit(context, ["show-ref", "--verify", "--quiet", branchTarget]) ? branchTarget : tryInternalGit(context, ["show-ref", "--verify", "--quiet", mainTarget]) ? mainTarget : null;
|
|
1095
|
-
if (!target) {
|
|
1096
|
-
throw new Error(`Internal remote has neither ${input.branchName} nor main`);
|
|
1097
|
-
}
|
|
1098
|
-
await runInternalGitAsync(context, ["reset", "--hard", target]);
|
|
1099
|
-
await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
|
|
1100
|
-
return getInternalCommitHash(context);
|
|
1101
|
-
}
|
|
1102
|
-
function findRepositorySyncBlockers(input) {
|
|
1103
|
-
const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
|
|
1104
|
-
const blockedBy = [];
|
|
1105
|
-
for (const process2 of input.processes) {
|
|
1106
|
-
const target = targetByKey.get(`${process2.projectId}:${process2.branchName}`);
|
|
1107
|
-
if (target) blockedBy.push({ ...target, kind: "process", id: process2.id });
|
|
1108
|
-
}
|
|
1109
|
-
for (const shell of input.shells) {
|
|
1110
|
-
const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
|
|
1111
|
-
if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
|
|
1112
|
-
}
|
|
1113
|
-
return blockedBy;
|
|
1114
|
-
}
|
|
1115
|
-
async function syncManifestProjectsFromInternal(input) {
|
|
1116
|
-
const requestedProjectIds = input.projectIds ? new Set(input.projectIds) : null;
|
|
1117
|
-
const targets = input.manifests.filter((manifest) => !requestedProjectIds || requestedProjectIds.has(manifest.projectId)).flatMap((manifest) => {
|
|
1118
|
-
const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
|
|
1119
|
-
return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
|
|
1120
|
-
});
|
|
1121
|
-
const blockedBy = findRepositorySyncBlockers({
|
|
1122
|
-
targets: targets.map(({ manifest, branchName }) => ({
|
|
1123
|
-
projectId: manifest.projectId,
|
|
1124
|
-
projectPath: manifest.projectPath,
|
|
1125
|
-
branchName
|
|
1126
|
-
})),
|
|
1127
|
-
processes: [...activeProcesses].map(([id, active]) => ({
|
|
1128
|
-
id,
|
|
1129
|
-
projectId: active.projectId,
|
|
1130
|
-
branchName: active.branchName
|
|
1131
|
-
})),
|
|
1132
|
-
shells: [...activePtys].map(([id, active]) => ({
|
|
1133
|
-
id,
|
|
1134
|
-
projectId: active.projectId,
|
|
1135
|
-
branchName: active.branchName
|
|
1136
|
-
}))
|
|
1137
|
-
});
|
|
1138
|
-
if (blockedBy.length > 0) {
|
|
1139
|
-
return { status: "blocked", results: [], blockedBy };
|
|
1140
|
-
}
|
|
1141
|
-
const results = [];
|
|
1142
|
-
for (const { manifest, branchName } of targets) {
|
|
1143
|
-
try {
|
|
1144
|
-
const commitHash = await forceSyncManifestBranchFromInternal({
|
|
1145
|
-
projectId: manifest.projectId,
|
|
1146
|
-
baseUrl: input.baseUrl,
|
|
1147
|
-
token: input.token,
|
|
1148
|
-
projectRoot: path.join(input.projectsRoot, manifest.repoSlug || manifest.projectId),
|
|
1149
|
-
syncRoot: input.syncRoot,
|
|
1150
|
-
branchName,
|
|
1151
|
-
manifest
|
|
1152
|
-
});
|
|
1153
|
-
results.push({
|
|
1154
|
-
projectId: manifest.projectId,
|
|
1155
|
-
projectPath: manifest.projectPath,
|
|
1156
|
-
branchName,
|
|
1157
|
-
status: "synced",
|
|
1158
|
-
commitHash
|
|
1159
|
-
});
|
|
1160
|
-
} catch (error) {
|
|
1161
|
-
results.push({
|
|
1162
|
-
projectId: manifest.projectId,
|
|
1163
|
-
projectPath: manifest.projectPath,
|
|
1164
|
-
branchName,
|
|
1165
|
-
status: "failed",
|
|
1166
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
911
|
return {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
912
|
+
branchPath: ensureVisibleGitCheckout({
|
|
913
|
+
projectRoot: input.projectRoot,
|
|
914
|
+
branchName: input.branchName,
|
|
915
|
+
githubRemoteUrl,
|
|
916
|
+
githubAuth,
|
|
917
|
+
githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
|
|
918
|
+
defaultBranch,
|
|
919
|
+
reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
|
|
920
|
+
})
|
|
1174
921
|
};
|
|
1175
922
|
}
|
|
1176
923
|
function resolveCommandCwd(branchPath, cwd) {
|
|
@@ -1221,15 +968,6 @@ async function streamCommandOutput(stream, onData) {
|
|
|
1221
968
|
function ensureOperationBranch(input) {
|
|
1222
969
|
return ensureBranchWorkspace(input);
|
|
1223
970
|
}
|
|
1224
|
-
function internalGitProcessEnv(workspace, env) {
|
|
1225
|
-
if (env?.R5D_USE_INTERNAL_GIT !== "1") {
|
|
1226
|
-
return {};
|
|
1227
|
-
}
|
|
1228
|
-
return {
|
|
1229
|
-
GIT_DIR: workspace.internalGit.gitDir,
|
|
1230
|
-
GIT_WORK_TREE: workspace.branchPath
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
971
|
function formatLineNumberedContent(content, offset, limit) {
|
|
1234
972
|
const allLines = content.split("\n");
|
|
1235
973
|
const totalLines = allLines.length;
|
|
@@ -1338,9 +1076,11 @@ const fileMutationQueues = /* @__PURE__ */ new Map();
|
|
|
1338
1076
|
async function withFileMutationQueue(key, operation) {
|
|
1339
1077
|
const previous = fileMutationQueues.get(key) ?? Promise.resolve();
|
|
1340
1078
|
let release;
|
|
1341
|
-
const current = previous.catch(() => void 0).then(
|
|
1342
|
-
|
|
1343
|
-
|
|
1079
|
+
const current = previous.catch(() => void 0).then(
|
|
1080
|
+
() => new Promise((resolve) => {
|
|
1081
|
+
release = resolve;
|
|
1082
|
+
})
|
|
1083
|
+
);
|
|
1344
1084
|
fileMutationQueues.set(key, current);
|
|
1345
1085
|
await previous.catch(() => void 0);
|
|
1346
1086
|
try {
|
|
@@ -1435,7 +1175,9 @@ async function executeEditFileOperation(input) {
|
|
|
1435
1175
|
throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
|
|
1436
1176
|
}
|
|
1437
1177
|
if (occurrences > 1) {
|
|
1438
|
-
throw new Error(
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
|
|
1180
|
+
);
|
|
1439
1181
|
}
|
|
1440
1182
|
const start = originalContent.indexOf(edit.oldText);
|
|
1441
1183
|
return { index, start, end: start + edit.oldText.length, edit };
|
|
@@ -1695,96 +1437,6 @@ async function executeViewFileBytesOperation(input) {
|
|
|
1695
1437
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1696
1438
|
return readWorkerImageFile(workspace.branchPath, input.message.filePath);
|
|
1697
1439
|
}
|
|
1698
|
-
function stagedBlobSizeBytes(context) {
|
|
1699
|
-
const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
|
|
1700
|
-
let total = 0;
|
|
1701
|
-
for (const name of names) {
|
|
1702
|
-
try {
|
|
1703
|
-
const sizeText = runInternalGit(context, ["cat-file", "-s", `:${name}`]);
|
|
1704
|
-
total += Number.parseInt(sizeText, 10) || 0;
|
|
1705
|
-
} catch {
|
|
1706
|
-
}
|
|
1707
|
-
if (total > MAX_SYNC_DIFF_BYTES) {
|
|
1708
|
-
return MAX_SYNC_DIFF_BYTES + 1;
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
return total;
|
|
1712
|
-
}
|
|
1713
|
-
function syncBranch(input) {
|
|
1714
|
-
const workspace = ensureOperationBranch(input);
|
|
1715
|
-
runInternalGit(workspace.internalGit, ["add", "-A", "--", ".", ":(exclude).git"]);
|
|
1716
|
-
const hasStagedChanges = !tryInternalGit(workspace.internalGit, ["diff", "--cached", "--quiet", "--", ".", ":(exclude).git"]);
|
|
1717
|
-
const status = getInternalStatus(workspace.internalGit);
|
|
1718
|
-
const currentCommit = getInternalCommitHash(workspace.internalGit);
|
|
1719
|
-
if (!hasStagedChanges) {
|
|
1720
|
-
return {
|
|
1721
|
-
type: "sync",
|
|
1722
|
-
changed: false,
|
|
1723
|
-
commitHash: currentCommit,
|
|
1724
|
-
diffSizeBytes: 0,
|
|
1725
|
-
status
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1728
|
-
const diffSizeBytes = stagedBlobSizeBytes(workspace.internalGit);
|
|
1729
|
-
if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
|
|
1730
|
-
return {
|
|
1731
|
-
type: "sync",
|
|
1732
|
-
changed: true,
|
|
1733
|
-
commitHash: currentCommit,
|
|
1734
|
-
diffSizeBytes,
|
|
1735
|
-
status,
|
|
1736
|
-
largeDiffBlocked: true,
|
|
1737
|
-
trigger: input.trigger
|
|
1738
|
-
};
|
|
1739
|
-
}
|
|
1740
|
-
const message = JSON.stringify({
|
|
1741
|
-
type: "agent_changes",
|
|
1742
|
-
sessionId: input.sessionId,
|
|
1743
|
-
parentNodeId: input.parentNodeId,
|
|
1744
|
-
...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
|
|
1745
|
-
});
|
|
1746
|
-
runInternalGit(workspace.internalGit, ["commit", "-m", message]);
|
|
1747
|
-
const commitHash = getInternalCommitHash(workspace.internalGit);
|
|
1748
|
-
runInternalGit(workspace.internalGit, ["push", R5D_REMOTE_NAME, `HEAD:refs/heads/${input.branchName}`]);
|
|
1749
|
-
return {
|
|
1750
|
-
type: "sync",
|
|
1751
|
-
changed: true,
|
|
1752
|
-
commitHash,
|
|
1753
|
-
diffSizeBytes,
|
|
1754
|
-
status: getInternalStatus(workspace.internalGit),
|
|
1755
|
-
trigger: input.trigger
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
function confirmLargeDiff(input) {
|
|
1759
|
-
const reason = input.reason.trim();
|
|
1760
|
-
if (!reason) {
|
|
1761
|
-
throw new Error("confirm_large_diff requires a non-empty reason");
|
|
1762
|
-
}
|
|
1763
|
-
const result = syncBranch({
|
|
1764
|
-
...input,
|
|
1765
|
-
confirmedLargeDiff: true,
|
|
1766
|
-
confirmationReason: reason
|
|
1767
|
-
});
|
|
1768
|
-
return {
|
|
1769
|
-
type: "confirm_large_diff",
|
|
1770
|
-
changed: result.changed,
|
|
1771
|
-
commitHash: result.commitHash,
|
|
1772
|
-
diffSizeBytes: result.diffSizeBytes,
|
|
1773
|
-
status: result.status,
|
|
1774
|
-
reason
|
|
1775
|
-
};
|
|
1776
|
-
}
|
|
1777
|
-
function pullBranch(input) {
|
|
1778
|
-
const workspace = ensureOperationBranch(input);
|
|
1779
|
-
runInternalGit(workspace.internalGit, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
|
|
1780
|
-
const target = input.commitHash ?? `${R5D_REMOTE_NAME}/${input.branchName}`;
|
|
1781
|
-
runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
|
|
1782
|
-
runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
|
|
1783
|
-
return {
|
|
1784
|
-
type: "pull_branch",
|
|
1785
|
-
commitHash: getInternalCommitHash(workspace.internalGit)
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
1440
|
async function executeOperation(input) {
|
|
1789
1441
|
switch (input.message.type) {
|
|
1790
1442
|
case "read":
|
|
@@ -1801,43 +1453,6 @@ async function executeOperation(input) {
|
|
|
1801
1453
|
return executeLsOperation({ ...input, message: input.message });
|
|
1802
1454
|
case "view_file_bytes":
|
|
1803
1455
|
return executeViewFileBytesOperation({ ...input, message: input.message });
|
|
1804
|
-
case "sync":
|
|
1805
|
-
return syncBranch({
|
|
1806
|
-
projectId: input.projectId,
|
|
1807
|
-
baseUrl: input.baseUrl,
|
|
1808
|
-
token: input.token,
|
|
1809
|
-
projectRoot: input.projectRoot,
|
|
1810
|
-
syncRoot: input.syncRoot,
|
|
1811
|
-
branchName: input.message.branchName,
|
|
1812
|
-
sessionId: input.message.sessionId,
|
|
1813
|
-
parentNodeId: input.message.parentNodeId,
|
|
1814
|
-
trigger: input.message.trigger,
|
|
1815
|
-
manifest: input.manifest
|
|
1816
|
-
});
|
|
1817
|
-
case "confirm_large_diff":
|
|
1818
|
-
return confirmLargeDiff({
|
|
1819
|
-
projectId: input.projectId,
|
|
1820
|
-
baseUrl: input.baseUrl,
|
|
1821
|
-
token: input.token,
|
|
1822
|
-
projectRoot: input.projectRoot,
|
|
1823
|
-
syncRoot: input.syncRoot,
|
|
1824
|
-
branchName: input.message.branchName,
|
|
1825
|
-
sessionId: input.message.sessionId,
|
|
1826
|
-
parentNodeId: input.message.parentNodeId,
|
|
1827
|
-
reason: input.message.reason,
|
|
1828
|
-
manifest: input.manifest
|
|
1829
|
-
});
|
|
1830
|
-
case "pull_branch":
|
|
1831
|
-
return pullBranch({
|
|
1832
|
-
projectId: input.projectId,
|
|
1833
|
-
baseUrl: input.baseUrl,
|
|
1834
|
-
token: input.token,
|
|
1835
|
-
projectRoot: input.projectRoot,
|
|
1836
|
-
syncRoot: input.syncRoot,
|
|
1837
|
-
branchName: input.message.branchName,
|
|
1838
|
-
commitHash: input.message.commitHash,
|
|
1839
|
-
manifest: input.manifest
|
|
1840
|
-
});
|
|
1841
1456
|
}
|
|
1842
1457
|
}
|
|
1843
1458
|
async function executeCommand(input) {
|
|
@@ -1869,7 +1484,11 @@ async function executeCommand(input) {
|
|
|
1869
1484
|
planRoot: input.planRoot,
|
|
1870
1485
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1871
1486
|
});
|
|
1872
|
-
const
|
|
1487
|
+
const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
|
|
1488
|
+
if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
|
|
1489
|
+
throw new Error("The canonical workspace checkout is not initialized on this worker");
|
|
1490
|
+
}
|
|
1491
|
+
const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
|
|
1873
1492
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1874
1493
|
cwd,
|
|
1875
1494
|
stdout: "pipe",
|
|
@@ -1879,7 +1498,6 @@ async function executeCommand(input) {
|
|
|
1879
1498
|
...process.env,
|
|
1880
1499
|
...githubProcessEnv(),
|
|
1881
1500
|
...input.message.env ?? {},
|
|
1882
|
-
...internalGitProcessEnv(workspace, input.message.env),
|
|
1883
1501
|
...artifactProcessEnv,
|
|
1884
1502
|
...planProcessEnv
|
|
1885
1503
|
}
|
|
@@ -1977,7 +1595,11 @@ async function executeStreamingCommand(input) {
|
|
|
1977
1595
|
planRoot: input.planRoot,
|
|
1978
1596
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1979
1597
|
});
|
|
1980
|
-
const
|
|
1598
|
+
const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
|
|
1599
|
+
if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
|
|
1600
|
+
throw new Error("The canonical workspace checkout is not initialized on this worker");
|
|
1601
|
+
}
|
|
1602
|
+
const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
|
|
1981
1603
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1982
1604
|
cwd,
|
|
1983
1605
|
stdout: "pipe",
|
|
@@ -1987,7 +1609,6 @@ async function executeStreamingCommand(input) {
|
|
|
1987
1609
|
...process.env,
|
|
1988
1610
|
...githubProcessEnv(),
|
|
1989
1611
|
...input.message.env ?? {},
|
|
1990
|
-
...internalGitProcessEnv(workspace, input.message.env),
|
|
1991
1612
|
...artifactProcessEnv,
|
|
1992
1613
|
...planProcessEnv
|
|
1993
1614
|
}
|
|
@@ -2045,22 +1666,26 @@ async function executeStreamingCommand(input) {
|
|
|
2045
1666
|
});
|
|
2046
1667
|
})
|
|
2047
1668
|
]);
|
|
2048
|
-
|
|
1669
|
+
const terminal = {
|
|
2049
1670
|
type: "exec_exit",
|
|
2050
1671
|
runId: input.message.runId,
|
|
2051
1672
|
exitCode,
|
|
2052
1673
|
durationMs: Date.now() - startedAt,
|
|
2053
1674
|
...timedOut ? { timedOut: true } : {}
|
|
2054
|
-
}
|
|
1675
|
+
};
|
|
1676
|
+
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
1677
|
+
sendWorkerMessage(input.ws, terminal);
|
|
2055
1678
|
} catch (error) {
|
|
2056
1679
|
const message = error instanceof Error ? error.message : String(error);
|
|
2057
1680
|
if (started) {
|
|
2058
|
-
|
|
1681
|
+
const terminal = {
|
|
2059
1682
|
type: "exec_error",
|
|
2060
1683
|
runId: input.message.runId,
|
|
2061
1684
|
error: message,
|
|
2062
1685
|
durationMs: Date.now() - startedAt
|
|
2063
|
-
}
|
|
1686
|
+
};
|
|
1687
|
+
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
1688
|
+
sendWorkerMessage(input.ws, terminal);
|
|
2064
1689
|
} else {
|
|
2065
1690
|
sendWorkerMessage(input.ws, {
|
|
2066
1691
|
type: "exec_start_error",
|
|
@@ -2398,10 +2023,7 @@ function resizePty(message) {
|
|
|
2398
2023
|
if (!ptyProcess) {
|
|
2399
2024
|
return;
|
|
2400
2025
|
}
|
|
2401
|
-
ptyProcess.resize(
|
|
2402
|
-
Math.max(1, Math.min(Math.floor(message.cols || 80), 500)),
|
|
2403
|
-
Math.max(1, Math.min(Math.floor(message.rows || 24), 500))
|
|
2404
|
-
);
|
|
2026
|
+
ptyProcess.resize(Math.max(1, Math.min(Math.floor(message.cols || 80), 500)), Math.max(1, Math.min(Math.floor(message.rows || 24), 500)));
|
|
2405
2027
|
}
|
|
2406
2028
|
function closePty(message) {
|
|
2407
2029
|
const ptyProcess = activePtys.get(message.ptyId);
|
|
@@ -2421,10 +2043,13 @@ function websocketUrl(baseUrl, label) {
|
|
|
2421
2043
|
const url = new URL("/worker/ws", baseUrl);
|
|
2422
2044
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2423
2045
|
url.searchParams.set("label", label);
|
|
2046
|
+
url.searchParams.set("version", getWorkerVersion());
|
|
2424
2047
|
return url.toString();
|
|
2425
2048
|
}
|
|
2426
2049
|
function projectRootFor(projectsRoot, projectId, manifestByProjectId) {
|
|
2427
|
-
|
|
2050
|
+
const manifest = manifestByProjectId.get(projectId);
|
|
2051
|
+
if (!manifest) throw new Error(`Project ${projectId} is missing from the worker workspace manifest`);
|
|
2052
|
+
return managedProjectRoot(projectsRoot, manifest.checkoutPathSegments);
|
|
2428
2053
|
}
|
|
2429
2054
|
async function startWorker(options) {
|
|
2430
2055
|
const config = readConfig(options.configPath);
|
|
@@ -2433,6 +2058,7 @@ async function startWorker(options) {
|
|
|
2433
2058
|
validateLabel(label);
|
|
2434
2059
|
const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
2435
2060
|
const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
2061
|
+
const workspaceShadowRoot = path.join(syncRoot, "workspace");
|
|
2436
2062
|
const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
2437
2063
|
const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
|
|
2438
2064
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
@@ -2454,10 +2080,104 @@ async function startWorker(options) {
|
|
|
2454
2080
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
2455
2081
|
fs.mkdirSync(planRoot, { recursive: true });
|
|
2456
2082
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
2457
|
-
|
|
2458
|
-
let
|
|
2083
|
+
const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
|
|
2084
|
+
let workspaceRemoteUrl = null;
|
|
2085
|
+
let activeWorkspaceIncidentId = null;
|
|
2086
|
+
let previousPeriodicFingerprint = null;
|
|
2087
|
+
let periodicWorkspaceScan;
|
|
2088
|
+
let periodicWorkspaceScanInFlight = false;
|
|
2089
|
+
let terminalReplayTimer;
|
|
2090
|
+
let workspaceSyncRequestsInFlight = 0;
|
|
2459
2091
|
let cliUpdateInProgress = false;
|
|
2460
2092
|
let reloadAfterClose = false;
|
|
2093
|
+
const workspaceSyncInput = (trigger, overrides = {}) => {
|
|
2094
|
+
if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
|
|
2095
|
+
return {
|
|
2096
|
+
workerLabel: label,
|
|
2097
|
+
remoteUrl: workspaceRemoteUrl,
|
|
2098
|
+
authHeader: `Authorization: Bearer ${token}`,
|
|
2099
|
+
projectsRoot,
|
|
2100
|
+
plansRoot: planRoot,
|
|
2101
|
+
shadowRoot: workspaceShadowRoot,
|
|
2102
|
+
projects: [...manifestByProjectId.values()],
|
|
2103
|
+
trigger,
|
|
2104
|
+
...overrides
|
|
2105
|
+
};
|
|
2106
|
+
};
|
|
2107
|
+
const sendWorkspaceSyncResult = (requestId, result) => {
|
|
2108
|
+
sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
|
|
2109
|
+
};
|
|
2110
|
+
const ensureAllVisibleWorkspaceCheckouts = () => {
|
|
2111
|
+
const created = [];
|
|
2112
|
+
for (const manifest of manifestByProjectId.values()) {
|
|
2113
|
+
for (const branchName of manifest.branches) {
|
|
2114
|
+
const branchPath = path.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
|
|
2115
|
+
const existed = fs.existsSync(path.join(branchPath, ".git"));
|
|
2116
|
+
const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
|
|
2117
|
+
ensureVisibleGitCheckout({
|
|
2118
|
+
projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
|
|
2119
|
+
branchName,
|
|
2120
|
+
githubRemoteUrl,
|
|
2121
|
+
githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
|
|
2122
|
+
githubAuthHeader: manifest.repoAuthHeader,
|
|
2123
|
+
defaultBranch: manifest.defaultBranch || "main",
|
|
2124
|
+
reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
|
|
2125
|
+
});
|
|
2126
|
+
if (!existed) created.push({ projectId: manifest.projectId, branchName });
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
return created;
|
|
2130
|
+
};
|
|
2131
|
+
const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
|
|
2132
|
+
workspaceSyncRequestsInFlight += 1;
|
|
2133
|
+
try {
|
|
2134
|
+
if (trigger.type === "plan_write" || trigger.type === "task_status" || trigger.type === "connect" || trigger.type === "inbound_head" || trigger.type === "manual") {
|
|
2135
|
+
for (const manifest of manifestByProjectId.values()) {
|
|
2136
|
+
for (const branchName of manifest.branches) {
|
|
2137
|
+
await syncProjectPlans({
|
|
2138
|
+
baseUrl,
|
|
2139
|
+
token,
|
|
2140
|
+
projectId: manifest.projectId,
|
|
2141
|
+
branchName,
|
|
2142
|
+
planRoot
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
|
|
2148
|
+
const result = await workspaceSyncSingleFlight.run(
|
|
2149
|
+
workspaceSyncInput(trigger, {
|
|
2150
|
+
...overrides,
|
|
2151
|
+
...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
|
|
2152
|
+
})
|
|
2153
|
+
);
|
|
2154
|
+
previousPeriodicFingerprint = null;
|
|
2155
|
+
sendWorkspaceSyncResult(requestId, result);
|
|
2156
|
+
return result;
|
|
2157
|
+
} catch (error) {
|
|
2158
|
+
const result = {
|
|
2159
|
+
type: "workspace_sync",
|
|
2160
|
+
attemptId: overrides.attemptId ?? crypto.randomUUID(),
|
|
2161
|
+
workerLabel: label,
|
|
2162
|
+
trigger,
|
|
2163
|
+
outcome: "failed",
|
|
2164
|
+
startingHead: null,
|
|
2165
|
+
rebaseCount: 0,
|
|
2166
|
+
diffSizeBytes: 0,
|
|
2167
|
+
gitStatus: "",
|
|
2168
|
+
affectedProjects: [],
|
|
2169
|
+
affectedPaths: [],
|
|
2170
|
+
discardedPaths: [],
|
|
2171
|
+
localChangesDiscarded: false,
|
|
2172
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2173
|
+
};
|
|
2174
|
+
previousPeriodicFingerprint = null;
|
|
2175
|
+
sendWorkspaceSyncResult(requestId, result);
|
|
2176
|
+
return result;
|
|
2177
|
+
} finally {
|
|
2178
|
+
workspaceSyncRequestsInFlight -= 1;
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2461
2181
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
2462
2182
|
headers: {
|
|
2463
2183
|
Authorization: `Bearer ${token}`
|
|
@@ -2502,6 +2222,15 @@ async function startWorker(options) {
|
|
|
2502
2222
|
};
|
|
2503
2223
|
ws.send(JSON.stringify(hello));
|
|
2504
2224
|
sendActiveProcessReport(ws);
|
|
2225
|
+
for (const terminal of pendingProcessTerminals.values()) {
|
|
2226
|
+
sendWorkerMessage(ws, terminal);
|
|
2227
|
+
}
|
|
2228
|
+
terminalReplayTimer = setInterval(() => {
|
|
2229
|
+
for (const terminal of pendingProcessTerminals.values()) {
|
|
2230
|
+
sendWorkerMessage(ws, terminal);
|
|
2231
|
+
}
|
|
2232
|
+
}, 5e3);
|
|
2233
|
+
terminalReplayTimer.unref();
|
|
2505
2234
|
process.stdout.write("[r5d-worker] connected\n");
|
|
2506
2235
|
});
|
|
2507
2236
|
ws.addEventListener("message", (event) => {
|
|
@@ -2510,92 +2239,73 @@ async function startWorker(options) {
|
|
|
2510
2239
|
if (message.type === "connected") {
|
|
2511
2240
|
return;
|
|
2512
2241
|
}
|
|
2513
|
-
if (message.type === "
|
|
2242
|
+
if (message.type === "workspace_manifest") {
|
|
2514
2243
|
configureGitHubAuth(message.githubCredential);
|
|
2515
2244
|
visibleGitIdentity = message.gitIdentity;
|
|
2245
|
+
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
2246
|
+
const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
|
|
2516
2247
|
manifestByProjectId.clear();
|
|
2517
|
-
for (const project of message.projects)
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
|
|
2521
|
-
`);
|
|
2522
|
-
return;
|
|
2523
|
-
}
|
|
2524
|
-
if (message.type === "sync_projects") {
|
|
2525
|
-
if (cliUpdateInProgress) {
|
|
2526
|
-
sendWorkerMessage(ws, {
|
|
2527
|
-
type: "sync_projects_result",
|
|
2528
|
-
requestId: message.requestId,
|
|
2529
|
-
result: {
|
|
2530
|
-
status: "blocked",
|
|
2531
|
-
results: [],
|
|
2532
|
-
blockedBy: []
|
|
2533
|
-
}
|
|
2534
|
-
});
|
|
2535
|
-
return;
|
|
2536
|
-
}
|
|
2537
|
-
let syncResult = { status: "completed", results: [], blockedBy: [] };
|
|
2538
|
-
const executeSync = async () => {
|
|
2539
|
-
repositorySyncInProgress = true;
|
|
2540
|
-
try {
|
|
2541
|
-
syncResult = await syncManifestProjectsFromInternal({
|
|
2542
|
-
baseUrl,
|
|
2543
|
-
token,
|
|
2544
|
-
projectsRoot,
|
|
2545
|
-
syncRoot,
|
|
2546
|
-
manifests: [...manifestByProjectId.values()],
|
|
2547
|
-
projectIds: message.projectIds
|
|
2548
|
-
});
|
|
2549
|
-
for (const result of syncResult.results) {
|
|
2550
|
-
if (result.status !== "synced") continue;
|
|
2551
|
-
try {
|
|
2552
|
-
await syncProjectPlans({
|
|
2553
|
-
baseUrl,
|
|
2554
|
-
token,
|
|
2555
|
-
projectId: result.projectId,
|
|
2556
|
-
branchName: result.branchName,
|
|
2557
|
-
planRoot
|
|
2558
|
-
});
|
|
2559
|
-
} catch (error) {
|
|
2560
|
-
process.stderr.write(
|
|
2561
|
-
`[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
2248
|
+
for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
|
|
2249
|
+
process.stdout.write(
|
|
2250
|
+
`[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
|
|
2562
2251
|
`
|
|
2563
|
-
);
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
} finally {
|
|
2567
|
-
repositorySyncInProgress = false;
|
|
2568
|
-
}
|
|
2569
|
-
};
|
|
2570
|
-
const queued = repositorySyncQueue.then(executeSync, executeSync);
|
|
2571
|
-
repositorySyncQueue = queued.then(
|
|
2572
|
-
() => void 0,
|
|
2573
|
-
() => void 0
|
|
2574
2252
|
);
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
{
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2253
|
+
if (!periodicWorkspaceScan) {
|
|
2254
|
+
const emptyFingerprint = createHash("sha256").update("").digest("hex");
|
|
2255
|
+
periodicWorkspaceScan = setInterval(() => {
|
|
2256
|
+
if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
|
|
2257
|
+
periodicWorkspaceScanInFlight = true;
|
|
2258
|
+
void (async () => {
|
|
2259
|
+
const input = workspaceSyncInput({ type: "periodic" });
|
|
2260
|
+
const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
|
|
2261
|
+
if (fingerprint === emptyFingerprint) {
|
|
2262
|
+
previousPeriodicFingerprint = null;
|
|
2263
|
+
return;
|
|
2587
2264
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2265
|
+
if (previousPeriodicFingerprint !== fingerprint) {
|
|
2266
|
+
previousPeriodicFingerprint = fingerprint;
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
previousPeriodicFingerprint = null;
|
|
2270
|
+
await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "periodic" });
|
|
2271
|
+
})().catch((error) => {
|
|
2272
|
+
process.stderr.write(
|
|
2273
|
+
`[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
|
|
2274
|
+
`
|
|
2275
|
+
);
|
|
2276
|
+
}).finally(() => {
|
|
2277
|
+
periodicWorkspaceScanInFlight = false;
|
|
2278
|
+
});
|
|
2279
|
+
}, WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
|
|
2280
|
+
periodicWorkspaceScan.unref();
|
|
2591
2281
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
if (message.type === "sync_workspace") {
|
|
2285
|
+
await runRequestedWorkspaceSync(message.requestId, message.trigger, {
|
|
2286
|
+
attemptId: message.attemptId,
|
|
2287
|
+
confirmedLargeDiff: message.confirmedLargeDiff,
|
|
2288
|
+
confirmationReason: message.confirmationReason,
|
|
2289
|
+
resetToCanonical: message.resetToCanonical,
|
|
2290
|
+
skipVisibleMirror: message.skipVisibleMirror
|
|
2596
2291
|
});
|
|
2597
2292
|
return;
|
|
2598
2293
|
}
|
|
2294
|
+
if (message.type === "workspace_head_updated") {
|
|
2295
|
+
if (!activeWorkspaceIncidentId) {
|
|
2296
|
+
await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "inbound_head", detail: message.commitHash });
|
|
2297
|
+
}
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (message.type === "workspace_incident_updated") {
|
|
2301
|
+
activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
|
|
2302
|
+
previousPeriodicFingerprint = null;
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
if (message.type === "exec_terminal_ack") {
|
|
2306
|
+
pendingProcessTerminals.delete(message.runId);
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2599
2309
|
if (message.type === "update_clis") {
|
|
2600
2310
|
if (cliUpdateInProgress) {
|
|
2601
2311
|
sendWorkerMessage(ws, {
|
|
@@ -2605,7 +2315,7 @@ async function startWorker(options) {
|
|
|
2605
2315
|
});
|
|
2606
2316
|
return;
|
|
2607
2317
|
}
|
|
2608
|
-
if (activeProcesses.size > 0 || activePtys.size > 0 ||
|
|
2318
|
+
if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
|
|
2609
2319
|
sendWorkerMessage(ws, {
|
|
2610
2320
|
type: "update_clis_result",
|
|
2611
2321
|
requestId: message.requestId,
|
|
@@ -2675,7 +2385,6 @@ async function startWorker(options) {
|
|
|
2675
2385
|
});
|
|
2676
2386
|
return;
|
|
2677
2387
|
}
|
|
2678
|
-
await repositorySyncQueue;
|
|
2679
2388
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2680
2389
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2681
2390
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
|
|
@@ -2706,7 +2415,6 @@ async function startWorker(options) {
|
|
|
2706
2415
|
});
|
|
2707
2416
|
return;
|
|
2708
2417
|
}
|
|
2709
|
-
await repositorySyncQueue;
|
|
2710
2418
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2711
2419
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2712
2420
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
@@ -2720,6 +2428,7 @@ async function startWorker(options) {
|
|
|
2720
2428
|
syncRoot,
|
|
2721
2429
|
artifactRoot,
|
|
2722
2430
|
planRoot,
|
|
2431
|
+
workspaceShadowRoot,
|
|
2723
2432
|
manifest
|
|
2724
2433
|
});
|
|
2725
2434
|
ws.send(JSON.stringify(result));
|
|
@@ -2735,7 +2444,6 @@ async function startWorker(options) {
|
|
|
2735
2444
|
});
|
|
2736
2445
|
return;
|
|
2737
2446
|
}
|
|
2738
|
-
await repositorySyncQueue;
|
|
2739
2447
|
sendWorkerMessage(ws, {
|
|
2740
2448
|
type: "exec_accepted",
|
|
2741
2449
|
requestId: message.requestId,
|
|
@@ -2756,6 +2464,7 @@ async function startWorker(options) {
|
|
|
2756
2464
|
syncRoot,
|
|
2757
2465
|
artifactRoot,
|
|
2758
2466
|
planRoot,
|
|
2467
|
+
workspaceShadowRoot,
|
|
2759
2468
|
manifest
|
|
2760
2469
|
}).catch((error) => {
|
|
2761
2470
|
sendWorkerMessage(ws, {
|
|
@@ -2775,9 +2484,8 @@ async function startWorker(options) {
|
|
|
2775
2484
|
}
|
|
2776
2485
|
return;
|
|
2777
2486
|
}
|
|
2778
|
-
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes"
|
|
2487
|
+
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
|
|
2779
2488
|
try {
|
|
2780
|
-
await repositorySyncQueue;
|
|
2781
2489
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2782
2490
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2783
2491
|
const result = await executeOperation({
|
|
@@ -2825,6 +2533,14 @@ async function startWorker(options) {
|
|
|
2825
2533
|
});
|
|
2826
2534
|
ws.addEventListener("close", (event) => {
|
|
2827
2535
|
stopHeartbeatWatchdog();
|
|
2536
|
+
if (periodicWorkspaceScan) {
|
|
2537
|
+
clearInterval(periodicWorkspaceScan);
|
|
2538
|
+
periodicWorkspaceScan = void 0;
|
|
2539
|
+
}
|
|
2540
|
+
if (terminalReplayTimer) {
|
|
2541
|
+
clearInterval(terminalReplayTimer);
|
|
2542
|
+
terminalReplayTimer = void 0;
|
|
2543
|
+
}
|
|
2828
2544
|
if (currentWorkerSocket === ws) {
|
|
2829
2545
|
currentWorkerSocket = null;
|
|
2830
2546
|
}
|
|
@@ -2835,15 +2551,17 @@ async function startWorker(options) {
|
|
|
2835
2551
|
if (reloadAfterClose) {
|
|
2836
2552
|
process.exit(WORKER_RELOAD_EXIT_CODE);
|
|
2837
2553
|
}
|
|
2838
|
-
if (activeProcesses.size > 0) {
|
|
2554
|
+
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
2839
2555
|
const delayMs = 2e3;
|
|
2840
|
-
process.stderr.write(
|
|
2841
|
-
`);
|
|
2556
|
+
process.stderr.write(
|
|
2557
|
+
`[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${delayMs}ms
|
|
2558
|
+
`
|
|
2559
|
+
);
|
|
2842
2560
|
const reconnect = () => {
|
|
2843
2561
|
void startWorker(options).catch((error) => {
|
|
2844
2562
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
2845
2563
|
`);
|
|
2846
|
-
if (activeProcesses.size > 0) {
|
|
2564
|
+
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
2847
2565
|
setTimeout(reconnect, delayMs);
|
|
2848
2566
|
return;
|
|
2849
2567
|
}
|
|
@@ -2898,7 +2616,6 @@ if (isCliEntrypoint()) {
|
|
|
2898
2616
|
}
|
|
2899
2617
|
export {
|
|
2900
2618
|
ensureVisibleGitCheckout,
|
|
2901
|
-
findRepositorySyncBlockers,
|
|
2902
2619
|
findWorkerFiles,
|
|
2903
2620
|
githubCliEnv,
|
|
2904
2621
|
grepWorkerFiles,
|
|
@@ -2911,7 +2628,6 @@ export {
|
|
|
2911
2628
|
resolveHostShell,
|
|
2912
2629
|
resolveProjectFilePath,
|
|
2913
2630
|
resolveWorkerFilePath,
|
|
2914
|
-
syncManifestProjectsFromInternal,
|
|
2915
2631
|
syncProjectPlans,
|
|
2916
2632
|
syncSessionArtifacts
|
|
2917
2633
|
};
|