From 412b8513e62928c120b5c68fb09e1e7a8f96fae9 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 9 Oct 2026 10:16:17 +0200 Subject: [PATCH] session: support multiple concurrent bypass requests Previously a session tracked only one pending bypass request. If agent A1 had a pending bypass, agent A2's bypass request would block waiting to be read from toolsrv's channel, effectively blocking all agents in the session. Now bypassPending is a map keyed by request ID: - SetBypassPending adds to the map instead of overwriting - BypassPending returns all pending requests (slice) - BypassPendingByID returns a specific request - ResolveBypass removes from the map by ID - 9P bypass file returns JSON array of all pending The bypass loop reads requests continuously without waiting for resolution, so multiple agents can have concurrent pending requests. --- cmd/olliesrv/internal/fs/spec.go | 10 +++---- cmd/olliesrv/internal/session/session.go | 37 +++++++++++++++++------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go index 33beb3f..c044b60 100644 --- a/cmd/olliesrv/internal/fs/spec.go +++ b/cmd/olliesrv/internal/fs/spec.go @@ -474,13 +474,13 @@ func buildSessionChildren( }, s.GoalSignal), ), virtfs.FileNode("bypass", 0666, - virtfs.Doc("Bypass request handling. Read: pending request JSON or empty. Write: 'id approve' or 'id deny'."), + virtfs.Doc("Bypass request handling. Read: pending requests JSON array. Write: 'id approve' or 'id deny'."), virtfs.Read(func() ([]byte, error) { - req := s.BypassPending() - if req == nil { - return nil, nil + reqs := s.BypassPending() + if len(reqs) == 0 { + return []byte("[]\n"), nil } - data, err := json.Marshal(req) + data, err := json.Marshal(reqs) if err != nil { return nil, err } diff --git a/cmd/olliesrv/internal/session/session.go b/cmd/olliesrv/internal/session/session.go index e89df5d..df7d0fe 100644 --- a/cmd/olliesrv/internal/session/session.go +++ b/cmd/olliesrv/internal/session/session.go @@ -63,9 +63,9 @@ type Session struct { modelsCache string modelsCacheAt time.Time - // Bypass request tracking + // Bypass request tracking (multiple concurrent requests supported) bypassMu sync.RWMutex - bypassPending *protocol.BypassRequest + bypassPending map[string]*protocol.BypassRequest // keyed by request ID bypassDialFn func() *toolclient.ToolsrvConn bypassSignalCh chan struct{} } @@ -252,7 +252,10 @@ func (s *Session) GoalSignal() <-chan struct{} { // SetBypassPending stores a pending bypass request and publishes an event. func (s *Session) SetBypassPending(req *protocol.BypassRequest, dialFn func() *toolclient.ToolsrvConn) { s.bypassMu.Lock() - s.bypassPending = req + if s.bypassPending == nil { + s.bypassPending = make(map[string]*protocol.BypassRequest) + } + s.bypassPending[req.ID] = req s.bypassDialFn = dialFn if s.bypassSignalCh != nil { close(s.bypassSignalCh) @@ -270,24 +273,38 @@ func (s *Session) SetBypassPending(req *protocol.BypassRequest, dialFn func() *t PublishEvent("session."+s.ID+".agent."+agentID+".bypass.request", req.ID+"\t"+cmd+"\t"+req.Cwd) } -// BypassPending returns the current pending bypass request, or nil if none. -func (s *Session) BypassPending() *protocol.BypassRequest { +// BypassPending returns all pending bypass requests. +func (s *Session) BypassPending() []*protocol.BypassRequest { s.bypassMu.RLock() defer s.bypassMu.RUnlock() - return s.bypassPending + out := make([]*protocol.BypassRequest, 0, len(s.bypassPending)) + for _, req := range s.bypassPending { + out = append(out, req) + } + return out +} + +// BypassPendingByID returns a specific pending request by ID. +func (s *Session) BypassPendingByID(id string) *protocol.BypassRequest { + s.bypassMu.RLock() + defer s.bypassMu.RUnlock() + return s.bypassPending[id] } // ResolveBypass resolves a pending bypass request and clears it. func (s *Session) ResolveBypass(id string, approved bool) error { s.bypassMu.Lock() - req := s.bypassPending + req, ok := s.bypassPending[id] dialFn := s.bypassDialFn - if req == nil || req.ID != id { + if !ok { s.bypassMu.Unlock() return fmt.Errorf("no pending bypass with id %s", id) } - s.bypassPending = nil - s.bypassDialFn = nil + delete(s.bypassPending, id) + // Only clear dialFn if no more pending requests + if len(s.bypassPending) == 0 { + s.bypassDialFn = nil + } if s.bypassSignalCh != nil { close(s.bypassSignalCh) }