fix(relay-node): wake idle Cursor CLI sessions with Relay messages
Problem
A Relay message sent to an idle cursor-agent session (for example dog-pipe) was staged but never woke the session. It showed up only at the session's next tool call or prompt. Cursor editor chats were fixed earlier by a stop hook that waits after a completed turn, but CLI sessions still returned from that hook at once.
Change
- The CLI
stophook waits. A Cursor CLI session'sstophook (cursor-hook --wait) now waits after a completed turn, like the editor's. It waits under thecursor-agentthat holds the conversation'sstore.db(wait::Holder::Cli), and stops waiting once that process quits or switches to another conversation. - Follow-up turns are tracked. The CLI runs no
beforeSubmitPromptfor a turn that astopfollow-up starts. Without a fix, the hook woke a session once and then never re-armed. A follow-up'sstop(loop_count > 0) for a turn that is neither current nor earlier now names that turn current once the turn before it ended (wait::stop_seen, which the third commit below made one rule for the CLI and the editor). - No five-follow-up cap. The cap that was hard-coded for CLI sessions is gone, which matches the waiting entry's
loop_limit: null.
Verification
Live testing used a real cursor-agent (2026.11.05-86047df) through the running Agent Relay Desktop core, with this branch's agent-relay-node cursor-hook entries added to ~/.cursor/hooks.json:
- Three wakes in a row. A registered, idle CLI session was messaged three times in a row (KIWI, MANGO, LYCHEE). Each message was handed over 2 to 3 seconds after sending, and each reply came back over the relay. Before the follow-up fix, only the first wake worked.
- A person types while the hook waits. The prompt runs at once, the old waiter retires, and a new one arms for the new turn.
- Ctrl-C while the hook waits. The CLI exits in about 3 seconds, and the hook and its waiter file are cleaned up.
- Headless mode.
cursor-agent -pruns no hooks at all, so headless runs are never held open. - Tests.
cargo test --lockedpasses forcrates/relay-node(2,360 lib tests plus integration suites), andcargo fmt --checkis clean. New unit tests cover a wake, a wake after a follow-up turn, a late older-turnstop, the agent quitting, and the agent switching conversation.
Known trade-off (same as the editor)
Cursor waits for every stop hook before it acts on any follow-up. While Agent Relay's hook is waiting, a followup_message returned by another user stop hook waits too, until a Relay message arrives or the wait ends after 59 minutes.
Second commit: Cursor editor chats reply and wake after Cursor-started turns
This was opened separately as #2122 and moved here because it touches the same turn tracking. It fixes three editor-side bugs.
- Replies. Cloud's hosted MCP can't send as any Cursor chat, because Cursor's MCP calls don't say which chat they come from. A Cursor session that declared
"mcp":"agent-relay-sessions"was still shown only that send tool, plus a line saying not to send any other way.relay_mcp_accessnow treats every Cursor session, editor and CLI, as socket-only. On macOS a Cursor editor conversation also gets the probe client (needs_probe_client), because Apple's curl hidesCURSOR_CONVERSATION_IDand curl replies were refused asnot_a_relay_session. - Editor turns Cursor starts itself. The editor runs no
beforeSubmitPromptfor a turn it starts itself, such as the "background task finished" turn. So.turnkept naming the previous turn, and the waitingstophook retired at once. Itsstopnow names that turn (wait::stop_seen, as revised in the third commit below), which corrects the note that "the editor names every turn as it starts". - A chat's first turn. A new chat couldn't put itself on the relay until Cursor wrote its transcript.
editor_callernow claims a conversation for a tool that names it under a verified extension host. That was already possible by runningcursor-hookfrom the same tool, and every othermay_claimcheck still applies.
Evidence (editor conversation b72f559d):
.turnheld two earlier turns after four turns. It would have held three if the 6:40 PM notification turn had runbeforeSubmitPrompt.- A message sent while the chat was idle was only handed over at its next tool call.
- Hosted
send_relay_messagefailed with "cannot verify this chat's desktop identity", whileagent-relay-probe sendfrom the same chat delivered.
Validation on this branch:
cargo testforcrates/relay-nodepasses: 2,363 lib tests plus all integration suites.cargo check --locked --no-default-features --tests,scripts/check-open-source.shandcargo fmt --checkpass, and clippy reports nothing in the touched files.- Each new test fails when its fix is reverted. I re-ran this for the merged editor call site.
Residual risk. None from timing since the third commit, which removed the 2 s window.
Third commit: review fixes
This commit addresses the review threads on this PR and on #2122.
- One clock-free rule for turns that run no
beforeSubmitPrompt. Greptile found that a follow-up's latestopcould replace a prompt submitted after it. CodeRabbit found that the editor's 2 s file-time window broke if the clock stepped back, and missed background turns that finished quickly. The fix:followup_turn_seenanduntracked_turn_seenare replaced bywait::stop_seen. Every provenstopmarks its own turn as ended in<id>.turn.- A
stopof a turn.turndoesn't know names that turn as current only once the current turn has ended, because Cursor starts a turn only after the previous one ended. - A prompt submitted in between hasn't ended, so it stays current.
- Missing, unreadable or older
.turnfiles still fail closed until the next prompt..turnstays at version 1, and files without the flag read as not ended.
- A second
cursor-agenton the same conversation. Devin, Greptile and CodeRabbit all found that a waiting CLI hook kept claiming after anothercursor-agentresumed its conversation. Before each claim,Waiting::sharednow checks the same one-holder rule ashook_caller. If another holder exists, the hook endsMovedand the message stays pending. - Channel and Connect replies. Devin found that a Cursor editor chat on macOS was handed curl for channel replies, channel subscriptions and Relay Connect replies. These now use the probe, as its direct-message replies do.
Validation:
cargo testforcrates/relay-nodepasses: 2,367 lib tests plus all integration suites.cargo check --locked --no-default-features --tests,scripts/check-open-source.shandcargo fmt --checkpass.- Reverting each fix makes its new test fail (nine mutations).
Not changed, with reasons in their threads:
- Other
stophooks' follow-ups wait for this one; see the known trade-off above. - A tool can claim a conversation no window owns yet. This was already possible through
cursor-hook. - A failed
.turnwrite stays failed closed until the next prompt.
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.