Skip to content

fix(serenity): remove dormant Semrush AI allocator, §10.1/§10.2 (SITES-49206) - #2995

Open
aliciadriani wants to merge 12 commits into
mainfrom
SITES-49206-remove-semrush-ai-allocator
Open

fix(serenity): remove dormant Semrush AI allocator, §10.1/§10.2 (SITES-49206)#2995
aliciadriani wants to merge 12 commits into
mainfrom
SITES-49206-remove-semrush-ai-allocator

Conversation

@aliciadriani

@aliciadriani aliciadriani commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Premise

Semrush no longer enforces AI project/prompt limits for LLMO workspaces reached via the Adobe proxy (confirmed 2026-07-28). The just-in-time (JIT) allocation machinery this PR removes only ever worked around limits that no longer apply, and its global kill-switch (SERENITY_DYNAMIC_ALLOCATION) was OFF in every deployed environment, so the removed code paths were dormant. Behaviour on the flag-OFF path is byte-for-byte identical before/after — a no-op headroom guard is simply replaced by the direct call it wrapped.

Part of the Serenity Post-GA cleanup (story SITES-49196). Inventory: https://github.com/adobe/serenity-docs/pull/62
Jira: https://jira.corp.adobe.com/browse/SITES-49206

What was removed (§10.1 / §10.2)

Modules deleted (+ their tests):

  • src/support/serenity/dynamic-allocation-active.js (kill-switch, ceiling resolver, createHeadroomGuard, retryOnQuota)
  • src/support/serenity/resource-manager.js (whole allocator: ensureAiHeadroom, releaseAiSurplus, readAiTotals, roundUpToBlock, modelChangeUnits, transferOnce/transferAndSettle, requireWorkspaceId, blocks/ceiling constants, the org-pool/brand-limit/workspace-busy error factories)
  • src/support/serenity/resource-lock.js (withResourceLock, clearResourceLocks)
  • scripts/serenity-rightsizing-sweep.mjs — deleted per §10.7: it hard-imports the now-deleted releaseAiSurplus and block constants from resource-manager.js, so it cannot compile or lint once the allocator is gone. It is pure allocator tooling with no life after the allocator. The metered-405 canary is kept (scripts/serenity-metered-405-canary.mjs); §10.7's "delete last" applies to it, not to the sweep. It reads getWorkspaceResources and drains via transferWorkspaceResources; only the latter is canary-scoped (the former has a live elements.js caller) — see the rest-transport.js note below.
  • Tests: resource-manager.test.js, resource-lock.test.js, dynamic-allocation-active.test.js, dynamic-allocation-fronting.test.js

Wiring unpicked:

  • controllers/serenity.js — removed the dynamicAllocationEnabled / brandAiCeiling helpers and the dynamicAllocation / parentWorkspaceId / ceiling options from all five subworkspace handler call sites.
  • brand-provisioning.js — dropped the flag/ceiling resolution and options.
  • handlers/prompts-subworkspace.js — removed the headroom guard, ensure, and retryOnQuota wrapping (direct calls restored).
  • handlers/markets-subworkspace.jsnot in the original file list, but it carried the largest share of the allocator wiring (headroom guards in create-market / update-models / generate-prompts, releaseAiSurplus on delete and net-model-removal, modelChangeUnits). Unpicked here because the modules can't be deleted while it imports them.
  • rest-transport.jstransferWorkspaceResources and getWorkspaceResources are both kept, but on different lifetimes. getWorkspaceResources has a live production callerelements.js checkAccess (GET .../brand-presence/access, LLMO-6747, the "workspace access needed" banner) — so it is not canary-scoped and stays regardless. transferWorkspaceResources is canary-scoped: the retained §10.7 metered-405 canary (which drains a throwaway sub-workspace through it) is its only caller, so it is retired with the canary under §10.6/§10.7 "delete last". A strict-tier type pin (test/types/strict/serenity-transport-strict.types.js) now guards both against a silent rename (the canary sits outside tsconfig/CI). Recorded in ADR-009. (Earlier revisions of this PR removed transferWorkspaceResources outright, and then wrongly coupled getWorkspaceResources to the canary — both corrected; thanks @rainer-friederich.)
  • errors.js — removed isPoolExhausted / isWorkspaceNotReady (+ orphaned bodyText) and the ORG_POOL_EXHAUSTED / BRAND_AI_LIMIT / WORKSPACE_BUSY codes. isWorkspaceBusyError does not exist in this repo (its elmo-side detector is out of scope).
  • Dead code the removal stranded, now cleared: handlers/markets.js countPublishedPrompts (allocator was its only caller) and quota-alerts.js alertPoolFreeThreshold (called only from resource-manager.js), plus their tests. The two wrapPublish publish-retry seams are kept as identity-default injection points for §10.3, with their JSDoc updated to stop describing the removed headroom.retryOnQuota machinery.
  • In-code references to SERENITY_DYNAMIC_ALLOCATION / SERENITY_BRAND_AI_CEILING_PROJECTS / SERENITY_BRAND_AI_CEILING_PROMPTS removed.

§10.2 (flat pre-carve): already absent — createSubworkspace posts no resources body. Only the stale module-header comment in workspace-lifecycle.js was updated.

Spec inconsistency, resolved the only way that compiles

allocation-metrics.js is trimmed, not deleted — and this is compliance, not a deviation. §10.6 keeps toQuotaExceededError / isMeteredQuota, which call recordRejection (errors.js:173) and recordMeteredQuotaClassifier (errors.js:112) respectively — yet §10.6's own delete set also names recordMeteredQuotaClassifier. Those two instructions cannot both be honoured: deleting the recorder (or the whole module) fails to compile against the §10.6-kept callers. So the six allocator-only recorders were removed and these two kept, with the test trimmed to match. Keeping them is the only resolution consistent with §10.6, and the §10.6 delete-LAST boundary is where they finally go.

