diff --git a/src/core/bot.ts b/src/core/bot.ts index 86024e5..8556448 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -825,12 +825,18 @@ export class LettaBot implements AgentSession { // Signal the stream loop to break this.cancelledKeys.add(convKey); - // Abort client-side stream + // Abort client-side stream and kill the session subprocess. + // abort() sends an interrupt control_request, but the CLI may not + // handle it if blocked on a long-running tool (e.g., Task subagent). + // invalidateSession() calls session.close() which kills the subprocess, + // closes the transport pump, and resolves all stream waiters with null + // -- guaranteeing the for-await loop in processMessage breaks. const session = this.sessionManager.getSession(convKey); if (session) { session.abort().catch(() => {}); log.info(`/cancel - aborted session stream (key=${convKey})`); } + this.sessionManager.invalidateSession(convKey); // Cancel server-side run (conversation-scoped) const convId = convKey === 'shared' @@ -1750,8 +1756,12 @@ export class LettaBot implements AgentSession { } lap('stream complete'); - // If cancelled, clean up partial state and return early + // If cancelled, clean up partial state and return early. + // Invalidate defensively in case the cancel handler's invalidation + // didn't fire (e.g., race with command dispatch). if (this.cancelledKeys.has(convKey)) { + this.sessionManager.invalidateSession(convKey); + session = null; if (messageId) { try { await adapter.editMessage(msg.chatId, messageId, '(Run cancelled.)');