fix(native): name the model choice when a provider rejects the model on a turn

Claude (ACPX) opens the provider session and then rejects an unknown
model on the first turn. The run is native_provider_model_rejected, but
its recovery action told the board to "explicitly resolve the ambiguous
session state", because the startup choose-model action applied only
when no provider session was established. Give a rejected model the
choose-model next action in that case too.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DottaandClaude committed 2026-11-10 04:10:34 -06:00
1 parent e552b2445d
commit 3881650fee
2 files changed
+45 -1

No files matched your search

@@ -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
@@ -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());