Repository navigation
fix(session): verify retained client liveness on open fast path #1776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
39851cb
1a857c2
2421a54
e6ca869
ec09a57
ea2176b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,6 +98,30 @@ export class BinarySession | |
| this.#providerRouter = providerRouter; | ||
| } | ||
|
|
||
| /** | ||
| * Fast-path liveness gate (#1719): short-circuit only when the retained | ||
| * client is genuinely alive. The probe reads the child's operation health | ||
| * rather than the composite's unconditionally-ok `health`, so a dead child | ||
| * falls through and a real client is started; an aborted caller leaves the | ||
| * session untouched. Returns the fast-path result, or undefined to fall | ||
| * through to the slow path. | ||
| */ | ||
| async #tryRetainedFastPath( | ||
| retained: SessionBinding, | ||
| target: BinaryTarget, | ||
| options: Pick<BinarySessionOpenOptions, "signal" | "snapshot">, | ||
| ): Promise<Result<BinaryTarget, AnalysisError> | undefined> { | ||
| const live = await this.#retainedClientLive(retained.client, options); | ||
| if (isAborted(options.signal)) | ||
| return err(new AnalysisCancelledError("open_binary")); | ||
| if (!live) return undefined; | ||
| if (options.snapshot !== undefined) { | ||
| const imported = this.importAnalysisSnapshot(options.snapshot); | ||
| if (!imported.ok) return imported; | ||
| } | ||
| return ok(target); | ||
| } | ||
|
|
||
| /** Identify the provider producing evidence for this session. */ | ||
| providerIdentity(operation?: AnalysisOperation): ProviderIdentity { | ||
| const route = this.#currentRoute(); | ||
|
|
@@ -247,12 +271,14 @@ export class BinarySession | |
| if (!pendingClosed.ok) return pendingClosed; | ||
| if (isAborted(options.signal)) | ||
| return err(new AnalysisCancelledError("open_binary")); | ||
| if (sameTarget && sameProfile) { | ||
| if (options.snapshot !== undefined) { | ||
| const imported = this.importAnalysisSnapshot(options.snapshot); | ||
| if (!imported.ok) return imported; | ||
| } | ||
| return ok(target); | ||
| const retained = sameTarget && sameProfile ? this.#active : undefined; | ||
| if (retained !== undefined) { | ||
| const fastPath = await this.#tryRetainedFastPath( | ||
| retained, | ||
| target, | ||
| options, | ||
| ); | ||
| if (fastPath !== undefined) return fastPath; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the retained client reports a dead state and its AGENTS.md reference: AGENTS.md:L11-L11 Useful? React with 👍 / 👎. |
||
| } | ||
| const previous = this.#active; | ||
| this.#active = undefined; | ||
|
|
@@ -703,6 +729,36 @@ export class BinarySession | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Probe whether a retained provider client is still live. | ||
| * | ||
| * On production routes the client is a CompositeProvider whose `health` | ||
| * answers unconditionally ok without touching a child, so `health` alone | ||
| * cannot see a dead child. `operationHealthSnapshot` reaches the | ||
| * instantiated children instead. Only the known-live states allow the | ||
| * fast path: `exited`/`unreachable` are dead, `unknown` is never | ||
| * evidence of health (it is what a recorded but uncharacterized failure | ||
| * reports), and `not_started` is a startup that never completed — in the | ||
| * Hopper timeout case the client holds retained resources that block the | ||
| * next `start()` until `close()` retries cleanup, so treating it as live | ||
| * would skip the retire path and hand the wedged client to subsequent | ||
| * analysis. The gate fails closed: without a snapshot signal, fall back | ||
| * to the `health` admission probe. | ||
| */ | ||
| async #retainedClientLive( | ||
| client: AnalysisClient, | ||
| options: Pick<BinarySessionOpenOptions, "signal">, | ||
| ): Promise<boolean> { | ||
| const snapshot = client.operationHealthSnapshot?.(); | ||
| if (snapshot !== undefined) | ||
| return snapshot.state === "idle" || snapshot.state === "busy"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a Hopper startup timeout with incomplete cleanup, one intervening analysis retry makes this test incorrectly pass: AGENTS.md reference: AGENTS.md:L19-L19 Useful? React with 👍 / 👎. |
||
| try { | ||
| return (await client.execute("health", {}, options)).ok; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the production CLI/MCP composition ( AGENTS.md reference: AGENTS.md:L44-L44 Useful? React with 👍 / 👎. |
||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| async #restore( | ||
| previous: SessionBinding | undefined, | ||
| ): Promise<Result<null, AnalysisError>> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the caller aborts after
#tryRetainedFastPathperforms its internal signal check but before this awaiting continuation resumes (for example, a provider hook schedules the abort in a following promise reaction), there is no post-awaitcheck here: a live result is returned as success, while a dead result immediately clears and closes the active binding and only notices cancellation after cleanup. Recheckoptions.signalat this async boundary before consumingfastPathso a cancelled same-target open neither succeeds nor mutates the session.AGENTS.md reference: AGENTS.md:L44-L44
Useful? React with 👍 / 👎.