Repository navigation
Description
What happened
I opened New tab, picked Claude Code (haiku), typed a first message and pressed Start. Claude Code started, but the message did not go. Chat showed it as held:
- Most often: "Not sent: the pane's terminal or agent session changed. Review the message before sending it again."
- Less often: "Not sent: the agent started for this message has ended. Review the message before sending it."
Nothing happens after that. The message waits until the user sees the card and presses Send now.
Expected: the message goes to Claude Code once it is ready. It does so in the launches where it is not held.
Cause
Claude Code runs short child processes in front of itself as it starts. herdr lists them before Claude Code and names no agent yet. A trace of one launch (a pane read every 50 ms, times from the create request):
254ms agent=null fg=[git 63517, claude 63509]
306ms agent=null fg=[gh 63525 "gh auth token", claude 63509]
408ms agent=null fg=[claude 63509]
510ms agent=null fg=[gh 63585 "gh auth token", claude 63509]
796ms agent=claude fg=[security 63603, grep 63598, grep 63597, sh 63594 "sh -c ps aux | grep …", gh 63585, claude 63509]
846ms agent=claude fg=[claude 63509]
- herdr's
agent.startanswers at once. The create route queues the first message 5 to 250 ms after the launch (server/workspace-routes.ts:152-156). A launch throughagent.startreports no pid, solaunchedPidisnull. - While herdr names no agent,
currentIdentitytakes the first process in front (server/core/pending.ts:414,?? info?.foreground_processes?.[0]?.pid). In the trace above, that isgit. queueFirstMessagebinds the message to that pid (server/core/pending.ts:759-762). Its only check is that the pid is not the shell's.- The child exits a few ms later. At the next pane event,
learnLaunchedchecks the message (server/core/pending.ts:813-814):- herdr names
claude:targetChangedsees another pid and holds the message withpending_target_changed. - herdr names no agent yet:
launchGonesees the bound process gone and holds the message withfirst_message_launch_ended.
- herdr names
Proposed fix
- At queue time, bind the message only to a process in front that runs the launched agent's program.
runsAgentProgramalready makes this test forcurrentIdentity'srecognized. - In every other case, leave the process unbound.
learn()binds the agent when herdr names it. A launch with the shell still in front already takes this path. - The guards from One agent-process identity for pending sends and command outcomes, with no synchronous ps on macOS #1545 and test(pending): start a first message's same-kind replacement before the first agent ends #2222 stay. When the agent itself is in front at queue time, the message stays bound to it. A replacement of the same kind then gets
pending_target_changed.
With this fix, 0 of 18 live launches held the message (see below).
How often
Often. It depends on the startup timing.
- A/B with a real Claude Code (haiku) in an isolated herdr session, two Ovis servers in ABBA order, headless Chromium at 1280×800.
main(feb3ae0) held the first message in 11 of 17 launches: 9pending_target_changed, 2first_message_launch_ended. One batch held 1 of 7, the next 10 of 10. An earlier set of batches held 23 of 32.- With the fix, 0 of 18 launches held it.
- A held message is never sent on its own. When it was not held,
mainsent it 4461 ms after Start (median, n=5).
Ovis version
main (feb3ae0, v0.4.21). The new unit test fails the same way on 8d28829.
How it runs
Ovis servers from checkouts, on an isolated herdr session.
Server PC
macOS 26.5.2, Apple M4 (Mac mini), herdr 0.9.0
Device and browser
Headless Chromium at 1280×800. The cause is on the server and is not specific to a device.
Agent and PC
Claude Code 2.1.316 (haiku), local PC
Activity
fix(chat): bind a first message to the launched agent, not a child Cl…
chore(release): 0.4.22 (#2473)