Stop becomes Kill when the job ignores it
A background job that ignores SIGTERM could not be stopped from the UI at all. Stop sends SIGTERM to every pid holding the job's output file, and the second click sent the same signal again — task-restart.ts already said "that is what the second click is for", and it was not. Reported after a job hung in another chat. The button now escalates instead of a second button appearing beside it. Press Stop, and if the job is still running three seconds later the same control becomes Kill in the destructive tone, and sends SIGKILL. Escalating rather than always offering Kill, because a permanent Kill button is a second destructive control competing with the first, and people reach for it by default — losing the graceful shutdown that usually works. The delay is the same argument: a clean exit takes well under a second, so offering Kill at once teaches everyone to press it first. `?force=1` on the existing DELETE rather than a second route: same verb on the same thing, differing only in how hard it asks. The member path escalates too — one `kill -KILL` run as them, since fuser and the kill must both be their own identity. Stated in the button's own title rather than hidden: SIGKILL gives the process no chance to release a port or take its children with it. That is the right trade for a hang and the reason it is not offered first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -353,7 +353,10 @@ chatRouter.get('/tasks/:id', async (ctx) => {
|
||||
// say "please stop that" is a turn, and it only works while the agent is responsive.
|
||||
chatRouter.delete('/tasks/:id', async (ctx) => {
|
||||
const who = await chatIdentity(ctx.get('user'));
|
||||
const result = await stopBackgroundTask(who, ctx.req.param('id'));
|
||||
// `?force=1` escalates to SIGKILL. A query parameter rather than a second route: it is the same verb on
|
||||
// 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);
|
||||
});
|
||||
|
||||
|
||||
@@ -767,8 +767,14 @@ export async function pidsHoldingFile(who: ChatIdentity, path: string): Promise<
|
||||
* id is validated, then matched only under this caller's own directories. An id that resolves to nothing
|
||||
* kills nothing.
|
||||
*
|
||||
* SIGTERM, not SIGKILL. A dev server with children should get the chance to take them down with it;
|
||||
* SIGKILL orphans them and leaves the port bound, which is worse than a job that took a second to stop.
|
||||
* SIGTERM by default, and SIGKILL only when asked. A dev server with children should get the chance to
|
||||
* take them down with it; SIGKILL orphans them and leaves the port bound, which is worse than a job that
|
||||
* took a second to stop.
|
||||
*
|
||||
* `force` exists because that reasoning stops applying the moment a process ignores SIGTERM: there is
|
||||
* then no graceful shutdown to protect, only a job that cannot be stopped from the UI at all. The comment
|
||||
* in task-restart.ts already said "that is what the second click is for" — this is the second click
|
||||
* actually doing something different from the first.
|
||||
*
|
||||
* Only agent-run SHELL jobs can be stopped this way. A subagent writes a `.jsonl` transcript rather than
|
||||
* holding a log open, so there is no fd to trace and this reports that rather than pretending.
|
||||
@@ -776,7 +782,8 @@ export async function pidsHoldingFile(who: ChatIdentity, path: string): Promise<
|
||||
export async function stopBackgroundTask(
|
||||
who: ChatIdentity,
|
||||
taskId: string,
|
||||
): Promise<{ stopped: boolean; reason?: string }> {
|
||||
force = false,
|
||||
): Promise<{ stopped: boolean; reason?: string; signal?: 'TERM' | 'KILL' }> {
|
||||
if (!TASK_ID_RE.test(taskId)) return { stopped: false, reason: 'bad task id' };
|
||||
const found = findTaskOutput(who, taskId);
|
||||
if (!found) return { stopped: false, reason: 'no such task' };
|
||||
|
||||
Reference in New Issue
Block a user