Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/bot/commands/abort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { logger } from "../../utils/logger.js";
import { t } from "../../i18n/index.js";
import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
import { assistantRunState } from "../assistant-run-state.js";
import { markAttachedSessionIdle } from "../../attach/service.js";
import { clearPromptResponseMode } from "../handlers/prompt.js";

type SessionState = "idle" | "busy" | "not-found";

Expand Down Expand Up @@ -106,6 +108,11 @@ export async function abortCurrentOperation(

if (abortError) {
logger.warn("[Abort] Abort request failed:", abortError);
// Safety net: clear attach-manager busy + prompt-response mode even when the
// server did not confirm the abort, otherwise the bot can wedge in "busy"
// forever due to two parallel busy trackers (attachManager + foregroundState).
await markAttachedSessionIdle(currentSession.id);
clearPromptResponseMode(currentSession.id);
if (notifyUser && chatId !== null && waitingMessageId !== null) {
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_unconfirmed"));
}
Expand All @@ -128,6 +135,8 @@ export async function abortCurrentOperation(
if (finalStatus === "idle" || finalStatus === "not-found") {
foregroundSessionState.markIdle(currentSession.id);
assistantRunState.clearRun(currentSession.id, "abort_confirmed");
await markAttachedSessionIdle(currentSession.id);
clearPromptResponseMode(currentSession.id);
if (notifyUser && chatId !== null && waitingMessageId !== null) {
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.success"));
}
Expand All @@ -139,6 +148,11 @@ export async function abortCurrentOperation(
} catch (error) {
clearTimeout(timeoutId);

// Safety net for AbortError / generic errors: release attach-manager busy
// so a transient network failure does not permanently wedge the bot.
await markAttachedSessionIdle(currentSession.id);
clearPromptResponseMode(currentSession.id);

if (error instanceof Error && error.name === "AbortError") {
if (notifyUser && chatId !== null && waitingMessageId !== null) {
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_timeout"));
Expand Down
25 changes: 25 additions & 0 deletions src/bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ let botInstance: Bot<Context> | null = null;
let chatIdInstance: number | null = null;
let commandsInitialized = false;
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
let busyReconciliationTimer: ReturnType<typeof setInterval> | null = null;
let unsubscribeReadyRestore: (() => void) | null = null;

const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
Expand Down Expand Up @@ -1055,6 +1056,11 @@ export function createBot(): Bot<Context> {
heartbeatTimer = null;
}

if (busyReconciliationTimer) {
clearInterval(busyReconciliationTimer);
busyReconciliationTimer = null;
}

const botOptions = createTelegramBotOptions(config.telegram);

const bot = new Bot(config.telegram.token, botOptions);
Expand Down Expand Up @@ -1094,6 +1100,20 @@ export function createBot(): Bot<Context> {
}
}, 5000);

// Periodic busy-state reconciliation, independent of the SSE stream.
// reconcileBusyState() is normally triggered by server.heartbeat SSE events,
// but if the stream silently dies the bot can wedge in "busy" forever.
// The function has its own RECONCILE_MIN_INTERVAL_MS=10s throttle, so this
// 15s timer naturally respects it.
busyReconciliationTimer = setInterval(() => {
const currentProject = getCurrentProject();
const directory = currentProject?.worktree;
if (!directory) {
return;
}
void reconcileBusyState(directory);
}, 15_000);

// Log all API calls for diagnostics
let lastGetUpdatesTime = Date.now();
bot.api.config.use(async (prev, method, payload, signal) => {
Expand Down Expand Up @@ -1509,6 +1529,11 @@ export function cleanupBotRuntime(reason: string): void {
heartbeatTimer = null;
}

if (busyReconciliationTimer) {
clearInterval(busyReconciliationTimer);
busyReconciliationTimer = null;
}

botInstance = null;
chatIdInstance = null;
}
Loading