@vibes.diy/api-svc 14.3.91 → 14.3.93

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.
@@ -5,11 +5,23 @@ import { createBlockStream, isCodeEnd, } from "@vibes.diy/call-ai-v2";
5
5
  import { lazyType } from "@vibes.diy/api-types/lazy-type";
6
6
  import { createBlockAccumulator, createStreamingResolver, resolveCodeBlocks } from "./prompt-streaming.js";
7
7
  import { synthesizeImports } from "./import-synth.js";
8
- import { checkOpens, OPENS_ADVISORY_CLASSES } from "./opens-check.js";
8
+ import { checkOpens, collapseOpensFindings, newOpensFindings, opensBaseline, opensFindingKey, OPENS_ADVISORY_CLASSES, } from "./opens-check.js";
9
+ import { fillDefect, gapLadder, identFill, intactnessDefect, mergeGaps, parseModule, regionFill, regionMessages, splice, } from "./repair-ladder.js";
9
10
  import { saveRepairFindings } from "./save-repair.js";
10
11
  import { composeRepairAsk, renderRepairFiles } from "./save-repair-io.js";
11
12
  import { createPromptAssetFetch, promptsPkgBaseUrl } from "./prompt-asset-fetch.js";
12
13
  import { loadActiveSettings, lookOptionForTurn } from "./prompt-assembly.js";
14
+ import { MercuryCircuitBreaker } from "./mercury-resilience.js";
15
+ export const INSTANT_MERCURY_FANOUT = 4;
16
+ export const INSTANT_REPAIR_MODE = "router";
17
+ export const INSTANT_LADDER_ROUNDS = 4;
18
+ export const INSTANT_THROTTLED_ROUNDS_MAX = 6;
19
+ export const INSTANT_REPAIR_ROWS_MAX = 24;
20
+ export const INSTANT_REEMIT_CLASSES = new Set(["out-of-scope-identifier", "undefined-identifier"]);
21
+ export function instantMercuryMaxAttempts(mode) {
22
+ return mode === "reemit" ? 3 : 1 + INSTANT_LADDER_ROUNDS + 1;
23
+ }
24
+ export const INSTANT_MERCURY_MAX_ATTEMPTS = instantMercuryMaxAttempts(INSTANT_REPAIR_MODE);
13
25
  export const INSTANT_MERCURY_MODEL = "inception/mercury-2.5";
14
26
  export const INSTANT_HAIKU_MODEL = "anthropic/claude-haiku-5.5";
15
27
  export const INSTANT_MAX_OUTPUT_TOKENS = 32_000;
@@ -17,6 +29,7 @@ export const INSTANT_MERCURY_CALL_MS = 40_000;
17
29
  export const INSTANT_HAIKU_CALL_MS = 90_000;
18
30
  export const INSTANT_STAGE_BUDGET_MS = 150_000;
19
31
  export const INSTANT_MAX_ATTEMPTS = 5;
32
+ export const INSTANT_MERCURY_BREAKER = new MercuryCircuitBreaker();
20
33
  export function instantModelChoice(name) {
21
34
  switch (name) {
22
35
  case "mercury":
@@ -26,9 +39,16 @@ export function instantModelChoice(name) {
26
39
  reasoning: { effort: "none" },
27
40
  reasoningEffort: "none",
28
41
  callMs: INSTANT_MERCURY_CALL_MS,
42
+ maxAttempts: INSTANT_MERCURY_MAX_ATTEMPTS,
29
43
  };
30
44
  case "haiku":
31
- return { name, id: INSTANT_HAIKU_MODEL, reasoning: { enabled: false }, callMs: INSTANT_HAIKU_CALL_MS };
45
+ return {
46
+ name,
47
+ id: INSTANT_HAIKU_MODEL,
48
+ reasoning: { enabled: false },
49
+ callMs: INSTANT_HAIKU_CALL_MS,
50
+ maxAttempts: INSTANT_MAX_ATTEMPTS,
51
+ };
32
52
  }
33
53
  }
34
54
  export const INSTANT_PARAGRAPH = "Build the interaction this sentence names, fully working, with realistic launch content. " +
