@ricsam/r5d-worker 0.0.36 → 0.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mjs/main.mjs CHANGED
@@ -9,6 +9,12 @@ import { configureVisibleGitIdentity } from "./git-identity.mjs";
9
9
  import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
10
10
  import { terminateProcessTree } from "./process-tree.mjs";
11
11
  import { superviseWorkerRuntime, WORKER_RELOAD_EXIT_CODE, WORKER_RUNTIME_ENV } from "./supervisor.mjs";
12
+ import {
13
+ WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
14
+ WorkspaceSyncSingleFlight,
15
+ calculateWorkspaceDiffFingerprint,
16
+ synchronizeWorkspace
17
+ } from "./workspace-sync.mjs";
12
18
  const DEFAULT_BASE_URL = "https://r5d.dev";
13
19
  const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
14
20
  const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
@@ -16,10 +22,8 @@ const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
16
22
  const DEFAULT_READ_LIMIT = 2e3;
17
23
  const DEFAULT_READ_MAX_BYTES = 5e4;
18
24
  const MAX_LINE_LENGTH = 2e3;
19
- const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
20
- const R5D_REMOTE_NAME = "r5d";
21
- const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
22
25
  const activeProcesses = /* @__PURE__ */ new Map();
26
+ const pendingProcessTerminals = /* @__PURE__ */ new Map();
23
27
  const cancelledProcessRuns = /* @__PURE__ */ new Set();
24
28
  const activePtys = /* @__PURE__ */ new Map();
25
29
  let currentWorkerSocket = null;
@@ -478,8 +482,10 @@ async function prepareArtifactEnvForShell(input) {
478
482
  artifactRoot: input.artifactRoot
479
483
  });
480
484
  } catch (error) {
481
- process.stderr.write(`[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
482
- `);
485
+ process.stderr.write(
486
+ `[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
487
+ `
488
+ );
483
489
  }
484
490
  const artifactsDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
485
491
  fs.mkdirSync(artifactsDir, { recursive: true });
@@ -535,7 +541,9 @@ function resolveCredentials(options, config) {
535
541
  throw new Error("Authentication required. Run `r5dctl auth login` or set R5D_WORKER_TOKEN/R5D_API_KEY.");
536
542
  }
537
543
  return {
538
- baseUrl: normalizeBaseUrl(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL),
544
+ baseUrl: normalizeBaseUrl(
545
+ options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL
546
+ ),
539
547
  token
540
548
  };
541
549
  }
@@ -589,27 +597,6 @@ function runGit(args, options = {}) {
589
597
  }
590
598
  return result.stdout.toString().trim();
591
599
  }
