Skip to content

[compass] Fix: a desktop action cut short by its deadline answers with a bare "A task was canceled." - #32

Merged
skulitom merged 6 commits into
mainfrom
compass/anode-fix-2026-10-09-deadline-reply
Oct 10, 2026
Merged

skulitom merged 6 commits into
mainfrom
compass/anode-fix-2026-10-09-deadline-reply

Conversation

@skulitom

@skulitom skulitom commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Report

From Compass's ANODE-BUGS.md inbox (not in this repo), entry B-023 (filed 2026-10-07 by compass-anode-fixer, from the second review of #24): a desktop action cut short by its deadline answers with a bare "TaskCanceledException: A task was canceled.". Since #24 the reply arrives in time, but it says neither that the request ran out of time nor whether anything ran, which an agent needs to know before it repeats an action.

Root cause

DesktopLease.HandleAsync stops a desktop action a second (or an eighth of a shorter time) before its caller gives up (#24). An admitted action that outlasts the time its request has left is cancelled through the deadline token: for example a seat_wait (10 s waitMs by default) that the daemon forwards with less than 10 s left after it waited behind a long seat_display change; DesktopWait rethrows the cancellation when it comes from that token. So is a request still queued in the seat host for the desktop behind an earlier action. Both throw OperationCanceledException, which reached JsonPipeServer.ServeAsync's catch-all and became "TaskCanceledException: A task was canceled.".

Fix

HandleAsync catches the cancellation when it is the deadline's own (deadline cancelled, the seat's stop token not) and answers errorCode: "timed_out", the time the request arrived with, and started:

{"ok": false, "errorCode": "timed_out", "started": false,
 "error": "The request ran out of its 800 ms while it waited behind an earlier desktop action, so it never started. Nothing was done."}

With started: true the text depends on what the action does:

  • read-only operations (by their tool's read-only hint: seat_wait, seat_observe, seat_screenshot, ...): "It only reads the desktop, so nothing changed, and it is safe to repeat.";
  • exec.start: "The command may have started: look for it with seat_job action=list before starting it again.";
  • everything else: "Part of it may have run: observe the desktop before acting again, and don't repeat it unseen."

A request whose time runs out before it is sent at all (in JsonPipeClient's queue, or when the daemon would forward it with almost no time left), the usual "never ran" case through the daemon, now answers timed_out with started: false too, with its old message ("... while it waited; nothing was sent"). A request that was sent and then timed out is unchanged, with no started, since it may have run.

A stopping seat (shutdown, Stop) still cancels as before. Once started, actions that bound their own work by the time keep their own failures (typing_stopped, display_restoring). The seat host logs each timeout (the lease's record line), as the catch-all used to.

docs/PROTOCOL.md (request deadlines) and docs/TROUBLESHOOTING.md ("A command timed out and the viewer is still connected") describe timed_out and started. One CHANGELOG entry. MCP clients get the failure as before: isError: true with the whole object, errorCode and started included, in structuredContent.

How it was tested

  • New quick check, agent deadline replies (injected dispatch, no seat):
    • a request queued for 800 ms behind a held action answers timed_out, started: false, "800 ms" and "Nothing was done", and is never dispatched (a counter);
    • an admitted action that ignores its time, given 1.5 s, answers started: true, "1.5 s" and "may have run";
    • a desktop.wait stopped partway says "nothing changed", not "may have run"; an exec.start points at seat_job action=list;
    • both timeouts leave a log line; the next action still runs; an action cancelled by a stopping seat still throws.
  • Extended checks: pipe queue deadline and pipe forwarded time left require timed_out and started: false on unsent requests; pipe late reply requires no started on a request that timed out after it was sent.
  • Mutations, 11 of 11 caught (each file restored byte for byte):
    1. a stopping seat's cancellation counted as a timeout (also caught by agent queue and disconnect);
    2. the old queue wait, which threw;
    3. the old admitted action, which failed with exactly TaskCanceledException: A task was canceled.;
    4. an admitted action reported as never started;
    5. the reply naming the stop time instead of the time the request had;
    6. read-only actions worded as input;
    7. exec.start worded as input;
    8. no log line;
    9. a queued, never-sent request with the old bare failure;
    10. a starved forwarded request with the old bare failure;
    11. a request sent and then timed out claiming it never started.
  • dotnet build -c Debug and selftest --quick: 98/98 on the final commit.
  • The FOCUS test command, scripts\build.ps1 -QuickTest -OutputDirectory artifacts\pkg-build: 98/98 on 429254f, and documentation links, anchors and tool names consistent.
  • No seat, lease, input, browser, Android or gamepad command ran, and the installed Anode's anode-control pipe was not touched.
  • Second reviews:
    • A fresh read-only Claude reviewer on 2793ea0: no P1. It raised one P2 (the "may have run" advice was wrong for read-only actions such as seat_wait, and a cut-short exec.start didn't point at its job) and four P3s: the doc overstated when started: false appears, since through the daemon the never-sent case ended in the pipe client; "report their own failure" holds only once started; the seat host no longer logged these; and the check didn't prove the queued request was never dispatched. All fixed in 8704adc and 429254f.
    • Codex: read-only (gpt-6-astra through bin/codex-review.sh, sandbox: read-only) on 429254f: "No actionable regressions were found in the diff or affected call paths. Deadline handling preserves shutdown cancellation and conservatively distinguishes unsent requests from potentially executed actions."

Needs a manual check in a seat

Not needed for review: the lease gate and the pipe client are covered with injected stand-ins. Reaching the seat-host case in a real seat needs a wait that outlasts the time its request has left, as above (a long seat_type does it only when a busy program stretches the typing well past its 45 s plan, since a wait behind typing that keeps to the plan still has about 15 s).

Merge note

It merges cleanly with main, with #30 (the 0.11.2 candidate) and with #31. If #30 merges first, git keeps this CHANGELOG entry inside the 0.11.2 section, which ships without this fix: move it under a new ## Unreleased heading above ## 0.11.2 when merging main in.

🤖 Generated with Claude Code

skulitom and others added 3 commits October 9, 2026 23:26
…A task was canceled."

DesktopLease stops a desktop action a second before its caller gives up.
An action that doesn't size its work by timeoutMs, or one still waiting for
the desktop behind an earlier action, was cancelled, and the pipe handler
turned the exception into "TaskCanceledException: A task was canceled.". The
reply arrived in time but didn't say the request ran out of time, or whether
anything had run.

HandleAsync now answers the deadline's own cancellation with errorCode
timed_out, the time the request had, and started: false ("Nothing was done")
for a request that never got the desktop, or started: true ("Part of it may
have run") for one stopped partway. A stopping seat still cancels as before.
A quick check ("agent deadline replies") covers both cases, the desktop
staying usable afterwards, and the stopping seat.

Reported in ANODE-BUGS.md as B-023.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…log (B-023)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ws from

Both of tonight's fixes inserted at the top of Unreleased and would conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

skulitom and others added 3 commits October 9, 2026 23:32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unsent requests

From the second review of this PR:
- A read-only action stopped partway (seat_wait, observe, screenshot) now says
  nothing changed and it is safe to repeat, from the tools' read-only hints;
  a cut-short exec.start points at seat_job action=list; only the rest says
  "Part of it may have run".
- A request whose time runs out before it is sent at all (in JsonPipeClient's
  queue, or the daemon's forwarding with almost no time left), the usual
  "never ran" case through the daemon, also answers errorCode timed_out with
  started: false.
- The seat host logs each timeout again, through the lease's record line.
- The check counts dispatches, so "Nothing was done" is proven, and covers the
  read-only and exec.start wording and the log lines; the transport checks
  require the unsent requests' errorCode and started.
- PROTOCOL: "once started" for operations that bound their own work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…art; NotSent's doc in its place

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@skulitom
skulitom merged commit 98173de into main Oct 10, 2026
1 check passed
@skulitom
skulitom deleted the compass/anode-fix-2026-10-09-deadline-reply branch October 10, 2026 00:24
@skulitom

Copy link
Copy Markdown
Owner Author

Compass review: ready to merge (merged by compass-merge, 2026-10-10)

timed_out applies only to the deadline's own cancellation: a stopping seat still throws, and the check covers that. started is false before admission and the semaphore is still released. ReadsOnly maps each op to exactly one tool, so an op with no tool falls back to the cautious "may have run". An unsent pipe request says started: false; a sent one never does.

skulitom added a commit that referenced this pull request Oct 10, 2026
#31 and #32

- scripts/test-chrome-form.ps1: the window's process went into $chrome, which in PowerShell is the
  same variable as the -Chrome parameter, so the session check always failed and the final report
  named a process instead of chrome.exe. It is now $chromeProcess.
- #31 (B-025) and #32 (B-023) merged before this candidate, so they ship in 0.11.2: the release
  notes list both, RELEASE-READINESS drops them from "Known and open", and the check count is 99.
- The changelog, readiness record and release-notes anchor date 0.11.2 to 10 October, when its
  contents were fixed again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
skulitom added a commit that referenced this pull request Oct 10, 2026
…e launch within days (#30)

* [compass] Anode 0.11.2 release candidate

- Version 0.11.2 in Anode.csproj, server.json, the MCPB manifest and the
  Claude Code plugin; CHANGELOG's Unreleased section becomes 0.11.2.
- .github/RELEASE_NOTES.md: what's new in 0.11.2, known issues, upgrade.
- docs/RELEASE-READINESS.md: a 0.11.2 section with what was checked
  without a seat, the owner's live check as exact commands (about 25
  minutes), what stays covered only by stand-ins, and the known open bugs.
- scripts/test-chrome-form.ps1: an opt-in live check for #28 and #29 on a
  local Chrome page (text field set_value read-back, <select> set_value
  failing instead of reporting success, the open list observed once).

Package manifests, INSTALL.md's pinned example and the README's known
issues change after the release, so main never points at a release that
doesn't exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [compass] 0.11.2 candidate: review fixes

- test-chrome-form.ps1: count Find-Control's results through @(...), since
  Windows PowerShell 5.1 gives a single PSCustomObject no Count; start
  Chrome inside the try, and clean up every Chrome process it started.
- RELEASE_NOTES: link the validation record on main, where the results
  are added after the release.
- RELEASE-READINESS: say which PRs are in scope; stop if no File Explorer
  window appeared.
- README: the 0.11.1 known issues now say 0.11.2 fixes them, which stays
  true inside the 0.11.2 package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: align pinned installer example with 0.11.2

* [compass] 0.11.2 candidate: fix test-chrome-form's Chrome variable; add #31 and #32

- scripts/test-chrome-form.ps1: the window's process went into $chrome, which in PowerShell is the
  same variable as the -Chrome parameter, so the session check always failed and the final report
  named a process instead of chrome.exe. It is now $chromeProcess.
- #31 (B-025) and #32 (B-023) merged before this candidate, so they ship in 0.11.2: the release
  notes list both, RELEASE-READINESS drops them from "Known and open", and the check count is 99.
- The changelog, readiness record and release-notes anchor date 0.11.2 to 10 October, when its
  contents were fixed again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant