Skip to content

[Bug]: TUI drops terminal assistant.message from non-streaming providers #1056

Description

@dennisonbertram

Work type

Bug / regression

Observed behavior

On exact origin/main commit fedcf6073135deb7cce1fa49921aa698a9cc7cd7, a real tmux-hosted harnessd plus real harnesscli -tui conversation completed twice through the stock fake provider's documented non-streaming Turn.Content path. In both turns the daemon persisted the assistant reply and emitted assistant.message followed by run.completed, but the TUI rendered only the user prompts. After turn one the pane showed 0%/200K ~ $0.0010; after turn two it still showed 0%/200K ~ $0.0015, with no assistant bubble.

First run: run_e4e9aadf-fdf5-417e-bbc6-9640fd80e5aa. Second run: run_f9ea72e5-3250-4152-b65b-dc177d9d3107. Conversation: run_e4e9aadf-fdf5-417e-bbc6-9640fd80e5aa.

Expected behavior

A valid terminal assistant.message must become visible as one assistant bubble even when no assistant.message.delta events preceded it. The response must also enter the TUI transcript exactly once, so export/session behavior and subsequent turns are trustworthy. Providers that emit deltas plus the same terminal full message must not render or record duplicate content. Empty or malformed terminal payloads must remain safe no-ops. Terminal run handling and input readiness must remain unchanged.

Reproduction

Preconditions:

  1. Build harnessd and harnesscli from fedcf607.
  2. Start harnessd in tmux with HARNESS_PROVIDER=fake, HARNESS_MODEL=fake-model, auth disabled, SQLite conversation persistence, and turns containing only content, usage, and cost (no fake deltas).
  3. Start the real harnesscli -tui in a second 120x40 tmux session against that daemon.

Exact actions:

  1. Submit first luna nonstream turn.
  2. Capture the pane and query run/events/conversation messages.
  3. Submit second luna nonstream turn in the same TUI and repeat correlation.

Actual result:

  • Pane contains both user prompts and no LUNA_NONSTREAM_REPLY_ONE or LUNA_NONSTREAM_REPLY_TWO bubble.
  • Run one is completed with output LUNA_NONSTREAM_REPLY_ONE, usage 22 tokens, cost $0.001.
  • Its SSE replay contains assistant.message at sequence 8 and run.completed at sequence 12, with no assistant.message.delta.
  • Run two is completed; conversation replay contains its assistant.message at sequence 9 and run.completed at sequence 13.
  • GET /v1/conversations/.../messages returns all four user/assistant messages.

Reproduction rate: 2/2 turns.

User and operational impact

Any non-streaming provider response, streamed provider fallback that returns final content without deltas, replay path, or compatible server/client combination that legitimately supplies only terminal assistant.message looks like a blank completed turn. The user cannot tell whether the agent answered, whether the run failed, or whether it is safe to continue; TUI export also omits the answer. The daemon data is intact and there is no security or persistence loss, but the primary interaction contract is broken. API transcript inspection is a workaround, not an acceptable product path.

Suspected seam and search evidence

Owning packages/types/functions:

  • cmd/harnesscli/tui/bridge.go: decodeSSE forwards all non-terminal events, including assistant.message, as SSEEventMsg.
  • cmd/harnesscli/tui/model.go: Model.Update handles assistant.message.delta but has no assistant.message case; SSEDoneMsg records only lastAssistantText accumulated from deltas.
  • internal/fakeprovider/provider.go: Turn.Content explicitly documents the non-streaming result; Deltas are separately optional.
  • internal/harness/runner.go: the runner emits terminal assistant.message and then run.completed.

Callers/consumers searched:

  • TUI SSE bridge/model/tests; CLI raw event streaming; macOS HarnessKit.Transcript; server web viz; ACP projection; runner event emission; fake provider contract.

Similar code paths checked:

  • macOS transcript tests explicitly reconcile final assistant.message with streamed deltas.
  • Web viz has both assistant.message.delta and assistant.message cases.
  • TUI tests cover delta completion but not terminal-only content.

