Skip to content

fix(tui): clear stale streaming status for completed unbound runs#64842

Closed
briandevans wants to merge 2 commits intoopenclaw:mainfrom
briandevans:codex/fix-tui-streaming-status-64825
Closed

fix(tui): clear stale streaming status for completed unbound runs#64842
briandevans wants to merge 2 commits intoopenclaw:mainfrom
briandevans:codex/fix-tui-streaming-status-64825

Conversation

@briandevans
Copy link
Copy Markdown
Contributor

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: the TUI can stay stuck on streaming after a run finishes if that run never bound as activeChatRunId.
  • Why it matters: completed turns look hung until the user manually clears the status, even though the reply/rendered state is already done.
  • What changed: finalizeRun() now falls back to the terminal status when no tracked runs remain, and empty local /btw finals use the same idle recovery path.
  • What did NOT change (scope boundary): concurrent-run status handling and history refresh behavior stay as-is.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: chat deltas always set streaming, but the terminal status transition only fired when wasActiveRun was true; local /btw runs can stream without ever binding as the active run, and their empty final path never had a fallback idle transition.
  • Missing detection / guardrail: there was no regression test for a local /btw run that renders delta text and then finishes with an empty final event.
  • Contributing context (if known): handleChatEvent() intentionally skips binding activeChatRunId for local /btw runs, so the edge case only appears on that unbound completion path.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/tui/tui-event-handlers.test.ts
  • Scenario the test should lock in: a local /btw run emits a visible delta, then completes with an empty final event, and the TUI status returns from streaming to idle.
  • Why this is the smallest reliable guardrail: it exercises the exact handler/state transition without needing a live model or interactive TUI session.
  • Existing test that already covers this (if any): the concurrent-run tests in the same file already cover the “do not clear another active run” side of this state machine.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Completed TUI turns that finish on the unbound local /btw path now return the status bar to idle instead of leaving it stuck on streaming.

Diagram (if applicable)

Before:
local /btw run -> delta sets streaming -> empty final arrives unbound -> status stays streaming

After:
local /btw run -> delta sets streaming -> empty final arrives unbound -> status returns to idle

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS (local dev machine)
  • Runtime/container: Node 22.17.1
  • Model/provider: N/A
  • Integration/channel (if any): TUI local /btw event-handler path
  • Relevant config (redacted): none

Steps

  1. Mark a run as local /btw so it does not bind activeChatRunId.
  2. Send a chat delta for that run so the handler renders text and sets streaming.
  3. Send the empty final event for the same run.

Expected

  • The TUI status returns to idle once the final event is processed.

Actual

  • Before this change, the status remained stuck on streaming.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: reproduced the handler seam locally before the fix (statusCalls: ["streaming"]) and after the fix (statusCalls: ["streaming", "idle"]); ran pnpm test src/tui/tui-event-handlers.test.ts, pnpm build, and pnpm check.
  • Edge cases checked: the existing concurrent-run regression in src/tui/tui-event-handlers.test.ts still passes, so another active run does not get forced to idle.
  • What you did not verify: a live interactive TUI session against a real provider.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: a terminal status fallback could clear streaming too early if another run were still in flight.
    • Mitigation: the new path only fires when no tracked runs remain, and the existing concurrent-run regression test stays green.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 11, 2026

Greptile Summary

This PR fixes a TUI bug where the streaming activity status remained stuck after a local /btw run finished without ever binding as activeChatRunId. The fix has two parts: finalizeRun gains a fallback (!state.activeChatRunId && sessionRuns.size === 0) guard (evaluated after the run is removed from state), and the BTW empty-final early-return block gets the same inline check. A focused regression test locks in the delta→empty-final→idle transition for unbound BTW runs, and the existing concurrent-run tests remain unaffected.

Confidence Score: 5/5

Safe to merge — the fix is narrowly scoped and the concurrent-run regression tests still pass.

All findings are P2. The logic is correct: both new checks are evaluated after the finalized run is removed from sessionRuns and activeChatRunId is cleared, so no early false-positive idle transition is possible during concurrent runs. The terminateRun asymmetry is a pre-existing gap explicitly out of scope.

No files require special attention.

Comments Outside Diff (1)

  1. src/tui/tui-event-handlers.ts, line 147-160 (link)

    P2 terminateRun has the same stuck-streaming gap for aborted/error unbound runs

    terminateRun (used for aborted and error states) still only clears the status when wasActiveRun is true. A local /btw run that emits a delta (setting status to streaming) and then aborts or errors will leave the status stuck on streaming — the same root cause as the bug this PR fixes, just on the terminate path. The PR explicitly scopes this out, which is reasonable for now, but it may be worth a follow-up issue to apply the same (!state.activeChatRunId && sessionRuns.size === 0) fallback here.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/tui/tui-event-handlers.ts
    Line: 147-160
    
    Comment:
    **`terminateRun` has the same stuck-streaming gap for aborted/error unbound runs**
    
    `terminateRun` (used for `aborted` and `error` states) still only clears the status when `wasActiveRun` is true. A local `/btw` run that emits a delta (setting status to `streaming`) and then aborts or errors will leave the status stuck on `streaming` — the same root cause as the bug this PR fixes, just on the terminate path. The PR explicitly scopes this out, which is reasonable for now, but it may be worth a follow-up issue to apply the same `(!state.activeChatRunId && sessionRuns.size === 0)` fallback here.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/tui/tui-event-handlers.ts
Line: 147-160

Comment:
**`terminateRun` has the same stuck-streaming gap for aborted/error unbound runs**

`terminateRun` (used for `aborted` and `error` states) still only clears the status when `wasActiveRun` is true. A local `/btw` run that emits a delta (setting status to `streaming`) and then aborts or errors will leave the status stuck on `streaming` — the same root cause as the bug this PR fixes, just on the terminate path. The PR explicitly scopes this out, which is reasonable for now, but it may be worth a follow-up issue to apply the same `(!state.activeChatRunId && sessionRuns.size === 0)` fallback here.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(tui): clear stale streaming status o..." | Re-trigger Greptile

@briandevans
Copy link
Copy Markdown
Contributor Author

Closing this because it appears blocked/unlikely to merge in its current state, and I’m preparing a narrower fresh attempt instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TUI stuck on 'streaming' indicator after run completes — finalizeRun() doesn't transition UI when wasActiveRun is false

1 participant