@coreplane/switchboard 1.243.0 → 1.244.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.
Files changed (133) hide show
  1. package/dist/assets/deploy/cloudflare-memory/worker.ts +148 -0
  2. package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +433 -117
  4. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +1 -1
  7. package/dist/assets/source.json +3 -3
  8. package/dist/assets/src/agents/registry.ts +1 -1
  9. package/dist/assets/src/core/authz/types.ts +10 -0
  10. package/dist/assets/src/core/budgets.ts +36 -0
  11. package/dist/assets/src/core/coordinator/contract.ts +32 -0
  12. package/dist/assets/src/core/coordinator/driver.ts +75 -12
  13. package/dist/assets/src/core/delivery.ts +1 -1
  14. package/dist/assets/src/core/runEvents.ts +50 -7
  15. package/dist/assets/src/core/runFriction.ts +2 -1
  16. package/dist/assets/src/core/runRecord.ts +31 -0
  17. package/dist/assets/src/core/ship/contract.ts +2 -1
  18. package/dist/assets/src/core/ship/coordinator.ts +211 -13
  19. package/dist/assets/src/core/ship/renewal.ts +155 -0
  20. package/dist/assets/src/core/trace/attrs.ts +11 -0
  21. package/dist/assets/src/core/trace/types.ts +4 -3
  22. package/dist/assets/src/execution/residentRefresh.ts +24 -4
  23. package/dist/assets/src/execution/residentText.ts +8 -3
  24. package/dist/assets/src/mcp/registry.ts +3 -0
  25. package/dist/assets/web/dist/.vite/manifest.json +417 -417
  26. package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
  27. package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
  28. package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
  29. package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
  30. package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
  31. package/dist/assets/web/dist/assets/{PendingTurnRow-BT9RhFZ7.js → PendingTurnRow-BHCSpsPf.js} +1 -1
  32. package/dist/assets/web/dist/assets/{ResidentDetailPage-DACalNVF.js → ResidentDetailPage-C5USV_8P.js} +1 -1
  33. package/dist/assets/web/dist/assets/{ResidentsIndexPage-SHPdu6uZ.js → ResidentsIndexPage-CSKisWnX.js} +1 -1
  34. package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
  35. package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
  36. package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
  37. package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
  38. package/dist/assets/web/dist/assets/{ScheduledPage-DJ8HiCPt.js → ScheduledPage-DBaChcJZ.js} +1 -1
  39. package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
  40. package/dist/assets/web/dist/assets/{StatusDot-Dw0T1M-P.js → StatusDot-_Q_bmReh.js} +1 -1
  41. package/dist/assets/web/dist/assets/{Tooltip-BbLuIAiS.js → Tooltip-Cafsgd4m.js} +1 -1
  42. package/dist/assets/web/dist/assets/{UnitRoutePage-DicUG96U.js → UnitRoutePage-CdoBZwtf.js} +1 -1
  43. package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
  44. package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
  45. package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
  46. package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
  47. package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
  48. package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
  49. package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
  50. package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
  51. package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
  52. package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
  53. package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
  54. package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
  55. package/dist/assets/web/dist/assets/{dist-twkFmSUY.js → dist-BtPs2AkQ.js} +2 -2
  56. package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
  57. package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
  58. package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
  59. package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
  60. package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
  61. package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
  62. package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
  63. package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
  64. package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
  65. package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
  66. package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
  67. package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
  68. package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
  69. package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
  70. package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
  71. package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
  72. package/dist/assets/web/dist/assets/{indexRow-B_s5tKyq.js → indexRow-BmK74Vp1.js} +1 -1
  73. package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
  74. package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
  75. package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
  76. package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
  77. package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
  78. package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
  79. package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
  80. package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
  81. package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
  82. package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
  83. package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
  84. package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
  85. package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
  86. package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
  87. package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
  88. package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
  89. package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
  90. package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
  91. package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
  92. package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
  93. package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
  94. package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
  95. package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
  96. package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
  97. package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
  98. package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
  99. package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
  100. package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
  101. package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
  102. package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
  103. package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
  104. package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
  105. package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
  106. package/dist/assets/web/dist/assets/{sseReplay-C9m_EB8J.js → sseReplay-DaRtc7nI.js} +4 -4
  107. package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
  108. package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
  109. package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
  110. package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
  111. package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
  112. package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
  113. package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
  114. package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
  115. package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
  116. package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
  117. package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
  118. package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
  119. package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
  120. package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
  121. package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
  122. package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
  123. package/dist/cli.js +2990 -706
  124. package/package.json +1 -1
  125. package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +0 -2
  126. package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
  127. package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +0 -2
  128. package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +0 -1
  129. package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +0 -9
  130. package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +0 -1
  131. package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +0 -1
  132. package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +0 -1
  133. package/dist/assets/web/dist/assets/main-d-w-tIKt.js +0 -28
@@ -30,11 +30,23 @@
30
30
  // arrive in the input rather than from the agent registry.
31
31
 
32
32
  import type { ShipRoundOutcome } from "../runEvents.js";
33
+ import type { Handoff } from "./handoff.js";
34
+ import { progressOf, renderRenewal, renewalDecision, type PushedHeadFact, type RenewalDecision } from "./renewal.js";
33
35
  import type { RunStatus } from "../runRecord.js";
34
36
  import { normalizeHead, sameCommit } from "../reviewedHead.js";
35
37
  import { formatFinding, type Finding, type FindingDisposition, type ReviewVerdictKind } from "../reviewVerdict.js";
36
38
  import { parsePlanUnit, planUnitIds } from "./contract.js";
37
- import { carve, loopPosition, MINUTE_MS, SHIP_WAIT, type Carve, type Loop } from "../budgets.js";
39
+ import {
40
+ carve,
41
+ DEFAULT_GRANT,
42
+ loopPosition,
43
+ MINUTE_MS,
44
+ SHIP_WAIT,
45
+ type Carve,
46
+ type Grant,
47
+ type GrantSource,
48
+ type Loop,
49
+ } from "../budgets.js";
38
50
 
39
51
  const MIN = MINUTE_MS;
40
52
 
@@ -391,7 +403,14 @@ export function presetOf(kind: RoundKind): ChildPreset {
391
403
  * turn: the unit's contract, or the review turn from the pull request and the
392
404
  * prior rounds' records, or the findings message from the review run's verdict. */
393
405
  export type Brief =
394
- | { kind: "contract"; unit: string; rebase: { branch: string; onto: string } }
406
+ | {
407
+ kind: "contract";
408
+ unit: string;
409
+ rebase: { branch: string; onto: string };
410
+ /** A renewal's segment (decision 0046): the child continues the previous
411
+ * segment's work from `from`, briefed with that run's write-up and handoff. */
412
+ continue?: { segment: number; from?: string; previousRunId?: string };
413
+ }
395
414
  | {
396
415
  kind: "review";
397
416
  unit: string;
@@ -455,6 +474,13 @@ export type ChildFacts =
455
474
  /** The dispositions a coding run recorded, as it submitted them: the machine matches them to the round's findings. */
456
475
  dispositions?: FindingDisposition[];
457
476
  handoff?: boolean;
477
+ /** The renewal's facts (decision 0046): the heads the run pushed, when
478
+ * its lease began, what it cost (null when a model had no price) and
479
+ * the handoff's lists — progress is read off these, never asked. */
480
+ pushed?: PushedHeadFact[];
481
+ leaseStartedAt?: number;
482
+ costUsd?: number | null;
483
+ handoffLists?: Handoff;
458
484
  };
459
485
 
460
486
  /** What heads the unit's branch on GitHub: nothing, an open pull request, or —
@@ -524,7 +550,38 @@ export type UnitEnding =
524
550
  /** A changes-requested review posted this round before the stop. */
525
551
  postedReview?: boolean;
526
552
  }
527
- | { kind: "aborted"; reason: string; round?: RoundRef; reviewRounds: number; finalReply?: string }
553
+ | {
554
+ kind: "aborted";
555
+ reason: string;
556
+ round?: RoundRef;
557
+ reviewRounds: number;
558
+ finalReply?: string;
559
+ /** A round-0 end without a pull request that was judged for renewal and
560
+ * refused: the decision and the card's sentence (decision 0046). */
561
+ renewal?: { decision: Extract<RenewalDecision, { renew: false }>; line: string };
562
+ }
563
+ /** The coding round ended at its lease with the unit unfinished, the row
564
+ * showed progress and the grant renewed: this segment is over and the next
565
+ * opens in the same thread from `from` under a fresh lease, the coding
566
+ * child's write-up as its request (decision 0046, Renewal). */
567
+ | {
568
+ kind: "continued";
569
+ round: RoundRef;
570
+ /** The coding child whose write-up and handoff the continuation is briefed with. */
571
+ runId: string;
572
+ /** The segment the renewal opens (the first segment is 1). */
573
+ segment: number;
574
+ from?: string;
575
+ renewalsLeft: number;
576
+ /** The session's spend so far, this segment's children included; null when a run's model had no price. */
577
+ spendUsd: number | null;
578
+ handoff?: Handoff;
579
+ /** The coding child's write-up: the checkpoint the continuation is briefed with. */
580
+ finalReply?: string;
581
+ line: string;
582
+ reviewRounds: number;
583
+ spent: ShipBudgetSpent;
584
+ }
528
585
  | { kind: "no_verdict"; round: RoundRef; reviewRounds: number; finalReply?: string }
529
586
  | { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number }
530
587
  | { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number };
@@ -562,6 +619,16 @@ export function findingsAtOrAbove(findings: readonly Finding[], level: AddressSe
562
619
  return findings.filter((f) => severityRank(f.severity) <= severityRank(level));
563
620
  }
564
621
 
