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:
@@ -357,7 +357,17 @@ chatRouter.delete('/tasks/:id', async (ctx) => {
|
|||||||
// the same thing, differing only in how hard it asks.
|
// the same thing, differing only in how hard it asks.
|
||||||
const force = ctx.req.query('force') === '1';
|
const force = ctx.req.query('force') === '1';
|
||||||
const result = await stopBackgroundTask(who, ctx.req.param('id'), force);
|
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.
|
// POST /chat/tasks/:id/restart — run this job's command again, without involving the agent.
|
||||||
|
|||||||
@@ -80,16 +80,17 @@ export const BackgroundTaskTray = ({ messages, sessionId }: BackgroundTaskTrayPr
|
|||||||
markStopRequested(taskId);
|
markStopRequested(taskId);
|
||||||
setStopAttempted((prev) => ({ ...prev, [taskId]: Date.now() }));
|
setStopAttempted((prev) => ({ ...prev, [taskId]: Date.now() }));
|
||||||
try {
|
try {
|
||||||
await client.delete(`/chat/tasks/${taskId}${force ? '?force=1' : ''}`);
|
// `gone` means the server could not find it: no output file, so no pid, so nothing running. That is
|
||||||
|
// authoritative rather than a failure — the chip was the last thing claiming otherwise, so it goes.
|
||||||
|
const result = await client.delete<{ stopped: boolean; gone?: boolean }>(
|
||||||
|
`/chat/tasks/${taskId}${force ? '?force=1' : ''}`,
|
||||||
|
);
|
||||||
|
if (result?.gone) dismiss(taskId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Kept as a fallback for a server that predates the idempotent DELETE: it answers 400 for a job it
|
||||||
|
// cannot find, and `useClient` has already raised a global toast by the time this runs — but the
|
||||||
|
// chip can at least still be cleared.
|
||||||
const message = err instanceof Error ? err.message : '';
|
const message = err instanceof Error ? err.message : '';
|
||||||
// The server saying it cannot FIND the job is authoritative, not an error to report: there is no
|
|
||||||
// output file, therefore no pid, therefore nothing running. The chip was the last thing claiming
|
|
||||||
// otherwise.
|
|
||||||
//
|
|
||||||
// Without this the tray showed a job that could not be stopped, killed or dismissed — a chip with
|
|
||||||
// no way out, because dismiss is only offered for a task the tray believes has finished. Pressing
|
|
||||||
// Stop then Kill both answered {"stopped":false,"reason":"no such task"} and changed nothing.
|
|
||||||
if (message.includes('no such task') || message.includes('already stopped')) {
|
if (message.includes('no such task') || message.includes('already stopped')) {
|
||||||
dismiss(taskId);
|
dismiss(taskId);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user