chore(server): acknowledge accepted prompts with HTTP 202

Christian Rocha and Charm Crush created

Return as soon as a prompt is accepted instead of keeping the request
open for the full agent turn. Workspace shutdown is reported as a
conflict so clients can distinguish rejection from accepted work.

Co-Authored-By: Charm Crush <crush@charm.land>

Change summary

internal/server/agent_cancel_test.go | 4 ++--
internal/server/proto.go             | 7 +++++--
2 files changed, 7 insertions(+), 4 deletions(-)

Detailed changes

internal/server/agent_cancel_test.go 🔗

@@ -139,7 +139,7 @@ func TestPostAgent_ReturnsOKOnContextCanceled(t *testing.T) {
 	// The handler returns immediately, before the dispatched run is
 	// released, because the run no longer owns the HTTP response.
 	rec := postAgent(t, c, t.Context(), wsID, "S1")
-	require.Equal(t, http.StatusOK, rec.Code, "fire-and-forget SendMessage must return 200 without waiting for the run")
+	require.Equal(t, http.StatusAccepted, rec.Code, "fire-and-forget SendMessage must return 202 without waiting for the run")
 
 	// The run is dispatched on a goroutine; let it return
 	// context.Canceled. Nothing from that path reaches the (already
@@ -169,7 +169,7 @@ func TestPostAgent_DetachesRequestContext(t *testing.T) {
 	// The handler returns immediately; the run keeps executing on its
 	// own goroutine bound to the workspace context.
 	rec := postAgent(t, c, reqCtx, wsID, "S1")
-	require.Equal(t, http.StatusOK, rec.Code)
+	require.Equal(t, http.StatusAccepted, rec.Code)
 
 	select {
 	case <-coord.entered:

internal/server/proto.go 🔗

@@ -740,9 +740,10 @@ func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Re
 //	@Accept			json
 //	@Param			id		path	string				true	"Workspace ID"
 //	@Param			request	body	proto.AgentMessage	true	"Agent message"
-//	@Success		200
+//	@Success		202
 //	@Failure		400	{object}	proto.Error
 //	@Failure		404	{object}	proto.Error
+//	@Failure		409	{object}	proto.Error
 //	@Failure		500	{object}	proto.Error
 //	@Router			/workspaces/{id}/agent [post]
 func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
@@ -767,7 +768,7 @@ func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.R
 		c.handleError(w, r, err)
 		return
 	}
-	w.WriteHeader(http.StatusOK)
+	w.WriteHeader(http.StatusAccepted)
 }
 
 // handlePostWorkspaceAgentInit initializes the agent for a workspace.
@@ -1061,6 +1062,8 @@ func (c *controllerV1) handleError(w http.ResponseWriter, r *http.Request, err e
 		status = http.StatusBadRequest
 	case errors.Is(err, backend.ErrClientNotAttached):
 		status = http.StatusNotFound
+	case errors.Is(err, backend.ErrWorkspaceClosing):
+		status = http.StatusConflict
 	}
 	c.server.logError(r, err.Error())
 	jsonError(w, status, err.Error())