@ricsam/r5d-worker 0.0.101 → 0.0.104
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 +467 -336
- 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 +172 -63
- 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 +473 -338
- 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 +172 -64
- 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 +35 -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 +73 -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
|
@@ -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
|
@@ -93,6 +93,12 @@ type WorkspaceSyncResult = {
|
|
|
93
93
|
affectedPaths: string[];
|
|
94
94
|
activeMountIds?: string[];
|
|
95
95
|
skippedMountIds?: string[];
|
|
96
|
+
/** Configured checkouts this cycle excluded because their directory is absent or is not a linked worktree of the project. */
|
|
97
|
+
missingCheckouts?: Array<{
|
|
98
|
+
projectId: string;
|
|
99
|
+
branchName: string;
|
|
100
|
+
reason: "missing" | "not_a_worktree";
|
|
101
|
+
}>;
|
|
96
102
|
activeProjectBranchPublications?: Array<{
|
|
97
103
|
branchId: string;
|
|
98
104
|
projectId: string;
|
|
@@ -185,6 +191,11 @@ type WorkerClientMessage = {
|
|
|
185
191
|
requestId: string;
|
|
186
192
|
result?: WorkerOperationResult;
|
|
187
193
|
error?: string;
|
|
194
|
+
} | {
|
|
195
|
+
/** Immediate receipt for a branch create/delete; `queuePosition` is 0 when it runs at once. */
|
|
196
|
+
type: "operation_queued";
|
|
197
|
+
requestId: string;
|
|
198
|
+
queuePosition: number;
|
|
188
199
|
} | {
|
|
189
200
|
type: "exec_result";
|
|
190
201
|
requestId: string;
|
|
@@ -290,13 +301,18 @@ type WorkerServerMessage = {
|
|
|
290
301
|
requiredAncestorHeads?: string[];
|
|
291
302
|
} | {
|
|
292
303
|
/**
|
|
293
|
-
* Content-derived tokens naming
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
304
|
+
* Content-derived tokens naming ref state the server read from disk:
|
|
305
|
+
* `tokens` per canonical project repository, `mountTokens` per
|
|
306
|
+
* checked-out project branch (keyed by project id then branch name),
|
|
307
|
+
* and the current head of the hidden workspace repository. Replace-all:
|
|
308
|
+
* anything absent from a message has no advertised token and the
|
|
309
|
+
* corresponding fetch runs. Older servers send `tokens` only. See
|
|
310
|
+
* `project-mirror-fetch-policy.ts`.
|
|
297
311
|
*/
|
|
298
312
|
type: "project_mirror_refs_tokens";
|
|
299
313
|
tokens: Record<string, string>;
|
|
314
|
+
mountTokens?: Record<string, Record<string, string>>;
|
|
315
|
+
workspaceHead?: string;
|
|
300
316
|
} | {
|
|
301
317
|
type: "create_project_branch";
|
|
302
318
|
requestId: string;
|
|
@@ -304,6 +320,8 @@ type WorkerServerMessage = {
|
|
|
304
320
|
projectId: string;
|
|
305
321
|
sourceBranch: string;
|
|
306
322
|
targetBranch: string;
|
|
323
|
+
/** `carry` copies the source checkout's uncommitted changes; `clean` checks out its commit only. */
|
|
324
|
+
workingTree: "carry" | "clean";
|
|
307
325
|
} | {
|
|
308
326
|
type: "delete_project_branch";
|
|
309
327
|
requestId: string;
|
|
@@ -618,14 +636,17 @@ export declare const workerPtyTestHarness: {
|
|
|
618
636
|
removeEnvFiles: typeof removePtyEnvFiles;
|
|
619
637
|
resolveEnvFileReferences: typeof resolvePtyEnvFileReferences;
|
|
620
638
|
commandHasWorkspaceEffect: typeof workerCommandHasWorkspaceEffect;
|
|
639
|
+
targetProcessEnv: typeof targetProcessEnv;
|
|
621
640
|
canonicalSyncTerminalHead: typeof canonicalSyncTerminalHead;
|
|
622
641
|
ptyIsWorkspaceBusy: typeof workerPtyIsWorkspaceBusy;
|
|
623
642
|
parseLinuxForegroundBusy: typeof parseLinuxPtyForegroundBusy;
|
|
624
643
|
};
|
|
625
644
|
export type WorkerBuiltInToolPaths = {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
645
|
+
rootDir?: string;
|
|
646
|
+
sessionId?: string;
|
|
647
|
+
projectId?: string;
|
|
648
|
+
branchName?: string;
|
|
649
|
+
activePlanId?: string;
|
|
629
650
|
};
|
|
630
651
|
export declare function isArtifactEnvPath(filePath: string): boolean;
|
|
631
652
|
export declare function syncSessionArtifacts(input: {
|
|
@@ -638,11 +659,11 @@ export declare function prepareArtifactEnvForShell(input: {
|
|
|
638
659
|
baseUrl: string;
|
|
639
660
|
token: string;
|
|
640
661
|
sessionId?: string;
|
|
641
|
-
|
|
662
|
+
rootDir: string;
|
|
642
663
|
}): Promise<Record<string, string>>;
|
|
643
664
|
export declare function preparePlanEnvForShell(input: {
|
|
644
665
|
target: WorkerSessionTarget;
|
|
645
|
-
|
|
666
|
+
rootDir: string;
|
|
646
667
|
activePlanId?: string;
|
|
647
668
|
}): Record<string, string>;
|
|
648
669
|
type ResolvedWorkerFilePath = {
|
|
@@ -943,8 +964,7 @@ export declare function prepareBuiltInToolPathsForTarget(input: {
|
|
|
943
964
|
token: string;
|
|
944
965
|
sessionId?: string;
|
|
945
966
|
activePlanId?: string;
|
|
946
|
-
|
|
947
|
-
planRoot: string;
|
|
967
|
+
rootDir: string;
|
|
948
968
|
access: "read" | "write";
|
|
949
969
|
}): Promise<WorkerBuiltInToolPaths | undefined>;
|
|
950
970
|
export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number, builtInPaths?: WorkerBuiltInToolPaths): WorkerReadFileResult;
|
|
@@ -960,8 +980,7 @@ declare function executeWriteFileOperation(input: {
|
|
|
960
980
|
resolvedTarget: ResolvedWorkerSessionTarget;
|
|
961
981
|
baseUrl: string;
|
|
962
982
|
token: string;
|
|
963
|
-
|
|
964
|
-
planRoot: string;
|
|
983
|
+
rootDir: string;
|
|
965
984
|
assertAdmission: () => void;
|
|
966
985
|
}): Promise<WorkerWriteFileResult>;
|
|
967
986
|
declare function executeEditFileOperation(input: {
|
|
@@ -971,8 +990,7 @@ declare function executeEditFileOperation(input: {
|
|
|
971
990
|
resolvedTarget: ResolvedWorkerSessionTarget;
|
|
972
991
|
baseUrl: string;
|
|
973
992
|
token: string;
|
|
974
|
-
|
|
975
|
-
planRoot: string;
|
|
993
|
+
rootDir: string;
|
|
976
994
|
assertAdmission: () => void;
|
|
977
995
|
}): Promise<WorkerEditFileResult>;
|
|
978
996
|
export declare function grepWorkerFiles(branchPath: string, input: {
|
|
@@ -992,14 +1010,14 @@ export declare function listWorkerDirectory(branchPath: string, inputPath?: stri
|
|
|
992
1010
|
export declare function listWorkerCodeDirectory(branchPath: string, inputPath: string): WorkerCodeListResult;
|
|
993
1011
|
export declare function readWorkerCodeFile(branchPath: string, inputPath: string): WorkerCodeReadResult;
|
|
994
1012
|
export declare function readWorkerImageFile(branchPath: string, filePath: string, builtInPaths?: WorkerBuiltInToolPaths): WorkerViewFileBytesResult;
|
|
1013
|
+
declare function targetProcessEnv(rootDir: string, target: WorkerSessionTarget): Record<string, string>;
|
|
995
1014
|
export declare function prepareShellEnvForTarget(input: {
|
|
996
1015
|
target: WorkerSessionTarget;
|
|
997
1016
|
baseUrl: string;
|
|
998
1017
|
token: string;
|
|
999
1018
|
sessionId?: string;
|
|
1000
1019
|
activePlanId?: string;
|
|
1001
|
-
|
|
1002
|
-
planRoot: string;
|
|
1020
|
+
rootDir: string;
|
|
1003
1021
|
}): Promise<Record<string, string>>;
|
|
1004
1022
|
declare function createNodePtyBridge(options: {
|
|
1005
1023
|
file: string;
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Decides
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* observation fetch would transfer); when the advertised token equals the
|
|
6
|
-
* token the local refs already cover, the fetch would be a no-op.
|
|
2
|
+
* Decides which part of a project's hidden mirror a periodic observation must
|
|
3
|
+
* fetch, from the tokens the server advertises and the tokens the worker's
|
|
4
|
+
* own mirror refs cover (both computed with `project-mirror-refs-token.ts`).
|
|
7
5
|
*
|
|
8
|
-
* Skipping is fail-open: any missing or mismatched information fetches.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* Skipping is fail-open: any missing or mismatched information fetches.
|
|
7
|
+
*
|
|
8
|
+
* There is deliberately no timed backstop refetch for matching tokens. The
|
|
9
|
+
* server recomputes every advertised token from the canonical repository on
|
|
10
|
+
* disk on each heartbeat sweep, so a matching token is a fresh statement that
|
|
11
|
+
* the fetch would transfer nothing, whoever moved the refs. The worker
|
|
12
|
+
* records the tokens it covers from its own refs after every fetch and lease
|
|
13
|
+
* push, with the same shared formula, so a skip can only be stale if the
|
|
14
|
+
* canonical repository moved after the sweep that produced the advertisement
|
|
15
|
+
* — which the next sweep corrects, exactly like a tip that lands mid-cycle —
|
|
16
|
+
* or if the formula itself were wrong, which the shared module and its locked
|
|
17
|
+
* vectors guard and a periodic blind fetch would only hide. A project or
|
|
18
|
+
* mount the server advertises no token for fetches every cycle, as before
|
|
19
|
+
* tokens existed.
|
|
13
20
|
*/
|
|
14
|
-
|
|
21
|
+
/** Why a fetch runs. `fail_closed` is never decided here: reset and remediation observation bypasses the gate. */
|
|
22
|
+
export type ProjectMirrorFetchReason = "skipping_disabled" | "no_advertised_token" | "never_fetched" | "token_changed" | "fail_closed";
|
|
15
23
|
export type ProjectMirrorFetchDecision = {
|
|
16
24
|
fetch: true;
|
|
17
|
-
reason:
|
|
25
|
+
reason: ProjectMirrorFetchReason;
|
|
18
26
|
} | {
|
|
19
27
|
fetch: false;
|
|
20
28
|
reason: "token_match";
|
|
@@ -22,8 +30,55 @@ export type ProjectMirrorFetchDecision = {
|
|
|
22
30
|
export declare function projectMirrorFetchDecision(input: {
|
|
23
31
|
advertisedToken: string | undefined;
|
|
24
32
|
fetchedToken: string | undefined;
|
|
25
|
-
lastFetchAtMs: number | undefined;
|
|
26
|
-
nowMs: number;
|
|
27
33
|
skippingEnabled: boolean;
|
|
28
|
-
backstopMs?: number;
|
|
29
34
|
}): ProjectMirrorFetchDecision;
|
|
35
|
+
export type ProjectMirrorObservationFetchPlan =
|
|
36
|
+
/** Fetch every canonical ref of the project, as observation always did before per-mount tokens. */
|
|
37
|
+
{
|
|
38
|
+
scope: "project";
|
|
39
|
+
reason: ProjectMirrorFetchReason;
|
|
40
|
+
}
|
|
41
|
+
/** Fetch only the named mounts' branch heads and handoff refs. */
|
|
42
|
+
| {
|
|
43
|
+
scope: "mounts";
|
|
44
|
+
mounts: Array<{
|
|
45
|
+
branchName: string;
|
|
46
|
+
reason: ProjectMirrorFetchReason;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
/** Every token the observation depends on matches; reuse the local refs. */
|
|
50
|
+
| {
|
|
51
|
+
scope: "none";
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Plan one project's observation fetch. With per-mount tokens advertised for
|
|
55
|
+
* the project, each mount decides for itself and only changed mounts fetch;
|
|
56
|
+
* without them (an older server) the project token decides for the whole
|
|
57
|
+
* mirror.
|
|
58
|
+
*/
|
|
59
|
+
export declare function planProjectMirrorObservationFetch(input: {
|
|
60
|
+
branchNames: readonly string[];
|
|
61
|
+
advertisedProjectToken: string | undefined;
|
|
62
|
+
/** Per-mount tokens for this project, keyed by branch name; undefined when the server advertised none. */
|
|
63
|
+
advertisedMountTokens: ReadonlyMap<string, string> | undefined;
|
|
64
|
+
fetchedProjectToken: string | undefined;
|
|
65
|
+
fetchedMountTokens: ReadonlyMap<string, string>;
|
|
66
|
+
skippingEnabled: boolean;
|
|
67
|
+
}): ProjectMirrorObservationFetchPlan;
|
|
68
|
+
export type ProjectMirrorRefsTokensAdvertisement = {
|
|
69
|
+
/** One token per project id. */
|
|
70
|
+
projectTokens: Map<string, string>;
|
|
71
|
+
/** Per-mount tokens keyed by project id then branch name; a project is present only when the server advertised mount tokens for it. */
|
|
72
|
+
mountTokens: Map<string, Map<string, string>>;
|
|
73
|
+
/** Current head of the user's hidden workspace repository, when advertised. */
|
|
74
|
+
workspaceHead: string | undefined;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Read a `project_mirror_refs_tokens` message. Older servers send only
|
|
78
|
+
* `tokens`; malformed entries are dropped, which fails open into fetching.
|
|
79
|
+
*/
|
|
80
|
+
export declare function parseProjectMirrorRefsTokensMessage(message: {
|
|
81
|
+
tokens?: unknown;
|
|
82
|
+
mountTokens?: unknown;
|
|
83
|
+
workspaceHead?: unknown;
|
|
84
|
+
}): ProjectMirrorRefsTokensAdvertisement;
|
|
@@ -1,15 +1,40 @@
|
|
|
1
|
+
/** Canonical namespaces the token hashes; also the observation fetch's sources. */
|
|
2
|
+
export declare const PROJECT_MIRROR_REFS_TOKEN_NAMESPACES: readonly ["refs/heads/", "refs/r5d/server-synced/"];
|
|
3
|
+
/** Worker-local copy of a canonical `refs/heads/*` ref. */
|
|
1
4
|
export declare const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
|
|
2
5
|
export declare const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
|
|
6
|
+
/** `git for-each-ref` format both sides list refs with; see parseProjectMirrorRefListing. */
|
|
7
|
+
export declare const PROJECT_MIRROR_REF_LISTING_FORMAT = "--format=%(objectname) %(refname)";
|
|
3
8
|
export type ProjectMirrorRef = {
|
|
4
9
|
refname: string;
|
|
5
10
|
objectName: string;
|
|
6
11
|
};
|
|
12
|
+
/** One checked-out project branch, identified by its durable incarnation id. */
|
|
13
|
+
export type ProjectMirrorMount = {
|
|
14
|
+
branchName: string;
|
|
15
|
+
branchId: string;
|
|
16
|
+
};
|
|
17
|
+
export type ProjectMirrorRefsTokens = {
|
|
18
|
+
/** Hash over every ref in the observed namespaces. */
|
|
19
|
+
project: string;
|
|
20
|
+
/** Hash over one mount's branch head plus its handoff refs, keyed by branch name. */
|
|
21
|
+
mounts: Map<string, string>;
|
|
22
|
+
};
|
|
7
23
|
/**
|
|
8
24
|
* Hash a ref listing into a mirror-refs token. Both sides sort here rather
|
|
9
25
|
* than trusting git's listing order: renaming `refs/r5d/mirror/*` back to
|
|
10
26
|
* `refs/heads/*` changes sort neighborhoods.
|
|
11
27
|
*/
|
|
12
28
|
export declare function hashProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): string;
|
|
29
|
+
/**
|
|
30
|
+
* Parse `git for-each-ref` output produced with
|
|
31
|
+
* PROJECT_MIRROR_REF_LISTING_FORMAT. Malformed rows throw rather than being
|
|
32
|
+
* dropped: a caller that cannot trust its listing must not produce a token.
|
|
33
|
+
* With `namespaces`, rows outside them throw too.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseProjectMirrorRefListing(output: string, options?: {
|
|
36
|
+
namespaces?: readonly string[];
|
|
37
|
+
}): ProjectMirrorRef[];
|
|
13
38
|
/**
|
|
14
39
|
* Rename locally fetched mirror refs into the namespaces the server hashes.
|
|
15
40
|
* Rows outside the two observed namespaces are dropped; the caller lists only
|
|
@@ -17,3 +42,12 @@ export declare function hashProjectMirrorRefs(refs: readonly ProjectMirrorRef[])
|
|
|
17
42
|
* it can only make tokens differ — which fails open into a fetch.
|
|
18
43
|
*/
|
|
19
44
|
export declare function canonicalizeLocalProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): ProjectMirrorRef[];
|
|
45
|
+
/** The canonical refs one mount's token covers: its branch head and its server-synced handoff refs. */
|
|
46
|
+
export declare function projectMirrorMountRefs(refs: readonly ProjectMirrorRef[], mount: ProjectMirrorMount): ProjectMirrorRef[];
|
|
47
|
+
/**
|
|
48
|
+
* Compute the project token and one token per mount from one canonical ref
|
|
49
|
+
* listing. A mount's token is the project formula applied to the mount's own
|
|
50
|
+
* refs, so a mount whose branch is the only ref of its project has the same
|
|
51
|
+
* token as the project.
|
|
52
|
+
*/
|
|
53
|
+
export declare function projectMirrorRefsTokens(refs: readonly ProjectMirrorRef[], mounts: readonly ProjectMirrorMount[]): ProjectMirrorRefsTokens;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import { type ProjectMirrorMount, type ProjectMirrorRefsTokens } from "./project-mirror-refs-token";
|
|
1
2
|
export declare const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
2
3
|
declare const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION: 1;
|
|
4
|
+
/**
|
|
5
|
+
* `project` snapshots cover every configured branch and restore by replacing
|
|
6
|
+
* the whole project root. `branches` snapshots cover only the checkouts one
|
|
7
|
+
* reconciliation rewrites and restore those paths individually, so a rollback
|
|
8
|
+
* never touches a checkout the transaction did not.
|
|
9
|
+
*/
|
|
10
|
+
type ProjectWorktreeSnapshotScope = "project" | "branches";
|
|
3
11
|
type ProjectWorktreeSnapshotManifest = {
|
|
4
12
|
version: typeof PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION;
|
|
5
13
|
state: "building" | "prepared" | "restoring" | "consumed";
|
|
14
|
+
scope: ProjectWorktreeSnapshotScope;
|
|
6
15
|
ownerProcessId: number;
|
|
7
16
|
ownerSessionId: string;
|
|
8
17
|
projectRoot: string;
|
|
@@ -124,10 +133,17 @@ export declare function ensureProjectWorktrees(input: {
|
|
|
124
133
|
onSnapshotProgress?: (progress: ProjectWorktreeSnapshotProgress) => void;
|
|
125
134
|
}): Promise<ProjectWorktreeState[]>;
|
|
126
135
|
export declare function projectWorktreeOperationInProgress(checkoutPath: string): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* How a new linked worktree's working tree starts. `carry` materializes the
|
|
138
|
+
* source checkout's Git-visible working state, uncommitted changes included;
|
|
139
|
+
* `clean` checks out the source branch's current commit and nothing else.
|
|
140
|
+
*/
|
|
141
|
+
export type ProjectWorktreeWorkingTreeMode = "carry" | "clean";
|
|
127
142
|
export declare function createLinkedProjectBranch(input: {
|
|
128
143
|
projectRoot: string;
|
|
129
144
|
sourceBranchName: string;
|
|
130
145
|
branchName: string;
|
|
146
|
+
workingTree: ProjectWorktreeWorkingTreeMode;
|
|
131
147
|
}): {
|
|
132
148
|
branchPath: string;
|
|
133
149
|
baseCommitHash: string;
|
|
@@ -143,17 +159,56 @@ export declare function createOrRetryLinkedProjectBranch(input: {
|
|
|
143
159
|
primaryBranchName: string;
|
|
144
160
|
sourceBranchName: string;
|
|
145
161
|
branchName: string;
|
|
162
|
+
workingTree: ProjectWorktreeWorkingTreeMode;
|
|
146
163
|
pendingRetry: boolean;
|
|
147
164
|
}): {
|
|
148
165
|
branchPath: string;
|
|
149
166
|
baseCommitHash: string;
|
|
150
167
|
};
|
|
168
|
+
export type ProjectCheckoutState = "linked" | "missing" | "not_a_worktree";
|
|
169
|
+
export type ProjectCheckoutInventory = {
|
|
170
|
+
/** Branches whose checkout is this project's primary clone or one of its linked worktrees. */
|
|
171
|
+
linkedBranchNames: string[];
|
|
172
|
+
missingCheckouts: Array<{
|
|
173
|
+
branchName: string;
|
|
174
|
+
checkoutPath: string;
|
|
175
|
+
reason: Exclude<ProjectCheckoutState, "linked">;
|
|
176
|
+
}>;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Classify one configured branch checkout without spawning git, cheap enough
|
|
180
|
+
* to run for every configured branch on every sync cycle. The primary
|
|
181
|
+
* checkout is `linked` when its `.git` is a directory; another branch is
|
|
182
|
+
* `linked` when its `.git` pointer file resolves into that primary clone's
|
|
183
|
+
* `.git/worktrees` registry and the registered directory still exists.
|
|
184
|
+
* `missing` means the checkout path is absent. Everything else (a hydrated
|
|
185
|
+
* directory that never became a checkout, a foreign clone, a pointer to a
|
|
186
|
+
* pruned registration) is `not_a_worktree`.
|
|
187
|
+
*/
|
|
188
|
+
export declare function classifyProjectCheckout(input: {
|
|
189
|
+
projectRoot: string;
|
|
190
|
+
primaryBranchName: string;
|
|
191
|
+
branchName: string;
|
|
192
|
+
}): ProjectCheckoutState;
|
|
193
|
+
/**
|
|
194
|
+
* Partition a ready project's configured branches into checkouts a sync cycle
|
|
195
|
+
* may use and checkouts it must skip. A primary clone that is not linked takes
|
|
196
|
+
* every branch with it: nothing can be observed or published through it.
|
|
197
|
+
*/
|
|
198
|
+
export declare function inventoryProjectCheckouts(input: {
|
|
199
|
+
projectRoot: string;
|
|
200
|
+
primaryBranchName: string;
|
|
201
|
+
branchNames: readonly string[];
|
|
202
|
+
}): ProjectCheckoutInventory;
|
|
203
|
+
export declare const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
|
|
151
204
|
export declare function deleteLinkedProjectBranch(input: {
|
|
152
205
|
projectRoot: string;
|
|
153
206
|
primaryBranchName: string;
|
|
154
207
|
branchName: string;
|
|
155
208
|
}): {
|
|
156
209
|
branchName: string;
|
|
210
|
+
/** Where a path that was not this project's linked worktree was set aside instead of being deleted. */
|
|
211
|
+
movedAsidePath?: string;
|
|
157
212
|
};
|
|
158
213
|
export declare function removeProjectWorktrees(input: {
|
|
159
214
|
projectRoot: string;
|
|
@@ -225,19 +280,28 @@ export declare function observeProjectMirrorHeads(input: Omit<ProjectMirrorHeadU
|
|
|
225
280
|
allowNonFastForward: boolean;
|
|
226
281
|
branchIds?: ReadonlyMap<string, string>;
|
|
227
282
|
refreshRemote?: boolean;
|
|
283
|
+
/**
|
|
284
|
+
* Fetch only these mounts' branch heads and handoff refs, because
|
|
285
|
+
* per-mount token gating found every other mount current. Undefined
|
|
286
|
+
* fetches the whole mirror; an empty set fetches nothing, exactly like
|
|
287
|
+
* `refreshRemote: false`.
|
|
288
|
+
*/
|
|
289
|
+
fetchOnlyBranches?: ReadonlySet<string>;
|
|
228
290
|
}): Promise<ProjectMirrorHeadObservation[]>;
|
|
229
291
|
/**
|
|
230
|
-
* Compute the
|
|
231
|
-
* token
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
292
|
+
* Compute the tokens the locally cached mirror state covers — the project
|
|
293
|
+
* token and one token per mount — from one listing of `refs/r5d/mirror/*` and
|
|
294
|
+
* `refs/r5d/server-synced/*`, with the formula shared with the server. After a
|
|
295
|
+
* fetch these name exactly the canonical state the fetch transferred; after a
|
|
296
|
+
* successful lease push (which updates `refs/r5d/mirror/*` above) they predict
|
|
297
|
+
* the server's next advertisement unless something else also moved the
|
|
298
|
+
* canonical repository, in which case the mismatch fails open into a fetch.
|
|
236
299
|
*/
|
|
237
|
-
export declare function
|
|
300
|
+
export declare function computeLocalProjectMirrorRefsTokens(input: {
|
|
238
301
|
projectRoot: string;
|
|
239
302
|
primaryBranchName: string;
|
|
240
|
-
|
|
303
|
+
mounts: readonly ProjectMirrorMount[];
|
|
304
|
+
}): Promise<ProjectMirrorRefsTokens>;
|
|
241
305
|
/** Apply only previously observed object ids; this function never refetches. */
|
|
242
306
|
export declare function applyObservedProjectMirrorHeads(input: {
|
|
243
307
|
projectRoot: string;
|
|
@@ -272,6 +336,7 @@ export declare const projectWorktreesTestHarness: {
|
|
|
272
336
|
projectRoot: string;
|
|
273
337
|
branches: readonly ProjectWorktreeBranch[];
|
|
274
338
|
temporaryRoot: string;
|
|
339
|
+
scope?: ProjectWorktreeSnapshotScope;
|
|
275
340
|
}): {
|
|
276
341
|
root: string;
|
|
277
342
|
};
|
|
@@ -38,6 +38,14 @@ export type WorkspaceGitMount = {
|
|
|
38
38
|
busy?: () => boolean;
|
|
39
39
|
/** Monotonic/opaque visible-mutation token used to catch short writes that begin and end between busy checks. */
|
|
40
40
|
mutationToken?: () => string;
|
|
41
|
+
/**
|
|
42
|
+
* The token a completed cycle observed when it last projected this mount.
|
|
43
|
+
* While the current token still equals it and the mount's hydration basis
|
|
44
|
+
* is the outer HEAD being projected, the outer subtree already carries the
|
|
45
|
+
* visible bytes and the mirror copy is skipped. This is a cross-cycle skip
|
|
46
|
+
* only; the per-cycle busy and token re-checks are unchanged.
|
|
47
|
+
*/
|
|
48
|
+
lastProjectedMutationToken?: string;
|
|
41
49
|
/** Activity-only fence used before checkout readiness is established on reconnect. */
|
|
42
50
|
busyForRecovery?: () => boolean;
|
|
43
51
|
};
|
|
@@ -86,8 +94,12 @@ export type WorkspaceGitSyncResult = {
|
|
|
86
94
|
affectedPaths: string[];
|
|
87
95
|
activeMountIds: string[];
|
|
88
96
|
skippedMountIds: string[];
|
|
97
|
+
/** Token each active mount carried when this cycle projected it; recorded by the caller to skip unchanged mounts later. */
|
|
98
|
+
projectedMutationTokens?: Record<string, string>;
|
|
89
99
|
/** Creator-local mounts whose first outer publication was measured against their source subtree. */
|
|
90
100
|
initialPublicationMountIds?: string[];
|
|
101
|
+
/** Network fetches of the outer workspace head this cycle ran; zero when the advertised head proved them unnecessary. */
|
|
102
|
+
remoteHeadFetches?: number;
|
|
91
103
|
conflictPaths?: string[];
|
|
92
104
|
conflictSnapshotRefs?: {
|
|
93
105
|
local: string;
|
|
@@ -150,9 +162,12 @@ export declare function ensureWorkspaceGitClone(input: {
|
|
|
150
162
|
email: string;
|
|
151
163
|
};
|
|
152
164
|
preserveResolutionInProgress?: boolean;
|
|
165
|
+
/** Server-advertised head of the workspace remote; see resolveWorkspaceRemoteHead. */
|
|
166
|
+
advertisedRemoteHead?: string | null;
|
|
153
167
|
}): Promise<{
|
|
154
168
|
localHead: string | null;
|
|
155
169
|
remoteHead: string | null;
|
|
170
|
+
remoteHeadFetched: boolean;
|
|
156
171
|
}>;
|
|
157
172
|
type ProjectedMountGitlinks = {
|
|
158
173
|
mount: WorkspaceGitMount;
|
|
@@ -204,7 +219,7 @@ export declare function resetWorkspaceGit(input: {
|
|
|
204
219
|
* such as a subtree a gitlink replaces (see `pathsReplacedByGitlinks`).
|
|
205
220
|
*/
|
|
206
221
|
declare function diffSizeBytes(workspacePath: string, baseRevision: string | null, headRevision: string, limit: number, pathspecs?: readonly string[]): Promise<number>;
|
|
207
|
-
export
|
|
222
|
+
export type SynchronizeWorkspaceGitInput = {
|
|
208
223
|
attemptId?: string;
|
|
209
224
|
workerLabel: string;
|
|
210
225
|
workspacePath: string;
|
|
@@ -224,6 +239,12 @@ export declare function synchronizeWorkspaceGit(input: {
|
|
|
224
239
|
skipMountMirror?: boolean;
|
|
225
240
|
/** Positive ancestry proof required before a remediation may integrate, publish, or hydrate. */
|
|
226
241
|
requiredAncestorHeads?: readonly string[];
|
|
242
|
+
/**
|
|
243
|
+
* Head of the workspace remote as the server last read it from disk. Equal
|
|
244
|
+
* to the local remote-tracking ref, it lets the cycle skip its workspace
|
|
245
|
+
* head fetches; see resolveWorkspaceRemoteHead. Omit to always fetch.
|
|
246
|
+
*/
|
|
247
|
+
advertisedRemoteHead?: string | null;
|
|
227
248
|
/** Revalidate the worker/config/incident generation after every async boundary. */
|
|
228
249
|
assertStillAdmitted?: () => void;
|
|
229
250
|
/** Test seam for exercising an incident transition during asynchronous diff measurement. */
|
|
@@ -234,7 +255,8 @@ export declare function synchronizeWorkspaceGit(input: {
|
|
|
234
255
|
}) => void | Promise<void>;
|
|
235
256
|
/** Announces each synchronous hydration transaction of this cycle; see WorkspaceHydrationTransactionHooks. */
|
|
236
257
|
hydrationHooks?: WorkspaceHydrationTransactionHooks;
|
|
237
|
-
}
|
|
258
|
+
};
|
|
259
|
+
export declare function synchronizeWorkspaceGit(input: SynchronizeWorkspaceGitInput): Promise<WorkspaceGitSyncResult>;
|
|
238
260
|
export declare const workspaceGitSyncTestHarness: {
|
|
239
261
|
commandArgs: typeof gitCommandArgs;
|
|
240
262
|
workspaceCloneCommandArgs: typeof workspaceCloneCommandArgs;
|