{"record":{"id":"83eb4ae4eef60a7a","repo":"zeroclaw-labs/zeroclaw","slug":"signal-rpc-error-code-msg","errorCode":null,"errorMessage":"Signal RPC error {code}: {msg}","messagePattern":"Signal RPC error (.+?): (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/signal.rs","lineNumber":376,"sourceCode":"\n        // 201 = success with no body (e.g. typing indicators)\n        if resp.status().as_u16() == 201 {\n            return Ok(None);\n        }\n\n        let text = resp.text().await?;\n        if text.is_empty() {\n            return Ok(None);\n        }\n\n        let parsed: serde_json::Value = serde_json::from_str(&text)?;\n        if let Some(err) = parsed.get(\"error\") {\n            let code = err.get(\"code\").and_then(|c| c.as_i64()).unwrap_or(-1);\n            let msg = err\n                .get(\"message\")\n                .and_then(|m| m.as_str())\n                .unwrap_or(\"unknown\");\n            anyhow::bail!(\"Signal RPC error {code}: {msg}\");\n        }\n\n        Ok(parsed.get(\"result\").cloned())\n    }\n\n    /// Process a single SSE envelope, returning one or more\n    /// `ChannelMessage`s. Most envelopes produce 0 or 1 messages; a\n    /// multi-select poll vote produces N (one per selected option).\n    ///\n    /// Inbound shape may be plain text (`dataMessage.message`) OR a\n    /// poll-vote (`dataMessage.pollAnswer` or `dataMessage.pollVote`). For\n    /// poll-votes we emit a synthetic message per selected option whose `content` is a\n    /// documented sentinel: `\"[choice-index]N\"` for real signal-cli\n    /// `pollVote` payloads, or `\"[choice]<selected-title>\"` when an\n    /// alternate payload supplies titles. Consumers\n    /// can match this prefix to correlate the vote with their original\n    /// option set, or ignore it if they don't handle poll votes.\n    ///","sourceCodeStart":358,"sourceCodeEnd":394,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/signal.rs#L358-L394","documentation":"Raised by SignalChannel::rpc_request when the signal-cli daemon's JSON-RPC endpoint ({http_url}/api/v1/rpc) answers with an envelope containing an 'error' object. The numeric code defaults to -1 and message to 'unknown' when the daemon omits them. Every outbound Signal operation flows through this — send_poll, send, start_typing, add_reaction, remove_reaction — so this error is the universal surface for signal-cli failures such as unregistered numbers, untrusted identities, or attachment problems.","triggerScenarios":"Any rpc_request(method, params) call where the parsed JSON-RPC response has .error: sending to a number not on Signal ('Unregistered user' style -1 errors), sending to the bot's own number, untrusted/changed safety numbers (identity changes), invalid recipient formats, or daemon-internal failures.","commonSituations":"Recipient number not E.164 or not registered on Signal; the signal-cli account's trust store missing an identity after the recipient reinstalled; signal-cli daemon version mismatch (method names like sendPollCreate or sendTyping vary by version); daemon websocket/rpc bridge misconfigured; sending to self which signal-cli rejects.","solutions":["Check signal-cli's own logs — the RPC message mirrors a stack trace there with the real cause","Verify the recipient number in E.164 format and registered on Signal (and not the bot's own number)","For untrusted-identity errors, re-verify trust for that number (trust/new-safety-number flow) or clear the broken session","Confirm the daemon version supports the RPC method being called (typing, polls, reactions differ across versions)"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(err) = channel.send(&recipient, &text).await {\n    let msg = format!(\"{err:#}\");\n    if msg.starts_with(\"Signal RPC error -1\") && msg.contains(\"Unregistered\") {\n        return Ok(mark_unreachable(recipient)); // permanent: number not on Signal, don't retry\n    }\n    if msg.starts_with(\"Signal RPC error\") {\n        tokio::time::sleep(Duration::from_secs(3)).await;\n        return channel.send(&recipient, &text).await; // transient daemon hiccup: one retry\n    }\n    return Err(err);\n}","preventionTips":["Normalize recipient numbers to E.164 and never send to the bot's own number","Handle identity/trust errors by re-verifying safety numbers instead of resending blindly","Keep signal-cli daemon version in lockstep with the RPC method names the channel uses"],"tags":["signal","signal-cli","json-rpc","rpc-error","channel","messaging"],"backgroundTag":"signal-cli-rpc-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}