diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 78f65266595..be4537caf08 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -8210,6 +8210,11 @@ const NATIVE_PROVIDER_STATE_INCOMPATIBLE_ACTION = const NATIVE_STARTUP_MODEL_REJECTED_ACTION = "The provider could not start the selected model before any provider work began. Choose a supported model for this agent, then retry the task. Automatic retries are stopped."; +// The provider session opened, then the provider refused the selected model on +// the turn. The task needs a model choice, not a reconciliation of session state. +const NATIVE_TURN_MODEL_REJECTED_ACTION = + "The provider rejected the selected model. Choose a supported model for this agent, then retry the task. Automatic retries are stopped."; + const NATIVE_CLEANUP_OPERATOR_RECOVERY_MESSAGE = "Send a new message to continue after Paperclip verifies that the previous provider and its tools have stopped. Clearing a task session does not resolve this quarantine. Automatic retries are stopped."; @@ -11913,6 +11918,8 @@ async function executePaperclipNativeSessionAttempt( ? "The work and waiting state are saved. Confirm shutdown of the retained execution and settle that same handoff without repeating provider work." : sourceFailureCode === "native_provider_model_rejected" && !recoveryEvidence.providerSessionEstablished ? NATIVE_STARTUP_MODEL_REJECTED_ACTION + : sourceFailureCode === "native_provider_model_rejected" + ? NATIVE_TURN_MODEL_REJECTED_ACTION : sourceFailureCode === NATIVE_PROVIDER_STATE_INCOMPATIBLE ? NATIVE_PROVIDER_STATE_INCOMPATIBLE_ACTION : sourceFailureCode === NATIVE_PROVIDER_SIDE_EFFECT_PENDING @@ -12170,6 +12177,8 @@ async function executePaperclipNativeSessionAttempt( ? "Verify that the failed provider stopped and reconcile its action outcomes. A linked continuation can proceed only after these checks succeed." : sourceFailureCode === "native_provider_model_rejected" && !recoveryEvidence.providerSessionEstablished ? NATIVE_STARTUP_MODEL_REJECTED_ACTION + : sourceFailureCode === "native_provider_model_rejected" + ? NATIVE_TURN_MODEL_REJECTED_ACTION : sourceFailureCode === NATIVE_PROVIDER_STATE_INCOMPATIBLE ? NATIVE_PROVIDER_STATE_INCOMPATIBLE_ACTION : sourceFailureCode === NATIVE_PROVIDER_SIDE_EFFECT_PENDING diff --git a/server/src/services/native-runtime/native-startup-retry.integration.test.ts b/server/src/services/native-runtime/native-startup-retry.integration.test.ts index 936a705a4c3..4d75db6db41 100644 --- a/server/src/services/native-runtime/native-startup-retry.integration.test.ts +++ b/server/src/services/native-runtime/native-startup-retry.integration.test.ts @@ -33,7 +33,7 @@ import { executePaperclipNativeSession, prepareNativeSessionOpenTimeoutRetry, sc import { buildNativeCompletionContract } from "./completion-contracts.js"; import { prepareNativeHeartbeatRun } from "./prepare-native-run.js"; import { claimNativeSessionResumptions } from "./native-finalization-reconciler.js"; -import { DurablePrpControlPlane } from "../../vendor/paperclip-runner/index.js"; +import { DurablePrpControlPlane, NativeProviderTerminalFailure } from "../../vendor/paperclip-runner/index.js"; import { createNativeRunnerExecutionDriver } from "./native-runner-execution-driver.js"; import { currentNativeControllerIdentity } from "./native-restart-recovery.js"; import { loadNativeRunnerCheckpoint } from "./native-runner-checkpoint.js"; @@ -785,6 +785,41 @@ describe("native provider startup retry", () => { }, ); + it("asks for a model choice when the provider rejects the model on an opened session's turn (Claude ACPX)", async () => { + const seeded = await seedRun(await createAgent()); + provider.execute.mockReset().mockImplementation(async () => { + await db.insert(heartbeatRunEvents).values({ + companyId, runId: seeded.runId, agentId: seeded.agentId, seq: 900, + eventType: "turn.started", stream: "system", + }); + // What the native runtime throws for Claude's AIR "selected model" request failure. + throw new NativeProviderTerminalFailure("RUNTIME", false, + "native_provider_model_rejected: ACP agent reported a terminal request failure."); + }); + await expect(executePaperclipNativeSession({ + db, execution: seeded.execution, runnerInstanceId: seeded.runnerInstanceId, + backend: { + descriptor: async () => { throw new Error("Unexpected provider descriptor call"); }, + openSession: async () => { throw new Error("Unexpected provider session call"); }, + }, + })).rejects.toThrow("native_provider_model_rejected"); + + const state = await read(seeded); + expect(state.coordinator).toMatchObject({ + phase: "terminal_failure", + failureCode: "native_provider_model_rejected", + nextAttemptAt: null, + failureDetail: expect.objectContaining({ providerSessionEstablished: true }), + }); + expect(String(state.coordinator.failureDetail?.nextAction)).toContain("Choose a supported model"); + expect(String(state.coordinator.failureDetail?.nextAction)).not.toContain("ambiguous"); + expect(state.run).toMatchObject({ status: "failed", errorCode: "native_provider_model_rejected" }); + expect(state.issue.status).toBe("blocked"); + expect(state.action).toMatchObject({ ownerType: "board", cause: "native_provider_model_rejected", wakePolicy: null }); + expect(state.action!.nextAction).toContain("Choose a supported model"); + expect(state.action!.nextAction).not.toContain("ambiguous"); + }); + it("blocks with a clear reason once the bounded startup budget is exhausted", async () => { const seeded = await seedRun(await createAgent()); await failStartup(seeded, sessionOpenFailure());