@ricsam/r5d-worker 0.0.101 → 0.0.103
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 +16 -3
- package/dist/cjs/main.cjs +390 -322
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-mirror-fetch-policy.cjs +48 -7
- package/dist/cjs/project-mirror-refs-token.cjs +36 -2
- package/dist/cjs/project-worktrees.cjs +49 -32
- package/dist/cjs/workspace-git-sync.cjs +45 -6
- package/dist/cjs/workspace-mutation-gate.cjs +35 -0
- package/dist/cjs/workspace-projection-ledger.cjs +81 -0
- package/dist/cjs/workspace-sync-coalescer.cjs +58 -0
- package/dist/mjs/main.mjs +395 -324
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-mirror-fetch-policy.mjs +46 -6
- package/dist/mjs/project-mirror-refs-token.mjs +30 -1
- package/dist/mjs/project-worktrees.mjs +52 -33
- package/dist/mjs/workspace-git-sync.mjs +45 -6
- package/dist/mjs/workspace-mutation-gate.mjs +35 -0
- package/dist/mjs/workspace-projection-ledger.mjs +57 -0
- package/dist/mjs/workspace-sync-coalescer.mjs +34 -0
- package/dist/types/main.d.ts +29 -17
- package/dist/types/project-mirror-fetch-policy.d.ts +70 -15
- package/dist/types/project-mirror-refs-token.d.ts +34 -0
- package/dist/types/project-worktrees.d.ts +26 -8
- package/dist/types/workspace-git-sync.d.ts +24 -2
- package/dist/types/workspace-mutation-gate.d.ts +24 -3
- package/dist/types/workspace-projection-ledger.d.ts +59 -0
- package/dist/types/workspace-sync-coalescer.d.ts +28 -0
- package/package.json +1 -1
package/dist/mjs/package.json
CHANGED
|
@@ -1,16 +1,56 @@
|
|
|
1
|
-
const PROJECT_MIRROR_FETCH_BACKSTOP_MS = 10 * 6e4;
|
|
2
1
|
function projectMirrorFetchDecision(input) {
|
|
3
2
|
if (!input.skippingEnabled) return { fetch: true, reason: "skipping_disabled" };
|
|
4
3
|
if (input.advertisedToken === void 0) return { fetch: true, reason: "no_advertised_token" };
|
|
5
4
|
if (input.fetchedToken === void 0) return { fetch: true, reason: "never_fetched" };
|
|
6
5
|
if (input.advertisedToken !== input.fetchedToken) return { fetch: true, reason: "token_changed" };
|
|
7
|
-
const backstopMs = input.backstopMs ?? PROJECT_MIRROR_FETCH_BACKSTOP_MS;
|
|
8
|
-
if (input.lastFetchAtMs === void 0 || input.nowMs - input.lastFetchAtMs >= backstopMs) {
|
|
9
|
-
return { fetch: true, reason: "backstop_elapsed" };
|
|
10
|
-
}
|
|
11
6
|
return { fetch: false, reason: "token_match" };
|
|
12
7
|
}
|
|
8
|
+
function planProjectMirrorObservationFetch(input) {
|
|
9
|
+
if (!input.skippingEnabled) return { scope: "project", reason: "skipping_disabled" };
|
|
10
|
+
if (input.advertisedMountTokens === void 0) {
|
|
11
|
+
const decision = projectMirrorFetchDecision({
|
|
12
|
+
advertisedToken: input.advertisedProjectToken,
|
|
13
|
+
fetchedToken: input.fetchedProjectToken,
|
|
14
|
+
skippingEnabled: true
|
|
15
|
+
});
|
|
16
|
+
return decision.fetch ? { scope: "project", reason: decision.reason } : { scope: "none" };
|
|
17
|
+
}
|
|
18
|
+
const mounts = [];
|
|
19
|
+
for (const branchName of [...input.branchNames].sort()) {
|
|
20
|
+
const decision = projectMirrorFetchDecision({
|
|
21
|
+
advertisedToken: input.advertisedMountTokens.get(branchName),
|
|
22
|
+
fetchedToken: input.fetchedMountTokens.get(branchName),
|
|
23
|
+
skippingEnabled: true
|
|
24
|
+
});
|
|
25
|
+
if (decision.fetch) mounts.push({ branchName, reason: decision.reason });
|
|
26
|
+
}
|
|
27
|
+
return mounts.length === 0 ? { scope: "none" } : { scope: "mounts", mounts };
|
|
28
|
+
}
|
|
29
|
+
const TOKEN_RE = /^[0-9a-f]{64}$/;
|
|
30
|
+
const GIT_OBJECT_NAME_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
31
|
+
function parseProjectMirrorRefsTokensMessage(message) {
|
|
32
|
+
const projectTokens = /* @__PURE__ */ new Map();
|
|
33
|
+
if (message.tokens && typeof message.tokens === "object") {
|
|
34
|
+
for (const [projectId, token] of Object.entries(message.tokens)) {
|
|
35
|
+
if (typeof token === "string" && TOKEN_RE.test(token)) projectTokens.set(projectId, token);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const mountTokens = /* @__PURE__ */ new Map();
|
|
39
|
+
if (message.mountTokens && typeof message.mountTokens === "object") {
|
|
40
|
+
for (const [projectId, tokensByBranch] of Object.entries(message.mountTokens)) {
|
|
41
|
+
if (!tokensByBranch || typeof tokensByBranch !== "object") continue;
|
|
42
|
+
const perMount = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const [branchName, token] of Object.entries(tokensByBranch)) {
|
|
44
|
+
if (typeof token === "string" && TOKEN_RE.test(token)) perMount.set(branchName, token);
|
|
45
|
+
}
|
|
46
|
+
mountTokens.set(projectId, perMount);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const workspaceHead = typeof message.workspaceHead === "string" && GIT_OBJECT_NAME_RE.test(message.workspaceHead) ? message.workspaceHead : void 0;
|
|
50
|
+
return { projectTokens, mountTokens, workspaceHead };
|
|
51
|
+
}
|
|
13
52
|
export {
|
|
14
|
-
|
|
53
|
+
parseProjectMirrorRefsTokensMessage,
|
|
54
|
+
planProjectMirrorObservationFetch,
|
|
15
55
|
projectMirrorFetchDecision
|
|
16
56
|
};
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
const PROJECT_MIRROR_REFS_TOKEN_NAMESPACES = ["refs/heads/", "refs/r5d/server-synced/"];
|
|
2
3
|
const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
|
|
3
4
|
const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
|
|
5
|
+
const PROJECT_MIRROR_REF_LISTING_FORMAT = "--format=%(objectname) %(refname)";
|
|
6
|
+
const GIT_OBJECT_NAME_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
4
7
|
function hashProjectMirrorRefs(refs) {
|
|
5
8
|
const lines = refs.map(({ objectName, refname }) => `${objectName} ${refname}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
6
9
|
return createHash("sha256").update(lines.join("\n")).digest("hex");
|
|
7
10
|
}
|
|
11
|
+
function parseProjectMirrorRefListing(output, options = {}) {
|
|
12
|
+
return output.split(/\r?\n/).filter(Boolean).map((row) => {
|
|
13
|
+
const separator = row.indexOf(" ");
|
|
14
|
+
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
15
|
+
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
16
|
+
if (!GIT_OBJECT_NAME_RE.test(objectName) || refname.length === 0 || options.namespaces !== void 0 && !options.namespaces.some((prefix) => refname.startsWith(prefix))) {
|
|
17
|
+
throw new Error(`Invalid ref listing row: ${row}`);
|
|
18
|
+
}
|
|
19
|
+
return { refname, objectName };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
8
22
|
function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
9
23
|
const canonical = [];
|
|
10
24
|
for (const { refname, objectName } of refs) {
|
|
@@ -16,9 +30,24 @@ function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
|
16
30
|
}
|
|
17
31
|
return canonical;
|
|
18
32
|
}
|
|
33
|
+
function projectMirrorMountRefs(refs, mount) {
|
|
34
|
+
const headRef = `refs/heads/${mount.branchName}`;
|
|
35
|
+
const handoffPrefix = `${PROJECT_SERVER_SYNCED_REF_PREFIX}${mount.branchId}/`;
|
|
36
|
+
return refs.filter(({ refname }) => refname === headRef || refname.startsWith(handoffPrefix));
|
|
37
|
+
}
|
|
38
|
+
function projectMirrorRefsTokens(refs, mounts) {
|
|
39
|
+
const mountTokens = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const mount of mounts) mountTokens.set(mount.branchName, hashProjectMirrorRefs(projectMirrorMountRefs(refs, mount)));
|
|
41
|
+
return { project: hashProjectMirrorRefs(refs), mounts: mountTokens };
|
|
42
|
+
}
|
|
19
43
|
export {
|
|
44
|
+
PROJECT_MIRROR_REFS_TOKEN_NAMESPACES,
|
|
45
|
+
PROJECT_MIRROR_REF_LISTING_FORMAT,
|
|
20
46
|
PROJECT_MIRROR_REF_PREFIX,
|
|
21
47
|
PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
22
48
|
canonicalizeLocalProjectMirrorRefs,
|
|
23
|
-
hashProjectMirrorRefs
|
|
49
|
+
hashProjectMirrorRefs,
|
|
50
|
+
parseProjectMirrorRefListing,
|
|
51
|
+
projectMirrorMountRefs,
|
|
52
|
+
projectMirrorRefsTokens
|
|
24
53
|
};
|
|
@@ -6,9 +6,11 @@ import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProc
|
|
|
6
6
|
import { validateManagedBranchName } from "./managed-paths.mjs";
|
|
7
7
|
import {
|
|
8
8
|
canonicalizeLocalProjectMirrorRefs,
|
|
9
|
-
|
|
9
|
+
parseProjectMirrorRefListing,
|
|
10
|
+
PROJECT_MIRROR_REF_LISTING_FORMAT,
|
|
10
11
|
PROJECT_MIRROR_REF_PREFIX,
|
|
11
|
-
PROJECT_SERVER_SYNCED_REF_PREFIX
|
|
12
|
+
PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
13
|
+
projectMirrorRefsTokens
|
|
12
14
|
} from "./project-mirror-refs-token.mjs";
|
|
13
15
|
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
14
16
|
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
@@ -1125,8 +1127,12 @@ function createLinkedProjectBranch(input) {
|
|
|
1125
1127
|
);
|
|
1126
1128
|
configureProjectBranchTracking(targetPath, input.branchName, input.branchName);
|
|
1127
1129
|
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
+
if (input.workingTree === "carry") {
|
|
1131
|
+
git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
|
|
1132
|
+
mirrorWorkingTree({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
|
|
1133
|
+
} else {
|
|
1134
|
+
git(targetPath, ["reset", "--hard", sourceHead], `check out linked worktree ${input.branchName}`);
|
|
1135
|
+
}
|
|
1130
1136
|
return { branchPath: targetPath, baseCommitHash: sourceHead };
|
|
1131
1137
|
} catch (error) {
|
|
1132
1138
|
if (branchCreated) {
|
|
@@ -1316,24 +1322,46 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1316
1322
|
throw new Error(`Hidden project mirror publication skipped ${skipped.branchName}${skipped.reason ? ` (${skipped.reason})` : ""}`);
|
|
1317
1323
|
}
|
|
1318
1324
|
}
|
|
1325
|
+
const PROJECT_MIRROR_FETCH_REFSPECS = ["+refs/heads/*:refs/r5d/mirror/*", "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"];
|
|
1326
|
+
function projectMirrorMountFetchRefspecs(branchName, branchId) {
|
|
1327
|
+
validateManagedBranchName(branchName);
|
|
1328
|
+
if (branchId !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1329
|
+
return [
|
|
1330
|
+
`+refs/heads/${branchName}:refs/r5d/mirror/${branchName}`,
|
|
1331
|
+
branchId === void 0 ? "+refs/r5d/server-synced/*:refs/r5d/server-synced/*" : `+refs/r5d/server-synced/${branchId}/*:refs/r5d/server-synced/${branchId}/*`
|
|
1332
|
+
];
|
|
1333
|
+
}
|
|
1334
|
+
async function fetchProjectMirrorRefs(input) {
|
|
1335
|
+
const fetch = (refspecs) => gitResultAsync(
|
|
1336
|
+
input.primaryPath,
|
|
1337
|
+
[
|
|
1338
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1339
|
+
"fetch",
|
|
1340
|
+
"--no-recurse-submodules",
|
|
1341
|
+
"--prune",
|
|
1342
|
+
input.mirrorUrl,
|
|
1343
|
+
...refspecs
|
|
1344
|
+
],
|
|
1345
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1346
|
+
);
|
|
1347
|
+
if (input.fetchOnlyBranches && input.fetchOnlyBranches.size > 0) {
|
|
1348
|
+
const refspecs = [...input.fetchOnlyBranches].sort().flatMap((branchName) => projectMirrorMountFetchRefspecs(branchName, input.branchIds?.get(branchName)));
|
|
1349
|
+
const scoped = await fetch(refspecs);
|
|
1350
|
+
if (scoped.exitCode === 0) return;
|
|
1351
|
+
if (!/(couldn't find remote ref|no such ref)/i.test(`${scoped.stderr}
|
|
1352
|
+
${scoped.stdout}`)) {
|
|
1353
|
+
throw new Error(`Refresh project head mirror: ${scoped.stderr || scoped.stdout || `git exited ${scoped.exitCode}`}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const whole = await fetch(PROJECT_MIRROR_FETCH_REFSPECS);
|
|
1357
|
+
if (whole.exitCode !== 0) {
|
|
1358
|
+
throw new Error(`Refresh project head mirror: ${whole.stderr || whole.stdout || `git exited ${whole.exitCode}`}`);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1319
1361
|
async function observeProjectMirrorHeads(input) {
|
|
1320
1362
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1321
|
-
if (input.refreshRemote !== false) {
|
|
1322
|
-
|
|
1323
|
-
primaryPath,
|
|
1324
|
-
[
|
|
1325
|
-
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1326
|
-
"fetch",
|
|
1327
|
-
"--no-recurse-submodules",
|
|
1328
|
-
"--prune",
|
|
1329
|
-
input.mirrorUrl,
|
|
1330
|
-
"+refs/heads/*:refs/r5d/mirror/*",
|
|
1331
|
-
"+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
|
|
1332
|
-
],
|
|
1333
|
-
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1334
|
-
);
|
|
1335
|
-
if (fetch.exitCode !== 0)
|
|
1336
|
-
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
1363
|
+
if (input.refreshRemote !== false && !(input.fetchOnlyBranches && input.fetchOnlyBranches.size === 0)) {
|
|
1364
|
+
await fetchProjectMirrorRefs({ ...input, primaryPath });
|
|
1337
1365
|
}
|
|
1338
1366
|
const observations = [];
|
|
1339
1367
|
for (const branchName of [...input.branchNames].sort()) {
|
|
@@ -1380,23 +1408,14 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1380
1408
|
}
|
|
1381
1409
|
return observations;
|
|
1382
1410
|
}
|
|
1383
|
-
async function
|
|
1411
|
+
async function computeLocalProjectMirrorRefsTokens(input) {
|
|
1384
1412
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1385
1413
|
const rows = await gitAsync(
|
|
1386
1414
|
primaryPath,
|
|
1387
|
-
["for-each-ref",
|
|
1415
|
+
["for-each-ref", PROJECT_MIRROR_REF_LISTING_FORMAT, PROJECT_MIRROR_REF_PREFIX, PROJECT_SERVER_SYNCED_REF_PREFIX],
|
|
1388
1416
|
"list cached project mirror refs"
|
|
1389
1417
|
);
|
|
1390
|
-
|
|
1391
|
-
const separator = row.indexOf(" ");
|
|
1392
|
-
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
1393
|
-
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
1394
|
-
if (!/^[0-9a-f]{40,64}$/.test(objectName) || refname.length === 0) {
|
|
1395
|
-
throw new Error(`Invalid cached mirror ref listing row: ${row}`);
|
|
1396
|
-
}
|
|
1397
|
-
return { refname, objectName };
|
|
1398
|
-
});
|
|
1399
|
-
return hashProjectMirrorRefs(canonicalizeLocalProjectMirrorRefs(refs));
|
|
1418
|
+
return projectMirrorRefsTokens(canonicalizeLocalProjectMirrorRefs(parseProjectMirrorRefListing(rows)), input.mounts);
|
|
1400
1419
|
}
|
|
1401
1420
|
async function applyObservedProjectMirrorHeads(input) {
|
|
1402
1421
|
const results = [];
|
|
@@ -1479,7 +1498,7 @@ export {
|
|
|
1479
1498
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1480
1499
|
applyObservedProjectMirrorHeads,
|
|
1481
1500
|
assertProjectMirrorHeadsPushed,
|
|
1482
|
-
|
|
1501
|
+
computeLocalProjectMirrorRefsTokens,
|
|
1483
1502
|
createLinkedProjectBranch,
|
|
1484
1503
|
createOrRetryLinkedProjectBranch,
|
|
1485
1504
|
deleteLinkedProjectBranch,
|
|
@@ -1352,6 +1352,19 @@ ${result.stdout}`;
|
|
|
1352
1352
|
}
|
|
1353
1353
|
return revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
1354
1354
|
}
|
|
1355
|
+
async function resolveWorkspaceRemoteHead(workspacePath, input) {
|
|
1356
|
+
const advertised = input.advertisedRemoteHead;
|
|
1357
|
+
if (advertised && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(advertised)) {
|
|
1358
|
+
const tracked = await revParseAsync(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
1359
|
+
if (tracked === advertised && await tryGitAsync(workspacePath, ["cat-file", "-e", `${tracked}^{commit}`])) {
|
|
1360
|
+
return { head: tracked, fetched: false };
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
head: await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername),
|
|
1365
|
+
fetched: true
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1355
1368
|
async function ensureWorkspaceGitClone(input) {
|
|
1356
1369
|
const workspacePath = path.resolve(input.workspacePath);
|
|
1357
1370
|
const workspaceParent = path.dirname(workspacePath);
|
|
@@ -1399,7 +1412,8 @@ async function ensureWorkspaceGitClone(input) {
|
|
|
1399
1412
|
if (!await revParseAsync(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && await tryGitAsync(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
1400
1413
|
await updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
|
|
1401
1414
|
}
|
|
1402
|
-
const
|
|
1415
|
+
const resolvedRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
|
|
1416
|
+
const remoteHead = resolvedRemoteHead.head;
|
|
1403
1417
|
let localHead = await revParseAsync(workspacePath, "HEAD");
|
|
1404
1418
|
if (!localHead && remoteHead) {
|
|
1405
1419
|
await runWorkspaceCheckoutTransition(workspacePath, async () => {
|
|
@@ -1420,7 +1434,7 @@ async function ensureWorkspaceGitClone(input) {
|
|
|
1420
1434
|
await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
1421
1435
|
localHead = remoteHead;
|
|
1422
1436
|
}
|
|
1423
|
-
return { localHead, remoteHead };
|
|
1437
|
+
return { localHead, remoteHead, remoteHeadFetched: resolvedRemoteHead.fetched };
|
|
1424
1438
|
}
|
|
1425
1439
|
function activeMounts(mounts) {
|
|
1426
1440
|
const active = [];
|
|
@@ -2069,6 +2083,11 @@ async function synchronizeWithFetchedHead(input) {
|
|
|
2069
2083
|
return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
|
|
2070
2084
|
}
|
|
2071
2085
|
async function synchronizeWorkspaceGit(input) {
|
|
2086
|
+
const remoteHeadFetches = { count: 0 };
|
|
2087
|
+
const result = await runWorkspaceGitSynchronization(input, remoteHeadFetches);
|
|
2088
|
+
return { ...result, remoteHeadFetches: remoteHeadFetches.count };
|
|
2089
|
+
}
|
|
2090
|
+
async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
2072
2091
|
const workspacePath = path.resolve(input.workspacePath);
|
|
2073
2092
|
const attemptId = input.attemptId ?? crypto.randomUUID();
|
|
2074
2093
|
const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
|
|
@@ -2077,6 +2096,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2077
2096
|
const requiredAncestorHeads = requiredWorkspaceAncestorHeads(input.requiredAncestorHeads);
|
|
2078
2097
|
const preserveResolutionInProgress = input.skipMountMirror === true;
|
|
2079
2098
|
const initial = await ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
|
|
2099
|
+
if (initial.remoteHeadFetched) remoteHeadFetches.count += 1;
|
|
2080
2100
|
assertWorkspaceRemediationAncestry({
|
|
2081
2101
|
workspacePath,
|
|
2082
2102
|
currentHead: initial.localHead,
|
|
@@ -2124,11 +2144,22 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2124
2144
|
const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
|
|
2125
2145
|
refreshCycleSkippedMounts();
|
|
2126
2146
|
const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
|
|
2147
|
+
const activeMountIds = [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort();
|
|
2127
2148
|
return {
|
|
2128
|
-
activeMountIds
|
|
2129
|
-
skippedMountIds: [...skippedIds].sort()
|
|
2149
|
+
activeMountIds,
|
|
2150
|
+
skippedMountIds: [...skippedIds].sort(),
|
|
2151
|
+
// An active mount's token is unchanged since capture (a moved token
|
|
2152
|
+
// lands it in cycleSkippedMountIds above), so this is the token its
|
|
2153
|
+
// projected bytes belong to.
|
|
2154
|
+
projectedMutationTokens: Object.fromEntries(
|
|
2155
|
+
activeMountIds.flatMap((id) => {
|
|
2156
|
+
const token = projectionMutationTokens.get(id);
|
|
2157
|
+
return token === void 0 ? [] : [[id, token]];
|
|
2158
|
+
})
|
|
2159
|
+
)
|
|
2130
2160
|
};
|
|
2131
2161
|
};
|
|
2162
|
+
const mountProjectionIsCurrent = (mount) => mount.lastProjectedMutationToken !== void 0 && !mount.deleteWhenSourceMissing && mount.preserveLocalOnHydrationBasisChange !== true && projectionBasisHead !== null && workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount) === projectionBasisHead && projectionMutationTokens.get(mount.id) === mount.lastProjectedMutationToken && mount.mutationToken?.() === mount.lastProjectedMutationToken;
|
|
2132
2163
|
if (!input.skipMountMirror) {
|
|
2133
2164
|
const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
|
|
2134
2165
|
const mergeSupport = mergeProjectionEnabled ? workspaceMergeProjectionSupport() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
|
|
@@ -2267,6 +2298,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2267
2298
|
}
|
|
2268
2299
|
};
|
|
2269
2300
|
const projectedFastMounts = [];
|
|
2301
|
+
const currentProjectionMounts = [];
|
|
2270
2302
|
if (!input.skipMountMirror) {
|
|
2271
2303
|
beginWorkspaceCheckoutTransition(workspacePath);
|
|
2272
2304
|
try {
|
|
@@ -2278,6 +2310,10 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2278
2310
|
cycleSkippedMountIds.add(mount.id);
|
|
2279
2311
|
continue;
|
|
2280
2312
|
}
|
|
2313
|
+
if (mountProjectionIsCurrent(mount)) {
|
|
2314
|
+
currentProjectionMounts.push(mount);
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2281
2317
|
projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
|
|
2282
2318
|
projectedFastMounts.push(mount);
|
|
2283
2319
|
await yieldToWorkerEventLoop();
|
|
@@ -2311,7 +2347,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2311
2347
|
const targetProjectionReceipt = receiptWithMountsAtHead(
|
|
2312
2348
|
projectionBasisReceipt,
|
|
2313
2349
|
projectedHead,
|
|
2314
|
-
projectedFastMounts,
|
|
2350
|
+
[...projectedFastMounts, ...currentProjectionMounts],
|
|
2315
2351
|
allProjectionReceiptMounts
|
|
2316
2352
|
);
|
|
2317
2353
|
const currentProjectionReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
@@ -2324,7 +2360,9 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2324
2360
|
}
|
|
2325
2361
|
await completeWorkspaceCheckoutTransition(workspacePath);
|
|
2326
2362
|
}
|
|
2327
|
-
|
|
2363
|
+
const prePublishRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
|
|
2364
|
+
if (prePublishRemoteHead.fetched) remoteHeadFetches.count += 1;
|
|
2365
|
+
let remoteHead = prePublishRemoteHead.head;
|
|
2328
2366
|
let rebaseCount = 0;
|
|
2329
2367
|
let updated = false;
|
|
2330
2368
|
for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
|
|
@@ -2481,6 +2519,7 @@ ${push.stdout}`)) {
|
|
|
2481
2519
|
throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
|
|
2482
2520
|
}
|
|
2483
2521
|
remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
2522
|
+
remoteHeadFetches.count += 1;
|
|
2484
2523
|
}
|
|
2485
2524
|
throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
|
|
2486
2525
|
}
|
|
@@ -27,8 +27,43 @@ class WorkspaceMutationGate {
|
|
|
27
27
|
release();
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
/** A worktree create/delete lease; see the branch lane in the class comment. */
|
|
31
|
+
acquireBranchMutation() {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
this.waiters.push({ kind: "branch", resolve });
|
|
34
|
+
this.drain();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async runBranchMutation(operation) {
|
|
38
|
+
const release = await this.acquireBranchMutation();
|
|
39
|
+
try {
|
|
40
|
+
return await operation();
|
|
41
|
+
} finally {
|
|
42
|
+
release();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Operations a branch mutation requested now would wait for: the running
|
|
47
|
+
* sync, if any, plus branch mutations queued behind it. Zero means it runs
|
|
48
|
+
* at once. Ordinary waiters are not counted because the branch lane admits
|
|
49
|
+
* ahead of them.
|
|
50
|
+
*/
|
|
51
|
+
branchMutationQueuePosition() {
|
|
52
|
+
if (!this.syncActive) return 0;
|
|
53
|
+
return 1 + this.waiters.filter((waiter) => waiter.kind === "branch").length;
|
|
54
|
+
}
|
|
30
55
|
drain() {
|
|
31
56
|
if (this.syncActive || this.waiters.length === 0) return;
|
|
57
|
+
for (let index = 0; index < this.waiters.length; ) {
|
|
58
|
+
const waiter = this.waiters[index];
|
|
59
|
+
if (waiter?.kind !== "branch") {
|
|
60
|
+
index += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
this.waiters.splice(index, 1);
|
|
64
|
+
this.activeMutations += 1;
|
|
65
|
+
waiter.resolve(this.releaseMutation());
|
|
66
|
+
}
|
|
32
67
|
const first = this.waiters[0];
|
|
33
68
|
if (first?.kind === "sync") {
|
|
34
69
|
if (this.activeMutations > 0) return;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const RETAINED_OUTCOMES = /* @__PURE__ */ new Set(["no_change", "updated", "published", "large_diff_blocked"]);
|
|
2
|
+
class WorkspaceProjectionLedger {
|
|
3
|
+
projectedTokens = /* @__PURE__ */ new Map();
|
|
4
|
+
lastCycle = null;
|
|
5
|
+
static recordKey(mount) {
|
|
6
|
+
return `${mount.hydrationIncarnationKey}\0${mount.id}`;
|
|
7
|
+
}
|
|
8
|
+
get lastCycleResult() {
|
|
9
|
+
return this.lastCycle;
|
|
10
|
+
}
|
|
11
|
+
lastProjectedToken(mount) {
|
|
12
|
+
return this.projectedTokens.get(WorkspaceProjectionLedger.recordKey(mount));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Record a completed cycle. `projectedMutationTokens` names the token each
|
|
16
|
+
* active mount carried when the cycle projected it; a mount absent from it
|
|
17
|
+
* keeps its earlier record. Any outcome that leaves the outer clone without
|
|
18
|
+
* a trustworthy projection clears the ledger.
|
|
19
|
+
*/
|
|
20
|
+
recordCycle(input) {
|
|
21
|
+
if (!RETAINED_OUTCOMES.has(input.result.outcome)) {
|
|
22
|
+
this.reset();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const mountsById = new Map(input.mounts.map((mount) => [mount.id, mount]));
|
|
26
|
+
for (const [mountId, token] of Object.entries(input.projectedMutationTokens ?? {})) {
|
|
27
|
+
const mount = mountsById.get(mountId);
|
|
28
|
+
if (mount) this.projectedTokens.set(WorkspaceProjectionLedger.recordKey(mount), token);
|
|
29
|
+
}
|
|
30
|
+
this.lastCycle = input.result;
|
|
31
|
+
}
|
|
32
|
+
reset() {
|
|
33
|
+
this.projectedTokens.clear();
|
|
34
|
+
this.lastCycle = null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The previous cycle's result when every request targets a configured
|
|
38
|
+
* mount and no mount's token moved since its recorded projection. A request
|
|
39
|
+
* without a target (a workspace-rooted command may touch any checkout), a
|
|
40
|
+
* mount without a token (a pending deletion), or pending deletion work all
|
|
41
|
+
* require a real cycle.
|
|
42
|
+
*/
|
|
43
|
+
answerFromLastCycle(input) {
|
|
44
|
+
if (!this.lastCycle || input.pendingDeletions || input.targetMountIds.length === 0) return null;
|
|
45
|
+
const mountIds = new Set(input.mounts.map(({ id }) => id));
|
|
46
|
+
if (!input.targetMountIds.every((mountId) => mountId !== null && mountIds.has(mountId))) return null;
|
|
47
|
+
for (const mount of input.mounts) {
|
|
48
|
+
if (!mount.mutationToken) return null;
|
|
49
|
+
const recorded = this.projectedTokens.get(WorkspaceProjectionLedger.recordKey(mount));
|
|
50
|
+
if (recorded === void 0 || recorded !== mount.mutationToken()) return null;
|
|
51
|
+
}
|
|
52
|
+
return this.lastCycle;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
WorkspaceProjectionLedger
|
|
57
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class WorkspaceSyncCoalescer {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
this.options = options;
|
|
4
|
+
}
|
|
5
|
+
options;
|
|
6
|
+
pendingBatch = null;
|
|
7
|
+
batchesStarted = 0;
|
|
8
|
+
/** Requests attached to the batch that has not started yet. */
|
|
9
|
+
get queuedRequestCount() {
|
|
10
|
+
return this.pendingBatch?.requests.length ?? 0;
|
|
11
|
+
}
|
|
12
|
+
/** Batches whose body has been entered, whether or not they ran a cycle. */
|
|
13
|
+
get startedBatchCount() {
|
|
14
|
+
return this.batchesStarted;
|
|
15
|
+
}
|
|
16
|
+
request(request) {
|
|
17
|
+
const queued = this.pendingBatch;
|
|
18
|
+
if (queued) {
|
|
19
|
+
queued.requests.push(request);
|
|
20
|
+
return queued.settlement;
|
|
21
|
+
}
|
|
22
|
+
const batch = { requests: [request], settlement: Promise.resolve() };
|
|
23
|
+
this.pendingBatch = batch;
|
|
24
|
+
batch.settlement = this.options.runExclusive(async () => {
|
|
25
|
+
if (this.pendingBatch === batch) this.pendingBatch = null;
|
|
26
|
+
this.batchesStarted += 1;
|
|
27
|
+
return await this.options.runBatch(batch.requests);
|
|
28
|
+
});
|
|
29
|
+
return batch.settlement;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
WorkspaceSyncCoalescer
|
|
34
|
+
};
|
package/dist/types/main.d.ts
CHANGED
|
@@ -185,6 +185,11 @@ type WorkerClientMessage = {
|
|
|
185
185
|
requestId: string;
|
|
186
186
|
result?: WorkerOperationResult;
|
|
187
187
|
error?: string;
|
|
188
|
+
} | {
|
|
189
|
+
/** Immediate receipt for a branch create/delete; `queuePosition` is 0 when it runs at once. */
|
|
190
|
+
type: "operation_queued";
|
|
191
|
+
requestId: string;
|
|
192
|
+
queuePosition: number;
|
|
188
193
|
} | {
|
|
189
194
|
type: "exec_result";
|
|
190
195
|
requestId: string;
|
|
@@ -290,13 +295,18 @@ type WorkerServerMessage = {
|
|
|
290
295
|
requiredAncestorHeads?: string[];
|
|
291
296
|
} | {
|
|
292
297
|
/**
|
|
293
|
-
* Content-derived tokens naming
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
298
|
+
* Content-derived tokens naming ref state the server read from disk:
|
|
299
|
+
* `tokens` per canonical project repository, `mountTokens` per
|
|
300
|
+
* checked-out project branch (keyed by project id then branch name),
|
|
301
|
+
* and the current head of the hidden workspace repository. Replace-all:
|
|
302
|
+
* anything absent from a message has no advertised token and the
|
|
303
|
+
* corresponding fetch runs. Older servers send `tokens` only. See
|
|
304
|
+
* `project-mirror-fetch-policy.ts`.
|
|
297
305
|
*/
|
|
298
306
|
type: "project_mirror_refs_tokens";
|
|
299
307
|
tokens: Record<string, string>;
|
|
308
|
+
mountTokens?: Record<string, Record<string, string>>;
|
|
309
|
+
workspaceHead?: string;
|
|
300
310
|
} | {
|
|
301
311
|
type: "create_project_branch";
|
|
302
312
|
requestId: string;
|
|
@@ -304,6 +314,8 @@ type WorkerServerMessage = {
|
|
|
304
314
|
projectId: string;
|
|
305
315
|
sourceBranch: string;
|
|
306
316
|
targetBranch: string;
|
|
317
|
+
/** `carry` copies the source checkout's uncommitted changes; `clean` checks out its commit only. */
|
|
318
|
+
workingTree: "carry" | "clean";
|
|
307
319
|
} | {
|
|
308
320
|
type: "delete_project_branch";
|
|
309
321
|
requestId: string;
|
|
@@ -618,14 +630,17 @@ export declare const workerPtyTestHarness: {
|
|
|
618
630
|
removeEnvFiles: typeof removePtyEnvFiles;
|
|
619
631
|
resolveEnvFileReferences: typeof resolvePtyEnvFileReferences;
|
|
620
632
|
commandHasWorkspaceEffect: typeof workerCommandHasWorkspaceEffect;
|
|
633
|
+
targetProcessEnv: typeof targetProcessEnv;
|
|
621
634
|
canonicalSyncTerminalHead: typeof canonicalSyncTerminalHead;
|
|
622
635
|
ptyIsWorkspaceBusy: typeof workerPtyIsWorkspaceBusy;
|
|
623
636
|
parseLinuxForegroundBusy: typeof parseLinuxPtyForegroundBusy;
|
|
624
637
|
};
|
|
625
638
|
export type WorkerBuiltInToolPaths = {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
639
|
+
rootDir?: string;
|
|
640
|
+
sessionId?: string;
|
|
641
|
+
projectId?: string;
|
|
642
|
+
branchName?: string;
|
|
643
|
+
activePlanId?: string;
|
|
629
644
|
};
|
|
630
645
|
export declare function isArtifactEnvPath(filePath: string): boolean;
|
|
631
646
|
export declare function syncSessionArtifacts(input: {
|
|
@@ -638,11 +653,11 @@ export declare function prepareArtifactEnvForShell(input: {
|
|
|
638
653
|
baseUrl: string;
|
|
639
654
|
token: string;
|
|
640
655
|
sessionId?: string;
|
|
641
|
-
|
|
656
|
+
rootDir: string;
|
|
642
657
|
}): Promise<Record<string, string>>;
|
|
643
658
|
export declare function preparePlanEnvForShell(input: {
|
|
644
659
|
target: WorkerSessionTarget;
|
|
645
|
-
|
|
660
|
+
rootDir: string;
|
|
646
661
|
activePlanId?: string;
|
|
647
662
|
}): Record<string, string>;
|
|
648
663
|
type ResolvedWorkerFilePath = {
|
|
@@ -943,8 +958,7 @@ export declare function prepareBuiltInToolPathsForTarget(input: {
|
|
|
943
958
|
token: string;
|
|
944
959
|
sessionId?: string;
|
|
945
960
|
activePlanId?: string;
|
|
946
|
-
|
|
947
|
-
planRoot: string;
|
|
961
|
+
rootDir: string;
|
|
948
962
|
access: "read" | "write";
|
|
949
963
|
}): Promise<WorkerBuiltInToolPaths | undefined>;
|
|
950
964
|
export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number, builtInPaths?: WorkerBuiltInToolPaths): WorkerReadFileResult;
|
|
@@ -960,8 +974,7 @@ declare function executeWriteFileOperation(input: {
|
|
|
960
974
|
resolvedTarget: ResolvedWorkerSessionTarget;
|
|
961
975
|
baseUrl: string;
|
|
962
976
|
token: string;
|
|
963
|
-
|
|
964
|
-
planRoot: string;
|
|
977
|
+
rootDir: string;
|
|
965
978
|
assertAdmission: () => void;
|
|
966
979
|
}): Promise<WorkerWriteFileResult>;
|
|
967
980
|
declare function executeEditFileOperation(input: {
|
|
@@ -971,8 +984,7 @@ declare function executeEditFileOperation(input: {
|
|
|
971
984
|
resolvedTarget: ResolvedWorkerSessionTarget;
|
|
972
985
|
baseUrl: string;
|
|
973
986
|
token: string;
|
|
974
|
-
|
|
975
|
-
planRoot: string;
|
|
987
|
+
rootDir: string;
|
|
976
988
|
assertAdmission: () => void;
|
|
977
989
|
}): Promise<WorkerEditFileResult>;
|
|
978
990
|
export declare function grepWorkerFiles(branchPath: string, input: {
|
|
@@ -992,14 +1004,14 @@ export declare function listWorkerDirectory(branchPath: string, inputPath?: stri
|
|
|
992
1004
|
export declare function listWorkerCodeDirectory(branchPath: string, inputPath: string): WorkerCodeListResult;
|
|
993
1005
|
export declare function readWorkerCodeFile(branchPath: string, inputPath: string): WorkerCodeReadResult;
|
|
994
1006
|
export declare function readWorkerImageFile(branchPath: string, filePath: string, builtInPaths?: WorkerBuiltInToolPaths): WorkerViewFileBytesResult;
|
|
1007
|
+
declare function targetProcessEnv(rootDir: string, target: WorkerSessionTarget): Record<string, string>;
|
|
995
1008
|
export declare function prepareShellEnvForTarget(input: {
|
|
996
1009
|
target: WorkerSessionTarget;
|
|
997
1010
|
baseUrl: string;
|
|
998
1011
|
token: string;
|
|
999
1012
|
sessionId?: string;
|
|
1000
1013
|
activePlanId?: string;
|
|
1001
|
-
|
|
1002
|
-
planRoot: string;
|
|
1014
|
+
rootDir: string;
|
|
1003
1015
|
}): Promise<Record<string, string>>;
|
|
1004
1016
|
declare function createNodePtyBridge(options: {
|
|
1005
1017
|
file: string;
|