Skip to content

fix(explorer): cancel superseded query admissions - #4823

Merged
ScriptedAlchemy merged 5 commits into
masterfrom
fix/4432-explorer-admission-cancel
Nov 9, 2026
Merged

ScriptedAlchemy merged 5 commits into
masterfrom
fix/4432-explorer-admission-cancel

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Scope — Refs #4432 (behavioral correctness slice; NOT full acceptance)

Problem: Explorer's controller used to accept every asynchronous POST response as the currently active query run. A slow older creation response arriving after a newer search could replace the newer run ID. Reset, supersession, scope changes and unmount stopped status polling but left accepted server-side work active until its deadline.

Change: Bind each create attempt to its exact scope and a monotonic client request generation. Only the current attempt can install an active run ID. Older attempts that finish after supersession/unmount explicitly cancel their own accepted run, even if React Query no longer has a mounted observer. Retiring an active search (including a scope switch or unmount) sends DELETE to the original project's route, not the new scope; completed runs are not cancelled. Source ordering, partial results, current status polling, server authorization and existing contracts remain unchanged.

Regressions committed: Two real React hook/fake-daemon DOM tests in controllerPolling.dom.test.tsx use a withheld earlier POST to assert the newer query remains active while the stale accepted run is cancelled, and assert supersession/unmount cancel the exact active run identities. Existing polling tests and assertions remain.

Review: Focused commits 856fad24 and 87a5dff, based on master 91c72aba7750; exactly two Explorer files. Independently reviewed GitHub commit diff and master...head compare; fast-forward branch push used force=false and exact expected previous head. No existing PR branch or peer changes overwritten.

Verification: Four nonzero standalone Node state-machine assertions passed (including baseline old-behavior failure and new-generation cancellation). This is not execution of the committed React tests. This runner has Node/TypeScript but no repository checkout, pnpm, React/Vitest dependencies or Bazel; the in-repo DOM tests have not been executed here. Exact-head hosted CI #42530450102 is queued and must run the Dashboard tests before this is considered fully verified. Do not claim a Rust/Bazel/native macOS pass if those jobs are skipped.

Outstanding issue acceptance: This is not the #4432 lifecycle consolidation. The dashboard's QUERY_RUNS job registry, independent create/status/cancel endpoints, polling and duplicated state/finality still exist. Complete #4432 needs a request-owned progressively streamed response or the canonical daemon operation stream with an explicit compatibility/migration plan, generated client contract/UI cutover, cross-project and real cancellation tests. Keep this draft unmerged and #4432 open on the evidence above. No placeholder replacement query manager was introduced.

Coordinating CI/eligible merges remains with the separate PR-maintenance routine. #4776 HOLD and #4763 READ ONLY.

Follow-up verification

The controller takes ownership when the admission response arrives, including when the surface unmounts before React Query finishes its callbacks. Pending late admissions are cancelled at their original scope. Already completed admissions require no cancellation. The polling fixture distinguishes GET status reads from DELETE cancellation. The callback-interleaving test failed before the change and passed afterward. The full Bazel dashboard suite passed all 2,245 tests, and dashboard typechecking passed. The existing draft hold and the remaining #4432 scope still apply.

@changeset-bot

changeset-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e4e23ad

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review November 9, 2026 20:55
@ScriptedAlchemy
ScriptedAlchemy merged commit adf0220 into master Nov 9, 2026
23 checks passed

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Comment on lines +244 to +250
useEffect(() => {
return () => {
requestGeneration.current += 1;
const previous = acceptedRun.current;
acceptedRun.current = null;
if (previous !== null) void cancelPlannerQuery(previous.scope, previous.id);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Deep-linked searches cancel during mount

With ?q=graph, requestGeneration advances during StrictMode's simulated cleanup, invalidating the restored search. useExplorerUrlState restores only once, so the admitted run gets cancelled and the search remains pending.

Learn more

The production root wraps the router in React.StrictMode. On initial mount, useExplorerUrlState submits a query from the URL in an effect guarded by restored. StrictMode runs effect setup, cleanup, and setup again without discarding refs. This cleanup invalidates that admission, but restored.current prevents the second setup from dispatching a replacement. The old admission is then cancelled and no status polling starts, leaving the restored query pending.

Example: Open /explorer?q=graph in the dashboard. The first effect dispatches graph; the simulated cleanup increments its generation. When its POST returns pending, the controller sends DELETE instead of installing the run; the page still shows graph as submitted.

Recommended fix: Distinguish an actual unmount from StrictMode's effect replay, or make URL restoration re-dispatch when the replay invalidates its admission. Add an ExplorerPage test under StrictMode that opens a ?q= URL, waits for admission, and checks that its run stays active.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +239 to +243
const retireAcceptedRun = () => {
const previous = acceptedRun.current;
acceptedRun.current = null;
if (previous !== null) void cancelPlannerQuery(previous.scope, previous.id);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Failed cancellations leave orphaned runs

When a superseding search's DELETE fails, retireAcceptedRun discards the run without checking the typed result. The old run keeps consuming server work until its deadline, with no cancellation retry or visible failure.

Learn more

fetchEnvelope returns { outcome: 'transport' } on offline and HTTP errors. The new fire-and-forget cancellation discards that result and clears acceptedRun first; the same pattern occurs for stale POST responses and unmount cleanup. No subsequent action retains the run identity to complete the cancellation. The daemon keeps pending runs until cancelled or timed out, and its run registry has finite capacity.

Example: Submit alpha, then beta while the DELETE for alpha receives HTTP 503. The controller forgets alpha and shows beta; alpha continues doing work, and nothing reports the failed cancellation.

Recommended fix: Centralize automatic cancellation and handle its EnvelopeResult explicitly. Retain failed cancellation identities for an appropriate bounded retry or expose a typed failure when retries are impossible; handle both non-2xx transport outcomes and rejected requests.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant