{"record":{"id":"113e060cdb35cec3","repo":"charmbracelet/crush","slug":"failed-to-send-message-to-agent-w","errorCode":null,"errorMessage":"failed to send message to agent: %w","messagePattern":"failed to send message to agent: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/client/proto.go","lineNumber":501,"sourceCode":"\treturn nil\n}\n\n// SendMessage sends a message to the agent for a workspace.\n//\n// When runID is non-empty it is echoed back on the resulting\n// proto.RunComplete event, giving the caller a unique correlator\n// for completion detection. Pass \"\" when the caller does not need\n// to distinguish its own turn's terminal event from any concurrent\n// turn on the same session (e.g. interactive TUI usage).\nfunc (c *Client) SendMessage(ctx context.Context, id string, sessionID, runID, prompt string, attachments ...message.Attachment) error {\n\trsp, err := c.post(ctx, fmt.Sprintf(\"/workspaces/%s/agent\", id), nil, jsonBody(proto.AgentMessage{\n\t\tSessionID:   sessionID,\n\t\tRunID:       runID,\n\t\tPrompt:      prompt,\n\t\tAttachments: proto.AttachmentsFromMessage(attachments),\n\t}), http.Header{\"Content-Type\": []string{\"application/json\"}})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to send message to agent: %w\", err)\n\t}\n\tdefer rsp.Body.Close()\n\tif rsp.StatusCode != http.StatusOK && rsp.StatusCode != http.StatusAccepted {\n\t\tif msg := decodeErrorMessage(rsp.Body); msg != \"\" {\n\t\t\treturn fmt.Errorf(\"failed to send message to agent: status code %d: %s\", rsp.StatusCode, msg)\n\t\t}\n\t\treturn fmt.Errorf(\"failed to send message to agent: status code %d\", rsp.StatusCode)\n\t}\n\treturn nil\n}\n\n// decodeErrorMessage attempts to decode the response body as a\n// proto.Error and returns its message. It returns an empty string\n// when the body is empty or cannot be decoded into a proto.Error\n// with a non-empty message, letting callers fall back to a\n// status-only error.\nfunc decodeErrorMessage(body io.Reader) string {\n\tvar e proto.Error","sourceCodeStart":483,"sourceCodeEnd":519,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/client/proto.go#L483-L519","documentation":"SendMessage posts a JSON message to /workspaces/{id}/agent/sessions/messages. This error wraps a transport failure from c.post (unreachable server, connection reset, timeout, request serialization failure) before any response is read. The underlying cause is preserved with %w.","triggerScenarios":"Calling SendMessage (e.g. from runNonInteractive) when the server is down, the connection drops mid-request, the request times out, or the request body cannot be constructed/sent.","commonSituations":"Non-interactive runs against a stopped server; large prompts/attachments timing out; flaky network in CI; proxy/firewall blocking POSTs.","solutions":["Confirm the server is running and the base URL/port are correct.","Test connectivity with a simple GET (e.g. GetAgentSessionInfo) to isolate transport issues.","Retry with backoff for transient network failures, especially in CI pipelines.","Check the wrapped cause (%w) to distinguish timeout vs connection-refused."],"exampleFix":"// before\nerr := client.SendMessage(ctx, wsID, sessionID, runID, prompt, nil)\n// after\nvar lastErr error\nfor i := 0; i < 3; i++ {\n    if lastErr = client.SendMessage(ctx, wsID, sessionID, runID, prompt, nil); lastErr == nil {\n        return nil\n    }\n    time.Sleep(time.Duration(1<<i) * time.Second)\n}\nreturn lastErr","handlingStrategy":"retry","validationCode":"// Go: validate inputs before sending\nif prompt == \"\" { return fmt.Errorf(\"prompt is empty\") }\nif workspaceID == \"\" || sessionID == \"\" {\n    return fmt.Errorf(\"workspace and session ids are required\")\n}","typeGuard":"// Go: distinguish transport errors from status errors\nfunc isTransportError(err error) bool {\n    return err != nil && !strings.Contains(err.Error(), \"status code\")\n}","tryCatchPattern":"err := client.SendMessage(ctx, wsID, sid, runID, prompt, attachments)\nfor retry := 0; err != nil && isTransportError(err) && retry < 3; retry++ {\n    time.Sleep(time.Duration(1<<retry) * time.Second)\n    err = client.SendMessage(ctx, wsID, sid, runID, prompt, attachments)\n}\nif err != nil { return err }","preventionTips":["Run non-interactive jobs only after a connectivity smoke test.","Set a context timeout larger than the longest expected send (attachments can be large).","Prefer idempotent sends or include a client request ID the server can dedupe.","Keep the client and server on compatible versions."],"tags":["network","http-client","message-send"],"backgroundTag":"http-request-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}