@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/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(`[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
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(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL),
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);
@@ -693,11 +646,6 @@ function resolveWorkerFilePath(branchPath, inputPath) {
693
646
  const displayPath = toProjectDisplayPath(resolvedBranchPath, absolutePath2);
694
647
  const repoRelativePath2 = displayPath === "." ? "" : displayPath;
695
648
  assertAllowedProjectPath(repoRelativePath2, inputPath);
696
- const realBranchPath2 = fs.existsSync(resolvedBranchPath) ? fs.realpathSync(resolvedBranchPath) : resolvedBranchPath;
697
- const realTargetPath2 = resolveThroughExistingAncestor(absolutePath2);
698
- if (!isInsideBranchPath(realBranchPath2, realTargetPath2)) {
699
- throw new Error(`Project file path escapes its checkout through a symbolic link: ${inputPath}`);
700
- }
701
649
  return {
702
650
  absolutePath: absolutePath2,
703
651
  displayPath,
@@ -716,11 +664,6 @@ function resolveWorkerFilePath(branchPath, inputPath) {
716
664
  if (!isInsideBranchPath(resolvedBranchPath, absolutePath)) {
717
665
  throw new Error(`Invalid project file path: ${inputPath}`);
718
666
  }
719
- const realBranchPath = fs.existsSync(resolvedBranchPath) ? fs.realpathSync(resolvedBranchPath) : resolvedBranchPath;
720
- const realTargetPath = resolveThroughExistingAncestor(absolutePath);
721
- if (!isInsideBranchPath(realBranchPath, realTargetPath)) {
722
- throw new Error(`Project file path escapes its checkout through a symbolic link: ${inputPath}`);
723
- }
724
667
  return {
725
668
  absolutePath,
726
669
  displayPath: repoRelativePath || ".",
@@ -731,81 +674,15 @@ function resolveWorkerFilePath(branchPath, inputPath) {
731
674
  function resolveProjectFilePath(branchPath, inputPath) {
732
675
  const resolved = resolveWorkerFilePath(branchPath, inputPath);
733
676
  if (resolved.scope === "host") {
734
- throw new Error(
735
- `Project file writes must resolve inside the selected managed checkout: ${inputPath}`
736
- );
677
+ throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
737
678
  }
738
679
  return resolved;
739
680
  }
740
- function resolveThroughExistingAncestor(inputPath) {
741
- const suffix = [];
742
- let cursor = path.resolve(inputPath);
743
- while (!fs.existsSync(cursor)) {
744
- const parent = path.dirname(cursor);
745
- if (parent === cursor) return path.resolve(inputPath);
746
- suffix.unshift(path.basename(cursor));
747
- cursor = parent;
748
- }
749
- return path.resolve(fs.realpathSync(cursor), ...suffix);
750
- }
751
- function resolveManagedCheckoutPath(input) {
752
- const originManifest = input.manifests.find((manifest) => manifest.projectId === input.originProjectId);
753
- const candidate = path.isAbsolute(input.inputPath) ? path.resolve(input.inputPath) : path.resolve(
754
- input.projectsRoot,
755
- originManifest?.repoSlug ?? input.originProjectId,
756
- input.originBranchName,
757
- input.inputPath
758
- );
759
- const resolvedCandidate = resolveThroughExistingAncestor(candidate);
760
- for (const manifest of input.manifests) {
761
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
762
- for (const branchName of branches) {
763
- const checkoutPath = path.resolve(input.projectsRoot, manifest.repoSlug || manifest.projectId, branchName);
764
- if (!hasNormalVisibleGitDir(checkoutPath)) continue;
765
- const resolvedCheckoutPath = fs.existsSync(checkoutPath) ? fs.realpathSync(checkoutPath) : checkoutPath;
766
- const matchesLexically = isInsideBranchPath(checkoutPath, candidate);
767
- if (!matchesLexically) continue;
768
- if (!isInsideBranchPath(resolvedCheckoutPath, resolvedCandidate)) {
769
- throw new Error(`Managed path escapes its checkout through a symbolic link: ${input.inputPath}`);
770
- }
771
- const repoRelativePath = toProjectDisplayPath(resolvedCheckoutPath, resolvedCandidate);
772
- assertAllowedProjectPath(repoRelativePath === "." ? "" : repoRelativePath, input.inputPath);
773
- return {
774
- type: "resolve_managed_path",
775
- resolved: {
776
- projectId: manifest.projectId,
777
- projectPath: manifest.projectPath,
778
- branchName,
779
- checkoutPath,
780
- inputPath: input.inputPath,
781
- absolutePath: candidate,
782
- repoRelativePath: repoRelativePath === "." ? "" : repoRelativePath
783
- }
784
- };
785
- }
786
- }
787
- return { type: "resolve_managed_path", resolved: null };
788
- }
789
- function resolveRemoteUrl(baseUrl, remoteUrl) {
790
- if (/^https?:\/\//i.test(remoteUrl)) {
791
- return remoteUrl;
792
- }
793
- return new URL(remoteUrl, baseUrl).toString();
794
- }
795
681
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
796
682
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
797
683
  }
798
- function internalGitAuth(baseUrl, token) {
799
- return {
800
- extraHeaderUrl: baseUrl,
801
- header: `Authorization: Bearer ${token}`
802
- };
803
- }
804
- function internalRemoteUrlFor(baseUrl, projectId, manifest) {
805
- return resolveRemoteUrl(baseUrl, manifest?.internalRemoteUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId));
806
- }
807
684
  function githubRemoteUrlFor(baseUrl, projectId, manifest) {
808
- return manifest?.repoHttpUrl ?? internalRemoteUrlFor(baseUrl, projectId, manifest);
685
+ return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
809
686
  }
810
687
  function gitExtraHeaderUrlForRemote(remoteUrl) {
811
688
  try {
@@ -823,12 +700,6 @@ function gitAuthForRemote(remoteUrl, authHeader) {
823
700
  header: authHeader
824
701
  };
825
702
  }
826
- function branchGitDirName(branchName) {
827
- return `${encodeURIComponent(branchName)}.git`;
828
- }
829
- function internalGitDirFor(syncRoot, projectId, branchName) {
830
- return path.join(syncRoot, projectId, branchGitDirName(branchName));
831
- }
832
703
  function hasNormalVisibleGitDir(branchPath) {
833
704
  const gitPath = path.join(branchPath, ".git");
834
705
  return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
@@ -840,13 +711,6 @@ function ensureOriginRemote(branchPath, remoteUrl) {
840
711
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
841
712
  }
842
713
  }
843
- async function ensureOriginRemoteAsync(branchPath, remoteUrl) {
844
- if (await tryGitAsync(["remote", "get-url", "origin"], { cwd: branchPath })) {
845
- await runGitAsync(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
846
- } else {
847
- await runGitAsync(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
848
- }
849
- }
850
714
  function shellQuote(value) {
851
715
  return `'${value.replace(/'/g, "'\\''")}'`;
852
716
  }
@@ -1042,217 +906,20 @@ function ensureVisibleGitCheckout(input) {
1042
906
  configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1043
907
  return branchPath;
1044
908
  }
1045
- function configureInternalRepo(context, remoteUrl) {
1046
- if (tryInternalGit(context, ["remote", "get-url", R5D_REMOTE_NAME])) {
1047
- runInternalGit(context, ["remote", "set-url", R5D_REMOTE_NAME, remoteUrl]);
1048
- } else {
1049
- runInternalGit(context, ["remote", "add", R5D_REMOTE_NAME, remoteUrl]);
1050
- }
1051
- if (tryInternalGit(context, ["remote", "get-url", LEGACY_BUILD_IT_NOW_REMOTE_NAME])) {
1052
- runInternalGit(context, ["remote", "remove", LEGACY_BUILD_IT_NOW_REMOTE_NAME]);
1053
- }
1054
- runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
1055
- runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
1056
- }
1057
- function ensureInternalSyncGit(input) {
1058
- validateBranchName(input.branchName);
1059
- const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1060
- fs.mkdirSync(path.dirname(gitDir), { recursive: true });
1061
- if (!fs.existsSync(gitDir)) {
1062
- runGit(["init", "--bare", gitDir]);
1063
- }
1064
- const auth = internalGitAuth(input.baseUrl, input.token);
1065
- const context = { gitDir, workTree: input.branchPath, auth };
1066
- const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1067
- configureInternalRepo(context, remoteUrl);
1068
- runInternalGit(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1069
- const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`]);
1070
- const target = hasRemoteBranch ? `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}` : `refs/remotes/${R5D_REMOTE_NAME}/main`;
1071
- const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
1072
- if (!hasHead || !hasInternalWorktreeChanges(context)) {
1073
- runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
1074
- }
1075
- return context;
1076
- }
1077
909
  function ensureBranchWorkspace(input) {
1078
910
  const defaultBranch = input.manifest?.defaultBranch || "main";
1079
911
  const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1080
912
  const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
1081
- const branchPath = ensureVisibleGitCheckout({
1082
- projectRoot: input.projectRoot,
1083
- branchName: input.branchName,
1084
- githubRemoteUrl,
1085
- githubAuth,
1086
- githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
1087
- defaultBranch,
1088
- reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
1089
- });
1090
- const internalGit = ensureInternalSyncGit({
1091
- projectId: input.projectId,
1092
- baseUrl: input.baseUrl,
1093
- token: input.token,
1094
- syncRoot: input.syncRoot,
1095
- branchPath,
1096
- branchName: input.branchName,
1097
- manifest: input.manifest
1098
- });
1099
- return { branchPath, internalGit };
1100
- }
1101
- async function ensureVisibleGitCheckoutForRepositorySync(input) {
1102
- validateBranchName(input.branchName);
1103
- fs.mkdirSync(input.projectRoot, { recursive: true });
1104
- const branchPath = path.join(input.projectRoot, input.branchName);
1105
- const createdCheckout = !hasNormalVisibleGitDir(branchPath);
1106
- if (createdCheckout) {
1107
- fs.rmSync(branchPath, { recursive: true, force: true });
1108
- fs.mkdirSync(path.dirname(branchPath), { recursive: true });
1109
- if (!await tryGitAsync(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
1110
- fs.mkdirSync(branchPath, { recursive: true });
1111
- await runGitAsync(["init"], { cwd: branchPath });
1112
- }
1113
- await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1114
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1115
- configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1116
- await tryGitAsync(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
1117
- checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1118
- return branchPath;
1119
- }
1120
- if (input.reconcileExistingOrigin) {
1121
- await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1122
- }
1123
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1124
- configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1125
- return branchPath;
1126
- }
1127
- async function forceSyncManifestBranchFromInternal(input) {
1128
- const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1129
- const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest.repoAuthHeader);
1130
- const branchPath = await ensureVisibleGitCheckoutForRepositorySync({
1131
- projectRoot: input.projectRoot,
1132
- branchName: input.branchName,
1133
- githubRemoteUrl,
1134
- githubAuth,
1135
- githubAuthHeader: input.manifest.repoAuthHeader,
1136
- defaultBranch: input.manifest.defaultBranch || "main",
1137
- reconcileExistingOrigin: Boolean(input.manifest.repoHttpUrl)
1138
- });
1139
- const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1140
- fs.mkdirSync(path.dirname(gitDir), { recursive: true });
1141
- if (!fs.existsSync(gitDir)) {
1142
- await runGitAsync(["init", "--bare", gitDir]);
1143
- }
1144
- const context = {
1145
- gitDir,
1146
- workTree: branchPath,
1147
- auth: internalGitAuth(input.baseUrl, input.token)
1148
- };
1149
- configureInternalRepo(context, internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest));
1150
- await runInternalGitAsync(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1151
- const branchTarget = `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`;
1152
- const mainTarget = `refs/remotes/${R5D_REMOTE_NAME}/main`;
1153
- const target = tryInternalGit(context, ["show-ref", "--verify", "--quiet", branchTarget]) ? branchTarget : tryInternalGit(context, ["show-ref", "--verify", "--quiet", mainTarget]) ? mainTarget : null;
1154
- if (!target) {
1155
- throw new Error(`Internal remote has neither ${input.branchName} nor main`);
1156
- }
1157
- if (hasInternalWorktreeChanges(context)) {
1158
- return null;
1159
- }
1160
- await runInternalGitAsync(context, ["reset", "--hard", target]);
1161
- await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1162
- return getInternalCommitHash(context);
1163
- }
1164
- function findRepositorySyncBlockers(input) {
1165
- const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
1166
- const blockedBy = [];
1167
- for (const shell of input.shells) {
1168
- const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
1169
- if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
1170
- }
1171
- return blockedBy;
1172
- }
1173
- function selectManifestSyncTargets(input) {
1174
- if (!input.requestedTargets) {
1175
- return input.manifests.flatMap((manifest) => {
1176
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1177
- return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1178
- });
1179
- }
1180
- const manifestsByProjectId = new Map(input.manifests.map((manifest) => [manifest.projectId, manifest]));
1181
- const selected = /* @__PURE__ */ new Map();
1182
- for (const target of input.requestedTargets) {
1183
- const manifest = manifestsByProjectId.get(target.projectId);
1184
- if (!manifest) {
1185
- continue;
1186
- }
1187
- selected.set(`${target.projectId}\0${target.branchName}`, { manifest, branchName: target.branchName });
1188
- }
1189
- return [...selected.values()];
1190
- }
1191
- async function syncManifestProjectsFromInternal(input) {
1192
- const targets = selectManifestSyncTargets({
1193
- manifests: input.manifests,
1194
- requestedTargets: input.requestedTargets
1195
- });
1196
- const blockedBy = findRepositorySyncBlockers({
1197
- targets: targets.map(({ manifest, branchName }) => ({
1198
- projectId: manifest.projectId,
1199
- projectPath: manifest.projectPath,
1200
- branchName
1201
- })),
1202
- shells: [...activePtys].map(([id, active]) => ({
1203
- id,
1204
- projectId: active.projectId,
1205
- branchName: active.branchName
1206
- }))
1207
- });
1208
- const blockedTargetKeys = new Set(blockedBy.map((blocker) => `${blocker.projectId}\0${blocker.branchName}`));
1209
- const results = [];
1210
- for (const { manifest, branchName } of targets) {
1211
- if (blockedTargetKeys.has(`${manifest.projectId}\0${branchName}`)) {
1212
- continue;
1213
- }
1214
- try {
1215
- const commitHash = await forceSyncManifestBranchFromInternal({
1216
- projectId: manifest.projectId,
1217
- baseUrl: input.baseUrl,
1218
- token: input.token,
1219
- projectRoot: path.join(input.projectsRoot, manifest.repoSlug || manifest.projectId),
1220
- syncRoot: input.syncRoot,
1221
- branchName,
1222
- manifest
1223
- });
1224
- if (commitHash === null) {
1225
- blockedBy.push({
1226
- projectId: manifest.projectId,
1227
- projectPath: manifest.projectPath,
1228
- branchName,
1229
- kind: "dirty_checkout",
1230
- id: `${manifest.projectId}:${branchName}`
1231
- });
1232
- continue;
1233
- }
1234
- results.push({
1235
- projectId: manifest.projectId,
1236
- projectPath: manifest.projectPath,
1237
- branchName,
1238
- status: "synced",
1239
- commitHash
1240
- });
1241
- } catch (error) {
1242
- results.push({
1243
- projectId: manifest.projectId,
1244
- projectPath: manifest.projectPath,
1245
- branchName,
1246
- status: "failed",
1247
- error: error instanceof Error ? error.message : String(error)
1248
- });
1249
- }
1250
- }
1251
- const hasFailures = results.some((result) => result.status === "failed");
1252
913
  return {
1253
- status: blockedBy.length > 0 && results.length === 0 ? "blocked" : blockedBy.length > 0 || hasFailures ? "partial" : "completed",
1254
- results,
1255
- blockedBy
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
+ })
1256
923
  };
1257
924
  }
1258
925
  function resolveCommandCwd(branchPath, cwd) {
@@ -1303,69 +970,6 @@ async function streamCommandOutput(stream, onData) {
1303
970
  function ensureOperationBranch(input) {
1304
971
  return ensureBranchWorkspace(input);
1305
972
  }
1306
- async function inspectManagedWorkspace(input) {
1307
- const candidates = input.manifests.flatMap((manifest) => {
1308
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1309
- return branches.map((branchName) => ({
1310
- manifest,
1311
- branchName,
1312
- checkoutPath: path.resolve(input.projectsRoot, manifest.repoSlug || manifest.projectId, branchName)
1313
- }));
1314
- });
1315
- const inspected = await Promise.all(
1316
- candidates.map(async ({ manifest, branchName, checkoutPath }) => {
1317
- const gitDir = internalGitDirFor(input.syncRoot, manifest.projectId, branchName);
1318
- if (!hasNormalVisibleGitDir(checkoutPath) || !fs.existsSync(gitDir)) return null;
1319
- const context = {
1320
- gitDir,
1321
- workTree: checkoutPath,
1322
- auth: internalGitAuth(input.baseUrl, input.token)
1323
- };
1324
- try {
1325
- const status = await runInternalGitAsync(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
1326
- let commitHash = null;
1327
- for (const revision of [
1328
- "HEAD",
1329
- `refs/remotes/${R5D_REMOTE_NAME}/${branchName}`,
1330
- `refs/remotes/${R5D_REMOTE_NAME}/main`
1331
- ]) {
1332
- try {
1333
- commitHash = await runInternalGitAsync(context, ["rev-parse", "--verify", revision]);
1334
- break;
1335
- } catch {
1336
- }
1337
- }
1338
- if (!commitHash) return null;
1339
- return {
1340
- projectId: manifest.projectId,
1341
- projectPath: manifest.projectPath,
1342
- branchName,
1343
- checkoutPath,
1344
- commitHash,
1345
- status,
1346
- dirty: status.trim().length > 0
1347
- };
1348
- } catch {
1349
- return null;
1350
- }
1351
- })
1352
- );
1353
- return {
1354
- type: "workspace_status",
1355
- checkouts: inspected.filter((checkout) => checkout !== null).sort(
1356
- (left, right) => left.projectPath.localeCompare(right.projectPath) || left.branchName.localeCompare(right.branchName)
1357
- )
1358
- };
1359
- }
1360
- function internalGitProcessEnv(workspace, env) {
1361
- if (env?.R5D_USE_INTERNAL_GIT !== "1") {
1362
- return {};
1363
- }
1364
- return {
1365
- GIT_DIR: workspace.internalGit.gitDir,
1366
- GIT_WORK_TREE: workspace.branchPath
1367
- };
1368
- }
1369
973
  function formatLineNumberedContent(content, offset, limit) {
1370
974
  const allLines = content.split("\n");
1371
975
  const totalLines = allLines.length;
@@ -1474,9 +1078,11 @@ const fileMutationQueues = /* @__PURE__ */ new Map();
1474
1078
  async function withFileMutationQueue(key, operation) {
1475
1079
  const previous = fileMutationQueues.get(key) ?? Promise.resolve();
1476
1080
  let release;
1477
- const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
1478
- release = resolve;
1479
- }));
1081
+ const current = previous.catch(() => void 0).then(
1082
+ () => new Promise((resolve) => {
1083
+ release = resolve;
1084
+ })
1085
+ );
1480
1086
  fileMutationQueues.set(key, current);
1481
1087
  await previous.catch(() => void 0);
1482
1088
  try {
@@ -1571,7 +1177,9 @@ async function executeEditFileOperation(input) {
1571
1177
  throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1572
1178
  }
1573
1179
  if (occurrences > 1) {
1574
- throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
1180
+ throw new Error(
1181
+ `edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
1182
+ );
1575
1183
  }
1576
1184
  const start = originalContent.indexOf(edit.oldText);
1577
1185
  return { index, start, end: start + edit.oldText.length, edit };
@@ -1831,114 +1439,8 @@ async function executeViewFileBytesOperation(input) {
1831
1439
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1832
1440
  return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1833
1441
  }
1834
- function stagedBlobSizeBytes(context) {
1835
- const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
1836
- let total = 0;
1837
- for (const name of names) {
1838
- try {
1839
- const sizeText = runInternalGit(context, ["cat-file", "-s", `:${name}`]);
1840
- total += Number.parseInt(sizeText, 10) || 0;
1841
- } catch {
1842
- }
1843
- if (total > MAX_SYNC_DIFF_BYTES) {
1844
- return MAX_SYNC_DIFF_BYTES + 1;
1845
- }
1846
- }
1847
- return total;
1848
- }
1849
- function syncBranch(input) {
1850
- const workspace = ensureOperationBranch(input);
1851
- runInternalGit(workspace.internalGit, ["add", "-A", "--", ".", ":(exclude).git"]);
1852
- const hasStagedChanges = !tryInternalGit(workspace.internalGit, ["diff", "--cached", "--quiet", "--", ".", ":(exclude).git"]);
1853
- const status = getInternalStatus(workspace.internalGit);
1854
- const currentCommit = getInternalCommitHash(workspace.internalGit);
1855
- if (!hasStagedChanges) {
1856
- return {
1857
- type: "sync",
1858
- changed: false,
1859
- commitHash: currentCommit,
1860
- diffSizeBytes: 0,
1861
- status
1862
- };
1863
- }
1864
- const diffSizeBytes = stagedBlobSizeBytes(workspace.internalGit);
1865
- if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
1866
- return {
1867
- type: "sync",
1868
- changed: true,
1869
- commitHash: currentCommit,
1870
- diffSizeBytes,
1871
- status,
1872
- largeDiffBlocked: true,
1873
- trigger: input.trigger
1874
- };
1875
- }
1876
- const message = JSON.stringify({
1877
- type: "agent_changes",
1878
- sessionId: input.sessionId,
1879
- parentNodeId: input.parentNodeId,
1880
- ...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
1881
- });
1882
- runInternalGit(workspace.internalGit, ["commit", "-m", message]);
1883
- const commitHash = getInternalCommitHash(workspace.internalGit);
1884
- runInternalGit(workspace.internalGit, ["push", R5D_REMOTE_NAME, `HEAD:refs/heads/${input.branchName}`]);
1885
- return {
1886
- type: "sync",
1887
- changed: true,
1888
- commitHash,
1889
- diffSizeBytes,
1890
- status: getInternalStatus(workspace.internalGit),
1891
- trigger: input.trigger
1892
- };
1893
- }
1894
- function confirmLargeDiff(input) {
1895
- const reason = input.reason.trim();
1896
- if (!reason) {
1897
- throw new Error("confirm_large_diff requires a non-empty reason");
1898
- }
1899
- const result = syncBranch({
1900
- ...input,
1901
- confirmedLargeDiff: true,
1902
- confirmationReason: reason
1903
- });
1904
- return {
1905
- type: "confirm_large_diff",
1906
- changed: result.changed,
1907
- commitHash: result.commitHash,
1908
- diffSizeBytes: result.diffSizeBytes,
1909
- status: result.status,
1910
- reason
1911
- };
1912
- }
1913
- function pullBranch(input) {
1914
- const workspace = ensureOperationBranch(input);
1915
- runInternalGit(workspace.internalGit, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1916
- const target = input.commitHash ?? `${R5D_REMOTE_NAME}/${input.branchName}`;
1917
- runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
1918
- runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1919
- return {
1920
- type: "pull_branch",
1921
- commitHash: getInternalCommitHash(workspace.internalGit)
1922
- };
1923
- }
1924
1442
  async function executeOperation(input) {
1925
1443
  switch (input.message.type) {
1926
- case "resolve_managed_path":
1927
- return resolveManagedCheckoutPath({
1928
- projectsRoot: input.projectsRoot,
1929
- manifests: input.manifests,
1930
- originProjectId: input.message.projectId,
1931
- originBranchName: input.message.branchName,
1932
- inputPath: input.message.inputPath
1933
- });
1934
- case "workspace_status":
1935
- return await inspectManagedWorkspace({
1936
- baseUrl: input.baseUrl,
1937
- token: input.token,
1938
- projectsRoot: input.projectsRoot,
1939
- syncRoot: input.syncRoot,
1940
- manifests: input.manifests
1941
- });
1942
1444
  case "read":
1943
1445
  return executeReadFileOperation({ ...input, message: input.message });
1944
1446
  case "write":
@@ -1953,43 +1455,6 @@ async function executeOperation(input) {
1953
1455
  return executeLsOperation({ ...input, message: input.message });
1954
1456
  case "view_file_bytes":
1955
1457
  return executeViewFileBytesOperation({ ...input, message: input.message });
1956
- case "sync":
1957
- return syncBranch({
1958
- projectId: input.projectId,
1959
- baseUrl: input.baseUrl,
1960
- token: input.token,
1961
- projectRoot: input.projectRoot,
1962
- syncRoot: input.syncRoot,
1963
- branchName: input.message.branchName,
1964
- sessionId: input.message.sessionId,
1965
- parentNodeId: input.message.parentNodeId,
1966
- trigger: input.message.trigger,
1967
- manifest: input.manifest
1968
- });
1969
- case "confirm_large_diff":
1970
- return confirmLargeDiff({
1971
- projectId: input.projectId,
1972
- baseUrl: input.baseUrl,
1973
- token: input.token,
1974
- projectRoot: input.projectRoot,
1975
- syncRoot: input.syncRoot,
1976
- branchName: input.message.branchName,
1977
- sessionId: input.message.sessionId,
1978
- parentNodeId: input.message.parentNodeId,
1979
- reason: input.message.reason,
1980
- manifest: input.manifest
1981
- });
1982
- case "pull_branch":
1983
- return pullBranch({
1984
- projectId: input.projectId,
1985
- baseUrl: input.baseUrl,
1986
- token: input.token,
1987
- projectRoot: input.projectRoot,
1988
- syncRoot: input.syncRoot,
1989
- branchName: input.message.branchName,
1990
- commitHash: input.message.commitHash,
1991
- manifest: input.manifest
1992
- });
1993
1458
  }
1994
1459
  }
1995
1460
  async function executeCommand(input) {
@@ -2021,7 +1486,11 @@ async function executeCommand(input) {
2021
1486
  planRoot: input.planRoot,
2022
1487
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2023
1488
  });
2024
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
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);
2025
1494
  const subprocess = Bun.spawn(input.message.argv, {
2026
1495
  cwd,
2027
1496
  stdout: "pipe",
@@ -2031,7 +1500,6 @@ async function executeCommand(input) {
2031
1500
  ...process.env,
2032
1501
  ...githubProcessEnv(),
2033
1502
  ...input.message.env ?? {},
2034
- ...internalGitProcessEnv(workspace, input.message.env),
2035
1503
  ...artifactProcessEnv,
2036
1504
  ...planProcessEnv
2037
1505
  }
@@ -2042,7 +1510,8 @@ async function executeCommand(input) {
2042
1510
  sessionId: input.message.sessionId ?? "",
2043
1511
  branchName: input.message.branchName,
2044
1512
  mode: "foreground",
2045
- startCommitHash: "",
1513
+ pid: subprocess.pid,
1514
+ processGroupId: process.platform === "win32" ? void 0 : subprocess.pid,
2046
1515
  argv: input.message.argv,
2047
1516
  command: input.message.argv.join(" "),
2048
1517
  cwd,
@@ -2128,7 +1597,11 @@ async function executeStreamingCommand(input) {
2128
1597
  planRoot: input.planRoot,
2129
1598
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2130
1599
  });
2131
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
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);
2132
1605
  const subprocess = Bun.spawn(input.message.argv, {
2133
1606
  cwd,
2134
1607
  stdout: "pipe",
@@ -2138,7 +1611,6 @@ async function executeStreamingCommand(input) {
2138
1611
  ...process.env,
2139
1612
  ...githubProcessEnv(),
2140
1613
  ...input.message.env ?? {},
2141
- ...internalGitProcessEnv(workspace, input.message.env),
2142
1614
  ...artifactProcessEnv,
2143
1615
  ...planProcessEnv
2144
1616
  }
@@ -2149,16 +1621,11 @@ async function executeStreamingCommand(input) {
2149
1621
  sessionId: input.message.sessionId,
2150
1622
  branchName: input.message.branchName,
2151
1623
  mode: input.message.mode,
2152
- startCommitHash: input.message.startCommitHash,
2153
- launchWorkspaceActionId: input.message.launchWorkspaceActionId,
2154
- credentialId: input.message.credentialId,
2155
1624
  pid: subprocess.pid,
2156
- processGroupId: subprocess.pid,
1625
+ processGroupId: process.platform === "win32" ? void 0 : subprocess.pid,
1626
+ credentialId: input.message.credentialId,
2157
1627
  argv: input.message.argv,
2158
1628
  command: input.message.command,
2159
- // Persist the resolved absolute cwd so reconnect reports and process
2160
- // history retain the actual execution location, including cross-project
2161
- // managed paths.
2162
1629
  cwd,
2163
1630
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2164
1631
  });
@@ -2169,7 +1636,7 @@ async function executeStreamingCommand(input) {
2169
1636
  runId: input.message.runId,
2170
1637
  cwd,
2171
1638
  pid: subprocess.pid,
2172
- processGroupId: subprocess.pid
1639
+ ...process.platform === "win32" ? {} : { processGroupId: subprocess.pid }
2173
1640
  });
2174
1641
  if (input.message.timeoutMs) {
2175
1642
  timeout = setTimeout(() => {
@@ -2201,22 +1668,26 @@ async function executeStreamingCommand(input) {
2201
1668
  });
2202
1669
  })
2203
1670
  ]);
