Skip to content

iOS accepts premature NDJSON EOF as success and leaves a permanent typing indicator #307

Closed

Description

@yxlyx

Summary

The iOS client treats an HTTP 200 NDJSON turn stream that reaches EOF without a terminal turn or error event as a successful turn. It persists the blank assistant placeholder, which continues rendering as an animated typing indicator even though streaming is false and the composer accepts another message.

Malformed NDJSON lines are also silently skipped, so a truncated/malformed stream can take the same false-success path.

Simulator reproduction

Tested on an iPhone 17 Pro simulator running iOS 26.5 with the current apps/ios/Graff sources, built into /tmp and launched with --autotest.

A controlled server returned:

  1. 201 {"session_id":"clean-eof"} from POST /v1/sessions
  2. HTTP 200 with an empty body from POST /v1/sessions/clean-eof

The mock received both requests. The app remained alive and re-enabled the composer, but left a blank assistant row displaying an indefinite typing animation with no error.

Root cause

GraffServeClient.streamTurn (apps/ios/Graff/Sources/GraffServeClient.swift:75-96) calls continuation.finish() after the line loop regardless of whether a terminal event was observed. Empty or undecodable lines are skipped.

ChatView.runTurn (ChatView.swift:119-150) creates a blank assistant placeholder before consuming the stream and treats clean completion as success. MessageBubble (ChatView.swift:220-237) decides to show TypingIndicator from message.text.isEmpty, not the actual streaming state.

Expected behavior

  • Require exactly one valid terminal turn or error event before treating a turn as complete.
  • Treat premature EOF or malformed protocol records as a transport/protocol failure.
  • Remove or replace the blank placeholder on failure.
  • Never show a typing indicator after the turn is no longer streaming.

Related

#285 covers thrown stream failures creating two assistant records. This is a separate clean-EOF/protocol-validation path where no error is thrown.

Activity

  1. justrach commented on Jul 31, 2026

    @justrach
    Owner

    Fixed in v0.0.226 (PR #328).

    The stream now distinguishes a terminal event from a bare EOF, and TypingIndicator is driven by an explicit TurnState rather than by content-emptiness. Verified on a real simulator: TURNSTATE-PASS asserts "garbage NDJSON does not decode" and "a terminal turn record decodes".

    Tests went 449 -> 575 on this release; every Zig fix carries a pinning test that was falsified independently (revert the production change, confirm the test goes red naming itself, restore).

  2. yxlyx commented on Aug 4, 2026

    @yxlyx
    CollaboratorAuthor

    Retested against feat/mcp-streamable-http-smolify at 32cd33cf on an iPhone 17 Pro iOS 26.4 simulator (Swift 6.3.3, current apps/ios sources) on 2026-08-04.

    A local mock returned 201 {"session_id":"mock-session"} for session creation and then HTTP 200 application/x-ndjson with an empty body for the turn. The app remained alive and re-enabled the composer, but the assistant placeholder remained a blank animated typing bubble; OCR showed the original user prompt followed directly by Type a message, with no error or terminal response. The mock received both requests. The controlled run was captured at apps/ios/test-build/live-eof.png locally.

    The current branch's GraffServeClient.streamTurn still unconditionally calls continuation.finish() at EOF, and ChatView.runTurn sets the turn idle without requiring a terminal turn/error event. The fix in 14ce015 is not an ancestor of this branch, so this issue is still present on the branch I tested despite this issue being marked completed.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions