Skip to content

fix(rank-tracking): finalize stuck runs after DFS snapshots (slim) - #5

Closed
Kroma86 wants to merge 60 commits into
mainfrom
fix/rank-check-finalize-hang-slim
Closed

Kroma86 wants to merge 60 commits into
mainfrom
fix/rank-check-finalize-hang-slim

Conversation

@Kroma86

@Kroma86 Kroma86 commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Slim PR of the rank-check finalize hang fix only (cherry-picked from 7b0eb18 onto current origin/main tip 7b9ee0e).
  • Replaces trust-merge risk from fat PR fix(rank-tracking): finalize stuck runs after DFS snapshots #4 (~78 commits / agency platform WIP ancestry).
  • Same behavior: flip DB status before awaiting PostHog shutdown, reclaim snapshot-complete blockers, cron watchdog via reconcileStuckRankCheckRuns.

Files (7)

  • rankCheckFinalize.ts + test
  • rankCheckReconciler.ts
  • RankCheckWorkflow.ts
  • rankCheckRunGuards.ts
  • posthog.ts (bounded shutdown)
  • server.ts (watchdog call only — no AI-vis/Sam cron imports from fat branch)

Test plan

  • Review diff vs main is exactly these 7 files
  • Do not merge/deploy without Jon yes
  • After deploy: confirm no stuck running rank checks + positions visible

Supersedes fat scope of #4 for merge trust. Leave #4 open for agency WIP history; use this PR for the finalize-only ship.

Made with Cursor

Replace leftover 'personal, noncommercial' boilerplate with a license
covering internal business use and client-services work, permit using
generated Outputs in client deliverables, and carve Outputs out of the
Section 2.2 commercial-exploitation restriction.
bensenescu and others added 24 commits August 28, 2026 16:16
SAM Durable Objects that exceed the memory limit mid-turn were being
re-run by Think's chat recovery, dying at the same point every time.
The bookkeeping that bounds the retry budget was OOMing too, so one
incident reached attempt 44 of a max of 10. Every attempt is a full
OpenRouter call that never reaches onChatResponse, so none of it was
metered: the production key burned ~$160/day against ~$2/day of
metered SAM spend.

- onChatRecovery returns { persist: true, continue: false }: the partial
  reply is still persisted and the user still gets the terminal banner,
  but no automatic re-inference is scheduled.
- continueLastTurn short-circuits the "recovery-continue" trigger so
  recovery alarms already queued in stuck DOs before this deploy are
  skipped without a model call.

Longer-term follow-ups (not in this PR): meter per step so interrupted
turns are still charged, and bring the turn footprint under the DO
memory limit.

Claude-Session: https://claude.ai/code/session_01Cr7Rtr4tj6HUC8WVSPc8R4
The OnboardingChatAgent Durable Object namespace has had zero invocations for
months and the "onboarding" credit feature zero events since June 2026. Nothing
in the app linked to /onboarding/chat any more, so the route was unreachable.

Removes the agent DO, its tools, the client route and UI, the server functions,
and the shared free-question cap. The SAM agent keeps the pieces it shared:
ChatComposer moved to src/client/features/sam, and openseo-fact-sheet.md moved
next to samChatTools.

wrangler.jsonc gains migration tag v5 with deleted_classes:
["OnboardingChatAgent"]. That destroys the namespace's stored conversations,
which is the intent. Alchemy derives its DO bindings from
wrangler.durable_objects.bindings, so dropping the binding there is the whole
change on the Alchemy side.

Claude-Session: https://claude.ai/code/session_019qJewihUwwbdtYDsHa73Gc
* Fix preview propagation checks and local rank-tracking seed

* Triage papercuts and clarify development workflows
…on disconnect (#566)

* fix(integrations): search GA4/GSC pickers and clear them on disconnect

Add client-side search to the GA4 and Search Console property pickers, and
unlink unused OAuth grants plus drop cached picker lists on disconnect so
stale accounts do not remain after the user clicks Disconnect (EVE-118).

Co-authored-by: Ben Senescu <bensenescu@users.noreply.github.com>

* style: format picker search and disconnect tests

Co-authored-by: Ben Senescu <bensenescu@users.noreply.github.com>

* refactor(integrations): reuse the shared searchable select in the GA4/GSC pickers

Replaces the hand-rolled search input + native <select> pair with a shared
SearchableSelect extracted from LocationSelect, so the country picker and both
Google pickers are one implementation. Simplifies the disconnect grant sweep
down to the loop it is, and drops the query-key indirection module in favour of
inline removeQueries where disconnect happens.

Claude-Session: https://claude.ai/code/session_01GJirVu872xMMfto8SrWBcF

* fix(ui): keep the searchable dropdown readable under a narrow trigger

Claude-Session: https://claude.ai/code/session_01GJirVu872xMMfto8SrWBcF

* fix(gsc): keep grants that a legacy unbound connection still relies on

The disconnect sweep now unlinks every grant no project of the caller's
uses, but connections saved before gsc_account_id existed have a NULL
account and never matched, so disconnecting one project revoked the grant
another project was minting tokens from. A NULL-account connection now
counts as using every grant.

Also tightens the GA4 no-property-bound test to the partial-sweep case its
GSC twin already covers, and drops the GSC null-account service test whose
invariant now lives in SQL.

Claude-Session: https://claude.ai/code/session_01GJirVu872xMMfto8SrWBcF

* fix(ui): keep keyboard focus in the searchable select

Enter, Escape and Tab now hand focus back to the trigger instead of
dropping it on the document body when the search box unmounts, so a
keyboard user lands on the next control (Save property) rather than at
the top of the page. Arrow keys step past disabled options, and the
menu opens with the current choice highlighted like a native select.

Claude-Session: https://claude.ai/code/session_01GJirVu872xMMfto8SrWBcF

* fix(integrations): stop the connection card clipping the property dropdown

The card shell had overflow-hidden, which cut the new absolutely
positioned picker menu off after a row or two on the Integrations page.
Nothing in the card needed the clip. Also drops the OAuth subject id from
the pickers' search keywords: it is never shown, so nobody types it.

Claude-Session: https://claude.ai/code/session_01GJirVu872xMMfto8SrWBcF

* fix(ui): float the searchable menu over page scroll containers again

Switching the menu from fixed to absolute put it back inside the pages'
overflow-auto wrappers, so on Keyword Research and Domain Overview the
country list was cut off until the page was scrolled. Back to fixed, with
the width copied from the trigger instead of resolving w-full against the
viewport, so the Google pickers still get a trigger-width menu. The
connection card's overflow-hidden is restored since fixed escapes it.
Typing now also lands the highlight on the first pickable match rather
than a disabled one.

Claude-Session: https://claude.ai/code/session_01GJirVu872xMMfto8SrWBcF

* fix(ui): keep the searchable menu pinned to its trigger while scrolling

A fixed menu left at its static position stays put when the page scrolls,
so it drifted away from the trigger. Position it from the trigger's
bounding rect and re-place it on scroll and resize, the way PortalMenu
does. Scrolling inside the option list is ignored so it stays smooth.

Claude-Session: https://claude.ai/code/session_01GJirVu872xMMfto8SrWBcF

* refactor(gsc,ga4): sweep unused Google grants with one correlated DELETE

Disconnect looped over the caller's grants, asked per grant whether a
connection still used it, and deleted one at a time. Replace that with a
single `DELETE ... WHERE NOT EXISTS (connection using this grant)` per
service, in the connection repositories, following the shape already used
in AuthRepository. The Search Console subquery keeps the legacy
NULL-account clause; Analytics does not need it (column is NOT NULL).

`existsForConnectorAccount` and `unlinkUserGrant` had no other callers.

The service tests that counted mocked delete-builder calls are replaced by
real-SQLite repository tests that seed the account table and assert which
rows survive: another user's grant, a grant another project of the caller
uses, a legacy NULL-account connection, and the other service's grant.
The service keeps one control-flow test: another member's connection
removes the project link and never sweeps.

Claude-Session: https://claude.ai/code/session_01GJirVu872xMMfto8SrWBcF

* fix(integrations): simplify property search and scope account removal

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Ben Senescu <bensenescu@users.noreply.github.com>
…ar them …" (#597)

This reverts commit 12fb0db421242cc864be3f16b252f59e3f9748b1.
Awaiting PostHog shutdown in the finalize step could wedge the workflow
after snapshots were already written, leaving status=running so the API
hid positions. Flip DB status first without awaiting telemetry, reclaim
snapshot-complete blockers as completed, and add a cron watchdog.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Kroma86

Kroma86 commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Closing: fork main is stale (c469a48), so this PR looked ~60 commits fat. Reopening the same tip against upstream every-app/open-seo main for a true 1-commit slim diff.

@Kroma86 Kroma86 closed this Sep 12, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8fbe64f. Configure here.

);
console.log(
`[rank-check] watchdog failed stale incomplete run ${run.id}`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Watchdog fails active long rank checks

High Severity

reconcileStuckRankCheckRuns fails any still-incomplete run after 25 minutes without checking whether the Cloudflare workflow is still active. Queued checks are designed around a 15-minute poll window plus post, collect, and live-fallback steps, and a legal-max config (1000 keywords × 2 devices) can still be collecting past that cutoff. The fail hides already-written snapshots (results only read completed runs), frees the one-active-run slot, and makes the real finalize step skip.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8fbe64f. Configure here.

if (input.requireFullCoverage) {
if (keywordsChecked === 0 || keywordsChecked < keywordsTotal) {
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Coverage ignores pending device snapshots

Medium Severity

requireFullCoverage treats a run as finished when every keyword has any snapshot, not one per device. Default devices is both, so the watchdog can mark the run completed after the first device returns while mobile (or desktop) tasks are still queued or in fallback. That releases the active-run slot early and shows a finished check with a missing device series.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8fbe64f. Configure here.

run.id,
"Rank check timed out before finalizing",
run,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Watchdog can overwrite completed runs

Medium Severity

When coverage is incomplete, the watchdog calls failRunIfActive with the run row from the initial stuck query. That helper trusts the passed status and updateRun has no WHERE status IN ('pending','running') guard. If workflow finalize completes the same run in between, the watchdog still writes failed and positions disappear from the API.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8fbe64f. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Left a non-blocking comment and did not approve: Cursor Bugbot completed with unresolved findings (including a high-severity watchdog issue), so this needs human review. Assigned a reviewer from CODEOWNERS and the rank-tracking history.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver (1)

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.

3 participants