Skip to content

Queue work for the agent, typed at it or dispatched beside it - #1

Merged
ShipsAfterHours merged 1 commit into
mainfrom
queue-work-for-the-agent
Aug 2, 2026
Merged

Queue work for the agent, typed at it or dispatched beside it#1
ShipsAfterHours merged 1 commit into
mainfrom
queue-work-for-the-agent

Conversation

@ShipsAfterHours

@ShipsAfterHours ShipsAfterHours commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Alt+A opens a queue of work for the hosted agent. An item is either sent — typed into the live session when it goes idle, continuing the conversation you have been having — or dispatched as its own claude -p --bg agent with none of that context, running beside you. One list, because it is one thought; the modes differ only in whether the work needs the conversation you have already had.

The hard half is when to type

A prompt typed at the wrong moment is not cosmetic. A permission dialog answers with the first character it is given; a half-written message gets the queued text spliced into the middle of it. Output quiescence cannot tell a finished turn from one waiting on a human, and screen scraping rots on every release of every agent.

Claude answers the question itself: it keeps a JSON record per session under ~/.claude/sessions/<pid>.json whose status is idle or busy. agentstate.rs reads it — adding nothing to the child's argument list and asking nobody to install a hook.

The load-bearing assumption was verified rather than assumed. Reading Claude's own status logic out of the 2.1.220 binary establishes that a permission dialog reports waiting, not idle, and a running subagent reports busy. The headline hazard cannot occur through that path. Two consequences are documented where they will be found: the vocabulary is busy/shell/idle/waiting and is published nowhere, so assume this is a version behind; and anything unreadable is Unknown, treated exactly as unsafely as busy.

The same audit found the case that does break it. Claude's inline autocomplete is not on the dialog stack the status derives from, so an Enter that accepts a completion leaves text in the composer with the record still reading idle. That cannot be fixed by classifying keystrokes — the key that submits and the key that does not are the same key. So typing sets the draft flag and only observing the agent go busy clears it: a message that was really submitted makes the agent work, and nothing else the user can press does. A draft typed and abandoned holds the queue until something is submitted, visibly. That is the safe direction to be wrong in.

Four conditions gate a send, and the fourth is the point: it is announced in the left title for three seconds first, and any keystroke at the agent withdraws it. A thing that types on your behalf must be stoppable by the reflex of starting to type.

Dispatching

Passes --permission-mode acceptEdits — the one grant of authority abeam makes to a process nobody is watching. Edits land without asking so the task can finish unattended; Bash still stops; --dangerously-skip-permissions is refused by a test asserting the whole argument vector rather than searching it. The prompt sits behind a -- fence, because it is user text and one leading dash without the fence would let a pasted paragraph choose the process's own permissions. No --worktree: creating a worktree in somebody's repository as a side effect of queueing a task is a structural change they did not ask for.

An npm-installed Claude cannot be dispatched with a multi-line prompt at all — the command line goes through cmd.exe, which cannot carry a newline in any form. Since a paste becomes one item, that is the ordinary shape of a queued task, so the refusal names the live-session mode as the route that works. A native claude.exe has no such limit. Stated as cmd.exe's constraint, which is what it is.

Keys

Alt+A was cleared against the same binary as the rest of the table: meta+a and alt+a both return zero matches, where the undeclared readline bindings that caught Alt+F do appear as text. A letter and not an F-key because it joins a set — Alt+G/Alt+E/Alt+S/Alt+A are the four workspace views. In-pane: i writes an item, a arms, d deletes, m switches mode, Enter does the selected one now. Arming is a and not space, because space pages in every pane and the F1 overlay promises it by name.

Review

Three adversarial reviewers went over the first working version. What they found after it was already green and passing:

  • Shift+Enter cleared the draft flag — it inserts a newline in Claude's composer rather than submitting, so a queued prompt would be pasted into a live draft three seconds later.
  • Eight mutations to the wiring survived the whole suite, including deleting the pty write entirely while every item still showed as sent. app.rs had no tests; it now has eight, each verified by applying its mutation and reverting.
  • Alt+Q silently suppressed the countdown — the left title was an if/else if chain, so a pending quit removed the only on-screen warning that abeam was about to type.
  • An unreadable session record answered with a neighbour's, which on a peerProtocol bump would have handed the queue a stranger's idle for a whole run.
  • Two sends in consecutive passes concatenated into one message with both items marked sent; a pending submit survived the user typing in the gap.

Dependencies

serde and serde_json, the one place the git pane's "shell out and parse it yourself" precedent does not carry: git status --porcelain=v2 is line-oriented and frozen, whereas this is JSON with string escapes, three observed entry shapes, and a peerProtocol field that exists so Claude can change the rest. A hand-rolled scanner on the path that decides whether it is safe to type at an agent is the wrong trade.

Testing

344 unit + 3 end-to-end tests pass; cargo clippy --workspace --all-targets -- -D warnings is clean.