FOLLOW-UP checklist (deferred)

  • §10.3 quota-405 handling — verify whether the empty-units publishMode:'best-effort' rejection is a distinct upstream check that survives the limits change; only then remove republishBestEffort's 405 swallow and the competitor/alias/brand-URL best-effort paths. Left fully in place here (the two wrapPublish seams remain wired at identity for this).
  • §10.4 — mysticat-data-service migration CLI allocation (separate repo).
  • §10.5 — spacecat-shared mock quota; close feat(project-engine-client): meter prompt units as texts × models in the mock (LLMO-5616) spacecat-shared#1795. The IT setUmMockQuota / __quota seam is deliberately retained (unconsumed today) for this change — see test/it/postgres/setup.js.
  • §10.6 quota-exhaustion handling (ERROR_CODES.QUOTA_EXCEEDED, toQuotaExceededError, isMeteredQuota, quota-alerts.js, the two kept allocation-metrics.js recorders, and the elmo half) — delete LAST, only after the §10.7 canary records the new upstream behaviour per env. This step also retires the canary itself and the one canary-scoped transport method (transferWorkspaceResources) — NOT getWorkspaceResources, which elements.js checkAccess keeps alive — and must settle the durable re-check of the limits-unenforced premise (the manual canary is only the interim one) — see ADR-009.
  • §10.7 canary — per-environment evidence — run scripts/serenity-metered-405-canary.mjs on dev, stage and prod with a live IMS token and a real throwaway sub-workspace id (manual HUMAN step — nothing schedules it) and record each result here before §10.6. This turns the no-carve premise from "vendor statement + single GM-migration observation" into per-env evidence, and it is the same run §10.6 needs regardless. Soak ("not the same week") is already met.
  • Ops: remove SERENITY_DYNAMIC_ALLOCATION / SERENITY_BRAND_AI_CEILING_* from Vault (dx_mysticat/{env}/api-service) — not touched here.
  • DAI-ICHI / v1 premise check: DAI-ICHI remains on v1 (pre-Brandalf). The limits-unenforced premise is stated for proxy-routed LLMO workspaces — confirm no removed guard was the sole protection for a live non-allocator cohort before removing §10.6.
  • Stale spec doc (out of scope here, flagged by @rainer-friederich): docs/specs/2026-06-15-serenity-subworkspace-dual-mode-implementation-plan.md:149,171,214 still describes transferWorkspaceResources as part of decommissionBrandWorkspace and the de-resourced re-grant path. Those paths predate this PR and no longer exist, so a §10.6 author greps up a phantom production caller for the method. Correct or annotate that planning doc before §10.6 acts on the caller inventory (left as tracking, not changed here, since it is a planning artefact outside this PR's scope).
  • Docs reconciled (this PR): added docs/decisions/009-remove-dormant-jit-allocator.md as the superseding record for the removal (ADR-008's rejected "delete the allocator" alternative is now the adopted decision); reconciled the ADR-007 / ADR-008 headers and inline pointers and docs/serenity.md (including its incorrect listing of the trimmed allocation-metrics.js as deleted). docs/runbooks/serenity-zombie-workspace-recovery.md was deleted with the allocator.

Semrush no longer enforces AI project/prompt limits for proxy-routed LLMO
workspaces (confirmed 2026-07-28), so the just-in-time (JIT) allocation
machinery only works around limits that no longer apply. This removes the
dormant allocator and unpicks its wiring.

Removed:
- Modules: dynamic-allocation-active.js, resource-manager.js, resource-lock.js
  (+ their test files, and the dynamic-allocation-fronting test).
- allocation-metrics.js: dropped the six allocator-only recorders; kept
  recordRejection + recordMeteredQuotaClassifier (load-bearing for the
  §10.6 quota-exhaustion handling that is deleted LAST).
- Wiring: controllers/serenity.js, brand-provisioning.js,
  handlers/prompts-subworkspace.js, handlers/markets-subworkspace.js
  (headroom guards, ensure/retryOnQuota, releaseAiSurplus, modelChangeUnits).
- rest-transport.js transferWorkspaceResources (allocator-only method).
- errors.js: isPoolExhausted / isWorkspaceNotReady predicates and the
  ORG_POOL_EXHAUSTED / BRAND_AI_LIMIT / WORKSPACE_BUSY codes.
- Flags SERENITY_DYNAMIC_ALLOCATION / SERENITY_BRAND_AI_CEILING_PROJECTS /
  SERENITY_BRAND_AI_CEILING_PROMPTS (in-code references only).
- scripts/serenity-rightsizing-sweep.mjs — pure allocator tooling that
  hard-imports the deleted releaseAiSurplus; could not compile without it.

§10.2 (flat pre-carve) was already absent — createSubworkspace posts no
resources body — so only the stale header comment was updated.

Kept (behaviour byte-for-byte, since the allocator flag was OFF in every
deployed environment): §10.3 publish best-effort/quota handling, §10.6
quota-exhaustion handling, and the §10.7 metered-405 canary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@aliciadriani
aliciadriani marked this pull request as ready for review August 5, 2026 13:34
…(§10.7)

Addresses PR-review doc-drift: the dormant JIT allocator was removed in this
PR but the docs still described it as live/dormant.

- ADR-007 (cross-container resource lock): marked SUPERSEDED — the race and its
  mitigation no longer exist.
- ADR-008 (no-subworkspace-carve): amendment note — the no-carve decision now
  unconditional; the "retained behind the flag" fallback is gone.
- docs/serenity.md: dropped "when to turn the JIT allocator on" + the
  "Dynamic AI resource allocation — operations" section (they instructed
  operators to enable now-deleted code); kept the still-valid no-carve
  rationale and pointed the premise re-check at the metered-405 canary.
- Deleted docs/runbooks/serenity-zombie-workspace-recovery.md — its condition
  (a partially-applied allocation transfer) is now unreachable.
- .gitignore: dropped the stale !scripts/serenity-rightsizing-sweep.mjs negation
  (the script was deleted with §10.1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

This PR will trigger no release when merged.

# ADR-007: Cross-container serialization for the dynamic-allocation absolute-set race

> **Applies only when `SERENITY_DYNAMIC_ALLOCATION` is on**, which it is not in any deployed
> **SUPERSEDED (2026-08, SITES-49206).** The dynamic (JIT) Semrush AI allocator this ADR concerns —

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (review, doc-drift §10.7): this PR deletes resource-lock.js/resource-manager.js, so the race this ADR documents no longer exists. Marked SUPERSEDED per §10.7 (kept as a decision record, not deleted).

@@ -1,5 +1,11 @@
# ADR-008: A brand sub-workspace carries no resource allocation

> **Update (2026-08, SITES-49206).** This decision stands and is now **unconditional**: the dormant

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (review, doc-drift §10.7): the no-carve decision here is now unconditional — the dormant JIT allocator it referenced as a flag-gated fallback is deleted in this PR. Amendment note added; core decision retained.

Comment thread docs/serenity.md
future re-tightening upstream surfaces as a failed canary rather than a silent customer-facing
error. Run it per environment when validating the premise.

> **Removed (SITES-49206):** the just-in-time (JIT) top-up allocator

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (review, doc-drift §10.7): removed the "when to turn the JIT allocator on" + "Dynamic AI resource allocation — operations" sections (they instructed operators to enable now-deleted code). Kept the valid no-carve rationale; premise re-check now points at the retained metered-405 canary.

@aliciadriani

aliciadriani commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Self-review pass (round 1) — applied fixes

Code review found the removal itself clean: no dangling references to removed symbols anywhere in src/, and type-check (base+strict) + 16,362 tests pass. The only findings were documentation drift (Should-Fix) — docs still described the deleted allocator as live/dormant. Addressed in commit 582d42e:

  • ADR-007 → marked SUPERSEDED (inline). ADR-008 → amendment note, decision now unconditional (inline). docs/serenity.md → removed the 'turn the allocator on' + operations sections, premise re-check now points at the canary (inline).
  • Deleted docs/runbooks/serenity-zombie-workspace-recovery.md — its trigger condition (a partially-applied allocation transfer) is now unreachable (§10.7).
  • .gitignore → dropped the stale !scripts/serenity-rightsizing-sweep.mjs negation (script deleted with §10.1).

Re-review: nothing further to address. One known nit deliberately left: src/support/serenity/quota-alerts.js (a §10.6 file, delete-last/canary-gated) has a comment referencing ensureAiHeadroom — it retires with that file in the §10.6 follow-up, not here.

@aliciadriani aliciadriani changed the title SITES-49206: remove dormant Semrush AI allocator (§10.1/§10.2) fix(serenity): remove dormant Semrush AI allocator, §10.1/§10.2 (SITES-49206) Aug 5, 2026
@aliciadriani aliciadriani self-assigned this Aug 5, 2026
@adobe adobe deleted a comment from MysticatBot Aug 5, 2026

@aliciadriani aliciadriani left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Full manual review

Summary

Removes the dormant JIT Semrush AI allocator (§10.1) and the flat pre-carve (§10.2): ~4.5k lines of source+tests deleted, plus wiring unpicked across the controller, both metered handlers, brand-provisioning, workspace-lifecycle, rest-transport, and errors. The allocator was OFF in every deployed environment and Semrush stopped enforcing AI limits for proxy-routed LLMO workspaces (confirmed 2026-07-28), so this is a pure dead-code removal. Overall quality: high — consistent, well-scoped, green.

Must Fix (blocks merge)

None.

Should Fix

None. (The doc-drift found in review — ADR-007/008, docs/serenity.md, the zombie runbook, .gitignore — was fixed in commit 582d42e with inline comments.)

Nits (optional)

  • quota-alerts.js QuotaCase typedef still lists orgPoolExhausted/brandAiLimit — cases the now-removed allocator was the only producer of. Harmless (kept §10.6 file, never enabled) and retires with §10.6; no action here.
  • The future §4 settle-poll removal planned to reuse isWorkspaceNotReady; it's removed here, so that later work re-introduces a bounded not-ready retry. Not a bug — just a forward note.

What's Good

  • Verified no dangling references: isDynamicAllocationEnabled, resolveBrandAiCeiling, ensureAiHeadroom, releaseAiSurplus, withResourceLock, createHeadroomGuard, transferWorkspaceResources, isPoolExhausted, isWorkspaceNotReady, bodyText, countPublishedPromptszero remaining callers in src/test.
  • Correct partial trim of allocation-metrics.js: recordRejection + recordMeteredQuotaClassifier are still imported by the deliberately-deferred §10.6 code (errors.js, markets-subworkspace.js) — deleting the whole module would have broken it. Verified.
  • Metered handlers: retryOnQuota/headroom.ensure were no-op passthroughs with the flag off (always), so the direct calls are byte-for-byte prod behaviour.
  • ci / build (incl. tests) green, type-check base+strict green, 16,362 tests pass.

Verdict: approve-worthy. Needs a non-author CODEOWNER approval to clear branch protection.

…0.1)

The `Serenity API — dynamic allocation ON (metered JIT via the live UM
mock)` block asserted the JIT allocator's behaviour — a live /resources
top-up transfer and a brandAiLimit 409 — which this PR removed. With the
allocator gone the writes correctly return 200/201, so the block failed
`expected 200 to equal 409` in ci/it-postgres. Deleted the block and its
now-unused SERENITY_MOCK_WORKSPACE_ID / SERENITY_ORG_PARENT_WS_ID imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	src/controllers/serenity.js
#	src/support/serenity/resource-manager.js
@aliciadriani
aliciadriani deployed to dev-branches August 12, 2026 09:48 — with GitHub Actions Active

@rainer-friederich rainer-friederich 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.

The removal itself is faithful to §10.1/§10.2 and behaviour-preserving. I checked the flag-OFF equivalence rather than taking it on trust: on main, createHeadroomGuard with enabled: false returns ensure: async () => ({ toppedUp: false }) and retryOnQuota: (fn) => fn(), and both publish seams already default to an identity wrapper (handlers/prompts.js:402, handlers/markets.js:1209), so every deleted wrapper was a literal passthrough. The positional parentWorkspaceId that ensureSubworkspace/assertNotParent need survives; only the allocator's copy in the options bags went.

Four things to change before this lands.

1. The rightsizing sweep is not a deviation — the description has it backwards

§10.7 prescribes exactly what this PR did:

scripts/serenity-rightsizing-sweep.mjs — [...] It goes with §10.1, which it cannot compile without — it hard-imports releaseAiSurplus and the block constants.

"Delete last" in §10.7 applies to the metered-405 canary, not the sweep. Deviation 2 should come out of the description; deleting the sweep is compliance, not divergence.

Deviation 1 (trimming allocation-metrics.js instead of deleting it) is correct and the spec is what's inconsistent: recordRejection is called by toQuotaExceededError (src/support/serenity/errors.js:173) and recordMeteredQuotaClassifier by isMeteredQuota (errors.js:112), both §10.6 keeps, while §10.6 itself names recordMeteredQuotaClassifier in its own delete set. Keeping the two is the only resolution that compiles — worth saying that plainly rather than as a deviation needing forgiveness.

2. Dead code the removal creates but does not clear

Each of these had exactly one production caller, and this PR deletes it:

  • countPublishedPrompts (src/support/serenity/handlers/markets.js:1095) — called only by the net-model-delta sizing in handleUpdateModelsSubworkspace. It now has no caller in src/; only its own unit test (test/support/serenity/handlers/markets.test.js:1886-1926) keeps it alive.
  • alertPoolFreeThreshold (src/support/serenity/quota-alerts.js:264) — called only from resource-manager.js:445 on main. It is the allocator's org-pool early-warning, so it is §10.1 in substance even though §10.6 files quota-alerts.js wholesale for later. Delete it here, or state in the follow-up list that §10.6 inherits a function that can no longer fire.
  • The two wrapPublish seams are now caller-less identity wrappers, and their JSDoc still describes the machinery this PR removes: handlers/prompts.js:381-386 ("The subworkspace create-prompts caller passes headroom.retryOnQuota (LLMO-6190 item 4)") and handlers/markets.js:1184-1196 (same). §10.3 revisits this seam anyway, so keeping the parameter is defensible — the doc text pointing at deleted code is not.

3. Test and IT-harness leftovers

  • transferWorkspaceResources is still stubbed and asserted to.not.have.been.called against a transport method that no longer exists: test/support/serenity/workspace-lifecycle.test.js (17 sites), test/support/serenity/brand-provisioning.test.js (7), test/support/serenity/handlers/markets-subworkspace.test.js:93,1021. Those assertions can no longer fail. The latter two files are already touched here.
  • setUmMockQuota (test/it/postgres/setup.js:165) has no consumer left once the flag-ON IT block goes, but is still exported and threaded into mockControls (test/it/postgres/serenity.test.js:16,21). Keeping the __quota control route until the spacecat-shared §10.5 change lands is reasonable — say so at the seam instead of leaving it looking wired.
  • Comments that still justify their subject by the removed guard: test/it/postgres/setup.js:158 ("Used by the dynamic-allocation flag-ON IT"), test/it/shared/seed-ids.js:69-71 ("the condition the dynamic-allocation JIT guard needs to engage"), test/it/postgres/seed-data/organizations.js:30, test/it/README.md:206, and test/support/serenity/handlers/markets.test.js:1916 ("resource-manager's modelChangeUnits sizing").

4. ADR-008 keeps two pointers that no longer resolve

docs/serenity.md and ADR-007 are handled well — the replacement paragraph naming the canary as the premise re-check is exactly what §10.7 asked for. ADR-008 is the gap: the new header marks what follows as historical, but two of those lines are live operator instructions, not history.

  • docs/decisions/008-no-subworkspace-resource-carve.md:70 — "scripts/serenity-rightsizing-sweep.mjs can reclaim them if a pool ever does matter" points at a file this PR deletes. The consequence is now that stranded carves have no remedy; the line should say that.
  • :76 — points at docs/serenity.md § When to turn the JIT allocator on, a section this PR removes.
  • :72-73 and :85 now describe the inverse of reality: the alternative recorded as "Delete the JIT allocator alongside the carve. Rejected" is the decision being taken here. An ADR whose rejected alternative becomes the adopted one normally gets a superseding record rather than a header note.

On the §10 precondition

§10 requires verifying once per environment that a metered write into a zero-allocation sub-workspace succeeds, and names scripts/serenity-metered-405-canary.mjs as the instrument. It is ordered as a hard gate before §10.6, so this is not a blocker for §10.1 — but §10.1 deletes the mitigation that the removed "when to turn the JIT allocator on" procedure pointed at, on a premise currently resting on the vendor statement plus the single GM-migration observation. The canary stays in the tree and needs one IMS token; running it on dev, stage and prod and recording the results in the follow-up list would turn the premise into per-environment evidence, and it is the same run §10.6 will need regardless. The soak requirement ("not the same week") is met.

Description

Drop the Verification block — lint, type-check and test counts are what the checks report. Everything else in the description is worth keeping.

One observation for the record, no action needed here: §10.1's "wiring to unpick" list omits handlers/markets-subworkspace.js, which carried the largest share of the wiring. Good catch finding it — the inventory under-scoped.

…e ADRs (SITES-49206)

Follows Rainer's review on #2995:
- delete dead countPublishedPrompts (markets.js) and alertPoolFreeThreshold
  (quota-alerts.js) — both had the removed allocator as their only caller — with
  their tests.
- keep the two wrapPublish injection seams but rewrite their JSDoc to stop
  pointing at the removed headroom.retryOnQuota machinery (§10.3 seam).
- drop the now-vacuous transferWorkspaceResources stubs/assertions from the
  workspace-lifecycle, brand-provisioning and markets-subworkspace tests
  (the transport method no longer exists).
- document the deliberately-retained setUmMockQuota / __quota IT seam at
  test/it/postgres/setup.js and serenity.test.js (kept for spacecat-shared §10.5);
  reconcile the stale guard-justifying comments in seed-ids.js, organizations.js
  and test/it/README.md.
- add ADR-009 as the superseding record for the allocator removal (ADR-008's
  rejected "delete the allocator" alternative is now adopted); reconcile the
  ADR-007/008 headers + inline pointers and docs/serenity.md (which had wrongly
  listed the trimmed allocation-metrics.js as deleted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aliciadriani
aliciadriani deployed to dev-branches August 12, 2026 11:21 — with GitHub Actions Active

@aliciadriani aliciadriani left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Inline notes marking each change made to address the review — one per fix, grouped by location.

* @param {any} [log]
* @returns {Promise<number>} number of published prompts on the project.
*/
export async function countPublishedPrompts(transport, semrushWorkspaceId, projectId, log) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (dead code cleared): deleted countPublishedPrompts. Its only production caller was the net-model-delta sizing in handleUpdateModelsSubworkspace, which this PR removed — so the function had no caller left in src/ and only its own unit test kept it alive. Removed here with that test.

* `headroom.retryOnQuota` (LLMO-6190 item 4) so a disguised metered-405 gets ONE bounded
* top-up+retry; flat-mode callers omit this param, so flat mode is untouched.
* the inner `publishProject` call. Retained as an injection seam for a future publish-retry
* wrapper (§10.3); no caller passes a non-identity wrapper today, so every publish is a plain call.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (stale JSDoc): the wrapPublish parameter is kept as an injection seam for a future publish-retry wrapper (§10.3), but its JSDoc used to describe the removed headroom.retryOnQuota (LLMO-6190) machinery. Rewritten to describe the seam as it now is: identity default, no non-identity caller today.

* item 4) so a disguised metered-405 gets ONE bounded top-up+retry per project BEFORE it is
* recorded as a failure; flat-mode callers omit this param, so flat mode is untouched.
* behavior). Retained as a per-project injection seam for a future publish-retry wrapper
* (§10.3); no caller passes a non-identity wrapper today, so every publish is a plain call.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (stale JSDoc): same wrapPublish seam as in markets.js — parameter retained (§10.3), JSDoc rewritten to stop pointing at the removed headroom.retryOnQuota machinery.

* @param {object} [log]
* @returns {Promise<void>}
*/
export async function alertPoolFreeThreshold({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (dead code cleared): deleted alertPoolFreeThreshold, the org-pool early-warning alert. Its only caller was resource-manager.js (the deleted allocator). It is §10.1 in substance even though §10.6 files quota-alerts.js wholesale for later, so removing it here avoids §10.6 inheriting a function that can never fire. Its tests were removed too.

* Global kill-switch (`SERENITY_QUOTA_ALERTS_ENABLED`, default OFF) mirrors the existing
* `SERENITY_*` env-flag pattern (dynamic-allocation-active.js, rest-transport.js) — adding this
* capability is not the same as turning it on; ops configure the channel/token and flip it.
* `SERENITY_*` env-flag pattern (rest-transport.js) — adding this capability is not the same as

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (stale reference): the module header cited dynamic-allocation-active.js (deleted by this PR) as an example of the SERENITY_* env-flag pattern. Trimmed to reference only a file that still exists.

Comment thread test/it/README.md Outdated
- **Transport target (`env.js`):** `SEMRUSH_PROJECTS_BASE_URL` → PE mock, `SEMRUSH_USERS_BASE_URL` → UM mock (the User-Manager origin split landed in api-service#2656; it falls back to the projects host when unset, so production needs no new config). `NODE_TLS_REJECT_UNAUTHORIZED=0` trusts the self-signed certs (IT process only). **None of these need Vault / deployed-env config.**
- **Auth (`SERENITY_ALLOW_NON_IMS_AUTH=true`, test-only):** the serenity controller normally forwards only IMS-typed tokens. This flag lets the harness's non-IMS JWT through. It's sound only against the mocks (which ignore the forwarded bearer); in production the real Semrush gateway validates the token end to end, so this never weakens deployed auth. The flag is never set in any deployed environment.
- **Statefulness / isolation:** the mocks are stateful within a run. Auth-exempt control routes — `POST /<prefix>/__reset`, `POST /<prefix>/__seed`, `GET /<prefix>/__dump`, `POST /<prefix>/__quota` — manage that. `setup.js` exposes `resetSemrushMocks()` for tests that mutate mock state, plus `dumpPeMock()` / `dumpUmMock()` (store snapshots) and `setUmMockQuota()` (UM resource metering); the mutating tests reach them through the factory's `mockControls` argument. Readiness is polled via `waitForSemrushMocks()`.
- **Statefulness / isolation:** the mocks are stateful within a run. Auth-exempt control routes — `POST /<prefix>/__reset`, `POST /<prefix>/__seed`, `GET /<prefix>/__dump`, `POST /<prefix>/__quota` — manage that. `setup.js` exposes `resetSemrushMocks()` for tests that mutate mock state, plus `dumpPeMock()` / `dumpUmMock()` (store snapshots) and `setUmMockQuota()` (UM resource metering — retained and still threaded through `mockControls`, but consumed by no test today after the allocator removal in SITES-49206; kept for the spacecat-shared §10.5 metered-write change); the mutating tests reach the others through the factory's `mockControls` argument. Readiness is polled via `waitForSemrushMocks()`.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (stale doc): the README presented setUmMockQuota() as reached by mutating tests. Noted it is retained-but-unconsumed after the allocator removal, kept for the spacecat-shared §10.5 change.

Comment thread docs/serenity.md
> `resource-lock.js`) and its operational surface — the "when to turn it on" flip, the
> zombie-workspace runbook, and the rightsizing sweep — have been deleted. Semrush no longer enforces
> AI limits for proxy-routed LLMO workspaces, so there is no allocation to top up, exhaust, or
> reclaim. `allocation-metrics.js` is **trimmed, not deleted** — `recordRejection` and

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (incorrect doc): this block listed allocation-metrics.js among the deleted modules, but it was trimmed (kept recordRejection / recordMeteredQuotaClassifier for the §10.6 classifier). Corrected, and added the ADR-009 reference for the removal.

@@ -0,0 +1,85 @@
# ADR-009: Remove the dormant JIT Semrush AI allocator

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (superseding record): new ADR-009 — ADR-008's rejected alternative "Delete the JIT allocator alongside the carve" is the decision this PR actually takes, so it gets a superseding record rather than only a header note. It documents the deletion, why the earlier rejection resolved, and the metered-405-canary as the standing per-env check.

@@ -1,5 +1,14 @@
# ADR-008: A brand sub-workspace carries no resource allocation

> **Update (2026-08, SITES-49206).** The **no-carve decision stands and is now unconditional.** Its

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (stale pointers / reversed decision): ADR-008's no-carve decision stands, but its secondary "retain the allocator dormant" decision is reversed. Upgraded the header to say so and point at ADR-009; fixed the two dead body pointers (the deleted rightsizing-sweep line → "no in-tree remedy"; the removed docs/serenity.md flip-procedure section) and marked the reversed Decision/Alternatives inline.

# ADR-007: Cross-container serialization for the dynamic-allocation absolute-set race

> **Applies only when `SERENITY_DYNAMIC_ALLOCATION` is on**, which it is not in any deployed
> **SUPERSEDED (2026-08, SITES-49206; see [ADR-009](009-remove-dormant-jit-allocator.md)).** The

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix (cross-reference): added a pointer to ADR-009 (the record of the allocator removal) in this already-superseded ADR's header, for navigability.

@aliciadriani

Copy link
Copy Markdown
Collaborator Author

@rainer-friederich thanks for the thorough review — all of it is addressed in a1abe03 (inline notes on each change above). Summary:

2 — dead code the removal stranded

  • Deleted countPublishedPrompts (markets.js) and alertPoolFreeThreshold (quota-alerts.js) — both had the removed allocator as their only caller — with their tests. Removing alertPoolFreeThreshold here keeps §10.6 from inheriting a function that can no longer fire.
  • Kept both wrapPublish seams (identity default, §10.3) but rewrote their JSDoc to stop describing the removed headroom.retryOnQuota machinery.

3 — test / IT-harness leftovers

  • Dropped every now-vacuous transferWorkspaceResources stub/assertion across workspace-lifecycle, brand-provisioning, and markets-subworkspace tests (the transport method is gone).
  • Documented the deliberately-retained setUmMockQuota / __quota seam at setup.js and serenity.test.js (kept for spacecat-shared §10.5), and reconciled the stale guard-justifying comments in seed-ids.js, organizations.js, and test/it/README.md.

4 — ADR-008 / superseding record

  • Added ADR-009 as the superseding record (ADR-008's rejected "delete the allocator" alternative is now the adopted decision), fixed ADR-008's two dead pointers (rightsizing-sweep → "no in-tree remedy"; the removed flip-procedure section) and marked its reversed Decision/Alternatives inline, and cross-referenced ADR-009 from ADR-007. Also caught that docs/serenity.md wrongly listed the trimmed allocation-metrics.js as deleted — corrected.

1 & description

  • Reframed the allocation-metrics.js trim as a spec inconsistency resolved the only way that compiles (not a deviation needing forgiveness), and removed the sweep "deviation" (deleting it is §10.7 compliance). Dropped the Verification block and sharpened the §10.7 canary follow-up to call for per-env (dev/stage/prod) evidence.

Not done, by design: the §10 metered-405 canary run (dev/stage/prod, live IMS token) stays a human step on the follow-up checklist.

Verification: lint clean, type-check (base + strict) green, 1452 serenity unit tests passing, no residual references to any removed symbol.

@rainer-friederich rainer-friederich 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.

Hey @aliciadriani,

Verdict: Request changes - every fix from the last round landed and verifies clean, but the same commit deletes a transport method that the retained premise-check script still calls.
Changes: removes the dormant JIT Semrush AI allocator with its wiring, tests, allocator-era script and docs, and adds ADR-009 as the superseding decision record (43 files).

Must fix before merge

  1. [Critical] The retained metered-405 canary calls transferWorkspaceResources, which this PR removes from the transport - src/support/serenity/rest-transport.js:1057 (details inline)
  2. [Important] ADR-009 records the canary as a standing signal, but section 10.7 makes it a manual probe that is itself deleted alongside the classifier - docs/decisions/009-remove-dormant-jit-allocator.md:54 (details inline)
Non-blocking (8): stale references the sweep did not reach

Each of these was accurate on main and became stale because of this PR, so they are removal residue rather than pre-existing debt.

  • nit: resource-lock.js's in-process lock cited as an existing peer cache, and clearResourceLocks cited as a sibling reset - src/support/serenity/quota-alerts.js:34 and :68 (the same header block whose line 25 this commit corrected)
  • nit: the QuotaCase union still admits orgPoolExhausted and brandAiLimit; at head every producer passes brandCarveExhausted, and their only former producer was resource-manager.js - src/support/serenity/quota-alerts.js:40
  • nit: cleanup guidance tells the operator to re-run ensureAiHeadroom - scripts/serenity-metered-405-canary.mjs:54
  • nit: the positional parent is documented as "the units pool when JIT allocation is on"; there is no JIT allocation at head - src/support/serenity/handlers/markets-subworkspace.js:480
  • nit: "// ensure was skipped: no settle/transfer was performed for this call" no longer has an assertion behind it (the test itself is fine - it still asserts the pre-resolved workspace) - test/support/serenity/handlers/markets-subworkspace.test.js:1019
  • nit: dumpUmMock is now consumer-less exactly like setUmMockQuota, but did not get the retention note this commit added to its sibling - test/it/postgres/setup.js:203, threaded at test/it/postgres/serenity.test.js:25, no longer destructured at test/it/shared/tests/serenity.js:843
  • nit: "the mutating tests reach the others through the factory's mockControls argument" - "the others" is now only dumpPeMock - test/it/README.md:206
  • suggestion: the retained seam gets two different forward owners - the code says section 10.3, ADR-009 says section 10.6 - docs/decisions/009-remove-dormant-jit-allocator.md:44 vs src/support/serenity/handlers/markets.js:1132 and handlers/prompts.js:383

Previously flagged, now resolved

  • Description no longer frames the sweep deletion or the allocation-metrics.js trim as deviations, and the Verification block is gone.
  • countPublishedPrompts and alertPoolFreeThreshold deleted with their tests; neither has a caller left anywhere at head.
  • Both wrapPublish JSDoc blocks rewritten; no non-identity caller exists at head, so the text is accurate.
  • All 26 vacuous transferWorkspaceResources stubs and assertions removed, and no surviving it() lost its only meaningful assertion.
  • setUmMockQuota / __quota retention documented at the seam and in the IT README; the guard-justifying comments now describe the surviving parentWorkspaceId requirement, which workspace-lifecycle.js:410 still enforces.
  • ADR-008's two dead pointers fixed and its reversed decision marked, ADR-009 added as the superseding record, ADR-007 cross-referenced, and docs/serenity.md corrected to stop listing the trimmed allocation-metrics.js as deleted.

Comment thread src/support/serenity/rest-transport.js Outdated
*
* NOTE (SITES-49206): the just-in-time allocator that read this before a metered op was removed
* once Semrush stopped enforcing AI limits for proxy-routed LLMO workspaces; this read has no
* in-repo caller today but is kept as part of the transport's workspace-resource surface.

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.

issue (blocking): scripts/serenity-metered-405-canary.mjs:122 calls transport.transferWorkspaceResources(...), and this PR deletes that method just below this note. The canary dies with a TypeError at step 2 (drain prompts.total to used) - after the getWorkspaceResources read on line 109, and before the publish that produces the evidence.

I checked every other transport method the script uses - listLanguages, createProject, createProjectTags, createPromptsByIds, publishProject, getWorkspaceResources. All six survive at head. transferWorkspaceResources is the only missing one, so the script is one method away from working.

Why this is more than a broken dev script:

  • Section 10.7 keeps the canary deliberately and makes it the instrument for section 10's precondition: "Run it per env, record the result, and remove it only once section 10.6's classifier is gone."
  • The description says the canary "only imports the transport" and is therefore safe to keep. It does only import the transport - the transport is what lost the method.
  • ADR-009, added in this commit, names the canary twice as the standing per-environment premise check and as the replacement for the removed flip procedure. docs/decisions/008-no-subworkspace-resource-carve.md:86 and :104 and docs/serenity.md:457 say the same. Four documented claims currently describe a script that cannot run.
  • ADR-009's own Alternatives section rejects keeping serenity-rightsizing-sweep.mjs because it "hard-imports releaseAiSurplus ... so it cannot compile once the allocator is gone". That is the same dependency argument, applied to the transport rather than to resource-manager.js, and it was not carried across to the canary.
  • Nothing in CI can catch this. scripts/ sits outside the tsconfig.json include list, the call is a dynamic property access so lint does not resolve it, and the script never executes in CI. The green checks are not evidence here.

There is also an asymmetry in this very note: getWorkspaceResources is kept as having "no in-repo caller today but kept as part of the transport's workspace-resource surface", while its pair is deleted. The canary's step 1 is getWorkspaceResources and its step 2 is transferWorkspaceResources - one consumer, both methods.

Smallest fix: keep transferWorkspaceResources under the same rationale as the method above it, until sections 10.6 and 10.7 retire the canary together - that is the same "delete last" sequencing section 10.7 already grants the canary. If it has to go now, rewrite the drain step against a surviving call and re-run the script end to end, then correct the "it only imports the transport" line in the description and record the dependency in ADR-009 so the later sections sequence it knowingly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed (now at branch head, commit 594dbd13). Restored transferWorkspaceResources on the transport with its unit test — you're right that it isn't allocator-only: it and getWorkspaceResources are the canary's read/drain pair. Both methods now carry a NOTE naming the canary as their sole remaining consumer and marking them delete-last with §10.6/§10.7. I also corrected the description's "removed transferWorkspaceResources" and "canary only imports the transport" claims, and recorded the dependency in ADR-009's Decision so §10.6/§10.7 sequence it knowingly.

Verified: node --check on the canary + a live typeof transport.transferWorkspaceResources === 'function' probe, type-check (base + strict) green, and the restored transport unit test passing.


1. *"The evidence supports 'our parents are unmetered', not 'no tenant is ever metered'."* Semrush's
confirmation plus the soak now support the stronger claim. The standing check that it still holds
is the `scripts/serenity-metered-405-canary.mjs` probe (serenity-docs#72 §10), run per

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.

issue (blocking): this line calls the canary "the standing check", and line 70 calls it "the replacement signal" for the removed flip procedure. Section 10.7 describes something narrower on both counts. It "needs a live IMS token and a real sub-workspace id, so a human runs it" - nothing schedules it, and this PR adds no scheduling. And it is marked "delete last": removed once section 10.6's classifier is gone.

So the only re-check mechanism this ADR records is itself queued for deletion inside the same cleanup. Once section 10.6 lands, this line, line 70, docs/decisions/008-no-subworkspace-resource-carve.md:86 and :104, and docs/serenity.md:457 all become dead pointers - the same defect class flagged in ADR-008 last round and repaired in this commit, reintroduced one step further out. Section 10.7 anticipates exactly this: "Whatever replaces this paragraph must say how the limits-unenforced premise gets re-checked, rather than pointing at a token that no longer exists."

This is the durable record for a 5,000-line deletion resting on a vendor behaviour change confirmed 2026-07-28, so a reader arriving cold after section 10.6 needs the successor named, not the interim probe.

Fix, prose only and on this branch: in Consequences, say that the canary is a manual per-environment probe which section 10.7 retires together with the section 10.6 classifier, and that the durable re-check beyond that point is an open question section 10.6 must settle before deleting both. Align the ADR-008 bullet at line 86 to the same wording so the two records do not diverge.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. Reframed the canary as an interim, manual per-environment probe that §10.7 retires together with the §10.6 classifier, and called out the durable re-check as an open question §10.6 must settle before deleting both. Applied to ADR-009 (both the rejection-grounds item and the Consequences bullet), the matching ADR-008 bullet (:86) and its Alternatives note (:104), and docs/serenity.md:457. ADR-009's Consequences now states explicitly that leaving a canary pointer past §10.6 would reintroduce the same dead-pointer defect class ADR-008 was repaired for last round.

@rainer-friederich rainer-friederich added the ai-reviewed Reviewed by AI label Aug 12, 2026
@aliciadriani
aliciadriani deployed to dev-branches August 12, 2026 12:13 — with GitHub Actions Active
…frame canary as interim probe (SITES-49206)

Rainer's re-review on #2995:
- [Critical] Restore transferWorkspaceResources on the transport (+ its unit test).
  The retained metered-405 canary (scripts/serenity-metered-405-canary.mjs:122) calls
  it; the PR had removed it, so the canary would TypeError at step 2. Neither it nor
  getWorkspaceResources is allocator-only any more — both are the canary's read/drain
  pair, retired with it under §10.6/§10.7 delete-last. Updated both methods' notes to
  name the canary as their consumer.
- [Important] Reframe the canary as an interim, MANUAL per-env probe that §10.7 retires
  together with the §10.6 classifier — the durable re-check beyond that is an open
  question §10.6 must settle. Fixed ADR-009, ADR-008 and docs/serenity.md, which had
  called it a "standing signal" / "replacement signal".
- Nits: quota-alerts.js header dropped resource-lock.js / clearResourceLocks refs;
  narrowed QuotaCase to brandCarveExhausted; canary cleanup note no longer says re-run
  ensureAiHeadroom; markets-subworkspace.js parent doc no longer cites JIT allocation;
  markets-subworkspace.test.js comment no longer claims a removed assertion; dumpUmMock
  got the same retention note as setUmMockQuota (both threaded-but-unconsumed; only
  dumpPeMock has a live consumer); README "the others" corrected; reconciled the
  wrapPublish seam owner (§10.3 in both code and ADR-009).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@aliciadriani aliciadriani left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review round: inline notes on each new fix. The two blocking items are answered in-thread; these mark the code/doc changes.

Comment thread src/support/serenity/rest-transport.js Outdated
* NOTE (SITES-49206): the just-in-time allocator that used to call this — `transferOnce` /
* `transferAndSettle` in the removed `resource-manager.js` — is gone. The sole remaining caller
* is the retained metered-405 canary (`scripts/serenity-metered-405-canary.mjs`, step 2), which
* drains a throwaway sub-workspace to zero prompt headroom to provoke the disguised 405. Kept —

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [Critical]: restored transferWorkspaceResources (with its unit test). The retained metered-405 canary drains a throwaway sub-workspace through this method, so it is not allocator-only — it and getWorkspaceResources (its NOTE updated too) are the canary's read/drain pair, retired together under §10.6/§10.7 delete-last.

/** @typedef {'brandCarveExhausted'|'orgPoolExhausted'|'brandAiLimit'} QuotaCase */
// `orgPoolExhausted` / `brandAiLimit` were produced only by the removed allocator (SITES-49206);
// every producer at head passes `brandCarveExhausted`.
/** @typedef {'brandCarveExhausted'} QuotaCase */

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review nit: narrowed QuotaCaseorgPoolExhausted / brandAiLimit were produced only by the removed allocator; every producer at head passes brandCarveExhausted.

* five times is one sales signal, not five pages." This is warm-Lambda-container-scoped state
* (no DynamoDB/persistent store), the same shape as this codebase's existing per-container caches
* (markets.js's language cache, resource-lock.js's in-process lock) — sufficient for collapsing a
* (markets.js's language cache) — sufficient for collapsing a

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review nit: header no longer cites resource-lock.js's in-process lock (deleted) as a peer cache; the clearResourceLocks sibling-reset mention on clearQuotaAlertDedup was dropped too.

* @param {object} brand - brand record/stub being provisioned.
* @param {string} parentWorkspaceId - the org parent workspace: the `assertNotParent` guard
* input, and the units pool when JIT allocation is on. Nothing is carved from it.
* @param {string} parentWorkspaceId - the org parent workspace, used as the `assertNotParent` guard

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review nit: the positional parentWorkspaceId doc no longer says it's "the units pool when JIT allocation is on" — there is no JIT allocation at head; it's only the assertNotParent guard input now.

* to run against a disposable dev/throwaway sub-workspace. Re-run `ensureAiHeadroom`/an ordinary
* API top-up (or just re-activate the brand) afterwards if the workspace needs to keep working, or
* decommission the throwaway workspace entirely.
* to run against a disposable dev/throwaway sub-workspace. To restore headroom afterwards, transfer

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review nit: cleanup guidance no longer tells the operator to re-run the removed ensureAiHeadroom; it now points at the inverse transferWorkspaceResources transfer (or re-activating the brand).

Comment thread test/it/README.md
- **Transport target (`env.js`):** `SEMRUSH_PROJECTS_BASE_URL` → PE mock, `SEMRUSH_USERS_BASE_URL` → UM mock (the User-Manager origin split landed in api-service#2656; it falls back to the projects host when unset, so production needs no new config). `NODE_TLS_REJECT_UNAUTHORIZED=0` trusts the self-signed certs (IT process only). **None of these need Vault / deployed-env config.**
- **Auth (`SERENITY_ALLOW_NON_IMS_AUTH=true`, test-only):** the serenity controller normally forwards only IMS-typed tokens. This flag lets the harness's non-IMS JWT through. It's sound only against the mocks (which ignore the forwarded bearer); in production the real Semrush gateway validates the token end to end, so this never weakens deployed auth. The flag is never set in any deployed environment.
- **Statefulness / isolation:** the mocks are stateful within a run. Auth-exempt control routes — `POST /<prefix>/__reset`, `POST /<prefix>/__seed`, `GET /<prefix>/__dump`, `POST /<prefix>/__quota` — manage that. `setup.js` exposes `resetSemrushMocks()` for tests that mutate mock state, plus `dumpPeMock()` / `dumpUmMock()` (store snapshots) and `setUmMockQuota()` (UM resource metering); the mutating tests reach them through the factory's `mockControls` argument. Readiness is polled via `waitForSemrushMocks()`.
- **Statefulness / isolation:** the mocks are stateful within a run. Auth-exempt control routes — `POST /<prefix>/__reset`, `POST /<prefix>/__seed`, `GET /<prefix>/__dump`, `POST /<prefix>/__quota` — manage that. `setup.js` exposes `resetSemrushMocks()` for tests that mutate mock state, plus `dumpPeMock()` (PE store snapshot — the one control a mutating test reaches today, through the factory's `mockControls` argument). `dumpUmMock()` (UM store snapshot) and `setUmMockQuota()` (UM resource metering) are also threaded through `mockControls` but are consumed by no test today after the allocator removal in SITES-49206 — retained for the spacecat-shared §10.5 metered-write change. Readiness is polled via `waitForSemrushMocks()`.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review nit: "the others" was inaccurate — only dumpPeMock has a live consumer; dumpUmMock / setUmMockQuota are retained-but-unconsumed.

re-meter sizing, both of which had the allocator as their only caller;
- the flag-ON integration path.

Three things at the metered-write boundary are **kept**, none belonging to the allocator any more:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix: records that getWorkspaceResources / transferWorkspaceResources are kept for the canary (delete-last), and splits the seam owners — wrapPublish → §10.3, classifier → §10.6 (aligning ADR with the code JSDoc).

procedure.** ADR-008 pointed operators at a "when to turn the JIT allocator on" runbook; that
section and the allocator it armed are both removed. *For now* the `serenity-metered-405-canary.mjs`
probe is the re-check — a **manual** per-environment run whose failure is the trigger to
re-introduce the allocator from history, not to re-enable a flag. It is **not** a standing signal:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [Important]: the canary is reframed as an interim manual probe that §10.7 retires with the §10.6 classifier; the durable re-check is called out as an open question §10.6 must settle.

- **A tenant whose parent enforces limits would get no sizing at all.** _(SITES-49206: this was the
risk the retained allocator covered. The allocator, and the "when to turn the JIT allocator on"
flip procedure this line pointed at in `docs/serenity.md`, are both removed — see ADR-009. The
re-check, for now, is the `scripts/serenity-metered-405-canary.mjs` probe — a **manual**

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [Important]: aligned ADR-008's bullet to the same "interim manual probe, retired with §10.6" wording as ADR-009.

Comment thread docs/serenity.md Outdated
The parent-pool premise is re-checked, not assumed: `scripts/serenity-metered-405-canary.mjs`
drives the real transport against a throwaway sub-workspace and publishes into zero headroom, so a
future re-tightening upstream surfaces as a failed canary rather than a silent customer-facing
error. It is a **manual** per-environment run (live IMS token + a real sub-workspace id; nothing

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [Important]: the canary paragraph now says it's a manual per-env probe retired with §10.6, not the durable re-check.

@aliciadriani

Copy link
Copy Markdown
Collaborator Author

@rainer-friederich re-review addressed — branch head 3c899583 (fix commit 594dbd13, rebased onto the latest main merge). Both blocking items answered in-thread; summary:

[Critical] canary's transport dependency — restored transferWorkspaceResources (with its unit test). It and getWorkspaceResources are the canary's read/drain pair, not allocator-only; both now carry a NOTE naming the canary and marking them delete-last with §10.6/§10.7. Recorded the dependency in ADR-009's Decision and corrected the description's "removed" / "only imports the transport" claims. You were exactly right about the asymmetry — one consumer, both methods.

[Important] canary over-described — reframed as an interim, manual per-env probe that §10.7 retires with the §10.6 classifier, with the durable re-check named as an open question §10.6 must settle. Applied across ADR-009, ADR-008 (:86, :104) and docs/serenity.md:457; ADR-009 now flags that leaving a pointer past §10.6 would reintroduce the dead-pointer defect class from last round.

Nits (all 8)resource-lock.js / clearResourceLocks header refs dropped; QuotaCase narrowed to brandCarveExhausted; canary cleanup note no longer cites ensureAiHeadroom; markets-subworkspace.js parent doc de-JIT'd; the assertion-less test comment reworded; dumpUmMock given the same retention note as setUmMockQuota (only dumpPeMock has a live consumer); README "the others" corrected; wrapPublish seam owner reconciled to §10.3 in both code and ADR-009.

Verified: lint + type-check (base + strict) green, 1234 serenity support tests + 121 transport/quota-alerts tests passing, node --check on the canary plus a live typeof probe confirming both restored methods resolve.

@rainer-friederich rainer-friederich 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.

Hey @aliciadriani,

Verdict: Request changes - the restore landed and every nit from the last round is clean, but the note that justifies it names the wrong caller, and the type guard that pinned the restored method did not come back with it.
Changes: removes the dormant JIT Semrush AI allocator with its wiring, tests, allocator-era script and docs, and adds ADR-009 as the superseding decision record (43 files).

Must fix before merge

  1. [Important] getWorkspaceResources is documented as canary-only, but elements.js checkAccess calls it in production - and ADR-009 plus the §10.6 checklist schedule it for deletion - src/support/serenity/rest-transport.js:1055 (details inline)
  2. [Important] The @ts-expect-error guard pinning transferWorkspaceResources was deleted with the method and not restored with it - test/types/base/serenity-transport.types.js:55 (details inline)
  3. [Important] The records make the canary the premise re-check but describe its outcome backwards, and it has no failure signal - docs/decisions/009-remove-dormant-jit-allocator.md:78 (details inline)
Non-blocking (3): minor issues and suggestions
  • nit: caseType: 'orgPoolExhausted' is still the caseType-varying arm of the dedup-key test, after this commit narrowed QuotaCase to brandCarveExhausted alone - test/support/serenity/quota-alerts.test.js:116
  • suggestion: the strict tier is the only one that reports an unknown member, so adding getWorkspaceResources and transferWorkspaceResources call lines to test/types/strict/serenity-transport-strict.types.js would give the canary's call site the compile-time gate it has never had - a rename would otherwise stay green across rest-transport.js and its unit test and break the canary again
  • nit: the delete-last coupling is documented one way only - the transport notes point at the canary, but the canary's own header says nothing about what its deletion strands - scripts/serenity-metered-405-canary.mjs:16

Previously flagged, now resolved

  • transferWorkspaceResources restored with a unit test whose payload shape matches both the canary's call site and the user-manager v2 transfer contract.
  • Canary reframed as an interim manual probe across ADR-009, ADR-008 and docs/serenity.md, with the durable re-check named as an open question for §10.6.
  • All eight nits landed: the resource-lock.js / clearResourceLocks references dropped, QuotaCase narrowed, canary cleanup no longer citing ensureAiHeadroom, the positional parent doc de-JIT'd, the assertion-less test comment reworded, dumpUmMock given the retention note, the IT README corrected, and the wrapPublish seam owner reconciled to §10.3 in both places.
  • The three test-file edits in this commit are comment-only; no assertion was weakened.

* NOTE: use this on the master id for the pool — `/parent/resources` returns the workspace's
* OWN allocation, not the master pool (live-verified 2026-07-02).
*
* NOTE (SITES-49206): the just-in-time allocator that read this before a metered op was removed

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.

issue (blocking): this NOTE says the canary is getWorkspaceResources's "one remaining caller". It is not. src/controllers/elements.js:703 calls it from checkAccess, the handler for GET /v2/orgs/:spaceCatId/brands/:brandId/serenity/brand-presence/access (LLMO-6747). That endpoint is the whole "Semrush workspace access needed" banner: it probes the resource-allowance endpoint with the caller's own IMS token and reads a 401/403 as hasAccess: false. It is live on main, untouched by this PR, and covered by test/controllers/elements.test.js.

The claim on the sibling below is correct - I checked transferWorkspaceResources at head and the canary really is its only caller outside the transport itself and the new unit test. So the two methods have two different lifetimes, and this commit binds them to one.

Why this is more than a comment being wrong: the same claim is now written into two places that outlive the comment.

  • docs/decisions/009-remove-dormant-jit-allocator.md:47 says both methods "are therefore retired with the canary under the same delete-last sequencing".
  • The description's §10.6 checklist item says that step "also retires the canary itself and its two retained transport methods (getWorkspaceResources / transferWorkspaceResources)".

That checklist is the instruction a future engineer executes. Following it deletes a method a production endpoint depends on, and the failure is quiet rather than loud: checkAccess documents every non-401/403 error as INDETERMINATE, which the UI treats as "assume access". A TypeError there maps to a 5xx, so the banner silently stops firing for users who genuinely lack workspace access, with nothing surfaced to them.

This is also last round's defect class, inverted. Then, a method was deleted because its surviving caller was invisible to CI. The fix for that records a caller inventory by hand, and the inventory under-counts - in the direction that looks safe.

Fix: name elements.js checkAccess here as the production caller and drop this method from the canary-coupled sequencing; only transferWorkspaceResources below is canary-scoped. Then split the pair in ADR-009:47 and in the description's §10.6 item so both record two methods with two lifetimes. While correcting that inventory: ADR-009's "Three things at the metered-write boundary are kept" at :44 is also missing the fourth retained surface - the IT __quota controls (setUmMockQuota / dumpUmMock, §10.5), which today exist only in the test files and not in the record a later section will read.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed (commit d96c01c8). You're right — elements.js checkAccess (GET .../brand-presence/access, LLMO-6747) is a live production caller, so getWorkspaceResources is not canary-scoped. Decoupled it everywhere:

  • The transport NOTE now names elements.js checkAccess as the production caller and says the method outlives the canary; only transferWorkspaceResources carries the canary-scoped/delete-last NOTE.
  • ADR-009's "kept surfaces" list is split into different lifetimes (canary-scoped transferWorkspaceResources vs. production-scoped getWorkspaceResources), and I added the fourth retained surface you flagged — the IT __quota controls (setUmMockQuota / dumpUmMock, §10.5).
  • The description's §10.6 checklist now retires only transferWorkspaceResources, explicitly not getWorkspaceResources.

The under-counted inventory was exactly the last-round defect inverted — thanks for catching the safe-looking direction.

// @ts-expect-error - string has no properties in common with { projects?, prompts? }.
await transport.transferWorkspaceResources(parentWorkspaceId, { ai: 'unlimited' });

// Argument types.

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.

issue (blocking): the assertion that pinned transferWorkspaceResources was removed here along with the method, and did not come back with it. On main this block reads:

  // Body shape, derived from the generated contract: `ai` carries unit counts.
  // @ts-expect-error - string has no properties in common with { projects?, prompts? }.
  await transport.transferWorkspaceResources(parentWorkspaceId, { ai: 'unlimited' });

Everything else about the method came back in this commit - the implementation, the JSDoc with both @param tags, the WorkspaceResources typedef at rest-transport.js:77, the unit test, the ADR entry. This one artefact did not, and its absence is silent: a missing @ts-expect-error fails nothing.

src/support/serenity/CLAUDE.md names this file as the enforcement, in rule 6: "A new transport method documents its own parameters ... test/types/base/serenity-transport.types.js pins both halves." docs/decisions/005-opt-in-type-checking.md says the same. This file's own header explains why both halves are load-bearing - dropping a transport method's @param tags makes its assertions legal again, so the assertion is what keeps the tags honest.

With it gone, a later change that strips those tags or widens payload back to {object} compiles clean. That matters more for this method than for its neighbours, because its only consumer is the canary, and this PR's own argument is that scripts/ sits outside tsconfig and never runs in CI - so nothing else is watching.

Fix: re-add the three lines after the createSubworkspace arity block. The signature and the typedef are unchanged from main, so the expected error should be unchanged; if it is not, tsc tells you immediately as TS2578.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed (commit d96c01c8). Re-added the three lines after the createSubworkspace arity block, verbatim from main:

// Body shape, derived from the generated contract: `ai` carries unit counts.
// @ts-expect-error - string has no properties in common with { projects?, prompts? }.
await transport.transferWorkspaceResources(parentWorkspaceId, { ai: 'unlimited' });

The signature and WorkspaceResources typedef are unchanged from main, so the expected error is unchanged — type-check (base + strict) is green, i.e. no TS2578. I also took your suggestion and pinned both getWorkspaceResources and transferWorkspaceResources in test/types/strict/serenity-transport-strict.types.js, so a rename now fails as TS2339 under strict rather than only breaking the canary at runtime.

- **A tenant whose parent enforces limits would get no sizing, and there is no longer a flip
procedure.** ADR-008 pointed operators at a "when to turn the JIT allocator on" runbook; that
section and the allocator it armed are both removed. *For now* the `serenity-metered-405-canary.mjs`
probe is the re-check — a **manual** per-environment run whose failure is the trigger to

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.

issue (blocking): "whose failure is the trigger to re-introduce the allocator" - the canary has no failure to observe, and its on-screen labels read the other way round.

The script was written for LLMO-6190 to capture the disguised-405 fixture, and it still speaks that language at head:

  • scripts/serenity-metered-405-canary.mjs:175 - "Publishing with zero prompt headroom - expecting the disguised metered-quota 405..."
  • :177 - "UNEXPECTED: publish succeeded. The workspace may not actually be at zero headroom..."
  • :179 - prints the 405 under "this is what isMeteredQuota must match"

Under this ADR's premise those meanings invert. A 405 at zero headroom means Semrush is enforcing again: the premise is broken and the allocator has to come back from history. A publish that succeeds at zero headroom is the confirmation the ADR wants. The script calls the first "expected" and the second "UNEXPECTED". And main() returns normally in both branches - exit(1) fires only on a usage error or an unexpected top-level throw - so "a failed canary" names nothing the script actually emits.

The labels themselves predate this PR. What this PR creates is the reliance on them: before it, ADR-008 pointed operators at the "when to turn the JIT allocator on" procedure in docs/serenity.md, and this commit replaces that pointer with the canary in three records at once - here at :78, at docs/decisions/008-no-subworkspace-resource-carve.md:87, and at docs/serenity.md:459. The description's §10.7 item then asks a human to run it on dev, stage and prod and record each result before §10.6. That operator gets UNEXPECTED: publish succeeded in the case where everything is fine, and will reasonably read it as a botched run.

It matters because this ADR accepts a one-way door - "re-introduced from history rather than re-enabled by a dead flag" - and names exactly one safeguard for it. A safeguard whose healthy result is printed as an anomaly is a nominal one.

Fix, prose only and on this branch: drop "failure" as the trigger vocabulary here, at ADR-008:87 and at docs/serenity.md:459, and name the observable outcome instead - the disguised 405 is the signal to re-introduce the allocator, a publish that succeeds against zero headroom confirms the premise holds. Then add two lines to the script header at scripts/serenity-metered-405-canary.mjs:16 saying it now has a second reading under ADR-009, and that its "expected" / "UNEXPECTED" labels refer to fixture capture rather than to the premise.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed (commit d96c01c8), prose-only across all three records plus the script header. Dropped "failure" as the trigger vocabulary and named the observable outcome instead: the canary exits 0 either way, so it's read by what the publish does — a publish that succeeds at zero headroom confirms the premise, and the disguised 405 means Semrush is enforcing again (the trigger to re-introduce the allocator). Applied at ADR-009:88-ish (Consequences), ADR-008:86, and docs/serenity.md:457. I also added a "SECOND READING" block to the script header (serenity-metered-405-canary.mjs:16) spelling out that its expected/UNEXPECTED labels are LLMO-6190 fixture-capture language and read the opposite way under this ADR's premise, plus the delete-last coupling to transferWorkspaceResources.

@aliciadriani
aliciadriani deployed to dev-branches August 12, 2026 13:19 — with GitHub Actions Active
…pe guard, correct canary outcome semantics (SITES-49206)

Rainer's third re-review on #2995:
- [Important] getWorkspaceResources is NOT canary-only: elements.js checkAccess
  (GET .../brand-presence/access, LLMO-6747) calls it in production. Decoupled it
  from the canary in the transport NOTE, ADR-009 (split the two methods into
  different lifetimes + added the IT __quota controls as a retained surface), and
  the PR description's §10.6 checklist. Only transferWorkspaceResources is
  canary-scoped/delete-last.
- [Important] Restored the transferWorkspaceResources @ts-expect-error body-shape
  guard in test/types/base/serenity-transport.types.js (deleted with the method,
  not restored with it). Also pinned both methods in the strict-tier types file so
  a rename is caught as TS2339 rather than only breaking the canary at runtime.
- [Important] Corrected the canary outcome vocabulary in ADR-009/ADR-008/serenity.md:
  the canary exits 0 either way; a publish that SUCCEEDS at zero headroom confirms
  the premise, a disguised 405 means Semrush is enforcing again. Documented the
  script's inverted expected/UNEXPECTED (fixture-capture) labels and the
  delete-last coupling in the canary header.
- Nit: quota-alerts dedup-key test no longer varies caseType with the removed
  orgPoolExhausted (uses rollbackFailed, a caseType the module still keys on).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@aliciadriani aliciadriani left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Third re-review round: inline notes on each new fix (the three blocking items are also answered in-thread).

*
* NOTE (SITES-49206): the just-in-time allocator that read this before a metered op was removed
* once Semrush stopped enforcing AI limits for proxy-routed LLMO workspaces, but this read has
* a live production caller — `elements.js` `checkAccess` (GET .../brand-presence/access,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [B1]: getWorkspaceResources is NOT canary-only — elements.js checkAccess (LLMO-6747 brand-presence access banner) calls it in production. This NOTE now names that caller and states the method outlives the canary; only transferWorkspaceResources (below) is canary-scoped/delete-last.

* NOTE (SITES-49206): the just-in-time allocator that used to call this — `transferOnce` /
* `transferAndSettle` in the removed `resource-manager.js` — is gone. The sole remaining caller
* is the retained metered-405 canary (`scripts/serenity-metered-405-canary.mjs`, step 2), which
* drains a throwaway sub-workspace to zero prompt headroom to provoke the disguised 405. Unlike

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [B1]: the transferWorkspaceResources NOTE no longer calls getWorkspaceResources its co-retired "read pair" — the two methods have different lifetimes; only this one is retired with the canary.

// whole value went untyped.
await transport.listLanguages();

// Compile-time gate for the workspace-resource surface (SITES-49206). Both methods are consumed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review suggestion (taken): pin both transport methods in the strict tier so a rename/removal is reported as TS2339 rather than only breaking the canary at runtime (the canary sits outside tsconfig/CI).

re-meter sizing, both of which had the allocator as their only caller;
- the flag-ON integration path.

Several surfaces are **kept**, none belonging to the allocator any more, and they have *different*

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [B1]: split the retained surfaces by lifetime — transferWorkspaceResources is canary-scoped, getWorkspaceResources has a production caller (elements.js), plus the fourth surface you flagged: the IT __quota controls (§10.5).

procedure.** ADR-008 pointed operators at a "when to turn the JIT allocator on" runbook; that
section and the allocator it armed are both removed. *For now* the `serenity-metered-405-canary.mjs`
probe is the re-check — a **manual** per-environment run. Read its outcome by what the publish does
at zero headroom, not by an exit code (it exits 0 either way): a **publish that succeeds** against

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [B3]: the canary exits 0 either way; a publish that succeeds confirms the premise and a disguised 405 means Semrush is enforcing again. Also flags that the script's on-screen labels read the opposite way (fixture-capture language).

risk the retained allocator covered. The allocator, and the "when to turn the JIT allocator on"
flip procedure this line pointed at in `docs/serenity.md`, are both removed — see ADR-009. The
re-check, for now, is the `scripts/serenity-metered-405-canary.mjs` probe — a **manual**
per-environment run read by what the publish does at zero headroom (it exits 0 either way): a

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [B3]: aligned ADR-008's canary bullet to the corrected outcome semantics (405 = enforcing again = re-introduce allocator; success = premise holds).

Comment thread docs/serenity.md
The parent-pool premise is re-checked, not assumed: `scripts/serenity-metered-405-canary.mjs`
drives the real transport against a throwaway sub-workspace and publishes into zero headroom. Read
the outcome by what the publish does, not by an exit code (it exits 0 either way): a publish that
**succeeds** at zero headroom confirms the premise holds, whereas the disguised **405** the script

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [B3]: corrected the canary outcome description here too, and noted the inverted on-screen labels.

* design doc says 405s as a disguised quota rejection (as opposed to a genuine Method-Not-Allowed).
* It prints the raw response status, headers, and body so a human can re-confirm the shape.
*
* SECOND READING (SITES-49206 / ADR-009): after the JIT allocator's removal this script is also the

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review fix [B3] + coupling nit: added a SECOND READING block — the script's expected/UNEXPECTED labels are LLMO-6190 fixture-capture language and invert under the ADR-009 premise; plus a DELETE-LAST COUPLING note (this script ⇄ transferWorkspaceResources; getWorkspaceResources is NOT coupled).

orgId: 'org-1', brandId: 'brand-1', caseType: 'orgPoolExhausted', dimension: 'prompts',
// A distinct caseType proves it participates in the dedup key. `brandCarveExhausted` is
// the only case `alertQuotaRejection` emits now (SITES-49206 narrowed QuotaCase), so this
// arm uses `rollbackFailed` — another caseType the module still keys on

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review nit: the dedup-key test no longer varies caseType with the removed orgPoolExhausted; it uses rollbackFailed, a caseType the module still keys on (alertRollbackFailure).

@aliciadriani

Copy link
Copy Markdown
Collaborator Author

@rainer-friederich third re-review addressed — branch head d96c01c8. All three blocking items answered in-thread; summary:

[Important] getWorkspaceResources caller inventory — you're right, it's not canary-only: elements.js checkAccess (LLMO-6747 brand-presence access banner) calls it in production. Decoupled it from the canary in the transport NOTE, ADR-009 (retained surfaces now split by lifetime), and the §10.6 checklist. Only transferWorkspaceResources is canary-scoped/delete-last. Added the fourth retained surface you flagged — the IT __quota controls (§10.5) — to ADR-009.

[Important] @ts-expect-error guard — restored the three-line body-shape assertion after the createSubworkspace arity block, verbatim from main (type-check green, no TS2578). Took the suggestion too: both transport methods are now pinned in the strict-tier types file, so a rename fails as TS2339 instead of only breaking the canary at runtime.

[Important] canary outcome semantics — dropped "failure" as the trigger vocabulary across ADR-009, ADR-008 and docs/serenity.md; named the observable outcome instead (the script exits 0 either way — a publish that succeeds confirms the premise, a disguised 405 means Semrush is enforcing again). Added a SECOND READING block to the script header noting its expected/UNEXPECTED labels are LLMO-6190 fixture-capture language and read the opposite way, plus the delete-last coupling to transferWorkspaceResources (and that getWorkspaceResources is not coupled).

Nit — the dedup-key test no longer varies caseType with the removed orgPoolExhausted; it uses rollbackFailed, a caseType the module still keys on.

Verified: lint + type-check (base + strict) green, 1403 serenity-support + elements-controller tests passing, node --check on the canary clean. Re-requesting your review.

@aliciadriani
aliciadriani deployed to dev-branches August 12, 2026 14:20 — with GitHub Actions Active

@rainer-friederich rainer-friederich 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.

Hey @aliciadriani,

Verdict: Approve - all three blocking items are fixed and verify clean, and the two nits landed with them.
Changes: removes the dormant JIT Semrush AI allocator with its wiring, tests, allocator-era script and docs, and adds ADR-009 as the superseding decision record (43 files).

Non-blocking (1): minor issues and suggestions
  • suggestion: the SECOND READING block sends the operator to the success branch as the premise-confirming result, but that branch still reads UNEXPECTED: publish succeeded. The workspace may not actually be at zero headroom, ..., and the script never re-reads the workspace after the drain - so "did the drain land" is an open confounder on exactly the reading this PR adds. One getWorkspaceResources call after the transfer, printing the post-drain totals, closes it and makes the header's existing "Prints the before/after totals" claim true - scripts/serenity-metered-405-canary.mjs:61 and :138

Out of scope, worth tracking: docs/specs/2026-06-15-serenity-subworkspace-dual-mode-implementation-plan.md:149,171,214 still describes transferWorkspaceResources as part of decommissionBrandWorkspace and the de-resourced re-grant path. Those paths predate this PR and no longer exist, so a section-10.6 author greps up a phantom production caller - worth a line on that follow-up rather than a change here.

Previously flagged, now resolved

  • The getWorkspaceResources caller inventory is corrected everywhere it was wrong: both transport NOTEs, ADR-009's kept-surfaces list (now per-item lifetimes, including the section-10.5 IT controls), the PR description's three references, and a new DELETE-LAST COUPLING block in the canary header that states the exclusion from the other direction.
  • The base-tier @ts-expect-error pin is restored character-identical to what was removed, and a strict-tier pin now names both transport methods so a rename reports TS2339 instead of only breaking the canary at runtime.
  • Canary outcome semantics corrected in ADR-008, ADR-009, docs/serenity.md and the script header: each names the publish outcome rather than a failure, records that the script exits 0 either way, and flags that its own labels read inverted.
  • quota-alerts.test.js's dedup arm now varies on rollbackFailed, a caseType the module genuinely emits.

…se-confirm confounder (SITES-49206)

Rainer's approval + non-blocking suggestion on #2995: the metered-405 canary
never re-read the workspace after the drain, so "did the drain land?" was an open
confounder on exactly the premise-confirming reading this PR added (a publish that
succeeds only confirms Semrush isn't enforcing if headroom was GENUINELY zero).
Add one getWorkspaceResources call after the transfer, print the post-drain
totals with a zero-headroom check, and reword the success/405 branches to read
against it — which also makes the header's existing "prints the before/after
totals" claim true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@aliciadriani aliciadriani left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Non-blocking suggestion addressed (post-approval): the canary now re-reads and prints the post-drain totals. Out-of-scope stale-spec-doc item tracked on the follow-up checklist.

ai: { projects: before.product_resources.ai.resources.projects.total, prompts: prompts.used },
});

// Re-read AFTER the drain and print the post-drain totals. Without this, "did the drain land?"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Non-blocking suggestion (taken): the canary now re-reads the workspace with getWorkspaceResources right after the drain and prints the post-drain totals with a zero-headroom check. This closes the "did the drain land?" confounder on the premise-confirming reading — a publish that succeeds only confirms Semrush isn't enforcing if headroom was genuinely zero — and makes the header's existing "prints the before/after totals" claim true. The success/405 branch messages now tell the operator to read against these post-drain totals.

@aliciadriani

Copy link
Copy Markdown
Collaborator Author

Thanks @rainer-friederich for the approval. Both items from the approval round handled in b5ed7226:

Non-blocking suggestion (the drain confounder) — taken. The canary now issues one getWorkspaceResources after the transfer and prints the post-drain totals with an explicit zero-headroom check, so the premise-confirming reading is no longer confounded by "did the drain land?" (a successful publish only confirms Semrush isn't enforcing if headroom was genuinely zero). The success and 405 branch messages now tell the operator to read against those printed totals, and the header's existing "prints the before/after totals" claim is finally true.

Out-of-scope stale spec doc — tracked, not changed here (it's a planning artefact). Added a follow-up checklist item: docs/specs/2026-06-15-serenity-subworkspace-dual-mode-implementation-plan.md:149,171,214 still describes transferWorkspaceResources in decommissionBrandWorkspace / the de-resourced re-grant path, which no longer exist — so a §10.6 author greps up a phantom production caller. Flagged it to be corrected/annotated before §10.6 acts on the caller inventory.

Verified: lint + node --check on the canary clean. (No type-check impact — the change is inside scripts/.)

@aliciadriani
aliciadriani deployed to dev-branches August 12, 2026 14:38 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed Reviewed by AI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants