@ricsam/r5d-worker 0.0.35 → 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 CHANGED
@@ -30,11 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  var main_exports = {};
31
31
  __export(main_exports, {
32
32
  ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
33
- findRepositorySyncBlockers: () => findRepositorySyncBlockers,
34
33
  findWorkerFiles: () => findWorkerFiles,
35
34
  githubCliEnv: () => githubCliEnv,
36
35
  grepWorkerFiles: () => grepWorkerFiles,
37
- inspectManagedWorkspace: () => inspectManagedWorkspace,
38
36
  isArtifactEnvPath: () => isArtifactEnvPath,
39
37
  listWorkerDirectory: () => listWorkerDirectory,
40
38
  prepareArtifactEnvForShell: () => prepareArtifactEnvForShell,
@@ -42,11 +40,8 @@ __export(main_exports, {
42
40
  readWorkerImageFile: () => readWorkerImageFile,
43
41
  readWorkerTextFile: () => readWorkerTextFile,
44
42
  resolveHostShell: () => resolveHostShell,
45
- resolveManagedCheckoutPath: () => resolveManagedCheckoutPath,
46
43
  resolveProjectFilePath: () => resolveProjectFilePath,
47
44
  resolveWorkerFilePath: () => resolveWorkerFilePath,
48
- selectManifestSyncTargets: () => selectManifestSyncTargets,
49
- syncManifestProjectsFromInternal: () => syncManifestProjectsFromInternal,
50
45
  syncProjectPlans: () => syncProjectPlans,
51
46
  syncSessionArtifacts: () => syncSessionArtifacts
52
47
  });
@@ -61,6 +56,7 @@ var import_git_identity = require("./git-identity.cjs");
61
56
  var import_heartbeat = require("./heartbeat.cjs");
62
57
  var import_process_tree = require("./process-tree.cjs");
63
58
  var import_supervisor = require("./supervisor.cjs");
59
+ var import_workspace_sync = require("./workspace-sync.cjs");
64
60
  const import_meta = {};
65
61
  const DEFAULT_BASE_URL = "https://r5d.dev";
66
62
  const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
@@ -69,10 +65,8 @@ const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
69
65
  const DEFAULT_READ_LIMIT = 2e3;
70
66
  const DEFAULT_READ_MAX_BYTES = 5e4;
71
67
  const MAX_LINE_LENGTH = 2e3;
72
- const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
73
- const R5D_REMOTE_NAME = "r5d";
74
- const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
75
68
  const activeProcesses = /* @__PURE__ */ new Map();
69
+ const pendingProcessTerminals = /* @__PURE__ */ new Map();
76
70
  const cancelledProcessRuns = /* @__PURE__ */ new Set();
77
71
  const activePtys = /* @__PURE__ */ new Map();
78
72
  let currentWorkerSocket = null;
@@ -531,8 +525,10 @@ async function prepareArtifactEnvForShell(input) {
531
525
  artifactRoot: input.artifactRoot
532
526
  });
533
527
  } catch (error) {
534
- process.stderr.write(`[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
535
- `);
528
+ process.stderr.write(
529
+ `[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
530
+ `
531
+ );
536
532
  }
537
533
  const artifactsDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
538
534
  import_node_fs.default.mkdirSync(artifactsDir, { recursive: true });
@@ -588,7 +584,9 @@ function resolveCredentials(options, config) {
588
584
  throw new Error("Authentication required. Run `r5dctl auth login` or set R5D_WORKER_TOKEN/R5D_API_KEY.");
589
585
  }
590
586
  return {
591
- baseUrl: normalizeBaseUrl(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL),
587
+ baseUrl: normalizeBaseUrl(
588
+ options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL
589
+ ),
592
590
  token
593
591
  };
594
592
  }
@@ -642,27 +640,6 @@ function runGit(args, options = {}) {
642
640
  }
643
641
  return result.stdout.toString().trim();
644
642
  }
645
- async function runGitAsync(args, options = {}) {
646
- const command = ["git", ...gitAuthArgs(options.auth), ...args];
647
- const subprocess = Bun.spawn(command, {
648
- cwd: options.cwd,
649
- stdout: "pipe",
650
- stderr: "pipe",
651
- env: {
652
- ...process.env,
653
- GIT_TERMINAL_PROMPT: "0"
654
- }
655
- });
656
- const [stdout, stderr, exitCode] = await Promise.all([
657
- collectStream(subprocess.stdout),
658
- collectStream(subprocess.stderr),
659
- subprocess.exited
660
- ]);
661
- if (exitCode !== 0) {
662
- throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
663
- }
664
- return stdout.trim();
665
- }
666
643
  function tryGit(args, options = {}) {
667
644
  try {
668
645
  runGit(args, options);
@@ -671,34 +648,6 @@ function tryGit(args, options = {}) {
671
648
  return false;
672
649
  }
673
650
  }
674
- async function tryGitAsync(args, options = {}) {
675
- try {
676
- await runGitAsync(args, options);
677
- return true;
678
- } catch {
679
- return false;
680
- }
681
- }
682
- function runInternalGit(context, args) {
683
- return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
684
- }
685
- function runInternalGitAsync(context, args) {
686
- return runGitAsync(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
687
- }
688
- function runInternalGitDir(context, args) {
689
- return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
690
- }
691
- function tryInternalGit(context, args) {
692
- try {
693
- runInternalGit(context, args);
694
- return true;
695
- } catch {
696
- return false;
697
- }
698
- }
699
- function getInternalCommitHash(context) {
700
- return runInternalGit(context, ["rev-parse", "HEAD"]);
701
- }
702
651
  function getGitBlobHashForContent(content) {
703
652
  const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
704
653
  return (0, import_node_crypto.createHash)("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
@@ -706,12 +655,6 @@ function getGitBlobHashForContent(content) {
706
655
  function hasWorktreeChanges(cwd) {
707
656
  return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
708
657
  }
709
- function getInternalStatus(context) {
710
- return runInternalGit(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
711
- }
712
- function hasInternalWorktreeChanges(context) {
713
- return getInternalStatus(context).trim().length > 0;
714
- }
715
658
  function isInsideBranchPath(branchPath, filePath) {
716
659
  const relative = import_node_path.default.relative(import_node_path.default.resolve(branchPath), import_node_path.default.resolve(filePath));
717
660
  return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
@@ -746,11 +689,6 @@ function resolveWorkerFilePath(branchPath, inputPath) {
746
689
  const displayPath = toProjectDisplayPath(resolvedBranchPath, absolutePath2);
747
690
  const repoRelativePath2 = displayPath === "." ? "" : displayPath;
748
691
  assertAllowedProjectPath(repoRelativePath2, inputPath);
749
- const realBranchPath2 = import_node_fs.default.existsSync(resolvedBranchPath) ? import_node_fs.default.realpathSync(resolvedBranchPath) : resolvedBranchPath;
750
- const realTargetPath2 = resolveThroughExistingAncestor(absolutePath2);
751
- if (!isInsideBranchPath(realBranchPath2, realTargetPath2)) {
752
- throw new Error(`Project file path escapes its checkout through a symbolic link: ${inputPath}`);
753
- }
754
692
  return {
755
693
  absolutePath: absolutePath2,
756
694
  displayPath,
@@ -769,11 +707,6 @@ function resolveWorkerFilePath(branchPath, inputPath) {
769
707
  if (!isInsideBranchPath(resolvedBranchPath, absolutePath)) {
770
708
  throw new Error(`Invalid project file path: ${inputPath}`);
771
709
  }
772
- const realBranchPath = import_node_fs.default.existsSync(resolvedBranchPath) ? import_node_fs.default.realpathSync(resolvedBranchPath) : resolvedBranchPath;
773
- const realTargetPath = resolveThroughExistingAncestor(absolutePath);
774
- if (!isInsideBranchPath(realBranchPath, realTargetPath)) {
775
- throw new Error(`Project file path escapes its checkout through a symbolic link: ${inputPath}`);
776
- }
777
710
  return {
778
711
  absolutePath,
779
712
  displayPath: repoRelativePath || ".",
@@ -784,81 +717,15 @@ function resolveWorkerFilePath(branchPath, inputPath) {
784
717
  function resolveProjectFilePath(branchPath, inputPath) {
785
718
  const resolved = resolveWorkerFilePath(branchPath, inputPath);
786
719
  if (resolved.scope === "host") {
787
- throw new Error(
788
- `Project file writes must resolve inside the selected managed checkout: ${inputPath}`
789
- );
720
+ throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
790
721
  }
791
722
  return resolved;
792
723
  }
793
- function resolveThroughExistingAncestor(inputPath) {
794
- const suffix = [];
795
- let cursor = import_node_path.default.resolve(inputPath);
796
- while (!import_node_fs.default.existsSync(cursor)) {
797
- const parent = import_node_path.default.dirname(cursor);
798
- if (parent === cursor) return import_node_path.default.resolve(inputPath);
799
- suffix.unshift(import_node_path.default.basename(cursor));
800
- cursor = parent;
801
- }
802
- return import_node_path.default.resolve(import_node_fs.default.realpathSync(cursor), ...suffix);
803
- }
804
- function resolveManagedCheckoutPath(input) {
805
- const originManifest = input.manifests.find((manifest) => manifest.projectId === input.originProjectId);
806
- const candidate = import_node_path.default.isAbsolute(input.inputPath) ? import_node_path.default.resolve(input.inputPath) : import_node_path.default.resolve(
807
- input.projectsRoot,
808
- originManifest?.repoSlug ?? input.originProjectId,
809
- input.originBranchName,
810
- input.inputPath
811
- );
812
- const resolvedCandidate = resolveThroughExistingAncestor(candidate);
813
- for (const manifest of input.manifests) {
814
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
815
- for (const branchName of branches) {
816
- const checkoutPath = import_node_path.default.resolve(input.projectsRoot, manifest.repoSlug || manifest.projectId, branchName);
817
- if (!hasNormalVisibleGitDir(checkoutPath)) continue;
818
- const resolvedCheckoutPath = import_node_fs.default.existsSync(checkoutPath) ? import_node_fs.default.realpathSync(checkoutPath) : checkoutPath;
819
- const matchesLexically = isInsideBranchPath(checkoutPath, candidate);
820
- if (!matchesLexically) continue;
821
- if (!isInsideBranchPath(resolvedCheckoutPath, resolvedCandidate)) {
822
- throw new Error(`Managed path escapes its checkout through a symbolic link: ${input.inputPath}`);
823
- }
824
- const repoRelativePath = toProjectDisplayPath(resolvedCheckoutPath, resolvedCandidate);
825
- assertAllowedProjectPath(repoRelativePath === "." ? "" : repoRelativePath, input.inputPath);
826
- return {
827
- type: "resolve_managed_path",
828
- resolved: {
829
- projectId: manifest.projectId,
830
- projectPath: manifest.projectPath,
831
- branchName,
832
- checkoutPath,
833
- inputPath: input.inputPath,
834
- absolutePath: candidate,
835
- repoRelativePath: repoRelativePath === "." ? "" : repoRelativePath
836
- }
837
- };
838
- }
839
- }
840
- return { type: "resolve_managed_path", resolved: null };
841
- }
842
- function resolveRemoteUrl(baseUrl, remoteUrl) {
843
- if (/^https?:\/\//i.test(remoteUrl)) {
844
- return remoteUrl;
845
- }
846
- return new URL(remoteUrl, baseUrl).toString();
847
- }
848
724
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
849
725
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
850
726
  }
851
- function internalGitAuth(baseUrl, token) {
852
- return {
853
- extraHeaderUrl: baseUrl,
854
- header: `Authorization: Bearer ${token}`
855
- };
856
- }
857
- function internalRemoteUrlFor(baseUrl, projectId, manifest) {
858
- return resolveRemoteUrl(baseUrl, manifest?.internalRemoteUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId));
859
- }
860
727
  function githubRemoteUrlFor(baseUrl, projectId, manifest) {
861
- return manifest?.repoHttpUrl ?? internalRemoteUrlFor(baseUrl, projectId, manifest);
728
+ return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
862
729
  }
863
730
  function gitExtraHeaderUrlForRemote(remoteUrl) {
864
731
  try {
@@ -876,12 +743,6 @@ function gitAuthForRemote(remoteUrl, authHeader) {
876
743
  header: authHeader
877
744
  };
878
745
  }
879
- function branchGitDirName(branchName) {
880
- return `${encodeURIComponent(branchName)}.git`;
881
- }
882
- function internalGitDirFor(syncRoot, projectId, branchName) {
883
- return import_node_path.default.join(syncRoot, projectId, branchGitDirName(branchName));
884
- }
885
746
  function hasNormalVisibleGitDir(branchPath) {
886
747
  const gitPath = import_node_path.default.join(branchPath, ".git");
887
748
  return import_node_fs.default.existsSync(gitPath) && import_node_fs.default.statSync(gitPath).isDirectory();
@@ -893,13 +754,6 @@ function ensureOriginRemote(branchPath, remoteUrl) {
893
754
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
894
755
  }
895
756
  }
896
- async function ensureOriginRemoteAsync(branchPath, remoteUrl) {
897
- if (await tryGitAsync(["remote", "get-url", "origin"], { cwd: branchPath })) {
898
- await runGitAsync(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
899
- } else {
900
- await runGitAsync(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
901
- }
902
- }
903
757
  function shellQuote(value) {
904
758
  return `'${value.replace(/'/g, "'\\''")}'`;
905
759
  }
@@ -1095,217 +949,20 @@ function ensureVisibleGitCheckout(input) {
1095
949
  (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1096
950
  return branchPath;
1097
951
  }
1098
- function configureInternalRepo(context, remoteUrl) {
1099
- if (tryInternalGit(context, ["remote", "get-url", R5D_REMOTE_NAME])) {
1100
- runInternalGit(context, ["remote", "set-url", R5D_REMOTE_NAME, remoteUrl]);
1101
- } else {
1102
- runInternalGit(context, ["remote", "add", R5D_REMOTE_NAME, remoteUrl]);
1103
- }
1104
- if (tryInternalGit(context, ["remote", "get-url", LEGACY_BUILD_IT_NOW_REMOTE_NAME])) {
1105
- runInternalGit(context, ["remote", "remove", LEGACY_BUILD_IT_NOW_REMOTE_NAME]);
1106
- }
1107
- runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
1108
- runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
1109
- }
1110
- function ensureInternalSyncGit(input) {
1111
- validateBranchName(input.branchName);
1112
- const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1113
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(gitDir), { recursive: true });
1114
- if (!import_node_fs.default.existsSync(gitDir)) {
1115
- runGit(["init", "--bare", gitDir]);
1116
- }
1117
- const auth = internalGitAuth(input.baseUrl, input.token);
1118
- const context = { gitDir, workTree: input.branchPath, auth };
1119
- const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1120
- configureInternalRepo(context, remoteUrl);
1121
- runInternalGit(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1122
- const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`]);
1123
- const target = hasRemoteBranch ? `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}` : `refs/remotes/${R5D_REMOTE_NAME}/main`;
1124
- const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
1125
- if (!hasHead || !hasInternalWorktreeChanges(context)) {
1126
- runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
1127
- }
1128
- return context;
1129
- }
1130
952
  function ensureBranchWorkspace(input) {
1131
953
  const defaultBranch = input.manifest?.defaultBranch || "main";
1132
954
  const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1133
955
  const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
1134
- const branchPath = ensureVisibleGitCheckout({
1135
- projectRoot: input.projectRoot,
1136
- branchName: input.branchName,
1137
- githubRemoteUrl,
1138
- githubAuth,
1139
- githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
1140
- defaultBranch,
1141
- reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
1142
- });
1143
- const internalGit = ensureInternalSyncGit({
1144
- projectId: input.projectId,
1145
- baseUrl: input.baseUrl,
1146
- token: input.token,
1147
- syncRoot: input.syncRoot,
1148
- branchPath,
1149
- branchName: input.branchName,
1150
- manifest: input.manifest
1151
- });
1152
- return { branchPath, internalGit };
1153
- }
1154
- async function ensureVisibleGitCheckoutForRepositorySync(input) {
1155
- validateBranchName(input.branchName);
1156
- import_node_fs.default.mkdirSync(input.projectRoot, { recursive: true });
1157
- const branchPath = import_node_path.default.join(input.projectRoot, input.branchName);
1158
- const createdCheckout = !hasNormalVisibleGitDir(branchPath);
1159
- if (createdCheckout) {
1160
- import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1161
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(branchPath), { recursive: true });
1162
- if (!await tryGitAsync(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
1163
- import_node_fs.default.mkdirSync(branchPath, { recursive: true });
1164
- await runGitAsync(["init"], { cwd: branchPath });
1165
- }
1166
- await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1167
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1168
- (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1169
- await tryGitAsync(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
1170
- checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1171
- return branchPath;
1172
- }
1173
- if (input.reconcileExistingOrigin) {
1174
- await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1175
- }
1176
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1177
- (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1178
- return branchPath;
1179
- }
1180
- async function forceSyncManifestBranchFromInternal(input) {
1181
- const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1182
- const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest.repoAuthHeader);
1183
- const branchPath = await ensureVisibleGitCheckoutForRepositorySync({
1184
- projectRoot: input.projectRoot,
1185
- branchName: input.branchName,
1186
- githubRemoteUrl,
1187
- githubAuth,
1188
- githubAuthHeader: input.manifest.repoAuthHeader,
1189
- defaultBranch: input.manifest.defaultBranch || "main",
1190
- reconcileExistingOrigin: Boolean(input.manifest.repoHttpUrl)
1191
- });
1192
- const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1193
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(gitDir), { recursive: true });
1194
- if (!import_node_fs.default.existsSync(gitDir)) {
1195
- await runGitAsync(["init", "--bare", gitDir]);
1196
- }
1197
- const context = {
1198
- gitDir,
1199
- workTree: branchPath,
1200
- auth: internalGitAuth(input.baseUrl, input.token)
1201
- };
1202
- configureInternalRepo(context, internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest));
1203
- await runInternalGitAsync(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1204
- const branchTarget = `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`;
1205
- const mainTarget = `refs/remotes/${R5D_REMOTE_NAME}/main`;
1206
- const target = tryInternalGit(context, ["show-ref", "--verify", "--quiet", branchTarget]) ? branchTarget : tryInternalGit(context, ["show-ref", "--verify", "--quiet", mainTarget]) ? mainTarget : null;
1207
- if (!target) {
1208
- throw new Error(`Internal remote has neither ${input.branchName} nor main`);
1209
- }
1210
- if (hasInternalWorktreeChanges(context)) {
1211
- return null;
1212
- }
1213
- await runInternalGitAsync(context, ["reset", "--hard", target]);
1214
- await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1215
- return getInternalCommitHash(context);
1216
- }
1217
- function findRepositorySyncBlockers(input) {
1218
- const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
1219
- const blockedBy = [];
1220
- for (const shell of input.shells) {
1221
- const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
1222
- if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
1223
- }
1224
- return blockedBy;
1225
- }
1226
- function selectManifestSyncTargets(input) {
1227
- if (!input.requestedTargets) {
1228
- return input.manifests.flatMap((manifest) => {
1229
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1230
- return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1231
- });
1232
- }
1233
- const manifestsByProjectId = new Map(input.manifests.map((manifest) => [manifest.projectId, manifest]));
1234
- const selected = /* @__PURE__ */ new Map();
1235
- for (const target of input.requestedTargets) {
1236
- const manifest = manifestsByProjectId.get(target.projectId);
1237
- if (!manifest) {
1238
- continue;
1239
- }
1240
- selected.set(`${target.projectId}\0${target.branchName}`, { manifest, branchName: target.branchName });
1241
- }
1242
- return [...selected.values()];
1243
- }
1244
- async function syncManifestProjectsFromInternal(input) {
1245
- const targets = selectManifestSyncTargets({
1246
- manifests: input.manifests,
1247
- requestedTargets: input.requestedTargets
1248
- });
1249
- const blockedBy = findRepositorySyncBlockers({
1250
- targets: targets.map(({ manifest, branchName }) => ({
1251
- projectId: manifest.projectId,
1252
- projectPath: manifest.projectPath,
1253
- branchName
1254
- })),
1255
- shells: [...activePtys].map(([id, active]) => ({
1256
- id,
1257
- projectId: active.projectId,
1258
- branchName: active.branchName
1259
- }))
1260
- });
1261
- const blockedTargetKeys = new Set(blockedBy.map((blocker) => `${blocker.projectId}\0${blocker.branchName}`));
1262
- const results = [];
1263
- for (const { manifest, branchName } of targets) {
1264
- if (blockedTargetKeys.has(`${manifest.projectId}\0${branchName}`)) {
1265
- continue;
1266
- }
1267
- try {
1268
- const commitHash = await forceSyncManifestBranchFromInternal({
1269
- projectId: manifest.projectId,
1270
- baseUrl: input.baseUrl,
1271
- token: input.token,
1272
- projectRoot: import_node_path.default.join(input.projectsRoot, manifest.repoSlug || manifest.projectId),
1273
- syncRoot: input.syncRoot,
1274
- branchName,
1275
- manifest
1276
- });
1277
- if (commitHash === null) {
1278
- blockedBy.push({
1279
- projectId: manifest.projectId,
1280
- projectPath: manifest.projectPath,
1281
- branchName,
1282
- kind: "dirty_checkout",
1283
- id: `${manifest.projectId}:${branchName}`
1284
- });
1285
- continue;
1286
- }
1287
- results.push({
1288
- projectId: manifest.projectId,
1289
- projectPath: manifest.projectPath,
1290
- branchName,
1291
- status: "synced",
1292
- commitHash
1293
- });
1294
- } catch (error) {
1295
- results.push({
1296
- projectId: manifest.projectId,
1297
- projectPath: manifest.projectPath,
1298
- branchName,
1299
- status: "failed",
1300
- error: error instanceof Error ? error.message : String(error)
1301
- });
1302
- }
1303
- }
1304
- const hasFailures = results.some((result) => result.status === "failed");
1305
956
  return {
1306
- status: blockedBy.length > 0 && results.length === 0 ? "blocked" : blockedBy.length > 0 || hasFailures ? "partial" : "completed",
1307
- results,
1308
- blockedBy
957
+ branchPath: ensureVisibleGitCheckout({
958
+ projectRoot: input.projectRoot,
959
+ branchName: input.branchName,
960
+ githubRemoteUrl,
961
+ githubAuth,
962
+ githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
963
+ defaultBranch,
964
+ reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
965
+ })
1309
966
  };
1310
967
  }
1311
968
  function resolveCommandCwd(branchPath, cwd) {
@@ -1356,69 +1013,6 @@ async function streamCommandOutput(stream, onData) {
1356
1013
  function ensureOperationBranch(input) {
1357
1014
  return ensureBranchWorkspace(input);
1358
1015
  }
1359
- async function inspectManagedWorkspace(input) {
1360
- const candidates = input.manifests.flatMap((manifest) => {
1361
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1362
- return branches.map((branchName) => ({
1363
- manifest,
1364
- branchName,
1365
- checkoutPath: import_node_path.default.resolve(input.projectsRoot, manifest.repoSlug || manifest.projectId, branchName)
1366
- }));
1367
- });
1368
- const inspected = await Promise.all(
1369
- candidates.map(async ({ manifest, branchName, checkoutPath }) => {
1370
- const gitDir = internalGitDirFor(input.syncRoot, manifest.projectId, branchName);
1371
- if (!hasNormalVisibleGitDir(checkoutPath) || !import_node_fs.default.existsSync(gitDir)) return null;
1372
- const context = {
1373
- gitDir,
1374
- workTree: checkoutPath,
1375
- auth: internalGitAuth(input.baseUrl, input.token)
1376
- };
1377
- try {
1378
- const status = await runInternalGitAsync(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
1379
- let commitHash = null;
1380
- for (const revision of [
1381
- "HEAD",
1382
- `refs/remotes/${R5D_REMOTE_NAME}/${branchName}`,
1383
- `refs/remotes/${R5D_REMOTE_NAME}/main`
1384
- ]) {
1385
- try {
1386
- commitHash = await runInternalGitAsync(context, ["rev-parse", "--verify", revision]);
1387
- break;
1388
- } catch {
1389
- }
1390
- }
1391
- if (!commitHash) return null;
1392
- return {
1393
- projectId: manifest.projectId,
1394
- projectPath: manifest.projectPath,
1395
- branchName,
1396
- checkoutPath,
1397
- commitHash,
1398
- status,
1399
- dirty: status.trim().length > 0
1400
- };
1401
- } catch {
1402
- return null;
1403
- }
1404
- })
1405
- );
1406
- return {
1407
- type: "workspace_status",
1408
- checkouts: inspected.filter((checkout) => checkout !== null).sort(
1409
- (left, right) => left.projectPath.localeCompare(right.projectPath) || left.branchName.localeCompare(right.branchName)
1410
- )
1411
- };
1412
- }
1413
- function internalGitProcessEnv(workspace, env) {
1414
- if (env?.R5D_USE_INTERNAL_GIT !== "1") {
1415
- return {};
1416
- }
1417
- return {
1418
- GIT_DIR: workspace.internalGit.gitDir,
1419
- GIT_WORK_TREE: workspace.branchPath
1420
- };
1421
- }
1422
1016
  function formatLineNumberedContent(content, offset, limit) {
1423
1017
  const allLines = content.split("\n");
1424
1018
  const totalLines = allLines.length;
@@ -1527,9 +1121,11 @@ const fileMutationQueues = /* @__PURE__ */ new Map();
1527
1121
  async function withFileMutationQueue(key, operation) {
1528
1122
  const previous = fileMutationQueues.get(key) ?? Promise.resolve();
1529
1123
  let release;
1530
- const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
1531
- release = resolve;
1532
- }));
1124
+ const current = previous.catch(() => void 0).then(
1125
+ () => new Promise((resolve) => {
1126
+ release = resolve;
1127
+ })
1128
+ );
1533
1129
  fileMutationQueues.set(key, current);
1534
1130
  await previous.catch(() => void 0);
1535
1131
  try {
@@ -1624,7 +1220,9 @@ async function executeEditFileOperation(input) {
1624
1220
  throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1625
1221
  }
1626
1222
  if (occurrences > 1) {
1627
- throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
1223
+ throw new Error(
1224
+ `edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
1225
+ );
1628
1226
  }
1629
1227
  const start = originalContent.indexOf(edit.oldText);
1630
1228
  return { index, start, end: start + edit.oldText.length, edit };
@@ -1884,114 +1482,8 @@ async function executeViewFileBytesOperation(input) {
1884
1482
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1885
1483
  return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1886
1484
  }
1887
- function stagedBlobSizeBytes(context) {
1888
- const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
1889
- let total = 0;
1890
- for (const name of names) {
1891
- try {
1892
- const sizeText = runInternalGit(context, ["cat-file", "-s", `:${name}`]);
1893
- total += Number.parseInt(sizeText, 10) || 0;
1894
- } catch {
1895
- }
1896
- if (total > MAX_SYNC_DIFF_BYTES) {
1897
- return MAX_SYNC_DIFF_BYTES + 1;
1898
- }
1899
- }
1900
- return total;
1901
- }
1902
- function syncBranch(input) {
1903
- const workspace = ensureOperationBranch(input);
1904
- runInternalGit(workspace.internalGit, ["add", "-A", "--", ".", ":(exclude).git"]);
1905
- const hasStagedChanges = !tryInternalGit(workspace.internalGit, ["diff", "--cached", "--quiet", "--", ".", ":(exclude).git"]);
1906
- const status = getInternalStatus(workspace.internalGit);
1907
- const currentCommit = getInternalCommitHash(workspace.internalGit);
1908
- if (!hasStagedChanges) {
1909
- return {
1910
- type: "sync",
1911
- changed: false,
1912
- commitHash: currentCommit,
1913
- diffSizeBytes: 0,
1914
- status
1915
- };
1916
- }
1917
- const diffSizeBytes = stagedBlobSizeBytes(workspace.internalGit);
1918
- if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
1919
- return {
1920
- type: "sync",
1921
- changed: true,
1922
- commitHash: currentCommit,
1923
- diffSizeBytes,
1924
- status,
1925
- largeDiffBlocked: true,
1926
- trigger: input.trigger
1927
- };
1928
- }
1929
- const message = JSON.stringify({
1930
- type: "agent_changes",
1931
- sessionId: input.sessionId,
1932
- parentNodeId: input.parentNodeId,
1933
- ...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
1934
- });
1935
- runInternalGit(workspace.internalGit, ["commit", "-m", message]);
1936
- const commitHash = getInternalCommitHash(workspace.internalGit);
1937
- runInternalGit(workspace.internalGit, ["push", R5D_REMOTE_NAME, `HEAD:refs/heads/${input.branchName}`]);
1938
- return {
1939
- type: "sync",
1940
- changed: true,
1941
- commitHash,
1942
- diffSizeBytes,
1943
- status: getInternalStatus(workspace.internalGit),
1944
- trigger: input.trigger
1945
- };
1946
- }
1947
- function confirmLargeDiff(input) {
1948
- const reason = input.reason.trim();
1949
- if (!reason) {
1950
- throw new Error("confirm_large_diff requires a non-empty reason");
1951
- }
1952
- const result = syncBranch({
1953
- ...input,
1954
- confirmedLargeDiff: true,
1955
- confirmationReason: reason
1956
- });
1957
- return {
1958
- type: "confirm_large_diff",
1959
- changed: result.changed,
1960
- commitHash: result.commitHash,
1961
- diffSizeBytes: result.diffSizeBytes,
1962
- status: result.status,
1963
- reason
1964
- };
1965
- }
1966
- function pullBranch(input) {
1967
- const workspace = ensureOperationBranch(input);
1968
- runInternalGit(workspace.internalGit, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1969
- const target = input.commitHash ?? `${R5D_REMOTE_NAME}/${input.branchName}`;
1970
- runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
1971
- runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1972
- return {
1973
- type: "pull_branch",
1974
- commitHash: getInternalCommitHash(workspace.internalGit)
1975
- };
1976
- }
1977
1485
  async function executeOperation(input) {
1978
1486
  switch (input.message.type) {
1979
- case "resolve_managed_path":
1980
- return resolveManagedCheckoutPath({
1981
- projectsRoot: input.projectsRoot,
1982
- manifests: input.manifests,
1983
- originProjectId: input.message.projectId,
1984
- originBranchName: input.message.branchName,
1985
- inputPath: input.message.inputPath
1986
- });
1987
- case "workspace_status":
1988
- return await inspectManagedWorkspace({
1989
- baseUrl: input.baseUrl,
1990
- token: input.token,
1991
- projectsRoot: input.projectsRoot,
1992
- syncRoot: input.syncRoot,
1993
- manifests: input.manifests
1994
- });
1995
1487
  case "read":
1996
1488
  return executeReadFileOperation({ ...input, message: input.message });
1997
1489
  case "write":
@@ -2006,43 +1498,6 @@ async function executeOperation(input) {
2006
1498
  return executeLsOperation({ ...input, message: input.message });
2007
1499
  case "view_file_bytes":
2008
1500
  return executeViewFileBytesOperation({ ...input, message: input.message });
2009
- case "sync":
2010
- return syncBranch({
2011
- projectId: input.projectId,
2012
- baseUrl: input.baseUrl,
2013
- token: input.token,
2014
- projectRoot: input.projectRoot,
2015
- syncRoot: input.syncRoot,
2016
- branchName: input.message.branchName,
2017
- sessionId: input.message.sessionId,
2018
- parentNodeId: input.message.parentNodeId,
2019
- trigger: input.message.trigger,
2020
- manifest: input.manifest
2021
- });
2022
- case "confirm_large_diff":
2023
- return confirmLargeDiff({
2024
- projectId: input.projectId,
2025
- baseUrl: input.baseUrl,
2026
- token: input.token,
2027
- projectRoot: input.projectRoot,
2028
- syncRoot: input.syncRoot,
2029
- branchName: input.message.branchName,
2030
- sessionId: input.message.sessionId,
2031
- parentNodeId: input.message.parentNodeId,
2032
- reason: input.message.reason,
2033
- manifest: input.manifest
2034
- });
2035
- case "pull_branch":
2036
- return pullBranch({
2037
- projectId: input.projectId,
2038
- baseUrl: input.baseUrl,
2039
- token: input.token,
2040
- projectRoot: input.projectRoot,
2041
- syncRoot: input.syncRoot,
2042
- branchName: input.message.branchName,
2043
- commitHash: input.message.commitHash,
2044
- manifest: input.manifest
2045
- });
2046
1501
  }
2047
1502
  }
2048
1503
  async function executeCommand(input) {
@@ -2074,7 +1529,11 @@ async function executeCommand(input) {
2074
1529
  planRoot: input.planRoot,
2075
1530
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2076
1531
  });
2077
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1532
+ const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1533
+ if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !import_node_fs.default.existsSync(import_node_path.default.join(commandRoot, ".git"))) {
1534
+ throw new Error("The canonical workspace checkout is not initialized on this worker");
1535
+ }
1536
+ const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
2078
1537
  const subprocess = Bun.spawn(input.message.argv, {
2079
1538
  cwd,
2080
1539
  stdout: "pipe",
@@ -2084,7 +1543,6 @@ async function executeCommand(input) {
2084
1543
  ...process.env,
2085
1544
  ...githubProcessEnv(),
2086
1545
  ...input.message.env ?? {},
2087
- ...internalGitProcessEnv(workspace, input.message.env),
2088
1546
  ...artifactProcessEnv,
2089
1547
  ...planProcessEnv
2090
1548
  }
@@ -2095,7 +1553,8 @@ async function executeCommand(input) {
2095
1553
  sessionId: input.message.sessionId ?? "",
2096
1554
  branchName: input.message.branchName,
2097
1555
  mode: "foreground",
2098
- startCommitHash: "",
1556
+ pid: subprocess.pid,
1557
+ processGroupId: process.platform === "win32" ? void 0 : subprocess.pid,
2099
1558
  argv: input.message.argv,
2100
1559
  command: input.message.argv.join(" "),
2101
1560
  cwd,
@@ -2181,7 +1640,11 @@ async function executeStreamingCommand(input) {
2181
1640
  planRoot: input.planRoot,
2182
1641
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2183
1642
  });
2184
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1643
+ const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1644
+ if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !import_node_fs.default.existsSync(import_node_path.default.join(commandRoot, ".git"))) {
1645
+ throw new Error("The canonical workspace checkout is not initialized on this worker");
1646
+ }
1647
+ const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
2185
1648
  const subprocess = Bun.spawn(input.message.argv, {
2186
1649
  cwd,
2187
1650
  stdout: "pipe",
@@ -2191,7 +1654,6 @@ async function executeStreamingCommand(input) {
2191
1654
  ...process.env,
2192
1655
  ...githubProcessEnv(),
2193
1656
  ...input.message.env ?? {},
2194
- ...internalGitProcessEnv(workspace, input.message.env),
2195
1657
  ...artifactProcessEnv,
2196
1658
  ...planProcessEnv
2197
1659
  }
@@ -2202,16 +1664,11 @@ async function executeStreamingCommand(input) {
2202
1664
  sessionId: input.message.sessionId,
2203
1665
  branchName: input.message.branchName,
2204
1666
  mode: input.message.mode,
2205
- startCommitHash: input.message.startCommitHash,
2206
- launchWorkspaceActionId: input.message.launchWorkspaceActionId,
2207
- credentialId: input.message.credentialId,
2208
1667
  pid: subprocess.pid,
2209
- processGroupId: subprocess.pid,
1668
+ processGroupId: process.platform === "win32" ? void 0 : subprocess.pid,
1669
+ credentialId: input.message.credentialId,
2210
1670
  argv: input.message.argv,
2211
1671
  command: input.message.command,
2212
- // Persist the resolved absolute cwd so reconnect reports and process
2213
- // history retain the actual execution location, including cross-project
2214
- // managed paths.
2215
1672
  cwd,
2216
1673
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2217
1674
  });
@@ -2222,7 +1679,7 @@ async function executeStreamingCommand(input) {
2222
1679
  runId: input.message.runId,
2223
1680
  cwd,
2224
1681
  pid: subprocess.pid,
2225
- processGroupId: subprocess.pid
1682
+ ...process.platform === "win32" ? {} : { processGroupId: subprocess.pid }
2226
1683
  });
2227
1684
  if (input.message.timeoutMs) {
2228
1685
  timeout = setTimeout(() => {
@@ -2254,22 +1711,26 @@ async function executeStreamingCommand(input) {
2254
1711
  });
2255
1712
  })
2256
1713
  ]);
2257
- sendWorkerMessage(input.ws, {
1714
+ const terminal = {
2258
1715
  type: "exec_exit",
2259
1716
  runId: input.message.runId,
2260
1717
  exitCode,
2261
1718
  durationMs: Date.now() - startedAt,
2262
1719
  ...timedOut ? { timedOut: true } : {}
2263
- });
1720
+ };
1721
+ pendingProcessTerminals.set(input.message.runId, terminal);
1722
+ sendWorkerMessage(input.ws, terminal);
2264
1723
  } catch (error) {
2265
1724
  const message = error instanceof Error ? error.message : String(error);
2266
1725
  if (started) {
2267
- sendWorkerMessage(input.ws, {
1726
+ const terminal = {
2268
1727
  type: "exec_error",
2269
1728
  runId: input.message.runId,
2270
1729
  error: message,
2271
1730
  durationMs: Date.now() - startedAt
2272
- });
1731
+ };
1732
+ pendingProcessTerminals.set(input.message.runId, terminal);
1733
+ sendWorkerMessage(input.ws, terminal);
2273
1734
  } else {
2274
1735
  sendWorkerMessage(input.ws, {
2275
1736
  type: "exec_start_error",
@@ -2301,12 +1762,12 @@ function buildActiveProcessReports() {
2301
1762
  projectId: active.projectId,
2302
1763
  sessionId: active.sessionId,
2303
1764
  branchName: active.branchName,
1765
+ platform: process.platform,
1766
+ arch: process.arch,
2304
1767
  mode: active.mode,
2305
- startCommitHash: active.startCommitHash,
2306
- ...active.launchWorkspaceActionId ? { launchWorkspaceActionId: active.launchWorkspaceActionId } : {},
2307
- ...active.credentialId ? { credentialId: active.credentialId } : {},
2308
1768
  ...active.pid !== void 0 ? { pid: active.pid } : {},
2309
1769
  ...active.processGroupId !== void 0 ? { processGroupId: active.processGroupId } : {},
1770
+ ...active.credentialId ? { credentialId: active.credentialId } : {},
2310
1771
  argv: active.argv,
2311
1772
  command: active.command,
2312
1773
  ...active.cwd ? { cwd: active.cwd } : {},
@@ -2607,10 +2068,7 @@ function resizePty(message) {
2607
2068
  if (!ptyProcess) {
2608
2069
  return;
2609
2070
  }
2610
- ptyProcess.resize(
2611
- Math.max(1, Math.min(Math.floor(message.cols || 80), 500)),
2612
- Math.max(1, Math.min(Math.floor(message.rows || 24), 500))
2613
- );
2071
+ ptyProcess.resize(Math.max(1, Math.min(Math.floor(message.cols || 80), 500)), Math.max(1, Math.min(Math.floor(message.rows || 24), 500)));
2614
2072
  }
2615
2073
  function closePty(message) {
2616
2074
  const ptyProcess = activePtys.get(message.ptyId);
@@ -2642,6 +2100,7 @@ async function startWorker(options) {
2642
2100
  validateLabel(label);
2643
2101
  const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
2644
2102
  const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
2103
+ const workspaceShadowRoot = import_node_path.default.join(syncRoot, "workspace");
2645
2104
  const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2646
2105
  const planRoot = import_node_path.default.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
2647
2106
  process.stdout.write(`[r5d-worker] label: ${label}
@@ -2663,10 +2122,104 @@ async function startWorker(options) {
2663
2122
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2664
2123
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2665
2124
  const manifestByProjectId = /* @__PURE__ */ new Map();
2666
- let repositorySyncQueue = Promise.resolve();
2667
- let repositorySyncInProgress = false;
2125
+ const workspaceSyncSingleFlight = new import_workspace_sync.WorkspaceSyncSingleFlight();
2126
+ let workspaceRemoteUrl = null;
2127
+ let activeWorkspaceIncidentId = null;
2128
+ let previousPeriodicFingerprint = null;
2129
+ let periodicWorkspaceScan;
2130
+ let periodicWorkspaceScanInFlight = false;
2131
+ let terminalReplayTimer;
2132
+ let workspaceSyncRequestsInFlight = 0;
2668
2133
  let cliUpdateInProgress = false;
2669
2134
  let reloadAfterClose = false;
2135
+ const workspaceSyncInput = (trigger, overrides = {}) => {
2136
+ if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2137
+ return {
2138
+ workerLabel: label,
2139
+ remoteUrl: workspaceRemoteUrl,
2140
+ authHeader: `Authorization: Bearer ${token}`,
2141
+ projectsRoot,
2142
+ plansRoot: planRoot,
2143
+ shadowRoot: workspaceShadowRoot,
2144
+ projects: [...manifestByProjectId.values()],
2145
+ trigger,
2146
+ ...overrides
2147
+ };
2148
+ };
2149
+ const sendWorkspaceSyncResult = (requestId, result) => {
2150
+ sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2151
+ };
2152
+ const ensureAllVisibleWorkspaceCheckouts = () => {
2153
+ const created = [];
2154
+ for (const manifest of manifestByProjectId.values()) {
2155
+ for (const branchName of manifest.branches) {
2156
+ const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2157
+ const existed = import_node_fs.default.existsSync(import_node_path.default.join(branchPath, ".git"));
2158
+ const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2159
+ ensureVisibleGitCheckout({
2160
+ projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2161
+ branchName,
2162
+ githubRemoteUrl,
2163
+ githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2164
+ githubAuthHeader: manifest.repoAuthHeader,
2165
+ defaultBranch: manifest.defaultBranch || "main",
2166
+ reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2167
+ });
2168
+ if (!existed) created.push({ projectId: manifest.projectId, branchName });
2169
+ }
2170
+ }
2171
+ return created;
2172
+ };
2173
+ const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2174
+ workspaceSyncRequestsInFlight += 1;
2175
+ try {
2176
+ if (trigger.type === "plan_write" || trigger.type === "task_status" || trigger.type === "connect" || trigger.type === "inbound_head" || trigger.type === "manual") {
2177
+ for (const manifest of manifestByProjectId.values()) {
2178
+ for (const branchName of manifest.branches) {
2179
+ await syncProjectPlans({
2180
+ baseUrl,
2181
+ token,
2182
+ projectId: manifest.projectId,
2183
+ branchName,
2184
+ planRoot
2185
+ });
2186
+ }
2187
+ }
2188
+ }
2189
+ const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2190
+ const result = await workspaceSyncSingleFlight.run(
2191
+ workspaceSyncInput(trigger, {
2192
+ ...overrides,
2193
+ ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2194
+ })
2195
+ );
2196
+ previousPeriodicFingerprint = null;
2197
+ sendWorkspaceSyncResult(requestId, result);
2198
+ return result;
2199
+ } catch (error) {
2200
+ const result = {
2201
+ type: "workspace_sync",
2202
+ attemptId: overrides.attemptId ?? crypto.randomUUID(),
2203
+ workerLabel: label,
2204
+ trigger,
2205
+ outcome: "failed",
2206
+ startingHead: null,
2207
+ rebaseCount: 0,
2208
+ diffSizeBytes: 0,
2209
+ gitStatus: "",
2210
+ affectedProjects: [],
2211
+ affectedPaths: [],
2212
+ discardedPaths: [],
2213
+ localChangesDiscarded: false,
2214
+ error: error instanceof Error ? error.message : String(error)
2215
+ };
2216
+ previousPeriodicFingerprint = null;
2217
+ sendWorkspaceSyncResult(requestId, result);
2218
+ return result;
2219
+ } finally {
2220
+ workspaceSyncRequestsInFlight -= 1;
2221
+ }
2222
+ };
2670
2223
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2671
2224
  headers: {
2672
2225
  Authorization: `Bearer ${token}`
@@ -2711,6 +2264,15 @@ async function startWorker(options) {
2711
2264
  };
2712
2265
  ws.send(JSON.stringify(hello));
2713
2266
  sendActiveProcessReport(ws);
2267
+ for (const terminal of pendingProcessTerminals.values()) {
2268
+ sendWorkerMessage(ws, terminal);
2269
+ }
2270
+ terminalReplayTimer = setInterval(() => {
2271
+ for (const terminal of pendingProcessTerminals.values()) {
2272
+ sendWorkerMessage(ws, terminal);
2273
+ }
2274
+ }, 5e3);
2275
+ terminalReplayTimer.unref();
2714
2276
  process.stdout.write("[r5d-worker] connected\n");
2715
2277
  });
2716
2278
  ws.addEventListener("message", (event) => {
@@ -2719,91 +2281,68 @@ async function startWorker(options) {
2719
2281
  if (message.type === "connected") {
2720
2282
  return;
2721
2283
  }
2722
- if (message.type === "project_manifest") {
2284
+ if (message.type === "workspace_manifest") {
2723
2285
  configureGitHubAuth(message.githubCredential);
2724
2286
  visibleGitIdentity = message.gitIdentity;
2287
+ workspaceRemoteUrl = message.workspaceRemoteUrl;
2725
2288
  manifestByProjectId.clear();
2726
- for (const project of message.projects) {
2727
- manifestByProjectId.set(project.projectId, project);
2728
- }
2729
- process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
2289
+ for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2290
+ process.stdout.write(`[r5d-worker] workspace manifest: ${message.projects.length} projects
2730
2291
  `);
2731
- return;
2732
- }
2733
- if (message.type === "sync_projects") {
2734
- if (cliUpdateInProgress) {
2735
- sendWorkerMessage(ws, {
2736
- type: "sync_projects_result",
2737
- requestId: message.requestId,
2738
- result: {
2739
- status: "blocked",
2740
- results: [],
2741
- blockedBy: []
2742
- }
2743
- });
2744
- return;
2745
- }
2746
- let syncResult = { status: "completed", results: [], blockedBy: [] };
2747
- const executeSync = async () => {
2748
- repositorySyncInProgress = true;
2749
- try {
2750
- syncResult = await syncManifestProjectsFromInternal({
2751
- baseUrl,
2752
- token,
2753
- projectsRoot,
2754
- syncRoot,
2755
- manifests: [...manifestByProjectId.values()],
2756
- requestedTargets: message.targets
2757
- });
2758
- for (const result of syncResult.results) {
2759
- if (result.status !== "synced") continue;
2760
- try {
2761
- await syncProjectPlans({
2762
- baseUrl,
2763
- token,
2764
- projectId: result.projectId,
2765
- branchName: result.branchName,
2766
- planRoot
2767
- });
2768
- } catch (error) {
2769
- process.stderr.write(
2770
- `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2771
- `
2772
- );
2292
+ if (!periodicWorkspaceScan) {
2293
+ const emptyFingerprint = (0, import_node_crypto.createHash)("sha256").update("").digest("hex");
2294
+ periodicWorkspaceScan = setInterval(() => {
2295
+ if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2296
+ periodicWorkspaceScanInFlight = true;
2297
+ void (async () => {
2298
+ const input = workspaceSyncInput({ type: "periodic" });
2299
+ const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2300
+ if (fingerprint === emptyFingerprint) {
2301
+ previousPeriodicFingerprint = null;
2302
+ return;
2773
2303
  }
2774
- }
2775
- } finally {
2776
- repositorySyncInProgress = false;
2777
- }
2778
- };
2779
- const queued = repositorySyncQueue.then(executeSync, executeSync);
2780
- repositorySyncQueue = queued.then(
2781
- () => void 0,
2782
- () => void 0
2783
- );
2784
- try {
2785
- await queued;
2786
- } catch (error) {
2787
- syncResult = {
2788
- status: "partial",
2789
- results: [
2790
- {
2791
- projectId: "worker",
2792
- projectPath: "worker",
2793
- branchName: "all",
2794
- status: "failed",
2795
- error: error instanceof Error ? error.message : String(error)
2304
+ if (previousPeriodicFingerprint !== fingerprint) {
2305
+ previousPeriodicFingerprint = fingerprint;
2306
+ return;
2796
2307
  }
2797
- ],
2798
- blockedBy: []
2799
- };
2308
+ previousPeriodicFingerprint = null;
2309
+ await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "periodic" });
2310
+ })().catch((error) => {
2311
+ process.stderr.write(
2312
+ `[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
2313
+ `
2314
+ );
2315
+ }).finally(() => {
2316
+ periodicWorkspaceScanInFlight = false;
2317
+ });
2318
+ }, import_workspace_sync.WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
2319
+ periodicWorkspaceScan.unref();
2800
2320
  }
2801
- sendWorkerMessage(ws, {
2802
- type: "sync_projects_result",
2803
- requestId: message.requestId,
2804
- result: syncResult
2321
+ return;
2322
+ }
2323
+ if (message.type === "sync_workspace") {
2324
+ await runRequestedWorkspaceSync(message.requestId, message.trigger, {
2325
+ attemptId: message.attemptId,
2326
+ confirmedLargeDiff: message.confirmedLargeDiff,
2327
+ confirmationReason: message.confirmationReason,
2328
+ resetToCanonical: message.resetToCanonical,
2329
+ skipVisibleMirror: message.skipVisibleMirror
2805
2330
  });
2806
- sendActiveProcessReport(ws);
2331
+ return;
2332
+ }
2333
+ if (message.type === "workspace_head_updated") {
2334
+ if (!activeWorkspaceIncidentId) {
2335
+ await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "inbound_head", detail: message.commitHash });
2336
+ }
2337
+ return;
2338
+ }
2339
+ if (message.type === "workspace_incident_updated") {
2340
+ activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
2341
+ previousPeriodicFingerprint = null;
2342
+ return;
2343
+ }
2344
+ if (message.type === "exec_terminal_ack") {
2345
+ pendingProcessTerminals.delete(message.runId);
2807
2346
  return;
2808
2347
  }
2809
2348
  if (message.type === "update_clis") {
@@ -2815,7 +2354,7 @@ async function startWorker(options) {
2815
2354
  });
2816
2355
  return;
2817
2356
  }
2818
- if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
2357
+ if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
2819
2358
  sendWorkerMessage(ws, {
2820
2359
  type: "update_clis_result",
2821
2360
  requestId: message.requestId,
@@ -2885,7 +2424,6 @@ async function startWorker(options) {
2885
2424
  });
2886
2425
  return;
2887
2426
  }
2888
- await repositorySyncQueue;
2889
2427
  const manifest = manifestByProjectId.get(message.projectId);
2890
2428
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2891
2429
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2916,7 +2454,6 @@ async function startWorker(options) {
2916
2454
  });
2917
2455
  return;
2918
2456
  }
2919
- await repositorySyncQueue;
2920
2457
  const manifest = manifestByProjectId.get(message.projectId);
2921
2458
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2922
2459
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2930,6 +2467,7 @@ async function startWorker(options) {
2930
2467
  syncRoot,
2931
2468
  artifactRoot,
2932
2469
  planRoot,
2470
+ workspaceShadowRoot,
2933
2471
  manifest
2934
2472
  });
2935
2473
  ws.send(JSON.stringify(result));
@@ -2945,7 +2483,6 @@ async function startWorker(options) {
2945
2483
  });
2946
2484
  return;
2947
2485
  }
2948
- await repositorySyncQueue;
2949
2486
  sendWorkerMessage(ws, {
2950
2487
  type: "exec_accepted",
2951
2488
  requestId: message.requestId,
@@ -2966,6 +2503,7 @@ async function startWorker(options) {
2966
2503
  syncRoot,
2967
2504
  artifactRoot,
2968
2505
  planRoot,
2506
+ workspaceShadowRoot,
2969
2507
  manifest
2970
2508
  }).catch((error) => {
2971
2509
  sendWorkerMessage(ws, {
@@ -2985,9 +2523,8 @@ async function startWorker(options) {
2985
2523
  }
2986
2524
  return;
2987
2525
  }
2988
- if (message.type === "resolve_managed_path" || message.type === "workspace_status" || message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
2526
+ if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
2989
2527
  try {
2990
- await repositorySyncQueue;
2991
2528
  const manifest = manifestByProjectId.get(message.projectId);
2992
2529
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2993
2530
  const result = await executeOperation({
@@ -2998,8 +2535,6 @@ async function startWorker(options) {
2998
2535
  projectRoot,
2999
2536
  syncRoot,
3000
2537
  artifactRoot,
3001
- projectsRoot,
3002
- manifests: [...manifestByProjectId.values()],
3003
2538
  manifest
3004
2539
  });
3005
2540
  ws.send(
@@ -3037,6 +2572,14 @@ async function startWorker(options) {
3037
2572
  });
3038
2573
  ws.addEventListener("close", (event) => {
3039
2574
  stopHeartbeatWatchdog();
2575
+ if (periodicWorkspaceScan) {
2576
+ clearInterval(periodicWorkspaceScan);
2577
+ periodicWorkspaceScan = void 0;
2578
+ }
2579
+ if (terminalReplayTimer) {
2580
+ clearInterval(terminalReplayTimer);
2581
+ terminalReplayTimer = void 0;
2582
+ }
3040
2583
  if (currentWorkerSocket === ws) {
3041
2584
  currentWorkerSocket = null;
3042
2585
  }
@@ -3047,15 +2590,17 @@ async function startWorker(options) {
3047
2590
  if (reloadAfterClose) {
3048
2591
  process.exit(import_supervisor.WORKER_RELOAD_EXIT_CODE);
3049
2592
  }
3050
- if (activeProcesses.size > 0) {
2593
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
3051
2594
  const delayMs = 2e3;
3052
- process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
3053
- `);
2595
+ process.stderr.write(
2596
+ `[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${delayMs}ms
2597
+ `
2598
+ );
3054
2599
  const reconnect = () => {
3055
2600
  void startWorker(options).catch((error) => {
3056
2601
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
3057
2602
  `);
3058
- if (activeProcesses.size > 0) {
2603
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
3059
2604
  setTimeout(reconnect, delayMs);
3060
2605
  return;
3061
2606
  }
@@ -3111,11 +2656,9 @@ if (isCliEntrypoint()) {
3111
2656
  // Annotate the CommonJS export names for ESM import in node:
3112
2657
  0 && (module.exports = {
3113
2658
  ensureVisibleGitCheckout,
3114
- findRepositorySyncBlockers,
3115
2659
  findWorkerFiles,
3116
2660
  githubCliEnv,
3117
2661
  grepWorkerFiles,
3118
- inspectManagedWorkspace,
3119
2662
  isArtifactEnvPath,
3120
2663
  listWorkerDirectory,
3121
2664
  prepareArtifactEnvForShell,
@@ -3123,11 +2666,8 @@ if (isCliEntrypoint()) {
3123
2666
  readWorkerImageFile,
3124
2667
  readWorkerTextFile,
3125
2668
  resolveHostShell,
3126
- resolveManagedCheckoutPath,
3127
2669
  resolveProjectFilePath,
3128
2670
  resolveWorkerFilePath,
3129
- selectManifestSyncTargets,
3130
- syncManifestProjectsFromInternal,
3131
2671
  syncProjectPlans,
3132
2672
  syncSessionArtifacts
3133
2673
  });