on main help sign in
fix: ensure proper resource cleanup and add retry logic for workspace creation
344eb32Ayman Bagabas
internal/client/proto.go+1
@@ -82,6 +82,7 @@ func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, er
82 }
82 }
83
83
84 if rsp.StatusCode != http.StatusOK {
84 if rsp.StatusCode != http.StatusOK {
85+ rsp.Body.Close()
85 return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode)
86 return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode)
86 }
87 }
87
88
internal/cmd/root.go+23−1
@@ -281,7 +281,29 @@ func setupClientApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, *prot
281 Env: os.Environ(),
281 Env: os.Environ(),
282 })
282 })
283 if err != nil {
283 if err != nil {
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 {
284− return nil, nil, fmt.Errorf("failed to create workspace: %v", err)
305+ return nil, nil, fmt.Errorf("failed to create workspace: %v", err)
306+ }
285 }
307 }
286
308
287 if shouldEnableMetrics(ws.Config) {
309 if shouldEnableMetrics(ws.Config) {
internal/config/resolve.go+14
@@ -14,6 +14,20 @@ type VariableResolver interface {
14 ResolveValue(value string) (string, error)
14 ResolveValue(value string) (string, error)
15}
15}
16
16
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+
17type Shell interface {
31type Shell interface {
18 Exec(ctx context.Context, command string) (stdout, stderr string, err error)
32 Exec(ctx context.Context, command string) (stdout, stderr string, err error)
19}
33}
internal/proto/mcp.go+8−7
@@ -4,6 +4,7 @@ import (
4 "encoding/json"
4 "encoding/json"
5 "errors"
5 "errors"
6 "fmt"
6 "fmt"
7+ "time"
7)
8)
8
9
9// MCPState represents the current state of an MCP client.
10// MCPState represents the current state of an MCP client.
@@ -125,13 +126,13 @@ func (e *MCPEvent) UnmarshalJSON(data []byte) error {
125// MCPClientInfo is the wire-format representation of an MCP client's
126// MCPClientInfo is the wire-format representation of an MCP client's
126// state, suitable for JSON transport between server and client.
127// state, suitable for JSON transport between server and client.
127type MCPClientInfo struct {
128type MCPClientInfo struct {
128− Name string `json:"name"`
129+ Name string `json:"name"`
129− State MCPState `json:"state"`
130+ State MCPState `json:"state"`
130− Error error `json:"error,omitempty"`
131+ Error error `json:"error,omitempty"`
131− ToolCount int `json:"tool_count,omitempty"`
132+ ToolCount int `json:"tool_count,omitempty"`
132− PromptCount int `json:"prompt_count,omitempty"`
133+ PromptCount int `json:"prompt_count,omitempty"`
133− ResourceCount int `json:"resource_count,omitempty"`
134+ ResourceCount int `json:"resource_count,omitempty"`
134− ConnectedAt int64 `json:"connected_at,omitempty"`
135+ ConnectedAt time.Time `json:"connected_at"`
135}
136}
136
137
137// MarshalJSON implements the [json.Marshaler] interface.
138// 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
253 ToolCount: v.Counts.Tools,
253 ToolCount: v.Counts.Tools,
254 PromptCount: v.Counts.Prompts,
254 PromptCount: v.Counts.Prompts,
255 ResourceCount: v.Counts.Resources,
255 ResourceCount: v.Counts.Resources,
256− ConnectedAt: v.ConnectedAt.Unix(),
256+ ConnectedAt: v.ConnectedAt,
257 }
257 }
258 }
258 }
259 jsonEncode(w, result)
259 jsonEncode(w, result)
internal/server/proto.go+6−3
@@ -366,8 +366,6 @@ func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Re
366func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
366func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
367 id := r.PathValue("id")
367 id := r.PathValue("id")
368
368
369− w.Header().Set("Accept", "application/json")
370−
371 var msg proto.AgentMessage
369 var msg proto.AgentMessage
372 if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
370 if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
373 c.server.logError(r, "Failed to decode request", "error", err)
371 c.server.logError(r, "Failed to decode request", "error", err)
@@ -379,6 +377,7 @@ func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.R
379 c.handleError(w, r, err)
377 c.handleError(w, r, err)
380 return
378 return
381 }
379 }
380+ w.WriteHeader(http.StatusOK)
382}
381}
383
382
384func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
383func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
@@ -387,6 +386,7 @@ func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *ht
387 c.handleError(w, r, err)
386 c.handleError(w, r, err)
388 return
387 return
389 }
388 }
389+ w.WriteHeader(http.StatusOK)
390}
390}
391
391
392func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
392func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
@@ -395,6 +395,7 @@ func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *
395 c.handleError(w, r, err)
395 c.handleError(w, r, err)
396 return
396 return
397 }
397 }
398+ w.WriteHeader(http.StatusOK)
398}
399}
399
400
400func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
401func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
@@ -439,13 +440,14 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.Respons
439 w.WriteHeader(http.StatusOK)
440 w.WriteHeader(http.StatusOK)
440}
441}
441
442
442−func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
443+func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
443 id := r.PathValue("id")
444 id := r.PathValue("id")
444 sid := r.PathValue("sid")
445 sid := r.PathValue("sid")
445 if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
446 if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
446 c.handleError(w, r, err)
447 c.handleError(w, r, err)
447 return
448 return
448 }
449 }
450+ w.WriteHeader(http.StatusOK)
449}
451}
450
452
451func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) {
453func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) {
@@ -484,6 +486,7 @@ func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter
484 c.handleError(w, r, err)
486 c.handleError(w, r, err)
485 return
487 return
486 }
488 }
489+ w.WriteHeader(http.StatusOK)
487}
490}
488
491
489func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
492func (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 {
143 mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/queued", c.handleGetWorkspaceAgentSessionPromptQueued)
143 mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/queued", c.handleGetWorkspaceAgentSessionPromptQueued)
144 mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/list", c.handleGetWorkspaceAgentSessionPromptList)
144 mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/list", c.handleGetWorkspaceAgentSessionPromptList)
145 mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/prompts/clear", c.handlePostWorkspaceAgentSessionPromptClear)
145 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)
147 mux.HandleFunc("GET /v1/workspaces/{id}/agent/default-small-model", c.handleGetWorkspaceAgentDefaultSmallModel)
147 mux.HandleFunc("GET /v1/workspaces/{id}/agent/default-small-model", c.handleGetWorkspaceAgentDefaultSmallModel)
148 mux.HandleFunc("POST /v1/workspaces/{id}/config/set", c.handlePostWorkspaceConfigSet)
148 mux.HandleFunc("POST /v1/workspaces/{id}/config/set", c.handlePostWorkspaceConfigSet)
149 mux.HandleFunc("POST /v1/workspaces/{id}/config/remove", c.handlePostWorkspaceConfigRemove)
149 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 {
350}
350}
351
351
352func (w *ClientWorkspace) Resolver() config.VariableResolver {
352func (w *ClientWorkspace) Resolver() config.VariableResolver {
353− // In client mode, variable resolution is handled server-side.
353+ return config.IdentityResolver()
354− return nil
355}
354}
356
355
357// -- Config mutations --
356// -- Config mutations --
@@ -447,7 +446,7 @@ func (w *ClientWorkspace) MCPGetStates() map[string]mcp.ClientInfo {
447 Prompts: v.PromptCount,
446 Prompts: v.PromptCount,
448 Resources: v.ResourceCount,
447 Resources: v.ResourceCount,
449 },
448 },
450− ConnectedAt: time.Unix(v.ConnectedAt, 0),
449+ ConnectedAt: v.ConnectedAt,
451 }
450 }
452 }
451 }
453 return result
452 return result
@@ -590,7 +589,8 @@ func translateEvent(ev any) tea.Msg {
590 },
589 },
591 }
590 }
592 default:
591 default:
592+ slog.Warn("Unknown event type in translateEvent", "type", fmt.Sprintf("%T", ev))
593− return ev.(tea.Msg)
593+ return nil
594 }
594 }
595}
595}
596
596