@ricsam/r5d-worker 0.0.36 → 0.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/main.cjs CHANGED
@@ -30,7 +30,6 @@ 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,
@@ -43,7 +42,6 @@ __export(main_exports, {
43
42
  resolveHostShell: () => resolveHostShell,
44
43
  resolveProjectFilePath: () => resolveProjectFilePath,
45
44
  resolveWorkerFilePath: () => resolveWorkerFilePath,
46
- syncManifestProjectsFromInternal: () => syncManifestProjectsFromInternal,
47
45
  syncProjectPlans: () => syncProjectPlans,
48
46
  syncSessionArtifacts: () => syncSessionArtifacts
49
47
  });
@@ -58,6 +56,7 @@ var import_git_identity = require("./git-identity.cjs");
58
56
  var import_heartbeat = require("./heartbeat.cjs");
59
57
  var import_process_tree = require("./process-tree.cjs");
60
58
  var import_supervisor = require("./supervisor.cjs");
59
+ var import_workspace_sync = require("./workspace-sync.cjs");
61
60
  const import_meta = {};
62
61
  const DEFAULT_BASE_URL = "https://r5d.dev";
63
62
  const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
@@ -66,10 +65,8 @@ const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
66
65
  const DEFAULT_READ_LIMIT = 2e3;
67
66
  const DEFAULT_READ_MAX_BYTES = 5e4;
68
67
  const MAX_LINE_LENGTH = 2e3;
69
- const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
70
- const R5D_REMOTE_NAME = "r5d";
71
- const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
72
68
  const activeProcesses = /* @__PURE__ */ new Map();
69
+ const pendingProcessTerminals = /* @__PURE__ */ new Map();
73
70
  const cancelledProcessRuns = /* @__PURE__ */ new Set();
74
71
  const activePtys = /* @__PURE__ */ new Map();
75
72
  let currentWorkerSocket = null;
@@ -528,8 +525,10 @@ async function prepareArtifactEnvForShell(input) {
528
525
  artifactRoot: input.artifactRoot
529
526
  });
530
527
  } catch (error) {
531
- process.stderr.write(`[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
532
- `);
528
+ process.stderr.write(
529
+ `[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
530
+ `
531
+ );
533
532
  }
534
533
  const artifactsDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
535
534
  import_node_fs.default.mkdirSync(artifactsDir, { recursive: true });
@@ -585,7 +584,9 @@ function resolveCredentials(options, config) {
585
584
  throw new Error("Authentication required. Run `r5dctl auth login` or set R5D_WORKER_TOKEN/R5D_API_KEY.");
586
585
  }
587
586
  return {
588
- 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
+ ),
589
590
  token
590
591
  };
591
592
  }
@@ -639,27 +640,6 @@ function runGit(args, options = {}) {
639
640
  }
640
641
  return result.stdout.toString().trim();
641
642
  }
642
- async function runGitAsync(args, options = {}) {
643
- const command = ["git", ...gitAuthArgs(options.auth), ...args];
644
- const subprocess = Bun.spawn(command, {
645
- cwd: options.cwd,
646
- stdout: "pipe",
647
- stderr: "pipe",
648
- env: {
649
- ...process.env,
650
- GIT_TERMINAL_PROMPT: "0"
651
- }
652
- });
653
- const [stdout, stderr, exitCode] = await Promise.all([
654
- collectStream(subprocess.stdout),
655
- collectStream(subprocess.stderr),
656
- subprocess.exited
657
- ]);
658
- if (exitCode !== 0) {
659
- throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
660
- }
661
- return stdout.trim();
662
- }
663
643
  function tryGit(args, options = {}) {
664
644
  try {
665
645
  runGit(args, options);
@@ -668,34 +648,6 @@ function tryGit(args, options = {}) {
668
648
  return false;
669
649
  }
670
650
  }
671
- async function tryGitAsync(args, options = {}) {
672
- try {
673
- await runGitAsync(args, options);
674
- return true;
675
- } catch {
676
- return false;
677
- }
678
- }
679
- function runInternalGit(context, args) {
680
- return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
681
- }
682
- function runInternalGitAsync(context, args) {
683
- return runGitAsync(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
684
- }
685
- function runInternalGitDir(context, args) {
686
- return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
687
- }
688
- function tryInternalGit(context, args) {
689
- try {
690
- runInternalGit(context, args);
691
- return true;
692
- } catch {
693
- return false;
694
- }
695
- }
696
- function getInternalCommitHash(context) {
697
- return runInternalGit(context, ["rev-parse", "HEAD"]);
698
- }
699
651
  function getGitBlobHashForContent(content) {
700
652
  const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
701
653
  return (0, import_node_crypto.createHash)("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
@@ -703,12 +655,6 @@ function getGitBlobHashForContent(content) {
703
655
  function hasWorktreeChanges(cwd) {
704
656
  return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
705
657
  }
706
- function getInternalStatus(context) {
707
- return runInternalGit(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
708
- }
709
- function hasInternalWorktreeChanges(context) {
710
- return getInternalStatus(context).trim().length > 0;
711
- }
712
658
  function isInsideBranchPath(branchPath, filePath) {
713
659
  const relative = import_node_path.default.relative(import_node_path.default.resolve(branchPath), import_node_path.default.resolve(filePath));
714
660
  return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
@@ -771,32 +717,15 @@ function resolveWorkerFilePath(branchPath, inputPath) {
771
717
  function resolveProjectFilePath(branchPath, inputPath) {
772
718
  const resolved = resolveWorkerFilePath(branchPath, inputPath);
773
719
  if (resolved.scope === "host") {
774
- throw new Error(
775
- `Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`
776
- );
720
+ throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
777
721
  }
778
722
  return resolved;
779
723
  }
780
- function resolveRemoteUrl(baseUrl, remoteUrl) {
781
- if (/^https?:\/\//i.test(remoteUrl)) {
782
- return remoteUrl;
783
- }
784
- return new URL(remoteUrl, baseUrl).toString();
785
- }
786
724
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
787
725
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
788
726
  }
789
- function internalGitAuth(baseUrl, token) {
790
- return {
791
- extraHeaderUrl: baseUrl,
792
- header: `Authorization: Bearer ${token}`
793
- };
794
- }
795
- function internalRemoteUrlFor(baseUrl, projectId, manifest) {
796
- return resolveRemoteUrl(baseUrl, manifest?.internalRemoteUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId));
797
- }
798
727
  function githubRemoteUrlFor(baseUrl, projectId, manifest) {
799
- return manifest?.repoHttpUrl ?? internalRemoteUrlFor(baseUrl, projectId, manifest);
728
+ return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
800
729
  }
801
730
  function gitExtraHeaderUrlForRemote(remoteUrl) {
802
731
  try {
@@ -814,12 +743,6 @@ function gitAuthForRemote(remoteUrl, authHeader) {
814
743
  header: authHeader
815
744
  };
816
745
  }
817
- function branchGitDirName(branchName) {
818
- return `${encodeURIComponent(branchName)}.git`;
819
- }
820
- function internalGitDirFor(syncRoot, projectId, branchName) {
821
- return import_node_path.default.join(syncRoot, projectId, branchGitDirName(branchName));
822
- }
823
746
  function hasNormalVisibleGitDir(branchPath) {
824
747
  const gitPath = import_node_path.default.join(branchPath, ".git");
825
748
  return import_node_fs.default.existsSync(gitPath) && import_node_fs.default.statSync(gitPath).isDirectory();
@@ -831,13 +754,6 @@ function ensureOriginRemote(branchPath, remoteUrl) {
831
754
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
832
755
  }
833
756
  }
834
- async function ensureOriginRemoteAsync(branchPath, remoteUrl) {
835
- if (await tryGitAsync(["remote", "get-url", "origin"], { cwd: branchPath })) {
836
- await runGitAsync(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
837
- } else {
838
- await runGitAsync(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
839
- }
840
- }
841
757
  function shellQuote(value) {
842
758
  return `'${value.replace(/'/g, "'\\''")}'`;
843
759
  }
@@ -1033,194 +949,20 @@ function ensureVisibleGitCheckout(input) {
1033
949
  (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1034
950
  return branchPath;
1035
951
  }
1036
- function configureInternalRepo(context, remoteUrl) {
1037
- if (tryInternalGit(context, ["remote", "get-url", R5D_REMOTE_NAME])) {
1038
- runInternalGit(context, ["remote", "set-url", R5D_REMOTE_NAME, remoteUrl]);
1039
- } else {
1040
- runInternalGit(context, ["remote", "add", R5D_REMOTE_NAME, remoteUrl]);
1041
- }
1042
- if (tryInternalGit(context, ["remote", "get-url", LEGACY_BUILD_IT_NOW_REMOTE_NAME])) {
1043
- runInternalGit(context, ["remote", "remove", LEGACY_BUILD_IT_NOW_REMOTE_NAME]);
1044
- }
1045
- runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
1046
- runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
1047
- }
1048
- function ensureInternalSyncGit(input) {
1049
- validateBranchName(input.branchName);
1050
- const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1051
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(gitDir), { recursive: true });
1052
- if (!import_node_fs.default.existsSync(gitDir)) {
1053
- runGit(["init", "--bare", gitDir]);
1054
- }
1055
- const auth = internalGitAuth(input.baseUrl, input.token);
1056
- const context = { gitDir, workTree: input.branchPath, auth };
1057
- const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1058
- configureInternalRepo(context, remoteUrl);
1059
- runInternalGit(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1060
- const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`]);
1061
- const target = hasRemoteBranch ? `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}` : `refs/remotes/${R5D_REMOTE_NAME}/main`;
1062
- const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
1063
- if (!hasHead || !hasInternalWorktreeChanges(context)) {
1064
- runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
1065
- }
1066
- return context;
1067
- }
1068
952
  function ensureBranchWorkspace(input) {
1069
953
  const defaultBranch = input.manifest?.defaultBranch || "main";
1070
954
  const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1071
955
  const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
1072
- const branchPath = ensureVisibleGitCheckout({
1073
- projectRoot: input.projectRoot,
1074
- branchName: input.branchName,
1075
- githubRemoteUrl,
1076
- githubAuth,
1077
- githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
1078
- defaultBranch,
1079
- reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
1080
- });
1081
- const internalGit = ensureInternalSyncGit({
1082
- projectId: input.projectId,
1083
- baseUrl: input.baseUrl,
1084
- token: input.token,
1085
- syncRoot: input.syncRoot,
1086
- branchPath,
1087
- branchName: input.branchName,
1088
- manifest: input.manifest
1089
- });
1090
- return { branchPath, internalGit };
1091
- }
1092
- async function ensureVisibleGitCheckoutForRepositorySync(input) {
1093
- validateBranchName(input.branchName);
1094
- import_node_fs.default.mkdirSync(input.projectRoot, { recursive: true });
1095
- const branchPath = import_node_path.default.join(input.projectRoot, input.branchName);
1096
- const createdCheckout = !hasNormalVisibleGitDir(branchPath);
1097
- if (createdCheckout) {
1098
- import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1099
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(branchPath), { recursive: true });
1100
- if (!await tryGitAsync(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
1101
- import_node_fs.default.mkdirSync(branchPath, { recursive: true });
1102
- await runGitAsync(["init"], { cwd: branchPath });
1103
- }
1104
- await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1105
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1106
- (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1107
- await tryGitAsync(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
1108
- checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1109
- return branchPath;
1110
- }
1111
- if (input.reconcileExistingOrigin) {
1112
- await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1113
- }
1114
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1115
- (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1116
- return branchPath;
1117
- }
1118
- async function forceSyncManifestBranchFromInternal(input) {
1119
- const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1120
- const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest.repoAuthHeader);
1121
- const branchPath = await ensureVisibleGitCheckoutForRepositorySync({
1122
- projectRoot: input.projectRoot,
1123
- branchName: input.branchName,
1124
- githubRemoteUrl,
1125
- githubAuth,
1126
- githubAuthHeader: input.manifest.repoAuthHeader,
1127
- defaultBranch: input.manifest.defaultBranch || "main",
1128
- reconcileExistingOrigin: Boolean(input.manifest.repoHttpUrl)
1129
- });
1130
- const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1131
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(gitDir), { recursive: true });
1132
- if (!import_node_fs.default.existsSync(gitDir)) {
1133
- await runGitAsync(["init", "--bare", gitDir]);
1134
- }
1135
- const context = {
1136
- gitDir,
1137
- workTree: branchPath,
1138
- auth: internalGitAuth(input.baseUrl, input.token)
1139
- };
1140
- configureInternalRepo(context, internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest));
1141
- await runInternalGitAsync(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1142
- const branchTarget = `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`;
1143
- const mainTarget = `refs/remotes/${R5D_REMOTE_NAME}/main`;
1144
- const target = tryInternalGit(context, ["show-ref", "--verify", "--quiet", branchTarget]) ? branchTarget : tryInternalGit(context, ["show-ref", "--verify", "--quiet", mainTarget]) ? mainTarget : null;
1145
- if (!target) {
1146
- throw new Error(`Internal remote has neither ${input.branchName} nor main`);
1147
- }
1148
- await runInternalGitAsync(context, ["reset", "--hard", target]);
1149
- await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1150
- return getInternalCommitHash(context);
1151
- }
1152
- function findRepositorySyncBlockers(input) {
1153
- const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
1154
- const blockedBy = [];
1155
- for (const process2 of input.processes) {
1156
- const target = targetByKey.get(`${process2.projectId}:${process2.branchName}`);
1157
- if (target) blockedBy.push({ ...target, kind: "process", id: process2.id });
1158
- }
1159
- for (const shell of input.shells) {
1160
- const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
1161
- if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
1162
- }
1163
- return blockedBy;
1164
- }
1165
- async function syncManifestProjectsFromInternal(input) {
1166
- const requestedProjectIds = input.projectIds ? new Set(input.projectIds) : null;
1167
- const targets = input.manifests.filter((manifest) => !requestedProjectIds || requestedProjectIds.has(manifest.projectId)).flatMap((manifest) => {
1168
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1169
- return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1170
- });
1171
- const blockedBy = findRepositorySyncBlockers({
1172
- targets: targets.map(({ manifest, branchName }) => ({
1173
- projectId: manifest.projectId,
1174
- projectPath: manifest.projectPath,
1175
- branchName
1176
- })),
1177
- processes: [...activeProcesses].map(([id, active]) => ({
1178
- id,
1179
- projectId: active.projectId,
1180
- branchName: active.branchName
1181
- })),
1182
- shells: [...activePtys].map(([id, active]) => ({
1183
- id,
1184
- projectId: active.projectId,
1185
- branchName: active.branchName
1186
- }))
1187
- });
1188
- if (blockedBy.length > 0) {
1189
- return { status: "blocked", results: [], blockedBy };
1190
- }
1191
- const results = [];
1192
- for (const { manifest, branchName } of targets) {
1193
- try {
1194
- const commitHash = await forceSyncManifestBranchFromInternal({
1195
- projectId: manifest.projectId,
1196
- baseUrl: input.baseUrl,
1197
- token: input.token,
1198
- projectRoot: import_node_path.default.join(input.projectsRoot, manifest.repoSlug || manifest.projectId),
1199
- syncRoot: input.syncRoot,
1200
- branchName,
1201
- manifest
1202
- });
1203
- results.push({
1204
- projectId: manifest.projectId,
1205
- projectPath: manifest.projectPath,
1206
- branchName,
1207
- status: "synced",
1208
- commitHash
1209
- });
1210
- } catch (error) {
1211
- results.push({
1212
- projectId: manifest.projectId,
1213
- projectPath: manifest.projectPath,
1214
- branchName,
1215
- status: "failed",
1216
- error: error instanceof Error ? error.message : String(error)
1217
- });
1218
- }
1219
- }
1220
956
  return {
1221
- status: results.some((result) => result.status === "failed") ? "partial" : "completed",
1222
- results,
1223
- 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
+ })
1224
966
  };
1225
967
  }
1226
968
  function resolveCommandCwd(branchPath, cwd) {
@@ -1271,15 +1013,6 @@ async function streamCommandOutput(stream, onData) {
1271
1013
  function ensureOperationBranch(input) {
1272
1014
  return ensureBranchWorkspace(input);
1273
1015
  }
1274
- function internalGitProcessEnv(workspace, env) {
1275
- if (env?.R5D_USE_INTERNAL_GIT !== "1") {
1276
- return {};
1277
- }
1278
- return {
1279
- GIT_DIR: workspace.internalGit.gitDir,
1280
- GIT_WORK_TREE: workspace.branchPath
1281
- };
1282
- }
1283
1016
  function formatLineNumberedContent(content, offset, limit) {
1284
1017
  const allLines = content.split("\n");
1285
1018
  const totalLines = allLines.length;
@@ -1388,9 +1121,11 @@ const fileMutationQueues = /* @__PURE__ */ new Map();
1388
1121
  async function withFileMutationQueue(key, operation) {
1389
1122
  const previous = fileMutationQueues.get(key) ?? Promise.resolve();
1390
1123
  let release;
1391
- const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
1392
- release = resolve;
1393
- }));
1124
+ const current = previous.catch(() => void 0).then(
1125
+ () => new Promise((resolve) => {
1126
+ release = resolve;
1127
+ })
1128
+ );
1394
1129
  fileMutationQueues.set(key, current);
1395
1130
  await previous.catch(() => void 0);
1396
1131
  try {
@@ -1485,7 +1220,9 @@ async function executeEditFileOperation(input) {
1485
1220
  throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1486
1221
  }
1487
1222
  if (occurrences > 1) {
1488
- 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
+ );
1489
1226
  }
1490
1227
  const start = originalContent.indexOf(edit.oldText);
1491
1228
  return { index, start, end: start + edit.oldText.length, edit };
@@ -1745,96 +1482,6 @@ async function executeViewFileBytesOperation(input) {
1745
1482
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1746
1483
  return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1747
1484
  }
1748
- function stagedBlobSizeBytes(context) {
1749
- const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
1750
- let total = 0;
1751
- for (const name of names) {
1752
- try {
1753
- const sizeText = runInternalGit(context, ["cat-file", "-s", `:${name}`]);
1754
- total += Number.parseInt(sizeText, 10) || 0;
1755
- } catch {
1756
- }
1757
- if (total > MAX_SYNC_DIFF_BYTES) {
1758
- return MAX_SYNC_DIFF_BYTES + 1;
1759
- }
1760
- }
1761
- return total;
1762
- }
1763
- function syncBranch(input) {
1764
- const workspace = ensureOperationBranch(input);
1765
- runInternalGit(workspace.internalGit, ["add", "-A", "--", ".", ":(exclude).git"]);
1766
- const hasStagedChanges = !tryInternalGit(workspace.internalGit, ["diff", "--cached", "--quiet", "--", ".", ":(exclude).git"]);
1767
- const status = getInternalStatus(workspace.internalGit);
1768
- const currentCommit = getInternalCommitHash(workspace.internalGit);
1769
- if (!hasStagedChanges) {
1770
- return {
1771
- type: "sync",
1772
- changed: false,
1773
- commitHash: currentCommit,
1774
- diffSizeBytes: 0,
1775
- status
1776
- };
1777
- }
1778
- const diffSizeBytes = stagedBlobSizeBytes(workspace.internalGit);
1779
- if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
1780
- return {
1781
- type: "sync",
1782
- changed: true,
1783
- commitHash: currentCommit,
1784
- diffSizeBytes,
1785
- status,
1786
- largeDiffBlocked: true,
1787
- trigger: input.trigger
1788
- };
1789
- }
1790
- const message = JSON.stringify({
1791
- type: "agent_changes",
1792
- sessionId: input.sessionId,
1793
- parentNodeId: input.parentNodeId,
1794
- ...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
1795
- });
1796
- runInternalGit(workspace.internalGit, ["commit", "-m", message]);
1797
- const commitHash = getInternalCommitHash(workspace.internalGit);
1798
- runInternalGit(workspace.internalGit, ["push", R5D_REMOTE_NAME, `HEAD:refs/heads/${input.branchName}`]);
1799
- return {
1800
- type: "sync",
1801
- changed: true,
1802
- commitHash,
1803
- diffSizeBytes,
1804
- status: getInternalStatus(workspace.internalGit),
1805
- trigger: input.trigger
1806
- };
1807
- }
1808
- function confirmLargeDiff(input) {
1809
- const reason = input.reason.trim();
1810
- if (!reason) {
1811
- throw new Error("confirm_large_diff requires a non-empty reason");
1812
- }
1813
- const result = syncBranch({
1814
- ...input,
1815
- confirmedLargeDiff: true,
1816
- confirmationReason: reason
1817
- });
1818
- return {
1819
- type: "confirm_large_diff",
1820
- changed: result.changed,
1821
- commitHash: result.commitHash,
1822
- diffSizeBytes: result.diffSizeBytes,
1823
- status: result.status,
1824
- reason
1825
- };
1826
- }
1827
- function pullBranch(input) {
1828
- const workspace = ensureOperationBranch(input);
1829
- runInternalGit(workspace.internalGit, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1830
- const target = input.commitHash ?? `${R5D_REMOTE_NAME}/${input.branchName}`;
1831
- runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
1832
- runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1833
- return {
1834
- type: "pull_branch",
1835
- commitHash: getInternalCommitHash(workspace.internalGit)
1836
- };
1837
- }
1838
1485
  async function executeOperation(input) {
1839
1486
  switch (input.message.type) {
1840
1487
  case "read":
@@ -1851,43 +1498,6 @@ async function executeOperation(input) {
1851
1498
  return executeLsOperation({ ...input, message: input.message });
1852
1499
  case "view_file_bytes":
1853
1500
  return executeViewFileBytesOperation({ ...input, message: input.message });
1854
- case "sync":
1855
- return syncBranch({
1856
- projectId: input.projectId,
1857
- baseUrl: input.baseUrl,
1858
- token: input.token,
1859
- projectRoot: input.projectRoot,
1860
- syncRoot: input.syncRoot,
1861
- branchName: input.message.branchName,
1862
- sessionId: input.message.sessionId,
1863
- parentNodeId: input.message.parentNodeId,
1864
- trigger: input.message.trigger,
1865
- manifest: input.manifest
1866
- });
1867
- case "confirm_large_diff":
1868
- return confirmLargeDiff({
1869
- projectId: input.projectId,
1870
- baseUrl: input.baseUrl,
1871
- token: input.token,
1872
- projectRoot: input.projectRoot,
1873
- syncRoot: input.syncRoot,
1874
- branchName: input.message.branchName,
1875
- sessionId: input.message.sessionId,
1876
- parentNodeId: input.message.parentNodeId,
1877
- reason: input.message.reason,
1878
- manifest: input.manifest
1879
- });
1880
- case "pull_branch":
1881
- return pullBranch({
1882
- projectId: input.projectId,
1883
- baseUrl: input.baseUrl,
1884
- token: input.token,
1885
- projectRoot: input.projectRoot,
1886
- syncRoot: input.syncRoot,
1887
- branchName: input.message.branchName,
1888
- commitHash: input.message.commitHash,
1889
- manifest: input.manifest
1890
- });
1891
1501
  }
1892
1502
  }
1893
1503
  async function executeCommand(input) {
@@ -1919,7 +1529,11 @@ async function executeCommand(input) {
1919
1529
  planRoot: input.planRoot,
1920
1530
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1921
1531
  });
1922
- 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);
1923
1537
  const subprocess = Bun.spawn(input.message.argv, {
1924
1538
  cwd,
1925
1539
  stdout: "pipe",
@@ -1929,7 +1543,6 @@ async function executeCommand(input) {
1929
1543
  ...process.env,
1930
1544
  ...githubProcessEnv(),
1931
1545
  ...input.message.env ?? {},
1932
- ...internalGitProcessEnv(workspace, input.message.env),
1933
1546
  ...artifactProcessEnv,
1934
1547
  ...planProcessEnv
1935
1548
  }
@@ -2027,7 +1640,11 @@ async function executeStreamingCommand(input) {
2027
1640
  planRoot: input.planRoot,
2028
1641
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2029
1642
  });
2030
- 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);
2031
1648
  const subprocess = Bun.spawn(input.message.argv, {
2032
1649
  cwd,
2033
1650
  stdout: "pipe",
@@ -2037,7 +1654,6 @@ async function executeStreamingCommand(input) {
2037
1654
  ...process.env,
2038
1655
  ...githubProcessEnv(),
2039
1656
  ...input.message.env ?? {},
2040
- ...internalGitProcessEnv(workspace, input.message.env),
2041
1657
  ...artifactProcessEnv,
2042
1658
  ...planProcessEnv
2043
1659
  }
@@ -2095,22 +1711,26 @@ async function executeStreamingCommand(input) {
2095
1711
  });
2096
1712
  })
2097
1713
  ]);
2098
- sendWorkerMessage(input.ws, {
1714
+ const terminal = {
2099
1715
  type: "exec_exit",
2100
1716
  runId: input.message.runId,
2101
1717
  exitCode,
2102
1718
  durationMs: Date.now() - startedAt,
2103
1719
  ...timedOut ? { timedOut: true } : {}
2104
- });
1720
+ };
1721
+ pendingProcessTerminals.set(input.message.runId, terminal);
1722
+ sendWorkerMessage(input.ws, terminal);
2105
1723
  } catch (error) {
2106
1724
  const message = error instanceof Error ? error.message : String(error);
2107
1725
  if (started) {
2108
- sendWorkerMessage(input.ws, {
1726
+ const terminal = {
2109
1727
  type: "exec_error",
2110
1728
  runId: input.message.runId,
2111
1729
  error: message,
2112
1730
  durationMs: Date.now() - startedAt
2113
- });
1731
+ };
1732
+ pendingProcessTerminals.set(input.message.runId, terminal);
1733
+ sendWorkerMessage(input.ws, terminal);
2114
1734
  } else {
2115
1735
  sendWorkerMessage(input.ws, {
2116
1736
  type: "exec_start_error",
@@ -2448,10 +2068,7 @@ function resizePty(message) {
2448
2068
  if (!ptyProcess) {
2449
2069
  return;
2450
2070
  }
2451
- ptyProcess.resize(
2452
- Math.max(1, Math.min(Math.floor(message.cols || 80), 500)),
2453
- Math.max(1, Math.min(Math.floor(message.rows || 24), 500))
2454
- );
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)));
2455
2072
  }
2456
2073
  function closePty(message) {
2457
2074
  const ptyProcess = activePtys.get(message.ptyId);
@@ -2483,6 +2100,7 @@ async function startWorker(options) {
2483
2100
  validateLabel(label);
2484
2101
  const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
2485
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");
2486
2104
  const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2487
2105
  const planRoot = import_node_path.default.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
2488
2106
  process.stdout.write(`[r5d-worker] label: ${label}
@@ -2504,10 +2122,104 @@ async function startWorker(options) {
2504
2122
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2505
2123
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2506
2124
  const manifestByProjectId = /* @__PURE__ */ new Map();
2507
- let repositorySyncQueue = Promise.resolve();
2508
- 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;
2509
2133
  let cliUpdateInProgress = false;
2510
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
+ };
2511
2223
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2512
2224
  headers: {
2513
2225
  Authorization: `Bearer ${token}`
@@ -2552,6 +2264,15 @@ async function startWorker(options) {
2552
2264
  };
2553
2265
  ws.send(JSON.stringify(hello));
2554
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();
2555
2276
  process.stdout.write("[r5d-worker] connected\n");
2556
2277
  });
2557
2278
  ws.addEventListener("message", (event) => {
@@ -2560,92 +2281,70 @@ async function startWorker(options) {
2560
2281
  if (message.type === "connected") {
2561
2282
  return;
2562
2283
  }
2563
- if (message.type === "project_manifest") {
2284
+ if (message.type === "workspace_manifest") {
2564
2285
  configureGitHubAuth(message.githubCredential);
2565
2286
  visibleGitIdentity = message.gitIdentity;
2287
+ workspaceRemoteUrl = message.workspaceRemoteUrl;
2566
2288
  manifestByProjectId.clear();
2567
- for (const project of message.projects) {
2568
- manifestByProjectId.set(project.projectId, project);
2569
- }
2570
- 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
2571
2291
  `);
2572
- return;
2573
- }
2574
- if (message.type === "sync_projects") {
2575
- if (cliUpdateInProgress) {
2576
- sendWorkerMessage(ws, {
2577
- type: "sync_projects_result",
2578
- requestId: message.requestId,
2579
- result: {
2580
- status: "blocked",
2581
- results: [],
2582
- blockedBy: []
2583
- }
2584
- });
2585
- return;
2586
- }
2587
- let syncResult = { status: "completed", results: [], blockedBy: [] };
2588
- const executeSync = async () => {
2589
- repositorySyncInProgress = true;
2590
- try {
2591
- syncResult = await syncManifestProjectsFromInternal({
2592
- baseUrl,
2593
- token,
2594
- projectsRoot,
2595
- syncRoot,
2596
- manifests: [...manifestByProjectId.values()],
2597
- projectIds: message.projectIds
2598
- });
2599
- for (const result of syncResult.results) {
2600
- if (result.status !== "synced") continue;
2601
- try {
2602
- await syncProjectPlans({
2603
- baseUrl,
2604
- token,
2605
- projectId: result.projectId,
2606
- branchName: result.branchName,
2607
- planRoot
2608
- });
2609
- } catch (error) {
2610
- process.stderr.write(
2611
- `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2612
- `
2613
- );
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;
2614
2303
  }
2615
- }
2616
- } finally {
2617
- repositorySyncInProgress = false;
2618
- }
2619
- };
2620
- const queued = repositorySyncQueue.then(executeSync, executeSync);
2621
- repositorySyncQueue = queued.then(
2622
- () => void 0,
2623
- () => void 0
2624
- );
2625
- try {
2626
- await queued;
2627
- } catch (error) {
2628
- syncResult = {
2629
- status: "partial",
2630
- results: [
2631
- {
2632
- projectId: "worker",
2633
- projectPath: "worker",
2634
- branchName: "all",
2635
- status: "failed",
2636
- error: error instanceof Error ? error.message : String(error)
2304
+ if (previousPeriodicFingerprint !== fingerprint) {
2305
+ previousPeriodicFingerprint = fingerprint;
2306
+ return;
2637
2307
  }
2638
- ],
2639
- blockedBy: []
2640
- };
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();
2641
2320
  }
2642
- sendWorkerMessage(ws, {
2643
- type: "sync_projects_result",
2644
- requestId: message.requestId,
2645
- 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
2646
2330
  });
2647
2331
  return;
2648
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);
2346
+ return;
2347
+ }
2649
2348
  if (message.type === "update_clis") {
2650
2349
  if (cliUpdateInProgress) {
2651
2350
  sendWorkerMessage(ws, {
@@ -2655,7 +2354,7 @@ async function startWorker(options) {
2655
2354
  });
2656
2355
  return;
2657
2356
  }
2658
- if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
2357
+ if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
2659
2358
  sendWorkerMessage(ws, {
2660
2359
  type: "update_clis_result",
2661
2360
  requestId: message.requestId,
@@ -2725,7 +2424,6 @@ async function startWorker(options) {
2725
2424
  });
2726
2425
  return;
2727
2426
  }
2728
- await repositorySyncQueue;
2729
2427
  const manifest = manifestByProjectId.get(message.projectId);
2730
2428
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2731
2429
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2756,7 +2454,6 @@ async function startWorker(options) {
2756
2454
  });
2757
2455
  return;
2758
2456
  }
2759
- await repositorySyncQueue;
2760
2457
  const manifest = manifestByProjectId.get(message.projectId);
2761
2458
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2762
2459
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2770,6 +2467,7 @@ async function startWorker(options) {
2770
2467
  syncRoot,
2771
2468
  artifactRoot,
2772
2469
  planRoot,
2470
+ workspaceShadowRoot,
2773
2471
  manifest
2774
2472
  });
2775
2473
  ws.send(JSON.stringify(result));
@@ -2785,7 +2483,6 @@ async function startWorker(options) {
2785
2483
  });
2786
2484
  return;
2787
2485
  }
2788
- await repositorySyncQueue;
2789
2486
  sendWorkerMessage(ws, {
2790
2487
  type: "exec_accepted",
2791
2488
  requestId: message.requestId,
@@ -2806,6 +2503,7 @@ async function startWorker(options) {
2806
2503
  syncRoot,
2807
2504
  artifactRoot,
2808
2505
  planRoot,
2506
+ workspaceShadowRoot,
2809
2507
  manifest
2810
2508
  }).catch((error) => {
2811
2509
  sendWorkerMessage(ws, {
@@ -2825,9 +2523,8 @@ async function startWorker(options) {
2825
2523
  }
2826
2524
  return;
2827
2525
  }
2828
- if (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") {
2829
2527
  try {
2830
- await repositorySyncQueue;
2831
2528
  const manifest = manifestByProjectId.get(message.projectId);
2832
2529
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2833
2530
  const result = await executeOperation({
@@ -2875,6 +2572,14 @@ async function startWorker(options) {
2875
2572
  });
2876
2573
  ws.addEventListener("close", (event) => {
2877
2574
  stopHeartbeatWatchdog();
2575
+ if (periodicWorkspaceScan) {
2576
+ clearInterval(periodicWorkspaceScan);
2577
+ periodicWorkspaceScan = void 0;
2578
+ }
2579
+ if (terminalReplayTimer) {
2580
+ clearInterval(terminalReplayTimer);
2581
+ terminalReplayTimer = void 0;
2582
+ }
2878
2583
  if (currentWorkerSocket === ws) {
2879
2584
  currentWorkerSocket = null;
2880
2585
  }
@@ -2885,15 +2590,17 @@ async function startWorker(options) {
2885
2590
  if (reloadAfterClose) {
2886
2591
  process.exit(import_supervisor.WORKER_RELOAD_EXIT_CODE);
2887
2592
  }
2888
- if (activeProcesses.size > 0) {
2593
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
2889
2594
  const delayMs = 2e3;
2890
- process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
2891
- `);
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
+ );
2892
2599
  const reconnect = () => {
2893
2600
  void startWorker(options).catch((error) => {
2894
2601
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
2895
2602
  `);
2896
- if (activeProcesses.size > 0) {
2603
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
2897
2604
  setTimeout(reconnect, delayMs);
2898
2605
  return;
2899
2606
  }
@@ -2949,7 +2656,6 @@ if (isCliEntrypoint()) {
2949
2656
  // Annotate the CommonJS export names for ESM import in node:
2950
2657
  0 && (module.exports = {
2951
2658
  ensureVisibleGitCheckout,
2952
- findRepositorySyncBlockers,
2953
2659
  findWorkerFiles,
2954
2660
  githubCliEnv,
2955
2661
  grepWorkerFiles,
@@ -2962,7 +2668,6 @@ if (isCliEntrypoint()) {
2962
2668
  resolveHostShell,
2963
2669
  resolveProjectFilePath,
2964
2670
  resolveWorkerFilePath,
2965
- syncManifestProjectsFromInternal,
2966
2671
  syncProjectPlans,
2967
2672
  syncSessionArtifacts
2968
2673
  });