Not covered: no live claude -p --bg was ever run — that would start an unattended agent with edit permission in this repo. The command construction is tested against a real cmd.exe (quotes, &, %VAR%, flag-shaped prompts, the fence, and a shim proving the child gets EOF rather than the console), but the first genuine dispatch is untried. Also uncovered: tick_panes' queue redraw while the pane is hidden, which needs a real one-second wait to test without flakiness.

You have three things you want next and the agent is mid-task. Until now
the choices were to interrupt it, hold them in your head, or write them
somewhere abeam cannot see. `Alt+A` is the somewhere, and it holds work
in the two shapes that word covers: an item is either *sent* — typed into
the live session when it goes idle, continuing the conversation you have
been having — or *dispatched* as its own `claude -p --bg` agent, with
none of that context, running beside you. They are one list because they
are one thought, and differ only in whether the work needs the
conversation you have already had.

Sending is the hard half, and all of it is about *when*. A prompt typed
at the wrong moment is not cosmetic: a permission dialog answers with the
first character it is given, and a half-written message gets the queued
text spliced into the middle of it. Output quiescence cannot tell a
finished turn from one waiting on a human, and screen scraping rots on
every release of every agent.

Claude answers the question itself. It keeps a JSON record per session
under `~/.claude/sessions/<pid>.json` whose `status` is `idle` or `busy`,
and `agentstate.rs` reads it — adding nothing to the child's argument
list and asking nobody to install a hook. Reading Claude's own status
logic out of the 2.1.220 binary settles the load-bearing question: a
permission dialog reports `waiting`, not `idle`, and a running subagent
reports `busy`. The nightmare above cannot happen through that path. Two
consequences are written down where they will be found: the vocabulary is
`busy`/`shell`/`idle`/`waiting` and is documented nowhere, so assume this
is a version behind; and anything abeam cannot read is `Unknown`, which
is treated exactly as unsafely as `busy`.

The same audit found the case that does break it, and it is why the draft
rule is shaped the way it is. Claude's inline autocomplete is not on the
dialog stack the status is derived from, so an `Enter` that accepts a
completion leaves the text in the composer with the record still reading
`idle`. That cannot be fixed by classifying keystrokes, because the key
that submits and the key that does not are the same key. So typing sets
the draft flag and *only observing the agent go busy* clears it: a
message that was really submitted makes the agent work, and nothing else
the user can press does. The cost is that a draft typed and abandoned
holds the queue until something is submitted, visibly. That is the safe
direction to be wrong in.

Four conditions therefore gate a send, and the last one is the point: it
is announced in the left title for three seconds first, and any keystroke
at the agent withdraws it. A thing that types on your behalf must be
stoppable by the reflex of starting to type.

Dispatching needs none of that, because nothing is being typed at. It
passes `--permission-mode acceptEdits`, which is the one grant of
authority abeam makes to a process nobody is watching — edits land
without asking so the task can finish unattended, `Bash` still stops, and
`--dangerously-skip-permissions` is refused by a test that asserts the
whole argument vector rather than searching it. The prompt sits behind a
`--` fence: it is user text, and one leading dash without the fence would
let a pasted paragraph choose the process's own permissions. No
`--worktree`, deliberately — creating a worktree in somebody's repository
as a side effect of queueing a task is a structural change they did not
ask for, and it belongs behind an explicit choice.

An npm-installed Claude cannot be dispatched with a multi-line prompt at
all: the command line goes through `cmd.exe`, which cannot carry a
newline in any form. Since a paste becomes one item, that is the ordinary
shape of a queued task, so the refusal names the live-session mode as the
route that works. A native `claude.exe` has no such limit. This is
`cmd.exe`'s constraint rather than abeam's, and it is stated as one.

`Alt+A` was cleared the way the rest of the table was, against the same
binary: `meta+a` and `alt+a` both return zero matches, where the
undeclared readline bindings that caught `Alt+F` do appear as text. It is
a letter and not an F-key because it joins a set — `Alt+G`, `Alt+E`,
`Alt+S`, `Alt+A` are the four workspace views, and a fourth spelled `F6`
would be a binding nobody groups with the other three. Arming is `a` and
not `space`: `space` pages in every pane and the F1 overlay promises it
by name, and a key that pages in three panes and toggles a mode in the
fourth is a key nobody can learn.

`serde` and `serde_json` are the only new dependencies, and they are the
one place the git pane's "shell out and parse it yourself" precedent does
not carry. `git status --porcelain=v2` is line-oriented and frozen; this
is JSON with string escapes, three observed entry shapes, and a
`peerProtocol` field that exists so Claude can change the rest. A
hand-rolled scanner on the path that decides whether it is safe to type
at an agent is the wrong trade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ShipsAfterHours ShipsAfterHours self-assigned this Aug 2, 2026
@ShipsAfterHours ShipsAfterHours added the enhancement New feature or request label Aug 2, 2026
@ShipsAfterHours
ShipsAfterHours merged commit 62ad1e5 into main Aug 2, 2026
1 check passed
@ShipsAfterHours
ShipsAfterHours deleted the queue-work-for-the-agent branch August 2, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant