Skip to content

fix(chat): stop active runs and cancel tool invocations#269

Merged
su-fen merged 10 commits into
Stack-Cairn:mainfrom
AlphaCatMeow:codex/fix/chat-stop-not-actually-stopping
Jul 26, 2026
Merged

fix(chat): stop active runs and cancel tool invocations#269
su-fen merged 10 commits into
Stack-Cairn:mainfrom
AlphaCatMeow:codex/fix/chat-stop-not-actually-stopping

Conversation

@AlphaCatMeow

Copy link
Copy Markdown
Contributor

Summary

  • release chat UI state immediately when a user stops an active or queued turn
  • propagate cancellation through MCP, shell, managed process, and SSH tool invocations
  • add runtime cancellation commands and token-safe cleanup in the Tauri backend
  • prevent locally cancelled gateway runs and queued turns from restarting or overwriting stopped state
  • keep desktop and gateway web chat/sidebar behavior in sync

Validation

  • pnpm --dir crates/agent-gui build
  • pnpm --dir crates/agent-gateway/web build
  • cargo check --manifest-path crates/agent-gui/src-tauri/Cargo.toml --tests
  • 72 targeted Node tests covering stop timing, gateway listeners, memory extraction, abortable invocation, MCP, shell, SSH, and sidebar state
  • git diff --check upstream/main..HEAD

Notes

The full frontend test commands exceeded the local runner's fixed 30-second execution window; the focused cancellation suites above completed successfully with no failures.

AlphaCatMeow and others added 10 commits July 25, 2026 15:15
…ed call

An abort while a call waited on the per-server serialization lock threw
before the lock's release() ran, leaving the chained tail promise
unresolved: every later call to that MCP server hung forever and only an
app restart recovered. Move the abortable wait into the same
release/cleanup scope as the call body — early release is safe because
the tail still chains behind the previous holder, so serialization for
subsequent callers is preserved.

Adds a regression test that deadlocks (2s timeout) without the fix.
finalizeConversationRun raced the history-persist barrier against the
gateway stream close and the terminal runtime snapshot inside a single
Promise.allSettled, so "done"/"cancelled" could reach the gateway before
the conversation was persisted — reverting the 26f2561 invariant that
protects WebUI clients from hydrating truncated conversations.

Serialize via finalizeChatRunInOrder: await the persist barrier first,
then run the two flushes together. The synchronous UI release and the
2s settle cap are unchanged, and the force-stop escape hatch still skips
the barrier on purpose (it exists for runs whose persistence is hung).
… path

- createToolRunId gains a uuid suffix: provider tool-call ids are not
  globally unique (local models reuse call_1 across conversations), and
  registering a duplicate run id cancels the previous registration's
  token, so two sessions could cancel each other's MCP/SSH runs.
- Drop the duplicated shell_cancel command; runtime_cancel is the single
  registry cancel entry point and shellTools reuses the shared
  requestRuntimeCancel helper (its bespoke retry loop also bailed on the
  first IPC error — now bounded retries continue past transient failures).
- Replace the 25ms wait_for_cancel polling loop with an event-driven
  ShellCancelFlag (AtomicBool + tokio Notify): blocking code still polls
  is_cancelled, async watchers await cancelled() without timers.
- sftp_transfer abort cleanup goes through invokeWithAbort onLateResult
  instead of a dangling per-signal listener: an abort during the start
  call now cancels the transfer as soon as its id surfaces.
- CI runs the shell_runner cancellation tests (previously never executed
  by the targeted Tauri test filters).
…ancels

- stopSending no longer falls back to "any running conversation": when
  the visible sending state and the running set were briefly out of
  sync, Stop could silently kill an unrelated background run.
- A gateway:chat-cancel that finds nothing to stop (no in-flight bridge
  request, no handler, no controller, not running) now consumes its own
  persistent stop intent — a leftover flag used to swallow the
  conversation's next send().
The settled-round CSS struck through and dimmed incomplete todo items in
every settled reply — including normally completed ones that
legitimately leave todos open. Keep the animation freeze for all settled
rounds, but gate the strike-through/dim on the reply actually being
aborted. Mirrored in GUI and WebUI.
# Conflicts:
#	.github/workflows/ci.yml
@su-fen
su-fen merged commit 4907847 into Stack-Cairn:main Jul 26, 2026
7 checks passed
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.

2 participants