@ricsam/r5d-worker 0.0.36 → 0.0.37
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/dist/cjs/main.cjs +225 -520
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-sync.cjs +652 -0
- package/dist/mjs/main.mjs +230 -518
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-sync.mjs +607 -0
- package/dist/types/main.d.ts +0 -50
- package/dist/types/workspace-sync.d.ts +73 -0
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -9,6 +9,12 @@ 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 {
|
|
13
|
+
WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
|
|
14
|
+
WorkspaceSyncSingleFlight,
|
|
15
|
+
calculateWorkspaceDiffFingerprint,
|
|
16
|
+
synchronizeWorkspace
|
|
17
|
+
} from "./workspace-sync.mjs";
|
|
12
18
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
13
19
|
const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
14
20
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
@@ -16,10 +22,8 @@ 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
|
}
|
|
@@ -589,27 +597,6 @@ function runGit(args, options = {}) {
|
|
|
589
597
|
}
|
|
590
598
|
return result.stdout.toString().trim();
|
|
591
599
|
}
|
|
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
600
|
function tryGit(args, options = {}) {
|
|
614
601
|
try {
|
|
615
602
|
runGit(args, options);
|
|
@@ -618,34 +605,6 @@ function tryGit(args, options = {}) {
|
|
|
618
605
|
return false;
|
|
619
606
|
}
|
|
620
607
|
}
|
|
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
608
|
function getGitBlobHashForContent(content) {
|
|
650
609
|
const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
651
610
|
return createHash("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
|
|
@@ -653,12 +612,6 @@ function getGitBlobHashForContent(content) {
|
|
|
653
612
|
function hasWorktreeChanges(cwd) {
|
|
654
613
|
return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
|
|
655
614
|
}
|
|
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
615
|
function isInsideBranchPath(branchPath, filePath) {
|
|
663
616
|
const relative = path.relative(path.resolve(branchPath), path.resolve(filePath));
|
|
664
617
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
@@ -721,32 +674,15 @@ function resolveWorkerFilePath(branchPath, inputPath) {
|
|
|
721
674
|
function resolveProjectFilePath(branchPath, inputPath) {
|
|
722
675
|
const resolved = resolveWorkerFilePath(branchPath, inputPath);
|
|
723
676
|
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
|
-
);
|
|
677
|
+
throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
|
|
727
678
|
}
|
|
728
679
|
return resolved;
|
|
729
680
|
}
|
|
730
|
-
function resolveRemoteUrl(baseUrl, remoteUrl) {
|
|
731
|
-
if (/^https?:\/\//i.test(remoteUrl)) {
|
|
732
|
-
return remoteUrl;
|
|
733
|
-
}
|
|
734
|
-
return new URL(remoteUrl, baseUrl).toString();
|
|
735
|
-
}
|
|
736
681
|
function fallbackInternalRemoteUrl(baseUrl, projectId) {
|
|
737
682
|
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
738
683
|
}
|
|
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
684
|
function githubRemoteUrlFor(baseUrl, projectId, manifest) {
|
|
749
|
-
return manifest?.repoHttpUrl ??
|
|
685
|
+
return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
|
|
750
686
|
}
|
|
751
687
|
function gitExtraHeaderUrlForRemote(remoteUrl) {
|
|
752
688
|
try {
|
|
@@ -764,12 +700,6 @@ function gitAuthForRemote(remoteUrl, authHeader) {
|
|
|
764
700
|
header: authHeader
|
|
765
701
|
};
|
|
766
702
|
}
|
|
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
703
|
function hasNormalVisibleGitDir(branchPath) {
|
|
774
704
|
const gitPath = path.join(branchPath, ".git");
|
|
775
705
|
return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
|
|
@@ -781,13 +711,6 @@ function ensureOriginRemote(branchPath, remoteUrl) {
|
|
|
781
711
|
runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
|
|
782
712
|
}
|
|
783
713
|
}
|
|
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
714
|
function shellQuote(value) {
|
|
792
715
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
793
716
|
}
|
|
@@ -983,194 +906,20 @@ function ensureVisibleGitCheckout(input) {
|
|
|
983
906
|
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
984
907
|
return branchPath;
|
|
985
908
|
}
|
|
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
909
|
function ensureBranchWorkspace(input) {
|
|
1019
910
|
const defaultBranch = input.manifest?.defaultBranch || "main";
|
|
1020
911
|
const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
1021
912
|
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
913
|
return {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
914
|
+
branchPath: ensureVisibleGitCheckout({
|
|
915
|
+
projectRoot: input.projectRoot,
|
|
916
|
+
branchName: input.branchName,
|
|
917
|
+
githubRemoteUrl,
|
|
918
|
+
githubAuth,
|
|
919
|
+
githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
|
|
920
|
+
defaultBranch,
|
|
921
|
+
reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
|
|
922
|
+
})
|
|
1174
923
|
};
|
|
1175
924
|
}
|
|
1176
925
|
function resolveCommandCwd(branchPath, cwd) {
|
|
@@ -1221,15 +970,6 @@ async function streamCommandOutput(stream, onData) {
|
|
|
1221
970
|
function ensureOperationBranch(input) {
|
|
1222
971
|
return ensureBranchWorkspace(input);
|
|
1223
972
|
}
|
|
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
973
|
function formatLineNumberedContent(content, offset, limit) {
|
|
1234
974
|
const allLines = content.split("\n");
|
|
1235
975
|
const totalLines = allLines.length;
|
|
@@ -1338,9 +1078,11 @@ const fileMutationQueues = /* @__PURE__ */ new Map();
|
|
|
1338
1078
|
async function withFileMutationQueue(key, operation) {
|
|
1339
1079
|
const previous = fileMutationQueues.get(key) ?? Promise.resolve();
|
|
1340
1080
|
let release;
|
|
1341
|
-
const current = previous.catch(() => void 0).then(
|
|
1342
|
-
|
|
1343
|
-
|
|
1081
|
+
const current = previous.catch(() => void 0).then(
|
|
1082
|
+
() => new Promise((resolve) => {
|
|
1083
|
+
release = resolve;
|
|
1084
|
+
})
|
|
1085
|
+
);
|
|
1344
1086
|
fileMutationQueues.set(key, current);
|
|
1345
1087
|
await previous.catch(() => void 0);
|
|
1346
1088
|
try {
|
|
@@ -1435,7 +1177,9 @@ async function executeEditFileOperation(input) {
|
|
|
1435
1177
|
throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
|
|
1436
1178
|
}
|
|
1437
1179
|
if (occurrences > 1) {
|
|
1438
|
-
throw new Error(
|
|
1180
|
+
throw new Error(
|
|
1181
|
+
`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
|
|
1182
|
+
);
|
|
1439
1183
|
}
|
|
1440
1184
|
const start = originalContent.indexOf(edit.oldText);
|
|
1441
1185
|
return { index, start, end: start + edit.oldText.length, edit };
|
|
@@ -1695,96 +1439,6 @@ async function executeViewFileBytesOperation(input) {
|
|
|
1695
1439
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1696
1440
|
return readWorkerImageFile(workspace.branchPath, input.message.filePath);
|
|
1697
1441
|
}
|
|
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
1442
|
async function executeOperation(input) {
|
|
1789
1443
|
switch (input.message.type) {
|
|
1790
1444
|
case "read":
|
|
@@ -1801,43 +1455,6 @@ async function executeOperation(input) {
|
|
|
1801
1455
|
return executeLsOperation({ ...input, message: input.message });
|
|
1802
1456
|
case "view_file_bytes":
|
|
1803
1457
|
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
1458
|
}
|
|
1842
1459
|
}
|
|
1843
1460
|
async function executeCommand(input) {
|
|
@@ -1869,7 +1486,11 @@ async function executeCommand(input) {
|
|
|
1869
1486
|
planRoot: input.planRoot,
|
|
1870
1487
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1871
1488
|
});
|
|
1872
|
-
const
|
|
1489
|
+
const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
|
|
1490
|
+
if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
|
|
1491
|
+
throw new Error("The canonical workspace checkout is not initialized on this worker");
|
|
1492
|
+
}
|
|
1493
|
+
const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
|
|
1873
1494
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1874
1495
|
cwd,
|
|
1875
1496
|
stdout: "pipe",
|
|
@@ -1879,7 +1500,6 @@ async function executeCommand(input) {
|
|
|
1879
1500
|
...process.env,
|
|
1880
1501
|
...githubProcessEnv(),
|
|
1881
1502
|
...input.message.env ?? {},
|
|
1882
|
-
...internalGitProcessEnv(workspace, input.message.env),
|
|
1883
1503
|
...artifactProcessEnv,
|
|
1884
1504
|
...planProcessEnv
|
|
1885
1505
|
}
|
|
@@ -1977,7 +1597,11 @@ async function executeStreamingCommand(input) {
|
|
|
1977
1597
|
planRoot: input.planRoot,
|
|
1978
1598
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1979
1599
|
});
|
|
1980
|
-
const
|
|
1600
|
+
const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
|
|
1601
|
+
if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
|
|
1602
|
+
throw new Error("The canonical workspace checkout is not initialized on this worker");
|
|
1603
|
+
}
|
|
1604
|
+
const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
|
|
1981
1605
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1982
1606
|
cwd,
|
|
1983
1607
|
stdout: "pipe",
|
|
@@ -1987,7 +1611,6 @@ async function executeStreamingCommand(input) {
|
|
|
1987
1611
|
...process.env,
|
|
1988
1612
|
...githubProcessEnv(),
|
|
1989
1613
|
...input.message.env ?? {},
|
|
1990
|
-
...internalGitProcessEnv(workspace, input.message.env),
|
|
1991
1614
|
...artifactProcessEnv,
|
|
1992
1615
|
...planProcessEnv
|
|
1993
1616
|
}
|
|
@@ -2045,22 +1668,26 @@ async function executeStreamingCommand(input) {
|
|
|
2045
1668
|
});
|
|
2046
1669
|
})
|
|
2047
1670
|
]);
|
|
2048
|
-
|
|
1671
|
+
const terminal = {
|
|
2049
1672
|
type: "exec_exit",
|
|
2050
1673
|
runId: input.message.runId,
|
|
2051
1674
|
exitCode,
|
|
2052
1675
|
durationMs: Date.now() - startedAt,
|
|
2053
1676
|
...timedOut ? { timedOut: true } : {}
|
|
2054
|
-
}
|
|
1677
|
+
};
|
|
1678
|
+
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
1679
|
+
sendWorkerMessage(input.ws, terminal);
|
|
2055
1680
|
} catch (error) {
|
|
2056
1681
|
const message = error instanceof Error ? error.message : String(error);
|
|
2057
1682
|
if (started) {
|
|
2058
|
-
|
|
1683
|
+
const terminal = {
|
|
2059
1684
|
type: "exec_error",
|
|
2060
1685
|
runId: input.message.runId,
|
|
2061
1686
|
error: message,
|
|
2062
1687
|
durationMs: Date.now() - startedAt
|
|
2063
|
-
}
|
|
1688
|
+
};
|
|
1689
|
+
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
1690
|
+
sendWorkerMessage(input.ws, terminal);
|
|
2064
1691
|
} else {
|
|
2065
1692
|
sendWorkerMessage(input.ws, {
|
|
2066
1693
|
type: "exec_start_error",
|
|
@@ -2398,10 +2025,7 @@ function resizePty(message) {
|
|
|
2398
2025
|
if (!ptyProcess) {
|
|
2399
2026
|
return;
|
|
2400
2027
|
}
|
|
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
|
-
);
|
|
2028
|
+
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
2029
|
}
|
|
2406
2030
|
function closePty(message) {
|
|
2407
2031
|
const ptyProcess = activePtys.get(message.ptyId);
|
|
@@ -2433,6 +2057,7 @@ async function startWorker(options) {
|
|
|
2433
2057
|
validateLabel(label);
|
|
2434
2058
|
const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
2435
2059
|
const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
2060
|
+
const workspaceShadowRoot = path.join(syncRoot, "workspace");
|
|
2436
2061
|
const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
2437
2062
|
const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
|
|
2438
2063
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
@@ -2454,10 +2079,104 @@ async function startWorker(options) {
|
|
|
2454
2079
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
2455
2080
|
fs.mkdirSync(planRoot, { recursive: true });
|
|
2456
2081
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
2457
|
-
|
|
2458
|
-
let
|
|
2082
|
+
const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
|
|
2083
|
+
let workspaceRemoteUrl = null;
|
|
2084
|
+
let activeWorkspaceIncidentId = null;
|
|
2085
|
+
let previousPeriodicFingerprint = null;
|
|
2086
|
+
let periodicWorkspaceScan;
|
|
2087
|
+
let periodicWorkspaceScanInFlight = false;
|
|
2088
|
+
let terminalReplayTimer;
|
|
2089
|
+
let workspaceSyncRequestsInFlight = 0;
|
|
2459
2090
|
let cliUpdateInProgress = false;
|
|
2460
2091
|
let reloadAfterClose = false;
|
|
2092
|
+
const workspaceSyncInput = (trigger, overrides = {}) => {
|
|
2093
|
+
if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
|
|
2094
|
+
return {
|
|
2095
|
+
workerLabel: label,
|
|
2096
|
+
remoteUrl: workspaceRemoteUrl,
|
|
2097
|
+
authHeader: `Authorization: Bearer ${token}`,
|
|
2098
|
+
projectsRoot,
|
|
2099
|
+
plansRoot: planRoot,
|
|
2100
|
+
shadowRoot: workspaceShadowRoot,
|
|
2101
|
+
projects: [...manifestByProjectId.values()],
|
|
2102
|
+
trigger,
|
|
2103
|
+
...overrides
|
|
2104
|
+
};
|
|
2105
|
+
};
|
|
2106
|
+
const sendWorkspaceSyncResult = (requestId, result) => {
|
|
2107
|
+
sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
|
|
2108
|
+
};
|
|
2109
|
+
const ensureAllVisibleWorkspaceCheckouts = () => {
|
|
2110
|
+
const created = [];
|
|
2111
|
+
for (const manifest of manifestByProjectId.values()) {
|
|
2112
|
+
for (const branchName of manifest.branches) {
|
|
2113
|
+
const branchPath = path.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
|
|
2114
|
+
const existed = fs.existsSync(path.join(branchPath, ".git"));
|
|
2115
|
+
const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
|
|
2116
|
+
ensureVisibleGitCheckout({
|
|
2117
|
+
projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
|
|
2118
|
+
branchName,
|
|
2119
|
+
githubRemoteUrl,
|
|
2120
|
+
githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
|
|
2121
|
+
githubAuthHeader: manifest.repoAuthHeader,
|
|
2122
|
+
defaultBranch: manifest.defaultBranch || "main",
|
|
2123
|
+
reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
|
|
2124
|
+
});
|
|
2125
|
+
if (!existed) created.push({ projectId: manifest.projectId, branchName });
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
return created;
|
|
2129
|
+
};
|
|
2130
|
+
const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
|
|
2131
|
+
workspaceSyncRequestsInFlight += 1;
|
|
2132
|
+
try {
|
|
2133
|
+
if (trigger.type === "plan_write" || trigger.type === "task_status" || trigger.type === "connect" || trigger.type === "inbound_head" || trigger.type === "manual") {
|
|
2134
|
+
for (const manifest of manifestByProjectId.values()) {
|
|
2135
|
+
for (const branchName of manifest.branches) {
|
|
2136
|
+
await syncProjectPlans({
|
|
2137
|
+
baseUrl,
|
|
2138
|
+
token,
|
|
2139
|
+
projectId: manifest.projectId,
|
|
2140
|
+
branchName,
|
|
2141
|
+
planRoot
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
|
|
2147
|
+
const result = await workspaceSyncSingleFlight.run(
|
|
2148
|
+
workspaceSyncInput(trigger, {
|
|
2149
|
+
...overrides,
|
|
2150
|
+
...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
|
|
2151
|
+
})
|
|
2152
|
+
);
|
|
2153
|
+
previousPeriodicFingerprint = null;
|
|
2154
|
+
sendWorkspaceSyncResult(requestId, result);
|
|
2155
|
+
return result;
|
|
2156
|
+
} catch (error) {
|
|
2157
|
+
const result = {
|
|
2158
|
+
type: "workspace_sync",
|
|
2159
|
+
attemptId: overrides.attemptId ?? crypto.randomUUID(),
|
|
2160
|
+
workerLabel: label,
|
|
2161
|
+
trigger,
|
|
2162
|
+
outcome: "failed",
|
|
2163
|
+
startingHead: null,
|
|
2164
|
+
rebaseCount: 0,
|
|
2165
|
+
diffSizeBytes: 0,
|
|
2166
|
+
gitStatus: "",
|
|
2167
|
+
affectedProjects: [],
|
|
2168
|
+
affectedPaths: [],
|
|
2169
|
+
discardedPaths: [],
|
|
2170
|
+
localChangesDiscarded: false,
|
|
2171
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2172
|
+
};
|
|
2173
|
+
previousPeriodicFingerprint = null;
|
|
2174
|
+
sendWorkspaceSyncResult(requestId, result);
|
|
2175
|
+
return result;
|
|
2176
|
+
} finally {
|
|
2177
|
+
workspaceSyncRequestsInFlight -= 1;
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2461
2180
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
2462
2181
|
headers: {
|
|
2463
2182
|
Authorization: `Bearer ${token}`
|
|
@@ -2502,6 +2221,15 @@ async function startWorker(options) {
|
|
|
2502
2221
|
};
|
|
2503
2222
|
ws.send(JSON.stringify(hello));
|
|
2504
2223
|
sendActiveProcessReport(ws);
|
|
2224
|
+
for (const terminal of pendingProcessTerminals.values()) {
|
|
2225
|
+
sendWorkerMessage(ws, terminal);
|
|
2226
|
+
}
|
|
2227
|
+
terminalReplayTimer = setInterval(() => {
|
|
2228
|
+
for (const terminal of pendingProcessTerminals.values()) {
|
|
2229
|
+
sendWorkerMessage(ws, terminal);
|
|
2230
|
+
}
|
|
2231
|
+
}, 5e3);
|
|
2232
|
+
terminalReplayTimer.unref();
|
|
2505
2233
|
process.stdout.write("[r5d-worker] connected\n");
|
|
2506
2234
|
});
|
|
2507
2235
|
ws.addEventListener("message", (event) => {
|
|
@@ -2510,92 +2238,70 @@ async function startWorker(options) {
|
|
|
2510
2238
|
if (message.type === "connected") {
|
|
2511
2239
|
return;
|
|
2512
2240
|
}
|
|
2513
|
-
if (message.type === "
|
|
2241
|
+
if (message.type === "workspace_manifest") {
|
|
2514
2242
|
configureGitHubAuth(message.githubCredential);
|
|
2515
2243
|
visibleGitIdentity = message.gitIdentity;
|
|
2244
|
+
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
2516
2245
|
manifestByProjectId.clear();
|
|
2517
|
-
for (const project of message.projects)
|
|
2518
|
-
|
|
2519
|
-
}
|
|
2520
|
-
process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
|
|
2246
|
+
for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
|
|
2247
|
+
process.stdout.write(`[r5d-worker] workspace manifest: ${message.projects.length} projects
|
|
2521
2248
|
`);
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
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)}
|
|
2562
|
-
`
|
|
2563
|
-
);
|
|
2249
|
+
if (!periodicWorkspaceScan) {
|
|
2250
|
+
const emptyFingerprint = createHash("sha256").update("").digest("hex");
|
|
2251
|
+
periodicWorkspaceScan = setInterval(() => {
|
|
2252
|
+
if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
|
|
2253
|
+
periodicWorkspaceScanInFlight = true;
|
|
2254
|
+
void (async () => {
|
|
2255
|
+
const input = workspaceSyncInput({ type: "periodic" });
|
|
2256
|
+
const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
|
|
2257
|
+
if (fingerprint === emptyFingerprint) {
|
|
2258
|
+
previousPeriodicFingerprint = null;
|
|
2259
|
+
return;
|
|
2564
2260
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
}
|
|
2569
|
-
};
|
|
2570
|
-
const queued = repositorySyncQueue.then(executeSync, executeSync);
|
|
2571
|
-
repositorySyncQueue = queued.then(
|
|
2572
|
-
() => void 0,
|
|
2573
|
-
() => void 0
|
|
2574
|
-
);
|
|
2575
|
-
try {
|
|
2576
|
-
await queued;
|
|
2577
|
-
} catch (error) {
|
|
2578
|
-
syncResult = {
|
|
2579
|
-
status: "partial",
|
|
2580
|
-
results: [
|
|
2581
|
-
{
|
|
2582
|
-
projectId: "worker",
|
|
2583
|
-
projectPath: "worker",
|
|
2584
|
-
branchName: "all",
|
|
2585
|
-
status: "failed",
|
|
2586
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2261
|
+
if (previousPeriodicFingerprint !== fingerprint) {
|
|
2262
|
+
previousPeriodicFingerprint = fingerprint;
|
|
2263
|
+
return;
|
|
2587
2264
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2265
|
+
previousPeriodicFingerprint = null;
|
|
2266
|
+
await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "periodic" });
|
|
2267
|
+
})().catch((error) => {
|
|
2268
|
+
process.stderr.write(
|
|
2269
|
+
`[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
|
|
2270
|
+
`
|
|
2271
|
+
);
|
|
2272
|
+
}).finally(() => {
|
|
2273
|
+
periodicWorkspaceScanInFlight = false;
|
|
2274
|
+
});
|
|
2275
|
+
}, WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
|
|
2276
|
+
periodicWorkspaceScan.unref();
|
|
2591
2277
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (message.type === "sync_workspace") {
|
|
2281
|
+
await runRequestedWorkspaceSync(message.requestId, message.trigger, {
|
|
2282
|
+
attemptId: message.attemptId,
|
|
2283
|
+
confirmedLargeDiff: message.confirmedLargeDiff,
|
|
2284
|
+
confirmationReason: message.confirmationReason,
|
|
2285
|
+
resetToCanonical: message.resetToCanonical,
|
|
2286
|
+
skipVisibleMirror: message.skipVisibleMirror
|
|
2596
2287
|
});
|
|
2597
2288
|
return;
|
|
2598
2289
|
}
|
|
2290
|
+
if (message.type === "workspace_head_updated") {
|
|
2291
|
+
if (!activeWorkspaceIncidentId) {
|
|
2292
|
+
await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "inbound_head", detail: message.commitHash });
|
|
2293
|
+
}
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
if (message.type === "workspace_incident_updated") {
|
|
2297
|
+
activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
|
|
2298
|
+
previousPeriodicFingerprint = null;
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
if (message.type === "exec_terminal_ack") {
|
|
2302
|
+
pendingProcessTerminals.delete(message.runId);
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2599
2305
|
if (message.type === "update_clis") {
|
|
2600
2306
|
if (cliUpdateInProgress) {
|
|
2601
2307
|
sendWorkerMessage(ws, {
|
|
@@ -2605,7 +2311,7 @@ async function startWorker(options) {
|
|
|
2605
2311
|
});
|
|
2606
2312
|
return;
|
|
2607
2313
|
}
|
|
2608
|
-
if (activeProcesses.size > 0 || activePtys.size > 0 ||
|
|
2314
|
+
if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
|
|
2609
2315
|
sendWorkerMessage(ws, {
|
|
2610
2316
|
type: "update_clis_result",
|
|
2611
2317
|
requestId: message.requestId,
|
|
@@ -2675,7 +2381,6 @@ async function startWorker(options) {
|
|
|
2675
2381
|
});
|
|
2676
2382
|
return;
|
|
2677
2383
|
}
|
|
2678
|
-
await repositorySyncQueue;
|
|
2679
2384
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2680
2385
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2681
2386
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
|
|
@@ -2706,7 +2411,6 @@ async function startWorker(options) {
|
|
|
2706
2411
|
});
|
|
2707
2412
|
return;
|
|
2708
2413
|
}
|
|
2709
|
-
await repositorySyncQueue;
|
|
2710
2414
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2711
2415
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2712
2416
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
@@ -2720,6 +2424,7 @@ async function startWorker(options) {
|
|
|
2720
2424
|
syncRoot,
|
|
2721
2425
|
artifactRoot,
|
|
2722
2426
|
planRoot,
|
|
2427
|
+
workspaceShadowRoot,
|
|
2723
2428
|
manifest
|
|
2724
2429
|
});
|
|
2725
2430
|
ws.send(JSON.stringify(result));
|
|
@@ -2735,7 +2440,6 @@ async function startWorker(options) {
|
|
|
2735
2440
|
});
|
|
2736
2441
|
return;
|
|
2737
2442
|
}
|
|
2738
|
-
await repositorySyncQueue;
|
|
2739
2443
|
sendWorkerMessage(ws, {
|
|
2740
2444
|
type: "exec_accepted",
|
|
2741
2445
|
requestId: message.requestId,
|
|
@@ -2756,6 +2460,7 @@ async function startWorker(options) {
|
|
|
2756
2460
|
syncRoot,
|
|
2757
2461
|
artifactRoot,
|
|
2758
2462
|
planRoot,
|
|
2463
|
+
workspaceShadowRoot,
|
|
2759
2464
|
manifest
|
|
2760
2465
|
}).catch((error) => {
|
|
2761
2466
|
sendWorkerMessage(ws, {
|
|
@@ -2775,9 +2480,8 @@ async function startWorker(options) {
|
|
|
2775
2480
|
}
|
|
2776
2481
|
return;
|
|
2777
2482
|
}
|
|
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"
|
|
2483
|
+
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
2484
|
try {
|
|
2780
|
-
await repositorySyncQueue;
|
|
2781
2485
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2782
2486
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2783
2487
|
const result = await executeOperation({
|
|
@@ -2825,6 +2529,14 @@ async function startWorker(options) {
|
|
|
2825
2529
|
});
|
|
2826
2530
|
ws.addEventListener("close", (event) => {
|
|
2827
2531
|
stopHeartbeatWatchdog();
|
|
2532
|
+
if (periodicWorkspaceScan) {
|
|
2533
|
+
clearInterval(periodicWorkspaceScan);
|
|
2534
|
+
periodicWorkspaceScan = void 0;
|
|
2535
|
+
}
|
|
2536
|
+
if (terminalReplayTimer) {
|
|
2537
|
+
clearInterval(terminalReplayTimer);
|
|
2538
|
+
terminalReplayTimer = void 0;
|
|
2539
|
+
}
|
|
2828
2540
|
if (currentWorkerSocket === ws) {
|
|
2829
2541
|
currentWorkerSocket = null;
|
|
2830
2542
|
}
|
|
@@ -2835,15 +2547,17 @@ async function startWorker(options) {
|
|
|
2835
2547
|
if (reloadAfterClose) {
|
|
2836
2548
|
process.exit(WORKER_RELOAD_EXIT_CODE);
|
|
2837
2549
|
}
|
|
2838
|
-
if (activeProcesses.size > 0) {
|
|
2550
|
+
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
2839
2551
|
const delayMs = 2e3;
|
|
2840
|
-
process.stderr.write(
|
|
2841
|
-
`);
|
|
2552
|
+
process.stderr.write(
|
|
2553
|
+
`[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${delayMs}ms
|
|
2554
|
+
`
|
|
2555
|
+
);
|
|
2842
2556
|
const reconnect = () => {
|
|
2843
2557
|
void startWorker(options).catch((error) => {
|
|
2844
2558
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
2845
2559
|
`);
|
|
2846
|
-
if (activeProcesses.size > 0) {
|
|
2560
|
+
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
2847
2561
|
setTimeout(reconnect, delayMs);
|
|
2848
2562
|
return;
|
|
2849
2563
|
}
|
|
@@ -2898,7 +2612,6 @@ if (isCliEntrypoint()) {
|
|
|
2898
2612
|
}
|
|
2899
2613
|
export {
|
|
2900
2614
|
ensureVisibleGitCheckout,
|
|
2901
|
-
findRepositorySyncBlockers,
|
|
2902
2615
|
findWorkerFiles,
|
|
2903
2616
|
githubCliEnv,
|
|
2904
2617
|
grepWorkerFiles,
|
|
@@ -2911,7 +2624,6 @@ export {
|
|
|
2911
2624
|
resolveHostShell,
|
|
2912
2625
|
resolveProjectFilePath,
|
|
2913
2626
|
resolveWorkerFilePath,
|
|
2914
|
-
syncManifestProjectsFromInternal,
|
|
2915
2627
|
syncProjectPlans,
|
|
2916
2628
|
syncSessionArtifacts
|
|
2917
2629
|
};
|