Skip to content

App messages queue silently until the local terminal Claude exits (regression from #1332 handoff deferral) #1388

Description

@k-o-n-t-o-r

Authorship note: This issue was investigated, drafted, and filed by Claude (Anthropic's AI assistant, running in Claude Code) at the request of and on behalf of this account's owner. The diagnosis below comes from reading the code and real session logs on the owner's machine.

Summary

Since PR #1332 (commit dafbd97, "fix(claude): preserve local process on remote handoff"), a message sent from the mobile app or webapp to a session that is running local interactive Claude Code no longer takes over the session. It is queued silently: nothing happens in the app, nothing is shown in the terminal, and the message is only picked up after the local Claude process exits, i.e. when the user presses Ctrl+C in the terminal.

For phone-first usage (start happy claude on the desktop, then drive the session from the phone) this breaks the core flow: the app appears hung, and the user has to walk back to the terminal and exit Claude before anything happens.

Environment

  • happy-cli 1.1.10-beta.10, built from a fork merge of upstream main at 17937dd; the relevant code path is unchanged on current upstream main (ff7a10d)
  • Linux x64, Node v26
  • Reproduced with both the mobile app and the webapp (self-hosted server, but nothing here is self-host specific)

Steps to reproduce

  1. Run happy claude in a terminal; leave the interactive Claude UI sitting idle at the prompt.
  2. From the app, send a message to this session.
  3. Observe: nothing happens. The message is queued (MessageQueue2.push(), doSwitch only sets a flag), local Claude keeps running, and there is no feedback in either the app or the terminal.
  4. Press Ctrl+C in the terminal.
  5. Only now does the loop enter remote mode, collect the queued message, and process it.

Log evidence

From a real session (timestamps preserved): the message was pushed at 14:51:25 and only collected at 15:20:29, after the terminal Claude was exited 29 minutes later.

[14:51:25.174] [MessageQueue2] push() called with mode hash: ac1744...
[14:51:25.174] [local]: doSwitch
[14:51:25.174] [MessageQueue2] push() completed. Queue size: 1
    ... no further activity; local interactive Claude keeps running idle ...
[15:20:29.498] [loop] Iteration with mode: remote        <- after Ctrl+C in the terminal
[15:20:29.531] [MessageQueue2] Collected batch of 1 messages with mode hash: ac1744...
[15:20:29.538] [claudeRemote] Thinking state changed to: true

Root cause

dafbd97 changed doSwitch in packages/happy-cli/src/claude/claudeLocalLauncher.ts from "abort the local process and switch to remote" to "set switchRequested and wait for the local process to exit naturally":

session.queue.setOnMessage((_message: string, _mode) => {
    // Remote messages request control from the app, but must not kill
    // the active local Claude Code process. The message remains queued
    // and is picked up by remote mode after local exits naturally.
    void doSwitch();
});

In interactive use the local process only exits "naturally" when the user exits it, so the queued message can wait forever.

The tradeoff

The old behavior had a real problem too: #1331 (an app message SIGTERMed local Claude and killed its running shell/monitor tasks), and before #974 the lost switch intent even presented as a crash (#982). So a plain revert just trades one bug for the other.

Proposed fix

Defer the handoff only while the local Claude is actually busy, and add feedback:

  1. If local Claude is idle (no active turn), abort and switch immediately, as before dafbd97. The CLI already tracks this state: claudeLocal reports it via onThinkingChange.
  2. If local Claude is mid-turn (thinking or running tools), keep the deferred behavior from feat: polish app session UX and Codex workflow support #1332 to protect in-flight work, but:
    • emit a session event so the app can show something like "message queued - a terminal session is active", instead of silence, and
    • hand off when the current turn finishes rather than waiting for process exit.

That preserves the #1331 fix while restoring the phone-first flow.

Workaround

We currently run a local revert of the dafbd97 behavior (immediate abort-and-switch; the queued message survives the handoff because the queue is not reset on switch). Happy to turn the proposal above into a PR if maintainers agree on a direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions