@ricsam/r5d-worker 0.0.73 → 0.0.75

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.
Files changed (32) hide show
  1. package/README.md +6 -4
  2. package/dist/cjs/main.cjs +1022 -976
  3. package/dist/cjs/package.json +1 -1
  4. package/dist/cjs/project-workspace-state.cjs +777 -0
  5. package/dist/cjs/project-worktrees.cjs +505 -0
  6. package/dist/cjs/working-tree-mirror.cjs +225 -0
  7. package/dist/cjs/workspace-git-sync.cjs +565 -0
  8. package/dist/cjs/workspace-incident-state.cjs +2 -38
  9. package/dist/mjs/main.mjs +1038 -987
  10. package/dist/mjs/package.json +1 -1
  11. package/dist/mjs/project-workspace-state.mjs +745 -0
  12. package/dist/mjs/project-worktrees.mjs +461 -0
  13. package/dist/mjs/working-tree-mirror.mjs +190 -0
  14. package/dist/mjs/workspace-git-sync.mjs +524 -0
  15. package/dist/mjs/workspace-incident-state.mjs +1 -35
  16. package/dist/types/main.d.ts +35 -55
  17. package/dist/types/project-workspace-state.d.ts +144 -0
  18. package/dist/types/project-worktrees.d.ts +101 -0
  19. package/dist/types/working-tree-mirror.d.ts +24 -0
  20. package/dist/types/workspace-git-sync.d.ts +97 -0
  21. package/dist/types/workspace-incident-state.d.ts +0 -17
  22. package/dist/types/workspace-mutation-gate.d.ts +3 -3
  23. package/package.json +1 -1
  24. package/dist/cjs/workspace-convergence.cjs +0 -280
  25. package/dist/cjs/workspace-manifest-admission.cjs +0 -60
  26. package/dist/cjs/workspace-sync.cjs +0 -2469
  27. package/dist/mjs/workspace-convergence.mjs +0 -236
  28. package/dist/mjs/workspace-manifest-admission.mjs +0 -26
  29. package/dist/mjs/workspace-sync.mjs +0 -2417
  30. package/dist/types/workspace-convergence.d.ts +0 -93
  31. package/dist/types/workspace-manifest-admission.d.ts +0 -22
  32. package/dist/types/workspace-sync.d.ts +0 -167
package/dist/mjs/main.mjs CHANGED
@@ -5,7 +5,6 @@ import { createHash } from "node:crypto";
5
5
  import os, { hostname } from "node:os";
6
6
  import { spawn as spawnChildProcess } from "node:child_process";
7
7
  import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
8
- import { configureVisibleGitIdentity } from "./git-identity.mjs";
9
8
  import {
10
9
  grantWorkerHeartbeatBusyGrace,
11
10
  hasWorkerHeartbeatTimedOutWithBusyGrace,
@@ -14,20 +13,9 @@ import {
14
13
  import { terminateProcessTree } from "./process-tree.mjs";
15
14
  import { createPtyOutputCoalescer } from "./pty-output-coalescer.mjs";
16
15
  import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
17
- import { applyWorkspaceIncidentUpdate, releasePendingWorkspaceHead, WorkspaceIncidentOrderingFence } from "./workspace-incident-state.mjs";
16
+ import { applyWorkspaceIncidentUpdate } from "./workspace-incident-state.mjs";
18
17
  import { acquireWorkspaceCommandMutation, runWorkspaceCommand } from "./workspace-command-sync-policy.mjs";
19
- import {
20
- WORKSPACE_PUBLICATION_QUIET_MS,
21
- WORKSPACE_IDLE_SAFETY_SCAN_MS,
22
- ExplicitWorkspacePublicationQueue,
23
- WorkspaceManifestRevisionFence,
24
- processWorkspaceConvergenceState,
25
- waitForWorkspacePublicationOnShutdown,
26
- workspaceBootstrapEstablishesVisibleBaseline,
27
- workspacePublicationIncidentAction,
28
- workspacePublicationTelemetry
29
- } from "./workspace-convergence.mjs";
30
- import { inspectWorkspaceManifestFilesystem } from "./workspace-manifest-admission.mjs";
18
+ import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
31
19
  import {
32
20
  isRetryableWorkerServerStatus,
33
21
  superviseWorkerRuntime,
@@ -38,11 +26,26 @@ import {
38
26
  } from "./supervisor.mjs";
39
27
  import { managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
40
28
  import {
41
- WorkspaceSyncSingleFlight,
42
- convergeWorkspaceHead,
43
- synchronizeWorkspace,
44
- workspaceProjectsForSync
45
- } from "./workspace-sync.mjs";
29
+ createLinkedProjectBranch,
30
+ deleteLinkedProjectBranch,
31
+ deleteProjectMirrorBranch,
32
+ ensureProjectWorktrees,
33
+ fastForwardProjectHeadsFromMirror,
34
+ projectWorktreeConfigurationFingerprint,
35
+ projectWorktreeOperationInProgress,
36
+ pushProjectMirrorHeads,
37
+ removeProjectWorktrees
38
+ } from "./project-worktrees.mjs";
39
+ import {
40
+ ProjectWorkspaceStateStore,
41
+ pruneAuthoritativelyDesiredBranchDeletions
42
+ } from "./project-workspace-state.mjs";
43
+ import {
44
+ ensureWorkspaceGitClone,
45
+ hydrateWorkspaceGitMounts,
46
+ resetWorkspaceGit,
47
+ synchronizeWorkspaceGit
48
+ } from "./workspace-git-sync.mjs";
46
49
  class WorkerServerUnavailableError extends Error {
47
50
  name = "WorkerServerUnavailableError";
48
51
  }
@@ -52,12 +55,34 @@ const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
52
55
  const DEFAULT_READ_LIMIT = 2e3;
53
56
  const DEFAULT_READ_MAX_BYTES = 5e4;
54
57
  const MAX_LINE_LENGTH = 2e3;
58
+ const WORKSPACE_GIT_QUIET_MS = 5e3;
59
+ const WORKSPACE_GIT_PERIODIC_MS = 6e4;
55
60
  const activeProcesses = /* @__PURE__ */ new Map();
56
61
  const pendingProcessTerminals = /* @__PURE__ */ new Map();
57
62
  const cancelledProcessRuns = /* @__PURE__ */ new Set();
58
63
  const activePtys = /* @__PURE__ */ new Map();
59
64
  let currentWorkerSocket = null;
60
- const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
65
+ const workspaceMutationGate = new WorkspaceMutationGate();
66
+ let workspaceSyncQueue = Promise.resolve();
67
+ const workspaceSyncSingleFlight = {
68
+ runExclusive(operation) {
69
+ const queued = workspaceMutationGate.runSync(operation);
70
+ workspaceSyncQueue = queued.then(
71
+ () => void 0,
72
+ () => void 0
73
+ );
74
+ return queued;
75
+ },
76
+ runMutation(operation) {
77
+ return workspaceMutationGate.runMutation(operation);
78
+ },
79
+ acquireMutation() {
80
+ return workspaceMutationGate.acquireMutation();
81
+ },
82
+ afterCurrent() {
83
+ return workspaceSyncQueue;
84
+ }
85
+ };
61
86
  let githubCredential = null;
62
87
  let visibleGitIdentity = null;
63
88
  function defaultConfigPath() {
@@ -620,20 +645,11 @@ function gitAuthArgs(auth) {
620
645
  const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
621
646
  return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
622
647
  }
623
- const visibleCheckoutGitTestHarness = {
624
- gitAuthArgs,
625
- commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args],
626
- cloneArgs: (remoteUrl, branchPath) => ["clone", "--no-recurse-submodules", "--origin", "origin", remoteUrl, branchPath],
627
- fetchArgs: (...args) => ["fetch", "--no-recurse-submodules", ...args]
648
+ const workerGitCommand = {
649
+ commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args]
628
650
  };
629
- function visibleCheckoutCloneArgs(remoteUrl, branchPath) {
630
- return visibleCheckoutGitTestHarness.cloneArgs(remoteUrl, branchPath);
631
- }
632
- function visibleCheckoutFetchArgs(...args) {
633
- return visibleCheckoutGitTestHarness.fetchArgs(...args);
634
- }
635
651
  function runGit(args, options = {}) {
636
- const command = visibleCheckoutGitTestHarness.commandArgs(args, options.auth);
652
+ const command = workerGitCommand.commandArgs(args, options.auth);
637
653
  const result = Bun.spawnSync(command, {
638
654
  cwd: options.cwd,
639
655
  stdout: "pipe",
@@ -662,9 +678,6 @@ function getGitBlobHashForContent(content) {
662
678
  const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
663
679
  return createHash("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
664
680
  }
665
- function hasWorktreeChanges(cwd) {
666
- return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
667
- }
668
681
  function isInsideBranchPath(branchPath, filePath) {
669
682
  const relative = path.relative(path.resolve(branchPath), path.resolve(filePath));
670
683
  return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
@@ -774,33 +787,9 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
774
787
  scope: "project"
775
788
  };
776
789
  }
777
- function fallbackInternalRemoteUrl(baseUrl, projectId) {
790
+ function canonicalProjectRemoteUrl(baseUrl, projectId) {
778
791
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
779
792
  }
780
- function visibleCheckoutRemoteFor(input) {
781
- const canonicalCheckout = input.manifest?.canonicalCheckouts?.find((checkout) => checkout.branchName === input.branchName);
782
- if (!canonicalCheckout && input.manifest) {
783
- if (!input.manifest.repoHttpUrl) {
784
- throw new Error(`Project ${input.projectId} has no connected repository; connect one before opening its checkouts`);
785
- }
786
- return {
787
- remoteUrl: input.manifest.repoHttpUrl,
788
- authHeader: input.manifest.repoAuthHeader ?? null,
789
- persistAuth: true,
790
- reconcileExistingOrigin: true,
791
- requireRemoteBranch: false,
792
- requiredBaseCommit: null
793
- };
794
- }
795
- return {
796
- remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
797
- authHeader: `Authorization: Bearer ${input.token}`,
798
- persistAuth: false,
799
- reconcileExistingOrigin: true,
800
- requireRemoteBranch: true,
801
- requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
802
- };
803
- }
804
793
  function gitExtraHeaderUrlForRemote(remoteUrl) {
805
794
  try {
806
795
  return new URL(remoteUrl).origin;
@@ -817,44 +806,25 @@ function gitAuthForRemote(remoteUrl, authHeader) {
817
806
  header: authHeader
818
807
  };
819
808
  }
820
- function hasNormalVisibleGitDir(branchPath) {
821
- const gitPath = path.join(branchPath, ".git");
822
- return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
823
- }
824
- function ensureOriginRemote(branchPath, remoteUrl) {
825
- if (tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) {
826
- runGit(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
827
- } else {
828
- runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
829
- }
830
- }
831
- function originRemoteUrl(branchPath) {
832
- if (!tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) return null;
833
- return runGit(["remote", "get-url", "origin"], { cwd: branchPath });
834
- }
835
809
  function shellQuote(value) {
836
810
  return `'${value.replace(/'/g, "'\\''")}'`;
837
811
  }
838
812
  function githubCredentialsPath() {
839
813
  return path.join(os.homedir(), ".r5d", "github-credentials");
840
814
  }
841
- function decodeBasicAuthHeader(authHeader) {
815
+ function decodeGitAuthHeader(authHeader) {
842
816
  const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
843
- if (!match) {
844
- return null;
845
- }
846
- const decoded = Buffer.from(match[1], "base64").toString("utf8");
847
- const separatorIndex = decoded.indexOf(":");
848
- if (separatorIndex <= 0) {
849
- return null;
817
+ if (match) {
818
+ const decoded = Buffer.from(match[1], "base64").toString("utf8");
819
+ const separatorIndex = decoded.indexOf(":");
820
+ if (separatorIndex <= 0) return null;
821
+ return { username: decoded.slice(0, separatorIndex), password: decoded.slice(separatorIndex + 1) };
850
822
  }
851
- return {
852
- username: decoded.slice(0, separatorIndex),
853
- password: decoded.slice(separatorIndex + 1)
854
- };
823
+ const bearer = /^Authorization:\s*Bearer\s+(.+)$/i.exec(authHeader.trim());
824
+ return bearer?.[1] ? { username: "r5d-worker", password: bearer[1] } : null;
855
825
  }
856
826
  function writeCredentialStoreEntry(remoteUrl, authHeader) {
857
- const credentials = decodeBasicAuthHeader(authHeader);
827
+ const credentials = decodeGitAuthHeader(authHeader);
858
828
  if (!credentials) {
859
829
  return null;
860
830
  }
@@ -888,29 +858,9 @@ function writeCredentialStoreEntry(remoteUrl, authHeader) {
888
858
  fs.chmodSync(storePath, 384);
889
859
  return storePath;
890
860
  }
891
- function configureVisibleGitHubAuth(branchPath, remoteUrl, authHeader) {
892
- if (!authHeader) {
893
- return;
894
- }
861
+ function credentialHelperForRemote(remoteUrl, authHeader) {
895
862
  const credentialStore = writeCredentialStoreEntry(remoteUrl, authHeader);
896
- if (credentialStore) {
897
- runGit(["config", "credential.helper", `store --file=${shellQuote(credentialStore)}`], { cwd: branchPath });
898
- runGit(["config", "credential.useHttpPath", "false"], { cwd: branchPath });
899
- return;
900
- }
901
- runGit(["config", `${gitExtraHeaderConfigKey(gitExtraHeaderUrlForRemote(remoteUrl))}`, authHeader], { cwd: branchPath });
902
- }
903
- function clearVisibleGitAuthentication(branchPath) {
904
- let extraHeaderKeys = [];
905
- try {
906
- extraHeaderKeys = runGit(["config", "--local", "--name-only", "--get-regexp", "^http\\..*extraheader$"], { cwd: branchPath }).split(/\r?\n/).filter(Boolean);
907
- } catch {
908
- }
909
- for (const key of extraHeaderKeys) {
910
- tryGit(["config", "--local", "--unset-all", key], { cwd: branchPath });
911
- }
912
- runGit(["config", "--local", "--replace-all", "credential.helper", ""], { cwd: branchPath });
913
- tryGit(["config", "--local", "--unset-all", "credential.useHttpPath"], { cwd: branchPath });
863
+ return credentialStore ? `store --file=${shellQuote(credentialStore)}` : null;
914
864
  }
915
865
  function dockerConfigPath() {
916
866
  return path.join(os.homedir(), ".docker", "config.json");
@@ -990,188 +940,26 @@ function githubProcessEnv() {
990
940
  ...githubCliEnv(githubCredential?.token)
991
941
  };
992
942
  }
993
- function checkoutVisibleBranch(input) {
994
- const branchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`], { cwd: input.branchPath });
995
- const remoteBranchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], {
996
- cwd: input.branchPath
997
- });
998
- const defaultRemoteExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.defaultBranch}`], {
999
- cwd: input.branchPath
1000
- });
1001
- const clean = !hasWorktreeChanges(input.branchPath);
1002
- if (remoteBranchExists && clean) {
1003
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
1004
- cwd: input.branchPath
1005
- });
1006
- return;
1007
- }
1008
- if (branchExists) {
1009
- runGit(["checkout", "--no-recurse-submodules", input.branchName], { cwd: input.branchPath });
1010
- return;
1011
- }
1012
- if (defaultRemoteExists) {
1013
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.defaultBranch}`], {
1014
- cwd: input.branchPath
1015
- });
1016
- return;
1017
- }
1018
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName], { cwd: input.branchPath });
1019
- }
1020
- function reconcileRewrittenCanonicalHistory(input) {
1021
- const remoteRef = `refs/remotes/origin/${input.branchName}`;
1022
- if (!tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) return;
1023
- if (!tryGit(["show-ref", "--verify", "--quiet", remoteRef], { cwd: input.branchPath })) return;
1024
- if (tryGit(["merge-base", "HEAD", remoteRef], { cwd: input.branchPath })) return;
1025
- if (hasWorktreeChanges(input.branchPath)) return;
1026
- const localTree = runGit(["rev-parse", "HEAD^{tree}"], { cwd: input.branchPath });
1027
- const remoteTree = runGit(["rev-parse", `${remoteRef}^{tree}`], { cwd: input.branchPath });
1028
- if (localTree !== remoteTree) return;
1029
- const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1030
- runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1031
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, remoteRef], { cwd: input.branchPath });
1032
- }
1033
- function migrateExistingCheckoutToRequiredRemote(input) {
1034
- if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
1035
- const dirtyWorkingTree = hasWorktreeChanges(input.branchPath);
1036
- if (dirtyWorkingTree && input.requiredBaseCommit) {
1037
- throw new Error(`Cannot migrate dirty checkout for ${input.branchName} to its canonical remote`);
1038
- }
1039
- const preserveWorkingTree = dirtyWorkingTree;
1040
- runGit(
1041
- visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1042
- {
1043
- cwd: input.branchPath,
1044
- auth: input.remoteAuth
1045
- }
1046
- );
1047
- const canonicalRemoteRef = `refs/remotes/origin/${input.branchName}`;
1048
- if (!tryGit(["show-ref", "--verify", "--quiet", canonicalRemoteRef], { cwd: input.branchPath })) {
1049
- throw new Error(`Canonical branch ${input.branchName} is unavailable during checkout migration`);
1050
- }
1051
- if (input.requiredBaseCommit && (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, canonicalRemoteRef], { cwd: input.branchPath }))) {
1052
- throw new Error(`Canonical branch ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1053
- }
1054
- ensureOriginRemote(input.branchPath, input.remoteUrl);
1055
- if (preserveWorkingTree) return;
1056
- if (tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) {
1057
- const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1058
- runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1059
- }
1060
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
1061
- cwd: input.branchPath
1062
- });
943
+ function primaryProjectBranch(config) {
944
+ if (config.branches.length === 0) throw new Error(`Project ${config.projectId} has no configured branches`);
945
+ if (config.branches.some(({ branchName }) => branchName === "main")) return "main";
946
+ if (config.branches.some(({ branchName }) => branchName === config.defaultBranch)) return config.defaultBranch;
947
+ return config.branches[0].branchName;
1063
948
  }
1064
- function assertCheckoutDerivedFromRequiredBase(input) {
1065
- const remoteRevision = `refs/remotes/origin/${input.branchName}`;
1066
- for (const revision of [remoteRevision, "HEAD"]) {
1067
- if (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, revision], { cwd: input.branchPath })) {
1068
- throw new Error(`Canonical checkout ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1069
- }
1070
- }
949
+ function configuredProjectRoot(projectsRoot, config) {
950
+ return managedProjectRoot(projectsRoot, config.checkoutPathSegments);
1071
951
  }
1072
- function ensureVisibleGitCheckout(input) {
1073
- validateBranchName(input.branchName);
1074
- fs.mkdirSync(input.projectRoot, { recursive: true });
1075
- const branchPath = path.join(input.projectRoot, input.branchName);
1076
- const createdCheckout = !hasNormalVisibleGitDir(branchPath);
1077
- if (createdCheckout) {
1078
- fs.rmSync(branchPath, { recursive: true, force: true });
1079
- fs.mkdirSync(path.dirname(branchPath), { recursive: true });
1080
- const cloned = tryGit(visibleCheckoutCloneArgs(input.remoteUrl, branchPath), {
1081
- auth: input.remoteAuth
1082
- });
1083
- if (!cloned && input.requireRemoteBranch) {
1084
- fs.rmSync(branchPath, { recursive: true, force: true });
1085
- throw new Error(`Failed to clone canonical checkout for branch ${input.branchName}`);
1086
- }
1087
- if (!cloned) {
1088
- fs.mkdirSync(branchPath, { recursive: true });
1089
- runGit(["init"], { cwd: branchPath });
1090
- }
1091
- ensureOriginRemote(branchPath, input.remoteUrl);
1092
- if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
1093
- else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
1094
- configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1095
- tryGit(visibleCheckoutFetchArgs("origin", "--prune"), { cwd: branchPath, auth: input.remoteAuth });
1096
- if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1097
- fs.rmSync(branchPath, { recursive: true, force: true });
1098
- throw new Error(`Canonical branch ${input.branchName} is unavailable after clone`);
1099
- }
1100
- checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1101
- if (input.requiredBaseCommit) {
1102
- try {
1103
- assertCheckoutDerivedFromRequiredBase({
1104
- branchPath,
1105
- branchName: input.branchName,
1106
- requiredBaseCommit: input.requiredBaseCommit
1107
- });
1108
- } catch (error) {
1109
- fs.rmSync(branchPath, { recursive: true, force: true });
1110
- throw error;
1111
- }
1112
- }
1113
- return branchPath;
1114
- }
1115
- if (input.requireRemoteBranch) {
1116
- if (input.allowRequiredRemoteMigration === false && originRemoteUrl(branchPath) !== input.remoteUrl) {
1117
- throw new Error(`Canonical checkout ${input.branchName} no longer uses its required remote`);
1118
- }
1119
- migrateExistingCheckoutToRequiredRemote({
1120
- branchPath,
1121
- branchName: input.branchName,
1122
- remoteUrl: input.remoteUrl,
1123
- remoteAuth: input.remoteAuth,
1124
- requiredBaseCommit: input.requiredBaseCommit
1125
- });
1126
- runGit(
1127
- visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1128
- {
1129
- cwd: branchPath,
1130
- auth: input.remoteAuth
1131
- }
1132
- );
1133
- }
1134
- if (input.reconcileExistingOrigin) {
1135
- ensureOriginRemote(branchPath, input.remoteUrl);
1136
- }
1137
- if (!input.requireRemoteBranch) {
1138
- tryGit(visibleCheckoutFetchArgs("origin", "--prune"), { cwd: branchPath, auth: input.remoteAuth });
1139
- }
1140
- if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1141
- throw new Error(`Existing checkout for ${input.branchName} does not contain its canonical remote branch`);
1142
- }
1143
- if (input.requireRemoteBranch) {
1144
- reconcileRewrittenCanonicalHistory({ branchPath, branchName: input.branchName });
1145
- }
1146
- if (input.requiredBaseCommit) {
1147
- assertCheckoutDerivedFromRequiredBase({
1148
- branchPath,
1149
- branchName: input.branchName,
1150
- requiredBaseCommit: input.requiredBaseCommit
1151
- });
1152
- }
1153
- if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
1154
- else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
1155
- configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1156
- return branchPath;
952
+ function configuredProjectBranchPath(projectsRoot, config, branchName) {
953
+ validateBranchName(branchName);
954
+ return path.join(configuredProjectRoot(projectsRoot, config), ...branchName.split("/"));
1157
955
  }
1158
- function ensureBranchWorktree(input) {
1159
- const defaultBranch = input.manifest?.defaultBranch || "main";
1160
- const remote = visibleCheckoutRemoteFor(input);
1161
- return {
1162
- branchPath: ensureVisibleGitCheckout({
1163
- projectRoot: input.projectRoot,
1164
- branchName: input.branchName,
1165
- remoteUrl: remote.remoteUrl,
1166
- remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
1167
- persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
1168
- defaultBranch,
1169
- reconcileExistingOrigin: remote.reconcileExistingOrigin,
1170
- requireRemoteBranch: remote.requireRemoteBranch,
1171
- requiredBaseCommit: remote.requiredBaseCommit,
1172
- clearPersistentAuth: !remote.persistAuth
1173
- })
1174
- };
956
+ function hasProjectWorktree(checkoutPath) {
957
+ try {
958
+ const stat = fs.lstatSync(path.join(checkoutPath, ".git"));
959
+ return stat.isFile() || stat.isDirectory();
960
+ } catch {
961
+ return false;
962
+ }
1175
963
  }
1176
964
  function describeWorkerSessionTarget(target) {
1177
965
  return target.type === "project" ? `${target.projectId}/${target.branchName}` : `${target.ownerUserId}/${target.rootProfile}`;
@@ -1182,40 +970,21 @@ function resolveWorkerSessionTarget(input) {
1182
970
  return { target: input.target, rootPath: input.projectsRoot };
1183
971
  }
1184
972
  if (!fs.existsSync(path.join(input.workspaceShadowRoot, ".git"))) {
1185
- throw new Error("The canonical workspace checkout is not initialized on this worker");
973
+ throw new Error("The workspace sync checkout is not initialized on this worker");
1186
974
  }
1187
975
  return { target: input.target, rootPath: input.workspaceShadowRoot };
1188
976
  }
1189
- const manifest = input.manifestByProjectId.get(input.target.projectId);
1190
- const projectRoot = projectRootFor(input.projectsRoot, input.target.projectId, input.manifestByProjectId);
1191
- const worktree = ensureBranchWorktree({
1192
- projectId: input.target.projectId,
1193
- baseUrl: input.baseUrl,
1194
- token: input.token,
1195
- projectRoot,
1196
- syncRoot: input.syncRoot,
1197
- branchName: input.target.branchName,
1198
- manifest
1199
- });
1200
- return { target: input.target, rootPath: worktree.branchPath, manifest };
1201
- }
1202
- function workspaceSyncCheckoutTargets(input) {
1203
- const selected = /* @__PURE__ */ new Map();
1204
- const add = (target) => {
1205
- selected.set(`${target.projectId}\0${target.branchName}`, target);
1206
- };
1207
- if (input.canonicalCheckoutOnly) {
1208
- add(input.canonicalCheckoutOnly);
1209
- } else if (input.includeAllManifestCheckouts) {
1210
- for (const project of input.projects) {
1211
- for (const branchName of project.branches) add({ projectId: project.projectId, branchName });
1212
- }
1213
- } else {
1214
- for (const target of input.pendingTargets) add(target);
977
+ const target = input.target;
978
+ const config = input.projectConfigById.get(target.projectId);
979
+ if (!config) throw new Error(`Project ${target.projectId} is missing from the worker workspace configuration`);
980
+ if (!config.branches.some(({ branchName }) => branchName === target.branchName)) {
981
+ throw new Error(`Project branch ${target.projectId}/${target.branchName} is missing from the worker workspace configuration`);
1215
982
  }
1216
- return [...selected.values()].sort(
1217
- (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
1218
- );
983
+ const branchPath = configuredProjectBranchPath(input.projectsRoot, config, target.branchName);
984
+ if (!hasProjectWorktree(branchPath)) {
985
+ throw new Error(`Project branch ${target.projectId}/${target.branchName} is not initialized on this worker`);
986
+ }
987
+ return { target, rootPath: branchPath, config };
1219
988
  }
1220
989
  function resolveCommandCwd(branchPath, cwd) {
1221
990
  if (!cwd) {
@@ -1739,6 +1508,77 @@ async function executeLsOperation(input) {
1739
1508
  });
1740
1509
  return listWorkerDirectory(input.resolvedTarget.rootPath, input.message.path, input.message.limit, builtInPaths);
1741
1510
  }
1511
+ function resolveWorkerCodePath(branchPath, inputPath) {
1512
+ if (typeof inputPath !== "string" || !inputPath.startsWith("/") || inputPath.includes("\0")) {
1513
+ throw new Error("Code path must be an absolute virtual path");
1514
+ }
1515
+ const rawSegments = inputPath.replace(/\\/g, "/").split("/");
1516
+ if (rawSegments.includes("..")) throw new Error(`Invalid code path: ${inputPath}`);
1517
+ const displayPath = path.posix.normalize(inputPath.replace(/\\/g, "/"));
1518
+ const relativePath = displayPath.replace(/^\/+/, "");
1519
+ const pathSegments = relativePath ? relativePath.split("/") : [];
1520
+ if (pathSegments.some((segment) => segment.toLowerCase() === ".git")) {
1521
+ throw new Error("Code paths cannot access Git metadata");
1522
+ }
1523
+ const resolvedBranchPath = path.resolve(branchPath);
1524
+ const absolutePath = path.resolve(resolvedBranchPath, ...pathSegments);
1525
+ assertInsideRoot(resolvedBranchPath, absolutePath, "Code path");
1526
+ if (!fs.existsSync(absolutePath)) throw new Error(`Code path not found: ${displayPath}`);
1527
+ const realBranchPath = fs.realpathSync(resolvedBranchPath);
1528
+ const realPath = fs.realpathSync(absolutePath);
1529
+ assertInsideRoot(realBranchPath, realPath, "Code path");
1530
+ const realRelativePath = path.relative(realBranchPath, realPath).split(path.sep);
1531
+ if (realRelativePath.some((segment) => segment.toLowerCase() === ".git")) {
1532
+ throw new Error("Code paths cannot access Git metadata");
1533
+ }
1534
+ return { absolutePath, displayPath };
1535
+ }
1536
+ function listWorkerCodeDirectory(branchPath, inputPath) {
1537
+ const resolved = resolveWorkerCodePath(branchPath, inputPath);
1538
+ if (!fs.statSync(resolved.absolutePath).isDirectory()) {
1539
+ throw new Error(`Code directory not found: ${resolved.displayPath}`);
1540
+ }
1541
+ const entries = fs.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => entry.name.toLowerCase() !== ".git").flatMap((entry) => {
1542
+ const entryPath = path.posix.join(resolved.displayPath, entry.name);
1543
+ try {
1544
+ const resolvedEntry = resolveWorkerCodePath(branchPath, entryPath);
1545
+ const stats = fs.statSync(resolvedEntry.absolutePath);
1546
+ if (!stats.isFile() && !stats.isDirectory()) return [];
1547
+ return [
1548
+ {
1549
+ name: entry.name,
1550
+ type: stats.isDirectory() ? "directory" : "file",
1551
+ size: stats.isDirectory() ? null : stats.size,
1552
+ mtime: stats.mtime.toISOString()
1553
+ }
1554
+ ];
1555
+ } catch {
1556
+ return [];
1557
+ }
1558
+ }).sort((left, right) => Number(right.type === "directory") - Number(left.type === "directory") || left.name.localeCompare(right.name));
1559
+ return { type: "code_list", path: resolved.displayPath, entries };
1560
+ }
1561
+ function readWorkerCodeFile(branchPath, inputPath) {
1562
+ const resolved = resolveWorkerCodePath(branchPath, inputPath);
1563
+ const stats = fs.statSync(resolved.absolutePath);
1564
+ if (!stats.isFile()) throw new Error(`Code file not found: ${resolved.displayPath}`);
1565
+ const bytes = fs.readFileSync(resolved.absolutePath);
1566
+ return {
1567
+ type: "code_read",
1568
+ path: resolved.displayPath,
1569
+ size: bytes.length,
1570
+ mtime: stats.mtime.toISOString(),
1571
+ base64: bytes.toString("base64")
1572
+ };
1573
+ }
1574
+ function executeCodeListOperation(input) {
1575
+ if (input.resolvedTarget.target.type !== "project") throw new Error("Code listing requires a project branch target");
1576
+ return listWorkerCodeDirectory(input.resolvedTarget.rootPath, input.message.path);
1577
+ }
1578
+ function executeCodeReadOperation(input) {
1579
+ if (input.resolvedTarget.target.type !== "project") throw new Error("Code reading requires a project branch target");
1580
+ return readWorkerCodeFile(input.resolvedTarget.rootPath, input.message.path);
1581
+ }
1742
1582
  function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1743
1583
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1744
1584
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
@@ -1791,6 +1631,10 @@ async function executeOperation(input) {
1791
1631
  return executeLsOperation({ ...input, message: input.message });
1792
1632
  case "view_file_bytes":
1793
1633
  return executeViewFileBytesOperation({ ...input, message: input.message });
1634
+ case "code_list":
1635
+ return executeCodeListOperation({ ...input, message: input.message });
1636
+ case "code_read":
1637
+ return executeCodeReadOperation({ ...input, message: input.message });
1794
1638
  }
1795
1639
  }
1796
1640
  function targetIdentityEnv(target) {
@@ -2390,19 +2234,16 @@ function websocketUrl(baseUrl, label) {
2390
2234
  url.searchParams.set("version", getWorkerVersion());
2391
2235
  return url.toString();
2392
2236
  }
2393
- function projectRootFor(projectsRoot, projectId, manifestByProjectId) {
2394
- const manifest = manifestByProjectId.get(projectId);
2395
- if (!manifest) throw new Error(`Project ${projectId} is missing from the worker workspace manifest`);
2396
- return managedProjectRoot(projectsRoot, manifest.checkoutPathSegments);
2397
- }
2398
2237
  async function startWorker(options) {
2399
2238
  process.on("uncaughtException", (error) => {
2400
2239
  process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
2401
2240
  `);
2402
2241
  });
2403
2242
  process.on("unhandledRejection", (reason) => {
2404
- process.stderr.write(`[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
2405
- `);
2243
+ process.stderr.write(
2244
+ `[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
2245
+ `
2246
+ );
2406
2247
  });
2407
2248
  const config = readConfig(options.configPath);
2408
2249
  const { baseUrl, token } = resolveCredentials(options, config);
@@ -2431,402 +2272,785 @@ async function startWorker(options) {
2431
2272
  fs.mkdirSync(syncRoot, { recursive: true });
2432
2273
  fs.mkdirSync(artifactRoot, { recursive: true });
2433
2274
  fs.mkdirSync(planRoot, { recursive: true });
2434
- const manifestByProjectId = /* @__PURE__ */ new Map();
2435
- const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2275
+ const projectWorkspaceStateStore = new ProjectWorkspaceStateStore({
2276
+ stateRoot: path.join(syncRoot, "project-state"),
2277
+ projectsRoot
2278
+ });
2279
+ let projectWorkspaceState = projectWorkspaceStateStore.read();
2280
+ const projectConfigById = /* @__PURE__ */ new Map();
2281
+ const pendingCheckouts = /* @__PURE__ */ new Map();
2282
+ const readyProjectIds = /* @__PURE__ */ new Set();
2283
+ const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
2284
+ const lastObservedProjectHeads = /* @__PURE__ */ new Map();
2285
+ const pendingMirrorDeletes = /* @__PURE__ */ new Map();
2436
2286
  let workspaceRemoteUrl = null;
2287
+ let workspaceGitIdentity = null;
2288
+ let workspaceConfigured = false;
2437
2289
  let activeWorkspaceIncidentId = null;
2438
- const workspaceIncidentOrdering = new WorkspaceIncidentOrderingFence();
2439
- const workspaceConvergence = processWorkspaceConvergenceState(path.join(syncRoot, "workspace-convergence.json"));
2440
- const workspaceManifestRevision = new WorkspaceManifestRevisionFence();
2441
- let workspaceReady = false;
2442
- let workspacePublicationTimer;
2443
- let workspaceSafetyScan;
2444
- let workspaceManifestHydrationTimer;
2445
- let workspaceManifestHydrationInFlight = false;
2446
- let workspaceSafetyScanInFlight = false;
2447
- let workspaceHeadConvergenceInFlight = false;
2448
- let pendingWorkspaceHead;
2449
- let pendingPublication;
2450
- const explicitWorkspacePublications = new ExplicitWorkspacePublicationQueue();
2290
+ let workspaceAutomaticTimer;
2291
+ let workspacePeriodicTimer;
2292
+ let pendingAutomaticTrigger;
2293
+ let automaticSyncInFlight = false;
2451
2294
  let terminalReplayTimer;
2452
2295
  let workspaceSyncRequestsInFlight = 0;
2453
2296
  let sessionArtifactSyncRequestsInFlight = 0;
2454
2297
  let cliUpdateInProgress = false;
2455
2298
  let reloadAfterClose = false;
2456
2299
  let shutdownAfterClose = false;
2457
- const workspaceSyncInput = (trigger, overrides = {}) => {
2458
- if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2459
- const projects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
2300
+ const bearerAuthHeader = `Authorization: Bearer ${token}`;
2301
+ const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2302
+ const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
2303
+ const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
2304
+ const workspaceProjectRelativePath = (projectId, branchName) => path.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
2305
+ const workspacePlanRelativePath = (projectId, branchName) => path.posix.join("plans", projectId, encodeURIComponent(branchName));
2306
+ const projectConnection = (project) => {
2307
+ const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
2308
+ const mirrorAuth = gitAuthForRemote(mirrorUrl, bearerAuthHeader);
2309
+ const originUrl = project.repoHttpUrl ?? mirrorUrl;
2310
+ const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
2311
+ const originAuth = gitAuthForRemote(originUrl, originHeader);
2312
+ const credentialHelper = originHeader ? credentialHelperForRemote(originUrl, originHeader) : null;
2313
+ return { mirrorUrl, mirrorAuth, originUrl, originAuth, credentialHelper };
2314
+ };
2315
+ const durableDeletionForBranch = (projectId, checkoutPathSegments, branchName) => projectWorkspaceState.tombstones.find(
2316
+ (tombstone) => tombstone.projectId === projectId && tombstone.checkoutPathSegments[0] === checkoutPathSegments[0] && tombstone.checkoutPathSegments[1] === checkoutPathSegments[1] && (tombstone.kind === "project" || tombstone.branchName === branchName)
2317
+ );
2318
+ const stageProjectBranchDeletion = (project, branchName, requireLocalBranch) => {
2319
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
2320
+ const primaryBranchName = primaryProjectBranch(project);
2321
+ const connection = projectConnection(project);
2322
+ const sourcePath = configuredProjectBranchPath(projectsRoot, project, branchName);
2323
+ if (requireLocalBranch || hasProjectWorktree(sourcePath)) {
2324
+ deleteLinkedProjectBranch({ projectRoot, primaryBranchName, branchName });
2325
+ }
2326
+ const planSourcePath = path.join(planRoot, project.projectId, ...branchName.split("/"));
2327
+ fs.rmSync(planSourcePath, { recursive: true, force: true });
2328
+ const key = projectBranchKey(project.projectId, branchName);
2329
+ const durableDeletion = durableDeletionForBranch(project.projectId, project.checkoutPathSegments, branchName);
2330
+ pendingCheckouts.delete(key);
2331
+ pendingMirrorDeletes.set(key, {
2332
+ id: projectMountId(project.projectId, branchName),
2333
+ projectId: project.projectId,
2334
+ branchName,
2335
+ gitDirectory: configuredProjectBranchPath(projectsRoot, project, primaryBranchName),
2336
+ mirrorUrl: connection.mirrorUrl,
2337
+ mirrorAuth: connection.mirrorAuth,
2338
+ sourcePath,
2339
+ workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branchName),
2340
+ planSourcePath,
2341
+ planWorkspaceRelativePath: workspacePlanRelativePath(project.projectId, branchName),
2342
+ ...durableDeletion ? { tombstoneId: durableDeletion.id } : {},
2343
+ ...durableDeletion?.kind === "project" ? { projectDeleted: !projectWorkspaceState.desiredProjects.some(({ projectId }) => projectId === project.projectId) } : {}
2344
+ });
2345
+ };
2346
+ const stageProjectDeletion = (project) => {
2347
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
2348
+ const connection = projectConnection(project);
2349
+ const branches = project.branches.map(({ branchName }) => ({
2350
+ branchName,
2351
+ sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
2352
+ planSourcePath: path.join(planRoot, project.projectId, ...branchName.split("/"))
2353
+ }));
2354
+ removeProjectWorktrees({ projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
2355
+ for (const branch of branches) {
2356
+ fs.rmSync(branch.planSourcePath, { recursive: true, force: true });
2357
+ const key = projectBranchKey(project.projectId, branch.branchName);
2358
+ const durableDeletion = durableDeletionForBranch(project.projectId, project.checkoutPathSegments, branch.branchName);
2359
+ pendingCheckouts.delete(key);
2360
+ pendingMirrorDeletes.set(key, {
2361
+ id: projectMountId(project.projectId, branch.branchName),
2362
+ projectId: project.projectId,
2363
+ branchName: branch.branchName,
2364
+ // The project Git directory is intentionally gone before the outer
2365
+ // tree is published. Use the outer workspace repository to delete the
2366
+ // hidden ref only after that publication succeeds.
2367
+ gitDirectory: workspaceShadowRoot,
2368
+ mirrorUrl: connection.mirrorUrl,
2369
+ mirrorAuth: connection.mirrorAuth,
2370
+ sourcePath: branch.sourcePath,
2371
+ workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
2372
+ planSourcePath: branch.planSourcePath,
2373
+ planWorkspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
2374
+ ...durableDeletion ? { tombstoneId: durableDeletion.id } : {},
2375
+ ...durableDeletion?.kind === "project" ? { projectDeleted: !projectWorkspaceState.desiredProjects.some(({ projectId }) => projectId === project.projectId) } : {}
2376
+ });
2377
+ }
2378
+ readyProjectIds.delete(project.projectId);
2379
+ reconciledProjectConfigFingerprints.delete(project.projectId);
2380
+ };
2381
+ const cleanupConfigForDurableDeletion = (deletion) => {
2382
+ const desiredLayout = projectWorkspaceState.desiredProjects.find(
2383
+ (project) => project.projectId === deletion.projectId && project.checkoutPathSegments[0] === deletion.checkoutPathSegments[0] && project.checkoutPathSegments[1] === deletion.checkoutPathSegments[1]
2384
+ );
2385
+ const liveConfig = projectConfigById.get(deletion.projectId);
2386
+ const branchNames = deletion.kind === "project" ? deletion.branchNames : desiredLayout?.branches.map(({ branchName }) => branchName) ?? [];
2387
+ const defaultBranch = desiredLayout?.defaultBranch ?? (branchNames.includes("main") ? "main" : branchNames[0] ?? "main");
2460
2388
  return {
2461
- workerLabel: label,
2462
- remoteUrl: workspaceRemoteUrl,
2463
- authHeader: `Authorization: Bearer ${token}`,
2464
- projectsRoot,
2465
- plansRoot: planRoot,
2466
- shadowRoot: workspaceShadowRoot,
2467
- projects,
2468
- trigger,
2469
- ...overrides
2389
+ projectId: deletion.projectId,
2390
+ checkoutPathSegments: [...deletion.checkoutPathSegments],
2391
+ projectPath: liveConfig?.projectPath ?? deletion.checkoutPathSegments.join("/"),
2392
+ repoHttpUrl: liveConfig?.repoHttpUrl ?? null,
2393
+ repoAuthHeader: liveConfig?.repoAuthHeader ?? null,
2394
+ defaultBranch,
2395
+ branches: branchNames.map(
2396
+ (branchName) => liveConfig?.branches.find((branch) => branch.branchName === branchName) ?? {
2397
+ branchName,
2398
+ sourceBranchName: null,
2399
+ // Deletion never consumes the baseline, but retain a structurally
2400
+ // valid value for the shared worker-project shape.
2401
+ baseCommitHash: "0".repeat(40)
2402
+ }
2403
+ )
2470
2404
  };
2471
2405
  };
2472
- const sendWorkspaceSyncResult = (authority, result) => {
2473
- try {
2474
- ws.send(JSON.stringify({ type: "workspace_sync_result", ...authority, result }));
2475
- } catch (error) {
2476
- process.stderr.write(
2477
- `[r5d-worker] failed to send workspace_sync_result on its issuing connection: ${error instanceof Error ? error.message : String(error)}
2478
- `
2479
- );
2406
+ const stageDurableWorkspaceDeletions = () => {
2407
+ for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
2408
+ const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
2409
+ if (deletion.kind === "project") {
2410
+ if (!deletion.projectDeleted) {
2411
+ if (fs.existsSync(deletion.managedPath)) {
2412
+ removeProjectWorktrees({ projectRoot: deletion.managedPath, branchNames: deletion.branchNames });
2413
+ }
2414
+ readyProjectIds.delete(deletion.projectId);
2415
+ reconciledProjectConfigFingerprints.delete(deletion.projectId);
2416
+ continue;
2417
+ }
2418
+ stageProjectDeletion(cleanupConfig);
2419
+ continue;
2420
+ }
2421
+ const primaryPath = deletion.primaryBranchName ? configuredProjectBranchPath(projectsRoot, cleanupConfig, deletion.primaryBranchName) : null;
2422
+ stageProjectBranchDeletion(cleanupConfig, deletion.branchName, Boolean(primaryPath && hasProjectWorktree(primaryPath)));
2423
+ }
2424
+ for (const deletion of projectWorkspaceState.pendingMirrorRefDeletions) {
2425
+ const key = projectBranchKey(deletion.projectId, deletion.branchName);
2426
+ const planSourcePath = path.join(planRoot, deletion.projectId, ...deletion.branchName.split("/"));
2427
+ fs.rmSync(planSourcePath, { recursive: true, force: true });
2428
+ pendingMirrorDeletes.set(key, {
2429
+ id: projectMountId(deletion.projectId, deletion.branchName),
2430
+ projectId: deletion.projectId,
2431
+ branchName: deletion.branchName,
2432
+ gitDirectory: workspaceShadowRoot,
2433
+ mirrorUrl: canonicalProjectRemoteUrl(baseUrl, deletion.projectId),
2434
+ mirrorAuth: gitAuthForRemote(canonicalProjectRemoteUrl(baseUrl, deletion.projectId), bearerAuthHeader),
2435
+ sourcePath: deletion.managedPath,
2436
+ workspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, deletion.branchName),
2437
+ planSourcePath,
2438
+ planWorkspaceRelativePath: workspacePlanRelativePath(deletion.projectId, deletion.branchName),
2439
+ tombstoneId: deletion.tombstoneId
2440
+ });
2480
2441
  }
2481
2442
  };
2482
- const sendWorkspaceReady = () => {
2483
- const manifestRevision = workspaceManifestRevision.readyRevision();
2484
- if (!workspaceReady || !manifestRevision) return;
2485
- try {
2486
- ws.send(
2487
- JSON.stringify({
2488
- type: "workspace_ready",
2489
- manifestRevision,
2490
- pendingCheckouts: [...pendingManifestCheckouts.values()].sort(
2491
- (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
2492
- ),
2493
- ...workspaceConvergence.snapshot()
2494
- })
2495
- );
2496
- } catch (error) {
2497
- process.stderr.write(`[r5d-worker] failed to send workspace readiness: ${error instanceof Error ? error.message : String(error)}
2498
- `);
2443
+ const pruneStaleOuterWorkspaceEntries = (entries) => {
2444
+ const remove = (candidate) => {
2445
+ if (candidate) fs.rmSync(candidate, { recursive: true, force: true });
2446
+ };
2447
+ for (const entry of entries) {
2448
+ if (entry.kind === "branch") {
2449
+ projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
2450
+ projectId: entry.projectId,
2451
+ branchName: entry.branchName
2452
+ });
2453
+ remove(entry.projectTreePath);
2454
+ remove(entry.planTreePath);
2455
+ continue;
2456
+ }
2457
+ remove(entry.projectTreePath);
2458
+ remove(entry.planTreePath);
2459
+ for (const branch of entry.branches) {
2460
+ remove(branch.projectTreePath);
2461
+ remove(branch.planTreePath);
2462
+ }
2499
2463
  }
2500
2464
  };
2501
- const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
2502
- const armWorkspacePublication = (trigger, options2 = { delayMs: WORKSPACE_PUBLICATION_QUIET_MS }) => {
2503
- workspaceConvergence.schedule();
2504
- if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2505
- sendWorkspaceReady();
2506
- if (options2.requestId && !explicitWorkspacePublications.schedule({ requestId: options2.requestId, trigger }, pendingPublication?.requestId)) {
2507
- return;
2508
- }
2509
- if (!options2.requestId && explicitWorkspacePublications.hasScheduled()) return;
2510
- if (workspacePublicationTimer) clearTimeout(workspacePublicationTimer);
2511
- workspacePublicationTimer = setTimeout(
2512
- () => {
2513
- workspacePublicationTimer = void 0;
2514
- if (options2.requestId) explicitWorkspacePublications.consumeScheduled(options2.requestId);
2515
- if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2516
- if (!workspaceReady || !workspaceRemoteUrl) {
2517
- armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2518
- return;
2519
- }
2520
- if (activeWorkspaceIncidentId) {
2521
- workspaceConvergence.block();
2522
- sendWorkspaceReady();
2523
- if (workspacePublicationIncidentAction(options2.requestId) === "defer_automatic") return;
2524
- if (pendingPublication) {
2525
- explicitWorkspacePublications.enqueue({ requestId: options2.requestId, trigger }, pendingPublication.requestId);
2526
- return;
2465
+ const effectiveWorkerProjects = (projects) => {
2466
+ const incomingById = new Map(projects.map((project) => [project.projectId, project]));
2467
+ const pending = new Set(
2468
+ projectWorkspaceState.locallyPendingCreatedBranches.map(({ projectId, branchName }) => projectBranchKey(projectId, branchName))
2469
+ );
2470
+ return projectWorkspaceState.effectiveDesiredProjects.flatMap((layout) => {
2471
+ const incoming = incomingById.get(layout.projectId);
2472
+ if (!incoming) return [];
2473
+ const existing = projectConfigById.get(layout.projectId);
2474
+ const branches = layout.branches.flatMap(({ branchName }) => {
2475
+ const configured = incoming.branches.find((branch) => branch.branchName === branchName);
2476
+ if (configured) return [configured];
2477
+ const retained = existing?.branches.find((branch) => branch.branchName === branchName);
2478
+ if (retained) return [retained];
2479
+ if (!pending.has(projectBranchKey(layout.projectId, branchName))) return [];
2480
+ const branchPath = configuredProjectBranchPath(
2481
+ projectsRoot,
2482
+ { ...incoming, checkoutPathSegments: layout.checkoutPathSegments },
2483
+ branchName
2484
+ );
2485
+ if (!hasProjectWorktree(branchPath)) return [];
2486
+ return [
2487
+ {
2488
+ branchName,
2489
+ sourceBranchName: null,
2490
+ baseCommitHash: runGit(["rev-parse", "HEAD"], { cwd: branchPath })
2527
2491
  }
2528
- const snapshot2 = workspaceConvergence.snapshot();
2529
- pendingPublication = {
2530
- requestId: options2.requestId,
2531
- trigger,
2532
- dirtyGeneration: snapshot2.dirtyGeneration,
2533
- visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2534
- explicit: true,
2535
- workInFlight: false,
2536
- requestedAt: Date.now()
2537
- };
2538
- try {
2539
- ws.send(
2540
- JSON.stringify({
2541
- type: "workspace_publication_request",
2542
- requestId: options2.requestId,
2543
- trigger,
2544
- dirtyGeneration: snapshot2.dirtyGeneration,
2545
- localHead: snapshot2.localHead,
2546
- desiredCanonicalHead: snapshot2.desiredCanonicalHead
2547
- })
2548
- );
2549
- } catch (error) {
2550
- pendingPublication = void 0;
2551
- workspaceConvergence.defer();
2492
+ ];
2493
+ });
2494
+ return [{ ...incoming, checkoutPathSegments: [...layout.checkoutPathSegments], branches }];
2495
+ });
2496
+ };
2497
+ const buildWorkspaceMounts = () => {
2498
+ const mounts = [];
2499
+ for (const project of [...projectConfigById.values()].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
2500
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
2501
+ for (const branch of [...project.branches].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
2502
+ const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
2503
+ mounts.push({
2504
+ id: projectMountId(project.projectId, branch.branchName),
2505
+ sourcePath: branchPath,
2506
+ workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
2507
+ sourceMode: "git",
2508
+ hydrateDeletionMode: "git",
2509
+ busy: () => hasProjectWorktree(branchPath) && projectWorktreeOperationInProgress(branchPath)
2510
+ });
2511
+ mounts.push({
2512
+ id: projectPlanMountId(project.projectId, branch.branchName),
2513
+ sourcePath: path.join(planRoot, project.projectId, ...branch.branchName.split("/")),
2514
+ workspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
2515
+ sourceMode: "all",
2516
+ hydrateDeletionMode: "all"
2517
+ });
2518
+ }
2519
+ void projectRoot;
2520
+ }
2521
+ mounts.push({
2522
+ id: "workspace-plans",
2523
+ sourcePath: path.join(planRoot, "workspace"),
2524
+ workspaceRelativePath: "workspace-plans",
2525
+ sourceMode: "all",
2526
+ hydrateDeletionMode: "all"
2527
+ });
2528
+ for (const deletion of pendingMirrorDeletes.values()) {
2529
+ mounts.push({
2530
+ id: deletion.id,
2531
+ sourcePath: deletion.sourcePath,
2532
+ workspaceRelativePath: deletion.workspaceRelativePath,
2533
+ sourceMode: "git",
2534
+ hydrateDeletionMode: "git",
2535
+ deleteWhenSourceMissing: true
2536
+ });
2537
+ mounts.push({
2538
+ id: `${deletion.id}:plan`,
2539
+ sourcePath: deletion.planSourcePath,
2540
+ workspaceRelativePath: deletion.planWorkspaceRelativePath,
2541
+ sourceMode: "all",
2542
+ hydrateDeletionMode: "all",
2543
+ deleteWhenSourceMissing: true
2544
+ });
2545
+ }
2546
+ return mounts;
2547
+ };
2548
+ const workspaceLocalHead = () => fs.existsSync(path.join(workspaceShadowRoot, ".git")) && tryGit(["rev-parse", "--verify", "HEAD"], { cwd: workspaceShadowRoot }) ? runGit(["rev-parse", "HEAD"], { cwd: workspaceShadowRoot }) : null;
2549
+ const workspaceGitStatus = () => {
2550
+ try {
2551
+ return runGit(["status", "--porcelain"], { cwd: workspaceShadowRoot });
2552
+ } catch {
2553
+ return "";
2554
+ }
2555
+ };
2556
+ const affectedProjects = (paths) => [
2557
+ ...new Set(
2558
+ paths.flatMap((filePath) => {
2559
+ const match = /^(?:projects|plans)\/([^/]+)\//.exec(filePath);
2560
+ return match?.[1] ? [match[1]] : [];
2561
+ })
2562
+ )
2563
+ ].sort();
2564
+ const ensureConfiguredProjects = () => {
2565
+ for (const project of projectConfigById.values()) {
2566
+ validateProjectId(project.projectId);
2567
+ for (const branch of project.branches) validateBranchName(branch.branchName);
2568
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
2569
+ const configFingerprint = projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: workspaceGitIdentity });
2570
+ const alreadyReady = readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === configFingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
2571
+ if (alreadyReady) {
2572
+ for (const branch of project.branches) pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
2573
+ continue;
2574
+ }
2575
+ try {
2576
+ const connection = projectConnection(project);
2577
+ const states = ensureProjectWorktrees({
2578
+ projectRoot,
2579
+ primaryBranchName: primaryProjectBranch(project),
2580
+ branches: project.branches,
2581
+ originUrl: connection.originUrl,
2582
+ originAuth: connection.originAuth,
2583
+ mirrorUrl: connection.mirrorUrl,
2584
+ mirrorAuth: connection.mirrorAuth,
2585
+ credentialHelper: connection.credentialHelper,
2586
+ gitIdentity: workspaceGitIdentity
2587
+ });
2588
+ readyProjectIds.add(project.projectId);
2589
+ reconciledProjectConfigFingerprints.set(project.projectId, configFingerprint);
2590
+ for (const state of states) {
2591
+ pendingCheckouts.delete(projectBranchKey(project.projectId, state.branchName));
2592
+ const key = projectBranchKey(project.projectId, state.branchName);
2593
+ if (!lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
2594
+ if ((state.ahead ?? 0) > 0) {
2552
2595
  process.stderr.write(
2553
- `[r5d-worker] failed to request blocked workspace publication authority: ${error instanceof Error ? error.message : String(error)}
2596
+ `[r5d-worker] ${project.projectPath}/${state.branchName} is ${state.ahead} commit(s) ahead of origin; switching workers may require publishing or reconciling those commits
2554
2597
  `
2555
2598
  );
2556
- armWorkspacePublication(trigger, { delayMs: 1e3, requestId: options2.requestId });
2557
2599
  }
2558
- return;
2559
2600
  }
2560
- if (pendingPublication) {
2561
- armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2562
- return;
2601
+ } catch (error) {
2602
+ readyProjectIds.delete(project.projectId);
2603
+ reconciledProjectConfigFingerprints.delete(project.projectId);
2604
+ for (const branch of project.branches) {
2605
+ pendingCheckouts.set(projectBranchKey(project.projectId, branch.branchName), {
2606
+ projectId: project.projectId,
2607
+ branchName: branch.branchName
2608
+ });
2563
2609
  }
2564
- const snapshot = workspaceConvergence.snapshot();
2565
- const requestId = options2.requestId ?? crypto.randomUUID();
2566
- pendingPublication = {
2567
- requestId,
2568
- trigger,
2569
- dirtyGeneration: snapshot.dirtyGeneration,
2570
- visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2571
- explicit: options2.requestId !== void 0,
2572
- workInFlight: false,
2573
- requestedAt: Date.now()
2574
- };
2610
+ process.stderr.write(
2611
+ `[r5d-worker] failed to initialize project ${project.projectPath}: ${error instanceof Error ? error.message : String(error)}
2612
+ `
2613
+ );
2614
+ }
2615
+ }
2616
+ };
2617
+ const collectAheadOfOriginBranches = () => {
2618
+ const aheadBranches = [];
2619
+ for (const project of projectConfigById.values()) {
2620
+ if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2621
+ const connection = projectConnection(project);
2622
+ const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
2623
+ if (!tryGit(["fetch", "--no-recurse-submodules", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*"], {
2624
+ cwd: primaryPath,
2625
+ auth: connection.originAuth
2626
+ })) {
2627
+ process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
2628
+ `);
2629
+ }
2630
+ for (const branch of project.branches) {
2631
+ const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
2632
+ if (!hasProjectWorktree(branchPath)) continue;
2575
2633
  try {
2576
- ws.send(
2577
- JSON.stringify({
2578
- type: "workspace_publication_request",
2579
- requestId,
2580
- trigger,
2581
- dirtyGeneration: snapshot.dirtyGeneration,
2582
- localHead: snapshot.localHead,
2583
- desiredCanonicalHead: snapshot.desiredCanonicalHead
2584
- })
2585
- );
2634
+ let ahead = 0;
2635
+ const originRef = `refs/remotes/origin/${branch.branchName}`;
2636
+ if (tryGit(["show-ref", "--verify", "--quiet", originRef], { cwd: branchPath })) {
2637
+ const counts = runGit(["rev-list", "--left-right", "--count", `${originRef}...HEAD`], { cwd: branchPath }).split(/\s+/).map(Number);
2638
+ ahead = Number.isSafeInteger(counts[1]) ? counts[1] : 0;
2639
+ } else if (tryGit(["cat-file", "-e", `${branch.baseCommitHash}^{commit}`], { cwd: branchPath })) {
2640
+ const count = Number(runGit(["rev-list", "--count", `${branch.baseCommitHash}..HEAD`], { cwd: branchPath }));
2641
+ ahead = Number.isSafeInteger(count) ? count : 0;
2642
+ }
2643
+ if (ahead > 0) aheadBranches.push({ projectId: project.projectId, branchName: branch.branchName, ahead });
2586
2644
  } catch (error) {
2587
- pendingPublication = void 0;
2588
- workspaceConvergence.defer();
2589
2645
  process.stderr.write(
2590
- `[r5d-worker] failed to request workspace publication: ${error instanceof Error ? error.message : String(error)}
2646
+ `[r5d-worker] could not measure origin divergence for ${project.projectPath}/${branch.branchName}: ${error instanceof Error ? error.message : String(error)}
2591
2647
  `
2592
2648
  );
2593
- armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2594
2649
  }
2595
- },
2596
- Math.max(0, options2.delayMs)
2650
+ }
2651
+ }
2652
+ return aheadBranches.sort(
2653
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
2597
2654
  );
2598
- workspacePublicationTimer.unref();
2599
2655
  };
2600
- const markWorkspaceDirty = (trigger, force = false) => {
2601
- workspaceConvergence.markDirty();
2602
- armWorkspacePublication(trigger, { delayMs: force ? 0 : WORKSPACE_PUBLICATION_QUIET_MS });
2603
- };
2604
- const convergeAvailableWorkspaceHead = async () => {
2605
- if (pendingWorkspaceHead === void 0 || !workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || workspaceHeadConvergenceInFlight) {
2606
- return;
2656
+ const pushChangedProjectHeads = (activeMountIds, publishedHead) => {
2657
+ const active = new Set(activeMountIds);
2658
+ for (const project of projectConfigById.values()) {
2659
+ if (!readyProjectIds.has(project.projectId)) continue;
2660
+ const changed = /* @__PURE__ */ new Set();
2661
+ for (const branch of project.branches) {
2662
+ if (!active.has(projectMountId(project.projectId, branch.branchName))) continue;
2663
+ const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
2664
+ if (!hasProjectWorktree(branchPath)) continue;
2665
+ const head = runGit(["rev-parse", "HEAD"], { cwd: branchPath });
2666
+ if (lastObservedProjectHeads.get(projectBranchKey(project.projectId, branch.branchName)) !== head) {
2667
+ changed.add(branch.branchName);
2668
+ }
2669
+ }
2670
+ if (changed.size === 0) continue;
2671
+ const connection = projectConnection(project);
2672
+ const results = pushProjectMirrorHeads({
2673
+ projectRoot: configuredProjectRoot(projectsRoot, project),
2674
+ branchNames: project.branches.map(({ branchName }) => branchName),
2675
+ mirrorUrl: connection.mirrorUrl,
2676
+ mirrorAuth: connection.mirrorAuth,
2677
+ onlyBranches: changed
2678
+ });
2679
+ for (const result of results) {
2680
+ if (result.pushed) lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
2681
+ }
2607
2682
  }
2608
- const advertisedHead = pendingWorkspaceHead;
2609
- pendingWorkspaceHead = void 0;
2610
- workspaceConvergence.observeCanonicalHead(advertisedHead);
2611
- workspaceHeadConvergenceInFlight = true;
2612
- try {
2613
- const snapshot = workspaceConvergence.snapshot();
2614
- const hydrateVisible = !pendingPublication && snapshot.dirtyGeneration === snapshot.publishedGeneration;
2615
- const converged = await workspaceSyncSingleFlight.runExclusive(
2616
- () => convergeWorkspaceHead(workspaceSyncInput({ type: "inbound_head", detail: advertisedHead ?? "unborn" }), {
2617
- hydrateVisible
2618
- })
2619
- );
2620
- workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
2621
- if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
2622
- if (hydrateVisible && converged.localChanges) {
2623
- markWorkspaceDirty({ type: "periodic", detail: "inbound overlap retained for next publication" });
2683
+ const publishedTombstoneIds = [
2684
+ ...new Set(
2685
+ [...pendingMirrorDeletes.values()].flatMap(
2686
+ (deletion) => active.has(deletion.id) && deletion.tombstoneId ? [deletion.tombstoneId] : []
2687
+ )
2688
+ )
2689
+ ];
2690
+ if (publishedTombstoneIds.length > 0) {
2691
+ projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
2692
+ tombstoneIds: publishedTombstoneIds,
2693
+ publishedHead
2694
+ });
2695
+ }
2696
+ for (const [key, deletion] of pendingMirrorDeletes) {
2697
+ if (!active.has(deletion.id)) continue;
2698
+ if (!deletion.projectDeleted) {
2699
+ deleteProjectMirrorBranch({
2700
+ gitDirectory: deletion.gitDirectory,
2701
+ branchName: deletion.branchName,
2702
+ mirrorUrl: deletion.mirrorUrl,
2703
+ mirrorAuth: deletion.mirrorAuth
2704
+ });
2624
2705
  }
2625
- sendWorkspaceReady();
2626
- } catch (error) {
2627
- pendingWorkspaceHead = advertisedHead;
2628
- process.stderr.write(
2629
- `[r5d-worker] failed to fetch available workspace head: ${error instanceof Error ? error.message : String(error)}
2706
+ if (deletion.tombstoneId && !deletion.projectDeleted) {
2707
+ projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
2708
+ tombstoneId: deletion.tombstoneId,
2709
+ branchName: deletion.branchName
2710
+ });
2711
+ }
2712
+ pendingMirrorDeletes.delete(key);
2713
+ lastObservedProjectHeads.delete(key);
2714
+ }
2715
+ };
2716
+ const refreshProjectHeadsAfterInboundWorkspace = () => {
2717
+ for (const project of projectConfigById.values()) {
2718
+ if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2719
+ try {
2720
+ const connection = projectConnection(project);
2721
+ const refreshed = fastForwardProjectHeadsFromMirror({
2722
+ projectRoot: configuredProjectRoot(projectsRoot, project),
2723
+ primaryBranchName: primaryProjectBranch(project),
2724
+ branchNames: project.branches.map(({ branchName }) => branchName),
2725
+ mirrorUrl: connection.mirrorUrl,
2726
+ mirrorAuth: connection.mirrorAuth
2727
+ });
2728
+ for (const state of refreshed) {
2729
+ lastObservedProjectHeads.set(
2730
+ projectBranchKey(project.projectId, state.branchName),
2731
+ state.fastForwarded && state.mirrorHead ? state.mirrorHead : state.previousHead
2732
+ );
2733
+ }
2734
+ } catch (error) {
2735
+ process.stderr.write(
2736
+ `[r5d-worker] failed to refresh project heads after workspace update: ${error instanceof Error ? error.message : String(error)}
2630
2737
  `
2631
- );
2632
- } finally {
2633
- workspaceHeadConvergenceInFlight = false;
2634
- heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
2635
- if (pendingWorkspaceHead !== void 0) {
2636
- setTimeout(() => void convergeAvailableWorkspaceHead(), 1e3).unref();
2738
+ );
2637
2739
  }
2638
2740
  }
2639
2741
  };
2640
- const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2641
- const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
2642
- (manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
2643
- );
2644
- const resolveMessageTarget = (target) => resolveWorkerSessionTarget({
2645
- target,
2646
- baseUrl,
2647
- token,
2648
- projectsRoot,
2649
- syncRoot,
2650
- workspaceShadowRoot,
2651
- manifestByProjectId
2742
+ const reseedProjectHeadsAfterWorkspaceReset = () => {
2743
+ for (const project of projectConfigById.values()) {
2744
+ if (!readyProjectIds.has(project.projectId)) continue;
2745
+ const connection = projectConnection(project);
2746
+ const states = ensureProjectWorktrees({
2747
+ projectRoot: configuredProjectRoot(projectsRoot, project),
2748
+ primaryBranchName: primaryProjectBranch(project),
2749
+ branches: project.branches,
2750
+ originUrl: connection.originUrl,
2751
+ originAuth: connection.originAuth,
2752
+ mirrorUrl: connection.mirrorUrl,
2753
+ mirrorAuth: connection.mirrorAuth,
2754
+ credentialHelper: connection.credentialHelper,
2755
+ gitIdentity: workspaceGitIdentity
2756
+ });
2757
+ for (const state of states) {
2758
+ lastObservedProjectHeads.set(projectBranchKey(project.projectId, state.branchName), state.head);
2759
+ }
2760
+ }
2761
+ };
2762
+ const mapWorkspaceGitResult = (attemptId, trigger, result) => ({
2763
+ type: "workspace_sync",
2764
+ attemptId,
2765
+ workerLabel: label,
2766
+ trigger,
2767
+ outcome: result.outcome === "pushed" ? "published" : result.outcome,
2768
+ startingHead: result.startingHead,
2769
+ ...result.localHead ? { localHead: result.localHead } : {},
2770
+ ...result.publishedHead ? { publishedHead: result.publishedHead } : {},
2771
+ rebaseCount: result.rebaseCount,
2772
+ diffSizeBytes: result.diffSizeBytes,
2773
+ gitStatus: workspaceGitStatus(),
2774
+ affectedProjects: affectedProjects(result.affectedPaths),
2775
+ affectedPaths: result.affectedPaths,
2776
+ discardedPaths: [],
2777
+ localChangesDiscarded: false,
2778
+ ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
2779
+ ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
2780
+ ...result.error ? { error: result.error } : {}
2652
2781
  });
2653
- const ensureVisibleWorktrees = (targets, options2 = {}) => {
2654
- const created = [];
2655
- for (const target of targets) {
2656
- const manifest = manifestByProjectId.get(target.projectId);
2657
- const key = manifestCheckoutKey(target.projectId, target.branchName);
2658
- if (!manifest || !manifest.branches.includes(target.branchName)) {
2659
- pendingManifestCheckouts.delete(key);
2660
- continue;
2782
+ const failedWorkspaceSyncResult = (attemptId, trigger, error) => ({
2783
+ type: "workspace_sync",
2784
+ attemptId,
2785
+ workerLabel: label,
2786
+ trigger,
2787
+ outcome: "failed",
2788
+ startingHead: workspaceLocalHead(),
2789
+ ...workspaceLocalHead() ? { localHead: workspaceLocalHead() } : {},
2790
+ rebaseCount: 0,
2791
+ diffSizeBytes: 0,
2792
+ gitStatus: workspaceGitStatus(),
2793
+ affectedProjects: [],
2794
+ affectedPaths: [],
2795
+ discardedPaths: [],
2796
+ localChangesDiscarded: false,
2797
+ error: error instanceof Error ? error.message : String(error)
2798
+ });
2799
+ const performWorkspaceSync = async (input) => {
2800
+ if (!workspaceRemoteUrl || !workspaceGitIdentity) throw new Error("Worker workspace configuration has not been received");
2801
+ const mounts = buildWorkspaceMounts();
2802
+ const remoteAuth = gitAuthForRemote(workspaceRemoteUrl, bearerAuthHeader);
2803
+ const credentialHelper = credentialHelperForRemote(workspaceRemoteUrl, bearerAuthHeader);
2804
+ if (input.resetToCanonical) {
2805
+ const reset = resetWorkspaceGit({
2806
+ workspacePath: workspaceShadowRoot,
2807
+ remoteUrl: workspaceRemoteUrl,
2808
+ remoteAuth,
2809
+ credentialHelper,
2810
+ gitIdentity: workspaceGitIdentity,
2811
+ mounts
2812
+ });
2813
+ reseedProjectHeadsAfterWorkspaceReset();
2814
+ return {
2815
+ type: "workspace_sync",
2816
+ attemptId: input.attemptId,
2817
+ workerLabel: label,
2818
+ trigger: input.trigger,
2819
+ outcome: "reset",
2820
+ startingHead: reset.startingHead,
2821
+ ...reset.localHead ? { localHead: reset.localHead } : {},
2822
+ ...reset.remoteHead ? { publishedHead: reset.remoteHead } : {},
2823
+ rebaseCount: 0,
2824
+ diffSizeBytes: 0,
2825
+ gitStatus: workspaceGitStatus(),
2826
+ affectedProjects: affectedProjects(reset.discardedPaths),
2827
+ affectedPaths: reset.discardedPaths,
2828
+ discardedPaths: reset.discardedPaths,
2829
+ localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
2830
+ };
2831
+ }
2832
+ const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
2833
+ const result = await synchronizeWorkspaceGit({
2834
+ attemptId: input.attemptId,
2835
+ workerLabel: label,
2836
+ workspacePath: workspaceShadowRoot,
2837
+ remoteUrl: workspaceRemoteUrl,
2838
+ remoteAuth,
2839
+ credentialHelper,
2840
+ gitIdentity: workspaceGitIdentity,
2841
+ mounts,
2842
+ commitDetail: input.confirmationReason ?? input.trigger.detail,
2843
+ allowLargeDiff: input.confirmedLargeDiff,
2844
+ skipMountMirror: outerRemediation,
2845
+ skipMountHydration: outerRemediation,
2846
+ afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
2847
+ pushChangedProjectHeads(activeMountIds, publishedHead);
2661
2848
  }
2662
- const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
2663
- const branchPath = path.join(projectRoot, target.branchName);
2664
- const existed = hasNormalVisibleGitDir(branchPath);
2665
- const remote = visibleCheckoutRemoteFor({
2666
- baseUrl,
2667
- token,
2668
- projectId: manifest.projectId,
2669
- branchName: target.branchName,
2670
- manifest
2849
+ });
2850
+ if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
2851
+ refreshProjectHeadsAfterInboundWorkspace();
2852
+ }
2853
+ return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
2854
+ };
2855
+ const sendWorkspaceSyncResult = (requestId, result) => {
2856
+ try {
2857
+ sendWorkerMessage(ws, {
2858
+ type: "workspace_sync_result",
2859
+ ...requestId ? { requestId } : {},
2860
+ result
2671
2861
  });
2672
- ensureVisibleGitCheckout({
2673
- projectRoot,
2674
- branchName: target.branchName,
2675
- remoteUrl: remote.remoteUrl,
2676
- remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
2677
- persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
2678
- defaultBranch: manifest.defaultBranch || "main",
2679
- reconcileExistingOrigin: remote.reconcileExistingOrigin,
2680
- requireRemoteBranch: remote.requireRemoteBranch,
2681
- requiredBaseCommit: remote.requiredBaseCommit,
2682
- allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requiredBaseCommit),
2683
- clearPersistentAuth: !remote.persistAuth
2862
+ } catch (error) {
2863
+ process.stderr.write(
2864
+ `[r5d-worker] failed to send workspace sync result: ${error instanceof Error ? error.message : String(error)}
2865
+ `
2866
+ );
2867
+ }
2868
+ };
2869
+ const runWorkspaceSync = async (input) => {
2870
+ const attemptId = input.attemptId ?? crypto.randomUUID();
2871
+ const requestedAt = Date.now();
2872
+ let queueEnteredAt = requestedAt;
2873
+ let syncStartedAt = requestedAt;
2874
+ workspaceSyncRequestsInFlight += 1;
2875
+ let result;
2876
+ try {
2877
+ result = await workspaceSyncSingleFlight.runExclusive(async () => {
2878
+ queueEnteredAt = Date.now();
2879
+ syncStartedAt = Date.now();
2880
+ try {
2881
+ return await performWorkspaceSync({
2882
+ attemptId,
2883
+ trigger: input.trigger,
2884
+ confirmedLargeDiff: input.confirmedLargeDiff,
2885
+ confirmationReason: input.confirmationReason,
2886
+ resetToCanonical: input.resetToCanonical
2887
+ });
2888
+ } catch (error) {
2889
+ return failedWorkspaceSyncResult(attemptId, input.trigger, error);
2890
+ }
2684
2891
  });
2685
- if (!existed) created.push(target);
2892
+ } finally {
2893
+ workspaceSyncRequestsInFlight -= 1;
2686
2894
  }
2687
- return created;
2895
+ const finishedAt = Date.now();
2896
+ result = {
2897
+ ...result,
2898
+ telemetry: {
2899
+ totalMs: finishedAt - requestedAt,
2900
+ queueMs: queueEnteredAt - requestedAt,
2901
+ prepareMs: 0,
2902
+ synchronizeMs: finishedAt - syncStartedAt
2903
+ }
2904
+ };
2905
+ if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result);
2906
+ return result;
2688
2907
  };
2689
- const schedulePendingManifestCheckoutHydration = (delayMs = 0) => {
2690
- if (workspaceManifestHydrationTimer || workspaceManifestHydrationInFlight || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2908
+ const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
2909
+ pendingAutomaticTrigger = trigger;
2910
+ if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight) {
2691
2911
  return;
2692
2912
  }
2693
- workspaceManifestHydrationTimer = setTimeout(
2913
+ if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
2914
+ workspaceAutomaticTimer = setTimeout(
2694
2915
  () => {
2695
- workspaceManifestHydrationTimer = void 0;
2696
- if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2697
- if (pendingManifestCheckouts.size > 0 && currentWorkerSocket === ws && ws.readyState === WebSocket.OPEN) {
2698
- schedulePendingManifestCheckoutHydration(1e3);
2699
- }
2700
- return;
2701
- }
2702
- workspaceManifestHydrationInFlight = true;
2703
- void workspaceSyncSingleFlight.runExclusive(async () => {
2704
- if (!workspaceReady || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2705
- return;
2706
- }
2707
- const targets = [...pendingManifestCheckouts.values()];
2708
- ensureVisibleWorktrees(targets, { strictCanonicalRevalidation: true });
2709
- for (const target of targets) {
2710
- const manifest = manifestByProjectId.get(target.projectId);
2711
- if (!manifest || !manifest.branches.includes(target.branchName)) continue;
2712
- const branchPath = path.join(projectRootFor(projectsRoot, target.projectId, manifestByProjectId), target.branchName);
2713
- if (hasNormalVisibleGitDir(branchPath)) {
2714
- pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
2715
- }
2916
+ workspaceAutomaticTimer = void 0;
2917
+ const scheduledTrigger = pendingAutomaticTrigger;
2918
+ pendingAutomaticTrigger = void 0;
2919
+ if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
2920
+ automaticSyncInFlight = true;
2921
+ void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
2922
+ if (result.outcome === "failed") {
2923
+ pendingAutomaticTrigger = scheduledTrigger;
2716
2924
  }
2717
- }).then(() => sendWorkspaceReady()).catch((error) => {
2718
- process.stderr.write(
2719
- `[r5d-worker] pending manifest checkout hydration failed: ${error instanceof Error ? error.message : String(error)}
2720
- `
2721
- );
2722
2925
  }).finally(() => {
2723
- workspaceManifestHydrationInFlight = false;
2926
+ automaticSyncInFlight = false;
2724
2927
  heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
2725
- if (pendingManifestCheckouts.size > 0) schedulePendingManifestCheckoutHydration(1e3);
2928
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
2929
+ scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
2930
+ }
2726
2931
  });
2727
2932
  },
2728
2933
  Math.max(0, delayMs)
2729
2934
  );
2730
- workspaceManifestHydrationTimer.unref();
2935
+ workspaceAutomaticTimer.unref();
2731
2936
  };
2732
- const runRequestedWorkspaceSync = async (authority, trigger, overrides = {}) => {
2733
- const requestedAt = Date.now();
2734
- let queueEnteredAt = requestedAt;
2735
- let prepareStartedAt = requestedAt;
2736
- let synchronizeStartedAt = requestedAt;
2737
- let synchronizeFinishedAt = requestedAt;
2937
+ const markWorkspaceDirty = (trigger, force = false) => {
2938
+ scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
2939
+ };
2940
+ const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
2941
+ const resolveMessageTarget = (target) => resolveWorkerSessionTarget({
2942
+ target,
2943
+ projectsRoot,
2944
+ workspaceShadowRoot,
2945
+ projectConfigById
2946
+ });
2947
+ const configureWorkerWorkspace = async (message) => {
2738
2948
  workspaceSyncRequestsInFlight += 1;
2739
2949
  try {
2740
- let pendingTargets = [];
2741
- const result = await workspaceSyncSingleFlight.runExclusive(async () => {
2742
- queueEnteredAt = Date.now();
2743
- const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2744
- const syncProjects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
2745
- if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2746
- const allowedCheckoutKeys = new Set(
2747
- syncProjects.flatMap((project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName)))
2748
- );
2749
- const scopedPendingTargets = [...pendingManifestCheckouts.values()].filter(
2750
- (target) => allowedCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2950
+ return await workspaceSyncSingleFlight.runExclusive(async () => {
2951
+ workspaceConfigured = false;
2952
+ configureGitHubAuth(message.githubCredential);
2953
+ visibleGitIdentity = message.gitIdentity;
2954
+ workspaceGitIdentity = message.gitIdentity;
2955
+ workspaceRemoteUrl = message.workspaceRemoteUrl;
2956
+ projectWorkspaceState = projectWorkspaceStateStore.reconcile({
2957
+ desiredProjects: message.projects
2958
+ });
2959
+ pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes, message.projects);
2960
+ const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
2961
+ ...project,
2962
+ branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
2963
+ }));
2964
+ const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
2965
+ for (const existingProject of projectConfigById.values()) {
2966
+ const nextProject = nextProjectConfigById.get(existingProject.projectId);
2967
+ if (!nextProject) {
2968
+ stageProjectDeletion(existingProject);
2969
+ continue;
2970
+ }
2971
+ const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
2972
+ for (const existingBranch of existingProject.branches) {
2973
+ if (!nextBranches.has(existingBranch.branchName)) {
2974
+ stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
2975
+ }
2976
+ }
2977
+ }
2978
+ projectConfigById.clear();
2979
+ for (const project of effectiveProjects) projectConfigById.set(project.projectId, project);
2980
+ const configuredKeys = new Set(
2981
+ effectiveProjects.flatMap((project) => project.branches.map(({ branchName }) => projectBranchKey(project.projectId, branchName)))
2751
2982
  );
2752
- prepareStartedAt = Date.now();
2753
- let checkoutTargets = [];
2754
- if (shouldEnsureCheckouts) {
2755
- checkoutTargets = workspaceSyncCheckoutTargets({
2756
- projects: syncProjects,
2757
- pendingTargets: scopedPendingTargets,
2758
- includeAllManifestCheckouts: trigger.type === "connect",
2759
- ...trigger.canonicalCheckoutOnly && trigger.projectId && trigger.branchName ? { canonicalCheckoutOnly: { projectId: trigger.projectId, branchName: trigger.branchName } } : {}
2760
- });
2983
+ for (const key of pendingCheckouts.keys()) {
2984
+ if (!configuredKeys.has(key)) pendingCheckouts.delete(key);
2761
2985
  }
2762
- pendingTargets = shouldEnsureCheckouts ? scopedPendingTargets : [];
2763
- const createdCheckouts = ensureVisibleWorktrees(checkoutTargets, {
2764
- strictCanonicalRevalidation: true
2765
- });
2766
- const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
2767
- for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
2768
- const key = manifestCheckoutKey(target.projectId, target.branchName);
2769
- if (allowedCheckoutKeys.has(key)) newVisibleCheckoutByKey.set(key, target);
2986
+ for (const projectId of [...readyProjectIds]) {
2987
+ if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
2770
2988
  }
2771
- const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2772
- const input = workspaceSyncInput(trigger, {
2773
- ...overrides,
2774
- ...shouldEnsureCheckouts && !trigger.canonicalCheckoutOnly ? { visibleBaselineHead: workspaceConvergence.visibleBaselineHead() } : {},
2775
- ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2989
+ const remoteAuth = gitAuthForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
2990
+ const credentialHelper = credentialHelperForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
2991
+ ensureWorkspaceGitClone({
2992
+ workspacePath: workspaceShadowRoot,
2993
+ remoteUrl: message.workspaceRemoteUrl,
2994
+ remoteAuth,
2995
+ credentialHelper,
2996
+ gitIdentity: message.gitIdentity
2776
2997
  });
2777
- synchronizeStartedAt = Date.now();
2778
- let synchronized;
2779
- try {
2780
- synchronized = await synchronizeWorkspace(input);
2781
- } finally {
2782
- synchronizeFinishedAt = Date.now();
2998
+ pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
2999
+ stageDurableWorkspaceDeletions();
3000
+ const mountsBeforeGit = buildWorkspaceMounts();
3001
+ hydrateWorkspaceGitMounts(
3002
+ workspaceShadowRoot,
3003
+ mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath))
3004
+ );
3005
+ ensureConfiguredProjects();
3006
+ let result = await performWorkspaceSync({
3007
+ attemptId: crypto.randomUUID(),
3008
+ trigger: { type: "connect" }
3009
+ });
3010
+ if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
3011
+ const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
3012
+ if (staleAfterInbound.length > 0) {
3013
+ pruneStaleOuterWorkspaceEntries(staleAfterInbound);
3014
+ stageDurableWorkspaceDeletions();
3015
+ result = await performWorkspaceSync({
3016
+ attemptId: crypto.randomUUID(),
3017
+ trigger: { type: "connect" }
3018
+ });
3019
+ }
2783
3020
  }
2784
- return synchronized;
2785
- });
2786
- const completedAt = Date.now();
2787
- const resultWithTelemetry = {
2788
- ...result,
2789
- telemetry: {
2790
- totalMs: completedAt - requestedAt,
2791
- queueMs: queueEnteredAt - requestedAt,
2792
- prepareMs: synchronizeStartedAt - prepareStartedAt,
2793
- synchronizeMs: synchronizeFinishedAt - synchronizeStartedAt
3021
+ const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
3022
+ if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
3023
+ const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
3024
+ (deletion) => deletion.kind === "project" && !deletion.projectDeleted ? [deletion.tombstoneId] : []
3025
+ );
3026
+ if (layoutCleanupIds.length > 0) {
3027
+ projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
3028
+ tombstoneIds: layoutCleanupIds,
3029
+ publishedHead
3030
+ });
3031
+ }
2794
3032
  }
2795
- };
2796
- if (resultWithTelemetry.outcome === "no_change" || resultWithTelemetry.outcome === "published" || resultWithTelemetry.outcome === "updated" || resultWithTelemetry.outcome === "conflict_reset") {
2797
- for (const target of pendingTargets) {
2798
- const key = manifestCheckoutKey(target.projectId, target.branchName);
2799
- if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
3033
+ workspaceConfigured = true;
3034
+ const pending = [...pendingCheckouts.values()].sort(
3035
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
3036
+ );
3037
+ return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
3038
+ });
3039
+ } catch (error) {
3040
+ workspaceConfigured = fs.existsSync(path.join(workspaceShadowRoot, ".git"));
3041
+ for (const project of message.projects) {
3042
+ for (const branch of project.branches) {
3043
+ pendingCheckouts.set(projectBranchKey(project.projectId, branch.branchName), {
3044
+ projectId: project.projectId,
3045
+ branchName: branch.branchName
3046
+ });
2800
3047
  }
2801
3048
  }
2802
- sendWorkspaceSyncResult(authority, resultWithTelemetry);
2803
- return resultWithTelemetry;
2804
- } catch (error) {
2805
- const completedAt = Date.now();
2806
- const result = {
2807
- type: "workspace_sync",
2808
- attemptId: overrides.attemptId ?? crypto.randomUUID(),
2809
- workerLabel: label,
2810
- trigger,
2811
- outcome: "failed",
2812
- startingHead: null,
2813
- rebaseCount: 0,
2814
- diffSizeBytes: 0,
2815
- gitStatus: "",
2816
- affectedProjects: [],
2817
- affectedPaths: [],
2818
- discardedPaths: [],
2819
- localChangesDiscarded: false,
2820
- telemetry: {
2821
- totalMs: completedAt - requestedAt,
2822
- queueMs: queueEnteredAt - requestedAt,
2823
- prepareMs: Math.max(0, synchronizeStartedAt - prepareStartedAt),
2824
- synchronizeMs: Math.max(0, synchronizeFinishedAt - synchronizeStartedAt)
2825
- },
2826
- error: error instanceof Error ? error.message : String(error)
3049
+ return {
3050
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
3051
+ pending: [...pendingCheckouts.values()],
3052
+ aheadOfOriginBranches: []
2827
3053
  };
2828
- sendWorkspaceSyncResult(authority, result);
2829
- return result;
2830
3054
  } finally {
2831
3055
  workspaceSyncRequestsInFlight -= 1;
2832
3056
  }
@@ -2843,17 +3067,26 @@ async function startWorker(options) {
2843
3067
  const handleGracefulShutdown = () => {
2844
3068
  if (shutdownAfterClose) return;
2845
3069
  shutdownAfterClose = true;
2846
- void waitForWorkspacePublicationOnShutdown({
2847
- snapshot: () => workspaceConvergence.snapshot(),
2848
- forcePublication: () => armWorkspacePublication({ type: "manual", detail: "graceful worker shutdown" }, { delayMs: 0 })
2849
- }).then((outcome) => {
2850
- process.stdout.write(`[r5d-worker] graceful workspace publication ${outcome}
2851
- `);
3070
+ if (workspaceAutomaticTimer) {
3071
+ clearTimeout(workspaceAutomaticTimer);
3072
+ workspaceAutomaticTimer = void 0;
3073
+ }
3074
+ void (async () => {
3075
+ if (workspaceConfigured && !activeWorkspaceIncidentId) {
3076
+ await runWorkspaceSync({
3077
+ trigger: { type: "manual", detail: "graceful worker shutdown" }
3078
+ });
3079
+ }
3080
+ await workspaceSyncSingleFlight.afterCurrent();
2852
3081
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
2853
3082
  ws.close(1e3, "Worker shutting down");
2854
3083
  } else {
2855
3084
  process.exit(0);
2856
3085
  }
3086
+ })().catch((error) => {
3087
+ process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
3088
+ `);
3089
+ ws.close(1011, "Worker shutdown sync failed");
2857
3090
  });
2858
3091
  };
2859
3092
  process.on("SIGTERM", handleGracefulShutdown);
@@ -2886,11 +3119,7 @@ async function startWorker(options) {
2886
3119
  r5dctlVersion: readInstalledCliVersion("r5dctl"),
2887
3120
  capabilities: {
2888
3121
  updateClis: true,
2889
- canonicalResolverCheckout: true,
2890
3122
  browserPortForwarding: true,
2891
- globalWorkspaceSyncLeaseV1: true,
2892
- eventualWorkspaceSyncV1: true,
2893
- workspaceIncidentCandidatePinV1: true,
2894
3123
  execStdinV1: true
2895
3124
  },
2896
3125
  projectRoot: projectsRoot,
@@ -2917,302 +3146,153 @@ async function startWorker(options) {
2917
3146
  if (message.type === "connected") {
2918
3147
  return;
2919
3148
  }
2920
- if (message.type === "workspace_manifest") {
2921
- const revisionToken = workspaceManifestRevision.begin(message.revision);
2922
- workspaceReady = false;
2923
- const manifestFilesystem = inspectWorkspaceManifestFilesystem({
2924
- projectsRoot,
2925
- workspaceShadowRoot,
2926
- projects: message.projects
2927
- });
2928
- const completed = await workspaceSyncSingleFlight.runExclusive(async () => {
2929
- if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2930
- const firstBootstrap = manifestFilesystem.firstBootstrap;
2931
- configureGitHubAuth(message.githubCredential);
2932
- visibleGitIdentity = message.gitIdentity;
2933
- workspaceRemoteUrl = message.workspaceRemoteUrl;
2934
- manifestByProjectId.clear();
2935
- for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2936
- const currentCheckouts = allManifestCheckouts();
2937
- const currentCheckoutKeys = new Set(
2938
- currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2939
- );
2940
- for (const key of pendingManifestCheckouts.keys()) {
2941
- if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2942
- }
2943
- for (const checkout of currentCheckouts) {
2944
- const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2945
- const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2946
- if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2947
- }
2948
- if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2949
- const created = firstBootstrap ? ensureVisibleWorktrees(currentCheckouts, { strictCanonicalRevalidation: true }) : [];
2950
- if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2951
- const bootstrapped = await convergeWorkspaceHead(workspaceSyncInput({ type: "connect" }, { newVisibleCheckouts: created }), {
2952
- // An existing checkout becomes routable after a fetch. Its
2953
- // visible tree converges asynchronously; first bootstrap alone
2954
- // must materialize the canonical snapshot before readiness.
2955
- // Re-check the incident immediately before convergence so a
2956
- // concurrently delivered fence forces fetch-only behavior.
2957
- hydrateVisible: firstBootstrap && !activeWorkspaceIncidentId
2958
- });
2959
- if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2960
- if (workspaceBootstrapEstablishesVisibleBaseline({
2961
- hydrated: bootstrapped.hydrated,
2962
- firstBootstrap,
2963
- incidentActive: Boolean(activeWorkspaceIncidentId),
2964
- currentLocalHead: workspaceConvergence.snapshot().localHead,
2965
- currentVisibleBaselineHead: workspaceConvergence.visibleBaselineHead()
2966
- })) {
2967
- workspaceConvergence.setLocalHead(bootstrapped.localHead);
2968
- }
2969
- workspaceConvergence.observeCanonicalHead(bootstrapped.canonicalHead);
2970
- for (const checkout of currentCheckouts) {
2971
- const manifest = manifestByProjectId.get(checkout.projectId);
2972
- if (!manifest) continue;
2973
- const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2974
- if (hasNormalVisibleGitDir(branchPath)) {
2975
- pendingManifestCheckouts.delete(manifestCheckoutKey(checkout.projectId, checkout.branchName));
2976
- }
2977
- }
2978
- process.stdout.write(`[r5d-worker] workspace manifest: ${message.projects.length} projects
2979
- `);
2980
- return true;
3149
+ if (message.type === "workspace_config") {
3150
+ const configured = await configureWorkerWorkspace(message);
3151
+ sendWorkerMessage(ws, {
3152
+ type: "workspace_configured",
3153
+ requestId: message.requestId,
3154
+ result: configured.result,
3155
+ pendingCheckouts: configured.pending,
3156
+ aheadOfOriginBranches: configured.aheadOfOriginBranches
2981
3157
  });
2982
- if (!completed || !workspaceManifestRevision.complete(revisionToken)) return;
2983
- workspaceReady = true;
2984
- sendWorkspaceReady();
2985
- schedulePendingManifestCheckoutHydration();
2986
- if (!workspaceSafetyScan) {
2987
- workspaceSafetyScan = setInterval(() => {
2988
- if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || workspaceSafetyScanInFlight) {
2989
- return;
2990
- }
2991
- workspaceSafetyScanInFlight = true;
2992
- void (async () => {
2993
- const before = workspaceConvergence.snapshot();
2994
- const hydrateVisible = before.dirtyGeneration === before.publishedGeneration;
2995
- const converged = await workspaceSyncSingleFlight.runExclusive(
2996
- () => convergeWorkspaceHead(workspaceSyncInput({ type: "periodic", detail: "idle convergence scan" }), {
2997
- hydrateVisible
2998
- })
2999
- );
3000
- workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
3001
- if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
3002
- if (converged.localChanges && hydrateVisible) {
3003
- markWorkspaceDirty({ type: "periodic", detail: "idle safety scan" });
3004
- }
3005
- sendWorkspaceReady();
3006
- })().catch((error) => {
3007
- process.stderr.write(
3008
- `[r5d-worker] idle workspace safety scan failed: ${error instanceof Error ? error.message : String(error)}
3158
+ process.stdout.write(
3159
+ `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
3009
3160
  `
3010
- );
3011
- }).finally(() => {
3012
- workspaceSafetyScanInFlight = false;
3013
- heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
3014
- });
3015
- }, WORKSPACE_IDLE_SAFETY_SCAN_MS);
3016
- workspaceSafetyScan.unref();
3161
+ );
3162
+ if (!workspacePeriodicTimer) {
3163
+ workspacePeriodicTimer = setInterval(() => {
3164
+ scheduleAutomaticWorkspaceSync({ type: "periodic", detail: "periodic workspace reconciliation" }, 0);
3165
+ }, WORKSPACE_GIT_PERIODIC_MS);
3166
+ workspacePeriodicTimer.unref();
3017
3167
  }
3018
- pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
3019
- void convergeAvailableWorkspaceHead();
3020
- if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3021
- armWorkspacePublication({ type: "periodic", detail: "resume dirty generation after reconnect" }, { delayMs: 0 });
3168
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
3169
+ scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
3022
3170
  }
3023
3171
  return;
3024
3172
  }
3025
3173
  if (message.type === "sync_workspace") {
3026
- await runRequestedWorkspaceSync(
3027
- {
3028
- requestId: message.requestId,
3029
- intentId: message.intentId,
3030
- leaseEpoch: message.leaseEpoch,
3031
- leaseToken: message.leaseToken
3032
- },
3033
- message.trigger,
3034
- {
3035
- attemptId: message.attemptId,
3036
- quarantineRef: message.quarantineRef,
3037
- confirmedLargeDiff: message.confirmedLargeDiff,
3038
- confirmationReason: message.confirmationReason,
3039
- expectedIncidentCandidateHead: message.expectedIncidentCandidateHead,
3040
- resetToCanonical: message.resetToCanonical,
3041
- skipVisibleMirror: message.skipVisibleMirror
3042
- }
3043
- );
3044
- return;
3045
- }
3046
- if (message.type === "workspace_head_available") {
3047
- workspaceConvergence.observeCanonicalHead(message.canonicalHead);
3048
- pendingWorkspaceHead = message.canonicalHead;
3049
- sendWorkspaceReady();
3050
- void convergeAvailableWorkspaceHead();
3051
- return;
3052
- }
3053
- if (message.type === "workspace_publication_now") {
3054
- armWorkspacePublication(message.trigger, { delayMs: 0, requestId: message.requestId });
3174
+ await runWorkspaceSync({
3175
+ requestId: message.requestId,
3176
+ attemptId: message.attemptId,
3177
+ trigger: message.trigger,
3178
+ confirmedLargeDiff: message.confirmedLargeDiff,
3179
+ confirmationReason: message.confirmationReason,
3180
+ resetToCanonical: message.resetToCanonical
3181
+ });
3055
3182
  return;
3056
3183
  }
3057
- if (message.type === "workspace_publication_authority") {
3058
- const publication = pendingPublication;
3059
- if (!publication || publication.requestId !== message.requestId) {
3060
- process.stderr.write(`[r5d-worker] ignoring publication authority for unknown request ${message.requestId}
3061
- `);
3062
- return;
3063
- }
3064
- if (message.status !== "granted") {
3065
- pendingPublication = void 0;
3066
- workspaceConvergence.observeCanonicalHead(message.canonicalHead);
3067
- const applyBlockedResponse = message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId);
3068
- if (applyBlockedResponse) {
3069
- workspaceConvergence.block();
3070
- if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3071
- } else {
3072
- workspaceConvergence.resetTransientPublicationState();
3073
- workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3074
- if (message.status === "busy") {
3075
- armWorkspacePublication(publication.trigger, {
3076
- delayMs: message.retryAfterMs ?? 1e3,
3077
- requestId: publication.requestId
3078
- });
3079
- } else if (!activeWorkspaceIncidentId) {
3080
- const explicit = explicitWorkspacePublications.next();
3081
- if (explicit) {
3082
- armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
3083
- } else if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3084
- armWorkspacePublication(publication.trigger, { delayMs: 0 });
3085
- }
3086
- }
3087
- }
3088
- sendWorkspaceReady();
3089
- return;
3090
- }
3091
- publication.attemptId = message.attemptId;
3092
- publication.authorityToken = message.authorityToken;
3093
- publication.workInFlight = true;
3094
- pendingPublication = publication;
3095
- if (currentWorkerSocket !== ws) {
3096
- publication.workInFlight = false;
3097
- workspaceConvergence.resetTransientPublicationState();
3098
- return;
3099
- }
3100
- workspaceConvergence.beginPreparing();
3101
- sendWorkspaceReady();
3102
- let prepareStartedAt = Date.now();
3103
- let synchronizeStartedAt = prepareStartedAt;
3104
- let synchronizeStarted = false;
3105
- let result;
3184
+ if (message.type === "create_project_branch") {
3106
3185
  try {
3107
- result = await workspaceSyncSingleFlight.runExclusive(async () => {
3108
- prepareStartedAt = Date.now();
3109
- const pendingTargets = [...pendingManifestCheckouts.values()];
3110
- const createdCheckouts = ensureVisibleWorktrees(pendingTargets, { strictCanonicalRevalidation: true });
3111
- synchronizeStartedAt = Date.now();
3112
- synchronizeStarted = true;
3113
- const prepared = await synchronizeWorkspace(
3114
- workspaceSyncInput(publication.trigger, {
3115
- attemptId: message.attemptId,
3116
- quarantineRef: message.quarantineRef,
3117
- expectedCanonicalHead: message.expectedHead,
3118
- visibleBaselineHead: publication.visibleBaselineHead,
3119
- newVisibleCheckouts: [...createdCheckouts, ...pendingTargets]
3120
- })
3121
- );
3122
- for (const target of pendingTargets) {
3123
- pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
3186
+ projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch({
3187
+ projectId: message.projectId,
3188
+ branchName: message.targetBranch
3189
+ });
3190
+ const created = await workspaceSyncSingleFlight.runMutation(() => {
3191
+ const project = projectConfigById.get(message.projectId);
3192
+ if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3193
+ if (!readyProjectIds.has(project.projectId)) {
3194
+ throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
3195
+ }
3196
+ if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
3197
+ throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
3198
+ }
3199
+ if (project.branches.some(({ branchName }) => branchName === message.targetBranch)) {
3200
+ throw new Error(`Project branch ${message.targetBranch} already exists`);
3201
+ }
3202
+ const createdBranch = createLinkedProjectBranch({
3203
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3204
+ sourceBranchName: message.sourceBranch,
3205
+ branchName: message.targetBranch
3206
+ });
3207
+ projectConfigById.set(project.projectId, {
3208
+ ...project,
3209
+ branches: [
3210
+ ...project.branches,
3211
+ {
3212
+ branchName: message.targetBranch,
3213
+ sourceBranchName: message.sourceBranch,
3214
+ baseCommitHash: createdBranch.baseCommitHash
3215
+ }
3216
+ ]
3217
+ });
3218
+ lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
3219
+ return createdBranch;
3220
+ });
3221
+ sendWorkerMessage(ws, {
3222
+ type: "operation_result",
3223
+ requestId: message.requestId,
3224
+ result: {
3225
+ type: "create_project_branch",
3226
+ branchName: message.targetBranch,
3227
+ baseCommitHash: created.baseCommitHash
3124
3228
  }
3125
- return prepared.outcome === "updated" ? { ...prepared, outcome: "no_change" } : prepared;
3126
3229
  });
3127
- } catch (error) {
3128
- if (!synchronizeStarted) synchronizeStartedAt = Date.now();
3129
- result = {
3130
- type: "workspace_sync",
3131
- attemptId: message.attemptId,
3132
- workerLabel: label,
3133
- trigger: publication.trigger,
3134
- outcome: "failed",
3135
- startingHead: message.expectedHead,
3136
- expectedHead: message.expectedHead,
3137
- rebaseCount: 0,
3138
- diffSizeBytes: 0,
3139
- gitStatus: "",
3140
- affectedProjects: [],
3141
- affectedPaths: [],
3142
- discardedPaths: [],
3143
- localChangesDiscarded: false,
3144
- error: error instanceof Error ? error.message : String(error)
3145
- };
3146
- }
3147
- const finishedAt = Date.now();
3148
- result = {
3149
- ...result,
3150
- dirtyGeneration: publication.dirtyGeneration,
3151
- telemetry: workspacePublicationTelemetry({
3152
- requestedAt: publication.requestedAt,
3153
- prepareStartedAt,
3154
- synchronizeStartedAt,
3155
- finishedAt
3156
- })
3157
- };
3158
- workspaceConvergence.beginSubmitting();
3159
- sendWorkspaceReady();
3160
- try {
3161
- ws.send(
3162
- JSON.stringify({
3163
- type: "workspace_publication_result",
3164
- requestId: publication.requestId,
3165
- attemptId: message.attemptId,
3166
- authorityToken: message.authorityToken,
3167
- dirtyGeneration: publication.dirtyGeneration,
3168
- result
3169
- })
3230
+ markWorkspaceDirty(
3231
+ {
3232
+ type: "manual",
3233
+ projectId: message.projectId,
3234
+ branchName: message.targetBranch,
3235
+ detail: `created project branch from ${message.sourceBranch}`
3236
+ },
3237
+ true
3170
3238
  );
3171
3239
  } catch (error) {
3172
- workspaceConvergence.resetTransientPublicationState();
3173
- throw error;
3174
- } finally {
3175
- publication.workInFlight = false;
3176
- if (currentWorkerSocket !== ws) workspaceConvergence.resetTransientPublicationState();
3240
+ sendWorkerMessage(ws, {
3241
+ type: "operation_result",
3242
+ requestId: message.requestId,
3243
+ error: error instanceof Error ? error.message : String(error)
3244
+ });
3177
3245
  }
3178
3246
  return;
3179
3247
  }
3180
- if (message.type === "workspace_publication_completed") {
3181
- const publication = pendingPublication;
3182
- if (!publication || publication.requestId !== message.requestId || publication.attemptId !== message.attemptId) {
3183
- process.stderr.write(`[r5d-worker] ignoring completion for unknown publication ${message.requestId}
3184
- `);
3185
- return;
3186
- }
3187
- pendingPublication = void 0;
3188
- workspaceConvergence.observeCanonicalHead(message.canonicalHead);
3189
- if (message.status === "published" || message.status === "no_change") {
3190
- workspaceConvergence.complete({
3191
- generation: Math.min(message.publishedGeneration ?? publication.dirtyGeneration, publication.dirtyGeneration),
3192
- canonicalHead: message.canonicalHead,
3193
- published: true
3248
+ if (message.type === "delete_project_branch") {
3249
+ try {
3250
+ const deletionKey = projectBranchKey(message.projectId, message.branchName);
3251
+ projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
3252
+ projectId: message.projectId,
3253
+ branchName: message.branchName
3194
3254
  });
3195
- } else if (message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId)) {
3196
- if (message.visibleBaselineHead) workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3197
- workspaceConvergence.block();
3198
- if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3199
- } else {
3200
- workspaceConvergence.resetTransientPublicationState();
3201
- workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3202
- }
3203
- sendWorkspaceReady();
3204
- const explicit = message.status === "retry" && publication.explicit ? { requestId: publication.requestId, trigger: publication.trigger } : explicitWorkspacePublications.next();
3205
- if (explicit) {
3206
- armWorkspacePublication(explicit.trigger, {
3207
- delayMs: message.retryAfterMs ?? 0,
3208
- requestId: explicit.requestId
3255
+ await workspaceSyncSingleFlight.runMutation(() => {
3256
+ const project = projectConfigById.get(message.projectId);
3257
+ if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3258
+ if (pendingMirrorDeletes.has(deletionKey)) return;
3259
+ stageProjectBranchDeletion(project, message.branchName, true);
3260
+ projectConfigById.set(project.projectId, {
3261
+ ...project,
3262
+ branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
3263
+ });
3264
+ });
3265
+ const result = await runWorkspaceSync({
3266
+ trigger: {
3267
+ type: "manual",
3268
+ projectId: message.projectId,
3269
+ branchName: message.branchName,
3270
+ detail: "deleted project branch"
3271
+ },
3272
+ confirmedLargeDiff: true,
3273
+ confirmationReason: "explicit project branch deletion",
3274
+ sendResult: false
3275
+ });
3276
+ if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
3277
+ throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
3278
+ }
3279
+ if (pendingMirrorDeletes.has(deletionKey)) {
3280
+ throw new Error("Project branch deletion did not remove the hidden mirror ref");
3281
+ }
3282
+ sendWorkerMessage(ws, {
3283
+ type: "operation_result",
3284
+ requestId: message.requestId,
3285
+ result: {
3286
+ type: "delete_project_branch",
3287
+ branchName: message.branchName
3288
+ }
3289
+ });
3290
+ } catch (error) {
3291
+ sendWorkerMessage(ws, {
3292
+ type: "operation_result",
3293
+ requestId: message.requestId,
3294
+ error: error instanceof Error ? error.message : String(error)
3209
3295
  });
3210
- } else if (message.status === "retry" || message.status === "failed" || workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3211
- armWorkspacePublication(publication.trigger, { delayMs: message.retryAfterMs ?? 1e3 });
3212
- }
3213
- if (message.canonicalHead !== workspaceConvergence.snapshot().localHead) {
3214
- pendingWorkspaceHead = message.canonicalHead;
3215
- void convergeAvailableWorkspaceHead();
3216
3296
  }
3217
3297
  return;
3218
3298
  }
@@ -3243,31 +3323,11 @@ async function startWorker(options) {
3243
3323
  return;
3244
3324
  }
3245
3325
  if (message.type === "workspace_incident_updated") {
3246
- workspaceIncidentOrdering.observe(message);
3247
3326
  const previousIncidentId = activeWorkspaceIncidentId;
3248
- const nextIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
3249
- if (message.incidentId && (previousIncidentId === message.incidentId || nextIncidentId === message.incidentId) && message.originWorkerLabel === label && message.visibleBaselineHead) {
3250
- workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3251
- }
3252
- activeWorkspaceIncidentId = nextIncidentId;
3253
- const releasedHead = releasePendingWorkspaceHead(
3254
- previousIncidentId,
3255
- activeWorkspaceIncidentId,
3256
- message.status === "resolved" || message.status === "confirmed" || message.status === "reset" ? message.incidentId : null
3257
- );
3258
- workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3259
- if (releasedHead.fetchAuthoritativeHead) {
3260
- pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
3261
- void convergeAvailableWorkspaceHead();
3327
+ activeWorkspaceIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
3328
+ if (previousIncidentId && !activeWorkspaceIncidentId && (message.status === "resolved" || message.status === "confirmed" || message.status === "reset")) {
3329
+ scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
3262
3330
  }
3263
- const explicit = !activeWorkspaceIncidentId ? explicitWorkspacePublications.next() : void 0;
3264
- if (explicit) {
3265
- armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
3266
- } else if (!activeWorkspaceIncidentId && workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3267
- armWorkspacePublication({ type: "periodic", detail: "resume after workspace incident" }, { delayMs: 0 });
3268
- }
3269
- if (!activeWorkspaceIncidentId) schedulePendingManifestCheckoutHydration();
3270
- sendWorkspaceReady();
3271
3331
  return;
3272
3332
  }
3273
3333
  if (message.type === "exec_terminal_ack") {
@@ -3458,7 +3518,7 @@ async function startWorker(options) {
3458
3518
  const activePty = activePtys.get(message.ptyId);
3459
3519
  closePty(message);
3460
3520
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
3461
- armWorkspacePublication({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, { delayMs: 0 });
3521
+ markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true);
3462
3522
  }
3463
3523
  return;
3464
3524
  }
@@ -3555,7 +3615,7 @@ async function startWorker(options) {
3555
3615
  });
3556
3616
  return;
3557
3617
  }
3558
- if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
3618
+ 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 === "code_list" || message.type === "code_read") {
3559
3619
  try {
3560
3620
  const result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
3561
3621
  const resolvedTarget = resolveMessageTarget(message.target);
@@ -3615,20 +3675,13 @@ async function startWorker(options) {
3615
3675
  process.off("SIGTERM", handleGracefulShutdown);
3616
3676
  process.off("SIGINT", handleGracefulShutdown);
3617
3677
  stopHeartbeatWatchdog();
3618
- if (workspacePublicationTimer) {
3619
- clearTimeout(workspacePublicationTimer);
3620
- workspacePublicationTimer = void 0;
3621
- }
3622
- if (!pendingPublication?.workInFlight) {
3623
- workspaceConvergence.resetTransientPublicationState();
3624
- }
3625
- if (workspaceSafetyScan) {
3626
- clearInterval(workspaceSafetyScan);
3627
- workspaceSafetyScan = void 0;
3678
+ if (workspaceAutomaticTimer) {
3679
+ clearTimeout(workspaceAutomaticTimer);
3680
+ workspaceAutomaticTimer = void 0;
3628
3681
  }
3629
- if (workspaceManifestHydrationTimer) {
3630
- clearTimeout(workspaceManifestHydrationTimer);
3631
- workspaceManifestHydrationTimer = void 0;
3682
+ if (workspacePeriodicTimer) {
3683
+ clearInterval(workspacePeriodicTimer);
3684
+ workspacePeriodicTimer = void 0;
3632
3685
  }
3633
3686
  if (terminalReplayTimer) {
3634
3687
  clearInterval(terminalReplayTimer);
@@ -3721,24 +3774,22 @@ if (isCliEntrypoint()) {
3721
3774
  export {
3722
3775
  describeWorkerSessionTarget,
3723
3776
  editWorkerTextFile,
3724
- ensureVisibleGitCheckout,
3725
3777
  findWorkerFiles,
3726
3778
  githubCliEnv,
3727
3779
  grepWorkerFiles,
3728
3780
  isArtifactEnvPath,
3781
+ listWorkerCodeDirectory,
3729
3782
  listWorkerDirectory,
3730
3783
  prepareArtifactEnvForShell,
3731
3784
  prepareBuiltInToolPathsForTarget,
3732
3785
  preparePlanEnvForShell,
3733
3786
  prepareShellEnvForTarget,
3787
+ readWorkerCodeFile,
3734
3788
  readWorkerImageFile,
3735
3789
  readWorkerTextFile,
3736
3790
  resolveHostShell,
3737
3791
  resolveWorkerFilePath,
3738
3792
  resolveWorkerSessionTarget,
3739
3793
  syncSessionArtifacts,
3740
- visibleCheckoutGitTestHarness,
3741
- visibleCheckoutRemoteFor,
3742
- workspaceSyncCheckoutTargets,
3743
3794
  writeWorkerTextFile
3744
3795
  };