@coreplane/switchboard 1.259.0 → 1.260.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/config/config.example.yaml +17 -24
- package/dist/assets/deploy/cloudflare/worker.ts +17 -56
- package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +6 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +178 -27
- package/dist/assets/deploy/cloudflare-resident/worker.ts +74 -52
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +6 -3
- package/dist/assets/deploy/profile.example.json +2 -1
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +3 -5
- package/dist/assets/src/core/authz/policy.ts +5 -0
- package/dist/assets/src/core/budgets.ts +19 -6
- package/dist/assets/src/core/coordinator/contract.ts +93 -5
- package/dist/assets/src/core/coordinator/driver.ts +395 -128
- package/dist/assets/src/core/memory/types.ts +5 -0
- package/dist/assets/src/core/pipelineStanding.ts +13 -4
- package/dist/assets/src/core/plane/decide.ts +53 -12
- package/dist/assets/src/core/provider.ts +5 -0
- package/dist/assets/src/core/runEvents.ts +22 -15
- package/dist/assets/src/core/runLedger/types.ts +3 -3
- package/dist/assets/src/core/runRecord.ts +64 -11
- package/dist/assets/src/core/ship/coordinator.ts +202 -53
- package/dist/assets/src/core/ship/renewal.ts +23 -8
- package/dist/assets/src/core/types.ts +11 -0
- package/dist/assets/src/deploy/liveGate.ts +8 -0
- package/dist/assets/src/deploy/profile.ts +8 -0
- package/dist/assets/src/deploy/restart.ts +49 -35
- package/dist/assets/web/dist/.vite/manifest.json +67 -67
- package/dist/assets/web/dist/assets/{DeliveryPage-DadntjSp.js → DeliveryPage-62qCSLvk.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-Dk3HRBc3.js +1 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DBPMLVsm.js → PendingTurnRow-CeTGOXUy.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-DLusMGQ0.js → PlanePage-RD0M6O6W.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CPnHnrxy.js → ResidentDetailPage-Dl2KId95.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-C9hASwHL.js → ResidentsIndexPage-DBy43I1R.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CwHDL-Mo.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-o1xzkTGZ.js → RunRoutePage-Du1HqLZs.js} +4 -4
- package/dist/assets/web/dist/assets/RunsIndexPage-BZphy3Hr.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-BIdfC0V3.js → ScheduledPage-B7jh898t.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-CnqWAjl3.js → SettingsPage-3uLEsiiE.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-DGNg0sIZ.js → SilentTurn-CKxSxnQC.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-D_D_iJS8.js → StatusDot-x7vcK0iE.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DVFmM7on.js → Tooltip-wVXCWhMp.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-7fAXFoB_.js → UnitRoutePage-V5BXK7FH.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-KNNkT4PZ.js +1 -0
- package/dist/assets/web/dist/assets/{dist-C1VX-_mm.js → dist-C-XGnvM4.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-BO_GIdF6.js → indexRow-Dxg7C8s9.js} +1 -1
- package/dist/assets/web/dist/assets/{main-Cl7xHG34.js → main-DAL--hyj.js} +2 -2
- package/dist/assets/web/dist/assets/sseReplay-BqgOggyQ.js +11 -0
- package/dist/cli.js +9136 -8197
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-BEEpTnVT.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-OPe7Z0Cz.js +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-0YAClYQh.js +0 -1
- package/dist/assets/web/dist/assets/budgets-BRLm6oID.js +0 -1
- package/dist/assets/web/dist/assets/sseReplay-BjlKmQsY.js +0 -11
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
// bot answer becomes the machine's return, and what is left to the platform's
|
|
8
8
|
// retry: a call that throws inside a step is the platform's to ask again under
|
|
9
9
|
// the one policy (twelve times, two minutes apart, constant — long enough for a
|
|
10
|
-
// bot deploy and its rollover), and the driver never catches it.
|
|
10
|
+
// bot deploy and its rollover), and the driver never catches it mid-step. A
|
|
11
|
+
// throw that escapes a unit's pipeline anyway — a stored answer the mappers
|
|
12
|
+
// cannot read, retries exhausted, the platform's own refusal — is told once as
|
|
13
|
+
// the unit's ending (kind `failed`, cause `step_threw`, the step and round and
|
|
14
|
+
// the throw's one line, issue 2100) before it is rethrown to fail the
|
|
15
|
+
// instance, so the unit's row never reads as the bare "no ending was
|
|
16
|
+
// recorded" seal.
|
|
11
17
|
//
|
|
12
18
|
// Every step's stored output is the bot's reply as the wire carried it — the
|
|
13
19
|
// status and the text of a JSON object the bot stamped with its clock (`at`) —
|
|
@@ -33,7 +39,7 @@
|
|
|
33
39
|
// plan's re-issue. A task string's ship branch waits for a person. Node-free:
|
|
34
40
|
// the shim Worker imports this by relative path.
|
|
35
41
|
|
|
36
|
-
import { DEFAULT_GRANT, GRANT_RENEWALS_MAX, IDLE_DAYS_MAX, type Grant, type GrantSource } from "../budgets.js";
|
|
42
|
+
import { DAY_MS, DEFAULT_GRANT, GRANT_RENEWALS_MAX, IDLE_DAYS_MAX, type Grant, type GrantSource } from "../budgets.js";
|
|
37
43
|
import { DEFAULT_VERBOSITY, isVerbosity, type Verbosity } from "../verbosity.js";
|
|
38
44
|
import {
|
|
39
45
|
applyReturn,
|
|
@@ -68,8 +74,11 @@ import {
|
|
|
68
74
|
childInterruptedEventType,
|
|
69
75
|
childResumedEventType,
|
|
70
76
|
isCoordinatorUnit,
|
|
77
|
+
isUnitWakeAnswer,
|
|
71
78
|
runFinishedEventType,
|
|
79
|
+
unitNudgeEventType,
|
|
72
80
|
type CoordinatorUnit,
|
|
81
|
+
type UnitWakeAnswer,
|
|
73
82
|
} from "./contract.js";
|
|
74
83
|
|
|
75
84
|
const MIN = 60_000;
|
|
@@ -102,6 +111,7 @@ export type CoordinatorStepRoute =
|
|
|
102
111
|
| "pr-check"
|
|
103
112
|
| "round"
|
|
104
113
|
| "unit-end"
|
|
114
|
+
| "unit-wake"
|
|
105
115
|
| "checks"
|
|
106
116
|
| "merge"
|
|
107
117
|
| "finish";
|
|
@@ -279,6 +289,11 @@ function readUnitStart(a: BotAnswer): { at: number } {
|
|
|
279
289
|
return { at: a.body.at };
|
|
280
290
|
}
|
|
281
291
|
|
|
292
|
+
function readWakeAnswer(a: BotAnswer): { answer: UnitWakeAnswer; at: number } {
|
|
293
|
+
if (a.body.ok !== true || !isUnitWakeAnswer(a.body.answer)) throw new UnreadableAnswer("unit-wake", a, "wake answer");
|
|
294
|
+
return { answer: a.body.answer, at: a.body.at };
|
|
295
|
+
}
|
|
296
|
+
|
|
282
297
|
function branchReturn(step: string, a: BotAnswer): StepReturn {
|
|
283
298
|
const { ok, reason, at } = a.body;
|
|
284
299
|
if (ok === true) return { type: "branch", step, ok: true, at };
|
|
@@ -428,6 +443,9 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
428
443
|
...(Array.isArray(a.body.fixupCommits) && a.body.fixupCommits.every((s: unknown) => typeof s === "string")
|
|
429
444
|
? { fixupCommits: a.body.fixupCommits as string[] }
|
|
430
445
|
: {}),
|
|
446
|
+
// The base's merge-queue rule beside the checks (issue 2011): the
|
|
447
|
+
// merge:person report says the person's merge is queued.
|
|
448
|
+
...(typeof a.body.baseHasMergeQueue === "boolean" ? { baseHasMergeQueue: a.body.baseHasMergeQueue } : {}),
|
|
431
449
|
},
|
|
432
450
|
at,
|
|
433
451
|
};
|
|
@@ -451,7 +469,7 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
451
469
|
* ask's answer carries `retried` instead: whether the re-run was dispatched,
|
|
452
470
|
* so the machine never waits on a head an undispatched re-run left unchanged. */
|
|
453
471
|
function checksReturn(step: string, a: BotAnswer): StepReturn {
|
|
454
|
-
const { ok, checks, draft, retried, at } = a.body;
|
|
472
|
+
const { ok, checks, draft, retried, refired, at } = a.body;
|
|
455
473
|
if (ok !== true) throw new UnreadableAnswer("checks", a, "ok");
|
|
456
474
|
return {
|
|
457
475
|
type: "checks",
|
|
@@ -461,6 +479,7 @@ function checksReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
461
479
|
// unit for the ready event instead of merging or ending without a cause.
|
|
462
480
|
...(draft === true ? { draft: true } : {}),
|
|
463
481
|
...(typeof retried === "boolean" ? { retried } : {}),
|
|
482
|
+
...(typeof refired === "boolean" ? { refired } : {}),
|
|
464
483
|
at,
|
|
465
484
|
};
|
|
466
485
|
}
|
|
@@ -471,7 +490,10 @@ const isRoundChecks = (v: unknown): v is RoundChecks =>
|
|
|
471
490
|
Array.isArray(v.pending) &&
|
|
472
491
|
v.pending.every((n: unknown) => typeof n === "string") &&
|
|
473
492
|
Array.isArray(v.failed) &&
|
|
474
|
-
v.failed.every((f: unknown) => isRecord(f) && typeof f.name === "string" && typeof f.conclusion === "string")
|
|
493
|
+
v.failed.every((f: unknown) => isRecord(f) && typeof f.name === "string" && typeof f.conclusion === "string") &&
|
|
494
|
+
(v.required === undefined ||
|
|
495
|
+
(Array.isArray(v.required) && v.required.every((n: unknown) => typeof n === "string"))) &&
|
|
496
|
+
(v.expected === undefined || (Array.isArray(v.expected) && v.expected.every((n: unknown) => typeof n === "string")));
|
|
475
497
|
|
|
476
498
|
function mergeReturn(step: string, a: BotAnswer): StepReturn {
|
|
477
499
|
const { ok, outcome, by, sha, mergedAt, reason, at } = a.body;
|
|
@@ -652,8 +674,8 @@ async function perform(
|
|
|
652
674
|
}
|
|
653
675
|
case "checks":
|
|
654
676
|
// The round's checks step (record 0055): the bot reads the check runs at
|
|
655
|
-
// the reviewed head with the merge door's own reading — or
|
|
656
|
-
//
|
|
677
|
+
// the reviewed head with the merge door's own reading — or performs one
|
|
678
|
+
// of the step's bounded recovery effects first.
|
|
657
679
|
return checksReturn(
|
|
658
680
|
action.step,
|
|
659
681
|
answerOf(
|
|
@@ -664,6 +686,7 @@ async function perform(
|
|
|
664
686
|
prNumber: action.prNumber,
|
|
665
687
|
headSha: action.headSha,
|
|
666
688
|
...(action.retry !== undefined ? { retry: action.retry } : {}),
|
|
689
|
+
...(action.refire === true ? { refire: true } : {}),
|
|
667
690
|
}),
|
|
668
691
|
),
|
|
669
692
|
),
|
|
@@ -686,8 +709,70 @@ async function perform(
|
|
|
686
709
|
}
|
|
687
710
|
}
|
|
688
711
|
|
|
712
|
+
/** The throw's one line, for the step-threw ending's report (issue 2100). */
|
|
713
|
+
function oneLineOf(err: unknown): string {
|
|
714
|
+
const line = (err instanceof Error ? err.message : String(err)).split("\n")[0]!.trim();
|
|
715
|
+
if (line.length === 0) return "an error with no message";
|
|
716
|
+
return line.length > REASON_MAX ? `${line.slice(0, REASON_MAX)}…` : line;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Where the machine was when a step threw: the step's own name and, inside a
|
|
720
|
+
* round, the round it belonged to. */
|
|
721
|
+
interface StepAt {
|
|
722
|
+
step: string;
|
|
723
|
+
round?: { index: number; kind: string };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** A step that throws inside the walk becomes the unit's ending (issue 2100):
|
|
727
|
+
* kind `failed`, cause `step_threw`, the step and round it was in and the
|
|
728
|
+
* throw's one line, posted in the user's words — best effort, before the
|
|
729
|
+
* throw is rethrown to fail the instance — so the unit's row carries a cause
|
|
730
|
+
* a person can read instead of the bare "no ending was recorded" seal (the
|
|
731
|
+
* walk-dies-before-unit-end path behind issues 2063 and 2100: run 21c50656
|
|
732
|
+
* died between round 2's review verdict and its outcome write). */
|
|
733
|
+
async function tellStepThrew(
|
|
734
|
+
step: StepRunner,
|
|
735
|
+
bot: CoordinatorBot,
|
|
736
|
+
prefix: string,
|
|
737
|
+
tag: { parentInstanceId: string; unit: string },
|
|
738
|
+
pr: { number: number; url: string } | undefined,
|
|
739
|
+
at: StepAt,
|
|
740
|
+
err: unknown,
|
|
741
|
+
): Promise<void> {
|
|
742
|
+
const line = oneLineOf(err);
|
|
743
|
+
const where =
|
|
744
|
+
at.round === undefined
|
|
745
|
+
? `at \`${at.step}\``
|
|
746
|
+
: at.round.kind === "review" && /\/review\/(read|checks)\b/.test(at.step)
|
|
747
|
+
? `after round ${at.round.index}'s review verdict (\`${at.step}\`)`
|
|
748
|
+
: `in round ${at.round.index} (\`${at.step}\`)`;
|
|
749
|
+
const report =
|
|
750
|
+
`⚠️ The runner failed ${where}: ${line}\n\n` +
|
|
751
|
+
"Re-issue `agent:ship` in this thread to continue — a pull request already approved with green checks resumes at the checks step, never at a fresh coding round.";
|
|
752
|
+
const body = {
|
|
753
|
+
...tag,
|
|
754
|
+
ending: {
|
|
755
|
+
kind: "failed",
|
|
756
|
+
cause: "step_threw",
|
|
757
|
+
step: at.step,
|
|
758
|
+
...(at.round !== undefined ? { round: at.round.index } : {}),
|
|
759
|
+
report,
|
|
760
|
+
threadReport: report,
|
|
761
|
+
},
|
|
762
|
+
...(pr !== undefined ? { pr } : {}),
|
|
763
|
+
};
|
|
764
|
+
try {
|
|
765
|
+
await step.do(`${prefix}/end/threw`, STEP_CONFIG, () => call(bot, "unit-end", body));
|
|
766
|
+
} catch {
|
|
767
|
+
// Best effort: the rethrow still fails the instance, and a bot that could
|
|
768
|
+
// not record the ending leaves the seal's line as before.
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
689
772
|
/** One unit's pipeline: its start, then the machine's steps until it ends;
|
|
690
773
|
* every round boundary and the ending told to the bot as they happen. */
|
|
774
|
+
type DrivenEnding = UnitEnding & { endedAt?: number };
|
|
775
|
+
|
|
691
776
|
async function runUnit(
|
|
692
777
|
step: StepRunner,
|
|
693
778
|
bot: CoordinatorBot,
|
|
@@ -695,16 +780,28 @@ async function runUnit(
|
|
|
695
780
|
node: PlanUnitNode,
|
|
696
781
|
plan: PlanFacts,
|
|
697
782
|
session?: LeaseSegmentProgress,
|
|
698
|
-
): Promise<
|
|
783
|
+
): Promise<DrivenEnding> {
|
|
699
784
|
const unit = node.id;
|
|
700
785
|
// A renewal's segment names its steps under the segment (`U10/s2/…`), so
|
|
701
786
|
// the Workflow's durable step cache never answers segment two with segment
|
|
702
787
|
// one's results (decision 0046).
|
|
703
788
|
const prefix = stepPrefixOf(unit, session);
|
|
704
789
|
const tag = { parentInstanceId: instanceId, unit };
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
)
|
|
790
|
+
// Where the machine is, tracked for the step-threw ending (issue 2100). The
|
|
791
|
+
// start belongs in the same net as the rest of the unit even though no
|
|
792
|
+
// pipeline state (and therefore no round) exists yet.
|
|
793
|
+
let last: StepAt = { step: `${prefix}/start` };
|
|
794
|
+
let start: ReturnType<typeof readUnitStart>;
|
|
795
|
+
try {
|
|
796
|
+
const startStep = `${prefix}/start`;
|
|
797
|
+
last = { step: startStep };
|
|
798
|
+
start = readUnitStart(
|
|
799
|
+
answerOf("unit-start", await step.do(startStep, STEP_CONFIG, () => call(bot, "unit-start", tag))),
|
|
800
|
+
);
|
|
801
|
+
} catch (err) {
|
|
802
|
+
await tellStepThrew(step, bot, prefix, tag, undefined, last, err);
|
|
803
|
+
throw err;
|
|
804
|
+
}
|
|
708
805
|
// A resume at review (agent-ship item 10) rides the unit's row: the pull
|
|
709
806
|
// request of ship's own the requester named opens the pipeline at its first
|
|
710
807
|
// review round, with no pre-check, no branch and no round 0.
|
|
@@ -739,108 +836,139 @@ async function runUnit(
|
|
|
739
836
|
start.at,
|
|
740
837
|
);
|
|
741
838
|
let notes = 0;
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
839
|
+
let endedAt: number | undefined;
|
|
840
|
+
try {
|
|
841
|
+
for (;;) {
|
|
842
|
+
const action = nextAction(state);
|
|
843
|
+
if (action.type === "end") return { ...action.ending, ...(endedAt !== undefined ? { endedAt } : {}) };
|
|
844
|
+
// Keep the current round across its non-round phases (for example merge)
|
|
845
|
+
// while replacing it whenever the pipeline names a new one.
|
|
846
|
+
last = {
|
|
847
|
+
step: action.step,
|
|
848
|
+
...("round" in state.phase
|
|
849
|
+
? { round: state.phase.round }
|
|
850
|
+
: last.round !== undefined
|
|
851
|
+
? { round: last.round }
|
|
852
|
+
: {}),
|
|
853
|
+
};
|
|
854
|
+
const transition = applyReturn(state, await perform(step, bot, instanceId, unit, action));
|
|
855
|
+
state = transition.state;
|
|
856
|
+
for (const note of transition.notes) {
|
|
857
|
+
if (note.type === "round") {
|
|
858
|
+
const body = {
|
|
859
|
+
...tag,
|
|
860
|
+
index: note.index,
|
|
861
|
+
agent: note.agent,
|
|
862
|
+
outcome: note.outcome,
|
|
863
|
+
...(note.gate !== undefined ? { gate: note.gate } : {}),
|
|
864
|
+
};
|
|
865
|
+
const noteStep = `${prefix}/note/${++notes}`;
|
|
866
|
+
last = { step: noteStep, round: { index: note.index, kind: note.agent } };
|
|
867
|
+
await step.do(noteStep, STEP_CONFIG, () => call(bot, "round", body));
|
|
868
|
+
} else {
|
|
869
|
+
// A merge_ready ending names the pull request as it is at the APPROVED
|
|
870
|
+
// head (agent-ship item 9): one more pr-check reads the facts fresh —
|
|
871
|
+
// auto-merge may have been switched on since the round's check, or it
|
|
872
|
+
// may already have fired, in which case the answer is `merged` and the
|
|
873
|
+
// report says so rather than naming a gate that has passed. An
|
|
874
|
+
// unreadable answer just leaves the facts out.
|
|
875
|
+
let endFacts: MergeReadyFacts | undefined;
|
|
876
|
+
if (note.ending.kind === "merge_ready") {
|
|
877
|
+
try {
|
|
878
|
+
const factsStep = `${prefix}/end/pr-facts`;
|
|
879
|
+
last = { step: factsStep, ...(last.round !== undefined ? { round: last.round } : {}) };
|
|
880
|
+
const check = prCheckReturn(
|
|
881
|
+
factsStep,
|
|
882
|
+
answerOf(
|
|
883
|
+
"pr-check",
|
|
884
|
+
await step.do(factsStep, STEP_CONFIG, () => call(bot, "pr-check", { ...tag, checks: true })),
|
|
773
885
|
),
|
|
774
|
-
)
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
886
|
+
);
|
|
887
|
+
if (check.type === "pr-check" && check.pr.state === "merged")
|
|
888
|
+
endFacts = { merged: { sha: check.pr.sha, mergedAt: check.pr.mergedAt } };
|
|
889
|
+
else if (check.type === "pr-check" && check.pr.state === "open")
|
|
890
|
+
endFacts = {
|
|
891
|
+
...(check.pr.autoMergeEnabled !== undefined ? { autoMergeEnabled: check.pr.autoMergeEnabled } : {}),
|
|
892
|
+
// The checks at the approved head (record 0055): the report's
|
|
893
|
+
// headline is a claim about them, never "merge-ready" over a red one.
|
|
894
|
+
...(check.pr.checks !== undefined ? { checks: check.pr.checks } : {}),
|
|
895
|
+
// The ready state beside them (agent-ship item 9): a conflicting
|
|
896
|
+
// head, or one carrying an unsquashed fix-up commit, is reported
|
|
897
|
+
// approved-but-not-merge-ready, never "merge-ready".
|
|
898
|
+
...(check.pr.mergeableState !== undefined ? { mergeableState: check.pr.mergeableState } : {}),
|
|
899
|
+
...(check.pr.fixupCommits !== undefined ? { fixupCommits: check.pr.fixupCommits } : {}),
|
|
900
|
+
// The base's merge-queue rule (issue 2011): the merge:person
|
|
901
|
+
// path names "queued" in its remaining-gate line.
|
|
902
|
+
...(check.pr.baseHasMergeQueue !== undefined
|
|
903
|
+
? { baseHasMergeQueue: check.pr.baseHasMergeQueue }
|
|
904
|
+
: {}),
|
|
905
|
+
};
|
|
906
|
+
} catch {
|
|
907
|
+
// the report simply omits the fact
|
|
908
|
+
}
|
|
792
909
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
910
|
+
// The last coding child's run is named so the bot can put its handoff
|
|
911
|
+
// — the deviations it recorded — on the unit's board issue beside the
|
|
912
|
+
// ending (agent-ship item 14).
|
|
913
|
+
const body = {
|
|
914
|
+
...tag,
|
|
915
|
+
// Two copies (routing-and-config item 28): the full report for the
|
|
916
|
+
// row and the board, and the thread's at the request's verbosity.
|
|
917
|
+
ending: {
|
|
918
|
+
kind: note.ending.kind,
|
|
919
|
+
report: renderUnitReport(state, endFacts),
|
|
920
|
+
threadReport: renderUnitReport(state, endFacts, state.input.verbosity ?? DEFAULT_VERBOSITY),
|
|
921
|
+
// An idle ending carries its continuation facts (record 0051): the
|
|
922
|
+
// bot writes them on the row's `idle` in place of an ending, with
|
|
923
|
+
// the coding run id it already receives below.
|
|
924
|
+
...(note.ending.kind === "idle"
|
|
925
|
+
? {
|
|
926
|
+
why: note.ending.why,
|
|
927
|
+
renewalsLeft: note.ending.renewalsLeft,
|
|
928
|
+
...(note.ending.from !== undefined ? { from: note.ending.from } : {}),
|
|
929
|
+
spendUsd: note.ending.spendUsd,
|
|
930
|
+
...(note.ending.handoff !== undefined ? { handoff: note.ending.handoff } : {}),
|
|
931
|
+
}
|
|
932
|
+
: {}),
|
|
933
|
+
},
|
|
934
|
+
...(state.pr !== undefined ? { pr: state.pr } : {}),
|
|
935
|
+
// A review_pending ending names the child's own last push so the next
|
|
936
|
+
// attempt's pre-check can start at the review round (the row's lastPush)
|
|
937
|
+
// — an idled one the same, off the idle's `from` (record 0051).
|
|
938
|
+
...(note.ending.kind === "review_pending" && note.ending.headSha !== undefined
|
|
939
|
+
? { headSha: note.ending.headSha }
|
|
940
|
+
: note.ending.kind === "idle" && note.ending.why === "review_pending" && note.ending.from !== undefined
|
|
941
|
+
? { headSha: note.ending.from }
|
|
942
|
+
: {}),
|
|
943
|
+
...(state.lastCodingRunId !== undefined ? { codingRunId: state.lastCodingRunId } : {}),
|
|
944
|
+
// A continued ending is a segment's end, not the unit's: the bot
|
|
945
|
+
// writes the renewal as a row keyed by the next segment's index
|
|
946
|
+
// (decision 0046), so a runner reclaimed here never renews twice.
|
|
947
|
+
...(note.ending.kind === "continued"
|
|
809
948
|
? {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
949
|
+
segment: {
|
|
950
|
+
index: note.ending.segment,
|
|
951
|
+
...(note.ending.from !== undefined ? { from: note.ending.from } : {}),
|
|
952
|
+
runId: note.ending.runId,
|
|
953
|
+
},
|
|
815
954
|
}
|
|
816
955
|
: {}),
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
: {}),
|
|
827
|
-
...(state.lastCodingRunId !== undefined ? { codingRunId: state.lastCodingRunId } : {}),
|
|
828
|
-
// A continued ending is a segment's end, not the unit's: the bot
|
|
829
|
-
// writes the renewal as a row keyed by the next segment's index
|
|
830
|
-
// (decision 0046), so a runner reclaimed here never renews twice.
|
|
831
|
-
...(note.ending.kind === "continued"
|
|
832
|
-
? {
|
|
833
|
-
segment: {
|
|
834
|
-
index: note.ending.segment,
|
|
835
|
-
...(note.ending.from !== undefined ? { from: note.ending.from } : {}),
|
|
836
|
-
runId: note.ending.runId,
|
|
837
|
-
},
|
|
838
|
-
}
|
|
839
|
-
: {}),
|
|
840
|
-
};
|
|
841
|
-
await step.do(`${prefix}/end`, STEP_CONFIG, () => call(bot, "unit-end", body));
|
|
956
|
+
};
|
|
957
|
+
const endStep = `${prefix}/end`;
|
|
958
|
+
last = { step: endStep, ...(last.round !== undefined ? { round: last.round } : {}) };
|
|
959
|
+
const endAnswer = answerOf(
|
|
960
|
+
"unit-end",
|
|
961
|
+
await step.do(endStep, STEP_CONFIG, () => call(bot, "unit-end", body)),
|
|
962
|
+
);
|
|
963
|
+
endedAt = endAnswer.body.at;
|
|
964
|
+
}
|
|
842
965
|
}
|
|
843
966
|
}
|
|
967
|
+
} catch (err) {
|
|
968
|
+
// The walk-dies-before-unit-end path (issue 2100): the throw becomes the
|
|
969
|
+
// unit's ending before it fails the instance, never the bare seal.
|
|
970
|
+
await tellStepThrew(step, bot, prefix, tag, state.pr, last, err);
|
|
971
|
+
throw err;
|
|
844
972
|
}
|
|
845
973
|
}
|
|
846
974
|
|
|
@@ -856,6 +984,24 @@ function blockedReport(unit: string, dep: string, depEnding: string): string {
|
|
|
856
984
|
return `⛔ Blocked: ${unit} waits on ${dep}, which ended ${depEnding}${person ? " — a person's merge" : ""}. Re-issue the plan naming the remaining units once it is ${person ? "merged" : "resolved"}.`;
|
|
857
985
|
}
|
|
858
986
|
|
|
987
|
+
/** End a unit that never entered `runUnit` under the same failure net. */
|
|
988
|
+
async function endUnrunUnit(
|
|
989
|
+
step: StepRunner,
|
|
990
|
+
bot: CoordinatorBot,
|
|
991
|
+
instanceId: string,
|
|
992
|
+
unit: string,
|
|
993
|
+
ending: { kind: "stopped" | "blocked"; report: string },
|
|
994
|
+
): Promise<void> {
|
|
995
|
+
const tag = { parentInstanceId: instanceId, unit };
|
|
996
|
+
const endStep = `${unit}/end`;
|
|
997
|
+
try {
|
|
998
|
+
await step.do(endStep, STEP_CONFIG, () => call(bot, "unit-end", { ...tag, ending }));
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
await tellStepThrew(step, bot, unit, tag, undefined, { step: endStep }, err);
|
|
1001
|
+
throw err;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
859
1005
|
/** The graph as one plan answer carries it: the instance's unit rows, in the plan's order. */
|
|
860
1006
|
function graphOf(plan: PlanFacts, instanceId: string): PlanGraph {
|
|
861
1007
|
return {
|
|
@@ -900,6 +1046,106 @@ function rereadCursor(cursor: PlanCursor, fresh: PlanGraph): { cursor: PlanCurso
|
|
|
900
1046
|
return { cursor: { order, status }, gone };
|
|
901
1047
|
}
|
|
902
1048
|
|
|
1049
|
+
type IdleResult =
|
|
1050
|
+
| { kind: "segment"; session: LeaseSegmentProgress }
|
|
1051
|
+
| { kind: "ending"; ending: Extract<UnitEnding, { kind: "idle_expired" }> | { kind: "stopped" } };
|
|
1052
|
+
|
|
1053
|
+
/** Park one unit behind its indexed wait. A timeout closes the idle; a wake
|
|
1054
|
+
* answer is durable at the bot, so this loop may safely move to the next index
|
|
1055
|
+
* after a transport failure without deciding the same event twice. */
|
|
1056
|
+
async function waitOnIdle(
|
|
1057
|
+
step: StepRunner,
|
|
1058
|
+
bot: CoordinatorBot,
|
|
1059
|
+
instanceId: string,
|
|
1060
|
+
unit: string,
|
|
1061
|
+
prefix: string,
|
|
1062
|
+
idle: Extract<DrivenEnding, { kind: "idle" }>,
|
|
1063
|
+
idleDays: number,
|
|
1064
|
+
currentSession: LeaseSegmentProgress | undefined,
|
|
1065
|
+
): Promise<IdleResult> {
|
|
1066
|
+
const deadline = (idle.endedAt ?? 0) + idleDays * DAY_MS;
|
|
1067
|
+
let now = idle.endedAt ?? 0;
|
|
1068
|
+
for (let index = 1; ; index += 1) {
|
|
1069
|
+
const waitId = `${prefix}/idle/${index}`;
|
|
1070
|
+
const timeout = Math.max(1, deadline - now);
|
|
1071
|
+
try {
|
|
1072
|
+
await step.waitForEvent(waitId, { type: unitNudgeEventType({ instanceId, unit }), timeout });
|
|
1073
|
+
} catch {
|
|
1074
|
+
const ending = { kind: "idle_expired" as const, reviewRounds: idle.reviewRounds };
|
|
1075
|
+
const endStep = `${waitId}/end`;
|
|
1076
|
+
await step.do(endStep, STEP_CONFIG, () =>
|
|
1077
|
+
call(bot, "unit-end", {
|
|
1078
|
+
parentInstanceId: instanceId,
|
|
1079
|
+
unit,
|
|
1080
|
+
ending: {
|
|
1081
|
+
kind: ending.kind,
|
|
1082
|
+
report: "⌛ Idle expired: no reply continued this unit before its idle window closed.",
|
|
1083
|
+
},
|
|
1084
|
+
}),
|
|
1085
|
+
);
|
|
1086
|
+
return { kind: "ending", ending };
|
|
1087
|
+
}
|
|
1088
|
+
let wake: { answer: UnitWakeAnswer; at: number };
|
|
1089
|
+
try {
|
|
1090
|
+
wake = readWakeAnswer(
|
|
1091
|
+
answerOf(
|
|
1092
|
+
"unit-wake",
|
|
1093
|
+
await step.do(`${waitId}/wake`, STEP_CONFIG, () =>
|
|
1094
|
+
call(bot, "unit-wake", { parentInstanceId: instanceId, unit, waitId }),
|
|
1095
|
+
),
|
|
1096
|
+
),
|
|
1097
|
+
);
|
|
1098
|
+
} catch {
|
|
1099
|
+
// The event remains unconsumed when the wake could not store an answer.
|
|
1100
|
+
// A stored answer is replayed by the next identity, so either way the
|
|
1101
|
+
// next indexed wait is the safe place to listen again.
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
now = wake.at;
|
|
1105
|
+
const answer = wake.answer;
|
|
1106
|
+
if (answer.kind === "answered") continue;
|
|
1107
|
+
if (answer.kind === "expired") {
|
|
1108
|
+
const ending = { kind: "idle_expired" as const, reviewRounds: idle.reviewRounds };
|
|
1109
|
+
await step.do(`${waitId}/end`, STEP_CONFIG, () =>
|
|
1110
|
+
call(bot, "unit-end", {
|
|
1111
|
+
parentInstanceId: instanceId,
|
|
1112
|
+
unit,
|
|
1113
|
+
ending: {
|
|
1114
|
+
kind: ending.kind,
|
|
1115
|
+
report: "⌛ Idle expired: the unit reached its indexed wake limit.",
|
|
1116
|
+
},
|
|
1117
|
+
}),
|
|
1118
|
+
);
|
|
1119
|
+
return { kind: "ending", ending };
|
|
1120
|
+
}
|
|
1121
|
+
if (answer.kind === "stopped") {
|
|
1122
|
+
await step.do(`${waitId}/end`, STEP_CONFIG, () =>
|
|
1123
|
+
call(bot, "unit-end", {
|
|
1124
|
+
parentInstanceId: instanceId,
|
|
1125
|
+
unit,
|
|
1126
|
+
ending: { kind: "stopped", report: "⏹ Stopped: the idle unit was ended by an operator." },
|
|
1127
|
+
}),
|
|
1128
|
+
);
|
|
1129
|
+
return { kind: "ending", ending: { kind: "stopped" } };
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
kind: "segment",
|
|
1133
|
+
session: {
|
|
1134
|
+
segment: answer.index,
|
|
1135
|
+
renewalsSpent: answer.leaseMs !== undefined ? (currentSession?.renewalsSpent ?? 0) : answer.index - 1,
|
|
1136
|
+
spendUsd: answer.spendUsd,
|
|
1137
|
+
...(answer.from !== undefined ? { continueFrom: answer.from } : {}),
|
|
1138
|
+
...(answer.runId !== undefined ? { previousRunId: answer.runId } : {}),
|
|
1139
|
+
...(answer.handoff !== undefined ? { previousHandoff: answer.handoff } : {}),
|
|
1140
|
+
...(answer.texts.length > 0 ? { texts: answer.texts } : {}),
|
|
1141
|
+
...(answer.leaseMs !== undefined
|
|
1142
|
+
? { resume: { leaseMs: answer.leaseMs, attempt: (currentSession?.resume?.attempt ?? 0) + 1 } }
|
|
1143
|
+
: {}),
|
|
1144
|
+
},
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
903
1149
|
/** The plan: its units one at a time in dependency order, then the endings of the units it never reached. */
|
|
904
1150
|
async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string): Promise<PlanRunSummary> {
|
|
905
1151
|
// The selection is read at every unit boundary (the orchestration-plane plan): the first read opens
|
|
@@ -926,8 +1172,7 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
926
1172
|
if (plan.stopped) {
|
|
927
1173
|
for (const id of cursor.order.filter((u) => endings[u] === undefined)) {
|
|
928
1174
|
endings[id] = "stopped";
|
|
929
|
-
|
|
930
|
-
await step.do(`${id}/end`, STEP_CONFIG, () => call(bot, "unit-end", body));
|
|
1175
|
+
await endUnrunUnit(step, bot, instanceId, id, { kind: "stopped", report: stoppedReport(id) });
|
|
931
1176
|
}
|
|
932
1177
|
break;
|
|
933
1178
|
}
|
|
@@ -935,21 +1180,45 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
935
1180
|
if (next === undefined) break;
|
|
936
1181
|
cursor = startUnit(graph, cursor, next);
|
|
937
1182
|
const node = graph.units.find((u) => u.id === next)!;
|
|
938
|
-
let
|
|
939
|
-
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1183
|
+
let session: LeaseSegmentProgress | undefined;
|
|
1184
|
+
let ending: DrivenEnding | { kind: "stopped" } = await runUnit(step, bot, instanceId, node, plan);
|
|
1185
|
+
// A machine-renewed continuation opens at once. An idle continuation parks
|
|
1186
|
+
// the whole walk and opens only when the durable wake answer names the
|
|
1187
|
+
// segment (or settles as stopped/expired).
|
|
1188
|
+
for (;;) {
|
|
1189
|
+
if (ending.kind === "continued") {
|
|
1190
|
+
const c = ending;
|
|
1191
|
+
session = {
|
|
1192
|
+
segment: c.segment,
|
|
1193
|
+
renewalsSpent: c.segment - 1,
|
|
1194
|
+
spendUsd: c.spendUsd,
|
|
1195
|
+
...(c.from !== undefined ? { continueFrom: c.from } : {}),
|
|
1196
|
+
previousRunId: c.runId,
|
|
1197
|
+
...(c.handoff !== undefined ? { previousHandoff: c.handoff } : {}),
|
|
1198
|
+
};
|
|
1199
|
+
ending = await runUnit(step, bot, instanceId, node, plan, session);
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (ending.kind === "idle") {
|
|
1203
|
+
const parked = await waitOnIdle(
|
|
1204
|
+
step,
|
|
1205
|
+
bot,
|
|
1206
|
+
instanceId,
|
|
1207
|
+
next,
|
|
1208
|
+
stepPrefixOf(next, session),
|
|
1209
|
+
ending,
|
|
1210
|
+
plan.idleDays,
|
|
1211
|
+
session,
|
|
1212
|
+
);
|
|
1213
|
+
if (parked.kind === "ending") {
|
|
1214
|
+
ending = parked.ending;
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
session = parked.session;
|
|
1218
|
+
ending = await runUnit(step, bot, instanceId, node, plan, session);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
break;
|
|
953
1222
|
}
|
|
954
1223
|
endings[next] = ending.kind;
|
|
955
1224
|
// A unit is done for its dependents when the base carries its scope: the
|
|
@@ -975,12 +1244,10 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
975
1244
|
for (const id of blocked) {
|
|
976
1245
|
const node = graph.units.find((u) => u.id === id)!;
|
|
977
1246
|
const dep = node.dependsOn.find((d) => cursor.status[d] === "failed" || cursor.status[d] === "blocked")!;
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
};
|
|
983
|
-
await step.do(`${id}/end`, STEP_CONFIG, () => call(bot, "unit-end", body));
|
|
1247
|
+
await endUnrunUnit(step, bot, instanceId, id, {
|
|
1248
|
+
kind: "blocked",
|
|
1249
|
+
report: blockedReport(id, dep, endings[dep]!),
|
|
1250
|
+
});
|
|
984
1251
|
}
|
|
985
1252
|
if (!plan.stopped && !cursorFinished(cursor))
|
|
986
1253
|
throw new Error(`the plan's cursor did not finish: ${JSON.stringify(cursor.status)}`);
|