@ricsam/r5d-worker 0.0.36 → 0.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,18 +56,17 @@ 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_managed_paths = require("./managed-paths.cjs");
60
+ var import_workspace_sync = require("./workspace-sync.cjs");
61
61
  const import_meta = {};
62
62
  const DEFAULT_BASE_URL = "https://r5d.dev";
63
63
  const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
64
64
  const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
65
- 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
  }
@@ -603,9 +604,7 @@ function validateProjectId(projectId) {
603
604
  }
604
605
  }
605
606
  function validateBranchName(branchName) {
606
- if (branchName !== "main" && !BRANCH_RE.test(branchName)) {
607
- throw new Error(`Invalid branch name from server: ${branchName}`);
608
- }
607
+ (0, import_managed_paths.validateManagedBranchName)(branchName);
609
608
  }
610
609
  function validatePlanId(planId) {
611
610
  if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
@@ -639,27 +638,6 @@ function runGit(args, options = {}) {
639
638
  }
640
639
  return result.stdout.toString().trim();
641
640
  }
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
641
  function tryGit(args, options = {}) {
664
642
  try {
665
643
  runGit(args, options);
@@ -668,34 +646,6 @@ function tryGit(args, options = {}) {
668
646
  return false;
669
647
  }
670
648
  }
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
649
  function getGitBlobHashForContent(content) {
700
650
  const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
701
651
  return (0, import_node_crypto.createHash)("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
@@ -703,12 +653,6 @@ function getGitBlobHashForContent(content) {
703
653
  function hasWorktreeChanges(cwd) {
704
654
  return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
705
655
  }
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
656
  function isInsideBranchPath(branchPath, filePath) {
713
657
  const relative = import_node_path.default.relative(import_node_path.default.resolve(branchPath), import_node_path.default.resolve(filePath));
714
658
  return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
@@ -771,32 +715,15 @@ function resolveWorkerFilePath(branchPath, inputPath) {
771
715
  function resolveProjectFilePath(branchPath, inputPath) {
772
716
  const resolved = resolveWorkerFilePath(branchPath, inputPath);
773
717
  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
- );
718
+ throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
777
719
  }
778
720
  return resolved;
779
721
  }
780
- function resolveRemoteUrl(baseUrl, remoteUrl) {
781
- if (/^https?:\/\//i.test(remoteUrl)) {
782
- return remoteUrl;
783
- }
784
- return new URL(remoteUrl, baseUrl).toString();
785
- }
786
722
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
787
723
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
788
724
  }
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
725
  function githubRemoteUrlFor(baseUrl, projectId, manifest) {
799
- return manifest?.repoHttpUrl ?? internalRemoteUrlFor(baseUrl, projectId, manifest);
726
+ return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
800
727
  }
801
728
  function gitExtraHeaderUrlForRemote(remoteUrl) {
802
729
  try {
@@ -814,12 +741,6 @@ function gitAuthForRemote(remoteUrl, authHeader) {
814
741
  header: authHeader
815
742
  };
816
743
  }
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
744
  function hasNormalVisibleGitDir(branchPath) {
824
745
  const gitPath = import_node_path.default.join(branchPath, ".git");
825
746
  return import_node_fs.default.existsSync(gitPath) && import_node_fs.default.statSync(gitPath).isDirectory();
@@ -831,13 +752,6 @@ function ensureOriginRemote(branchPath, remoteUrl) {
831
752
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
832
753
  }
833
754
  }
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
755
  function shellQuote(value) {
842
756
  return `'${value.replace(/'/g, "'\\''")}'`;
843
757
  }
@@ -1033,194 +947,20 @@ function ensureVisibleGitCheckout(input) {
1033
947
  (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1034
948
  return branchPath;
1035
949
  }
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
950
  function ensureBranchWorkspace(input) {
1069
951
  const defaultBranch = input.manifest?.defaultBranch || "main";
1070
952
  const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1071
953
  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
954
  return {
1221
- status: results.some((result) => result.status === "failed") ? "partial" : "completed",
1222
- results,
1223
- blockedBy: []
955
+ branchPath: ensureVisibleGitCheckout({
956
+ projectRoot: input.projectRoot,
957
+ branchName: input.branchName,
958
+ githubRemoteUrl,
959
+ githubAuth,
960
+ githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
961
+ defaultBranch,
962
+ reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
963
+ })
1224
964
  };
1225
965
  }
1226
966
  function resolveCommandCwd(branchPath, cwd) {
@@ -1271,15 +1011,6 @@ async function streamCommandOutput(stream, onData) {
1271
1011
  function ensureOperationBranch(input) {
1272
1012
  return ensureBranchWorkspace(input);
1273
1013
  }
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
1014
  function formatLineNumberedContent(content, offset, limit) {
1284
1015
  const allLines = content.split("\n");
1285
1016
  const totalLines = allLines.length;
@@ -1388,9 +1119,11 @@ const fileMutationQueues = /* @__PURE__ */ new Map();
1388
1119
  async function withFileMutationQueue(key, operation) {
1389
1120
  const previous = fileMutationQueues.get(key) ?? Promise.resolve();
1390
1121
  let release;
1391
- const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
1392
- release = resolve;
1393
- }));
1122
+ const current = previous.catch(() => void 0).then(
1123
+ () => new Promise((resolve) => {
1124
+ release = resolve;
1125
+ })
1126
+ );
1394
1127
  fileMutationQueues.set(key, current);
1395
1128
  await previous.catch(() => void 0);
1396
1129
  try {
@@ -1485,7 +1218,9 @@ async function executeEditFileOperation(input) {
1485
1218
  throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1486
1219
  }
1487
1220
  if (occurrences > 1) {
1488
- throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
1221
+ throw new Error(
1222
+ `edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
1223
+ );
1489
1224
  }
1490
1225
  const start = originalContent.indexOf(edit.oldText);
1491
1226
  return { index, start, end: start + edit.oldText.length, edit };
@@ -1745,96 +1480,6 @@ async function executeViewFileBytesOperation(input) {
1745
1480
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1746
1481
  return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1747
1482
  }
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
1483
  async function executeOperation(input) {
1839
1484
  switch (input.message.type) {
1840
1485
  case "read":
@@ -1851,43 +1496,6 @@ async function executeOperation(input) {
1851
1496
  return executeLsOperation({ ...input, message: input.message });
1852
1497
  case "view_file_bytes":
1853
1498
  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
1499
  }
1892
1500
  }
1893
1501
  async function executeCommand(input) {
@@ -1919,7 +1527,11 @@ async function executeCommand(input) {
1919
1527
  planRoot: input.planRoot,
1920
1528
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1921
1529
  });
1922
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1530
+ const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1531
+ if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !import_node_fs.default.existsSync(import_node_path.default.join(commandRoot, ".git"))) {
1532
+ throw new Error("The canonical workspace checkout is not initialized on this worker");
1533
+ }
1534
+ const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
1923
1535
  const subprocess = Bun.spawn(input.message.argv, {
1924
1536
  cwd,
1925
1537
  stdout: "pipe",
@@ -1929,7 +1541,6 @@ async function executeCommand(input) {
1929
1541
  ...process.env,
1930
1542
  ...githubProcessEnv(),
1931
1543
  ...input.message.env ?? {},
1932
- ...internalGitProcessEnv(workspace, input.message.env),
1933
1544
  ...artifactProcessEnv,
1934
1545
  ...planProcessEnv
1935
1546
  }
@@ -2027,7 +1638,11 @@ async function executeStreamingCommand(input) {
2027
1638
  planRoot: input.planRoot,
2028
1639
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2029
1640
  });
2030
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1641
+ const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1642
+ if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !import_node_fs.default.existsSync(import_node_path.default.join(commandRoot, ".git"))) {
1643
+ throw new Error("The canonical workspace checkout is not initialized on this worker");
1644
+ }
1645
+ const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
2031
1646
  const subprocess = Bun.spawn(input.message.argv, {
2032
1647
  cwd,
2033
1648
  stdout: "pipe",
@@ -2037,7 +1652,6 @@ async function executeStreamingCommand(input) {
2037
1652
  ...process.env,
2038
1653
  ...githubProcessEnv(),
2039
1654
  ...input.message.env ?? {},
2040
- ...internalGitProcessEnv(workspace, input.message.env),
2041
1655
  ...artifactProcessEnv,
2042
1656
  ...planProcessEnv
2043
1657
  }
@@ -2095,22 +1709,26 @@ async function executeStreamingCommand(input) {
2095
1709
  });
2096
1710
  })
2097
1711
  ]);
2098
- sendWorkerMessage(input.ws, {
1712
+ const terminal = {
2099
1713
  type: "exec_exit",
2100
1714
  runId: input.message.runId,
2101
1715
  exitCode,
2102
1716
  durationMs: Date.now() - startedAt,
2103
1717
  ...timedOut ? { timedOut: true } : {}
2104
- });
1718
+ };
1719
+ pendingProcessTerminals.set(input.message.runId, terminal);
1720
+ sendWorkerMessage(input.ws, terminal);
2105
1721
  } catch (error) {
2106
1722
  const message = error instanceof Error ? error.message : String(error);
2107
1723
  if (started) {
2108
- sendWorkerMessage(input.ws, {
1724
+ const terminal = {
2109
1725
  type: "exec_error",
2110
1726
  runId: input.message.runId,
2111
1727
  error: message,
2112
1728
  durationMs: Date.now() - startedAt
2113
- });
1729
+ };
1730
+ pendingProcessTerminals.set(input.message.runId, terminal);
1731
+ sendWorkerMessage(input.ws, terminal);
2114
1732
  } else {
2115
1733
  sendWorkerMessage(input.ws, {
2116
1734
  type: "exec_start_error",
@@ -2448,10 +2066,7 @@ function resizePty(message) {
2448
2066
  if (!ptyProcess) {
2449
2067
  return;
2450
2068
  }
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
- );
2069
+ 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
2070
  }
2456
2071
  function closePty(message) {
2457
2072
  const ptyProcess = activePtys.get(message.ptyId);
@@ -2471,10 +2086,13 @@ function websocketUrl(baseUrl, label) {
2471
2086
  const url = new URL("/worker/ws", baseUrl);
2472
2087
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2473
2088
  url.searchParams.set("label", label);
2089
+ url.searchParams.set("version", getWorkerVersion());
2474
2090
  return url.toString();
2475
2091
  }
2476
2092
  function projectRootFor(projectsRoot, projectId, manifestByProjectId) {
2477
- return import_node_path.default.join(projectsRoot, manifestByProjectId.get(projectId)?.repoSlug ?? projectId);
2093
+ const manifest = manifestByProjectId.get(projectId);
2094
+ if (!manifest) throw new Error(`Project ${projectId} is missing from the worker workspace manifest`);
2095
+ return (0, import_managed_paths.managedProjectRoot)(projectsRoot, manifest.checkoutPathSegments);
2478
2096
  }
2479
2097
  async function startWorker(options) {
2480
2098
  const config = readConfig(options.configPath);
@@ -2483,6 +2101,7 @@ async function startWorker(options) {
2483
2101
  validateLabel(label);
2484
2102
  const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
2485
2103
  const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
2104
+ const workspaceShadowRoot = import_node_path.default.join(syncRoot, "workspace");
2486
2105
  const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2487
2106
  const planRoot = import_node_path.default.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
2488
2107
  process.stdout.write(`[r5d-worker] label: ${label}
@@ -2504,10 +2123,104 @@ async function startWorker(options) {
2504
2123
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2505
2124
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2506
2125
  const manifestByProjectId = /* @__PURE__ */ new Map();
2507
- let repositorySyncQueue = Promise.resolve();
2508
- let repositorySyncInProgress = false;
2126
+ const workspaceSyncSingleFlight = new import_workspace_sync.WorkspaceSyncSingleFlight();
2127
+ let workspaceRemoteUrl = null;
2128
+ let activeWorkspaceIncidentId = null;
2129
+ let previousPeriodicFingerprint = null;
2130
+ let periodicWorkspaceScan;
2131
+ let periodicWorkspaceScanInFlight = false;
2132
+ let terminalReplayTimer;
2133
+ let workspaceSyncRequestsInFlight = 0;
2509
2134
  let cliUpdateInProgress = false;
2510
2135
  let reloadAfterClose = false;
2136
+ const workspaceSyncInput = (trigger, overrides = {}) => {
2137
+ if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2138
+ return {
2139
+ workerLabel: label,
2140
+ remoteUrl: workspaceRemoteUrl,
2141
+ authHeader: `Authorization: Bearer ${token}`,
2142
+ projectsRoot,
2143
+ plansRoot: planRoot,
2144
+ shadowRoot: workspaceShadowRoot,
2145
+ projects: [...manifestByProjectId.values()],
2146
+ trigger,
2147
+ ...overrides
2148
+ };
2149
+ };
2150
+ const sendWorkspaceSyncResult = (requestId, result) => {
2151
+ sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2152
+ };
2153
+ const ensureAllVisibleWorkspaceCheckouts = () => {
2154
+ const created = [];
2155
+ for (const manifest of manifestByProjectId.values()) {
2156
+ for (const branchName of manifest.branches) {
2157
+ const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2158
+ const existed = import_node_fs.default.existsSync(import_node_path.default.join(branchPath, ".git"));
2159
+ const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2160
+ ensureVisibleGitCheckout({
2161
+ projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2162
+ branchName,
2163
+ githubRemoteUrl,
2164
+ githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2165
+ githubAuthHeader: manifest.repoAuthHeader,
2166
+ defaultBranch: manifest.defaultBranch || "main",
2167
+ reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2168
+ });
2169
+ if (!existed) created.push({ projectId: manifest.projectId, branchName });
2170
+ }
2171
+ }
2172
+ return created;
2173
+ };
2174
+ const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2175
+ workspaceSyncRequestsInFlight += 1;
2176
+ try {
2177
+ if (trigger.type === "plan_write" || trigger.type === "task_status" || trigger.type === "connect" || trigger.type === "inbound_head" || trigger.type === "manual") {
2178
+ for (const manifest of manifestByProjectId.values()) {
2179
+ for (const branchName of manifest.branches) {
2180
+ await syncProjectPlans({
2181
+ baseUrl,
2182
+ token,
2183
+ projectId: manifest.projectId,
2184
+ branchName,
2185
+ planRoot
2186
+ });
2187
+ }
2188
+ }
2189
+ }
2190
+ const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2191
+ const result = await workspaceSyncSingleFlight.run(
2192
+ workspaceSyncInput(trigger, {
2193
+ ...overrides,
2194
+ ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2195
+ })
2196
+ );
2197
+ previousPeriodicFingerprint = null;
2198
+ sendWorkspaceSyncResult(requestId, result);
2199
+ return result;
2200
+ } catch (error) {
2201
+ const result = {
2202
+ type: "workspace_sync",
2203
+ attemptId: overrides.attemptId ?? crypto.randomUUID(),
2204
+ workerLabel: label,
2205
+ trigger,
2206
+ outcome: "failed",
2207
+ startingHead: null,
2208
+ rebaseCount: 0,
2209
+ diffSizeBytes: 0,
2210
+ gitStatus: "",
2211
+ affectedProjects: [],
2212
+ affectedPaths: [],
2213
+ discardedPaths: [],
2214
+ localChangesDiscarded: false,
2215
+ error: error instanceof Error ? error.message : String(error)
2216
+ };
2217
+ previousPeriodicFingerprint = null;
2218
+ sendWorkspaceSyncResult(requestId, result);
2219
+ return result;
2220
+ } finally {
2221
+ workspaceSyncRequestsInFlight -= 1;
2222
+ }
2223
+ };
2511
2224
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2512
2225
  headers: {
2513
2226
  Authorization: `Bearer ${token}`
@@ -2552,6 +2265,15 @@ async function startWorker(options) {
2552
2265
  };
2553
2266
  ws.send(JSON.stringify(hello));
2554
2267
  sendActiveProcessReport(ws);
2268
+ for (const terminal of pendingProcessTerminals.values()) {
2269
+ sendWorkerMessage(ws, terminal);
2270
+ }
2271
+ terminalReplayTimer = setInterval(() => {
2272
+ for (const terminal of pendingProcessTerminals.values()) {
2273
+ sendWorkerMessage(ws, terminal);
2274
+ }
2275
+ }, 5e3);
2276
+ terminalReplayTimer.unref();
2555
2277
  process.stdout.write("[r5d-worker] connected\n");
2556
2278
  });
2557
2279
  ws.addEventListener("message", (event) => {
@@ -2560,92 +2282,73 @@ async function startWorker(options) {
2560
2282
  if (message.type === "connected") {
2561
2283
  return;
2562
2284
  }
2563
- if (message.type === "project_manifest") {
2285
+ if (message.type === "workspace_manifest") {
2564
2286
  configureGitHubAuth(message.githubCredential);
2565
2287
  visibleGitIdentity = message.gitIdentity;
2288
+ workspaceRemoteUrl = message.workspaceRemoteUrl;
2289
+ const migratedProjectIds = (0, import_managed_paths.migrateLegacyProjectRoots)(projectsRoot, message.projects);
2566
2290
  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
2571
- `);
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)}
2291
+ for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2292
+ process.stdout.write(
2293
+ `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2612
2294
  `
2613
- );
2614
- }
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
2295
  );
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)
2296
+ if (!periodicWorkspaceScan) {
2297
+ const emptyFingerprint = (0, import_node_crypto.createHash)("sha256").update("").digest("hex");
2298
+ periodicWorkspaceScan = setInterval(() => {
2299
+ if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2300
+ periodicWorkspaceScanInFlight = true;
2301
+ void (async () => {
2302
+ const input = workspaceSyncInput({ type: "periodic" });
2303
+ const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2304
+ if (fingerprint === emptyFingerprint) {
2305
+ previousPeriodicFingerprint = null;
2306
+ return;
2637
2307
  }
2638
- ],
2639
- blockedBy: []
2640
- };
2308
+ if (previousPeriodicFingerprint !== fingerprint) {
2309
+ previousPeriodicFingerprint = fingerprint;
2310
+ return;
2311
+ }
2312
+ previousPeriodicFingerprint = null;
2313
+ await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "periodic" });
2314
+ })().catch((error) => {
2315
+ process.stderr.write(
2316
+ `[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
2317
+ `
2318
+ );
2319
+ }).finally(() => {
2320
+ periodicWorkspaceScanInFlight = false;
2321
+ });
2322
+ }, import_workspace_sync.WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
2323
+ periodicWorkspaceScan.unref();
2641
2324
  }
2642
- sendWorkerMessage(ws, {
2643
- type: "sync_projects_result",
2644
- requestId: message.requestId,
2645
- result: syncResult
2325
+ return;
2326
+ }
2327
+ if (message.type === "sync_workspace") {
2328
+ await runRequestedWorkspaceSync(message.requestId, message.trigger, {
2329
+ attemptId: message.attemptId,
2330
+ confirmedLargeDiff: message.confirmedLargeDiff,
2331
+ confirmationReason: message.confirmationReason,
2332
+ resetToCanonical: message.resetToCanonical,
2333
+ skipVisibleMirror: message.skipVisibleMirror
2646
2334
  });
2647
2335
  return;
2648
2336
  }
2337
+ if (message.type === "workspace_head_updated") {
2338
+ if (!activeWorkspaceIncidentId) {
2339
+ await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "inbound_head", detail: message.commitHash });
2340
+ }
2341
+ return;
2342
+ }
2343
+ if (message.type === "workspace_incident_updated") {
2344
+ activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
2345
+ previousPeriodicFingerprint = null;
2346
+ return;
2347
+ }
2348
+ if (message.type === "exec_terminal_ack") {
2349
+ pendingProcessTerminals.delete(message.runId);
2350
+ return;
2351
+ }
2649
2352
  if (message.type === "update_clis") {
2650
2353
  if (cliUpdateInProgress) {
2651
2354
  sendWorkerMessage(ws, {
@@ -2655,7 +2358,7 @@ async function startWorker(options) {
2655
2358
  });
2656
2359
  return;
2657
2360
  }
2658
- if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
2361
+ if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
2659
2362
  sendWorkerMessage(ws, {
2660
2363
  type: "update_clis_result",
2661
2364
  requestId: message.requestId,
@@ -2725,7 +2428,6 @@ async function startWorker(options) {
2725
2428
  });
2726
2429
  return;
2727
2430
  }
2728
- await repositorySyncQueue;
2729
2431
  const manifest = manifestByProjectId.get(message.projectId);
2730
2432
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2731
2433
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2756,7 +2458,6 @@ async function startWorker(options) {
2756
2458
  });
2757
2459
  return;
2758
2460
  }
2759
- await repositorySyncQueue;
2760
2461
  const manifest = manifestByProjectId.get(message.projectId);
2761
2462
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2762
2463
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2770,6 +2471,7 @@ async function startWorker(options) {
2770
2471
  syncRoot,
2771
2472
  artifactRoot,
2772
2473
  planRoot,
2474
+ workspaceShadowRoot,
2773
2475
  manifest
2774
2476
  });
2775
2477
  ws.send(JSON.stringify(result));
@@ -2785,7 +2487,6 @@ async function startWorker(options) {
2785
2487
  });
2786
2488
  return;
2787
2489
  }
2788
- await repositorySyncQueue;
2789
2490
  sendWorkerMessage(ws, {
2790
2491
  type: "exec_accepted",
2791
2492
  requestId: message.requestId,
@@ -2806,6 +2507,7 @@ async function startWorker(options) {
2806
2507
  syncRoot,
2807
2508
  artifactRoot,
2808
2509
  planRoot,
2510
+ workspaceShadowRoot,
2809
2511
  manifest
2810
2512
  }).catch((error) => {
2811
2513
  sendWorkerMessage(ws, {
@@ -2825,9 +2527,8 @@ async function startWorker(options) {
2825
2527
  }
2826
2528
  return;
2827
2529
  }
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") {
2530
+ 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
2531
  try {
2830
- await repositorySyncQueue;
2831
2532
  const manifest = manifestByProjectId.get(message.projectId);
2832
2533
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2833
2534
  const result = await executeOperation({
@@ -2875,6 +2576,14 @@ async function startWorker(options) {
2875
2576
  });
2876
2577
  ws.addEventListener("close", (event) => {
2877
2578
  stopHeartbeatWatchdog();
2579
+ if (periodicWorkspaceScan) {
2580
+ clearInterval(periodicWorkspaceScan);
2581
+ periodicWorkspaceScan = void 0;
2582
+ }
2583
+ if (terminalReplayTimer) {
2584
+ clearInterval(terminalReplayTimer);
2585
+ terminalReplayTimer = void 0;
2586
+ }
2878
2587
  if (currentWorkerSocket === ws) {
2879
2588
  currentWorkerSocket = null;
2880
2589
  }
@@ -2885,15 +2594,17 @@ async function startWorker(options) {
2885
2594
  if (reloadAfterClose) {
2886
2595
  process.exit(import_supervisor.WORKER_RELOAD_EXIT_CODE);
2887
2596
  }
2888
- if (activeProcesses.size > 0) {
2597
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
2889
2598
  const delayMs = 2e3;
2890
- process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
2891
- `);
2599
+ process.stderr.write(
2600
+ `[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${delayMs}ms
2601
+ `
2602
+ );
2892
2603
  const reconnect = () => {
2893
2604
  void startWorker(options).catch((error) => {
2894
2605
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
2895
2606
  `);
2896
- if (activeProcesses.size > 0) {
2607
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
2897
2608
  setTimeout(reconnect, delayMs);
2898
2609
  return;
2899
2610
  }
@@ -2949,7 +2660,6 @@ if (isCliEntrypoint()) {
2949
2660
  // Annotate the CommonJS export names for ESM import in node:
2950
2661
  0 && (module.exports = {
2951
2662
  ensureVisibleGitCheckout,
2952
- findRepositorySyncBlockers,
2953
2663
  findWorkerFiles,
2954
2664
  githubCliEnv,
2955
2665
  grepWorkerFiles,
@@ -2962,7 +2672,6 @@ if (isCliEntrypoint()) {
2962
2672
  resolveHostShell,
2963
2673
  resolveProjectFilePath,
2964
2674
  resolveWorkerFilePath,
2965
- syncManifestProjectsFromInternal,
2966
2675
  syncProjectPlans,
2967
2676
  syncSessionArtifacts
2968
2677
  });