fix(runner): classify ACPX admission failures

This commit is contained in:
Dotta 2026-09-02 21:29:45 -05:00
parent d49c011403
commit 9611da2b38
4 changed files with 32 additions and 17 deletions

View File

@ -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<void> | null,
@ -42,8 +51,7 @@ export async function readSidecarHostStatusWithin(
host.status(),
new Promise<never>((_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<void> | null,
): Promise<void> {
const recovered = recoverSidecarHostCleanup(host, cleanup);
return prior
? combineSidecarHostCleanups([prior, recovered])
: recovered;
return prior ? combineSidecarHostCleanups([prior, recovered]) : recovered;
}
export function reportAuthoritativeSidecarHostCleanupFailure(

View File

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

View File

@ -22,7 +22,10 @@ export async function requireVerifiedAcpxModel(
profile: QualifiedAcpxProfile,
): Promise<AcpxModelStatus> {
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,

View File

@ -48,6 +48,8 @@ const RUNTIME_ADMISSION_VERIFICATION_TIMEOUT_MS = 8_000;
const activeRuntimeHostCleanupOwners = new Set<Promise<unknown>>();
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<RunnerToolBridgeOptions, "secret">;
export interface AcpxRetainedCleanupFailure {
resource:
| "credential"
| "provider_lifetime"
| "command"
| "runtime"
| "tool_bridge";
"credential" | "provider_lifetime" | "command" | "runtime" | "tool_bridge";
attempt: number;
error: unknown;
}