Skip to content

readinessReconcile wedges ~5min after live start: lastCompletedAtMs frozen while heartbeat ticks, dispatch never resumes #351

Description

@khaliqgant

readinessReconcile wedges about five minutes after live start, and dispatch never resumes. This is a different fault from the registration outage fixed in #343 — that one is genuinely closed.

Evidence

Two authenticated /evidence reads of the live container, 15 minutes apart:

field 20:36 read 20:51 read
heartbeat.updatedAt 20:36:12.764Z 20:51:28.069Z
readinessReconcile.state healthy stalled
readinessReconcile.lastStartedAtMs 1787516980583 1787516980583 (unchanged)
readinessReconcile.lastCompletedAtMs 1787516920577 1787516920577 (unchanged)
readinessReconcile.consecutiveFailures 0 0

Decoded:

  • last completed cycle: 20:28:40Z
  • last started cycle: 20:29:40Z
  • heartbeat still updating at: 20:51:28Z

The daemon started at 20:24:05Z, completed exactly one reconcile cycle at 20:28:40Z (lastDurationMs: 1424), started the next at 20:29:40Z, and has not finished it in 22 minutes.

consecutiveFailures: 0 is the important part: it is not erroring and retrying, it is blocked inside a call. A failure counter cannot see a hang, and the heartbeat timer keeps ticking straight through it — so every surface except lastCompletedAtMs reports healthy.

Why this matters beyond the immediate stall

The rest of the system looks perfect: fleetControlPlane: closed, eventListener: subscribed, fleet agent factory-cloud-b712dab5 online in presence, phase: running. Those are all real — the registration fix worked. But dispatch is still dead, and only one field in the entire evidence payload says so.

Canary #350 was filed at 20:38Z matching the deployed gate exactly (requireLabel: factory, requireTitlePrefix: [factory], factory routed). Twelve-plus reconcile intervals later it has zero comments and no label change. That is consistent with the reconcile loop never running again.

Deliverable

Find the unbounded call inside the readinessReconcile cycle and bound it.

The likely shape is an await on a fleet/presence/roster/relayfile read with no per-call timeout. Note that a deadline checked between awaits does not bound a call that never returns — the bound has to be on the call itself.

Required:

  1. Per-call timeouts inside the reconcile cycle, not a loop-level deadline.
  2. A stall must be loud. A cycle that exceeds its budget should fail, increment a counter, and surface a reason — stalled with consecutiveFailures: 0 and no lastError gives an operator nothing to act on. Whatever it was waiting on should be named.
  3. Self-healing: the loop must start the next cycle rather than staying wedged until reboot. Restart-on-failure never fires for a hang, which is why this survived 22 minutes.
  4. Test: a reconcile whose dependency never resolves ⇒ the cycle aborts, the counter increments, and the next cycle still starts. Plus a control proving the test would notice if the abort were removed.

Diagnosis first

Before changing anything, establish which call is hanging. A fix aimed at the wrong await will look correct and change nothing. lastDurationMs: 1424 for the one cycle that did complete is a useful baseline — normal is ~1.4s, so whatever is blocking is pathological, not slow.

Also worth checking as a possible input: agent presence currently returns 2281 agents (~261KB). If reconcile reads full presence per cycle, payload growth is a plausible trigger for a call that used to return.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions