From 9611da2b38c080e7608d760facba51ad3feed4e4 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 2 Sep 2026 21:29:45 -0500 Subject: [PATCH] fix(runner): classify ACPX admission failures --- .../src/cli/acpx-sidecar-lifecycle.ts | 16 +++++++++++----- .../src/drivers/acpx/codex-runtime-adapter.ts | 10 ++++++---- .../src/drivers/acpx/model-verification.ts | 15 ++++++++++++--- .../src/drivers/acpx/runtime-host.ts | 8 +++----- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/paperclip-runner/src/cli/acpx-sidecar-lifecycle.ts b/packages/paperclip-runner/src/cli/acpx-sidecar-lifecycle.ts index 902798507b..d5c12111db 100644 --- a/packages/paperclip-runner/src/cli/acpx-sidecar-lifecycle.ts +++ b/packages/paperclip-runner/src/cli/acpx-sidecar-lifecycle.ts @@ -7,6 +7,15 @@ export interface OpenedAcpxSidecarHost { const FAILED_ADMISSION_CLOSE_TIMEOUT_MS = 8_000; const ACTIVE_HOST_CLEANUP_ATTEMPTS = 4; +class AcpxSidecarStatusReadTimeoutError extends Error { + readonly code = "ACPX_SIDECAR_STATUS_READ_TIMEOUT"; + + constructor() { + super("ACPX session status read exceeded its timeout"); + this.name = "AcpxSidecarStatusReadTimeoutError"; + } +} + export function hasSidecarSessionOwnership( host: unknown, activeHostCleanup: Promise | null, @@ -42,8 +51,7 @@ export async function readSidecarHostStatusWithin( host.status(), new Promise((_resolve, reject) => { timer = setTimeout( - () => - reject(new Error("ACPX session status read exceeded its timeout")), + () => reject(new AcpxSidecarStatusReadTimeoutError()), timeoutMs, ); timer.unref(); @@ -129,9 +137,7 @@ export function recoverAndCombineSidecarHostCleanup( prior: Promise | null, ): Promise { const recovered = recoverSidecarHostCleanup(host, cleanup); - return prior - ? combineSidecarHostCleanups([prior, recovered]) - : recovered; + return prior ? combineSidecarHostCleanups([prior, recovered]) : recovered; } export function reportAuthoritativeSidecarHostCleanupFailure( diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts index 83ab1efc08..8ccdb20bbb 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -1043,8 +1043,9 @@ async function persistedRuntimeStatus( const recordId = handle.acpxRecordId ?? handle.sessionKey; const record = await sessionStore.load(recordId); if (!record) { - throw new Error( - "The pinned ACPX runtime omitted its persisted session record", + throw Object.assign( + new Error("The pinned ACPX runtime omitted its persisted session record"), + { code: "ACPX_PERSISTED_SESSION_MISSING" }, ); } if ( @@ -1052,8 +1053,9 @@ async function persistedRuntimeStatus( record.acpSessionId !== identity.backendSessionId || record.agentSessionId !== identity.agentSessionId ) { - throw new Error( - "The persisted ACPX session identity changed after admission", + throw Object.assign( + new Error("The persisted ACPX session identity changed after admission"), + { code: "ACPX_PERSISTED_SESSION_IDENTITY_MISMATCH" }, ); } const currentModelId = record.acpx?.current_model_id; diff --git a/packages/paperclip-runner/src/drivers/acpx/model-verification.ts b/packages/paperclip-runner/src/drivers/acpx/model-verification.ts index 2eb49ac6e4..6a875c94ac 100644 --- a/packages/paperclip-runner/src/drivers/acpx/model-verification.ts +++ b/packages/paperclip-runner/src/drivers/acpx/model-verification.ts @@ -22,7 +22,10 @@ export async function requireVerifiedAcpxModel( profile: QualifiedAcpxProfile, ): Promise { if (!control.getStatus) { - throw new Error("ACPX agent cannot verify its effective model"); + throw acpxModelVerificationError( + "ACPX_MODEL_STATUS_UNAVAILABLE", + "ACPX agent cannot verify its effective model", + ); } const requestedModel = profile.qualificationModel; let status = await control.getStatus(); @@ -31,7 +34,8 @@ export async function requireVerifiedAcpxModel( status.models?.currentModelId !== requestedModel; if (mustSelectCanonical) { if (!control.setModel) { - throw new Error( + throw acpxModelVerificationError( + "ACPX_MODEL_SELECTION_UNAVAILABLE", "ACPX agent cannot verify its canonical model through ACP config options", ); } @@ -39,13 +43,18 @@ export async function requireVerifiedAcpxModel( status = await control.getStatus(); } if (status.models?.currentModelId !== profile.reportedModelId) { - throw new Error( + throw acpxModelVerificationError( + "ACPX_EFFECTIVE_MODEL_MISMATCH", `ACPX effective model mismatch: requested ${requestedModel}, expected ACP selector ${profile.reportedModelId}, received ${status.models?.currentModelId ?? "unverified"}`, ); } return normalizeQualifiedModelStatus(status, profile); } +function acpxModelVerificationError(code: string, message: string): Error { + return Object.assign(new Error(message), { code }); +} + function normalizeQualifiedModelStatus( status: AcpxModelStatus, profile: QualifiedAcpxProfile, diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts index c568b613b1..a8fd65fe95 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts @@ -48,6 +48,8 @@ const RUNTIME_ADMISSION_VERIFICATION_TIMEOUT_MS = 8_000; const activeRuntimeHostCleanupOwners = new Set>(); class AcpxRuntimeAdmissionTimeoutError extends Error { + readonly code = "ACPX_RUNTIME_ADMISSION_VERIFICATION_TIMEOUT"; + constructor() { super("ACPX runtime admission verification exceeded its deadline"); this.name = "AcpxRuntimeAdmissionTimeoutError"; @@ -126,11 +128,7 @@ export type AcpxSemanticToolSession = Omit; export interface AcpxRetainedCleanupFailure { resource: - | "credential" - | "provider_lifetime" - | "command" - | "runtime" - | "tool_bridge"; + "credential" | "provider_lifetime" | "command" | "runtime" | "tool_bridge"; attempt: number; error: unknown; }