@dynamicagents/plugins 0.12.0 → 0.14.0
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 +58 -77
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +55 -0
- package/dist/claude-code/cards.js +283 -0
- package/dist/claude-code/config.d.ts +28 -63
- package/dist/claude-code/config.js +0 -11
- package/dist/claude-code/egress.d.ts +8 -1
- package/dist/claude-code/egress.js +42 -4
- package/dist/claude-code/events.d.ts +106 -9
- package/dist/claude-code/events.js +201 -56
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +16 -90
- package/dist/claude-code/index.js +14 -276
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +366 -0
- package/dist/claude-code/run.d.ts +230 -116
- package/dist/claude-code/run.js +380 -167
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +288 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +1 -5
- package/dist/computer/context.js +7 -10
- package/dist/computer/file-lock.js +5 -4
- package/dist/computer/gate.d.ts +4 -4
- package/dist/computer/gate.js +5 -5
- package/dist/computer/index.d.ts +40 -258
- package/dist/computer/index.js +71 -324
- package/dist/computer/paths.d.ts +36 -16
- package/dist/computer/paths.js +42 -29
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +1 -40
- package/dist/computer/render.js +2 -68
- package/dist/computer/tools-exec.js +12 -13
- package/dist/computer/tools-file.d.ts +9 -1
- package/dist/computer/tools-file.js +22 -82
- package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +4 -5
- package/dist/repo/checkout.js +31 -8
- package/dist/repo/context.d.ts +35 -9
- package/dist/repo/context.js +101 -49
- package/dist/repo/index.d.ts +73 -28
- package/dist/repo/index.js +112 -96
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.js +1 -1
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
- package/dist/repo/tools-review.d.ts +9 -0
- package/dist/repo/tools-review.js +67 -47
- package/dist/repo/tools-worktree.js +144 -60
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +4 -4
- package/dist/{computer → workspace}/advisory.js +2 -2
- package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
- package/dist/{computer/host → workspace}/ca-trust.js +12 -3
- package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
- package/dist/{computer/host → workspace}/container-deps.js +26 -1
- package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- package/dist/workspace/deps-snapshot.d.ts +110 -0
- package/dist/workspace/deps-snapshot.js +424 -0
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
- package/dist/{computer/host → workspace}/git-host.js +72 -2
- package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
- package/dist/{computer/host → workspace}/git-identity.js +1 -1
- package/dist/workspace/index.d.ts +44 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
- package/dist/{computer/host → workspace}/install-job.js +346 -178
- package/dist/{computer → workspace}/install.d.ts +17 -8
- package/dist/{computer → workspace}/install.js +24 -6
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/{computer → workspace}/shell.d.ts +5 -5
- package/dist/{computer → workspace}/shell.js +6 -6
- package/dist/{computer/host → workspace}/wake.d.ts +3 -1
- package/package.json +17 -37
- package/dist/arc-agi/analysis.d.ts +0 -322
- package/dist/arc-agi/analysis.js +0 -802
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -24
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -40
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -24
- package/dist/arc-agi/main-agent.js +0 -62
- package/dist/arc-agi/recipe.d.ts +0 -48
- package/dist/arc-agi/recipe.js +0 -97
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -29
- package/dist/arc-agi/soul.js +0 -79
- package/dist/arc-agi/store.d.ts +0 -153
- package/dist/arc-agi/store.js +0 -215
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -574
- package/dist/arc-agi/types.d.ts +0 -211
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -133
- package/dist/computer/read.js +0 -200
- package/dist/computer/tools-find.js +0 -216
- package/dist/recall/index.d.ts +0 -105
- package/dist/recall/index.js +0 -181
- package/dist/repo/tools-forge.d.ts +0 -4
- package/dist/triage/index.d.ts +0 -360
- package/dist/triage/index.js +0 -311
- /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
- /package/dist/{computer/host → workspace}/sync.js +0 -0
- /package/dist/{computer/host → workspace}/wake.js +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { shellQuote } from "@cloudflare/computer";
|
|
2
2
|
import { JobLifecycle } from "@dynamicagents/core/job";
|
|
3
|
-
// Leaf modules rather than
|
|
3
|
+
// Leaf modules rather than `./index.ts`, and that is structural: the barrel
|
|
4
4
|
// re-exports this directory, so reaching it from here would be a cycle — through
|
|
5
5
|
// a module that builds a class at import time, where initialisation order
|
|
6
6
|
// decides whether the base is `undefined`.
|
|
7
|
-
import { installFingerprint, resolveInstallCommand } from "
|
|
8
|
-
import { pathExists } from "
|
|
7
|
+
import { installFingerprint, resolveInstallCommand, rootFingerprint } from "./install.js";
|
|
8
|
+
import { pathExists } from "./read.js";
|
|
9
9
|
import { deploymentFault } from "./container-fault.js";
|
|
10
10
|
import { INSTALLED_MARKER } from "./container-deps.js";
|
|
11
|
-
import { truncateOutput } from "
|
|
11
|
+
import { truncateOutput } from "./format.js";
|
|
12
12
|
/**
|
|
13
13
|
* Whether a thrown value is the runtime reporting a replaced container.
|
|
14
14
|
*
|
|
@@ -27,7 +27,7 @@ function execWasLost(err) {
|
|
|
27
27
|
* ## The rule every guard here serves
|
|
28
28
|
*
|
|
29
29
|
* `running` is the one install state that **blocks work**: the plugin's gate
|
|
30
|
-
* waits on it and then refuses to run. Every other state is a fact
|
|
30
|
+
* waits on it and then refuses to run. Every other state is a fact an agent
|
|
31
31
|
* can act on. So a `running` record must never outlive the command it describes,
|
|
32
32
|
* and the ways it can are not all reachable from one place — the spawn can fail
|
|
33
33
|
* before a drain is attached, a drain can be cut short by an eviction, `getExec`
|
|
@@ -38,8 +38,9 @@ function execWasLost(err) {
|
|
|
38
38
|
* ## What decides that an install is needed
|
|
39
39
|
*
|
|
40
40
|
* The tree lives on the container's disk (see `./container-deps.ts`), so it goes
|
|
41
|
-
* with the container. {@link CONTAINER_TREE_KEY} records a successful install
|
|
42
|
-
*
|
|
41
|
+
* with the container. {@link CONTAINER_TREE_KEY} records a successful install — or
|
|
42
|
+
* a tree restored with a snapshot of the container that ran one, through
|
|
43
|
+
* {@link InstallJob.adopt} — and is cleared whenever that container is seen gone.
|
|
43
44
|
*/
|
|
44
45
|
/**
|
|
45
46
|
* The install's job id, which is also the key its state record lives under.
|
|
@@ -78,6 +79,11 @@ const INSTALL_STALE_MS = 5 * 60_000;
|
|
|
78
79
|
const CONTAINER_TREE_KEY = "install:tree";
|
|
79
80
|
/** Records a deployed object may still hold, deleted on the next install. */
|
|
80
81
|
const RETIRED_KEYS = ["install:syncing", "install:completed"];
|
|
82
|
+
/**
|
|
83
|
+
* The command a reservation's placeholder names, until the install's own is
|
|
84
|
+
* resolved. The gate renders it while it waits.
|
|
85
|
+
*/
|
|
86
|
+
const RESOLVING = "(being resolved)";
|
|
81
87
|
/**
|
|
82
88
|
* The exec id an install runs under.
|
|
83
89
|
*
|
|
@@ -108,8 +114,12 @@ export class InstallJob {
|
|
|
108
114
|
*
|
|
109
115
|
* **Arming writes `running` before anything is running**, which holds the gate
|
|
110
116
|
* shut in the moments before the alarm fires. The alarm then presents the stamp
|
|
111
|
-
* arming wrote to `
|
|
117
|
+
* arming wrote to `reserve`, which recognises its own placeholder and nothing
|
|
112
118
|
* else — taking over any `running` record instead would be displacement again.
|
|
119
|
+
*
|
|
120
|
+
* Every write a run makes goes through the lifecycle's `generation`, under
|
|
121
|
+
* the `startedAt` its reservation returned, so a run that has been displaced
|
|
122
|
+
* writes nothing.
|
|
113
123
|
*/
|
|
114
124
|
#job;
|
|
115
125
|
// Built here rather than as a field initializer, which would run before the
|
|
@@ -181,7 +191,7 @@ export class InstallJob {
|
|
|
181
191
|
dir: context.dir
|
|
182
192
|
});
|
|
183
193
|
// Everything the arming handshake needs — the placeholder write, the stamp
|
|
184
|
-
// the alarm presents to `
|
|
194
|
+
// the alarm presents to `reserve`, the cooldown floor and the run intent — in
|
|
185
195
|
// one call, and unwound as a unit if the intent cannot be scheduled.
|
|
186
196
|
await this.#job.arm({ command: state.command });
|
|
187
197
|
}
|
|
@@ -203,6 +213,57 @@ export class InstallJob {
|
|
|
203
213
|
async dir() {
|
|
204
214
|
return (await this.#job.context())?.dir;
|
|
205
215
|
}
|
|
216
|
+
/** Where the last install ran, and for which repository's override. */
|
|
217
|
+
async target() {
|
|
218
|
+
const context = await this.#job.context();
|
|
219
|
+
if (!context?.dir)
|
|
220
|
+
return undefined;
|
|
221
|
+
return {
|
|
222
|
+
dir: context.dir,
|
|
223
|
+
...(context.repo ? { repo: context.repo } : {})
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The fingerprint an install into `target` would write as its marker, from the
|
|
228
|
+
* checkout as it stands — `null` when there is nothing to install there.
|
|
229
|
+
*/
|
|
230
|
+
async fingerprintFor(target) {
|
|
231
|
+
const probe = this.#probe();
|
|
232
|
+
const resolution = await resolveInstallCommand(probe, target.dir, this.deps.plan(), target.repo);
|
|
233
|
+
return await installFingerprint(probe, target.dir, resolution);
|
|
234
|
+
}
|
|
235
|
+
/** What `dir`'s install inputs hash to — see `rootFingerprint`. */
|
|
236
|
+
async rootFingerprint(dir) {
|
|
237
|
+
return await rootFingerprint(this.#probe(), dir, this.deps.plan());
|
|
238
|
+
}
|
|
239
|
+
/** The tree this container holds, if a finished install says it does. */
|
|
240
|
+
async tree() {
|
|
241
|
+
const tree = await this.#tree();
|
|
242
|
+
return (tree && { dir: tree.dir, fingerprint: tree.fingerprint, at: tree.at });
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Record a tree this container came up holding — restored with a snapshot of
|
|
246
|
+
* the container that installed it — as if this container had installed it.
|
|
247
|
+
*
|
|
248
|
+
* Only for a tree whose marker the setup read and matched against the checkout
|
|
249
|
+
* as it stands; see `./deps-snapshot.ts`. So the last install's context moves
|
|
250
|
+
* to it too: a checkout that moved since that install would otherwise read as
|
|
251
|
+
* missing its tree, and arm an install that finds the tree already there.
|
|
252
|
+
*/
|
|
253
|
+
async adopt(tree) {
|
|
254
|
+
await this.deps.storage.put(CONTAINER_TREE_KEY, {
|
|
255
|
+
dir: tree.dir,
|
|
256
|
+
fingerprint: tree.fingerprint,
|
|
257
|
+
at: Date.now()
|
|
258
|
+
});
|
|
259
|
+
const context = await this.#job.context();
|
|
260
|
+
if (context?.dir === tree.dir && context.fingerprint !== tree.fingerprint)
|
|
261
|
+
await this.#job.putContext({ ...context, fingerprint: tree.fingerprint });
|
|
262
|
+
console.info(`[${this.deps.tag()}] adopted a restored dependency tree`, {
|
|
263
|
+
id: this.deps.id(),
|
|
264
|
+
dir: tree.dir
|
|
265
|
+
});
|
|
266
|
+
}
|
|
206
267
|
/** The container went away, and its tree with it. */
|
|
207
268
|
async containerGone() {
|
|
208
269
|
await this.deps.storage.delete(CONTAINER_TREE_KEY);
|
|
@@ -239,7 +300,7 @@ export class InstallJob {
|
|
|
239
300
|
*
|
|
240
301
|
* Called from `repo_clone` through the repo plugin's `afterCheckout` hook, so
|
|
241
302
|
* it runs inside a model turn and must not block on the install — 225 seconds
|
|
242
|
-
* for slack-gatekeeper, against a
|
|
303
|
+
* for slack-gatekeeper, against a turn that is cut at fifteen minutes.
|
|
243
304
|
*
|
|
244
305
|
* That caller is a model turn, which lives long enough to hold the drain handed
|
|
245
306
|
* to `ctx.waitUntil` below. **A short-lived caller cannot**, which is why an
|
|
@@ -273,35 +334,10 @@ export class InstallJob {
|
|
|
273
334
|
async #beginInstall(req, own, opts) {
|
|
274
335
|
await this.deps.touch();
|
|
275
336
|
await this.deps.ready();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
* `repo_clone` calls this and a retried chunk calls it again. Every call
|
|
280
|
-
* spawns with the same {@link INSTALL_EXEC_ID}, so without this each would
|
|
281
|
-
* displace the last while the displaced command's drain stayed attached
|
|
282
|
-
* through `ctx.waitUntil` — then wrote *its* outcome over a record
|
|
283
|
-
* describing an install still running perfectly well.
|
|
284
|
-
*
|
|
285
|
-
* {@link state} rather than the lifecycle's raw `read()`, so a `running`
|
|
286
|
-
* record left by a dead isolate is resolved here rather than blocking a
|
|
287
|
-
* legitimate retry forever.
|
|
288
|
-
*
|
|
289
|
-
* `takeOverArmedAt` is the one exemption, narrow on purpose: the alarm's
|
|
290
|
-
* placeholder is a `running` record for an install that has not started, so
|
|
291
|
-
* the alarm must pass its own guard and only its own. Matching the exact
|
|
292
|
-
* `startedAt` it wrote is what stops that becoming "take over any running
|
|
293
|
-
* install", which is displacement again in a new hat.
|
|
294
|
-
*/
|
|
337
|
+
// {@link state} rather than the lifecycle's raw `read()`: a `running` record
|
|
338
|
+
// left by a dead isolate is repaired on the way past, and one nobody is
|
|
339
|
+
// draining is re-attached.
|
|
295
340
|
const current = await this.state();
|
|
296
|
-
const claim = this.#job.claim(current, this.deps.timeoutMs(), opts?.takeOverArmedAt);
|
|
297
|
-
if (!claim.ok) {
|
|
298
|
-
console.info(`[${this.deps.tag()}] an install is already in flight`, {
|
|
299
|
-
id: this.deps.id(),
|
|
300
|
-
command: claim.current.command,
|
|
301
|
-
seconds: Math.round((Date.now() - claim.current.startedAt) / 1000)
|
|
302
|
-
});
|
|
303
|
-
return claim.current;
|
|
304
|
-
}
|
|
305
341
|
/**
|
|
306
342
|
* A full workspace refuses the install and **writes nothing**.
|
|
307
343
|
*
|
|
@@ -325,11 +361,126 @@ export class InstallJob {
|
|
|
325
361
|
});
|
|
326
362
|
return current;
|
|
327
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* One install at a time — the hazard is displacement.
|
|
366
|
+
*
|
|
367
|
+
* `repo_clone` calls this and a recovered turn calls it again. Every call
|
|
368
|
+
* spawns with the same {@link INSTALL_EXEC_ID}, so without this each would
|
|
369
|
+
* displace the last while the displaced command's drain stayed attached
|
|
370
|
+
* through `ctx.waitUntil` — then wrote *its* outcome over a record
|
|
371
|
+
* describing an install still running perfectly well.
|
|
372
|
+
*
|
|
373
|
+
* Reserved rather than checked: everything between here and the spawn —
|
|
374
|
+
* probing the checkout, hashing the lockfile — awaits the container, and a
|
|
375
|
+
* check that wrote nothing would let a second caller through in that time.
|
|
376
|
+
*
|
|
377
|
+
* `takeOverArmedAt` is the one exemption, narrow on purpose: the alarm's
|
|
378
|
+
* placeholder is a `running` record for an install that has not started, so
|
|
379
|
+
* the alarm must pass its own guard and only its own. Matching the exact
|
|
380
|
+
* `startedAt` it wrote is what stops that becoming "take over any running
|
|
381
|
+
* install", which is displacement again in a new hat.
|
|
382
|
+
*/
|
|
383
|
+
const reserved = await this.#job.reserve({ command: RESOLVING }, this.deps.timeoutMs(), opts?.takeOverArmedAt);
|
|
384
|
+
if (!reserved.ok) {
|
|
385
|
+
console.info(`[${this.deps.tag()}] an install is already in flight`, {
|
|
386
|
+
id: this.deps.id(),
|
|
387
|
+
command: reserved.current.command,
|
|
388
|
+
seconds: Math.round((Date.now() - reserved.current.startedAt) / 1000)
|
|
389
|
+
});
|
|
390
|
+
return reserved.current;
|
|
391
|
+
}
|
|
392
|
+
const { startedAt } = reserved;
|
|
393
|
+
const run = this.#job.generation(startedAt);
|
|
394
|
+
this.#owned.add(startedAt);
|
|
395
|
+
let handed = false;
|
|
396
|
+
try {
|
|
397
|
+
return await this.#spawnReserved(req, run, reserved, own, () => {
|
|
398
|
+
handed = true;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
if (!handed)
|
|
403
|
+
this.#owned.delete(startedAt);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Everything after the reservation: prepare, spawn, and hand the command to
|
|
408
|
+
* `own`. Split out so {@link #beginInstall} can hold the generation for exactly
|
|
409
|
+
* this long — `handing` is called as the drain takes it over.
|
|
410
|
+
*/
|
|
411
|
+
async #spawnReserved(req, run, reserved, own, handing) {
|
|
412
|
+
const { startedAt } = reserved;
|
|
328
413
|
// Nothing trusts the interception CA here, deliberately: the workspace's own
|
|
329
414
|
// `ready` does it, above every early return in this method. An install
|
|
330
415
|
// already in flight and a full workspace both return before this line, and
|
|
331
416
|
// neither of them means the container cannot speak TLS — the full workspace
|
|
332
417
|
// least of all, since egress is what the agent needs to dig itself out.
|
|
418
|
+
let prepared;
|
|
419
|
+
try {
|
|
420
|
+
prepared = await this.#prepare(req, run, startedAt);
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
await this.#release(run, reserved.previous, err);
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
if (prepared.kind === "settled")
|
|
427
|
+
return prepared.state;
|
|
428
|
+
const { resolution, fingerprint, state } = prepared;
|
|
429
|
+
let handle;
|
|
430
|
+
try {
|
|
431
|
+
// The marker lets a new isolate check {@link CONTAINER_TREE_KEY} against
|
|
432
|
+
// the container; see `reconcile`.
|
|
433
|
+
const marked = `${resolution.command} && printf %s ` +
|
|
434
|
+
`${shellQuote(fingerprint ?? "none")} > node_modules/${INSTALLED_MARKER}`;
|
|
435
|
+
handle = await this.deps.workspace().runtime.exec(marked, {
|
|
436
|
+
id: INSTALL_EXEC_ID,
|
|
437
|
+
cwd: req.dir,
|
|
438
|
+
encoding: "utf8",
|
|
439
|
+
timeoutMs: this.deps.timeoutMs()
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
// The command never started, so nothing will ever drain it and no
|
|
444
|
+
// re-attach can find it. Close the record here: a `failed` install is
|
|
445
|
+
// recoverable — the agent is told what happened and can run the command
|
|
446
|
+
// itself — where a `running` one that nobody owns is not.
|
|
447
|
+
//
|
|
448
|
+
// Which sentence, though, depends on why. The default advice — run it
|
|
449
|
+
// yourself with `bash` — is good for a container that is merely
|
|
450
|
+
// unreachable, and useless for a deployment fault: the same container is
|
|
451
|
+
// what `bash` would have to reach. Under one of those, say what an
|
|
452
|
+
// operator has to do and do not send the agent after a command that
|
|
453
|
+
// cannot run either.
|
|
454
|
+
const fault = deploymentFault(err);
|
|
455
|
+
console.error(`[${this.deps.tag()}] ${fault?.summary ?? "the install could not be started"}`, {
|
|
456
|
+
id: this.deps.id(),
|
|
457
|
+
command: resolution.command,
|
|
458
|
+
...(fault ? { remedy: fault.remedy } : {}),
|
|
459
|
+
err: String(err)
|
|
460
|
+
});
|
|
461
|
+
const failed = {
|
|
462
|
+
state: "failed",
|
|
463
|
+
command: resolution.command,
|
|
464
|
+
finishedAt: Date.now(),
|
|
465
|
+
error: fault
|
|
466
|
+
? `the install could not be started: ${fault.remedy}`
|
|
467
|
+
: `the install could not be started (${String(err)}). The container ` +
|
|
468
|
+
"was most likely unreachable. Run the command yourself with bash, " +
|
|
469
|
+
"or clone again to retry it."
|
|
470
|
+
};
|
|
471
|
+
return await this.#settle(run, failed);
|
|
472
|
+
}
|
|
473
|
+
handing();
|
|
474
|
+
await own(handle, startedAt);
|
|
475
|
+
return state;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Everything between a reservation and the spawn: resolve the command, and
|
|
479
|
+
* settle the run here when there is nothing to install or the tree is already
|
|
480
|
+
* this lockfile's. Otherwise write this install's own `running` record and
|
|
481
|
+
* context, and arm the watchdog.
|
|
482
|
+
*/
|
|
483
|
+
async #prepare(req, run, startedAt) {
|
|
333
484
|
const probe = this.#probe();
|
|
334
485
|
const resolution = await resolveInstallCommand(probe, req.dir, this.deps.plan(), req.repo);
|
|
335
486
|
if (resolution.kind === "skip") {
|
|
@@ -337,7 +488,6 @@ export class InstallJob {
|
|
|
337
488
|
state: "skipped",
|
|
338
489
|
reason: resolution.reason
|
|
339
490
|
};
|
|
340
|
-
await this.#job.write(state);
|
|
341
491
|
/**
|
|
342
492
|
* The common branch, and the one whose absence is indistinguishable from
|
|
343
493
|
* never having been called.
|
|
@@ -355,7 +505,7 @@ export class InstallJob {
|
|
|
355
505
|
...(req.repo ? { repo: req.repo } : {}),
|
|
356
506
|
reason: resolution.reason
|
|
357
507
|
});
|
|
358
|
-
return state;
|
|
508
|
+
return { kind: "settled", state: await this.#settle(run, state) };
|
|
359
509
|
}
|
|
360
510
|
const fingerprint = await installFingerprint(probe, req.dir, resolution);
|
|
361
511
|
// Skipped only for this container's own tree, for this lockfile.
|
|
@@ -371,18 +521,18 @@ export class InstallJob {
|
|
|
371
521
|
ms: 0,
|
|
372
522
|
tail: "dependencies already installed in this container for this lockfile"
|
|
373
523
|
};
|
|
374
|
-
await this.#
|
|
375
|
-
return state;
|
|
524
|
+
return { kind: "settled", state: await this.#settle(run, state) };
|
|
376
525
|
}
|
|
377
526
|
// The tree is rebuilt from here, so nothing vouches for it until exit 0.
|
|
378
527
|
await this.deps.storage.delete([CONTAINER_TREE_KEY, ...RETIRED_KEYS]);
|
|
379
|
-
const startedAt = Date.now();
|
|
380
528
|
const state = {
|
|
381
529
|
state: "running",
|
|
382
530
|
command: resolution.command,
|
|
383
531
|
startedAt
|
|
384
532
|
};
|
|
385
|
-
await
|
|
533
|
+
if (!(await run.write(state))) {
|
|
534
|
+
return { kind: "settled", state: await this.#job.read() };
|
|
535
|
+
}
|
|
386
536
|
await this.#job.putContext({
|
|
387
537
|
dir: req.dir,
|
|
388
538
|
// Kept so a reinstall the alarm drives — which has no caller to ask —
|
|
@@ -391,8 +541,7 @@ export class InstallJob {
|
|
|
391
541
|
// on every later run, installing a different tree than the first time.
|
|
392
542
|
...(req.repo ? { repo: req.repo } : {}),
|
|
393
543
|
fingerprint,
|
|
394
|
-
command: resolution.command
|
|
395
|
-
startedAt
|
|
544
|
+
command: resolution.command
|
|
396
545
|
});
|
|
397
546
|
/**
|
|
398
547
|
* Armed **before** the spawn — the hazard is an isolate that dies between
|
|
@@ -408,54 +557,33 @@ export class InstallJob {
|
|
|
408
557
|
* `running`, so finishing first costs one wake-up.
|
|
409
558
|
*/
|
|
410
559
|
await this.#job.armWatch();
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// The command never started, so nothing will ever drain it and no
|
|
426
|
-
// re-attach can find it. Close the record here: a `failed` install is
|
|
427
|
-
// recoverable — the subagent is told what happened and can run the command
|
|
428
|
-
// itself — where a `running` one that nobody owns is not.
|
|
429
|
-
//
|
|
430
|
-
// Which sentence, though, depends on why. The default advice — run it
|
|
431
|
-
// yourself with `sb_exec` — is good for a container that is merely
|
|
432
|
-
// unreachable, and useless for a deployment fault: the same container is
|
|
433
|
-
// what `sb_exec` would have to reach. Under one of those, say what an
|
|
434
|
-
// operator has to do and do not send the subagent after a command that
|
|
435
|
-
// cannot run either.
|
|
436
|
-
const fault = deploymentFault(err);
|
|
437
|
-
console.error(`[${this.deps.tag()}] ${fault?.summary ?? "the install could not be started"}`, {
|
|
438
|
-
id: this.deps.id(),
|
|
439
|
-
command: resolution.command,
|
|
440
|
-
...(fault ? { remedy: fault.remedy } : {}),
|
|
441
|
-
err: String(err)
|
|
442
|
-
});
|
|
443
|
-
const failed = {
|
|
560
|
+
return { kind: "spawn", resolution, fingerprint, state };
|
|
561
|
+
}
|
|
562
|
+
/** Settle `run` with `state`, or say what stands if it was displaced. */
|
|
563
|
+
async #settle(run, state) {
|
|
564
|
+
return (await run.write(state)) ? state : await this.#job.read();
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Hand a reservation back when nothing was spawned under it: the record as it
|
|
568
|
+
* was, unless that was a run of its own — the alarm's placeholder — which
|
|
569
|
+
* nothing would now finish, so `failed`.
|
|
570
|
+
*/
|
|
571
|
+
async #release(run, previous, err) {
|
|
572
|
+
const back = previous.state === "running"
|
|
573
|
+
? {
|
|
444
574
|
state: "failed",
|
|
445
|
-
command:
|
|
575
|
+
command: previous.command,
|
|
446
576
|
finishedAt: Date.now(),
|
|
447
|
-
error:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
577
|
+
error: `the install could not be prepared (${String(err)}). Run the ` +
|
|
578
|
+
"command yourself with bash, or clone again to retry it."
|
|
579
|
+
}
|
|
580
|
+
: previous;
|
|
581
|
+
try {
|
|
582
|
+
await run.write(back);
|
|
583
|
+
}
|
|
584
|
+
catch (rollback) {
|
|
585
|
+
throw new AggregateError([err, rollback], "the install could not be prepared, and its reservation could not be released");
|
|
456
586
|
}
|
|
457
|
-
await own(handle, startedAt);
|
|
458
|
-
return state;
|
|
459
587
|
}
|
|
460
588
|
/**
|
|
461
589
|
* Whether this container holds a finished tree, asked only when it qualifies a
|
|
@@ -504,83 +632,102 @@ export class InstallJob {
|
|
|
504
632
|
finishedAt: Date.now(),
|
|
505
633
|
error: `the install has been running for ${minutes} minutes without ` +
|
|
506
634
|
"reporting, which is past its timeout — it is not going to finish. " +
|
|
507
|
-
"Run the command yourself with
|
|
635
|
+
"Run the command yourself with bash if you still need it."
|
|
508
636
|
};
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
637
|
+
// Under the record's own generation, so a reservation that took the record
|
|
638
|
+
// since it was read is not overwritten.
|
|
639
|
+
const repaired = await this.#job
|
|
640
|
+
.generation(state.startedAt)
|
|
641
|
+
.write(failed);
|
|
642
|
+
return repaired ? failed : await this.#job.read();
|
|
512
643
|
}
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
return state;
|
|
644
|
+
if (await this.#drivenHere(state.startedAt))
|
|
645
|
+
return state;
|
|
646
|
+
await this.#reattachInstall(state.startedAt);
|
|
647
|
+
return await this.#job.read();
|
|
518
648
|
}
|
|
519
649
|
/**
|
|
520
650
|
* When a reinstall was queued, if one is waiting to run.
|
|
521
651
|
*
|
|
522
|
-
* Read by {@link file
|
|
652
|
+
* Read by {@link file://./advisory.ts deriveAdvisories}, which is the only
|
|
523
653
|
* caller that needs it: a `failed` record with a repair already queued is a
|
|
524
654
|
* transient condition wearing a permanent record's clothes.
|
|
525
655
|
*/
|
|
526
656
|
async reinstallArmedAt() {
|
|
527
657
|
return await this.#job.armedAt();
|
|
528
658
|
}
|
|
529
|
-
/** True while this isolate holds the drain, so the watchdog leaves it alone. */
|
|
530
|
-
#draining = false;
|
|
531
|
-
async #drainInstall(handle,
|
|
532
659
|
/**
|
|
533
|
-
* The
|
|
660
|
+
* The generations this isolate is driving: reserved, taken over from the
|
|
661
|
+
* alarm's placeholder, or draining — until the drain ends.
|
|
534
662
|
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
663
|
+
* The re-attach is for a `running` record nobody here is driving, which is one
|
|
664
|
+
* a dead isolate left. Asked of a generation that is reserved but not yet
|
|
665
|
+
* spawned, it finds no exec, or the last container's, and writes `failed` over
|
|
666
|
+
* an install about to start, whose own verdict is then discarded as
|
|
667
|
+
* superseded. Owning from the reservation rather than from the drain is what
|
|
668
|
+
* closes that window: resolving and spawning await the container for seconds.
|
|
669
|
+
*/
|
|
670
|
+
#owned = new Set();
|
|
671
|
+
/**
|
|
672
|
+
* Whether a `running` record's install is this isolate's to finish: one it
|
|
673
|
+
* owns, or the alarm's placeholder still waiting for the alarm, whose
|
|
674
|
+
* `startedAt` is the arming stamp.
|
|
675
|
+
*/
|
|
676
|
+
async #drivenHere(startedAt) {
|
|
677
|
+
return (this.#owned.has(startedAt) || (await this.#job.armedAt()) === startedAt);
|
|
678
|
+
}
|
|
679
|
+
async #drainInstall(handle,
|
|
680
|
+
/**
|
|
681
|
+
* The generation this drain writes under: the `startedAt` its reservation
|
|
682
|
+
* returned, or — re-attached — the running record's.
|
|
541
683
|
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
684
|
+
* Passed in rather than read below: this method's first `await` is reached
|
|
685
|
+
* long after `#beginInstall` returned, and a record read then may already be
|
|
686
|
+
* a later install's, whose generation this drain would adopt.
|
|
545
687
|
*/
|
|
546
|
-
|
|
547
|
-
this.#
|
|
688
|
+
startedAt) {
|
|
689
|
+
this.#owned.add(startedAt);
|
|
548
690
|
const context = await this.#job.context();
|
|
549
691
|
const command = context?.command ?? "(unknown)";
|
|
550
|
-
const startedAt = ownedAt ?? context?.startedAt ?? Date.now();
|
|
551
692
|
/**
|
|
552
693
|
* Whether this drain still owns the record.
|
|
553
694
|
*
|
|
554
|
-
* The
|
|
555
|
-
* place; this makes it harmless if one ever does. A drain can outlive
|
|
556
|
-
* command it was watching — `ctx.waitUntil` keeps running after the RPC
|
|
695
|
+
* The reservation in `#beginInstall` stops two installs overlapping in the
|
|
696
|
+
* first place; this makes it harmless if one ever does. A drain can outlive
|
|
697
|
+
* the command it was watching — `ctx.waitUntil` keeps running after the RPC
|
|
557
698
|
* returns — and the damage a late one does is silent: it writes a verdict
|
|
558
699
|
* about a finished command over a record describing a live one, and every
|
|
559
|
-
* `
|
|
560
|
-
*
|
|
561
|
-
* `startedAt` is the generation marker. `#beginInstall` rewrites the context
|
|
562
|
-
* before it spawns, so a drain whose stamp no longer matches has been
|
|
563
|
-
* superseded and has nothing useful left to say. The marker also **latches**:
|
|
564
|
-
* ownership is not recoverable, so a stamp that happens to match again does
|
|
565
|
-
* not hand the record back.
|
|
700
|
+
* `bash` then reads a result that belongs to nothing.
|
|
566
701
|
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
702
|
+
* Every write goes through `run.write`, which checks and writes in one step
|
|
703
|
+
* and clears the watchdog with a verdict. `stillMine` is asked only before
|
|
704
|
+
* this drain's own side effects. The superseded line is logged once — a
|
|
705
|
+
* superseded drain can be refused on both the success and the error path.
|
|
569
706
|
*/
|
|
570
|
-
const
|
|
707
|
+
const run = this.#job.generation(startedAt);
|
|
571
708
|
let logged = false;
|
|
709
|
+
const superseded = async () => {
|
|
710
|
+
if (logged)
|
|
711
|
+
return;
|
|
712
|
+
logged = true;
|
|
713
|
+
const current = await this.#job.read();
|
|
714
|
+
console.warn(`[${this.deps.tag()}] discarding a superseded install drain`, {
|
|
715
|
+
id: this.deps.id(),
|
|
716
|
+
command,
|
|
717
|
+
startedAt,
|
|
718
|
+
current: current.state === "running" ? current.startedAt : current.state
|
|
719
|
+
});
|
|
720
|
+
};
|
|
572
721
|
const stillMine = async () => {
|
|
573
|
-
if (await
|
|
722
|
+
if (await run.stillMine())
|
|
574
723
|
return true;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
});
|
|
583
|
-
}
|
|
724
|
+
await superseded();
|
|
725
|
+
return false;
|
|
726
|
+
};
|
|
727
|
+
const settle = async (verdict, also) => {
|
|
728
|
+
if (await run.write(verdict, also))
|
|
729
|
+
return true;
|
|
730
|
+
await superseded();
|
|
584
731
|
return false;
|
|
585
732
|
};
|
|
586
733
|
try {
|
|
@@ -593,33 +740,48 @@ export class InstallJob {
|
|
|
593
740
|
if (!(await stillMine()))
|
|
594
741
|
return;
|
|
595
742
|
if (result.exitCode === 0) {
|
|
596
|
-
|
|
597
|
-
|
|
743
|
+
// With the verdict, in the same write: the tree is this install's only if
|
|
744
|
+
// the verdict is.
|
|
745
|
+
const tree = context?.dir
|
|
746
|
+
? {
|
|
598
747
|
dir: context.dir,
|
|
599
748
|
fingerprint: context.fingerprint,
|
|
600
749
|
at: Date.now()
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
}
|
|
750
|
+
}
|
|
751
|
+
: undefined;
|
|
604
752
|
console.info(`[${this.deps.tag()}] install finished`, {
|
|
605
753
|
id: this.deps.id(),
|
|
606
754
|
command,
|
|
607
755
|
seconds: Math.round((Date.now() - startedAt) / 1000)
|
|
608
756
|
});
|
|
609
|
-
|
|
757
|
+
const installed = tree?.fingerprint
|
|
758
|
+
? { dir: tree.dir, fingerprint: tree.fingerprint, at: tree.at }
|
|
759
|
+
: undefined;
|
|
760
|
+
const kept = await settle({
|
|
610
761
|
state: "done",
|
|
611
762
|
command,
|
|
612
763
|
exitCode: 0,
|
|
613
764
|
finishedAt: Date.now(),
|
|
614
765
|
ms: Date.now() - startedAt,
|
|
615
766
|
tail
|
|
616
|
-
}
|
|
767
|
+
}, tree
|
|
768
|
+
? {
|
|
769
|
+
...(installed ? this.deps.installedRecords(installed) : {}),
|
|
770
|
+
[CONTAINER_TREE_KEY]: tree
|
|
771
|
+
}
|
|
772
|
+
: undefined);
|
|
773
|
+
// Outside the verdict: a host that cannot act on the news must not turn
|
|
774
|
+
// a finished install into a failed one, which the catch below would.
|
|
775
|
+
if (kept && installed)
|
|
776
|
+
await this.deps.installed(installed).catch((err) => {
|
|
777
|
+
console.warn(`[${this.deps.tag()}] could not act on a finished install`, { id: this.deps.id(), err: String(err) });
|
|
778
|
+
});
|
|
617
779
|
}
|
|
618
780
|
else {
|
|
619
781
|
// Logged, and this line is not optional. This is the *ordinary* way an
|
|
620
782
|
// install fails — the other paths are all exceptional — and it used to
|
|
621
783
|
// write the record and say nothing, so an operator looking at why the
|
|
622
|
-
//
|
|
784
|
+
// agent was complaining found the complaint and no cause. The tail
|
|
623
785
|
// is the install's own last words; without it the only copy is inside a
|
|
624
786
|
// Durable Object nobody can query.
|
|
625
787
|
console.error(`[${this.deps.tag()}] install failed`, {
|
|
@@ -629,7 +791,7 @@ export class InstallJob {
|
|
|
629
791
|
seconds: Math.round((Date.now() - startedAt) / 1000),
|
|
630
792
|
tail: truncateOutput(tail, 1000)
|
|
631
793
|
});
|
|
632
|
-
await
|
|
794
|
+
await settle({
|
|
633
795
|
state: "failed",
|
|
634
796
|
command,
|
|
635
797
|
finishedAt: Date.now(),
|
|
@@ -658,7 +820,7 @@ export class InstallJob {
|
|
|
658
820
|
seconds: Math.round((Date.now() - startedAt) / 1000),
|
|
659
821
|
err: String(err)
|
|
660
822
|
});
|
|
661
|
-
await
|
|
823
|
+
await settle({
|
|
662
824
|
state: "failed",
|
|
663
825
|
command,
|
|
664
826
|
finishedAt: Date.now(),
|
|
@@ -666,13 +828,8 @@ export class InstallJob {
|
|
|
666
828
|
});
|
|
667
829
|
}
|
|
668
830
|
finally {
|
|
669
|
-
this.#
|
|
831
|
+
this.#owned.delete(startedAt);
|
|
670
832
|
handle[Symbol.dispose]();
|
|
671
|
-
// Not if this drain was superseded: the watchdog belongs to whichever
|
|
672
|
-
// install owns the record now, and clearing it here would disarm the one
|
|
673
|
-
// recovery path the *live* install has.
|
|
674
|
-
if (!generation.superseded())
|
|
675
|
-
await this.#job.clearWatch();
|
|
676
833
|
}
|
|
677
834
|
}
|
|
678
835
|
/**
|
|
@@ -681,10 +838,12 @@ export class InstallJob {
|
|
|
681
838
|
* `getExec` with `resume: "tail"` re-opens the stream of a command that is
|
|
682
839
|
* still running in the container — or replays the end of one that finished
|
|
683
840
|
* while nobody was listening, which is the case that would otherwise leave the
|
|
684
|
-
* record stuck at `running` and every `
|
|
841
|
+
* record stuck at `running` and every `bash` blocked behind it.
|
|
685
842
|
*/
|
|
686
|
-
async #reattachInstall(
|
|
687
|
-
|
|
843
|
+
async #reattachInstall(
|
|
844
|
+
/** The running record's `startedAt`: the generation it picks up. */
|
|
845
|
+
startedAt) {
|
|
846
|
+
if (await this.#drivenHere(startedAt))
|
|
688
847
|
return;
|
|
689
848
|
try {
|
|
690
849
|
const handle = await this.deps
|
|
@@ -693,12 +852,12 @@ export class InstallJob {
|
|
|
693
852
|
encoding: "utf8",
|
|
694
853
|
resume: "tail"
|
|
695
854
|
});
|
|
696
|
-
this.deps.waitUntil(this.#drainInstall(handle));
|
|
855
|
+
this.deps.waitUntil(this.#drainInstall(handle, startedAt));
|
|
697
856
|
}
|
|
698
857
|
catch (err) {
|
|
699
858
|
// The exec is gone entirely — the container was replaced under it. Say so
|
|
700
859
|
// rather than leaving the gate closed forever; the next checkout starts a
|
|
701
|
-
// new install, and `
|
|
860
|
+
// new install, and `bash` can run in the meantime.
|
|
702
861
|
if (execWasLost(err))
|
|
703
862
|
await this.deps.containerGone();
|
|
704
863
|
// Same split as the spawn path: a replaced container is worth re-running
|
|
@@ -717,16 +876,15 @@ export class InstallJob {
|
|
|
717
876
|
err: String(err)
|
|
718
877
|
});
|
|
719
878
|
const context = await this.#job.context();
|
|
720
|
-
await this.#job.write({
|
|
879
|
+
await this.#job.generation(startedAt).write({
|
|
721
880
|
state: "failed",
|
|
722
881
|
command: context?.command ?? "(unknown)",
|
|
723
882
|
finishedAt: Date.now(),
|
|
724
883
|
error: fault
|
|
725
884
|
? `the install stopped without reporting: ${fault.remedy}`
|
|
726
885
|
: "the install stopped without reporting — its container was most " +
|
|
727
|
-
"likely replaced. Re-run it with
|
|
886
|
+
"likely replaced. Re-run it with bash, or clone again to restart it."
|
|
728
887
|
});
|
|
729
|
-
await this.#job.clearWatch();
|
|
730
888
|
}
|
|
731
889
|
}
|
|
732
890
|
/**
|
|
@@ -734,24 +892,34 @@ export class InstallJob {
|
|
|
734
892
|
*
|
|
735
893
|
* `clearArmed` first and unconditionally: this handler runs for minutes, and
|
|
736
894
|
* the stamp left in place is what a second arming would recognise as its own.
|
|
737
|
-
*
|
|
895
|
+
* The reservation in `#beginInstall` would catch a double, but the cheaper
|
|
738
896
|
* answer is not to schedule one.
|
|
739
897
|
*/
|
|
740
898
|
async onRun() {
|
|
741
899
|
const context = await this.#job.context();
|
|
742
900
|
const armedAt = await this.#job.armedAt();
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
901
|
+
// Owned before the arm is cleared: from then until the reservation takes it
|
|
902
|
+
// over, the placeholder is a `running` record that only this call will finish.
|
|
903
|
+
if (armedAt !== undefined)
|
|
904
|
+
this.#owned.add(armedAt);
|
|
905
|
+
try {
|
|
906
|
+
await this.#job.clearArmed();
|
|
907
|
+
if (!context?.dir || armedAt === undefined)
|
|
908
|
+
return;
|
|
909
|
+
const state = await this.#awaited({
|
|
910
|
+
dir: context.dir,
|
|
911
|
+
...(context.repo ? { repo: context.repo } : {})
|
|
912
|
+
}, armedAt);
|
|
913
|
+
console.info(`[${this.deps.tag()}] armed reinstall finished`, {
|
|
914
|
+
id: this.deps.id(),
|
|
915
|
+
dir: context.dir,
|
|
916
|
+
state: state.state
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
finally {
|
|
920
|
+
if (armedAt !== undefined)
|
|
921
|
+
this.#owned.delete(armedAt);
|
|
922
|
+
}
|
|
755
923
|
}
|
|
756
924
|
/** The watchdog: an install still running that nobody is draining. */
|
|
757
925
|
async onWatch() {
|
|
@@ -760,7 +928,7 @@ export class InstallJob {
|
|
|
760
928
|
return;
|
|
761
929
|
// Still running and nobody draining it: this isolate is new since the
|
|
762
930
|
// command started. Re-attach, and come back if it is still going.
|
|
763
|
-
await this.#reattachInstall();
|
|
931
|
+
await this.#reattachInstall(state.startedAt);
|
|
764
932
|
await this.#job.armWatch();
|
|
765
933
|
}
|
|
766
934
|
}
|