Commands/symbol searches used:

  • rg -n "assistant\\.message(\\.delta)?|run\\.completed" cmd internal macapp scripts
  • focused reads of cmd/harnesscli/tui/{bridge.go,model.go,sse_events_test.go}, internal/fakeprovider/provider.go, and runner emission.

Current hypothesis: the TUI event reducer lacks final-message reconciliation.

Evidence ruling out adjacent seams: raw SSE, run record, run summary, HTTP transcript, and SQLite transcript all contain the correct content; the fake provider contract explicitly separates non-streaming Content from optional streaming Deltas.

Blast-radius impact map

Callers and data flow: SSE server -> StartSSEBridgeWithOptions/decodeSSE -> SSEEventMsg -> Model.Update -> viewport and transcript. Fix the existing reducer; do not change server/provider event production.

Config/env/defaults: None — searched fake-provider and TUI configuration; reproduction uses existing documented variables and no new setting is required.

API/CLI/wire formats/tools: TUI consumes the existing assistant.message payload {content}. No endpoint, request, response, event name, schema, CLI flag, or tool change.

Persistence/schema/cache: None — HTTP and SQLite transcripts already persist correct data; no migration or repair.

Concurrency/lifecycle/cancellation/retries: preserve SSE ordering, event ID tracking, polling continuation, terminal cleanup, and run re-enablement. Reconciliation must be idempotent for delta+final and final-only sequences.

Security/auth/permissions/privacy: None — no auth, permission, secret, prompt, or trust-boundary change; only already-authorized event content is rendered.

TUI/web/macOS/other clients: TUI is affected. Web and macOS already recognize terminal messages and serve as compatibility references. Raw CLI and ACP are unchanged.

Provider/model/tool catalog: no provider or catalog change. Verify both legitimate final-only content and delta+final behavior.

Deployment/observability/runbooks: ordinary CLI binary rollout. Verification signal is pane content plus exact SSE/API correlation; rollback is reverting the small TUI reducer change.

Compatibility: additive client compatibility with existing event contract. Preserve streamed providers, malformed/empty payload behavior, multi-turn conversation IDs, export, and terminal state.

Existing tests/fixtures: extend TUI SSE reducer coverage with terminal-only and delta+terminal dedupe; use existing fake provider/live harness path for real proof.

Documentation: engineering, observational, system, long-term-thinking logs as required; plan/impact artifacts and indexes. Public API docs are unaffected because the wire contract does not change.

Regression test first

Test file and name: add an external-package behavior test under cmd/harnesscli/tui, named for terminal-only assistant.message reconciliation, plus a streamed-delta + identical terminal-message dedupe case.

Red command:
go test ./cmd/harnesscli/tui -run 'TestSSEEventMsg_AssistantMessage' -count=1

Expected failure: terminal-only content is absent from View() and Transcript(); current implementation ignores the event.

Why that proves the bug: it drives the public Update(SSEEventMsg) boundary using the exact payload delivered by the real SSE bridge, not an internal helper or source-string assertion.

False-positive controls: assert the run remains active until SSEDoneMsg; assert final-only content appears once after completion; assert delta+final content appears/records exactly once; assert malformed/empty terminal content is ignored safely.

Fix boundaries

In scope:

  • Reconcile assistant.message into the active assistant bubble when it adds missing terminal content.
  • Preserve delta streaming and transcript finalization.
  • Add deterministic reducer tests and rerun the real two-turn tmux path.

Out of scope:

  • Server/provider event changes, synthetic deltas, API/schema changes, transcript rehydration redesign, web/macOS changes, usage accounting redesign, or unrelated TUI cleanup.

Existing abstraction to repair: Model.Update's SSE event reducer and existing active assistant bubble/transcript state.

Why no parallel abstraction is needed: both streamed and terminal content already converge through lastAssistantText, renderActiveAssistantBubble, and SSEDoneMsg transcript finalization.

Diagnostic and observability evidence

Red evidence is a pane capture with two user prompts, no assistant markers, and 0%/200K, correlated with:

  • run JSON/status/output/usage/cost;
  • raw run and conversation SSE event IDs;
  • HTTP conversation transcript;
  • SQLite conversation rows;
  • harnessd startup/runtime logs.

After the fix, the same pane must show both marker replies once, transcript export/model state must contain both once, and the same API/SSE evidence must remain unchanged. Test data contains only synthetic marker text and no credentials.

Verification plan

  • Red/green: go test ./cmd/harnesscli/tui -run 'TestSSEEventMsg_AssistantMessage' -count=1.
  • Focused package: go test ./cmd/harnesscli/tui -count=1.
  • Race: go test -race ./cmd/harnesscli/tui -count=1.
  • Adjacent CLI: go test ./cmd/harnesscli/... -count=1.
  • Full: ./scripts/test-regression.sh in tmux, zero failures.
  • Real path: rebuild exact branch binaries; repeat the stock fake-provider, tmux-hosted harnessd + real TUI two-turn flow; capture pane, IDs, SSE, API transcript, SQLite, and logs. Confirm both replies render once and a third prompt remains possible.

Rollout and rollback

Release with the next normal harnesscli binary. No ordering, migration, feature flag, or data repair is needed because server and persisted contracts already contain the answer. Monitor regression/live proof for missing or duplicated assistant bubbles. Roll back by reverting the TUI reducer commit; no persisted state changes require reversal.

Documentation and handoff

Update:

  • docs/logs/long-term-thinking-log.md with command/user intent and success definition.
  • docs/logs/engineering-log.md with symptom, root cause, red/green fix, and verification.
  • docs/logs/observational-log.md with live two-turn evidence.
  • docs/logs/system-log.md with the TUI event-consumption invariant if materially changed.
  • docs/logs/INDEX.md only if descriptions materially change; plan and impact-map files plus docs/plans/INDEX.md.

Public docs/API reference: unaffected — existing wire behavior is corrected in one client.

Related #1000 only as an adjacent conversation-visibility reliability epic; this TUI-specific defect is not a scheduled-automation child and its PR must close this bug issue, not the epic.

Definition of done

  • Deterministic two-turn reproduction recorded with pane/SSE/API/SQLite/log correlation.
  • Permanent behavior-level regression test fails before the fix.
  • Existing TUI reducer is fixed without server/provider changes.
  • Final-only and delta+final paths render and export exactly once.
  • Targeted, package, race, adjacent CLI, and full regression gates pass.
  • Real tmux-hosted TUI path proves two visible replies and continued input.
  • Engineering/observational/system intent logs and plan indexes are current.
  • One reviewable PR closes this issue; no merge.

Process acknowledgement

  • I will preserve the failing regression test, update the issue if the root cause changes, and record any newly discovered bug as its own issue.

Central review acceptance amendment (PR #1059)

Review comment 3687040783 identified an additional supported sequence inside this bug's compatibility boundary: one provider step emits assistant.message.delta, then a tool card is appended and updated, and a later step emits only terminal assistant.message. The terminal reply must not use tail replacement against the intervening tool card.

Additional acceptance criteria:

  • Preserve the earlier streamed assistant bubble and the tool card exactly once, in order, when the later final-only response is reconciled.
  • Render the later terminal response as its own assistant bubble without corrupting the tool card's recorded line offsets.
  • Preserve tool-card lifecycle updates after the assistant response is appended.
  • Re-delivery/reconnect of the terminal event must remain idempotent: no duplicate assistant bubble, tool card, or transcript entry.
  • Add a behavior-level regression using the realistic delta -> tool start/output/completion -> terminal full-message event order and observe it fail before the fix.

Scope remains the existing TUI viewport/bubble ownership seam. Server, provider, persistence, schemas, web/macOS clients, and the unrelated hosted race tracked by #1044 / PR #1045 remain out of scope.

Luna exact-head finalization amendment

Luna's exact-head re-review found that repeated run.completed delivery appends the same assistant transcript entry more than once because lastAssistantText remains consumable after terminal finalization. Live evidence includes conversation run_74b26fed..., direct run run_1970e646-4d82-4e27-b42f-42cd1438511a, and the captured artifacts /tmp/luna-review-pane-tui-final.txt, /tmp/luna-review-tui-messages.json, and /tmp/luna-review-live-{run,sse,messages}.json/txt.

Additional acceptance criteria:

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions