Files
PaperClipAI/tests/runner-e2e/failure-classifier.ts
T
DottaandPaperclip 812b9e1a65 fix(e2e): normalize remote admission failures without private diagnostics
Preserve typed HTTP and transport timeout classification while removing raw response bodies and causes from admission reports. Reject unknown failures and malformed JSON without weakening ownership or deadline checks.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-30 08:48:21 -05:00

59 lines
3.1 KiB
TypeScript

import { ObservedStateTimeout, RemoteAdmissionReadError } from "./api.js";
import type { FailureClass } from "./types.js";
const TRANSIENT =
/(?:\b429\b|\b5\d\d\b|rate.?limit|ECONN(?:RESET|REFUSED)|socket hang up|network (?:error|interruption|timeout)|service unavailable|(?:provider|server|bootstrap|browser|webserver|health|daytona|sandbox|ingress|preview|connection|harness).*(?:temporar|timed? out|timeout|closed|failed|unavailable|interrupt|reset|refused|create|start|connect)|(?:timed? out|timeout).*(?:provider|server|bootstrap|browser|webserver|health|daytona|sandbox|ingress|preview|connection|harness))/i;
const PERMANENT =
/(?:missing (?:credential|fixture secret)|invalid.*(?:credential|api key)|unauthorized|forbidden|qualification|model.*(?:unsupported|incompatible)|artifact.*incompatible|runner_remote_.*(?:incompatible|unavailable)|immutable image digest)/i;
// These controller checkpoint failures are reproducible protocol defects, even
// when their outer error is described as a provider transport failure.
const NON_RETRYABLE_SESSION_CLOSE =
/(?:native_session_close_unrecoverable|runner did not durably suspend before checkpoint)/i;
// Recovery cannot repair a provider session-open rejection that explicitly
// declares itself non-retryable. Keep generic transport/start failures transient.
const NON_RETRYABLE_ACPX_SESSION_OPEN =
/native_session_recovery_failed[\s\S]*ACPX sidecar command session\.open was rejected\s*\([^)]*\bretryable\s*=\s*false\b/i;
const CANDIDATE =
/(?:matcher|expected.*observed|marker|issue status|run status|runtime mode|wrong output|missing output)/i;
// File-transfer deadlines are sandbox transport failures. Do not generalize
// this to all RPC timeouts: runner protocol defects must remain visible.
const SANDBOX_TRANSFER_TIMEOUT =
/RPC call "environmentSync(?:In|Out)" timed out after \d+ms/i;
export function classifyFailure(error: unknown): FailureClass {
if (error instanceof ObservedStateTimeout || error instanceof RemoteAdmissionReadError) return error.failureClass;
const message =
error instanceof Error ? `${error.name}: ${error.message}` : String(error);
if (/browser bootstrap failed before task creation/i.test(message))
return "transient_infrastructure";
if (/secret.*(?:leak|plaintext|redaction)/i.test(message))
return "secret_leak";
if (/cleanup|teardown|lease.*release/i.test(message)) {
return TRANSIENT.test(message)
? "transient_infrastructure"
: "cleanup_failure";
}
if (PERMANENT.test(message)) return "permanent_infrastructure";
if (
/chat_idle_state_invariant/.test(message) ||
NON_RETRYABLE_SESSION_CLOSE.test(message) ||
NON_RETRYABLE_ACPX_SESSION_OPEN.test(message)
)
return "candidate_failure";
if (TRANSIENT.test(message) || SANDBOX_TRANSFER_TIMEOUT.test(message))
return "transient_infrastructure";
if (CANDIDATE.test(message)) return "candidate_failure";
return "candidate_failure";
}
export function shouldRetryFailure(
failureClass: FailureClass,
maxAutomaticRetries = 1,
) {
return (
maxAutomaticRetries > 0 &&
(failureClass === "transient_infrastructure" ||
failureClass === "provider_variance")
);
}