Public: anyone can read this; the crew can change it.

/talk has Stop: a turn can be ended mid-reply and its run's processes end with it #8835

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

/talk can now stop a reply mid-answer: Send turns into Stop while a reply streams, Escape stops too, and the stop is in the API and the MCP.

shipped production is serving it deploy bbd5a0b

opened this

/talk can now stop a reply mid-answer: Send turns into Stop while a reply streams, Escape stops too, and the stop is in the API and the MCP.

Stopping ends the agent's whole process group, so anything it started goes too. What it had said stays, followed by a "Stopped." agent turn. Any Confirm still waiting is cancelled before the process goes down, so a late Confirm can't apply it. Writes held after the stop was asked are cancelled too.

What I ran: tsc, design-lint, openapi-routes, and the talk, conversation, MCP and OpenAPI-contract tests all pass, including test/mcp-size.test.ts. I did not run bun scripts/gate.ts: it timed out at 120 s here and I did not retry it. I also did not run the whole suite. scripts/fast.ts named 1079 files because the change touches src/core/fence.ts, so I ran the covering tests by hand. The forge's check runs everything on the push.

What changed

  • API: POST /api/v1/conversation/stop answers 202. It answers 409 when no answer is running, 403 for a cross-origin request or a read-only token, and 403 for the conversation's own via token.
  • MCP: the existing stop tool takes no repo or run and then stops the forge's own agent. No new tool, so the ceiling is unchanged. The agent's own token is refused.
  • Page script: Send becomes Stop (and Escape works) while a reply streams, and returns to Send when the turn ends. Enter with text still steers the running turn.
  • Tests:

- test/conversation-turn.test.ts starts a turn with the fake harness, which spawns a child process and has a held write pending. After the stop it finds the child dead, the partial turn kept with "Stopped.", and the held write unusable. - test/conversation-talk.test.ts covers the API and MCP refusals. - test/talk-page.test.ts covers the Stop button and Escape.

What I left out

  • A stopped run is recorded as failed in conversation_run, because adding a stopped state would need a migration.
  • I did not add a Stop command to the CLI or ssh talk.
  • The "Stopped." marker is an ordinary agent turn, with no special styling.

Try it: on a preview, send a long request on /talk, then press Stop or Escape.

Closes #8818

commented

/talk can now stop a reply mid-answer: Send turns into Stop while a reply streams, Escape stops too, and the stop is in the API and the MCP.

Stopping ends the agent's whole process group, so anything it started goes too. What it had said stays, followed by a "Stopped." agent turn. Any Confirm still waiting is cancelled before the process goes down, so a late Confirm can't apply it. Writes held after the stop was asked are cancelled too.

What I ran: tsc, design-lint, openapi-routes, and the talk, conversation, MCP and OpenAPI-contract tests all pass, including test/mcp-size.test.ts. I did not run bun scripts/gate.ts: it timed out at 120 s here and I did not retry it. I also did not run the whole suite. scripts/fast.ts named 1079 files because the change touches src/core/fence.ts, so I ran the covering tests by hand. The forge's check runs everything on the push.

What changed

  • API: POST /api/v1/conversation/stop answers 202. It answers 409 when no answer is running, 403 for a cross-origin request or a read-only token, and 403 for the conversation's own via token.
  • MCP: the existing stop tool takes no repo or run and then stops the forge's own agent. No new tool, so the ceiling is unchanged. The agent's own token is refused.
  • Page script: Send becomes Stop (and Escape works) while a reply streams, and returns to Send when the turn ends. Enter with text still steers the running turn.
  • Tests:

- test/conversation-turn.test.ts starts a turn with the fake harness, which spawns a child process and has a held write pending. After the stop it finds the child dead, the partial turn kept with "Stopped.", and the held write unusable. - test/conversation-talk.test.ts covers the API and MCP refusals. - test/talk-page.test.ts covers the Stop button and Escape.

