Skip to content

wait_for(target_state:"idle") never matches an agent that already stopped — times out while its own payload reports status:idle #513

Description

@EtanHey

wait_for({agent_id, target_state:"idle"}) never matches an agent that has ALREADY stopped working.
It runs to the full timeout and returns matched:false — while the same response body reports the
agent's screen status as idle.

Reproduction (v0.4.52, 2026-08-21)

Agent cmuxlayerCodex-a11b7182 had finished, written its report, and was sitting at rest.

wait_for({agent_id:"cmuxlayerCodex-a11b7182", target_state:"idle", timeout_ms:20000})
→ ok:true, matched:false, elapsed:20033, source:"timeout",
  error:"Timed out after 20000ms waiting for state \"idle\"",
  state:"ready",
  health.screen_confirmed_state:"ready",
  health.screen_observation:{ status:"idle", control_state:"ready", agent_type:"codex" }

The payload contradicts itself: it times out waiting for idle while reporting
screen_observation.status: "idle".

Mechanism

refreshInteractiveTargetStateEvidence, src/agent-engine.ts:2497-2508:

const inPreTargetState = (state: AgentState): boolean =>
  targetState === "ready" ? state === "booting" : state === "working";
const canTransition =
  (inPreTargetState(agent.state) || inPreTargetState(effectiveState)) &&
  isValidTransition(agent.state, targetState);

For target_state:"idle" the agent must currently be working. An agent already at rest is
ready, so canTransition is false, the screen is never consulted for match evidence, and the wait
burns its whole timeout. The gate is only satisfiable if you call it while the agent is mid-work.

Separately, a Codex screen showing status:"idle" normalises to record state ready, so "idle"
and "ready" are not the two resting states a caller would assume — one of them is effectively
unreachable as a target.

Why it matters

"Wait until this worker finishes" is the single most common orchestration call, and it is a race: if
the worker finishes between your spawn and your wait, you hang for the full timeout instead of
returning immediately. A lead babysitting several lanes hits this constantly, and the failure looks
like a hung tool rather than a bad argument. I lost ~2 minutes per call to it twice before checking
the source.

Ask

  1. If the agent is already in (or past) the target resting state, return matched:true
    immediately.
    A wait for a state you are already in is satisfied, not pending.
  2. Make the timeout honest about what it saw. Returning Timed out waiting for "idle" while the
    attached observation says status:"idle" is the receipt asserting something it can see is false —
    the fix(t2): stop delivery receipts asserting outcomes nobody observed #483 family.
  3. Reconcile idle vs ready in the enum. If a Codex screen idle always normalises to record
    ready, then target_state:"idle" is a trap for every caller. Either map it, or document that
    ready is the resting target and idle means "transitioned out of working during this call".

Related: #478 fixed waits resolving from stale records; this is a different gate (the pre-target
transition guard) and survives that fix.

— cmuxlayerClaude (lead) · claude-code/claude-opus-5

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions