@coreplane/switchboard 1.262.1 → 1.262.2

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.262.1",
3
+ "version": "1.262.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.262.1",
9
+ "version": "1.262.2",
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.262.1",
21063
+ "version": "1.262.2",
21064
21064
  "license": "Apache-2.0",
21065
21065
  "dependencies": {
21066
21066
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.262.1",
3
+ "version": "1.262.2",
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",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.262.1",
3
- "commit": "b7f8f15f4bb351a30811697a93d6bac4d67a5db9",
4
- "builtAt": "2026-09-25T14:45:23.162Z"
2
+ "version": "1.262.2",
3
+ "commit": "6e30af1fca5413c37ae12556c8863f4fa0126476",
4
+ "builtAt": "2026-09-25T17:00:14.499Z"
5
5
  }
@@ -514,6 +514,10 @@ export interface OriginalUnitRecovery {
514
514
  /** The exact terminal value replaced by the claim, for fail-closed rollback
515
515
  * if the Workflow cannot be admitted. */
516
516
  previousEnding: NonNullable<CoordinatorUnit["ending"]>;
517
+ /** Original binding fields before an evidence-backed repair. The empty object
518
+ * means both were absent; retained across restart for admission rollback,
519
+ * then retired when the recovery records progress. */
520
+ previousBinding?: { publication?: ExistingPrPublicationBinding; lastPush?: string };
517
521
  /** The separate Workflow execution checkpoint. This is transport identity,
518
522
  * not a replacement coordinator/unit identity. */
519
523
  workflowId: string;
@@ -813,6 +817,13 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
813
817
  r.recovery.findingsKey !== undefined)) &&
814
818
  (r.recovery.findings === undefined ||
815
819
  (Array.isArray(r.recovery.findings) && r.recovery.findings.every(isFindingShape))) &&
820
+ (r.recovery.previousBinding === undefined ||
821
+ (isObject(r.recovery.previousBinding) &&
822
+ (r.recovery.previousBinding.publication === undefined ||
823
+ isPublication(r.recovery.previousBinding.publication)) &&
824
+ (r.recovery.previousBinding.lastPush === undefined ||
825
+ (typeof r.recovery.previousBinding.lastPush === "string" &&
826
+ /^[0-9a-f]{40}$/i.test(r.recovery.previousBinding.lastPush))))) &&
816
827
  isObject(r.recovery.previousEnding) &&
817
828
  isText(r.recovery.previousEnding.kind) &&
818
829
  typeof r.recovery.previousEnding.report === "string" &&
package/dist/cli.js CHANGED
@@ -5808,7 +5808,7 @@ function isCoordinatorUnit(v) {
5808
5808
  (x) => isObject(x) && isFinite(x.index) && isText(x.agent) && isText(x.outcome) && isFinite(x.at) && (x.gate === void 0 || isRoundGate(x.gate))
5809
5809
  ))
5810
5810
  return false;
5811
- if (r.recovery !== void 0 && !(isObject(r.recovery) && (r.recovery.kind === "findings" || r.recovery.kind === "review") && typeof r.recovery.round === "number" && Number.isInteger(r.recovery.round) && r.recovery.round >= 1 && typeof r.recovery.expectedHeadSha === "string" && /^[0-9a-f]{40}$/i.test(r.recovery.expectedHeadSha) && isFinite(r.recovery.remainingMs) && r.recovery.remainingMs > 0 && isFinite(r.recovery.claimedAt) && typeof r.recovery.step === "string" && STEP_NAME_PATTERN.test(r.recovery.step) && isText(r.recovery.reviewRunId) && (r.recovery.findingsRunId === void 0 || isText(r.recovery.findingsRunId)) && (r.recovery.findingsKey === void 0 || isText(r.recovery.findingsKey)) && (r.recovery.findingsRunId === void 0 && r.recovery.findingsKey === void 0 || r.recovery.kind === "review" && r.recovery.findingsRunId !== void 0 && r.recovery.findingsKey !== void 0) && (r.recovery.findings === void 0 || Array.isArray(r.recovery.findings) && r.recovery.findings.every(isFindingShape)) && isObject(r.recovery.previousEnding) && isText(r.recovery.previousEnding.kind) && typeof r.recovery.previousEnding.report === "string" && isFinite(r.recovery.previousEnding.at) && typeof r.recovery.workflowId === "string" && INSTANCE_ID_PATTERN.test(r.recovery.workflowId) && isFinite(r.recovery.deadlineAt) && isText(r.recovery.reviewKey)))
5811
+ if (r.recovery !== void 0 && !(isObject(r.recovery) && (r.recovery.kind === "findings" || r.recovery.kind === "review") && typeof r.recovery.round === "number" && Number.isInteger(r.recovery.round) && r.recovery.round >= 1 && typeof r.recovery.expectedHeadSha === "string" && /^[0-9a-f]{40}$/i.test(r.recovery.expectedHeadSha) && isFinite(r.recovery.remainingMs) && r.recovery.remainingMs > 0 && isFinite(r.recovery.claimedAt) && typeof r.recovery.step === "string" && STEP_NAME_PATTERN.test(r.recovery.step) && isText(r.recovery.reviewRunId) && (r.recovery.findingsRunId === void 0 || isText(r.recovery.findingsRunId)) && (r.recovery.findingsKey === void 0 || isText(r.recovery.findingsKey)) && (r.recovery.findingsRunId === void 0 && r.recovery.findingsKey === void 0 || r.recovery.kind === "review" && r.recovery.findingsRunId !== void 0 && r.recovery.findingsKey !== void 0) && (r.recovery.findings === void 0 || Array.isArray(r.recovery.findings) && r.recovery.findings.every(isFindingShape)) && (r.recovery.previousBinding === void 0 || isObject(r.recovery.previousBinding) && (r.recovery.previousBinding.publication === void 0 || isPublication(r.recovery.previousBinding.publication)) && (r.recovery.previousBinding.lastPush === void 0 || typeof r.recovery.previousBinding.lastPush === "string" && /^[0-9a-f]{40}$/i.test(r.recovery.previousBinding.lastPush))) && isObject(r.recovery.previousEnding) && isText(r.recovery.previousEnding.kind) && typeof r.recovery.previousEnding.report === "string" && isFinite(r.recovery.previousEnding.at) && typeof r.recovery.workflowId === "string" && INSTANCE_ID_PATTERN.test(r.recovery.workflowId) && isFinite(r.recovery.deadlineAt) && isText(r.recovery.reviewKey)))
5812
5812
  return false;
5813
5813
  if (r.recoveryReceipt !== void 0 && (!isObject(r.recoveryReceipt) || !isText(r.recoveryReceipt.reviewRunId) || typeof r.recoveryReceipt.workflowId !== "string" || !INSTANCE_ID_PATTERN.test(r.recoveryReceipt.workflowId) || !isFinite(r.recoveryReceipt.at)))
5814
5814
  return false;
@@ -55102,7 +55102,8 @@ async function runShipBranch(deps, msg, io, ctx) {
55102
55102
  () => deps.recoverOriginalUnit === void 0 ? Promise.resolve({ status: 503, body: { error: "original-unit recovery is unavailable" } }) : deps.recoverOriginalUnit(recovery, { userId: msg.userId, threadKey: msg.threadKey })
55103
55103
  );
55104
55104
  if (answer.status !== 200) {
55105
- const reason2 = typeof answer.body.error === "string" ? answer.body.error : "original-unit recovery was refused";
55105
+ const error = typeof answer.body.error === "string" ? answer.body.error : "original-unit recovery was refused";
55106
+ const reason2 = error === "recovery_budget_unknown" && typeof answer.body.reason === "string" ? `${error}: ${answer.body.reason}` : error;
55106
55107
  await refuse3(
55107
55108
  refusalOf("setup_failed", reason2),
55108
55109
  () => card.done(shell.close({ kind: "refused", icon: "\u{1F6AB}", reason: reason2, ...closeLines(clock4(), false) }))
@@ -73649,16 +73650,36 @@ async function steerChild(body, deps) {
73649
73650
  const steered = await deps.steerChild?.(body.runId, text, instance) ?? false;
73650
73651
  return json3(200, { ok: true, outcome: steered ? "steered" : "not_live", at });
73651
73652
  }
73652
- async function advanceRecoveryPublication(deps, instance, row2, facts2, runId3) {
73653
- if (row2.recovery === void 0 || row2.publication === void 0 || facts2.headSha === row2.publication.expectedHeadSha)
73654
- return row2;
73655
- const child = await deps.runs.getRun(runId3);
73656
- const expectedPrefix = `${instance.id}:${row2.unit}/recovery/`;
73653
+ async function advanceFindingsPublication(deps, instance, row2, facts2, runId3) {
73654
+ if (row2.publication === void 0 || facts2.headSha === row2.publication.expectedHeadSha) return row2;
73655
+ const child = await deps.runs.getRun(runId3, { include: "messages" });
73657
73656
  const owner = { instanceId: instance.id, unit: row2.unit };
73658
- if (!child.ok || child.value.finished !== true || child.value.status !== "completed" || child.value.agent !== "coding" || child.value.parentInstanceId !== instance.id || child.value.userId !== instance.userId || child.value.repo?.toLowerCase() !== instance.repo.toLowerCase() || child.value.threadKey !== (row2.threadKey ?? instance.threadKey) || child.value.idempotencyKey?.startsWith(expectedPrefix) !== true || !/\/findings(?:\/a[1-9][0-9]*)?$/.test(child.value.idempotencyKey) || !fullHead(facts2.headSha) || child.value.headSha !== facts2.headSha || child.value.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) !== true || facts2.state !== "open" || facts2.sameRepoHead !== true || facts2.headBranchExists !== true || facts2.headRef !== row2.publication.headRef || facts2.baseRef !== row2.publication.baseRef || facts2.verifiedHead?.repo.toLowerCase() !== instance.repo.toLowerCase() || facts2.verifiedHead.ref !== row2.publication.publicationRef || facts2.verifiedHead.sha !== facts2.headSha || !samePublicationOwner(row2.publication.owner, owner))
73657
+ const latestReview = [...row2.rounds].reverse().find((round2) => round2.agent === "review");
73658
+ const segment = row2.segments?.reduce((latest, entry) => Math.max(latest, entry.index), 1) ?? 1;
73659
+ const prefix = row2.recovery !== void 0 ? `${row2.unit}/recovery` : stepPrefixOf(row2.unit, segment > 1 ? { segment, renewalsSpent: 0, spendUsd: null } : void 0);
73660
+ const findingsPrefix = `${instance.id}:${prefix}/${latestReview?.index}/findings`;
73661
+ if (row2.recovery === void 0) {
73662
+ const tags = child.ok ? child.value.events?.filter((event) => event.type === "coordinator_tag") ?? [] : [];
73663
+ const tag = tags.length === 1 ? tags[0] : void 0;
73664
+ if (row2.ending !== void 0 || row2.idle !== void 0 || latestReview?.outcome !== "request_changes" || !child.ok || child.value.startedAt < latestReview.at || tag?.parentInstanceId !== instance.id || tag.unit !== row2.unit || tag.base !== instance.base || JSON.stringify(tag.publication) !== JSON.stringify(row2.publication))
73665
+ throw new PublicationBindingRefusal("publication_facts_mismatch");
73666
+ const listing = await deps.runs.listRuns({
73667
+ status: "finished",
73668
+ visibleTo: EVERY_RUN,
73669
+ threadKey: row2.threadKey ?? instance.threadKey,
73670
+ limit: RUN_LIST_MAX_LIMIT
73671
+ });
73672
+ if (listing.storeUnavailable || listing.nextBefore !== void 0 || listing.runs.filter(
73673
+ (run2) => run2.parentInstanceId === instance.id && isStepAttempt(run2.idempotencyKey, findingsPrefix)
73674
+ ).length !== 1)
73675
+ throw new PublicationBindingRefusal("publication_facts_mismatch");
73676
+ }
73677
+ if (!child.ok || child.value.finished !== true || child.value.status !== "completed" || child.value.agent !== "coding" || child.value.parentInstanceId !== instance.id || child.value.userId !== instance.userId || child.value.repo?.toLowerCase() !== instance.repo.toLowerCase() || child.value.threadKey !== (row2.threadKey ?? instance.threadKey) || (row2.recovery !== void 0 ? child.value.idempotencyKey?.startsWith(`${instance.id}:${row2.unit}/recovery/`) !== true || !/\/findings(?:\/a[1-9][0-9]*)?$/.test(child.value.idempotencyKey ?? "") : !isStepAttempt(child.value.idempotencyKey, findingsPrefix)) || !fullHead(facts2.headSha) || child.value.headSha !== facts2.headSha || child.value.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) !== true || facts2.state !== "open" || facts2.sameRepoHead !== true || facts2.headBranchExists !== true || facts2.headRef !== row2.publication.headRef || facts2.baseRef !== row2.publication.baseRef || facts2.verifiedHead?.repo.toLowerCase() !== instance.repo.toLowerCase() || facts2.verifiedHead.ref !== row2.publication.publicationRef || facts2.verifiedHead.sha !== facts2.headSha || row2.publication.repo.toLowerCase() !== instance.repo.toLowerCase() || row2.publication.pr !== row2.pr?.number || row2.publication.baseRef !== instance.base || row2.publication.headRef !== row2.branch || row2.publication.publicationRef !== row2.branch || !samePublicationOwner(row2.publication.owner, owner))
73659
73678
  throw new PublicationBindingRefusal("publication_facts_mismatch");
73660
73679
  try {
73661
- if (deps.runnerOwnership?.claim(instance.repo, row2.publication.pr, owner) !== true)
73680
+ if (row2.recovery !== void 0 && deps.runnerOwnership?.claim(instance.repo, row2.publication.pr, owner) !== true)
73681
+ throw new PublicationBindingRefusal("publication_ownership_changed");
73682
+ if (!samePublicationOwner(deps.runnerOwnership?.owner(instance.repo, row2.publication.pr), owner))
73662
73683
  throw new PublicationBindingRefusal("publication_ownership_changed");
73663
73684
  } catch (err2) {
73664
73685
  if (err2 instanceof PublicationBindingRefusal) throw err2;
@@ -73668,7 +73689,7 @@ async function advanceRecoveryPublication(deps, instance, row2, facts2, runId3)
73668
73689
  ...row2,
73669
73690
  lastPush: facts2.headSha,
73670
73691
  publication: { ...row2.publication, expectedHeadSha: facts2.headSha },
73671
- recovery: { ...row2.recovery, expectedHeadSha: facts2.headSha }
73692
+ ...row2.recovery !== void 0 ? { recovery: { ...row2.recovery, previousBinding: void 0, expectedHeadSha: facts2.headSha } } : {}
73672
73693
  };
73673
73694
  let replaced;
73674
73695
  try {
@@ -73682,6 +73703,16 @@ async function advanceRecoveryPublication(deps, instance, row2, facts2, runId3)
73682
73703
  throw new PublicationBindingRefusal(
73683
73704
  replaced?.reason === "stale" ? "publication_binding_stale" : "publication_store_unavailable"
73684
73705
  );
73706
+ let stillOwned = false;
73707
+ try {
73708
+ stillOwned = samePublicationOwner(deps.runnerOwnership?.owner(instance.repo, row2.publication.pr), owner);
73709
+ } catch {
73710
+ }
73711
+ if (!stillOwned) {
73712
+ const restored = await deps.instances.compareAndReplaceUnit(updated, row2).catch(() => void 0);
73713
+ if (restored?.ok !== true) throw new PublicationBindingRefusal("publication_rollback_failed");
73714
+ throw new PublicationBindingRefusal("publication_ownership_changed");
73715
+ }
73685
73716
  return updated;
73686
73717
  }
73687
73718
  async function bindOpenPullRequest(deps, instance, row2, pr, facts2) {
@@ -73773,11 +73804,11 @@ async function recoverOriginalUnit(body, deps, caller) {
73773
73804
  if (row2.idle !== void 0 && row2.ending !== void 0)
73774
73805
  return json3(409, { ok: false, error: "unit_lifecycle_ambiguous", at });
73775
73806
  const originalOwner = { instanceId: instance.id, unit: row2.unit };
73776
- const publication = row2.publication;
73807
+ let publication = row2.publication;
73777
73808
  const pr = row2.pr;
73778
73809
  const base = instance.base;
73779
73810
  let expectedHead = row2.lastPush ?? publication?.expectedHeadSha;
73780
- if (pr === void 0 || base === void 0 || !fullHead(expectedHead) || publication === void 0 || publication.repo.toLowerCase() !== instance.repo.toLowerCase() || publication.pr !== pr.number || publication.headRef !== row2.branch || publication.baseRef !== base || publication.publicationRef !== row2.branch || !fullHead(publication.expectedHeadSha) || !samePublicationOwner(publication.owner, originalOwner))
73811
+ if (pr === void 0 || base === void 0 || expectedHead !== void 0 && !fullHead(expectedHead) || publication !== void 0 && (publication.repo.toLowerCase() !== instance.repo.toLowerCase() || publication.pr !== pr.number || publication.headRef !== row2.branch || publication.baseRef !== base || publication.publicationRef !== row2.branch || !fullHead(publication.expectedHeadSha) || !samePublicationOwner(publication.owner, originalOwner)))
73781
73812
  return json3(409, { ok: false, error: "recovery_binding_mismatch", at });
73782
73813
  const existingClaim = row2.recovery;
73783
73814
  let kind;
@@ -73808,10 +73839,14 @@ async function recoverOriginalUnit(body, deps, caller) {
73808
73839
  findingsRunId,
73809
73840
  findingsKey
73810
73841
  } = existingClaim);
73811
- if (existingClaim.expectedHeadSha !== expectedHead || publication.expectedHeadSha !== expectedHead)
73842
+ if (publication === void 0 || existingClaim.expectedHeadSha !== expectedHead || publication.expectedHeadSha !== expectedHead)
73812
73843
  return json3(409, { ok: false, error: "recovery_binding_mismatch", at });
73813
73844
  const { recovery: _recovery, ...withoutRecovery } = row2;
73814
73845
  originalRow = { ...withoutRecovery, ending: existingClaim.previousEnding };
73846
+ if (existingClaim.previousBinding !== void 0) {
73847
+ const { publication: _publication, lastPush: _lastPush, ...prior } = originalRow;
73848
+ originalRow = { ...prior, ...existingClaim.previousBinding };
73849
+ }
73815
73850
  } else {
73816
73851
  if (row2.idle !== void 0 || row2.ending === void 0)
73817
73852
  return json3(409, { ok: false, error: "unit_not_terminal", at });
@@ -73831,30 +73866,35 @@ async function recoverOriginalUnit(body, deps, caller) {
73831
73866
  return json3(409, { ok: false, error: "recovery_ending_mismatch", at });
73832
73867
  round2 = boundary.index;
73833
73868
  const caps = instance.caps;
73834
- if (caps === void 0 || row2.startedAt === void 0 || !Number.isFinite(row2.ending.at))
73835
- return json3(409, { ok: false, error: "recovery_budget_unknown", at });
73836
- if (instance.grant?.costCapUsd !== void 0) return json3(409, { ok: false, error: "recovery_budget_unknown", at });
73869
+ const unknownBudget = (reason2) => json3(409, { ok: false, error: "recovery_budget_unknown", reason: reason2, at });
73870
+ if (caps === void 0) return unknownBudget("caps_missing");
73871
+ if (!Number.isSafeInteger(caps.maxRounds) || caps.maxRounds < 1 || !Number.isFinite(caps.maxMinutes) || caps.maxMinutes <= 0)
73872
+ return unknownBudget("caps_invalid");
73873
+ if (row2.startedAt === void 0) return unknownBudget("started_at_missing");
73874
+ if (!Number.isFinite(row2.startedAt)) return unknownBudget("started_at_invalid");
73875
+ if (!Number.isFinite(row2.ending.at)) return unknownBudget("ending_at_invalid");
73876
+ if (instance.grant?.costCapUsd !== void 0) return unknownBudget("cost_cap_spend_unknown");
73837
73877
  if (round2 >= caps.maxRounds) return json3(409, { ok: false, error: "recovery_rounds_exhausted", at });
73838
73878
  const latestSegmentIndex = row2.segments?.reduce((latest, candidate) => Math.max(latest, candidate.index), 1) ?? 1;
73839
73879
  const latestSegments = row2.segments?.filter((candidate) => candidate.index === latestSegmentIndex) ?? [];
73840
- if (latestSegments.length > 1) return json3(409, { ok: false, error: "recovery_budget_unknown", at });
73880
+ if (latestSegments.length > 1) return unknownBudget("latest_segment_ambiguous");
73841
73881
  const segment = latestSegments[0];
73842
73882
  const leaseStartedAt = segment?.at ?? row2.startedAt;
73843
73883
  const resumedLeases = Object.values(row2.wakes ?? {}).filter(
73844
73884
  (answer) => answer.kind === "segment" && answer.index === latestSegmentIndex && answer.leaseMs !== void 0
73845
73885
  );
73846
- if (resumedLeases.length > 0) return json3(409, { ok: false, error: "recovery_budget_unknown", at });
73886
+ if (resumedLeases.length > 0) return unknownBudget("resume_time_missing");
73847
73887
  remainingMs = minutesToMs(caps.maxMinutes) - (at - leaseStartedAt);
73848
73888
  const reviewThreadKey = row2.reviewThread?.threadKey ?? row2.threadKey ?? instance.threadKey;
73849
73889
  const unitThreadKey = row2.threadKey ?? instance.threadKey;
73850
73890
  const listing = await deps.runs.listRuns({
73851
- status: "finished",
73891
+ status: publication === void 0 ? "all" : "finished",
73852
73892
  visibleTo: EVERY_RUN,
73853
73893
  threadKey: reviewThreadKey,
73854
73894
  limit: RUN_LIST_MAX_LIMIT
73855
73895
  });
73856
73896
  const unitListing = unitThreadKey === reviewThreadKey ? listing : await deps.runs.listRuns({
73857
- status: "finished",
73897
+ status: publication === void 0 ? "all" : "finished",
73858
73898
  visibleTo: EVERY_RUN,
73859
73899
  threadKey: unitThreadKey,
73860
73900
  limit: RUN_LIST_MAX_LIMIT
@@ -73863,18 +73903,51 @@ async function recoverOriginalUnit(body, deps, caller) {
73863
73903
  row2.unit,
73864
73904
  latestSegmentIndex > 1 ? { segment: latestSegmentIndex, renewalsSpent: 0, spendUsd: null } : void 0
73865
73905
  );
73866
- const reviewedHead = publication.expectedHeadSha;
73906
+ if (listing.storeUnavailable || unitListing.storeUnavailable || listing.nextBefore !== void 0 || unitListing.nextBefore !== void 0)
73907
+ return json3(409, { ok: false, error: "recovery_evidence_incomplete", at });
73867
73908
  const reviewKeyPrefix = `${instance.id}:${segmentPrefix}/${round2}/review`;
73868
73909
  const candidates2 = listing.runs.filter((run2) => {
73869
- if (!run2.finished || run2.agent !== "review" || run2.parentInstanceId !== instance.id || run2.idempotencyKey !== reviewKeyPrefix && run2.idempotencyKey?.startsWith(`${reviewKeyPrefix}/a`) !== true || run2.userId !== instance.userId || run2.repo?.toLowerCase() !== instance.repo.toLowerCase() || run2.threadKey !== reviewThreadKey)
73910
+ if (!run2.finished || run2.agent !== "review" || run2.parentInstanceId !== instance.id || !isStepAttempt(run2.idempotencyKey, reviewKeyPrefix) || run2.userId !== instance.userId || run2.repo?.toLowerCase() !== instance.repo.toLowerCase() || run2.threadKey !== reviewThreadKey)
73870
73911
  return false;
73871
- if (run2.reviewHead !== reviewedHead) return false;
73912
+ if (!fullHead(run2.reviewHead) || publication !== void 0 && run2.reviewHead !== publication.expectedHeadSha)
73913
+ return false;
73914
+ const reviewedHead2 = run2.reviewHead;
73872
73915
  if (kind === "review") return run2.verdict === void 0 && run2.reviewPost === void 0;
73873
- return run2.verdict?.verdict === "request_changes" && run2.reviewHead === reviewedHead && run2.reviewPost?.posted === true && run2.reviewPost.head === reviewedHead && run2.reviewPost.verdict === "request_changes" && run2.reviewPost.target.repo.toLowerCase() === instance.repo.toLowerCase() && run2.reviewPost.target.number === pr.number;
73916
+ return run2.verdict?.verdict === "request_changes" && run2.reviewHead === reviewedHead2 && run2.reviewPost?.posted === true && run2.reviewPost.head === reviewedHead2 && run2.reviewPost.verdict === "request_changes" && run2.reviewPost.target.repo.toLowerCase() === instance.repo.toLowerCase() && run2.reviewPost.target.number === pr.number;
73874
73917
  });
73875
73918
  if (candidates2.length !== 1) return json3(409, { ok: false, error: "recovery_review_evidence_ambiguous", at });
73876
- reviewRunId = candidates2[0].id;
73877
- reviewKey = candidates2[0].idempotencyKey;
73919
+ const review = candidates2[0];
73920
+ const reviewedHead = review.reviewHead;
73921
+ if (publication === void 0) {
73922
+ if (listing.runs.filter(
73923
+ (run2) => run2.parentInstanceId === instance.id && isStepAttempt(run2.idempotencyKey, reviewKeyPrefix)
73924
+ ).length !== 1)
73925
+ return json3(409, { ok: false, error: "recovery_review_evidence_ambiguous", at });
73926
+ if ([...listing.runs, ...unitListing.runs].some(
73927
+ (run2) => !run2.finished && (run2.agent === "coding" || run2.agent === "review")
73928
+ ))
73929
+ return json3(409, { ok: false, error: "recovery_child_active", at });
73930
+ const codingKey = `${instance.id}:${segmentPrefix}/${round2 === 1 ? "0/coding" : `${round2 - 1}/findings`}`;
73931
+ const coding = unitListing.runs.filter(
73932
+ (run2) => run2.parentInstanceId === instance.id && isStepAttempt(run2.idempotencyKey, codingKey)
73933
+ );
73934
+ const child = coding.length === 1 ? coding[0] : void 0;
73935
+ if (child?.finished !== true || child.status !== "completed" || child.agent !== "coding" || child.userId !== instance.userId || child.repo?.toLowerCase() !== instance.repo.toLowerCase() || child.threadKey !== unitThreadKey || child.headSha !== reviewedHead || child.pushed?.some((push) => push.ref === row2.branch && push.sha === reviewedHead) !== true || child.pr !== void 0 && (child.pr.number !== pr.number || child.pr.head !== void 0 && child.pr.head !== row2.branch) || child.finishedAt === void 0 || child.startedAt < leaseStartedAt || child.finishedAt > review.startedAt || review.finishedAt === void 0 || review.finishedAt > row2.ending.at || expectedHead !== void 0 && expectedHead !== reviewedHead)
73936
+ return json3(409, { ok: false, error: "recovery_binding_evidence_ambiguous", at });
73937
+ publication = {
73938
+ repo: instance.repo,
73939
+ pr: pr.number,
73940
+ headRef: row2.branch,
73941
+ baseRef: base,
73942
+ expectedHeadSha: reviewedHead,
73943
+ publicationRef: row2.branch,
73944
+ owner: originalOwner
73945
+ };
73946
+ expectedHead = reviewedHead;
73947
+ claimRow = { ...row2, lastPush: reviewedHead, publication };
73948
+ }
73949
+ reviewRunId = review.id;
73950
+ reviewKey = review.idempotencyKey;
73878
73951
  try {
73879
73952
  facts2 = await deps.fetchPrFacts({ repo: instance.repo, number: pr.number });
73880
73953
  } catch (err2) {
@@ -73888,7 +73961,7 @@ async function recoverOriginalUnit(body, deps, caller) {
73888
73961
  const findingsPrefix = `${instance.id}:${segmentPrefix}/${boundary.index}/findings`;
73889
73962
  const headMoved = facts2.headSha !== reviewedHead || expectedHead !== reviewedHead;
73890
73963
  const completedFindings = unitListing.runs.filter(
73891
- (run2) => boundary.outcome === "request_changes" && row2.ending?.cause === "step_threw" && row2.ending.step === findingsStep && row2.ending.round === boundary.index && (expectedHead === reviewedHead || expectedHead === facts2.headSha) && run2.finished && run2.status === "completed" && run2.agent === "coding" && run2.parentInstanceId === instance.id && run2.userId === instance.userId && run2.repo?.toLowerCase() === instance.repo.toLowerCase() && run2.threadKey === unitThreadKey && (run2.idempotencyKey === findingsPrefix || run2.idempotencyKey?.startsWith(`${findingsPrefix}/a`) === true) && run2.headSha === facts2.headSha && (!headMoved || run2.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) === true) && run2.startedAt >= (candidates2[0].finishedAt ?? candidates2[0].startedAt) && (run2.finishedAt === void 0 || run2.finishedAt <= row2.ending.at)
73964
+ (run2) => boundary.outcome === "request_changes" && row2.ending?.cause === "step_threw" && row2.ending.step === findingsStep && row2.ending.round === boundary.index && (expectedHead === reviewedHead || expectedHead === facts2.headSha) && run2.finished && run2.status === "completed" && run2.agent === "coding" && run2.parentInstanceId === instance.id && run2.userId === instance.userId && run2.repo?.toLowerCase() === instance.repo.toLowerCase() && run2.threadKey === unitThreadKey && isStepAttempt(run2.idempotencyKey, findingsPrefix) && run2.headSha === facts2.headSha && (!headMoved || run2.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) === true) && run2.startedAt >= (candidates2[0].finishedAt ?? candidates2[0].startedAt) && (run2.finishedAt === void 0 || run2.finishedAt <= row2.ending.at)
73892
73965
  );
73893
73966
  if (completedFindings.length > 1 || headMoved && completedFindings.length !== 1)
73894
73967
  return json3(409, { ok: false, error: "recovery_head_moved", at });
@@ -73899,7 +73972,7 @@ async function recoverOriginalUnit(body, deps, caller) {
73899
73972
  kind = "review";
73900
73973
  round2 = boundary.index + 1;
73901
73974
  claimRow = {
73902
- ...row2,
73975
+ ...claimRow,
73903
73976
  lastPush: expectedHead,
73904
73977
  publication: { ...publication, expectedHeadSha: expectedHead }
73905
73978
  };
@@ -74006,6 +74079,12 @@ async function recoverOriginalUnit(body, deps, caller) {
74006
74079
  ...findingsKey !== void 0 ? { findingsKey } : {},
74007
74080
  ...findings !== void 0 ? { findings } : {},
74008
74081
  previousEnding: row2.ending,
74082
+ ...claimRow !== row2 ? {
74083
+ previousBinding: {
74084
+ ...row2.publication !== void 0 ? { publication: row2.publication } : {},
74085
+ ...row2.lastPush !== void 0 ? { lastPush: row2.lastPush } : {}
74086
+ }
74087
+ } : {},
74009
74088
  workflowId,
74010
74089
  deadlineAt,
74011
74090
  reviewKey
@@ -74090,6 +74169,8 @@ async function prCheck(body, deps) {
74090
74169
  };
74091
74170
  try {
74092
74171
  if (follow !== void 0) {
74172
+ if (unit.row?.pr !== void 0 && unit.row.pr.number !== follow)
74173
+ throw new PublicationBindingRefusal("publication_facts_mismatch");
74093
74174
  const facts2 = await deps.fetchPrFacts({ repo: instance.repo, number: follow });
74094
74175
  if (facts2 === void 0) throw new Error(`could not read ${instance.repo}#${follow}`);
74095
74176
  const fallbackUrl = unit.row?.pr?.url ?? `https://github.com/${instance.repo}/pull/${follow}`;
@@ -74100,7 +74181,7 @@ async function prCheck(body, deps) {
74100
74181
  await rememberTerminal(followedPr);
74101
74182
  return json3(200, { ok: true, ...state2, at });
74102
74183
  }
74103
- const bindingRow = unit.row !== void 0 && unit.row.recovery !== void 0 && recover !== void 0 ? await advanceRecoveryPublication(deps, instance, unit.row, facts2, recover.runId) : unit.row;
74184
+ const bindingRow = unit.row !== void 0 && recover !== void 0 ? await advanceFindingsPublication(deps, instance, unit.row, facts2, recover.runId) : unit.row;
74104
74185
  await bindOpenPullRequest(deps, instance, bindingRow, followedPr, facts2);
74105
74186
  const prRef = { repo: instance.repo, number: follow };
74106
74187
  const headSha = facts2.headSha;
@@ -74505,7 +74586,10 @@ async function round(body, deps) {
74505
74586
  return json3(200, { ok: true, at: previous.at });
74506
74587
  const updated = {
74507
74588
  ...row2,
74508
- rounds: [...row2.rounds, note]
74589
+ rounds: [...row2.rounds, note],
74590
+ // The transport has started doing work. Admission rollback must no longer
74591
+ // remove publication authority already used by this recovery.
74592
+ ...row2.recovery?.previousBinding !== void 0 ? { recovery: { ...row2.recovery, previousBinding: void 0 } } : {}
74509
74593
  };
74510
74594
  if (row2.recovery !== void 0) {
74511
74595
  let replaced;
@@ -75531,7 +75615,7 @@ function createAdminCoordinatorHandler(deps) {
75531
75615
  })();
75532
75616
  };
75533
75617
  }
75534
- var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json3, describe12, isGenerated2, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, githubSecondStart, CHILD_SUPERSESSION_REASONS, PublicationBindingRefusal, samePublicationOwner, fullHead, ROUND_OUTCOMES, ENDING_ICON, STEPS;
75618
+ var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json3, describe12, isGenerated2, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, githubSecondStart, CHILD_SUPERSESSION_REASONS, PublicationBindingRefusal, samePublicationOwner, fullHead, isStepAttempt, ROUND_OUTCOMES, ENDING_ICON, STEPS;
75535
75619
  var init_adminCoordinator = __esm({
75536
75620
  "../../src/channels/adminCoordinator.ts"() {
75537
75621
  "use strict";
@@ -75596,6 +75680,7 @@ var init_adminCoordinator = __esm({
75596
75680
  };
75597
75681
  samePublicationOwner = (left, right) => left?.instanceId === right.instanceId && left.unit === right.unit;
75598
75682
  fullHead = (value) => typeof value === "string" && /^[0-9a-f]{40}$/i.test(value);
75683
+ isStepAttempt = (key, step) => key === step || key?.startsWith(`${step}/a`) === true && /^[1-9][0-9]*$/.test(key.slice(step.length + 2));
75599
75684
  ROUND_OUTCOMES = [
75600
75685
  "started",
75601
75686
  "pr_opened",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreplane/switchboard",
3
- "version": "1.262.1",
3
+ "version": "1.262.2",
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",