Skip to content

Agent status records, sucoder attention, and a coordinator example - #89

Merged
ligon merged 5 commits into
mainfrom
coder/attention-status
Nov 3, 2026
Merged

ligon merged 5 commits into
mainfrom
coder/attention-status

Conversation

@ligon

@ligon ligon commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

Lets one screen show which agent sessions need a reply, instead of tabbing through a tmux per project.

  • Status records. With attention.hooks: true, SuCoder-launched Claude and Codex sessions write one small record per session (~/.sucoder/attention/<mirror>--<target>.rec, wherever the agent runs) on each turn, tool use, permission request, finished turn and session end. Off by default.
    • Claude: --settings hooks; only blocking Notification types count as waiting.
    • Codex (0.164): -c hooks.<Event>=... overrides, identical for every launch so that Codex's per-definition trust needs one review per account; the record key rides in SUCODER_ATTENTION_KEY. A Codex turn idle for a minute is reported as waiting (kind idle), as Claude reports itself. Nothing is written to ~/.codex/config.toml by SuCoder, and --dangerously-bypass-hook-trust is not used.
  • sucoder attention. Lists every session, waiting ones first, across vantage and each cluster (one read per cluster, over warm connections). Modes: --json, --watch N (dashboard that also writes a cache and runs notify_command), --cached, and --tmux (one line for status-right, no config load).
  • Untracked sessions. A running session with no record (launched before the hooks were on, or by a harness without hooks) is listed as untracked instead of vanishing: local launches from the process table, cluster jobs from squeue in the same round trip as the records.
  • Push filter. notify_kinds (permission, question, idle, other) and notify_after_minutes.
  • Relay. From the agent account, attention runs as the human through relay attention; --watch and --cache are refused there.
  • Agent-side startup fix. The startup check ran sudo -u <agent> even when already running as the agent, so every agent-side command (relay, peek, message, attention) failed with "cannot read configuration". It now checks directly in that case.
  • Coordinator example (examples/coordinator/): a v0 brief for a local Claude session that triages waiting sessions and suggests replies, a notifier for ntfy and the coordinator's pane, and setup notes.

Design note: docs/attention.org.

Testing

  • tests/test_attention.py: 120 tests, including the real hook shell for both harnesses, the listing and live-session scripts, the process scan, idle synthesis, the relay argv, and the notifier against a fake tmux.
  • CI: the test job is pinned to ubuntu-24.04, because ubuntu-latest (now 26.04) has no Python 3.9 build and the 3.9 leg failed at setup. The Codex TOML tests skip on 3.9 (no tomllib).
  • Full suite on vantage: the 49 failures (codex_state_runner, codex_state, codex_app_server, direct_remote, timer_lifecycle) also fail on main; nothing new.
  • Live on vantage: a hooked Claude session's record, readable by the human through the coder group; untracked detection of a running session; notifier delivery into a real tmux pane; Codex 0.164 accepting the eight -c hooks, one "trust all" covering every later launch, and a real record from UserPromptSubmit and SessionEnd.
  • Not yet seen live: a hooked session on Savio; Codex PermissionRequest, Stop, Interrupt and the request_user_input question from a real model turn.

🤖 Generated with Claude Code

Ethan Ligon & Sue Coder and others added 5 commits November 1, 2026 19:22
Detection, not steering, is what costs the human when several agent
sessions run at once: knowing without looking which session has stopped
at a permission prompt or is waiting for input.  `sucoder sessions`
cannot say; its pane probe tells a live agent from a bare shell, and a
waiting Claude looks the same as a working one from ps.

With `attention.hooks: true`, a SuCoder-launched Claude session gets
hooks through `claude --settings` (added to the user's own; a command
that already passes --settings is left alone).  Each hook is plain POSIX
shell that atomically writes ~/.sucoder/attention/<mirror>--<target>.rec
wherever the agent runs: a header line and the hook's JSON payload.
Notification marks the session waiting (permission prompt, or about a
minute idle at the prompt); PostToolUse records only its event name,
never the tool output.  The hook always exits 0.

`sucoder attention` reads this host's agent-account records and each
cluster's, one round trip per cluster, and lists waiting sessions first.
--watch redraws, keeps ~/.sucoder/attention.json and runs
attention.notify_command for each session that starts waiting, using
warm connections only.  --tmux prints one status-bar line from that
cache and skips configuration loading and startup checks.

Verified against Claude Code 2.1.316 on vantage: a --settings hook fired
and wrote a group-readable record.  Design, the coordinator agent and
open items: docs/attention.org.

