fix(hopper): diagnose MCP provider failures with stage, state, and request context (#1044)

* fix(hopper): diagnose MCP provider failures with stage, state, and request context

* fix(hopper): retain health at the actual provider lifecycle boundary

* test(hopper): distinguish socket loss from later observed process exit

* test(hopper): cover observed exit separately from socket-first replies

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
This commit is contained in:
Alan Vo
2026-10-08 09:33:56 -07:00
committed by GitHub
parent b5c2954ad6
commit 5a67c7fc54
20 changed files with 990 additions and 62 deletions
+2
View File
@@ -339,6 +339,8 @@ export class BinarySession
this.#active?.client.runtimeLineageSnapshots?.() ?? [],
requestActivitySnapshots:
this.#active?.client.requestActivitySnapshots?.() ?? [],
providerOperationHealth:
this.#active?.client.operationHealthSnapshot?.() ?? null,
});
}
@@ -1,5 +1,6 @@
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { ProviderOperationHealth } from "../../domain/providerOperationHealth.js";
import type {
CapabilityDescriptor,
ProviderRequestActivitySnapshot,
@@ -21,6 +22,7 @@ interface BinarySessionStatusInput {
readonly runId: string | undefined;
readonly runtimeLineageSnapshots: readonly ProviderRuntimeLineageSnapshot[];
readonly requestActivitySnapshots: readonly ProviderRequestActivitySnapshot[];
readonly providerOperationHealth?: ProviderOperationHealth | null;
}
/** Project internal provider routing state into the caller-visible session status. */
@@ -32,6 +34,7 @@ export const binarySessionStatus = ({
runId,
runtimeLineageSnapshots,
requestActivitySnapshots,
providerOperationHealth,
}: BinarySessionStatusInput): JsonValue => {
const configuredProvider = router.configuredIdentity();
const provider = providerSummary(configuredProvider);
@@ -85,6 +88,9 @@ export const binarySessionStatus = ({
process_lineage: processLineageStatus(runtimeLineageSnapshots),
},
analysis_activity: requestActivityStatus(requestActivitySnapshots),
provider_operation_health: providerOperationHealthStatus(
providerOperationHealth,
),
};
return target === undefined
? { open: false, ...common }
@@ -203,3 +209,20 @@ const capabilityStatus = (
},
limitations: [...descriptor.limitations],
});
const providerOperationHealthStatus = (
health: ProviderOperationHealth | null | undefined,
): JsonValue => {
if (health === undefined || health === null) return null;
return {
state: health.state,
stage: health.stage,
retry_action: health.retryAction,
exit_code: health.exitCode,
requests: health.requests.map((request) => ({
request_id: request.requestId,
operation: request.operation,
stage: request.stage,
})),
};
};
@@ -152,6 +152,18 @@ export class CompositeProvider implements AnalysisProvider {
.sort((left, right) =>
left.provider.id.localeCompare(right.provider.id),
),
operationHealthSnapshot: () => {
for (const client of clients.values()) {
const snapshot = client.operationHealthSnapshot?.();
if (snapshot !== undefined && snapshot.state !== "idle")
return snapshot;
}
for (const client of clients.values()) {
const snapshot = client.operationHealthSnapshot?.();
if (snapshot !== undefined) return snapshot;
}
return undefined;
},
closeWithOutcome,
close: async () => {
await closeWithOutcome();
@@ -3,6 +3,7 @@ import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import type { ProviderOperationHealth } from "../../domain/providerOperationHealth.js";
import { err } from "../../domain/result.js";
import { ABORTED, waitForAbortable } from "./AbortablePromise.js";
import type {
@@ -115,6 +116,10 @@ class LazyAnalysisClient implements AnalysisClient {
return this.#client?.requestActivitySnapshots?.() ?? [];
}
operationHealthSnapshot(): ProviderOperationHealth | undefined {
return this.#client?.operationHealthSnapshot?.();
}
async close(): Promise<void> {
this.#closed = true;
if (this.#loading === undefined) return;