fix(pr-daemon): defer session creation when maestro is unreachable

When maestro goes down, the daemon only sees aoe sessions. If a PR's
session lives in maestro, route() finds no match and creates a
duplicate on aoe. Track maestro health with exponential backoff and
suppress session creation until it answers again.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Miguel Palhas
2026-09-09 15:24:03 +01:00
co-authored by Claude Opus 4.6
parent 6b3cabe76a
commit 7d95f5ceff
+18
View File
@@ -482,14 +482,26 @@ const MAESTRO_STATE: Record<string, string> = {
// working -- so it degrades to an empty list. Logged once per outage, because
// silently routing to half the sessions is exactly the failure this fixes.
let maestroWarned = false;
let maestroHealthy = true;
let maestroBackoff = 10_000;
let maestroNextRetry = 0;
const MAESTRO_BACKOFF_MAX = 300_000;
async function maestroRows(): Promise<any[]> {
if (Date.now() < maestroNextRetry) return [];
try {
const rows = JSON.parse(await maestro(["list", "--json"])).sessions ?? [];
maestroWarned = false;
maestroHealthy = true;
maestroBackoff = 10_000;
maestroNextRetry = 0;
return rows;
} catch (e) {
if (!maestroWarned) log(`maestro list failed, its sessions are invisible until it answers: ${e}`);
maestroWarned = true;
maestroHealthy = false;
maestroNextRetry = Date.now() + maestroBackoff;
maestroBackoff = Math.min(maestroBackoff * 2, MAESTRO_BACKOFF_MAX);
return [];
}
}
@@ -927,6 +939,12 @@ async function evaluate(prs: Pr[], mentioned: Set<string>, budget: { sessions: n
const skill = role === "land" ? "land" : "review-pr";
if (!session) {
// Maestro is unreachable: the session we cannot see may be the one
// that owns this branch. Defer creation until maestro answers again.
if (!maestroHealthy) {
log(`maestro unreachable, deferring ${full.key} (${role}) to avoid duplicates`);
continue;
}
// Epoch gates session creation, not hint delivery: an old PR you want
// covered gets covered by starting a session on its branch by hand.
if (preEpoch && !mentioned.has(full.key)) continue;