@coreplane/switchboard 1.266.1 → 1.267.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.266.1",
3
+ "version": "1.267.0",
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",
@@ -102,6 +102,7 @@
102
102
  "@opencode/protocol": "2.0.3",
103
103
  "@opencode/schema": "2.0.3",
104
104
  "@types/node": "^24.0.0",
105
+ "@vue/compiler-sfc": "3.5.42",
105
106
  "effect": "4.0.0-rc.112",
106
107
  "eslint": "^10.10.0",
107
108
  "eslint-config-prettier": "^10.1.8",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.266.1",
3
- "commit": "2b840cc31a6aae35accff736dad1dc74a30c2db3",
4
- "builtAt": "2026-09-26T22:15:46.755Z"
2
+ "version": "1.267.0",
3
+ "commit": "594f06834184ea4ff5559b00a8fc265f33e3db64",
4
+ "builtAt": "2026-09-28T22:36:17.080Z"
5
5
  }
@@ -493,11 +493,20 @@ export interface RoundGate {
493
493
  findings: string[];
494
494
  }
495
495
 
496
+ /** The original grant and historically priced children carried through a recovery checkpoint. */
497
+ export interface RecoveryAccounting {
498
+ spendUsd: number;
499
+ children: { runId: string; key: string; usd: number }[];
500
+ grant: Grant;
501
+ renewalsSpent: number;
502
+ }
503
+
496
504
  /** One admitted continuation of an ended original unit. The claim replaces
497
505
  * the terminal interpretation before a child starts; its step remains under
498
506
  * the original instance/unit idempotency namespace. */
499
507
  export interface OriginalUnitRecovery {
500
508
  kind: "findings" | "review";
509
+ accounting?: RecoveryAccounting;
501
510
  round: number;
502
511
  expectedHeadSha: string;
503
512
  remainingMs: number;
@@ -531,6 +540,7 @@ export interface OriginalUnitRecovery {
531
540
  }
532
541
 
533
542
  export interface OriginalUnitRecoveryReceipt {
543
+ accounting?: RecoveryAccounting;
534
544
  reviewRunId: string;
535
545
  workflowId: string;
536
546
  at: number;
@@ -697,7 +707,6 @@ const isRoundGate = (v: unknown): boolean =>
697
707
  isAddressSeverity(v.level) &&
698
708
  Array.isArray(v.findings) &&
699
709
  v.findings.every((f) => typeof f === "string");
700
-
701
710
  export const isHumanGatePending = (v: unknown): v is HumanGatePending =>
702
711
  isObject(v) &&
703
712
  isObject(v.pr) &&
@@ -758,6 +767,24 @@ export const isUnitWakeAnswer = (v: unknown): v is UnitWakeAnswer => {
758
767
  );
759
768
  };
760
769
 
770
+ const isDollars = (v: unknown): v is number => isFinite(v) && v >= 0;
771
+ const isRecoveryAccounting = (v: unknown): v is RecoveryAccounting =>
772
+ isObject(v) &&
773
+ isDollars(v.spendUsd) &&
774
+ Array.isArray(v.children) &&
775
+ v.children.length > 0 &&
776
+ v.children.every((c) => isObject(c) && isText(c.runId) && isText(c.key) && isDollars(c.usd)) &&
777
+ new Set(v.children.map((c) => c.runId)).size === v.children.length &&
778
+ new Set(v.children.map((c) => c.key)).size === v.children.length &&
779
+ v.children.reduce((sum, c) => sum + c.usd, 0) === v.spendUsd &&
780
+ isObject(v.grant) &&
781
+ isDollars(v.grant.renewals) &&
782
+ Number.isSafeInteger(v.grant.renewals) &&
783
+ (v.grant.costCapUsd === undefined || (isDollars(v.grant.costCapUsd) && v.grant.costCapUsd > v.spendUsd)) &&
784
+ isDollars(v.renewalsSpent) &&
785
+ Number.isSafeInteger(v.renewalsSpent) &&
786
+ v.renewalsSpent <= v.grant.renewals;
787
+
761
788
  export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
762
789
  if (!isObject(v)) return false;
763
790
  const r = v;
@@ -811,6 +838,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
811
838
  typeof r.recovery.step === "string" &&
812
839
  STEP_NAME_PATTERN.test(r.recovery.step) &&
813
840
  isText(r.recovery.reviewRunId) &&
841
+ (r.recovery.accounting === undefined || isRecoveryAccounting(r.recovery.accounting)) &&
814
842
  (r.recovery.findingsRunId === undefined || isText(r.recovery.findingsRunId)) &&
815
843
  (r.recovery.findingsKey === undefined || isText(r.recovery.findingsKey)) &&
816
844
  ((r.recovery.findingsRunId === undefined && r.recovery.findingsKey === undefined) ||
@@ -841,6 +869,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
841
869
  r.recoveryReceipt !== undefined &&
842
870
  (!isObject(r.recoveryReceipt) ||
843
871
  !isText(r.recoveryReceipt.reviewRunId) ||
872
+ (r.recoveryReceipt.accounting !== undefined && !isRecoveryAccounting(r.recoveryReceipt.accounting)) ||
844
873
  typeof r.recoveryReceipt.workflowId !== "string" ||
845
874
  !INSTANCE_ID_PATTERN.test(r.recoveryReceipt.workflowId) ||
846
875
  !isFinite(r.recoveryReceipt.at))
@@ -423,7 +423,8 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
423
423
  ...(typeof costUsd === "number" || costUsd === null ? { costUsd } : {}),
424
424
  ...(handoffLists !== undefined ? { handoffLists } : {}),
425
425
  // The failure by name (run-history item 57), shape-checked: a
426
- // `provider_transient` drives the round-0 re-run (agent-ship item 9).
426
+ // A provider transient or incomplete local model stream drives the
427
+ // round-0 re-run (agent-ship item 9).
427
428
  ...(isRecord(failure) && typeof failure.kind === "string" ? { failure: { kind: failure.kind } } : {}),
428
429
  // What ended an interrupted child (issue 1876): the ending's sentence
429
430
  // names the cause instead of claiming a bot restart for every one.
@@ -1043,7 +1044,15 @@ async function runUnit(
1043
1044
  addressSeveritySource: plan.addressSeveritySource,
1044
1045
  // Recovery spends only the lease already carried by the claim. It never
1045
1046
  // opens another segment or idles for a renewal in this checkpoint.
1046
- grant: row?.recovery !== undefined ? { renewals: 0 } : plan.grant,
1047
+ grant:
1048
+ row?.recovery === undefined
1049
+ ? plan.grant
1050
+ : (row.recovery.accounting?.grant ??
1051
+ // A missing capped carry must still stop before dispatch, while an
1052
+ // uncapped legacy checkpoint must not acquire plan renewals.
1053
+ (plan.grant?.costCapUsd !== undefined
1054
+ ? { renewals: 0, costCapUsd: plan.grant.costCapUsd }
1055
+ : { renewals: 0 })),
1047
1056
  grantSource: plan.grantSource,
1048
1057
  verbosity: plan.verbosity,
1049
1058
  idleDays: row?.recovery !== undefined ? 0 : plan.idleDays,
@@ -1054,7 +1063,13 @@ async function runUnit(
1054
1063
  ...(lastPush !== undefined ? { lastPush } : {}),
1055
1064
  ...(session !== undefined ? { session } : {}),
1056
1065
  ...(row?.recovery !== undefined
1057
- ? { recovery: { remainingMs: row.recovery.remainingMs, unitKey: `${instanceId}:${unit}` } }
1066
+ ? {
1067
+ recovery: {
1068
+ remainingMs: row.recovery.remainingMs,
1069
+ unitKey: `${instanceId}:${unit}`,
1070
+ ...(row.recovery.accounting !== undefined ? { renewalsSpent: row.recovery.accounting.renewalsSpent } : {}),
1071
+ },
1072
+ }
1058
1073
  : {}),
1059
1074
  };
1060
1075
  let state: UnitPipelineState =
@@ -1065,6 +1080,7 @@ async function runUnit(
1065
1080
  pr: row.pr!,
1066
1081
  expectedHeadSha: row.recovery.expectedHeadSha,
1067
1082
  reviewRunId: row.recovery.reviewRunId,
1083
+ ...(row.recovery.accounting !== undefined ? { spendUsd: row.recovery.accounting.spendUsd } : {}),
1068
1084
  ...(row.recovery.findingsRunId !== undefined ? { findingsRunId: row.recovery.findingsRunId } : {}),
1069
1085
  ...(row.recovery.findings !== undefined ? { findings: row.recovery.findings } : {}),
1070
1086
  })
@@ -572,12 +572,15 @@ function isRunPullRequestShape(v: unknown): v is RunPullRequest {
572
572
  * explanation's words — so the session's next seed leaves the refused
573
573
  * request out of its tail (docs/reference/specs/session-log.md item 9).
574
574
  * `provider_transient`: the run's model call failed on a provider transient
575
- * (a gateway 5xx, a cut stream, a gateway timeout) with the harness's retry
575
+ * (a gateway 5xx or timeout) with the harness's retry
576
576
  * ladder spent — the ship runner reads it off the child's record to re-run a
577
577
  * round-0 child that pushed nothing instead of aborting the unit
578
- * (docs/reference/specs/agent-ship.md item 9, issue 1932). A failure without
578
+ * (docs/reference/specs/agent-ship.md item 9, issue 1932).
579
+ * `model_stream_incomplete`: pi's answer stream ended incomplete after its
580
+ * local retry lease, with no provider-down evidence; Ship grants the same
581
+ * one re-run when nothing was pushed. A failure without
579
582
  * a name here leaves the record without the field. */
580
- export const RUN_FAILURE_KINDS = ["policy_refusal", "provider_transient"] as const;
583
+ export const RUN_FAILURE_KINDS = ["policy_refusal", "provider_transient", "model_stream_incomplete"] as const;
581
584
  export type RunFailureKind = (typeof RUN_FAILURE_KINDS)[number];
582
585
  export interface RunFailure {
583
586
  kind: RunFailureKind;
@@ -630,8 +630,8 @@ export type ChildFacts =
630
630
  costUsd?: number | null;
631
631
  handoffLists?: Handoff;
632
632
  /** The failure by name off a `failed` run's record (run-history item 57):
633
- * `provider_transient` marks a child a gateway 5xx, a cut stream or a
634
- * gateway timeout ended past the harness's retry ladder (issue 1932). */
633
+ * provider transport failure or an incomplete local model stream may
634
+ * earn one round-0 re-run when nothing was pushed (issue 1932). */
635
635
  failure?: { kind: string };
636
636
  /** What ended an `interrupted` child, off its record's own events (issue
637
637
  * 1876): the ending's sentence names it instead of claiming a bot
@@ -996,14 +996,14 @@ export type UnitEnding =
996
996
  }
997
997
  | { kind: "no_verdict"; round: RoundRef; reviewRounds: number; finalReply?: string }
998
998
  | { kind: "idle_expired"; reviewRounds: number }
999
- /** Round 0's coding child died on a provider transient — a model-gateway
1000
- * 5xx, a cut stream, a gateway timeout, past the harness's retry ladder —
999
+ /** Round 0's coding child died on a retryable model-call failure — a
1000
+ * provider transient or an incomplete local stream past its retry ladder —
1001
1001
  * with nothing pushed, TWICE: the first such death re-ran the round once
1002
1002
  * (the ledger row and the branch untouched, a re-run costs only minutes),
1003
1003
  * and the second is the ending (agent-ship item 9, issue 1932). Named
1004
1004
  * `transient` so it reads as a condition beside `checks_failed` and `held`
1005
1005
  * in the plane's table, never as the child failing on its task. */
1006
- | { kind: "transient"; round: RoundRef; runId: string; reviewRounds: number }
1006
+ | { kind: "transient"; round: RoundRef; runId: string; reviewRounds: number; streamIncomplete?: true }
1007
1007
  | {
1008
1008
  kind: "interrupted";
1009
1009
  round: RoundRef;
@@ -1182,7 +1182,7 @@ export interface UnitPipelineInput {
1182
1182
  /** A terminal original unit re-entered at its unchanged reviewed head. The
1183
1183
  * exact remaining lease is carried in milliseconds; the step prefix keeps
1184
1184
  * every new durable step under `<original instance>:<unit>/recovery/...`. */
1185
- recovery?: { remainingMs: number; unitKey: string };
1185
+ recovery?: { remainingMs: number; unitKey: string; renewalsSpent?: number };
1186
1186
  }
1187
1187
 
1188
1188
  type Phase =
@@ -1249,11 +1249,12 @@ type Phase =
1249
1249
  dead?: "failed" | "interrupted";
1250
1250
  /** What ended the dead child (issue 1876), for the `interrupted` ending's sentence. */
1251
1251
  cause?: InterruptionCause;
1252
- /** The dead child's record names a provider transient (`failure:
1253
- * provider_transient`, issue 1932): with nothing pushed, round 0 is
1254
- * re-run once instead of the unit aborting; a second transient in the
1255
- * same round is the `transient` ending. */
1252
+ /** The dead child's record names a retryable model-call failure
1253
+ * (run-history item 57): with nothing pushed, round 0 is re-run once;
1254
+ * a second such failure is the `transient` ending. */
1256
1255
  transient?: true;
1256
+ /** The retry signal was a local incomplete stream, not provider-down. */
1257
+ streamIncomplete?: true;
1257
1258
  }
1258
1259
  /** `queued`: the door enqueued the pull request — the base takes changes
1259
1260
  * only through a merge queue (issue 2011) — so every later ask reads the
@@ -1417,6 +1418,7 @@ export function openRecoveredUnitPipeline(
1417
1418
  pr: PrRef;
1418
1419
  expectedHeadSha: string;
1419
1420
  reviewRunId: string;
1421
+ spendUsd?: number;
1420
1422
  findingsRunId?: string;
1421
1423
  findings?: Finding[];
1422
1424
  },
@@ -1435,12 +1437,10 @@ export function openRecoveredUnitPipeline(
1435
1437
  reviewRestarts: 0,
1436
1438
  pr: recovery.pr,
1437
1439
  lastReviewHead: recovery.expectedHeadSha,
1438
- // Legacy terminal rows do not carry a complete lifetime category split or
1439
- // cumulative dollars. Recovery therefore measures only this checkpoint;
1440
- // admission refuses a cost-capped row rather than resetting an enforced
1441
- // total, and reports label this split as checkpoint-local.
1440
+ // Elapsed categories remain checkpoint-local. Enforced cumulative dollars
1441
+ // come only from the admission claim's complete persisted child evidence.
1442
1442
  spentMs: { coding: 0, review: 0, waiting: 0 },
1443
- spendUsd: 0,
1443
+ spendUsd: recovery.spendUsd ?? (input.grant?.costCapUsd === undefined ? 0 : null),
1444
1444
  findingsByRound: recovery.kind === "findings" ? { [recovery.round]: recovery.findings ?? [] } : {},
1445
1445
  humanAnswersByRound: {},
1446
1446
  dispositionsByRound: {},
@@ -1747,7 +1747,10 @@ function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEndin
1747
1747
  const grant = s.input.grant ?? DEFAULT_GRANT;
1748
1748
  // Unspent: an idle spends no renewal — the wake's segment does (this plan's
1749
1749
  // fifth unit) — so the row says what the grant still holds.
1750
- const renewalsLeft = Math.max(0, grant.renewals - (s.input.session?.renewalsSpent ?? 0));
1750
+ const renewalsLeft = Math.max(
1751
+ 0,
1752
+ grant.renewals - (s.input.recovery?.renewalsSpent ?? s.input.session?.renewalsSpent ?? 0),
1753
+ );
1751
1754
  // The head to continue from: a segment's own; a review_pending's reviewed
1752
1755
  // head — the pull request's, known even when the coding record carried none,
1753
1756
  // so the re-issue's resume-at-review fact survives the idle; else the last
@@ -1832,7 +1835,10 @@ function parkHumanGate(
1832
1835
  kind: "idle",
1833
1836
  why: "held",
1834
1837
  idled: held,
1835
- renewalsLeft: Math.max(0, grant.renewals - (s.input.session?.renewalsSpent ?? 0)),
1838
+ renewalsLeft: Math.max(
1839
+ 0,
1840
+ grant.renewals - (s.input.recovery?.renewalsSpent ?? s.input.session?.renewalsSpent ?? 0),
1841
+ ),
1836
1842
  ...(s.lastReviewHead !== undefined ? { from: s.lastReviewHead } : {}),
1837
1843
  ...(s.lastCodingRunId !== undefined ? { runId: s.lastCodingRunId } : {}),
1838
1844
  spendUsd: s.spendUsd,
@@ -1878,10 +1884,29 @@ function capEnding(s: UnitPipelineState, round?: RoundRef, refused?: Carve): Uni
1878
1884
  };
1879
1885
  }
1880
1886
 
1887
+ /** A recovered unit keeps its original dollar cap across the checkpoint. */
1888
+ function recoveryCostEnding(s: UnitPipelineState): UnitEnding | undefined {
1889
+ const cap = s.input.grant?.costCapUsd;
1890
+ if (s.input.recovery === undefined || cap === undefined) return undefined;
1891
+ if (s.spendUsd !== null && Number.isFinite(s.spendUsd) && s.spendUsd >= 0 && s.spendUsd < cap) return undefined;
1892
+ return {
1893
+ kind: "aborted",
1894
+ reason:
1895
+ s.spendUsd === null
1896
+ ? `The original $${cap} cost cap cannot be proven: cumulative child spend is unknown.`
1897
+ : `The original $${cap} cost cap is exhausted; cumulative child spend is $${s.spendUsd}.`,
1898
+ reviewRounds: s.reviewRounds,
1899
+ };
1900
+ }
1901
+
1881
1902
  /** Start a round if its carve holds (agent-ship item 8): a round the
1882
1903
  * remainder cannot carve above its floor is not dispatched, and the unit ends
1883
1904
  * at the cap naming the round, what it would have got and the floor. */
1884
1905
  function enterRound(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNote[] = []): Transition {
1906
+ if (s.input.recovery !== undefined && round.index > s.input.caps.maxRounds)
1907
+ return end(s, { kind: "round_cap", maxRounds: s.input.caps.maxRounds, reviewRounds: s.reviewRounds }, notes);
1908
+ const costEnding = recoveryCostEnding(s);
1909
+ if (costEnding !== undefined) return end(s, costEnding, notes);
1885
1910
  const carved = roundCarve(s, round);
1886
1911
  if (carved.kind === "refused") return end(s, capEnding(s, round, carved), notes);
1887
1912
  const reviewRounds = round.kind === "review" ? round.index : s.reviewRounds;
@@ -1976,6 +2001,11 @@ function settleCoding(
1976
2001
  }
1977
2002
  : {}),
1978
2003
  };
2004
+ // A completed findings child may have moved the head. Its verification
2005
+ // read reconciles that head before the cap ends the unit.
2006
+ const costEnding = recoveryCostEnding(next);
2007
+ if (costEnding !== undefined && !(round.kind === "findings" && facts.status === "completed"))
2008
+ return end(next, costEnding, [roundNote(round, "aborted")]);
1979
2009
  const readySalvage = round.kind === "coding" && completedSameHeadSalvage(next, facts.pushed, facts);
1980
2010
  const checkpoint = interruptedCheckpoint(next, facts.pushed, round.kind === "findings" ? undefined : facts);
1981
2011
  const mode = stopMode(facts.status);
@@ -2060,9 +2090,11 @@ function settleCoding(
2060
2090
  const cause =
2061
2091
  facts.failure?.kind === "provider_transient"
2062
2092
  ? "the model provider's transport retry budget was spent"
2063
- : facts.status === "failed"
2064
- ? "it failed before finishing"
2065
- : "it ended before its work was ready for review";
2093
+ : facts.failure?.kind === "model_stream_incomplete"
2094
+ ? "the model stream ended before a complete answer"
2095
+ : facts.status === "failed"
2096
+ ? "it failed before finishing"
2097
+ : "it ended before its work was ready for review";
2066
2098
  return end(
2067
2099
  next,
2068
2100
  {
@@ -2088,7 +2120,10 @@ function settleCoding(
2088
2120
  round,
2089
2121
  runId,
2090
2122
  dead: "failed",
2091
- ...(facts.failure?.kind === "provider_transient" ? { transient: true as const } : {}),
2123
+ ...(facts.failure?.kind === "provider_transient" || facts.failure?.kind === "model_stream_incomplete"
2124
+ ? { transient: true as const }
2125
+ : {}),
2126
+ ...(facts.failure?.kind === "model_stream_incomplete" ? { streamIncomplete: true as const } : {}),
2092
2127
  },
2093
2128
  },
2094
2129
  notes: [],
@@ -2117,10 +2152,11 @@ function settleReview(
2117
2152
  facts: Extract<ChildFacts, { finished: true }>,
2118
2153
  ): Transition {
2119
2154
  const mode = stopMode(facts.status);
2155
+ const charged = { ...s, spendUsd: addSpend(s.spendUsd, facts.costUsd) };
2120
2156
  if (!facts.verdict) {
2121
2157
  if (mode !== undefined)
2122
2158
  return end(
2123
- s,
2159
+ charged,
2124
2160
  {
2125
2161
  kind: "stopped",
2126
2162
  mode,
@@ -2131,7 +2167,7 @@ function settleReview(
2131
2167
  [roundNote(round, "stopped")],
2132
2168
  );
2133
2169
  return end(
2134
- s,
2170
+ charged,
2135
2171
  {
2136
2172
  kind: "no_verdict",
2137
2173
  round,
@@ -2143,13 +2179,14 @@ function settleReview(
2143
2179
  }
2144
2180
  const verdict = facts.verdict;
2145
2181
  const next: UnitPipelineState = {
2146
- ...s,
2147
- spendUsd: addSpend(s.spendUsd, facts.costUsd),
2182
+ ...charged,
2148
2183
  findingsByRound: { ...s.findingsByRound, [round.index]: verdict.findings },
2149
2184
  ...(facts.reviewHead !== undefined ? { lastReviewHead: facts.reviewHead } : {}),
2150
2185
  ...(verdict.summary !== undefined ? { lastVerdictSummary: verdict.summary } : {}),
2151
2186
  };
2152
2187
  const notes = [roundNote(round, verdict.verdict)];
2188
+ const costEnding = recoveryCostEnding(next);
2189
+ if (costEnding !== undefined) return end(next, costEnding, notes);
2153
2190
  if (verdict.verdict === "approve") {
2154
2191
  // Merge-ready stands on the POSTED approval: an approve whose post did
2155
2192
  // not land left no approving review on the pull request. The reason is
@@ -2776,15 +2813,23 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
2776
2813
  [roundNote(round, "aborted")],
2777
2814
  );
2778
2815
  if (phase.dead === "failed") {
2779
- // A provider transient with nothing pushed is not the child's failure
2816
+ // A retryable model-call failure with nothing pushed is not the child's failure
2780
2817
  // (issue 1932): the ledger row and the branch are untouched, so round 0
2781
2818
  // is re-run once — a fresh attempt under fresh step names — and only a
2782
2819
  // second transient in the same round is the ending, named `transient`.
2783
2820
  if (phase.transient && round.kind === "coding" && pr.unrecovered === "no_commits") {
2784
2821
  if ((round.attempt ?? 1) < 2) return enterRound(s, { ...round, attempt: 2 }, [roundNote(round, "transient")]);
2785
- return end(s, { kind: "transient", round, runId: phase.runId, reviewRounds: s.reviewRounds }, [
2786
- roundNote(round, "transient"),
2787
- ]);
2822
+ return end(
2823
+ s,
2824
+ {
2825
+ kind: "transient",
2826
+ round,
2827
+ runId: phase.runId,
2828
+ reviewRounds: s.reviewRounds,
2829
+ ...(phase.streamIncomplete ? { streamIncomplete: true as const } : {}),
2830
+ },
2831
+ [roundNote(round, "transient")],
2832
+ );
2788
2833
  }
2789
2834
  // The abort repeats the bot's reason for recovering nothing, and claims
2790
2835
  // no more than the answer carried.
@@ -3825,11 +3870,10 @@ function renderUnitReportWithWake(
3825
3870
  const level = s.input.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
3826
3871
  const levelLine = `Severity addressed: ${level} and above (set by ${s.input.addressSeveritySource ?? "org"}).`;
3827
3872
  // The grant as the instance carries it (decision 0046): spent of granted,
3828
- // the cap when one is set, and who granted it — zero of zero until a scope
3829
- // or a directive says otherwise, and nothing spends it before the renewal
3830
- // decision exists.
3873
+ // the cap when one is set, and who granted it. A capped recovery carries
3874
+ // proven spent renewals; an uncapped legacy checkpoint has zero capacity.
3831
3875
  const grant = s.input.grant ?? DEFAULT_GRANT;
3832
- const grantLine = `Renewals: 0 of ${grant.renewals} spent${grant.costCapUsd !== undefined ? `, cost cap $${grant.costCapUsd}` : ""} (granted by ${s.input.grantSource ?? "org"}).`;
3876
+ const grantLine = `Renewals: ${s.input.recovery?.renewalsSpent ?? 0} of ${grant.renewals} spent${grant.costCapUsd !== undefined ? `, cost cap $${grant.costCapUsd}` : ""} (granted by ${s.input.grantSource ?? "org"}).`;
3833
3877
  const lastFindings = s.findingsByRound[e.reviewRounds] ?? [];
3834
3878
  const skipped = lastFindings.filter((f) => !findingsAtOrAbove([f], level).length);
3835
3879
  const skippedLine =
@@ -4076,11 +4120,11 @@ function renderUnitReportWithWake(
4076
4120
  ]);
4077
4121
  case "transient":
4078
4122
  if (!shows(verbosity, "verbose"))
4079
- return `⚠️ Aborted after ${rounds}: this is a bug — the model provider failed twice and no automatic retry remains.${prLine}`;
4123
+ return `⚠️ Aborted after ${rounds}: this is a bug — the model call failed twice and no automatic retry remains.${prLine}`;
4080
4124
  return join([
4081
- `⚠️ The coding child of round ${e.round.index} (run ${e.runId}) died on a provider transient — a model-gateway 5xx, a cut stream or a gateway timeout past the harness's retry ladder — with nothing pushed, after the round was already re-run once for the same reason. The task itself was never the problem.`,
4125
+ `⚠️ The coding child of round ${e.round.index} (run ${e.runId}) ${e.streamIncomplete ? "ended because the model stream ended before a complete answer" : "died on a provider transient — a model-gateway 5xx or gateway timeout past the harness's retry ladder"} — with nothing pushed, after the round was already re-run once for a retryable model-call failure. The task itself was never the problem.`,
4082
4126
  writeUpPointer(s, e.round.kind, s.lastCodingRunId),
4083
- `⚠️ This is a bug: ship ended after ${rounds} because its automatic provider retry was spent.`,
4127
+ `⚠️ This is a bug: ship ended after ${rounds} because its automatic model-call retry was spent.`,
4084
4128
  nextAction(),
4085
4129
  ]);
4086
4130
  case "no_verdict":
@@ -25,6 +25,8 @@ export interface AttrDomain {
25
25
  // slack.receive
26
26
  caughtUp: boolean;
27
27
  files: number;
28
+ /** Files found on the canonical source message after event metadata is normalized. */
29
+ resolvedFiles: number;
28
30
  dedupe: "fresh" | "duplicate";
29
31
  /** How the requester was found (slack-channel.md item 13): the sender, the configured relay app's footer, or the app itself. */
30
32
  requester: "message" | "relay-footer" | "bot";
@@ -193,6 +195,7 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
193
195
  runId: "string",
194
196
  caughtUp: "boolean",
195
197
  files: "number",
198
+ resolvedFiles: "number",
196
199
  dedupe: "string",
197
200
  requester: "string",
198
201
  intake: "string",