Skip to content

fix(tui): configuring a channel starts it, and the roster stops disagreeing with itself - #497

Merged
sulthannauval merged 3 commits into
mainfrom
fix/tui-channel-lifecycle
Aug 14, 2026
Merged

fix(tui): configuring a channel starts it, and the roster stops disagreeing with itself#497
sulthannauval merged 3 commits into
mainfrom
fix/tui-channel-lifecycle

Conversation

@sulthannauval

Copy link
Copy Markdown
Member

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.config immediately, with a comment explaining why: so the next
provisioner's clone sees this provisioner's writes. The change detector then
computed "previous" from that already-swapped config, 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.

Two more made it worse. QQ appeared nowhere in src/tui/app.rs — missing
from both private roster copies — so configuring it left the count at zero and
/channels listed it in neither section. And detection by count meant rotating
a 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

  • Compare before the swap, and by content. The swap stays exactly where it
    is — moving it reintroduces the bug it was added to fix. channels_fingerprint
    serialises channels_config; unprovable equality counts as a difference,
    because a restart costs a reconnect and a missed restart costs the session.
  • Restart from the save-complete branch, not only from 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.
  • Both roster copies deleted. count_configured_channels (15 entries,
    section-presence) and channel_status_summary (15 entries, credential-presence)
    become configured_channel_count and channel_status_roster, derived from the
    same 16-entry CHANNEL_CATALOG as the rest of the runtime. The
    credential-presence predicate — the better of the two — moves with them.
  • Status honesty: one snapshot() per panel, mark_terminated() on both
    early returns, a panic in the channels task mapped to mark_failed, and
    looks_running() used instead of re-inlining the 5-second heuristic.

Step 3's per-channel question: the acceptable option

AutoStartState stays 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_count
were 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-alpha build of this branch.

  1. /setup telegram starts the listener without leaving the TUI — ✅
    Provisioner completed → System: ✓ Channel settings saved — restarting listener(s) now. appeared, and /channels went from "1 configured" to
    "2 configured · 16 available" with ✓ Telegram runtime polling, in the
    same session. (The token was a placeholder, so the listener then fails auth —
    that is the token, not the lifecycle.)
  2. Token rotation restarts — ✅ Re-ran /setup telegram with a different
    placeholder 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
    .
  3. QQ appears and is dispatched — ✅ With only QQ configured, the splash
    listed it under "Available Channels" and /channels showed
    ✓ QQ runtime polling under Configured. Before this change it appeared
    in neither list.
  4. Killing the channels task stops the "polling" claim — ❌ not observed.
    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 a
    live observation I did not make.

The drive found a defect the tests did not

The first drive produced two restart lines for one /setup: the
save-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 the
config 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_config consumes. 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.
  • Strict delta gate → No blocking strict lint issues on changed Rust lines.
  • cargo check --locked for --no-default-features, --features hardware,
    --features browser-native → all clean.
  • Mutation checks — reverted, tests proven to fail, restored:
    • the detector made unconditionally-false (the old shape) →
      a_saved_channel_change_is_detected_before_the_swap and
      token_rotation_is_detected FAILED.
    • QQ removed from CHANNEL_CATALOG
      roster_includes_qq_and_matches_the_shared_catalog FAILED.

Risk and rollback

  • Risk: MED. Channels now restart on paths that previously did nothing, which
    is the fix; the double-restart that behaviour first produced is closed and
    drive-verified.
  • Rollback: revert this PR. No config or schema change.

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

PR intake checks found warnings (non-blocking)

Fast safe checks found advisory issues. CI lint/test/build gates still enforce merge quality.

  • Missing required PR template sections: ## Summary, ## Validation Evidence (required), ## Security Impact (required), ## Privacy and Data Hygiene (required), ## Rollback Plan (required)
  • Incomplete required PR template fields: summary problem, summary why it matters, summary what changed, validation commands, security risk/mitigation, privacy status, rollback plan

Action items:

  1. Complete required PR template sections/fields.
  2. Remove tabs, trailing whitespace, and merge conflict markers from added lines.
  3. Re-run local checks before pushing:
    • ./scripts/ci/rust_quality_gate.sh
    • ./scripts/ci/rust_strict_delta_gate.sh
    • ./scripts/ci/docs_quality_gate.sh

Run logs: https://github.com/RantAI-dev/RantAIClaw/actions/runs/31769889508

Detected blocking line issues (sample):

  • none

Detected advisory line issues (sample):

  • none

@github-actions github-actions Bot added docs Auto scope: docs/markdown/template files changed. channel Auto scope: src/channels/** changed. labels Aug 14, 2026
@sulthannauval sulthannauval added the ci:full Run full CI including Lint Gate + Test even on PRs label Aug 14, 2026
@sulthannauval sulthannauval reopened this Aug 14, 2026
@github-actions github-actions Bot added size: L Auto size: 501-1000 non-doc changed lines. risk: medium Auto risk: src/** or dependency/config changes. distinguished contributor Contributor with 50+ merged PRs. labels Aug 14, 2026
…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
sulthannauval force-pushed the fix/tui-channel-lifecycle branch from 79743f9 to 2f1b05a Compare August 14, 2026 04:26
@sulthannauval
sulthannauval merged commit 97526db into main Aug 14, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel Auto scope: src/channels/** changed. ci:full Run full CI including Lint Gate + Test even on PRs distinguished contributor Contributor with 50+ merged PRs. docs Auto scope: docs/markdown/template files changed. risk: medium Auto risk: src/** or dependency/config changes. size: L Auto size: 501-1000 non-doc changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant