Skip to content

Commit 355b8db

Browse files
bluedbirdclaude
andcommitted
feat: add /kill command to delete sessions
Adds a /kill <name> command that interrupts any active run, deletes the session and its message history, and auto-switches to the most recent remaining session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e23594f commit 355b8db

4 files changed

Lines changed: 83 additions & 0 deletions

File tree

packages/core/src/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,6 @@ export interface SessionStore {
9494
getActiveSession(channel: string, chatId: string): Promise<Session | null>;
9595
setSessionStatus(sessionId: string, status: import('./types').SessionStatus): Promise<void>;
9696
setTitle(sessionId: string, title: string): Promise<void>;
97+
deleteSession(sessionId: string): Promise<void>;
9798
resetStuckSessions(): Promise<number>;
9899
}

packages/gateway/src/commands.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,57 @@ export function createCommandHandler(deps: CommandDeps): CommandHandler {
109109
return true;
110110
}
111111

112+
async function cmdKill(
113+
channel: string,
114+
chatId: string,
115+
args: string,
116+
reply: ReplyFn,
117+
): Promise<true> {
118+
const nameOrId = args.trim();
119+
if (!nameOrId) {
120+
await reply('Usage: /kill <session-name>');
121+
return true;
122+
}
123+
124+
try {
125+
// Resolve the session first to get its ID for interruption
126+
const sessions = await sessionManager.listSessions(channel, chatId);
127+
const target =
128+
sessions.find((s) => s.name === nameOrId) ??
129+
sessions.find((s) => s.id.startsWith(nameOrId));
130+
131+
if (target) {
132+
await deps.agentRunner.interrupt(target.id);
133+
}
134+
135+
const { deletedId, wasActive } = await sessionManager.deleteSession(
136+
channel,
137+
chatId,
138+
nameOrId,
139+
);
140+
141+
let msg = `Killed session \`${nameOrId}\` (${deletedId.slice(0, 8)})`;
142+
if (wasActive) {
143+
const active = await sessionManager.getActiveSession(channel, chatId);
144+
msg += active
145+
? `\nSwitched to \`${sessionLabel(active)}\``
146+
: '\nNo sessions left — send a message to start one.';
147+
}
148+
await reply(msg);
149+
} catch (err) {
150+
await reply(getErrorMessage(err));
151+
}
152+
return true;
153+
}
154+
112155
async function cmdHelp(reply: ReplyFn): Promise<true> {
113156
const help = [
114157
'Available commands:',
115158
'',
116159
'/new [name] — Start a new session',
117160
'/use <name> — Switch to a session',
118161
'/sessions — List all sessions',
162+
'/kill <name> — Delete a session and its history',
119163
'/ping — Check if Homie is alive',
120164
'/status — Show system status',
121165
'/help — Show this help',
@@ -174,6 +218,8 @@ export function createCommandHandler(deps: CommandDeps): CommandHandler {
174218
return cmdUse(channel, chatId, args, reply);
175219
case 'sessions':
176220
return cmdSessions(channel, chatId, reply);
221+
case 'kill':
222+
return cmdKill(channel, chatId, args, reply);
177223
case 'ping':
178224
return cmdPing(reply);
179225
default:

packages/persistence/src/session-store.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ export function createSessionStore(db: Database): SessionStore {
154154
return rows.reverse().map(rowToMessage);
155155
},
156156

157+
async deleteSession(sessionId) {
158+
db.prepare('DELETE FROM active_sessions WHERE session_id = ?').run(sessionId);
159+
db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
160+
db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
161+
},
162+
157163
async resetSession(channel, chatId) {
158164
const active = await getActiveSession(channel, chatId);
159165
if (!active) return null;

packages/sessions/src/session-manager.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export interface SessionManager {
2424
getHistory(sessionId: string, limit: number): Promise<Message[]>;
2525
getSession(sessionId: string): Promise<Session | null>;
2626
resetSession(channel: string, chatId: string): Promise<string | null>;
27+
deleteSession(
28+
channel: string,
29+
chatId: string,
30+
nameOrId: string,
31+
): Promise<{ deletedId: string; wasActive: boolean }>;
2732
setProcessing(sessionId: string): Promise<void>;
2833
setIdle(sessionId: string): Promise<void>;
2934
setTitle(sessionId: string, title: string): Promise<void>;
@@ -103,6 +108,31 @@ export function createSessionManager(store: SessionStore): SessionManager {
103108
return store.getById(sessionId);
104109
},
105110

111+
async deleteSession(channel, chatId, nameOrId) {
112+
let session = await store.getSessionByName(channel, chatId, nameOrId);
113+
if (!session) {
114+
const all = await store.listSessionsByChat(channel, chatId);
115+
session = all.find((s) => s.id.startsWith(nameOrId)) ?? null;
116+
}
117+
if (!session) throw new Error(`No session "${nameOrId}" found`);
118+
119+
const active = await store.getActiveSession(channel, chatId);
120+
const wasActive = active?.id === session.id;
121+
122+
await store.deleteSession(session.id);
123+
124+
if (wasActive) {
125+
// Switch to the most recent remaining session
126+
const remaining = await store.listSessionsByChat(channel, chatId);
127+
if (remaining.length > 0) {
128+
await store.setActiveSession(channel, chatId, remaining[0]!.id);
129+
}
130+
}
131+
132+
log.info('Session deleted', { sessionId: session.id, name: session.name, chatId });
133+
return { deletedId: session.id, wasActive };
134+
},
135+
106136
async resetSession(channel, chatId) {
107137
const oldId = await store.resetSession(channel, chatId);
108138
if (oldId) {

0 commit comments

Comments
 (0)