Summary
In remote mode, the first message sent from the phone is silently dropped and the agent is never invoked — no subprocess, no output, no error. The CLI sits at [MessageQueue2] Waiting for messages... (the app shows "Starting new Claude session…" / "Continuing Claude session…") forever. Every subsequent message is dropped the same way, so the session is permanently stuck until you switch back to local.
The cause is client‑side, in packages/happy-cli/src/api/apiSession.ts, and is present on current main.
Environment
- happy:
1.1.10-beta.10 (also reproduces on latest 1.1.8; the code path is unchanged on main)
- Claude Code
2.1.178 · node v20.20.2 · macOS (arm64)
Steps to reproduce
- Start a session in remote mode (
happy --happy-starting-mode remote, or hand a fresh session off to the phone).
- From the phone, send the first message.
→ The message shows as sent on the phone, but the agent never runs and the CLI hangs.
Root cause
The socket new-message handler in apiSession.ts discards the first live message of a session:
// apiSession.ts (main)
if (data.body.t === 'new-message') {
const messageSeq = data.body.message?.seq;
if (this.lastSeq === 0) { // L203
this.receiveSync.invalidate(); // defer to a REST catch-up…
return; // …and drop the live message we already hold
}
if (typeof messageSeq !== 'number' || messageSeq !== this.lastSeq + 1 || data.body.message.content.t !== 'encrypted') {
this.receiveSync.invalidate();
return;
}
const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
this.routeIncomingMessage(body);
this.lastSeq = messageSeq;
}
lastSeq starts at 0 (L115) and is only ever advanced by routed messages; it is not seeded from the session's stored seq on load/connect.
- On
connect, the handler calls receiveSync.invalidate() → fetchMessages(after_seq = lastSeq = 0). For a brand‑new remote session there are no messages yet, so it routes nothing and lastSeq stays 0.
- When the first phone message then arrives live (
new-message, seq: 1, content.t: 'encrypted' — the full encrypted payload is in hand), the lastSeq === 0 branch discards it and trusts the REST catch‑up. In practice that catch‑up does not deliver the just‑pushed message, so lastSeq never leaves 0 and every later message is dropped by the same branch.
- In remote mode the run loop is blocked awaiting the message queue (
[MessageQueue2] Waiting for messages...), so query() / the agent is never started → a silent, permanent hang.
Evidence (happy 1.1.10-beta.10)
[remote]: Continuing existing session: null
[claudeRemote] Found --resume with session ID: afc27193-…
[MessageQueue2] Waiting for messages...
Socket connected successfully
[SOCKET] [UPDATE] Received update: # body.t:"new-message", message.seq:1, content.t:"encrypted"
# …nothing routed, no agent spawned, for ~5 minutes — until the user manually switched to local
This was a fresh Happy session resuming a Claude session via --resume <id>; HAPPY_RECONNECT_SESSION_ID was not set, so skipExistingMessages() was not involved (see Related).
Suggested fix
Don't throw away a message that was already received over the socket. Any of:
- In the
new-message handler, route the live message directly when content.t === 'encrypted' and it is the next expected seq — i.e. treat lastSeq === 0 && seq === 1 as valid instead of returning.
- Seed
lastSeq from the session's server‑side message seq when the session is loaded, so the gate never fires for the legitimate first message.
- Close the REST/live gap so
fetchMessages reliably returns a message that was just pushed live.
Related
Summary
In remote mode, the first message sent from the phone is silently dropped and the agent is never invoked — no subprocess, no output, no error. The CLI sits at
[MessageQueue2] Waiting for messages...(the app shows "Starting new Claude session…" / "Continuing Claude session…") forever. Every subsequent message is dropped the same way, so the session is permanently stuck until you switch back to local.The cause is client‑side, in
packages/happy-cli/src/api/apiSession.ts, and is present on currentmain.Environment
1.1.10-beta.10(also reproduces onlatest1.1.8; the code path is unchanged onmain)2.1.178· nodev20.20.2· macOS (arm64)Steps to reproduce
happy --happy-starting-mode remote, or hand a fresh session off to the phone).→ The message shows as sent on the phone, but the agent never runs and the CLI hangs.
Root cause
The socket
new-messagehandler inapiSession.tsdiscards the first live message of a session:lastSeqstarts at0(L115) and is only ever advanced by routed messages; it is not seeded from the session's stored seq on load/connect.connect, the handler callsreceiveSync.invalidate()→fetchMessages(after_seq = lastSeq = 0). For a brand‑new remote session there are no messages yet, so it routes nothing andlastSeqstays0.new-message,seq: 1,content.t: 'encrypted'— the full encrypted payload is in hand), thelastSeq === 0branch discards it and trusts the REST catch‑up. In practice that catch‑up does not deliver the just‑pushed message, solastSeqnever leaves0and every later message is dropped by the same branch.[MessageQueue2] Waiting for messages...), soquery()/ the agent is never started → a silent, permanent hang.Evidence (happy 1.1.10-beta.10)
This was a fresh Happy session resuming a Claude session via
--resume <id>;HAPPY_RECONNECT_SESSION_IDwas not set, soskipExistingMessages()was not involved (see Related).Suggested fix
Don't throw away a message that was already received over the socket. Any of:
new-messagehandler, route the live message directly whencontent.t === 'encrypted'and it is the next expected seq — i.e. treatlastSeq === 0 && seq === 1as valid instead of returning.lastSeqfrom the session's server‑side message seq when the session is loaded, so the gate never fires for the legitimate first message.fetchMessagesreliably returns a message that was just pushed live.Related
main: the CLI drops a message it did receive over the socket.lastSeq === 0/ seq‑gate pattern on the app side (happy-app/sources/sync/sync.ts);apiSession.ts(CLI) has the same shape and is unaddressed.HAPPY_RECONNECT_SESSION_IDreconnect,skipExistingMessages()(L687) makesfetchMessagesadvancelastSeqwithout routing the messages it fetches — another way the first post‑reconnect message is permanently dropped.