From d60306420de281547d6c86155b55974193255ff1 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 2 Sep 2026 20:58:45 -0500 Subject: [PATCH] fix(runner): retain bounded ACPX startup diagnosis --- .../runner-core/src/acpx_sidecar_transport.rs | 8 +++++ .../src/cli/acpx-sidecar-input.test.ts | 11 +++++++ .../src/cli/acpx-sidecar-input.ts | 29 ++++++++++++++++++- .../src/drivers/acpx/codex-runtime-adapter.ts | 13 +++++---- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs index 033422533c..34f000ecd9 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs @@ -618,6 +618,7 @@ fn response_error_classification(error: &ResponseError) -> &'static str { "SESSION_MODEL_REPLAY_FAILED" => return "session_model_replay_failed", "SESSION_CONFIG_OPTION_REPLAY_FAILED" => return "session_config_option_replay_failed", "CLAUDE_ACP_SESSION_CREATE_TIMEOUT" => return "claude_session_create_timeout", + "ACPX_SESSION_HANDSHAKE_TIMEOUT" => return "session_handshake_timeout", _ => {} } match error.message.as_str() { @@ -706,6 +707,13 @@ mod tests { )), "session_handshake_timeout" ); + assert_eq!( + response_error_classification(&error( + "ACPX_SESSION_HANDSHAKE_TIMEOUT", + "bounded provider admission failed", + )), + "session_handshake_timeout" + ); assert_eq!( response_error_classification(&error("ACP_MODEL_UNSUPPORTED", "violet-circuit-4821",)), "requested_model_unsupported" diff --git a/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts b/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts index d07389fee1..25dc6b3ae6 100644 --- a/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts +++ b/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts @@ -130,6 +130,14 @@ describe("ACPX sidecar input sequencing", () => { code: "ACP_MODEL_UNSUPPORTED", detailCode: "AGENT_STARTUP_FAILED", }); + const nestedHandshake = new AggregateError( + [ + Object.assign(new Error("admission deadline"), { + name: "AcpxSessionHandshakeTimeoutError", + }), + ], + "runtime initialization cleanup failed", + ); expect(acpxSidecarErrorCode(missingModule)).toBe( "AGENT_STARTUP_FAILED.MODULE_NOT_FOUND", @@ -141,5 +149,8 @@ describe("ACPX sidecar input sequencing", () => { "violet-circuit-4821", ); expect(acpxSidecarErrorCode(model)).toBe("ACP_MODEL_UNSUPPORTED"); + expect(acpxSidecarErrorCode(nestedHandshake)).toBe( + "ACPX_SESSION_HANDSHAKE_TIMEOUT", + ); }); }); diff --git a/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts b/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts index 8e20380525..d5dbdcfcc6 100644 --- a/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts +++ b/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts @@ -35,13 +35,40 @@ export function acpxBootstrapBlockedError( * contributes a closed category and is never copied into the code itself. */ export function acpxSidecarErrorCode(error: Error): string { + const pending: Error[] = [error]; + const observed = new Set(); + while (pending.length > 0 && observed.size < 16) { + const current = pending.shift()!; + if (observed.has(current)) continue; + observed.add(current); + const code = directAcpxSidecarErrorCode(current); + if (code !== null) return code; + + const details = current as Error & Record; + if (current instanceof AggregateError) { + for (const nested of current.errors) { + if (nested instanceof Error) pending.push(nested); + } + } + if (details.cause instanceof Error) pending.push(details.cause); + } + return "acpx_sidecar_command_failed"; +} + +function directAcpxSidecarErrorCode(error: Error): string | null { const details = error as Error & Record; const code = typeof details.code === "string" ? details.code : typeof details.detailCode === "string" ? details.detailCode - : "acpx_sidecar_command_failed"; + : null; + if (code === null) { + return error.name === "AcpxSessionHandshakeTimeoutError" || + error.message === "ACPX session handshake exceeded its admission deadline" + ? "ACPX_SESSION_HANDSHAKE_TIMEOUT" + : null; + } if (code !== "AGENT_STARTUP_FAILED") return code; const stderr = 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 087bb6e628..ee5e4f0d68 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -50,7 +50,10 @@ export const DEFAULT_CODEX_ACPX_RUNTIME_SHUTDOWN_BOUND_MS = // only after the exact attempt reaches a terminal outcome. const activeRuntimeCleanupOwners = new Set>(); const activeCodexRuntimeCleanupOwners = new Set>(); -const SESSION_HANDSHAKE_TIMEOUT_MS = 8_000; +// Provider initialization may include a cold native app-server start on a +// minimally provisioned runner. Keep admission finite while allowing the +// qualified runtime enough time to complete that local handshake. +const SESSION_HANDSHAKE_TIMEOUT_MS = 30_000; class AcpxRuntimeCloseTimeoutError extends Error { constructor() { @@ -67,6 +70,8 @@ class AcpxRuntimeCloseFinalTimeoutError extends Error { } class AcpxSessionHandshakeTimeoutError extends Error { + readonly code = "ACPX_SESSION_HANDSHAKE_TIMEOUT"; + constructor() { super("ACPX session handshake exceeded its admission deadline"); this.name = "AcpxSessionHandshakeTimeoutError"; @@ -828,7 +833,7 @@ function runtimePort( } if ( lateReconciliationAttempts >= - MAX_LATE_RUNTIME_CLEANUP_RECONCILIATION_ATTEMPTS + MAX_LATE_RUNTIME_CLEANUP_RECONCILIATION_ATTEMPTS ) { return; } @@ -1127,9 +1132,7 @@ function delay(timeoutMs: number): Promise { return new Promise((resolve) => setTimeout(resolve, timeoutMs)); } -type ProviderExitOutcome = - | { exited: true } - | { exited: false; error: unknown }; +type ProviderExitOutcome = { exited: true } | { exited: false; error: unknown }; class ProviderExitObservation { #outcome: ProviderExitOutcome | null = null;