2204
- sendWorkerMessage(input.ws, {
1671
+ const terminal = {
2205
1672
  type: "exec_exit",
2206
1673
  runId: input.message.runId,
2207
1674
  exitCode,
2208
1675
  durationMs: Date.now() - startedAt,
2209
1676
  ...timedOut ? { timedOut: true } : {}
2210
- });
1677
+ };
1678
+ pendingProcessTerminals.set(input.message.runId, terminal);
1679
+ sendWorkerMessage(input.ws, terminal);
2211
1680
  } catch (error) {
2212
1681
  const message = error instanceof Error ? error.message : String(error);
2213
1682
  if (started) {
2214
- sendWorkerMessage(input.ws, {
1683
+ const terminal = {
2215
1684
  type: "exec_error",
2216
1685
  runId: input.message.runId,
2217
1686
  error: message,
2218
1687
  durationMs: Date.now() - startedAt
2219
- });
1688
+ };
1689
+ pendingProcessTerminals.set(input.message.runId, terminal);
1690
+ sendWorkerMessage(input.ws, terminal);
2220
1691
  } else {
2221
1692
  sendWorkerMessage(input.ws, {
2222
1693
  type: "exec_start_error",
@@ -2248,12 +1719,12 @@ function buildActiveProcessReports() {
2248
1719
  projectId: active.projectId,
2249
1720
  sessionId: active.sessionId,
2250
1721
  branchName: active.branchName,
1722
+ platform: process.platform,
1723
+ arch: process.arch,
2251
1724
  mode: active.mode,
2252
- startCommitHash: active.startCommitHash,
2253
- ...active.launchWorkspaceActionId ? { launchWorkspaceActionId: active.launchWorkspaceActionId } : {},
2254
- ...active.credentialId ? { credentialId: active.credentialId } : {},
2255
1725
  ...active.pid !== void 0 ? { pid: active.pid } : {},
2256
1726
  ...active.processGroupId !== void 0 ? { processGroupId: active.processGroupId } : {},
1727
+ ...active.credentialId ? { credentialId: active.credentialId } : {},
2257
1728
  argv: active.argv,
2258
1729
  command: active.command,
2259
1730
  ...active.cwd ? { cwd: active.cwd } : {},
@@ -2554,10 +2025,7 @@ function resizePty(message) {
2554
2025
  if (!ptyProcess) {
2555
2026
  return;
2556
2027
  }
2557
- ptyProcess.resize(
2558
- Math.max(1, Math.min(Math.floor(message.cols || 80), 500)),
2559
- Math.max(1, Math.min(Math.floor(message.rows || 24), 500))
2560
- );
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)));
2561
2029
  }
2562
2030
  function closePty(message) {
2563
2031
  const ptyProcess = activePtys.get(message.ptyId);
@@ -2589,6 +2057,7 @@ async function startWorker(options) {
2589
2057
  validateLabel(label);
2590
2058
  const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
2591
2059
  const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
2060
+ const workspaceShadowRoot = path.join(syncRoot, "workspace");
2592
2061
  const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2593
2062
  const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
2594
2063
  process.stdout.write(`[r5d-worker] label: ${label}
@@ -2610,10 +2079,104 @@ async function startWorker(options) {
2610
2079
  fs.mkdirSync(artifactRoot, { recursive: true });
2611
2080
  fs.mkdirSync(planRoot, { recursive: true });
2612
2081
  const manifestByProjectId = /* @__PURE__ */ new Map();
2613
- let repositorySyncQueue = Promise.resolve();
2614
- let repositorySyncInProgress = false;
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;
2615
2090
  let cliUpdateInProgress = false;
2616
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
+ };
2617
2180
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2618
2181
  headers: {
2619
2182
  Authorization: `Bearer ${token}`
@@ -2658,6 +2221,15 @@ async function startWorker(options) {
2658
2221
  };
2659
2222
  ws.send(JSON.stringify(hello));
2660
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();
2661
2233
  process.stdout.write("[r5d-worker] connected\n");
2662
2234
  });
2663
2235
  ws.addEventListener("message", (event) => {
@@ -2666,91 +2238,68 @@ async function startWorker(options) {
2666
2238
  if (message.type === "connected") {
2667
2239
  return;
2668
2240
  }
2669
- if (message.type === "project_manifest") {
2241
+ if (message.type === "workspace_manifest") {
2670
2242
  configureGitHubAuth(message.githubCredential);
2671
2243
  visibleGitIdentity = message.gitIdentity;
2244
+ workspaceRemoteUrl = message.workspaceRemoteUrl;
2672
2245
  manifestByProjectId.clear();
2673
- for (const project of message.projects) {
2674
- manifestByProjectId.set(project.projectId, project);
2675
- }
2676
- 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
2677
2248
  `);
2678
- return;
2679
- }
2680
- if (message.type === "sync_projects") {
2681
- if (cliUpdateInProgress) {
2682
- sendWorkerMessage(ws, {
2683
- type: "sync_projects_result",
2684
- requestId: message.requestId,
2685
- result: {
2686
- status: "blocked",
2687
- results: [],
2688
- blockedBy: []
2689
- }
2690
- });
2691
- return;
2692
- }
2693
- let syncResult = { status: "completed", results: [], blockedBy: [] };
2694
- const executeSync = async () => {
2695
- repositorySyncInProgress = true;
2696
- try {
2697
- syncResult = await syncManifestProjectsFromInternal({
2698
- baseUrl,
2699
- token,
2700
- projectsRoot,
2701
- syncRoot,
2702
- manifests: [...manifestByProjectId.values()],
2703
- requestedTargets: message.targets
2704
- });
2705
- for (const result of syncResult.results) {
2706
- if (result.status !== "synced") continue;
2707
- try {
2708
- await syncProjectPlans({
2709
- baseUrl,
2710
- token,
2711
- projectId: result.projectId,
2712
- branchName: result.branchName,
2713
- planRoot
2714
- });
2715
- } catch (error) {
2716
- process.stderr.write(
2717
- `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2718
- `
2719
- );
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;
2720
2260
  }
2721
- }
2722
- } finally {
2723
- repositorySyncInProgress = false;
2724
- }
2725
- };
2726
- const queued = repositorySyncQueue.then(executeSync, executeSync);
2727
- repositorySyncQueue = queued.then(
2728
- () => void 0,
2729
- () => void 0
2730
- );
2731
- try {
2732
- await queued;
2733
- } catch (error) {
2734
- syncResult = {
2735
- status: "partial",
2736
- results: [
2737
- {
2738
- projectId: "worker",
2739
- projectPath: "worker",
2740
- branchName: "all",
2741
- status: "failed",
2742
- error: error instanceof Error ? error.message : String(error)
2261
+ if (previousPeriodicFingerprint !== fingerprint) {
2262
+ previousPeriodicFingerprint = fingerprint;
2263
+ return;
2743
2264
  }
2744
- ],
2745
- blockedBy: []
2746
- };
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();
2747
2277
  }
2748
- sendWorkerMessage(ws, {
2749
- type: "sync_projects_result",
2750
- requestId: message.requestId,
2751
- result: syncResult
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
2752
2287
  });
2753
- sendActiveProcessReport(ws);
2288
+ return;
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);
2754
2303
  return;
2755
2304
  }
2756
2305
  if (message.type === "update_clis") {
@@ -2762,7 +2311,7 @@ async function startWorker(options) {
2762
2311
  });
2763
2312
  return;
2764
2313
  }
2765
- if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
2314
+ if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
2766
2315
  sendWorkerMessage(ws, {
2767
2316
  type: "update_clis_result",
2768
2317
  requestId: message.requestId,
@@ -2832,7 +2381,6 @@ async function startWorker(options) {
2832
2381
  });
2833
2382
  return;
2834
2383
  }
2835
- await repositorySyncQueue;
2836
2384
  const manifest = manifestByProjectId.get(message.projectId);
2837
2385
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2838
2386
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2863,7 +2411,6 @@ async function startWorker(options) {
2863
2411
  });
2864
2412
  return;
2865
2413
  }
