tickmarkr 2.5.4 → 2.5.6

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 (78) hide show
  1. package/README.md +3 -1
  2. package/dist/adapters/registry.js +6 -1
  3. package/dist/adapters/types.d.ts +3 -0
  4. package/dist/cli/commands/approve.d.ts +1 -0
  5. package/dist/cli/commands/approve.js +118 -9
  6. package/dist/cli/commands/doctor.d.ts +10 -0
  7. package/dist/cli/commands/doctor.js +44 -29
  8. package/dist/cli/commands/fleet.js +199 -33
  9. package/dist/cli/commands/init.js +196 -6
  10. package/dist/cli/commands/plan.js +20 -4
  11. package/dist/cli/commands/resume.js +4 -2
  12. package/dist/cli/commands/run.js +11 -2
  13. package/dist/cli/commands/verify.js +102 -81
  14. package/dist/cli/help.d.ts +2 -0
  15. package/dist/cli/help.js +3 -1
  16. package/dist/config/config.d.ts +25 -8
  17. package/dist/config/config.js +41 -29
  18. package/dist/config/fleet-overlay.d.ts +3 -9
  19. package/dist/config/fleet-overlay.js +114 -19
  20. package/dist/config/fleet-why.d.ts +7 -0
  21. package/dist/config/fleet-why.js +5 -0
  22. package/dist/drivers/index.d.ts +15 -1
  23. package/dist/drivers/index.js +39 -10
  24. package/dist/drivers/orca.d.ts +119 -10
  25. package/dist/drivers/orca.js +781 -110
  26. package/dist/gates/baseline.d.ts +17 -3
  27. package/dist/gates/baseline.js +63 -15
  28. package/dist/gates/cache.d.ts +101 -0
  29. package/dist/gates/cache.js +401 -0
  30. package/dist/gates/llm.d.ts +3 -0
  31. package/dist/gates/llm.js +11 -0
  32. package/dist/gates/review.d.ts +28 -3
  33. package/dist/gates/review.js +106 -14
  34. package/dist/gates/run-gates.d.ts +9 -1
  35. package/dist/gates/run-gates.js +407 -102
  36. package/dist/gates/test-manifest.d.ts +128 -0
  37. package/dist/gates/test-manifest.js +463 -0
  38. package/dist/gates/test-reporter.d.ts +4 -0
  39. package/dist/gates/test-reporter.js +57 -0
  40. package/dist/graph/graph.d.ts +2 -0
  41. package/dist/graph/graph.js +45 -1
  42. package/dist/route/preference.d.ts +22 -1
  43. package/dist/route/preference.js +123 -25
  44. package/dist/route/router.js +31 -6
  45. package/dist/run/consult.js +5 -4
  46. package/dist/run/daemon.d.ts +15 -0
  47. package/dist/run/daemon.js +638 -167
  48. package/dist/run/execution-budget.d.ts +25 -0
  49. package/dist/run/execution-budget.js +142 -0
  50. package/dist/run/git.d.ts +50 -1
  51. package/dist/run/git.js +131 -12
  52. package/dist/run/journal.d.ts +16 -2
  53. package/dist/run/journal.js +115 -25
  54. package/dist/run/lease.d.ts +58 -0
  55. package/dist/run/lease.js +310 -0
  56. package/dist/run/merge.d.ts +2 -0
  57. package/dist/run/merge.js +91 -3
  58. package/dist/run/operator-state.d.ts +11 -0
  59. package/dist/run/operator-state.js +17 -3
  60. package/dist/run/recovery.d.ts +8 -0
  61. package/dist/run/recovery.js +25 -0
  62. package/dist/run/repair-selection.d.ts +12 -0
  63. package/dist/run/repair-selection.js +56 -0
  64. package/dist/run/stall.d.ts +6 -1
  65. package/dist/run/stall.js +60 -3
  66. package/dist/tui/cockpit/board.d.ts +96 -0
  67. package/dist/tui/cockpit/board.js +346 -0
  68. package/dist/tui/cockpit/decision-actions.js +2 -0
  69. package/dist/tui/cockpit/layout.d.ts +5 -1
  70. package/dist/tui/cockpit/layout.js +8 -3
  71. package/dist/tui/cockpit/live-runtime.js +71 -21
  72. package/dist/tui/cockpit/run-view.d.ts +7 -5
  73. package/dist/tui/cockpit/run-view.js +12 -11
  74. package/dist/tui/ink/fleet-app.d.ts +49 -27
  75. package/dist/tui/ink/fleet-app.js +229 -38
  76. package/package.json +2 -2
  77. package/skills/tickmarkr-overseer/SKILL.md +173 -113
  78. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
