@ricsam/r5d-worker 0.0.121 → 0.0.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/main.cjs +22 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-checkout-garbage.cjs +190 -0
- package/dist/cjs/project-worktrees.cjs +9 -2
- package/dist/cjs/recovery-store.cjs +13 -0
- package/dist/mjs/main.mjs +22 -1
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-checkout-garbage.mjs +150 -0
- package/dist/mjs/project-worktrees.mjs +9 -2
- package/dist/mjs/recovery-store.mjs +13 -0
- package/dist/types/project-checkout-garbage.d.ts +69 -0
- package/dist/types/project-worktrees.d.ts +6 -0
- package/dist/types/recovery-store.d.ts +8 -0
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -74,6 +74,7 @@ var import_bun_sqlite = require("bun:sqlite");
|
|
|
74
74
|
var import_cli_update = require("./cli-update.cjs");
|
|
75
75
|
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
76
76
|
var import_process_tree = require("./process-tree.cjs");
|
|
77
|
+
var import_project_checkout_garbage = require("./project-checkout-garbage.cjs");
|
|
77
78
|
var import_pty_output_coalescer = require("./pty-output-coalescer.cjs");
|
|
78
79
|
var import_port_forward_client = require("./port-forward-client.cjs");
|
|
79
80
|
var import_registry_auth = require("./registry-auth.cjs");
|
|
@@ -274,6 +275,15 @@ let workerAdmissionGeneration = 0;
|
|
|
274
275
|
const workspaceMutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
|
|
275
276
|
let workspaceSyncQueue = Promise.resolve();
|
|
276
277
|
let startupProjectSnapshotRecoveryCompleted = false;
|
|
278
|
+
const checkoutGarbageCollectors = /* @__PURE__ */ new Map();
|
|
279
|
+
function checkoutGarbageCollectorFor(projectsRoot) {
|
|
280
|
+
let collector = checkoutGarbageCollectors.get(projectsRoot);
|
|
281
|
+
if (!collector) {
|
|
282
|
+
collector = new import_project_checkout_garbage.ProjectCheckoutGarbageCollector({ projectsRoot });
|
|
283
|
+
checkoutGarbageCollectors.set(projectsRoot, collector);
|
|
284
|
+
}
|
|
285
|
+
return collector;
|
|
286
|
+
}
|
|
277
287
|
const workspaceSyncSingleFlight = {
|
|
278
288
|
runExclusive(operation) {
|
|
279
289
|
const queued = workspaceMutationGate.runSync(operation);
|
|
@@ -3704,6 +3714,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
3704
3714
|
projectRuntime.initializedWorkspaceState = initializedWorkspaceState;
|
|
3705
3715
|
const projectWorkspaceStateStore = initializedWorkspaceState.store;
|
|
3706
3716
|
let projectWorkspaceState = initializedWorkspaceState.store.read();
|
|
3717
|
+
const checkoutGarbageCollector = checkoutGarbageCollectorFor(projectsRoot);
|
|
3718
|
+
const staleStagedCheckouts = checkoutGarbageCollector.sweep();
|
|
3719
|
+
if (staleStagedCheckouts > 0) {
|
|
3720
|
+
process.stderr.write(`[r5d-worker] collecting ${staleStagedCheckouts} deleted checkout(s) left from an earlier run
|
|
3721
|
+
`);
|
|
3722
|
+
}
|
|
3707
3723
|
const { projectConfigById, readyProjectIds, reconciledProjectConfigFingerprints } = projectRuntime;
|
|
3708
3724
|
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
3709
3725
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
@@ -3818,6 +3834,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
3818
3834
|
`
|
|
3819
3835
|
);
|
|
3820
3836
|
}
|
|
3837
|
+
if (deleted.stagedForCollectionPath) checkoutGarbageCollector.enqueue(deleted.stagedForCollectionPath);
|
|
3821
3838
|
}
|
|
3822
3839
|
const planSourcePath = import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/"));
|
|
3823
3840
|
import_node_fs.default.rmSync(planSourcePath, { recursive: true, force: true });
|
|
@@ -3901,6 +3918,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
3901
3918
|
`
|
|
3902
3919
|
);
|
|
3903
3920
|
}
|
|
3921
|
+
if (deleted.stagedForCollectionPath) checkoutGarbageCollector.enqueue(deleted.stagedForCollectionPath);
|
|
3904
3922
|
import_node_fs.default.rmSync(import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
|
|
3905
3923
|
readyProjectIds.delete(project.projectId);
|
|
3906
3924
|
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
@@ -5835,6 +5853,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5835
5853
|
workspaceIncidentConfigDeferralV1: true,
|
|
5836
5854
|
workspaceConfigResetToCanonicalV1: true,
|
|
5837
5855
|
projectBranchDeletionFastAckV1: true,
|
|
5856
|
+
projectBranchDeletionStagedRemovalV1: true,
|
|
5838
5857
|
projectMirrorLeaseV1: true,
|
|
5839
5858
|
projectMirrorRefsTokensV1: true,
|
|
5840
5859
|
projectBranchWorkingTreeModeV1: true
|
|
@@ -5936,7 +5955,9 @@ async function startWorker(options, projectRuntime = {
|
|
|
5936
5955
|
if (admission !== "new") {
|
|
5937
5956
|
const response = recoveryStore.response(operationRequestId);
|
|
5938
5957
|
if (response && admission !== "unknown") sendReplayWorkerMessage(ws, response);
|
|
5939
|
-
|
|
5958
|
+
if (!(admission === "unknown" && message.type === "delete_project_branch" && recoveryStore.readmitUnknownBranchDeletion(operationRequestId))) {
|
|
5959
|
+
return;
|
|
5960
|
+
}
|
|
5940
5961
|
}
|
|
5941
5962
|
}
|
|
5942
5963
|
const messageAdmissionGeneration = workerAdmissionGeneration;
|
package/dist/cjs/package.json
CHANGED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var project_checkout_garbage_exports = {};
|
|
30
|
+
__export(project_checkout_garbage_exports, {
|
|
31
|
+
PROJECT_DELETED_CHECKOUTS_DIRECTORY: () => PROJECT_DELETED_CHECKOUTS_DIRECTORY,
|
|
32
|
+
ProjectCheckoutGarbageCollector: () => ProjectCheckoutGarbageCollector,
|
|
33
|
+
discoverStagedProjectCheckouts: () => discoverStagedProjectCheckouts,
|
|
34
|
+
isStagedProjectCheckoutPath: () => isStagedProjectCheckoutPath,
|
|
35
|
+
ownedStagedPath: () => ownedStagedPath,
|
|
36
|
+
removeStagedProjectCheckout: () => removeStagedProjectCheckout,
|
|
37
|
+
stageProjectCheckoutForDeletion: () => stageProjectCheckoutForDeletion
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(project_checkout_garbage_exports);
|
|
40
|
+
var import_node_crypto = require("node:crypto");
|
|
41
|
+
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
42
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
43
|
+
const PROJECT_DELETED_CHECKOUTS_DIRECTORY = ".r5d-deleted";
|
|
44
|
+
function isRealDirectory(candidate) {
|
|
45
|
+
try {
|
|
46
|
+
return import_node_fs.default.lstatSync(candidate).isDirectory();
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function stageProjectCheckoutForDeletion(projectRoot, branchName, checkoutPath) {
|
|
52
|
+
const garbageRoot = import_node_path.default.join(projectRoot, PROJECT_DELETED_CHECKOUTS_DIRECTORY);
|
|
53
|
+
if (!isRealDirectory(checkoutPath)) throw new Error(`Cannot stage ${checkoutPath} for deletion: not a directory`);
|
|
54
|
+
import_node_fs.default.mkdirSync(garbageRoot, { recursive: true });
|
|
55
|
+
if (!isRealDirectory(garbageRoot)) throw new Error(`Cannot stage ${checkoutPath} for deletion: ${garbageRoot} is not a directory`);
|
|
56
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
57
|
+
const target = import_node_path.default.join(garbageRoot, `${branchName.replace(/\//g, "__")}-${timestamp}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`);
|
|
58
|
+
import_node_fs.default.renameSync(checkoutPath, target);
|
|
59
|
+
return target;
|
|
60
|
+
}
|
|
61
|
+
function isStagedProjectCheckoutPath(projectsRoot, candidate) {
|
|
62
|
+
const relative = import_node_path.default.relative(import_node_path.default.resolve(projectsRoot), import_node_path.default.resolve(candidate));
|
|
63
|
+
if (!relative || relative.startsWith("..") || import_node_path.default.isAbsolute(relative)) return false;
|
|
64
|
+
const segments = relative.split(import_node_path.default.sep);
|
|
65
|
+
return segments.length === 4 && segments[2] === PROJECT_DELETED_CHECKOUTS_DIRECTORY && segments[3] !== "" && !segments[3].startsWith(".");
|
|
66
|
+
}
|
|
67
|
+
function ownedStagedPath(projectsRoot, candidate) {
|
|
68
|
+
const root = import_node_path.default.resolve(projectsRoot);
|
|
69
|
+
const resolved = import_node_path.default.resolve(candidate);
|
|
70
|
+
if (!isStagedProjectCheckoutPath(root, resolved)) return null;
|
|
71
|
+
let current = root;
|
|
72
|
+
for (const segment of import_node_path.default.relative(root, import_node_path.default.dirname(resolved)).split(import_node_path.default.sep)) {
|
|
73
|
+
current = import_node_path.default.join(current, segment);
|
|
74
|
+
if (!isRealDirectory(current)) return null;
|
|
75
|
+
}
|
|
76
|
+
let stat;
|
|
77
|
+
try {
|
|
78
|
+
stat = import_node_fs.default.lstatSync(resolved);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (stat.isSymbolicLink()) return { kind: "link" };
|
|
83
|
+
return stat.isDirectory() ? { kind: "directory" } : null;
|
|
84
|
+
}
|
|
85
|
+
function realDirectoryEntries(directory) {
|
|
86
|
+
if (!isRealDirectory(directory)) return [];
|
|
87
|
+
try {
|
|
88
|
+
return import_node_fs.default.readdirSync(directory, { withFileTypes: true }).map((entry) => entry.name).sort();
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function discoverStagedProjectCheckouts(projectsRoot) {
|
|
94
|
+
const root = import_node_path.default.resolve(projectsRoot);
|
|
95
|
+
const staged = [];
|
|
96
|
+
for (const namespace of realDirectoryEntries(root)) {
|
|
97
|
+
if (namespace.startsWith(".")) continue;
|
|
98
|
+
for (const project of realDirectoryEntries(import_node_path.default.join(root, namespace))) {
|
|
99
|
+
if (project.startsWith(".")) continue;
|
|
100
|
+
const garbageRoot = import_node_path.default.join(root, namespace, project, PROJECT_DELETED_CHECKOUTS_DIRECTORY);
|
|
101
|
+
for (const entry of realDirectoryEntries(garbageRoot)) {
|
|
102
|
+
const candidate = import_node_path.default.join(garbageRoot, entry);
|
|
103
|
+
if (ownedStagedPath(root, candidate)) staged.push(candidate);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return staged;
|
|
108
|
+
}
|
|
109
|
+
async function removeStagedProjectCheckout(stagedPath) {
|
|
110
|
+
if (import_node_fs.default.lstatSync(stagedPath).isSymbolicLink()) {
|
|
111
|
+
import_node_fs.default.unlinkSync(stagedPath);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (process.platform === "win32") {
|
|
115
|
+
await import_node_fs.default.promises.rm(stagedPath, { recursive: true, force: true });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const command = ["rm", "-rf", "--", stagedPath];
|
|
119
|
+
if (Bun.which("nice")) command.unshift("nice", "-n", "19");
|
|
120
|
+
if (process.platform === "linux" && Bun.which("ionice")) command.unshift("ionice", "-c", "3");
|
|
121
|
+
const child = Bun.spawn(command, { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
|
|
122
|
+
const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
|
|
123
|
+
if (exitCode !== 0) throw new Error(`rm exited ${exitCode}: ${stderr.trim()}`);
|
|
124
|
+
if (import_node_fs.default.existsSync(stagedPath)) throw new Error("staged checkout still exists after removal");
|
|
125
|
+
}
|
|
126
|
+
class ProjectCheckoutGarbageCollector {
|
|
127
|
+
projectsRoot;
|
|
128
|
+
remove;
|
|
129
|
+
log;
|
|
130
|
+
queued = /* @__PURE__ */ new Set();
|
|
131
|
+
tail = Promise.resolve();
|
|
132
|
+
collectedCount = 0;
|
|
133
|
+
constructor(options) {
|
|
134
|
+
this.projectsRoot = import_node_path.default.resolve(options.projectsRoot);
|
|
135
|
+
this.remove = options.remove ?? removeStagedProjectCheckout;
|
|
136
|
+
this.log = options.log ?? ((message) => process.stderr.write(`${message}
|
|
137
|
+
`));
|
|
138
|
+
}
|
|
139
|
+
/** Number of removals that have completed successfully. */
|
|
140
|
+
get collected() {
|
|
141
|
+
return this.collectedCount;
|
|
142
|
+
}
|
|
143
|
+
/** Resolves once everything queued so far has been attempted. */
|
|
144
|
+
get idle() {
|
|
145
|
+
return this.tail;
|
|
146
|
+
}
|
|
147
|
+
enqueue(stagedPath) {
|
|
148
|
+
const resolved = import_node_path.default.resolve(stagedPath);
|
|
149
|
+
if (!ownedStagedPath(this.projectsRoot, resolved)) {
|
|
150
|
+
throw new Error(`Refusing to collect ${stagedPath}: not a staged checkout under ${this.projectsRoot}`);
|
|
151
|
+
}
|
|
152
|
+
if (this.queued.has(resolved)) return;
|
|
153
|
+
this.queued.add(resolved);
|
|
154
|
+
this.tail = this.tail.then(async () => {
|
|
155
|
+
try {
|
|
156
|
+
const owned = ownedStagedPath(this.projectsRoot, resolved);
|
|
157
|
+
if (owned) {
|
|
158
|
+
const startedAt = performance.now();
|
|
159
|
+
await this.remove(resolved);
|
|
160
|
+
this.collectedCount += 1;
|
|
161
|
+
this.log(`[r5d-worker] collected deleted checkout ${resolved} in ${Math.round(performance.now() - startedAt)}ms`);
|
|
162
|
+
} else if (import_node_fs.default.existsSync(resolved) || isRealDirectory(import_node_path.default.dirname(resolved))) {
|
|
163
|
+
this.log(`[r5d-worker] skipped deleted checkout ${resolved}: no longer an owned staged checkout`);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.log(
|
|
167
|
+
`[r5d-worker] deleted checkout ${resolved} could not be collected yet: ${error instanceof Error ? error.message : String(error)}`
|
|
168
|
+
);
|
|
169
|
+
} finally {
|
|
170
|
+
this.queued.delete(resolved);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** Enqueue every staged checkout left behind by an earlier process. */
|
|
175
|
+
sweep() {
|
|
176
|
+
const staged = discoverStagedProjectCheckouts(this.projectsRoot);
|
|
177
|
+
for (const stagedPath of staged) this.enqueue(stagedPath);
|
|
178
|
+
return staged.length;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
182
|
+
0 && (module.exports = {
|
|
183
|
+
PROJECT_DELETED_CHECKOUTS_DIRECTORY,
|
|
184
|
+
ProjectCheckoutGarbageCollector,
|
|
185
|
+
discoverStagedProjectCheckouts,
|
|
186
|
+
isStagedProjectCheckoutPath,
|
|
187
|
+
ownedStagedPath,
|
|
188
|
+
removeStagedProjectCheckout,
|
|
189
|
+
stageProjectCheckoutForDeletion
|
|
190
|
+
});
|
|
@@ -61,6 +61,7 @@ var import_node_os = __toESM(require("node:os"), 1);
|
|
|
61
61
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
62
62
|
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
63
63
|
var import_managed_paths = require("./managed-paths.cjs");
|
|
64
|
+
var import_project_checkout_garbage = require("./project-checkout-garbage.cjs");
|
|
64
65
|
var import_project_mirror_refs_token = require("./project-mirror-refs-token.cjs");
|
|
65
66
|
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
66
67
|
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
@@ -1340,6 +1341,7 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1340
1341
|
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
1341
1342
|
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1342
1343
|
let movedAsidePath;
|
|
1344
|
+
let stagedForCollectionPath;
|
|
1343
1345
|
if (import_node_fs.default.existsSync(checkoutPath)) {
|
|
1344
1346
|
const checkoutCommonDir = import_node_fs.default.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
|
|
1345
1347
|
if (checkoutCommonDir !== primaryCommonDir) {
|
|
@@ -1349,7 +1351,8 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1349
1351
|
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1350
1352
|
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
1351
1353
|
}
|
|
1352
|
-
|
|
1354
|
+
stagedForCollectionPath = (0, import_project_checkout_garbage.stageProjectCheckoutForDeletion)(input.projectRoot, input.branchName, checkoutPath);
|
|
1355
|
+
git(primaryPath, ["worktree", "prune"], `prune linked worktree ${input.branchName}`);
|
|
1353
1356
|
}
|
|
1354
1357
|
} else {
|
|
1355
1358
|
tryGit(primaryPath, ["worktree", "prune"]);
|
|
@@ -1358,7 +1361,11 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1358
1361
|
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
1359
1362
|
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
1360
1363
|
}
|
|
1361
|
-
return {
|
|
1364
|
+
return {
|
|
1365
|
+
branchName: input.branchName,
|
|
1366
|
+
...movedAsidePath ? { movedAsidePath } : {},
|
|
1367
|
+
...stagedForCollectionPath ? { stagedForCollectionPath } : {}
|
|
1368
|
+
};
|
|
1362
1369
|
}
|
|
1363
1370
|
function removeProjectWorktrees(input) {
|
|
1364
1371
|
const projectRoot = import_node_path.default.resolve(input.projectRoot);
|
|
@@ -98,6 +98,19 @@ class WorkerRecoveryStore {
|
|
|
98
98
|
isUnknown(requestId) {
|
|
99
99
|
return this.row(requestId)?.state === "unknown";
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Re-admit an operation whose crash outcome is unknown so it can run again
|
|
103
|
+
* and record a result. Only a branch deletion qualifies: it is idempotent
|
|
104
|
+
* for its exact incarnation (durable tombstone plus incarnation preflight),
|
|
105
|
+
* so a rerun converges on the same outcome. Every other unknown operation
|
|
106
|
+
* keeps the no-replay invariant.
|
|
107
|
+
*/
|
|
108
|
+
readmitUnknownBranchDeletion(requestId) {
|
|
109
|
+
const row = this.row(requestId);
|
|
110
|
+
if (!row || row.state !== "unknown" || row.request_type !== "delete_project_branch") return false;
|
|
111
|
+
this.db.run("UPDATE operations SET state='accepted', response=NULL WHERE request_id=?", [row.request_id]);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
101
114
|
unknown(requestId) {
|
|
102
115
|
const row = this.row(requestId);
|
|
103
116
|
if (row) this.db.run("UPDATE operations SET state='unknown' WHERE request_id=?", [row.request_id]);
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { Database } from "bun:sqlite";
|
|
|
25
25
|
import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
|
|
26
26
|
import { gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
27
27
|
import { terminateProcessTree } from "./process-tree.mjs";
|
|
28
|
+
import { ProjectCheckoutGarbageCollector } from "./project-checkout-garbage.mjs";
|
|
28
29
|
import { createPtyOutputCoalescer } from "./pty-output-coalescer.mjs";
|
|
29
30
|
import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
|
|
30
31
|
import {
|
|
@@ -293,6 +294,15 @@ let workerAdmissionGeneration = 0;
|
|
|
293
294
|
const workspaceMutationGate = new WorkspaceMutationGate();
|
|
294
295
|
let workspaceSyncQueue = Promise.resolve();
|
|
295
296
|
let startupProjectSnapshotRecoveryCompleted = false;
|
|
297
|
+
const checkoutGarbageCollectors = /* @__PURE__ */ new Map();
|
|
298
|
+
function checkoutGarbageCollectorFor(projectsRoot) {
|
|
299
|
+
let collector = checkoutGarbageCollectors.get(projectsRoot);
|
|
300
|
+
if (!collector) {
|
|
301
|
+
collector = new ProjectCheckoutGarbageCollector({ projectsRoot });
|
|
302
|
+
checkoutGarbageCollectors.set(projectsRoot, collector);
|
|
303
|
+
}
|
|
304
|
+
return collector;
|
|
305
|
+
}
|
|
296
306
|
const workspaceSyncSingleFlight = {
|
|
297
307
|
runExclusive(operation) {
|
|
298
308
|
const queued = workspaceMutationGate.runSync(operation);
|
|
@@ -3723,6 +3733,12 @@ async function startWorker(options, projectRuntime = {
|
|
|
3723
3733
|
projectRuntime.initializedWorkspaceState = initializedWorkspaceState;
|
|
3724
3734
|
const projectWorkspaceStateStore = initializedWorkspaceState.store;
|
|
3725
3735
|
let projectWorkspaceState = initializedWorkspaceState.store.read();
|
|
3736
|
+
const checkoutGarbageCollector = checkoutGarbageCollectorFor(projectsRoot);
|
|
3737
|
+
const staleStagedCheckouts = checkoutGarbageCollector.sweep();
|
|
3738
|
+
if (staleStagedCheckouts > 0) {
|
|
3739
|
+
process.stderr.write(`[r5d-worker] collecting ${staleStagedCheckouts} deleted checkout(s) left from an earlier run
|
|
3740
|
+
`);
|
|
3741
|
+
}
|
|
3726
3742
|
const { projectConfigById, readyProjectIds, reconciledProjectConfigFingerprints } = projectRuntime;
|
|
3727
3743
|
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
3728
3744
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
@@ -3837,6 +3853,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
3837
3853
|
`
|
|
3838
3854
|
);
|
|
3839
3855
|
}
|
|
3856
|
+
if (deleted.stagedForCollectionPath) checkoutGarbageCollector.enqueue(deleted.stagedForCollectionPath);
|
|
3840
3857
|
}
|
|
3841
3858
|
const planSourcePath = path.join(planRoot, project.projectId, ...branchName.split("/"));
|
|
3842
3859
|
fs.rmSync(planSourcePath, { recursive: true, force: true });
|
|
@@ -3920,6 +3937,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
3920
3937
|
`
|
|
3921
3938
|
);
|
|
3922
3939
|
}
|
|
3940
|
+
if (deleted.stagedForCollectionPath) checkoutGarbageCollector.enqueue(deleted.stagedForCollectionPath);
|
|
3923
3941
|
fs.rmSync(path.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
|
|
3924
3942
|
readyProjectIds.delete(project.projectId);
|
|
3925
3943
|
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
@@ -5854,6 +5872,7 @@ async function startWorker(options, projectRuntime = {
|
|
|
5854
5872
|
workspaceIncidentConfigDeferralV1: true,
|
|
5855
5873
|
workspaceConfigResetToCanonicalV1: true,
|
|
5856
5874
|
projectBranchDeletionFastAckV1: true,
|
|
5875
|
+
projectBranchDeletionStagedRemovalV1: true,
|
|
5857
5876
|
projectMirrorLeaseV1: true,
|
|
5858
5877
|
projectMirrorRefsTokensV1: true,
|
|
5859
5878
|
projectBranchWorkingTreeModeV1: true
|
|
@@ -5955,7 +5974,9 @@ async function startWorker(options, projectRuntime = {
|
|
|
5955
5974
|
if (admission !== "new") {
|
|
5956
5975
|
const response = recoveryStore.response(operationRequestId);
|
|
5957
5976
|
if (response && admission !== "unknown") sendReplayWorkerMessage(ws, response);
|
|
5958
|
-
|
|
5977
|
+
if (!(admission === "unknown" && message.type === "delete_project_branch" && recoveryStore.readmitUnknownBranchDeletion(operationRequestId))) {
|
|
5978
|
+
return;
|
|
5979
|
+
}
|
|
5959
5980
|
}
|
|
5960
5981
|
}
|
|
5961
5982
|
const messageAdmissionGeneration = workerAdmissionGeneration;
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const PROJECT_DELETED_CHECKOUTS_DIRECTORY = ".r5d-deleted";
|
|
5
|
+
function isRealDirectory(candidate) {
|
|
6
|
+
try {
|
|
7
|
+
return fs.lstatSync(candidate).isDirectory();
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function stageProjectCheckoutForDeletion(projectRoot, branchName, checkoutPath) {
|
|
13
|
+
const garbageRoot = path.join(projectRoot, PROJECT_DELETED_CHECKOUTS_DIRECTORY);
|
|
14
|
+
if (!isRealDirectory(checkoutPath)) throw new Error(`Cannot stage ${checkoutPath} for deletion: not a directory`);
|
|
15
|
+
fs.mkdirSync(garbageRoot, { recursive: true });
|
|
16
|
+
if (!isRealDirectory(garbageRoot)) throw new Error(`Cannot stage ${checkoutPath} for deletion: ${garbageRoot} is not a directory`);
|
|
17
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
18
|
+
const target = path.join(garbageRoot, `${branchName.replace(/\//g, "__")}-${timestamp}-${randomUUID().slice(0, 8)}`);
|
|
19
|
+
fs.renameSync(checkoutPath, target);
|
|
20
|
+
return target;
|
|
21
|
+
}
|
|
22
|
+
function isStagedProjectCheckoutPath(projectsRoot, candidate) {
|
|
23
|
+
const relative = path.relative(path.resolve(projectsRoot), path.resolve(candidate));
|
|
24
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return false;
|
|
25
|
+
const segments = relative.split(path.sep);
|
|
26
|
+
return segments.length === 4 && segments[2] === PROJECT_DELETED_CHECKOUTS_DIRECTORY && segments[3] !== "" && !segments[3].startsWith(".");
|
|
27
|
+
}
|
|
28
|
+
function ownedStagedPath(projectsRoot, candidate) {
|
|
29
|
+
const root = path.resolve(projectsRoot);
|
|
30
|
+
const resolved = path.resolve(candidate);
|
|
31
|
+
if (!isStagedProjectCheckoutPath(root, resolved)) return null;
|
|
32
|
+
let current = root;
|
|
33
|
+
for (const segment of path.relative(root, path.dirname(resolved)).split(path.sep)) {
|
|
34
|
+
current = path.join(current, segment);
|
|
35
|
+
if (!isRealDirectory(current)) return null;
|
|
36
|
+
}
|
|
37
|
+
let stat;
|
|
38
|
+
try {
|
|
39
|
+
stat = fs.lstatSync(resolved);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (stat.isSymbolicLink()) return { kind: "link" };
|
|
44
|
+
return stat.isDirectory() ? { kind: "directory" } : null;
|
|
45
|
+
}
|
|
46
|
+
function realDirectoryEntries(directory) {
|
|
47
|
+
if (!isRealDirectory(directory)) return [];
|
|
48
|
+
try {
|
|
49
|
+
return fs.readdirSync(directory, { withFileTypes: true }).map((entry) => entry.name).sort();
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function discoverStagedProjectCheckouts(projectsRoot) {
|
|
55
|
+
const root = path.resolve(projectsRoot);
|
|
56
|
+
const staged = [];
|
|
57
|
+
for (const namespace of realDirectoryEntries(root)) {
|
|
58
|
+
if (namespace.startsWith(".")) continue;
|
|
59
|
+
for (const project of realDirectoryEntries(path.join(root, namespace))) {
|
|
60
|
+
if (project.startsWith(".")) continue;
|
|
61
|
+
const garbageRoot = path.join(root, namespace, project, PROJECT_DELETED_CHECKOUTS_DIRECTORY);
|
|
62
|
+
for (const entry of realDirectoryEntries(garbageRoot)) {
|
|
63
|
+
const candidate = path.join(garbageRoot, entry);
|
|
64
|
+
if (ownedStagedPath(root, candidate)) staged.push(candidate);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return staged;
|
|
69
|
+
}
|
|
70
|
+
async function removeStagedProjectCheckout(stagedPath) {
|
|
71
|
+
if (fs.lstatSync(stagedPath).isSymbolicLink()) {
|
|
72
|
+
fs.unlinkSync(stagedPath);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (process.platform === "win32") {
|
|
76
|
+
await fs.promises.rm(stagedPath, { recursive: true, force: true });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const command = ["rm", "-rf", "--", stagedPath];
|
|
80
|
+
if (Bun.which("nice")) command.unshift("nice", "-n", "19");
|
|
81
|
+
if (process.platform === "linux" && Bun.which("ionice")) command.unshift("ionice", "-c", "3");
|
|
82
|
+
const child = Bun.spawn(command, { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
|
|
83
|
+
const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
|
|
84
|
+
if (exitCode !== 0) throw new Error(`rm exited ${exitCode}: ${stderr.trim()}`);
|
|
85
|
+
if (fs.existsSync(stagedPath)) throw new Error("staged checkout still exists after removal");
|
|
86
|
+
}
|
|
87
|
+
class ProjectCheckoutGarbageCollector {
|
|
88
|
+
projectsRoot;
|
|
89
|
+
remove;
|
|
90
|
+
log;
|
|
91
|
+
queued = /* @__PURE__ */ new Set();
|
|
92
|
+
tail = Promise.resolve();
|
|
93
|
+
collectedCount = 0;
|
|
94
|
+
constructor(options) {
|
|
95
|
+
this.projectsRoot = path.resolve(options.projectsRoot);
|
|
96
|
+
this.remove = options.remove ?? removeStagedProjectCheckout;
|
|
97
|
+
this.log = options.log ?? ((message) => process.stderr.write(`${message}
|
|
98
|
+
`));
|
|
99
|
+
}
|
|
100
|
+
/** Number of removals that have completed successfully. */
|
|
101
|
+
get collected() {
|
|
102
|
+
return this.collectedCount;
|
|
103
|
+
}
|
|
104
|
+
/** Resolves once everything queued so far has been attempted. */
|
|
105
|
+
get idle() {
|
|
106
|
+
return this.tail;
|
|
107
|
+
}
|
|
108
|
+
enqueue(stagedPath) {
|
|
109
|
+
const resolved = path.resolve(stagedPath);
|
|
110
|
+
if (!ownedStagedPath(this.projectsRoot, resolved)) {
|
|
111
|
+
throw new Error(`Refusing to collect ${stagedPath}: not a staged checkout under ${this.projectsRoot}`);
|
|
112
|
+
}
|
|
113
|
+
if (this.queued.has(resolved)) return;
|
|
114
|
+
this.queued.add(resolved);
|
|
115
|
+
this.tail = this.tail.then(async () => {
|
|
116
|
+
try {
|
|
117
|
+
const owned = ownedStagedPath(this.projectsRoot, resolved);
|
|
118
|
+
if (owned) {
|
|
119
|
+
const startedAt = performance.now();
|
|
120
|
+
await this.remove(resolved);
|
|
121
|
+
this.collectedCount += 1;
|
|
122
|
+
this.log(`[r5d-worker] collected deleted checkout ${resolved} in ${Math.round(performance.now() - startedAt)}ms`);
|
|
123
|
+
} else if (fs.existsSync(resolved) || isRealDirectory(path.dirname(resolved))) {
|
|
124
|
+
this.log(`[r5d-worker] skipped deleted checkout ${resolved}: no longer an owned staged checkout`);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.log(
|
|
128
|
+
`[r5d-worker] deleted checkout ${resolved} could not be collected yet: ${error instanceof Error ? error.message : String(error)}`
|
|
129
|
+
);
|
|
130
|
+
} finally {
|
|
131
|
+
this.queued.delete(resolved);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Enqueue every staged checkout left behind by an earlier process. */
|
|
136
|
+
sweep() {
|
|
137
|
+
const staged = discoverStagedProjectCheckouts(this.projectsRoot);
|
|
138
|
+
for (const stagedPath of staged) this.enqueue(stagedPath);
|
|
139
|
+
return staged.length;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
PROJECT_DELETED_CHECKOUTS_DIRECTORY,
|
|
144
|
+
ProjectCheckoutGarbageCollector,
|
|
145
|
+
discoverStagedProjectCheckouts,
|
|
146
|
+
isStagedProjectCheckoutPath,
|
|
147
|
+
ownedStagedPath,
|
|
148
|
+
removeStagedProjectCheckout,
|
|
149
|
+
stageProjectCheckoutForDeletion
|
|
150
|
+
};
|
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
6
6
|
import { validateManagedBranchName } from "./managed-paths.mjs";
|
|
7
|
+
import { stageProjectCheckoutForDeletion } from "./project-checkout-garbage.mjs";
|
|
7
8
|
import {
|
|
8
9
|
canonicalizeLocalProjectMirrorRefs,
|
|
9
10
|
parseProjectMirrorRefListing,
|
|
@@ -1290,6 +1291,7 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1290
1291
|
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
1291
1292
|
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1292
1293
|
let movedAsidePath;
|
|
1294
|
+
let stagedForCollectionPath;
|
|
1293
1295
|
if (fs.existsSync(checkoutPath)) {
|
|
1294
1296
|
const checkoutCommonDir = fs.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
|
|
1295
1297
|
if (checkoutCommonDir !== primaryCommonDir) {
|
|
@@ -1299,7 +1301,8 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1299
1301
|
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1300
1302
|
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
1301
1303
|
}
|
|
1302
|
-
|
|
1304
|
+
stagedForCollectionPath = stageProjectCheckoutForDeletion(input.projectRoot, input.branchName, checkoutPath);
|
|
1305
|
+
git(primaryPath, ["worktree", "prune"], `prune linked worktree ${input.branchName}`);
|
|
1303
1306
|
}
|
|
1304
1307
|
} else {
|
|
1305
1308
|
tryGit(primaryPath, ["worktree", "prune"]);
|
|
@@ -1308,7 +1311,11 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1308
1311
|
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
1309
1312
|
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
1310
1313
|
}
|
|
1311
|
-
return {
|
|
1314
|
+
return {
|
|
1315
|
+
branchName: input.branchName,
|
|
1316
|
+
...movedAsidePath ? { movedAsidePath } : {},
|
|
1317
|
+
...stagedForCollectionPath ? { stagedForCollectionPath } : {}
|
|
1318
|
+
};
|
|
1312
1319
|
}
|
|
1313
1320
|
function removeProjectWorktrees(input) {
|
|
1314
1321
|
const projectRoot = path.resolve(input.projectRoot);
|
|
@@ -64,6 +64,19 @@ class WorkerRecoveryStore {
|
|
|
64
64
|
isUnknown(requestId) {
|
|
65
65
|
return this.row(requestId)?.state === "unknown";
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Re-admit an operation whose crash outcome is unknown so it can run again
|
|
69
|
+
* and record a result. Only a branch deletion qualifies: it is idempotent
|
|
70
|
+
* for its exact incarnation (durable tombstone plus incarnation preflight),
|
|
71
|
+
* so a rerun converges on the same outcome. Every other unknown operation
|
|
72
|
+
* keeps the no-replay invariant.
|
|
73
|
+
*/
|
|
74
|
+
readmitUnknownBranchDeletion(requestId) {
|
|
75
|
+
const row = this.row(requestId);
|
|
76
|
+
if (!row || row.state !== "unknown" || row.request_type !== "delete_project_branch") return false;
|
|
77
|
+
this.db.run("UPDATE operations SET state='accepted', response=NULL WHERE request_id=?", [row.request_id]);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
67
80
|
unknown(requestId) {
|
|
68
81
|
const row = this.row(requestId);
|
|
69
82
|
if (row) this.db.run("UPDATE operations SET state='unknown' WHERE request_id=?", [row.request_id]);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a deleted branch checkout waits for collection. Staging is the only
|
|
3
|
+
* platform write into this directory and a live checkout always lives at
|
|
4
|
+
* `<projectRoot>/<branch>`, so a crash between staging and collection leaves
|
|
5
|
+
* nothing that a later sweep could confuse with a replacement branch. The
|
|
6
|
+
* filesystem is user-managed, though, so the collector never trusts the name
|
|
7
|
+
* alone: it removes only real directories or leaf links that sit under a real
|
|
8
|
+
* garbage directory of a real project directory (see `ownedStagedPath`).
|
|
9
|
+
* Distinct from `.r5d-removed`, which preserves paths that were not this
|
|
10
|
+
* project's worktree.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PROJECT_DELETED_CHECKOUTS_DIRECTORY = ".r5d-deleted";
|
|
13
|
+
/**
|
|
14
|
+
* Rename a checkout this project owns out of the active namespace. One
|
|
15
|
+
* same-filesystem rename takes milliseconds whatever the tree holds, so the
|
|
16
|
+
* branch mutation lease is released long before the bytes are gone. The
|
|
17
|
+
* garbage directory must be a real directory: a planted link would make the
|
|
18
|
+
* later collection walk through it, so staging refuses rather than continue.
|
|
19
|
+
*/
|
|
20
|
+
export declare function stageProjectCheckoutForDeletion(projectRoot: string, branchName: string, checkoutPath: string): string;
|
|
21
|
+
/** True when `candidate` is lexically a direct entry of some project's garbage directory under `projectsRoot`. */
|
|
22
|
+
export declare function isStagedProjectCheckoutPath(projectsRoot: string, candidate: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Whether a lexically staged path is safe to remove right now: every ancestor
|
|
25
|
+
* below the projects root is a real directory (a linked namespace, project,
|
|
26
|
+
* or garbage directory would let a removal reach an unrelated tree through
|
|
27
|
+
* the link), and the entry itself is a real directory or a leaf link. Checked
|
|
28
|
+
* again immediately before the removal starts, never only at enqueue time.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ownedStagedPath(projectsRoot: string, candidate: string): {
|
|
31
|
+
kind: "directory" | "link";
|
|
32
|
+
} | null;
|
|
33
|
+
/** Every owned staged checkout under `<projectsRoot>/<namespace>/<project>/.r5d-deleted/`; linked ancestors are not followed. */
|
|
34
|
+
export declare function discoverStagedProjectCheckouts(projectsRoot: string): string[];
|
|
35
|
+
/**
|
|
36
|
+
* Delete a staged tree in a child process so a large recursive removal never
|
|
37
|
+
* runs on the worker event loop, at the lowest CPU and (where supported) I/O
|
|
38
|
+
* priority so live agents on the same volume keep their share. A leaf link is
|
|
39
|
+
* unlinked in place; its target is never followed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function removeStagedProjectCheckout(stagedPath: string): Promise<void>;
|
|
42
|
+
type Logger = (message: string) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Collects staged checkouts one at a time, outside every workspace lease.
|
|
45
|
+
* Entries are enqueued right after staging and by `sweep()` at worker start,
|
|
46
|
+
* so a crash at any point converges on the next start. A failed removal is
|
|
47
|
+
* logged and left for the next sweep.
|
|
48
|
+
*/
|
|
49
|
+
export declare class ProjectCheckoutGarbageCollector {
|
|
50
|
+
private readonly projectsRoot;
|
|
51
|
+
private readonly remove;
|
|
52
|
+
private readonly log;
|
|
53
|
+
private readonly queued;
|
|
54
|
+
private tail;
|
|
55
|
+
private collectedCount;
|
|
56
|
+
constructor(options: {
|
|
57
|
+
projectsRoot: string;
|
|
58
|
+
remove?: (stagedPath: string) => Promise<void>;
|
|
59
|
+
log?: Logger;
|
|
60
|
+
});
|
|
61
|
+
/** Number of removals that have completed successfully. */
|
|
62
|
+
get collected(): number;
|
|
63
|
+
/** Resolves once everything queued so far has been attempted. */
|
|
64
|
+
get idle(): Promise<void>;
|
|
65
|
+
enqueue(stagedPath: string): void;
|
|
66
|
+
/** Enqueue every staged checkout left behind by an earlier process. */
|
|
67
|
+
sweep(): number;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -209,6 +209,12 @@ export declare function deleteLinkedProjectBranch(input: {
|
|
|
209
209
|
branchName: string;
|
|
210
210
|
/** Where a path that was not this project's linked worktree was set aside instead of being deleted. */
|
|
211
211
|
movedAsidePath?: string;
|
|
212
|
+
/**
|
|
213
|
+
* Where the linked worktree's tree now waits for garbage collection. The
|
|
214
|
+
* branch is already gone from Git; the caller hands this path to the
|
|
215
|
+
* collector so the recursive removal runs outside every workspace lease.
|
|
216
|
+
*/
|
|
217
|
+
stagedForCollectionPath?: string;
|
|
212
218
|
};
|
|
213
219
|
export declare function removeProjectWorktrees(input: {
|
|
214
220
|
projectRoot: string;
|
|
@@ -16,6 +16,14 @@ export declare class WorkerRecoveryStore {
|
|
|
16
16
|
requestId: string;
|
|
17
17
|
}): "new" | "duplicate" | "unknown" | "conflict";
|
|
18
18
|
isUnknown(requestId: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Re-admit an operation whose crash outcome is unknown so it can run again
|
|
21
|
+
* and record a result. Only a branch deletion qualifies: it is idempotent
|
|
22
|
+
* for its exact incarnation (durable tombstone plus incarnation preflight),
|
|
23
|
+
* so a rerun converges on the same outcome. Every other unknown operation
|
|
24
|
+
* keeps the no-replay invariant.
|
|
25
|
+
*/
|
|
26
|
+
readmitUnknownBranchDeletion(requestId: string): boolean;
|
|
19
27
|
unknown(requestId: string): void;
|
|
20
28
|
unknownRun(runId: string): void;
|
|
21
29
|
record(message: Message): Message;
|