@coreplane/switchboard 1.267.2 → 1.268.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 (52) hide show
  1. package/dist/assets/deploy/cloudflare-memory/testDiagnostics.ts +44 -3
  2. package/dist/assets/deploy/cloudflare-memory/testDiagnosticsProtocol.ts +7 -0
  3. package/dist/assets/deploy/cloudflare-memory/testDiagnosticsReporter.ts +22 -0
  4. package/dist/assets/deploy/cloudflare-memory/testFetch.ts +21 -0
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +66 -1
  6. package/dist/assets/deploy/cloudflare-resident/imageReconcileState.ts +28 -0
  7. package/dist/assets/deploy/cloudflare-resident/runRegistration.ts +68 -0
  8. package/dist/assets/deploy/cloudflare-resident/shared.ts +2 -3
  9. package/dist/assets/deploy/cloudflare-resident/worker.ts +541 -192
  10. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +17 -2
  11. package/dist/assets/package-lock.json +3 -3
  12. package/dist/assets/package.json +1 -1
  13. package/dist/assets/source.json +3 -3
  14. package/dist/assets/src/agents/registry.ts +2 -2
  15. package/dist/assets/src/core/budgets.ts +6 -0
  16. package/dist/assets/src/core/coordinator/contract.ts +14 -8
  17. package/dist/assets/src/core/costs.ts +2 -0
  18. package/dist/assets/src/core/runEvents.ts +18 -1
  19. package/dist/assets/src/core/runLedger/types.ts +2 -0
  20. package/dist/assets/src/core/runRecord.ts +27 -1
  21. package/dist/assets/src/core/runUsage.ts +53 -0
  22. package/dist/assets/src/core/ship/coordinator.ts +4 -1
  23. package/dist/assets/src/core/shipSpend.ts +52 -0
  24. package/dist/assets/src/core/trace/attrs.ts +22 -0
  25. package/dist/assets/src/core/trace/tracer.ts +10 -2
  26. package/dist/assets/src/core/trace/types.ts +12 -1
  27. package/dist/assets/src/core/trace/workerTrace.ts +2 -2
  28. package/dist/assets/src/execution/residentPoolSpends.ts +9 -0
  29. package/dist/assets/src/execution/residentRecreateAdmission.ts +28 -0
  30. package/dist/assets/src/execution/seedPlan.ts +13 -4
  31. package/dist/assets/web/dist/.vite/manifest.json +31 -31
  32. package/dist/assets/web/dist/assets/CostsPage-6P9rJ7k_.js +1 -0
  33. package/dist/assets/web/dist/assets/{HomePage-BQmun9kS.js → HomePage-B4LGwXHO.js} +1 -1
  34. package/dist/assets/web/dist/assets/ResidentDetailPage-jpGfsR5n.js +1 -0
  35. package/dist/assets/web/dist/assets/ResidentsIndexPage-C4-5nG9M.js +1 -0
  36. package/dist/assets/web/dist/assets/{RunFoldRow-C1w-mwch.js → RunFoldRow-u4FTh57c.js} +1 -1
  37. package/dist/assets/web/dist/assets/{RunRoutePage-zWMHTzla.js → RunRoutePage-CxOKb7oC.js} +4 -4
  38. package/dist/assets/web/dist/assets/{RunsIndexPage-DF16D2Gg.js → RunsIndexPage-CULSlv_d.js} +1 -1
  39. package/dist/assets/web/dist/assets/{ScheduledPage-CyRs0fXN.js → ScheduledPage-CDjkZuqM.js} +1 -1
  40. package/dist/assets/web/dist/assets/{SettingsPage-BRuMCHSj.js → SettingsPage-BMSomotV.js} +1 -1
  41. package/dist/assets/web/dist/assets/{StatusDot-BIAQN7iD.js → StatusDot-XxLkmRaK.js} +1 -1
  42. package/dist/assets/web/dist/assets/{Tooltip-CxiQ7luL.js → Tooltip-qB73RL4b.js} +1 -1
  43. package/dist/assets/web/dist/assets/{UnitRoutePage-BQB5mAAl.js → UnitRoutePage-DFRWAdOT.js} +1 -1
  44. package/dist/assets/web/dist/assets/{dist-I2OEmPqz.js → dist-BsLBAkQJ.js} +1 -1
  45. package/dist/assets/web/dist/assets/{main-DYwKyLY5.js → main-9ynpo780.js} +10 -10
  46. package/dist/assets/web/dist/assets/main-DSiJ9sHU.css +1 -0
  47. package/dist/cli.js +1178 -431
  48. package/package.json +1 -1
  49. package/dist/assets/web/dist/assets/CostsPage-RRggVvIx.js +0 -1
  50. package/dist/assets/web/dist/assets/ResidentDetailPage-CT6v9INM.js +0 -1
  51. package/dist/assets/web/dist/assets/ResidentsIndexPage-CWqPFf40.js +0 -1
  52. package/dist/assets/web/dist/assets/main-DPB0UO1-.css +0 -1
