Skip to content

Commit 4a96190

Browse files
Geng Tangsisyphus-dev-ai
authored andcommitted
fix(bot): start Telegram prompts with promptAsync
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent b51dea4 commit 4a96190

2 files changed

Lines changed: 95 additions & 15 deletions

File tree

src/bot/handlers/prompt.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ export async function processUserPrompt(
275275
};
276276

277277
logger.info(
278-
`[Bot] Calling session.prompt (fire-and-forget) with agent=${currentAgent}, fileCount=${fileParts.length}...`,
278+
`[Bot] Calling session.promptAsync (start-only) with agent=${currentAgent}, fileCount=${fileParts.length}...`,
279279
);
280280

281281
foregroundSessionState.markBusy(currentSession.id);
@@ -292,13 +292,14 @@ export async function processUserPrompt(
292292
externalUserInputSuppressionManager.register(currentSession.id, text);
293293
}
294294

295-
// CRITICAL: DO NOT wait for session.prompt to complete.
296-
// If we wait, the handler will not finish and grammY will not call getUpdates,
297-
// which blocks receiving button callback_query updates.
298-
// The processing result will arrive via SSE events.
295+
// CRITICAL: Use the async prompt start endpoint here.
296+
// session.prompt streams the full assistant response and can outlive the original
297+
// Telegram message handler, which turns late transport failures into misleading
298+
// "failed to send" messages even after the run has already started.
299+
// The actual assistant result still arrives via the SSE event subscription.
299300
safeBackgroundTask({
300-
taskName: "session.prompt",
301-
task: () => opencodeClient.session.prompt(promptOptions),
301+
taskName: "session.promptAsync",
302+
task: () => opencodeClient.session.promptAsync(promptOptions),
302303
onSuccess: ({ error }) => {
303304
if (error) {
304305
foregroundSessionState.markIdle(currentSession.id);
@@ -307,28 +308,28 @@ export async function processUserPrompt(
307308
clearPromptResponseMode(currentSession.id);
308309
const details = formatErrorDetails(error, 6000);
309310
logger.error(
310-
"[Bot] OpenCode API returned an error for session.prompt",
311+
"[Bot] OpenCode API returned an error for session.promptAsync",
311312
promptErrorLogContext,
312313
);
313-
logger.error("[Bot] session.prompt error details:", details);
314-
logger.error("[Bot] session.prompt raw API error object:", error);
314+
logger.error("[Bot] session.promptAsync error details:", details);
315+
logger.error("[Bot] session.promptAsync raw API error object:", error);
315316

316317
// Send user-friendly error via API directly because ctx is no longer available
317318
void bot.api.sendMessage(ctx.chat!.id, t("bot.prompt_send_error")).catch(() => {});
318319
return;
319320
}
320321

321-
logger.info("[Bot] session.prompt completed");
322+
logger.info("[Bot] session.promptAsync accepted");
322323
},
323324
onError: (error) => {
324325
foregroundSessionState.markIdle(currentSession.id);
325326
void markAttachedSessionIdle(currentSession.id);
326327
assistantRunState.clearRun(currentSession.id, "session_prompt_background_error");
327328
clearPromptResponseMode(currentSession.id);
328329
const details = formatErrorDetails(error, 6000);
329-
logger.error("[Bot] session.prompt background task failed", promptErrorLogContext);
330-
logger.error("[Bot] session.prompt background failure details:", details);
331-
logger.error("[Bot] session.prompt raw background error object:", error);
330+
logger.error("[Bot] session.promptAsync background task failed", promptErrorLogContext);
331+
logger.error("[Bot] session.promptAsync background failure details:", details);
332+
logger.error("[Bot] session.promptAsync raw background error object:", error);
332333
void bot.api.sendMessage(ctx.chat!.id, t("bot.prompt_send_error")).catch(() => {});
333334
},
334335
});

tests/bot/handlers/prompt.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const mocked = vi.hoisted(() => ({
1111
} as { id: string; title: string; directory: string } | null,
1212
sessionStatusMock: vi.fn(),
1313
sessionPromptMock: vi.fn(),
14+
sessionPromptAsyncMock: vi.fn(),
1415
sessionCreateMock: vi.fn(),
1516
suppressionRegisterMock: vi.fn(),
1617
safeBackgroundTaskMock: vi.fn(),
@@ -24,6 +25,7 @@ vi.mock("../../../src/opencode/client.js", () => ({
2425
session: {
2526
status: mocked.sessionStatusMock,
2627
prompt: mocked.sessionPromptMock,
28+
promptAsync: mocked.sessionPromptAsyncMock,
2729
create: mocked.sessionCreateMock,
2830
},
2931
},
@@ -144,11 +146,25 @@ function createContext(): Context {
144146

145147
function createDeps(): ProcessPromptDeps {
146148
return {
147-
bot: { api: { sendMessage: vi.fn() } } as unknown as Bot<Context>,
149+
bot: { api: { sendMessage: vi.fn().mockResolvedValue(undefined) } } as unknown as Bot<Context>,
148150
ensureEventSubscription: vi.fn().mockResolvedValue(undefined),
149151
};
150152
}
151153

154+
function getScheduledBackgroundTask(): {
155+
task: () => Promise<unknown>;
156+
onSuccess?: (value: { error: unknown | null }) => void;
157+
onError?: (error: unknown) => void;
158+
} {
159+
const [[options]] = mocked.safeBackgroundTaskMock.mock.calls as [[{
160+
task: () => Promise<unknown>;
161+
onSuccess?: (value: { error: unknown | null }) => void;
162+
onError?: (error: unknown) => void;
163+
}]];
164+
165+
return options;
166+
}
167+
152168
describe("bot/handlers/prompt", () => {
153169
beforeEach(() => {
154170
mocked.currentProject = { id: "project-1", worktree: "D:\\Projects\\Repo" };
@@ -159,6 +175,7 @@ describe("bot/handlers/prompt", () => {
159175
};
160176
mocked.sessionStatusMock.mockReset();
161177
mocked.sessionPromptMock.mockReset();
178+
mocked.sessionPromptAsyncMock.mockReset();
162179
mocked.sessionCreateMock.mockReset();
163180
mocked.suppressionRegisterMock.mockReset();
164181
mocked.safeBackgroundTaskMock.mockReset();
@@ -179,6 +196,7 @@ describe("bot/handlers/prompt", () => {
179196
error: null,
180197
});
181198
mocked.sessionPromptMock.mockResolvedValue({ data: {}, error: null });
199+
mocked.sessionPromptAsyncMock.mockResolvedValue({ data: {}, error: null });
182200
});
183201

184202
it("registers suppression entry for text prompts", async () => {
@@ -198,6 +216,67 @@ describe("bot/handlers/prompt", () => {
198216
expect(mocked.suppressionRegisterMock).toHaveBeenCalledWith("session-1", "Review README");
199217
});
200218

219+
it("starts prompts through promptAsync instead of the streaming prompt endpoint", async () => {
220+
const handled = await processUserPrompt(createContext(), "Review README", createDeps());
221+
222+
expect(handled).toBe(true);
223+
224+
const backgroundTask = getScheduledBackgroundTask();
225+
await backgroundTask.task();
226+
227+
expect(mocked.sessionPromptAsyncMock).toHaveBeenCalledWith({
228+
sessionID: "session-1",
229+
directory: "D:\\Projects\\Repo",
230+
parts: [{ type: "text", text: "Review README" }],
231+
agent: "build",
232+
model: {
233+
providerID: "openai",
234+
modelID: "gpt-5",
235+
},
236+
variant: "default",
237+
});
238+
expect(mocked.sessionPromptMock).not.toHaveBeenCalled();
239+
});
240+
241+
it("still notifies the user when promptAsync reports a real start error", async () => {
242+
const ctx = createContext();
243+
const deps = createDeps();
244+
245+
const handled = await processUserPrompt(ctx, "Review README", deps);
246+
247+
expect(handled).toBe(true);
248+
249+
const backgroundTask = getScheduledBackgroundTask();
250+
backgroundTask.onSuccess?.({ error: new Error("request start failed") });
251+
252+
expect(deps.bot.api.sendMessage).toHaveBeenCalledWith(
253+
777,
254+
"Failed to send request to OpenCode.",
255+
);
256+
});
257+
258+
it("still notifies the user when promptAsync rejects before the run starts", async () => {
259+
const ctx = createContext();
260+
const deps = createDeps();
261+
262+
const handled = await processUserPrompt(ctx, "Review README", deps);
263+
264+
expect(handled).toBe(true);
265+
266+
const backgroundTask = getScheduledBackgroundTask();
267+
const startError = new Error("network down");
268+
mocked.sessionPromptAsyncMock.mockRejectedValueOnce(startError);
269+
270+
await backgroundTask.task().catch((error) => {
271+
backgroundTask.onError?.(error);
272+
});
273+
274+
expect(deps.bot.api.sendMessage).toHaveBeenCalledWith(
275+
777,
276+
"Failed to send request to OpenCode.",
277+
);
278+
});
279+
201280
it("does not register suppression entry for file-only prompts", async () => {
202281
const handled = await processUserPrompt(createContext(), "", createDeps(), [
203282
{

0 commit comments

Comments
 (0)