Skip to content

fix: release-review follow-ups for account removal (workflow-state rejections, stats-row creator, sweep pages) - #9164

Merged
waleedlatif1 merged 8 commits into
stagingfrom
fix/account-removal-review-followups
Oct 17, 2026
Merged

waleedlatif1 merged 8 commits into
stagingfrom
fix/account-removal-review-followups

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Fixes the real findings from the v0.9.26 release review (#9155) on the account-removal work (#9139, #9141, #9146), plus one gap the review surfaced. Every other thread on that release was answered there.

Workflow-state rejections complete instead of alerting (background/workflow-execution.ts, lib/execution/preprocessing.ts)

  • Preprocessing rejects a workflow that is gone (deleted or archived), not attached to a workspace, or not deployed only after a successful read. By design those rejections write no execution log, so fix(trigger): complete async workflow runs whose failure is already recorded instead of failing them #9139's "complete only once the rejection was logged" gate never passed for them and the Trigger run still failed and alerted.
  • PreprocessExecutionError now carries an internal workflowState, and isWorkflowStateRejection() lets the task complete those rejections without waiting for a log.
  • The field is internal on purpose: every surface copies named fields, so no API output (v2 execute details, its stream error event, MCP error data) changes.
  • Quota-type rejections still complete only once logged, now classified by meaning instead of a status-code set:
    • suspended account and usage limit, via getDeterministicAdmissionRejectionCode;
    • reservation headroom, via isReservationHeadroomDenial, kept out of the shared classifier so webhook and polling retries are unchanged;
    • rate limit, via RATE_LIMIT_EXCEEDED_CODE.
  • Retryable and 5xx rejections still throw.
  • This also quiets queued jobs of workflows that were deleted with an account.

One creator for fallback user_stats rows (lib/billing/core/usage.ts)

  • Two paths created a missing stats row without the deleted-identity check signup makes:
    • the storage tracker's in-transaction upsert;
    • the ownership-transfer insert.
  • createUserStatsIfMissing(executor, userId) is now the only fallback creator. A row it creates gets that check (email; linked sign-ins only for the signup request), which runs in a savepoint and fails open. An existing row still costs exactly one conflicting insert.
  • forfeitFreeTierAllowance(executor, userId) merges the two forfeit paths: a transaction on db, a savepoint inside a caller's transaction.
  • ensureUserStatsExists is now a wrapper; the storage tracker and the ownership transfer call the creator.
  • One stated delta: a stats row created by an ownership transfer now starts at getFreeTierLimit(), like every other creator, instead of the column default. The two differ only when FREE_TIER_COST_LIMIT is set.

Retired Project storage sweep finishes only on the provider's last page (lib/projects/files/prefix-cleanup.ts)

  • It inferred completion from a short page, but Blob, GCS and S3-compatibles can return short or empty pages that are not final, so cleanup could finish early and orphan objects. It now ends on page.done.
  • Within a run it follows the continuation token past pages that removed nothing, and stalls (spending an attempt) when the token stops advancing.

Storage reference guard (lib/users/storage-reference-guard.ts): also checks each URL with its #fragment removed. That only adds candidates, so it can only keep more objects.

Docs: cancelByWorkspace is scoped to persisted queue jobs.

Type of Change

  • Bug fix

Testing

  • Each behavioral change has a test that went red with the change reverted.

    • Preprocessing: a test per tagged producer, and a failed read stays untagged.
    • Task: deleted, not-attached and undeployed complete with no log; suspended, usage, headroom and rate limit throw when unlogged.
    • Project sweep: short and empty non-final pages, and a stuck token.
    • Guard: fragments, including the malformed-encoding path.
  • Integration (real Postgres, run locally): deleted-identity 22/22, covering:

    • a storage-created row and an ownership-transfer-created row withheld for a deleted identity;
    • an existing row untouched;
    • fail-open under a lock timeout, with the caller's transaction intact.

    Also foundation 35/35 and storage-sweep 3/3.

  • Unit and integration-adjacent suites: 1104 tests across background, preprocessing, admission, billing, Project cleanup and users.

  • Independent reviews: a correctness review (savepoints verified on real Postgres, no lock-order change, no transient 403/404), a design review (which drove the typed classifier and the single creator), and a final full-diff review with a per-rejection behavior table.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced

…before they started

Not-found, archived, undeployed, unattached and suspended rejections come
from a successful read and never wait on an execution log (not-found and
undeployed never write one), so they complete without the hasCompleted gate.
Quota rejections (402/429) still complete only once their log is saved.
…rovider's last page

Azure Blob, GCS and S3-compatible providers can return a short or empty page
that is not the last, which the sweep read as done and left the rest of the
prefix orphaned. The sweep now ends only on page.done, and follows a non-final
page that removed nothing to the next one within the run.
…ent still names

A relative /api/files/serve/<key>#frag kept the fragment in its candidate key,
so a live reference could be missed and the object purged. The guard now also
reads the path without its fragment, on both the decoded and fallback paths.
…side signup

A storage write or an ownership transfer that creates a missing user_stats row
gave it the default free allowance without the deleted-identity check
ensureUserStatsExists makes. When either insert creates the row, the same
email-only check now runs on the caller's transaction, in a savepoint that
fails open; an existing row costs no extra query.
…status code

Preprocessing tags not-found, archived, not-attached and not-deployed
rejections with an internal workflowState (isWorkflowStateRejection); these
write no execution log by design, so the job completes at once. Quota and
account rejections (usage limit, spent reservation headroom, rate limit,
suspended account) complete only once their log is saved; a suspended account
goes back behind that gate.

The tag is a separate field rather than a code because v2 execute, its
streaming error and MCP serve copy error.code into their responses; a new code
on a 404 would change those outputs. Headroom denials are matched through a
new isReservationHeadroomDenial next to their descriptors instead of joining
the deterministic-rejection set, which webhooks and polling read to stop
retries and which deliberately leaves headroom out.
…reator

createUserStatsIfMissing inserts the row on the caller's executor and, only
when it created the row, runs the deleted-identity check and forfeits the
allowance (a transaction on db, a savepoint inside a caller's transaction),
failing open. ensureUserStatsExists, the storage write and the ownership
transfer all use it; the two forfeit paths merge into one.

One behavior delta: a stats row the ownership transfer creates for the new
owner now starts at getFreeTierLimit() instead of the schema default, the same
as every other creator (identical unless FREE_TIER_COST_LIMIT is set).

The admin credits route keeps its own insert: it is not behavior-identical
(short id, no usageLimitUpdatedAt, a plain insert that throws on a concurrent
create rather than a no-op), and it rewrites the limit right after.
… email check

isDeletedAccountIdentity reads on db as it did before; only
userHasDeletedAccountIdentity takes a caller's transaction, so its savepoint
wrapper moves inline. Hoist the headroom descriptors to a module constant.
@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Oct 17, 2026 3:02am UTC

Request Review

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 17 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

This PR changes payment, billing, or quota logic. Ultrareviews find 2.4x more serious bugs than standard reviews. Comment @cubic-dev-ai ultrareview to run one.

Turn on auto-fix | Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

This PR fixes account-removal follow-ups across execution jobs, billing stats, and storage cleanup.

  • Async jobs finish workflow-state refusals without waiting for a log.
  • Storage writes and ownership transfers use one stats-row creator.
  • Retired Project cleanup finishes only on the provider's last page.
  • Stored asset links still protect keys before a URL fragment.
  • The queue docs say workspace cancellation skips in-memory runs.

Intentional behavior acknowledged by waleedlatif1: failed identity checks grant the allowance rather than block the caller; ownership-transfer-created rows use getFreeTierLimit() instead of the column default.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Preprocessing refuses execution] --> B{Retryable?}
  B -->|Yes| C[Fail job]
  B -->|No| D{Workflow state refusal?}
  D -->|Yes| E[Complete with failure result]
  D -->|No| F{Known account or quota refusal?}
  F -->|No| C
  F -->|Yes| G{Log saved?}
  G -->|Yes| E
  G -->|No| C
Loading

Reviews (1) · Last reviewed commit: "refactor(billing): drop the unused execu..." · Reviewed by Greptile

@waleedlatif1
waleedlatif1 merged commit b7394be into staging Oct 17, 2026
89 of 91 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/account-removal-review-followups branch October 17, 2026 03:21

This branch was previously deployed

1 inactive deployment
Preview — 9b5e8f45 Deployed Oct 17, 2026 by vercel[bot]
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