@@ -149,11 +169,23 @@ export function autofixAndCheck(args) {
149
169
  const fixed = synthesizeImports({ vfs: args.vfs, paths: args.changed.filter((path) => args.vfs.has(path)), look: args.look });
150
170
  const files = [...fixed.vfs].map(([filename, content]) => ({ type: "code-block", filename, lang: "jsx", content }));
151
171
  const parse = saveRepairFindings(files, new Set(args.changed)).map((finding) => `${finding.path.replace(/^\/+/, "")}: ${finding.message}`);
152
- const opens = checkOpens(fixed.vfs)
153
- .findings.filter((finding) => OPENS_ADVISORY_CLASSES.has(finding.class) === false)
154
- .map((finding) => `${finding.file.replace(/^\/+/, "")}:${finding.line}: ${finding.message}`);
172
+ const verdict = checkOpens(fixed.vfs);
173
+ const blocking = newOpensFindings(verdict, args.baseline ?? new Set());
174
+ const line = (finding) => `${finding.file.replace(/^\/+/, "")}:${finding.line}: ${finding.message}`;
175
+ const opens = collapseOpensFindings(blocking).map(line);
176
+ const advisory = verdict.findings
177
+ .filter((finding) => OPENS_ADVISORY_CLASSES.has(finding.class) === false && blocking.includes(finding) === false)
178
+ .map(line);
155
179
  const findings = [...parse, ...opens];
156
- return { vfs: fixed.vfs, opens: findings.length === 0, findings, notes: fixed.notes };
180
+ return {
181
+ vfs: fixed.vfs,
182
+ opens: findings.length === 0,
183
+ findings,
184
+ blocking,
185
+ unlocated: parse.length,
186
+ advisory,
187
+ notes: fixed.notes,
188
+ };
157
189
  }
158
190
  const completionBody = lazyType(() => type({
159
191
  "model?": "string",
@@ -212,6 +244,13 @@ export function instantModelCallFor(vctx) {
212
244
  });
213
245
  };
214
246
  }
