Skip to content

fix: wait for an in-progress reply before sending after a reload - #59

Merged
jerelvelarde merged 1 commit into
CopilotKit:mainfrom
asasemahmed:fix/wait-for-active-run
Oct 6, 2026
Merged

jerelvelarde merged 1 commit into
CopilotKit:mainfrom
asasemahmed:fix/wait-for-active-run

Conversation

@asasemahmed

@asasemahmed asasemahmed commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Reloading the app while a reply is still running, then sending a message, fails with:

Thread <uuid> is locked

A Retry response button appears next to it, and retrying would ask for the earlier reply again.

The cause: hydrate() marks the conversation loaded as soon as replayed messages arrive, while connectAgent is still attached to the running reply. The next turn is therefore sent while the server still holds the thread lock. CopilotKit reports the refusal as agent_thread_locked, then again as agent_run_failed.

Now:

  • New turns stay queued until connectAgent returns. In connect mode it returns once the thread is idle, so after a reload the earlier reply finishes streaming in first, and queued messages then send automatically. The history still shows immediately, and the composer stays usable.
  • If a queued turn is still refused with agent_thread_locked (for example, another device is running the thread), the message goes back first in the queue and the queue is put on hold, instead of showing the raw lock error. Send queued messages resumes it. The server refused the turn before running it, so no work was done.
  • A choice from a choice card that is put back this way stays pending and settles when it is actually sent (runQueued does not resolve or forget it).
  • The lock filter applies only while a queued message is being sent. Retry response is not a queued turn, so a lock refusal on Retry still shows its error.
  • runConversationTurn keeps fix: keep failed turns in saved history from breaking the chat #54's ignore list and reports the first, specific failure; ConversationTurnError carries its code.

Interaction with #54

#54 ignores replayed RUN_ERROR events (agent_run_error_event) while history loads. Because connectAgent now also waits for a reply that was running before the reload, that window covers the rest of that reply: if it fails during the wait, its error is not shown as a banner, the same limitation #54 documents, now for longer. The failed turn stays in the transcript, and turns sent after the wait report errors as usual.

Verification

  • tests/conversation-sdk.test.ts (real CopilotKitCore where possible), together with fix: keep failed turns in saved history from breaking the chat #54's tests:
    • a run refused with AgentThreadLockedError rejects with code agent_thread_locked;
    • with replayed errors ignored, a later lock refusal is still the reported failure;
    • the banner rule hides replayed errors only while replaying and lock refusals only during a queued turn; a lock on Retry is shown.
  • apps/mobile/test/conversation-queue.test.ts: a refused message is restored first in line, once.
  • Rebased onto main. pnpm typecheck, biome ci on the changed files and pnpm build:web pass. pnpm test: 426 passed, 1 failed, 3 skipped. The failure is worker recovery preserves downloads when metadata cannot be inspected, which needs symlink permission on Windows and fails on main the same way.
  • The reload scenario (send a long browse request, reload, send a follow-up) was checked in the web app before this rebase: the follow-up showed as Up next, then sent and was answered, with no lock errors. It was not re-run live after the rebase.

Integration limits

Tested on web. The change is in shared chat state handling, and native layout is unchanged.

@davidmckayv

Copy link
Copy Markdown
Contributor

Needs a rebase before merge. It conflicts with #54 in chat.tsx and interacts with it: connect now spans the whole live run, which widens the window where #54 swallows a run error. Rebase onto main after #54 merges, document that interaction, and confirm Retry still surfaces an error, since the new global lock-error filter also hides lock errors from Retry.

@jerelvelarde jerelvelarde left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value: recovering an active reply across reload and queueing follow-up messages is useful. Six queue/SDK tests passed; no new security blocker identified. Please integrate current main before approval: chat.tsx, conversation-run.ts and SDK tests have substantive conflicts with the merged history-error handling and Jev runQueued wiring. Preserve those behaviors alongside reconnect/queue behavior and rerun the combined regressions.

@asasemahmed
asasemahmed force-pushed the fix/wait-for-active-run branch from 3825375 to 1614381 Compare October 6, 2026 22:15
@asasemahmed

Copy link
Copy Markdown
Contributor Author

Rebased onto main with #54's history-error handling and the Jev runQueued wiring kept:

  • Retry: the lock filter is no longer global. It applies only while a queued message is being sent, so a lock refusal on Retry response still shows its error. showsRunError holds that rule and is covered by a test.
  • Choices: a choice put back by a lock refusal stays pending in runQueued and settles when it is actually sent.
  • fix: keep failed turns in saved history from breaking the chat #54 interaction: documented in the description. Waiting in connectAgent extends the window where a replayed-style run error is not shown as a banner.
  • Combined regressions: fix: keep failed turns in saved history from breaking the chat #54's SDK tests plus the lock-code, ignore-list-plus-lock and banner-rule tests all pass.

@jerelvelarde jerelvelarde left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value: reconnect now holds queued turns until an in-progress reply completes. Template fit: current-main integration preserves history-error filtering, replay counters, Jev choice completion, and Android keyboard behavior; the description accurately states live-testing limits. Security: no introduced security issue found in the changed queue/SDK/UI paths. Verification: 19 focused tests, mobile typecheck, and formatting pass. Executing the actual runQueued handler confirms a held choice stays pending and resolves exactly once after resend. All seven CI checks pass.

@jerelvelarde
jerelvelarde merged commit 76a4678 into CopilotKit:main Oct 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants