Skip to content

fix: Hands-off mode crash-loops after a SimpleX Server reinstall (7.0.0:2) - #15

Merged
helix-nine merged 3 commits into
Start9-Community:masterfrom
lundog:fix-hands-off-crash
Sep 2, 2026
Merged

fix: Hands-off mode crash-loops after a SimpleX Server reinstall (7.0.0:2)#15
helix-nine merged 3 commits into
Start9-Community:masterfrom
lundog:fix-hands-off-crash

Conversation

@lundog

@lundog lundog commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

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 --server into protocol_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:

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 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. computeStartEnv still resolves them, because that read is what registers the const watch on the SimpleX Server's address, and returns 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 removes a second failure visible in the same logs: a throwing main left 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 a cannot hold subcontainer: already destroyed rejection 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.md and the action's own description are updated to say so.

A second commit stops configureServers writing when the selection hasn't changed. applyProtocol can'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 no serverId and 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 websocket health check, and it passed as soon as the port bound — ahead of the one-shot. websocket now requires sync-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. manageProfile no longer reaches computeStartEnv, so toggling the mode changes no container env; the live syncClientSettings already below handles hands-off → managed.

Smaller ones: AGENTS.md still carried the "make no WebSocket writes at all" 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 goes through i18n() 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 --check and ncc build are clean locally.

lundog and others added 3 commits August 29, 2026 17:20
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 helix-nine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@helix-nine
helix-nine merged commit c887ead into Start9-Community:master Sep 2, 2026
3 checks passed
@lundog
lundog deleted the fix-hands-off-crash branch September 2, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants