Skip to content

fix(agent): honor a cancel that lands while a turn is being set up - #76472

Open
charlesvien wants to merge 3 commits into
masterfrom
haacked/adapter-cancel-during-setup
Open

fix(agent): honor a cancel that lands while a turn is being set up#76472
charlesvien wants to merge 3 commits into
masterfrom
haacked/adapter-cancel-during-setup

Conversation

@charlesvien

Copy link
Copy Markdown
Member

Ports PostHog/code#3809 into the monorepo (PostHog/code main is frozen after the desktop import; open PRs there are remade here). Commits preserve the original authorship.

Problem

A cancel arriving while prompt() was still setting up got silently dropped. That setup can run for seconds: ensureLocalToolsConnected awaits an MCP status RPC bounded at 5s, and an unknown slash command reloads skills before that. Incoming messages are dispatched concurrently, so a cancel landing in that window found an empty turn queue and no active turn. interrupt() had nothing to settle and armed no force-cancel backstop, so setup went on to hand the message to the SDK. The turn then ran to completion and reported end_turn, with tools still executing after the user pressed stop.

Changes

session.cancelSeq counts cancels on the session. prompt() snapshots it on entry before any await, then re-checks it once setup finishes. A higher count means the cancel targets this prompt, so it returns cancelled without pushing to the SDK. That is the last point where the turn can still be stopped, and no await separates the check from the push.

The count is the whole test. session.cancelled belongs to the session, and activateTurn clears it for whichever turn runs next, so a prompt that raced past the stalled one into the queue erases it — a local-only command such as /context skips the pre-prompt status check entirely and can get there first. The count survives that. The flag itself is deliberately left standing on this path, because an earlier turn may still be settling against it.

Cancels landing outside that window behave as before. Once a turn is queued interrupt() sweeps it, and once it is active the existing backstop applies.

cancel() now assigns interruptReason on every call, including when the client supplies none, so a turn no longer reports a stale reason left by an earlier cancel.

The Codex adapter takes cancelSeq: 0 as an inert field. It resets cancelled synchronously at prompt entry and refuses concurrent prompts, so it has no equivalent window.

The package's engines.node moves to >=22.19.0 to match the root, which the new test's Promise.withResolvers requires.

One downstream effect worth knowing about: focusing a worktree broadcasts a moving_to_worktree cancel to local sessions, and that cancel now registers in the setup window where it used to slip through. Autoresearch treats any cancelled as a user stop and pauses the run. This already happens on main whenever the cancel lands on an active turn, so the change makes it consistent rather than introducing it. Whether that case should be an interrupted with auto-recovery instead of a paused is a separate question in autoresearch.ts.

How did you test this code?

New test file covering the race, 6 cases. Neutralizing the early return fails 4 of them, so they guard the behavior rather than just describing it. The sixth case drives the concurrent interleaving: one prompt stalled in setup, a cancel, then a /context that queues and activates in that window. Putting the session.cancelled term back into the check fails that case alone.

Full @posthog/agent suite: 1648 tests across 98 files, all passing. pnpm typecheck clean.