622
+ export interface UnitSession {
623
+ segment: number;
624
+ renewalsSpent: number;
625
+ spendUsd: number | null;
626
+ continueFrom?: string;
627
+ /** The previous segment's coding run: its write-up and handoff brief the continuation. */
628
+ previousRunId?: string;
629
+ previousHandoff?: Handoff;
630
+ }
631
+
565
632
  export interface UnitPipelineInput {
566
633
  unit: { id: string; branch: string };
567
634
  repo: string;
@@ -578,6 +645,18 @@ export interface UnitPipelineInput {
578
645
  addressSeverity?: AddressSeverity;
579
646
  /** Which layer set the level in force; named in the round header and the ending. */
580
647
  addressSeveritySource?: AddressSeveritySource;
648
+ /** The grant the request carried (decision 0046, the renewable lease):
649
+ * renewals and a cost cap, and which layer granted it — named in the report;
650
+ * absent reads as zero renewals and no cap, the org's. Nothing here spends
651
+ * it yet: the renewal decision is the segment's end, not this unit's. */
652
+ grant?: Grant;
653
+ grantSource?: GrantSource;
654
+ /** The segment this pipeline runs (decision 0046, Renewal): absent for the
655
+ * first; a renewal's carries its number, the renewals spent before it, the
656
+ * session's spend so far, the sha it continues from and the previous
657
+ * segment's handoff. Every step name of a later segment is prefixed with
658
+ * it, so the Workflow's durable steps never collide across segments. */
659
+ session?: UnitSession;
581
660
  /** The instance's mark (agent-ship item 16): a generated one-unit plan — a
582
661
  * `plan` with an id and no `path` — whose unit runs in the requesting
583
662
  * thread and is re-issued with the request's own text, never a plan path. */
@@ -606,6 +685,10 @@ type Phase =
606
685
  runId: string;
607
686
  childHead?: string;
608
687
  finalReply?: string;
688
+ /** The renewal's facts off the child's record (decision 0046): its pushed heads, its lease's start, its handoff. */
689
+ childPushed?: PushedHeadFact[];
690
+ childLeaseStartedAt?: number;
691
+ childHandoff?: Handoff;
609
692
  /** The coding child died (`failed` or `interrupted`) after it may have
610
693
  * pushed: the pr-check recovers a pushed branch by opening its pull
611
694
  * request; with nothing pushed the unit ends with the child's own reason. */
@@ -639,6 +722,9 @@ export interface UnitPipelineState {
639
722
  readonly lastCodingRunId?: string;
640
723
  /** How the budget went so far, accrued as each answer moves the clock. */
641
724
  readonly spentMs: ShipBudgetSpent;
725
+ /** The session's dollars so far: the input's from earlier segments plus each
726
+ * child's cost as its record is read; null once any run's cost is unknown. */
727
+ readonly spendUsd: number | null;
642
728
  readonly ending?: UnitEnding;
643
729
  }
644
730
 
@@ -674,6 +760,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
674
760
  phase: { at: "pre-check" },
675
761
  reviewRounds: 0,
676
762
  spentMs: { coding: 0, review: 0, waiting: 0 },
763
+ spendUsd: input.session?.spendUsd ?? 0,
677
764
  findingsByRound: {},
678
765
  dispositionsByRound: {},
679
766
  reviewRunByRound: {},
@@ -713,12 +800,33 @@ function roundCarve(s: UnitPipelineState, round: RoundRef): Carve {
713
800
  return carve(remainingMs(s), { kind, index: loopPosition(loopOf(s), kind, round.index) }, loopOf(s));
714
801
  }
715
802
 
716
- const roundStep = (s: UnitPipelineState, round: RoundRef) => `${s.input.unit.id}/${round.index}/${round.kind}`;
803
+ /** The prefix every step of this pipeline is named under: the unit id, and for
804
+ * a renewal's segment the segment too (`U10/s2/…`), so the Workflow's durable
805
+ * step cache never hands segment two the answers of segment one. */
806
+ export const stepPrefixOf = (unit: string, session: UnitSession | undefined): string =>
807
+ session !== undefined && session.segment > 1 ? `${unit}/s${session.segment}` : unit;
808
+ const stepPrefix = (s: UnitPipelineState) => stepPrefixOf(s.input.unit.id, s.input.session);
809
+ const roundStep = (s: UnitPipelineState, round: RoundRef) => `${stepPrefix(s)}/${round.index}/${round.kind}`;
717
810
 
718
811
  function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
719
812
  const unit = s.input.unit.id;
720
- if (round.kind === "coding")
721
- return { kind: "contract", unit, rebase: { branch: s.input.unit.branch, onto: s.input.base } };
813
+ if (round.kind === "coding") {
814
+ const session = s.input.session;
815
+ return {
816
+ kind: "contract",
817
+ unit,
818
+ rebase: { branch: s.input.unit.branch, onto: s.input.base },
819
+ ...(session !== undefined && session.segment > 1
820
+ ? {
821
+ continue: {
822
+ segment: session.segment,
823
+ ...(session.continueFrom !== undefined ? { from: session.continueFrom } : {}),
824
+ ...(session.previousRunId !== undefined ? { previousRunId: session.previousRunId } : {}),
825
+ },
826
+ }
827
+ : {}),
828
+ };
829
+ }
722
830
  const pr = s.pr!.number;
723
831
  if (round.kind === "findings") return { kind: "findings", unit, pr, reviewRunId: s.reviewRunByRound[round.index]! };
724
832
  const priorReview = s.reviewRunByRound[round.index - 1];
@@ -754,7 +862,7 @@ export function matchDispositions(
754
862
  /** The step the machine is at. Pure over the state: asked before every step
755
863
  * and again after a replay, it names the same step for the same state. */
756
864
  export function nextAction(s: UnitPipelineState): CoordinatorAction {
757
- const unit = s.input.unit.id;
865
+ const unit = stepPrefix(s);
758
866
  const p = s.phase;
759
867
  switch (p.at) {
760
868
  case "pre-check":
@@ -815,6 +923,14 @@ interface Transition {
815
923
 
816
924
  const ENDED: Phase = { at: "ended" };
817
925
 
926
+ /** The session's dollars after one more child: unknown (null) once any run's
927
+ * cost is — a total that left a run out would understate the spend a cap
928
+ * judges — and a record without the field counts as unknown too. */
929
+ function addSpend(sum: number | null, cost: number | null | undefined): number | null {
930
+ if (sum === null || cost === null || cost === undefined) return null;
931
+ return sum + cost;
932
+ }
933
+
818
934
  function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[] = []): Transition {
819
935
  return { state: { ...s, phase: ENDED, ending }, notes: [...notes, { type: "ended", ending }] };
820
936
  }
@@ -885,6 +1001,7 @@ function settleCoding(
885
1001
  // round's findings enter the state (agent-ship item 6).
886
1002
  let next: UnitPipelineState = {
887
1003
  ...s,
1004
+ spendUsd: addSpend(s.spendUsd, facts.costUsd),
888
1005
  ...(facts.pr !== undefined ? { pr: { number: facts.pr.number, url: facts.pr.url } } : {}),
889
1006
  ...(round.kind === "findings" && facts.dispositions !== undefined
890
1007
  ? {
@@ -922,6 +1039,9 @@ function settleCoding(
922
1039
  runId,
923
1040
  ...(facts.headSha !== undefined ? { childHead: facts.headSha } : {}),
924
1041
  ...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
1042
+ ...(facts.pushed !== undefined ? { childPushed: facts.pushed } : {}),
1043
+ ...(facts.leaseStartedAt !== undefined ? { childLeaseStartedAt: facts.leaseStartedAt } : {}),
1044
+ ...(facts.handoffLists !== undefined ? { childHandoff: facts.handoffLists } : {}),
925
1045
  },
926
1046
  };
927
1047
  return { state: next, notes: [] };
@@ -961,6 +1081,7 @@ function settleReview(
961
1081
  const verdict = facts.verdict;
962
1082
  const next: UnitPipelineState = {
963
1083
  ...s,
1084
+ spendUsd: addSpend(s.spendUsd, facts.costUsd),
964
1085
  findingsByRound: { ...s.findingsByRound, [round.index]: verdict.findings },
965
1086
  ...(facts.reviewHead !== undefined ? { lastReviewHead: facts.reviewHead } : {}),
966
1087
  ...(verdict.summary !== undefined ? { lastVerdictSummary: verdict.summary } : {}),
@@ -1122,15 +1243,72 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
1122
1243
  [roundNote(round, "aborted")],
1123
1244
  );
1124
1245
  }
1125
- const reason =
1126
- round.index === 0
1127
- ? `⚠️ Ship ended at round 0: the coding round ended without opening a pull request (a clarifying question, a budget write-up, an unproven push or a description-less push ends the pipeline here). No review round ran.`
1128
- : `⚠️ Round ${round.index}'s findings step left no open pull request heading \`${s.input.unit.branch}\` — the pull request was closed out from under the pipeline and none was reopened, so there is nothing to re-review.`;
1246
+ // Round 0 ended without a pull request: the segment is over with the unit
1247
+ // unfinished, and the grant decides whether the next opens (decision
1248
+ // 0046, Renewal). Progress is read off the child's record — a head pushed
1249
+ // to the unit's branch since the segment started, or a handoff that moved —
1250
+ // never off its words; the decision then asks the grant's count, the cap
1251
+ // and the fit, in that order, and a refusal names the clause. A plain
1252
+ // abort keeps its old shape when nothing was pushed under a grant of zero:
1253
+ // a clarifying question is not a stop to explain.
1254
+ if (round.index === 0) {
1255
+ const grant = s.input.grant ?? DEFAULT_GRANT;
1256
+ const session = s.input.session;
1257
+ const progress = progressOf({
1258
+ branch: s.input.unit.branch,
1259
+ pushed: phase.childPushed ?? [],
1260
+ ...(session?.continueFrom !== undefined ? { startHead: session.continueFrom } : {}),
1261
+ ...(phase.childLeaseStartedAt !== undefined ? { leaseStartedAt: phase.childLeaseStartedAt } : {}),
1262
+ ...(session?.previousHandoff !== undefined && phase.childHandoff !== undefined
1263
+ ? { handoff: { previous: session.previousHandoff, current: phase.childHandoff } }
1264
+ : {}),
1265
+ });
1266
+ const decision = renewalDecision({
1267
+ grant,
1268
+ renewalsSpent: session?.renewalsSpent ?? 0,
1269
+ spendUsd: s.spendUsd,
1270
+ progress,
1271
+ pipeline: s.input.caps,
1272
+ });
1273
+ const line = renderRenewal(decision, grant);
1274
+ if (decision.renew)
1275
+ return end(
1276
+ s,
1277
+ {
1278
+ kind: "continued",
1279
+ round,
1280
+ runId: phase.runId,
1281
+ segment: decision.segment,
1282
+ ...(decision.from !== undefined ? { from: decision.from } : {}),
1283
+ renewalsLeft: decision.renewalsLeft,
1284
+ spendUsd: s.spendUsd,
1285
+ ...(phase.childHandoff !== undefined ? { handoff: phase.childHandoff } : {}),
1286
+ ...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
1287
+ line,
1288
+ reviewRounds: s.reviewRounds,
1289
+ spent: s.spentMs,
1290
+ },
1291
+ [roundNote(round, "continued")],
1292
+ );
1293
+ const judged = grant.renewals > 0 || progress.progressed;
1294
+ return end(
1295
+ s,
1296
+ {
1297
+ kind: "aborted",
1298
+ reason: `⚠️ Ship ended at round 0: the coding round ended without opening a pull request (a clarifying question, a budget write-up, an unproven push or a description-less push ends the pipeline here). No review round ran.`,
1299
+ round,
1300
+ reviewRounds: s.reviewRounds,
1301
+ ...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
1302
+ ...(judged ? { renewal: { decision, line } } : {}),
1303
+ },
1304
+ [roundNote(round, "aborted")],
1305
+ );
1306
+ }
1129
1307
  return end(
1130
1308
  s,
1131
1309
  {
1132
1310
  kind: "aborted",
1133
- reason,
1311
+ reason: `⚠️ Round ${round.index}'s findings step left no open pull request heading \`${s.input.unit.branch}\` — the pull request was closed out from under the pipeline and none was reopened, so there is nothing to re-review.`,
1134
1312
  round,
1135
1313
  reviewRounds: s.reviewRounds,
1136
1314
  ...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
@@ -1475,6 +1653,12 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1475
1653
  // stated decision, never a silent one.
1476
1654
  const level = s.input.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
1477
1655
  const levelLine = `Severity addressed: ${level} and above (set by ${s.input.addressSeveritySource ?? "org"}).`;
1656
+ // The grant as the instance carries it (decision 0046): spent of granted,
1657
+ // the cap when one is set, and who granted it — zero of zero until a scope
1658
+ // or a directive says otherwise, and nothing spends it before the renewal
1659
+ // decision exists.
1660
+ const grant = s.input.grant ?? DEFAULT_GRANT;
1661
+ const grantLine = `Renewals: 0 of ${grant.renewals} spent${grant.costCapUsd !== undefined ? `, cost cap $${grant.costCapUsd}` : ""} (granted by ${s.input.grantSource ?? "org"}).`;
1478
1662
  const lastFindings = s.findingsByRound[e.reviewRounds] ?? [];
1479
1663
  const skipped = lastFindings.filter((f) => !findingsAtOrAbove([f], level).length);
1480
1664
  const skippedLine =
@@ -1490,6 +1674,7 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1490
1674
  `✅ Merged after ${rounds}: ${e.pr.url} (squash \`${e.sha.slice(0, 7)}\`) — merged by the plan runner under \`plan:merge\`: the review approved at this head and the guards were green.`,
1491
1675
  verdictLine,
1492
1676
  levelLine,
1677
+ grantLine,
1493
1678
  ...(skippedLine ? [skippedLine] : []),
1494
1679
  declinedLine,
1495
1680
  ].join("\n");
@@ -1498,6 +1683,7 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1498
1683
  `✅ Merge-ready after ${rounds}: ${e.pr.url}`,
1499
1684
  verdictLine,
1500
1685
  levelLine,
1686
+ grantLine,
1501
1687
  ...(skippedLine ? [skippedLine] : []),
1502
1688
  declinedLine,
1503
1689
  // What the driver read at the approved head when it composed this
@@ -1543,7 +1729,19 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1543
1729
  reissue,
1544
1730
  ]);
1545
1731
  case "aborted":
1546
- return join([e.finalReply, e.reason, `⚠️ Ship aborted after ${rounds}.`, reissue]);
1732
+ return join([
1733
+ e.finalReply,
1734
+ e.reason,
1735
+ e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
1736
+ `⚠️ Ship aborted after ${rounds}.`,
1737
+ reissue,
1738
+ ]);
1739
+ case "continued":
1740
+ return join([
1741
+ e.finalReply,
1742
+ `🔁 Segment ${e.segment - 1} ended at its lease with the unit unfinished — ${e.line}. Segment ${e.segment} opens in this thread${e.from !== undefined ? ` from \`${e.from.slice(0, 7)}\`` : ""} under a fresh ${s.input.caps.maxMinutes}-minute lease, with this segment's write-up as its request; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`,
1743
+ budgetSplitLine(e.spent, s.input.caps.maxMinutes),
1744
+ ]);
1547
1745
  case "no_verdict":
1548
1746
  return join([
1549
1747
  `⚠️ Review round ${e.round.index} ended without a submitted verdict (budget, refusal, or stop) — ship never converts that into a request for changes, so no findings step ran.`,
@@ -0,0 +1,155 @@
1
+ // The renewal decision (decision 0046, Renewal: the grant decides, the lease
2
+ // continues). A lease is sized by the infrastructure; the problem is bounded by
3
+ // the grant. When a segment ends with its unit unfinished, the runner reads
4
+ // progress off the row — never off a model's word — and renews only when three
5
+ // things hold: the row shows progress, the grant has a renewal left and the
6
+ // spend is under its cap, and the pipeline's ask still passes the fit.
7
+ // Otherwise it stops and names the clause that failed; a person's reply spends
8
+ // a renewal by hand. Pure and node-free: the unit machine (a Worker) and the
9
+ // bot's routes compute the same decision from the same facts.
10
+
11
+ import { fit, type Grant, type Pipeline } from "../budgets.js";
12
+ import type { Handoff } from "./handoff.js";
13
+
14
+ /** A head the run pushed, as the record carries it (run-history item 2). */
15
+ export interface PushedHeadFact {
16
+ ref: string;
17
+ sha: string;
18
+ at?: number;
19
+ }
20
+
21
+ export interface ProgressInput {
22
+ /** The unit's branch: a push to any other ref is not the unit's progress. */
23
+ branch: string;
24
+ /** The heads the segment's coding run pushed (`RunRecord.pushed`). */
25
+ pushed: readonly PushedHeadFact[];
26
+ /** The head the segment started from — the previous segment's recorded sha.
27
+ * Absent on a fresh branch, where any push is progress. */
28
+ startHead?: string;
29
+ /** When the segment's lease began; a push stamped before it is not this segment's. */
30
+ leaseStartedAt?: number;
31
+ /** The previous segment's handoff and this one's, when both exist. */
32
+ handoff?: { previous: Handoff; current: Handoff };
33
+ }
34
+
35
+ export type Progress =
36
+ | { progressed: true; by: "push"; sha: string }
37
+ | { progressed: true; by: "handoff" }
38
+ | { progressed: false; why: string };
39
+
40
+ /** Two shas name one commit when the shorter is a prefix of the longer. */
41
+ const sameSha = (a: string, b: string): boolean => {
42
+ const x = a.toLowerCase();
43
+ const y = b.toLowerCase();
44
+ return x.length <= y.length ? y.startsWith(x) : x.startsWith(y);
45
+ };
46
+
47
+ /** Progress is a fact the row records after unit eight and the runner reads
48
+ * without a model: the last head pushed to the unit's branch that differs from
49
+ * the head the segment started at and is not older than the lease; failing
50
+ * that, a handoff whose follow-ups shrank or whose deviations grew. */
51
+ export function progressOf(input: ProgressInput): Progress {
52
+ const last = [...input.pushed].reverse().find((h) => h.ref === input.branch);
53
+ if (last !== undefined) {
54
+ const newerThanLease =
55
+ input.leaseStartedAt === undefined || last.at === undefined || last.at >= input.leaseStartedAt;
56
+ const moved = input.startHead === undefined || !sameSha(last.sha, input.startHead);
57
+ if (newerThanLease && moved) return { progressed: true, by: "push", sha: last.sha };
58
+ }
59
+ const pushWhy = `no head newer than the lease's start was pushed to \`${input.branch}\``;
60
+ if (input.handoff === undefined) return { progressed: false, why: pushWhy };
61
+ const { previous, current } = input.handoff;
62
+ if (current.followUps.length < previous.followUps.length || current.deviations.length > previous.deviations.length)
63
+ return { progressed: true, by: "handoff" };
64
+ return { progressed: false, why: `${pushWhy} and the handoff is unchanged` };
65
+ }
66
+
67
+ export interface RenewalInput {
68
+ grant: Grant;
69
+ /** Renewals already spent under this request: the segments beyond the first. */
70
+ renewalsSpent: number;
71
+ /** Dollars spent under this request so far; null when a run's model had no
72
+ * price, since a total that left a model's tokens out would understate it. */
73
+ spendUsd: number | null;
74
+ progress: Progress;
75
+ /** The pipeline the next segment would run: its fit is re-asserted before every segment. */
76
+ pipeline: Pipeline;
77
+ }
78
+
79
+ export type RenewalWhy = "no_progress" | "grant_exhausted" | "cost_cap" | "unfit";
80
+
81
+ export type RenewalDecision =
82
+ | { renew: true; segment: number; from?: string; renewalsLeft: number }
83
+ | { renew: false; why: RenewalWhy; detail: string; renewalsLeft: number };
84
+
85
+ const usd = (n: number): string => `$${n.toFixed(2)}`;
86
+
87
+ /** The decision, in the order the record states the clauses: progress, the
88
+ * grant's count, the cap, the fit. `segment` is the one the renewal opens
89
+ * (the first segment is 1); `from` the sha it continues from when a push made
90
+ * the progress. */
91
+ export function renewalDecision(input: RenewalInput): RenewalDecision {
92
+ const renewalsLeft = Math.max(0, input.grant.renewals - input.renewalsSpent);
93
+ if (!input.progress.progressed) return { renew: false, why: "no_progress", detail: input.progress.why, renewalsLeft };
94
+ if (renewalsLeft === 0) {
95
+ const detail =
96
+ input.grant.renewals === 0
97
+ ? "the grant holds no renewals"
98
+ : `the grant's ${input.grant.renewals} renewal${input.grant.renewals === 1 ? "" : "s"} are spent`;
99
+ return { renew: false, why: "grant_exhausted", detail, renewalsLeft };
100
+ }
101
+ const cap = input.grant.costCapUsd;
102
+ if (cap !== undefined) {
103
+ if (input.spendUsd === null)
104
+ return {
105
+ renew: false,
106
+ why: "cost_cap",
107
+ detail: `spend is unknown (a model had no price) under the grant's cap of $${cap}`,
108
+ renewalsLeft,
109
+ };
110
+ if (input.spendUsd >= cap)
111
+ return {
112
+ renew: false,
113
+ why: "cost_cap",
114
+ detail: `spend ${usd(input.spendUsd)} reached the grant's cap of $${cap}`,
115
+ renewalsLeft,
116
+ };
117
+ }
118
+ const held = fit(input.pipeline);
119
+ if (!held.ok)
120
+ return {
121
+ renew: false,
122
+ why: "unfit",
123
+ detail: `a ${held.have}-minute segment cannot hold the ship loop (${input.pipeline.maxRounds} review rounds need ${held.need} min)`,
124
+ renewalsLeft,
125
+ };
126
+ return {
127
+ renew: true,
128
+ segment: input.renewalsSpent + 2,
129
+ ...(input.progress.by === "push" ? { from: input.progress.sha } : {}),
130
+ renewalsLeft: renewalsLeft - 1,
131
+ };
132
+ }
133
+
134
+ /** The card's words (the record's trace, steps 4 and 9): a renewal names its
135
+ * number of the grant's and the sha it continues from; a stop names the
136
+ * clause and, when renewals remain, the reply that spends one by hand. */
137
+ export function renderRenewal(decision: RenewalDecision, grant: Grant): string {
138
+ if (decision.renew)
139
+ return `renewal ${decision.segment - 1} of ${grant.renewals}, continues ${decision.from !== undefined ? decision.from.slice(0, 7) : "the branch's head"}`;
140
+ const holds =
141
+ decision.renewalsLeft > 0
142
+ ? `grant holds ${decision.renewalsLeft} renewal${decision.renewalsLeft === 1 ? "" : "s"}`
143
+ : "the grant holds no renewals";
144
+ switch (decision.why) {
145
+ case "no_progress":
146
+ return decision.renewalsLeft > 0
147
+ ? `no progress in the last lease; ${holds}; reply continue to spend one`
148
+ : `no progress in the last lease; ${holds}`;
149
+ case "cost_cap":
150
+ return decision.renewalsLeft > 0 ? `${decision.detail}; ${holds} unspent` : decision.detail;
151
+ case "grant_exhausted":
152
+ case "unfit":
153
+ return decision.detail;
154
+ }
155
+ }
@@ -34,6 +34,14 @@ export interface AttrDomain {
34
34
  /** The `<provider>/<model>` that took the turn — a ship run's children answer
35
35
  * on different models, and the run page badges the switch per step. */
36
36
  model: string;
37
+ /** The tools a proxied call offered the model (model-proxy.md item 6): how
38
+ * many, their names sorted and comma-joined, and the request's `tool_choice`
39
+ * by its word — `auto` when tools came and nothing was said, `none` when no
40
+ * tools came or the request said so, `any` for OpenAI's `required`, `tool`
41
+ * for a named tool. */
42
+ tools: number;
43
+ toolNames: string;
44
+ toolChoice: "auto" | "none" | "any" | "tool";
37
45
  stopReason: "end_turn" | "tool_use" | "max_tokens" | "stop_sequence" | "other";
38
46
  inputTokens: number;
39
47
  outputTokens: number;
@@ -159,6 +167,9 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
159
167
  backend: "string",
160
168
  command: "string",
161
169
  model: "string",
170
+ tools: "number",
171
+ toolNames: "string",
172
+ toolChoice: "string",
162
173
  stopReason: "string",
163
174
  inputTokens: "number",
164
175
  outputTokens: "number",
@@ -70,9 +70,10 @@ export interface TraceOptions {
70
70
  span?: Span;
71
71
  /** Which door the call came through when it was not the surface's own
72
72
  * grammar: `route` for a command the request router bound from prose
73
- * (record 0036). Copied onto the registry's audit line, never read by a
74
- * handler. Absent for a typed command. */
75
- source?: "route";
73
+ * (record 0036), `confirm` for a stored input run at a confirmation's
74
+ * click (record 0044). Copied onto the registry's audit line, never read
75
+ * by a handler. Absent for a typed command. */
76
+ source?: "route" | "confirm";
76
77
  }
77
78
 
78
79
  export interface Span {
@@ -186,6 +186,22 @@ export function killStaleBuildProcessesCommand(user: string, dir: string): strin
186
186
  * failure however it died. */
187
187
  const INTERRUPTION_SIGNATURE = /\bexit 143\b|Session terminated|SIGTERM|^restore-interrupted:/;
188
188
 
189
+ /** The wordings git gives when the MIRROR itself is broken: its `origin`
190
+ * remote gone from `config`, the repository not a repository, an object store
191
+ * with bad, corrupt or unreadable objects, a local file it cannot open. None
192
+ * of these is GitHub's doing, and none is cured by waiting for GitHub. */
193
+ const MIRROR_FAILURE_WORDING =
194
+ /'origin' does not appear to be a git repository|not a git repository|\bbad object\b|\bcorrupt\b|unable to read (?:tree|object|commit|blob)|object file .* is empty|cannot open \.git\/|unable to open object|missing object/i;
195
+
196
+ /** Whether a failed mirror fetch failed on the mirror (the resident's own
197
+ * disk — item 67 counts it) rather than on GitHub (`github-unreachable`, a
198
+ * serviceable reason that parks on repeat, item 16b). GitHub is the default:
199
+ * a message names the mirror only through the wordings above, so an unknown
200
+ * failure keeps the reading it always had. */
201
+ export function fetchFailureIsMirrors(message: string): boolean {
202
+ return MIRROR_FAILURE_WORDING.test(message);
203
+ }
204
+
189
205
  /** Message wording of the Sandbox SDK's runtime-replacement error family — the
190
206
  * container went away UNDER a live SDK call, so the failure never reaches the
191
207
  * shell-kill signature above: a step that dies this way surfaces as
@@ -339,10 +355,14 @@ export const WAKE_PORT_READY_MS = 3 * 60_000;
339
355
  * SDK by a test. */
340
356
  export const SDK_RUNTIME_RECORD_KEY = "currentRuntimeIdentity";
341
357
 
342
- /** The DOMException the SDK's connect abort raises: `AbortError`, message
343
- * `The operation was aborted`. Anchored on the whole sentence so a command's
344
- * own `Aborted (core dumped)` never matches. */
345
- export const RUNTIME_UNREACHABLE_WORDING = /\bThe operation was aborted\b/;
358
+ /** The DOMException the SDK's connect abort raises — `fetchUpgradeAttempt`
359
+ * calls `controller.abort()` with no reason when `DEFAULT_CONNECT_TIMEOUT_MS`
360
+ * passes, so the runtime's own `AbortError` with the message exactly `The
361
+ * operation was aborted`. Anchored on the WHOLE message, so a command's own
362
+ * `Aborted (core dumped)` never matches and neither does a `TimeoutError`'s
363
+ * `The operation was aborted due to timeout` — the text `AbortSignal.timeout`
364
+ * raises, a route's own bound on a slow GitHub, not a silent runtime. */
365
+ export const RUNTIME_UNREACHABLE_WORDING = /^The operation was aborted\.?$/;
346
366
 
347
367
  /** Is this one link of an error's cause chain the SDK's connect abort? By
348
368
  * name first (the DOMException's `AbortError`), by the DOMException's message
@@ -38,7 +38,12 @@ export function residentErrorText(text: string): string {
38
38
  return redactAndCap(stripAnsi(text), RESIDENT_ERROR_CAP);
39
39
  }
40
40
 
41
- const SANITIZED_FIELDS = ["error", "reason", "summary"] as const;
41
+ /** The free-text fields a resident body carries: `error` (the reply), `reason`
42
+ * (the answer's own word or a lifecycle reason), `stateReason` (the lifecycle
43
+ * reason beside `state` on a thread route's 503 — the same remote text
44
+ * `reason` used to carry there, item 62), `summary`. A field added to a
45
+ * resident answer that carries remote output is added HERE first. */
46
+ const SANITIZED_FIELDS = ["error", "reason", "stateReason", "summary"] as const;
42
47
 
43
48
  /** How deep the sanitizer descends. The deepest resident shape today is
44
49
  * `/residents` → `residents[]` → `live` → `reason` (depth 3); the bound exists
@@ -48,8 +53,8 @@ const SANITIZE_DEPTH = 4;
48
53
  /** A parsed resident body with its free-text fields made safe, at every level:
49
54
  * `/residents` nests each resident's `state`/`reason` (or an `error`) under
50
55
  * `residents[].live`, and `repo list` renders those. In each plain object only
51
- * `error`, `reason` and `summary` are touched (when strings) — `error` at the
52
- * reply-sized cap, the other two at the card-sized one; `stderr` is
56
+ * `error`, `reason`, `stateReason` and `summary` are touched (when strings) —
57
+ * `error` at the reply-sized cap, the others at the card-sized one; `stderr` is
53
58
  * rewritten only when it mirrors `error` (the thread routes' failure shape
54
59
  * copies the error into stderr). Every other field — `needs`, `state`,
55
60
  * `stdout`, `status`, bindings, numbers — passes through untouched. Arrays are
@@ -238,6 +238,9 @@ export interface McpServerView {
238
238
  promotedFromName?: string;
239
239
  /** A user row's owner (the id after `user:` in `scopeKey`) as a display name, when the lookup answered. */
240
240
  ownerName?: string;
241
+ /** A channel row's channel (the id after `channel:` in `scopeKey`) as its name without the hash,
242
+ * when the lookup answered. */
243
+ channelName?: string;
241
244
  /** A higher tier holds this name, so runs use that entry and this one is idle (`mcpServersFor`'s
242
245
  * `shadowedBy`, the winning scope key — `org`, or `channel:<id>`); absent when this entry serves. */
243
246
  shadowedBy?: string;