@@ -156,9 +156,53 @@ export function addEvidence(g, id, patch) {
156
156
  : t),
157
157
  };
158
158
  }
159
+ // OBS-1018: chain depth = number of downstream edges on the longest dependency path from a task to
160
+ // a leaf below it (a task nothing depends on is 0). Counted over the whole graph, status-blind: a
161
+ // root's critical path does not shrink because part of it already ran.
162
+ export function chainDepth(g) {
163
+ const dependents = new Map(g.tasks.map((t) => [t.id, []]));
164
+ for (const t of g.tasks)
165
+ for (const d of t.deps)
166
+ dependents.get(d)?.push(t.id);
167
+ const depth = new Map();
168
+ const visit = (id) => {
169
+ const known = depth.get(id);
170
+ if (known !== undefined)
171
+ return known;
172
+ const below = dependents.get(id) ?? [];
173
+ const value = below.length ? 1 + Math.max(...below.map(visit)) : 0; // acyclic: validateGraph rejects cycles
174
+ depth.set(id, value);
175
+ return value;
176
+ };
177
+ for (const t of g.tasks)
178
+ visit(t.id);
179
+ return depth;
180
+ }
181
+ // OBS-1018: admission is critical-path order — deepest chain root first, ties in declaration order.
182
+ // The daemon's dispatch loop slices this list unchanged. Stated limit: resume-restore of previously
183
+ // in-flight attempts keeps its own precedence (src/run/daemon.ts); only fresh admission is ordered here.
159
184
  export function readyTasks(g) {
160
185
  const done = new Set(g.tasks.filter((t) => t.status === "done").map((t) => t.id));
161
- return g.tasks.filter((t) => t.status === "pending" && t.deps.every((d) => done.has(d)));
186
+ const depth = chainDepth(g);
187
+ return g.tasks
188
+ .filter((t) => t.status === "pending" && t.deps.every((d) => done.has(d)))
189
+ .sort((a, b) => depth.get(b.id) - depth.get(a.id)); // Array#sort is stable: equal depths keep declaration order
190
+ }
191
+ // OBS-1018: the dispatch wave each pending task would enter at `concurrency` slots if every wave
192
+ // took one tick — computed by draining readyTasks, so plan and daemon can never disagree on order.
193
+ // Tasks already past pending carry no wave.
194
+ export function dispatchWaves(g, concurrency) {
195
+ const waves = new Map();
196
+ let sim = g;
197
+ for (let wave = 1;; wave++) {
198
+ const batch = readyTasks(sim).slice(0, Math.max(1, concurrency));
199
+ if (!batch.length)
200
+ return waves;
201
+ for (const t of batch) {
202
+ waves.set(t.id, wave);
203
+ sim = setStatus(sim, t.id, "done");
204
+ }
205
+ }
162
206
  }
163
207
  export function isComplete(g) {
164
208
  return g.tasks.every((t) => t.status === "done");
@@ -28,10 +28,31 @@ export declare function preferRanks(c: {
28
28
  shape: string;
29
29
  rank: number;
30
30
  }[];
31
+ export declare function isExplicitIdOfAliasFamily(candidateModel: string, alias: string): boolean;
32
+ export declare function entryMatchesChannel(entry: string, c: {
33
+ adapter: string;
34
+ model: string;
35
+ identity?: string;
36
+ }, allowFamilyMatching?: boolean): boolean;
37
+ export interface ExclusionScope {
38
+ scope: string;
39
+ path: string;
40
+ configPath: string;
41
+ entry: string;
42
+ by: "deny" | "allow";
43
+ }
44
+ export declare function exclusionCollector(c: {
45
+ adapter: string;
46
+ model: string;
47
+ identity?: string;
48
+ }, routingOrCfg: TickmarkrConfig["routing"] | TickmarkrConfig, role?: PreferenceRole): ExclusionScope[];
49
+ export declare const collectExclusions: typeof exclusionCollector;
50
+ export declare const exclusionProvenance: typeof exclusionCollector;
31
51
  export declare function disallowedBy(c: {
32
52
  adapter: string;
33
53
  model: string;
34
- }, routing: TickmarkrConfig["routing"], role?: PreferenceRole): Disallowed | null;
54
+ identity?: string;
55
+ }, routingOrCfg: TickmarkrConfig["routing"] | TickmarkrConfig, role?: PreferenceRole): Disallowed | null;
35
56
  export interface DenyPreferCollision {
36
57
  kind: "prefer" | "pin" | "pool";
37
58
  shape: string;
@@ -5,19 +5,24 @@ const PREFERENCE_ROLES = ["worker", "judge", "review", "consult"];
5
5
  /** Provider identity comes from the served model, not a gateway adapter's stamped vendor. */
6
6
  export function modelProvider(model, fallback = "unknown") {
7
7
  const id = model.toLowerCase();
8
- const prefix = id.includes("/") ? id.slice(0, id.indexOf("/")) : "";
9
- if (prefix === "openai" || prefix === "openai-codex" || /^(?:gpt|o\d)/.test(id))
8
+ const sepIdx = id.search(/[/:\s]/);
9
+ const prefix = sepIdx !== -1 ? id.slice(0, sepIdx) : "";
10
+ const remainder = sepIdx !== -1 ? id.slice(sepIdx + 1) : id;
11
+ if (prefix === "openai" || prefix === "openai-codex" || /^(?:gpt|o\d)/.test(remainder))
10
12
  return "openai";
11
- if (prefix === "anthropic" || /^(?:claude|opus|sonnet|haiku|fable)(?:-|$)/.test(id))
13
+ if (prefix === "anthropic" || /^(?:claude|opus|sonnet|haiku|fable)(?:-|$)/.test(remainder))
12
14
  return "anthropic";
13
- if (prefix === "google" || /^gemini(?:-|$)/.test(id))
15
+ if (prefix === "google" || /^gemini(?:-|$)/.test(remainder))
14
16
  return "google";
15
- if (prefix === "xai" || /^grok(?:-|$)/.test(id))
17
+ if (prefix === "xai" || prefix === "xai-oauth" || /^(?:grok|xai)(?:-|$)/.test(remainder) || id.startsWith("xai-oauth"))
16
18
  return "xai";
17
- if (["zai", "zhipu", "zai-coding-plan"].includes(prefix) || /^glm(?:-|$)/.test(id))
19
+ if (["zai", "zhipu", "zai-coding-plan"].includes(prefix) || /^glm(?:-|$)/.test(remainder))
18
20
  return "zhipu";
19
- if (["kimi-code", "moonshot"].includes(prefix) || /^kimi(?:-|$)/.test(id))
21
+ if (["kimi-code", "moonshot"].includes(prefix) || /^kimi(?:-|$)/.test(remainder))
20
22
  return "moonshot";
23
+ const normalizedFallback = fallback.toLowerCase();
24
+ if (normalizedFallback === "xai-oauth" || normalizedFallback === "xai")
25
+ return "xai";
21
26
  return fallback;
22
27
  }
23
28
  // Router naming remains explicit while sharing the review gate's exact function object.
@@ -51,7 +56,9 @@ export function excludedChannels(cfg, adapters, health) {
51
56
  if (!health[id]?.installed || !health[id]?.authed)
52
57
  continue;
53
58
  for (const c of channelsFromConfig(id, cfg)) {
54
- const d = disallowedBy(c, cfg.routing);
59
+ const identity = health[id]?.modelAuth?.[c.model]?.identity ?? health[id]?.modelIdentities?.[c.model];
60
+ const chan = identity ? { ...c, identity } : c;
61
+ const d = disallowedBy(chan, cfg.routing);
55
62
  if (d)
56
63
  out.push({ key: channelKey(c), d });
57
64
  }
@@ -75,25 +82,116 @@ export function preferRanks(c, cfg) {
75
82
  }
76
83
  return out;
77
84
  }
78
- // entries in either list accept adapter id, model id, or adapter:model (grammar A1)
79
- export function disallowedBy(c, routing, role = "worker") {
80
- const { allow, deny } = routing;
81
- const matches = (e) => e === c.adapter || e === c.model || e === channelKey(c);
82
- const workerDeny = role === "worker" ? deny?.workers : undefined;
83
- const d = [
84
- ...(deny?.adapters ?? []),
85
- ...(deny?.models ?? []),
86
- ...(workerDeny?.adapters ?? []),
87
- ...(workerDeny?.models ?? []),
88
- ].find(matches);
89
- if (d)
90
- return { by: "deny", entry: d };
85
+ export function isExplicitIdOfAliasFamily(candidateModel, alias) {
86
+ if (candidateModel === alias)
87
+ return true;
88
+ const tokens = candidateModel.toLowerCase().split(/[^a-z0-9]+/);
89
+ return tokens.includes(alias.toLowerCase());
90
+ }
91
+ export function entryMatchesChannel(entry, c, allowFamilyMatching = false) {
92
+ if (entry === c.adapter)
93
+ return true;
94
+ if (entry === c.model)
95
+ return true;
96
+ if (entry === channelKey(c))
97
+ return true;
98
+ if (c.identity) {
99
+ if (entry === c.identity)
100
+ return true;
101
+ if (entry === `${c.adapter}:${c.identity}`)
102
+ return true;
103
+ }
104
+ if (!c.identity && allowFamilyMatching) {
105
+ const colon = entry.indexOf(":");
106
+ if (colon !== -1) {
107
+ const entryAdapter = entry.slice(0, colon);
108
+ const entryModel = entry.slice(colon + 1);
109
+ if (entryAdapter === c.adapter && isExplicitIdOfAliasFamily(entryModel, c.model)) {
110
+ return true;
111
+ }
112
+ }
113
+ else {
114
+ if (isExplicitIdOfAliasFamily(entry, c.model)) {
115
+ return true;
116
+ }
117
+ }
118
+ }
119
+ return false;
120
+ }
121
+ export function exclusionCollector(c, routingOrCfg, role = "worker") {
122
+ const routing = "routing" in routingOrCfg ? routingOrCfg.routing : routingOrCfg;
123
+ const out = [];
124
+ const { allow, deny } = routing ?? {};
125
+ for (const entry of deny?.adapters ?? []) {
126
+ if (entryMatchesChannel(entry, c, true)) {
127
+ out.push({
128
+ scope: "routing.deny.adapters",
129
+ path: "routing.deny.adapters",
130
+ configPath: "routing.deny.adapters",
131
+ entry,
132
+ by: "deny",
133
+ });
134
+ }
135
+ }
136
+ for (const entry of deny?.models ?? []) {
137
+ if (entryMatchesChannel(entry, c, true)) {
138
+ out.push({
139
+ scope: "routing.deny.models",
140
+ path: "routing.deny.models",
141
+ configPath: "routing.deny.models",
142
+ entry,
143
+ by: "deny",
144
+ });
145
+ }
146
+ }
147
+ if (role === "worker") {
148
+ for (const entry of deny?.workers?.adapters ?? []) {
149
+ if (entryMatchesChannel(entry, c, true)) {
150
+ out.push({
151
+ scope: "routing.deny.workers.adapters",
152
+ path: "routing.deny.workers.adapters",
153
+ configPath: "routing.deny.workers.adapters",
154
+ entry,
155
+ by: "deny",
156
+ });
157
+ }
158
+ }
159
+ for (const entry of deny?.workers?.models ?? []) {
160
+ if (entryMatchesChannel(entry, c, true)) {
161
+ out.push({
162
+ scope: "routing.deny.workers.models",
163
+ path: "routing.deny.workers.models",
164
+ configPath: "routing.deny.workers.models",
165
+ entry,
166
+ by: "deny",
167
+ });
168
+ }
169
+ }
170
+ }
91
171
  if (allow) {
92
- const entries = [...(allow.adapters ?? []), ...(allow.models ?? [])];
93
- if (!entries.some(matches))
94
- return { by: "allow", entry: entries.join(", ") || "(empty allowlist)" };
172
+ const allAllow = [...(allow.adapters ?? []), ...(allow.models ?? [])];
173
+ const admitted = allAllow.some((e) => entryMatchesChannel(e, c, false));
174
+ if (!admitted) {
175
+ out.push({
176
+ scope: "routing.allow",
177
+ path: "routing.allow",
178
+ configPath: "routing.allow",
179
+ entry: allAllow.join(", ") || "(empty allowlist)",
180
+ by: "allow",
181
+ });
182
+ }
95
183
  }
96
- return null;
184
+ return out;
185
+ }
186
+ export const collectExclusions = exclusionCollector;
187
+ export const exclusionProvenance = exclusionCollector;
188
+ // entries in either list accept adapter id, model id, or adapter:model (grammar A1)
189
+ export function disallowedBy(c, routingOrCfg, role = "worker") {
190
+ const exclusions = exclusionCollector(c, routingOrCfg, role);
191
+ if (exclusions.length === 0)
192
+ return null;
193
+ const first = exclusions[0];
194
+ return { by: first.by, entry: first.entry };
97
195
  }
98
196
  // Delegates to route()'s preflightPrefer — doctor/resume preflight reuses the router grammar,
99
197
  // not a second colon/tier/model parser of the same config.
@@ -103,6 +103,32 @@ function withoutExcluded(channels, exclude) {
103
103
  // OBS-57: in-run demotion after consecutive no-trailer windows — route around poisoned channels for the rest of the run.
104
104
  return channels.filter((c) => !exclude.has(channelKey(c)));
105
105
  }
106
+ // Pools spread providers before tier, independently within each marginal-cost run. Stable
107
+ // cost sorting preserves declaration order when forming groups; gateways share the served
108
+ // model's provider, so adding a second transport does not buy that provider another turn.
109
+ function rankAnyPool(live, task) {
110
+ const sorted = [...live].sort((a, b) => marginalCostRank(a) - marginalCostRank(b));
111
+ const out = [];
112
+ for (let i = 0; i < sorted.length;) {
113
+ let j = i + 1;
114
+ while (j < sorted.length && marginalCostRank(sorted[j]) === marginalCostRank(sorted[i]))
115
+ j++;
116
+ const groups = new Map();
117
+ for (const c of sorted.slice(i, j)) {
118
+ const provider = routingModelProvider(c.model, c.vendor);
119
+ const group = groups.get(provider) ?? [];
120
+ group.push(c);
121
+ groups.set(provider, group);
122
+ }
123
+ const ordered = [...groups.values()];
124
+ const k = spreadOffset(task) % ordered.length;
125
+ for (const group of [...ordered.slice(k), ...ordered.slice(0, k)]) {
126
+ out.push(...group.sort((a, b) => TIER_RANK[a.tier] - TIER_RANK[b.tier]));
127
+ }
128
+ i = j;
129
+ }
130
+ return out;
131
+ }
106
132
  const maybeSlaLint = (lints, task, profile, slaMinutes, c) => {
107
133
  if (slaMinutes === undefined || !profile)
108
134
  return;
@@ -206,21 +232,20 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
206
232
  // live preserves pool declaration order — ordered mode and any-mode stable-sort ties depend on it
207
233
  const live = pool.channels
208
234
  .map((id) => channels.find((c) => channelKey(c) === id))
209
- .filter((c) => c !== undefined);
235
+ .filter((c) => c !== undefined && (!taskFloor || TIER_RANK[c.tier] >= TIER_RANK[taskFloor]));
210
236
  if (!live.length) {
211
- throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel — declared: ${pool.channels.join(", ")}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
237
+ throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel${taskFloor ? ` at task floor ${taskFloor}` : ""} — declared: ${pool.channels.join(", ")}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
212
238
  }
213
- // ponytail: no learned/explore/spread inside pools in v1 — a pool pick is fully static; the
214
- // upgrade path is feeding the pool-filtered set through the learned path below.
239
+ // No learned/explore inside pools: any spreads deterministically; ordered keeps declaration order.
215
240
  const chosen = pool.mode === "ordered"
216
241
  ? live[0]
217
- : [...live].sort((a, b) => marginalCostRank(a) - marginalCostRank(b) || TIER_RANK[a.tier] - TIER_RANK[b.tier])[0];
242
+ : rankAnyPool(live, task)[0];
218
243
  lintMapPinFloor(chosen.tier, "pool");
219
244
  maybeSlaLint(lints, task, profile, slaMinutes, chosen);
220
245
  return { assignment: toAssignment(chosen), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pool ${pool.mode} ${channelKey(chosen)} (config routing.map)` };
221
246
  }
222
247
  const baseTier = floor ?? "cheap";
223
- // D-04: a task floor is hard only on floor/auto paths; the map-pin branch above stays supreme.
248
+ // A task floor is hard on pool and floor/auto paths; the map-pin branch above stays supreme.
224
249
  const minTier = taskFloor && TIER_RANK[taskFloor] > TIER_RANK[baseTier] ? taskFloor : baseTier;
225
250
  if (prefActive)
226
251
  for (const p of prefer ?? [])
@@ -164,10 +164,11 @@ opts = {}) {
164
164
  out = r.stdout + r.stderr;
165
165
  }
166
166
  else {
167
- // Preserve the adapter contract for CLIs that cannot seed their TUI: supported adapters use
168
- // the interactive form, while a declared null keeps the existing visible print fallback.
169
- const command = adapter.interactiveCommand(promptFile, seatModel)
170
- ?? adapter.headlessCommand(promptFile, seatModel);
167
+ // OBS-1009: the pane runs the HEADLESS command. A seeded interactive session answers on screen
168
+ // but never exits, so the exit trailer after it never runs, the wait runs to the stall cap and
169
+ // the 300-line read is a TUI frame. The headless command exits, the trailer prints, and the
170
+ // pane is harvested through dewrap exactly like a judge or review seat.
171
+ const command = adapter.headlessCommand(promptFile, seatModel);
171
172
  // T8: role-first pane name for fleet visibility (consult · T2); consultSeq stays on the dossier artifact only
172
173
  const slot = await driver.slot(cwd, gatePaneName("consult", d.taskId), {
173
174
  label: `CONSULT ${d.taskId}`,
@@ -6,6 +6,8 @@ import { type Baseline } from "../gates/baseline.js";
6
6
  import type { GateResult } from "../gates/types.js";
7
7
  import { Journal, type JournalEvent } from "./journal.js";
8
8
  export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
9
+ export declare function setAttemptHardTimeoutMsForTests(ms: number): void;
10
+ export declare function resetAttemptHardTimeoutMsForTests(): void;
9
11
  export { harvestCpuFlatWindowMs, resetHarvestCpuFlatMsForTests, setHarvestCpuFlatMsForTests, workerTreeCpuMs, } from "./stall.js";
10
12
  export interface RunOptions {
11
13
  runId?: string;
@@ -126,6 +128,19 @@ export declare const WORKER_STARTUP_WINDOW_BYTES: number;
126
128
  /** Test seam — exercises both sides of the startup boundary without a production-length fixture. */
127
129
  export declare function setWorkerStartupWindowMsForTests(ms: number): void;
128
130
  export declare function resetWorkerStartupWindowMsForTests(): void;
131
+ export interface StartupFailureEvidence {
132
+ matchedBytes: string;
133
+ offset: number;
134
+ row: string;
135
+ rowNumber: number;
136
+ }
137
+ /** One seat, one startup prefix. Once execution or its input box is seen, later panes are ineligible. */
138
+ export declare class StartupFailureDetector {
139
+ private readonly adapter;
140
+ private closed;
141
+ constructor(adapter: Pick<WorkerAdapter, "inputBox" | "harnessBannerRows">);
142
+ sample(output: string, launchedAt: number | undefined): StartupFailureEvidence | undefined;
143
+ }
129
144
  /** Test seam — shrink the quota-banner silence gate without minute-long sleeps. */
130
145
  export declare function setQuotaBannerSilentMsForTests(ms: number): void;
131
146
  export declare function resetQuotaBannerSilentMsForTests(): void;