@coreplane/switchboard 1.261.2 → 1.261.3
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.
|
@@ -2732,6 +2732,38 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2732
2732
|
return { ok: true };
|
|
2733
2733
|
}
|
|
2734
2734
|
|
|
2735
|
+
/** One compare-and-replace transaction reserves a legacy continuation for
|
|
2736
|
+
* exactly one caller. The whole expected JSON is the fence: any intervening
|
|
2737
|
+
* unit write, including another recovery, makes this caller stale. */
|
|
2738
|
+
async claimLegacyContinuation(
|
|
2739
|
+
expected: CoordinatorUnit,
|
|
2740
|
+
recovered: CoordinatorUnit,
|
|
2741
|
+
now: number,
|
|
2742
|
+
): Promise<{ ok: true } | { ok: false; reason: "stale" }> {
|
|
2743
|
+
let out: { ok: true } | { ok: false; reason: "stale" } = { ok: true };
|
|
2744
|
+
this.ctx.storage.transactionSync(() => {
|
|
2745
|
+
const row = this.sql
|
|
2746
|
+
.exec<{ json: string }>(
|
|
2747
|
+
`SELECT json FROM coordinator_units WHERE instance_id = ? AND unit = ?`,
|
|
2748
|
+
expected.instanceId,
|
|
2749
|
+
expected.unit,
|
|
2750
|
+
)
|
|
2751
|
+
.toArray()[0];
|
|
2752
|
+
if (row?.json !== JSON.stringify(expected)) {
|
|
2753
|
+
out = { ok: false, reason: "stale" };
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
this.sql.exec(
|
|
2757
|
+
`UPDATE coordinator_units SET json = ?, updated_at = ? WHERE instance_id = ? AND unit = ?`,
|
|
2758
|
+
JSON.stringify(recovered),
|
|
2759
|
+
now,
|
|
2760
|
+
expected.instanceId,
|
|
2761
|
+
expected.unit,
|
|
2762
|
+
);
|
|
2763
|
+
});
|
|
2764
|
+
return out;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2735
2767
|
async listUnits(instanceId: string): Promise<CoordinatorUnit[]> {
|
|
2736
2768
|
return this.sql
|
|
2737
2769
|
.exec<{ json: string }>(`SELECT json FROM coordinator_units WHERE instance_id = ? ORDER BY rowid`, instanceId)
|
|
@@ -5187,6 +5219,7 @@ const LEDGER_ROUTES = new Set([
|
|
|
5187
5219
|
"/runs/coordinator/get",
|
|
5188
5220
|
"/runs/coordinator/stop",
|
|
5189
5221
|
"/runs/coordinator/units/put",
|
|
5222
|
+
"/runs/coordinator/units/claim-legacy-continuation",
|
|
5190
5223
|
"/runs/coordinator/units/list",
|
|
5191
5224
|
"/runs/coordinator/events/append",
|
|
5192
5225
|
"/runs/coordinator/events/list",
|
|
@@ -5886,6 +5919,17 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
|
|
|
5886
5919
|
console.log(`[runs/coordinator/units/put] ${key.value} ${units[0]!.instanceId} ${units.length} row(s)`);
|
|
5887
5920
|
return json(r);
|
|
5888
5921
|
}
|
|
5922
|
+
if (pathname === "/runs/coordinator/units/claim-legacy-continuation") {
|
|
5923
|
+
if (!isCoordinatorUnit(b.expected) || !isCoordinatorUnit(b.recovered))
|
|
5924
|
+
return json({ error: "expected and recovered must be coordinator unit rows" }, 400);
|
|
5925
|
+
if (b.expected.instanceId !== b.recovered.instanceId || b.expected.unit !== b.recovered.unit)
|
|
5926
|
+
return json({ error: "expected and recovered must name the same unit" }, 400);
|
|
5927
|
+
const r = await stub.claimLegacyContinuation(b.expected, b.recovered, now);
|
|
5928
|
+
console.log(
|
|
5929
|
+
`[runs/coordinator/units/claim-legacy-continuation] ${key.value} ${b.expected.instanceId}:${b.expected.unit} → ${r.ok ? "claimed" : r.reason}`,
|
|
5930
|
+
);
|
|
5931
|
+
return r.ok ? json(r) : json(r, 409);
|
|
5932
|
+
}
|
|
5889
5933
|
if (pathname === "/runs/coordinator/units/list") {
|
|
5890
5934
|
if (typeof b.instanceId !== "string" || !INSTANCE_ID_PATTERN.test(b.instanceId))
|
|
5891
5935
|
return json({ error: "instanceId must be a Workflow instance id" }, 400);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.261.
|
|
3
|
+
"version": "1.261.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.261.
|
|
9
|
+
"version": "1.261.3",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -21060,7 +21060,7 @@
|
|
|
21060
21060
|
},
|
|
21061
21061
|
"packages/switchboard": {
|
|
21062
21062
|
"name": "@coreplane/switchboard",
|
|
21063
|
-
"version": "1.261.
|
|
21063
|
+
"version": "1.261.3",
|
|
21064
21064
|
"license": "Apache-2.0",
|
|
21065
21065
|
"dependencies": {
|
|
21066
21066
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.261.
|
|
3
|
+
"version": "1.261.3",
|
|
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",
|
package/dist/assets/source.json
CHANGED
|
@@ -1137,14 +1137,16 @@ async function runUnit(
|
|
|
1137
1137
|
: {}),
|
|
1138
1138
|
},
|
|
1139
1139
|
...(state.pr !== undefined ? { pr: state.pr } : {}),
|
|
1140
|
-
// A
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
...(ending.kind === "
|
|
1144
|
-
? { headSha:
|
|
1145
|
-
: ending.kind === "
|
|
1146
|
-
? { headSha: ending.
|
|
1147
|
-
:
|
|
1140
|
+
// A merge_ready ending retains the exact head its final approval
|
|
1141
|
+
// reviewed. Review-pending endings retain the coding child's push;
|
|
1142
|
+
// both become the row's durable lastPush for a later attempt.
|
|
1143
|
+
...(ending.kind === "merge_ready" && state.lastReviewHead !== undefined
|
|
1144
|
+
? { headSha: state.lastReviewHead }
|
|
1145
|
+
: ending.kind === "review_pending" && ending.headSha !== undefined
|
|
1146
|
+
? { headSha: ending.headSha }
|
|
1147
|
+
: ending.kind === "idle" && ending.why === "review_pending" && ending.from !== undefined
|
|
1148
|
+
? { headSha: ending.from }
|
|
1149
|
+
: {}),
|
|
1148
1150
|
...(state.lastCodingRunId !== undefined ? { codingRunId: state.lastCodingRunId } : {}),
|
|
1149
1151
|
// A continued ending is a segment's end, not the unit's: the bot
|
|
1150
1152
|
// writes the renewal as a row keyed by the next segment's index
|
package/dist/cli.js
CHANGED
|
@@ -34687,6 +34687,9 @@ var init_instanceStore = __esm({
|
|
|
34687
34687
|
async putUnits(_units) {
|
|
34688
34688
|
return { ok: false, reason: "unavailable" };
|
|
34689
34689
|
}
|
|
34690
|
+
async claimLegacyContinuation(_expected, _recovered) {
|
|
34691
|
+
return { ok: false, reason: "unavailable" };
|
|
34692
|
+
}
|
|
34690
34693
|
async listUnits(_instanceId) {
|
|
34691
34694
|
return [];
|
|
34692
34695
|
}
|
|
@@ -34761,6 +34764,15 @@ var init_instanceStore = __esm({
|
|
|
34761
34764
|
if (d.ok === true) return { ok: true };
|
|
34762
34765
|
throw new Error(`coordinator store /runs/coordinator/units/put: unexpected answer (HTTP ${r.status})`);
|
|
34763
34766
|
}
|
|
34767
|
+
async claimLegacyContinuation(expected, recovered) {
|
|
34768
|
+
const r = await this.post("/runs/coordinator/units/claim-legacy-continuation", { expected, recovered });
|
|
34769
|
+
const d = r.data;
|
|
34770
|
+
if (r.status === 409 && d.reason === "stale") return { ok: false, reason: "stale" };
|
|
34771
|
+
if (d.ok === true) return { ok: true };
|
|
34772
|
+
throw new Error(
|
|
34773
|
+
`coordinator store /runs/coordinator/units/claim-legacy-continuation: unexpected answer (HTTP ${r.status})`
|
|
34774
|
+
);
|
|
34775
|
+
}
|
|
34764
34776
|
async listUnits(instanceId) {
|
|
34765
34777
|
const r = await this.post("/runs/coordinator/units/list", { instanceId });
|
|
34766
34778
|
const d = r.data;
|
|
@@ -54774,75 +54786,103 @@ async function handOffToCoordinatorUnchecked(deps, input) {
|
|
|
54774
54786
|
}
|
|
54775
54787
|
async function start(deps, input, instance, units, where, write) {
|
|
54776
54788
|
const log = deps.log ?? console.log;
|
|
54777
|
-
const
|
|
54778
|
-
if (
|
|
54779
|
-
|
|
54789
|
+
const reservation = await input.beforeStart?.();
|
|
54790
|
+
if (reservation?.ok === false)
|
|
54791
|
+
return { status: "aborted", reply: reservation.refusal.text, refusal: reservation.refusal };
|
|
54792
|
+
let started = false;
|
|
54793
|
+
try {
|
|
54794
|
+
const put = write === "replace" ? await deps.instances.replace(instance) : await deps.instances.put(instance);
|
|
54795
|
+
if (!put.ok) {
|
|
54796
|
+
if (put.reason === "unavailable")
|
|
54797
|
+
return refused5(
|
|
54798
|
+
"plan_history_unavailable",
|
|
54799
|
+
"\u26A0\uFE0F The plan runner needs run history on the state Worker (`runHistory.worker`): the instance record could not be written, so nothing ran."
|
|
54800
|
+
);
|
|
54780
54801
|
return refused5(
|
|
54781
|
-
"
|
|
54782
|
-
|
|
54802
|
+
"plan_runner_conflict",
|
|
54803
|
+
`\u{1F6AB} A runner for \`${instance.id}\` was just recorded by another request, so this request started nothing; the recorded runner owns the pipeline.`
|
|
54783
54804
|
);
|
|
54784
|
-
|
|
54785
|
-
|
|
54786
|
-
|
|
54787
|
-
);
|
|
54788
|
-
}
|
|
54789
|
-
const rows = await deps.instances.putUnits(units);
|
|
54790
|
-
if (!rows.ok)
|
|
54791
|
-
return refused5(
|
|
54792
|
-
"plan_history_unavailable",
|
|
54793
|
-
"\u26A0\uFE0F The plan runner needs run history on the state Worker: the unit rows could not be written, so nothing ran."
|
|
54794
|
-
);
|
|
54795
|
-
const accepted = [...input.msg.images ?? [], ...input.msg.documents ?? []];
|
|
54796
|
-
if (instance.plan?.path === void 0 && accepted.length > 0) {
|
|
54797
|
-
const unit = units[0];
|
|
54798
|
-
const seeded = await deps.instances.appendEvent(
|
|
54799
|
-
{ instanceId: instance.id, unit: unit.unit },
|
|
54800
|
-
{
|
|
54801
|
-
id: `${instance.id}:${unit.unit}:ship-request`,
|
|
54802
|
-
sender: input.msg.userId,
|
|
54803
|
-
...input.msg.userName !== void 0 ? { senderName: input.msg.userName } : {},
|
|
54804
|
-
text: "Attachments from the ship request.",
|
|
54805
|
-
attachments: accepted,
|
|
54806
|
-
mode: "steer",
|
|
54807
|
-
at: input.now
|
|
54808
|
-
}
|
|
54809
|
-
);
|
|
54810
|
-
if (!seeded.ok)
|
|
54805
|
+
}
|
|
54806
|
+
const rows = await deps.instances.putUnits(units);
|
|
54807
|
+
if (!rows.ok)
|
|
54811
54808
|
return refused5(
|
|
54812
54809
|
"plan_history_unavailable",
|
|
54813
|
-
"\u26A0\uFE0F The plan runner needs run history on the state Worker: the
|
|
54810
|
+
"\u26A0\uFE0F The plan runner needs run history on the state Worker: the unit rows could not be written, so nothing ran."
|
|
54814
54811
|
);
|
|
54815
|
-
|
|
54816
|
-
|
|
54817
|
-
|
|
54818
|
-
|
|
54819
|
-
|
|
54820
|
-
|
|
54821
|
-
|
|
54822
|
-
|
|
54823
|
-
|
|
54824
|
-
|
|
54825
|
-
|
|
54826
|
-
|
|
54827
|
-
|
|
54828
|
-
|
|
54829
|
-
instanceId: instance.id
|
|
54830
|
-
};
|
|
54831
|
-
}
|
|
54832
|
-
case "duplicate": {
|
|
54833
|
-
const status3 = answer.status !== void 0 ? `, status: ${answer.status}` : "";
|
|
54834
|
-
return refused5(
|
|
54835
|
-
"plan_instance_orphaned",
|
|
54836
|
-
`\u{1F6AB} A Workflow instance \`${instance.id}\` already exists on the platform${status3} but the state Worker knew nothing of it \u2014 a person decides; re-issuing will not resume it.`
|
|
54812
|
+
const accepted = [...input.msg.images ?? [], ...input.msg.documents ?? []];
|
|
54813
|
+
if (instance.plan?.path === void 0 && accepted.length > 0) {
|
|
54814
|
+
const unit = units[0];
|
|
54815
|
+
const seeded = await deps.instances.appendEvent(
|
|
54816
|
+
{ instanceId: instance.id, unit: unit.unit },
|
|
54817
|
+
{
|
|
54818
|
+
id: `${instance.id}:${unit.unit}:ship-request`,
|
|
54819
|
+
sender: input.msg.userId,
|
|
54820
|
+
...input.msg.userName !== void 0 ? { senderName: input.msg.userName } : {},
|
|
54821
|
+
text: "Attachments from the ship request.",
|
|
54822
|
+
attachments: accepted,
|
|
54823
|
+
mode: "steer",
|
|
54824
|
+
at: input.now
|
|
54825
|
+
}
|
|
54837
54826
|
);
|
|
54827
|
+
if (!seeded.ok)
|
|
54828
|
+
return refused5(
|
|
54829
|
+
"plan_history_unavailable",
|
|
54830
|
+
"\u26A0\uFE0F The plan runner needs run history on the state Worker: the ship request's attachments could not be written, so nothing ran."
|
|
54831
|
+
);
|
|
54838
54832
|
}
|
|
54839
|
-
|
|
54840
|
-
|
|
54841
|
-
|
|
54842
|
-
|
|
54843
|
-
|
|
54844
|
-
|
|
54845
|
-
|
|
54833
|
+
if (reservation?.ok === true) {
|
|
54834
|
+
const unit = units[0];
|
|
54835
|
+
if (unit === void 0)
|
|
54836
|
+
return refused5(
|
|
54837
|
+
"setup_failed",
|
|
54838
|
+
"\u26A0\uFE0F The plan runner had no durable unit to own at its final start gate, so nothing ran."
|
|
54839
|
+
);
|
|
54840
|
+
try {
|
|
54841
|
+
await reservation.commit({ instanceId: instance.id, unit: unit.unit });
|
|
54842
|
+
} catch (err2) {
|
|
54843
|
+
return refused5(
|
|
54844
|
+
"setup_failed",
|
|
54845
|
+
`\u26A0\uFE0F The plan runner could not transfer existing-pull-request ownership to its new attempt (${describe7(err2)}), so nothing ran.`
|
|
54846
|
+
);
|
|
54847
|
+
}
|
|
54848
|
+
}
|
|
54849
|
+
let answer;
|
|
54850
|
+
try {
|
|
54851
|
+
answer = await deps.create(instance.id);
|
|
54852
|
+
} catch (err2) {
|
|
54853
|
+
answer = { kind: "unanswered", reason: describe7(err2) };
|
|
54854
|
+
}
|
|
54855
|
+
switch (answer.kind) {
|
|
54856
|
+
case "created": {
|
|
54857
|
+
if (reservation?.ok === true) reservation.complete();
|
|
54858
|
+
started = true;
|
|
54859
|
+
log(`[ship] ${input.msg.threadKey}: handed to the plan runner ${instance.id} (${units.length} unit(s))`);
|
|
54860
|
+
const replaced = write === "replace" ? ["the records of an earlier attempt that never started were replaced"] : [];
|
|
54861
|
+
return {
|
|
54862
|
+
status: "completed",
|
|
54863
|
+
reply: handedOff([...where, ...replaced]),
|
|
54864
|
+
instanceId: instance.id
|
|
54865
|
+
};
|
|
54866
|
+
}
|
|
54867
|
+
case "duplicate": {
|
|
54868
|
+
const status3 = answer.status !== void 0 ? `, status: ${answer.status}` : "";
|
|
54869
|
+
return refused5(
|
|
54870
|
+
"plan_instance_orphaned",
|
|
54871
|
+
`\u{1F6AB} A Workflow instance \`${instance.id}\` already exists on the platform${status3} but the state Worker knew nothing of it \u2014 a person decides; re-issuing will not resume it.`
|
|
54872
|
+
);
|
|
54873
|
+
}
|
|
54874
|
+
case "failed":
|
|
54875
|
+
case "unanswered":
|
|
54876
|
+
log(`[ship] ${input.msg.threadKey}: the plan runner ${instance.id} could not be started \u2014 ${answer.reason}`);
|
|
54877
|
+
return refused5(
|
|
54878
|
+
"plan_start_failed",
|
|
54879
|
+
`\u26A0\uFE0F This is a bug: the plan runner could not be started (${answer.reason}), nothing ran, and no automatic start retry was scheduled.`
|
|
54880
|
+
);
|
|
54881
|
+
}
|
|
54882
|
+
const unreachable3 = answer;
|
|
54883
|
+
return unreachable3;
|
|
54884
|
+
} finally {
|
|
54885
|
+
if (!started && reservation?.ok === true) await reservation.abort();
|
|
54846
54886
|
}
|
|
54847
54887
|
}
|
|
54848
54888
|
function handedOff(facts2) {
|
|
@@ -55296,6 +55336,7 @@ async function runShipBranch(deps, msg, io, ctx) {
|
|
|
55296
55336
|
entry,
|
|
55297
55337
|
requestText: directives.text,
|
|
55298
55338
|
...ctx.reissuePlanId !== void 0 ? { reissuePlanId: ctx.reissuePlanId } : {},
|
|
55339
|
+
...ctx.beforeCoordinatorStart !== void 0 ? { beforeStart: ctx.beforeCoordinatorStart } : {},
|
|
55299
55340
|
msg,
|
|
55300
55341
|
agentSource: ctx.agentSource,
|
|
55301
55342
|
runId: run2.id,
|
|
@@ -56089,6 +56130,29 @@ function continuationBudgetOf(instance, unit) {
|
|
|
56089
56130
|
if (remainingRounds <= 0) return { kind: "exhausted", budget: "review-round" };
|
|
56090
56131
|
return { kind: "remaining", caps: { maxRounds: remainingRounds, maxMinutes: remainingMinutes } };
|
|
56091
56132
|
}
|
|
56133
|
+
async function legacyReviewedHeadOf(runs, instance, unit, prNumber) {
|
|
56134
|
+
const listed = await runs.listUnitRuns(unitKeyOf({ instanceId: instance.id, unit: unit.unit }), { kind: "all" }).catch(() => void 0);
|
|
56135
|
+
if (listed === void 0 || !listed.ok || listed.value.unit !== unitKeyOf({ instanceId: instance.id, unit: unit.unit }) || listed.value.instanceId !== instance.id || listed.value.id !== unit.unit || listed.value.instance.id !== instance.id || listed.value.instance.repo.toLowerCase() !== instance.repo.toLowerCase())
|
|
56136
|
+
return void 0;
|
|
56137
|
+
const prefix = `${instance.id}:${unit.unit}/`;
|
|
56138
|
+
const owned = listed.value.runs.filter(
|
|
56139
|
+
(run2) => run2.finished && run2.status === "completed" && run2.parentInstanceId === instance.id && run2.idempotencyKey?.startsWith(prefix) === true && run2.userId === instance.userId && run2.repo?.toLowerCase() === instance.repo.toLowerCase()
|
|
56140
|
+
);
|
|
56141
|
+
const coding = new Set(
|
|
56142
|
+
owned.flatMap(
|
|
56143
|
+
(run2) => run2.agent === "coding" && run2.pr?.number === prNumber && typeof run2.headSha === "string" && FULL_SHA2.test(run2.headSha) ? [run2.headSha.toLowerCase()] : []
|
|
56144
|
+
)
|
|
56145
|
+
);
|
|
56146
|
+
const approved = new Set(
|
|
56147
|
+
owned.flatMap((run2) => {
|
|
56148
|
+
const head = run2.reviewHead;
|
|
56149
|
+
const post = run2.reviewPost;
|
|
56150
|
+
return run2.agent === "review" && typeof head === "string" && FULL_SHA2.test(head) && run2.verdict?.verdict === "approve" && run2.verdict.head?.toLowerCase() === head.toLowerCase() && post?.posted === true && post.target.repo.toLowerCase() === instance.repo.toLowerCase() && post.target.number === prNumber && post.head.toLowerCase() === head.toLowerCase() && post.verdict === "approve" ? [head.toLowerCase()] : [];
|
|
56151
|
+
})
|
|
56152
|
+
);
|
|
56153
|
+
const exact = [...approved].filter((head) => coding.has(head));
|
|
56154
|
+
return exact.length === 1 ? exact[0] : void 0;
|
|
56155
|
+
}
|
|
56092
56156
|
async function dispatch(deps, msg, io, opts = {}) {
|
|
56093
56157
|
const ended = { status: "completed" };
|
|
56094
56158
|
const resume = opts.resume;
|
|
@@ -56173,6 +56237,7 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
56173
56237
|
let stoppedWhileAttaching;
|
|
56174
56238
|
let resumeRowClosed = false;
|
|
56175
56239
|
let restartRequest;
|
|
56240
|
+
let releaseLegacyOwnership;
|
|
56176
56241
|
const reservationHooks = {
|
|
56177
56242
|
onStop: (mode) => void registered?.control.requestStop(mode),
|
|
56178
56243
|
onFenced: () => {
|
|
@@ -56191,6 +56256,8 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
56191
56256
|
let operatorRepo;
|
|
56192
56257
|
let reissuePlanId;
|
|
56193
56258
|
let reissueCaps;
|
|
56259
|
+
let beforeCoordinatorStart;
|
|
56260
|
+
let reserveLegacyOwnership;
|
|
56194
56261
|
let operatorThread;
|
|
56195
56262
|
let pageOwner;
|
|
56196
56263
|
if (operatorMode !== "off") {
|
|
@@ -56408,7 +56475,19 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
56408
56475
|
await recordPendingOperator();
|
|
56409
56476
|
return ended;
|
|
56410
56477
|
}
|
|
56411
|
-
|
|
56478
|
+
let expectedHead = owner.unit.lastPush;
|
|
56479
|
+
const recoverLegacyBinding = owner.unit.ending?.kind === "merge_ready" && (expectedHead === void 0 || owner.unit.publication === void 0);
|
|
56480
|
+
if (recoverLegacyBinding) {
|
|
56481
|
+
const recovered = await legacyReviewedHeadOf(runsService, instance, owner.unit, recordedPr.number);
|
|
56482
|
+
if (recovered === void 0 || expectedHead !== void 0 && expectedHead.toLowerCase() !== recovered) {
|
|
56483
|
+
await io.reply(
|
|
56484
|
+
`Unit ${owner.unit.unit} has no single exact reviewed head in its durable child records, so continuation did not start. Nothing else ran.`
|
|
56485
|
+
);
|
|
56486
|
+
await recordPendingOperator();
|
|
56487
|
+
return ended;
|
|
56488
|
+
}
|
|
56489
|
+
expectedHead = recovered;
|
|
56490
|
+
}
|
|
56412
56491
|
if (expectedHead === void 0) {
|
|
56413
56492
|
await io.reply(
|
|
56414
56493
|
`Unit ${owner.unit.unit} does not retain the pull request's durable expected head, so continuation did not start. Nothing else ran.`
|
|
@@ -56447,6 +56526,140 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
56447
56526
|
await recordPendingOperator();
|
|
56448
56527
|
return ended;
|
|
56449
56528
|
}
|
|
56529
|
+
if (recoverLegacyBinding) {
|
|
56530
|
+
const baseRef = instance.base;
|
|
56531
|
+
if (baseRef === void 0 || facts2.baseRef !== baseRef) {
|
|
56532
|
+
await io.reply(
|
|
56533
|
+
`${instance.repo}#${recordedPr.number} no longer has the pipeline's verifiable base ref, so continuation did not start. Nothing else ran.`
|
|
56534
|
+
);
|
|
56535
|
+
await recordPendingOperator();
|
|
56536
|
+
return ended;
|
|
56537
|
+
}
|
|
56538
|
+
const existingBinding = owner.unit.publication;
|
|
56539
|
+
if (existingBinding !== void 0 && (existingBinding.repo.toLowerCase() !== instance.repo.toLowerCase() || existingBinding.pr !== recordedPr.number || existingBinding.headRef !== owner.unit.branch || existingBinding.baseRef !== baseRef || existingBinding.expectedHeadSha.toLowerCase() !== expectedHead || existingBinding.publicationRef !== owner.unit.branch || existingBinding.owner.instanceId !== owner.instanceId || existingBinding.owner.unit !== owner.unit.unit)) {
|
|
56540
|
+
await io.reply(
|
|
56541
|
+
`Unit ${owner.unit.unit}'s durable publication binding no longer matches this pipeline, so continuation did not start. Nothing else ran.`
|
|
56542
|
+
);
|
|
56543
|
+
await recordPendingOperator();
|
|
56544
|
+
return ended;
|
|
56545
|
+
}
|
|
56546
|
+
let currentOwner;
|
|
56547
|
+
const runnerOwnership = deps.runnerOwnership;
|
|
56548
|
+
try {
|
|
56549
|
+
if (runnerOwnership === void 0) throw new Error("runner ownership is unavailable");
|
|
56550
|
+
currentOwner = runnerOwnership.owner(instance.repo, recordedPr.number);
|
|
56551
|
+
} catch {
|
|
56552
|
+
await io.reply(
|
|
56553
|
+
`Runner ownership for ${instance.repo}#${recordedPr.number} could not be verified, so continuation did not start. Nothing else ran.`
|
|
56554
|
+
);
|
|
56555
|
+
await recordPendingOperator();
|
|
56556
|
+
return ended;
|
|
56557
|
+
}
|
|
56558
|
+
if (currentOwner !== void 0 && (currentOwner.instanceId !== owner.instanceId || currentOwner.unit !== owner.unit.unit)) {
|
|
56559
|
+
await io.reply(
|
|
56560
|
+
`${instance.repo}#${recordedPr.number} is owned by another runner, so continuation of ${owner.instanceId}:${owner.unit.unit} did not start. Nothing else ran.`
|
|
56561
|
+
);
|
|
56562
|
+
await recordPendingOperator();
|
|
56563
|
+
return ended;
|
|
56564
|
+
}
|
|
56565
|
+
const legacyUnit = owner.unit;
|
|
56566
|
+
const coordinatorInstances = deps.coordinatorInstances;
|
|
56567
|
+
const reservationOwner = { instanceId: owner.instanceId, unit: owner.unit.unit };
|
|
56568
|
+
const recoveredUnit = {
|
|
56569
|
+
...legacyUnit,
|
|
56570
|
+
lastPush: expectedHead,
|
|
56571
|
+
publication: {
|
|
56572
|
+
repo: instance.repo,
|
|
56573
|
+
pr: recordedPr.number,
|
|
56574
|
+
headRef: legacyUnit.branch,
|
|
56575
|
+
baseRef,
|
|
56576
|
+
expectedHeadSha: expectedHead,
|
|
56577
|
+
publicationRef: legacyUnit.branch,
|
|
56578
|
+
owner: reservationOwner
|
|
56579
|
+
}
|
|
56580
|
+
};
|
|
56581
|
+
let token2;
|
|
56582
|
+
let transferredOwner;
|
|
56583
|
+
releaseLegacyOwnership = () => {
|
|
56584
|
+
if (token2 !== void 0) {
|
|
56585
|
+
const released = runnerOwnership.releaseReservation(instance.repo, recordedPr.number, token2);
|
|
56586
|
+
if (released) token2 = void 0;
|
|
56587
|
+
return released;
|
|
56588
|
+
}
|
|
56589
|
+
if (transferredOwner !== void 0) {
|
|
56590
|
+
const released = runnerOwnership.release(instance.repo, recordedPr.number, transferredOwner);
|
|
56591
|
+
if (released) transferredOwner = void 0;
|
|
56592
|
+
return released;
|
|
56593
|
+
}
|
|
56594
|
+
return true;
|
|
56595
|
+
};
|
|
56596
|
+
reserveLegacyOwnership = () => {
|
|
56597
|
+
token2 = runnerOwnership.reserve(instance.repo, recordedPr.number, reservationOwner);
|
|
56598
|
+
if (token2 !== void 0) return void 0;
|
|
56599
|
+
const competing = runnerOwnership.owner(instance.repo, recordedPr.number);
|
|
56600
|
+
const sameContinuation = competing?.instanceId === reservationOwner.instanceId && competing.unit === reservationOwner.unit;
|
|
56601
|
+
return refusalOf(
|
|
56602
|
+
"setup_failed",
|
|
56603
|
+
sameContinuation ? `Unit ${legacyUnit.unit} changed while its legacy continuation was being reserved, so continuation did not start. Nothing else ran.` : `${instance.repo}#${recordedPr.number} is owned by another runner, so continuation of ${owner.instanceId}:${legacyUnit.unit} did not start. Nothing else ran.`
|
|
56604
|
+
);
|
|
56605
|
+
};
|
|
56606
|
+
beforeCoordinatorStart = async () => {
|
|
56607
|
+
const activeToken = token2;
|
|
56608
|
+
if (activeToken === void 0)
|
|
56609
|
+
return {
|
|
56610
|
+
ok: false,
|
|
56611
|
+
refusal: refusalOf(
|
|
56612
|
+
"setup_failed",
|
|
56613
|
+
`Unit ${legacyUnit.unit}'s ownership reservation was lost, so continuation did not start. Nothing else ran.`
|
|
56614
|
+
)
|
|
56615
|
+
};
|
|
56616
|
+
const persisted = await coordinatorInstances.claimLegacyContinuation(legacyUnit, recoveredUnit).catch(() => void 0);
|
|
56617
|
+
if (persisted?.ok !== true) {
|
|
56618
|
+
const released = releaseLegacyOwnership?.() ?? true;
|
|
56619
|
+
const reason2 = persisted?.reason === "stale" ? `Unit ${legacyUnit.unit} changed while its legacy continuation was being reserved, so continuation did not start.` : `Unit ${legacyUnit.unit}'s recovered continuation binding could not be stored, so continuation did not start.`;
|
|
56620
|
+
const text = released ? `${reason2} Nothing else ran.` : `${reason2} Its ownership reservation could not be released. Nothing else ran.`;
|
|
56621
|
+
return { ok: false, refusal: refusalOf("setup_failed", text) };
|
|
56622
|
+
}
|
|
56623
|
+
const reservedOwner = runnerOwnership.owner(instance.repo, recordedPr.number);
|
|
56624
|
+
if (reservedOwner?.instanceId !== reservationOwner.instanceId || reservedOwner.unit !== reservationOwner.unit) {
|
|
56625
|
+
const restored = await coordinatorInstances.claimLegacyContinuation(recoveredUnit, legacyUnit).catch(() => void 0);
|
|
56626
|
+
const released = releaseLegacyOwnership?.() ?? true;
|
|
56627
|
+
const rollbackFailure = restored?.ok === true ? void 0 : restored?.reason ?? "unavailable";
|
|
56628
|
+
const cleanupFailure = released ? "" : " and its ownership reservation could not be released";
|
|
56629
|
+
const text = rollbackFailure === void 0 ? `${instance.repo}#${recordedPr.number} changed runner ownership while its legacy continuation was being reserved${cleanupFailure}, so continuation did not start. Nothing else ran.` : `${instance.repo}#${recordedPr.number} changed runner ownership while its legacy continuation was being reserved, and the exact legacy row could not be restored (${rollbackFailure})${cleanupFailure}; continuation did not start. Nothing else ran.`;
|
|
56630
|
+
return { ok: false, refusal: refusalOf("setup_failed", text) };
|
|
56631
|
+
}
|
|
56632
|
+
owner.unit = recoveredUnit;
|
|
56633
|
+
return {
|
|
56634
|
+
ok: true,
|
|
56635
|
+
commit: async (nextOwner) => {
|
|
56636
|
+
if (!runnerOwnership.transferReservation(
|
|
56637
|
+
instance.repo,
|
|
56638
|
+
recordedPr.number,
|
|
56639
|
+
activeToken,
|
|
56640
|
+
reservationOwner,
|
|
56641
|
+
nextOwner
|
|
56642
|
+
))
|
|
56643
|
+
throw new Error("legacy continuation ownership reservation or current owner changed");
|
|
56644
|
+
token2 = void 0;
|
|
56645
|
+
transferredOwner = nextOwner;
|
|
56646
|
+
},
|
|
56647
|
+
complete: () => {
|
|
56648
|
+
transferredOwner = void 0;
|
|
56649
|
+
},
|
|
56650
|
+
abort: async () => {
|
|
56651
|
+
const restored = await coordinatorInstances.claimLegacyContinuation(recoveredUnit, legacyUnit).catch(() => void 0);
|
|
56652
|
+
const released = releaseLegacyOwnership?.() ?? true;
|
|
56653
|
+
owner.unit = legacyUnit;
|
|
56654
|
+
const failures = [
|
|
56655
|
+
...restored?.ok === true ? [] : [`legacy continuation rollback failed (${restored?.reason ?? "unavailable"})`],
|
|
56656
|
+
...released ? [] : ["legacy continuation ownership cleanup failed"]
|
|
56657
|
+
];
|
|
56658
|
+
if (failures.length > 0) throw new Error(failures.join("; "));
|
|
56659
|
+
}
|
|
56660
|
+
};
|
|
56661
|
+
};
|
|
56662
|
+
}
|
|
56450
56663
|
reissuePlanId = instance.plan.id;
|
|
56451
56664
|
reissueCaps = continuationBudget.caps;
|
|
56452
56665
|
const durableRequest = parseDirectives(original);
|
|
@@ -56499,6 +56712,12 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
56499
56712
|
});
|
|
56500
56713
|
if (profileGate.kind === "refused") return ended;
|
|
56501
56714
|
const { profile } = profileGate;
|
|
56715
|
+
const legacyOwnershipRefusal = reserveLegacyOwnership?.();
|
|
56716
|
+
if (legacyOwnershipRefusal !== void 0) {
|
|
56717
|
+
await io.reply(legacyOwnershipRefusal.text);
|
|
56718
|
+
await recordPendingOperator();
|
|
56719
|
+
return ended;
|
|
56720
|
+
}
|
|
56502
56721
|
const earlyRepoTarget = agent.name === "review" ? resolveRepoTarget(deps, {
|
|
56503
56722
|
msg,
|
|
56504
56723
|
history,
|
|
@@ -56677,6 +56896,7 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
56677
56896
|
card,
|
|
56678
56897
|
directives,
|
|
56679
56898
|
...reissuePlanId !== void 0 ? { reissuePlanId } : {},
|
|
56899
|
+
...beforeCoordinatorStart !== void 0 ? { beforeCoordinatorStart } : {},
|
|
56680
56900
|
...reissueCaps !== void 0 ? { reissueCaps: { ...reissueCaps, maxMinutes: Math.min(reissueCaps.maxMinutes, profile.minutes) } } : {},
|
|
56681
56901
|
sticky,
|
|
56682
56902
|
history,
|
|
@@ -57389,6 +57609,7 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
57389
57609
|
});
|
|
57390
57610
|
} finally {
|
|
57391
57611
|
clearInterval(setupHeartbeat);
|
|
57612
|
+
releaseLegacyOwnership?.();
|
|
57392
57613
|
ending.drain(void 0);
|
|
57393
57614
|
if (registered) deps.runBearers?.revoke(registered.id);
|
|
57394
57615
|
if (resume && ledgerRun && !runLoopStarted && !resumeRowClosed) {
|
|
@@ -57576,7 +57797,7 @@ async function dispatchClick(deps, click) {
|
|
|
57576
57797
|
activeRuns--;
|
|
57577
57798
|
}
|
|
57578
57799
|
}
|
|
57579
|
-
var activeRuns;
|
|
57800
|
+
var activeRuns, FULL_SHA2;
|
|
57580
57801
|
var init_dispatcher = __esm({
|
|
57581
57802
|
"../../src/core/dispatcher.ts"() {
|
|
57582
57803
|
"use strict";
|
|
@@ -57642,6 +57863,7 @@ var init_dispatcher = __esm({
|
|
|
57642
57863
|
init_types5();
|
|
57643
57864
|
init_decisionRecordReservation();
|
|
57644
57865
|
activeRuns = 0;
|
|
57866
|
+
FULL_SHA2 = /^[0-9a-f]{40}$/i;
|
|
57645
57867
|
}
|
|
57646
57868
|
});
|
|
57647
57869
|
|
|
@@ -71522,22 +71744,73 @@ var init_runnerOwnership = __esm({
|
|
|
71522
71744
|
recovered = /* @__PURE__ */ new Set();
|
|
71523
71745
|
claimedOwners = /* @__PURE__ */ new Map();
|
|
71524
71746
|
recoveredOwners = /* @__PURE__ */ new Map();
|
|
71747
|
+
reservations = /* @__PURE__ */ new Map();
|
|
71525
71748
|
localRecoveredInstances = /* @__PURE__ */ new Set();
|
|
71526
71749
|
recoveryComplete;
|
|
71527
71750
|
constructor(requiresRecovery) {
|
|
71528
71751
|
this.recoveryComplete = !requiresRecovery;
|
|
71529
71752
|
}
|
|
71753
|
+
/** Claim without displacing a different live runner. The read and write are
|
|
71754
|
+
* synchronous, so a caller owns the process-local fence before its next
|
|
71755
|
+
* awaited durable transition. Repeating the same owner's claim is idempotent. */
|
|
71530
71756
|
claim(repo, prNumber, owner) {
|
|
71531
71757
|
const key = runnerOwnedPullKey(repo, prNumber);
|
|
71758
|
+
const current = this.claimedOwners.get(key) ?? this.recoveredOwners.get(key);
|
|
71759
|
+
const alreadyOwned = this.claimed.has(key) || this.recovered.has(key);
|
|
71760
|
+
if (this.reservations.has(key)) return false;
|
|
71761
|
+
if (alreadyOwned && (owner === void 0 || current === void 0 || current.instanceId !== owner.instanceId || current.unit !== owner.unit))
|
|
71762
|
+
return false;
|
|
71532
71763
|
this.claimed.add(key);
|
|
71533
71764
|
if (owner !== void 0) this.claimedOwners.set(key, owner);
|
|
71765
|
+
return true;
|
|
71534
71766
|
}
|
|
71535
|
-
|
|
71767
|
+
/** Exclusively reserve an unowned pull request across an awaited durable
|
|
71768
|
+
* transition. Even the same runner identity cannot reserve it twice: the
|
|
71769
|
+
* returned token, not the owner fields, identifies the one caller. */
|
|
71770
|
+
reserve(repo, prNumber, owner) {
|
|
71536
71771
|
const key = runnerOwnedPullKey(repo, prNumber);
|
|
71772
|
+
if (this.claimed.has(key) || this.recovered.has(key) || this.reservations.has(key)) return void 0;
|
|
71773
|
+
const token2 = Symbol(key);
|
|
71774
|
+
this.reservations.set(key, token2);
|
|
71775
|
+
this.claimed.add(key);
|
|
71776
|
+
this.claimedOwners.set(key, owner);
|
|
71777
|
+
return token2;
|
|
71778
|
+
}
|
|
71779
|
+
/** Move an exclusive recovery reservation to the durable attempt that is
|
|
71780
|
+
* about to start. Both the unforgeable token and the currently recorded
|
|
71781
|
+
* owner must still match; a stale caller can neither displace nor adopt a
|
|
71782
|
+
* successor. The transfer consumes the reservation but retains ownership. */
|
|
71783
|
+
transferReservation(repo, prNumber, token2, currentOwner, nextOwner) {
|
|
71784
|
+
const key = runnerOwnedPullKey(repo, prNumber);
|
|
71785
|
+
const current = this.claimedOwners.get(key);
|
|
71786
|
+
if (this.reservations.get(key) !== token2 || !this.claimed.has(key) || current?.instanceId !== currentOwner.instanceId || current.unit !== currentOwner.unit)
|
|
71787
|
+
return false;
|
|
71788
|
+
this.reservations.delete(key);
|
|
71789
|
+
this.claimedOwners.set(key, nextOwner);
|
|
71790
|
+
return true;
|
|
71791
|
+
}
|
|
71792
|
+
releaseReservation(repo, prNumber, token2) {
|
|
71793
|
+
const key = runnerOwnedPullKey(repo, prNumber);
|
|
71794
|
+
if (this.reservations.get(key) !== token2) return false;
|
|
71795
|
+
this.reservations.delete(key);
|
|
71796
|
+
this.claimed.delete(key);
|
|
71797
|
+
this.claimedOwners.delete(key);
|
|
71798
|
+
return true;
|
|
71799
|
+
}
|
|
71800
|
+
/** Release unconditionally for the runner's ordinary end, or conditionally
|
|
71801
|
+
* for a provisional claim whose cleanup must not erase a successor. */
|
|
71802
|
+
release(repo, prNumber, owner) {
|
|
71803
|
+
const key = runnerOwnedPullKey(repo, prNumber);
|
|
71804
|
+
if (owner !== void 0) {
|
|
71805
|
+
const current = this.claimedOwners.get(key) ?? this.recoveredOwners.get(key);
|
|
71806
|
+
if (current?.instanceId !== owner.instanceId || current.unit !== owner.unit) return false;
|
|
71807
|
+
}
|
|
71808
|
+
this.reservations.delete(key);
|
|
71537
71809
|
this.claimed.delete(key);
|
|
71538
71810
|
this.recovered.delete(key);
|
|
71539
71811
|
this.claimedOwners.delete(key);
|
|
71540
71812
|
this.recoveredOwners.delete(key);
|
|
71813
|
+
return true;
|
|
71541
71814
|
}
|
|
71542
71815
|
owns(repo, prNumber) {
|
|
71543
71816
|
if (!this.recoveryComplete) throw new Error("runner ownership recovery is still in progress");
|
|
@@ -75544,6 +75817,7 @@ async function runBot() {
|
|
|
75544
75817
|
) : new NullMetricsService();
|
|
75545
75818
|
const sweepState = { roundSpent: /* @__PURE__ */ new Set() };
|
|
75546
75819
|
const runnerOwnership = new RunnerOwnershipFence(capabilities.runLedger);
|
|
75820
|
+
deps.runnerOwnership = runnerOwnership;
|
|
75547
75821
|
const pullsWiring = /* @__PURE__ */ (() => {
|
|
75548
75822
|
const state = sweepState;
|
|
75549
75823
|
const chains = /* @__PURE__ */ new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coreplane/switchboard",
|
|
3
|
-
"version": "1.261.
|
|
3
|
+
"version": "1.261.3",
|
|
4
4
|
"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.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://openswitchboard.dev",
|