Repository navigation
[compass] Fix: a request queued behind a long seat operation loses its reply and detaches the seat - #24
Merged
Conversation
MCP and the CLI give a seat request 60 s, and the daemon forwards it on its one connection to the seat host with the same timeoutMs. The seat host started a fresh clock when the request arrived, so time spent queued behind another long operation, such as typing, was counted twice. A queued request could still be running when the daemon gave up on it: JsonPipeClient then closed the connection, the daemon marked the seat detached, and the reply was lost. JsonPipeClient.RequestAsync can now send the time left as timeoutMs when the request goes out (sendTimeLeft), and the daemon's forwards use it through AnodeDaemon.SendOnAsync. In the seat host, DesktopLease takes its own wait for an earlier action off before dispatch, and HandleOwnedAsync takes off a wait for the previous lease's display restore. Every operation that bounds its work by timeoutMs (typing, emulator start, Steam launch) then answers in time. No new envelope field: timeoutMs already exists on both hops, so an older peer is unaffected. Reported in Compass's Anode inbox as B-017. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…overs it SeatDisplay.RestoredAsync takes the request and spends the time it waited for the previous lease's restore, instead of SeatHost measuring around the call. "display restored after a lease" now checks that an action which waited for the restore keeps less than its 10,000 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…ed waits Review follow-up. - The lease's deadline now falls a second (or an eighth of a shorter time) before the request's timeoutMs, so an action cut short by it, such as a queued seat_wait, still answers before the daemon gives up, and the seat connection survives. Typing's own stop (7/8) comes before it, unchanged. - Waits under 100 ms aren't counted (JsonPipeClient.TimeLeft), so the time forwarded to the seat, and limits derived from it such as the 3,001 characters one call may type, no longer depend on clock ticks. - A request whose time ran out while it queued isn't sent. - A refusal of text too long for a call that waited says the call had less than the usual 60 s. - PROTOCOL.md, the CHANGELOG and comments say exactly which operations size their work by timeoutMs and that the rest are stopped before it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second review follow-up (P3s): a forwarded request left with less than 100 ms (an eighth of a shorter time), or whose deadline already fired, isn't sent, so its late reply can't cost the connection; waits under a sixteenth of a short time aren't counted either. "pipe forwarded time left" now checks the unsent path, and "agent queue time" gives its live stop 240 ms of slack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
Compass review: ready to merge Tested by compass-merge on 2026-10-07 on head
Not merged tonight only because of the five-merges-per-run cap. The next compass-merge run merges it if nothing changes. |
This was referenced Oct 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The report
Compass's Anode inbox (
ANODE-BUGS.md), B-017: long seat operations count their time from when the seat host starts them, not from the caller's deadline. Filed during #18's reviews. compass-merge's Codex review of #18 added a concrete case:seat_typecalls. The second waits about 45 s for the daemon's one seat connection, then gets a fresh 60 s clock in the seat host.JsonPipeClientcloses the seat connection, and the daemon marks the seatdetached.Root cause
MCP and the CLI give a seat request 60 s (or
timeoutMs). The daemon forwards it with the sametimeoutMson its single connection to the seat host, and that connection carries one request at a time.The seat host counted its time from when the request arrived:
DesktopLease.HandleAsyncstarted a fulltimeoutMsclock;So time spent queued in the daemon was counted twice. So was time in the seat host before the operation began: the same agent's earlier action, or the previous lease's display restore.
There was a second problem. An operation cut off by the lease's deadline, rather than sized by
timeoutMs, was stopped at the same moment the daemon gave up. Its reply usually lost the race. Then:detacheduntil someone reconnected it.The fix
Each hop hands on the time left, in the existing
timeoutMsfield, and the seat stops in time to answer.JsonPipeClient.RequestAsyncgainssendTimeLeft, which setstimeoutMsto the time left as the request goes out, after any queue. A request left with almost no time (under 100 ms, or an eighth of a shorter time) isn't sent: its reply would come too late, and a late reply costs the connection. The daemon's forwards use it throughAnodeDaemon.SendOnAsync.DesktopLeasetakes its wait for an earlier action off, throughAgentAccess.Spend.SeatDisplay.RestoredAsynctakes off the wait for a display restore.DesktopLease.StopAfterMsstops a desktop action a second (or an eighth of a shorter time) before its caller gives up. So even an action cut short, such as a queuedseat_wait, answers in time, and the connection survives. Typing's own stop at 7/8 comes earlier and is unchanged.JsonPipeClient.TimeLeft). Otherwise a clock tick would shrink the time and randomly refuse a 3,001-characterseat_typethat fits.In the reported case: the second call reaches the seat with about 15 s left. It is refused before anything is typed, with a message that says why, and the seat stays attached.
Why not the absolute deadline the inbox suggested. It needs a new envelope field. Argument checks are strict, so an older running daemon would refuse every request that carries it.
timeoutMsalready exists on both hops: it is validated as 1 to 180000, and every seat host since 0.5.0 accepts and strips it.What's left. The client-to-daemon hop still starts its clock a few milliseconds before the daemon. MCP serializes tool calls before its clock starts, and each CLI process has its own connection, so there is no queue there.
Docs: PROTOCOL.md (the envelope paragraph), the CHANGELOG, and comments in
TextTyping,SeatEmulatorsandAgentAccess.How I tested it
scripts\build.ps1 -QuickTest -OutputDirectory artifacts\pkg-build): 93/93 quick checks pass, the 91 onmainplus two new ones, on dc7cc07, 88df5aa, 9d1ccde and 7158c1e. The documentation check is consistent.timeoutMs;TimeLeftignores waits under 100 ms;DesktopLeasewith a fake clock:Spendignores a 99 ms wait;StopAfterMsgives 59,000, 7,000, 875 and 1;selftest --quick, each file restored byte for byte):Spendadding; and the restore wait not taken off, caught once it moved intoSeatDisplay. Not caught:SeatHost.HandleOwnedAsyncdropping therequestargument in its one-line call, which no quick check reaches without a seat host.TimeLeftand inSpend; and a nearly-expired request sent anyway. Not caught: the stamp skippingTimeLeft, since the check's bound for an unqueued request is loose on purpose, to keep slow CI runners from flaking.TimeLeftitself is checked exactly.git merge-tree) with [compass] Anode Business, ready to switch on: the terms and a README line that it isn't on sale yet #19, [compass] Fix: seat_observe fails on File Explorer windows ("Object reference not set to an instance of an object") #20, [compass] Fix: a packaged app's daemon keeps its state in the app's package folder, which the docs and rendering --restore missed #21, [compass] Before the launch: the README's "Known issues in 0.11.1" #22 and [compass] Fix: a seat_type call Windows refuses partway loses the count of what was typed #23: all clean.anode-controlpipe.Second review
Codex is at its usage limit until 9 October, so fresh read-only Claude subagents reviewed.
First pass, on dc7cc07: problems found. It confirmed the reported case is fixed end to end. It also confirmed that every seat-side reader of
timeoutMsreads it after the deduction, that "no new envelope field" holds, and that leaving the client-to-daemon gap is honest. Its findings, all fixed in 88df5aa and 9d1ccde:timeoutMs. The lease's deadline cut them off just as the daemon gave up, so the docs overclaimed. Fixed with the stop before the caller's deadline, and the docs now say exactly that.Second pass, on 88df5aa and 9d1ccde: all P3, nothing blocking. It confirmed that all five first-pass findings are fixed. It checked that
StopAfterMscuts no operation's own limit short in normal use: typing,seat_wait, audio,exec.start, emulator start,display.setand Steam launch. It also foundRestoredAsync's accounting and the new checks sound. Its P3s:timed_outerror code, with "nothing was done" when the action was never admitted, would be clearer.CI: green on 88df5aa, 9d1ccde and 7158c1e.
All four open fix PRs merged together on
main(#20, #21, #23, #24, locally): 95/95 quick checks, documentation consistent.Still to check in a real seat
Run two
anode typecalls of about 2,500 characters each, into Notepad, from two terminals about a second apart, with the same agent's lease. The second should come back refused inside 60 s, saying it had less time, andanode statusshould stayready.🤖 Generated with Claude Code