@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.
Files changed (129) hide show
  1. package/dist/assets/deploy/cloudflare-memory/backgroundTasks.ts +27 -3
  2. package/dist/assets/deploy/cloudflare-memory/testDiagnostics.ts +227 -0
  3. package/dist/assets/deploy/cloudflare-memory/testDiagnosticsProtocol.ts +22 -0
  4. package/dist/assets/deploy/cloudflare-memory/testDiagnosticsReporter.ts +88 -0
  5. package/dist/assets/deploy/cloudflare-memory/tsconfig.json +14 -2
  6. package/dist/assets/deploy/cloudflare-resident/Dockerfile +11 -2
  7. package/dist/assets/package-lock.json +3 -3
  8. package/dist/assets/package.json +4 -3
  9. package/dist/assets/project.json +4 -0
  10. package/dist/assets/source.json +3 -3
  11. package/dist/assets/src/agents/registry.ts +6 -5
  12. package/dist/assets/src/core/coordinator/contract.ts +48 -2
  13. package/dist/assets/src/core/coordinator/driver.ts +40 -5
  14. package/dist/assets/src/core/reviewVerdict.ts +15 -5
  15. package/dist/assets/src/core/runFriction.ts +1 -1
  16. package/dist/assets/src/core/ship/contract.ts +2 -2
  17. package/dist/assets/src/core/ship/coordinator.ts +236 -108
  18. package/dist/assets/src/core/ship/renewal.ts +3 -1
  19. package/dist/assets/src/execution/bashTimeout.ts +1 -1
  20. package/dist/assets/src/execution/sandboxErrors.ts +6 -6
  21. package/dist/assets/web/dist/.vite/manifest.json +488 -488
  22. package/dist/assets/web/dist/assets/{AppShell-CSXDCV4x.js → AppShell-DPJVhUGP.js} +1 -1
  23. package/dist/assets/web/dist/assets/{CostChart-DsuPS2g7.js → CostChart-4TeDdbvJ.js} +1 -1
  24. package/dist/assets/web/dist/assets/CostsPage-8HDgvuWB.js +1 -0
  25. package/dist/assets/web/dist/assets/{DeliveryPage-62qCSLvk.js → DeliveryPage-BaemWKW8.js} +1 -1
  26. package/dist/assets/web/dist/assets/{HomePage-Dk3HRBc3.js → HomePage-fgKQHLaO.js} +1 -1
  27. package/dist/assets/web/dist/assets/{InputMenu-BdZXnibG.js → InputMenu-BRV35UWP.js} +1 -1
  28. package/dist/assets/web/dist/assets/{MetricsPage-o5fSaCTe.js → MetricsPage-BQBuIuYU.js} +1 -1
  29. package/dist/assets/web/dist/assets/{NotFoundPage-DHuWyjlq.js → NotFoundPage-CDjMfCcj.js} +1 -1
  30. package/dist/assets/web/dist/assets/{PendingTurnRow-CeTGOXUy.js → PendingTurnRow-vuWiDzvz.js} +1 -1
  31. package/dist/assets/web/dist/assets/{PlanePage-RD0M6O6W.js → PlanePage-DXMJf8FB.js} +1 -1
  32. package/dist/assets/web/dist/assets/{ResidentDetailPage-Dl2KId95.js → ResidentDetailPage-DdC8yU_S.js} +1 -1
  33. package/dist/assets/web/dist/assets/{ResidentsIndexPage-DBy43I1R.js → ResidentsIndexPage-CSM-KODv.js} +1 -1
  34. package/dist/assets/web/dist/assets/{RunFoldRow-CwHDL-Mo.js → RunFoldRow-Csik3wD6.js} +1 -1
  35. package/dist/assets/web/dist/assets/{RunRoutePage-Du1HqLZs.js → RunRoutePage-jo6NZxsP.js} +3 -3
  36. package/dist/assets/web/dist/assets/{RunsIndexPage-BZphy3Hr.js → RunsIndexPage-Dx1Y4sSq.js} +1 -1
  37. package/dist/assets/web/dist/assets/{RunsTabs-kHbbxVHx.js → RunsTabs-C7Ua3XX5.js} +1 -1
  38. package/dist/assets/web/dist/assets/{ScheduledPage-B7jh898t.js → ScheduledPage-BuB_NZyR.js} +1 -1
  39. package/dist/assets/web/dist/assets/{SettingSelect-BfID5nF4.js → SettingSelect-CEeA4iuC.js} +1 -1
  40. package/dist/assets/web/dist/assets/{SettingsPage-3uLEsiiE.js → SettingsPage-Cn96jB6Q.js} +1 -1
  41. package/dist/assets/web/dist/assets/{SilentTurn-CKxSxnQC.js → SilentTurn-D2cJfpEq.js} +1 -1
  42. package/dist/assets/web/dist/assets/{SlackMark-CtiHrNjr.js → SlackMark-DB0MGiF5.js} +1 -1
  43. package/dist/assets/web/dist/assets/{StatusDot-x7vcK0iE.js → StatusDot-DDHUCPVJ.js} +1 -1
  44. package/dist/assets/web/dist/assets/{Tooltip-wVXCWhMp.js → Tooltip-B4VIHkAI.js} +1 -1
  45. package/dist/assets/web/dist/assets/{UnitRoutePage-V5BXK7FH.js → UnitRoutePage-C-2DfjLI.js} +1 -1
  46. package/dist/assets/web/dist/assets/{angular-html-C8b5YTHf.js → angular-html-BRRhsdQE.js} +1 -1
  47. package/dist/assets/web/dist/assets/{angular-ts-C3f4WYNe.js → angular-ts-BFj-mqCm.js} +1 -1
  48. package/dist/assets/web/dist/assets/{apl-DSHxUJc3.js → apl-CsBuy1IL.js} +1 -1
  49. package/dist/assets/web/dist/assets/{astro-BSGCCBP6.js → astro-cVaqxBZq.js} +1 -1
  50. package/dist/assets/web/dist/assets/{blade-DFeYrMKk.js → blade-MEP5mWha.js} +1 -1
  51. package/dist/assets/web/dist/assets/{browser-B6T5hjfR.js → browser-CCImen5O.js} +1 -1
  52. package/dist/assets/web/dist/assets/{c-L_QOnFVv.js → c-D4NGQ_Pd.js} +1 -1
  53. package/dist/assets/web/dist/assets/{chapel-DLQWUwhu.js → chapel-BkWImsIy.js} +1 -1
  54. package/dist/assets/web/dist/assets/{cobol-1DlMmrEO.js → cobol-RPDSPSET.js} +1 -1
  55. package/dist/assets/web/dist/assets/{coffee-BTB6os6N.js → coffee-DZ5z7oVX.js} +1 -1
  56. package/dist/assets/web/dist/assets/{cpp-BvJI2z3u.js → cpp-CrBtIaRZ.js} +1 -1
  57. package/dist/assets/web/dist/assets/{crystal-B11wGW_h.js → crystal-DJesYi6f.js} +1 -1
  58. package/dist/assets/web/dist/assets/{css-BiIWx5Pw.js → css-DO9mjau-.js} +1 -1
  59. package/dist/assets/web/dist/assets/{dist-C-XGnvM4.js → dist-Cf1CmCSK.js} +2 -2
  60. package/dist/assets/web/dist/assets/{edge-nSglEXaD.js → edge-C3VzbSW0.js} +1 -1
  61. package/dist/assets/web/dist/assets/{elixir-x7o8qMl8.js → elixir-BkD_Vhfe.js} +1 -1
  62. package/dist/assets/web/dist/assets/{elm-DGrRyqTQ.js → elm-KIM0mFPk.js} +1 -1
  63. package/dist/assets/web/dist/assets/{erb--_dh58Nw.js → erb-D2ruCqb2.js} +1 -1
  64. package/dist/assets/web/dist/assets/{eventSource-Bj_dZf6M.js → eventSource-Cv0AFlMi.js} +1 -1
  65. package/dist/assets/web/dist/assets/{git-rebase-DbpUXwyg.js → git-rebase-CnfbQTxA.js} +1 -1
  66. package/dist/assets/web/dist/assets/{glimmer-js-BG0CuZG5.js → glimmer-js-CbiwFXWH.js} +1 -1
  67. package/dist/assets/web/dist/assets/{glimmer-ts-CQIl5qyR.js → glimmer-ts-DV8ii0Pv.js} +1 -1
  68. package/dist/assets/web/dist/assets/{glsl-9PJNQTcw.js → glsl-CgmznWkA.js} +1 -1
  69. package/dist/assets/web/dist/assets/{graphql-CzVTzjEI.js → graphql-BK-bBYfH.js} +1 -1
  70. package/dist/assets/web/dist/assets/{hack-BhCmAz3Q.js → hack-DnwTTP5n.js} +1 -1
  71. package/dist/assets/web/dist/assets/{haml-BmI12COR.js → haml-DAVJsWwT.js} +1 -1
  72. package/dist/assets/web/dist/assets/{handlebars-Cib-ISTS.js → handlebars-voOVfxFL.js} +1 -1
  73. package/dist/assets/web/dist/assets/{html-D3lDapvI.js → html-3FTqwaPk.js} +1 -1
  74. package/dist/assets/web/dist/assets/{html-derivative-DRPY8vOK.js → html-derivative-CSlhLDQn.js} +1 -1
  75. package/dist/assets/web/dist/assets/{http-B9VttK6m.js → http-Vx4ilPfS.js} +1 -1
  76. package/dist/assets/web/dist/assets/{hurl-C0saNJYj.js → hurl-DxyZDE3R.js} +1 -1
  77. package/dist/assets/web/dist/assets/{java-BCFbtYAj.js → java-nWqdjNJG.js} +1 -1
  78. package/dist/assets/web/dist/assets/{javascript-nwBjnrhh.js → javascript-Dr82HOwp.js} +1 -1
  79. package/dist/assets/web/dist/assets/{jinja-CMYO9byN.js → jinja-BhjLrPOt.js} +1 -1
  80. package/dist/assets/web/dist/assets/{jison-Cko8HnE_.js → jison-DilGIUa8.js} +1 -1
  81. package/dist/assets/web/dist/assets/{json-Dzv__YsX.js → json-CrOcvmhJ.js} +1 -1
  82. package/dist/assets/web/dist/assets/{jsx-CTFxagih.js → jsx-BW_RX-fo.js} +1 -1
  83. package/dist/assets/web/dist/assets/{julia-_Xvten1-.js → julia-CWH071DB.js} +1 -1
  84. package/dist/assets/web/dist/assets/{just-bLps4qOu.js → just-DkhhoWBy.js} +1 -1
  85. package/dist/assets/web/dist/assets/{latex-D6UaYsUP.js → latex-cuHJ4TUT.js} +1 -1
  86. package/dist/assets/web/dist/assets/{liquid-CoDIeULa.js → liquid-DG6afYQN.js} +1 -1
  87. package/dist/assets/web/dist/assets/{lua-D1KFRnVf.js → lua-C5CMnCPS.js} +1 -1
  88. package/dist/assets/web/dist/assets/{main-DAL--hyj.js → main-DxPqaFu9.js} +3 -3
  89. package/dist/assets/web/dist/assets/{marko-B5N5XcOw.js → marko-Dy2_DuC4.js} +1 -1
  90. package/dist/assets/web/dist/assets/{mdc-uxzjQO3q.js → mdc-BMKb_EeA.js} +1 -1
  91. package/dist/assets/web/dist/assets/{nginx-BMJxs2TK.js → nginx-og0RgMKf.js} +1 -1
  92. package/dist/assets/web/dist/assets/{nim-DqQ3-8p1.js → nim-CQxTQQP0.js} +1 -1
  93. package/dist/assets/web/dist/assets/{org-DsY6LK59.js → org-b5lkq_Pl.js} +1 -1
  94. package/dist/assets/web/dist/assets/{perl--pq9iJFp.js → perl-D9X5_i_J.js} +1 -1
  95. package/dist/assets/web/dist/assets/{php-RHCMbsM2.js → php-CwUxvLX0.js} +1 -1
  96. package/dist/assets/web/dist/assets/{pug-CFNq_FCp.js → pug-uOIOExr0.js} +1 -1
  97. package/dist/assets/web/dist/assets/{qml-C9Acx9E8.js → qml-g5RoADf-.js} +1 -1
  98. package/dist/assets/web/dist/assets/{r-DxlU8FBd.js → r-5LdAopD3.js} +1 -1
  99. package/dist/assets/web/dist/assets/{razor-CLJ2kKih.js → razor-DwrvFLYQ.js} +1 -1
  100. package/dist/assets/web/dist/assets/{regexp-CzmdGoEp.js → regexp-dGtAaxC-.js} +1 -1
  101. package/dist/assets/web/dist/assets/{rst-D9yuxBET.js → rst-wFdW0brr.js} +1 -1
  102. package/dist/assets/web/dist/assets/{ruby-CIRcEvnD.js → ruby-DPglN6I9.js} +1 -1
  103. package/dist/assets/web/dist/assets/{sas-LOl-uUPh.js → sas-CUbCs40d.js} +1 -1
  104. package/dist/assets/web/dist/assets/{scss-BX2_sZHY.js → scss-B-DUiI1c.js} +1 -1
  105. package/dist/assets/web/dist/assets/{shellscript-4d-QvKkY.js → shellscript-lGOV7Ram.js} +1 -1
  106. package/dist/assets/web/dist/assets/{shellsession-DhB16UGy.js → shellsession-BDWtAAjr.js} +1 -1
  107. package/dist/assets/web/dist/assets/{soy-Cm1xTUBr.js → soy-zod_dcOy.js} +1 -1
  108. package/dist/assets/web/dist/assets/{sql-BsfcBJdu.js → sql-BG0HSSoM.js} +1 -1
  109. package/dist/assets/web/dist/assets/{sseReplay-BqgOggyQ.js → sseReplay-BzoeVxZB.js} +1 -1
  110. package/dist/assets/web/dist/assets/{stata-Dwat3BSw.js → stata-BVUMLp1W.js} +1 -1
  111. package/dist/assets/web/dist/assets/{surrealql-P4rtxzAW.js → surrealql-DiSUbHKF.js} +1 -1
  112. package/dist/assets/web/dist/assets/{svelte-DTcsi8S8.js → svelte-DZenT45Y.js} +1 -1
  113. package/dist/assets/web/dist/assets/{templ-D-kP0Tg5.js → templ-BWKRNM0-.js} +1 -1
  114. package/dist/assets/web/dist/assets/{tex-D-4vuedK.js → tex-CbMzBlkK.js} +1 -1
  115. package/dist/assets/web/dist/assets/{ts-tags-jEEWYxsu.js → ts-tags-CZsVorDN.js} +1 -1
  116. package/dist/assets/web/dist/assets/{tsx-CFnlrVzT.js → tsx-R1r3mH4W.js} +1 -1
  117. package/dist/assets/web/dist/assets/{twig-D5rGrPjs.js → twig-BjPMyjKH.js} +1 -1
  118. package/dist/assets/web/dist/assets/{typescript-nYIQ35or.js → typescript-Bx-66VN_.js} +1 -1
  119. package/dist/assets/web/dist/assets/{typst-3-UX8dna.js → typst-fW037Eo6.js} +1 -1
  120. package/dist/assets/web/dist/assets/{vue-CLIPIX03.js → vue-D1V_GNxW.js} +1 -1
  121. package/dist/assets/web/dist/assets/{vue-html-Ci9CpkHU.js → vue-html-C-4pTZ-s.js} +1 -1
  122. package/dist/assets/web/dist/assets/{vue-vine-DL5pEYYk.js → vue-vine-C-QsX3hR.js} +1 -1
  123. package/dist/assets/web/dist/assets/{wallClock-DmaC1sKA.js → wallClock-x0hjr0J_.js} +1 -1
  124. package/dist/assets/web/dist/assets/{xml-bO80qikC.js → xml-BJTJXPgC.js} +1 -1
  125. package/dist/assets/web/dist/assets/{xsl-BPze60Ej.js → xsl-Xc9lukqe.js} +1 -1
  126. package/dist/assets/web/dist/assets/{yaml-BQUSon_o.js → yaml-Dw-FMsQa.js} +1 -1
  127. package/dist/cli.js +505 -253
  128. package/package.json +1 -1
  129. 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
