From 81f7379fb6820ce3c1a1b0e4441f31337f47ceae Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Mon, 13 Apr 2026 13:37:41 +0200 Subject: [PATCH] 9p: split clunk into sync (Rerror) and async (fire-and-forget) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prompt, enqueue, ctl writes run async — they may block for an entire agent turn and don't benefit from Rerror. Everything else (disk I/O, state mutations, s/new) runs synchronously so errors surface as Rerror without holding the connection. --- internal/p9/server.go | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/internal/p9/server.go b/internal/p9/server.go index 787d00d..6a1f966 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -737,13 +737,29 @@ func (s *Server) clunk(cs *connState, fc *plan9.Fcall) *plan9.Fcall { } cs.mu.Unlock() if len(data) > 0 { - if err := s.handleWrite(path, strings.TrimSpace(string(data))); err != nil { + input := strings.TrimSpace(string(data)) + if s.isAsyncWrite(path) { + go s.handleWrite(path, input) //nolint:errcheck + } else if err := s.handleWrite(path, input); err != nil { return errFcall(fc, err.Error()) } } return &plan9.Fcall{Type: plan9.Rclunk, Tag: fc.Tag} } +// isAsyncWrite returns true for paths where writes may block (agent turns) +// and Rerror is not useful. +func (s *Server) isAsyncWrite(path string) bool { + if !strings.HasPrefix(path, "/s/") { + return false + } + switch pathBase(path) { + case "prompt", "enqueue", "ctl": + return true + } + return false +} + func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() f, ok := cs.fids[fc.Fid] @@ -861,13 +877,10 @@ func (s *Server) handleWrite(path, input string) error { switch fileName { case "prompt": - // prompt blocks for the entire agent turn; run async. - go func() { - sess.core.Submit(sess.ctx, input, publish) - sess.mu.Lock() - sess.replyVers++ - sess.mu.Unlock() - }() + sess.core.Submit(sess.ctx, input, publish) + sess.mu.Lock() + sess.replyVers++ + sess.mu.Unlock() return nil case "enqueue":