Tests: tests/test_attention.py (43).  The full suite's 49 failures on
vantage reproduce unchanged on main (Codex runner, SSH include, /tmp
permission tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only blocking notifications mark a session waiting.  Claude tags each
Notification with notification_type (read from Claude Code 2.1.316), and
several are informational: auth_success, agent_completed, computer_use_*.
The Notification hook now carries a matcher for the blocking types, each
mapped to a kind -- permission, question or idle -- and a record from an
older Claude without the field is classified by its message, else other.

attention.notify_kinds (default all) and attention.notify_after_minutes
(default 0) bound the push volume: every finished turn becomes an idle
request a minute later.  A request is one record, notified once; the
watcher's first pass is its baseline.  The notifier also gets
SUCODER_ATTENTION_KIND.

`attention --cached` lists the watcher's last pass without connecting.
On the agent account with session_relay, `attention` and `--cached` run
as the relay's user through `relay attention`, like `sessions`; --watch
and --cache are refused there, so the caller can start no loop and pick
no file under the human's home.  The record keeps transcript_path, so a
coordinator running as the agent account can read a local session's
transcript, which peek cannot reach.

examples/coordinator/: setup, a v0 brief (triage on request, one block
per waiting session, send only what the human approves, never answer a
permission prompt) and attention-notify, which posts to ntfy and types
one sanitized line into the coordinator's tmux pane, skipping the
coordinator's own mirror.  The tmux route exists because `message`
reaches cluster sessions only.

Found on vantage and recorded, not changed: every coder-side sucoder
command stops at the startup check, because ~coder/.sucoder/config.yaml
is owned by coder.  The relay needs that fixed by the human first.

Tests: tests/test_attention.py now 70, including the notifier script
through a fake tmux.  Full suite: 2505 passed; the same 49 vantage
failures as on main, none new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…artup

A session launched before attention.hooks was on (or by an older SuCoder,
or a harness without hooks) writes no record, and `sucoder attention`
silently left it out: on vantage the coordinator could not see a Savio job
or the SuCoder session, both started before the install.

The listing now also asks what is running and shows a live session with
no record, or only an `ended` one, as `untracked`:

- this host: the process table.  A local launch is
  `sudo -u <agent> bash -c '... cd <mirror> || exit 1; umask ..; <agent>'`
  and cmdline is world-readable, so the human-side watcher sees it; the
  directory maps to a configured mirror, and a Claude --session-id gives
  the transcript path the coordinator reads;
- a cluster: `squeue --me` fused into the existing records read (one round
  trip), mapped to mirror/target as `sessions` does; RUNNING jobs only;
- a host without a scheduler: its sucoder- tmux sessions.

Untracked sorts after idle, counts on the tmux line, never notifies.  A
live query that does not answer is an error, not an absence.

Also: the startup check ran `sudo -u <agent> test -r config` even when
already running as the agent, which needs a sudoers rule the agent lacks,
so every coder-side command (relay, peek, message, attention) failed with
"cannot read configuration" for a readable file.  It now checks directly
when the current user is the agent, as _get_use_sudo_for_agent already
does for later commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
attention.hooks: true now also hooks Codex (codex-cli 0.164.0), writing
the same ~/.sucoder/attention/<key>.rec records, so `sucoder attention`,
the watcher and notify_kinds treat both harnesses alike.

- Per launch, as `-c hooks.<Event>=[...]` overrides; nothing is written
  to ~/.codex/config.toml by SuCoder.  Events: SessionStart (matcher
  excludes post-compaction), UserPromptSubmit, PostToolUse -> working;
  PermissionRequest -> waiting/permission; PreToolUse on
  request_user_input -> waiting/question; Stop, Interrupt -> idle;
  SessionEnd -> ended.
- Codex trusts a -c hook by a hash of its definition, so the hook command
  is identical for every launch and the key rides in
  SUCODER_ATTENTION_KEY (an `env` argv prefix, outermost but for the
  environment file: no extra cluster round trip).  Verified in a
  throwaway CODEX_HOME: one "trust all", then a launch with another key
  ran every hook without review.  --dangerously-bypass-hook-trust is not
  used.
- Codex has no idle notification: a Codex Stop/Interrupt a minute old is
  promoted to waiting/idle (synthesize_idle_waits), dated when the minute
  ran out, so it notifies once like Claude's idle_prompt.
- Records carry the harness in a fourth header field (older records read
  as Claude's); a truncated payload (a large PermissionRequest input) is
  salvaged for its leading fields instead of reading as unknown.
- The untracked-session scan looks past the `env` prefix.

Not yet seen live: PermissionRequest, Stop, Interrupt and the
request_user_input question from a real model turn; to verify on the
first real launch (docs/attention.org, Codex).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ubuntu-latest moved to 26.04 on 2026-10-19, and actions/setup-python
has no 3.9 build for it, so the 3.9 leg failed at setup and cancelled
the 3.11 leg. Pin the test job to ubuntu-24.04. Dropping 3.9 support is
a separate decision (pyproject still says ^3.9).

The Codex tests imported tomllib at module level, which would have
failed collection of all of test_attention.py on 3.9. Use
pytest.importorskip inside the helper, as test_remote_control does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ligon
ligon merged commit 3a68deb into main Nov 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant