You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
wait --timeout defaults to 540s, not 600s. Claude Code caps a single Bash call at 600s, so the previous default raced its own tool call and would have been killed just before returning — measured against real harness ceilings after Codex reported a 30s initial yield and a 300s continuation-poll cap in its own shell interface (confirmed independently in an interactive Codex session and in a headless codex exec worker). Both facts are now in SKILL.md: an empty return with the session still alive is not a result, keep polling the same session, and never start a second wait.
Cut the event payloads down to the delta. Every event carried backend, repo, model, effort, owner — all chosen by the caller at spawn and keyed by the task name it is already holding — and wait re-sent the entire fleet on every return. Roughly half of every row, and ~80% of a wait return, was context the reader already had. Events now carry task, state, age_s, plus last_activity only where it is the thing that makes the event judgeable (dropped on done, where it is a constant, and next to an escalated question, where the question says more). armed is a name→state map, heartbeat and a wait timeout are one string per task (api-tests working 14m quiet 5m) instead of full rows, and wait returns the fleet only on idle, where "nothing is running" is the cue to go collect. Measured on a three-task fleet: change 59→22 tokens, armed 197→40, heartbeat 235→31, wait return 341→50. Full detail stays one status/list call away.
Derive the owner from CLAUDE_CODE_SESSION_ID as well, so the export CDX_OWNER=… that had to be repeated in every Bash call disappears on both supported harnesses — with it goes the failure mode where forgetting it once silently splits a session's fleet in two. SKILL.md's setup block drops to three lines and its owner paragraph from five sentences to two.
Make the stall watchdog report instead of kill. It was the one timeout in the system that actually aborted a worker: 5 minutes without byte growth meant SIGINT/SIGKILL, state stalled, and a send "continue" to get going again. It cannot distinguish a hung worker from one sitting inside an eight-minute test run, so it killed real work — and its original justification, that a hang would otherwise sit unnoticed forever, is exactly what watch/wait now solve better. --stall-after (still 300s) now raises a stall_suspect flag on the still-working, still-untouched task; watch/wait emit a stall_suspect event immediately (and a recovered one if output returns), list sorts flagged tasks above quietly-progressing ones, and status reports quiet_for_s. The hard kill moves to its own --hard-kill-after (default 3600, 0 disables it), which still produces the stalled state and stays resumable exactly as before. A flag is deliberately not a new state, so the send/clean/kill guards keep working unchanged.
The supervisor patches meta mid-turn instead of writing back its in-memory copy (patch_meta re-reads, merges, then writes non-creatingly). It holds meta for the length of a turn while the CLI writes to the same file, so its old blind write could silently undo a concurrent kill or send — a narrow race before, but the watchdog now writes mid-turn on every flag change.
The supervisor's stall sampling interval adapts to the threshold (min(30s, threshold)), so a short threshold is no longer detected up to a full 30s interval late. Production thresholds are unaffected.
Add cdx wait: the pull twin of watch for harnesses that cannot turn a background process's stdout into a new turn. One finite, owner-scoped, blocking call that returns on the first fleet change, on its --timeout (default 600), or immediately when nothing is running — reason says which. It always exits 0, never touches a worker, and carries the whole fleet plus the events that ended the wait, so the caller acts on the payload and calls it again with no follow-up list. watch assumed a push path that only some harnesses have (Codex, for one, cannot restart a finished turn from stdout), and documenting it as the general workflow was wrong.
Derive the task owner from a harness session id (CODEX_THREAD_ID, namespaced as codex:<id>) when CDX_OWNER is not set, before falling back to the cwd. The cwd fallback is the one that silently splits a fleet between two chats in one checkout, and it was reachable by forgetting a single export in a single Bash call.
Add cdx watch: a long-lived, owner-scoped event stream that replaces the wait-and-re-arm dance as the way an orchestrator learns what its tasks are doing. It emits one line per task leaving working (with an escalated QUESTION: inlined), a heartbeat line every 10 minutes while anything is still working, and a line for its own poll failures — silence from it only ever means "nothing happened", never "the watcher died". It is armed once per session (Claude Code: Monitor with persistent: true; any other harness runs it as a background process and reads the same stdout) and never re-armed: the orchestrator is woken by the line, not by the process exiting. The heartbeat stops while nothing is working, so an idle session stays quiet, and it carries every uncollected result, so a lost change event still resurfaces within ten minutes.
Split liveness from collection. 0.7.0 gave result --wait a 600s bound so a lost completion notification couldn't strand the session, but that made a planned check-in look like a failure: it raised exit 6 with error: on stderr and — despite --json — nothing on stdout, next to a state table where "timeout" means stalled → send "continue". Orchestrators read it as "the subagent aborted, resume it". Session liveness is now watch's job alone, so result --wait goes back to a plain upper bound (--timeout default 600 → 3600) and its expiry returns exit 10, "still working", with a real JSON payload (reason: timeout, waited_s) and the task untouched. Exit code 6 is no longer emitted by anything.
result on a working task without --wait also emits its JSON payload now (reason: no_wait) instead of printing nothing to stdout and a bare line to stderr, closing the same --json contract hole.
Stamp killed before signalling the process in cdx kill, not after. derive_state treats killed as sticky, so the old order left a window where a concurrent reader saw a dead process against still-working meta and derived — or persisted — a spurious failed. Harmless while readers only ran on demand; watch polls on a timer and lands in that window regularly.
--json output flushes per line, so a consumer reading watch through a pipe gets each event immediately instead of when an 8K block fills.
list (and watch) skip a task directory whose meta.json has already been removed, instead of deriving a phantom state from empty meta and reporting a cleaned task as a fresh failure.