|
8 | 8 | "maps" |
9 | 9 | "strings" |
10 | 10 | "sync" |
| 11 | + "sync/atomic" |
11 | 12 |
|
12 | 13 | "github.com/ethpandaops/codex-agent-sdk-go/internal/config" |
13 | 14 | sdkerrors "github.com/ethpandaops/codex-agent-sdk-go/internal/errors" |
@@ -73,6 +74,10 @@ type AppServerAdapter struct { |
73 | 74 | pendingRPCRequests map[string]int64 |
74 | 75 | sdkMCPServerNames map[string]struct{} |
75 | 76 |
|
| 77 | + // messagesClosed is set before closing the messages channel so that |
| 78 | + // senders on other goroutines can avoid a send-on-closed-channel panic. |
| 79 | + messagesClosed atomic.Bool |
| 80 | + |
76 | 81 | wg sync.WaitGroup |
77 | 82 | } |
78 | 83 |
|
@@ -1408,7 +1413,10 @@ func (a *AppServerAdapter) handleUserMessage( |
1408 | 1413 | // translates them into exec-event format messages. |
1409 | 1414 | func (a *AppServerAdapter) readLoop() { |
1410 | 1415 | defer a.wg.Done() |
1411 | | - defer close(a.messages) |
| 1416 | + defer func() { |
| 1417 | + a.messagesClosed.Store(true) |
| 1418 | + close(a.messages) |
| 1419 | + }() |
1412 | 1420 | defer close(a.errs) |
1413 | 1421 |
|
1414 | 1422 | notifications := a.inner.Notifications() |
@@ -2268,6 +2276,21 @@ func (a *AppServerAdapter) injectControlResponse( |
2268 | 2276 | requestID string, |
2269 | 2277 | responseData map[string]any, |
2270 | 2278 | ) { |
| 2279 | + // Guard against sending on a closed channel. The readLoop goroutine |
| 2280 | + // closes a.messages when the inner transport shuts down, which can |
| 2281 | + // race with control-request handlers still running on other goroutines. |
| 2282 | + // The atomic check handles the common case; the recover covers the |
| 2283 | + // narrow window between the check and the send. |
| 2284 | + if a.messagesClosed.Load() { |
| 2285 | + return |
| 2286 | + } |
| 2287 | + |
| 2288 | + defer func() { |
| 2289 | + if r := recover(); r != nil { |
| 2290 | + a.log.Debug("inject suppressed: messages channel closed during shutdown") |
| 2291 | + } |
| 2292 | + }() |
| 2293 | + |
2271 | 2294 | msg := map[string]any{ |
2272 | 2295 | "type": "control_response", |
2273 | 2296 | "response": responseData, |
|
0 commit comments