fix: Hands-off mode crash-loops after a SimpleX Server reinstall (7.0.0:2) - #15
Conversation
configureServers reads `/_servers`, mutates the group list, and writes it back unconditionally. applyProtocol cannot edit a custom row in place — it tombstones the existing rows and appends replacements — so re-applying an unchanged selection deletes and recreates every custom server on every start. The row ids climb (32 -> 34, 33 -> 35 across one restart), each start costs a chat-database write, and none of it changes what the client uses. It also contradicts the sync's own stated principle: the module docstring says live values are read first and written only on drift, which was true of syncClientSettings and never of configureServers. The selection is now projected to what the operator actually chose — tombstones dropped, each remaining row reduced to address plus enabled — snapshotted before the mutation and compared after. The projection is what makes this work: a rebuilt list is structurally different from the one it came from even when nothing moved, because the replacement rows carry no serverId, so comparing the raw structures would report drift every time. The failure mode that matters here is a wrong skip, not a wrong write, so the transitions are covered: unchanged local, changed fingerprint, custom to presets, presets to custom, unchanged presets, and one protocol changing alone. Only the four that move the selection write. The changed-fingerprint case is the one holding up the rebind fix — same host and port, new CA — and it still writes. Verified on StartOS 0.4.0 across a SimpleX Server reinstall, ordinary restarts, and Configure Client submits: server ids now hold steady when the selection is unchanged and still update when it moves.
In hands-off mode relays were passed to the container as SMP_SERVERS / XFTP_SERVERS. simplex-chat INSERTs an env `--server` into `protocol_servers`, which is unique on (user_id, host, port) and not on fingerprint — so a relay whose address is already stored under an older fingerprint aborts startup: SQLite3 returned ErrorConstraint while attempting to perform step: UNIQUE constraint failed: protocol_servers.user_id, protocol_servers.host, protocol_servers.port Reinstalling a SimpleX Server produces exactly that shape: same host, same port, new CA fingerprint. The client never opened its socket, the entrypoint timed out after 60s and exited, and the service crash-looped. Managed mode was unaffected because it passes no relay env at all and applies the selection over the operator-servers API, which replaces those rows rather than inserting. This was reachable only because the address watch started working: before the rebind fix the watcher was dead, so a fingerprint change never restarted anything and the bridge simply kept running against a stale relay. That turned a silent degradation into a crash loop, so it lands as its own fix rather than as a note against the previous one. Relays are no longer passed as env in either mode. computeStartEnv still resolves them — that read is what registers the `const` watch on the SimpleX Server's address — and hands them to the caller; the sync-settings oneshot now runs in both modes and applies them over the WS, with syncClientSettings the only part gated on managed mode. An unresolvable relay is no longer fatal in either mode. Hands-off threw only because it had no later chance to apply relays, and now it has one. That also removes a second failure: a throwing main left StartOS retrying every 10s, each attempt registering another address watch, and all ten discharged together when the dependency returned — ten restarts in one second, and a "cannot hold subcontainer: already destroyed" rejection out of the pile-up. Configure Client follows: relay changes apply live in both modes, so only file retention and switching the ownership mode still restart the service. The ownership choice now means the profile and nothing else — display name, picture, and the other chat settings — with relays owned by StartOS either way. README, instructions.md and the action's own description say so. Verified on StartOS 0.4.0 in hands-off mode with Local relays: reinstalling the SimpleX Server no longer crash-loops, the bridge picks up the new fingerprint on its own, and changing the relay selection applies without a restart.
… restart Applying relays over the WS instead of as env moves them from the container's launch to a post-ready one-shot, which opens two gaps the branch documented rather than closed. A dependent gates on the `websocket` health check, and that check passed as soon as the port bound — before the one-shot had applied anything. `websocket` now requires `sync-settings`, so a consumer never connects ahead of the relay selection. Outside clients holding a bearer token are not gated by health; README says so. And an unresolvable local relay was left non-fatal with only a console.warn. On an existing profile that is harmless, but simplex-chat uses its built-in presets whenever the DB has no rows, so on a fresh install it means running on SimpleX's public relays — the outcome README calls unacceptable. The check now reports failure while a non-public selection has not landed. The daemon still comes up, so the address watch can still rescue a fingerprint rotation without a crash loop. `manageProfile` no longer reaches `computeStartEnv`, so toggling the ownership mode changes no container env; the restart it triggered is gone and the live `syncClientSettings` below handles the hands-off to managed direction. Also: AGENTS.md still carried the rule this branch inverts, and main.ts's comment still described relays going in as env and an unresolvable one failing the start. The new console.warn went through i18n like its neighbours, the release notes lost the mechanism, and the comment blocks lost the parts that narrate the change rather than describe the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
helix-nine
left a comment
There was a problem hiding this comment.
Approving lundog's two commits.
The root-cause fix is the right one: dropping the --server env entirely and making the operator-servers API the single writer removes the mode fork rather than working around the UNIQUE constraint. computeStartEnv no longer reads manageProfile at all. The skip-when-unchanged commit is correct too — projecting to address + enabled with tombstones dropped is the only comparison that survives applyProtocol replacing rows that carry no serverId.
I pushed a third commit closing the two gaps the branch documented rather than fixed — the dependent-visible window before the one-shot lands, and a fresh install silently running on public presets when a local relay can't be resolved — plus the stale AGENTS.md rule and main.ts comment, i18n on the new warn, and the release-notes trim. Details in the PR body.
Hands-off mode crash-loops after a SimpleX Server reinstall. Relays were passed to the container as
SMP_SERVERS/XFTP_SERVERS, and simplex-chat INSERTs an env--serverintoprotocol_servers, which is unique on(user_id, host, port)and not on fingerprint. A relay whose address is already stored under an older fingerprint aborts startup:Reinstalling a SimpleX Server produces exactly that shape — same host, same port, new CA fingerprint. The client never opens its socket, the entrypoint times out after 60s and exits, and the service loops. Managed mode was unaffected: it passes no relay env and applies the selection over the operator-servers API, which replaces those rows instead of inserting.
This was only reachable because #14 made the address watch work. Before that the watcher was dead, so a fingerprint change never restarted anything and the bridge kept running against a stale relay — a silent degradation rather than a crash. Fixing the watch is what turned it into one, which is why this is a separate fix and not an amendment.
Relays are no longer passed as env in either mode.
computeStartEnvstill resolves them, because that read is what registers theconstwatch on the SimpleX Server's address, and returns them to the caller; thesync-settingsoneshot now runs in both modes and applies them over the WS, withsyncClientSettingsthe only part gated on managed mode.An unresolvable relay is no longer fatal in either mode — hands-off threw only because it had no later chance to apply relays, and now it has one. That removes a second failure visible in the same logs: a throwing
mainleft StartOS retrying every 10 seconds, each attempt registering another address watch, and all ten fired together when the dependency came back. Ten restarts in one second, plus acannot hold subcontainer: already destroyedrejection out of the pile-up.Configure Client follows from that: relay changes apply live in both modes, so only file retention still restarts the service.
The ownership choice now means the profile and nothing else — display name, picture, and the other chat settings — with relays owned by StartOS either way. The README,
instructions.mdand the action's own description are updated to say so.A second commit stops
configureServerswriting when the selection hasn't changed.applyProtocolcan't edit a custom row in place — it tombstones the old rows and appends replacements — so re-applying an unchanged selection deleted and recreated every custom server on each start, climbing the row ids (32 → 34, 33 → 35 across one restart) for no effect. The selection is now projected to address plus enabled with tombstones dropped, snapshotted before the mutation and compared after. Comparing the raw structures wouldn't work, since replacement rows carry noserverIdand would report drift every time. Transitions are covered so a wrong skip can't slip through: unchanged local, changed fingerprint, custom → presets, presets → custom, unchanged presets, and one protocol moving alone — only the four that move the selection write.Verified on StartOS 0.4.0 in hands-off mode with Local relays. Reinstalling the SimpleX Server no longer crash-loops; the bridge picks up the new fingerprint on its own; changing the relay selection applies with no restart; and across the reinstall, ordinary restarts and Configure Client submits, server ids hold steady when the selection is unchanged and still update when it moves.
Review pass (helix-nine)
Moving relays off the container's launch and onto a post-ready one-shot opens two gaps this branch documented rather than closed, so the review commit closes them.
A dependent could connect before the relays landed. Consumers gate on the
websockethealth check, and it passed as soon as the port bound — ahead of the one-shot.websocketnowrequiressync-settings, so it does not go green until the selection has been applied. Outside clients holding a bearer token are not gated by health; the README says so instead of implying the window is unavoidable.An unresolvable local relay was left non-fatal with only a
console.warn. On an existing profile that is harmless — the old rows stay. But simplex-chat falls back to its built-in presets whenever the DB has no rows, so on a fresh install it meant running on SimpleX's public relays, the outcome the README calls unacceptable. The check now reports failure while a non-public selection has not landed. The daemon still comes up, so the address watch can still rescue a fingerprint rotation without a crash loop.The ownership-mode restart is gone.
manageProfileno longer reachescomputeStartEnv, so toggling the mode changes no container env; the livesyncClientSettingsalready below handles hands-off → managed.Smaller ones:
AGENTS.mdstill carried the "make no WebSocket writes at all" rule this branch inverts, andmain.ts's comment still described relays going in as env and an unresolvable one failing the start. The newconsole.warngoes throughi18n()like its neighbours (keys 97/98, all five locales). The release notes lost the mechanism and gained the health-check change. The comment blocks lost the parts that narrate the change rather than describe the code.tsc --noEmit,prettier --checkandncc buildare clean locally.