@@ -1,11 +1,16 @@
1
1
  import { afterEach, beforeEach, type TestContext } from "vitest";
2
+ import { systemClock } from "../../src/core/trace/clock.ts";
2
3
  import {
3
4
  assertNoPendingBackgroundTasks,
4
5
  backgroundTaskDiagnostics,
5
6
  beginBackgroundTaskDiagnostics,
6
7
  endBackgroundTaskDiagnostics,
7
8
  } from "./backgroundTasks.ts";
8
- import { MEMORY_DIAGNOSTICS_ANNOTATION, type MemoryTestAnnotation } from "./testDiagnosticsProtocol.ts";
9
+ import {
10
+ MEMORY_DIAGNOSTICS_ANNOTATION,
11
+ type MemoryTestAnnotation,
12
+ type MemoryTestOperation,
13
+ } from "./testDiagnosticsProtocol.ts";
9
14
 
10
15
  type TimerKind = "timeout" | "interval";
11
16
  type TimerHandler = (...args: unknown[]) => unknown;
@@ -24,6 +29,7 @@ interface RunningTest {
24
29
  name: string;
25
30
  trackTimers: boolean;
26
31
  timers: Map<ReturnType<typeof setTimeout>, PendingTimer>;
32
+ operations: MemoryTestOperation[];
27
33
  }
28
34
  interface TimerOriginals {
29
35
  setTimeout: typeof globalThis.setTimeout;
@@ -167,6 +173,40 @@ function poolWorker(): string {
167
173
  return `pool ${poolId ?? "unknown"} / worker ${workerId ?? "unknown"}`;
168
174
  }
169
175
 
176
+ export async function recordMemoryTestOperation<T>(label: string, run: () => Promise<T>): Promise<T> {
177
+ const running = diagnosticGlobal()[RUNNING];
178
+ const operation: MemoryTestOperation = { label, state: "pending" };
179
+ running?.operations.push(operation);
180
+ const started = systemClock();
181
+ try {
182
+ const result = await run();
183
+ operation.state = "passed";
184
+ return result;
185
+ } catch (error) {
186
+ operation.state = "failed";
187
+ throw error;
188
+ } finally {
189
+ operation.durationMs = Math.max(0, systemClock() - started);
190
+ }
191
+ }
192
+
193
+ export function recordMemoryTestRequest(label: string, fetch: () => Promise<Response>): Promise<Response>;
194
+ export function recordMemoryTestRequest<T>(
195
+ label: string,
196
+ fetch: () => Promise<Response>,
197
+ consume: (response: Response) => T | Promise<T>,
198
+ ): Promise<T>;
199
+ export function recordMemoryTestRequest<T>(
200
+ label: string,
201
+ fetch: () => Promise<Response>,
202
+ consume?: (response: Response) => T | Promise<T>,
203
+ ): Promise<Response | T> {
204
+ return recordMemoryTestOperation(label, async () => {
205
+ const response = await fetch();
206
+ return consume ? consume(response) : response;
207
+ });
208
+ }
209
+
170
210
  function pendingTimerLabels(running: RunningTest): string[] {
171
211
  running.trackTimers = false;
172
212
  return [...running.timers.values()].map((timer) => timer.label);
@@ -191,19 +231,20 @@ export function installMemoryTestDiagnostics(): void {
191
231
  installTimerTrap();
192
232
 
193
233
  beforeEach((context) => {
194
- diagnosticGlobal()[RUNNING] = { name: testName(context), trackTimers: true, timers: new Map() };
234
+ diagnosticGlobal()[RUNNING] = { name: testName(context), trackTimers: true, timers: new Map(), operations: [] };
195
235
  beginBackgroundTaskDiagnostics();
196
236
  });
197
237
 
198
238
  afterEach((context) => {
199
239
  const root = diagnosticGlobal();
200
- const running = root[RUNNING] ?? { name: testName(context), trackTimers: false, timers: new Map() };
240
+ const running = root[RUNNING] ?? { name: testName(context), trackTimers: false, timers: new Map(), operations: [] };
201
241
  const background = backgroundTaskDiagnostics();
202
242
  const timers = pendingTimerLabels(running);
203
243
  const annotation: MemoryTestAnnotation = {
204
244
  poolWorker: poolWorker(),
205
245
  ...background,
206
246
  pendingTimers: timers,
247
+ operations: running.operations.map((operation) => ({ ...operation })),
207
248
  };
208
249
 
209
250
  const problems: Error[] = [];
@@ -1,10 +1,17 @@
1
1
  export const MEMORY_DIAGNOSTICS_ANNOTATION = "memory-worker-diagnostics";
2
2
 
3
+ export interface MemoryTestOperation {
4
+ label: string;
5
+ state: "pending" | "passed" | "failed";
6
+ durationMs?: number;
7
+ }
8
+
3
9
  export interface MemoryTestAnnotation {
4
10
  poolWorker: string;
5
11
  registeredBackgroundTasks: string[];
6
12
  pendingPromises: string[];
7
13
  pendingTimers: string[];
14
+ operations: MemoryTestOperation[];
8
15
  }
9
16
 
10
17
  export interface MemoryTestDiagnostic extends MemoryTestAnnotation {
@@ -30,6 +30,7 @@ function annotationFor(testCase: TestCase): MemoryTestAnnotation {
30
30
  registeredBackgroundTasks: [],
31
31
  pendingPromises: [],
32
32
  pendingTimers: [],
33
+ operations: [],
33
34
  };
34
35
  }
35
36
 
@@ -84,5 +85,26 @@ export class MemoryDiagnosticsReporter implements Reporter {
84
85
  ? "no completed tests"
85
86
  : slowest.map((test) => `${Math.round(test.durationMs)}ms ${test.test}`).join("; ");
86
87
  this.#writeLog(`[memory diagnostics] slowest ${slowest.length}: ${summary}`);
88
+ for (const test of this.#tests) {
89
+ if (test.operations.length === 0 || (test.state !== "failed" && !slowest.includes(test))) continue;
90
+ const selected =
91
+ test.state === "failed"
92
+ ? [
93
+ ...new Set([
94
+ ...test.operations.filter((operation) => operation.state === "pending"),
95
+ ...test.operations.slice(-8),
96
+ ]),
97
+ ]
98
+ : [...test.operations].sort((a, b) => (b.durationMs ?? Infinity) - (a.durationMs ?? Infinity)).slice(0, 3);
99
+ const operations = selected
100
+ .map(
101
+ (operation) =>
102
+ `${operation.label} ${operation.state} ${operation.durationMs === undefined ? "pending" : `${operation.durationMs}ms`}`,
103
+ )
104
+ .join("; ");
105
+ this.#writeLog(
106
+ `[memory diagnostics] ${test.test}: ${selected.length}/${test.operations.length} operation(s): ${operations}`,
107
+ );
108
+ }
87
109
  }
88
110
  }
@@ -0,0 +1,21 @@
1
+ import { SELF } from "cloudflare:test";
2
+ import { recordMemoryTestRequest } from "./testDiagnostics.ts";
3
+
4
+ /** A request stays pending until the test has consumed its response body. */
5
+ export function fetchMemoryTest(url: string, init?: RequestInit): Promise<Response>;
6
+ export function fetchMemoryTest<T>(
7
+ url: string,
8
+ init: RequestInit | undefined,
9
+ consume: (response: Response) => T | Promise<T>,
10
+ ): Promise<T>;
11
+ export function fetchMemoryTest<T>(
12
+ url: string,
13
+ init?: RequestInit,
14
+ consume?: (response: Response) => T | Promise<T>,
15
+ ): Promise<Response | T> {
16
+ const method = init?.method ?? "GET";
17
+ const pathname = new URL(url).pathname;
18
+ return consume
19
+ ? recordMemoryTestRequest(`${method} ${pathname}`, () => SELF.fetch(url, init), consume)
20
+ : recordMemoryTestRequest(`${method} ${pathname}`, () => SELF.fetch(url, init));
21
+ }
@@ -1674,6 +1674,11 @@ export class RunHistoryDO extends DurableObject<Env> {
1674
1674
  tools_json TEXT NOT NULL,
1675
1675
  state_json TEXT NOT NULL
1676
1676
  );
1677
+ CREATE TABLE IF NOT EXISTS resident_claim_clock (
1678
+ id INTEGER PRIMARY KEY CHECK (id = 1),
1679
+ value INTEGER NOT NULL
1680
+ );
1681
+ INSERT OR IGNORE INTO resident_claim_clock (id, value) VALUES (1, 0);
1677
1682
  CREATE TABLE IF NOT EXISTS run_steps (
1678
1683
  run_id TEXT NOT NULL,
1679
1684
  step INTEGER NOT NULL,
@@ -2920,6 +2925,33 @@ export class RunHistoryDO extends DurableObject<Env> {
2920
2925
  return r ? rowToLive(r) : undefined;
2921
2926
  }
2922
2927
 
2928
+ /** A later ledger owner gets a larger attachment fence, even for the same run ID. */
2929
+ async residentClaim(
2930
+ runId: string,
2931
+ gen: string,
2932
+ threadKey: string,
2933
+ ): Promise<{ ok: true; fence: number } | { ok: false; reason: "fenced" | "unknown-run" }> {
2934
+ let out: { ok: true; fence: number } | { ok: false; reason: "fenced" | "unknown-run" } = {
2935
+ ok: false,
2936
+ reason: "unknown-run",
2937
+ };
2938
+ this.ctx.storage.transactionSync(() => {
2939
+ const row = this.liveRow(runId);
2940
+ if (!row) return;
2941
+ if (row.ownerGen !== gen || row.threadKey !== threadKey) {
2942
+ out = { ok: false, reason: "fenced" };
2943
+ return;
2944
+ }
2945
+ this.sql.exec(`UPDATE resident_claim_clock SET value = value + 1 WHERE id = 1`);
2946
+ const fence = this.sql
2947
+ .exec<{ value: number }>(`SELECT value FROM resident_claim_clock WHERE id = 1`)
2948
+ .toArray()[0]?.value;
2949
+ if (fence === undefined || !Number.isSafeInteger(fence)) throw new Error("resident claim fence unavailable");
2950
+ out = { ok: true, fence };
2951
+ });
2952
+ return out;
2953
+ }
2954
+
2923
2955
  /** The `runs` table's column migrations, run at every construction and
2924
2956
  * idempotent: a table created before a column existed gains it, with the
2925
2957
  * value a row written back then should read. The run-visibility stamp: a
@@ -4099,6 +4131,9 @@ export class RunHistoryDO extends DurableObject<Env> {
4099
4131
  userId: row.user_id,
4100
4132
  ...(who.userName ? { userName: who.userName } : {}),
4101
4133
  ...(who.parentRunId ? { parentRunId: who.parentRunId } : {}),
4134
+ ...(who.parentInstanceId ? { parentInstanceId: who.parentInstanceId } : {}),
4135
+ ...(who.idempotencyKey ? { idempotencyKey: who.idempotencyKey } : {}),
4136
+ ...(who.costCapUsd !== undefined ? { costCapUsd: who.costCapUsd } : {}),
4102
4137
  threadKey: row.thread_key,
4103
4138
  channelId: row.channel_id,
4104
4139
  ...(row.agent ? { agent: row.agent } : {}),
@@ -4347,12 +4382,23 @@ function parseUsageJson(raw: string | null): RunUsage | undefined {
4347
4382
  }
4348
4383
 
4349
4384
  /** The two identity fields the by-user aggregate needs off a summary, read leniently. */
4350
- function identityOfSummary(raw: string): { userName?: string; parentRunId?: string } {
4385
+ function identityOfSummary(raw: string): {
4386
+ userName?: string;
4387
+ parentRunId?: string;
4388
+ parentInstanceId?: string;
4389
+ idempotencyKey?: string;
4390
+ costCapUsd?: number;
4391
+ } {
4351
4392
  try {
4352
4393
  const s = JSON.parse(raw) as Record<string, unknown>;
4353
4394
  return {
4354
4395
  ...(typeof s.userName === "string" && s.userName ? { userName: s.userName } : {}),
4355
4396
  ...(typeof s.parentRunId === "string" && s.parentRunId ? { parentRunId: s.parentRunId } : {}),
4397
+ ...(typeof s.parentInstanceId === "string" && s.parentInstanceId ? { parentInstanceId: s.parentInstanceId } : {}),
4398
+ ...(typeof s.idempotencyKey === "string" && s.idempotencyKey ? { idempotencyKey: s.idempotencyKey } : {}),
4399
+ ...(typeof s.costCapUsd === "number" && Number.isFinite(s.costCapUsd) && s.costCapUsd > 0
4400
+ ? { costCapUsd: s.costCapUsd }
4401
+ : {}),
4356
4402
  };
4357
4403
  } catch {
4358
4404
  return {};
@@ -5318,6 +5364,7 @@ const LEDGER_ROUTES = new Set([
5318
5364
  "/runs/coordinator/wake",
5319
5365
  "/runs/decision-record/reserve",
5320
5366
  "/runs/claim",
5367
+ "/runs/resident-claim",
5321
5368
  "/runs/heartbeat",
5322
5369
  "/runs/append",
5323
5370
  "/runs/live-state",
@@ -5656,6 +5703,14 @@ function parseClaim(b: Record<string, unknown>): Validated<ClaimRequest> {
5656
5703
  if (typeof meta.idempotencyKey !== "string" || !IDEMPOTENCY_KEY_PATTERN.test(meta.idempotencyKey))
5657
5704
  return invalid("run.meta.idempotencyKey must be <parentInstanceId>:<step>");
5658
5705
  }
5706
+ if (
5707
+ meta.costCapUsd !== undefined &&
5708
+ (meta.parentInstanceId === undefined ||
5709
+ typeof meta.costCapUsd !== "number" ||
5710
+ !Number.isFinite(meta.costCapUsd) ||
5711
+ meta.costCapUsd <= 0)
5712
+ )
5713
+ return invalid("run.meta.costCapUsd must be a positive coordinator cap");
5659
5714
  // The restart tag (record 0064) names the predecessor run whose windows the
5660
5715
  // claim reuses and rides the `child-resumed` event's reason: a run id or
5661
5716
  // absent, never another shape.
@@ -5924,6 +5979,16 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
5924
5979
  );
5925
5980
  return r.ok ? json(r) : json(r, 409);
5926
5981
  }
5982
+ if (pathname === "/runs/resident-claim") {
5983
+ const runId = parseRunId(b.runId);
5984
+ if (!runId.ok) return json({ error: runId.error }, 400);
5985
+ const g = gen(b.gen);
5986
+ if (!g.ok) return json({ error: g.error }, 400);
5987
+ if (typeof b.threadKey !== "string" || b.threadKey.length === 0 || b.threadKey.length > 256)
5988
+ return json({ error: "threadKey must be a non-empty string" }, 400);
5989
+ const result = await stub.residentClaim(runId.value, g.value, b.threadKey);
5990
+ return result.ok ? json(result) : json(result, 409);
5991
+ }
5927
5992
  if (pathname === "/runs/live") return json({ runs: await stub.listLive() });
5928
5993
  if (pathname === "/runs/reclaim") {
5929
5994
  const g = gen(b.gen);
@@ -0,0 +1,28 @@
1
+ /** One deploy's durable progress for a resident container. A repeated
2
+ * reconcile may retry a deferred stop or collect an inactive container's
3
+ * report, but must not stop a replacement that is still hydrating. */
4
+ export interface DeployImageReconcileState {
5
+ build: string;
6
+ cycleIssued: boolean;
7
+ cycleStoppedAt?: string;
8
+ containerBeforeCycle?: string;
9
+ }
10
+
11
+ export type DeployImageReconcileAction = "start" | "retry-cycle" | "await-report" | "verified";
12
+
13
+ export function nextDeployImageReconcile(
14
+ build: string,
15
+ stored: unknown,
16
+ reportPending: boolean,
17
+ ): DeployImageReconcileAction {
18
+ if (!stored || typeof stored !== "object") return "start";
19
+ const state = stored as Record<string, unknown>;
20
+ if (state.build !== build || typeof state.cycleIssued !== "boolean") return "start";
21
+ if (!reportPending) return "verified";
22
+ return state.cycleIssued ? "await-report" : "retry-cycle";
23
+ }
24
+
25
+ /** A fresh boot after the requested stop can finish hydration and report. */
26
+ export function replacementContainerStarted(before: string | undefined, current: string | undefined): boolean {
27
+ return before !== undefined && current !== undefined && before !== current;
28
+ }
@@ -0,0 +1,68 @@
1
+ /** A command is counted separately. Between commands, a registration protects
2
+ * the run only through its budget or the sweep's inactivity boundary. */
3
+ export function registeredRunNeedsProtection(
4
+ lastAttachAt: string | undefined,
5
+ opInFlight: number,
6
+ cutoff: number,
7
+ deadlineAt?: number,
8
+ now?: number,
9
+ ): boolean {
10
+ if (opInFlight > 0) return false; // the operation counter already covers it
11
+ if (deadlineAt !== undefined && now !== undefined && Number.isSafeInteger(deadlineAt)) return now <= deadlineAt;
12
+ const last = Date.parse(lastAttachAt ?? "");
13
+ return !Number.isFinite(last) || last >= cutoff;
14
+ }
15
+
16
+ /** Only a provably live registration owned by this run can bypass the gates
17
+ * that keep new work off a draining or stale resident. */
18
+ export function registeredRunAllowsClaim(
19
+ registration: { runId?: string; ownerGen?: string; ownerFence?: number } | undefined,
20
+ runId: string | undefined,
21
+ ownerGen: string | undefined,
22
+ ownerFence?: number,
23
+ ): boolean {
24
+ if (!registration) return true;
25
+ if (ownerFence !== undefined && registration.ownerFence !== undefined)
26
+ return (
27
+ ownerFence > registration.ownerFence ||
28
+ (ownerFence === registration.ownerFence && registration.runId === runId && registration.ownerGen === ownerGen)
29
+ );
30
+ if (registration.ownerFence !== undefined) return false;
31
+ if (ownerFence !== undefined) return true; // first ledger-backed claim over a legacy row
32
+ return registration.runId === runId && registration.ownerGen === ownerGen;
33
+ }
34
+
35
+ export function registeredRunAllowsReattach(
36
+ registration: { runId?: string; ownerGen?: string; ownerFence?: number; deadlineAt?: number } | undefined,
37
+ runId: string | undefined,
38
+ now: number,
39
+ graceMs: number,
40
+ ownerGen?: string,
41
+ ownerFence?: number,
42
+ ): boolean {
43
+ const deadlineAt = registration?.deadlineAt;
44
+ return (
45
+ runId !== undefined &&
46
+ registration?.runId === runId &&
47
+ registeredRunAllowsClaim(registration, runId, ownerGen, ownerFence) &&
48
+ deadlineAt !== undefined &&
49
+ Number.isSafeInteger(deadlineAt) &&
50
+ now <= deadlineAt + graceMs
51
+ );
52
+ }
53
+
54
+ /** A release may affect only the registration opened by its run. Legacy
55
+ * callers may release legacy registrations, but cannot release a named run. */
56
+ export function registeredRunOwnsRelease(
57
+ registration: { runId?: string; ownerGen?: string; ownerFence?: number } | undefined,
58
+ runId: string | undefined,
59
+ ownerGen?: string,
60
+ ownerFence?: number,
61
+ ): boolean {
62
+ return (
63
+ registration !== undefined &&
64
+ registration.runId === runId &&
65
+ registration.ownerGen === ownerGen &&
66
+ registration.ownerFence === ownerFence
67
+ );
68
+ }
@@ -30,9 +30,8 @@ export const REFRESH_INTERVAL_S = 600;
30
30
  * ten-minute buckets since the last instance. */
31
31
  export const IDLE_REFRESH_INTERVAL_S = 6 * 60 * 60;
32
32
 
33
- /** The thread user pool the image carries (`worker2`..`worker17`): one OS
34
- * user per attached thread, and the bound on how many bindings one sweep
35
- * step can have to check. */
33
+ /** The image's `worker2`..`worker17` pool: 16 one-use identities per VM
34
+ * generation for thread owners and disposable operations. */
36
35
  export const THREAD_POOL_SIZE = 16;
37
36
 
38
37
  /** Exec budgets. Each is a refresh step's own budget too (refresh.ts), so a