@coreplane/switchboard 1.266.0 → 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.
- package/dist/assets/deploy/cloudflare/worker.ts +7 -1
- package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +5 -1
- 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-memory/worker.ts +48 -3
- package/dist/assets/deploy/cloudflare-resident/legacyCredentials.ts +43 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +102 -141
- 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/channels/githubDoorPaths.ts +36 -0
- package/dist/assets/src/core/coordinator/contract.ts +32 -1
- package/dist/assets/src/core/coordinator/driver.ts +19 -3
- package/dist/assets/src/core/refusal.ts +1 -0
- package/dist/assets/src/core/runEvents.ts +1 -0
- package/dist/assets/src/core/runRecord.ts +47 -3
- package/dist/assets/src/core/ship/coordinator.ts +80 -36
- package/dist/assets/src/core/trace/attrs.ts +3 -0
- package/dist/assets/src/execution/residentReadonly.ts +9 -8
- package/dist/assets/src/execution/residentSteps.ts +2 -1
- package/dist/assets/src/execution/seedPlan.ts +2 -2
- package/dist/assets/web/dist/.vite/manifest.json +49 -49
- package/dist/assets/web/dist/assets/{HomePage-B2Y-Fa9V.js → HomePage-BfpIrKDX.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-DIC86uWA.js → PendingTurnRow-C6hroa00.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-CfrbMh8t.js → PlanePage-C2Cb1mkT.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DRNwx2--.js → ResidentDetailPage-Bd9_Q87l.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-EHKMtX69.js → ResidentsIndexPage-C8eS7hxp.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-CAdpX4Gn.js → RunFoldRow-DrfFauYJ.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-DppLhu79.js → RunRoutePage-B_eXfNAF.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-C7hx5O1X.js → RunsIndexPage-BpQu36Dm.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-Dk2kMnYT.js → ScheduledPage-DGHcjcSt.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-DAf6R383.js → SettingsPage-IfQ71QjN.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-CPED_XCV.js → SilentTurn-D9ON15a8.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-IkfsHGXg.js → StatusDot-DyFjGg4V.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CmQVfAl2.js → Tooltip-BvPkV4kZ.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BjfuZVWr.js → UnitRoutePage-BNe3nBIG.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-Dg6HTzpQ.js → dist-v3Py3_QR.js} +1 -1
- package/dist/assets/web/dist/assets/{main-Bk1L-bNk.js → main-ks3kBojV.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-DGRDRLua.js → sseReplay-DuALaBPq.js} +1 -1
- package/dist/cli.js +2629 -935
- package/package.json +1 -1
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "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",
|
package/dist/assets/source.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The public Git door surface, shared by the bot and its Cloudflare edge. */
|
|
2
|
+
export const GIT_DOOR_PATH =
|
|
3
|
+
/^\/(?:git\/)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\.git\/(info\/refs|git-upload-pack|git-receive-pack)$/;
|
|
4
|
+
|
|
5
|
+
export function isGithubDoorPath(path: string): boolean {
|
|
6
|
+
return path === "/api/graphql" || path.startsWith("/api/v3/") || GIT_DOOR_PATH.test(path);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function githubDoorMethodAllowed(url: URL, method: string): boolean {
|
|
10
|
+
const path = url.pathname;
|
|
11
|
+
if (path === "/api/graphql") return method === "POST";
|
|
12
|
+
if (path.startsWith("/api/v3/")) return method === "GET" || method === "HEAD";
|
|
13
|
+
const git = GIT_DOOR_PATH.exec(path);
|
|
14
|
+
if (!git) return false;
|
|
15
|
+
if (git[3] === "info/refs")
|
|
16
|
+
return method === "GET" && ["git-upload-pack", "git-receive-pack"].includes(url.searchParams.get("service") ?? "");
|
|
17
|
+
return method === "POST";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A configured git.* host exposes only the door. A door path on any other
|
|
21
|
+
* host, including workers.dev and the dashboard host, is never forwarded. */
|
|
22
|
+
export function githubDoorEdgeRoute(url: URL, method: string, configuredBase?: string): "door" | "other" | "refuse" {
|
|
23
|
+
const doorPath = isGithubDoorPath(url.pathname);
|
|
24
|
+
if (!configuredBase) return doorPath ? "refuse" : "other";
|
|
25
|
+
let base: URL;
|
|
26
|
+
try {
|
|
27
|
+
base = new URL(configuredBase);
|
|
28
|
+
} catch {
|
|
29
|
+
return "refuse";
|
|
30
|
+
}
|
|
31
|
+
if (base.protocol !== "https:" || base.pathname !== "/" || base.search || base.hash) return "refuse";
|
|
32
|
+
if (url.host === base.host) {
|
|
33
|
+
return url.protocol === "https:" && doorPath && githubDoorMethodAllowed(url, method) ? "door" : "refuse";
|
|
34
|
+
}
|
|
35
|
+
return doorPath ? "refuse" : "other";
|
|
36
|
+
}
|
|
@@ -313,6 +313,8 @@ export interface ExistingPrPublicationBinding {
|
|
|
313
313
|
export interface CoordinatorTag {
|
|
314
314
|
parentInstanceId: string;
|
|
315
315
|
idempotencyKey: string;
|
|
316
|
+
/** The unit's durable write branch, read from its row at child admission. */
|
|
317
|
+
branch?: string;
|
|
316
318
|
/** Workflow transport for a recovered child. Identity and idempotency remain
|
|
317
319
|
* on `parentInstanceId`; only lifecycle wake-ups use this checkpoint id. */
|
|
318
320
|
transportWorkflowId?: string;
|
|
@@ -491,11 +493,20 @@ export interface RoundGate {
|
|
|
491
493
|
findings: string[];
|
|
492
494
|
}
|
|
493
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
|
+
|
|
494
504
|
/** One admitted continuation of an ended original unit. The claim replaces
|
|
495
505
|
* the terminal interpretation before a child starts; its step remains under
|
|
496
506
|
* the original instance/unit idempotency namespace. */
|
|
497
507
|
export interface OriginalUnitRecovery {
|
|
498
508
|
kind: "findings" | "review";
|
|
509
|
+
accounting?: RecoveryAccounting;
|
|
499
510
|
round: number;
|
|
500
511
|
expectedHeadSha: string;
|
|
501
512
|
remainingMs: number;
|
|
@@ -529,6 +540,7 @@ export interface OriginalUnitRecovery {
|
|
|
529
540
|
}
|
|
530
541
|
|
|
531
542
|
export interface OriginalUnitRecoveryReceipt {
|
|
543
|
+
accounting?: RecoveryAccounting;
|
|
532
544
|
reviewRunId: string;
|
|
533
545
|
workflowId: string;
|
|
534
546
|
at: number;
|
|
@@ -695,7 +707,6 @@ const isRoundGate = (v: unknown): boolean =>
|
|
|
695
707
|
isAddressSeverity(v.level) &&
|
|
696
708
|
Array.isArray(v.findings) &&
|
|
697
709
|
v.findings.every((f) => typeof f === "string");
|
|
698
|
-
|
|
699
710
|
export const isHumanGatePending = (v: unknown): v is HumanGatePending =>
|
|
700
711
|
isObject(v) &&
|
|
701
712
|
isObject(v.pr) &&
|
|
@@ -756,6 +767,24 @@ export const isUnitWakeAnswer = (v: unknown): v is UnitWakeAnswer => {
|
|
|
756
767
|
);
|
|
757
768
|
};
|
|
758
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
|
+
|
|
759
788
|
export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
760
789
|
if (!isObject(v)) return false;
|
|
761
790
|
const r = v;
|
|
@@ -809,6 +838,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
809
838
|
typeof r.recovery.step === "string" &&
|
|
810
839
|
STEP_NAME_PATTERN.test(r.recovery.step) &&
|
|
811
840
|
isText(r.recovery.reviewRunId) &&
|
|
841
|
+
(r.recovery.accounting === undefined || isRecoveryAccounting(r.recovery.accounting)) &&
|
|
812
842
|
(r.recovery.findingsRunId === undefined || isText(r.recovery.findingsRunId)) &&
|
|
813
843
|
(r.recovery.findingsKey === undefined || isText(r.recovery.findingsKey)) &&
|
|
814
844
|
((r.recovery.findingsRunId === undefined && r.recovery.findingsKey === undefined) ||
|
|
@@ -839,6 +869,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
839
869
|
r.recoveryReceipt !== undefined &&
|
|
840
870
|
(!isObject(r.recoveryReceipt) ||
|
|
841
871
|
!isText(r.recoveryReceipt.reviewRunId) ||
|
|
872
|
+
(r.recoveryReceipt.accounting !== undefined && !isRecoveryAccounting(r.recoveryReceipt.accounting)) ||
|
|
842
873
|
typeof r.recoveryReceipt.workflowId !== "string" ||
|
|
843
874
|
!INSTANCE_ID_PATTERN.test(r.recoveryReceipt.workflowId) ||
|
|
844
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
|
-
//
|
|
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:
|
|
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
|
-
? {
|
|
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
|
})
|
|
@@ -64,6 +64,7 @@ const CAUSE_OF = {
|
|
|
64
64
|
// one pipeline per thread (record 0060): the host key's claim answered thread-live
|
|
65
65
|
ship_thread_live: "request",
|
|
66
66
|
setup_failed: "system",
|
|
67
|
+
run_budget_exhausted: "system",
|
|
67
68
|
publication_ownership_unknown: "system",
|
|
68
69
|
// the click on a confirmation (confirm.ts)
|
|
69
70
|
confirmation_used: "request",
|
|
@@ -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
|
|
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).
|
|
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;
|
|
@@ -702,9 +705,50 @@ export function clampListLimit(limit: number | undefined): number {
|
|
|
702
705
|
return Math.min(RUN_LIST_MAX_LIMIT, Math.max(1, Math.floor(limit)));
|
|
703
706
|
}
|
|
704
707
|
|
|
708
|
+
/** The recovery audit must see either identity claim, including contradictory
|
|
709
|
+
* parents, and any activity in the original threads. Never AND these claims. */
|
|
710
|
+
export interface RecoveryEvidenceScope {
|
|
711
|
+
instanceId: string;
|
|
712
|
+
unit: string;
|
|
713
|
+
threadKeys: string[];
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function isRecoveryEvidenceScope(value: unknown): value is RecoveryEvidenceScope {
|
|
717
|
+
if (typeof value !== "object" || value === null) return false;
|
|
718
|
+
const scope = value as RecoveryEvidenceScope;
|
|
719
|
+
return (
|
|
720
|
+
typeof scope.instanceId === "string" &&
|
|
721
|
+
scope.instanceId.length > 0 &&
|
|
722
|
+
scope.instanceId.length <= 256 &&
|
|
723
|
+
typeof scope.unit === "string" &&
|
|
724
|
+
/^U[1-9][0-9]*$/.test(scope.unit) &&
|
|
725
|
+
scope.unit.length <= 64 &&
|
|
726
|
+
Array.isArray(scope.threadKeys) &&
|
|
727
|
+
scope.threadKeys.length > 0 &&
|
|
728
|
+
scope.threadKeys.length <= 2 &&
|
|
729
|
+
scope.threadKeys.every((key) => typeof key === "string" && key.length > 0 && key.length <= 256)
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function matchesRecoveryEvidence(
|
|
734
|
+
run: { parentInstanceId?: string; idempotencyKey?: string; threadKey?: string },
|
|
735
|
+
scope: RecoveryEvidenceScope,
|
|
736
|
+
): boolean {
|
|
737
|
+
const unitKey = `${scope.instanceId}:${scope.unit}`;
|
|
738
|
+
return (
|
|
739
|
+
run.parentInstanceId === scope.instanceId ||
|
|
740
|
+
run.idempotencyKey === unitKey ||
|
|
741
|
+
run.idempotencyKey?.startsWith(`${unitKey}/`) === true ||
|
|
742
|
+
(run.threadKey !== undefined && scope.threadKeys.includes(run.threadKey))
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
705
746
|
/** The `list` query — the same fields on the wire (`/runs/list`) and in the
|
|
706
747
|
* `RunStore` interface. */
|
|
707
748
|
export interface RunListOptions {
|
|
749
|
+
/** Internal admission audit: a complete bounded relevant set, never a UI page.
|
|
750
|
+
* A Worker must explicitly attest completeness; a full page is not evidence. */
|
|
751
|
+
recoveryEvidence?: RecoveryEvidenceScope;
|
|
708
752
|
/** Rows to return: default `RUN_LIST_DEFAULT_LIMIT`, capped at `RUN_LIST_MAX_LIMIT`. */
|
|
709
753
|
limit?: number;
|
|
710
754
|
/** Cursor: only runs ordered after the last row seen — `finishedAt` strictly
|
|
@@ -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 =
|
|
@@ -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
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
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
|
-
//
|
|
1439
|
-
//
|
|
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(
|
|
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(
|
|
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.
|
|
2064
|
-
? "
|
|
2065
|
-
:
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
|
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(
|
|
2786
|
-
|
|
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
|
|
3829
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Mode is sticky per attach, not per thread: the binding records the mode
|
|
17
17
|
* the tree was last built for, and an attach in the OTHER mode recreates the
|
|
18
|
-
* tree (a
|
|
19
|
-
* writable
|
|
20
|
-
*
|
|
18
|
+
* tree (a mirror-origin tree must never be handed to a writable run, and a
|
|
19
|
+
* writable tree's door origin must never survive into a read-only run).
|
|
20
|
+
* Recreating is the simple safe option — the only
|
|
21
21
|
* state a tree carries between attaches is scratch files, and a mode switch
|
|
22
22
|
* on one thread is rare (a thread is normally one agent for its whole life). */
|
|
23
23
|
|
|
@@ -37,12 +37,13 @@ export interface ReadonlyAttachPlan {
|
|
|
37
37
|
/** True when the existing tree was built for the other mode and must be wiped
|
|
38
38
|
* before reuse — evaluated before the ordinary dirty/stale checks. */
|
|
39
39
|
modeSwitch: boolean;
|
|
40
|
-
/**
|
|
40
|
+
/** Legacy mode projection. The Worker uses the Git door for writable trees;
|
|
41
|
+
* neither mode writes an App token into a thread tree. */
|
|
41
42
|
credentialFile: boolean;
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
43
|
+
/** Read-only mode requires cleanup; the Worker also scrubs writable trees
|
|
44
|
+
* before attaching the Git door. */
|
|
44
45
|
scrubCredentials: boolean;
|
|
45
|
-
/**
|
|
46
|
+
/** Legacy origin fallback; the Worker overrides writable origin with the door. */
|
|
46
47
|
originUrl: string;
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -51,7 +52,7 @@ export function planReadonlyAttach(input: {
|
|
|
51
52
|
/** The thread's existing binding, if any. `evicted` trees are gone from disk
|
|
52
53
|
* and are recreated anyway, so their recorded mode is irrelevant. */
|
|
53
54
|
prior?: { readonly?: boolean; evicted?: boolean };
|
|
54
|
-
/** `owner/name` of the repo — the
|
|
55
|
+
/** `owner/name` of the repo — used by the legacy origin fallback. */
|
|
55
56
|
slug: string;
|
|
56
57
|
/** The resident's bare mirror path — the read-only origin. Thread users are
|
|
57
58
|
* denied traversal into it (root:worker1 750), so fetch/push fail while
|
|
@@ -51,6 +51,8 @@ export const RESIDENT_STEP_LABELS = {
|
|
|
51
51
|
"worktree-chown": "setting the worktree owner",
|
|
52
52
|
"op-chown": "setting the op tree's owner",
|
|
53
53
|
"worktree-clean": "cleaning the worktree",
|
|
54
|
+
"thread-cred-file-scrub": "removing an old workspace credential",
|
|
55
|
+
"thread-cred-helper-scrub": "removing an old Git credential helper",
|
|
54
56
|
"clean-workspace": "cleaning the workspace",
|
|
55
57
|
"clean-before-restore": "cleaning before the restore",
|
|
56
58
|
"break-mirror": "removing the mirror's config (fault injection)",
|
|
@@ -64,7 +66,6 @@ export const RESIDENT_STEP_LABELS = {
|
|
|
64
66
|
"op-dir": "preparing a directory",
|
|
65
67
|
"ops-dir": "preparing a directory",
|
|
66
68
|
"stage-dir": "preparing a directory",
|
|
67
|
-
stat: "checking a file",
|
|
68
69
|
"boot-id": "reading the container's identity",
|
|
69
70
|
touch: "touching a marker",
|
|
70
71
|
nproc: "counting CPUs",
|
|
@@ -153,8 +153,8 @@ export function parseSeed(v: unknown): { ok: true; seed: SandboxSeed } | { ok: f
|
|
|
153
153
|
* 3. the deps view, when one was restored, replaces whatever `node_modules`
|
|
154
154
|
* the checkout carries (older snapshots still hold one);
|
|
155
155
|
* 4. the thread's ref is fetched from origin (the credential is the exec
|
|
156
|
-
*
|
|
157
|
-
*
|
|
156
|
+
* run bearer through the Git door and per-command env — never a token
|
|
157
|
+
* in this text) and checked out, at its resolved head when the
|
|
158
158
|
* fetch holds it, else at the fetched tip; without a thread ref the
|
|
159
159
|
* checkout stays on the snapshot's branch;
|
|
160
160
|
* 5. the head is printed last: the answer's `sha`. */
|