charmbracelet/crush · error

failed to decode agent status: %w

Error message

failed to decode agent status: %w

What it means

After a successful checkStatus, GetAgentInfo decodes the body into proto.AgentInfo. If the body is not valid JSON matching proto.AgentInfo (empty body, HTML from a proxy, schema drift), the decode fails and this error wraps the json error.

Source

Thrown at internal/client/proto.go:468

	if rsp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to clear session agent queued prompts: status code %d", rsp.StatusCode)
	}
	return nil
}

// GetAgentInfo retrieves the agent status for a workspace.
func (c *Client) GetAgentInfo(ctx context.Context, id string) (*proto.AgentInfo, error) {
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get agent status: %w", err)
	}
	defer rsp.Body.Close()
	if err := checkStatus(rsp); err != nil {
		return nil, fmt.Errorf("failed to get agent status: %w", err)
	}
	var info proto.AgentInfo
	if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
		return nil, fmt.Errorf("failed to decode agent status: %w", err)
	}
	return &info, nil
}

// UpdateAgent triggers an agent model update on the server.
func (c *Client) UpdateAgent(ctx context.Context, id string) error {
	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/update", id), nil, nil, nil)
	if err != nil {
		return fmt.Errorf("failed to update agent: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to update agent: status code %d", rsp.StatusCode)
	}
	return nil
}

// SendMessage sends a message to the agent for a workspace.

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Align client and server versions so proto.AgentInfo matches
  2. Log the raw response body on decode failure
  3. Check for proxies altering the response
  4. Update the generated proto types if the server schema changed

Example fix

// before
var info proto.AgentInfo
if err := json.NewDecoder(rsp.Body).Decode(&info); err != nil {
	return nil, fmt.Errorf("failed to decode agent status: %w", err)
}
// after
body, _ := io.ReadAll(rsp.Body)
var info proto.AgentInfo
if err := json.Unmarshal(body, &info); err != nil {
	return nil, fmt.Errorf("decode agent status %q: %w", body, err)
}
Defensive patterns

Strategy: type-guard

Type guard

func decodeAgentInfo(r io.Reader) (*proto.AgentInfo, error) {
	body, err := io.ReadAll(r)
	if err != nil { return nil, err }
	if len(bytes.TrimSpace(body)) == 0 { return nil, errors.New("empty agent status body") }
	var info proto.AgentInfo
	if err := json.Unmarshal(body, &info); err != nil {
		return nil, fmt.Errorf("agent status body %q: %w", body, err)
	}
	return &info, nil
}

Try / catch

info, err := client.GetAgentInfo(ctx, wsID)
if err != nil && strings.Contains(err.Error(), "failed to decode agent status") {
	// do not retry: schema mismatch — log body and check versions
}

Prevention

When it happens

Trigger: Server returns 200 with a body that fails to unmarshal into proto.AgentInfo — version mismatch where fields changed types, empty response, or proxy-injected content.

Common situations: Client and server built from different versions after a proto.AgentInfo schema change; misbehaving reverse proxy; server returning null or a wrapped object instead of the expected struct.

Understand the failure class

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/81530c0a4f6572a9. Report an issue: GitHub.