fix(ai): stop resumed companion turns from asking answered questions again (#638)

Resuming an interrupted companion turn kept the answered questions in
questions_json, and the client-direct runner ended any run with leftover
questions as interrupted. The question form reappeared after the reply.

Clear questions when a turn resumes, as the server-side runner did before
client-direct calls, and only pause when this run actually asks.

Fixes #637

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yulei666andClaude Opus 5.5 authored and GitHub committed 2026-10-21 23:15:55 +08:00
1 parent d2a87a74a6
commit 00ddaa0fed
4 files changed
+46 -2

No files matched your search

+3 -1
View File
@@ -217,7 +217,9 @@ export const resumeCompanionTurn = async (db: DatabaseAdapter, scope: CompanionS
await listCompanionTurns(db, scope);
const row = await getCompanionTurn(db, scope, id);
if (!row || row.status !== "interrupted") throw new AppError("companion_resume_unavailable", "This request cannot be continued.", 409);
const result = await db.prepare(`UPDATE companion_turns SET status = 'running', expires_at = ?, answers_json = COALESCE(?, answers_json)
// Answered questions are resolved; keeping them would end the resumed run as interrupted again.
const result = await db.prepare(`UPDATE companion_turns SET status = 'running', expires_at = ?, answers_json = COALESCE(?, answers_json),
questions_json = '[]'
WHERE workspace_id = ? AND owner_id = ? AND id = ? AND status = 'interrupted'`).bind(
companionTurnLease(), answers ? JSON.stringify(answers.slice(0, 3)) : null, ...bindScope(scope), id).run();
if (Number(result.meta.changes) !== 1) throw new AppError("companion_busy", "A conversation is already running or this request was already submitted. Refresh to recover its result.", 409);
+26
View File
@@ -434,6 +434,32 @@ describe("companion client-direct HTTP contracts", () => {
expect((await getCompanionTurn(setup.db, scope, payload.id)).status).toBe("completed");
});
test("resume clears answered questions so the resumed turn can complete (#637)", async () => {
const setup = fixture({ loadCredentials: async () => credentials });
const payload = input({ message: "翻译这篇笔记", allowNotes: true });
expect((await setup.request("turns/prepare", "POST", payload)).status).toBe(200);
const asked = await setup.request(`turns/${payload.id}/tools`, "POST", {
name: "ask_user_question",
input: { questions: [{ id: "language", prompt: "翻译成哪种语言?", inputType: "single_select", options: [{ id: "en", label: "English" }] }] },
response: "", process: "",
});
const askedBody = await asked.json();
expect(askedBody.pause).toBe(true);
expect((await setup.request(`turns/${payload.id}/complete`, "POST", { response: "", status: "interrupted" })).status).toBe(200);
expect((await setup.request(`turns/${payload.id}/resume/prepare`, "POST", {})).status).toBe(400);
const resumed = await setup.request(`turns/${payload.id}/resume/prepare`, "POST", {
answers: [{ questionId: "language", optionIds: ["en"] }],
});
expect(resumed.status).toBe(200);
const prepared = await resumed.json();
expect(prepared.turn.questions).toEqual([]);
expect(prepared.messages.at(-1).content).toContain("\"optionIds\":[\"en\"]");
const completed = await setup.request(`turns/${payload.id}/complete`, "POST", { response: "Translated note.", status: "completed" });
expect((await completed.json()).turn).toMatchObject({ status: "completed", questions: [], response: "Translated note." });
});
});
describe("actual AI SDK companion runtime", () => {