Skip to content

Report a Stop that lands mid-action as a stop - #403

Merged
davidmckayv merged 3 commits into
CopilotKit:mainfrom
kevin9327:fix/stop-is-a-stop
Sep 6, 2026
Merged

Report a Stop that lands mid-action as a stop#403
davidmckayv merged 3 commits into
CopilotKit:mainfrom
kevin9327:fix/stop-is-a-stop

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

A person presses Stop on a click, a keystroke or a long shell command, and is told:

The assistant's computer is not running.

It is running. They stopped it.

The transport already knows how to say that, and says it only for a Stop that arrives before the
request leaves:

if (caller?.aborted) {
  throw new ComputerUnavailableError("The action was stopped.");
}

The caller's signal is then handed to fetch precisely so a Stop can land mid-flight:

signal: caller
  ? AbortSignal.any([caller, AbortSignal.timeout(timeoutMs)])
  : AbortSignal.timeout(timeoutMs),

and a fetch aborted that way rejects with an AbortError. The catch sorts errors into exactly two
kinds, and AbortError is neither:

error instanceof Error && error.name === "TimeoutError"
  ? "The assistant's computer did not respond in time."
  : "The assistant's computer is not running."

Both of those are statements about the infrastructure, and the mid-flight window is the whole point
of passing the signal at all — a Stop pressed while a 600-second shell command runs is exactly the
case the abort plumbing exists for.

Why the message matters beyond the model

It is not only what the Bot reads. The gateway writes it into the action's audit row:

await write(auditStore, {
  toolName, botId, actor, element, ref, 
  failure: error instanceof Error ? error.message : "The action failed.",
});

So a person deliberately stopping an action is recorded on the audit page as the computer being
down. That is the same class of thing as #302 and #351: a row that names something other than what
happened. It also fires whenever the browser tab closes mid-request, since Bun aborts
context.req.raw.signal then, so a closed tab is filed as an outage too.

Fix

The caller's own abort is answered first, with the sentence the pre-flight check already uses.
Anything else reaches the existing two branches untouched.

if (caller?.aborted) {
  throw new ComputerUnavailableError("The action was stopped.");
}

Reading caller.aborted rather than the thrown error's name is deliberate: it asks the question
that actually matters — did the person stop this — instead of inferring it from a DOMException name
that varies between runtimes.

Where it runs

  • New state that outlives a request? None. The signal belongs to the request being served.
  • What happens on the second replica? Unchanged. The Stop arrives on the same connection as
    the action it stops, so the process that is serving it is the one that sees the abort; nothing
    here is shared between processes.
  • Anything serialised? No.
  • Anything fanned out to a browser? No new fan-out. The corrected message travels back on
    the same response and into the same audit row as before.
  • New listener, port, or schedule? None.

Boundary and audit

  • Every acting call still goes through the gateway: untouched. This is the transport underneath
    it, and only the wording of one failure changes.
  • New refusals and new failures each write a row: the row was already written. It now says the
    action was stopped instead of naming an outage that did not happen.
  • Nothing new is trusted from the client: the abort is the server's own signal for the request
    it is serving, not a claim in a body.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Tests

Three in server/tests/computer-client.test.ts, under a new a person's Stop group:

  • a Stop that lands mid-action is reported as a stop. The stub aborts the signal it was handed and
    rejects with the AbortError a real fetch rejects with.
  • a Stop pressed before the request leaves still says the same thing, and the fetch is never sent.
  • a computer that is really unreachable still says it is not running, which is the check that the
    other branch did not move.

Against main the first fails:

Expected substring: "The action was stopped."
Received message: "The assistant's computer is not running."

How I tested

Windows 11, Bun 1.3.14. bun test server/tests/computer-client.test.ts is 21 passed, 0 failed, up
from 18 by the three new tests. The neighbouring suites are unchanged: computer-gateway 57 passed,
computer-sandbox 8, computer-provider 12, computer-session-guard 5, all with no failures.
bun run --filter server typecheck and bunx biome check are clean.

@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.

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep-reviewed against live code (correctness, governance, no vendor/secret/scale issues). Composed build+tests green. CHANGELOG/format rebase on CI-validated substance.

@davidmckayv
davidmckayv merged commit d90e6c7 into CopilotKit:main Sep 6, 2026
14 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