fix(tui): configuring a channel starts it, and the roster stops disagreeing with itself - #497
Merged
Merged
Conversation
PR intake checks found warnings (non-blocking)Fast safe checks found advisory issues. CI lint/test/build gates still enforce merge quality.
Action items:
Run logs: https://github.com/RantAI-dev/RantAIClaw/actions/runs/31769889508 Detected blocking line issues (sample):
Detected advisory line issues (sample):
|
…reeing with itself The documented setup flow — `/setup channels` → picker → provisioner — never started the channel it had just configured. The cause was a fix for a different bug. The save handler swaps the new config into `self.config` immediately, deliberately, so the next provisioner's clone sees this provisioner's writes. The change detector then read "previous" from that already-swapped value, so previous always equalled new, `channels_changed` was always false, and the restart never fired: config written, "✓ configured" printed, live runtime untouched for the rest of the session. - The comparison is captured BEFORE the swap, and the swap stays where it is. - It compares content, not count. A token rotation is count-neutral, so rotating a leaked credential left the listener polling with the old one and printed nothing at all. - The restart fires from the save-complete branch, not only from `reload_config` — the operator no longer has to press Esc for it to happen. During the first-run wizard, which saves several channels in sequence, it is deferred to the end rather than flapping every listener once per provisioner. Both private roster copies are gone. `count_configured_channels` (15 entries, section-presence) and `channel_status_summary` (15 entries, credential-presence) are replaced by `configured_channel_count` and `channel_status_roster` beside the 16-entry `CHANNEL_CATALOG` they should always have derived from. That fixes QQ — which appeared nowhere in `app.rs`, so configuring it left the count at zero and `/channels` listed it in neither section — and the Matrix/Lark offer-versus-display mismatch, because one list cannot disagree with itself. Status honesty: - One `snapshot()` per panel build. It was read three times while building one panel, so the rows could contradict each other inside a single render. - Both early returns in `restart_channels` call `mark_terminated()`; they used to leave the state at `Starting`, which renders as "running" after 5s. - A panic in the spawned channels task now maps to `mark_failed` — it reached neither arm of the match, so the state stayed `Starting` forever. - Per-channel rows are labelled "runtime …" rather than implying per-channel knowledge the process-wide state does not carry. Widening `AutoStartState` to a per-channel map changes a contract shared with the CLI and is recorded as the follow-up.
sulthannauval
force-pushed
the
fix/tui-channel-lifecycle
branch
from
August 14, 2026 04:26
79743f9 to
2f1b05a
Compare
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.
Problem
The TUI's primary channel-setup flow —
/setup channels→ picker → provisioner— never started the channel it just configured.
The cause was a fix for a different bug. The save handler swaps the new config
into
self.configimmediately, with a comment explaining why: so the nextprovisioner's clone sees this provisioner's writes. The change detector then
computed "previous" from that already-swapped config, so previous always equalled
new,
channels_changedwas always false, and the restart never fired. Configwritten, "✓ configured" printed, live runtime untouched for the rest of the
session.
Two more made it worse. QQ appeared nowhere in
src/tui/app.rs— missingfrom both private roster copies — so configuring it left the count at zero and
/channelslisted it in neither section. And detection by count meant rotatinga leaked bot token was count-neutral: the listener kept polling with the
credential the operator had just revoked, with no status line at all.
Change
is — moving it reintroduces the bug it was added to fix.
channels_fingerprintserialises
channels_config; unprovable equality counts as a difference,because a restart costs a reconnect and a missed restart costs the session.
reload_config.During the first-run wizard (several saves in sequence) it is deferred to the
end rather than flapping every listener once per provisioner.
count_configured_channels(15 entries,section-presence) and
channel_status_summary(15 entries, credential-presence)become
configured_channel_countandchannel_status_roster, derived from thesame 16-entry
CHANNEL_CATALOGas the rest of the runtime. Thecredential-presence predicate — the better of the two — moves with them.
snapshot()per panel,mark_terminated()on bothearly returns, a panic in the channels task mapped to
mark_failed, andlooks_running()used instead of re-inlining the 5-second heuristic.Step 3's per-channel question: the acceptable option
AutoStartStatestays global and the rows are relabelled ("runtime polling","runtime stopped") so the display stops implying per-channel knowledge it does
not have. Widening the state to a per-channel map changes a contract shared with
the CLI — the plan's own STOP condition 3 — and belongs with that work, not here.
Scope deviation, stated
channel_has_credentials/channel_status_roster/configured_channel_countwere added to
src/channels/mod.rs, which the plan lists as out of scope("consume the factory, do not edit it"). Step 2 explicitly asks for the
credential predicate to become part of the shared roster rather than a TUI-local
variant, which cannot be done without adding it there. The factory itself is
untouched; these sit beside the catalog they derive from.
Drive observations (step 4)
Driven in tmux against a scratch
HOME,v0.19.0-alphabuild of this branch./setup telegramstarts the listener without leaving the TUI — ✅Provisioner completed →
System: ✓ Channel settings saved — restarting listener(s) now.appeared, and/channelswent from "1 configured" to"2 configured · 16 available" with
✓ Telegram runtime polling, in thesame session. (The token was a placeholder, so the listener then fails auth —
that is the token, not the lifecycle.)
/setup telegramwith a differentplaceholder token. Still "2 configured" (count-neutral, so the old detector
could not have fired) and a second restart line appeared: 2 restart lines
for 2 setups, one each.
listed it under "Available Channels" and
/channelsshowed✓ QQ runtime pollingunder Configured. Before this change it appearedin neither list.
The TUI owns that task internally and I had no clean way to kill it from
outside without also killing the process. The teardown paths are covered by
the
mark_terminated()calls and the relabelling, but I am not claiming alive observation I did not make.
The drive found a defect the tests did not
The first drive produced two restart lines for one
/setup: thesave-complete branch restarted, then
reload_config(which runs on Esc)restarted again — costing the Telegram 409 window twice, the exact thing the
plan warns about.
My first fix memoised the fingerprint the runtime was restarted for. It did not
work, and the second drive proved it:
save()encrypts credentials, so theconfig written to disk does not serialise identically to the one held in memory,
and the fingerprints never matched. Replaced with a one-shot flag the next
reload_configconsumes. Third drive: exactly one restart line.This is why the plan says a green test run is not sufficient evidence here.
Validation
cargo test --lib tui::→ 351 passed, 0 failed (was 346).cargo test --test tui_integration→ 4 passed.cargo test --lib channels::→ 921 passed, 0 failed, 1 ignored.cargo fmt --all -- --check→ clean.cargo check --lockedfor--no-default-features,--features hardware,--features browser-native→ all clean.a_saved_channel_change_is_detected_before_the_swapandtoken_rotation_is_detectedFAILED.CHANNEL_CATALOG→roster_includes_qq_and_matches_the_shared_catalogFAILED.Risk and rollback
is the fix; the double-restart that behaviour first produced is closed and
drive-verified.