You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
iOS accepts premature NDJSON EOF as success and leaves a permanent typing indicator #307
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:
201 {"session_id":"clean-eof"} from POST /v1/sessions
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.
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).
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.
Activity
fix: eleven more open issues (#261 #63 #296 #289 #309 #307 #285 #317 #…
justrach commentedon Jul 31, 2026
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).
yxlyx commentedon Aug 4, 2026
Retested against
feat/mcp-streamable-http-smolifyat32cd33cfon an iPhone 17 Pro iOS 26.4 simulator (Swift 6.3.3, currentapps/iossources) on 2026-08-04.A local mock returned
201 {"session_id":"mock-session"}for session creation and thenHTTP 200 application/x-ndjsonwith 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 byType a message, with no error or terminal response. The mock received both requests. The controlled run was captured atapps/ios/test-build/live-eof.pnglocally.The current branch's
GraffServeClient.streamTurnstill unconditionally callscontinuation.finish()at EOF, andChatView.runTurnsets the turn idle without requiring a terminalturn/errorevent. The fix in14ce015is not an ancestor of this branch, so this issue is still present on the branch I tested despite this issue being marked completed.