Detailed changes
@@ -82,6 +82,7 @@ func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, er
}
if rsp.StatusCode != http.StatusOK {
+ rsp.Body.Close()
return nil, fmt.Errorf("failed to subscribe to events: status code %d", rsp.StatusCode)
}
@@ -281,7 +281,29 @@ func setupClientApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, *prot
Env: os.Environ(),
})
if err != nil {
- return nil, nil, fmt.Errorf("failed to create workspace: %v", err)
+ // The server socket may exist before the HTTP handler is ready.
+ // Retry a few times with a short backoff.
+ for range 5 {
+ select {
+ case <-ctx.Done():
+ return nil, nil, ctx.Err()
+ case <-time.After(200 * time.Millisecond):
+ }
+ ws, err = c.CreateWorkspace(ctx, proto.Workspace{
+ Path: cwd,
+ DataDir: dataDir,
+ Debug: debug,
+ YOLO: yolo,
+ Version: version.Version,
+ Env: os.Environ(),
+ })
+ if err == nil {
+ break
+ }
+ }
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create workspace: %v", err)
+ }
}
if shouldEnableMetrics(ws.Config) {
@@ -14,6 +14,20 @@ type VariableResolver interface {
ResolveValue(value string) (string, error)
}
+// identityResolver is a no-op resolver that returns values unchanged.
+// Used in client mode where variable resolution is handled server-side.
+type identityResolver struct{}
+
+func (identityResolver) ResolveValue(value string) (string, error) {
+ return value, nil
+}
+
+// IdentityResolver returns a VariableResolver that passes values through
+// unchanged.
+func IdentityResolver() VariableResolver {
+ return identityResolver{}
+}
+
type Shell interface {
Exec(ctx context.Context, command string) (stdout, stderr string, err error)
}
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "time"
)
// MCPState represents the current state of an MCP client.
@@ -125,13 +126,13 @@ func (e *MCPEvent) UnmarshalJSON(data []byte) error {
// MCPClientInfo is the wire-format representation of an MCP client's
// state, suitable for JSON transport between server and client.
type MCPClientInfo struct {
- Name string `json:"name"`
- State MCPState `json:"state"`
- Error error `json:"error,omitempty"`
- ToolCount int `json:"tool_count,omitempty"`
- PromptCount int `json:"prompt_count,omitempty"`
- ResourceCount int `json:"resource_count,omitempty"`
- ConnectedAt int64 `json:"connected_at,omitempty"`
+ Name string `json:"name"`
+ State MCPState `json:"state"`
+ Error error `json:"error,omitempty"`
+ ToolCount int `json:"tool_count,omitempty"`
+ PromptCount int `json:"prompt_count,omitempty"`
+ ResourceCount int `json:"resource_count,omitempty"`
+ ConnectedAt time.Time `json:"connected_at"`
}
// MarshalJSON implements the [json.Marshaler] interface.
@@ -253,7 +253,7 @@ func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *htt
ToolCount: v.Counts.Tools,
PromptCount: v.Counts.Prompts,
ResourceCount: v.Counts.Resources,
- ConnectedAt: v.ConnectedAt.Unix(),
+ ConnectedAt: v.ConnectedAt,
}
}
jsonEncode(w, result)
@@ -366,8 +366,6 @@ func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Re
func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
- w.Header().Set("Accept", "application/json")
-
var msg proto.AgentMessage
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
c.server.logError(r, "Failed to decode request", "error", err)
@@ -379,6 +377,7 @@ func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.R
c.handleError(w, r, err)
return
}
+ w.WriteHeader(http.StatusOK)
}
func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) {
@@ -387,6 +386,7 @@ func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *ht
c.handleError(w, r, err)
return
}
+ w.WriteHeader(http.StatusOK)
}
func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) {
@@ -395,6 +395,7 @@ func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *
c.handleError(w, r, err)
return
}
+ w.WriteHeader(http.StatusOK)
}
func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) {
@@ -439,13 +440,14 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.Respons
w.WriteHeader(http.StatusOK)
}
-func (c *controllerV1) handleGetWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
+func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
sid := r.PathValue("sid")
if err := c.backend.SummarizeSession(r.Context(), id, sid); err != nil {
c.handleError(w, r, err)
return
}
+ w.WriteHeader(http.StatusOK)
}
func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) {
@@ -484,6 +486,7 @@ func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter
c.handleError(w, r, err)
return
}
+ w.WriteHeader(http.StatusOK)
}
func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) {
@@ -143,7 +143,7 @@ func NewServer(cfg *config.ConfigStore, network, address string) *Server {
mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/queued", c.handleGetWorkspaceAgentSessionPromptQueued)
mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/list", c.handleGetWorkspaceAgentSessionPromptList)
mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/prompts/clear", c.handlePostWorkspaceAgentSessionPromptClear)
- mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/summarize", c.handleGetWorkspaceAgentSessionSummarize)
+ mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/summarize", c.handlePostWorkspaceAgentSessionSummarize)
mux.HandleFunc("GET /v1/workspaces/{id}/agent/default-small-model", c.handleGetWorkspaceAgentDefaultSmallModel)
mux.HandleFunc("POST /v1/workspaces/{id}/config/set", c.handlePostWorkspaceConfigSet)
mux.HandleFunc("POST /v1/workspaces/{id}/config/remove", c.handlePostWorkspaceConfigRemove)
@@ -350,8 +350,7 @@ func (w *ClientWorkspace) WorkingDir() string {
}
func (w *ClientWorkspace) Resolver() config.VariableResolver {
- // In client mode, variable resolution is handled server-side.
- return nil
+ return config.IdentityResolver()
}
// -- Config mutations --
@@ -447,7 +446,7 @@ func (w *ClientWorkspace) MCPGetStates() map[string]mcp.ClientInfo {
Prompts: v.PromptCount,
Resources: v.ResourceCount,
},
- ConnectedAt: time.Unix(v.ConnectedAt, 0),
+ ConnectedAt: v.ConnectedAt,
}
}
return result
@@ -590,7 +589,8 @@ func translateEvent(ev any) tea.Msg {
},
}
default:
- return ev.(tea.Msg)
+ slog.Warn("Unknown event type in translateEvent", "type", fmt.Sprintf("%T", ev))
+ return nil
}
}