rotate-agent

Quota-triggered failover from Claude Code, Codex, Cursor, Hermes or OpenClaw. When armed, it reads the sender quota and tries other-vendor models in place, then receiving agents in the configured orde

SKILL.md

GitHub

rotate-agent

Needs: tmux, jq, GNU timeout, and sno with Reach, heartbeat and subscription-quota-check, plus the handoff skill. The provider-reading script also uses awk and OpenClaw's session exporter. Install programs through sno setup. A seat is a registered terminal address (see join-talk). The owner owns the work; the watchman reads ticks and acts on them, preferably from a seat other than the busy sender.

Rotation keeps one authorized long run working when its model's quota is low. Agent ids are claude-code, codex, cursor, hermes, openclaw. Selection belongs to sno subscription-quota-check --pick handoff: other-vendor models inside the sender first, then other agents in handoff_order. Never replace the returned order with your own. A Cursor quota reading belongs to one conversation, so --from cursor needs --sender <A-seat> on the preflight and --seat <A-seat> on the watch; without it the reading is unreadable and the watch holds. A Cursor receiver is always a new Cursor CLI seat on a tmux window (sno reach spawn cursor-agent --window). The trigger is the threshold read only; a quota error message from Cursor does not start a rotation.

Quota normally is read only on request or refusal. Asking to arm rotation authorizes these periodic sender reads. Only the sender is read on ordinary ticks; the full pick runs once at a crossing. No other process should poll quota.

Three seats

seat who does
working agent (A) the agent executing the task registers; checkpoints after each task; hands off when ordered
watchman an agent with background execution and delivery of its results arms, reads ticks, walks candidates
receiver (B) an agent chosen by the pick starts on a tmux window; takes the same checkout and authority

Working agent: three lines added to its prompt

$join-talk
After every completed task run `sno handoff-checkpoint <record>` outside the checkout, then update Done and Next. Commit only if the owner authorized it.
When told to ROTATE, pause edits and run $handoff with the specified receiver kind, model and provider.

$join-talk and $handoff are the Codex form; in Claude Code and Cursor write /join-talk and /handoff.

Checkpoints limit lost progress when A cannot answer. Registration lets the watchman reach A. A seat marked stale is not proof that its agent is gone: check its terminal or process before treating it as dead.

Watchman: arm, read, act

The scripts ship under this skill's scripts/; use their full paths or put them on PATH. A next-agent candidate can be tested before arming:

rotate-agent-preflight --from codex --to claude-code --cwd <checkout> --sender <A-seat> --model <M> --provider <P>

It checks tools, checkout, sender registration, the two selected model quotas, and a trial receiver's fresh answer. The trial seat is removed. unread quota is available; actual start and answer decide whether it works. Do not arm on a FAIL: repair the named failure or try the next candidate. The preflight is not needed to read a tick.

Create a fresh empty log with mktemp "${TMPDIR:-/tmp}/heartbeat.XXXXXX" and a new state path under ${XDG_STATE_HOME:-$HOME/.local/state}/rotate-agent/<label>.state. Arming with Claude Code's tools (other harnesses use their own equivalents):

Bash({
  run_in_background: true,
  command: "sno heartbeat --interval 5 --max-hours 0 --label rotate-agent-<work> --log \"<LOG>\" \
    -- rotate-agent-watch --from <agent> --seat <A-seat> --threshold-pct 2 --state \"<STATE>\" --cwd \"<checkout>\""
})
Monitor({
  command: "tail --pid=<from the arm output> -F -n +1 \"<LOG>\"",
  persistent: true,
  description: "rotate-agent ticks"
})

Cursor has no Monitor: run the same sno heartbeat command from Cursor's shell and follow the reader instruction it prints for a Cursor conversation (the heartbeat skill says how ticks reach Cursor).

--max-hours 0 removes heartbeat's default 24-hour limit; use a finite value when requested. Hermes and OpenClaw instead run one background command per tick: sleep <interval-seconds>; sno heartbeat --label rotate-agent-<work> --max-ticks 1 -- rotate-agent-watch --from <agent> --seat <A-seat> --state <STATE> --cwd <checkout>. Re-arm after each native completion notice. Hermes delivers the notice at the next turn boundary; OpenClaw delivers during a turn. OpenClaw's command must run without an approval prompt for its completion notice to arrive. Prefer a separate watchman.

line meaning watchman does
HOLD from=… remaining=N% threshold=T% above threshold keep watching
HOLD unreadable … sender quota unknown keep watching; report repeated failures
HOLD no-candidate … no available receiver sender continues; keep watching
ROTATE … verdict=… candidates=[…] low remaining share, or wait/owner_action walk the candidates below
DONE rotated … a real handoff was recorded wait for COMPLETE
COMPLETE … new commit after the recorded handoff stop this heartbeat

