/** * The one bounded client for the resident daemon's loopback wake (#878). * * Extracted from `resume` because three different successful transitions leave * queued work waiting for the next scheduled pass — a promotion to the queue * label, an unblock that restores eligibility, and `resume` itself — and each * one waiting up to five minutes reads, from outside, exactly like a stalled * queue. On 2026-08-21T17:57Z three claimable issues sat unadmitted against * `workers 0 / 3` until the 18:02:33Z pass, and the operator reasonably asked * whether a second dispatcher was needed. It was not: the resident daemon * already owns claims and capacity, and `POST /wake` already exists — the * transitions simply never used it. * * What a wake is NOT: it starts no pass of its own, claims nothing, and skips * nothing. It shortens a sleep. The pass it prompts re-reads every pause file, * drain fence, lane interlock, budget and routing rule exactly as a scheduled * pass does, so a project that is held stays held. Anything else — a shorter * interval, a second `daemon --once`, waking only from the tick — makes the * delay look smaller while leaving idle slots or breaking single-owner safety. * * Every failure is honest and non-fatal: the caller's own mutation already * committed, so a missing or unreachable daemon degrades to "the next scheduled * pass will claim" rather than an error that suggests the mutation failed. That * now includes an unreadable bearer token: `POST /wake` is authenticated (Phase * 4), and a wake nobody can authenticate is one more reason the pass waits — * never an exception thrown through a mutation that already landed. */ import { httpAuthHeader, httpTokenPath } from "./http-token.ts"; import { livingDaemon } from "./lifecycle.ts"; /** What the wake attempt did, as the line a human reads after the mutation. */ export async function wakeDispatch(projectName: string): Promise { const daemon = livingDaemon(); if (daemon === undefined) { return "daemon not running — claiming starts when the daemon next starts or ticks"; } // The daemon mints this at start, and `livingDaemon()` just said one is // running — so an absent token is a real anomaly (a different // `$OMP_CONDUCTOR_HOME`, or a hand-deleted file), not the ordinary "no daemon // has ever run here" that `missingHttpTokenMessage()` describes. Say what is // missing and where, and degrade exactly like an unreachable daemon: the // mutation stands, the pass is merely not brought forward. const auth = httpAuthHeader(); if (auth === undefined) { return ( `daemon wake failed — no readable HTTP token at ${httpTokenPath()}; ` + "the next scheduled pass will claim" ); } let response: Response; try { response = await fetch(`http://127.0.0.1:${daemon.port}/wake`, { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ project: projectName }), }); } catch { return "daemon wake failed — daemon unreachable; the next scheduled pass will claim"; } if (!response.ok) { return `daemon wake failed (HTTP ${response.status}); the next scheduled pass will claim`; } return "daemon dispatch loop woken — claiming starts on an immediate pass"; }