looply-cli 0.5.6 → 0.5.8
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/.src-hash +1 -1
- package/dist/daemon-cmd.d.ts.map +1 -1
- package/dist/daemon-cmd.js +37 -23
- package/dist/daemon-cmd.js.map +1 -1
- package/dist/git-driver.d.ts +10 -0
- package/dist/git-driver.d.ts.map +1 -1
- package/dist/git-driver.js +22 -0
- package/dist/git-driver.js.map +1 -1
- package/dist/workspace-provisioner.d.ts +47 -3
- package/dist/workspace-provisioner.d.ts.map +1 -1
- package/dist/workspace-provisioner.js +73 -2
- package/dist/workspace-provisioner.js.map +1 -1
- package/dist/workspace.d.ts +50 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +205 -10
- package/dist/workspace.js.map +1 -1
- package/package.json +2 -2
- package/win/contained-node.cjs +27 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProvisionBackoff = void 0;
|
|
3
4
|
exports.nodeWorkspaceProvisioner = nodeWorkspaceProvisioner;
|
|
4
5
|
exports.provisionOnce = provisionOnce;
|
|
5
6
|
const node_os_1 = require("node:os");
|
|
@@ -67,25 +68,95 @@ function declaredCarry(cwd) {
|
|
|
67
68
|
return [];
|
|
68
69
|
return (0, config_1.parseConfig)((0, node_fs_1.readFileSync)(file, 'utf8')).run.carryIntoCheckouts ?? [];
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* 🔴 A PROVISIONING THAT FAILS IS NOT TRIED AGAIN NEXT CADENCE — IT WAS, AND IT
|
|
73
|
+
* FILLED A DISK.
|
|
74
|
+
*
|
|
75
|
+
* "A failure is a line, tried again next cadence" was the whole policy, and a
|
|
76
|
+
* failure that is deterministic — the same carry refused the same way every
|
|
77
|
+
* time — then ran once a minute for as long as the daemon lived. MEASURED,
|
|
78
|
+
* 2026-10-16: Tabsy's daemon asked for one checkout for one goal every ~60 s,
|
|
79
|
+
* each attempt died in the carry (EPERM), and each left a ~0.7 GB worktree
|
|
80
|
+
* behind: 156 copies, 105 GB, C: at 0 bytes. `createWorkspace` now removes what
|
|
81
|
+
* a failed attempt made; this makes sure there are a handful of attempts a
|
|
82
|
+
* night rather than six hundred.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ ONE WAIT FOR THE WHOLE PROVISIONER, NOT ONE PER REQUEST. A provisioning
|
|
85
|
+
* that fails for one request fails for the next — it is the same carry from the
|
|
86
|
+
* same project — so a second request must not buy a fresh attempt. Doubling
|
|
87
|
+
* from `baseMs`, held at `capMs`; a success clears it. Built ONCE per daemon: a
|
|
88
|
+
* backoff made per cadence remembers nothing.
|
|
89
|
+
*/
|
|
90
|
+
class ProvisionBackoff {
|
|
91
|
+
baseMs;
|
|
92
|
+
capMs;
|
|
93
|
+
failures = 0;
|
|
94
|
+
until = null;
|
|
95
|
+
constructor(baseMs = 60_000, capMs = 3_600_000) {
|
|
96
|
+
this.baseMs = baseMs;
|
|
97
|
+
this.capMs = capMs;
|
|
98
|
+
}
|
|
99
|
+
/** When the next attempt may be made, or null when it may be made now. */
|
|
100
|
+
waiting(now) {
|
|
101
|
+
return this.until !== null && now < this.until ? this.until : null;
|
|
102
|
+
}
|
|
103
|
+
/** Record a failure at `now`; answers when the next attempt may be made. */
|
|
104
|
+
failed(now) {
|
|
105
|
+
this.failures += 1;
|
|
106
|
+
const wait = Math.min(this.capMs, this.baseMs * 2 ** Math.min(this.failures - 1, 30));
|
|
107
|
+
this.until = now + wait;
|
|
108
|
+
return this.until;
|
|
109
|
+
}
|
|
110
|
+
succeeded() {
|
|
111
|
+
this.failures = 0;
|
|
112
|
+
this.until = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.ProvisionBackoff = ProvisionBackoff;
|
|
70
116
|
/**
|
|
71
117
|
* Answer the tick's request, if there was one and provisioning is on. The log
|
|
72
118
|
* line names the checkout and the daemon's reason; a failure names itself.
|
|
73
119
|
*/
|
|
74
|
-
async function provisionOnce(request, provisioner, log) {
|
|
120
|
+
async function provisionOnce(request, provisioner, log, guard = {}) {
|
|
75
121
|
if (request === null)
|
|
76
122
|
return { outcome: 'none', path: null };
|
|
77
123
|
if (provisioner === null) {
|
|
78
124
|
log(`daemon : the scheduler asked for a checkout ${request.because}, but provisioning is off — not created`);
|
|
79
125
|
return { outcome: 'none', path: null };
|
|
80
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* ⚠️ THE HOLD FIRST, AND A HOLD THAT CANNOT BE READ IS A HOLD — the caller's
|
|
129
|
+
* reader already answers so for an unreadable control file, and a reader that
|
|
130
|
+
* throws is treated the same way rather than as permission.
|
|
131
|
+
*/
|
|
132
|
+
if (guard.held !== undefined) {
|
|
133
|
+
let held;
|
|
134
|
+
try {
|
|
135
|
+
held = await guard.held();
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
held = `the hold could not be read: ${e.message}`;
|
|
139
|
+
}
|
|
140
|
+
if (held !== null) {
|
|
141
|
+
log(`daemon : the scheduler asked for a checkout ${request.because}, but this machine is held (${held}) — not created`);
|
|
142
|
+
return { outcome: 'held', path: null };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const now = guard.now ?? (() => Date.now());
|
|
146
|
+
// Silent inside the wait: the failure's own line already said when the next attempt is.
|
|
147
|
+
if (guard.backoff?.waiting(now()) != null)
|
|
148
|
+
return { outcome: 'deferred', path: null };
|
|
81
149
|
try {
|
|
82
150
|
const path = await provisioner();
|
|
151
|
+
guard.backoff?.succeeded();
|
|
83
152
|
log(`daemon : provisioned workspace ${path} ${request.because}`);
|
|
84
153
|
// The path comes back so the caller can keep the reaper off it this cadence (M4-4).
|
|
85
154
|
return { outcome: 'provisioned', path };
|
|
86
155
|
}
|
|
87
156
|
catch (e) {
|
|
88
|
-
|
|
157
|
+
const next = guard.backoff?.failed(now());
|
|
158
|
+
const after = next === undefined ? '' : ` — not tried again until ${new Date(next).toISOString()}`;
|
|
159
|
+
log(`daemon : could not provision a workspace ${request.because} — ${e.message}${after}`);
|
|
89
160
|
return { outcome: 'failed', path: null };
|
|
90
161
|
}
|
|
91
162
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-provisioner.js","sourceRoot":"","sources":["../src/workspace-provisioner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"workspace-provisioner.js","sourceRoot":"","sources":["../src/workspace-provisioner.ts"],"names":[],"mappings":"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"}
|
package/dist/workspace.d.ts
CHANGED
|
@@ -62,6 +62,56 @@ export declare function forgetCreationMarker(cwd: string, name: string): boolean
|
|
|
62
62
|
* the same identity rule `containerName` and `providerSessionDir` already use.
|
|
63
63
|
*/
|
|
64
64
|
export declare function workspacesRoot(cwd: string, home?: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Give a new checkout the project's `.looply/config.yaml` when git did not.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 A PROJECT THAT KEEPS `.looply/` OUT OF GIT GOT CHECKOUTS WITH NO CONFIG.
|
|
69
|
+
* Tabsy excludes `/.looply/` (`.git/info/exclude`), so `git worktree add` made a
|
|
70
|
+
* checkout without the file, and every Start there died at once — "config:
|
|
71
|
+
* cannot read …\.looply\config.yaml", three attempts, then human_required
|
|
72
|
+
* (2026-10-16, the first time parallel checkouts could be made at all).
|
|
73
|
+
*
|
|
74
|
+
* ⚠️ NEVER OVER A TRACKED ONE. If the checkout has the file, git put it there
|
|
75
|
+
* and it is what that commit says; this only fills the gap. A link is refused,
|
|
76
|
+
* for the recursive-delete reason in `checkout-carry.ts`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function carryProjectConfig(projectRoot: string, checkoutDir: string): boolean;
|
|
79
|
+
/** The machine-level settings file: `<home>/.looply/machine-settings.json`. */
|
|
80
|
+
export declare const MACHINE_SETTINGS_FILE = "machine-settings.json";
|
|
81
|
+
/** No new checkout is made when its drive has less free space than this (GB). */
|
|
82
|
+
export declare const DEFAULT_MIN_FREE_GB = 20;
|
|
83
|
+
/**
|
|
84
|
+
* `minFreeGb` from the machine settings, else {@link DEFAULT_MIN_FREE_GB}. Zero
|
|
85
|
+
* turns the check off; anything that is not a non-negative number throws, for
|
|
86
|
+
* the same reason as `workspacesBase`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function minFreeGb(home?: string): number;
|
|
89
|
+
/**
|
|
90
|
+
* Why no checkout may be made under `root` right now, or `null` when one may.
|
|
91
|
+
*
|
|
92
|
+
* 🔴 A CHECKOUT IS ~0.8 GB AND THE DRIVE WAS NOT ASKED. On 2026-10-16 checkouts
|
|
93
|
+
* kept being made until C: had 0 bytes, which also took down every other process
|
|
94
|
+
* on the machine. The answer is a sentence a person can act on, because it is
|
|
95
|
+
* what the run and the ticket say.
|
|
96
|
+
*/
|
|
97
|
+
export declare function roomForCheckout(root: string, home?: string, freeBytes?: (dir: string) => number): string | null;
|
|
98
|
+
/**
|
|
99
|
+
* The directory every project's workspaces root sits under.
|
|
100
|
+
*
|
|
101
|
+
* 🔴 A CHECKOUT IS THE SIZE OF THE PROJECT PLUS ITS `node_modules` (Tabsy:
|
|
102
|
+
* ~0.8 GB each), and it always landed on the home drive. On 2026-10-16 that
|
|
103
|
+
* filled the owner's C: (about 100 GB of checkouts). `workspacesRoot` in
|
|
104
|
+
* `<home>/.looply/machine-settings.json` moves them to another drive, e.g.
|
|
105
|
+
* `{ "workspacesRoot": "D:\\looply-home\\workspaces" }`.
|
|
106
|
+
*
|
|
107
|
+
* ⚠️ READ FROM THE HOME, NEVER FROM THE REPOSITORY. `.looply/config.yaml` lives
|
|
108
|
+
* inside the worktree the model may edit, and this path is created, copied into
|
|
109
|
+
* and deleted recursively.
|
|
110
|
+
*
|
|
111
|
+
* ⚠️ AN UNUSABLE SETTING THROWS. Falling back to the home drive silently is the
|
|
112
|
+
* exact outcome the setting exists to prevent.
|
|
113
|
+
*/
|
|
114
|
+
export declare function workspacesBase(home?: string): string;
|
|
65
115
|
/**
|
|
66
116
|
* One live execution, as the project can see it.
|
|
67
117
|
*
|
package/dist/workspace.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
|
package/dist/workspace.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RunRegistry = exports.WorkspaceError = exports.nodeProbe = exports.WORKSPACES_DIR = void 0;
|
|
3
|
+
exports.RunRegistry = exports.WorkspaceError = exports.nodeProbe = exports.DEFAULT_MIN_FREE_GB = exports.MACHINE_SETTINGS_FILE = exports.WORKSPACES_DIR = void 0;
|
|
4
4
|
exports.forgetCreationMarker = forgetCreationMarker;
|
|
5
5
|
exports.workspacesRoot = workspacesRoot;
|
|
6
|
+
exports.carryProjectConfig = carryProjectConfig;
|
|
7
|
+
exports.minFreeGb = minFreeGb;
|
|
8
|
+
exports.roomForCheckout = roomForCheckout;
|
|
9
|
+
exports.workspacesBase = workspacesBase;
|
|
6
10
|
exports.requestControlEverywhere = requestControlEverywhere;
|
|
7
11
|
exports.createWorkspace = createWorkspace;
|
|
8
12
|
exports.reapableCheckout = reapableCheckout;
|
|
@@ -128,7 +132,138 @@ function workspacesRoot(cwd, home = (0, node_os_1.homedir)()) {
|
|
|
128
132
|
*/
|
|
129
133
|
const root = (0, trusted_git_1.repoIdentityPath)(cwd);
|
|
130
134
|
const key = (0, node_crypto_1.createHash)('sha256').update(root.toLowerCase(), 'utf8').digest('hex').slice(0, 16);
|
|
131
|
-
return (0, node_path_1.join)(home,
|
|
135
|
+
return (0, node_path_1.join)(workspacesBase(home), key);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Give a new checkout the project's `.looply/config.yaml` when git did not.
|
|
139
|
+
*
|
|
140
|
+
* 🔴 A PROJECT THAT KEEPS `.looply/` OUT OF GIT GOT CHECKOUTS WITH NO CONFIG.
|
|
141
|
+
* Tabsy excludes `/.looply/` (`.git/info/exclude`), so `git worktree add` made a
|
|
142
|
+
* checkout without the file, and every Start there died at once — "config:
|
|
143
|
+
* cannot read …\.looply\config.yaml", three attempts, then human_required
|
|
144
|
+
* (2026-10-16, the first time parallel checkouts could be made at all).
|
|
145
|
+
*
|
|
146
|
+
* ⚠️ NEVER OVER A TRACKED ONE. If the checkout has the file, git put it there
|
|
147
|
+
* and it is what that commit says; this only fills the gap. A link is refused,
|
|
148
|
+
* for the recursive-delete reason in `checkout-carry.ts`.
|
|
149
|
+
*/
|
|
150
|
+
function carryProjectConfig(projectRoot, checkoutDir) {
|
|
151
|
+
const to = (0, node_path_1.join)(checkoutDir, git_driver_1.STATE_DIR, 'config.yaml');
|
|
152
|
+
if ((0, node_fs_1.existsSync)(to))
|
|
153
|
+
return false;
|
|
154
|
+
const from = (0, node_path_1.join)(projectRoot, git_driver_1.STATE_DIR, 'config.yaml');
|
|
155
|
+
if (!(0, node_fs_1.existsSync)(from))
|
|
156
|
+
return false;
|
|
157
|
+
if ((0, node_fs_1.lstatSync)(from).isSymbolicLink()) {
|
|
158
|
+
throw new Error(`${from} is a link; it is not copied into a checkout`);
|
|
159
|
+
}
|
|
160
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(to), { recursive: true });
|
|
161
|
+
(0, node_fs_1.cpSync)(from, to);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
/** The machine-level settings file: `<home>/.looply/machine-settings.json`. */
|
|
165
|
+
exports.MACHINE_SETTINGS_FILE = 'machine-settings.json';
|
|
166
|
+
/** No new checkout is made when its drive has less free space than this (GB). */
|
|
167
|
+
exports.DEFAULT_MIN_FREE_GB = 20;
|
|
168
|
+
/**
|
|
169
|
+
* `minFreeGb` from the machine settings, else {@link DEFAULT_MIN_FREE_GB}. Zero
|
|
170
|
+
* turns the check off; anything that is not a non-negative number throws, for
|
|
171
|
+
* the same reason as `workspacesBase`.
|
|
172
|
+
*/
|
|
173
|
+
function minFreeGb(home = (0, node_os_1.homedir)()) {
|
|
174
|
+
const file = (0, node_path_1.join)(home, '.looply', exports.MACHINE_SETTINGS_FILE);
|
|
175
|
+
let raw;
|
|
176
|
+
try {
|
|
177
|
+
raw = (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
if (e.code === 'ENOENT')
|
|
181
|
+
return exports.DEFAULT_MIN_FREE_GB;
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
let value;
|
|
185
|
+
try {
|
|
186
|
+
value = JSON.parse(raw)?.minFreeGb;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
throw new Error(`${file} is not valid JSON`);
|
|
190
|
+
}
|
|
191
|
+
if (value === undefined)
|
|
192
|
+
return exports.DEFAULT_MIN_FREE_GB;
|
|
193
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
194
|
+
throw new Error(`${file}: "minFreeGb" must be a number of GB, 0 or more`);
|
|
195
|
+
}
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Why no checkout may be made under `root` right now, or `null` when one may.
|
|
200
|
+
*
|
|
201
|
+
* 🔴 A CHECKOUT IS ~0.8 GB AND THE DRIVE WAS NOT ASKED. On 2026-10-16 checkouts
|
|
202
|
+
* kept being made until C: had 0 bytes, which also took down every other process
|
|
203
|
+
* on the machine. The answer is a sentence a person can act on, because it is
|
|
204
|
+
* what the run and the ticket say.
|
|
205
|
+
*/
|
|
206
|
+
function roomForCheckout(root, home = (0, node_os_1.homedir)(), freeBytes = (dir) => {
|
|
207
|
+
const s = (0, node_fs_1.statfsSync)(dir);
|
|
208
|
+
return Number(s.bavail) * Number(s.bsize);
|
|
209
|
+
}) {
|
|
210
|
+
const min = minFreeGb(home);
|
|
211
|
+
if (min === 0)
|
|
212
|
+
return null;
|
|
213
|
+
const free = freeBytes(root) / 1e9;
|
|
214
|
+
if (free >= min)
|
|
215
|
+
return null;
|
|
216
|
+
return (`not enough disk space for a new checkout: ${free.toFixed(1)} GB free where checkouts go (${root}), ` +
|
|
217
|
+
`and at least ${min} GB must stay free. Free some space, or move checkouts to another drive ` +
|
|
218
|
+
`("workspacesRoot" in ~/.looply/${exports.MACHINE_SETTINGS_FILE}).`);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The directory every project's workspaces root sits under.
|
|
222
|
+
*
|
|
223
|
+
* 🔴 A CHECKOUT IS THE SIZE OF THE PROJECT PLUS ITS `node_modules` (Tabsy:
|
|
224
|
+
* ~0.8 GB each), and it always landed on the home drive. On 2026-10-16 that
|
|
225
|
+
* filled the owner's C: (about 100 GB of checkouts). `workspacesRoot` in
|
|
226
|
+
* `<home>/.looply/machine-settings.json` moves them to another drive, e.g.
|
|
227
|
+
* `{ "workspacesRoot": "D:\\looply-home\\workspaces" }`.
|
|
228
|
+
*
|
|
229
|
+
* ⚠️ READ FROM THE HOME, NEVER FROM THE REPOSITORY. `.looply/config.yaml` lives
|
|
230
|
+
* inside the worktree the model may edit, and this path is created, copied into
|
|
231
|
+
* and deleted recursively.
|
|
232
|
+
*
|
|
233
|
+
* ⚠️ AN UNUSABLE SETTING THROWS. Falling back to the home drive silently is the
|
|
234
|
+
* exact outcome the setting exists to prevent.
|
|
235
|
+
*/
|
|
236
|
+
function workspacesBase(home = (0, node_os_1.homedir)()) {
|
|
237
|
+
// `LOOPLY_WORKSPACES_ROOT` wins over the file, for a machine whose home is not writable or is shared.
|
|
238
|
+
const fromEnv = process.env.LOOPLY_WORKSPACES_ROOT;
|
|
239
|
+
if (fromEnv !== undefined && fromEnv !== '') {
|
|
240
|
+
if (!(0, node_path_1.isAbsolute)(fromEnv))
|
|
241
|
+
throw new Error('LOOPLY_WORKSPACES_ROOT must be an absolute path');
|
|
242
|
+
return fromEnv;
|
|
243
|
+
}
|
|
244
|
+
const file = (0, node_path_1.join)(home, '.looply', exports.MACHINE_SETTINGS_FILE);
|
|
245
|
+
let raw;
|
|
246
|
+
try {
|
|
247
|
+
raw = (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
if (e.code === 'ENOENT')
|
|
251
|
+
return (0, node_path_1.join)(home, '.looply', exports.WORKSPACES_DIR);
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
let value;
|
|
255
|
+
try {
|
|
256
|
+
value = JSON.parse(raw)?.workspacesRoot;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
throw new Error(`${file} is not valid JSON, so where workspaces go is unknown`);
|
|
260
|
+
}
|
|
261
|
+
if (value === undefined)
|
|
262
|
+
return (0, node_path_1.join)(home, '.looply', exports.WORKSPACES_DIR);
|
|
263
|
+
if (typeof value !== 'string' || !(0, node_path_1.isAbsolute)(value)) {
|
|
264
|
+
throw new Error(`${file}: "workspacesRoot" must be an absolute path`);
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
132
267
|
}
|
|
133
268
|
exports.nodeProbe = run_lease_1.nodeProbe;
|
|
134
269
|
/**
|
|
@@ -1149,17 +1284,30 @@ carry = []) {
|
|
|
1149
1284
|
if ((0, node_fs_1.existsSync)(path)) {
|
|
1150
1285
|
throw new WorkspaceError(`a workspace already exists at ${path} — refusing to reuse it`);
|
|
1151
1286
|
}
|
|
1287
|
+
const room = roomForCheckout(root, home);
|
|
1288
|
+
if (room !== null)
|
|
1289
|
+
throw new WorkspaceError(room);
|
|
1152
1290
|
await git.addWorktree(path, branch, baseSha);
|
|
1153
1291
|
/**
|
|
1154
1292
|
* Immediately after the checkout and BEFORE the claim below, so a workspace is
|
|
1155
1293
|
* never offered to a run in a state where its gate is guaranteed to fail.
|
|
1156
1294
|
*/
|
|
1157
|
-
|
|
1295
|
+
{
|
|
1158
1296
|
try {
|
|
1159
|
-
(
|
|
1297
|
+
carryProjectConfig(cwd, path);
|
|
1298
|
+
if (carry.length > 0)
|
|
1299
|
+
(0, checkout_carry_1.carryIntoCheckout)(cwd, path, carry);
|
|
1160
1300
|
}
|
|
1161
1301
|
catch (e) {
|
|
1162
|
-
|
|
1302
|
+
/**
|
|
1303
|
+
* 🔴 AND WHAT FAILED IS REMOVED, NOT LEFT FOR A REAPER. This threw with
|
|
1304
|
+
* the checkout still on disk, registered with git, on its own branch — and
|
|
1305
|
+
* with no marker and no record, so nothing but `--reap` would ever retire
|
|
1306
|
+
* it. MEASURED, 2026-10-16: the daemon retried every cadence and kept every
|
|
1307
|
+
* failure, 156 checkouts of ~0.7 GB each until C: had nothing left.
|
|
1308
|
+
*/
|
|
1309
|
+
const kept = await discardFreshWorkspace(git, root, path, branch, baseSha);
|
|
1310
|
+
throw new WorkspaceError(`the workspace could not be provisioned: ${e.message}${kept}`);
|
|
1163
1311
|
}
|
|
1164
1312
|
}
|
|
1165
1313
|
/**
|
|
@@ -1198,16 +1346,63 @@ carry = []) {
|
|
|
1198
1346
|
* So it fails closed: the worktree this just created is removed and the
|
|
1199
1347
|
* caller is told. A workspace that cannot be protected is not a workspace.
|
|
1200
1348
|
*/
|
|
1349
|
+
const kept = await discardFreshWorkspace(git, root, path, branch, baseSha);
|
|
1350
|
+
throw new WorkspaceError(`the workspace at ${path} could not be claimed for this run (${e.message}), so it was not kept${kept}`);
|
|
1351
|
+
}
|
|
1352
|
+
return path;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Undo a `worktree add` this process made moments ago and nothing has used:
|
|
1356
|
+
* the directory, git's registration of it, and the branch cut for it. Answers
|
|
1357
|
+
* '' when all of it went, or a clause naming what is left.
|
|
1358
|
+
*
|
|
1359
|
+
* ⚠️ WHY A DIRECTORY DELETE IS ALLOWED HERE WHEN `removeWorktree` REFUSES TO
|
|
1360
|
+
* FORCE. `--force` is refused because it deletes ignored files a person may
|
|
1361
|
+
* value. This checkout is seconds old: everything in it is either git's own
|
|
1362
|
+
* checkout of a commit or what `carryIntoCheckout` copied into it — copies, with
|
|
1363
|
+
* every link left out — so there is nothing of anyone's to lose. Still only a
|
|
1364
|
+
* DIRECT child of the workspaces root, and only after git's own remove has had
|
|
1365
|
+
* the first say.
|
|
1366
|
+
*
|
|
1367
|
+
* ⚠️ THE BRANCH ONLY IF IT IS STILL WHERE IT WAS CUT (`deleteFreshBranch` is a
|
|
1368
|
+
* compare-and-delete), and never when `worktree add` itself failed — then the
|
|
1369
|
+
* branch may be somebody else's, which is why that call is outside this path.
|
|
1370
|
+
*/
|
|
1371
|
+
async function discardFreshWorkspace(git, root, path, branch, baseSha) {
|
|
1372
|
+
const left = [];
|
|
1373
|
+
try {
|
|
1374
|
+
await git.removeWorktree(path);
|
|
1375
|
+
}
|
|
1376
|
+
catch {
|
|
1377
|
+
if ((0, node_fs_1.existsSync)(path) && (0, node_path_1.resolve)((0, node_path_1.dirname)(path)) === (0, node_path_1.resolve)(root)) {
|
|
1378
|
+
try {
|
|
1379
|
+
(0, node_fs_1.rmSync)(path, { recursive: true, force: true, maxRetries: 3 });
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
// Said below.
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
await git.pruneWorktrees();
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
left.push('its git registration');
|
|
1391
|
+
}
|
|
1392
|
+
// A checkout still on disk still has the branch checked out; deleting it would orphan that HEAD.
|
|
1393
|
+
if ((0, node_fs_1.existsSync)(path)) {
|
|
1394
|
+
left.push(`the directory ${path}`, `the branch ${branch}`);
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1201
1397
|
try {
|
|
1202
|
-
await git.
|
|
1203
|
-
|
|
1398
|
+
if (!(await git.deleteFreshBranch(branch, baseSha)))
|
|
1399
|
+
left.push(`the branch ${branch}`);
|
|
1204
1400
|
}
|
|
1205
1401
|
catch {
|
|
1206
|
-
|
|
1402
|
+
left.push(`the branch ${branch}`);
|
|
1207
1403
|
}
|
|
1208
|
-
throw new WorkspaceError(`the workspace at ${path} could not be claimed for this run (${e.message}), so it was not kept`);
|
|
1209
1404
|
}
|
|
1210
|
-
return
|
|
1405
|
+
return left.length === 0 ? '' : ` (could not remove ${left.join(', ')} — \`looply runs --reap\` retires it)`;
|
|
1211
1406
|
}
|
|
1212
1407
|
/**
|
|
1213
1408
|
* Whether a workspace may be reaped, and why not when it may not.
|