@coreplane/switchboard 1.260.2 → 1.260.4
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/backgroundTasks.ts +27 -3
- package/dist/assets/deploy/cloudflare-memory/testDiagnostics.ts +227 -0
- package/dist/assets/deploy/cloudflare-memory/testDiagnosticsProtocol.ts +22 -0
- package/dist/assets/deploy/cloudflare-memory/testDiagnosticsReporter.ts +88 -0
- package/dist/assets/deploy/cloudflare-memory/tsconfig.json +14 -2
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +11 -2
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +4 -3
- package/dist/assets/project.json +4 -0
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +6 -5
- package/dist/assets/src/core/coordinator/contract.ts +48 -2
- package/dist/assets/src/core/coordinator/driver.ts +40 -5
- package/dist/assets/src/core/reviewVerdict.ts +15 -5
- package/dist/assets/src/core/runFriction.ts +1 -1
- package/dist/assets/src/core/ship/contract.ts +2 -2
- package/dist/assets/src/core/ship/coordinator.ts +236 -108
- package/dist/assets/src/core/ship/renewal.ts +3 -1
- package/dist/assets/src/execution/bashTimeout.ts +1 -1
- package/dist/assets/src/execution/sandboxErrors.ts +6 -6
- package/dist/assets/web/dist/.vite/manifest.json +488 -488
- package/dist/assets/web/dist/assets/{AppShell-CSXDCV4x.js → AppShell-DPJVhUGP.js} +1 -1
- package/dist/assets/web/dist/assets/{CostChart-DsuPS2g7.js → CostChart-4TeDdbvJ.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-8HDgvuWB.js +1 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-62qCSLvk.js → DeliveryPage-BaemWKW8.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-Dk3HRBc3.js → HomePage-fgKQHLaO.js} +1 -1
- package/dist/assets/web/dist/assets/{InputMenu-BdZXnibG.js → InputMenu-BRV35UWP.js} +1 -1
- package/dist/assets/web/dist/assets/{MetricsPage-o5fSaCTe.js → MetricsPage-BQBuIuYU.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-DHuWyjlq.js → NotFoundPage-CDjMfCcj.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-CeTGOXUy.js → PendingTurnRow-vuWiDzvz.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-RD0M6O6W.js → PlanePage-DXMJf8FB.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Dl2KId95.js → ResidentDetailPage-DdC8yU_S.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-DBy43I1R.js → ResidentsIndexPage-CSM-KODv.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-CwHDL-Mo.js → RunFoldRow-Csik3wD6.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-Du1HqLZs.js → RunRoutePage-jo6NZxsP.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-BZphy3Hr.js → RunsIndexPage-Dx1Y4sSq.js} +1 -1
- package/dist/assets/web/dist/assets/{RunsTabs-kHbbxVHx.js → RunsTabs-C7Ua3XX5.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-B7jh898t.js → ScheduledPage-BuB_NZyR.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingSelect-BfID5nF4.js → SettingSelect-CEeA4iuC.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-3uLEsiiE.js → SettingsPage-Cn96jB6Q.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-CKxSxnQC.js → SilentTurn-D2cJfpEq.js} +1 -1
- package/dist/assets/web/dist/assets/{SlackMark-CtiHrNjr.js → SlackMark-DB0MGiF5.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-x7vcK0iE.js → StatusDot-DDHUCPVJ.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-wVXCWhMp.js → Tooltip-B4VIHkAI.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-V5BXK7FH.js → UnitRoutePage-C-2DfjLI.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-C8b5YTHf.js → angular-html-BRRhsdQE.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-C3f4WYNe.js → angular-ts-BFj-mqCm.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DSHxUJc3.js → apl-CsBuy1IL.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-BSGCCBP6.js → astro-cVaqxBZq.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-DFeYrMKk.js → blade-MEP5mWha.js} +1 -1
- package/dist/assets/web/dist/assets/{browser-B6T5hjfR.js → browser-CCImen5O.js} +1 -1
- package/dist/assets/web/dist/assets/{c-L_QOnFVv.js → c-D4NGQ_Pd.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DLQWUwhu.js → chapel-BkWImsIy.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-1DlMmrEO.js → cobol-RPDSPSET.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-BTB6os6N.js → coffee-DZ5z7oVX.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-BvJI2z3u.js → cpp-CrBtIaRZ.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-B11wGW_h.js → crystal-DJesYi6f.js} +1 -1
- package/dist/assets/web/dist/assets/{css-BiIWx5Pw.js → css-DO9mjau-.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-C-XGnvM4.js → dist-Cf1CmCSK.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-nSglEXaD.js → edge-C3VzbSW0.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-x7o8qMl8.js → elixir-BkD_Vhfe.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DGrRyqTQ.js → elm-KIM0mFPk.js} +1 -1
- package/dist/assets/web/dist/assets/{erb--_dh58Nw.js → erb-D2ruCqb2.js} +1 -1
- package/dist/assets/web/dist/assets/{eventSource-Bj_dZf6M.js → eventSource-Cv0AFlMi.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DbpUXwyg.js → git-rebase-CnfbQTxA.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-BG0CuZG5.js → glimmer-js-CbiwFXWH.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CQIl5qyR.js → glimmer-ts-DV8ii0Pv.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-9PJNQTcw.js → glsl-CgmznWkA.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-CzVTzjEI.js → graphql-BK-bBYfH.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BhCmAz3Q.js → hack-DnwTTP5n.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-BmI12COR.js → haml-DAVJsWwT.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-Cib-ISTS.js → handlebars-voOVfxFL.js} +1 -1
- package/dist/assets/web/dist/assets/{html-D3lDapvI.js → html-3FTqwaPk.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DRPY8vOK.js → html-derivative-CSlhLDQn.js} +1 -1
- package/dist/assets/web/dist/assets/{http-B9VttK6m.js → http-Vx4ilPfS.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-C0saNJYj.js → hurl-DxyZDE3R.js} +1 -1
- package/dist/assets/web/dist/assets/{java-BCFbtYAj.js → java-nWqdjNJG.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-nwBjnrhh.js → javascript-Dr82HOwp.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-CMYO9byN.js → jinja-BhjLrPOt.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-Cko8HnE_.js → jison-DilGIUa8.js} +1 -1
- package/dist/assets/web/dist/assets/{json-Dzv__YsX.js → json-CrOcvmhJ.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CTFxagih.js → jsx-BW_RX-fo.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-_Xvten1-.js → julia-CWH071DB.js} +1 -1
- package/dist/assets/web/dist/assets/{just-bLps4qOu.js → just-DkhhoWBy.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-D6UaYsUP.js → latex-cuHJ4TUT.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CoDIeULa.js → liquid-DG6afYQN.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-D1KFRnVf.js → lua-C5CMnCPS.js} +1 -1
- package/dist/assets/web/dist/assets/{main-DAL--hyj.js → main-DxPqaFu9.js} +3 -3
- package/dist/assets/web/dist/assets/{marko-B5N5XcOw.js → marko-Dy2_DuC4.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-uxzjQO3q.js → mdc-BMKb_EeA.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-BMJxs2TK.js → nginx-og0RgMKf.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DqQ3-8p1.js → nim-CQxTQQP0.js} +1 -1
- package/dist/assets/web/dist/assets/{org-DsY6LK59.js → org-b5lkq_Pl.js} +1 -1
- package/dist/assets/web/dist/assets/{perl--pq9iJFp.js → perl-D9X5_i_J.js} +1 -1
- package/dist/assets/web/dist/assets/{php-RHCMbsM2.js → php-CwUxvLX0.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-CFNq_FCp.js → pug-uOIOExr0.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-C9Acx9E8.js → qml-g5RoADf-.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DxlU8FBd.js → r-5LdAopD3.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-CLJ2kKih.js → razor-DwrvFLYQ.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CzmdGoEp.js → regexp-dGtAaxC-.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-D9yuxBET.js → rst-wFdW0brr.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-CIRcEvnD.js → ruby-DPglN6I9.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-LOl-uUPh.js → sas-CUbCs40d.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-BX2_sZHY.js → scss-B-DUiI1c.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-4d-QvKkY.js → shellscript-lGOV7Ram.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-DhB16UGy.js → shellsession-BDWtAAjr.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-Cm1xTUBr.js → soy-zod_dcOy.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-BsfcBJdu.js → sql-BG0HSSoM.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-BqgOggyQ.js → sseReplay-BzoeVxZB.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-Dwat3BSw.js → stata-BVUMLp1W.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-P4rtxzAW.js → surrealql-DiSUbHKF.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DTcsi8S8.js → svelte-DZenT45Y.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-D-kP0Tg5.js → templ-BWKRNM0-.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-D-4vuedK.js → tex-CbMzBlkK.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-jEEWYxsu.js → ts-tags-CZsVorDN.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-CFnlrVzT.js → tsx-R1r3mH4W.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-D5rGrPjs.js → twig-BjPMyjKH.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-nYIQ35or.js → typescript-Bx-66VN_.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-3-UX8dna.js → typst-fW037Eo6.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CLIPIX03.js → vue-D1V_GNxW.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Ci9CpkHU.js → vue-html-C-4pTZ-s.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-DL5pEYYk.js → vue-vine-C-QsX3hR.js} +1 -1
- package/dist/assets/web/dist/assets/{wallClock-DmaC1sKA.js → wallClock-x0hjr0J_.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-bO80qikC.js → xml-BJTJXPgC.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BPze60Ej.js → xsl-Xc9lukqe.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BQUSon_o.js → yaml-Dw-FMsQa.js} +1 -1
- package/dist/cli.js +505 -253
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +0 -1
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
type WaitUntilContext = Pick<DurableObjectState, "waitUntil">;
|
|
2
2
|
|
|
3
3
|
const TASKS = Symbol.for("switchboard.memory-worker.pending-background-tasks");
|
|
4
|
+
const REGISTRATIONS = Symbol.for("switchboard.memory-worker.background-task-registrations");
|
|
4
5
|
type PendingTasks = Map<Promise<unknown>, string>;
|
|
6
|
+
type Registrations = string[];
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
type DiagnosticRoot = typeof globalThis & {
|
|
9
|
+
[TASKS]?: PendingTasks;
|
|
10
|
+
[REGISTRATIONS]?: Registrations;
|
|
9
11
|
};
|
|
10
12
|
|
|
13
|
+
const root = (): DiagnosticRoot => globalThis as DiagnosticRoot;
|
|
14
|
+
const pendingTasks = (): PendingTasks => (root()[TASKS] ??= new Map());
|
|
15
|
+
|
|
16
|
+
export function beginBackgroundTaskDiagnostics(): void {
|
|
17
|
+
root()[REGISTRATIONS] = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function endBackgroundTaskDiagnostics(): void {
|
|
21
|
+
delete root()[REGISTRATIONS];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function backgroundTaskDiagnostics(): {
|
|
25
|
+
registeredBackgroundTasks: string[];
|
|
26
|
+
pendingPromises: string[];
|
|
27
|
+
} {
|
|
28
|
+
return {
|
|
29
|
+
registeredBackgroundTasks: [...(root()[REGISTRATIONS] ?? [])],
|
|
30
|
+
pendingPromises: [...pendingTasks().values()],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
/** Keep deliberately detached Worker work in the runtime's request lifetime.
|
|
12
35
|
* The process-wide label set is also the test seam: a case cannot finish while
|
|
13
36
|
* work it started is still able to contend with the next case. */
|
|
14
37
|
export function holdBackgroundTask(context: WaitUntilContext, label: string, task: Promise<unknown>): void {
|
|
15
38
|
const pending = pendingTasks();
|
|
39
|
+
root()[REGISTRATIONS]?.push(label);
|
|
16
40
|
const held = task.finally(() => pending.delete(held));
|
|
17
41
|
pending.set(held, label);
|
|
18
42
|
context.waitUntil(held);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { afterEach, beforeEach, type TestContext } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
assertNoPendingBackgroundTasks,
|
|
4
|
+
backgroundTaskDiagnostics,
|
|
5
|
+
beginBackgroundTaskDiagnostics,
|
|
6
|
+
endBackgroundTaskDiagnostics,
|
|
7
|
+
} from "./backgroundTasks.ts";
|
|
8
|
+
import { MEMORY_DIAGNOSTICS_ANNOTATION, type MemoryTestAnnotation } from "./testDiagnosticsProtocol.ts";
|
|
9
|
+
|
|
10
|
+
type TimerKind = "timeout" | "interval";
|
|
11
|
+
type TimerHandler = (...args: unknown[]) => unknown;
|
|
12
|
+
type TimerSetter = (
|
|
13
|
+
handler: TimerHandler,
|
|
14
|
+
delay?: number,
|
|
15
|
+
...args: unknown[]
|
|
16
|
+
) => ReturnType<typeof globalThis.setTimeout>;
|
|
17
|
+
interface PendingTimer {
|
|
18
|
+
id: ReturnType<typeof setTimeout>;
|
|
19
|
+
kind: TimerKind;
|
|
20
|
+
label: string;
|
|
21
|
+
owner: string;
|
|
22
|
+
}
|
|
23
|
+
interface RunningTest {
|
|
24
|
+
name: string;
|
|
25
|
+
trackTimers: boolean;
|
|
26
|
+
timers: Map<ReturnType<typeof setTimeout>, PendingTimer>;
|
|
27
|
+
}
|
|
28
|
+
interface TimerOriginals {
|
|
29
|
+
setTimeout: typeof globalThis.setTimeout;
|
|
30
|
+
clearTimeout: typeof globalThis.clearTimeout;
|
|
31
|
+
setInterval: typeof globalThis.setInterval;
|
|
32
|
+
clearInterval: typeof globalThis.clearInterval;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const RUNNING = Symbol.for("switchboard.memory-worker.running-test");
|
|
36
|
+
const TIMER_ORIGINALS = Symbol.for("switchboard.memory-worker.timer-originals");
|
|
37
|
+
const REJECTION_TRAP = Symbol.for("switchboard.memory-worker.rejection-trap");
|
|
38
|
+
|
|
39
|
+
type DiagnosticGlobal = typeof globalThis & {
|
|
40
|
+
[RUNNING]?: RunningTest;
|
|
41
|
+
[TIMER_ORIGINALS]?: TimerOriginals;
|
|
42
|
+
[REJECTION_TRAP]?: boolean;
|
|
43
|
+
__vitest_worker__?: { ctx?: { poolId?: number; workerId?: number } };
|
|
44
|
+
process?: {
|
|
45
|
+
env?: Record<string, string | undefined>;
|
|
46
|
+
prependListener?: (event: "unhandledRejection", listener: (reason: unknown) => void) => void;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const diagnosticGlobal = (): DiagnosticGlobal => globalThis as DiagnosticGlobal;
|
|
51
|
+
|
|
52
|
+
function reasonText(reason: unknown): string {
|
|
53
|
+
return reason instanceof Error ? reason.message : String(reason);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runningTestName(): string {
|
|
57
|
+
return diagnosticGlobal()[RUNNING]?.name ?? "<no test>";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function unhandledRejection(reason: unknown): never {
|
|
61
|
+
throw new Error(`unhandled rejection while "${runningTestName()}" was running: ${reasonText(reason)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function installUnhandledRejectionTrap(): void {
|
|
65
|
+
const root = diagnosticGlobal();
|
|
66
|
+
if (root[REJECTION_TRAP]) return;
|
|
67
|
+
root[REJECTION_TRAP] = true;
|
|
68
|
+
|
|
69
|
+
if (typeof root.addEventListener === "function") {
|
|
70
|
+
root.addEventListener("unhandledrejection", (event) => {
|
|
71
|
+
const rejection = event as PromiseRejectionEvent;
|
|
72
|
+
rejection.preventDefault();
|
|
73
|
+
unhandledRejection(rejection.reason);
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
root.process?.prependListener?.("unhandledRejection", unhandledRejection);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function timerLabel(kind: TimerKind, handler: TimerHandler, delay?: number): string {
|
|
82
|
+
const callback = typeof handler === "function" && handler.name ? handler.name : "anonymous";
|
|
83
|
+
return `${kind} ${callback} (${delay ?? 0}ms)`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function strayTimerError(timer: PendingTimer): Error {
|
|
87
|
+
return new Error(`stray ${timer.label} from "${timer.owner}" fired while "${runningTestName()}" was running`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function installTimerTrap(): void {
|
|
91
|
+
const root = diagnosticGlobal();
|
|
92
|
+
if (root[TIMER_ORIGINALS]) return;
|
|
93
|
+
|
|
94
|
+
const originals: TimerOriginals = {
|
|
95
|
+
setTimeout: root.setTimeout.bind(root),
|
|
96
|
+
clearTimeout: root.clearTimeout.bind(root),
|
|
97
|
+
setInterval: root.setInterval.bind(root),
|
|
98
|
+
clearInterval: root.clearInterval.bind(root),
|
|
99
|
+
};
|
|
100
|
+
root[TIMER_ORIGINALS] = originals;
|
|
101
|
+
|
|
102
|
+
root.setTimeout = ((handler: TimerHandler, delay?: number, ...args: unknown[]) => {
|
|
103
|
+
const running = root[RUNNING];
|
|
104
|
+
if (!running?.trackTimers || typeof handler !== "function") {
|
|
105
|
+
return (originals.setTimeout as unknown as TimerSetter)(handler, delay, ...args);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const pending: PendingTimer = {
|
|
109
|
+
id: undefined as unknown as ReturnType<typeof setTimeout>,
|
|
110
|
+
kind: "timeout",
|
|
111
|
+
label: timerLabel("timeout", handler, delay),
|
|
112
|
+
owner: running.name,
|
|
113
|
+
};
|
|
114
|
+
const id = (originals.setTimeout as unknown as TimerSetter)(
|
|
115
|
+
(...callbackArgs: unknown[]) => {
|
|
116
|
+
running.timers.delete(id);
|
|
117
|
+
if (root[RUNNING]?.name !== pending.owner) throw strayTimerError(pending);
|
|
118
|
+
handler(...callbackArgs);
|
|
119
|
+
},
|
|
120
|
+
delay,
|
|
121
|
+
...args,
|
|
122
|
+
);
|
|
123
|
+
pending.id = id;
|
|
124
|
+
running.timers.set(id, pending);
|
|
125
|
+
return id;
|
|
126
|
+
}) as typeof globalThis.setTimeout;
|
|
127
|
+
|
|
128
|
+
root.setInterval = ((handler: TimerHandler, delay?: number, ...args: unknown[]) => {
|
|
129
|
+
const running = root[RUNNING];
|
|
130
|
+
if (!running?.trackTimers || typeof handler !== "function") {
|
|
131
|
+
return (originals.setInterval as unknown as TimerSetter)(handler, delay, ...args);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const pending: PendingTimer = {
|
|
135
|
+
id: undefined as unknown as ReturnType<typeof setTimeout>,
|
|
136
|
+
kind: "interval",
|
|
137
|
+
label: timerLabel("interval", handler, delay),
|
|
138
|
+
owner: running.name,
|
|
139
|
+
};
|
|
140
|
+
const id = (originals.setInterval as unknown as TimerSetter)(
|
|
141
|
+
(...callbackArgs: unknown[]) => {
|
|
142
|
+
if (root[RUNNING]?.name !== pending.owner) throw strayTimerError(pending);
|
|
143
|
+
handler(...callbackArgs);
|
|
144
|
+
},
|
|
145
|
+
delay,
|
|
146
|
+
...args,
|
|
147
|
+
);
|
|
148
|
+
pending.id = id;
|
|
149
|
+
running.timers.set(id, pending);
|
|
150
|
+
return id;
|
|
151
|
+
}) as typeof globalThis.setInterval;
|
|
152
|
+
|
|
153
|
+
root.clearTimeout = ((id?: ReturnType<typeof setTimeout>) => {
|
|
154
|
+
root[RUNNING]?.timers.delete(id as ReturnType<typeof setTimeout>);
|
|
155
|
+
return originals.clearTimeout(id);
|
|
156
|
+
}) as typeof globalThis.clearTimeout;
|
|
157
|
+
root.clearInterval = ((id?: ReturnType<typeof setInterval>) => {
|
|
158
|
+
root[RUNNING]?.timers.delete(id as ReturnType<typeof setTimeout>);
|
|
159
|
+
return originals.clearInterval(id);
|
|
160
|
+
}) as typeof globalThis.clearInterval;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function poolWorker(): string {
|
|
164
|
+
const root = diagnosticGlobal();
|
|
165
|
+
const poolId = root.__vitest_worker__?.ctx?.poolId ?? root.process?.env?.VITEST_POOL_ID;
|
|
166
|
+
const workerId = root.__vitest_worker__?.ctx?.workerId ?? root.process?.env?.VITEST_WORKER_ID;
|
|
167
|
+
return `pool ${poolId ?? "unknown"} / worker ${workerId ?? "unknown"}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function pendingTimerLabels(running: RunningTest): string[] {
|
|
171
|
+
running.trackTimers = false;
|
|
172
|
+
return [...running.timers.values()].map((timer) => timer.label);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function clearPendingTimers(running: RunningTest): void {
|
|
176
|
+
const originals = diagnosticGlobal()[TIMER_ORIGINALS];
|
|
177
|
+
if (!originals) return;
|
|
178
|
+
for (const timer of running.timers.values()) {
|
|
179
|
+
if (timer.kind === "interval") originals.clearInterval(timer.id);
|
|
180
|
+
else originals.clearTimeout(timer.id);
|
|
181
|
+
}
|
|
182
|
+
running.timers.clear();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function testName(context: TestContext): string {
|
|
186
|
+
return context.task.fullTestName || context.task.name;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function installMemoryTestDiagnostics(): void {
|
|
190
|
+
installUnhandledRejectionTrap();
|
|
191
|
+
installTimerTrap();
|
|
192
|
+
|
|
193
|
+
beforeEach((context) => {
|
|
194
|
+
diagnosticGlobal()[RUNNING] = { name: testName(context), trackTimers: true, timers: new Map() };
|
|
195
|
+
beginBackgroundTaskDiagnostics();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
afterEach((context) => {
|
|
199
|
+
const root = diagnosticGlobal();
|
|
200
|
+
const running = root[RUNNING] ?? { name: testName(context), trackTimers: false, timers: new Map() };
|
|
201
|
+
const background = backgroundTaskDiagnostics();
|
|
202
|
+
const timers = pendingTimerLabels(running);
|
|
203
|
+
const annotation: MemoryTestAnnotation = {
|
|
204
|
+
poolWorker: poolWorker(),
|
|
205
|
+
...background,
|
|
206
|
+
pendingTimers: timers,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const problems: Error[] = [];
|
|
210
|
+
try {
|
|
211
|
+
assertNoPendingBackgroundTasks();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
problems.push(error instanceof Error ? error : new Error(String(error)));
|
|
214
|
+
}
|
|
215
|
+
if (timers.length > 0) {
|
|
216
|
+
problems.push(new Error(`test ended with ${timers.length} pending timer(s): ${timers.join(", ")}`));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
(context.task.meta as Record<string, unknown>)[MEMORY_DIAGNOSTICS_ANNOTATION] = annotation;
|
|
220
|
+
clearPendingTimers(running);
|
|
221
|
+
endBackgroundTaskDiagnostics();
|
|
222
|
+
delete root[RUNNING];
|
|
223
|
+
|
|
224
|
+
if (problems.length === 1) throw problems[0];
|
|
225
|
+
if (problems.length > 1) throw new AggregateError(problems, "test ended with pending asynchronous work");
|
|
226
|
+
});
|
|
227
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const MEMORY_DIAGNOSTICS_ANNOTATION = "memory-worker-diagnostics";
|
|
2
|
+
|
|
3
|
+
export interface MemoryTestAnnotation {
|
|
4
|
+
poolWorker: string;
|
|
5
|
+
registeredBackgroundTasks: string[];
|
|
6
|
+
pendingPromises: string[];
|
|
7
|
+
pendingTimers: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MemoryTestDiagnostic extends MemoryTestAnnotation {
|
|
11
|
+
test: string;
|
|
12
|
+
file: string;
|
|
13
|
+
state: "passed" | "failed" | "skipped" | "pending";
|
|
14
|
+
durationMs: number;
|
|
15
|
+
startedAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MemoryTestDiagnosticsArtifact {
|
|
19
|
+
reason: "passed" | "failed" | "interrupted";
|
|
20
|
+
unhandledErrors: string[];
|
|
21
|
+
tests: MemoryTestDiagnostic[];
|
|
22
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { Reporter, TestRunEndReason } from "vitest/reporters";
|
|
4
|
+
import {
|
|
5
|
+
MEMORY_DIAGNOSTICS_ANNOTATION,
|
|
6
|
+
type MemoryTestAnnotation,
|
|
7
|
+
type MemoryTestDiagnostic,
|
|
8
|
+
type MemoryTestDiagnosticsArtifact,
|
|
9
|
+
} from "./testDiagnosticsProtocol.ts";
|
|
10
|
+
|
|
11
|
+
type TestCase = Parameters<NonNullable<Reporter["onTestCaseResult"]>>[0];
|
|
12
|
+
|
|
13
|
+
export interface MemoryDiagnosticsReporterOptions {
|
|
14
|
+
outputFile?: string;
|
|
15
|
+
writeLog?: (line: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function annotationFor(testCase: TestCase): MemoryTestAnnotation {
|
|
19
|
+
const annotation = (testCase.meta() as Record<string, unknown>)[MEMORY_DIAGNOSTICS_ANNOTATION];
|
|
20
|
+
if (
|
|
21
|
+
typeof annotation === "object" &&
|
|
22
|
+
annotation !== null &&
|
|
23
|
+
"poolWorker" in annotation &&
|
|
24
|
+
typeof annotation.poolWorker === "string"
|
|
25
|
+
) {
|
|
26
|
+
return annotation as MemoryTestAnnotation;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
poolWorker: "unknown",
|
|
30
|
+
registeredBackgroundTasks: [],
|
|
31
|
+
pendingPromises: [],
|
|
32
|
+
pendingTimers: [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function errorMessage(error: { message?: string } | unknown): string {
|
|
37
|
+
return typeof error === "object" && error !== null && "message" in error && typeof error.message === "string"
|
|
38
|
+
? error.message
|
|
39
|
+
: String(error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class MemoryDiagnosticsReporter implements Reporter {
|
|
43
|
+
readonly #outputFile: string;
|
|
44
|
+
readonly #writeLog: (line: string) => void;
|
|
45
|
+
readonly #tests: MemoryTestDiagnostic[] = [];
|
|
46
|
+
|
|
47
|
+
constructor(options: MemoryDiagnosticsReporterOptions = {}) {
|
|
48
|
+
this.#outputFile =
|
|
49
|
+
options.outputFile ??
|
|
50
|
+
process.env.MEMORY_TEST_DIAGNOSTICS_PATH ??
|
|
51
|
+
path.join(process.cwd(), "artifacts", "memory-test-diagnostics.json");
|
|
52
|
+
this.#writeLog = options.writeLog ?? ((line) => process.stdout.write(`${line}\n`));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onTestCaseResult(testCase: TestCase): void {
|
|
56
|
+
const diagnostic = testCase.diagnostic();
|
|
57
|
+
const result = testCase.result();
|
|
58
|
+
this.#tests.push({
|
|
59
|
+
test: testCase.fullName,
|
|
60
|
+
file: testCase.module.moduleId,
|
|
61
|
+
state: result.state,
|
|
62
|
+
durationMs: diagnostic?.duration ?? 0,
|
|
63
|
+
startedAt: diagnostic?.startTime ?? 0,
|
|
64
|
+
...annotationFor(testCase),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async onTestRunEnd(
|
|
69
|
+
_testModules: Parameters<NonNullable<Reporter["onTestRunEnd"]>>[0],
|
|
70
|
+
unhandledErrors: Parameters<NonNullable<Reporter["onTestRunEnd"]>>[1],
|
|
71
|
+
reason: TestRunEndReason,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
const artifact: MemoryTestDiagnosticsArtifact = {
|
|
74
|
+
reason,
|
|
75
|
+
unhandledErrors: unhandledErrors.map(errorMessage),
|
|
76
|
+
tests: this.#tests,
|
|
77
|
+
};
|
|
78
|
+
await mkdir(path.dirname(this.#outputFile), { recursive: true });
|
|
79
|
+
await writeFile(this.#outputFile, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
|
|
80
|
+
|
|
81
|
+
const slowest = [...this.#tests].sort((a, b) => b.durationMs - a.durationMs).slice(0, 5);
|
|
82
|
+
const summary =
|
|
83
|
+
slowest.length === 0
|
|
84
|
+
? "no completed tests"
|
|
85
|
+
: slowest.map((test) => `${Math.round(test.durationMs)}ms ${test.test}`).join("; ");
|
|
86
|
+
this.#writeLog(`[memory diagnostics] slowest ${slowest.length}: ${summary}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -7,11 +7,23 @@
|
|
|
7
7
|
"module": "esnext",
|
|
8
8
|
"moduleResolution": "bundler",
|
|
9
9
|
"lib": ["esnext"],
|
|
10
|
-
"types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers/types"],
|
|
10
|
+
"types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers/types", "node"],
|
|
11
11
|
"strict": true,
|
|
12
12
|
"noEmit": true,
|
|
13
13
|
"skipLibCheck": true,
|
|
14
14
|
"allowImportingTsExtensions": true
|
|
15
15
|
},
|
|
16
|
-
"include": [
|
|
16
|
+
"include": [
|
|
17
|
+
"worker.ts",
|
|
18
|
+
"worker.test.ts",
|
|
19
|
+
"schedules.test.ts",
|
|
20
|
+
"backgroundTasks.ts",
|
|
21
|
+
"backgroundTasks.test.ts",
|
|
22
|
+
"testDiagnostics.ts",
|
|
23
|
+
"testDiagnosticsProtocol.ts",
|
|
24
|
+
"testDiagnosticsReporter.ts",
|
|
25
|
+
"testSetup.ts",
|
|
26
|
+
"vitest.config.ts",
|
|
27
|
+
"test-env.d.ts"
|
|
28
|
+
]
|
|
17
29
|
}
|
|
@@ -152,10 +152,19 @@ RUN rm -f /usr/local/bin/bun \
|
|
|
152
152
|
# The build asserts the binary with the probe the extract script uses at run
|
|
153
153
|
# time (`command -v`): `unsquashfs -version` exits 1 on squashfs-tools 4.5 (this
|
|
154
154
|
# base) when no filesystem is named, and failed the 1.2.0 image build that way.
|
|
155
|
+
#
|
|
156
|
+
# ripgrep (`rg`) + fd: the grep/find tools call these binaries directly. Ubuntu
|
|
157
|
+
# packages fd as `fd-find` with the binary named `fdfind`, so the image exposes
|
|
158
|
+
# the conventional `fd` name from /usr/local/bin (ahead of /usr/bin on PATH).
|
|
159
|
+
# Both are image tools, never downloaded by a run; their PATH probes make a
|
|
160
|
+
# missing package or link fail the image build instead of every resident run.
|
|
155
161
|
RUN apt-get update \
|
|
156
|
-
&& apt-get install -y --no-install-recommends squashfs-tools \
|
|
162
|
+
&& apt-get install -y --no-install-recommends squashfs-tools ripgrep fd-find \
|
|
163
|
+
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
|
157
164
|
&& rm -rf /var/lib/apt/lists/* \
|
|
158
|
-
&& command -v unsquashfs >/dev/null
|
|
165
|
+
&& command -v unsquashfs >/dev/null \
|
|
166
|
+
&& command -v rg >/dev/null \
|
|
167
|
+
&& command -v fd >/dev/null
|
|
159
168
|
|
|
160
169
|
# The toolchain for what a run BUILDS and LOOKS AT — the same layer as the
|
|
161
170
|
# sandbox image's (deploy/cloudflare-sandbox/Dockerfile);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.260.
|
|
3
|
+
"version": "1.260.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.260.
|
|
9
|
+
"version": "1.260.4",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -20445,7 +20445,7 @@
|
|
|
20445
20445
|
},
|
|
20446
20446
|
"packages/switchboard": {
|
|
20447
20447
|
"name": "@coreplane/switchboard",
|
|
20448
|
-
"version": "1.260.
|
|
20448
|
+
"version": "1.260.4",
|
|
20449
20449
|
"license": "Apache-2.0",
|
|
20450
20450
|
"dependencies": {
|
|
20451
20451
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.260.
|
|
3
|
+
"version": "1.260.4",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"cli": "tsx src/cli.ts",
|
|
40
40
|
"verify": "npm run verify:root && npm run verify --workspaces --if-present && npm run check:site",
|
|
41
41
|
"verify:root": "npm run check:consistency && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:dist",
|
|
42
|
-
"check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run check:deps-drift && npm run licenses:check && npm run docs:check && npm run pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run vocabulary:check && npm run check:project-facts && npm run check:registry-drift && npm run agents:check && npm run clock:check && npm run screenshots:check",
|
|
42
|
+
"check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run check:deps-drift && npm run licenses:check && npm run docs:check && npm run pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run vocabulary:check && npm run user-message:check && npm run check:project-facts && npm run check:registry-drift && npm run agents:check && npm run clock:check && npm run screenshots:check",
|
|
43
43
|
"ci:gate": "node scripts/ci-gate.mjs",
|
|
44
|
-
"fix": "npm run docs:gen && npm run pr-title:gen && npm run agents:gen && npm run clock:gen && npm run hygiene:gen && npm run vocabulary:gen && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
|
|
44
|
+
"fix": "npm run docs:gen && npm run pr-title:gen && npm run agents:gen && npm run clock:gen && npm run hygiene:gen && npm run vocabulary:gen && npm run user-message:check -- --write && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
|
|
45
45
|
"deploy:gen": "npm run --silent cli -- deploy init",
|
|
46
46
|
"deploy:check": "npm run --silent cli -- deploy init --check",
|
|
47
47
|
"check:lockfile": "node scripts/check-lockfile.mjs",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"hygiene:gen": "node scripts/public-hygiene.mjs --write",
|
|
76
76
|
"vocabulary:check": "node scripts/vocabulary-check.mjs",
|
|
77
77
|
"vocabulary:gen": "node scripts/vocabulary-check.mjs --write",
|
|
78
|
+
"user-message:check": "node scripts/user-message-check.mjs",
|
|
78
79
|
"docs:changed": "node scripts/docs-changed.mjs",
|
|
79
80
|
"deploy:targets": "node scripts/deploy-targets.mjs",
|
|
80
81
|
"docs:dev": "npm run dev -w docs --",
|
package/dist/assets/project.json
CHANGED
|
@@ -183,6 +183,10 @@
|
|
|
183
183
|
"does": "Records the remaining internal words; refuses growth unless `-- --force`.",
|
|
184
184
|
"when": "Part of `fix`."
|
|
185
185
|
},
|
|
186
|
+
"user-message:check": {
|
|
187
|
+
"does": "No user-facing statement delegates recovery.",
|
|
188
|
+
"when": "Part of `check:consistency`; the baseline only shrinks."
|
|
189
|
+
},
|
|
186
190
|
"agents:gen": {
|
|
187
191
|
"does": "Writes the Commands table in AGENTS.md from `package.json` and this file.",
|
|
188
192
|
"when": "After adding or changing a script; part of `fix`."
|
package/dist/assets/source.json
CHANGED
|
@@ -191,7 +191,7 @@ const CONTRACT_HEADINGS_LIST = Object.values(CONTRACT_SECTION_HEADINGS)
|
|
|
191
191
|
// assembled by the child, which would choose what to leave out. Both coding
|
|
192
192
|
// prompts carry this verbatim so the resident and sandbox children read the
|
|
193
193
|
// same rule; the review prompts name the same block and the same severity.
|
|
194
|
-
const UNIT_CONTRACT = `UNIT CONTRACT: when your first user turn carries a \`${CONTRACT_HEADING}\` block — its sub-headings, in this order: ${CONTRACT_HEADINGS_LIST} — it is the contract for one plan unit, rendered by Switchboard from the plan itself, and it outranks any free-text task beside it. Do its first instruction first: the rebase of the unit's branch onto the merged parent
|
|
194
|
+
const UNIT_CONTRACT = `UNIT CONTRACT: when your first user turn carries a \`${CONTRACT_HEADING}\` block — its sub-headings, in this order: ${CONTRACT_HEADINGS_LIST} — it is the contract for one plan unit, rendered by Switchboard from the plan itself, and it outranks any free-text task beside it. Do its first instruction first: the rebase of the unit's branch onto the merged parent. Then implement the unit's section as written: every test scenario it lists is added as a test, every spec row it names is updated so its proof binding resolves, the agent rules are followed, and no guard it names is weakened. The review is handed the same block and checks the diff against it: a test scenario the unit listed and the diff did not add is a finding at minor severity — the same severity as a spec contradiction. Never edit the plan record itself; where the unit is wrong or a criterion could not be proven, say so in the handoff and in your final message.`;
|
|
195
195
|
|
|
196
196
|
// The unit handoff (docs/reference/specs/agent-coding.md item 9; agent-ship.md
|
|
197
197
|
// item 14): the contract's return edge, as data. A child that ran for a plan
|
|
@@ -289,10 +289,11 @@ export const FAST_GATES_BEFORE_PUSH =
|
|
|
289
289
|
// (src/core/ship/contract.ts).
|
|
290
290
|
export const REBASE_BEFORE_PUSH =
|
|
291
291
|
"REBASE BEFORE EVERY PUSH — always, not configurable. Immediately before each push: fetch your base branch, " +
|
|
292
|
-
"rebase your branch onto it, resolve any conflict with the context you already have
|
|
293
|
-
"AGENTS.md says how a generated file is regenerated
|
|
294
|
-
"
|
|
295
|
-
"
|
|
292
|
+
"rebase your branch onto it, and resolve any conflict with the context you already have in one bounded model " +
|
|
293
|
+
"round — the thread's context and the repository's AGENTS.md (which says how a generated file is regenerated: " +
|
|
294
|
+
"regenerate it, never hand-merge it). If you cannot resolve it inside that bound, end the round; otherwise " +
|
|
295
|
+
"re-run THE FAST GATES on the rebased tree, and only then push. Every head that reaches review is then current " +
|
|
296
|
+
"with its base when it lands, and no unit ends merge-ready behind a sibling that merged first.";
|
|
296
297
|
|
|
297
298
|
const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
|
|
298
299
|
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
//
|
|
11
11
|
import { IDLE_DAYS_MAX, type Grant, type GrantSource } from "../budgets.js";
|
|
12
12
|
import { isVerbosity, type Verbosity } from "../verbosity.js";
|
|
13
|
-
import {
|
|
13
|
+
import { isFindingShape } from "../reviewVerdict.js";
|
|
14
|
+
import {
|
|
15
|
+
isAddressSeverity,
|
|
16
|
+
type AddressSeverity,
|
|
17
|
+
type AddressSeveritySource,
|
|
18
|
+
type HumanGatePending,
|
|
19
|
+
} from "../ship/coordinator.js";
|
|
14
20
|
import { isHandoffShape, type Handoff } from "../ship/handoff.js";
|
|
15
21
|
|
|
16
22
|
// A coordinator is a Workflow instance in the shim Worker whose children are
|
|
@@ -421,6 +427,8 @@ export interface UnitIdle {
|
|
|
421
427
|
runId?: string;
|
|
422
428
|
spendUsd: number | null;
|
|
423
429
|
handoff?: Handoff;
|
|
430
|
+
/** The human-gated question this idle is waiting on. */
|
|
431
|
+
humanGate?: HumanGatePending;
|
|
424
432
|
wakes: number;
|
|
425
433
|
}
|
|
426
434
|
|
|
@@ -439,6 +447,7 @@ export type UnitWakeAnswer =
|
|
|
439
447
|
texts: string[];
|
|
440
448
|
senders: string[];
|
|
441
449
|
leaseMs?: number;
|
|
450
|
+
humanGate?: HumanGatePending;
|
|
442
451
|
}
|
|
443
452
|
| { kind: "answered"; reply: string }
|
|
444
453
|
| { kind: "stopped" }
|
|
@@ -579,6 +588,23 @@ const isRoundGate = (v: unknown): boolean =>
|
|
|
579
588
|
Array.isArray(v.findings) &&
|
|
580
589
|
v.findings.every((f) => typeof f === "string");
|
|
581
590
|
|
|
591
|
+
export const isHumanGatePending = (v: unknown): v is HumanGatePending =>
|
|
592
|
+
isObject(v) &&
|
|
593
|
+
isObject(v.pr) &&
|
|
594
|
+
typeof v.pr.number === "number" &&
|
|
595
|
+
Number.isInteger(v.pr.number) &&
|
|
596
|
+
v.pr.number > 0 &&
|
|
597
|
+
isText(v.pr.url) &&
|
|
598
|
+
typeof v.round === "number" &&
|
|
599
|
+
Number.isInteger(v.round) &&
|
|
600
|
+
v.round >= 1 &&
|
|
601
|
+
Array.isArray(v.findings) &&
|
|
602
|
+
v.findings.every((finding: unknown) => isFindingShape(finding)) &&
|
|
603
|
+
(v.verdict === "approve" || v.verdict === "request_changes") &&
|
|
604
|
+
(v.reviewRunId === undefined || isText(v.reviewRunId)) &&
|
|
605
|
+
(v.headSha === undefined || isText(v.headSha)) &&
|
|
606
|
+
(v.askedAt === undefined || isFinite(v.askedAt));
|
|
607
|
+
|
|
582
608
|
const isUnitIdle = (v: unknown): boolean =>
|
|
583
609
|
isObject(v) &&
|
|
584
610
|
isText(v.why, IDLE_WHY_MAX) &&
|
|
@@ -588,6 +614,7 @@ const isUnitIdle = (v: unknown): boolean =>
|
|
|
588
614
|
(v.runId === undefined || isText(v.runId)) &&
|
|
589
615
|
(v.spendUsd === null || isFinite(v.spendUsd)) &&
|
|
590
616
|
(v.handoff === undefined || isHandoffShape(v.handoff)) &&
|
|
617
|
+
(v.humanGate === undefined || isHumanGatePending(v.humanGate)) &&
|
|
591
618
|
isCount(v.wakes);
|
|
592
619
|
|
|
593
620
|
const isSegment = (v: unknown): boolean =>
|
|
@@ -616,7 +643,8 @@ export const isUnitWakeAnswer = (v: unknown): v is UnitWakeAnswer => {
|
|
|
616
643
|
v.texts.every((text) => typeof text === "string") &&
|
|
617
644
|
Array.isArray(v.senders) &&
|
|
618
645
|
v.senders.every((sender) => isText(sender)) &&
|
|
619
|
-
(v.leaseMs === undefined || (isFinite(v.leaseMs) && v.leaseMs > 0))
|
|
646
|
+
(v.leaseMs === undefined || (isFinite(v.leaseMs) && v.leaseMs > 0)) &&
|
|
647
|
+
(v.humanGate === undefined || isHumanGatePending(v.humanGate))
|
|
620
648
|
);
|
|
621
649
|
};
|
|
622
650
|
|
|
@@ -693,6 +721,24 @@ export type RunFinishedSend =
|
|
|
693
721
|
| { kind: "no-binding"; instance: string }
|
|
694
722
|
| { kind: "failed"; instance: string; type: string; reason: string };
|
|
695
723
|
|
|
724
|
+
/** Wake one unit after durable outside input was appended. The payload is
|
|
725
|
+
* deliberately empty: the unit re-reads its own event list before acting. */
|
|
726
|
+
export async function sendUnitNudge(
|
|
727
|
+
workflow: WorkflowSender | undefined,
|
|
728
|
+
key: { instanceId: string; unit: string },
|
|
729
|
+
): Promise<RunFinishedSend> {
|
|
730
|
+
const instance = key.instanceId;
|
|
731
|
+
if (!workflow) return { kind: "no-binding", instance };
|
|
732
|
+
const type = unitNudgeEventType(key);
|
|
733
|
+
try {
|
|
734
|
+
const handle = await workflow.get(instance);
|
|
735
|
+
await handle.sendEvent({ type, payload: {} });
|
|
736
|
+
return { kind: "sent", instance, type };
|
|
737
|
+
} catch (err) {
|
|
738
|
+
return { kind: "failed", instance, type, reason: err instanceof Error ? err.message : String(err) };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
696
742
|
/** The one send per settled head (http-ingress.md item 12): best effort like
|
|
697
743
|
* `sendRunFinished` — a refusal is answered, never thrown, and the parent's
|
|
698
744
|
* bounded merge wait times out on its own. */
|