@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.
Files changed (132) hide show
  1. package/README.md +58 -77
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +55 -0
  5. package/dist/claude-code/cards.js +283 -0
  6. package/dist/claude-code/config.d.ts +28 -63
  7. package/dist/claude-code/config.js +0 -11
  8. package/dist/claude-code/egress.d.ts +8 -1
  9. package/dist/claude-code/egress.js +42 -4
  10. package/dist/claude-code/events.d.ts +106 -9
  11. package/dist/claude-code/events.js +201 -56
  12. package/dist/claude-code/github.d.ts +39 -0
  13. package/dist/claude-code/github.js +82 -0
  14. package/dist/claude-code/index.d.ts +16 -90
  15. package/dist/claude-code/index.js +14 -276
  16. package/dist/claude-code/model.d.ts +152 -0
  17. package/dist/claude-code/model.js +366 -0
  18. package/dist/claude-code/run.d.ts +230 -116
  19. package/dist/claude-code/run.js +380 -167
  20. package/dist/claude-code/session.d.ts +131 -0
  21. package/dist/claude-code/session.js +288 -0
  22. package/dist/claude-code/spec.d.ts +46 -0
  23. package/dist/claude-code/spec.js +83 -0
  24. package/dist/claude-code/verified.d.ts +15 -0
  25. package/dist/claude-code/verified.js +15 -0
  26. package/dist/computer/context.d.ts +1 -5
  27. package/dist/computer/context.js +7 -10
  28. package/dist/computer/file-lock.js +5 -4
  29. package/dist/computer/gate.d.ts +4 -4
  30. package/dist/computer/gate.js +5 -5
  31. package/dist/computer/index.d.ts +40 -258
  32. package/dist/computer/index.js +71 -324
  33. package/dist/computer/paths.d.ts +36 -16
  34. package/dist/computer/paths.js +42 -29
  35. package/dist/computer/proxy.d.ts +45 -0
  36. package/dist/computer/proxy.js +234 -0
  37. package/dist/computer/render.d.ts +1 -40
  38. package/dist/computer/render.js +2 -68
  39. package/dist/computer/tools-exec.js +12 -13
  40. package/dist/computer/tools-file.d.ts +9 -1
  41. package/dist/computer/tools-file.js +22 -82
  42. package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
  43. package/dist/computer/tools-grep.js +129 -0
  44. package/dist/repo/checkout.d.ts +4 -5
  45. package/dist/repo/checkout.js +31 -8
  46. package/dist/repo/context.d.ts +35 -9
  47. package/dist/repo/context.js +101 -49
  48. package/dist/repo/index.d.ts +73 -28
  49. package/dist/repo/index.js +112 -96
  50. package/dist/repo/tools-checks.d.ts +3 -0
  51. package/dist/repo/tools-checks.js +112 -0
  52. package/dist/repo/tools-clone.js +1 -1
  53. package/dist/repo/tools-pr.d.ts +14 -0
  54. package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
  55. package/dist/repo/tools-review.d.ts +9 -0
  56. package/dist/repo/tools-review.js +67 -47
  57. package/dist/repo/tools-worktree.js +144 -60
  58. package/dist/repo/tools-worktrees.d.ts +12 -0
  59. package/dist/repo/tools-worktrees.js +48 -0
  60. package/dist/repo/url.d.ts +2 -2
  61. package/dist/repo/url.js +7 -7
  62. package/dist/scratch/index.d.ts +4 -4
  63. package/dist/scratch/index.js +12 -11
  64. package/dist/{computer → workspace}/advisory.d.ts +4 -4
  65. package/dist/{computer → workspace}/advisory.js +2 -2
  66. package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
  67. package/dist/{computer/host → workspace}/ca-trust.js +12 -3
  68. package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
  69. package/dist/{computer/host → workspace}/container-deps.js +26 -1
  70. package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
  71. package/dist/{computer/host → workspace}/container-fault.js +23 -8
  72. package/dist/workspace/deps-snapshot.d.ts +110 -0
  73. package/dist/workspace/deps-snapshot.js +424 -0
  74. package/dist/workspace/exec.d.ts +94 -0
  75. package/dist/workspace/exec.js +153 -0
  76. package/dist/workspace/format.d.ts +36 -0
  77. package/dist/workspace/format.js +57 -0
  78. package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
  79. package/dist/{computer/host → workspace}/git-host.js +72 -2
  80. package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
  81. package/dist/{computer/host → workspace}/git-identity.js +1 -1
  82. package/dist/workspace/index.d.ts +44 -47
  83. package/dist/workspace/index.js +52 -112
  84. package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
  85. package/dist/{computer/host → workspace}/install-job.js +346 -178
  86. package/dist/{computer → workspace}/install.d.ts +17 -8
  87. package/dist/{computer → workspace}/install.js +24 -6
  88. package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
  89. package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
  90. package/dist/workspace/open.d.ts +95 -0
  91. package/dist/workspace/open.js +63 -0
  92. package/dist/workspace/read.d.ts +16 -0
  93. package/dist/workspace/read.js +22 -0
  94. package/dist/{computer → workspace}/shell.d.ts +5 -5
  95. package/dist/{computer → workspace}/shell.js +6 -6
  96. package/dist/{computer/host → workspace}/wake.d.ts +3 -1
  97. package/package.json +17 -37
  98. package/dist/arc-agi/analysis.d.ts +0 -322
  99. package/dist/arc-agi/analysis.js +0 -802
  100. package/dist/arc-agi/client.d.ts +0 -82
  101. package/dist/arc-agi/client.js +0 -113
  102. package/dist/arc-agi/game-tools.d.ts +0 -24
  103. package/dist/arc-agi/game-tools.js +0 -26
  104. package/dist/arc-agi/index.d.ts +0 -40
  105. package/dist/arc-agi/index.js +0 -175
  106. package/dist/arc-agi/main-agent.d.ts +0 -24
  107. package/dist/arc-agi/main-agent.js +0 -62
  108. package/dist/arc-agi/recipe.d.ts +0 -48
  109. package/dist/arc-agi/recipe.js +0 -97
  110. package/dist/arc-agi/scorecard.d.ts +0 -118
  111. package/dist/arc-agi/scorecard.js +0 -163
  112. package/dist/arc-agi/soul.d.ts +0 -29
  113. package/dist/arc-agi/soul.js +0 -79
  114. package/dist/arc-agi/store.d.ts +0 -153
  115. package/dist/arc-agi/store.js +0 -215
  116. package/dist/arc-agi/tools.d.ts +0 -36
  117. package/dist/arc-agi/tools.js +0 -574
  118. package/dist/arc-agi/types.d.ts +0 -211
  119. package/dist/arc-agi/types.js +0 -19
  120. package/dist/claude-code/recipe.d.ts +0 -53
  121. package/dist/claude-code/recipe.js +0 -100
  122. package/dist/computer/read.d.ts +0 -133
  123. package/dist/computer/read.js +0 -200
  124. package/dist/computer/tools-find.js +0 -216
  125. package/dist/recall/index.d.ts +0 -105
  126. package/dist/recall/index.js +0 -181
  127. package/dist/repo/tools-forge.d.ts +0 -4
  128. package/dist/triage/index.d.ts +0 -360
  129. package/dist/triage/index.js +0 -311
  130. /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
  131. /package/dist/{computer/host → workspace}/sync.js +0 -0
  132. /package/dist/{computer/host → workspace}/wake.js +0 -0
package/dist/repo/url.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * naming it": every pattern here turns away a string that resolves to something
7
7
  * other than what it appears to name.
8
8
  */
9
- /** The forges a clone URL may name. `RepoConfig.allowedHosts` overrides it. */
9
+ /** The hosts a clone URL may name. `RepoConfig.allowedHosts` overrides it. */
10
10
  export const DEFAULT_ALLOWED_HOSTS = ["github.com"];
11
11
  /** Branch names a push must never target, whatever the model believes. */
12
12
  export const PROTECTED_BRANCHES = new Set([
@@ -88,7 +88,7 @@ export function sameRepoUrl(a, b) {
88
88
  return canonical(a) === canonical(b);
89
89
  }
90
90
  /**
91
- * A name a forge would actually issue — and one that is safe to use as a path
91
+ * A name GitHub would actually issue — and one that is safe to use as a path
92
92
  * segment and as a key.
93
93
  *
94
94
  * GitHub's rule for an owner and a repository alike is alphanumerics, `-`, `_`
@@ -101,17 +101,17 @@ export function sameRepoUrl(a, b) {
101
101
  * is a separator in somebody else's key format, which is how two callers end up
102
102
  * sharing one workspace.
103
103
  */
104
- const FORGE_NAME = /^[A-Za-z0-9._-]+$/;
105
- function isForgeName(name) {
104
+ const GITHUB_NAME = /^[A-Za-z0-9._-]+$/;
105
+ function isGitHubName(name) {
106
106
  // `.` and `..` pass the character class and are exactly the two that must not.
107
- return name !== "." && name !== ".." && FORGE_NAME.test(name);
107
+ return name !== "." && name !== ".." && GITHUB_NAME.test(name);
108
108
  }
109
109
  /**
110
110
  * `owner/repo` out of a repository URL, or `undefined` if it is not one.
111
111
  *
112
112
  * Anchored on the **parsed host** rather than matched in the string, so
113
113
  * `https://evil.example.com/github.com/owner/repo` is not a github.com URL.
114
- * Both names are then checked against {@link isForgeName}, so a name that could
114
+ * Both names are then checked against {@link isGitHubName}, so a name that could
115
115
  * traverse a path or split somebody else's key never gets as far as being one.
116
116
  *
117
117
  * `undefined` is a refusal at every caller, never a reason to carry on with a
@@ -125,7 +125,7 @@ export function parseRepo(url, allowedHosts = DEFAULT_ALLOWED_HOSTS) {
125
125
  if (!match)
126
126
  return undefined;
127
127
  const [, owner, repo] = match;
128
- if (!isForgeName(owner) || !isForgeName(repo))
128
+ if (!isGitHubName(owner) || !isGitHubName(repo))
129
129
  return undefined;
130
130
  return { owner, repo };
131
131
  }
@@ -42,7 +42,7 @@ import type { AgentPlugin } from "@dynamicagents/core";
42
42
  /** Where a scratchpad lives, unless the host says otherwise. */
43
43
  export declare const DEFAULT_SCRATCH_DIR = "/workspace/scratch";
44
44
  /**
45
- * Run one command in the host's container. Matches `computerExec`'s shape.
45
+ * Run one command in the host's container. Matches `workspaceExec`'s shape.
46
46
  *
47
47
  * Injected rather than imported, like `/repo`'s: `npm run verify:exports` fails
48
48
  * any subpath whose module graph reaches a sibling's, and a scratchpad that
@@ -54,7 +54,7 @@ export type ScratchExec = (command: string, options?: {
54
54
  env?: Record<string, string | undefined>;
55
55
  timeout?: number;
56
56
  /**
57
- * The executing subtask's runtime state, forwarded **opaquely** — the same
57
+ * The running sub-agent's `runtime()`, forwarded **opaquely** — the same
58
58
  * pass-through `/repo` does, for the same reason. This plugin never looks
59
59
  * inside it.
60
60
  */
@@ -96,7 +96,7 @@ export interface ScratchConfig {
96
96
  * Runs commands in the container holding the workspace.
97
97
  *
98
98
  * Uncredentialed, and nothing here ever needs a credential: a scratchpad has
99
- * no remote, so no operation in this plugin talks to a forge.
99
+ * no remote, so no operation in this plugin talks to GitHub.
100
100
  */
101
101
  exec: ScratchExec;
102
102
  /** Where the scratchpad lives. Defaults to {@link DEFAULT_SCRATCH_DIR}. */
@@ -153,6 +153,6 @@ export interface ScratchConfig {
153
153
  * carries the same copy for the same reason.
154
154
  */
155
155
  export declare function truncateOutput(text: string, max: number): string;
156
- /** The tool name, exported so a host restricting its main agent can name it. */
156
+ /** The tool name, exported so a host restricting the plugin can name it. */
157
157
  export declare const SCRATCH_OPEN_TOOL = "scratch_open";
158
158
  export declare function scratch(config: ScratchConfig): AgentPlugin;
@@ -68,10 +68,10 @@ export function truncateOutput(text, max) {
68
68
  return text.slice(0, Math.max(0, max));
69
69
  return (text.slice(0, half) + marker(text.length - half * 2) + text.slice(-half));
70
70
  }
71
- /** The tool name, exported so a host restricting its main agent can name it. */
71
+ /** The tool name, exported so a host restricting the plugin can name it. */
72
72
  export const SCRATCH_OPEN_TOOL = "scratch_open";
73
73
  /**
74
- * What the delegating agent is told.
74
+ * What the model is told.
75
75
  *
76
76
  * The mixing rule at the end is the line worth its tokens. A host that keys one
77
77
  * workspace selection per caller — which is what makes a scratchpad routable at
@@ -294,19 +294,20 @@ export function scratch(config) {
294
294
  .filter(Boolean)
295
295
  .join(" "), maxChars);
296
296
  };
297
+ const context = capabilityFor(dir);
297
298
  return definePlugin({
298
- key: "scratch",
299
+ name: "scratch",
299
300
  /**
300
- * The main agent's, and no tool family — which is a decision rather than an
301
- * omission.
301
+ * For a parent, never a sub-agent — which the host decides by installing it
302
+ * on one and not the other.
302
303
  *
303
304
  * Opening a scratchpad *selects a workspace*, exactly as `repo_clone` does.
304
- * A subagent holding this could re-point the workspace its parent prepared
305
+ * A sub-agent holding this could re-point the workspace its parent prepared
305
306
  * half-way through its own run, which is the hazard a host's per-caller
306
- * selection already has to document. A delegated run works in whatever it
307
- * was given.
307
+ * selection already has to document. A sub-agent works in whatever it was
308
+ * given.
308
309
  */
309
- mainAgentTools: () => ({
310
+ tools: (ctx) => ({
310
311
  [SCRATCH_OPEN_TOOL]: tool({
311
312
  description: "Open a scratchpad: a git repository with no remote, in your container, " +
312
313
  "for work that does not need a cloned repository. Use it before " +
@@ -318,9 +319,9 @@ export function scratch(config) {
318
319
  .optional()
319
320
  .describe("Discard everything in the scratchpad first, including files an earlier task left")
320
321
  }),
321
- execute: ({ reset }) => open(reset, undefined)
322
+ execute: ({ reset }) => open(reset, ctx.runtime())
322
323
  })
323
324
  }),
324
- capability: capabilityFor(dir)
325
+ context: [{ provider: { get: async () => context } }]
325
326
  });
326
327
  }
@@ -17,9 +17,9 @@ import type { InstallState } from "./install.js";
17
17
  * A ceiling is a hard wall about the Durable Object, and the two are then told
18
18
  * apart only by reading prose.
19
19
  * - Capacity must not travel through a channel gated by
20
- * {@link file://./gate.ts needsDependencies}, or a full workspace says nothing
20
+ * {@link file://../computer/gate.ts needsDependencies}, or a full workspace says nothing
21
21
  * at all to `echo hi > file.txt` — the command whose write is being lost.
22
- * - "The tree is fine now", after a subagent installed by hand, has no honest
22
+ * - "The tree is fine now", after an agent installed by hand, has no honest
23
23
  * spelling as a job record: fabricating a `done` rests on the presence of a
24
24
  * `node_modules` directory, which the wreckage of the very install being
25
25
  * overridden already satisfies.
@@ -56,7 +56,7 @@ export type WorkspaceAdvisory = {
56
56
  * Reported rather than acted on, because its presence proves nothing in
57
57
  * either direction. A half-finished `npm ci` leaves the directory behind
58
58
  * with an incomplete tree, so treating existence as success hides the
59
- * common failure outright; and a subagent that re-ran the install by hand
59
+ * common failure outright; and an agent that re-ran the install by hand
60
60
  * leaves a record the host cannot update, since the host cannot see an
61
61
  * install it did not start. Both look identical from here, so the reader
62
62
  * is told which way the ambiguity runs rather than being handed a guess.
@@ -181,7 +181,7 @@ export declare function deriveAdvisories(input: AdvisoryInput): readonly Workspa
181
181
  /**
182
182
  * Everything a session should know before it starts, or `undefined` for silence.
183
183
  *
184
- * The counterpart to {@link file://./gate.ts execGate}, for the reader that is a
184
+ * The counterpart to {@link file://../computer/gate.ts execGate}, for the reader that is a
185
185
  * whole run rather than one command. It cannot be blocked and does not need to
186
186
  * be: it can install dependencies itself, and the only thing it must not do is
187
187
  * work silently against a workspace that is not what it assumes.
@@ -1,4 +1,4 @@
1
- import { humanMs } from "./render.js";
1
+ import { humanMs } from "./format.js";
2
2
  /** What each advisory *means*. The one exhaustive home for policy. */
3
3
  export function shapeOf(advisory) {
4
4
  switch (advisory.kind) {
@@ -188,7 +188,7 @@ export function deriveAdvisories(input) {
188
188
  /**
189
189
  * Everything a session should know before it starts, or `undefined` for silence.
190
190
  *
191
- * The counterpart to {@link file://./gate.ts execGate}, for the reader that is a
191
+ * The counterpart to {@link file://../computer/gate.ts execGate}, for the reader that is a
192
192
  * whole run rather than one command. It cannot be blocked and does not need to
193
193
  * be: it can install dependencies itself, and the only thing it must not do is
194
194
  * work silently against a workspace that is not what it assumes.
@@ -70,7 +70,14 @@ export declare class ContainerTrust {
70
70
  #private;
71
71
  private readonly deps;
72
72
  constructor(deps: ContainerTrustDeps);
73
- /** Forget that any container was trusted. */
73
+ /**
74
+ * Forget that any container was trusted.
75
+ *
76
+ * Drops the standing watch too, because this is also reached for a container
77
+ * that keeps running — a `running` read, a lost exec. The watch it leaves
78
+ * behind ignores that container's real exit, so without a fresh one nothing
79
+ * would clear the flag again and the replacement would be vouched for.
80
+ */
74
81
  forget(): void;
75
82
  /**
76
83
  * Trust the CA if this container has not been trusted yet.
@@ -127,7 +127,7 @@ export class ContainerTrust {
127
127
  #trusted = false;
128
128
  /** Whether an exit watch is standing for the container now running. */
129
129
  #watching = false;
130
- /** Bumped by {@link forget}, so a watch settling after it is ignored. */
130
+ /** Bumped by {@link forget}, so a watch armed before it is ignored. */
131
131
  #generation = 0;
132
132
  /**
133
133
  * Whether this container's deployment fault has already been reported.
@@ -141,10 +141,18 @@ export class ContainerTrust {
141
141
  constructor(deps) {
142
142
  this.deps = deps;
143
143
  }
144
- /** Forget that any container was trusted. */
144
+ /**
145
+ * Forget that any container was trusted.
146
+ *
147
+ * Drops the standing watch too, because this is also reached for a container
148
+ * that keeps running — a `running` read, a lost exec. The watch it leaves
149
+ * behind ignores that container's real exit, so without a fresh one nothing
150
+ * would clear the flag again and the replacement would be vouched for.
151
+ */
145
152
  forget() {
146
153
  this.#generation += 1;
147
154
  this.#trusted = false;
155
+ this.#watching = false;
148
156
  this.#faultReported = false;
149
157
  }
150
158
  /**
@@ -171,8 +179,9 @@ export class ContainerTrust {
171
179
  return;
172
180
  const generation = this.#generation;
173
181
  try {
182
+ // A stale watch returns before touching anything: `#watching` belongs to
183
+ // whichever watch was armed since.
174
184
  const done = () => {
175
- this.#watching = false;
176
185
  if (generation !== this.#generation)
177
186
  return;
178
187
  this.forget();
@@ -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
- 'mkdir -p "$disk" "$nm" && mount --bind "$disk" "$nm" && exit 0',
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 -)"`
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Everything else this host catches is the kind of thing a retry clears: a
5
5
  * container that is starting, a connection that dropped, a command that died
6
- * with it. These two are not. They describe a Worker and a container
7
- * application that cannot work together no matter how many times they are
8
- * asked, and they end only when an operator deploys.
6
+ * with it. These are not. They describe a Worker and a container application
7
+ * that cannot work together no matter how many times they are asked, and they
8
+ * end only when an operator deploys.
9
9
  *
10
10
  * **This changes what is said, not what is done.** No caller branches its
11
11
  * control flow on the answer, and none should start: the match is on a string
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Everything else this host catches is the kind of thing a retry clears: a
5
5
  * container that is starting, a connection that dropped, a command that died
6
- * with it. These two are not. They describe a Worker and a container
7
- * application that cannot work together no matter how many times they are
8
- * asked, and they end only when an operator deploys.
6
+ * with it. These are not. They describe a Worker and a container application
7
+ * that cannot work together no matter how many times they are asked, and they
8
+ * end only when an operator deploys.
9
9
  *
10
10
  * **This changes what is said, not what is done.** No caller branches its
11
11
  * control flow on the answer, and none should start: the match is on a string
@@ -21,14 +21,18 @@
21
21
  /**
22
22
  * Match on the shape the backend formats, not on the prose after it.
23
23
  *
24
- * `[stage=auth]` is the stage label `CloudflareContainerBackend` writes into
25
- * every message it throws from that stage, and the container-application
26
- * sentence is the runtime's own, surfaced verbatim through the backend's
27
- * `priorExit=` field. Both are stabler than the explanations they introduce, and
28
- * neither has a `code` to key on the way a lost exec does.
24
+ * `[stage=auth]` is the stage label `ContainerBackend` writes into every
25
+ * message it throws from that stage, and the container-application sentence is
26
+ * the runtime's own, surfaced verbatim through the backend's `priorExit=` field.
27
+ * `prepared images` is in both of the backend's refusals to start without an
28
+ * image — none prepared at all, and none under the key it was asked for — and
29
+ * survives the reconnect wrapper's truncation of either. All are stabler than
30
+ * the explanations they introduce, and none has a `code` to key on the way a
31
+ * lost exec does.
29
32
  */
30
33
  const AUTH_STAGE = "[stage=auth]";
31
34
  const NO_APPLICATION = "no container application";
35
+ const NO_IMAGE = "prepared images";
32
36
  /**
33
37
  * Whether this error says the deployment is wrong, and what to say about it.
34
38
  *
@@ -54,5 +58,16 @@ export function deploymentFault(err) {
54
58
  "creates it — deleting it does not cause it to be rebuilt — so this " +
55
59
  "clears when an operator redeploys, and not before."
56
60
  };
61
+ if (text.includes(NO_IMAGE))
62
+ return {
63
+ summary: "this Worker prepared no image for the workspace container",
64
+ remedy: "the container application has no image under the name this workspace " +
65
+ "starts, so no container can start. The workspace schedules its own " +
66
+ 'containers, which takes `scheduling_policy: "durable_object"` and an ' +
67
+ "`images` entry for that name in the Worker's containers block, and an " +
68
+ "application created under another policy is deleted before that " +
69
+ "deploy rather than switched. This clears when an operator redeploys " +
70
+ "with both in place, and not before."
71
+ };
57
72
  return undefined;
58
73
  }
@@ -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
+ }