feat(serenity): opt-in async provisioning for createMarket, createBrandForOrg, activate (LLMO-7352/LLMO-7418) - #3246
Open
IrisAlexandrescu wants to merge 8 commits into
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
IrisAlexandrescu
force-pushed
the
feat/LLMO-7418-provisioning-worker
branch
from
September 9, 2026 16:00
70fe12d to
ae4b0a9
Compare
IrisAlexandrescu
force-pushed
the
feat/LLMO-7418-async-provisioning-endpoints
branch
from
September 9, 2026 16:14
57504cb to
2d095bd
Compare
IrisAlexandrescu
pushed a commit
that referenced
this pull request
Sep 9, 2026
…de call sites (LLMO-7352/LLMO-7418 Phase 4) Extends the opt-in `async: true` contract (PR-C, #3246) to the last 3 ensureSubworkspace call sites that were still fully synchronous: createBrandForOrg's bare-create (no semrushMarket) branch, and activate's pending->active and bare-reactivation branches. These 3 sites use ensureSubworkspace's `createReadiness: 'skip'` mode (persist the workspace pointer immediately after a single create call, without polling for settle) rather than 'poll' mode, so they never had the in-request settle-poll latency/timeout problem PR-A/PR-B/PR-C fixed for the other 2 sites. This is deliberate hardening for consistency and defense against a slow/hanging single Semrush create call, not a fix for a still-open incident — the original LLMO-7352 symptom is already closed by PR-A alone, since any workspace pointer's first real use (e.g. adding a market) always runs the now-fail-fast pollUntilCreated first. - createBrandForOrg's bare-create branch mirrors its sibling hasSemrushMarket branch exactly: async:true persists the row first, then hands off to provision-workspace-job with no chained job (no project to create). - activate's two skip-mode branches each mint a provisioning attempt and chain to a new activate-brand-workspace-job, which does the one piece of business logic the generic worker doesn't: flipping (or re-affirming) the brand's status to active once the workspace is confirmed ready. One job handler covers both branches; `wasPending` in its metadata distinguishes a real pending->active transition (502 on save failure) from a no-op reactivation re-affirm (207). - Existing guardAgainstConcurrentProvisioning calls on the sync branches are unchanged (still needed to protect a sync caller against a concurrent async attempt from another endpoint). Stacked on PR-C (#3246). 30 new unit tests; 954 passing overall, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
This PR will trigger a minor release when merged. |
IrisAlexandrescu
pushed a commit
that referenced
this pull request
Sep 9, 2026
…de call sites (LLMO-7352/LLMO-7418 Phase 4) Extends the opt-in `async: true` contract (PR-C, #3246) to the last 3 ensureSubworkspace call sites that were still fully synchronous: createBrandForOrg's bare-create (no semrushMarket) branch, and activate's pending->active and bare-reactivation branches. These 3 sites use ensureSubworkspace's `createReadiness: 'skip'` mode (persist the workspace pointer immediately after a single create call, without polling for settle) rather than 'poll' mode, so they never had the in-request settle-poll latency/timeout problem PR-A/PR-B/PR-C fixed for the other 2 sites. This is deliberate hardening for consistency and defense against a slow/hanging single Semrush create call, not a fix for a still-open incident — the original LLMO-7352 symptom is already closed by PR-A alone, since any workspace pointer's first real use (e.g. adding a market) always runs the now-fail-fast pollUntilCreated first. - createBrandForOrg's bare-create branch mirrors its sibling hasSemrushMarket branch exactly: async:true persists the row first, then hands off to provision-workspace-job with no chained job (no project to create). - activate's two skip-mode branches each mint a provisioning attempt and chain to a new activate-brand-workspace-job, which does the one piece of business logic the generic worker doesn't: flipping (or re-affirming) the brand's status to active once the workspace is confirmed ready. One job handler covers both branches; `wasPending` in its metadata distinguishes a real pending->active transition (502 on save failure) from a no-op reactivation re-affirm (207). - Existing guardAgainstConcurrentProvisioning calls on the sync branches are unchanged (still needed to protect a sync caller against a concurrent async attempt from another endpoint). Stacked on PR-C (#3246). 30 new unit tests; 954 passing overall, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
IrisAlexandrescu
force-pushed
the
feat/LLMO-7418-async-provisioning-endpoints
branch
from
September 9, 2026 17:24
fafceee to
6064817
Compare
IrisAlexandrescu
pushed a commit
that referenced
this pull request
Sep 9, 2026
…de call sites (LLMO-7352/LLMO-7418 Phase 4) Extends the opt-in `async: true` contract (PR-C, #3246) to the last 3 ensureSubworkspace call sites that were still fully synchronous: createBrandForOrg's bare-create (no semrushMarket) branch, and activate's pending->active and bare-reactivation branches. These 3 sites use ensureSubworkspace's `createReadiness: 'skip'` mode (persist the workspace pointer immediately after a single create call, without polling for settle) rather than 'poll' mode, so they never had the in-request settle-poll latency/timeout problem PR-A/PR-B/PR-C fixed for the other 2 sites. This is deliberate hardening for consistency and defense against a slow/hanging single Semrush create call, not a fix for a still-open incident — the original LLMO-7352 symptom is already closed by PR-A alone, since any workspace pointer's first real use (e.g. adding a market) always runs the now-fail-fast pollUntilCreated first. - createBrandForOrg's bare-create branch mirrors its sibling hasSemrushMarket branch exactly: async:true persists the row first, then hands off to provision-workspace-job with no chained job (no project to create). - activate's two skip-mode branches each mint a provisioning attempt and chain to a new activate-brand-workspace-job, which does the one piece of business logic the generic worker doesn't: flipping (or re-affirming) the brand's status to active once the workspace is confirmed ready. One job handler covers both branches; `wasPending` in its metadata distinguishes a real pending->active transition (502 on save failure) from a no-op reactivation re-affirm (207). - Existing guardAgainstConcurrentProvisioning calls on the sync branches are unchanged (still needed to protect a sync caller against a concurrent async attempt from another endpoint). Stacked on PR-C (#3246). 30 new unit tests; 954 passing overall, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
IrisAlexandrescu
temporarily deployed
to
dev-branches
September 9, 2026 17:59 — with
GitHub Actions
Inactive
IrisAlexandrescu
force-pushed
the
feat/LLMO-7418-async-provisioning-endpoints
branch
2 times, most recently
from
September 9, 2026 18:25
8e279ab to
553f05a
Compare
IrisAlexandrescu
pushed a commit
that referenced
this pull request
Sep 9, 2026
…de call sites (LLMO-7352/LLMO-7418 Phase 4) Extends the opt-in `async: true` contract (PR-C, #3246) to the last 3 ensureSubworkspace call sites that were still fully synchronous: createBrandForOrg's bare-create (no semrushMarket) branch, and activate's pending->active and bare-reactivation branches. These 3 sites use ensureSubworkspace's `createReadiness: 'skip'` mode (persist the workspace pointer immediately after a single create call, without polling for settle) rather than 'poll' mode, so they never had the in-request settle-poll latency/timeout problem PR-A/PR-B/PR-C fixed for the other 2 sites. This is deliberate hardening for consistency and defense against a slow/hanging single Semrush create call, not a fix for a still-open incident — the original LLMO-7352 symptom is already closed by PR-A alone, since any workspace pointer's first real use (e.g. adding a market) always runs the now-fail-fast pollUntilCreated first. - createBrandForOrg's bare-create branch mirrors its sibling hasSemrushMarket branch exactly: async:true persists the row first, then hands off to provision-workspace-job with no chained job (no project to create). - activate's two skip-mode branches each mint a provisioning attempt and chain to a new activate-brand-workspace-job, which does the one piece of business logic the generic worker doesn't: flipping (or re-affirming) the brand's status to active once the workspace is confirmed ready. One job handler covers both branches; `wasPending` in its metadata distinguishes a real pending->active transition (502 on save failure) from a no-op reactivation re-affirm (207). - Existing guardAgainstConcurrentProvisioning calls on the sync branches are unchanged (still needed to protect a sync caller against a concurrent async attempt from another endpoint). Stacked on PR-C (#3246). 30 new unit tests; 954 passing overall, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
IrisAlexandrescu
temporarily deployed
to
dev-branches
September 9, 2026 18:57 — with
GitHub Actions
Inactive
IrisAlexandrescu
force-pushed
the
feat/LLMO-7418-provisioning-worker
branch
from
September 10, 2026 07:42
fce9e83 to
09c741f
Compare
IrisAlexandrescu
force-pushed
the
feat/LLMO-7418-async-provisioning-endpoints
branch
from
September 10, 2026 08:00
553f05a to
ab699df
Compare
IrisAlexandrescu
temporarily deployed
to
dev-branches
September 10, 2026 08:12 — with
GitHub Actions
Inactive
IrisAlexandrescu
pushed a commit
that referenced
this pull request
Sep 10, 2026
…de call sites (LLMO-7352/LLMO-7418 Phase 4) Extends the opt-in `async: true` contract (PR-C, #3246) to the last 3 ensureSubworkspace call sites that were still fully synchronous: createBrandForOrg's bare-create (no semrushMarket) branch, and activate's pending->active and bare-reactivation branches. These 3 sites use ensureSubworkspace's `createReadiness: 'skip'` mode (persist the workspace pointer immediately after a single create call, without polling for settle) rather than 'poll' mode, so they never had the in-request settle-poll latency/timeout problem PR-A/PR-B/PR-C fixed for the other 2 sites. This is deliberate hardening for consistency and defense against a slow/hanging single Semrush create call, not a fix for a still-open incident — the original LLMO-7352 symptom is already closed by PR-A alone, since any workspace pointer's first real use (e.g. adding a market) always runs the now-fail-fast pollUntilCreated first. - createBrandForOrg's bare-create branch mirrors its sibling hasSemrushMarket branch exactly: async:true persists the row first, then hands off to provision-workspace-job with no chained job (no project to create). - activate's two skip-mode branches each mint a provisioning attempt and chain to a new activate-brand-workspace-job, which does the one piece of business logic the generic worker doesn't: flipping (or re-affirming) the brand's status to active once the workspace is confirmed ready. One job handler covers both branches; `wasPending` in its metadata distinguishes a real pending->active transition (502 on save failure) from a no-op reactivation re-affirm (207). - Existing guardAgainstConcurrentProvisioning calls on the sync branches are unchanged (still needed to protect a sync caller against a concurrent async attempt from another endpoint). Stacked on PR-C (#3246). 30 new unit tests; 954 passing overall, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
IrisAlexandrescu
force-pushed
the
feat/LLMO-7418-async-provisioning-endpoints
branch
from
September 10, 2026 10:23
ab699df to
aae939f
Compare
IrisAlexandrescu
pushed a commit
that referenced
this pull request
Sep 10, 2026
…de call sites (LLMO-7352/LLMO-7418 Phase 4) Extends the opt-in `async: true` contract (PR-C, #3246) to the last 3 ensureSubworkspace call sites that were still fully synchronous: createBrandForOrg's bare-create (no semrushMarket) branch, and activate's pending->active and bare-reactivation branches. These 3 sites use ensureSubworkspace's `createReadiness: 'skip'` mode (persist the workspace pointer immediately after a single create call, without polling for settle) rather than 'poll' mode, so they never had the in-request settle-poll latency/timeout problem PR-A/PR-B/PR-C fixed for the other 2 sites. This is deliberate hardening for consistency and defense against a slow/hanging single Semrush create call, not a fix for a still-open incident — the original LLMO-7352 symptom is already closed by PR-A alone, since any workspace pointer's first real use (e.g. adding a market) always runs the now-fail-fast pollUntilCreated first. - createBrandForOrg's bare-create branch mirrors its sibling hasSemrushMarket branch exactly: async:true persists the row first, then hands off to provision-workspace-job with no chained job (no project to create). - activate's two skip-mode branches each mint a provisioning attempt and chain to a new activate-brand-workspace-job, which does the one piece of business logic the generic worker doesn't: flipping (or re-affirming) the brand's status to active once the workspace is confirmed ready. One job handler covers both branches; `wasPending` in its metadata distinguishes a real pending->active transition (502 on save failure) from a no-op reactivation re-affirm (207). - Existing guardAgainstConcurrentProvisioning calls on the sync branches are unchanged (still needed to protect a sync caller against a concurrent async attempt from another endpoint). Stacked on PR-C (#3246). 30 new unit tests; 954 passing overall, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
IrisAlexandrescu
temporarily deployed
to
dev-branches
September 10, 2026 10:36 — with
GitHub Actions
Inactive
…ndForOrg, activate (LLMO-7352/LLMO-7418)
Converts the three remaining synchronous Semrush sub-workspace/project
provisioning endpoints to an opt-in async model, gated by a per-request
`async: true` flag (mirroring the existing `createPrompts` precedent) so
default behavior is unchanged for every current consumer.
- createMarket, createBrandForOrg's initial market, and activate's
project-activation branch now mint a provisioning attempt and enqueue
provision-workspace-job -> {create-market,activate-markets}-job when
`async: true` is supplied, returning 202 with a pollable jobId.
- Business logic shared between the sync controller path and the async
job handler is extracted into create-market-orchestration.js and
activate-markets-orchestration.js.
- guardAgainstConcurrentProvisioning protects every synchronous branch
against racing a live async attempt for the same brand.
- Adds a generic /serenity/jobs/:jobId polling alias.
- Adds recordFreshBrandProvisioningStartFailure so a brand row created in
the same request is marked failed (not left silently inert) if minting
the provisioning attempt itself throws.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-sites reconcile stale attempts (LLMO-7418 external-review Findings 1, 9, 11) Finding 1: guardAgainstConcurrentProvisioning's read (getBrandProvisioningState) threw a bare Error on ANY PostgREST failure, including "column does not exist" — meaning if this service ever deploys even briefly ahead of the mysticat-data-service migrations that add the provisioning columns, every synchronous createMarket/activate call 500s on an endpoint that worked a moment ago. Preserve the underlying SQLSTATE on the thrown error and degrade to a no-op specifically for 42703 (undefined_column) — no column means no attempt could possibly be in flight, so proceeding is correct, not just convenient. Any other read error still fails closed, unchanged. Finding 11 (adjacent, same function): a missing/unparseable updated_at yielded NaN, which is never < the staleness threshold, so it fell through to "reconcile as stale" — capable of tearing down a genuinely fresh, healthy attempt. Treat NaN as "assume fresh" instead (409, never silently reconciled away). Finding 9: beginProvisioningAttempt's own CAS has no staleness awareness, so a stuck pending row (crashed worker, DLQ'd message) 409s forever once a brand's callers are async-only — there is no scheduled sweep (architecturally impossible; Semrush only accepts user-token auth). Call guardAgainstConcurrentProvisioning immediately before beginProvisioningAttempt at the two async begin-sites in this branch (createMarket, activate's project-activation batch) that operate on a pre-existing brand id — reusing the guard's own tested reconcile-or-409 logic rather than inventing new logic. The brand-create async sites (createBrandForOrg) are NOT touched: they mint a brand-new, freshly-generated UUID in the same request, so no prior attempt could exist for it — adding the guard there would be a pure no-op read on every call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…terminal hop (LLMO-7418 external-review Finding 8)
The runner marks the ORIGINAL AsyncJob COMPLETED on any non-throwing handler
return, including a self-requeue ({ requeuedJobId }, workspace not yet
settled) or a chain hand-off ({ provisioningStatus: 'ready', chainedJobId },
the chained market-create/activate work not yet started). getPromptsJobStatus
(the same handler serving the generic /serenity/jobs/:jobId polling alias)
just echoed that first hop's status/result verbatim, so a client polling
during a chain saw a premature "COMPLETED" the moment the FIRST hop merely
handed off, with no market created and no way to tell the difference from a
genuinely finished request. The frontend consumers built against this
contract this session all resolve on the first COMPLETED with no chain
awareness.
Fix is entirely backend-scoped, so no frontend change is needed: when the
polled job is COMPLETED and its result carries chainedJobId or
requeuedJobId, follow that pointer (recursively, capped at 10 hops against a
corrupt/cyclic chain) and report the EFFECTIVE terminal hop's status/result/
error instead. jobId/jobType in the response still reflect the ORIGINALLY-
requested job, so a caller polling a fixed URL never needs to learn
intermediate hop ids. Updated the two OpenAPI descriptions that previously
overclaimed "result is the same body a synchronous 2xx would have
returned" without qualifying that this only became true once the whole
chain, not just the first hop, had run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…op job id (LLMO-7418 external-review Findings 12, 17) - provision-workspace-job.js: enqueueChainedJobIfConfigured now retries up to 3 times with backoff before giving up, since a transient enqueue failure right after ready-promotion previously stranded a ready workspace with no follow-up job and no retry. - serenity.js: createMarket and activate's async batch-market branch now call updateProvisioningJobId (best-effort) immediately after the first hop's createAndEnqueueJob, so the job id is recorded even if the worker never reaches its own self-requeue path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…path, cancel-first on deactivate (LLMO-7418 external-review N4/N5, N2, Finding 3)
- N4/N5 (Option B): the shared getPromptsJobStatus handler now keys off the job family.
Provisioning jobs report jobType 'provisionWorkspace' (not the classifyPrompts default)
and have their requeue/chain followed to the effective terminal hop; classifyPrompts jobs
are no longer chain-followed, restoring their pre-stack polling contract (a large CSV import
that self-requeues again returns its hop-0 {created,skipped} result, not a later hop's shape).
- N2: activate's async batch-market branch passes the brand name as the sub-workspace title.
An active flat-mode brand reaches this branch with no pointer, so the worker takes its
create path -- which fail-fasts without a title. createMarket deliberately does NOT pass one
(subworkspace-mode invariant means the worker polls the existing pointer; the rare
pointer-cleared race is an acceptable clear fail-fast, since minting a workspace mid
add-market is not a valid outcome).
- Finding 3 (deactivate half): cancelProvisioningAttempt now runs FIRST in deactivate, before
decommission/clear/save. Once it flips provisioning_status to 'failed' a concurrent worker
hop can no longer win its promote CAS and resurrect the brand, and it is no longer skipped
when the pointer/status save throws.
Tests: N4 label, N5 no-follow-for-classify, activate title, chain-follow fixtures marked as
provisioning jobs. serenity suite 237 passing. eslint + type-check clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…reporting success (LLMO-7418 external-review Finding 12)
After the workspace is promoted ready, if the configured chained job (the market-create /
activate work the caller actually requested) cannot be enqueued even after the bounded retries,
the handler now THROWS instead of returning a plain `{ provisioningStatus: 'ready' }`. Previously
that plain return made the runner mark the job COMPLETED and the client poll back a green success
while no market was ever created and nothing would retry it.
A new `candidatePromoted` flag makes the throw safe: once promoteProvisioningReady succeeds the
candidate is the brand's canonical, live workspace, so the outer catch skips BOTH cleanup (never
empty a live workspace) and failBestEffort (never mark an already-ready attempt failed). The job
goes FAILED so the caller learns the chained work did not run; the brand stays ready/active and
the caller can safely re-issue the request.
Tests: the Finding 12 test now asserts the throw + no-deface + no-cleanup + full retry count.
provision-workspace-job suite 43 passing. eslint + type-check clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ling the attempt (LLMO-7418 external-review, adversarial) deactivate loads the brand BEFORE cancelProvisioningAttempt, so its in-memory semrush_sub_workspace_id is a pre-cancel snapshot. A worker hop whose promoteProvisioningReady committed in the loadBrand -> cancel window has since written a canonical pointer that object cannot see; acting on the stale null skipped the entire decommission / pointer-clear / tombstone block, leaving that workspace live (with the customer's projects intact) behind a "successful" deactivate. Re-read the pointer via the existing brandPointerReloader after the cancel and prefer the fresh value, falling back to the snapshot when the reloader cannot read (it is best-effort by contract and returns null on missing data-access). The cancel-first ordering already closed the resurrection half of this race; this closes the stale-read half. Test: a promote landing mid-flight (findById returns the stale snapshot first, the promoted brand second) is still decommissioned with the freshly-promoted workspace id. serenity suite 238 passing. eslint + type-check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IrisAlexandrescu
force-pushed
the
feat/LLMO-7418-async-provisioning-endpoints
branch
from
September 10, 2026 11:17
aae939f to
3a1ffe9
Compare
IrisAlexandrescu
pushed a commit
that referenced
this pull request
Sep 10, 2026
…de call sites (LLMO-7352/LLMO-7418 Phase 4) Extends the opt-in `async: true` contract (PR-C, #3246) to the last 3 ensureSubworkspace call sites that were still fully synchronous: createBrandForOrg's bare-create (no semrushMarket) branch, and activate's pending->active and bare-reactivation branches. These 3 sites use ensureSubworkspace's `createReadiness: 'skip'` mode (persist the workspace pointer immediately after a single create call, without polling for settle) rather than 'poll' mode, so they never had the in-request settle-poll latency/timeout problem PR-A/PR-B/PR-C fixed for the other 2 sites. This is deliberate hardening for consistency and defense against a slow/hanging single Semrush create call, not a fix for a still-open incident — the original LLMO-7352 symptom is already closed by PR-A alone, since any workspace pointer's first real use (e.g. adding a market) always runs the now-fail-fast pollUntilCreated first. - createBrandForOrg's bare-create branch mirrors its sibling hasSemrushMarket branch exactly: async:true persists the row first, then hands off to provision-workspace-job with no chained job (no project to create). - activate's two skip-mode branches each mint a provisioning attempt and chain to a new activate-brand-workspace-job, which does the one piece of business logic the generic worker doesn't: flipping (or re-affirming) the brand's status to active once the workspace is confirmed ready. One job handler covers both branches; `wasPending` in its metadata distinguishes a real pending->active transition (502 on save failure) from a no-op reactivation re-affirm (207). - Existing guardAgainstConcurrentProvisioning calls on the sync branches are unchanged (still needed to protect a sync caller against a concurrent async attempt from another endpoint). Stacked on PR-C (#3246). 30 new unit tests; 954 passing overall, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
IrisAlexandrescu
temporarily deployed
to
dev-branches
September 10, 2026 11:26 — with
GitHub Actions
Inactive
…of writing to the org parent workspace (LLMO-7418 external-review B1) A brand whose sub-workspace pointer is not yet written resolves to `mode: 'flat'` with `workspaceId = the ORG'S SHARED PARENT` (workspace-resolver.js: if the brand has no sub-workspace id it returns the parent). That is correct for a genuinely flat brand and WRONG for a Semrush brand that is merely mid-provisioning. Async creation makes that window reachable: the brand is persisted before Semrush confirms the workspace, so for the whole provisioning window it looks flat. Add Market in that window created and PUBLISHED a project in the shared org workspace -- consuming the org's shared allocation, bound in the DB to this brand -- and the moment the real pointer landed the brand flipped to sub-workspace mode and that market became invisible: an orphaned upstream project plus a stale mapping row. The window is the full workspace create + settle poll (tens of seconds to minutes), and the create wizard's own success screen walks the user straight into Add Market, so the race is normally lost rather than rarely. createMarket's flat branch now reads the brand's provisioning state and refuses with a 409 (`semrush_provisioning_incomplete`) when an attempt is `pending` or `failed` -- both mean this is a sub-workspace brand with no workspace of its own yet. A genuinely flat brand (non-Serenity org) has no provisioning row and is unaffected. The state read is best-effort: a read failure never turns an otherwise-valid flat create into an error. This is the server-side half, and the half that actually closes the hole -- UI gating of the Add Market affordance on semrushProvisioningStatus is the remaining (frontend) half, tracked for LLMO-7419 along with the epic's AC17. Tests: both refusal cases pinned (pending and failed), asserting the flat handler is never reached. serenity suite 240 passing. eslint + type-check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IrisAlexandrescu
pushed a commit
that referenced
this pull request
Sep 10, 2026
…de call sites (LLMO-7352/LLMO-7418 Phase 4) Extends the opt-in `async: true` contract (PR-C, #3246) to the last 3 ensureSubworkspace call sites that were still fully synchronous: createBrandForOrg's bare-create (no semrushMarket) branch, and activate's pending->active and bare-reactivation branches. These 3 sites use ensureSubworkspace's `createReadiness: 'skip'` mode (persist the workspace pointer immediately after a single create call, without polling for settle) rather than 'poll' mode, so they never had the in-request settle-poll latency/timeout problem PR-A/PR-B/PR-C fixed for the other 2 sites. This is deliberate hardening for consistency and defense against a slow/hanging single Semrush create call, not a fix for a still-open incident — the original LLMO-7352 symptom is already closed by PR-A alone, since any workspace pointer's first real use (e.g. adding a market) always runs the now-fail-fast pollUntilCreated first. - createBrandForOrg's bare-create branch mirrors its sibling hasSemrushMarket branch exactly: async:true persists the row first, then hands off to provision-workspace-job with no chained job (no project to create). - activate's two skip-mode branches each mint a provisioning attempt and chain to a new activate-brand-workspace-job, which does the one piece of business logic the generic worker doesn't: flipping (or re-affirming) the brand's status to active once the workspace is confirmed ready. One job handler covers both branches; `wasPending` in its metadata distinguishes a real pending->active transition (502 on save failure) from a no-op reactivation re-affirm (207). - Existing guardAgainstConcurrentProvisioning calls on the sync branches are unchanged (still needed to protect a sync caller against a concurrent async attempt from another endpoint). Stacked on PR-C (#3246). 30 new unit tests; 954 passing overall, 0 regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Third PR in the LLMO-7352/LLMO-7418 stack (after #3223 and #3233). Converts the three remaining synchronous Semrush sub-workspace/project provisioning endpoints —
createMarket,createBrandForOrg's initial-market create, andactivate's project-activation branch — to use the async worker built in #3233, gated by a per-request opt-in flag rather than an unconditional cutover.async: truein the request body enqueuesprovision-workspace-jobchained to the relevant follow-up job and returns202with a pollablejobId. Absent/false(the default) runs the exact same synchronous logic these endpoints have always run — this mirrors the existingcreatePromptsasyncflag precedent already in this codebase, so no current consumer's behavior changes until they explicitly ask for async. This flag is a migration aid, not a permanent dual-mode API surface, and should be retired once consumers move over.create-market-orchestration.js/activate-markets-orchestration.js, called directly by the controller (sync) or by a thin job-handler adapter (async).guardAgainstConcurrentProvisioningis called from every synchronous branch so a sync request can't race a live async attempt for the same brand (and vice versa).beginProvisioningAttemptitself throws right after a brand row is persisted increateBrandForOrg,recordFreshBrandProvisioningStartFailuremarks that just-created rowfailed(unconditional update — safe only because the brand id was minted moments earlier in the same request) instead of leaving it silently inert./serenity/jobs/:jobIdroute alias (job-type-agnostic, same handler as the existing prompts-jobs route).Reviewed with a 4-perspective adversarial pass (architect / race-condition / senior-engineer-correctness / test-coverage) and a security review; the two real bugs and test gaps the code review surfaced are fixed and covered here. The security review found no high-confidence issues; one medium-confidence, same-tenant-only gap (an unredacted upstream error message can reach the new job-status polling response for the brand's own owner, in code paths outside the per-market sanitizing catch) is a good fast-follow but isn't a blocker since it discloses no capability beyond what the caller already holds.
Test plan
npx mocha test/controllers/serenity.test.js test/controllers/brands.test.js test/support/brands-storage.test.js— 924 passing, 0 failingnpm run lint— cleannpm run type-check— cleanasyncvalue → 400, explicitasync: falseregression,beginProvisioningAttempt-throws compensation path,createMarketconcurrency-guard wiring/409create-market-orchestration.test.js/activate-markets-orchestration.test.js/*-job.test.jscover the extracted orchestration + job-handler adapters exhaustively🤖 Generated with Claude Code