592
- async function runGitAsync(args, options = {}) {
593
- const command = ["git", ...gitAuthArgs(options.auth), ...args];
594
- const subprocess = Bun.spawn(command, {
595
- cwd: options.cwd,
596
- stdout: "pipe",
597
- stderr: "pipe",
598
- env: {
599
- ...process.env,
600
- GIT_TERMINAL_PROMPT: "0"
601
- }
602
- });
603
- const [stdout, stderr, exitCode] = await Promise.all([
604
- collectStream(subprocess.stdout),
605
- collectStream(subprocess.stderr),
606
- subprocess.exited
607
- ]);
608
- if (exitCode !== 0) {
609
- throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
610
- }
611
- return stdout.trim();
612
- }
613
600
  function tryGit(args, options = {}) {
614
601
  try {
615
602
  runGit(args, options);
@@ -618,34 +605,6 @@ function tryGit(args, options = {}) {
618
605
  return false;
619
606
  }
620
607
  }
621
- async function tryGitAsync(args, options = {}) {
622
- try {
623
- await runGitAsync(args, options);
624
- return true;
625
- } catch {
626
- return false;
627
- }
628
- }
629
- function runInternalGit(context, args) {
630
- return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
631
- }
632
- function runInternalGitAsync(context, args) {
633
- return runGitAsync(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
634
- }
635
- function runInternalGitDir(context, args) {
636
- return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
637
- }
638
- function tryInternalGit(context, args) {
639
- try {
640
- runInternalGit(context, args);
641
- return true;
642
- } catch {
643
- return false;
644
- }
645
- }
646
- function getInternalCommitHash(context) {
647
- return runInternalGit(context, ["rev-parse", "HEAD"]);
648
- }
649
608
  function getGitBlobHashForContent(content) {
650
609
  const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
651
610
  return createHash("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
@@ -653,12 +612,6 @@ function getGitBlobHashForContent(content) {
653
612
  function hasWorktreeChanges(cwd) {
654
613
  return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
655
614
  }
656
- function getInternalStatus(context) {
657
- return runInternalGit(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
658
- }
659
- function hasInternalWorktreeChanges(context) {
660
- return getInternalStatus(context).trim().length > 0;
661
- }
662
615
  function isInsideBranchPath(branchPath, filePath) {
663
616
  const relative = path.relative(path.resolve(branchPath), path.resolve(filePath));
664
617
  return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
@@ -721,32 +674,15 @@ function resolveWorkerFilePath(branchPath, inputPath) {
721
674
  function resolveProjectFilePath(branchPath, inputPath) {
722
675
  const resolved = resolveWorkerFilePath(branchPath, inputPath);
723
676
  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
- );
677
+ throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
727
678
  }
728
679
  return resolved;
729
680
  }
730
- function resolveRemoteUrl(baseUrl, remoteUrl) {
731
- if (/^https?:\/\//i.test(remoteUrl)) {
732
- return remoteUrl;
733
- }
734
- return new URL(remoteUrl, baseUrl).toString();
735
- }
736
681
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
737
682
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
738
683
  }
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
684
  function githubRemoteUrlFor(baseUrl, projectId, manifest) {
749
- return manifest?.repoHttpUrl ?? internalRemoteUrlFor(baseUrl, projectId, manifest);
685
+ return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
750
686
  }
751
687
  function gitExtraHeaderUrlForRemote(remoteUrl) {
752
688
  try {
@@ -764,12 +700,6 @@ function gitAuthForRemote(remoteUrl, authHeader) {
764
700
  header: authHeader
765
701
  };
766
702
  }
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
703
  function hasNormalVisibleGitDir(branchPath) {
774
704
  const gitPath = path.join(branchPath, ".git");
775
705
  return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
@@ -781,13 +711,6 @@ function ensureOriginRemote(branchPath, remoteUrl) {
781
711
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
782
712
  }
783
713
  }
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
714
  function shellQuote(value) {
792
715
  return `'${value.replace(/'/g, "'\\''")}'`;
793
716
  }
@@ -983,194 +906,20 @@ function ensureVisibleGitCheckout(input) {
983
906
  configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
984
907
  return branchPath;
985
908
  }
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
909
  function ensureBranchWorkspace(input) {
1019
910
  const defaultBranch = input.manifest?.defaultBranch || "main";
1020
911
  const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1021
912
  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
913
  return {
1171
- status: results.some((result) => result.status === "failed") ? "partial" : "completed",
1172
- results,
1173
- blockedBy: []
914
+ branchPath: ensureVisibleGitCheckout({
915
+ projectRoot: input.projectRoot,
916
+ branchName: input.branchName,
917
+ githubRemoteUrl,
918
+ githubAuth,
919
+ githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
920
+ defaultBranch,
921
+ reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
922
+ })
1174
923
  };
1175
924
  }
1176
925
  function resolveCommandCwd(branchPath, cwd) {
@@ -1221,15 +970,6 @@ async function streamCommandOutput(stream, onData) {
1221
970
  function ensureOperationBranch(input) {
1222
971
  return ensureBranchWorkspace(input);
1223
972
  }
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
973
  function formatLineNumberedContent(content, offset, limit) {
1234
974
  const allLines = content.split("\n");
1235
975
  const totalLines = allLines.length;
@@ -1338,9 +1078,11 @@ const fileMutationQueues = /* @__PURE__ */ new Map();
1338
1078
  async function withFileMutationQueue(key, operation) {
1339
1079
  const previous = fileMutationQueues.get(key) ?? Promise.resolve();
1340
1080
  let release;
1341
- const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
1342
- release = resolve;
1343
- }));
1081
+ const current = previous.catch(() => void 0).then(
1082
+ () => new Promise((resolve) => {
1083
+ release = resolve;
1084
+ })
1085
+ );
1344
1086
  fileMutationQueues.set(key, current);
1345
1087
  await previous.catch(() => void 0);
1346
1088
  try {
@@ -1435,7 +1177,9 @@ async function executeEditFileOperation(input) {
1435
1177
  throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1436
1178
  }
1437
1179
  if (occurrences > 1) {
1438
- throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
1180
+ throw new Error(
1181
+ `edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
1182
+ );
1439
1183
  }
1440
1184
  const start = originalContent.indexOf(edit.oldText);
1441
1185
  return { index, start, end: start + edit.oldText.length, edit };
@@ -1695,96 +1439,6 @@ async function executeViewFileBytesOperation(input) {
1695
1439
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1696
1440
  return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1697
1441
  }
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
1442
  async function executeOperation(input) {
1789
1443
  switch (input.message.type) {
1790
1444
  case "read":
@@ -1801,43 +1455,6 @@ async function executeOperation(input) {
1801
1455
  return executeLsOperation({ ...input, message: input.message });
1802
1456
  case "view_file_bytes":
1803
1457
  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
1458
  }
1842
1459
  }
1843
1460
  async function executeCommand(input) {
@@ -1869,7 +1486,11 @@ async function executeCommand(input) {
1869
1486
  planRoot: input.planRoot,
1870
1487
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1871
1488
  });
1872
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1489
+ const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1490
+ if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
1491
+ throw new Error("The canonical workspace checkout is not initialized on this worker");
1492
+ }
1493
+ const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
1873
1494
  const subprocess = Bun.spawn(input.message.argv, {
1874
1495
  cwd,
1875
1496
  stdout: "pipe",
@@ -1879,7 +1500,6 @@ async function executeCommand(input) {
1879
1500
  ...process.env,
1880
1501
  ...githubProcessEnv(),
1881
1502
  ...input.message.env ?? {},
1882
- ...internalGitProcessEnv(workspace, input.message.env),
1883
1503
  ...artifactProcessEnv,
1884
1504
  ...planProcessEnv
1885
1505
  }
@@ -1977,7 +1597,11 @@ async function executeStreamingCommand(input) {
1977
1597
  planRoot: input.planRoot,
1978
1598
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1979
1599
  });
1980
- const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1600
+ const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1601
+ if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
1602
+ throw new Error("The canonical workspace checkout is not initialized on this worker");
1603
+ }
1604
+ const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
1981
1605
  const subprocess = Bun.spawn(input.message.argv, {
1982
1606
  cwd,
1983
1607
  stdout: "pipe",
@@ -1987,7 +1611,6 @@ async function executeStreamingCommand(input) {
1987
1611
  ...process.env,
1988
1612
  ...githubProcessEnv(),
1989
1613
  ...input.message.env ?? {},
1990
- ...internalGitProcessEnv(workspace, input.message.env),
1991
1614
  ...artifactProcessEnv,
1992
1615
  ...planProcessEnv
1993
1616
  }
@@ -2045,22 +1668,26 @@ async function executeStreamingCommand(input) {
2045
1668
  });
2046
1669
  })
2047
1670
  ]);
2048
- sendWorkerMessage(input.ws, {
1671
+ const terminal = {
2049
1672
  type: "exec_exit",
2050
1673
  runId: input.message.runId,
2051
1674
  exitCode,
2052
1675
  durationMs: Date.now() - startedAt,
2053
1676
  ...timedOut ? { timedOut: true } : {}
2054
- });
1677
+ };
1678
+ pendingProcessTerminals.set(input.message.runId, terminal);
1679
+ sendWorkerMessage(input.ws, terminal);
2055
1680
  } catch (error) {
2056
1681
  const message = error instanceof Error ? error.message : String(error);
2057
1682
  if (started) {
2058
- sendWorkerMessage(input.ws, {
1683
+ const terminal = {
2059
1684
  type: "exec_error",
2060
1685
  runId: input.message.runId,
2061
1686
  error: message,
2062
1687
  durationMs: Date.now() - startedAt
2063
- });
1688
+ };
1689
+ pendingProcessTerminals.set(input.message.runId, terminal);
1690
+ sendWorkerMessage(input.ws, terminal);
2064
1691
  } else {
2065
1692
  sendWorkerMessage(input.ws, {
2066
1693
  type: "exec_start_error",
@@ -2398,10 +2025,7 @@ function resizePty(message) {
2398
2025
  if (!ptyProcess) {
2399
2026
  return;
2400
2027
  }
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
- );
2028
+ ptyProcess.resize(Math.max(1, Math.min(Math.floor(message.cols || 80), 500)), Math.max(1, Math.min(Math.floor(message.rows || 24), 500)));
2405
2029
  }
2406
2030
  function closePty(message) {
2407
2031
  const ptyProcess = activePtys.get(message.ptyId);
@@ -2433,6 +2057,7 @@ async function startWorker(options) {
2433
2057
  validateLabel(label);
2434
2058
  const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
2435
2059
  const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
2060
+ const workspaceShadowRoot = path.join(syncRoot, "workspace");
2436
2061
  const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2437
2062
  const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
2438
2063
  process.stdout.write(`[r5d-worker] label: ${label}
@@ -2454,10 +2079,104 @@ async function startWorker(options) {
2454
2079
  fs.mkdirSync(artifactRoot, { recursive: true });
2455
2080
  fs.mkdirSync(planRoot, { recursive: true });
2456
2081
  const manifestByProjectId = /* @__PURE__ */ new Map();
2457
- let repositorySyncQueue = Promise.resolve();
2458
- let repositorySyncInProgress = false;
2082
+ const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
2083
+ let workspaceRemoteUrl = null;
2084
+ let activeWorkspaceIncidentId = null;
2085
+ let previousPeriodicFingerprint = null;
2086
+ let periodicWorkspaceScan;
2087
+ let periodicWorkspaceScanInFlight = false;
2088
+ let terminalReplayTimer;
2089
+ let workspaceSyncRequestsInFlight = 0;
2459
2090
  let cliUpdateInProgress = false;
2460
2091
  let reloadAfterClose = false;
2092
+ const workspaceSyncInput = (trigger, overrides = {}) => {
2093
+ if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2094
+ return {
2095
+ workerLabel: label,
2096
+ remoteUrl: workspaceRemoteUrl,
2097
+ authHeader: `Authorization: Bearer ${token}`,
2098
+ projectsRoot,
2099
+ plansRoot: planRoot,
2100
+ shadowRoot: workspaceShadowRoot,
2101
+ projects: [...manifestByProjectId.values()],
2102
+ trigger,
2103
+ ...overrides
2104
+ };
2105
+ };
2106
+ const sendWorkspaceSyncResult = (requestId, result) => {
2107
+ sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2108
+ };
2109
+ const ensureAllVisibleWorkspaceCheckouts = () => {
2110
+ const created = [];
2111
+ for (const manifest of manifestByProjectId.values()) {
2112
+ for (const branchName of manifest.branches) {
2113
+ const branchPath = path.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2114
+ const existed = fs.existsSync(path.join(branchPath, ".git"));
2115
+ const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2116
+ ensureVisibleGitCheckout({
2117
+ projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2118
+ branchName,
2119
+ githubRemoteUrl,
2120
+ githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2121
+ githubAuthHeader: manifest.repoAuthHeader,
2122
+ defaultBranch: manifest.defaultBranch || "main",
2123
+ reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2124
+ });
2125
+ if (!existed) created.push({ projectId: manifest.projectId, branchName });
2126
+ }
2127
+ }
2128
+ return created;
2129
+ };
2130
+ const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2131
+ workspaceSyncRequestsInFlight += 1;
2132
+ try {
2133
+ if (trigger.type === "plan_write" || trigger.type === "task_status" || trigger.type === "connect" || trigger.type === "inbound_head" || trigger.type === "manual") {
2134
+ for (const manifest of manifestByProjectId.values()) {
2135
+ for (const branchName of manifest.branches) {
2136
+ await syncProjectPlans({
2137
+ baseUrl,
2138
+ token,
2139
+ projectId: manifest.projectId,
2140
+ branchName,
2141
+ planRoot
2142
+ });
2143
+ }
2144
+ }
2145
+ }
2146
+ const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2147
+ const result = await workspaceSyncSingleFlight.run(
2148
+ workspaceSyncInput(trigger, {
2149
+ ...overrides,
2150
+ ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2151
+ })
2152
+ );
2153
+ previousPeriodicFingerprint = null;
2154
+ sendWorkspaceSyncResult(requestId, result);
2155
+ return result;
2156
+ } catch (error) {
2157
+ const result = {
2158
+ type: "workspace_sync",
2159
+ attemptId: overrides.attemptId ?? crypto.randomUUID(),
2160
+ workerLabel: label,
2161
+ trigger,
2162
+ outcome: "failed",
2163
+ startingHead: null,
2164
+ rebaseCount: 0,
2165
+ diffSizeBytes: 0,
2166
+ gitStatus: "",
2167
+ affectedProjects: [],
2168
+ affectedPaths: [],
2169
+ discardedPaths: [],
2170
+ localChangesDiscarded: false,
2171
+ error: error instanceof Error ? error.message : String(error)
2172
+ };
2173
+ previousPeriodicFingerprint = null;
2174
+ sendWorkspaceSyncResult(requestId, result);
2175
+ return result;
2176
+ } finally {
2177
+ workspaceSyncRequestsInFlight -= 1;
2178
+ }
2179
+ };
2461
2180
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2462
2181
  headers: {
2463
2182
  Authorization: `Bearer ${token}`
@@ -2502,6 +2221,15 @@ async function startWorker(options) {
2502
2221
  };
2503
2222
  ws.send(JSON.stringify(hello));
2504
2223
  sendActiveProcessReport(ws);
2224
+ for (const terminal of pendingProcessTerminals.values()) {
2225
+ sendWorkerMessage(ws, terminal);
2226
+ }
2227
+ terminalReplayTimer = setInterval(() => {
2228
+ for (const terminal of pendingProcessTerminals.values()) {
2229
+ sendWorkerMessage(ws, terminal);
2230
+ }
2231
+ }, 5e3);
2232
+ terminalReplayTimer.unref();
2505
2233
  process.stdout.write("[r5d-worker] connected\n");
2506
2234
  });
2507
2235
  ws.addEventListener("message", (event) => {
@@ -2510,92 +2238,70 @@ async function startWorker(options) {
2510
2238
  if (message.type === "connected") {
2511
2239
  return;
2512
2240
  }
2513
- if (message.type === "project_manifest") {
2241
+ if (message.type === "workspace_manifest") {
2514
2242
  configureGitHubAuth(message.githubCredential);
2515
2243
  visibleGitIdentity = message.gitIdentity;
2244
+ workspaceRemoteUrl = message.workspaceRemoteUrl;
2516
2245
  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
2246
+ for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2247
+ process.stdout.write(`[r5d-worker] workspace manifest: ${message.projects.length} projects
2521
2248
  `);
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)}
2562
- `
2563
- );
2249
+ if (!periodicWorkspaceScan) {
2250
+ const emptyFingerprint = createHash("sha256").update("").digest("hex");
2251
+ periodicWorkspaceScan = setInterval(() => {
2252
+ if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2253
+ periodicWorkspaceScanInFlight = true;
2254
+ void (async () => {
2255
+ const input = workspaceSyncInput({ type: "periodic" });
2256
+ const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2257
+ if (fingerprint === emptyFingerprint) {
2258
+ previousPeriodicFingerprint = null;
2259
+ return;
2564
2260
  }
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
- );
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)
2261
+ if (previousPeriodicFingerprint !== fingerprint) {
2262
+ previousPeriodicFingerprint = fingerprint;
2263
+ return;
2587
2264
  }
2588
- ],
2589
- blockedBy: []
2590
- };
2265
+ previousPeriodicFingerprint = null;
2266
+ await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "periodic" });
2267
+ })().catch((error) => {
2268
+ process.stderr.write(
2269
+ `[r5d-worker] periodic workspace synchronization failed: ${error instanceof Error ? error.message : String(error)}
2270
+ `
2271
+ );
2272
+ }).finally(() => {
2273
+ periodicWorkspaceScanInFlight = false;
2274
+ });
2275
+ }, WORKSPACE_PERIODIC_SCAN_INTERVAL_MS);
2276
+ periodicWorkspaceScan.unref();
2591
2277
  }
2592
- sendWorkerMessage(ws, {
2593
- type: "sync_projects_result",
2594
- requestId: message.requestId,
2595
- result: syncResult
2278
+ return;
2279
+ }
2280
+ if (message.type === "sync_workspace") {
2281
+ await runRequestedWorkspaceSync(message.requestId, message.trigger, {
2282
+ attemptId: message.attemptId,
2283
+ confirmedLargeDiff: message.confirmedLargeDiff,
2284
+ confirmationReason: message.confirmationReason,
2285
+ resetToCanonical: message.resetToCanonical,
2286
+ skipVisibleMirror: message.skipVisibleMirror
2596
2287
  });
2597
2288
  return;
2598
2289
  }
2290
+ if (message.type === "workspace_head_updated") {
2291
+ if (!activeWorkspaceIncidentId) {
2292
+ await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "inbound_head", detail: message.commitHash });
2293
+ }
2294
+ return;
2295
+ }
2296
+ if (message.type === "workspace_incident_updated") {
2297
+ activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
2298
+ previousPeriodicFingerprint = null;
2299
+ return;
2300
+ }
2301
+ if (message.type === "exec_terminal_ack") {
2302
+ pendingProcessTerminals.delete(message.runId);
2303
+ return;
2304
+ }
2599
2305
  if (message.type === "update_clis") {
2600
2306
  if (cliUpdateInProgress) {
2601
2307
  sendWorkerMessage(ws, {
@@ -2605,7 +2311,7 @@ async function startWorker(options) {
2605
2311
  });
2606
2312
  return;
2607
2313
  }
2608
- if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
2314
+ if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
2609
2315
  sendWorkerMessage(ws, {
2610
2316
  type: "update_clis_result",
2611
2317
  requestId: message.requestId,
@@ -2675,7 +2381,6 @@ async function startWorker(options) {
2675
2381
  });
2676
2382
  return;
2677
2383
  }
2678
- await repositorySyncQueue;
2679
2384
  const manifest = manifestByProjectId.get(message.projectId);
2680
2385
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2681
2386
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2706,7 +2411,6 @@ async function startWorker(options) {
2706
2411
  });
2707
2412
  return;
2708
2413
  }
2709
- await repositorySyncQueue;
2710
2414
  const manifest = manifestByProjectId.get(message.projectId);
2711
2415
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2712
2416
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2720,6 +2424,7 @@ async function startWorker(options) {
2720
2424
  syncRoot,
2721
2425
  artifactRoot,
2722
2426
  planRoot,
2427
+ workspaceShadowRoot,
2723
2428
  manifest
2724
2429
  });
2725
2430
  ws.send(JSON.stringify(result));
@@ -2735,7 +2440,6 @@ async function startWorker(options) {
2735
2440
  });
2736
2441
  return;
2737
2442
  }
2738
- await repositorySyncQueue;
2739
2443
  sendWorkerMessage(ws, {
2740
2444
  type: "exec_accepted",
2741
2445
  requestId: message.requestId,
@@ -2756,6 +2460,7 @@ async function startWorker(options) {
2756
2460
  syncRoot,
2757
2461
  artifactRoot,
2758
2462
  planRoot,
2463
+ workspaceShadowRoot,
2759
2464
  manifest
2760
2465
  }).catch((error) => {
2761
2466
  sendWorkerMessage(ws, {
@@ -2775,9 +2480,8 @@ async function startWorker(options) {
2775
2480
  }
2776
2481
  return;
2777
2482
  }
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") {
2483
+ if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
2779
2484
  try {
2780
- await repositorySyncQueue;
2781
2485
  const manifest = manifestByProjectId.get(message.projectId);
2782
2486
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2783
2487
  const result = await executeOperation({
@@ -2825,6 +2529,14 @@ async function startWorker(options) {
2825
2529
  });
2826
2530
  ws.addEventListener("close", (event) => {
2827
2531
  stopHeartbeatWatchdog();
2532
+ if (periodicWorkspaceScan) {
2533
+ clearInterval(periodicWorkspaceScan);
2534
+ periodicWorkspaceScan = void 0;
2535
+ }
2536
+ if (terminalReplayTimer) {
2537
+ clearInterval(terminalReplayTimer);
2538
+ terminalReplayTimer = void 0;
2539
+ }
2828
2540
  if (currentWorkerSocket === ws) {
2829
2541
  currentWorkerSocket = null;
2830
2542
  }
@@ -2835,15 +2547,17 @@ async function startWorker(options) {
2835
2547
  if (reloadAfterClose) {
2836
2548
  process.exit(WORKER_RELOAD_EXIT_CODE);
2837
2549
  }
2838
- if (activeProcesses.size > 0) {
2550
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
2839
2551
  const delayMs = 2e3;
2840
- process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
2841
- `);
2552
+ process.stderr.write(
2553
+ `[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${delayMs}ms
2554
+ `
2555
+ );
2842
2556
  const reconnect = () => {
2843
2557
  void startWorker(options).catch((error) => {
2844
2558
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
2845
2559
  `);
2846
- if (activeProcesses.size > 0) {
2560
+ if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
2847
2561
  setTimeout(reconnect, delayMs);
2848
2562
  return;
2849
2563
  }
@@ -2898,7 +2612,6 @@ if (isCliEntrypoint()) {
2898
2612
  }
2899
2613
  export {
2900
2614
  ensureVisibleGitCheckout,
2901
- findRepositorySyncBlockers,
2902
2615
  findWorkerFiles,
2903
2616
  githubCliEnv,
2904
2617
  grepWorkerFiles,
@@ -2911,7 +2624,6 @@ export {
2911
2624
  resolveHostShell,
2912
2625
  resolveProjectFilePath,
2913
2626
  resolveWorkerFilePath,
2914
- syncManifestProjectsFromInternal,
2915
2627
  syncProjectPlans,
2916
2628
  syncSessionArtifacts
2917
2629
  };