From 45d670a7dce7d45e837777fe70ecf5dd0ff8af9c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 19 May 2026 12:24:37 +0900 Subject: [PATCH] fix(plugin): constrain Anthropic prefill guard Tighten the assistant-tail repair after review so only the Anthropic provider is treated as prefill-rejecting, and so assistant-tail model metadata is evaluated independently from the last user model. Add regression coverage for an allowed user model followed by a rejecting Anthropic assistant tail, plus a non-Anthropic provider carrying a Claude-looking model id. Plan: plans/fix-anthropic-assistant-prefill-tail.md --- src/plugin/messages-transform.test.ts | 49 +++++++++++++++++++++++++++ src/plugin/messages-transform.ts | 11 +++--- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/plugin/messages-transform.test.ts b/src/plugin/messages-transform.test.ts index acd85abd5..06eeb7005 100644 --- a/src/plugin/messages-transform.test.ts +++ b/src/plugin/messages-transform.test.ts @@ -286,6 +286,48 @@ describe("createMessagesTransformHandler", () => { }) }) + it("#given the assistant tail identifies a rejecting Anthropic model after an allowed user model #when messages transform runs #then it appends a synthetic user recovery turn", async () => { + //#given + const messages: TestMessage[] = [ + { + info: { + id: "msg_user_allowed_then_rejecting_assistant", + role: "user", + sessionID: "ses_allowed_then_rejecting_assistant", + agent: "sisyphus", + model: { providerID: "openai", modelID: "gpt-5.4" }, + }, + parts: [{ type: "text", text: "continue" }], + }, + { + info: { + id: "msg_assistant_rejecting_metadata", + role: "assistant", + sessionID: "ses_allowed_then_rejecting_assistant", + model: { providerID: "anthropic", modelID: "claude-opus-4-6" }, + }, + parts: [{ type: "text", text: "done" }], + }, + ] + + //#when + await runHandler(makeHooks({}), messages) + + //#then + expect(messages).toHaveLength(3) + expect(messages.at(-1)?.info).toMatchObject({ + role: "user", + sessionID: "ses_allowed_then_rejecting_assistant", + agent: "sisyphus", + model: { providerID: "openai", modelID: "gpt-5.4" }, + }) + expect(messages.at(-1)?.parts[0]).toMatchObject({ + type: "text", + text: "[internal] Continue from the previous assistant state.", + synthetic: true, + }) + }) + it("#given rejecting model metadata uses direct provider and model fields #when messages transform runs #then it appends a synthetic user recovery turn", async () => { //#given const messages: TestMessage[] = [ @@ -353,6 +395,13 @@ describe("createMessagesTransformHandler", () => { name: "missing model", userInfo: { role: "user" }, }, + { + name: "non-anthropic provider", + userInfo: { + role: "user", + model: { providerID: "opencode", modelID: "claude-opus-4-7" }, + }, + }, ] for (const scenario of scenarios) { diff --git a/src/plugin/messages-transform.ts b/src/plugin/messages-transform.ts index 083944679..8c455f053 100644 --- a/src/plugin/messages-transform.ts +++ b/src/plugin/messages-transform.ts @@ -5,11 +5,7 @@ import { normalizeModelID } from "../shared/model-normalization" import type { CreatedHooks } from "../create-hooks" const ASSISTANT_PREFILL_RECOVERY_TEXT = "[internal] Continue from the previous assistant state." -const ASSISTANT_PREFILL_UNSUPPORTED_PROVIDERS = new Set([ - "anthropic", - "google-vertex-anthropic", - "opencode", -]) +const ASSISTANT_PREFILL_UNSUPPORTED_PROVIDER = "anthropic" const ASSISTANT_PREFILL_UNSUPPORTED_MODEL_PREFIXES = [ "claude-opus-4-7", "claude-opus-4-6", @@ -98,7 +94,7 @@ function shouldRepairAssistantPrefillForModel(model: ModelIdentifier | undefined } const providerID = model.providerID.toLowerCase() - if (!ASSISTANT_PREFILL_UNSUPPORTED_PROVIDERS.has(providerID)) { + if (providerID !== ASSISTANT_PREFILL_UNSUPPORTED_PROVIDER) { return false } @@ -162,7 +158,8 @@ function ensureUserTurnAfterAssistantTail(output: MessagesTransformOutput): void } const shouldRepairAssistantTail = hasInternalContinuationTrigger(output.messages) || - shouldRepairAssistantPrefillForModel(findLastUserModel(output.messages) ?? readModelIdentifier(lastMessage.info)) + shouldRepairAssistantPrefillForModel(findLastUserModel(output.messages)) || + shouldRepairAssistantPrefillForModel(readModelIdentifier(lastMessage.info)) if (!shouldRepairAssistantTail) { return }