mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
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:
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());
|
||||
|
||||
Reference in new issue
Block a user