stopping a job that is already gone is a success, not an error

The chip could not be cleared by any means, and the toast could not be silenced
from the call site: useClient raises a GLOBAL onError before it throws, so
handling the failure where Stop is pressed was always going to be too late.

DELETE is idempotent. Asking for a job to stop when it is already gone has
achieved what was asked, so "no such task" and "already stopped" now answer 200
with `gone: true` instead of 400. No error, no toast, and the client clears the
chip on the flag.

A genuine refusal — a malformed id, a subagent with no fd to trace — is still
400.

The previous client-side check stays as a fallback, for a browser talking to a
server that has not been updated yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 14:47:28 +00:00
co-authored by Claude Opus 5
parent 0f56aae803
commit 520fad2288
2 changed files with 20 additions and 9 deletions
+11 -1
View File
@@ -357,7 +357,17 @@ chatRouter.delete('/tasks/:id', async (ctx) => {
// the same thing, differing only in how hard it asks.
const force = ctx.req.query('force') === '1';
const result = await stopBackgroundTask(who, ctx.req.param('id'), force);
return ctx.json(result, result.stopped ? 200 : 400);
// "no such task" and "already stopped" are 200, not 400.
//
// DELETE is idempotent: asking for a job to stop when it is already gone has achieved what was asked.
// Reporting it as an error made the tray shout `{"stopped":false,"reason":"no such task"}` at somebody
// pressing Stop on a chip that could not be stopped, killed or dismissed — and because `useClient`
// raises a GLOBAL toast before the caller ever sees the failure, no amount of handling at the call
// site could quiet it.
//
// A genuine refusal — a malformed id, a subagent that has no fd to trace — is still 400.
const gone = result.reason === 'no such task' || result.reason === 'already stopped';
return ctx.json({ ...result, gone }, result.stopped || gone ? 200 : 400);
});
// POST /chat/tasks/:id/restart — run this job's command again, without involving the agent.