feat(serenity): dynamic AI resource allocation — consolidated allocator + kill-switch + hot-path wiring (LLMO-5616)#2764
Conversation
…nsport read (LLMO-5616) PR 2 of the dynamic just-in-time Semrush AI resource-allocation work (api-service side) — the pure, transport-injected allocator + the resource read + error classification. Unit-only, no handler wiring / flag / carve change yet (that's PR 3, gated on the shared mock release). Independent of PR 1: the pinned user-manager-client @1.3.1 already types GET /resources. - rest-transport.js: getWorkspaceResources(workspaceId) (read the MASTER's own /resources for the org pool — /parent/resources is child-relative, per Gate 0). - errors.js: ORG_POOL_EXHAUSTED/BRAND_AI_LIMIT codes + body/message classifiers (isPoolExhausted vs the transient isWorkspaceNotReady, isMeteredQuota, isRateLimited). - resource-manager.js: ensureAiHeadroom / releaseAiSurplus (pure), roundUpToBlock, PROJECT_BLOCK=1 / PROMPT_BLOCK=100, need calculators, a strict fail-loud readAiTotals over the nested product_resources.ai.resources.*, budget-sized settle poll + bounded "workspace not ready" retry, typed 409s via ErrorWithStatusCode.code (no mapError change — keeps flag-OFF byte-for-byte). releaseAiSurplus is best-effort and never drops below a floor. - workspace-lifecycle.js: export pollUntilCreated for reuse. Design: serenity-docs#22 (Gate-0 live-verified). Tests: +26 unit; full suite green, coverage 99.94% lines / 99.28% branches (gate 90%); type-check + lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…default) codecov/patch flagged 3 diff lines: the dead `instanceof` guard in errors.js bodyText (callers already short-circuit on it) — removed; and resource-manager's realSleep default, never hit because tests inject poll.sleep — add a DEFAULT_POLL.sleep test. Both files now 100%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ror (LLMO-5616) Address MysticatBot on #2749: - isMeteredQuota: match ONLY an explicit quota signal in the body ('quota' / 'allocation exhausted'), not any 405 with a null/string body — a legitimate Method-Not-Allowed is no longer absorbed. Exact disguised-405 body is canary-pinned (PR 4). - transferAndSettle: an exhausted 'workspace not ready' now throws a transient workspaceBusy (503, retryable), not orgPoolExhausted — lock contention is not pool exhaustion. - bodyText: never fall back to e.message (the transport URL) — avoids classifier matches on the request URL. Clarify NOT_READY_RETRIES = 3 retries (4 attempts). Both files 100% patch coverage; type-check + lint + full suite (13591) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-5616) Non-blocking nits from MysticatBot's approval on #2749: - releaseAiSurplus now swallows only EXPECTED failures (SerenityTransportError / ErrorWithStatusCode); unexpected bugs (TypeError, malformed-response Error) propagate so they surface in monitoring. - Object.freeze DIMS for consistency with DEFAULT_BLOCKS / DEFAULT_POLL. (Nit 3 — mapError must not echo workspace UUIDs to end users — is a PR-3 verification item where the controller wiring lands; nothing surfaces these messages in PR 2.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…llocation (LLMO-5616)
PR 3, layer 2: ensureSubworkspace takes an options bag; when { dynamicAllocation: true } it SKIPS
the flat resourceAllocation(marketCount) re-grant on an existing sub-workspace — JIT top-up (the
metered handlers, layer 3) owns sizing, and re-flattening would undo a top-up or, on an ON→OFF
rollback of a grown child, risk total < used. Flag OFF (default) is byte-for-byte unchanged.
Call sites still default to OFF until layer 3 threads auth.dynamicAllocation through.
Unit tests assert ON-skip and OFF-byte-for-byte (spy on transferWorkspaceResources); type-check +
lint + full suite (13606) green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fy comment (LLMO-5616) MysticatBot approval nits on #2751: assert getWorkspaceStatus (the readiness settle) is still called once in the dynamic-allocation ON path (guards against accidentally dropping the pre-poll), and add a comment that the pre-poll runs regardless of mode. (Positional-param consolidation into a config bag deferred to the follow-up caller-wiring PR, as suggested.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t hot-path wiring (LLMO-5616) Consolidates the allocator core (#2749) and carve-skip (#2751) with the actual handler-fronting that was never built, per Rainer's direction in serenity-docs#22. - Replace the per-org DB flag (#2750) with a single GLOBAL env/Vault kill-switch (SERENITY_DYNAMIC_ALLOCATION, default OFF; dynamic-allocation-active.js). - Fail-fast hot path: ensureAiHeadroom does ONE transfer, no settle poll; a still-settling child returns a retryable 503 (workspaceBusy) immediately. transferAndSettle (poll + retry) stays for the async/reconciler release path. - Front the metered-write handlers behind the OFF switch: create-market (project seam + publish seam sized from used+drafted), create-prompts (publish seam), update-models (publishedTexts x Δmodels). Enforcement choke-point test. - Same-child concurrency lock (resource-lock.js) serializing per-child top-ups. - Flag OFF is byte-for-byte: the guard is a genuine no-op (zero transport calls). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Hey @byteclimber,
Verdict: Request changes - two material concerns around the advisory pool precheck producing spurious 409s and internal workspace IDs leaking in client-facing error messages.
Complexity: HIGH - large diff (2094 lines, 18 files); API surface signal.
Changes: Adds just-in-time Semrush AI resource allocation behind a global OFF-by-default kill-switch, replacing the flat pre-carve with a fail-fast headroom guard fronting all subworkspace metered-write handlers (18 files).
Must fix before merge
- [Important] Advisory pool precheck throws spurious 409 when pool has capacity -
src/support/serenity/resource-manager.js:258 - [Important] Internal workspace IDs and pool numbers leak to clients via error messages -
src/support/serenity/resource-manager.js:170
Finding 1: Advisory pool precheck is TOCTOU and can produce false 409s
The master pool free-space check (free = master.total - master.used) runs as a non-atomic advisory READ before transferOnce. Under concurrent requests (even across children of the same master), the precheck can see stale free space and throw orgPoolExhausted (409) when the transfer would actually succeed. The false-positive direction is the problem: the client gets a terminal 409 even though the pool had capacity when the transfer ran.
The code says "the transfer 422 is authoritative" - which is correct. But the advisory check fires a hard throw before the transfer gets a chance to be the authority.
Fix options:
- Demote from a hard throw to
log.warn+ proceed to transfer (let the 422 be the authority as intended) - Remove the advisory precheck entirely (the transfer 422 handles the terminal case; the precheck only saves one round-trip in the exhausted case)
- Keep but document as fail-fast-conservative (may reject operations that would succeed under concurrent releases)
Finding 2: Error messages expose internal Semrush workspace UUIDs and pool numbers
orgPoolExhausted, brandAiLimit, and workspaceBusy embed internal details in ErrorWithStatusCode.message:
Org pool ${dim} free ${free} < needed ${delta} for ${childId}
Brand AI ${dim} allocation ${target} exceeds ceiling ${cap} for ${childId}
Sub-workspace ${workspaceId} is provisioning, retry
These flow through mapError to the client response. The existing redactUpstreamMessage pattern establishes that internal Semrush details should not reach API consumers.
Fix: Use a generic client message (e.g., "Resource pool exhausted") and log the detailed string with IDs/numbers separately at the throw site.
Non-blocking (6): minor issues and suggestions
- nit:
handleUpdateModelsSubworkspacedouble-readslistSliceModelswhen allocation is ON (once for delta calc, once insidesyncModelsForProject) adding an extra hot-path round-trip -src/support/serenity/handlers/markets-subworkspace.js:797 - nit: Controller lines ~1009 and ~1104 pass
{ dynamicAllocation: ... }withoutmasterId, silently degrading to no-op guard when the flag is ON - add a comment confirming intentional -src/controllers/serenity.js:1009 - suggestion:
isMeteredQuotamatches bare substringquotawhich could misfire if upstream returns a legitimate 405 containing that word - match a more specific string -src/support/serenity/errors.js:86 - suggestion:
resource-lock.jschains Map has no queue-depth cap - a stuck transport timeout blocks every subsequent caller for that child until Lambda times out -src/support/serenity/resource-lock.js:47 - suggestion:
countPublishedPromptslacks a dedicated unit test for the pagination-ceiling/truncation path (50-page cap, warn log, floor-count return) -src/support/serenity/handlers/markets.js:967 - suggestion: Serialization test in
dynamic-allocation-active.test.jsdoes not prove the second read sees post-transfer state (stub always returns zeros) - make the stub stateful to assert the critical section invariant end-to-end -test/support/serenity/dynamic-allocation-active.test.js:82
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 2m 30s | Cost: $9.88 | Commit: ee6077ffe6837189c302cfc095f9ddd1bcb61589
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…mote pool precheck (LLMO-5616) Must-fix: - Advisory pool precheck no longer throws: it is a non-atomic read that races concurrent top-ups and could 409 spuriously with capacity left. Demoted to a warn (a pool-free gauge signal) + proceed; the transfer 422 is the single authoritative exhaustion signal. - Client-facing error messages (orgPoolExhausted/brandAiLimit/workspaceBusy) are now generic and carry no internal workspace ids or pool numbers; the detailed context is logged at the throw site instead. The distinguishing signal stays on `.code`. Nits: stateful serialization test that actually proves the read-after-write critical section; countPublishedPrompts pagination-ceiling tests; double-read + lock-queue-bound + masterId comments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
This PR will trigger a minor release when merged. |
There was a problem hiding this comment.
Hey @byteclimber,
Verdict: Approve - both prior blocking findings are resolved; the new commit is clean and well-tested.
Complexity: HIGH - large diff (2094 lines, 19 files); API surface signal.
Changes: Adds just-in-time Semrush AI resource allocation behind a global OFF-by-default kill-switch, replacing the flat pre-carve with a fail-fast headroom guard fronting all subworkspace metered-write handlers (19 files).
Previously flagged, now resolved
- Advisory pool precheck is now genuinely advisory - logs a warning and proceeds to the transfer; the
422 "insufficient units"is the single authoritative exhaustion signal (no more spurious 409s from stale reads). - Error messages are now generic with no internal workspace IDs or pool numbers - detailed context (childId, dim, free, delta) is logged separately at the throw site via structured
log.warncalls. - Serialization test is now stateful (proves the critical section serializes the read-then-absolute-set by asserting exactly one transfer when two concurrent ops each need +1 project).
Non-blocking (2): minor issues and suggestions
- nit:
transferOnceaddedlogparameter but the JSDoc@param {any} [log]is marked optional whiletransferAndSettlehas it required in the signature - minor doc inconsistency -src/support/serenity/resource-manager.js:234 - suggestion: The
countPublishedPromptstests cover the page-ceiling and empty-project cases well; consider adding a test for whentransport.listPromptsByTagsrejects (error propagation on the pagination walk) -test/support/serenity/handlers/markets.test.js:1660
Note: CI build is still pending - verify it passes before merge.
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 5m 3s | Cost: $1.87 | Commit: 89f7a0df7ab7a4cfdc0e186d8727ce783a62353f
If this code review was useful, please react with 👍. Otherwise, react with 👎.
… coverage (LLMO-5616) Adds a releaseAiSurplus test where the release transfer 422s "insufficient units": transferAndSettle maps it to orgPoolExhausted and releaseAiSurplus swallows it best-effort. Closes the 3-line codecov/patch gap in resource-manager.js (now 100%). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nsolidated # Conflicts: # src/controllers/serenity.js # src/support/serenity/handlers/markets-subworkspace.js # src/support/serenity/handlers/prompts-subworkspace.js
rainer-friederich
left a comment
There was a problem hiding this comment.
Approving. Reviewed against the design (https://github.com/adobe/serenity-docs/pull/22) and the mock foundation (adobe/spacecat-shared#1767).
Architecture is sound and the implementation is faithful to the spec, with three deliberate, pre-approved deviations: the global env kill-switch (replacing the per-org DB flag), the fail-fast hot path (one transfer, no settle poll, retryable 503 on a still-settling child), and PROJECT_BLOCK = 1. The OFF path is proven byte-for-byte (spy-based transport-sequence tests + the full suite green with the flag defaulting OFF), so this is safe to merge and deploy to prod dormant — SERENITY_DYNAMIC_ALLOCATION defaults OFF and this changes nothing until the flag is flipped.
This lands the consume-side fronting only. The remaining work gates the flag flip, not this merge — all api-service fast-follows plus one ops step, tracked on the spec PR:
- fail-fast release wiring on delete / model-remove (inline, no new infra);
- the 405 guard —
isMeteredQuotais built + unit-tested but not yet consumed anywhere insrc; - a per-brand ceiling — supported by
ensureAiHeadroombut not passed by the controller today; - an ON-path integration test (writable now against the mock
__quota/PARENT_WITH_CHILD_METEREDseed); - a pre-flip guard asserting
childId/masterIdare present (a missing master currently silently no-ops the ON path); - the live-gateway canary to pin the disguised-405 body and validate the post-transfer write race.
None of those block this merge.
…delete-release fast-follow, no rightsizing sweep (LLMO-5616) Comment-only (no behavior change), mirroring serenity-docs#22 (Rainer 2026-07-08) at the code sites: - releaseAiSurplus: inline/fail-fast/best-effort by design, no async infra needed (self-heals). - delete handler: release wiring is a fast-follow; keep the same inline shape when it lands. - carve-skip: no rightsizing/backfill sweep exists or is planned — evaluated + rejected, not deferred. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rainer-friederich
left a comment
There was a problem hiding this comment.
Overall looks great, let's do the following:
- rename
masterIdtoparentWorkspaceId - rename
childIdtosubWorkspaceId - Missing masterId | parentWorkspaceId should fail loud
- Can we have integration tests against the "on" feature flag using our existing mocks? Needs a release of adobe/spacecat-shared#1767
- Can we solve the open question regarding
includeDraftedupfront?
This then needsw a follow up PR I guess with all the still required extnesions:
- ceiling threaded from the controller
- pre-flip guard.
- release-on-delete
- isMeteredQuota retry
Afterward we need acanary to do that.
…l-loud on missing ids, resolve includeDrafted (LLMO-5616) Addresses PR review feedback: 1. Rename `masterId` → `parentWorkspaceId` and `childId` → `subWorkspaceId` across the allocator (resource-manager), the headroom guard, both fronting handlers, and the controller — aligning with the domain vocabulary already used by workspace-resolver / brand-provisioning (parentWorkspaceId). 2. The rename surfaced that handleCreateMarketSubworkspace received the parent id BOTH positionally (arg 3, used by ensureSubworkspace) AND redundantly in its options bag — dropped the redundant options-bag copy; the JIT units pool is the positional `parentWorkspaceId`. Controller call sites updated accordingly. 3. Fail LOUD on a missing/blank subWorkspaceId or parentWorkspaceId in ensureAiHeadroom (new `requireWorkspaceId` guard → throws, 500) rather than silently reading `''` and draining the wrong pool / throwing opaquely. 4. Resolve the `includeDrafted` open question at the update-models seam: per design §2 (write-only metering — the prompt dimension is consumed at prompt WRITE, not re-gated at publish), publishing drafted prompts triggers no prompt-dimension quota gate, so sizing `publishedTexts × netDelta` is correct and `includeDrafted` is not needed here. Comment resolved; §8 canary confirms. type-check + lint clean; resource-manager.js and dynamic-allocation-active.js at 100% coverage (added a fail-loud unit test); controller tests updated to assert the parent id is threaded positionally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks @rainer-friederich — addressed in
On the follow-up PR — agreed, I'll do the remaining extensions there as you scoped: ceiling threaded from the controller, the pre-flip guard, release-on-delete, and the Green: |
There was a problem hiding this comment.
Hey @byteclimber,
Verdict: Approve - rainer's feedback (rename, fail-loud guard, includeDrafted resolution) is addressed cleanly; no blocking concerns.
Complexity: HIGH - large diff (2471 lines, 21 files); API surface signal.
Changes: Renames masterId/childId to parentWorkspaceId/subWorkspaceId across the allocator, adds a fail-loud requireWorkspaceId guard, and resolves the includeDrafted design question per spec section 2 (21 files).
Note: CI build is still pending - verify it passes before merge.
Non-blocking (2): minor issues and suggestions
- nit:
test/it/shared/tests/serenity.jsandtest/support/serenity/handlers/tags.test.jsrenamechildIdtosubWorkspaceIdin a tag-hierarchy context where the variable holds a child TAG id (a topic-category child of a root tag), not a sub-workspace id - a mechanical find-and-replace collateral that muddies the intent for the next reader. Consider reverting tochildIdor renaming tochildTagId. - suggestion:
releaseAiSurplusinsrc/support/serenity/resource-manager.jsdoes not callrequireWorkspaceIdlikeensureAiHeadroomdoes - a blanksubWorkspaceIdwould produce an opaque transport error rather than the clear 500. Adding the same guard would improve debuggability on the release path. -src/support/serenity/resource-manager.js:1046
Previously flagged, now resolved
masterIdrenamed toparentWorkspaceIdacross the allocator, headroom guard, fronting handlers, and controller (vocabulary alignment with workspace-resolver/brand-provisioning).childIdrenamed tosubWorkspaceIdeverywhere.- Missing workspace id now fails loud via
requireWorkspaceIdguard (throws Error -> 500) instead of silently reading empty string. includeDraftedopen question resolved per spec section 2 (write-only metering: no publish-time prompt gate).- Serialization test is now stateful (proves the critical section serializes via exactly one transfer when two concurrent ops each need +1 project).
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 0m 4s | Cost: $4.45 | Commit: bb307b4cb7fd88053973dd5321435652624238c2
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…on (LLMO-5616) Addresses Rainer's review item #4 — integration test against the dynamic-allocation feature flag ON, using the existing Semrush mocks (now on the released 1.5.0 metered User Manager mock). - Bump @adobe/spacecat-shared-user-manager-client 1.3.1 → 1.5.0 (adobe/spacecat-shared#1767 merged + released) so the IT pulls the mock image with per-workspace AI resource accounting (SERENITY_UM_MOCK_TAG follows the installed client version). - Give ORG_1 a Semrush parent workspace (SERENITY_ORG_PARENT_WS_ID) so BRAND_1's sub-workspace resolves a non-null parentWorkspaceId — the condition the JIT guard needs to engage — pointed at an existing UM-mock workspace so the advisory pool read resolves. - Add setUmMockQuota / dumpUmMock harness helpers (the mock's __quota / __dump control routes), threaded into the shared serenity test factory. - New IT: with the global SERENITY_DYNAMIC_ALLOCATION env ON and the child seeded at a zero resource total, a real create-market fronts project headroom and tops the child up from 0 via a live /resources transfer to the mock. Asserting the total grew is a positive over-the-wire proof that the flag-ON JIT engaged (a flag-OFF run never transfers) and that the global kill-switch toggle takes effect per request. The prompt write path runs as a smoke; prompt texts×models sizing stays unit-test-covered. Verified locally against the live container stack: serenity IT 36 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
✅ Item #4 (flag-ON integration test) — done in Now that adobe/spacecat-shared#1767 is merged + released (user-manager-client 1.5.0, with the metered mock image), I bumped the dep and added a flag-ON IT to the serenity suite:
Verified locally against the live container stack: serenity IT 36 passing. That closes all five of your immediate items (1–3 in |
There was a problem hiding this comment.
Hey @byteclimber,
Verdict: Approve - clean flag-ON integration test; no blocking concerns.
Complexity: HIGH - large diff (2706 lines, 27 files); API surface signal.
Changes: Adds a flag-ON metered integration test for the JIT dynamic allocation path, proving the end-to-end top-up fires via a live /resources transfer to the UM mock (7 files).
Non-blocking (2): minor issues and suggestions
- nit:
SERENITY_ORG_PARENT_WS_IDreuses the same UUID asSERENITY_MOCK_PROJECT_ID(b2c3d4e5-...) - sharing an identifier across semantically different entity types (project vs parent workspace) can make future test failures confusing to debug if both constants are referenced in the same test context. A distinct UUID would be clearer. -test/it/shared/seed-ids.js:76 - suggestion:
childTotal(dump, dim)returnsundefinedon any shape mismatch (missingworkspace_resources, missing CHILD record, missingaisub-structure), which combined with.to.be.greaterThan(0)would produce a confusing assertion (expected undefined to be above 0). A guard assertion likeexpect(rec, 'child record not found in dump').to.existbefore accessing the total would make failures actionable. -test/it/shared/tests/serenity.js
Previously flagged, now resolved
- Flag-ON integration test now covers the metered JIT top-up end-to-end via the live UM mock (Rainer's review item 4).
@adobe/spacecat-shared-user-manager-clientbumped to 1.5.0 (prerequisite for metered mock image).
Note: CI build is still pending - verify it passes before merge.
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 5s | Cost: $4.35 | Commit: 7a068a4019aebdd86f8777ef22d3c34d09342e2c
If this code review was useful, please react with 👍. Otherwise, react with 👎.
|
Re-reviewed the branch at Item 5 — confirmed. Spec section 2 (write-only metering: the prompt dimension is consumed at prompt write and is not re-gated at publish) is the intended model. The Item 3 — the fail-loud guard is unreachable in production
const idsMissing = !subWorkspaceId || !parentWorkspaceId || ...
if (!enabled || idsMissing) {
if (enabled) { log?.warn?.('... — skipping JIT top-up for this request', ...); }
return { enabled: false, ensure: async () => ({ toppedUp: false }) };
}
Why it matters at flip time: under the flag the flat pre-carve is skipped as well, so a brand whose org has no parent workspace gets neither a carve nor a JIT top-up. Its sub-workspace sits at zero AI resources and the metered write fails at the Semrush gateway with an opaque error instead of the clear 500. The new IT is itself the evidence — ORG_1 had to be given a parent workspace for the guard to engage at all, which means the pre-existing seed orgs had none. Please move the check into MysticatBot non-blocking findings still openWorth doing before the flag flip:
Cosmetic, but should be cleaned up rather than dropped:
Deliberately deferred and fine as-is: the duplicate Separate ticketThe pre-existing |
Follow-ups are now trackedEverything deferred out of this PR — the rollout hardening listed in the description, the items I scoped for the follow-up PR, the open MysticatBot findings, and the pre-existing pool leak — now has a ticket. All four sit under epic LLMO-5005 and are assigned to @byteclimber. LLMO-6190 — flag-flip readiness: correctness gates before https://jira.corp.adobe.com/browse/LLMO-6190 The fail-loud guard for a missing The fail-loud guard is still my ask for this PR. If it lands here, strike it from the story rather than doing it twice. LLMO-6191 — rollout hardening: rightsizing sweep, observability, cross-container serialization https://jira.corp.adobe.com/browse/LLMO-6191 The one-time rightsizing backfill of already-carved children (without it the pre-carve pool drain persists for every existing brand); pool-free gauge, top-up p99, exhaustion and classifier-match rates, not-ready retry rate, release-failure rate; the distributed lock for the absolute-set race across Lambda containers; the safety-valve timeout on LLMO-6192 — close out the open review findings https://jira.corp.adobe.com/browse/LLMO-6192
LLMO-6189 — https://jira.corp.adobe.com/browse/LLMO-6189 Filed as a Bug, since it is pre-existing on main, live today, and independent of this PR and of the flag. All four shipped call sites — |
…ck safety valve, and hygiene (LLMO-5616) Item 3 (BLOCKING): the requireWorkspaceId fail-loud guard added for missing workspace ids was unreachable — createHeadroomGuard tested subWorkspaceId/ parentWorkspaceId first and returned a silent no-op guard before ever calling ensureAiHeadroom (the only place the check ran). Flag ON with a missing parentWorkspaceId was still a silent skip. Moved the check into createHeadroomGuard itself: it now throws when enabled && ids are missing, instead of warn-and-no-op. A brand whose org has no parent workspace now fails loud (500) instead of silently sitting at zero AI resources with no carve and no top-up. Also addressed, per the same review: - withResourceLock had no timeout on the predecessor promise — a hung transport call would block every subsequent same-child write forever, until the container recycles. Added a LOCK_TIMEOUT_MS safety valve (10s default, overridable): a waiter stops waiting on a hung predecessor and runs anyway, trading a bounded/rare clobber risk for guaranteed forward progress. The predecessor's own task is not cancelled, only the queue stops waiting on it. - releaseAiSurplus did not call requireWorkspaceId (unlike ensureAiHeadroom) — a blank subWorkspaceId produced an opaque transport error instead of a clear 500. Added. - countPublishedPrompts propagated an unhandled rejection mid-pagination-walk, failing the whole metered write. Now treats a mid-walk failure as truncation (same as the page-ceiling case) and returns the counted-so-far; added the missing rejection-path test. - Rename collateral: an earlier masterId/childId -> parentWorkspaceId/ subWorkspaceId sweep over-reached into two TAG-id parameters (tags.test.js, the IT) that have nothing to do with workspace ids. Renamed to childTagId. - releaseAiSurplus's best-effort failure log now uses structured metadata instead of string interpolation, matching the rest of the file. - transferOnce/transferAndSettle's log param JSDoc is now consistently optional. - SERENITY_ORG_PARENT_WS_ID in the IT seed ids reused SERENITY_MOCK_PROJECT_ID's UUID silently. Verified via the mock's source that a genuinely distinct third workspace isn't practically available (the UM mock's exists() gate + the mock/* subpath being in-monorepo-only for an external consumer) — made the reuse an explicit, documented alias (single source of truth) instead of a copy-pasted literal. - The flag-ON IT's childTotal helper returned undefined on a shape mismatch, producing an unhelpful "expected undefined to be above 0" failure. Added an existence guard with an actionable message. isMeteredQuota's substring match is left as-is per Rainer's own review — explicitly deferred to the PR-4 live-gateway canary, not a regression to fix here. Verified: type-check + lint clean; full unit suite 14303 passing (all coverage thresholds met); live serenity IT re-run against real containers, 36 passing (confirms the flag-ON fix works end-to-end, not just against mocks). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the re-review @rainer-friederich — addressed in Item 3 (BLOCKING) — fixed properly this time. You were right: MysticatBot's non-blocking findings — all done:
Cosmetic — all done:
Separate ticket — confirmed: Verified: type-check + lint clean, full unit suite 14303 passing, live serenity IT re-run against real containers — 36 passing (confirms the fix holds end-to-end, not just against mocks). |
There was a problem hiding this comment.
Hey @byteclimber,
Verdict: Approve - clean fix commit addressing all of Rainer's review feedback; no blocking concerns.
Complexity: HIGH - large diff (2706 lines, 27 files); API surface signal.
Changes: Moves the fail-loud guard to the guard construction point (fixing reachability), adds a safety-valve timeout to the resource lock, and handles mid-walk upstream failures in prompt counting (12 files).
Non-blocking (1): minor issues and suggestions
- nit: The 12-line block comment explaining why
SERENITY_ORG_PARENT_WS_IDaliasesSERENITY_MOCK_PROJECT_IDis disproportionate to a single assignment - 3 lines ("aliases the mock project id because the UM mock 403s on unregistered workspace ids, and this is the only available mock workspace not already claimed by BRAND_1") would capture the essential reason without the implementation trivia about__seedandpackage.jsonexports -test/it/shared/seed-ids.js:72
Previously flagged, now resolved
- Fail-loud guard (
requireWorkspaceId) moved from insideensureAiHeadroom(unreachable from the handler path) tocreateHeadroomGuardconstruction - now actually fires when flag ON + ids missing (Rainer's item 3). - Safety-valve timeout added to
withResourceLock- a hung predecessor no longer blocks same-child ops forever (MysticatBot suggestion from prior reviews). countPublishedPromptsmid-walk upstream failure now returns counted-so-far instead of rejecting (MysticatBot suggestion).releaseAiSurplusstructured logging fix (no more string-interpolated workspace ids).- IT
childTotalhelper now has a guard assertion for actionable failure messages. - Tag test variable renamed from
subWorkspaceIdtochildTagIdto eliminate vocabulary confusion with workspace allocator ids.
Note: CI build is still pending - verify it passes before merge.
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 0m 57s | Cost: $5.88 | Commit: e299803b010373ed7d9ac90aadd5b02081c46dac
If this code review was useful, please react with 👍. Otherwise, react with 👎.
rainer-friederich
left a comment
There was a problem hiding this comment.
LGTM, please turn this on on prod and do the proper canary check with our testing org
# [1.646.0](v1.645.0...v1.646.0) (2026-07-14) ### Bug Fixes * **SITES-47960:** accept caller-supplied x-promise-token in page-relationships ([#2806](#2806)) ([de3572b](de3572b)) * **slack:** forward mode to non-prerender audit types (LLMO-6167) ([#2807](#2807)) ([50c98bb](50c98bb)) ### Features * **serenity:** dynamic AI resource allocation — consolidated allocator + kill-switch + hot-path wiring (LLMO-5616) ([#2764](#2764)) ([674334a](674334a)), closes [#2750](#2750)
|
🎉 This PR is included in version 1.646.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
What & why
Just-in-time Semrush AI resource allocation — the parent-org pool is carved onto brand sub-workspaces up front (flat), which both under- and over-allocates on the tiny gold pool (13 projects / 800 prompts). This replaces the flat carve with a JIT top-up at the metering moment.
This PR is shaped directly by Rainer's direction on serenity-docs#22 (comment): consolidate the three approved-but-inert PRs into one and land them together with the actual handler-fronting, behind an OFF switch — "add" is not "enable."
Supersedes (will be closed once this is green)
This PR supersedes and replaces:
ensureSubworkspacecarve-skip (brought in; now keyed off the global switch).Key decisions (call-outs for reviewers)
1. Global kill-switch replaces the per-org DB flag (supersedes #2750)
Per Rainer's explicit direction, the per-org
dynamic_allocationDB flag is replaced by a single global env/Vault booleanSERENITY_DYNAMIC_ALLOCATION(default OFF), read once per request offcontext.env— the same shape this repo already uses for its other global serenity toggles (SERENITY_ALLOW_WORKSPACE_DELETE, etc.). No PostgREST read, no per-org cache. Rollback is one env flip. Lives indynamic-allocation-active.js.2. Fail-fast hot path — NO settle poll on the synchronous request (behavior change vs the original plan)
A Semrush
/resourcestransfer now gates every Adobe write. The original allocator blocked on a 12s settle poll + a 4-attempt not-ready retry (1+2+3s backoff) — that blows the ~15s Fastly/Lambda edge budget and risks a 504 with a half-applied absolute transfer stranded. Per Rainer's hard constraint,ensureAiHeadroomnow does ONE transfer attempt, no settle poll; a still-settling child returns a retryable503(workspaceBusy, "provisioning, retry") immediately and the client/UI retries. The poll+retrytransferAndSettleremains for the async/reconciler release path only (not the request path).3. Actual handler-fronting (was never built), behind the OFF switch
Metered-write handlers front their metered op through a headroom guard:
createProject; prompt headroom beforepublishProject, sized fromused + drafted(staleness-immune — the post-publishusedreconciles async/stale-low).used + drafted) beforepublishAffected.publishedTexts × Δmodelsbefore the sharedsyncModelsForProjectpublishes — fronted at the mode-guarded caller, not inside sharedsyncModelsForProject, so flat mode stays byte-for-byte.4. Same-child concurrency lock
resource-lock.jsserializes per-child top-ups (in-process), closing the stale-read-clobber-of-absolute-sets under-provision race for the intra-requestmapLimitfan-out and same-container concurrency. Cross-container serialization is a PR-4 follow-up (see below).5. Flag OFF is byte-for-byte
When OFF (or child/master ids missing), the guard is a genuine no-op — zero transport calls. Proven by the spy-based transport-call-sequence tests (
dynamic-allocation-fronting.test.js) and by the full pre-existing suite passing unchanged with the flag defaulting OFF.Enforcement choke point
dynamic-allocation-fronting.test.jsasserts every subworkspace metered-write handler reads child headroom when the flag is ON — a future metered handler added without fronting fails this test.Tests / gates
npm test— 14167 passing, coverage 99.9% / 99.2% / 99.1% / 99.9% (gate: 90% aggregate).npm run lintclean;npm run type-check(blocking// @ts-checkgate) clean.used+draftedpublish sizing, need=0 no-op, partial/idempotent transfer, pool-exhaustion vs brand-limit vs transient-422, stale-lowused, concurrency lock, flag-OFF byte-for-byte, enforcement choke point.Explicitly deferred, tracked as follow-up (PR-4 rollout hardening)
Not built here (operational scope; the flag stays OFF until these land):
orgPoolExhausted/brandAiLimitrate, 405-classifier match ratio, not-ready-retry rate, release-failure rate + the paging split.releaseAiSurpluswiring — built + tested but not wired into the delete request path (no async/reconciler mechanism exists yet; wiring it synchronously would block/strand). Consumer is the deferred reconciler.The §3 doc drift in serenity-docs (
PROJECT_BLOCK5→1) is fixed in a separate small serenity-docs PR.Defaults
SERENITY_DYNAMIC_ALLOCATIONdefaults OFF — this PR adds capability, it does not enable it.🤖 Generated with Claude Code