After the port: from products/desktop/, pnpm install --frozen-lockfile, pnpm typecheck (24/24 clean) and the full @posthog/agent suite (1773 tests across 101 files, all passing, the new race file's 6 cases included).

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

N/A

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

This PR is a mechanical port of PostHog/code#3809 (author: haacked) into products/desktop/, done with Claude Code following the repo's /porting-code-prs skill: git am -3 --directory=products/desktop/ over the source patch series, preserving the original commits and authorship. It applied without conflicts; no source changes beyond the path relocation.


Created with PostHog Code

haacked added 2 commits August 2, 2026 17:34
A cancel arriving after `prompt()` was called but before the prompt reached
the SDK was silently dropped. Setup can take seconds: `ensureLocalToolsConnected`
("pre-prompt") awaits an MCP status RPC bounded at 5s, and an unknown slash
command reloads skills first. Incoming messages are dispatched concurrently, so
a cancel in that window found an empty turn queue and no active turn.
`interrupt()` had nothing to settle and armed no force-cancel backstop, and
setup went on to hand the message to the SDK, which ran the turn to completion
and reported `end_turn` while tools kept executing.

`session.cancelSeq` counts cancels. `prompt()` snapshots it on entry, before any
await, and re-checks it once setup finishes: a higher count means the cancel
targets this prompt, so it returns `cancelled` without pushing to the SDK. That
is the last point where the turn can still be stopped, and no await separates
the check from the push. `session.cancelled` is left standing, because an
earlier turn may still be settling against it; activation clears it for the next
turn that runs.

Cancels landing outside that window are unchanged. Once a turn is queued
`interrupt()` sweeps it, and once it is active the existing backstop applies.

`cancel()` now assigns `interruptReason` on every call, including when the
client supplies none, so it always describes the current cancel instead of
reporting a stale reason left by an earlier one.

Raise the package's `engines.node` to match the root's Node 22 requirement,
which the new test's `Promise.withResolvers` depends on.

Claude-Session: https://claude.ai/code/session_01KT3ZQ47bqT9mW4fi458Q6V
The bail also required `session.cancelled`, which any turn's activation
clears. A local-only command skips the pre-prompt status check the stalled
prompt is parked in, so it can queue and activate in that window; the reset
erased the very cancel the check exists to catch, and the cancelled prompt
went to the SDK anyway.

`cancelSeq` is monotonic and incremented only in `cancel()`, in the same
synchronous block that sets `cancelled`, so comparing it against the entry
snapshot survives an unrelated activation. The flag stays untouched here,
because an earlier turn may still be settling against it.

Claude-Session: https://claude.ai/code/session_01KT3ZQ47bqT9mW4fi458Q6V
@charlesvien charlesvien added the feature/desktop Feature Tag: Desktop label Aug 3, 2026
@trunk-io

trunk-io Bot commented Aug 3, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(agent): decide the setup-window canc..." | Re-trigger Greptile

@charlesvien

Copy link
Copy Markdown
Member Author

@haacked this is your PostHog/code#3809 remade in the monorepo (main there is frozen after the desktop import). Your commits are intact, only paths moved. Can you review and approve if it looks good?

@trunk-io

trunk-io Bot commented Aug 3, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@charlesvien charlesvien added the stamphog Request AI approval (no full review) label Aug 3, 2026
@stamphog

stamphog Bot commented Aug 3, 2026

Copy link
Copy Markdown

Note

🤖 stamphog reviewed 3dcfba4527b2821a031da09010d41d59ad03e28d — verdict: REFUSED

Gates denied on size/tier (297 lines/10 files, single-area fix) and a deny-list "auth" hit that's a false positive (only match is the word "authorship" in the PR description, not an auth-code change); the manifest-only Node engine bump has no scripts/lifecycle changes. The change itself is a well-tested concurrency fix in the desktop agent, but it has zero human reviews (only bot reactions), so it needs a human sign-off before merging given the gate denial.

  • 👍 on the PR from greptile-apps[bot], hex-security-app[bot].
  • Gates denied: exceeds T1 size/tier ceiling (297 lines, 10 files) and hit a deny-list keyword match (false positive: 'authorship' in description, no actual auth code touched)
  • No human review yet — only bot reactions (greptile, hex-security) and a request-for-review comment to @haacked that hasn't been answered
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list matches: auth
size 31L, 4F substantive, 297L/10F incl. docs/generated/snapshots — within ceiling
tier classified as T2-never: T2-never (297L, 10F, single-area, fix)
stamphog 2.0.0b4 .stamphog/policy.yml @ d9e4168 · reviewed head 3dcfba4

@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Aug 3, 2026
@charlesvien
charlesvien requested a review from tatoalo August 3, 2026 06:31
@charlesvien

Copy link
Copy Markdown
Member Author

@tatoalo can you review this one? haacked made it on the code repo (PostHog/code#3809) and I remade it here after the desktop import.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature/desktop Feature Tag: Desktop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants