@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/mjs/main.mjs CHANGED
@@ -9,17 +9,21 @@ import { configureVisibleGitIdentity } from "./git-identity.mjs";
9
9
  import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
10
10
  import { terminateProcessTree } from "./process-tree.mjs";
11
11
  import { superviseWorkerRuntime, WORKER_RELOAD_EXIT_CODE, WORKER_RUNTIME_ENV } from "./supervisor.mjs";
12
+ import { managedProjectRoot, migrateLegacyProjectRoots, validateManagedBranchName } from "./managed-paths.mjs";
13
+ import {
14
+ WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
15
+ WorkspaceSyncSingleFlight,
16
+ calculateWorkspaceDiffFingerprint,
17
+ synchronizeWorkspace
18
+ } from "./workspace-sync.mjs";
12
19
  const DEFAULT_BASE_URL = "https://r5d.dev";
13
20
  const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
14
21
  const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
15
- const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
16
22
  const DEFAULT_READ_LIMIT = 2e3;
17
23
  const DEFAULT_READ_MAX_BYTES = 5e4;
18
24
  const MAX_LINE_LENGTH = 2e3;
19
- const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
20
- const R5D_REMOTE_NAME = "r5d";
21
- const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
22
25
  const activeProcesses = /* @__PURE__ */ new Map();
26
+ const pendingProcessTerminals = /* @__PURE__ */ new Map();
23
27
  const cancelledProcessRuns = /* @__PURE__ */ new Set();
24
28
  const activePtys = /* @__PURE__ */ new Map();
25
29
  let currentWorkerSocket = null;
@@ -478,8 +482,10 @@ async function prepareArtifactEnvForShell(input) {
478
482
  artifactRoot: input.artifactRoot
479
483
  });
480
484
  } catch (error) {
481
- process.stderr.write(`[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
482
- `);
485
+ process.stderr.write(
486
+ `[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
487
+ `
488
+ );
483
489
  }
484
490
  const artifactsDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
485
491
  fs.mkdirSync(artifactsDir, { recursive: true });
@@ -535,7 +541,9 @@ function resolveCredentials(options, config) {
535
541
  throw new Error("Authentication required. Run `r5dctl auth login` or set R5D_WORKER_TOKEN/R5D_API_KEY.");
536
542
  }
537
543
  return {
538
- baseUrl: normalizeBaseUrl(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL),
544
+ baseUrl: normalizeBaseUrl(
545
+ options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL
546
+ ),
539
547
  token
540
548
  };
541
549
  }
@@ -553,9 +561,7 @@ function validateProjectId(projectId) {
553
561
  }
554
562
  }
555
563
  function validateBranchName(branchName) {
556
- if (branchName !== "main" && !BRANCH_RE.test(branchName)) {
557
- throw new Error(`Invalid branch name from server: ${branchName}`);
558
- }
564
+ validateManagedBranchName(branchName);
559
565
  }
560
566
  function validatePlanId(planId) {
561
567
  if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
@@ -589,27 +595,6 @@ function runGit(args, options = {}) {
589
595
  }
590
596
  return result.stdout.toString().trim();
591
597
  }
592
- async function runGitAsync(args, options = {}) {
593
- const command = ["git", ...gitAuthArgs(options.auth), ...args];
594
- const subprocess = Bun.spawn(command, {
595
- cwd: options.cwd,
596
- stdout: "pipe",
597
- stderr: "pipe",
598
- env: {
599
- ...process.env,
600
- GIT_TERMINAL_PROMPT: "0"
601
- }
602
- });
603
- const [stdout, stderr, exitCode] = await Promise.all([
604
- collectStream(subprocess.stdout),
605
- collectStream(subprocess.stderr),
606
- subprocess.exited
607
- ]);
608
- if (exitCode !== 0) {
609
- throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
610
- }
611
- return stdout.trim();
612
- }
613
598
  function tryGit(args, options = {}) {
614
599
  try {
615
600
  runGit(args, options);
@@ -618,34 +603,6 @@ function tryGit(args, options = {}) {
618
603
  return false;
619
604
  }
620
605
  }
621
- async function tryGitAsync(args, options = {}) {
622
- try {
623
- await runGitAsync(args, options);
624
- return true;
625
- } catch {
626
- return false;
627
- }
628
- }
629
- function runInternalGit(context, args) {
630
- return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
631
- }
632
- function runInternalGitAsync(context, args) {
633
- return runGitAsync(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
634
- }
635
- function runInternalGitDir(context, args) {
636
- return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
637
- }
638
- function tryInternalGit(context, args) {
639
- try {
640
- runInternalGit(context, args);
641
- return true;
642
- } catch {
643
- return false;
644
- }
645
- }
646
- function getInternalCommitHash(context) {
647
- return runInternalGit(context, ["rev-parse", "HEAD"]);
648
- }
649
606
  function getGitBlobHashForContent(content) {
650
607
  const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
651
608
  return createHash("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
@@ -653,12 +610,6 @@ function getGitBlobHashForContent(content) {
653
610
  function hasWorktreeChanges(cwd) {
654
611
  return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
655
612
  }
656
- function getInternalStatus(context) {
657
- return runInternalGit(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
658
- }
659
- function hasInternalWorktreeChanges(context) {
660
- return getInternalStatus(context).trim().length > 0;
661
- }
662
613
  function isInsideBranchPath(branchPath, filePath) {
663
614
  const relative = path.relative(path.resolve(branchPath), path.resolve(filePath));
664
615
  return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
@@ -721,32 +672,15 @@ function resolveWorkerFilePath(branchPath, inputPath) {
721
672
  function resolveProjectFilePath(branchPath, inputPath) {
722
673
  const resolved = resolveWorkerFilePath(branchPath, inputPath);
723
674
  if (resolved.scope === "host") {
724
- throw new Error(
725
- `Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`
726
- );
675
+ throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
727
676
  }
728
677
  return resolved;
729
678
  }
730
- function resolveRemoteUrl(baseUrl, remoteUrl) {
731
- if (/^https?:\/\//i.test(remoteUrl)) {
732
- return remoteUrl;
733
- }
734
- return new URL(remoteUrl, baseUrl).toString();
735
- }
736
679
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
737
680
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
738
681
  }
739
- function internalGitAuth(baseUrl, token) {
740
- return {
741
- extraHeaderUrl: baseUrl,
742
- header: `Authorization: Bearer ${token}`
743
- };
744
- }
745
- function internalRemoteUrlFor(baseUrl, projectId, manifest) {
746
- return resolveRemoteUrl(baseUrl, manifest?.internalRemoteUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId));
747
- }
748
682
  function githubRemoteUrlFor(baseUrl, projectId, manifest) {
749
- return manifest?.repoHttpUrl ?? internalRemoteUrlFor(baseUrl, projectId, manifest);
683
+ return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
750
684
  }
751
685
  function gitExtraHeaderUrlForRemote(remoteUrl) {
752
686
  try {
@@ -764,12 +698,6 @@ function gitAuthForRemote(remoteUrl, authHeader) {
764
698
  header: authHeader
765
699
  };
766
700
  }
767
- function branchGitDirName(branchName) {
768
- return `${encodeURIComponent(branchName)}.git`;
769
- }
770
- function internalGitDirFor(syncRoot, projectId, branchName) {
771
- return path.join(syncRoot, projectId, branchGitDirName(branchName));
772
- }
773
701
  function hasNormalVisibleGitDir(branchPath) {
774
702
  const gitPath = path.join(branchPath, ".git");
775
703
  return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
@@ -781,13 +709,6 @@ function ensureOriginRemote(branchPath, remoteUrl) {
781
709
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
782
710
  }
783
711
  }
784
- async function ensureOriginRemoteAsync(branchPath, remoteUrl) {
785
- if (await tryGitAsync(["remote", "get-url", "origin"], { cwd: branchPath })) {
786
- await runGitAsync(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
787
- } else {
788
- await runGitAsync(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
789
- }
790
- }
791
712
  function shellQuote(value) {
792
713
  return `'${value.replace(/'/g, "'\\''")}'`;
793
714
  }
@@ -983,194 +904,20 @@ function ensureVisibleGitCheckout(input) {
983
904
  configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
984
905
  return branchPath;
985
906
  }
986
- function configureInternalRepo(context, remoteUrl) {
987
- if (tryInternalGit(context, ["remote", "get-url", R5D_REMOTE_NAME])) {
988
- runInternalGit(context, ["remote", "set-url", R5D_REMOTE_NAME, remoteUrl]);
989
- } else {
990
- runInternalGit(context, ["remote", "add", R5D_REMOTE_NAME, remoteUrl]);
991
- }
992
- if (tryInternalGit(context, ["remote", "get-url", LEGACY_BUILD_IT_NOW_REMOTE_NAME])) {
993
- runInternalGit(context, ["remote", "remove", LEGACY_BUILD_IT_NOW_REMOTE_NAME]);
994
- }
995
- runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
996
- runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
997
- }
998
- function ensureInternalSyncGit(input) {
999
- validateBranchName(input.branchName);
1000
- const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1001
- fs.mkdirSync(path.dirname(gitDir), { recursive: true });
1002
- if (!fs.existsSync(gitDir)) {
1003
- runGit(["init", "--bare", gitDir]);
1004
- }
1005
- const auth = internalGitAuth(input.baseUrl, input.token);
1006
- const context = { gitDir, workTree: input.branchPath, auth };
1007
- const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1008
- configureInternalRepo(context, remoteUrl);
1009
- runInternalGit(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1010
- const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`]);
1011
- const target = hasRemoteBranch ? `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}` : `refs/remotes/${R5D_REMOTE_NAME}/main`;
1012
- const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
1013
- if (!hasHead || !hasInternalWorktreeChanges(context)) {
1014
- runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
1015
- }
1016
- return context;
1017
- }
1018
907
  function ensureBranchWorkspace(input) {
1019
908
  const defaultBranch = input.manifest?.defaultBranch || "main";
1020
909
  const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1021
910
  const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
1022
- const branchPath = ensureVisibleGitCheckout({
1023
- projectRoot: input.projectRoot,
1024
- branchName: input.branchName,
1025
- githubRemoteUrl,
1026
- githubAuth,
1027
- githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
1028
- defaultBranch,
1029
- reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
1030
- });
1031
- const internalGit = ensureInternalSyncGit({
1032
- projectId: input.projectId,
1033
- baseUrl: input.baseUrl,
1034
- token: input.token,
1035
- syncRoot: input.syncRoot,
1036
- branchPath,
1037
- branchName: input.branchName,
1038
- manifest: input.manifest
1039
- });
1040
- return { branchPath, internalGit };
1041
- }
1042
- async function ensureVisibleGitCheckoutForRepositorySync(input) {
1043
- validateBranchName(input.branchName);
1044
- fs.mkdirSync(input.projectRoot, { recursive: true });
1045
- const branchPath = path.join(input.projectRoot, input.branchName);
1046
- const createdCheckout = !hasNormalVisibleGitDir(branchPath);
1047
- if (createdCheckout) {
1048
- fs.rmSync(branchPath, { recursive: true, force: true });
1049
- fs.mkdirSync(path.dirname(branchPath), { recursive: true });
1050
- if (!await tryGitAsync(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
1051
- fs.mkdirSync(branchPath, { recursive: true });
1052
- await runGitAsync(["init"], { cwd: branchPath });
1053
- }
1054
- await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1055
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1056
- configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1057
- await tryGitAsync(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
1058
- checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1059
- return branchPath;
1060
- }
1061
- if (input.reconcileExistingOrigin) {
1062
- await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1063
- }
1064
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1065
- configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1066
- return branchPath;
1067
- }
1068
- async function forceSyncManifestBranchFromInternal(input) {
1069
- const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1070
- const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest.repoAuthHeader);
1071
- const branchPath = await ensureVisibleGitCheckoutForRepositorySync({
1072
- projectRoot: input.projectRoot,
1073
- branchName: input.branchName,
1074
- githubRemoteUrl,
1075
- githubAuth,
1076
- githubAuthHeader: input.manifest.repoAuthHeader,
1077
- defaultBranch: input.manifest.defaultBranch || "main",
1078
- reconcileExistingOrigin: Boolean(input.manifest.repoHttpUrl)
1079
- });
1080
- const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1081
- fs.mkdirSync(path.dirname(gitDir), { recursive: true });
1082
- if (!fs.existsSync(gitDir)) {
1083
- await runGitAsync(["init", "--bare", gitDir]);
1084
- }
1085
- const context = {
1086
- gitDir,
1087
- workTree: branchPath,
1088
- auth: internalGitAuth(input.baseUrl, input.token)
1089
- };
1090
- configureInternalRepo(context, internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest));
1091
- await runInternalGitAsync(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1092
- const branchTarget = `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`;
1093
- const mainTarget = `refs/remotes/${R5D_REMOTE_NAME}/main`;
1094
- const target = tryInternalGit(context, ["show-ref", "--verify", "--quiet", branchTarget]) ? branchTarget : tryInternalGit(context, ["show-ref", "--verify", "--quiet", mainTarget]) ? mainTarget : null;
1095
- if (!target) {
1096
- throw new Error(`Internal remote has neither ${input.branchName} nor main`);
1097
- }
1098
- await runInternalGitAsync(context, ["reset", "--hard", target]);
1099
- await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1100
- return getInternalCommitHash(context);
1101
- }
1102
- function findRepositorySyncBlockers(input) {
1103
- const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
1104
- const blockedBy = [];
1105
- for (const process2 of input.processes) {
1106
- const target = targetByKey.get(`${process2.projectId}:${process2.branchName}`);
1107
- if (target) blockedBy.push({ ...target, kind: "process", id: process2.id });
1108
- }
1109
- for (const shell of input.shells) {
1110
- const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
1111
- if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
1112
- }
1113
- return blockedBy;
1114
- }
1115
- async function syncManifestProjectsFromInternal(input) {
1116
- const requestedProjectIds = input.projectIds ? new Set(input.projectIds) : null;
1117
- const targets = input.manifests.filter((manifest) => !requestedProjectIds || requestedProjectIds.has(manifest.projectId)).flatMap((manifest) => {
1118
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1119
- return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1120
- });
1121
- const blockedBy = findRepositorySyncBlockers({
1122
- targets: targets.map(({ manifest, branchName }) => ({
1123
- projectId: manifest.projectId,
1124
- projectPath: manifest.projectPath,
1125
- branchName
1126
- })),
1127
- processes: [...activeProcesses].map(([id, active]) => ({
1128
- id,
1129
- projectId: active.projectId,
1130
- branchName: active.branchName
1131
- })),
1132
- shells: [...activePtys].map(([id, active]) => ({
1133
- id,
1134
- projectId: active.projectId,
1135
- branchName: active.branchName
1136
- }))
1137
- });
1138
- if (blockedBy.length > 0) {
1139
- return { status: "blocked", results: [], blockedBy };
1140
- }
1141
- const results = [];
1142
- for (const { manifest, branchName } of targets) {
1143
- try {
1144
- const commitHash = await forceSyncManifestBranchFromInternal({
1145
- projectId: manifest.projectId,
1146
- baseUrl: input.baseUrl,
1147
- token: input.token,
1148
- projectRoot: path.join(input.projectsRoot, manifest.repoSlug || manifest.projectId),
1149
- syncRoot: input.syncRoot,
1150
- branchName,
1151
- manifest
1152
- });
1153
- results.push({
1154
- projectId: manifest.projectId,
1155
- projectPath: manifest.projectPath,
1156
- branchName,
1157
- status: "synced",
1158
- commitHash
1159
- });
1160
- } catch (error) {
1161
- results.push({
1162
- projectId: manifest.projectId,
1163
- projectPath: manifest.projectPath,
1164
- branchName,
1165
- status: "failed",
1166
- error: error instanceof Error ? error.message : String(error)
1167
- });
1168
- }
1169
- }
1170
911
  return {
1171
- status: results.some((result) => result.status === "failed") ? "partial" : "completed",
1172
- results,
1173
- blockedBy: []
912
+ branchPath: ensureVisibleGitCheckout({
913
+ projectRoot: input.projectRoot,
914
+ branchName: input.branchName,
915
+ githubRemoteUrl,
916
+ githubAuth,
917
+ githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
918
+ defaultBranch,
919
+ reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
920
+ })
1174
921
  };
1175
922
  }
1176
923
  function resolveCommandCwd(branchPath, cwd) {
@@ -1221,15 +968,6 @@ async function streamCommandOutput(stream, onData) {
1221
968
  function ensureOperationBranch(input) {
1222
969
  return ensureBranchWorkspace(input);
1223
970
  }
1224
- function internalGitProcessEnv(workspace, env) {
1225
- if (env?.R5D_USE_INTERNAL_GIT !== "1") {
1226
- return {};
1227
- }
1228
- return {
1229
- GIT_DIR: workspace.internalGit.gitDir,
1230
- GIT_WORK_TREE: workspace.branchPath
1231
- };
1232
- }
1233
971
  function formatLineNumberedContent(content, offset, limit) {
1234
972
  const allLines = content.split("\n");
1235
973
  const totalLines = allLines.length;
@@ -1338,9 +1076,11 @@ const fileMutationQueues = /* @__PURE__ */ new Map();
1338
1076
  async function withFileMutationQueue(key, operation) {
1339
1077
  const previous = fileMutationQueues.get(key) ?? Promise.resolve();
1340
1078
  let release;
1341
- const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
1342
- release = resolve;
1343
- }));
1079
+ const current = previous.catch(() => void 0).then(
1080
+ () => new Promise((resolve) => {
1081
+ release = resolve;
1082
+ })
1083
+ );
1344
1084
  fileMutationQueues.set(key, current);
1345
1085
  await previous.catch(() => void 0);
1346
1086
  try {
@@ -1435,7 +1175,9 @@ async function executeEditFileOperation(input) {
1435
1175
  throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1436
1176
  }
1437
1177
  if (occurrences > 1) {
1438
- throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
1178
+ throw new Error(
1179
+ `edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
1180
+ );
1439
1181
  }
1440
1182
  const start = originalContent.indexOf(edit.oldText);
1441
1183
  return { index, start, end: start + edit.oldText.length, edit };
@@ -1695,96 +1437,6 @@ async function executeViewFileBytesOperation(input) {
1695
1437
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1696
1438
  return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1697
1439
  }
1698
- function stagedBlobSizeBytes(context) {
1699
- const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
1700
- let total = 0;
1701
- for (const name of names) {
1702
- try {
1703
- const sizeText = runInternalGit(context, ["cat-file", "-s", `:${name}`]);
1704
- total += Number.parseInt(sizeText, 10) || 0;
1705
- } catch {
1706
- }
1707
- if (total > MAX_SYNC_DIFF_BYTES) {
1708
- return MAX_SYNC_DIFF_BYTES + 1;
1709
- }
1710
- }
1711
- return total;
1712
- }
1713
- function syncBranch(input) {
1714
- const workspace = ensureOperationBranch(input);
1715
- runInternalGit(workspace.internalGit, ["add", "-A", "--", ".", ":(exclude).git"]);
1716
- const hasStagedChanges = !tryInternalGit(workspace.internalGit, ["diff", "--cached", "--quiet", "--", ".", ":(exclude).git"]);
1717
- const status = getInternalStatus(workspace.internalGit);
1718
- const currentCommit = getInternalCommitHash(workspace.internalGit);
1719
- if (!hasStagedChanges) {
1720
- return {
1721
- type: "sync",
1722
- changed: false,
1723
- commitHash: currentCommit,
1724
- diffSizeBytes: 0,
1725
- status
1726
- };
1727
- }
1728
- const diffSizeBytes = stagedBlobSizeBytes(workspace.internalGit);
1729
- if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
1730
- return {
1731
- type: "sync",
1732
- changed: true,
1733
- commitHash: currentCommit,
1734
- diffSizeBytes,
1735
- status,
1736
- largeDiffBlocked: true,
1737
- trigger: input.trigger
1738
- };
1739
- }
1740
- const message = JSON.stringify({
1741
- type: "agent_changes",
1742
- sessionId: input.sessionId,
1743
- parentNodeId: input.parentNodeId,
1744
- ...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
1745
- });
1746
- runInternalGit(workspace.internalGit, ["commit", "-m", message]);
1747
- const commitHash = getInternalCommitHash(workspace.internalGit);
1748
- runInternalGit(workspace.internalGit, ["push", R5D_REMOTE_NAME, `HEAD:refs/heads/${input.branchName}`]);
1749
- return {
1750
- type: "sync",
1751
- changed: true,
1752
- commitHash,
1753
- diffSizeBytes,
1754
- status: getInternalStatus(workspace.internalGit),
1755
- trigger: input.trigger
1756
- };
1757
- }
1758
- function confirmLargeDiff(input) {
1759
- const reason = input.reason.trim();
1760
- if (!reason) {
1761
- throw new Error("confirm_large_diff requires a non-empty reason");
1762
- }
1763
- const result = syncBranch({
1764
- ...input,
1765
- confirmedLargeDiff: true,
1766
- confirmationReason: reason
1767
- });
1768
- return {
1769
- type: "confirm_large_diff",
1770
- changed: result.changed,
1771
- commitHash: result.commitHash,
1772
- diffSizeBytes: result.diffSizeBytes,
1773
- status: result.status,
1774
- reason
1775
- };
1776
- }
1777
- function pullBranch(input) {
1778
- const workspace = ensureOperationBranch(input);
1779
- runInternalGit(workspace.internalGit, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1780
- const target = input.commitHash ?? `${R5D_REMOTE_NAME}/${input.branchName}`;
1781
- runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
1782
- runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1783
- return {
1784
- type: "pull_branch",
1785
- commitHash: getInternalCommitHash(workspace.internalGit)
1786
- };
1787
- }
1788
1440
  async function executeOperation(input) {
1789
1441
  switch (input.message.type) {
1790
1442
  case "read":
@@ -1801,43 +1453,6 @@ async function executeOperation(input) {
1801
1453
  return executeLsOperation({ ...input, message: input.message });
1802
1454
  case "view_file_bytes":
1803
1455
  return executeViewFileBytesOperation({ ...input, message: input.message });
1804
- case "sync":
1805
- return syncBranch({
1806
- projectId: input.projectId,
1807
- baseUrl: input.baseUrl,
1808
- token: input.token,
1809
- projectRoot: input.projectRoot,
1810
- syncRoot: input.syncRoot,
1811
- branchName: input.message.branchName,
1812
- sessionId: input.message.sessionId,
1813
- parentNodeId: input.message.parentNodeId,
1814
- trigger: input.message.trigger,
1815
- manifest: input.manifest
1816
- });
1817
- case "confirm_large_diff":
1818
- return confirmLargeDiff({
1819
- projectId: input.projectId,
1820
- baseUrl: input.baseUrl,
1821
- token: input.token,
1822
- projectRoot: input.projectRoot,
1823
- syncRoot: input.syncRoot,
1824
- branchName: input.message.branchName,
1825
- sessionId: input.message.sessionId,
1826
- parentNodeId: input.message.parentNodeId,
1827
- reason: input.message.reason,
1828
- manifest: input.manifest
1829
- });
1830
- case "pull_branch":
1831
- return pullBranch({
1832
- projectId: input.projectId,
1833
- baseUrl: input.baseUrl,
1834
- token: input.token,
1835
- projectRoot: input.projectRoot,
1836
- syncRoot: input.syncRoot,
1837
- branchName: input.message.branchName,
1838
- commitHash: input.message.commitHash,
1839
- manifest: input.manifest
1840
- });
1841
1456
  }
1842
1457
  }
1843
1458
  async function executeCommand(input) {
@@ -1869,7 +1484,11 @@ async function executeCommand(input) {
1869
1484
  planRoot: input.planRoot,
1870
1485
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1871
1486
  });
1872
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1487
+ const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1488
+ if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
1489
+ throw new Error("The canonical workspace checkout is not initialized on this worker");
1490
+ }
1491
+ const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
1873
1492
  const subprocess = Bun.spawn(input.message.argv, {
1874
1493
  cwd,
1875
1494
  stdout: "pipe",
@@ -1879,7 +1498,6 @@ async function executeCommand(input) {
1879
1498
  ...process.env,
1880
1499
  ...githubProcessEnv(),
1881
1500
  ...input.message.env ?? {},
1882
- ...internalGitProcessEnv(workspace, input.message.env),
1883
1501
  ...artifactProcessEnv,
1884
1502
  ...planProcessEnv
1885
1503
  }
@@ -1977,7 +1595,11 @@ async function executeStreamingCommand(input) {
1977
1595
  planRoot: input.planRoot,
1978
1596
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1979
1597
  });
1980
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1598
+ const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1599
+ if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
1600
+ throw new Error("The canonical workspace checkout is not initialized on this worker");
1601
+ }
1602
+ const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
1981
1603
  const subprocess = Bun.spawn(input.message.argv, {
1982
1604
  cwd,
1983
1605
  stdout: "pipe",
@@ -1987,7 +1609,6 @@ async function executeStreamingCommand(input) {
1987
1609
  ...process.env,
1988
1610
  ...githubProcessEnv(),
1989
1611
  ...input.message.env ?? {},
1990
- ...internalGitProcessEnv(workspace, input.message.env),
1991
1612
  ...artifactProcessEnv,
1992
1613
  ...planProcessEnv
1993
1614
  }
@@ -2045,22 +1666,26 @@ async function executeStreamingCommand(input) {
2045
1666
  });
2046
1667
  })
2047
1668
  ]);
2048
- sendWorkerMessage(input.ws, {
1669
+ const terminal = {
2049
1670
  type: "exec_exit",
2050
1671
  runId: input.message.runId,
2051
1672
  exitCode,
2052
1673
  durationMs: Date.now() - startedAt,
2053
1674
  ...timedOut ? { timedOut: true } : {}
2054
- });
1675
+ };
1676
+ pendingProcessTerminals.set(input.message.runId, terminal);
1677
+ sendWorkerMessage(input.ws, terminal);
2055
1678
  } catch (error) {
2056
1679
  const message = error instanceof Error ? error.message : String(error);
2057
1680
  if (started) {
2058
- sendWorkerMessage(input.ws, {
1681
+ const terminal = {
2059
1682
  type: "exec_error",
2060
1683
  runId: input.message.runId,
2061
1684
  error: message,
2062
1685
  durationMs: Date.now() - startedAt
2063
- });
1686
+ };
1687
+ pendingProcessTerminals.set(input.message.runId, terminal);
1688
+ sendWorkerMessage(input.ws, terminal);
2064
1689
  } else {
2065
1690
  sendWorkerMessage(input.ws, {
2066
1691
  type: "exec_start_error",
@@ -2398,10 +2023,7 @@ function resizePty(message) {
2398
2023
  if (!ptyProcess) {
2399
2024
  return;
2400
2025
  }
2401
- ptyProcess.resize(
2402
- Math.max(1, Math.min(Math.floor(message.cols || 80), 500)),
2403
- Math.max(1, Math.min(Math.floor(message.rows || 24), 500))
2404
- );
2026
+ ptyProcess.resize(Math.max(1, Math.min(Math.floor(message.cols || 80), 500)), Math.max(1, Math.min(Math.floor(message.rows || 24), 500)));
2405
2027
  }
2406
2028
  function closePty(message) {
2407
2029
  const ptyProcess = activePtys.get(message.ptyId);
@@ -2421,10 +2043,13 @@ function websocketUrl(baseUrl, label) {
2421
2043
  const url = new URL("/worker/ws", baseUrl);
2422
2044
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2423
2045
  url.searchParams.set("label", label);
2046
+ url.searchParams.set("version", getWorkerVersion());
2424
2047
  return url.toString();
2425
2048
  }
2426
2049
  function projectRootFor(projectsRoot, projectId, manifestByProjectId) {
2427
- return path.join(projectsRoot, manifestByProjectId.get(projectId)?.repoSlug ?? projectId);
2050
+ const manifest = manifestByProjectId.get(projectId);
2051
+ if (!manifest) throw new Error(`Project ${projectId} is missing from the worker workspace manifest`);
2052
+ return managedProjectRoot(projectsRoot, manifest.checkoutPathSegments);
2428
2053
  }
2429
2054
  async function startWorker(options) {
2430
2055
  const config = readConfig(options.configPath);
@@ -2433,6 +2058,7 @@ async function startWorker(options) {
2433
2058
  validateLabel(label);
2434
2059
  const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
2435
2060
  const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
2061
+ const workspaceShadowRoot = path.join(syncRoot, "workspace");
2436
2062
  const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2437
2063
  const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
2438
2064
  process.stdout.write(`[r5d-worker] label: ${label}
@@ -2454,10 +2080,104 @@ async function startWorker(options) {
2454
2080
  fs.mkdirSync(artifactRoot, { recursive: true });
2455
2081
  fs.mkdirSync(planRoot, { recursive: true });
2456
2082
  const manifestByProjectId = /* @__PURE__ */ new Map();
2457
- let repositorySyncQueue = Promise.resolve();
2458
- let repositorySyncInProgress = false;
2083
+ const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
2084
+ let workspaceRemoteUrl = null;
2085
+ let activeWorkspaceIncidentId = null;
2086
+ let previousPeriodicFingerprint = null;
2087
+ let periodicWorkspaceScan;
2088
+ let periodicWorkspaceScanInFlight = false;
2089
+ let terminalReplayTimer;
2090
+ let workspaceSyncRequestsInFlight = 0;
2459
2091
  let cliUpdateInProgress = false;
2460
2092
  let reloadAfterClose = false;
2093
+ const workspaceSyncInput = (trigger, overrides = {}) => {
2094
+ if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2095
+ return {
2096
+ workerLabel: label,
2097
+ remoteUrl: workspaceRemoteUrl,
2098
+ authHeader: `Authorization: Bearer ${token}`,
2099
+ projectsRoot,
2100
+ plansRoot: planRoot,
2101
+ shadowRoot: workspaceShadowRoot,
2102
+ projects: [...manifestByProjectId.values()],
2103
+ trigger,
2104
+ ...overrides
2105
+ };
2106
+ };
2107
+ const sendWorkspaceSyncResult = (requestId, result) => {
2108
+ sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2109
+ };
2110
+ const ensureAllVisibleWorkspaceCheckouts = () => {
2111
+ const created = [];
2112
+ for (const manifest of manifestByProjectId.values()) {
2113
+ for (const branchName of manifest.branches) {
2114
+ const branchPath = path.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2115
+ const existed = fs.existsSync(path.join(branchPath, ".git"));
2116
+ const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2117
+ ensureVisibleGitCheckout({
2118
+ projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2119
+ branchName,
2120
+ githubRemoteUrl,
2121
+ githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2122
+ githubAuthHeader: manifest.repoAuthHeader,
2123
+ defaultBranch: manifest.defaultBranch || "main",
2124
+ reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2125
+ });
2126
+ if (!existed) created.push({ projectId: manifest.projectId, branchName });
2127
+ }
2128
+ }
2129
+ return created;
2130
+ };
2131
+ const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2132
+ workspaceSyncRequestsInFlight += 1;
2133
+ try {
2134
+ if (trigger.type === "plan_write" || trigger.type === "task_status" || trigger.type === "connect" || trigger.type === "inbound_head" || trigger.type === "manual") {
2135
+ for (const manifest of manifestByProjectId.values()) {
2136
+ for (const branchName of manifest.branches) {
2137
+ await syncProjectPlans({
2138
+ baseUrl,
2139
+ token,
2140
+ projectId: manifest.projectId,
2141
+ branchName,
2142
+ planRoot
2143
+ });
2144
+ }
2145
+ }
2146
+ }
2147
+ const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2148
+ const result = await workspaceSyncSingleFlight.run(
2149
+ workspaceSyncInput(trigger, {
2150
+ ...overrides,
2151
+ ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2152
+ })
2153
+ );
2154
+ previousPeriodicFingerprint = null;
2155
+ sendWorkspaceSyncResult(requestId, result);
2156
+ return result;
2157
+ } catch (error) {
2158
+ const result = {
2159
+ type: "workspace_sync",
2160
+ attemptId: overrides.attemptId ?? crypto.randomUUID(),
2161
+ workerLabel: label,
2162
+ trigger,
2163
+ outcome: "failed",
2164
+ startingHead: null,
2165
+ rebaseCount: 0,
2166
+ diffSizeBytes: 0,
2167
+ gitStatus: "",
2168
+ affectedProjects: [],
2169
+ affectedPaths: [],
2170
+ discardedPaths: [],
2171
+ localChangesDiscarded: false,
2172
+ error: error instanceof Error ? error.message : String(error)
2173
+ };
2174
+ previousPeriodicFingerprint = null;
2175
+ sendWorkspaceSyncResult(requestId, result);
2176
+ return result;
2177
+ } finally {
2178
+ workspaceSyncRequestsInFlight -= 1;
2179
+ }
2180
+ };
2461
2181
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2462
2182
  headers: {
2463
2183
  Authorization: `Bearer ${token}`
@@ -2502,6 +2222,15 @@ async function startWorker(options) {
2502
2222
  };
2503
2223
  ws.send(JSON.stringify(hello));
2504
2224
  sendActiveProcessReport(ws);
2225
+ for (const terminal of pendingProcessTerminals.values()) {
2226
+ sendWorkerMessage(ws, terminal);
2227
+ }
2228
+ terminalReplayTimer = setInterval(() => {
2229
+ for (const terminal of pendingProcessTerminals.values()) {
2230
+ sendWorkerMessage(ws, terminal);
2231
+ }
2232
+ }, 5e3);
2233
+ terminalReplayTimer.unref();
2505
2234
  process.stdout.write("[r5d-worker] connected\n");
2506
2235
  });
2507
2236
  ws.addEventListener("message", (event) => {
@@ -2510,92 +2239,73 @@ async function startWorker(options) {
2510
2239
  if (message.type === "connected") {
2511
2240
  return;
2512
2241
  }
2513
- if (message.type === "project_manifest") {
2242
+ if (message.type === "workspace_manifest") {
2514
2243
  configureGitHubAuth(message.githubCredential);
2515
2244
  visibleGitIdentity = message.gitIdentity;
2245
+ workspaceRemoteUrl = message.workspaceRemoteUrl;
2246
+ const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
2516
2247
  manifestByProjectId.clear();
2517
- for (const project of message.projects) {
2518
- manifestByProjectId.set(project.projectId, project);
2519
- }
2520
- process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
2521
- `);
2522
- return;
2523
- }
2524
- if (message.type === "sync_projects") {
2525
- if (cliUpdateInProgress) {
2526
- sendWorkerMessage(ws, {
2527
- type: "sync_projects_result",
2528
- requestId: message.requestId,
2529
- result: {
2530
- status: "blocked",
2531
- results: [],
2532
- blockedBy: []
2533
- }
2534
- });
2535
- return;
2536
- }
2537
- let syncResult = { status: "completed", results: [], blockedBy: [] };
2538
- const executeSync = async () => {
2539
- repositorySyncInProgress = true;
2540
- try {
2541
- syncResult = await syncManifestProjectsFromInternal({
2542
- baseUrl,
2543
- token,
2544
- projectsRoot,
2545
- syncRoot,
2546
- manifests: [...manifestByProjectId.values()],
2547
- projectIds: message.projectIds
2548
- });
2549
- for (const result of syncResult.results) {
2550
- if (result.status !== "synced") continue;
2551
- try {
2552
- await syncProjectPlans({
2553
- baseUrl,
2554
- token,
2555
- projectId: result.projectId,
2556
- branchName: result.branchName,
2557
- planRoot
2558
- });
2559
- } catch (error) {
2560
- process.stderr.write(
2561
- `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2248
+ for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2249
+ process.stdout.write(
2250
+ `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2562
2251
  `
2563
- );
2564
- }
2565
- }
2566
- } finally {
2567
- repositorySyncInProgress = false;
2568
- }
2569
- };
2570
- const queued = repositorySyncQueue.then(executeSync, executeSync);
2571
- repositorySyncQueue = queued.then(
2572
- () => void 0,
2573
- () => void 0
2574
2252
  );
2575
- try {
2576
- await queued;
2577
- } catch (error) {
2578
- syncResult = {
2579
- status: "partial",
2580
- results: [
2581
- {
2582
- projectId: "worker",
2583
- projectPath: "worker",
2584
- branchName: "all",
2585
- status: "failed",
2586
- error: error instanceof Error ? error.message : String(error)
2253
+ if (!periodicWorkspaceScan) {
2254
+ const emptyFingerprint = createHash("sha256").update("").digest("hex");
2255
+ periodicWorkspaceScan = setInterval(() => {
2256
+ if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2257
+ periodicWorkspaceScanInFlight = true;
2258
+ void (async () => {
2259
+ const input = workspaceSyncInput({ type: "periodic" });
2260
+ const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2261
+ if (fingerprint === emptyFingerprint) {
2262
+ previousPeriodicFingerprint = null;
2263
+ return;
2587
2264
  }
2588
- ],
2589
- blockedBy: []
2590
- };
2265
+ if (previousPeriodicFingerprint !== fingerprint) {
2266
+ previousPeriodicFingerprint = fingerprint;
2267
+ return;
2268
+ }
2269
+ previousPeriodicFingerprint = null;
2270
+ await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "periodic" });
2271
+ })().catch((error) => {
2272
+ process.stderr.write(
2273
+ `[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
2274
+ `
2275
+ );
2276
+ }).finally(() => {
2277
+ periodicWorkspaceScanInFlight = false;
2278
+ });
2279
+ }, WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
2280
+ periodicWorkspaceScan.unref();
2591
2281
  }
2592
- sendWorkerMessage(ws, {
2593
- type: "sync_projects_result",
2594
- requestId: message.requestId,
2595
- result: syncResult
2282
+ return;
2283
+ }
2284
+ if (message.type === "sync_workspace") {
2285
+ await runRequestedWorkspaceSync(message.requestId, message.trigger, {
2286
+ attemptId: message.attemptId,
2287
+ confirmedLargeDiff: message.confirmedLargeDiff,
2288
+ confirmationReason: message.confirmationReason,
2289
+ resetToCanonical: message.resetToCanonical,
2290
+ skipVisibleMirror: message.skipVisibleMirror
2596
2291
  });
2597
2292
  return;
2598
2293
  }
2294
+ if (message.type === "workspace_head_updated") {
2295
+ if (!activeWorkspaceIncidentId) {
2296
+ await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "inbound_head", detail: message.commitHash });
2297
+ }
2298
+ return;
2299
+ }
2300
+ if (message.type === "workspace_incident_updated") {
2301
+ activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
2302
+ previousPeriodicFingerprint = null;
2303
+ return;
2304
+ }
2305
+ if (message.type === "exec_terminal_ack") {
2306
+ pendingProcessTerminals.delete(message.runId);
2307
+ return;
2308
+ }
2599
2309
  if (message.type === "update_clis") {
2600
2310
  if (cliUpdateInProgress) {
2601
2311
  sendWorkerMessage(ws, {
@@ -2605,7 +2315,7 @@ async function startWorker(options) {
2605
2315
  });
2606
2316
  return;
2607
2317
  }
2608
- if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
2318
+ if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
2609
2319
  sendWorkerMessage(ws, {
2610
2320
  type: "update_clis_result",
2611
2321
  requestId: message.requestId,
@@ -2675,7 +2385,6 @@ async function startWorker(options) {
2675
2385
  });
2676
2386
  return;
2677
2387
  }
2678
- await repositorySyncQueue;
2679
2388
  const manifest = manifestByProjectId.get(message.projectId);
2680
2389
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2681
2390
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2706,7 +2415,6 @@ async function startWorker(options) {
2706
2415
  });
2707
2416
  return;
2708
2417
  }
2709
- await repositorySyncQueue;
2710
2418
  const manifest = manifestByProjectId.get(message.projectId);
2711
2419
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2712
2420
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2720,6 +2428,7 @@ async function startWorker(options) {
2720
2428
  syncRoot,
2721
2429
  artifactRoot,
2722
2430
  planRoot,
2431
+ workspaceShadowRoot,
2723
2432
  manifest
2724
2433
  });
2725
2434
  ws.send(JSON.stringify(result));
@@ -2735,7 +2444,6 @@ async function startWorker(options) {
2735
2444
  });
2736
2445
  return;
2737
2446
  }
2738
- await repositorySyncQueue;
2739
2447
  sendWorkerMessage(ws, {
2740
2448
  type: "exec_accepted",
2741
2449
  requestId: message.requestId,
@@ -2756,6 +2464,7 @@ async function startWorker(options) {
2756
2464
  syncRoot,
2757
2465
  artifactRoot,
2758
2466
  planRoot,
2467
+ workspaceShadowRoot,
2759
2468
  manifest
2760
2469
  }).catch((error) => {
2761
2470
  sendWorkerMessage(ws, {
@@ -2775,9 +2484,8 @@ async function startWorker(options) {
2775
2484
  }
2776
2485
  return;
2777
2486
  }
2778
- if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
2487
+ if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
2779
2488
  try {
2780
- await repositorySyncQueue;
2781
2489
  const manifest = manifestByProjectId.get(message.projectId);
2782
2490
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2783
2491
  const result = await executeOperation({
@@ -2825,6 +2533,14 @@ async function startWorker(options) {
2825
2533
  });
2826
2534
  ws.addEventListener("close", (event) => {
2827
2535
  stopHeartbeatWatchdog();
2536
+ if (periodicWorkspaceScan) {
2537
+ clearInterval(periodicWorkspaceScan);
2538
+ periodicWorkspaceScan = void 0;
2539
+ }
2540
+ if (terminalReplayTimer) {
2541
+ clearInterval(terminalReplayTimer);
2542
+ terminalReplayTimer = void 0;
2543
+ }
2828
2544
  if (currentWorkerSocket === ws) {
2829
2545
  currentWorkerSocket = null;
2830
2546
  }
@@ -2835,15 +2551,17 @@ async function startWorker(options) {
2835
2551
  if (reloadAfterClose) {
2836
2552
  process.exit(WORKER_RELOAD_EXIT_CODE);
2837
2553
  }
2838
- if (activeProcesses.size > 0) {
2554
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
2839
2555
  const delayMs = 2e3;
2840
- process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
2841
- `);
2556
+ process.stderr.write(
2557
+ `[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${delayMs}ms
2558
+ `
2559
+ );
2842
2560
  const reconnect = () => {
2843
2561
  void startWorker(options).catch((error) => {
2844
2562
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
2845
2563
  `);
2846
- if (activeProcesses.size > 0) {
2564
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
2847
2565
  setTimeout(reconnect, delayMs);
2848
2566
  return;
2849
2567
  }
@@ -2898,7 +2616,6 @@ if (isCliEntrypoint()) {
2898
2616
  }
2899
2617
  export {
2900
2618
  ensureVisibleGitCheckout,
2901
- findRepositorySyncBlockers,
2902
2619
  findWorkerFiles,
2903
2620
  githubCliEnv,
2904
2621
  grepWorkerFiles,
@@ -2911,7 +2628,6 @@ export {
2911
2628
  resolveHostShell,
2912
2629
  resolveProjectFilePath,
2913
2630
  resolveWorkerFilePath,
2914
- syncManifestProjectsFromInternal,
2915
2631
  syncProjectPlans,
2916
2632
  syncSessionArtifacts
2917
2633
  };