agentMessageSend (agent.go:95-188) reads the session (line 104) but never checks session.State; updateAgentSession(state="running") (agent.go:164-168) is an unconditional overwrite, not a compare-and-reject. async defaults to true, so even a sequential second agent.message_send before the first job finishes launches a second agent subprocess in the same session.Cwd (the task worktree). Under HTTP each POST is its own goroutine with no global lock, so two concurrent sends also both appendAgentMessage(user) (line 116), both readAgentMessages a different partial history (line 119), and both RunCommand — two subprocesses mutating the same worktree while the messages NDJSON degrades to USER/USER/ASSISTANT/ASSISTANT.
Fix: reject agent.message_send when session.State == "running" (compare-and-set), i.e. one in-flight turn per session. Pairs with the R0 write-serialization work. Distinct from #24/#25.
agentMessageSend(agent.go:95-188) reads the session (line 104) but never checkssession.State;updateAgentSession(state="running")(agent.go:164-168) is an unconditional overwrite, not a compare-and-reject.asyncdefaults to true, so even a sequential secondagent.message_sendbefore the first job finishes launches a second agent subprocess in the samesession.Cwd(the task worktree). Under HTTP each POST is its own goroutine with no global lock, so two concurrent sends also bothappendAgentMessage(user)(line 116), bothreadAgentMessagesa different partial history (line 119), and bothRunCommand— two subprocesses mutating the same worktree while the messages NDJSON degrades to USER/USER/ASSISTANT/ASSISTANT.Fix: reject
agent.message_sendwhensession.State == "running"(compare-and-set), i.e. one in-flight turn per session. Pairs with the R0 write-serialization work. Distinct from #24/#25.