- const pendingTasks = (): PendingTasks => {
7
- const root = globalThis as typeof globalThis & { [TASKS]?: PendingTasks };
8
- return (root[TASKS] ??= new Map());
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": ["worker.ts", "worker.test.ts", "friction.test.ts", "schedules.test.ts", "test-env.d.ts"]
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.2",
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.2",
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.2",
20448
+ "version": "1.260.4",
20449
20449
  "license": "Apache-2.0",
20450
20450
  "dependencies": {
20451
20451
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.260.2",
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 --",
@@ -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`."
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.260.2",
3
- "commit": "95c0153dc141f2e586238634e66970fea994bf7b",
4
- "builtAt": "2026-09-21T20:25:49.397Z"
2
+ "version": "1.260.4",
3
+ "commit": "714dbd34ad03841fe421bfdc78648854b4c8dbc1",
4
+ "builtAt": "2026-09-22T07:09:50.930Z"
5
5
  }
@@ -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 (a conflict ends the unit — report it and stop; never resolve it by force). 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.`;
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 (the repository's " +
293
- "AGENTS.md says how a generated file is regenerated — regenerate it, never hand-merge it), re-run THE FAST " +
294
- "GATES on the rebased tree, and only then push. Every head that reaches review is then current with its " +
295
- "base when it lands, and no unit ends merge-ready behind a sibling that merged first.";
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 { isAddressSeverity, type AddressSeverity, type AddressSeveritySource } from "../ship/coordinator.js";
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. */