@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.
- package/dist/assets/deploy/cloudflare-memory/testDiagnostics.ts +44 -3
- package/dist/assets/deploy/cloudflare-memory/testDiagnosticsProtocol.ts +7 -0
- package/dist/assets/deploy/cloudflare-memory/testDiagnosticsReporter.ts +22 -0
- package/dist/assets/deploy/cloudflare-memory/testFetch.ts +21 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +66 -1
- package/dist/assets/deploy/cloudflare-resident/imageReconcileState.ts +28 -0
- package/dist/assets/deploy/cloudflare-resident/runRegistration.ts +68 -0
- package/dist/assets/deploy/cloudflare-resident/shared.ts +2 -3
- package/dist/assets/deploy/cloudflare-resident/worker.ts +541 -192
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +17 -2
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +2 -2
- package/dist/assets/src/core/budgets.ts +6 -0
- package/dist/assets/src/core/coordinator/contract.ts +14 -8
- package/dist/assets/src/core/costs.ts +2 -0
- package/dist/assets/src/core/runEvents.ts +18 -1
- package/dist/assets/src/core/runLedger/types.ts +2 -0
- package/dist/assets/src/core/runRecord.ts +27 -1
- package/dist/assets/src/core/runUsage.ts +53 -0
- package/dist/assets/src/core/ship/coordinator.ts +4 -1
- package/dist/assets/src/core/shipSpend.ts +52 -0
- package/dist/assets/src/core/trace/attrs.ts +22 -0
- package/dist/assets/src/core/trace/tracer.ts +10 -2
- package/dist/assets/src/core/trace/types.ts +12 -1
- package/dist/assets/src/core/trace/workerTrace.ts +2 -2
- package/dist/assets/src/execution/residentPoolSpends.ts +9 -0
- package/dist/assets/src/execution/residentRecreateAdmission.ts +28 -0
- package/dist/assets/src/execution/seedPlan.ts +13 -4
- package/dist/assets/web/dist/.vite/manifest.json +31 -31
- package/dist/assets/web/dist/assets/CostsPage-6P9rJ7k_.js +1 -0
- package/dist/assets/web/dist/assets/{HomePage-BQmun9kS.js → HomePage-B4LGwXHO.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-jpGfsR5n.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-C4-5nG9M.js +1 -0
- package/dist/assets/web/dist/assets/{RunFoldRow-C1w-mwch.js → RunFoldRow-u4FTh57c.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-zWMHTzla.js → RunRoutePage-CxOKb7oC.js} +4 -4
- package/dist/assets/web/dist/assets/{RunsIndexPage-DF16D2Gg.js → RunsIndexPage-CULSlv_d.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-CyRs0fXN.js → ScheduledPage-CDjkZuqM.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BRuMCHSj.js → SettingsPage-BMSomotV.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BIAQN7iD.js → StatusDot-XxLkmRaK.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CxiQ7luL.js → Tooltip-qB73RL4b.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BQB5mAAl.js → UnitRoutePage-DFRWAdOT.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-I2OEmPqz.js → dist-BsLBAkQJ.js} +1 -1
- package/dist/assets/web/dist/assets/{main-DYwKyLY5.js → main-9ynpo780.js} +10 -10
- package/dist/assets/web/dist/assets/main-DSiJ9sHU.css +1 -0
- package/dist/cli.js +1178 -431
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-RRggVvIx.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-CT6v9INM.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CWqPFf40.js +0 -1
- 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 {
|
|
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): {
|
|
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
|
|
34
|
-
*
|
|
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
|