2866
- await repositorySyncQueue;
2867
2414
  const manifest = manifestByProjectId.get(message.projectId);
2868
2415
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2869
2416
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2877,6 +2424,7 @@ async function startWorker(options) {
2877
2424
  syncRoot,
2878
2425
  artifactRoot,
2879
2426
  planRoot,
2427
+ workspaceShadowRoot,
2880
2428
  manifest
2881
2429
  });
2882
2430
  ws.send(JSON.stringify(result));
@@ -2892,7 +2440,6 @@ async function startWorker(options) {
2892
2440
  });
2893
2441
  return;
2894
2442
  }
2895
- await repositorySyncQueue;
2896
2443
  sendWorkerMessage(ws, {
2897
2444
  type: "exec_accepted",
2898
2445
  requestId: message.requestId,
@@ -2913,6 +2460,7 @@ async function startWorker(options) {
2913
2460
  syncRoot,
2914
2461
  artifactRoot,
2915
2462
  planRoot,
2463
+ workspaceShadowRoot,
2916
2464
  manifest
2917
2465
  }).catch((error) => {
2918
2466
  sendWorkerMessage(ws, {
@@ -2932,9 +2480,8 @@ async function startWorker(options) {
2932
2480
  }
2933
2481
  return;
2934
2482
  }
2935
- 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") {
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") {
2936
2484
  try {
2937
- await repositorySyncQueue;
2938
2485
  const manifest = manifestByProjectId.get(message.projectId);
2939
2486
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2940
2487
  const result = await executeOperation({
@@ -2945,8 +2492,6 @@ async function startWorker(options) {
2945
2492
  projectRoot,
2946
2493
  syncRoot,
2947
2494
  artifactRoot,
2948
- projectsRoot,
2949
- manifests: [...manifestByProjectId.values()],
2950
2495
  manifest
2951
2496
  });
2952
2497
  ws.send(
@@ -2984,6 +2529,14 @@ async function startWorker(options) {
2984
2529
  });
2985
2530
  ws.addEventListener("close", (event) => {
2986
2531
  stopHeartbeatWatchdog();
2532
+ if (periodicWorkspaceScan) {
2533
+ clearInterval(periodicWorkspaceScan);
2534
+ periodicWorkspaceScan = void 0;
2535
+ }
2536
+ if (terminalReplayTimer) {
2537
+ clearInterval(terminalReplayTimer);
2538
+ terminalReplayTimer = void 0;
2539
+ }
2987
2540
  if (currentWorkerSocket === ws) {
2988
2541
  currentWorkerSocket = null;
2989
2542
  }
@@ -2994,15 +2547,17 @@ async function startWorker(options) {
2994
2547
  if (reloadAfterClose) {
2995
2548
  process.exit(WORKER_RELOAD_EXIT_CODE);
2996
2549
  }
2997
- if (activeProcesses.size > 0) {
2550
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
2998
2551
  const delayMs = 2e3;
2999
- process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
3000
- `);
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
+ );
3001
2556
  const reconnect = () => {
3002
2557
  void startWorker(options).catch((error) => {
3003
2558
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
3004
2559
  `);
3005
- if (activeProcesses.size > 0) {
2560
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
3006
2561
  setTimeout(reconnect, delayMs);
3007
2562
  return;
3008
2563
  }
@@ -3057,11 +2612,9 @@ if (isCliEntrypoint()) {
3057
2612
  }
3058
2613
  export {
3059
2614
  ensureVisibleGitCheckout,
3060
- findRepositorySyncBlockers,
3061
2615
  findWorkerFiles,
3062
2616
  githubCliEnv,
3063
2617
  grepWorkerFiles,
3064
- inspectManagedWorkspace,
3065
2618
  isArtifactEnvPath,
3066
2619
  listWorkerDirectory,
3067
2620
  prepareArtifactEnvForShell,
@@ -3069,11 +2622,8 @@ export {
3069
2622
  readWorkerImageFile,
3070
2623
  readWorkerTextFile,
3071
2624
  resolveHostShell,
3072
- resolveManagedCheckoutPath,
3073
2625
  resolveProjectFilePath,
3074
2626
  resolveWorkerFilePath,
3075
- selectManifestSyncTargets,
3076
- syncManifestProjectsFromInternal,
3077
2627
  syncProjectPlans,
3078
2628
  syncSessionArtifacts
3079
2629
  };