charmbracelet/crush · error

failed to decode session agent info: %w

Error message

failed to decode session agent info: %w

What it means

Thrown by Client.GetAgentSessionInfo in internal/client/proto.go when the JSON response body from GET /workspaces/{id}/agent/sessions/{id} cannot be decoded into proto.AgentSession. It wraps the underlying json.Decoder error, so the cause is a malformed, truncated, or non-JSON body (e.g. an HTML error page or a proxy gateway response) that slipped past the 200-status check. Callers such as AgentIsSessionBusy then cannot determine the session's busy state.

Source

Thrown at internal/client/proto.go:558

	if err := json.NewDecoder(rsp.Body).Decode(&resp); err != nil {
		return proto.ShellCommandResponse{}, fmt.Errorf("failed to decode shell command response: %w", err)
	}
	return resp, nil
}

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

// AgentSummarizeSession requests a session summarization.
func (c *Client) AgentSummarizeSession(ctx context.Context, id string, sessionID string) error {
	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent/sessions/%s/summarize", id, sessionID), nil, nil, nil)
	if err != nil {
		return fmt.Errorf("failed to summarize session: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to summarize session: status code %d", rsp.StatusCode)
	}
	return nil
}

// InitiateAgentProcessing triggers agent initialization on the server.

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Read the wrapped cause (errors.Unwrap / %w chain) to see the exact json.SyntaxError or UnmarshalTypeError and fix the payload accordingly.
  2. Verify the client is pointed at the correct crush server address/port and that no proxy is intercepting the request (curl the endpoint directly).
  3. Confirm client and server versions match so the AgentSession JSON schema is the same on both sides.
  4. Retry the call — a truncated body from a transient network drop will succeed on retry.

Example fix

// before: cannot tell decode failure from empty body
info, err := client.GetAgentSessionInfo(ctx, wsID, sessionID)
if err != nil {
    return err
}
// after: inspect the wrapped cause
info, err := client.GetAgentSessionInfo(ctx, wsID, sessionID)
if err != nil {
    var syn *json.SyntaxError
    if errors.As(err, &syn) {
        log.Printf("non-JSON response at offset %d; check proxy/server address", syn.Offset)
    }
    return fmt.Errorf("session agent info unavailable: %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: confirm the endpoint returns JSON before calling the client
rsp, err := http.Get(serverURL + "/workspaces/" + wsID + "/agent/sessions/" + sessionID)
if err == nil {
    defer rsp.Body.Close()
    ct := rsp.Header.Get("Content-Type")
    if !strings.Contains(ct, "application/json") {
        return fmt.Errorf("expected JSON, got %q — check proxy/server address", ct)
    }
}

Type guard

func isDecodeError(err error) bool {
    var syn *json.SyntaxError
    var ute *json.UnmarshalTypeError
    return errors.As(err, &syn) || errors.As(err, &ute)
}

Try / catch

info, err := client.GetAgentSessionInfo(ctx, wsID, sessionID)
if err != nil {
    var syn *json.SyntaxError
    if errors.As(err, &syn) {
        // non-JSON body: likely proxy or wrong endpoint
        return retryWithDirectConnection(ctx, wsID, sessionID)
    }
    return err
}

Prevention

When it happens

Trigger: Calling GetAgentSessionInfo when the server returns HTTP 200 but with a body that is not valid JSON matching proto.AgentSession: empty body, HTML from a misconfigured reverse proxy, JSON with incompatible field types (e.g. string where a number is expected), or a truncated response from a dropped connection mid-body.

Common situations: Running behind an nginx/traefik proxy that intercepts the request; pointing the client at a wrong port where another service answers 200; server version mismatch where the API returns a changed schema; TLS-terminating proxy injecting an error page; connection cut before the body finished streaming.

Understand the failure class

Related errors


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