247
+ function isThrottle(error) {
248
+ return /\bstatus 429\b/.test(error.message);
249
+ }
250
+ function throttleWaitMs(n, random) {
251
+ const exp = Math.min(8_000, 1_000 * 2 ** Math.max(0, n - 1));
252
+ return Math.round(exp / 2 + random() * (exp / 2));
253
+ }
215
254
  async function callWithin(call, req, cancel) {
216
255
  const abort = new AbortController();
217
256
  const timer = setTimeout(function expire() {
@@ -255,39 +294,267 @@ function pauseUnlessCancelled(ms, cancel) {
255
294
  done();
256
295
  });
257
296
  }
297
+ function findingIds(findings) {
298
+ const seen = new Map();
299
+ return findings.map((finding) => {
300
+ const key = opensFindingKey(finding);
301
+ const n = seen.get(key) ?? 0;
302
+ seen.set(key, n + 1);
303
+ return `${key}#${n}`;
304
+ });
305
+ }
306
+ function countByKey(findings) {
307
+ const out = new Map();
308
+ for (const finding of findings)
309
+ out.set(opensFindingKey(finding), (out.get(opensFindingKey(finding)) ?? 0) + 1);
310
+ return out;
311
+ }
312
+ function ladderable(path) {
313
+ return /\.(jsx?|mjs)$/.test(path);
314
+ }
315
+ export function repairIntactness(baseline, after, exempt = new Set()) {
316
+ for (const [path, was] of baseline.files) {
317
+ const now = after.get(path);
318
+ if (now === undefined || now === was || isSourcePath(path) === false)
319
+ continue;
320
+ const defect = intactnessDefect(was, now, exempt, baseline.lines);
321
+ if (defect !== undefined) {
322
+ return { path: path.replace(/^\/+/, ""), defect, ratio: now.split("\n").length / Math.max(1, was.split("\n").length) };
323
+ }
324
+ }
325
+ return undefined;
326
+ }
327
+ export function intactnessReask(defect) {
328
+ switch (defect) {
329
+ case "shrank":
330
+ return "the reply: it left part of the app out — write the whole file, every line that already worked";
331
+ case "grew":
332
+ return "the reply: it rewrote far more than the fix needed — change only what the findings require";
333
+ case "names":
334
+ return "the reply: it dropped functions, components or variables the app still uses — keep every one that already worked";
335
+ case "loop":
336
+ return "the reply: it repeats the same lines over and over — write each line once";
337
+ }
338
+ }
339
+ function isSourcePath(path) {
340
+ return /\.(jsx?|tsx?|mjs)$/.test(path);
341
+ }
342
+ async function ladderRound(args) {
343
+ const t0 = args.now();
344
+ const ids = findingIds(args.findings);
345
+ const items = [];
346
+ for (let i = 0; i < args.findings.length; i++) {
347
+ const finding = args.findings[i];
348
+ const src = args.current.get(finding.file);
349
+ if (src === undefined || ladderable(finding.file) === false)
350
+ continue;
351
+ const program = parseModule(src);
352
+ if (program === undefined)
353
+ continue;
354
+ const ladder = gapLadder(src, program, finding);
355
+ const rung = args.rungs.get(ids[i]) ?? 0;
356
+ const gap = ladder[rung];
357
+ if (gap !== undefined)
358
+ items.push({ id: ids[i], finding, gap });
359
+ }
360
+ const groups = [...new Set(items.map((item) => item.finding.file))].flatMap((file) => {
361
+ const src = args.current.get(file) ?? "";
362
+ const inFile = items.filter((item) => item.finding.file === file);
363
+ return mergeGaps(src, inFile.map((item) => item.gap)).map((merged) => ({ file, src, gap: merged.gap, members: merged.members.map((i) => inFile[i]) }));
364
+ });
365
+ if (groups.length === 0) {
366
+ return { round: { kind: "ladder", ms: 0, gaps: [] }, usage: [], touched: [], noGaps: true, throttled: false, cancelled: false };
367
+ }
368
+ const usage = [];
369
+ const fills = await Promise.all(groups.map(async (group) => {
370
+ const messages = regionMessages({
371
+ system: args.system,
372
+ path: group.file,
373
+ src: group.src,
374
+ gap: group.gap,
375
+ findings: group.members.map((member) => member.finding),
376
+ userFirst: args.model.name === "haiku",
377
+ });
378
+ const answer = await callWithin(args.call, { model: args.model, messages, timeoutMs: args.timeoutMs }, args.cancel);
379
+ if (answer.kind === "cancelled")
380
+ return { result: "rejected:cancelled", climbs: false };
381
+ if (answer.kind === "deadline")
382
+ return { result: "rejected:deadline", climbs: false };
383
+ if (answer.reply.isErr()) {
384
+ return isThrottle(answer.reply.Err())
385
+ ? { result: "rejected:throttled", climbs: false }
386
+ : { result: "rejected:error", climbs: false };
387
+ }
388
+ const reply = answer.reply.Ok();
389
+ if (reply.usage !== undefined)
390
+ usage.push(reply.usage);
391
+ const raw = regionFill(reply.text, group.src.slice(group.gap.start, group.gap.end));
392
+ if (raw === undefined)
393
+ return { result: "rejected:no-fill", climbs: true };
394
+ const fill = group.gap.node === "Identifier" ? identFill(group.src, group.gap, raw) : raw;
395
+ const defect = fillDefect(group.src, group.gap, fill);
396
+ if (defect !== undefined)
397
+ return { result: `rejected:${defect}`, climbs: true };
398
+ return { fill: group.gap.node === "Identifier" ? fill.trim() : fill, result: "accepted", climbs: false };
399
+ }));
400
+ const before = countByKey(args.findings);
401
+ const kept = [];
402
+ let gated;
403
+ let overridable;
404
+ const results = fills.map((f) => f.result);
405
+ for (let i = 0; i < groups.length; i++) {
406
+ const fill = fills[i].fill;
407
+ if (fill === undefined)
408
+ continue;
409
+ const trial = [...kept, { group: groups[i], fill }];
410
+ const vfs = new Map(args.current);
411
+ for (const file of new Set(trial.map((t) => t.group.file))) {
412
+ const inFile = trial.filter((t) => t.group.file === file);
413
+ vfs.set(file, splice(args.current.get(file) ?? "", inFile.map((t) => t.group.gap), inFile.map((t) => t.fill)));
414
+ }
415
+ const touched = [...new Set(trial.map((t) => t.group.file))];
416
+ const checked = autofixAndCheck({ vfs, changed: [...args.changed, ...touched], look: args.look, baseline: args.baseline });
417
+ const after = countByKey(checked.blocking);
418
+ const added = checked.unlocated > 0 || [...after].some(([key, n]) => n > (before.get(key) ?? 0));
419
+ const served = countByKey(groups[i].members.map((member) => member.finding));
420
+ const gone = [...served].every(([key, n]) => (after.get(key) ?? 0) <= (before.get(key) ?? 0) - n);
421
+ const gutted = repairIntactness(args.preRepair, checked.vfs, args.exempt);
422
+ if (added)
423
+ results[i] = "rejected:regress";
424
+ else if (gone === false)
425
+ results[i] = "rejected:still";
426
+ else if (gutted !== undefined) {
427
+ results[i] = `rejected:gutted:${gutted.defect}`;
428
+ if (checked.opens && (overridable === undefined || Math.abs(1 - gutted.ratio) < Math.abs(1 - overridable.ratio))) {
429
+ overridable = { gated: checked, ratio: gutted.ratio, touched };
430
+ }
431
+ }
432
+ else {
433
+ kept.push({ group: groups[i], fill });
434
+ gated = checked;
435
+ }
436
+ }
437
+ for (let i = 0; i < groups.length; i++) {
438
+ const refused = results[i] !== "accepted" && (fills[i].climbs || fills[i].fill !== undefined);
439
+ if (refused === false)
440
+ continue;
441
+ for (const member of groups[i].members)
442
+ args.rungs.set(member.id, (args.rungs.get(member.id) ?? 0) + 1);
443
+ }
444
+ return {
445
+ round: {
446
+ kind: "ladder",
447
+ ms: args.now() - t0,
448
+ gaps: groups.slice(0, 8).map((group, i) => ({
449
+ rung: group.gap.level,
450
+ lines: group.gap.endLine - group.gap.startLine + 1,
451
+ result: results[i],
452
+ })),
453
+ },
454
+ usage,
455
+ ...(gated === undefined ? {} : { gated }),
456
+ ...(overridable === undefined ? {} : { overridable }),
457
+ touched: [...new Set(kept.map((k) => k.group.file))],
458
+ noGaps: false,
459
+ throttled: results.length > 0 && results.every((r) => r === "rejected:throttled") && usage.length === 0,
460
+ cancelled: results.some((r) => r === "rejected:cancelled"),
461
+ };
462
+ }
258
463
  export async function runInstantCandidate(args) {
464
+ const baseline = args.baseline ?? opensBaseline(args.base);
259
465
  const now = args.now ?? Date.now;
260
- const maxAttempts = args.maxAttempts ?? INSTANT_MAX_ATTEMPTS;
466
+ const random = args.random ?? Math.random;
467
+ const repairMode = args.repairMode ?? INSTANT_REPAIR_MODE;
468
+ let route = repairMode === "router" ? undefined : repairMode;
469
+ const ladderBudget = () => (route === "ladder" ? (args.ladderRounds ?? INSTANT_LADDER_ROUNDS) : 0);
470
+ const modeLabel = () => (repairMode === "router" ? `router:${route ?? "none"}` : repairMode);
261
471
  const { model } = args.spec;
472
+ const maxAttempts = args.maxAttempts ?? model.maxAttempts;
473
+ const system = args.spec.messages.filter((message) => message.role === "system");
262
474
  const usage = [];
475
+ const repairs = [];
263
476
  const messages = [...args.spec.messages];
477
+ const rungs = new Map();
264
478
  let attempts = 0;
479
+ let throttled = 0;
480
+ let throttledRounds = 0;
481
+ let repairsDropped = 0;
265
482
  let providerErrors = 0;
483
+ let ladderRun = 0;
266
484
  let firstFindings = [];
485
+ let liveFindings = [];
267
486
  let lastError = "no attempt";
268
487
  let current = args.base;
269
488
  let changed = new Set();
489
+ let repairing = false;
490
+ const editBase = sourceFiles(args.base);
491
+ let preRepair = editBase.size > 0 ? { files: editBase, lines: "shrink" } : undefined;
492
+ const repairNames = new Set();
493
+ let overridable;
494
+ let reemitStartedAt;
495
+ function record(row) {
496
+ if (repairs.length < INSTANT_REPAIR_ROWS_MAX)
497
+ repairs.push(row);
498
+ else
499
+ repairsDropped++;
500
+ }
270
501
  function end(gate, extra) {
271
502
  return {
272
503
  name: model.name,
504
+ candidate: args.spec.label ?? model.name,
273
505
  model: model.id,
274
506
  gate,
275
507
  attempts,
508
+ throttled,
276
509
  ms: now() - args.startedAt,
277
510
  usage,
278
511
  abortedInFlight: false,
279
512
  findings: firstFindings,
513
+ repairs,
514
+ ...(repairsDropped > 0 ? { repairsDropped } : {}),
280
515
  ...extra,
281
516
  };
282
517
  }
518
+ function noteOverridable(candidate) {
519
+ if (overridable === undefined || Math.abs(1 - candidate.ratio) < Math.abs(1 - overridable.ratio))
520
+ overridable = candidate;
521
+ }
522
+ function exhausted(error) {
523
+ if (overridable === undefined)
524
+ return end("fail", { error });
525
+ record({ kind: overridable.kind, mode: modeLabel(), ms: 0, result: "accepted:gate-overridden" });
526
+ return end("pass", { vfs: overridable.gated.vfs, changed: [...new Set([...changed, ...overridable.touched])] });
527
+ }
528
+ function waitWithin(ms) {
529
+ return pauseUnlessCancelled(Math.max(0, Math.min(ms, args.deadlineAt - now())), args.cancel);
530
+ }
531
+ const paidLeft = () => maxAttempts - (attempts - throttled);
532
+ function queueReemit(findings, lastReply) {
533
+ const shown = lastReply ??
534
+ renderRepairFiles({
535
+ vfs: new Map([...changed].flatMap((path) => {
536
+ const content = current.get(path);
537
+ return content === undefined ? [] : [[path, content]];
538
+ })),
539
+ intro: "Here is the app.",
540
+ });
541
+ messages.push({ role: "assistant", content: [{ type: "text", text: shown }] });
542
+ messages.push({ role: "user", content: [{ type: "text", text: composeRepairAsk(findings) }] });
543
+ reemitStartedAt = now();
544
+ }
283
545
  for (;;) {
284
546
  if (args.cancel.aborted)
285
547
  return end("cancelled", {});
286
548
  const left = args.deadlineAt - now();
287
549
  if (left <= 0)
288
- return end("fail", { error: `stage budget spent after ${attempts} call(s): ${lastError}` });
289
- if (attempts >= maxAttempts)
290
- return end("fail", { error: `${attempts} calls without a passing answer: ${lastError}` });
550
+ return exhausted(`stage budget spent after ${attempts} call(s): ${lastError}`);
551
+ if (paidLeft() <= 0)
552
+ return exhausted(`${attempts} calls without a passing answer: ${lastError}`);
553
+ const hold = args.throttle === undefined ? 0 : Math.min(args.throttle.holdUntil - now(), left);
554
+ if (hold > 0) {
555
+ await pauseUnlessCancelled(hold, args.cancel);
556
+ continue;
557
+ }
291
558
  attempts++;
292
559
  const answer = await callWithin(args.call, { model, messages, timeoutMs: Math.min(model.callMs, left) }, args.cancel);
293
560
  if (answer.kind === "cancelled")
@@ -299,34 +566,138 @@ export async function runInstantCandidate(args) {
299
566
  if (answer.reply.isErr()) {
300
567
  lastError = answer.reply.Err().message;
301
568
  providerErrors++;
302
- await pauseUnlessCancelled(Math.min(8_000, 1_000 * 2 ** (providerErrors - 1)), args.cancel);
569
+ if (isThrottle(answer.reply.Err())) {
570
+ throttled++;
571
+ const wait = throttleWaitMs(providerErrors, random);
572
+ if (args.throttle === undefined)
573
+ await waitWithin(wait);
574
+ else
575
+ args.throttle.holdUntil = Math.max(args.throttle.holdUntil, now() + wait);
576
+ continue;
577
+ }
578
+ await waitWithin(Math.min(8_000, 1_000 * 2 ** (providerErrors - 1)));
303
579
  continue;
304
580
  }
305
581
  providerErrors = 0;
306
582
  const reply = answer.reply.Ok();
307
583
  if (reply.usage !== undefined)
308
584
  usage.push(reply.usage);
585
+ const reemitMs = reemitStartedAt === undefined ? undefined : now() - reemitStartedAt;
586
+ reemitStartedAt = undefined;
309
587
  const parsed = await parseCandidateText(reply.text, current);
310
588
  if (parsed.isErr()) {
311
589
  lastError = parsed.Err().message;
590
+ repairing = true;
591
+ preRepair ??= { files: current, lines: "none" };
592
+ if (reemitMs !== undefined)
593
+ record({ kind: "reemit", mode: modeLabel(), ms: reemitMs, result: "rejected:no-file" });
312
594
  messages.push({ role: "assistant", content: [{ type: "text", text: reply.text }] });
313
595
  messages.push({ role: "user", content: [{ type: "text", text: composeRepairAsk([`the reply: ${lastError}`]) }] });
314
596
  continue;
315
597
  }
316
- changed = new Set([...changed, ...parsed.Ok().changed]);
317
- const checked = autofixAndCheck({ vfs: parsed.Ok().vfs, changed: [...changed], look: args.look });
598
+ const nextChanged = new Set([...changed, ...parsed.Ok().changed]);
599
+ const checked = autofixAndCheck({ vfs: parsed.Ok().vfs, changed: [...nextChanged], look: args.look, baseline });
318
600
  const gated = parsed.Ok().applyErrors.length === 0
319
601
  ? checked
320
602
  : { ...checked, opens: false, findings: [...parsed.Ok().applyErrors, ...checked.findings] };
603
+ const applyErrors = parsed.Ok().applyErrors.length;
321
604
  if (gated.opens) {
322
- return end("pass", { vfs: gated.vfs, changed: [...changed] });
605
+ const gutted = repairing && preRepair !== undefined ? repairIntactness(preRepair, gated.vfs, repairNames) : undefined;
606
+ if (gutted === undefined) {
607
+ if (reemitMs !== undefined)
608
+ record({ kind: "reemit", mode: modeLabel(), ms: reemitMs, result: "accepted" });
609
+ return end("pass", { vfs: gated.vfs, changed: [...nextChanged] });
610
+ }
611
+ record({ kind: "reemit", mode: modeLabel(), ms: reemitMs ?? 0, result: `rejected:gutted:${gutted.defect}` });
612
+ noteOverridable({ gated, ratio: gutted.ratio, touched: [...nextChanged], kind: "reemit" });
613
+ lastError = `the repair failed the intactness gate (${gutted.path}: ${gutted.defect})`;
614
+ queueReemit([...liveFindings, intactnessReask(gutted.defect)], reply.text);
615
+ continue;
616
+ }
617
+ if (reemitMs !== undefined) {
618
+ record({ kind: "reemit", mode: modeLabel(), ms: reemitMs, result: "rejected:still" });
619
+ rungs.clear();
323
620
  }
621
+ preRepair ??= { files: gated.vfs, lines: gated.unlocated === 0 && applyErrors === 0 ? "both" : "none" };
622
+ repairing = true;
623
+ changed = nextChanged;
324
624
  if (firstFindings.length === 0)
325
625
  firstFindings = gated.findings.slice(0, 6);
326
626
  lastError = gated.findings.slice(0, 3).join("; ");
627
+ liveFindings = gated.findings;
628
+ for (const finding of gated.blocking)
629
+ if (finding.name !== undefined)
630
+ repairNames.add(finding.name);
327
631
  current = gated.vfs;
328
- messages.push({ role: "assistant", content: [{ type: "text", text: reply.text }] });
329
- messages.push({ role: "user", content: [{ type: "text", text: composeRepairAsk(gated.findings) }] });
632
+ route ??=
633
+ gated.blocking.some((finding) => INSTANT_REEMIT_CLASSES.has(finding.class)) ||
634
+ gated.unlocated > 0 ||
635
+ parsed.Ok().applyErrors.length > 0
636
+ ? "reemit"
637
+ : "ladder";
638
+ let state = gated;
639
+ while (ladderRun < ladderBudget() &&
640
+ paidLeft() > 1 &&
641
+ throttledRounds < INSTANT_THROTTLED_ROUNDS_MAX &&
642
+ applyErrors === 0 &&
643
+ state.unlocated === 0) {
644
+ if (args.cancel.aborted)
645
+ return end("cancelled", {});
646
+ const roundLeft = args.deadlineAt - now();
647
+ if (roundLeft <= 0)
648
+ return exhausted(`stage budget spent after ${attempts} call(s): ${lastError}`);
649
+ const round = await ladderRound({
650
+ call: args.call,
651
+ model,
652
+ system,
653
+ cancel: args.cancel,
654
+ timeoutMs: Math.min(model.callMs, roundLeft),
655
+ current,
656
+ findings: state.blocking,
657
+ rungs,
658
+ changed,
659
+ look: args.look,
660
+ baseline,
661
+ preRepair: preRepair ?? { files: current, lines: "none" },
662
+ exempt: repairNames,
663
+ ...(args.throttle === undefined ? {} : { throttle: args.throttle }),
664
+ now,
665
+ });
666
+ if (round.noGaps)
667
+ break;
668
+ attempts++;
669
+ ladderRun++;
670
+ usage.push(...round.usage);
671
+ record({ ...round.round, mode: modeLabel() });
672
+ if (round.cancelled && args.cancel.aborted)
673
+ return end("cancelled", { abortedInFlight: true });
674
+ if (round.overridable !== undefined)
675
+ noteOverridable({ ...round.overridable, kind: "ladder" });
676
+ if (round.throttled) {
677
+ throttled++;
678
+ ladderRun--;
679
+ throttledRounds++;
680
+ const wait = throttleWaitMs(++providerErrors, random);
681
+ if (args.throttle !== undefined)
682
+ args.throttle.holdUntil = Math.max(args.throttle.holdUntil, now() + wait);
683
+ await waitWithin((args.throttle?.holdUntil ?? now() + wait) - now());
684
+ continue;
685
+ }
686
+ providerErrors = 0;
687
+ if (round.gated === undefined)
688
+ continue;
689
+ changed = new Set([...changed, ...round.touched]);
690
+ current = round.gated.vfs;
691
+ state = round.gated;
692
+ if (state.opens)
693
+ return end("pass", { vfs: state.vfs, changed: [...changed] });
694
+ lastError = state.findings.slice(0, 3).join("; ");
695
+ liveFindings = state.findings;
696
+ for (const finding of state.blocking)
697
+ if (finding.name !== undefined)
698
+ repairNames.add(finding.name);
699
+ }
700
+ queueReemit(state.findings, state === gated ? reply.text : undefined);
330
701
  }
331
702
  }
332
703
  export async function raceInstantCandidates(args) {
@@ -358,20 +729,24 @@ export async function raceInstantCandidates(args) {
358
729
  export function instantReceipt(race) {
359
730
  return {
360
731
  candidates: race.outcomes.map((outcome) => ({
732
+ candidate: outcome.candidate,
361
733
  model: outcome.model,
362
734
  ms: outcome.ms,
363
735
  gate: outcome.gate,
364
736
  attempts: outcome.attempts,
737
+ ...(outcome.throttled > 0 ? { throttled: outcome.throttled } : {}),
738
+ ...(outcome.repairs.length > 0 ? { repairs: outcome.repairs } : {}),
739
+ ...(outcome.repairsDropped === undefined ? {} : { repairsDropped: outcome.repairsDropped }),
365
740
  ...(outcome.abortedInFlight ? { abortedInFlight: true } : {}),
366
741
  })),
367
- ...(race.winner === undefined ? {} : { winner: race.winner.model }),
742
+ ...(race.winner === undefined ? {} : { winner: race.winner.model, winnerCandidate: race.winner.candidate }),
368
743
  };
369
744
  }
370
745
  export function instantTurnText(race) {
371
746
  const winner = race.winner;
372
747
  if (winner === undefined || winner.vfs === undefined || winner.changed === undefined) {
373
748
  const why = race.outcomes
374
- .map((outcome) => `${outcome.name}: ${outcome.error ?? outcome.findings.slice(0, 2).join("; ")}`)
749
+ .map((outcome) => `${outcome.candidate}: ${outcome.error ?? outcome.findings.slice(0, 2).join("; ")}`)
375
750
  .join("\n");
376
751
  return `The instant build wrote no files that open, so nothing was saved.\n\n${why}\n`;
377
752
  }
@@ -415,29 +790,72 @@ export function mercuryMessages(args) {
415
790
  { role: "user", content: [{ type: "text", text: ask }] },
416
791
  ];
417
792
  }
418
- export async function runInstantBuild(args) {
419
- const { assembly } = args;
420
- const specs = [
421
- ...(assembly.mercurySystem === undefined
422
- ? []
423
- : [
424
- {
425
- model: instantModelChoice("mercury"),
426
- messages: mercuryMessages({ system: assembly.mercurySystem, instruction: assembly.instruction, base: assembly.base }),
427
- },
428
- ]),
429
- { model: instantModelChoice("haiku"), messages: args.haikuMessages },
430
- ];
431
- const race = await raceInstantCandidates({
432
- specs,
433
- budgetMs: args.budgetMs ?? INSTANT_STAGE_BUDGET_MS,
434
- turn: args.turn,
435
- run: ({ spec, cancel, startedAt, deadlineAt }) => runInstantCandidate({ call: args.call, spec, base: assembly.base, look: assembly.look, cancel, startedAt, deadlineAt }),
436
- });
793
+ function crashedCandidate(spec, startedAt, e) {
437
794
  return {
438
- response: instantResponse({ race, promptId: args.promptId }),
439
- receipt: instantReceipt(race),
440
- model: race.winner?.model ?? INSTANT_HAIKU_MODEL,
795
+ name: spec.model.name,
796
+ candidate: spec.label ?? spec.model.name,
797
+ model: spec.model.id,
798
+ gate: "fail",
799
+ attempts: 0,
800
+ throttled: 0,
801
+ ms: Date.now() - startedAt,
802
+ usage: [],
803
+ abortedInFlight: false,
804
+ findings: [],
805
+ repairs: [],
806
+ error: `candidate crashed: ${e instanceof Error ? e.message : String(e)}`,
441
807
  };
442
808
  }
809
+ export function instantSpecs(args) {
810
+ const { assembly } = args;
811
+ const system = assembly.mercurySystem;
812
+ const mercury = system === undefined
813
+ ? []
814
+ : Array.from({ length: Math.max(0, args.fanout) }, (_, i) => ({
815
+ model: instantModelChoice("mercury"),
816
+ label: `mercury-${i + 1}`,
817
+ messages: mercuryMessages({ system, instruction: assembly.instruction, base: assembly.base }),
818
+ }));
819
+ return [...mercury, { model: instantModelChoice("haiku"), label: "haiku", messages: args.haikuMessages }];
820
+ }
821
+ export async function runInstantBuild(args) {
822
+ const { assembly } = args;
823
+ const breaker = args.breaker ?? INSTANT_MERCURY_BREAKER;
824
+ const admission = assembly.mercurySystem === undefined ? undefined : breaker.admit();
825
+ const fanout = admission === undefined || admission === "closed" ? (args.mercuryFanout ?? INSTANT_MERCURY_FANOUT) : 1;
826
+ const specs = instantSpecs({ assembly, haikuMessages: args.haikuMessages, fanout });
827
+ const mercuryThrottle = { holdUntil: 0 };
828
+ const baseline = opensBaseline(sourceFiles(assembly.base));
829
+ let race;
830
+ try {
831
+ race = await raceInstantCandidates({
832
+ specs,
833
+ budgetMs: args.budgetMs ?? INSTANT_STAGE_BUDGET_MS,
834
+ turn: args.turn,
835
+ run: ({ spec, cancel, startedAt, deadlineAt }) => runInstantCandidate({
836
+ call: args.call,
837
+ spec,
838
+ base: assembly.base,
839
+ look: assembly.look,
840
+ cancel,
841
+ startedAt,
842
+ deadlineAt,
843
+ baseline,
844
+ ...(spec.model.name === "mercury" ? { throttle: mercuryThrottle } : {}),
845
+ }).catch((e) => crashedCandidate(spec, startedAt, e)),
846
+ });
847
+ return {
848
+ response: instantResponse({ race, promptId: args.promptId }),
849
+ receipt: instantReceipt(race),
850
+ model: race.winner?.model ?? INSTANT_HAIKU_MODEL,
851
+ };
852
+ }
853
+ finally {
854
+ if (admission !== undefined && admission !== "open") {
855
+ const mercury = race?.outcomes.filter((outcome) => outcome.name === "mercury") ?? [];
856
+ const trouble = mercury.some((o) => o.throttled > 0) && mercury.every((o) => o.usage.length === 0);
857
+ breaker.record(admission, race === undefined ? { ok: false, cause: "error" } : trouble ? { ok: false, cause: "rate-limited" } : { ok: true });
858
+ }
859
+ }
860
+ }
443
861
  //# sourceMappingURL=instant-build.js.map