Skip to content

Fix Claude Agent re-list streams, empty-reply errors, simulator clipboard, large uploads - #1805

Merged
justrach merged 5 commits into
mainfrom
fix/yuxi-issues
Nov 2, 2026
Merged

justrach merged 5 commits into
mainfrom
fix/yuxi-issues

Conversation

@justrach

@justrach justrach commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #1804 (its commit is the first one here); merging this PR lands both.

Claude Agent: a response streaming during a tool re-list is kept (#1804)

While graff waited for Claude Code to re-read its tool list, it dropped stream events. A response already under way lost its message_start, and the turn failed with unparseable response. Lines read during that wait are now kept in order and read first by the turn. scripts/test-claude-agent.py covers it with a fake claude that streams during the re-list.

ACP: explain an exhausted empty-reply retry (#1802)

LiveTurn.errorMessage used last_api_error only for ApiError and auth failures, so EmptyCompletion reached clients as a bare error name. It now carries the explanation: the turn stopped, the conversation is kept, and sending again retries. The retry policy is unchanged. acp_live_turn.zig's tests are now compiled into the unit suite.

Simulator tests no longer overwrite the Mac's clipboard (#1801)

With the Simulator app open and Edit > Automatically Sync Pasteboard on (PasteboardAutomaticSync, the default), a simulator test that copies text replaces the host clipboard. A shell command that runs simulator tests (xcodebuild test / test-without-building, xctest) or writes a simulator pasteboard (simctl pbcopy / pbsync) is now refused in two cases: while the app is open with sync on, or when that can't be checked. The refusal asks the user to quit the app or turn sync off. graff never changes the setting and never saves or restores the clipboard. GRAFF_ALLOW_SIM_PASTEBOARD=1 turns the check off. A project's own test script is not recognized.

Large uploads are bounded by progress, not the head deadline (#1785)

Connect, upload and the response-head wait now have separate budgets (src/http_send.zig):

  • Connect: the head budget.
  • Upload: written in 16 KiB chunks. It fails after the head budget with no chunk accepted, or after head + 1 s per 16 KiB in total (15 min cap).
  • Head: measured from the last body byte, the head budget plus 1 s per 64 KiB, clamped to the read budget.

HTTP/2 uses the size-scaled total, since http-zig reports no upload progress. Esc still cancels every stage. Loopback tests cover two cases: a paced 2 MiB upload completes past the head budget, and a peer that stops reading fails within a bound.

A loaded or forked ACP chat is a peer under its own session id (#1803)

session/load and session/fork switch the chat to another save, but its peer presence record kept the process's startup name. Mail sent to the chat's ACP session id, which is how Harness reaches a working graff chat (justrach/harness#729), found no live agent. Both now re-register their labels, as /resume and /fork in the REPL already do. scripts/test-acp-session-load.py checks graff peer list after a load; v0.302.69 fails that check.

Tests

Tier 1 green locally (3627 tests).

Closes #1785, closes #1801, closes #1802.

While graff waited for Claude Code to re-read its tool list, it dropped
every stream event that arrived. When Claude Code was already streaming
the model's next response, the turn then read that response without its
message_start and failed with "unparseable response". Lines read while
waiting on a control answer are now kept in order and read first by the
turn; a cancel that arrives then drops its held call as in the turn.
…or (#1802)

A turn whose provider kept returning empty replies ended over ACP as a
bare "EmptyCompletion": LiveTurn.errorMessage used last_api_error only for
ApiError and auth failures. EmptyCompletion now carries the existing
explanation, reworded to say the turn stopped, the conversation is kept,
and sending again retries. The retry policy is unchanged.
…oard (#1801)

With the Simulator app open and Edit > Automatically Sync Pasteboard on
(PasteboardAutomaticSync, on by default), a simulator test that copies
text replaces the host clipboard, and nothing restores it. A shell command
that runs simulator tests (xcodebuild test / test-without-building,
xctest) or writes a simulator pasteboard (simctl pbcopy / pbsync) is now
refused while the app is open with sync on, or when that cannot be
checked, with an explanation that asks the user to quit the app or turn
sync off. graff never changes the setting and never saves or restores the
clipboard. GRAFF_ALLOW_SIM_PASTEBOARD=1 turns the check off.

acp_live_turn.zig's tests are now compiled into the unit suite.
…1785)

The streaming POST raced connection setup, the body upload and the response
head against one flat head deadline, so a large body still draining to a
live peer failed with HungRequest and the retry re-sent the same upload.

Each stage now has its own budget (http_send.zig). Connect: the head
budget. Upload: written in 16 KiB chunks; fails after the head budget with
no chunk accepted, or after head + 1 s per 16 KiB (15 min cap) in total.
Head: from the last body byte, the head budget plus 1 s per 64 KiB still
queued, clamped to the read budget (the WebSocket send policy). HTTP/2
gets the same stages; http-zig reports no DATA progress, so its upload
uses the size-scaled total only. Esc still cancels every stage.

A loopback peer that drains the body in paced chunks now completes past
the head budget, and one that stops reading still fails within a bound.
ACP session/load and session/fork switch the session to the loaded (or
branched) save, but the peer presence record kept the process's startup
name. Mail addressed to the chat's ACP session id, which is how Harness
reaches a working graff chat, then found no live agent. Both now
re-register their labels, as /resume and /fork in the REPL already do.
@justrach
justrach merged commit 2e69579 into main Nov 2, 2026
6 checks passed
@justrach
justrach deleted the fix/yuxi-issues branch November 2, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant