@coreplane/switchboard 1.267.2 → 1.267.3
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 +43 -0
- package/dist/assets/deploy/cloudflare-resident/runRegistration.ts +68 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +379 -165
- 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 +2 -0
- package/dist/assets/src/core/runEvents.ts +17 -1
- package/dist/assets/src/core/runRecord.ts +17 -1
- package/dist/assets/src/core/ship/coordinator.ts +4 -1
- 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/seedPlan.ts +13 -4
- package/dist/cli.js +796 -208
- package/package.json +1 -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
|
|
@@ -5318,6 +5350,7 @@ const LEDGER_ROUTES = new Set([
|
|
|
5318
5350
|
"/runs/coordinator/wake",
|
|
5319
5351
|
"/runs/decision-record/reserve",
|
|
5320
5352
|
"/runs/claim",
|
|
5353
|
+
"/runs/resident-claim",
|
|
5321
5354
|
"/runs/heartbeat",
|
|
5322
5355
|
"/runs/append",
|
|
5323
5356
|
"/runs/live-state",
|
|
@@ -5924,6 +5957,16 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
|
|
|
5924
5957
|
);
|
|
5925
5958
|
return r.ok ? json(r) : json(r, 409);
|
|
5926
5959
|
}
|
|
5960
|
+
if (pathname === "/runs/resident-claim") {
|
|
5961
|
+
const runId = parseRunId(b.runId);
|
|
5962
|
+
if (!runId.ok) return json({ error: runId.error }, 400);
|
|
5963
|
+
const g = gen(b.gen);
|
|
5964
|
+
if (!g.ok) return json({ error: g.error }, 400);
|
|
5965
|
+
if (typeof b.threadKey !== "string" || b.threadKey.length === 0 || b.threadKey.length > 256)
|
|
5966
|
+
return json({ error: "threadKey must be a non-empty string" }, 400);
|
|
5967
|
+
const result = await stub.residentClaim(runId.value, g.value, b.threadKey);
|
|
5968
|
+
return result.ok ? json(result) : json(result, 409);
|
|
5969
|
+
}
|
|
5927
5970
|
if (pathname === "/runs/live") return json({ runs: await stub.listLive() });
|
|
5928
5971
|
if (pathname === "/runs/reclaim") {
|
|
5929
5972
|
const g = gen(b.gen);
|
|
@@ -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
|
+
}
|