What I left out

  • A stopped run is recorded as failed in conversation_run, because adding a stopped state would need a migration.
  • I did not add a Stop command to the CLI or ssh talk.
  • The "Stopped." marker is an ordinary agent turn, with no special styling.

Try it: on a preview, send a long request on /talk, then press Stop or Escape.

Changes requested — before the latest push

Two changes needed, both from the review list.

  1. Both doors (fix first): Stop exists on the page, the API and the MCP, but not in the terminal. src/tui/ssh-talk.ts has no way to stop a running turn (Ctrl-C or a /stop command, calling stopTalk). What a person can do on /talk they must be able to do over ssh. Add it with a test, and make sure the stop reaches web viewers live. The pull description calls this out as left out, but the list makes it a change request.
  1. Docs: docs/features/talk.md is untouched. Add a section on Stop, covering the button, Escape, the API route, the MCP stop with no repo, the Stopped. marker and the cancelled Confirms. Do not put it in README.

Smaller points to fold into the same round:

  • The MCP stop schema now has required: [], so repo and run are no longer required when stopping a crew run. Passing run without repo should return a clear error, not silently reach repoArg with the schema promising nothing.
  • stoppers.delete(conversation.id) in finally is unconditional, but live is guarded by run id. Guard it the same way, or a stale finish could drop a newer turn's stopper.
  • A stopped run is stored as failed, so stops are counted as failures in run vitals. If a migration is too much for this pull, open an issue for a stopped state.

The security review found no problems. The stop checks identity, same origin and token scope, and refuses the agent's own via token. It cancels held writes before the kill and again after, and the test proves the process dies. The page script works with JavaScript off, since Send still posts the form.

commented

Stop now works over ssh as well as on the page, the API and the MCP. I pushed the fixes for every point in the review to the same branch. bun x tsc --noEmit, bun scripts/gate.ts and the two conversation test files (31 tests) pass. I did not run scripts/design-lint.ts, since nothing under src/web/ changed.

  • Both doors: ssh <forge> talk /stop (or --stop) calls stopTalk instead of sending words. The change is in src/tui/ssh-talk.ts. It exits 0 when it stops a turn and 1 with "no answer is running" when none runs. Web viewers see the stop live: it goes through the same stopTurn path as the other doors, so the Stopped. line reaches them over the existing stream.
  • Docs: docs/features/talk.md has a new Stop section. It covers the button, Escape, the API route, the MCP stop with no repo, the Stopped. line, the cancelled Confirms and the ssh command. It is not in README.
  • MCP stop: giving run without repo, or repo without run, now returns an explicit error, "give both". It no longer falls through to repoArg.
  • Stopper race: stoppers now stores {runId, stop}, and finally only deletes the stopper that belongs to its own run, the same guard live has.
  • Failed vs stopped: a stopped run is still stored as failed. I did not add a migration here and opened #8847 for a stopped state.

The new tests cover /stop over ssh when nothing runs (exit 1, no turn added) and the MCP argument check. I added no ssh test where a turn really runs and is stopped. The existing test/conversation-turn.test.ts test shows a running turn's process dies on stopTurn, and ssh reaches the same stopTalk.

talk /stop is only recognised as the whole message. A sentence containing /stop is sent as normal words.

Approved

Approved. The two asks from my last round are met: ssh talk /stop calls stopTalk and docs/features/talk.md has the Stop section (README untouched), and the MCP stop rejects repo without run and the reverse. The stopper race is guarded by runId. The stop path checks identity, same origin and token scope, and the agent's own via token is refused. Held writes are cancelled before the kill and again after. The test starts a turn with the fake harness, stops it, finds the child process dead and the partial turn kept with Stopped., and finds the held write unusable.

Non-blocking: docs/features/talk.md says the API answers 200, but the route and handler answer 202. Fix the doc.

Non-blocking: no test covers an ssh stop of a turn that is really running. It shares stopTalk with the other doors, so I did not hold the pull for it.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs