mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(daemon): ask the tolerant question while the clock can still fund the answer
The launcher asked the cheap question first and the patient one last. That is backwards. This path is only reached because a 3s health check timed out, so every 4s probe re-asks on a stricter budget than the one that triaged the daemon here — it can only ever agree. The one ask that could disagree ran last, by which point the clock could fund its handshake but not its answer, and a daemon that answered in 12s was read as dead and replaced along with its agents. Three changes, one idea: never make an ask you cannot afford to hear out. - The patient ask goes first, with every millisecond the answer does not need. - Cheap retries follow it, and stop once the clock cannot fund both halves. Funded to knock but not to listen is not an ask. - The adoption connect is capped. It is not a classification step — it acquires a lease preserveDaemon() re-establishes anyway — but on a daemon that accepts the socket and never completes hello it would spend the entire classification budget, leaving nothing for the probes that protect that daemon's sessions. The test double now forwards the connect budget instead of dropping it, so what the launcher was willing to wait for is observable at all.
This commit is contained in:
@@ -315,13 +315,15 @@ vi.mock('./daemon-health', () => ({
|
||||
|
||||
vi.mock('./client', () => ({
|
||||
// Mirror ensureConnected onto the bounded variant so every existing double keeps its
|
||||
// behaviour — including the ones whose whole point is that connecting throws.
|
||||
// behaviour — including the ones whose whole point is that connecting throws. The budget is
|
||||
// forwarded rather than dropped so a test can see what the launcher was willing to wait.
|
||||
DaemonClient: function DaemonClientDouble(...args: unknown[]) {
|
||||
const instance = (daemonClientMock as unknown as (...a: unknown[]) => Record<string, unknown>)(
|
||||
...args
|
||||
)
|
||||
if (instance && typeof instance === 'object' && !('ensureConnectedWithin' in instance)) {
|
||||
instance.ensureConnectedWithin = instance.ensureConnected
|
||||
instance.ensureConnectedWithin = (budgetMs?: number) =>
|
||||
(instance.ensureConnected as (ms?: number) => unknown)(budgetMs)
|
||||
}
|
||||
return instance
|
||||
}
|
||||
@@ -3195,11 +3197,11 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => {
|
||||
'/fake/token'
|
||||
)
|
||||
expect(forkMock).toHaveBeenCalled()
|
||||
// 1 adoption + 1 initial probe + the retries + 1 patient probe + 1 post-fork adoption.
|
||||
// The patient probe is the point: retrying a four-second handshake never reaches a
|
||||
// daemon that consistently needs longer, so the launcher spends what is left of the
|
||||
// clock on a single tolerant ask before it is willing to replace anything.
|
||||
expect(daemonClientMock).toHaveBeenCalledTimes(4 + WEDGED_DAEMON_GRACE_RETRIES)
|
||||
// 1 adoption + 1 patient probe + the cheap retries + 1 post-fork adoption. The patient
|
||||
// probe comes first and is the point: retrying a four-second handshake never reaches a
|
||||
// daemon that consistently needs longer, and spending the clock on the cheap asks first
|
||||
// left the tolerant one unable to fund an answer.
|
||||
expect(daemonClientMock).toHaveBeenCalledTimes(3 + WEDGED_DAEMON_GRACE_RETRIES)
|
||||
// Why: this replace path used to kill the daemon with no log, so a post-hoc
|
||||
// reader could not tell it apart from an adoption; the verdict must be recorded.
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
|
||||
Reference in New Issue
Block a user