feat(jsonrpc2): add manual receive mode to prevent message loss#835
Open
strichter wants to merge 2 commits into
Open
feat(jsonrpc2): add manual receive mode to prevent message loss#835strichter wants to merge 2 commits into
strichter wants to merge 2 commits into
Conversation
added 2 commits
April 8, 2026 16:16
In the default 'on-start' receive mode, signal-cli's daemon
auto-pulls inbound messages and pushes them to its JSON-RPC clients
the moment they arrive from the Signal servers. If no websocket
subscriber is currently attached to /v1/receive/{number} — for
example during a brief subscriber redeploy — the receiver loop in
signal-cli-rest-api drops the message via a non-blocking channel
send (see the 'no receiver' debug log in ReceiveData). signal-cli
has already acknowledged the message to Signal, so it is never
redelivered.
Add support for signal-cli's --receive-mode=manual, opt-in via the
new JSON_RPC_RECEIVE_MODE=manual environment variable. In manual
mode signal-cli does NOT auto-receive; signal-cli-rest-api now
issues subscribeReceive when a websocket subscriber attaches and
unsubscribeReceive when the last subscriber detaches. While no
subscriber is attached, signal-cli does not pull messages from the
Signal servers, so they remain buffered server-side under Signal's
normal retention rules and are delivered to the next subscriber
that attaches.
Implementation:
* jsonrpc2-helper.go: thread JSON_RPC_RECEIVE_MODE through to the
daemon launch command line.
* jsonrpc2.go: add per-account subscription tracking with
refcounting; subscribeReceive/unsubscribeReceive helpers; an
unwrap step in ReceiveData so manual-mode notifications
({subscription,result}) and auto-mode notifications (envelope
directly) reach downstream consumers in the same shape.
* client.go, api.go: thread the account number through
GetReceiveChannel so the JsonRpc2Client can attach the right
subscription.
The change is fully backward compatible: when JSON_RPC_RECEIVE_MODE
is unset or set to 'on-start', signal-cli runs in auto mode and the
new subscribeReceive code path is bypassed (account is empty, no
RPC issued).
Closes bbernhard#255 (the same root cause: no subscriber → 'no receiver'
drop).
bbernhard
reviewed
Apr 11, 2026
| } | ||
|
|
||
| if account != "" { | ||
| if err := r.acquireReceiveSubscription(account); err != nil { |
Owner
There was a problem hiding this comment.
I think I don't understand that code part here. Isn't account always non-empty? So, I guess we would end up always in this code branch, no matter which mode is selected via JSON_RPC_RECEIVE_MODE.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #834. Same root cause as the long-standing #255 ("Couldn't send message to golang channel, as there's no receiver").
Summary
In `MODE=json-rpc` with default `--receive-mode=on-start`, signal-cli auto-pulls inbound messages and pushes them as JSON-RPC notifications regardless of whether any websocket subscriber is currently attached. If no subscriber is reading the channel at the moment a notification arrives, the receiver loop in `src/client/jsonrpc2.go` drops the message via a non-blocking channel send and only logs a debug line. signal-cli has already acked the message to Signal at that point, so it is never redelivered.
This PR adds opt-in support for signal-cli's `--receive-mode=manual`, where the daemon does NOT auto-receive and clients must explicitly call `subscribeReceive` to start receiving. While no subscription is active, messages stay queued server-side under Signal's normal retention rules.
See #834 for the full reproduction, root cause analysis, and validation results.
Changes
Backward compatibility
Fully backward compatible:
Validation
Local validation against a real Signal account, real signal-cli-rest-api container in `MODE=json-rpc` mode, and a real bot consumer (nora-ng-2). Test scenario: stop the websocket client, send 10 messages from another Signal account into the group during a 60-second outage, restart the client, count delivered messages.
The signal-api log trace clearly shows the new lifecycle:
```
20:22:25 Subscribed to receive notifications for account +1XXXXXXXXXX (subscription=0)
20:22:47 Unsubscribed from receive notifications for account +1XXXXXXXXXX (subscription=0)
20:28:32 Subscribed to receive notifications for account +1XXXXXXXXXX (subscription=1)
```
Between unsubscribe (20:22:47) and resubscribe (20:28:32), 10 messages were sent into the group. After resubscribe, all 10 were delivered to the websocket client within ~10 seconds.
Notes for review
Open questions