@coreplane/switchboard 1.266.1 → 1.267.1
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.
- package/dist/assets/deploy/cloudflare-memory/package.json +1 -1
- package/dist/assets/deploy/cloudflare-memory/tsconfig.json +1 -1
- package/dist/assets/deploy/cloudflare-resident/gc.ts +1 -0
- package/dist/assets/deploy/cloudflare-resident/keyedAsyncLock.ts +20 -0
- package/dist/assets/deploy/cloudflare-resident/legacyCredentials.ts +57 -0
- package/dist/assets/deploy/cloudflare-resident/orphanThreadUsers.ts +14 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +646 -204
- package/dist/assets/package-lock.json +4003 -4040
- package/dist/assets/package.json +2 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/budgets.ts +10 -0
- package/dist/assets/src/core/coordinator/contract.ts +59 -1
- package/dist/assets/src/core/coordinator/driver.ts +34 -5
- package/dist/assets/src/core/runRecord.ts +38 -3
- package/dist/assets/src/core/ship/coordinator.ts +121 -39
- package/dist/assets/src/core/trace/attrs.ts +3 -0
- package/dist/assets/src/execution/githubMintScope.ts +49 -0
- package/dist/assets/src/execution/residentCredentials.ts +9 -37
- package/dist/assets/src/execution/residentDestroyGate.ts +12 -0
- package/dist/assets/src/execution/residentPoolSpends.ts +96 -0
- package/dist/assets/src/execution/residentRecreateAdmission.ts +41 -0
- package/dist/assets/src/execution/residentSteps.ts +1 -0
- package/dist/assets/web/dist/.vite/manifest.json +67 -67
- package/dist/assets/web/dist/assets/{DeliveryPage-BUJckSuW.js → DeliveryPage-QyS6_Q0m.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-BfpIrKDX.js → HomePage-Bm40r0wF.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-C6hroa00.js → PendingTurnRow-BqY-EaZR.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-C2Cb1mkT.js → PlanePage-VmzjNLAz.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Bd9_Q87l.js → ResidentDetailPage-hjX3tRCh.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-C8eS7hxp.js → ResidentsIndexPage-BKGnlCEh.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-DrfFauYJ.js → RunFoldRow-BOWvSoIv.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-B_eXfNAF.js → RunRoutePage-D9ztf-uE.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-BpQu36Dm.js → RunsIndexPage-DTYQbhtr.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-DGHcjcSt.js → ScheduledPage-BAYR0tuv.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-IfQ71QjN.js → SettingsPage-BlYBmCG9.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-D9ON15a8.js → SilentTurn-CHfzRQW8.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-DyFjGg4V.js → StatusDot-BMkUFVE8.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BvPkV4kZ.js → Tooltip-Dfot16IA.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BNe3nBIG.js → UnitRoutePage-BdZoT-4d.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-CJHiTZmM.js +1 -0
- package/dist/assets/web/dist/assets/{dist-v3Py3_QR.js → dist-XRrc0hhm.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-BsWcHHVh.js → indexRow-BGjjLnN5.js} +1 -1
- package/dist/assets/web/dist/assets/{main-ks3kBojV.js → main-B32N1vUh.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-DuALaBPq.js → sseReplay-BsNQPLmJ.js} +2 -2
- package/dist/cli.js +1708 -625
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/budgets-BQgcCyvb.js +0 -1
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.267.1",
|
|
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",
|
package/dist/assets/source.json
CHANGED
|
@@ -64,6 +64,16 @@ export const QUESTION_TTL_MS = DAY_MS;
|
|
|
64
64
|
* that FAILED is never cached — the next read asks again. */
|
|
65
65
|
export const AUTHOR_BINDING_TTL_MS = 15 * MINUTE_MS;
|
|
66
66
|
|
|
67
|
+
/** Maximum local lifetime of any Git receive-pack forward. The bounded
|
|
68
|
+
* request prevents a write connection outliving its run indefinitely after
|
|
69
|
+
* revocation. An aborted remote write has an unknown outcome; existing-PR
|
|
70
|
+
* writes retain their owned pending intent for authoritative reconciliation. */
|
|
71
|
+
export const GIT_RECEIVE_PACK_FORWARD_TIMEOUT_MS = 5 * MINUTE_MS;
|
|
72
|
+
/** The run's final record waits for that one forwarded report, plus a small
|
|
73
|
+
* durable-receipt margin, before sealing. If it still has no answer, the
|
|
74
|
+
* pending intent stays as an uncertain write and no retry is authorized. */
|
|
75
|
+
export const GIT_PUBLICATION_SETTLE_TIMEOUT_MS = GIT_RECEIVE_PACK_FORWARD_TIMEOUT_MS + 10 * SECOND_MS;
|
|
76
|
+
|
|
67
77
|
/** How long a dispatch's FIRST attach to a resident — a fresh run's, or a
|
|
68
78
|
* resumed run's re-attach to its recorded worktree — waits for the resident
|
|
69
79
|
* to wake when the Worker typed its refusal as the platform's transient
|
|
@@ -493,11 +493,30 @@ export interface RoundGate {
|
|
|
493
493
|
findings: string[];
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
+
/** A later human review is evidence to investigate, never typed findings. */
|
|
497
|
+
export interface RecoveryReviewEvidence {
|
|
498
|
+
id: number;
|
|
499
|
+
reviewer: { login: string; id: number };
|
|
500
|
+
headSha: string;
|
|
501
|
+
submittedAt: number;
|
|
502
|
+
body: string;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** The original grant and historically priced children carried through a recovery checkpoint. */
|
|
506
|
+
export interface RecoveryAccounting {
|
|
507
|
+
spendUsd: number;
|
|
508
|
+
children: { runId: string; key: string; usd: number }[];
|
|
509
|
+
grant: Grant;
|
|
510
|
+
renewalsSpent: number;
|
|
511
|
+
}
|
|
512
|
+
|
|
496
513
|
/** One admitted continuation of an ended original unit. The claim replaces
|
|
497
514
|
* the terminal interpretation before a child starts; its step remains under
|
|
498
515
|
* the original instance/unit idempotency namespace. */
|
|
499
516
|
export interface OriginalUnitRecovery {
|
|
500
517
|
kind: "findings" | "review";
|
|
518
|
+
externalReview?: RecoveryReviewEvidence;
|
|
519
|
+
accounting?: RecoveryAccounting;
|
|
501
520
|
round: number;
|
|
502
521
|
expectedHeadSha: string;
|
|
503
522
|
remainingMs: number;
|
|
@@ -531,6 +550,8 @@ export interface OriginalUnitRecovery {
|
|
|
531
550
|
}
|
|
532
551
|
|
|
533
552
|
export interface OriginalUnitRecoveryReceipt {
|
|
553
|
+
externalReview?: RecoveryReviewEvidence;
|
|
554
|
+
accounting?: RecoveryAccounting;
|
|
534
555
|
reviewRunId: string;
|
|
535
556
|
workflowId: string;
|
|
536
557
|
at: number;
|
|
@@ -697,7 +718,6 @@ const isRoundGate = (v: unknown): boolean =>
|
|
|
697
718
|
isAddressSeverity(v.level) &&
|
|
698
719
|
Array.isArray(v.findings) &&
|
|
699
720
|
v.findings.every((f) => typeof f === "string");
|
|
700
|
-
|
|
701
721
|
export const isHumanGatePending = (v: unknown): v is HumanGatePending =>
|
|
702
722
|
isObject(v) &&
|
|
703
723
|
isObject(v.pr) &&
|
|
@@ -758,6 +778,35 @@ export const isUnitWakeAnswer = (v: unknown): v is UnitWakeAnswer => {
|
|
|
758
778
|
);
|
|
759
779
|
};
|
|
760
780
|
|
|
781
|
+
const isPositiveId = (v: unknown): v is number => typeof v === "number" && Number.isSafeInteger(v) && v > 0;
|
|
782
|
+
const isDollars = (v: unknown): v is number => isFinite(v) && v >= 0;
|
|
783
|
+
const isRecoveryReview = (v: unknown): v is RecoveryReviewEvidence =>
|
|
784
|
+
isObject(v) &&
|
|
785
|
+
isPositiveId(v.id) &&
|
|
786
|
+
isObject(v.reviewer) &&
|
|
787
|
+
isText(v.reviewer.login) &&
|
|
788
|
+
isPositiveId(v.reviewer.id) &&
|
|
789
|
+
typeof v.headSha === "string" &&
|
|
790
|
+
/^[0-9a-f]{40}$/i.test(v.headSha) &&
|
|
791
|
+
isFinite(v.submittedAt) &&
|
|
792
|
+
typeof v.body === "string";
|
|
793
|
+
const isRecoveryAccounting = (v: unknown): v is RecoveryAccounting =>
|
|
794
|
+
isObject(v) &&
|
|
795
|
+
isDollars(v.spendUsd) &&
|
|
796
|
+
Array.isArray(v.children) &&
|
|
797
|
+
v.children.length > 0 &&
|
|
798
|
+
v.children.every((c) => isObject(c) && isText(c.runId) && isText(c.key) && isDollars(c.usd)) &&
|
|
799
|
+
new Set(v.children.map((c) => c.runId)).size === v.children.length &&
|
|
800
|
+
new Set(v.children.map((c) => c.key)).size === v.children.length &&
|
|
801
|
+
v.children.reduce((sum, c) => sum + c.usd, 0) === v.spendUsd &&
|
|
802
|
+
isObject(v.grant) &&
|
|
803
|
+
isDollars(v.grant.renewals) &&
|
|
804
|
+
Number.isSafeInteger(v.grant.renewals) &&
|
|
805
|
+
(v.grant.costCapUsd === undefined || (isDollars(v.grant.costCapUsd) && v.grant.costCapUsd > v.spendUsd)) &&
|
|
806
|
+
isDollars(v.renewalsSpent) &&
|
|
807
|
+
Number.isSafeInteger(v.renewalsSpent) &&
|
|
808
|
+
v.renewalsSpent <= v.grant.renewals;
|
|
809
|
+
|
|
761
810
|
export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
762
811
|
if (!isObject(v)) return false;
|
|
763
812
|
const r = v;
|
|
@@ -811,6 +860,13 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
811
860
|
typeof r.recovery.step === "string" &&
|
|
812
861
|
STEP_NAME_PATTERN.test(r.recovery.step) &&
|
|
813
862
|
isText(r.recovery.reviewRunId) &&
|
|
863
|
+
(r.recovery.accounting === undefined || isRecoveryAccounting(r.recovery.accounting)) &&
|
|
864
|
+
(r.recovery.externalReview === undefined ||
|
|
865
|
+
(isRecoveryReview(r.recovery.externalReview) &&
|
|
866
|
+
r.recovery.kind === "review" &&
|
|
867
|
+
isRecoveryAccounting(r.recovery.accounting) &&
|
|
868
|
+
r.recovery.findings === undefined &&
|
|
869
|
+
r.recovery.findingsRunId === undefined)) &&
|
|
814
870
|
(r.recovery.findingsRunId === undefined || isText(r.recovery.findingsRunId)) &&
|
|
815
871
|
(r.recovery.findingsKey === undefined || isText(r.recovery.findingsKey)) &&
|
|
816
872
|
((r.recovery.findingsRunId === undefined && r.recovery.findingsKey === undefined) ||
|
|
@@ -841,6 +897,8 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
841
897
|
r.recoveryReceipt !== undefined &&
|
|
842
898
|
(!isObject(r.recoveryReceipt) ||
|
|
843
899
|
!isText(r.recoveryReceipt.reviewRunId) ||
|
|
900
|
+
(r.recoveryReceipt.externalReview !== undefined && !isRecoveryReview(r.recoveryReceipt.externalReview)) ||
|
|
901
|
+
(r.recoveryReceipt.accounting !== undefined && !isRecoveryAccounting(r.recoveryReceipt.accounting)) ||
|
|
844
902
|
typeof r.recoveryReceipt.workflowId !== "string" ||
|
|
845
903
|
!INSTANCE_ID_PATTERN.test(r.recoveryReceipt.workflowId) ||
|
|
846
904
|
!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
|
-
//
|
|
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.
|
|
@@ -686,6 +687,18 @@ function isOpaqueNotFound(reply: BotReply): boolean {
|
|
|
686
687
|
}
|
|
687
688
|
}
|
|
688
689
|
|
|
690
|
+
/** A finished, addressed child whose final record is still being sealed.
|
|
691
|
+
* Unlike an opaque 404, this answer already establishes the child's identity. */
|
|
692
|
+
function isRecordPending(reply: BotReply): boolean {
|
|
693
|
+
if (reply.status !== 409) return false;
|
|
694
|
+
try {
|
|
695
|
+
const body = JSON.parse(reply.text) as { ok?: unknown; error?: unknown };
|
|
696
|
+
return body?.ok === false && body.error === "record_pending";
|
|
697
|
+
} catch {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
689
702
|
async function readRecordStep(
|
|
690
703
|
step: StepRunner,
|
|
691
704
|
bot: CoordinatorBot,
|
|
@@ -695,8 +708,9 @@ async function readRecordStep(
|
|
|
695
708
|
for (let retry = 0; ; retry++) {
|
|
696
709
|
const name = retry === 0 ? action.step : `${action.step}/record-read/${retry}`;
|
|
697
710
|
const reply = await step.do(name, STEP_CONFIG, () => call(bot, "read-record", body, true));
|
|
698
|
-
|
|
699
|
-
|
|
711
|
+
const pending = isRecordPending(reply);
|
|
712
|
+
const eventQualifiedMissing = action.finishedObserved === true && isOpaqueNotFound(reply);
|
|
713
|
+
if ((!pending && !eventQualifiedMissing) || retry >= SHIP_RECORD_VISIBILITY.retries) return reply;
|
|
700
714
|
await step.sleep(`${action.step}/record-visible/${retry + 1}`, SHIP_RECORD_VISIBILITY.retryMs);
|
|
701
715
|
}
|
|
702
716
|
}
|
|
@@ -1043,7 +1057,15 @@ async function runUnit(
|
|
|
1043
1057
|
addressSeveritySource: plan.addressSeveritySource,
|
|
1044
1058
|
// Recovery spends only the lease already carried by the claim. It never
|
|
1045
1059
|
// opens another segment or idles for a renewal in this checkpoint.
|
|
1046
|
-
grant:
|
|
1060
|
+
grant:
|
|
1061
|
+
row?.recovery === undefined
|
|
1062
|
+
? plan.grant
|
|
1063
|
+
: (row.recovery.accounting?.grant ??
|
|
1064
|
+
// A missing capped carry must still stop before dispatch, while an
|
|
1065
|
+
// uncapped legacy checkpoint must not acquire plan renewals.
|
|
1066
|
+
(plan.grant?.costCapUsd !== undefined
|
|
1067
|
+
? { renewals: 0, costCapUsd: plan.grant.costCapUsd }
|
|
1068
|
+
: { renewals: 0 })),
|
|
1047
1069
|
grantSource: plan.grantSource,
|
|
1048
1070
|
verbosity: plan.verbosity,
|
|
1049
1071
|
idleDays: row?.recovery !== undefined ? 0 : plan.idleDays,
|
|
@@ -1054,7 +1076,13 @@ async function runUnit(
|
|
|
1054
1076
|
...(lastPush !== undefined ? { lastPush } : {}),
|
|
1055
1077
|
...(session !== undefined ? { session } : {}),
|
|
1056
1078
|
...(row?.recovery !== undefined
|
|
1057
|
-
? {
|
|
1079
|
+
? {
|
|
1080
|
+
recovery: {
|
|
1081
|
+
remainingMs: row.recovery.remainingMs,
|
|
1082
|
+
unitKey: `${instanceId}:${unit}`,
|
|
1083
|
+
...(row.recovery.accounting !== undefined ? { renewalsSpent: row.recovery.accounting.renewalsSpent } : {}),
|
|
1084
|
+
},
|
|
1085
|
+
}
|
|
1058
1086
|
: {}),
|
|
1059
1087
|
};
|
|
1060
1088
|
let state: UnitPipelineState =
|
|
@@ -1065,6 +1093,7 @@ async function runUnit(
|
|
|
1065
1093
|
pr: row.pr!,
|
|
1066
1094
|
expectedHeadSha: row.recovery.expectedHeadSha,
|
|
1067
1095
|
reviewRunId: row.recovery.reviewRunId,
|
|
1096
|
+
...(row.recovery.accounting !== undefined ? { spendUsd: row.recovery.accounting.spendUsd } : {}),
|
|
1068
1097
|
...(row.recovery.findingsRunId !== undefined ? { findingsRunId: row.recovery.findingsRunId } : {}),
|
|
1069
1098
|
...(row.recovery.findings !== undefined ? { findings: row.recovery.findings } : {}),
|
|
1070
1099
|
})
|
|
@@ -279,6 +279,17 @@ export interface RunRecord {
|
|
|
279
279
|
* last `pushed_head` event's sha winning — the fact a renewal reads to know
|
|
280
280
|
* the run made progress. Present only on a run that pushed. */
|
|
281
281
|
pushed?: PushedHead[];
|
|
282
|
+
/** A Git-door write whose remote outcome was still uncertain at seal. The
|
|
283
|
+
* live ledger row is deleted at finish, so recovery retains this exact
|
|
284
|
+
* ref transition until head reconciliation. A PR number exists only for a
|
|
285
|
+
* verified existing-PR publication. */
|
|
286
|
+
doorPublicationPending?: {
|
|
287
|
+
id: string;
|
|
288
|
+
repo: string;
|
|
289
|
+
pr?: number;
|
|
290
|
+
owner?: { instanceId: string; unit: string };
|
|
291
|
+
update: { ref: string; old: string; next: string };
|
|
292
|
+
};
|
|
282
293
|
/** What the run cost in tokens, per model, summed from its `model.turn`
|
|
283
294
|
* spans at finish (`usageOfEvents`; docs/reference/specs/costs.md, cost by user).
|
|
284
295
|
* Every record written since carries it (zero turns included); one written
|
|
@@ -572,12 +583,15 @@ function isRunPullRequestShape(v: unknown): v is RunPullRequest {
|
|
|
572
583
|
* explanation's words — so the session's next seed leaves the refused
|
|
573
584
|
* request out of its tail (docs/reference/specs/session-log.md item 9).
|
|
574
585
|
* `provider_transient`: the run's model call failed on a provider transient
|
|
575
|
-
* (a gateway 5xx
|
|
586
|
+
* (a gateway 5xx or timeout) with the harness's retry
|
|
576
587
|
* ladder spent — the ship runner reads it off the child's record to re-run a
|
|
577
588
|
* round-0 child that pushed nothing instead of aborting the unit
|
|
578
|
-
* (docs/reference/specs/agent-ship.md item 9, issue 1932).
|
|
589
|
+
* (docs/reference/specs/agent-ship.md item 9, issue 1932).
|
|
590
|
+
* `model_stream_incomplete`: pi's answer stream ended incomplete after its
|
|
591
|
+
* local retry lease, with no provider-down evidence; Ship grants the same
|
|
592
|
+
* one re-run when nothing was pushed. A failure without
|
|
579
593
|
* a name here leaves the record without the field. */
|
|
580
|
-
export const RUN_FAILURE_KINDS = ["policy_refusal", "provider_transient"] as const;
|
|
594
|
+
export const RUN_FAILURE_KINDS = ["policy_refusal", "provider_transient", "model_stream_incomplete"] as const;
|
|
581
595
|
export type RunFailureKind = (typeof RUN_FAILURE_KINDS)[number];
|
|
582
596
|
export interface RunFailure {
|
|
583
597
|
kind: RunFailureKind;
|
|
@@ -1121,6 +1135,27 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
1121
1135
|
)
|
|
1122
1136
|
)
|
|
1123
1137
|
return false;
|
|
1138
|
+
if (r.doorPublicationPending !== undefined) {
|
|
1139
|
+
const pending = r.doorPublicationPending;
|
|
1140
|
+
if (typeof pending !== "object" || pending === null) return false;
|
|
1141
|
+
const { id, repo, pr, owner, update } = pending as Record<string, unknown>;
|
|
1142
|
+
if (
|
|
1143
|
+
typeof id !== "string" ||
|
|
1144
|
+
!id ||
|
|
1145
|
+
typeof repo !== "string" ||
|
|
1146
|
+
!repo ||
|
|
1147
|
+
(pr !== undefined && (!Number.isInteger(pr) || (pr as number) < 1))
|
|
1148
|
+
)
|
|
1149
|
+
return false;
|
|
1150
|
+
if (owner !== undefined && (typeof owner !== "object" || owner === null)) return false;
|
|
1151
|
+
if (typeof update !== "object" || update === null) return false;
|
|
1152
|
+
const o = owner as Record<string, unknown> | undefined;
|
|
1153
|
+
const u = update as Record<string, unknown>;
|
|
1154
|
+
if (o !== undefined && (typeof o.instanceId !== "string" || typeof o.unit !== "string")) return false;
|
|
1155
|
+
if (typeof u.ref !== "string" || !u.ref.startsWith("refs/heads/")) return false;
|
|
1156
|
+
if (typeof u.old !== "string" || !/^[0-9a-f]{40}$/.test(u.old)) return false;
|
|
1157
|
+
if (typeof u.next !== "string" || !/^[0-9a-f]{40}$/.test(u.next)) return false;
|
|
1158
|
+
}
|
|
1124
1159
|
// A parent is named by a run id (item 46): the same shape as the record's own.
|
|
1125
1160
|
if (r.parentRunId !== undefined && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
|
|
1126
1161
|
return false;
|
|
@@ -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
|
-
*
|
|
634
|
-
*
|
|
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
|
|
1000
|
-
*
|
|
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 =
|
|
@@ -1243,17 +1243,21 @@ type Phase =
|
|
|
1243
1243
|
* must independently prove an open branch and pull request at exactly
|
|
1244
1244
|
* `childHead`; neither a push record nor this flag is remote evidence. */
|
|
1245
1245
|
findingsReady?: true;
|
|
1246
|
+
/** A capped recovery must verify this pushed head before ending, but
|
|
1247
|
+
* missing findings outputs still forbid review or merge-ready success. */
|
|
1248
|
+
findingsIncomplete?: string[];
|
|
1246
1249
|
/** The coding child died (`failed` or `interrupted`) after it may have
|
|
1247
1250
|
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
1248
1251
|
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
1249
1252
|
dead?: "failed" | "interrupted";
|
|
1250
1253
|
/** What ended the dead child (issue 1876), for the `interrupted` ending's sentence. */
|
|
1251
1254
|
cause?: InterruptionCause;
|
|
1252
|
-
/** The dead child's record names a
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1255
|
-
* same round is the `transient` ending. */
|
|
1255
|
+
/** The dead child's record names a retryable model-call failure
|
|
1256
|
+
* (run-history item 57): with nothing pushed, round 0 is re-run once;
|
|
1257
|
+
* a second such failure is the `transient` ending. */
|
|
1256
1258
|
transient?: true;
|
|
1259
|
+
/** The retry signal was a local incomplete stream, not provider-down. */
|
|
1260
|
+
streamIncomplete?: true;
|
|
1257
1261
|
}
|
|
1258
1262
|
/** `queued`: the door enqueued the pull request — the base takes changes
|
|
1259
1263
|
* only through a merge queue (issue 2011) — so every later ask reads the
|
|
@@ -1417,6 +1421,7 @@ export function openRecoveredUnitPipeline(
|
|
|
1417
1421
|
pr: PrRef;
|
|
1418
1422
|
expectedHeadSha: string;
|
|
1419
1423
|
reviewRunId: string;
|
|
1424
|
+
spendUsd?: number;
|
|
1420
1425
|
findingsRunId?: string;
|
|
1421
1426
|
findings?: Finding[];
|
|
1422
1427
|
},
|
|
@@ -1435,12 +1440,10 @@ export function openRecoveredUnitPipeline(
|
|
|
1435
1440
|
reviewRestarts: 0,
|
|
1436
1441
|
pr: recovery.pr,
|
|
1437
1442
|
lastReviewHead: recovery.expectedHeadSha,
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
// admission refuses a cost-capped row rather than resetting an enforced
|
|
1441
|
-
// total, and reports label this split as checkpoint-local.
|
|
1443
|
+
// Elapsed categories remain checkpoint-local. Enforced cumulative dollars
|
|
1444
|
+
// come only from the admission claim's complete persisted child evidence.
|
|
1442
1445
|
spentMs: { coding: 0, review: 0, waiting: 0 },
|
|
1443
|
-
spendUsd: 0,
|
|
1446
|
+
spendUsd: recovery.spendUsd ?? (input.grant?.costCapUsd === undefined ? 0 : null),
|
|
1444
1447
|
findingsByRound: recovery.kind === "findings" ? { [recovery.round]: recovery.findings ?? [] } : {},
|
|
1445
1448
|
humanAnswersByRound: {},
|
|
1446
1449
|
dispositionsByRound: {},
|
|
@@ -1648,7 +1651,7 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
1648
1651
|
return {
|
|
1649
1652
|
type: "pr-check",
|
|
1650
1653
|
step: `${roundStep(s, p.round)}/pr-check`,
|
|
1651
|
-
...(p.dead !== undefined || p.recover === true || p.findingsReady === true
|
|
1654
|
+
...(p.dead !== undefined || p.recover === true || p.findingsReady === true || p.findingsIncomplete !== undefined
|
|
1652
1655
|
? { recover: { runId: p.runId } }
|
|
1653
1656
|
: {}),
|
|
1654
1657
|
// The adopted pull request rides the check so the bot can follow it
|
|
@@ -1747,7 +1750,10 @@ function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEndin
|
|
|
1747
1750
|
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
1748
1751
|
// Unspent: an idle spends no renewal — the wake's segment does (this plan's
|
|
1749
1752
|
// fifth unit) — so the row says what the grant still holds.
|
|
1750
|
-
const renewalsLeft = Math.max(
|
|
1753
|
+
const renewalsLeft = Math.max(
|
|
1754
|
+
0,
|
|
1755
|
+
grant.renewals - (s.input.recovery?.renewalsSpent ?? s.input.session?.renewalsSpent ?? 0),
|
|
1756
|
+
);
|
|
1751
1757
|
// The head to continue from: a segment's own; a review_pending's reviewed
|
|
1752
1758
|
// head — the pull request's, known even when the coding record carried none,
|
|
1753
1759
|
// so the re-issue's resume-at-review fact survives the idle; else the last
|
|
@@ -1832,7 +1838,10 @@ function parkHumanGate(
|
|
|
1832
1838
|
kind: "idle",
|
|
1833
1839
|
why: "held",
|
|
1834
1840
|
idled: held,
|
|
1835
|
-
renewalsLeft: Math.max(
|
|
1841
|
+
renewalsLeft: Math.max(
|
|
1842
|
+
0,
|
|
1843
|
+
grant.renewals - (s.input.recovery?.renewalsSpent ?? s.input.session?.renewalsSpent ?? 0),
|
|
1844
|
+
),
|
|
1836
1845
|
...(s.lastReviewHead !== undefined ? { from: s.lastReviewHead } : {}),
|
|
1837
1846
|
...(s.lastCodingRunId !== undefined ? { runId: s.lastCodingRunId } : {}),
|
|
1838
1847
|
spendUsd: s.spendUsd,
|
|
@@ -1878,10 +1887,30 @@ function capEnding(s: UnitPipelineState, round?: RoundRef, refused?: Carve): Uni
|
|
|
1878
1887
|
};
|
|
1879
1888
|
}
|
|
1880
1889
|
|
|
1890
|
+
/** Recovery cannot buy another child (or claim readiness) after the original
|
|
1891
|
+
* dollar cap is spent. Unknown cost is not zero and never resets that cap. */
|
|
1892
|
+
function recoveryCostEnding(s: UnitPipelineState): UnitEnding | undefined {
|
|
1893
|
+
const cap = s.input.grant?.costCapUsd;
|
|
1894
|
+
if (s.input.recovery === undefined || cap === undefined) return undefined;
|
|
1895
|
+
if (s.spendUsd !== null && Number.isFinite(s.spendUsd) && s.spendUsd >= 0 && s.spendUsd < cap) return undefined;
|
|
1896
|
+
return {
|
|
1897
|
+
kind: "aborted",
|
|
1898
|
+
reason:
|
|
1899
|
+
s.spendUsd === null
|
|
1900
|
+
? `The original $${cap} cost cap cannot be proven: cumulative child spend is unknown.`
|
|
1901
|
+
: `The original $${cap} cost cap is exhausted; cumulative child spend is $${s.spendUsd}.`,
|
|
1902
|
+
reviewRounds: s.reviewRounds,
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1881
1906
|
/** Start a round if its carve holds (agent-ship item 8): a round the
|
|
1882
1907
|
* remainder cannot carve above its floor is not dispatched, and the unit ends
|
|
1883
1908
|
* at the cap naming the round, what it would have got and the floor. */
|
|
1884
1909
|
function enterRound(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNote[] = []): Transition {
|
|
1910
|
+
if (s.input.recovery !== undefined && round.index > s.input.caps.maxRounds)
|
|
1911
|
+
return end(s, { kind: "round_cap", maxRounds: s.input.caps.maxRounds, reviewRounds: s.reviewRounds }, notes);
|
|
1912
|
+
const costEnding = recoveryCostEnding(s);
|
|
1913
|
+
if (costEnding !== undefined) return end(s, costEnding, notes);
|
|
1885
1914
|
const carved = roundCarve(s, round);
|
|
1886
1915
|
if (carved.kind === "refused") return end(s, capEnding(s, round, carved), notes);
|
|
1887
1916
|
const reviewRounds = round.kind === "review" ? round.index : s.reviewRounds;
|
|
@@ -1976,6 +2005,11 @@ function settleCoding(
|
|
|
1976
2005
|
}
|
|
1977
2006
|
: {}),
|
|
1978
2007
|
};
|
|
2008
|
+
// A completed findings child may have moved the head. Its verification
|
|
2009
|
+
// read reconciles that head before the cap ends the unit.
|
|
2010
|
+
const costEnding = recoveryCostEnding(next);
|
|
2011
|
+
if (costEnding !== undefined && !(round.kind === "findings" && facts.status === "completed"))
|
|
2012
|
+
return end(next, costEnding, [roundNote(round, "aborted")]);
|
|
1979
2013
|
const readySalvage = round.kind === "coding" && completedSameHeadSalvage(next, facts.pushed, facts);
|
|
1980
2014
|
const checkpoint = interruptedCheckpoint(next, facts.pushed, round.kind === "findings" ? undefined : facts);
|
|
1981
2015
|
const mode = stopMode(facts.status);
|
|
@@ -2007,6 +2041,21 @@ function settleCoding(
|
|
|
2007
2041
|
if (facts.description !== true) missingOutputs.push("missing updated pull request description");
|
|
2008
2042
|
const observedHead = fullHead(facts.headSha);
|
|
2009
2043
|
if (observedHead === undefined) missingOutputs.push("missing final observed commit");
|
|
2044
|
+
if (
|
|
2045
|
+
missingOutputs.length > 0 &&
|
|
2046
|
+
costEnding !== undefined &&
|
|
2047
|
+
observedHead !== undefined &&
|
|
2048
|
+
facts.pushed?.length === 1 &&
|
|
2049
|
+
facts.pushed[0]?.ref === next.input.unit.branch &&
|
|
2050
|
+
facts.pushed[0]?.sha === observedHead
|
|
2051
|
+
)
|
|
2052
|
+
return {
|
|
2053
|
+
state: {
|
|
2054
|
+
...next,
|
|
2055
|
+
phase: { at: "pr-check", round, runId, childHead: observedHead, findingsIncomplete: missingOutputs },
|
|
2056
|
+
},
|
|
2057
|
+
notes: [],
|
|
2058
|
+
};
|
|
2010
2059
|
if (missingOutputs.length > 0)
|
|
2011
2060
|
return end(
|
|
2012
2061
|
next,
|
|
@@ -2060,9 +2109,11 @@ function settleCoding(
|
|
|
2060
2109
|
const cause =
|
|
2061
2110
|
facts.failure?.kind === "provider_transient"
|
|
2062
2111
|
? "the model provider's transport retry budget was spent"
|
|
2063
|
-
: facts.
|
|
2064
|
-
? "
|
|
2065
|
-
:
|
|
2112
|
+
: facts.failure?.kind === "model_stream_incomplete"
|
|
2113
|
+
? "the model stream ended before a complete answer"
|
|
2114
|
+
: facts.status === "failed"
|
|
2115
|
+
? "it failed before finishing"
|
|
2116
|
+
: "it ended before its work was ready for review";
|
|
2066
2117
|
return end(
|
|
2067
2118
|
next,
|
|
2068
2119
|
{
|
|
@@ -2088,7 +2139,10 @@ function settleCoding(
|
|
|
2088
2139
|
round,
|
|
2089
2140
|
runId,
|
|
2090
2141
|
dead: "failed",
|
|
2091
|
-
...(facts.failure?.kind === "provider_transient"
|
|
2142
|
+
...(facts.failure?.kind === "provider_transient" || facts.failure?.kind === "model_stream_incomplete"
|
|
2143
|
+
? { transient: true as const }
|
|
2144
|
+
: {}),
|
|
2145
|
+
...(facts.failure?.kind === "model_stream_incomplete" ? { streamIncomplete: true as const } : {}),
|
|
2092
2146
|
},
|
|
2093
2147
|
},
|
|
2094
2148
|
notes: [],
|
|
@@ -2117,10 +2171,11 @@ function settleReview(
|
|
|
2117
2171
|
facts: Extract<ChildFacts, { finished: true }>,
|
|
2118
2172
|
): Transition {
|
|
2119
2173
|
const mode = stopMode(facts.status);
|
|
2174
|
+
const charged = { ...s, spendUsd: addSpend(s.spendUsd, facts.costUsd) };
|
|
2120
2175
|
if (!facts.verdict) {
|
|
2121
2176
|
if (mode !== undefined)
|
|
2122
2177
|
return end(
|
|
2123
|
-
|
|
2178
|
+
charged,
|
|
2124
2179
|
{
|
|
2125
2180
|
kind: "stopped",
|
|
2126
2181
|
mode,
|
|
@@ -2131,7 +2186,7 @@ function settleReview(
|
|
|
2131
2186
|
[roundNote(round, "stopped")],
|
|
2132
2187
|
);
|
|
2133
2188
|
return end(
|
|
2134
|
-
|
|
2189
|
+
charged,
|
|
2135
2190
|
{
|
|
2136
2191
|
kind: "no_verdict",
|
|
2137
2192
|
round,
|
|
@@ -2143,13 +2198,14 @@ function settleReview(
|
|
|
2143
2198
|
}
|
|
2144
2199
|
const verdict = facts.verdict;
|
|
2145
2200
|
const next: UnitPipelineState = {
|
|
2146
|
-
...
|
|
2147
|
-
spendUsd: addSpend(s.spendUsd, facts.costUsd),
|
|
2201
|
+
...charged,
|
|
2148
2202
|
findingsByRound: { ...s.findingsByRound, [round.index]: verdict.findings },
|
|
2149
2203
|
...(facts.reviewHead !== undefined ? { lastReviewHead: facts.reviewHead } : {}),
|
|
2150
2204
|
...(verdict.summary !== undefined ? { lastVerdictSummary: verdict.summary } : {}),
|
|
2151
2205
|
};
|
|
2152
2206
|
const notes = [roundNote(round, verdict.verdict)];
|
|
2207
|
+
const costEnding = recoveryCostEnding(next);
|
|
2208
|
+
if (costEnding !== undefined) return end(next, costEnding, notes);
|
|
2153
2209
|
if (verdict.verdict === "approve") {
|
|
2154
2210
|
// Merge-ready stands on the POSTED approval: an approve whose post did
|
|
2155
2211
|
// not land left no approving review on the pull request. The reason is
|
|
@@ -2619,7 +2675,7 @@ function finishSupersededChild(
|
|
|
2619
2675
|
if (phase.round.kind === "findings") {
|
|
2620
2676
|
const settled = settleCoding(s, phase.round, phase.runId, facts);
|
|
2621
2677
|
const ready = settled.state.phase;
|
|
2622
|
-
if (ready.at === "pr-check" && ready.findingsReady === true)
|
|
2678
|
+
if (ready.at === "pr-check" && (ready.findingsReady === true || ready.findingsIncomplete !== undefined))
|
|
2623
2679
|
return settlePrCheck(settled.state, ready, supersession.pullRequest);
|
|
2624
2680
|
return settled;
|
|
2625
2681
|
}
|
|
@@ -2672,11 +2728,24 @@ export function reconcileTerminalPr(s: UnitPipelineState, pr: PrCheck): UnitPipe
|
|
|
2672
2728
|
/** The pull request heading the branch after round 0 or a findings step. */
|
|
2673
2729
|
function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-check" }>, pr: PrCheck): Transition {
|
|
2674
2730
|
const { round } = phase;
|
|
2731
|
+
const endIncompleteFindings = (verified: UnitPipelineState): Transition => {
|
|
2732
|
+
const cap = recoveryCostEnding(verified);
|
|
2733
|
+
return end(
|
|
2734
|
+
verified,
|
|
2735
|
+
{
|
|
2736
|
+
kind: "aborted",
|
|
2737
|
+
reason: `${cap?.kind === "aborted" ? `${cap.reason} ` : ""}The completed findings work still lacks required results (${phase.findingsIncomplete?.join("; ")}); no review started.`,
|
|
2738
|
+
round,
|
|
2739
|
+
reviewRounds: verified.reviewRounds,
|
|
2740
|
+
},
|
|
2741
|
+
[roundNote(round, "aborted")],
|
|
2742
|
+
);
|
|
2743
|
+
};
|
|
2675
2744
|
// A completed findings run is not ready because it says it pushed. It is
|
|
2676
2745
|
// ready only when this independent read sees the adopted pull request at the
|
|
2677
2746
|
// exact full observed source head (and, while open, its live branch). Every
|
|
2678
2747
|
// other remote fact is a resumable stop with no review or merged success.
|
|
2679
|
-
if (phase.findingsReady === true) {
|
|
2748
|
+
if (phase.findingsReady === true || phase.findingsIncomplete !== undefined) {
|
|
2680
2749
|
const observedHead = fullHead(phase.childHead)!;
|
|
2681
2750
|
const withPr = pr.state !== "none" ? { ...s, pr: { number: pr.prNumber, url: pr.url } } : s;
|
|
2682
2751
|
if (pr.state === "merged") {
|
|
@@ -2708,6 +2777,8 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
2708
2777
|
},
|
|
2709
2778
|
[roundNote(round, "aborted")],
|
|
2710
2779
|
);
|
|
2780
|
+
if (phase.findingsIncomplete !== undefined)
|
|
2781
|
+
return endIncompleteFindings({ ...withPr, lastReviewHead: remoteHead });
|
|
2711
2782
|
return foundMerged(withPr, pr, [roundNote(round, "completed")]);
|
|
2712
2783
|
}
|
|
2713
2784
|
if (pr.state !== "open" || pr.headBranchExists === false)
|
|
@@ -2751,6 +2822,10 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
2751
2822
|
},
|
|
2752
2823
|
[roundNote(round, "aborted")],
|
|
2753
2824
|
);
|
|
2825
|
+
const reconciled = { ...withPr, lastReviewHead: remoteHead };
|
|
2826
|
+
if (phase.findingsIncomplete !== undefined) return endIncompleteFindings(reconciled);
|
|
2827
|
+
const costEnding = recoveryCostEnding(reconciled);
|
|
2828
|
+
if (costEnding !== undefined) return end(reconciled, costEnding, [roundNote(round, "aborted")]);
|
|
2754
2829
|
return roundOnOpenPr(withPr, phase, pr);
|
|
2755
2830
|
}
|
|
2756
2831
|
// The merge landed during the round: the child found nothing left to ship
|
|
@@ -2776,15 +2851,23 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
2776
2851
|
[roundNote(round, "aborted")],
|
|
2777
2852
|
);
|
|
2778
2853
|
if (phase.dead === "failed") {
|
|
2779
|
-
// A
|
|
2854
|
+
// A retryable model-call failure with nothing pushed is not the child's failure
|
|
2780
2855
|
// (issue 1932): the ledger row and the branch are untouched, so round 0
|
|
2781
2856
|
// is re-run once — a fresh attempt under fresh step names — and only a
|
|
2782
2857
|
// second transient in the same round is the ending, named `transient`.
|
|
2783
2858
|
if (phase.transient && round.kind === "coding" && pr.unrecovered === "no_commits") {
|
|
2784
2859
|
if ((round.attempt ?? 1) < 2) return enterRound(s, { ...round, attempt: 2 }, [roundNote(round, "transient")]);
|
|
2785
|
-
return end(
|
|
2786
|
-
|
|
2787
|
-
|
|
2860
|
+
return end(
|
|
2861
|
+
s,
|
|
2862
|
+
{
|
|
2863
|
+
kind: "transient",
|
|
2864
|
+
round,
|
|
2865
|
+
runId: phase.runId,
|
|
2866
|
+
reviewRounds: s.reviewRounds,
|
|
2867
|
+
...(phase.streamIncomplete ? { streamIncomplete: true as const } : {}),
|
|
2868
|
+
},
|
|
2869
|
+
[roundNote(round, "transient")],
|
|
2870
|
+
);
|
|
2788
2871
|
}
|
|
2789
2872
|
// The abort repeats the bot's reason for recovering nothing, and claims
|
|
2790
2873
|
// no more than the answer carried.
|
|
@@ -3825,11 +3908,10 @@ function renderUnitReportWithWake(
|
|
|
3825
3908
|
const level = s.input.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
|
|
3826
3909
|
const levelLine = `Severity addressed: ${level} and above (set by ${s.input.addressSeveritySource ?? "org"}).`;
|
|
3827
3910
|
// The grant as the instance carries it (decision 0046): spent of granted,
|
|
3828
|
-
// the cap when one is set, and who granted it
|
|
3829
|
-
//
|
|
3830
|
-
// decision exists.
|
|
3911
|
+
// the cap when one is set, and who granted it. A capped recovery carries
|
|
3912
|
+
// proven spent renewals; an uncapped legacy checkpoint has zero capacity.
|
|
3831
3913
|
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"}).`;
|
|
3914
|
+
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
3915
|
const lastFindings = s.findingsByRound[e.reviewRounds] ?? [];
|
|
3834
3916
|
const skipped = lastFindings.filter((f) => !findingsAtOrAbove([f], level).length);
|
|
3835
3917
|
const skippedLine =
|
|
@@ -4076,11 +4158,11 @@ function renderUnitReportWithWake(
|
|
|
4076
4158
|
]);
|
|
4077
4159
|
case "transient":
|
|
4078
4160
|
if (!shows(verbosity, "verbose"))
|
|
4079
|
-
return `⚠️ Aborted after ${rounds}: this is a bug — the model
|
|
4161
|
+
return `⚠️ Aborted after ${rounds}: this is a bug — the model call failed twice and no automatic retry remains.${prLine}`;
|
|
4080
4162
|
return join([
|
|
4081
|
-
`⚠️ The coding child of round ${e.round.index} (run ${e.runId}) died on a provider transient — a model-gateway 5xx
|
|
4163
|
+
`⚠️ 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
4164
|
writeUpPointer(s, e.round.kind, s.lastCodingRunId),
|
|
4083
|
-
`⚠️ This is a bug: ship ended after ${rounds} because its automatic
|
|
4165
|
+
`⚠️ This is a bug: ship ended after ${rounds} because its automatic model-call retry was spent.`,
|
|
4084
4166
|
nextAction(),
|
|
4085
4167
|
]);
|
|
4086
4168
|
case "no_verdict":
|