on main help sign in
fix: ensure proper resource cleanup and add retry logic for workspace creation
commit344eb3223b01d7d8e4c88e135bc20c5c0b550777 authorAyman Bagabas <ayman.bagabas@gmail.com> dateThu 12 Mar 2026, 22:09 (7 months ago) parente70c8ba
8 files changed+58−17
internal/client/proto.go+1 internal/cmd/root.go+23−1 internal/config/resolve.go+14 internal/proto/mcp.go+8−7 internal/server/config.go+1−1 internal/server/proto.go+6−3 internal/server/server.go+1−1 internal/workspace/client_workspace.go+4−4
internal/client/proto.go+1
@@ -82,6 +82,7 @@ func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, er
8282 }
8383
8484 if rsp.StatusCode != http.StatusOK {
85+ rsp.Body.Close()
8586 return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode)
8687 }
8788
internal/cmd/root.go+23−1
@@ -281,7 +281,29 @@ func setupClientApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, *prot
281281 Env: os.Environ(),
282282 })
283283 if err != nil {
284− return nil, nil, fmt.Errorf("failed to create workspace: %v", err)
284+ // The server socket may exist before the HTTP handler is ready.
285+ // Retry a few times with a short backoff.
286+ for range 5 {
287+ select {
288+ case <-ctx.Done():
289+ return nil, nil, ctx.Err()
290+ case <-time.After(200 * time.Millisecond):
291+ }
292+ ws, err = c.CreateWorkspace(ctx, proto.Workspace{
293+ Path: cwd,
294+ DataDir: dataDir,
295+ Debug: debug,
296+ YOLO: yolo,
297+ Version: version.Version,
298+ Env: os.Environ(),
299+ })
300+ if err == nil {
301+ break
302+ }
303+ }
304+ if err != nil {
305+ return nil, nil, fmt.Errorf("failed to create workspace: %v", err)
306+ }
285307 }
286308
287309 if shouldEnableMetrics(ws.Config) {
internal/config/resolve.go+14
@@ -14,6 +14,20 @@ type VariableResolver interface {
1414 ResolveValue(value string) (string, error)
1515}
1616
17+// identityResolver is a no-op resolver that returns values unchanged.
18+// Used in client mode where variable resolution is handled server-side.
19+type identityResolver struct{}
20+
21+func (identityResolver) ResolveValue(value string) (string, error) {
22+ return value, nil
23+}
24+
25+// IdentityResolver returns a VariableResolver that passes values through
26+// unchanged.
27+func IdentityResolver() VariableResolver {
28+ return identityResolver{}
29+}
30+
1731type Shell interface {
1832 Exec(ctx context.Context, command string) (stdout, stderr string, err error)
1933}
internal/proto/mcp.go+8−7
@@ -4,6 +4,7 @@ import (
44 "encoding/json"
55 "errors"
66 "fmt"
7+ "time"
78)
89
910// MCPState represents the current state of an MCP client.
@@ -125,13 +126,13 @@ func (e *MCPEvent) UnmarshalJSON(data []byte) error {
125126// MCPClientInfo is the wire-format representation of an MCP client's
126127// state, suitable for JSON transport between server and client.
127128type MCPClientInfo struct {
128− Name string `json:"name"`
129− State MCPState `json:"state"`
130− Error error `json:"error,omitempty"`
131− ToolCount int `json:"tool_count,omitempty"`
132− PromptCount int `json:"prompt_count,omitempty"`
133− ResourceCount int `json:"resource_count,omitempty"`
134− ConnectedAt int64 `json:"connected_at,omitempty"`
129+ Name string `json:"name"`
130+ State MCPState `json:"state"`
131+ Error error `json:"error,omitempty"`
132+ ToolCount int `json:"tool_count,omitempty"`
133+ PromptCount int `json:"prompt_count,omitempty"`
134+ ResourceCount int `json:"resource_count,omitempty"`
135+ ConnectedAt time.Time `json:"connected_at"`
135136}
136137
137138// MarshalJSON implements the [json.Marshaler] interface.
internal/server/config.go+1−1
@@ -253,7 +253,7 @@ func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *htt
253253 ToolCount: v.Counts.Tools,
254254 PromptCount: v.Counts.Prompts,
255255 ResourceCount: v.Counts.Resources,
256− ConnectedAt: v.ConnectedAt.Unix(),
256+ ConnectedAt: v.ConnectedAt,
257257 }
258258 }
259259 jsonEncode(w, result)
internal/server/proto.go+6−3
@@ -366,8 +366,6 @@ func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Re
366366func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
367367 id := r.PathValue("id")
368368
369− w.Header().Set("Accept", "application/json")
370−
371369 var msg proto.AgentMessage
372370 if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
373371 c.server.logError(r, "Failed to decode request", "error", err)
@@ -379,6 +377,7 @@ func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.R
379377 c.handleError(w, r, err)
380378 return
381379 }
380+ w.WriteHeader(http.StatusOK)
382381}
383382
384383func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
@@ -387,6 +386,7 @@ func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *ht
387386 c.handleError(w, r, err)
388387 return
389388 }
389+ w.WriteHeader(http.StatusOK)
390390}
391391
392392func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
@@ -395,6 +395,7 @@ func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *
395395 c.handleError(w, r, err)
396396 return
397397 }
398+ w.WriteHeader(http.StatusOK)
398399}
399400
400401func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
@@ -439,13 +440,14 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.Respons
439440 w.WriteHeader(http.StatusOK)
440441}
441442
442−func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
443+func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
443444 id := r.PathValue("id")
444445 sid := r.PathValue("sid")
445446 if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
446447 c.handleError(w, r, err)
447448 return
448449 }
450+ w.WriteHeader(http.StatusOK)
449451}
450452
451453func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) {
@@ -484,6 +486,7 @@ func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter
484486 c.handleError(w, r, err)
485487 return
486488 }
489+ w.WriteHeader(http.StatusOK)
487490}
488491
489492func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
internal/server/server.go+1−1
@@ -143,7 +143,7 @@ func NewServer(cfg *config.ConfigStore, network, address string) *Server {
143143 mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/queued", c.handleGetWorkspaceAgentSessionPromptQueued)
144144 mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/list", c.handleGetWorkspaceAgentSessionPromptList)
145145 mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/prompts/clear", c.handlePostWorkspaceAgentSessionPromptClear)
146− mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/summarize", c.handleGetWorkspaceAgentSessionSummarize)
146+ mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/summarize", c.handlePostWorkspaceAgentSessionSummarize)
147147 mux.HandleFunc("GET /v1/workspaces/{id}/agent/default-small-model", c.handleGetWorkspaceAgentDefaultSmallModel)
148148 mux.HandleFunc("POST /v1/workspaces/{id}/config/set", c.handlePostWorkspaceConfigSet)
149149 mux.HandleFunc("POST /v1/workspaces/{id}/config/remove", c.handlePostWorkspaceConfigRemove)
internal/workspace/client_workspace.go+4−4
@@ -350,8 +350,7 @@ func (w *ClientWorkspace) WorkingDir() string {
350350}
351351
352352func (w *ClientWorkspace) Resolver() config.VariableResolver {
353− // In client mode, variable resolution is handled server-side.
354− return nil
353+ return config.IdentityResolver()
355354}
356355
357356// -- Config mutations --
@@ -447,7 +446,7 @@ func (w *ClientWorkspace) MCPGetStates() map[string]mcp.ClientInfo {
447446 Prompts: v.PromptCount,
448447 Resources: v.ResourceCount,
449448 },
450− ConnectedAt: time.Unix(v.ConnectedAt, 0),
449+ ConnectedAt: v.ConnectedAt,
451450 }
452451 }
453452 return result
@@ -590,7 +589,8 @@ func translateEvent(ev any) tea.Msg {
590589 },
591590 }
592591 default:
593− return ev.(tea.Msg)
592+ slog.Warn("Unknown event type in translateEvent", "type", fmt.Sprintf("%T", ev))
593+ return nil
594594 }
595595}
596596