feat: add /stop command to kill running agent process
- /stop, /cancel, /kill aliases all supported - SIGTERM first (allows graceful AbortController cleanup), SIGKILL after 5s - Wired through session-commands → GroupQueue.killProcess()
This commit is contained in:
@@ -503,6 +503,43 @@ export class GroupQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately kill the running agent process for a group.
|
||||||
|
* Returns true if a process was killed, false if nothing was running.
|
||||||
|
*/
|
||||||
|
killProcess(groupJid: string): boolean {
|
||||||
|
const state = this.getGroup(groupJid);
|
||||||
|
const proc = state.process;
|
||||||
|
if (!proc || !this.isProcessAlive(proc)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
groupJid,
|
||||||
|
runId: state.currentRunId,
|
||||||
|
processName: state.processName,
|
||||||
|
},
|
||||||
|
'Killing agent process via /stop command',
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// SIGTERM allows the runner to call AbortController.abort() for graceful cleanup.
|
||||||
|
// Falls back to SIGKILL after 5 seconds if the process doesn't exit.
|
||||||
|
proc.kill('SIGTERM');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.isProcessAlive(proc)) {
|
||||||
|
try {
|
||||||
|
proc.kill('SIGKILL');
|
||||||
|
} catch {
|
||||||
|
/* already dead */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
} catch {
|
||||||
|
/* already dead */
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private async runForGroup(
|
private async runForGroup(
|
||||||
groupJid: string,
|
groupJid: string,
|
||||||
reason: 'messages' | 'drain',
|
reason: 'messages' | 'drain',
|
||||||
|
|||||||
@@ -581,6 +581,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
});
|
});
|
||||||
return formatRoomReviewReadyMessage(result);
|
return formatRoomReviewReadyMessage(result);
|
||||||
},
|
},
|
||||||
|
killProcess: () => deps.queue.killProcess(chatJid),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (cmdResult.handled) return cmdResult.success;
|
if (cmdResult.handled) return cmdResult.success;
|
||||||
@@ -917,7 +918,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ function makeDeps(
|
|||||||
isAdminSender: vi.fn().mockReturnValue(false),
|
isAdminSender: vi.fn().mockReturnValue(false),
|
||||||
canSenderInteract: vi.fn().mockReturnValue(true),
|
canSenderInteract: vi.fn().mockReturnValue(true),
|
||||||
markReviewReady: vi.fn().mockResolvedValue('Review snapshot updated.'),
|
markReviewReady: vi.fn().mockResolvedValue('Review snapshot updated.'),
|
||||||
|
killProcess: vi.fn().mockReturnValue(false),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export function extractSessionCommand(
|
|||||||
if (text === '/compact') return '/compact';
|
if (text === '/compact') return '/compact';
|
||||||
if (text === '/clear') return '/clear';
|
if (text === '/clear') return '/clear';
|
||||||
if (text === '/review' || text === '/review-ready') return '/review';
|
if (text === '/review' || text === '/review-ready') return '/review';
|
||||||
|
if (text === '/stop' || text === '/cancel' || text === '/kill') return '/stop';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +80,8 @@ export interface SessionCommandDeps {
|
|||||||
/** Whether the denied sender would normally be allowed to interact (for denial messages). */
|
/** Whether the denied sender would normally be allowed to interact (for denial messages). */
|
||||||
canSenderInteract: (msg: NewMessage) => boolean;
|
canSenderInteract: (msg: NewMessage) => boolean;
|
||||||
markReviewReady: () => Promise<string | null>;
|
markReviewReady: () => Promise<string | null>;
|
||||||
|
/** Kill the currently running agent process for this group. Returns true if a process was killed. */
|
||||||
|
killProcess: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resultToText(result: string | object | null | undefined): string {
|
function resultToText(result: string | object | null | undefined): string {
|
||||||
@@ -173,6 +176,17 @@ export async function handleSessionCommand(opts: {
|
|||||||
return { handled: true, success: true };
|
return { handled: true, success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command === '/stop') {
|
||||||
|
const killed = deps.killProcess();
|
||||||
|
deps.advanceCursor(cmdMsg.timestamp);
|
||||||
|
await deps.sendMessage(
|
||||||
|
killed
|
||||||
|
? 'Agent stopped.'
|
||||||
|
: 'No agent is currently running in this room.',
|
||||||
|
);
|
||||||
|
return { handled: true, success: true };
|
||||||
|
}
|
||||||
|
|
||||||
const cmdIndex = missedMessages.indexOf(cmdMsg);
|
const cmdIndex = missedMessages.indexOf(cmdMsg);
|
||||||
const preCompactMsgs = missedMessages.slice(0, cmdIndex);
|
const preCompactMsgs = missedMessages.slice(0, cmdIndex);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user