Walk every candidate in order

Read candidates[], not just chosen. Keep the original model and raw provider from the triggering line's sender object (--pick's from). Log each failed operation, agent, model, seat and error; then take the next candidate. go, short_only and unread candidates are available.

For in_place:true, run:

rotate-agent-switch --agent <agent> --seat <A-seat> --model <M> --provider <P> --original-model <O> --original-provider <OP>

It polls the pane footer every three seconds. The entire first attempt has a 100-second deadline; recovery starts with a fresh 150-second deadline, shortened only if needed to keep the entire operation within 280 seconds. Timed-out commands are killed immediately. Each phase prints its elapsed seconds on stderr. Hermes is idle when the last 15 pane lines contain a line starting with ❯ and no msg=interrupt, including when the composer shows a rotating suggestion; OpenClaw must show connected | idle. It sends Hermes /model M --provider P, OpenClaw /model P/M, Cursor /model M through sno reach call <seat> <command> --raw, never as a queued model command. It waits for idle again and calls continue with at most 60 seconds. Confirmation requires exit status 0 and non-empty reply text; Reach does not return JSON here. Hermes and OpenClaw use rotate-agent-answer-provider --agent <agent> --seat <A-seat> --latest --with-model to read <provider> <model> from the last successful Hermes API call or the last OpenClaw assistant answer's runtime event. Unanswered background turns do not erase that result. Without --with-model, output remains the provider only. A switch needs a new assistant answer from P and the selected current model. A logged custom provider also matches a configured custom endpoint name when the answer model is M; it never substitutes for openai, openai-codex, codex, chatgpt, anthropic, claude, google, gemini, xai, grok, openrouter, deepseek, nvidia, zai, kimi or minimax. Only SWITCH OK counts. Then append switched <UTC-timestamp> model=<M> vendor=<V> to the state file. This does not stop the watch: the next tick reads the new model, and another crossing runs the pick again. Never write rotated for an in-place switch.

Failure restores O/OP with the same idle, raw command and confirmed-turn procedure. SWITCH FAIL restored=yes means try the next candidate with the sender still able to hand off. restored=no means all next-agent candidates use resume from disk.

For a next-agent candidate, map claude-code to launch kind claude, cursor to cursor-agent; the other launch kinds match their ids. Pass --model M and the raw --provider P to sno reach spawn, always with --window. Reach initializes an OpenClaw model while idle before sending its startup context.

When A can answer, send the order:

sno reach call <A-seat> "ROTATE: run the handoff skill now; receiver kind <kind>, model <M>, provider <P>, vendor <V>; spawn --window in the same checkout. Verify the receiver answer vendor before release. Report to <owner session>." --expect HANDOFF_RELEASED --timeout 280

The handoff skill verifies readiness without writes. Hermes/OpenClaw readiness must be attributed using rotate-agent-answer-provider --agent <agent> --seat <B-seat> --receipt <REACH-RECEIPT-id> with the call's deliveryReceipt. No provider or a wrong provider fails that candidate before A releases. Claude Code and Codex have fixed vendors. Cursor uses the integrated seat's concrete model/vendor reading. An uncertain release is not permission to start competing receivers: inspect the seat before proceeding. A known start or acknowledgement failure tries the next.

If the crossing was wait/owner_action, or restoring A failed, use:

sno rotate-agent-resume --to <agent> --cwd <checkout> --checkpoint <record> --work <work> --model <M> --provider <P> --report-to <owner session>

Use a distinct work label per candidate attempt. The record must belong to the same checkout. MATCH means unchanged; DRIFT is passed to B to inspect before continuing. B reads the whole record and emits fresh RESUME_ACK without editing. The script checks its answer provider before releasing B to continue; failed acknowledgement leaves B paused and logs the failure, so the caller can try the next candidate. A missing release receipt needs inspection: B may already own the work. The last line on success is RESUMED seat=<address> verify=MATCH|DRIFT.

B prints DONE_<work> when finished, or BLOCKED_<work> <reason> when the owner is needed. It never prints DONE_<work> while any task waits on the owner. After confirmed transfer append rotated <UTC-timestamp> from=<agent> to=<agent> remaining=<N>% commits=<count> with the count printed by the triggering tick. Only this real handoff ends rotation under that state path; another handoff uses a fresh label and state path.

If every candidate fails, report HOLD no-candidate; A continues when still able. Do not make a blind read or a changed model name into success. Hermes provider comes from the last successful API call in the receipt's turn span, not a subsequent background notice; OpenClaw comes from the runtime provider of that answer in its session export. Unknown attribution is failure, and independent work continues.