@ricsam/r5d-worker 0.0.74 → 0.0.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/cjs/main.cjs +1031 -976
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-workspace-state.cjs +777 -0
- package/dist/cjs/project-worktrees.cjs +559 -0
- package/dist/cjs/working-tree-mirror.cjs +225 -0
- package/dist/cjs/workspace-git-sync.cjs +565 -0
- package/dist/cjs/workspace-incident-state.cjs +2 -38
- package/dist/mjs/main.mjs +1048 -987
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-workspace-state.mjs +745 -0
- package/dist/mjs/project-worktrees.mjs +513 -0
- package/dist/mjs/working-tree-mirror.mjs +190 -0
- package/dist/mjs/workspace-git-sync.mjs +524 -0
- package/dist/mjs/workspace-incident-state.mjs +1 -35
- package/dist/types/main.d.ts +35 -55
- package/dist/types/project-workspace-state.d.ts +144 -0
- package/dist/types/project-worktrees.d.ts +112 -0
- package/dist/types/working-tree-mirror.d.ts +24 -0
- package/dist/types/workspace-git-sync.d.ts +97 -0
- package/dist/types/workspace-incident-state.d.ts +0 -17
- package/dist/types/workspace-mutation-gate.d.ts +3 -3
- package/package.json +1 -1
- package/dist/cjs/workspace-convergence.cjs +0 -280
- package/dist/cjs/workspace-manifest-admission.cjs +0 -60
- package/dist/cjs/workspace-sync.cjs +0 -2469
- package/dist/mjs/workspace-convergence.mjs +0 -236
- package/dist/mjs/workspace-manifest-admission.mjs +0 -26
- package/dist/mjs/workspace-sync.mjs +0 -2417
- package/dist/types/workspace-convergence.d.ts +0 -93
- package/dist/types/workspace-manifest-admission.d.ts +0 -22
- 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
|
|
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,27 @@ import {
|
|
|
38
26
|
} from "./supervisor.mjs";
|
|
39
27
|
import { managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
|
|
40
28
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
29
|
+
cleanupStaleProjectWorktreeSnapshots,
|
|
30
|
+
createLinkedProjectBranch,
|
|
31
|
+
deleteLinkedProjectBranch,
|
|
32
|
+
deleteProjectMirrorBranch,
|
|
33
|
+
ensureProjectWorktrees,
|
|
34
|
+
fastForwardProjectHeadsFromMirror,
|
|
35
|
+
projectWorktreeConfigurationFingerprint,
|
|
36
|
+
projectWorktreeOperationInProgress,
|
|
37
|
+
pushProjectMirrorHeads,
|
|
38
|
+
removeProjectWorktrees
|
|
39
|
+
} from "./project-worktrees.mjs";
|
|
40
|
+
import {
|
|
41
|
+
ProjectWorkspaceStateStore,
|
|
42
|
+
pruneAuthoritativelyDesiredBranchDeletions
|
|
43
|
+
} from "./project-workspace-state.mjs";
|
|
44
|
+
import {
|
|
45
|
+
ensureWorkspaceGitClone,
|
|
46
|
+
hydrateWorkspaceGitMounts,
|
|
47
|
+
resetWorkspaceGit,
|
|
48
|
+
synchronizeWorkspaceGit
|
|
49
|
+
} from "./workspace-git-sync.mjs";
|
|
46
50
|
class WorkerServerUnavailableError extends Error {
|
|
47
51
|
name = "WorkerServerUnavailableError";
|
|
48
52
|
}
|
|
@@ -52,12 +56,34 @@ const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
|
52
56
|
const DEFAULT_READ_LIMIT = 2e3;
|
|
53
57
|
const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
54
58
|
const MAX_LINE_LENGTH = 2e3;
|
|
59
|
+
const WORKSPACE_GIT_QUIET_MS = 5e3;
|
|
60
|
+
const WORKSPACE_GIT_PERIODIC_MS = 6e4;
|
|
55
61
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
56
62
|
const pendingProcessTerminals = /* @__PURE__ */ new Map();
|
|
57
63
|
const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
58
64
|
const activePtys = /* @__PURE__ */ new Map();
|
|
59
65
|
let currentWorkerSocket = null;
|
|
60
|
-
const
|
|
66
|
+
const workspaceMutationGate = new WorkspaceMutationGate();
|
|
67
|
+
let workspaceSyncQueue = Promise.resolve();
|
|
68
|
+
const workspaceSyncSingleFlight = {
|
|
69
|
+
runExclusive(operation) {
|
|
70
|
+
const queued = workspaceMutationGate.runSync(operation);
|
|
71
|
+
workspaceSyncQueue = queued.then(
|
|
72
|
+
() => void 0,
|
|
73
|
+
() => void 0
|
|
74
|
+
);
|
|
75
|
+
return queued;
|
|
76
|
+
},
|
|
77
|
+
runMutation(operation) {
|
|
78
|
+
return workspaceMutationGate.runMutation(operation);
|
|
79
|
+
},
|
|
80
|
+
acquireMutation() {
|
|
81
|
+
return workspaceMutationGate.acquireMutation();
|
|
82
|
+
},
|
|
83
|
+
afterCurrent() {
|
|
84
|
+
return workspaceSyncQueue;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
61
87
|
let githubCredential = null;
|
|
62
88
|
let visibleGitIdentity = null;
|
|
63
89
|
function defaultConfigPath() {
|
|
@@ -620,20 +646,11 @@ function gitAuthArgs(auth) {
|
|
|
620
646
|
const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
|
|
621
647
|
return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
|
|
622
648
|
}
|
|
623
|
-
const
|
|
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]
|
|
649
|
+
const workerGitCommand = {
|
|
650
|
+
commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args]
|
|
628
651
|
};
|
|
629
|
-
function visibleCheckoutCloneArgs(remoteUrl, branchPath) {
|
|
630
|
-
return visibleCheckoutGitTestHarness.cloneArgs(remoteUrl, branchPath);
|
|
631
|
-
}
|
|
632
|
-
function visibleCheckoutFetchArgs(...args) {
|
|
633
|
-
return visibleCheckoutGitTestHarness.fetchArgs(...args);
|
|
634
|
-
}
|
|
635
652
|
function runGit(args, options = {}) {
|
|
636
|
-
const command =
|
|
653
|
+
const command = workerGitCommand.commandArgs(args, options.auth);
|
|
637
654
|
const result = Bun.spawnSync(command, {
|
|
638
655
|
cwd: options.cwd,
|
|
639
656
|
stdout: "pipe",
|
|
@@ -662,9 +679,6 @@ function getGitBlobHashForContent(content) {
|
|
|
662
679
|
const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
663
680
|
return createHash("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
|
|
664
681
|
}
|
|
665
|
-
function hasWorktreeChanges(cwd) {
|
|
666
|
-
return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
|
|
667
|
-
}
|
|
668
682
|
function isInsideBranchPath(branchPath, filePath) {
|
|
669
683
|
const relative = path.relative(path.resolve(branchPath), path.resolve(filePath));
|
|
670
684
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
@@ -774,33 +788,9 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
|
|
|
774
788
|
scope: "project"
|
|
775
789
|
};
|
|
776
790
|
}
|
|
777
|
-
function
|
|
791
|
+
function canonicalProjectRemoteUrl(baseUrl, projectId) {
|
|
778
792
|
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
779
793
|
}
|
|
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
794
|
function gitExtraHeaderUrlForRemote(remoteUrl) {
|
|
805
795
|
try {
|
|
806
796
|
return new URL(remoteUrl).origin;
|
|
@@ -817,44 +807,25 @@ function gitAuthForRemote(remoteUrl, authHeader) {
|
|
|
817
807
|
header: authHeader
|
|
818
808
|
};
|
|
819
809
|
}
|
|
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
810
|
function shellQuote(value) {
|
|
836
811
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
837
812
|
}
|
|
838
813
|
function githubCredentialsPath() {
|
|
839
814
|
return path.join(os.homedir(), ".r5d", "github-credentials");
|
|
840
815
|
}
|
|
841
|
-
function
|
|
816
|
+
function decodeGitAuthHeader(authHeader) {
|
|
842
817
|
const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
|
|
843
|
-
if (
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
if (separatorIndex <= 0) {
|
|
849
|
-
return null;
|
|
818
|
+
if (match) {
|
|
819
|
+
const decoded = Buffer.from(match[1], "base64").toString("utf8");
|
|
820
|
+
const separatorIndex = decoded.indexOf(":");
|
|
821
|
+
if (separatorIndex <= 0) return null;
|
|
822
|
+
return { username: decoded.slice(0, separatorIndex), password: decoded.slice(separatorIndex + 1) };
|
|
850
823
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
password: decoded.slice(separatorIndex + 1)
|
|
854
|
-
};
|
|
824
|
+
const bearer = /^Authorization:\s*Bearer\s+(.+)$/i.exec(authHeader.trim());
|
|
825
|
+
return bearer?.[1] ? { username: "r5d-worker", password: bearer[1] } : null;
|
|
855
826
|
}
|
|
856
827
|
function writeCredentialStoreEntry(remoteUrl, authHeader) {
|
|
857
|
-
const credentials =
|
|
828
|
+
const credentials = decodeGitAuthHeader(authHeader);
|
|
858
829
|
if (!credentials) {
|
|
859
830
|
return null;
|
|
860
831
|
}
|
|
@@ -888,29 +859,9 @@ function writeCredentialStoreEntry(remoteUrl, authHeader) {
|
|
|
888
859
|
fs.chmodSync(storePath, 384);
|
|
889
860
|
return storePath;
|
|
890
861
|
}
|
|
891
|
-
function
|
|
892
|
-
if (!authHeader) {
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
862
|
+
function credentialHelperForRemote(remoteUrl, authHeader) {
|
|
895
863
|
const credentialStore = writeCredentialStoreEntry(remoteUrl, authHeader);
|
|
896
|
-
|
|
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 });
|
|
864
|
+
return credentialStore ? `store --file=${shellQuote(credentialStore)}` : null;
|
|
914
865
|
}
|
|
915
866
|
function dockerConfigPath() {
|
|
916
867
|
return path.join(os.homedir(), ".docker", "config.json");
|
|
@@ -990,188 +941,26 @@ function githubProcessEnv() {
|
|
|
990
941
|
...githubCliEnv(githubCredential?.token)
|
|
991
942
|
};
|
|
992
943
|
}
|
|
993
|
-
function
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
});
|
|
944
|
+
function primaryProjectBranch(config) {
|
|
945
|
+
if (config.branches.length === 0) throw new Error(`Project ${config.projectId} has no configured branches`);
|
|
946
|
+
if (config.branches.some(({ branchName }) => branchName === "main")) return "main";
|
|
947
|
+
if (config.branches.some(({ branchName }) => branchName === config.defaultBranch)) return config.defaultBranch;
|
|
948
|
+
return config.branches[0].branchName;
|
|
1063
949
|
}
|
|
1064
|
-
function
|
|
1065
|
-
|
|
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
|
-
}
|
|
950
|
+
function configuredProjectRoot(projectsRoot, config) {
|
|
951
|
+
return managedProjectRoot(projectsRoot, config.checkoutPathSegments);
|
|
1071
952
|
}
|
|
1072
|
-
function
|
|
1073
|
-
validateBranchName(
|
|
1074
|
-
|
|
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;
|
|
953
|
+
function configuredProjectBranchPath(projectsRoot, config, branchName) {
|
|
954
|
+
validateBranchName(branchName);
|
|
955
|
+
return path.join(configuredProjectRoot(projectsRoot, config), ...branchName.split("/"));
|
|
1157
956
|
}
|
|
1158
|
-
function
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
};
|
|
957
|
+
function hasProjectWorktree(checkoutPath) {
|
|
958
|
+
try {
|
|
959
|
+
const stat = fs.lstatSync(path.join(checkoutPath, ".git"));
|
|
960
|
+
return stat.isFile() || stat.isDirectory();
|
|
961
|
+
} catch {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
1175
964
|
}
|
|
1176
965
|
function describeWorkerSessionTarget(target) {
|
|
1177
966
|
return target.type === "project" ? `${target.projectId}/${target.branchName}` : `${target.ownerUserId}/${target.rootProfile}`;
|
|
@@ -1182,40 +971,21 @@ function resolveWorkerSessionTarget(input) {
|
|
|
1182
971
|
return { target: input.target, rootPath: input.projectsRoot };
|
|
1183
972
|
}
|
|
1184
973
|
if (!fs.existsSync(path.join(input.workspaceShadowRoot, ".git"))) {
|
|
1185
|
-
throw new Error("The
|
|
974
|
+
throw new Error("The workspace sync checkout is not initialized on this worker");
|
|
1186
975
|
}
|
|
1187
976
|
return { target: input.target, rootPath: input.workspaceShadowRoot };
|
|
1188
977
|
}
|
|
1189
|
-
const
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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);
|
|
978
|
+
const target = input.target;
|
|
979
|
+
const config = input.projectConfigById.get(target.projectId);
|
|
980
|
+
if (!config) throw new Error(`Project ${target.projectId} is missing from the worker workspace configuration`);
|
|
981
|
+
if (!config.branches.some(({ branchName }) => branchName === target.branchName)) {
|
|
982
|
+
throw new Error(`Project branch ${target.projectId}/${target.branchName} is missing from the worker workspace configuration`);
|
|
1215
983
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
984
|
+
const branchPath = configuredProjectBranchPath(input.projectsRoot, config, target.branchName);
|
|
985
|
+
if (!hasProjectWorktree(branchPath)) {
|
|
986
|
+
throw new Error(`Project branch ${target.projectId}/${target.branchName} is not initialized on this worker`);
|
|
987
|
+
}
|
|
988
|
+
return { target, rootPath: branchPath, config };
|
|
1219
989
|
}
|
|
1220
990
|
function resolveCommandCwd(branchPath, cwd) {
|
|
1221
991
|
if (!cwd) {
|
|
@@ -1739,6 +1509,77 @@ async function executeLsOperation(input) {
|
|
|
1739
1509
|
});
|
|
1740
1510
|
return listWorkerDirectory(input.resolvedTarget.rootPath, input.message.path, input.message.limit, builtInPaths);
|
|
1741
1511
|
}
|
|
1512
|
+
function resolveWorkerCodePath(branchPath, inputPath) {
|
|
1513
|
+
if (typeof inputPath !== "string" || !inputPath.startsWith("/") || inputPath.includes("\0")) {
|
|
1514
|
+
throw new Error("Code path must be an absolute virtual path");
|
|
1515
|
+
}
|
|
1516
|
+
const rawSegments = inputPath.replace(/\\/g, "/").split("/");
|
|
1517
|
+
if (rawSegments.includes("..")) throw new Error(`Invalid code path: ${inputPath}`);
|
|
1518
|
+
const displayPath = path.posix.normalize(inputPath.replace(/\\/g, "/"));
|
|
1519
|
+
const relativePath = displayPath.replace(/^\/+/, "");
|
|
1520
|
+
const pathSegments = relativePath ? relativePath.split("/") : [];
|
|
1521
|
+
if (pathSegments.some((segment) => segment.toLowerCase() === ".git")) {
|
|
1522
|
+
throw new Error("Code paths cannot access Git metadata");
|
|
1523
|
+
}
|
|
1524
|
+
const resolvedBranchPath = path.resolve(branchPath);
|
|
1525
|
+
const absolutePath = path.resolve(resolvedBranchPath, ...pathSegments);
|
|
1526
|
+
assertInsideRoot(resolvedBranchPath, absolutePath, "Code path");
|
|
1527
|
+
if (!fs.existsSync(absolutePath)) throw new Error(`Code path not found: ${displayPath}`);
|
|
1528
|
+
const realBranchPath = fs.realpathSync(resolvedBranchPath);
|
|
1529
|
+
const realPath = fs.realpathSync(absolutePath);
|
|
1530
|
+
assertInsideRoot(realBranchPath, realPath, "Code path");
|
|
1531
|
+
const realRelativePath = path.relative(realBranchPath, realPath).split(path.sep);
|
|
1532
|
+
if (realRelativePath.some((segment) => segment.toLowerCase() === ".git")) {
|
|
1533
|
+
throw new Error("Code paths cannot access Git metadata");
|
|
1534
|
+
}
|
|
1535
|
+
return { absolutePath, displayPath };
|
|
1536
|
+
}
|
|
1537
|
+
function listWorkerCodeDirectory(branchPath, inputPath) {
|
|
1538
|
+
const resolved = resolveWorkerCodePath(branchPath, inputPath);
|
|
1539
|
+
if (!fs.statSync(resolved.absolutePath).isDirectory()) {
|
|
1540
|
+
throw new Error(`Code directory not found: ${resolved.displayPath}`);
|
|
1541
|
+
}
|
|
1542
|
+
const entries = fs.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => entry.name.toLowerCase() !== ".git").flatMap((entry) => {
|
|
1543
|
+
const entryPath = path.posix.join(resolved.displayPath, entry.name);
|
|
1544
|
+
try {
|
|
1545
|
+
const resolvedEntry = resolveWorkerCodePath(branchPath, entryPath);
|
|
1546
|
+
const stats = fs.statSync(resolvedEntry.absolutePath);
|
|
1547
|
+
if (!stats.isFile() && !stats.isDirectory()) return [];
|
|
1548
|
+
return [
|
|
1549
|
+
{
|
|
1550
|
+
name: entry.name,
|
|
1551
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
1552
|
+
size: stats.isDirectory() ? null : stats.size,
|
|
1553
|
+
mtime: stats.mtime.toISOString()
|
|
1554
|
+
}
|
|
1555
|
+
];
|
|
1556
|
+
} catch {
|
|
1557
|
+
return [];
|
|
1558
|
+
}
|
|
1559
|
+
}).sort((left, right) => Number(right.type === "directory") - Number(left.type === "directory") || left.name.localeCompare(right.name));
|
|
1560
|
+
return { type: "code_list", path: resolved.displayPath, entries };
|
|
1561
|
+
}
|
|
1562
|
+
function readWorkerCodeFile(branchPath, inputPath) {
|
|
1563
|
+
const resolved = resolveWorkerCodePath(branchPath, inputPath);
|
|
1564
|
+
const stats = fs.statSync(resolved.absolutePath);
|
|
1565
|
+
if (!stats.isFile()) throw new Error(`Code file not found: ${resolved.displayPath}`);
|
|
1566
|
+
const bytes = fs.readFileSync(resolved.absolutePath);
|
|
1567
|
+
return {
|
|
1568
|
+
type: "code_read",
|
|
1569
|
+
path: resolved.displayPath,
|
|
1570
|
+
size: bytes.length,
|
|
1571
|
+
mtime: stats.mtime.toISOString(),
|
|
1572
|
+
base64: bytes.toString("base64")
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
function executeCodeListOperation(input) {
|
|
1576
|
+
if (input.resolvedTarget.target.type !== "project") throw new Error("Code listing requires a project branch target");
|
|
1577
|
+
return listWorkerCodeDirectory(input.resolvedTarget.rootPath, input.message.path);
|
|
1578
|
+
}
|
|
1579
|
+
function executeCodeReadOperation(input) {
|
|
1580
|
+
if (input.resolvedTarget.target.type !== "project") throw new Error("Code reading requires a project branch target");
|
|
1581
|
+
return readWorkerCodeFile(input.resolvedTarget.rootPath, input.message.path);
|
|
1582
|
+
}
|
|
1742
1583
|
function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
1743
1584
|
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
1744
1585
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
@@ -1791,6 +1632,10 @@ async function executeOperation(input) {
|
|
|
1791
1632
|
return executeLsOperation({ ...input, message: input.message });
|
|
1792
1633
|
case "view_file_bytes":
|
|
1793
1634
|
return executeViewFileBytesOperation({ ...input, message: input.message });
|
|
1635
|
+
case "code_list":
|
|
1636
|
+
return executeCodeListOperation({ ...input, message: input.message });
|
|
1637
|
+
case "code_read":
|
|
1638
|
+
return executeCodeReadOperation({ ...input, message: input.message });
|
|
1794
1639
|
}
|
|
1795
1640
|
}
|
|
1796
1641
|
function targetIdentityEnv(target) {
|
|
@@ -2390,19 +2235,16 @@ function websocketUrl(baseUrl, label) {
|
|
|
2390
2235
|
url.searchParams.set("version", getWorkerVersion());
|
|
2391
2236
|
return url.toString();
|
|
2392
2237
|
}
|
|
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
2238
|
async function startWorker(options) {
|
|
2399
2239
|
process.on("uncaughtException", (error) => {
|
|
2400
2240
|
process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
2401
2241
|
`);
|
|
2402
2242
|
});
|
|
2403
2243
|
process.on("unhandledRejection", (reason) => {
|
|
2404
|
-
process.stderr.write(
|
|
2405
|
-
`)
|
|
2244
|
+
process.stderr.write(
|
|
2245
|
+
`[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
|
|
2246
|
+
`
|
|
2247
|
+
);
|
|
2406
2248
|
});
|
|
2407
2249
|
const config = readConfig(options.configPath);
|
|
2408
2250
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
@@ -2425,408 +2267,800 @@ async function startWorker(options) {
|
|
|
2425
2267
|
`);
|
|
2426
2268
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
2427
2269
|
`);
|
|
2270
|
+
const snapshotCleanup = cleanupStaleProjectWorktreeSnapshots();
|
|
2271
|
+
if (snapshotCleanup.removed.length > 0) {
|
|
2272
|
+
process.stdout.write(`[r5d-worker] removed ${snapshotCleanup.removed.length} stale project snapshot(s)
|
|
2273
|
+
`);
|
|
2274
|
+
}
|
|
2275
|
+
for (const failure of snapshotCleanup.failed) {
|
|
2276
|
+
process.stderr.write(`[r5d-worker] failed to remove stale project snapshot ${failure.path}: ${failure.error}
|
|
2277
|
+
`);
|
|
2278
|
+
}
|
|
2428
2279
|
runGit(["--version"]);
|
|
2429
2280
|
await verifyR5dctlAuth(baseUrl, token);
|
|
2430
2281
|
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
2431
2282
|
fs.mkdirSync(syncRoot, { recursive: true });
|
|
2432
2283
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
2433
2284
|
fs.mkdirSync(planRoot, { recursive: true });
|
|
2434
|
-
const
|
|
2435
|
-
|
|
2285
|
+
const projectWorkspaceStateStore = new ProjectWorkspaceStateStore({
|
|
2286
|
+
stateRoot: path.join(syncRoot, "project-state"),
|
|
2287
|
+
projectsRoot
|
|
2288
|
+
});
|
|
2289
|
+
let projectWorkspaceState = projectWorkspaceStateStore.read();
|
|
2290
|
+
const projectConfigById = /* @__PURE__ */ new Map();
|
|
2291
|
+
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
2292
|
+
const readyProjectIds = /* @__PURE__ */ new Set();
|
|
2293
|
+
const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
|
|
2294
|
+
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
2295
|
+
const pendingMirrorDeletes = /* @__PURE__ */ new Map();
|
|
2436
2296
|
let workspaceRemoteUrl = null;
|
|
2297
|
+
let workspaceGitIdentity = null;
|
|
2298
|
+
let workspaceConfigured = false;
|
|
2437
2299
|
let activeWorkspaceIncidentId = null;
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
let
|
|
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();
|
|
2300
|
+
let workspaceAutomaticTimer;
|
|
2301
|
+
let workspacePeriodicTimer;
|
|
2302
|
+
let pendingAutomaticTrigger;
|
|
2303
|
+
let automaticSyncInFlight = false;
|
|
2451
2304
|
let terminalReplayTimer;
|
|
2452
2305
|
let workspaceSyncRequestsInFlight = 0;
|
|
2453
2306
|
let sessionArtifactSyncRequestsInFlight = 0;
|
|
2454
2307
|
let cliUpdateInProgress = false;
|
|
2455
2308
|
let reloadAfterClose = false;
|
|
2456
2309
|
let shutdownAfterClose = false;
|
|
2457
|
-
const
|
|
2458
|
-
|
|
2459
|
-
|
|
2310
|
+
const bearerAuthHeader = `Authorization: Bearer ${token}`;
|
|
2311
|
+
const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
|
|
2312
|
+
const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
|
|
2313
|
+
const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
|
|
2314
|
+
const workspaceProjectRelativePath = (projectId, branchName) => path.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
|
|
2315
|
+
const workspacePlanRelativePath = (projectId, branchName) => path.posix.join("plans", projectId, encodeURIComponent(branchName));
|
|
2316
|
+
const projectConnection = (project) => {
|
|
2317
|
+
const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
|
|
2318
|
+
const mirrorAuth = gitAuthForRemote(mirrorUrl, bearerAuthHeader);
|
|
2319
|
+
const originUrl = project.repoHttpUrl ?? mirrorUrl;
|
|
2320
|
+
const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
|
|
2321
|
+
const originAuth = gitAuthForRemote(originUrl, originHeader);
|
|
2322
|
+
const credentialHelper = originHeader ? credentialHelperForRemote(originUrl, originHeader) : null;
|
|
2323
|
+
return { mirrorUrl, mirrorAuth, originUrl, originAuth, credentialHelper };
|
|
2324
|
+
};
|
|
2325
|
+
const durableDeletionForBranch = (projectId, checkoutPathSegments, branchName) => projectWorkspaceState.tombstones.find(
|
|
2326
|
+
(tombstone) => tombstone.projectId === projectId && tombstone.checkoutPathSegments[0] === checkoutPathSegments[0] && tombstone.checkoutPathSegments[1] === checkoutPathSegments[1] && (tombstone.kind === "project" || tombstone.branchName === branchName)
|
|
2327
|
+
);
|
|
2328
|
+
const stageProjectBranchDeletion = (project, branchName, requireLocalBranch) => {
|
|
2329
|
+
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
2330
|
+
const primaryBranchName = primaryProjectBranch(project);
|
|
2331
|
+
const connection = projectConnection(project);
|
|
2332
|
+
const sourcePath = configuredProjectBranchPath(projectsRoot, project, branchName);
|
|
2333
|
+
if (requireLocalBranch || hasProjectWorktree(sourcePath)) {
|
|
2334
|
+
deleteLinkedProjectBranch({ projectRoot, primaryBranchName, branchName });
|
|
2335
|
+
}
|
|
2336
|
+
const planSourcePath = path.join(planRoot, project.projectId, ...branchName.split("/"));
|
|
2337
|
+
fs.rmSync(planSourcePath, { recursive: true, force: true });
|
|
2338
|
+
const key = projectBranchKey(project.projectId, branchName);
|
|
2339
|
+
const durableDeletion = durableDeletionForBranch(project.projectId, project.checkoutPathSegments, branchName);
|
|
2340
|
+
pendingCheckouts.delete(key);
|
|
2341
|
+
pendingMirrorDeletes.set(key, {
|
|
2342
|
+
id: projectMountId(project.projectId, branchName),
|
|
2343
|
+
projectId: project.projectId,
|
|
2344
|
+
branchName,
|
|
2345
|
+
gitDirectory: configuredProjectBranchPath(projectsRoot, project, primaryBranchName),
|
|
2346
|
+
mirrorUrl: connection.mirrorUrl,
|
|
2347
|
+
mirrorAuth: connection.mirrorAuth,
|
|
2348
|
+
sourcePath,
|
|
2349
|
+
workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branchName),
|
|
2350
|
+
planSourcePath,
|
|
2351
|
+
planWorkspaceRelativePath: workspacePlanRelativePath(project.projectId, branchName),
|
|
2352
|
+
...durableDeletion ? { tombstoneId: durableDeletion.id } : {},
|
|
2353
|
+
...durableDeletion?.kind === "project" ? { projectDeleted: !projectWorkspaceState.desiredProjects.some(({ projectId }) => projectId === project.projectId) } : {}
|
|
2354
|
+
});
|
|
2355
|
+
};
|
|
2356
|
+
const stageProjectDeletion = (project) => {
|
|
2357
|
+
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
2358
|
+
const connection = projectConnection(project);
|
|
2359
|
+
const branches = project.branches.map(({ branchName }) => ({
|
|
2360
|
+
branchName,
|
|
2361
|
+
sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
|
|
2362
|
+
planSourcePath: path.join(planRoot, project.projectId, ...branchName.split("/"))
|
|
2363
|
+
}));
|
|
2364
|
+
removeProjectWorktrees({ projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
|
|
2365
|
+
for (const branch of branches) {
|
|
2366
|
+
fs.rmSync(branch.planSourcePath, { recursive: true, force: true });
|
|
2367
|
+
const key = projectBranchKey(project.projectId, branch.branchName);
|
|
2368
|
+
const durableDeletion = durableDeletionForBranch(project.projectId, project.checkoutPathSegments, branch.branchName);
|
|
2369
|
+
pendingCheckouts.delete(key);
|
|
2370
|
+
pendingMirrorDeletes.set(key, {
|
|
2371
|
+
id: projectMountId(project.projectId, branch.branchName),
|
|
2372
|
+
projectId: project.projectId,
|
|
2373
|
+
branchName: branch.branchName,
|
|
2374
|
+
// The project Git directory is intentionally gone before the outer
|
|
2375
|
+
// tree is published. Use the outer workspace repository to delete the
|
|
2376
|
+
// hidden ref only after that publication succeeds.
|
|
2377
|
+
gitDirectory: workspaceShadowRoot,
|
|
2378
|
+
mirrorUrl: connection.mirrorUrl,
|
|
2379
|
+
mirrorAuth: connection.mirrorAuth,
|
|
2380
|
+
sourcePath: branch.sourcePath,
|
|
2381
|
+
workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
|
|
2382
|
+
planSourcePath: branch.planSourcePath,
|
|
2383
|
+
planWorkspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
|
|
2384
|
+
...durableDeletion ? { tombstoneId: durableDeletion.id } : {},
|
|
2385
|
+
...durableDeletion?.kind === "project" ? { projectDeleted: !projectWorkspaceState.desiredProjects.some(({ projectId }) => projectId === project.projectId) } : {}
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
readyProjectIds.delete(project.projectId);
|
|
2389
|
+
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
2390
|
+
};
|
|
2391
|
+
const cleanupConfigForDurableDeletion = (deletion) => {
|
|
2392
|
+
const desiredLayout = projectWorkspaceState.desiredProjects.find(
|
|
2393
|
+
(project) => project.projectId === deletion.projectId && project.checkoutPathSegments[0] === deletion.checkoutPathSegments[0] && project.checkoutPathSegments[1] === deletion.checkoutPathSegments[1]
|
|
2394
|
+
);
|
|
2395
|
+
const liveConfig = projectConfigById.get(deletion.projectId);
|
|
2396
|
+
const branchNames = deletion.kind === "project" ? deletion.branchNames : desiredLayout?.branches.map(({ branchName }) => branchName) ?? [];
|
|
2397
|
+
const defaultBranch = desiredLayout?.defaultBranch ?? (branchNames.includes("main") ? "main" : branchNames[0] ?? "main");
|
|
2460
2398
|
return {
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2399
|
+
projectId: deletion.projectId,
|
|
2400
|
+
checkoutPathSegments: [...deletion.checkoutPathSegments],
|
|
2401
|
+
projectPath: liveConfig?.projectPath ?? deletion.checkoutPathSegments.join("/"),
|
|
2402
|
+
repoHttpUrl: liveConfig?.repoHttpUrl ?? null,
|
|
2403
|
+
repoAuthHeader: liveConfig?.repoAuthHeader ?? null,
|
|
2404
|
+
defaultBranch,
|
|
2405
|
+
branches: branchNames.map(
|
|
2406
|
+
(branchName) => liveConfig?.branches.find((branch) => branch.branchName === branchName) ?? {
|
|
2407
|
+
branchName,
|
|
2408
|
+
sourceBranchName: null,
|
|
2409
|
+
// Deletion never consumes the baseline, but retain a structurally
|
|
2410
|
+
// valid value for the shared worker-project shape.
|
|
2411
|
+
baseCommitHash: "0".repeat(40)
|
|
2412
|
+
}
|
|
2413
|
+
)
|
|
2470
2414
|
};
|
|
2471
2415
|
};
|
|
2472
|
-
const
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2416
|
+
const stageDurableWorkspaceDeletions = () => {
|
|
2417
|
+
for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
|
|
2418
|
+
const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
|
|
2419
|
+
if (deletion.kind === "project") {
|
|
2420
|
+
if (!deletion.projectDeleted) {
|
|
2421
|
+
if (fs.existsSync(deletion.managedPath)) {
|
|
2422
|
+
removeProjectWorktrees({ projectRoot: deletion.managedPath, branchNames: deletion.branchNames });
|
|
2423
|
+
}
|
|
2424
|
+
readyProjectIds.delete(deletion.projectId);
|
|
2425
|
+
reconciledProjectConfigFingerprints.delete(deletion.projectId);
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
stageProjectDeletion(cleanupConfig);
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
const primaryPath = deletion.primaryBranchName ? configuredProjectBranchPath(projectsRoot, cleanupConfig, deletion.primaryBranchName) : null;
|
|
2432
|
+
stageProjectBranchDeletion(cleanupConfig, deletion.branchName, Boolean(primaryPath && hasProjectWorktree(primaryPath)));
|
|
2433
|
+
}
|
|
2434
|
+
for (const deletion of projectWorkspaceState.pendingMirrorRefDeletions) {
|
|
2435
|
+
const key = projectBranchKey(deletion.projectId, deletion.branchName);
|
|
2436
|
+
const planSourcePath = path.join(planRoot, deletion.projectId, ...deletion.branchName.split("/"));
|
|
2437
|
+
fs.rmSync(planSourcePath, { recursive: true, force: true });
|
|
2438
|
+
pendingMirrorDeletes.set(key, {
|
|
2439
|
+
id: projectMountId(deletion.projectId, deletion.branchName),
|
|
2440
|
+
projectId: deletion.projectId,
|
|
2441
|
+
branchName: deletion.branchName,
|
|
2442
|
+
gitDirectory: workspaceShadowRoot,
|
|
2443
|
+
mirrorUrl: canonicalProjectRemoteUrl(baseUrl, deletion.projectId),
|
|
2444
|
+
mirrorAuth: gitAuthForRemote(canonicalProjectRemoteUrl(baseUrl, deletion.projectId), bearerAuthHeader),
|
|
2445
|
+
sourcePath: deletion.managedPath,
|
|
2446
|
+
workspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, deletion.branchName),
|
|
2447
|
+
planSourcePath,
|
|
2448
|
+
planWorkspaceRelativePath: workspacePlanRelativePath(deletion.projectId, deletion.branchName),
|
|
2449
|
+
tombstoneId: deletion.tombstoneId
|
|
2450
|
+
});
|
|
2480
2451
|
}
|
|
2481
2452
|
};
|
|
2482
|
-
const
|
|
2483
|
-
const
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2453
|
+
const pruneStaleOuterWorkspaceEntries = (entries) => {
|
|
2454
|
+
const remove = (candidate) => {
|
|
2455
|
+
if (candidate) fs.rmSync(candidate, { recursive: true, force: true });
|
|
2456
|
+
};
|
|
2457
|
+
for (const entry of entries) {
|
|
2458
|
+
if (entry.kind === "branch") {
|
|
2459
|
+
projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
|
|
2460
|
+
projectId: entry.projectId,
|
|
2461
|
+
branchName: entry.branchName
|
|
2462
|
+
});
|
|
2463
|
+
remove(entry.projectTreePath);
|
|
2464
|
+
remove(entry.planTreePath);
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
remove(entry.projectTreePath);
|
|
2468
|
+
remove(entry.planTreePath);
|
|
2469
|
+
for (const branch of entry.branches) {
|
|
2470
|
+
remove(branch.projectTreePath);
|
|
2471
|
+
remove(branch.planTreePath);
|
|
2472
|
+
}
|
|
2499
2473
|
}
|
|
2500
2474
|
};
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
if (
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2475
|
+
const effectiveWorkerProjects = (projects) => {
|
|
2476
|
+
const incomingById = new Map(projects.map((project) => [project.projectId, project]));
|
|
2477
|
+
const pending = new Set(
|
|
2478
|
+
projectWorkspaceState.locallyPendingCreatedBranches.map(({ projectId, branchName }) => projectBranchKey(projectId, branchName))
|
|
2479
|
+
);
|
|
2480
|
+
return projectWorkspaceState.effectiveDesiredProjects.flatMap((layout) => {
|
|
2481
|
+
const incoming = incomingById.get(layout.projectId);
|
|
2482
|
+
if (!incoming) return [];
|
|
2483
|
+
const existing = projectConfigById.get(layout.projectId);
|
|
2484
|
+
const branches = layout.branches.flatMap(({ branchName }) => {
|
|
2485
|
+
const configured = incoming.branches.find((branch) => branch.branchName === branchName);
|
|
2486
|
+
if (configured) return [configured];
|
|
2487
|
+
const retained = existing?.branches.find((branch) => branch.branchName === branchName);
|
|
2488
|
+
if (retained) return [retained];
|
|
2489
|
+
if (!pending.has(projectBranchKey(layout.projectId, branchName))) return [];
|
|
2490
|
+
const branchPath = configuredProjectBranchPath(
|
|
2491
|
+
projectsRoot,
|
|
2492
|
+
{ ...incoming, checkoutPathSegments: layout.checkoutPathSegments },
|
|
2493
|
+
branchName
|
|
2494
|
+
);
|
|
2495
|
+
if (!hasProjectWorktree(branchPath)) return [];
|
|
2496
|
+
return [
|
|
2497
|
+
{
|
|
2498
|
+
branchName,
|
|
2499
|
+
sourceBranchName: null,
|
|
2500
|
+
baseCommitHash: runGit(["rev-parse", "HEAD"], { cwd: branchPath })
|
|
2527
2501
|
}
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2502
|
+
];
|
|
2503
|
+
});
|
|
2504
|
+
return [{ ...incoming, checkoutPathSegments: [...layout.checkoutPathSegments], branches }];
|
|
2505
|
+
});
|
|
2506
|
+
};
|
|
2507
|
+
const buildWorkspaceMounts = () => {
|
|
2508
|
+
const mounts = [];
|
|
2509
|
+
for (const project of [...projectConfigById.values()].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
|
|
2510
|
+
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
2511
|
+
for (const branch of [...project.branches].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
|
|
2512
|
+
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
2513
|
+
mounts.push({
|
|
2514
|
+
id: projectMountId(project.projectId, branch.branchName),
|
|
2515
|
+
sourcePath: branchPath,
|
|
2516
|
+
workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
|
|
2517
|
+
sourceMode: "git",
|
|
2518
|
+
hydrateDeletionMode: "git",
|
|
2519
|
+
busy: () => hasProjectWorktree(branchPath) && projectWorktreeOperationInProgress(branchPath)
|
|
2520
|
+
});
|
|
2521
|
+
mounts.push({
|
|
2522
|
+
id: projectPlanMountId(project.projectId, branch.branchName),
|
|
2523
|
+
sourcePath: path.join(planRoot, project.projectId, ...branch.branchName.split("/")),
|
|
2524
|
+
workspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
|
|
2525
|
+
sourceMode: "all",
|
|
2526
|
+
hydrateDeletionMode: "all"
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
void projectRoot;
|
|
2530
|
+
}
|
|
2531
|
+
mounts.push({
|
|
2532
|
+
id: "workspace-plans",
|
|
2533
|
+
sourcePath: path.join(planRoot, "workspace"),
|
|
2534
|
+
workspaceRelativePath: "workspace-plans",
|
|
2535
|
+
sourceMode: "all",
|
|
2536
|
+
hydrateDeletionMode: "all"
|
|
2537
|
+
});
|
|
2538
|
+
for (const deletion of pendingMirrorDeletes.values()) {
|
|
2539
|
+
mounts.push({
|
|
2540
|
+
id: deletion.id,
|
|
2541
|
+
sourcePath: deletion.sourcePath,
|
|
2542
|
+
workspaceRelativePath: deletion.workspaceRelativePath,
|
|
2543
|
+
sourceMode: "git",
|
|
2544
|
+
hydrateDeletionMode: "git",
|
|
2545
|
+
deleteWhenSourceMissing: true
|
|
2546
|
+
});
|
|
2547
|
+
mounts.push({
|
|
2548
|
+
id: `${deletion.id}:plan`,
|
|
2549
|
+
sourcePath: deletion.planSourcePath,
|
|
2550
|
+
workspaceRelativePath: deletion.planWorkspaceRelativePath,
|
|
2551
|
+
sourceMode: "all",
|
|
2552
|
+
hydrateDeletionMode: "all",
|
|
2553
|
+
deleteWhenSourceMissing: true
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
return mounts;
|
|
2557
|
+
};
|
|
2558
|
+
const workspaceLocalHead = () => fs.existsSync(path.join(workspaceShadowRoot, ".git")) && tryGit(["rev-parse", "--verify", "HEAD"], { cwd: workspaceShadowRoot }) ? runGit(["rev-parse", "HEAD"], { cwd: workspaceShadowRoot }) : null;
|
|
2559
|
+
const workspaceGitStatus = () => {
|
|
2560
|
+
try {
|
|
2561
|
+
return runGit(["status", "--porcelain"], { cwd: workspaceShadowRoot });
|
|
2562
|
+
} catch {
|
|
2563
|
+
return "";
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
const affectedProjects = (paths) => [
|
|
2567
|
+
...new Set(
|
|
2568
|
+
paths.flatMap((filePath) => {
|
|
2569
|
+
const match = /^(?:projects|plans)\/([^/]+)\//.exec(filePath);
|
|
2570
|
+
return match?.[1] ? [match[1]] : [];
|
|
2571
|
+
})
|
|
2572
|
+
)
|
|
2573
|
+
].sort();
|
|
2574
|
+
const ensureConfiguredProjects = () => {
|
|
2575
|
+
for (const project of projectConfigById.values()) {
|
|
2576
|
+
validateProjectId(project.projectId);
|
|
2577
|
+
for (const branch of project.branches) validateBranchName(branch.branchName);
|
|
2578
|
+
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
2579
|
+
const configFingerprint = projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: workspaceGitIdentity });
|
|
2580
|
+
const alreadyReady = readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === configFingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
|
|
2581
|
+
if (alreadyReady) {
|
|
2582
|
+
for (const branch of project.branches) pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
|
|
2583
|
+
continue;
|
|
2584
|
+
}
|
|
2585
|
+
try {
|
|
2586
|
+
const connection = projectConnection(project);
|
|
2587
|
+
const states = ensureProjectWorktrees({
|
|
2588
|
+
projectRoot,
|
|
2589
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
2590
|
+
branches: project.branches,
|
|
2591
|
+
originUrl: connection.originUrl,
|
|
2592
|
+
originAuth: connection.originAuth,
|
|
2593
|
+
mirrorUrl: connection.mirrorUrl,
|
|
2594
|
+
mirrorAuth: connection.mirrorAuth,
|
|
2595
|
+
credentialHelper: connection.credentialHelper,
|
|
2596
|
+
gitIdentity: workspaceGitIdentity
|
|
2597
|
+
});
|
|
2598
|
+
readyProjectIds.add(project.projectId);
|
|
2599
|
+
reconciledProjectConfigFingerprints.set(project.projectId, configFingerprint);
|
|
2600
|
+
for (const state of states) {
|
|
2601
|
+
pendingCheckouts.delete(projectBranchKey(project.projectId, state.branchName));
|
|
2602
|
+
const key = projectBranchKey(project.projectId, state.branchName);
|
|
2603
|
+
if (!lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
|
|
2604
|
+
if ((state.ahead ?? 0) > 0) {
|
|
2552
2605
|
process.stderr.write(
|
|
2553
|
-
`[r5d-worker]
|
|
2606
|
+
`[r5d-worker] ${project.projectPath}/${state.branchName} is ${state.ahead} commit(s) ahead of origin; switching workers may require publishing or reconciling those commits
|
|
2554
2607
|
`
|
|
2555
2608
|
);
|
|
2556
|
-
armWorkspacePublication(trigger, { delayMs: 1e3, requestId: options2.requestId });
|
|
2557
2609
|
}
|
|
2558
|
-
return;
|
|
2559
2610
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
readyProjectIds.delete(project.projectId);
|
|
2613
|
+
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
2614
|
+
for (const branch of project.branches) {
|
|
2615
|
+
pendingCheckouts.set(projectBranchKey(project.projectId, branch.branchName), {
|
|
2616
|
+
projectId: project.projectId,
|
|
2617
|
+
branchName: branch.branchName
|
|
2618
|
+
});
|
|
2563
2619
|
}
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2620
|
+
process.stderr.write(
|
|
2621
|
+
`[r5d-worker] failed to initialize project ${project.projectPath}: ${error instanceof Error ? error.message : String(error)}
|
|
2622
|
+
`
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2627
|
+
const collectAheadOfOriginBranches = () => {
|
|
2628
|
+
const aheadBranches = [];
|
|
2629
|
+
for (const project of projectConfigById.values()) {
|
|
2630
|
+
if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
2631
|
+
const connection = projectConnection(project);
|
|
2632
|
+
const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
|
|
2633
|
+
if (!tryGit(["fetch", "--no-recurse-submodules", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*"], {
|
|
2634
|
+
cwd: primaryPath,
|
|
2635
|
+
auth: connection.originAuth
|
|
2636
|
+
})) {
|
|
2637
|
+
process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
|
|
2638
|
+
`);
|
|
2639
|
+
}
|
|
2640
|
+
for (const branch of project.branches) {
|
|
2641
|
+
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
2642
|
+
if (!hasProjectWorktree(branchPath)) continue;
|
|
2575
2643
|
try {
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
);
|
|
2644
|
+
let ahead = 0;
|
|
2645
|
+
const originRef = `refs/remotes/origin/${branch.branchName}`;
|
|
2646
|
+
if (tryGit(["show-ref", "--verify", "--quiet", originRef], { cwd: branchPath })) {
|
|
2647
|
+
const counts = runGit(["rev-list", "--left-right", "--count", `${originRef}...HEAD`], { cwd: branchPath }).split(/\s+/).map(Number);
|
|
2648
|
+
ahead = Number.isSafeInteger(counts[1]) ? counts[1] : 0;
|
|
2649
|
+
} else if (tryGit(["cat-file", "-e", `${branch.baseCommitHash}^{commit}`], { cwd: branchPath })) {
|
|
2650
|
+
const count = Number(runGit(["rev-list", "--count", `${branch.baseCommitHash}..HEAD`], { cwd: branchPath }));
|
|
2651
|
+
ahead = Number.isSafeInteger(count) ? count : 0;
|
|
2652
|
+
}
|
|
2653
|
+
if (ahead > 0) aheadBranches.push({ projectId: project.projectId, branchName: branch.branchName, ahead });
|
|
2586
2654
|
} catch (error) {
|
|
2587
|
-
pendingPublication = void 0;
|
|
2588
|
-
workspaceConvergence.defer();
|
|
2589
2655
|
process.stderr.write(
|
|
2590
|
-
`[r5d-worker]
|
|
2656
|
+
`[r5d-worker] could not measure origin divergence for ${project.projectPath}/${branch.branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
2591
2657
|
`
|
|
2592
2658
|
);
|
|
2593
|
-
armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
|
|
2594
2659
|
}
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
return aheadBranches.sort(
|
|
2663
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
2597
2664
|
);
|
|
2598
|
-
workspacePublicationTimer.unref();
|
|
2599
2665
|
};
|
|
2600
|
-
const
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2666
|
+
const pushChangedProjectHeads = (activeMountIds, publishedHead) => {
|
|
2667
|
+
const active = new Set(activeMountIds);
|
|
2668
|
+
for (const project of projectConfigById.values()) {
|
|
2669
|
+
if (!readyProjectIds.has(project.projectId)) continue;
|
|
2670
|
+
const changed = /* @__PURE__ */ new Set();
|
|
2671
|
+
for (const branch of project.branches) {
|
|
2672
|
+
if (!active.has(projectMountId(project.projectId, branch.branchName))) continue;
|
|
2673
|
+
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
2674
|
+
if (!hasProjectWorktree(branchPath)) continue;
|
|
2675
|
+
const head = runGit(["rev-parse", "HEAD"], { cwd: branchPath });
|
|
2676
|
+
if (lastObservedProjectHeads.get(projectBranchKey(project.projectId, branch.branchName)) !== head) {
|
|
2677
|
+
changed.add(branch.branchName);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
if (changed.size === 0) continue;
|
|
2681
|
+
const connection = projectConnection(project);
|
|
2682
|
+
const results = pushProjectMirrorHeads({
|
|
2683
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
2684
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
2685
|
+
mirrorUrl: connection.mirrorUrl,
|
|
2686
|
+
mirrorAuth: connection.mirrorAuth,
|
|
2687
|
+
onlyBranches: changed
|
|
2688
|
+
});
|
|
2689
|
+
for (const result of results) {
|
|
2690
|
+
if (result.pushed) lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
|
|
2691
|
+
}
|
|
2607
2692
|
}
|
|
2608
|
-
const
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
);
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
if (
|
|
2623
|
-
|
|
2693
|
+
const publishedTombstoneIds = [
|
|
2694
|
+
...new Set(
|
|
2695
|
+
[...pendingMirrorDeletes.values()].flatMap(
|
|
2696
|
+
(deletion) => active.has(deletion.id) && deletion.tombstoneId ? [deletion.tombstoneId] : []
|
|
2697
|
+
)
|
|
2698
|
+
)
|
|
2699
|
+
];
|
|
2700
|
+
if (publishedTombstoneIds.length > 0) {
|
|
2701
|
+
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
2702
|
+
tombstoneIds: publishedTombstoneIds,
|
|
2703
|
+
publishedHead
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
for (const [key, deletion] of pendingMirrorDeletes) {
|
|
2707
|
+
if (!active.has(deletion.id)) continue;
|
|
2708
|
+
if (!deletion.projectDeleted) {
|
|
2709
|
+
deleteProjectMirrorBranch({
|
|
2710
|
+
gitDirectory: deletion.gitDirectory,
|
|
2711
|
+
branchName: deletion.branchName,
|
|
2712
|
+
mirrorUrl: deletion.mirrorUrl,
|
|
2713
|
+
mirrorAuth: deletion.mirrorAuth
|
|
2714
|
+
});
|
|
2624
2715
|
}
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2716
|
+
if (deletion.tombstoneId && !deletion.projectDeleted) {
|
|
2717
|
+
projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
2718
|
+
tombstoneId: deletion.tombstoneId,
|
|
2719
|
+
branchName: deletion.branchName
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
pendingMirrorDeletes.delete(key);
|
|
2723
|
+
lastObservedProjectHeads.delete(key);
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
const refreshProjectHeadsAfterInboundWorkspace = () => {
|
|
2727
|
+
for (const project of projectConfigById.values()) {
|
|
2728
|
+
if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
2729
|
+
try {
|
|
2730
|
+
const connection = projectConnection(project);
|
|
2731
|
+
const refreshed = fastForwardProjectHeadsFromMirror({
|
|
2732
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
2733
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
2734
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
2735
|
+
mirrorUrl: connection.mirrorUrl,
|
|
2736
|
+
mirrorAuth: connection.mirrorAuth
|
|
2737
|
+
});
|
|
2738
|
+
for (const state of refreshed) {
|
|
2739
|
+
lastObservedProjectHeads.set(
|
|
2740
|
+
projectBranchKey(project.projectId, state.branchName),
|
|
2741
|
+
state.fastForwarded && state.mirrorHead ? state.mirrorHead : state.previousHead
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
} catch (error) {
|
|
2745
|
+
process.stderr.write(
|
|
2746
|
+
`[r5d-worker] failed to refresh project heads after workspace update: ${error instanceof Error ? error.message : String(error)}
|
|
2630
2747
|
`
|
|
2631
|
-
|
|
2632
|
-
} finally {
|
|
2633
|
-
workspaceHeadConvergenceInFlight = false;
|
|
2634
|
-
heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
|
|
2635
|
-
if (pendingWorkspaceHead !== void 0) {
|
|
2636
|
-
setTimeout(() => void convergeAvailableWorkspaceHead(), 1e3).unref();
|
|
2748
|
+
);
|
|
2637
2749
|
}
|
|
2638
2750
|
}
|
|
2639
2751
|
};
|
|
2640
|
-
const
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2752
|
+
const reseedProjectHeadsAfterWorkspaceReset = () => {
|
|
2753
|
+
for (const project of projectConfigById.values()) {
|
|
2754
|
+
if (!readyProjectIds.has(project.projectId)) continue;
|
|
2755
|
+
const connection = projectConnection(project);
|
|
2756
|
+
const states = ensureProjectWorktrees({
|
|
2757
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
2758
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
2759
|
+
branches: project.branches,
|
|
2760
|
+
originUrl: connection.originUrl,
|
|
2761
|
+
originAuth: connection.originAuth,
|
|
2762
|
+
mirrorUrl: connection.mirrorUrl,
|
|
2763
|
+
mirrorAuth: connection.mirrorAuth,
|
|
2764
|
+
credentialHelper: connection.credentialHelper,
|
|
2765
|
+
gitIdentity: workspaceGitIdentity
|
|
2766
|
+
});
|
|
2767
|
+
for (const state of states) {
|
|
2768
|
+
lastObservedProjectHeads.set(projectBranchKey(project.projectId, state.branchName), state.head);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
const mapWorkspaceGitResult = (attemptId, trigger, result) => ({
|
|
2773
|
+
type: "workspace_sync",
|
|
2774
|
+
attemptId,
|
|
2775
|
+
workerLabel: label,
|
|
2776
|
+
trigger,
|
|
2777
|
+
outcome: result.outcome === "pushed" ? "published" : result.outcome,
|
|
2778
|
+
startingHead: result.startingHead,
|
|
2779
|
+
...result.localHead ? { localHead: result.localHead } : {},
|
|
2780
|
+
...result.publishedHead ? { publishedHead: result.publishedHead } : {},
|
|
2781
|
+
rebaseCount: result.rebaseCount,
|
|
2782
|
+
diffSizeBytes: result.diffSizeBytes,
|
|
2783
|
+
gitStatus: workspaceGitStatus(),
|
|
2784
|
+
affectedProjects: affectedProjects(result.affectedPaths),
|
|
2785
|
+
affectedPaths: result.affectedPaths,
|
|
2786
|
+
discardedPaths: [],
|
|
2787
|
+
localChangesDiscarded: false,
|
|
2788
|
+
...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
|
|
2789
|
+
...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
|
|
2790
|
+
...result.error ? { error: result.error } : {}
|
|
2652
2791
|
});
|
|
2653
|
-
const
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2792
|
+
const failedWorkspaceSyncResult = (attemptId, trigger, error) => ({
|
|
2793
|
+
type: "workspace_sync",
|
|
2794
|
+
attemptId,
|
|
2795
|
+
workerLabel: label,
|
|
2796
|
+
trigger,
|
|
2797
|
+
outcome: "failed",
|
|
2798
|
+
startingHead: workspaceLocalHead(),
|
|
2799
|
+
...workspaceLocalHead() ? { localHead: workspaceLocalHead() } : {},
|
|
2800
|
+
rebaseCount: 0,
|
|
2801
|
+
diffSizeBytes: 0,
|
|
2802
|
+
gitStatus: workspaceGitStatus(),
|
|
2803
|
+
affectedProjects: [],
|
|
2804
|
+
affectedPaths: [],
|
|
2805
|
+
discardedPaths: [],
|
|
2806
|
+
localChangesDiscarded: false,
|
|
2807
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2808
|
+
});
|
|
2809
|
+
const performWorkspaceSync = async (input) => {
|
|
2810
|
+
if (!workspaceRemoteUrl || !workspaceGitIdentity) throw new Error("Worker workspace configuration has not been received");
|
|
2811
|
+
const mounts = buildWorkspaceMounts();
|
|
2812
|
+
const remoteAuth = gitAuthForRemote(workspaceRemoteUrl, bearerAuthHeader);
|
|
2813
|
+
const credentialHelper = credentialHelperForRemote(workspaceRemoteUrl, bearerAuthHeader);
|
|
2814
|
+
if (input.resetToCanonical) {
|
|
2815
|
+
const reset = resetWorkspaceGit({
|
|
2816
|
+
workspacePath: workspaceShadowRoot,
|
|
2817
|
+
remoteUrl: workspaceRemoteUrl,
|
|
2818
|
+
remoteAuth,
|
|
2819
|
+
credentialHelper,
|
|
2820
|
+
gitIdentity: workspaceGitIdentity,
|
|
2821
|
+
mounts
|
|
2822
|
+
});
|
|
2823
|
+
reseedProjectHeadsAfterWorkspaceReset();
|
|
2824
|
+
return {
|
|
2825
|
+
type: "workspace_sync",
|
|
2826
|
+
attemptId: input.attemptId,
|
|
2827
|
+
workerLabel: label,
|
|
2828
|
+
trigger: input.trigger,
|
|
2829
|
+
outcome: "reset",
|
|
2830
|
+
startingHead: reset.startingHead,
|
|
2831
|
+
...reset.localHead ? { localHead: reset.localHead } : {},
|
|
2832
|
+
...reset.remoteHead ? { publishedHead: reset.remoteHead } : {},
|
|
2833
|
+
rebaseCount: 0,
|
|
2834
|
+
diffSizeBytes: 0,
|
|
2835
|
+
gitStatus: workspaceGitStatus(),
|
|
2836
|
+
affectedProjects: affectedProjects(reset.discardedPaths),
|
|
2837
|
+
affectedPaths: reset.discardedPaths,
|
|
2838
|
+
discardedPaths: reset.discardedPaths,
|
|
2839
|
+
localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
2843
|
+
const result = await synchronizeWorkspaceGit({
|
|
2844
|
+
attemptId: input.attemptId,
|
|
2845
|
+
workerLabel: label,
|
|
2846
|
+
workspacePath: workspaceShadowRoot,
|
|
2847
|
+
remoteUrl: workspaceRemoteUrl,
|
|
2848
|
+
remoteAuth,
|
|
2849
|
+
credentialHelper,
|
|
2850
|
+
gitIdentity: workspaceGitIdentity,
|
|
2851
|
+
mounts,
|
|
2852
|
+
commitDetail: input.confirmationReason ?? input.trigger.detail,
|
|
2853
|
+
allowLargeDiff: input.confirmedLargeDiff,
|
|
2854
|
+
skipMountMirror: outerRemediation,
|
|
2855
|
+
skipMountHydration: outerRemediation,
|
|
2856
|
+
afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
|
|
2857
|
+
pushChangedProjectHeads(activeMountIds, publishedHead);
|
|
2661
2858
|
}
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2859
|
+
});
|
|
2860
|
+
if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
|
|
2861
|
+
refreshProjectHeadsAfterInboundWorkspace();
|
|
2862
|
+
}
|
|
2863
|
+
return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
2864
|
+
};
|
|
2865
|
+
const sendWorkspaceSyncResult = (requestId, result) => {
|
|
2866
|
+
try {
|
|
2867
|
+
sendWorkerMessage(ws, {
|
|
2868
|
+
type: "workspace_sync_result",
|
|
2869
|
+
...requestId ? { requestId } : {},
|
|
2870
|
+
result
|
|
2671
2871
|
});
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2872
|
+
} catch (error) {
|
|
2873
|
+
process.stderr.write(
|
|
2874
|
+
`[r5d-worker] failed to send workspace sync result: ${error instanceof Error ? error.message : String(error)}
|
|
2875
|
+
`
|
|
2876
|
+
);
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
const runWorkspaceSync = async (input) => {
|
|
2880
|
+
const attemptId = input.attemptId ?? crypto.randomUUID();
|
|
2881
|
+
const requestedAt = Date.now();
|
|
2882
|
+
let queueEnteredAt = requestedAt;
|
|
2883
|
+
let syncStartedAt = requestedAt;
|
|
2884
|
+
workspaceSyncRequestsInFlight += 1;
|
|
2885
|
+
let result;
|
|
2886
|
+
try {
|
|
2887
|
+
result = await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
2888
|
+
queueEnteredAt = Date.now();
|
|
2889
|
+
syncStartedAt = Date.now();
|
|
2890
|
+
try {
|
|
2891
|
+
return await performWorkspaceSync({
|
|
2892
|
+
attemptId,
|
|
2893
|
+
trigger: input.trigger,
|
|
2894
|
+
confirmedLargeDiff: input.confirmedLargeDiff,
|
|
2895
|
+
confirmationReason: input.confirmationReason,
|
|
2896
|
+
resetToCanonical: input.resetToCanonical
|
|
2897
|
+
});
|
|
2898
|
+
} catch (error) {
|
|
2899
|
+
return failedWorkspaceSyncResult(attemptId, input.trigger, error);
|
|
2900
|
+
}
|
|
2684
2901
|
});
|
|
2685
|
-
|
|
2902
|
+
} finally {
|
|
2903
|
+
workspaceSyncRequestsInFlight -= 1;
|
|
2686
2904
|
}
|
|
2687
|
-
|
|
2905
|
+
const finishedAt = Date.now();
|
|
2906
|
+
result = {
|
|
2907
|
+
...result,
|
|
2908
|
+
telemetry: {
|
|
2909
|
+
totalMs: finishedAt - requestedAt,
|
|
2910
|
+
queueMs: queueEnteredAt - requestedAt,
|
|
2911
|
+
prepareMs: 0,
|
|
2912
|
+
synchronizeMs: finishedAt - syncStartedAt
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result);
|
|
2916
|
+
return result;
|
|
2688
2917
|
};
|
|
2689
|
-
const
|
|
2690
|
-
|
|
2918
|
+
const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
|
|
2919
|
+
pendingAutomaticTrigger = trigger;
|
|
2920
|
+
if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight) {
|
|
2691
2921
|
return;
|
|
2692
2922
|
}
|
|
2693
|
-
|
|
2923
|
+
if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
|
|
2924
|
+
workspaceAutomaticTimer = setTimeout(
|
|
2694
2925
|
() => {
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
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
|
-
}
|
|
2926
|
+
workspaceAutomaticTimer = void 0;
|
|
2927
|
+
const scheduledTrigger = pendingAutomaticTrigger;
|
|
2928
|
+
pendingAutomaticTrigger = void 0;
|
|
2929
|
+
if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
|
|
2930
|
+
automaticSyncInFlight = true;
|
|
2931
|
+
void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
|
|
2932
|
+
if (result.outcome === "failed") {
|
|
2933
|
+
pendingAutomaticTrigger = scheduledTrigger;
|
|
2716
2934
|
}
|
|
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
2935
|
}).finally(() => {
|
|
2723
|
-
|
|
2936
|
+
automaticSyncInFlight = false;
|
|
2724
2937
|
heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
|
|
2725
|
-
if (
|
|
2938
|
+
if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
|
|
2939
|
+
scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
|
|
2940
|
+
}
|
|
2726
2941
|
});
|
|
2727
2942
|
},
|
|
2728
2943
|
Math.max(0, delayMs)
|
|
2729
2944
|
);
|
|
2730
|
-
|
|
2945
|
+
workspaceAutomaticTimer.unref();
|
|
2731
2946
|
};
|
|
2732
|
-
const
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2947
|
+
const markWorkspaceDirty = (trigger, force = false) => {
|
|
2948
|
+
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
2949
|
+
};
|
|
2950
|
+
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
2951
|
+
const resolveMessageTarget = (target) => resolveWorkerSessionTarget({
|
|
2952
|
+
target,
|
|
2953
|
+
projectsRoot,
|
|
2954
|
+
workspaceShadowRoot,
|
|
2955
|
+
projectConfigById
|
|
2956
|
+
});
|
|
2957
|
+
const configureWorkerWorkspace = async (message) => {
|
|
2738
2958
|
workspaceSyncRequestsInFlight += 1;
|
|
2739
2959
|
try {
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
);
|
|
2749
|
-
|
|
2750
|
-
|
|
2960
|
+
return await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
2961
|
+
workspaceConfigured = false;
|
|
2962
|
+
configureGitHubAuth(message.githubCredential);
|
|
2963
|
+
visibleGitIdentity = message.gitIdentity;
|
|
2964
|
+
workspaceGitIdentity = message.gitIdentity;
|
|
2965
|
+
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
2966
|
+
projectWorkspaceState = projectWorkspaceStateStore.reconcile({
|
|
2967
|
+
desiredProjects: message.projects
|
|
2968
|
+
});
|
|
2969
|
+
pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes, message.projects);
|
|
2970
|
+
const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
|
|
2971
|
+
...project,
|
|
2972
|
+
branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
|
|
2973
|
+
}));
|
|
2974
|
+
const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
|
|
2975
|
+
for (const existingProject of projectConfigById.values()) {
|
|
2976
|
+
const nextProject = nextProjectConfigById.get(existingProject.projectId);
|
|
2977
|
+
if (!nextProject) {
|
|
2978
|
+
stageProjectDeletion(existingProject);
|
|
2979
|
+
continue;
|
|
2980
|
+
}
|
|
2981
|
+
const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
|
|
2982
|
+
for (const existingBranch of existingProject.branches) {
|
|
2983
|
+
if (!nextBranches.has(existingBranch.branchName)) {
|
|
2984
|
+
stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
projectConfigById.clear();
|
|
2989
|
+
for (const project of effectiveProjects) projectConfigById.set(project.projectId, project);
|
|
2990
|
+
const configuredKeys = new Set(
|
|
2991
|
+
effectiveProjects.flatMap((project) => project.branches.map(({ branchName }) => projectBranchKey(project.projectId, branchName)))
|
|
2751
2992
|
);
|
|
2752
|
-
|
|
2753
|
-
|
|
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
|
-
});
|
|
2993
|
+
for (const key of pendingCheckouts.keys()) {
|
|
2994
|
+
if (!configuredKeys.has(key)) pendingCheckouts.delete(key);
|
|
2761
2995
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
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);
|
|
2996
|
+
for (const projectId of [...readyProjectIds]) {
|
|
2997
|
+
if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
|
|
2770
2998
|
}
|
|
2771
|
-
const
|
|
2772
|
-
const
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2999
|
+
const remoteAuth = gitAuthForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
|
|
3000
|
+
const credentialHelper = credentialHelperForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
|
|
3001
|
+
ensureWorkspaceGitClone({
|
|
3002
|
+
workspacePath: workspaceShadowRoot,
|
|
3003
|
+
remoteUrl: message.workspaceRemoteUrl,
|
|
3004
|
+
remoteAuth,
|
|
3005
|
+
credentialHelper,
|
|
3006
|
+
gitIdentity: message.gitIdentity
|
|
2776
3007
|
});
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
3008
|
+
pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
|
|
3009
|
+
stageDurableWorkspaceDeletions();
|
|
3010
|
+
const mountsBeforeGit = buildWorkspaceMounts();
|
|
3011
|
+
hydrateWorkspaceGitMounts(
|
|
3012
|
+
workspaceShadowRoot,
|
|
3013
|
+
mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath))
|
|
3014
|
+
);
|
|
3015
|
+
ensureConfiguredProjects();
|
|
3016
|
+
let result = await performWorkspaceSync({
|
|
3017
|
+
attemptId: crypto.randomUUID(),
|
|
3018
|
+
trigger: { type: "connect" }
|
|
3019
|
+
});
|
|
3020
|
+
if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
3021
|
+
const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
|
|
3022
|
+
if (staleAfterInbound.length > 0) {
|
|
3023
|
+
pruneStaleOuterWorkspaceEntries(staleAfterInbound);
|
|
3024
|
+
stageDurableWorkspaceDeletions();
|
|
3025
|
+
result = await performWorkspaceSync({
|
|
3026
|
+
attemptId: crypto.randomUUID(),
|
|
3027
|
+
trigger: { type: "connect" }
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
2783
3030
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
3031
|
+
const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
|
|
3032
|
+
if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
3033
|
+
const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
|
|
3034
|
+
(deletion) => deletion.kind === "project" && !deletion.projectDeleted ? [deletion.tombstoneId] : []
|
|
3035
|
+
);
|
|
3036
|
+
if (layoutCleanupIds.length > 0) {
|
|
3037
|
+
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
3038
|
+
tombstoneIds: layoutCleanupIds,
|
|
3039
|
+
publishedHead
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
2794
3042
|
}
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3043
|
+
workspaceConfigured = true;
|
|
3044
|
+
const pending = [...pendingCheckouts.values()].sort(
|
|
3045
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
3046
|
+
);
|
|
3047
|
+
return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
|
|
3048
|
+
});
|
|
3049
|
+
} catch (error) {
|
|
3050
|
+
workspaceConfigured = fs.existsSync(path.join(workspaceShadowRoot, ".git"));
|
|
3051
|
+
for (const project of message.projects) {
|
|
3052
|
+
for (const branch of project.branches) {
|
|
3053
|
+
pendingCheckouts.set(projectBranchKey(project.projectId, branch.branchName), {
|
|
3054
|
+
projectId: project.projectId,
|
|
3055
|
+
branchName: branch.branchName
|
|
3056
|
+
});
|
|
2800
3057
|
}
|
|
2801
3058
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
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)
|
|
3059
|
+
return {
|
|
3060
|
+
result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
|
|
3061
|
+
pending: [...pendingCheckouts.values()],
|
|
3062
|
+
aheadOfOriginBranches: []
|
|
2827
3063
|
};
|
|
2828
|
-
sendWorkspaceSyncResult(authority, result);
|
|
2829
|
-
return result;
|
|
2830
3064
|
} finally {
|
|
2831
3065
|
workspaceSyncRequestsInFlight -= 1;
|
|
2832
3066
|
}
|
|
@@ -2843,17 +3077,26 @@ async function startWorker(options) {
|
|
|
2843
3077
|
const handleGracefulShutdown = () => {
|
|
2844
3078
|
if (shutdownAfterClose) return;
|
|
2845
3079
|
shutdownAfterClose = true;
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
|
|
3080
|
+
if (workspaceAutomaticTimer) {
|
|
3081
|
+
clearTimeout(workspaceAutomaticTimer);
|
|
3082
|
+
workspaceAutomaticTimer = void 0;
|
|
3083
|
+
}
|
|
3084
|
+
void (async () => {
|
|
3085
|
+
if (workspaceConfigured && !activeWorkspaceIncidentId) {
|
|
3086
|
+
await runWorkspaceSync({
|
|
3087
|
+
trigger: { type: "manual", detail: "graceful worker shutdown" }
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
await workspaceSyncSingleFlight.afterCurrent();
|
|
2852
3091
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
2853
3092
|
ws.close(1e3, "Worker shutting down");
|
|
2854
3093
|
} else {
|
|
2855
3094
|
process.exit(0);
|
|
2856
3095
|
}
|
|
3096
|
+
})().catch((error) => {
|
|
3097
|
+
process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
|
|
3098
|
+
`);
|
|
3099
|
+
ws.close(1011, "Worker shutdown sync failed");
|
|
2857
3100
|
});
|
|
2858
3101
|
};
|
|
2859
3102
|
process.on("SIGTERM", handleGracefulShutdown);
|
|
@@ -2886,11 +3129,7 @@ async function startWorker(options) {
|
|
|
2886
3129
|
r5dctlVersion: readInstalledCliVersion("r5dctl"),
|
|
2887
3130
|
capabilities: {
|
|
2888
3131
|
updateClis: true,
|
|
2889
|
-
canonicalResolverCheckout: true,
|
|
2890
3132
|
browserPortForwarding: true,
|
|
2891
|
-
globalWorkspaceSyncLeaseV1: true,
|
|
2892
|
-
eventualWorkspaceSyncV1: true,
|
|
2893
|
-
workspaceIncidentCandidatePinV1: true,
|
|
2894
3133
|
execStdinV1: true
|
|
2895
3134
|
},
|
|
2896
3135
|
projectRoot: projectsRoot,
|
|
@@ -2917,302 +3156,153 @@ async function startWorker(options) {
|
|
|
2917
3156
|
if (message.type === "connected") {
|
|
2918
3157
|
return;
|
|
2919
3158
|
}
|
|
2920
|
-
if (message.type === "
|
|
2921
|
-
const
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
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;
|
|
3159
|
+
if (message.type === "workspace_config") {
|
|
3160
|
+
const configured = await configureWorkerWorkspace(message);
|
|
3161
|
+
sendWorkerMessage(ws, {
|
|
3162
|
+
type: "workspace_configured",
|
|
3163
|
+
requestId: message.requestId,
|
|
3164
|
+
result: configured.result,
|
|
3165
|
+
pendingCheckouts: configured.pending,
|
|
3166
|
+
aheadOfOriginBranches: configured.aheadOfOriginBranches
|
|
2981
3167
|
});
|
|
2982
|
-
|
|
2983
|
-
|
|
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)}
|
|
3168
|
+
process.stdout.write(
|
|
3169
|
+
`[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
|
|
3009
3170
|
`
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
workspaceSafetyScan.unref();
|
|
3171
|
+
);
|
|
3172
|
+
if (!workspacePeriodicTimer) {
|
|
3173
|
+
workspacePeriodicTimer = setInterval(() => {
|
|
3174
|
+
scheduleAutomaticWorkspaceSync({ type: "periodic", detail: "periodic workspace reconciliation" }, 0);
|
|
3175
|
+
}, WORKSPACE_GIT_PERIODIC_MS);
|
|
3176
|
+
workspacePeriodicTimer.unref();
|
|
3017
3177
|
}
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
|
|
3021
|
-
armWorkspacePublication({ type: "periodic", detail: "resume dirty generation after reconnect" }, { delayMs: 0 });
|
|
3178
|
+
if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
|
|
3179
|
+
scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
|
|
3022
3180
|
}
|
|
3023
3181
|
return;
|
|
3024
3182
|
}
|
|
3025
3183
|
if (message.type === "sync_workspace") {
|
|
3026
|
-
await
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
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 });
|
|
3184
|
+
await runWorkspaceSync({
|
|
3185
|
+
requestId: message.requestId,
|
|
3186
|
+
attemptId: message.attemptId,
|
|
3187
|
+
trigger: message.trigger,
|
|
3188
|
+
confirmedLargeDiff: message.confirmedLargeDiff,
|
|
3189
|
+
confirmationReason: message.confirmationReason,
|
|
3190
|
+
resetToCanonical: message.resetToCanonical
|
|
3191
|
+
});
|
|
3055
3192
|
return;
|
|
3056
3193
|
}
|
|
3057
|
-
if (message.type === "
|
|
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;
|
|
3194
|
+
if (message.type === "create_project_branch") {
|
|
3106
3195
|
try {
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
for (const target of pendingTargets) {
|
|
3123
|
-
pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
|
|
3196
|
+
projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch({
|
|
3197
|
+
projectId: message.projectId,
|
|
3198
|
+
branchName: message.targetBranch
|
|
3199
|
+
});
|
|
3200
|
+
const created = await workspaceSyncSingleFlight.runMutation(() => {
|
|
3201
|
+
const project = projectConfigById.get(message.projectId);
|
|
3202
|
+
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3203
|
+
if (!readyProjectIds.has(project.projectId)) {
|
|
3204
|
+
throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
|
|
3205
|
+
}
|
|
3206
|
+
if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
|
|
3207
|
+
throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
|
|
3208
|
+
}
|
|
3209
|
+
if (project.branches.some(({ branchName }) => branchName === message.targetBranch)) {
|
|
3210
|
+
throw new Error(`Project branch ${message.targetBranch} already exists`);
|
|
3124
3211
|
}
|
|
3125
|
-
|
|
3212
|
+
const createdBranch = createLinkedProjectBranch({
|
|
3213
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
3214
|
+
sourceBranchName: message.sourceBranch,
|
|
3215
|
+
branchName: message.targetBranch
|
|
3216
|
+
});
|
|
3217
|
+
projectConfigById.set(project.projectId, {
|
|
3218
|
+
...project,
|
|
3219
|
+
branches: [
|
|
3220
|
+
...project.branches,
|
|
3221
|
+
{
|
|
3222
|
+
branchName: message.targetBranch,
|
|
3223
|
+
sourceBranchName: message.sourceBranch,
|
|
3224
|
+
baseCommitHash: createdBranch.baseCommitHash
|
|
3225
|
+
}
|
|
3226
|
+
]
|
|
3227
|
+
});
|
|
3228
|
+
lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
|
|
3229
|
+
return createdBranch;
|
|
3126
3230
|
});
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
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
|
-
})
|
|
3231
|
+
sendWorkerMessage(ws, {
|
|
3232
|
+
type: "operation_result",
|
|
3233
|
+
requestId: message.requestId,
|
|
3234
|
+
result: {
|
|
3235
|
+
type: "create_project_branch",
|
|
3236
|
+
branchName: message.targetBranch,
|
|
3237
|
+
baseCommitHash: created.baseCommitHash
|
|
3238
|
+
}
|
|
3239
|
+
});
|
|
3240
|
+
markWorkspaceDirty(
|
|
3241
|
+
{
|
|
3242
|
+
type: "manual",
|
|
3243
|
+
projectId: message.projectId,
|
|
3244
|
+
branchName: message.targetBranch,
|
|
3245
|
+
detail: `created project branch from ${message.sourceBranch}`
|
|
3246
|
+
},
|
|
3247
|
+
true
|
|
3170
3248
|
);
|
|
3171
3249
|
} catch (error) {
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3250
|
+
sendWorkerMessage(ws, {
|
|
3251
|
+
type: "operation_result",
|
|
3252
|
+
requestId: message.requestId,
|
|
3253
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3254
|
+
});
|
|
3177
3255
|
}
|
|
3178
3256
|
return;
|
|
3179
3257
|
}
|
|
3180
|
-
if (message.type === "
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
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
|
|
3258
|
+
if (message.type === "delete_project_branch") {
|
|
3259
|
+
try {
|
|
3260
|
+
const deletionKey = projectBranchKey(message.projectId, message.branchName);
|
|
3261
|
+
projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
|
|
3262
|
+
projectId: message.projectId,
|
|
3263
|
+
branchName: message.branchName
|
|
3194
3264
|
});
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3265
|
+
await workspaceSyncSingleFlight.runMutation(() => {
|
|
3266
|
+
const project = projectConfigById.get(message.projectId);
|
|
3267
|
+
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3268
|
+
if (pendingMirrorDeletes.has(deletionKey)) return;
|
|
3269
|
+
stageProjectBranchDeletion(project, message.branchName, true);
|
|
3270
|
+
projectConfigById.set(project.projectId, {
|
|
3271
|
+
...project,
|
|
3272
|
+
branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
|
|
3273
|
+
});
|
|
3274
|
+
});
|
|
3275
|
+
const result = await runWorkspaceSync({
|
|
3276
|
+
trigger: {
|
|
3277
|
+
type: "manual",
|
|
3278
|
+
projectId: message.projectId,
|
|
3279
|
+
branchName: message.branchName,
|
|
3280
|
+
detail: "deleted project branch"
|
|
3281
|
+
},
|
|
3282
|
+
confirmedLargeDiff: true,
|
|
3283
|
+
confirmationReason: "explicit project branch deletion",
|
|
3284
|
+
sendResult: false
|
|
3285
|
+
});
|
|
3286
|
+
if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
|
|
3287
|
+
throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
|
|
3288
|
+
}
|
|
3289
|
+
if (pendingMirrorDeletes.has(deletionKey)) {
|
|
3290
|
+
throw new Error("Project branch deletion did not remove the hidden mirror ref");
|
|
3291
|
+
}
|
|
3292
|
+
sendWorkerMessage(ws, {
|
|
3293
|
+
type: "operation_result",
|
|
3294
|
+
requestId: message.requestId,
|
|
3295
|
+
result: {
|
|
3296
|
+
type: "delete_project_branch",
|
|
3297
|
+
branchName: message.branchName
|
|
3298
|
+
}
|
|
3299
|
+
});
|
|
3300
|
+
} catch (error) {
|
|
3301
|
+
sendWorkerMessage(ws, {
|
|
3302
|
+
type: "operation_result",
|
|
3303
|
+
requestId: message.requestId,
|
|
3304
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3209
3305
|
});
|
|
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
3306
|
}
|
|
3217
3307
|
return;
|
|
3218
3308
|
}
|
|
@@ -3243,31 +3333,11 @@ async function startWorker(options) {
|
|
|
3243
3333
|
return;
|
|
3244
3334
|
}
|
|
3245
3335
|
if (message.type === "workspace_incident_updated") {
|
|
3246
|
-
workspaceIncidentOrdering.observe(message);
|
|
3247
3336
|
const previousIncidentId = activeWorkspaceIncidentId;
|
|
3248
|
-
|
|
3249
|
-
if (
|
|
3250
|
-
|
|
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();
|
|
3337
|
+
activeWorkspaceIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
|
|
3338
|
+
if (previousIncidentId && !activeWorkspaceIncidentId && (message.status === "resolved" || message.status === "confirmed" || message.status === "reset")) {
|
|
3339
|
+
scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
|
|
3262
3340
|
}
|
|
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
3341
|
return;
|
|
3272
3342
|
}
|
|
3273
3343
|
if (message.type === "exec_terminal_ack") {
|
|
@@ -3458,7 +3528,7 @@ async function startWorker(options) {
|
|
|
3458
3528
|
const activePty = activePtys.get(message.ptyId);
|
|
3459
3529
|
closePty(message);
|
|
3460
3530
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
3461
|
-
|
|
3531
|
+
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true);
|
|
3462
3532
|
}
|
|
3463
3533
|
return;
|
|
3464
3534
|
}
|
|
@@ -3555,7 +3625,7 @@ async function startWorker(options) {
|
|
|
3555
3625
|
});
|
|
3556
3626
|
return;
|
|
3557
3627
|
}
|
|
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") {
|
|
3628
|
+
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
3629
|
try {
|
|
3560
3630
|
const result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
|
|
3561
3631
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
@@ -3615,20 +3685,13 @@ async function startWorker(options) {
|
|
|
3615
3685
|
process.off("SIGTERM", handleGracefulShutdown);
|
|
3616
3686
|
process.off("SIGINT", handleGracefulShutdown);
|
|
3617
3687
|
stopHeartbeatWatchdog();
|
|
3618
|
-
if (
|
|
3619
|
-
clearTimeout(
|
|
3620
|
-
|
|
3621
|
-
}
|
|
3622
|
-
if (!pendingPublication?.workInFlight) {
|
|
3623
|
-
workspaceConvergence.resetTransientPublicationState();
|
|
3624
|
-
}
|
|
3625
|
-
if (workspaceSafetyScan) {
|
|
3626
|
-
clearInterval(workspaceSafetyScan);
|
|
3627
|
-
workspaceSafetyScan = void 0;
|
|
3688
|
+
if (workspaceAutomaticTimer) {
|
|
3689
|
+
clearTimeout(workspaceAutomaticTimer);
|
|
3690
|
+
workspaceAutomaticTimer = void 0;
|
|
3628
3691
|
}
|
|
3629
|
-
if (
|
|
3630
|
-
|
|
3631
|
-
|
|
3692
|
+
if (workspacePeriodicTimer) {
|
|
3693
|
+
clearInterval(workspacePeriodicTimer);
|
|
3694
|
+
workspacePeriodicTimer = void 0;
|
|
3632
3695
|
}
|
|
3633
3696
|
if (terminalReplayTimer) {
|
|
3634
3697
|
clearInterval(terminalReplayTimer);
|
|
@@ -3721,24 +3784,22 @@ if (isCliEntrypoint()) {
|
|
|
3721
3784
|
export {
|
|
3722
3785
|
describeWorkerSessionTarget,
|
|
3723
3786
|
editWorkerTextFile,
|
|
3724
|
-
ensureVisibleGitCheckout,
|
|
3725
3787
|
findWorkerFiles,
|
|
3726
3788
|
githubCliEnv,
|
|
3727
3789
|
grepWorkerFiles,
|
|
3728
3790
|
isArtifactEnvPath,
|
|
3791
|
+
listWorkerCodeDirectory,
|
|
3729
3792
|
listWorkerDirectory,
|
|
3730
3793
|
prepareArtifactEnvForShell,
|
|
3731
3794
|
prepareBuiltInToolPathsForTarget,
|
|
3732
3795
|
preparePlanEnvForShell,
|
|
3733
3796
|
prepareShellEnvForTarget,
|
|
3797
|
+
readWorkerCodeFile,
|
|
3734
3798
|
readWorkerImageFile,
|
|
3735
3799
|
readWorkerTextFile,
|
|
3736
3800
|
resolveHostShell,
|
|
3737
3801
|
resolveWorkerFilePath,
|
|
3738
3802
|
resolveWorkerSessionTarget,
|
|
3739
3803
|
syncSessionArtifacts,
|
|
3740
|
-
visibleCheckoutGitTestHarness,
|
|
3741
|
-
visibleCheckoutRemoteFor,
|
|
3742
|
-
workspaceSyncCheckoutTargets,
|
|
3743
3804
|
writeWorkerTextFile
|
|
3744
3805
|
};
|