@dynamicagents/plugins 0.13.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/dist/claude-code/cards.d.ts +2 -0
- package/dist/claude-code/cards.js +11 -0
- package/dist/claude-code/model.js +26 -2
- package/dist/claude-code/run.d.ts +21 -4
- package/dist/claude-code/run.js +51 -39
- package/dist/claude-code/session.js +4 -1
- package/dist/claude-code/verified.d.ts +1 -1
- package/dist/claude-code/verified.js +1 -1
- package/dist/workspace/container-deps.d.ts +4 -0
- package/dist/workspace/container-deps.js +26 -1
- package/dist/workspace/deps-snapshot.d.ts +110 -0
- package/dist/workspace/deps-snapshot.js +424 -0
- package/dist/workspace/index.d.ts +2 -1
- package/dist/workspace/install-job.d.ts +49 -0
- package/dist/workspace/install-job.js +75 -7
- package/dist/workspace/install.d.ts +9 -0
- package/dist/workspace/install.js +21 -3
- package/dist/workspace/object.d.ts +24 -4
- package/dist/workspace/object.js +130 -16
- package/dist/workspace/wake.d.ts +2 -0
- package/package.json +11 -11
|
@@ -50,4 +50,6 @@ export declare function thinkingCard(durationMs: number | undefined, messageId:
|
|
|
50
50
|
export declare function initCard(event: InitEvent): Card;
|
|
51
51
|
/** A session's end, with what its `result` line says about it. */
|
|
52
52
|
export declare function resultCard(result: ClaudeCodeResult): Card;
|
|
53
|
+
/** A session its run gave up on before reading its end, and why. */
|
|
54
|
+
export declare function lostCard(why: string): Card;
|
|
53
55
|
export {};
|
|
@@ -261,6 +261,17 @@ export function resultCard(result) {
|
|
|
261
261
|
}
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
|
+
/** A session its run gave up on before reading its end, and why. */
|
|
265
|
+
export function lostCard(why) {
|
|
266
|
+
return {
|
|
267
|
+
text: "Session ended before its run read it to the end",
|
|
268
|
+
detail: {
|
|
269
|
+
title: "Session",
|
|
270
|
+
status: "error",
|
|
271
|
+
sections: [section("Why", why, "text")]
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
264
275
|
function duration(ms) {
|
|
265
276
|
const s = Math.round(ms / 1000);
|
|
266
277
|
if (s < 60)
|
|
@@ -51,8 +51,9 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
53
|
import { isPlatformTransientError } from "agents";
|
|
54
|
+
import { lostCard } from "./cards.js";
|
|
54
55
|
import { claudeCodeSession, requireCredentials } from "./session.js";
|
|
55
|
-
import { execIdFor, followUpExecIdFor } from "./run.js";
|
|
56
|
+
import { AttachGivenUpError, execIdFor, followUpExecIdFor } from "./run.js";
|
|
56
57
|
/** Zeroed: the session's tokens are the CLI's context, not this conversation's. */
|
|
57
58
|
const USAGE = {
|
|
58
59
|
inputTokens: {
|
|
@@ -138,6 +139,25 @@ export function claudeCodeModel(options) {
|
|
|
138
139
|
},
|
|
139
140
|
...(signal ? { signal } : {})
|
|
140
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* The session's end card, filed where the run gives up on a session it will
|
|
144
|
+
* never read to its end: a re-attach given up on, or a cancel that stops it.
|
|
145
|
+
* Without it the transcript shows a session that never ended, and a retry's
|
|
146
|
+
* start reads as more of it.
|
|
147
|
+
*
|
|
148
|
+
* Not on any other throw: a transient one is retried by re-entering this
|
|
149
|
+
* turn, which reads on from the stored cursor. Keyed on the exec, so a card
|
|
150
|
+
* filed twice is filed once.
|
|
151
|
+
*/
|
|
152
|
+
const fileLost = async (execId, reason) => {
|
|
153
|
+
const card = lostCard(reason instanceof Error ? reason.message : String(reason));
|
|
154
|
+
await options
|
|
155
|
+
.note(`${execId}:lost`, card.text, card.detail)
|
|
156
|
+
.catch((err) => console.warn("[claude-code] a lost session's end was not filed", {
|
|
157
|
+
execId,
|
|
158
|
+
err: String(err)
|
|
159
|
+
}));
|
|
160
|
+
};
|
|
141
161
|
/**
|
|
142
162
|
* Drain one exec to its end, starting it or re-attaching from the cursor.
|
|
143
163
|
*
|
|
@@ -164,6 +184,8 @@ export function claudeCodeModel(options) {
|
|
|
164
184
|
const after = await storage.get(KEYS.cursor);
|
|
165
185
|
const moved = after?.execId === execId &&
|
|
166
186
|
(cursor?.execId !== execId || after.seq > cursor.seq);
|
|
187
|
+
if (err instanceof AttachGivenUpError)
|
|
188
|
+
await fileLost(execId, err);
|
|
167
189
|
if (!isStreamCut(err) || signal?.aborted || (stalled && !moved))
|
|
168
190
|
throw err;
|
|
169
191
|
stalled = !moved;
|
|
@@ -184,7 +206,9 @@ export function claudeCodeModel(options) {
|
|
|
184
206
|
runId,
|
|
185
207
|
err: String(err)
|
|
186
208
|
}));
|
|
187
|
-
|
|
209
|
+
const reason = signal?.reason ?? new Error("the session was stopped");
|
|
210
|
+
await fileLost(execId, reason);
|
|
211
|
+
throw reason;
|
|
188
212
|
}
|
|
189
213
|
return {
|
|
190
214
|
exitCode: outcome.exitCode,
|
|
@@ -403,6 +403,14 @@ export declare function isExecBusy(err: unknown): boolean;
|
|
|
403
403
|
* nothing to recover and retrying only discovers that again.
|
|
404
404
|
*/
|
|
405
405
|
export declare function isExecLost(err: unknown): boolean;
|
|
406
|
+
/**
|
|
407
|
+
* {@link attachRun} giving up on a subscriber that was never released. Unlike
|
|
408
|
+
* a transient failure, nothing retries it: the run fails, and its host stops
|
|
409
|
+
* the session.
|
|
410
|
+
*/
|
|
411
|
+
export declare class AttachGivenUpError extends Error {
|
|
412
|
+
name: string;
|
|
413
|
+
}
|
|
406
414
|
/**
|
|
407
415
|
* Start a session, detached.
|
|
408
416
|
*
|
|
@@ -426,8 +434,18 @@ export declare function startRun(runtime: SessionRuntime, options: LaunchOptions
|
|
|
426
434
|
/** Stops the fallback's wait — see {@link AttachOptions.signal}. */
|
|
427
435
|
signal?: AbortSignal;
|
|
428
436
|
}): Promise<WorkspaceRuntimeExecHandle<"utf8">>;
|
|
429
|
-
/** The clock and the wait, injectable so a spec can drive the bound. */
|
|
430
437
|
export interface AttachOptions {
|
|
438
|
+
/**
|
|
439
|
+
* How long to wait out a subscriber that has not been released: the
|
|
440
|
+
* session's own `timeoutMs`.
|
|
441
|
+
*
|
|
442
|
+
* Giving up fails the run, and a failed run is stopped by its host — so a
|
|
443
|
+
* shorter bound kills a healthy session, and past this one the session is
|
|
444
|
+
* over anyway. How long a release actually took is logged when the attach
|
|
445
|
+
* lands.
|
|
446
|
+
*/
|
|
447
|
+
maxWaitMs: number;
|
|
448
|
+
/** The clock and the wait, injectable so a spec can drive the bound. */
|
|
431
449
|
now?: () => number;
|
|
432
450
|
wait?: (ms: number) => Promise<void>;
|
|
433
451
|
/** Stops the wait — see `DrainOptions.signal`. */
|
|
@@ -447,10 +465,9 @@ export interface AttachOptions {
|
|
|
447
465
|
*
|
|
448
466
|
* Everything else is rethrown on the first look, `EEXEC_LOST` above all: it is
|
|
449
467
|
* the runtime saying the container was replaced, `resume` turns it into a report
|
|
450
|
-
* for the model, and a retry loop would sit on it
|
|
451
|
-
* learning nothing.
|
|
468
|
+
* for the model, and a retry loop would sit on it learning nothing.
|
|
452
469
|
*/
|
|
453
|
-
export declare function attachRun(runtime: SessionRuntime, cursor: DrainCursor, options
|
|
470
|
+
export declare function attachRun(runtime: SessionRuntime, cursor: DrainCursor, options: AttachOptions): Promise<WorkspaceRuntimeExecHandle<"utf8">>;
|
|
454
471
|
/**
|
|
455
472
|
* Drain a session to its end.
|
|
456
473
|
*
|
package/dist/claude-code/run.js
CHANGED
|
@@ -334,6 +334,10 @@ export function isExecLost(err) {
|
|
|
334
334
|
* caller can await to know the attachment is gone. {@link attachRun} answers by
|
|
335
335
|
* waiting rather than by failing.
|
|
336
336
|
*
|
|
337
|
+
* A workspace object reset by the platform leaves its dead connection's
|
|
338
|
+
* attachment held far longer: past thirty seconds, with the session mid-call
|
|
339
|
+
* and printing nothing.
|
|
340
|
+
*
|
|
337
341
|
* **Matched on the message, not on a code, and that is not laziness.** This one
|
|
338
342
|
* is raised inside the container image rather than by the runtime client: the
|
|
339
343
|
* string appears nowhere in the Worker bundle, and it arrives here as a plain
|
|
@@ -350,29 +354,18 @@ function isExecSubscribed(err) {
|
|
|
350
354
|
e.message.includes("already has a live subscriber"));
|
|
351
355
|
}
|
|
352
356
|
/**
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* isolate that is genuinely wedged holding the stream releases it when it dies,
|
|
357
|
-
* and nothing here can tell that apart from one that is a moment from
|
|
358
|
-
* finishing. Exceeding this rethrows, so the turn fails and Think's recovery
|
|
359
|
-
* retries it on a fresh isolate — which is the right escalation, and the one
|
|
360
|
-
* this whole mechanism exists to stop being the *first* resort.
|
|
361
|
-
*
|
|
362
|
-
* Sized well above what the race costs in practice. The race normally clears
|
|
363
|
-
* on the first or second look, and a deployment where it took fifteen seconds is
|
|
364
|
-
* what this is sized to outlast.
|
|
357
|
+
* {@link attachRun} giving up on a subscriber that was never released. Unlike
|
|
358
|
+
* a transient failure, nothing retries it: the run fails, and its host stops
|
|
359
|
+
* the session.
|
|
365
360
|
*/
|
|
366
|
-
|
|
361
|
+
export class AttachGivenUpError extends Error {
|
|
362
|
+
name = "AttachGivenUpError";
|
|
363
|
+
}
|
|
367
364
|
/** The first gap between attach attempts, doubled up to {@link ATTACH_CAP_MS}. */
|
|
368
365
|
const ATTACH_BACKOFF_MS = 250;
|
|
369
366
|
/**
|
|
370
|
-
* The ceiling on that doubling
|
|
371
|
-
*
|
|
372
|
-
* Without it the last gap before {@link ATTACH_MAX_MS} would be most of the
|
|
373
|
-
* budget, so a subscriber released early in it would still be waited out to the
|
|
374
|
-
* end. What this buys is that the wait ends soon after the release, rather than
|
|
375
|
-
* at the next power of two.
|
|
367
|
+
* The ceiling on that doubling, so the wait ends soon after the release rather
|
|
368
|
+
* than at the next power of two.
|
|
376
369
|
*/
|
|
377
370
|
const ATTACH_CAP_MS = 2_000;
|
|
378
371
|
/**
|
|
@@ -408,7 +401,8 @@ export async function startRun(runtime, options) {
|
|
|
408
401
|
throw err;
|
|
409
402
|
console.info("[claude-code] a session is already running under this id — attaching", { execId: options.execId });
|
|
410
403
|
return await attachRun(runtime, freshCursor(options.execId), {
|
|
411
|
-
signal: options.signal
|
|
404
|
+
signal: options.signal,
|
|
405
|
+
maxWaitMs: options.timeoutMs
|
|
412
406
|
});
|
|
413
407
|
}
|
|
414
408
|
}
|
|
@@ -426,17 +420,19 @@ export async function startRun(runtime, options) {
|
|
|
426
420
|
*
|
|
427
421
|
* Everything else is rethrown on the first look, `EEXEC_LOST` above all: it is
|
|
428
422
|
* the runtime saying the container was replaced, `resume` turns it into a report
|
|
429
|
-
* for the model, and a retry loop would sit on it
|
|
430
|
-
* learning nothing.
|
|
423
|
+
* for the model, and a retry loop would sit on it learning nothing.
|
|
431
424
|
*/
|
|
432
|
-
export async function attachRun(runtime, cursor, options
|
|
425
|
+
export async function attachRun(runtime, cursor, options) {
|
|
433
426
|
const now = options.now ?? Date.now;
|
|
434
427
|
const wait = options.wait ?? sleep;
|
|
435
|
-
const
|
|
428
|
+
const started = now();
|
|
429
|
+
const deadline = started + options.maxWaitMs;
|
|
436
430
|
let backoff = ATTACH_BACKOFF_MS;
|
|
431
|
+
let waited = false;
|
|
437
432
|
for (;;) {
|
|
433
|
+
let handle;
|
|
438
434
|
try {
|
|
439
|
-
|
|
435
|
+
handle = await runtime.getExec(cursor.execId, {
|
|
440
436
|
encoding: "utf8",
|
|
441
437
|
// `0` is a legal seq and also the beginning, so a fresh cursor resumes
|
|
442
438
|
// from the start either way. A stored cursor names its own place.
|
|
@@ -444,24 +440,40 @@ export async function attachRun(runtime, cursor, options = {}) {
|
|
|
444
440
|
});
|
|
445
441
|
}
|
|
446
442
|
catch (err) {
|
|
447
|
-
|
|
448
|
-
// fails on a subscriber that is never coming back — just not first.
|
|
449
|
-
if (!isExecSubscribed(err) ||
|
|
450
|
-
now() >= deadline ||
|
|
451
|
-
options.signal?.aborted)
|
|
443
|
+
if (!isExecSubscribed(err) || options.signal?.aborted)
|
|
452
444
|
throw err;
|
|
445
|
+
if (now() >= deadline) {
|
|
446
|
+
const waitedMs = now() - started;
|
|
447
|
+
console.warn("[claude-code] gave up waiting out the previous drain", {
|
|
448
|
+
execId: cursor.execId,
|
|
449
|
+
waitedMs
|
|
450
|
+
});
|
|
451
|
+
throw new AttachGivenUpError("claude-code: the session's stream to its container was cut, and the " +
|
|
452
|
+
"container held on to the old connection for the whole of the " +
|
|
453
|
+
"session's time limit, so it could not be re-attached. The cut is " +
|
|
454
|
+
"the platform's, not the session's; what the session did so far is " +
|
|
455
|
+
"in its workspace and on the transcript.", { cause: err });
|
|
456
|
+
}
|
|
457
|
+
// Logged once each way rather than per look: how long a release takes
|
|
458
|
+
// is the number worth reading, and over a session-long bound a line per
|
|
459
|
+
// look would bury it.
|
|
460
|
+
if (!waited) {
|
|
461
|
+
waited = true;
|
|
462
|
+
console.info("[claude-code] the previous drain is still attached — waiting", { execId: cursor.execId, maxWaitMs: options.maxWaitMs });
|
|
463
|
+
}
|
|
453
464
|
// Clamped to what is left, so the last gap lands *on* the deadline rather
|
|
454
|
-
// than past it
|
|
455
|
-
|
|
456
|
-
// would be a number in a comment rather than one the code keeps.
|
|
457
|
-
const waitMs = Math.min(backoff, deadline - now());
|
|
458
|
-
// One line per wait, not per attempt: this is the condition whose
|
|
459
|
-
// frequency is worth watching, and it went unnamed in the logs for as
|
|
460
|
-
// long as retries were absorbing it one at a time.
|
|
461
|
-
console.info("[claude-code] the previous drain is still attached — waiting", { execId: cursor.execId, waitMs });
|
|
462
|
-
await wait(waitMs);
|
|
465
|
+
// than past it, and the bound is one the code keeps.
|
|
466
|
+
await wait(Math.min(backoff, deadline - now()));
|
|
463
467
|
backoff = Math.min(backoff * 2, ATTACH_CAP_MS);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (waited) {
|
|
471
|
+
console.info("[claude-code] attached after waiting out the previous drain", {
|
|
472
|
+
execId: cursor.execId,
|
|
473
|
+
waitedMs: now() - started
|
|
474
|
+
});
|
|
464
475
|
}
|
|
476
|
+
return handle;
|
|
465
477
|
}
|
|
466
478
|
}
|
|
467
479
|
/**
|
|
@@ -186,7 +186,10 @@ export function claudeCodeSession(config) {
|
|
|
186
186
|
try {
|
|
187
187
|
let handle;
|
|
188
188
|
try {
|
|
189
|
-
handle = await attachRun(runtime, cursor, {
|
|
189
|
+
handle = await attachRun(runtime, cursor, {
|
|
190
|
+
signal: sinks.signal,
|
|
191
|
+
maxWaitMs: timeoutMs
|
|
192
|
+
});
|
|
190
193
|
}
|
|
191
194
|
catch (err) {
|
|
192
195
|
if (!isExecLost(err))
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type Workspace } from "@cloudflare/computer";
|
|
2
2
|
/** Written into the tree by a successful host install; see `./install-job.ts`. */
|
|
3
3
|
export declare const INSTALLED_MARKER = ".da-installed";
|
|
4
|
+
/** Every package root with a tree on this container's disk, one per line. */
|
|
5
|
+
export declare const DEPS_ROOTS_COMMAND = "for f in /var/lib/workspace-deps/*.root; do [ -f \"$f\" ] && { cat \"$f\"; echo; }; done; true";
|
|
6
|
+
/** {@link DEPS_ROOTS_COMMAND}'s output as paths, each once. */
|
|
7
|
+
export declare function parseDepsRoots(stdout: string): string[];
|
|
4
8
|
/**
|
|
5
9
|
* The per-container setup, and a read of the marker under `dir`.
|
|
6
10
|
*
|
|
@@ -65,6 +65,11 @@ import { deploymentFault } from "./container-fault.js";
|
|
|
65
65
|
* this installs ahead of the real binaries on `PATH`. Mounting earlier cannot
|
|
66
66
|
* work: a bootstrap that initialises submodules and installs each of them in one
|
|
67
67
|
* command creates the directories the mounts belong in.
|
|
68
|
+
*
|
|
69
|
+
* A container restored from a snapshot (see `./deps-snapshot.ts`) has the trees
|
|
70
|
+
* on its disk and none of the mounts, which are kernel state. So each mount
|
|
71
|
+
* records its package root beside its tree, and the setup mounts every recorded
|
|
72
|
+
* root again before it reads the marker through one.
|
|
68
73
|
*/
|
|
69
74
|
/** Where the trees live on the container's disk. */
|
|
70
75
|
const DEPS_DISK = "/var/lib/workspace-deps";
|
|
@@ -86,6 +91,23 @@ const WRAPPED = [
|
|
|
86
91
|
];
|
|
87
92
|
/** Written into the tree by a successful host install; see `./install-job.ts`. */
|
|
88
93
|
export const INSTALLED_MARKER = ".da-installed";
|
|
94
|
+
/**
|
|
95
|
+
* The package root a tree belongs to, written beside it as `<tree>.root`.
|
|
96
|
+
*
|
|
97
|
+
* Beside rather than inside: `npm ci` empties the tree it installs into, and the
|
|
98
|
+
* hash a tree is named by cannot be turned back into the path.
|
|
99
|
+
*/
|
|
100
|
+
const ROOT_SUFFIX = ".root";
|
|
101
|
+
/** Every package root with a tree on this container's disk, one per line. */
|
|
102
|
+
export const DEPS_ROOTS_COMMAND = `for f in ${DEPS_DISK}/*${ROOT_SUFFIX}; do [ -f "$f" ] && { cat "$f"; echo; }; done; true`;
|
|
103
|
+
/** {@link DEPS_ROOTS_COMMAND}'s output as paths, each once. */
|
|
104
|
+
export function parseDepsRoots(stdout) {
|
|
105
|
+
const roots = stdout
|
|
106
|
+
.split("\n")
|
|
107
|
+
.map((line) => line.trim())
|
|
108
|
+
.filter((line) => line.startsWith("/"));
|
|
109
|
+
return [...new Set(roots)];
|
|
110
|
+
}
|
|
89
111
|
/**
|
|
90
112
|
* The per-container setup, and a read of the marker under `dir`.
|
|
91
113
|
*
|
|
@@ -118,7 +140,7 @@ export function depsSetupCommand(workspaceDir, dir) {
|
|
|
118
140
|
'nm="$dir/node_modules"',
|
|
119
141
|
'mountpoint -q "$nm" && exit 0',
|
|
120
142
|
`disk="${DEPS_DISK}/$(printf %s "$dir" | sha256sum | cut -c1-16)"`,
|
|
121
|
-
|
|
143
|
+
`mkdir -p "$disk" "$nm" && mount --bind "$disk" "$nm" && { printf %s "$dir" > "$disk${ROOT_SUFFIX}"; exit 0; }`,
|
|
122
144
|
`echo "workspace-deps: could not put $nm on the container's disk" >&2`,
|
|
123
145
|
"exit 1"
|
|
124
146
|
].join("\n");
|
|
@@ -139,6 +161,9 @@ export function depsSetupCommand(workspaceDir, dir) {
|
|
|
139
161
|
`install -m 755 /tmp/workspace-deps-mount ${MOUNT_HELPER} || exit 1`,
|
|
140
162
|
`printf '%s\\n' ${shellQuote(wrapper)} > /tmp/workspace-deps-wrap`,
|
|
141
163
|
`for tool in ${WRAPPED.join(" ")}; do install -m 755 /tmp/workspace-deps-wrap "${WRAPPER_DIR}/$tool" || exit 1; done`,
|
|
164
|
+
// A root that is not in this checkout is left alone by the helper, and one
|
|
165
|
+
// that cannot be mounted reads as no marker below — an install, not a fault.
|
|
166
|
+
`for f in ${DEPS_DISK}/*${ROOT_SUFFIX}; do [ -f "$f" ] && ${MOUNT_HELPER} "$(cat "$f")"; done`,
|
|
142
167
|
...(dir
|
|
143
168
|
? [
|
|
144
169
|
`echo "MARKER $(cat ${shellQuote(`${dir}/node_modules/${INSTALLED_MARKER}`)} 2>/dev/null || echo -)"`
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Workspace } from "@cloudflare/computer";
|
|
2
|
+
export interface DepsSnapshotRecord {
|
|
3
|
+
/** The platform's snapshot id. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** In bytes, as the platform reported it. */
|
|
6
|
+
size: number;
|
|
7
|
+
/** The image reference it was taken on — see the file comment. */
|
|
8
|
+
image: string;
|
|
9
|
+
/** Where the install ran. */
|
|
10
|
+
dir: string;
|
|
11
|
+
/** The install fingerprint its marker holds. */
|
|
12
|
+
fingerprint: string;
|
|
13
|
+
/** Every package root with a tree on the disk, and its inputs' digest then. */
|
|
14
|
+
roots: {
|
|
15
|
+
dir: string;
|
|
16
|
+
fingerprint: string | null;
|
|
17
|
+
}[];
|
|
18
|
+
/** {@link packageRootSet} of the checkout then. */
|
|
19
|
+
rootSet: string;
|
|
20
|
+
at: number;
|
|
21
|
+
}
|
|
22
|
+
/** What a restore is checked against: the install a cold start would run. */
|
|
23
|
+
export interface ExpectedTree {
|
|
24
|
+
dir: string;
|
|
25
|
+
fingerprint: string;
|
|
26
|
+
}
|
|
27
|
+
/** A tree an install finished, as its record names it — `at` tells two apart. */
|
|
28
|
+
export interface InstalledTree extends ExpectedTree {
|
|
29
|
+
at: number;
|
|
30
|
+
}
|
|
31
|
+
export interface DepsSnapshotDeps {
|
|
32
|
+
storage: DurableObjectStorage;
|
|
33
|
+
container: () => Container | undefined;
|
|
34
|
+
/** The image a start from no snapshot would boot. */
|
|
35
|
+
image: () => string | undefined;
|
|
36
|
+
workspace: () => Workspace;
|
|
37
|
+
rootFingerprint: (dir: string) => Promise<string | null>;
|
|
38
|
+
/** The tree the install record says this container holds. */
|
|
39
|
+
tree: () => Promise<{
|
|
40
|
+
dir: string;
|
|
41
|
+
fingerprint: string | null;
|
|
42
|
+
at: number;
|
|
43
|
+
} | undefined>;
|
|
44
|
+
tag: () => string;
|
|
45
|
+
id: () => string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Which package roots a checkout has, as one digest: every directory under
|
|
49
|
+
* `dir` with a `package.json`, and the submodules `.gitmodules` declares — one a
|
|
50
|
+
* bootstrap clones in the container has no `package.json` here until then.
|
|
51
|
+
*/
|
|
52
|
+
export declare function packageRootSet(fs: Workspace["fs"], dir: string): Promise<string>;
|
|
53
|
+
export declare class DepsSnapshot {
|
|
54
|
+
#private;
|
|
55
|
+
private readonly deps;
|
|
56
|
+
readonly launch: {
|
|
57
|
+
readonly containerSnapshot: ContainerSnapshotRestoreParams | undefined;
|
|
58
|
+
};
|
|
59
|
+
constructor(deps: DepsSnapshotDeps);
|
|
60
|
+
/**
|
|
61
|
+
* Restore with the next start if the record fits it, and say which record.
|
|
62
|
+
*
|
|
63
|
+
* `expected` is a thunk because it reads the checkout, and a workspace with no
|
|
64
|
+
* snapshot or one from another image should not pay for that.
|
|
65
|
+
*/
|
|
66
|
+
arm(expected: () => Promise<ExpectedTree | undefined>): Promise<DepsSnapshotRecord | undefined>;
|
|
67
|
+
/** Forget an armed restore, taken or not. */
|
|
68
|
+
disarm(): void;
|
|
69
|
+
/**
|
|
70
|
+
* Whether the container that came up holds `record`'s tree, judged by the
|
|
71
|
+
* marker the setup read in it.
|
|
72
|
+
*
|
|
73
|
+
* Drops the record when a start took it and the tree is not there; leaves it
|
|
74
|
+
* when no start did, since nothing was learned about it.
|
|
75
|
+
*/
|
|
76
|
+
restored(record: DepsSnapshotRecord, marker: string | null | undefined): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Snapshot the running container, which has just installed `tree`, after any
|
|
79
|
+
* snapshot already in flight — a later install's request is never dropped for
|
|
80
|
+
* an earlier one's.
|
|
81
|
+
*
|
|
82
|
+
* Never throws: a container that cannot be snapshotted installs again next
|
|
83
|
+
* time, which is today's cost and no worse.
|
|
84
|
+
*/
|
|
85
|
+
take(tree: InstalledTree): Promise<void>;
|
|
86
|
+
/** Snapshots in flight, finished — for whatever is about to stop the container. */
|
|
87
|
+
settled(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* The entries that ask for `tree`'s snapshot, for the install to write with
|
|
90
|
+
* its verdict — see the file comment, and {@link takeDue}.
|
|
91
|
+
*/
|
|
92
|
+
dueEntries(tree: InstalledTree): Record<string, unknown>;
|
|
93
|
+
/** Take the snapshot an install asked for, if one is waiting. */
|
|
94
|
+
takeDue(): Promise<void>;
|
|
95
|
+
/** The container went, and the disk an asked-for snapshot was of with it. */
|
|
96
|
+
forgetDue(): Promise<void>;
|
|
97
|
+
get(): Promise<DepsSnapshotRecord | undefined>;
|
|
98
|
+
/**
|
|
99
|
+
* Start from another workspace's snapshot — one taken for the same checkout
|
|
100
|
+
* path on the same image, as a worktree shares with the checkout it was cut
|
|
101
|
+
* from.
|
|
102
|
+
*
|
|
103
|
+
* Kept out when this workspace's own snapshot already fits, and refused when
|
|
104
|
+
* the offered one does not.
|
|
105
|
+
*/
|
|
106
|
+
seed(record: DepsSnapshotRecord, expected: () => Promise<ExpectedTree | undefined>): Promise<{
|
|
107
|
+
seeded: boolean;
|
|
108
|
+
reason?: string;
|
|
109
|
+
}>;
|
|
110
|
+
}
|