fix(runner): retain bounded ACPX startup diagnosis

This commit is contained in:
Dotta 2026-09-02 20:58:45 -05:00
parent e9998cbfdb
commit d60306420d
4 changed files with 55 additions and 6 deletions

View File

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

View File

@ -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",
);
});
});

View File

@ -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<Error>();
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<string, unknown>;
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<string, unknown>;
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 =

View File

@ -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<Promise<unknown>>();
const activeCodexRuntimeCleanupOwners = new Set<Promise<unknown>>();
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<void> {
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;