Final remediation audit · 12 July 2026 · private WREA work

Email Campaign Studio: pre-production release decision

What was fixed, what the fixes mean in normal language, and the remaining gates between reviewed code and live customer email.

Decision: GO for PR update and merge after CI; NO-GO for deploy or sending

The seven requested blockers are fixed and independently re-audited.

The local branch is now suitable to push to PR #4464 and merge into pre_production once CI is green. It is eight commits ahead of the remote PR. This is not approval to deploy, run production migrations, enrol customers or send email.

16guarded feature migrations
627automated tests passed
0remaining merge blockers
0production changes or sends

What this means

The database concern remains resolved

The original 15 migrations passed the production-shaped MySQL rehearsal, including the 1.27 million-row entity_tags index. The new 16th migration adds four progress fields to the feature's small launch table, uses the same fail-fast metadata-lock guard, applied successfully in MySQL testing, and never alters the large msg_emails table.

The missing real paths are now tested

The suite grew from 605 to 627 tests. It now exercises the real scheduled digest path, the real preference-form POST shape, Rescue consent, continuous discovery, cursor-based launch batches, transient retries, stale-worker ownership, explicit timing, and edit-versus-launch locking.

CEO translation: “GO for merge” means the code is ready for the normal PR and CI process. It does not mean the feature is live. Deployment, production backfill, owner-only test delivery and the first real campaign remain deliberately separate gates.

The eight findings: what they mean now

1Fixed: existing scheduled agent digests stay operational

The system now distinguishes a normal scheduled digest from an explicitly marked mass-email queue item. The fact that agent digests use marketing IP 149.72.200.78 no longer changes their business purpose. The real scheduler-path test proves a normal digest is not blocked by marketing-only suppression, while a true mass email still is.

2Fixed: an ordinary preference save does not unsubscribe a vendor

This affected one contact at a time, not every vendor in the CRM. On the property-owner Communication Preferences page, the browser always sends a hidden field identifying the unsubscribe form. “Ordinary save” meant that person pressed Save without selecting the visible unsubscribe from all marketing checkbox. The old controller mistook the hidden form marker for the person's choice. It now acts only on the visible all_tags selection. Tests prove hidden field alone means no suppression, no tag removal and no digest change; selecting the visible option performs the intended full unsubscribe.

3Fixed: a genuine seller help request is valid Rescue evidence

“Property-derived evidence” is the seller's CRM property/enquiry record: which enquiry it was, when it was created and that the person asked WREA for agent help. The campaign required the internal scope label requested_agent_help, but the evidence object omitted that label, so a legitimate request failed a technical comparison. The evidence now carries the same narrow scope. This does not invent broader consent; it correctly labels the existing request. A real Studio preview test proves the seller can now qualify.

4Fixed: Rescue is genuinely always-on

A separate discovery process runs every ten minutes for active Rescue campaigns only. It prioritises brand-new stalled enquiries, then uses spare capacity to reconsider temporary failures such as a previously missing email. It never automatically restarts stopped, completed, holdout, unsubscribed or permanently excluded journeys, and it pauses discovery while the initial launch is still preparing.

5Accepted for now: the final audience can move during preparation

You explicitly asked not to fix this item. Final Review shows a current count, then background preparation recalculates eligibility. A person who newly qualifies during that period may be included; someone newly suppressed or no longer relevant is excluded. The CRM now says this honestly instead of promising a frozen audience. This is an accepted product choice, not a hidden defect or merge blocker.

6Fixed: large preparation is bounded, resumable and retry-safe

Audience preparation now works in batches of 1,000 with a stored cursor, batch ownership token and cumulative counts. The job has a 600-second limit and three attempts, below production's 1,200-second queue retry window. A transient failure releases only its own claim and retries; only final retry exhaustion pauses the campaign. Replayed work reuses the existing enrolment and counts its persisted status, so it cannot duplicate people or inflate totals.

7Fixed: Send now and Schedule are separate, explicit choices

Yes, there is now a real Send now option. Send now means: after the two final confirmations, prepare the audience and make eligible messages due at the next safe opportunity. It still respects the 7:00 am to 9:00 pm local window, hourly speed limit and every suppression check; at 10:00 pm it waits until 7:00 am. Schedule requires a valid future local date and time, rechecked at Preview, Final Review and Launch. Missing, invalid or expired scheduling stops visibly and can never silently become immediate sending.

8Fixed: editing and launching cannot cross over

The CRM now locks the current campaign row before checking whether it is still editable and whether journeys already exist. A stale browser page cannot save an edit after another request has started launch or created enrolments. Two concurrency tests prove both cases are rejected.

Risk to existing CRM email

Existing pathCurrent audited resultRequired position before deploy
Scheduled agent digestsPROTECTED Queue-backed operational digests remain operational even on the marketing IP.Real scheduler-path regression proves both normal digest and explicit mass-email directions.
Vendor preference savePROTECTED Hidden form marker alone cannot create suppression.Only the person's visible unsubscribe-all choice triggers contact-wide marketing suppression.
Campaign Studio large audienceBOUNDED Preparation uses 1,000-person batches, stored progress and safe retries.Keep the production queue timeout and retry settings in the verified order.
Transactional and unclassified workflow emailUNCHANGED These remain outside marketing suppression and frequency caps.Inspect delayed jobs before the eventual worker restart as an operational precaution.
Separate legacy mass-email toolingOUTSIDE THIS FEATURE Campaign Studio does not rewrite that older screen.It is not a blocker for this merge; review separately before using it for a new large send.
Important distinction: marketing suppression still does its intended job for genuinely marketing-classed messages. The fixes protect operational digests and ordinary preference saves without weakening unsubscribe, spam-complaint, bounce or blacklist protection for Campaign Studio sends.

What was verified

AreaResultEvidence and meaning
MySQL compatibilityPASSThe original 15 migrations applied to a disposable production-schema database on MySQL 8.0.45. Migration 16 affects only the small feature launch table, applied in MySQL testing and carries the same fail-fast metadata-lock guard. No production write occurred.
Large existing-table indexPASSRehearsed after copying all 1,265,977 current production entity_tags rows; completed successfully using the intended online shape.
Automated suitePASS627 tests / 1,971 assertions passed after the final reliability correction. This includes every newly identified real path and the full existing application suite.
PHP and diff hygienePASSChanged PHP files linted cleanly and git diff --check passed.
Deployment orderingPASSLatest Forge output shows maintenance mode, code update, forced migrations, cache rebuild, queue restart and application recovery in the right order.
Launch queue designPASSPreparation is no longer one unbounded 60-second job. Stored cursors, ownership tokens, 600-second job timeout, three attempts and final-failure handling were tested together.
SendGrid signed eventsACTIVEProduction configuration has verification enabled and recent signed webhook events are being recorded.
Independent reviewPASSClaude reviewed the full branch, then separately reviewed the final retry correction. Its final finding ledger is empty.
Release readinessGO FOR MERGE AFTER CINo code-level merge blocker remains. Deploy and sending remain separately gated.

Codex and Claude Code comparison

Claude Code rechecked

Every requested fix against the actual branch, including existing digest isolation, unsubscribe behavior, consent scope, MySQL migration guards, batching, retry ownership, timing and scheduler behavior. It found no critical or high issue and gave a final GO.

Codex rechecked

The full application suite and all seven remediation commits. Codex challenged Claude's first description of transient retries, found that retries would still stop too early, corrected the lifecycle, and added four more tests. Claude then reviewed that delta and confirmed the issue closed.

Consolidated result: both reviewers say GO for updating and merging the PR after CI. Neither reviewer is authorising deployment or real sending.

The earlier Claude addendum remains useful history, but the final full-branch review and post-retry delta review are the canonical positions.

What happens next

  1. Push the eight local commits: this updates PR #4464; it does not deploy anything.
  2. Require green CI and Felix review: Felix can use the technical appendix below to understand the feature and the remediation.
  3. Merge to pre_production only: merge proof is not deployment proof.
  4. Plan an off-peak staged deployment: use the verified maintenance, migration, cache and worker-restart order; keep migration lock timeouts fail-fast.
  5. Complete operational gates: production workflow inventory, suppression backfill dry-run and reconciliation, webhook signature check, worker health and queue capacity check.
  6. Owner-only email testing: use only trpersonal@gmail.com and thomasroberts@whichrealestateagent.com.au, verify headers and one-click unsubscribe end to end.
  7. Return for a separate first-campaign decision: deployment does not activate Rescue, prepare a Nurture audience or authorise a customer send.
Still intentionally open: item 5's moving preparation-time audience, and separate lower-priority cleanup already documented in the PR. Neither is hidden; item 5 is your explicit decision and the interface describes it.

Felix technical appendix

FindingImplementationProof
1. Scheduled digest purposeContactService now uses an explicit mass-email marker rather than the existence of a queue row.PASS Real scheduler test proves operational digest and true mass-email behavior in both directions. Commit 8dbad08cf.
2. Preference saveMarketingTagLinkController acts on the visible all_tags selection, not hidden form identity.PASS HTTP tests cover ordinary Save and explicit full unsubscribe. Commit e6ae41a1e.
3. Rescue consent scopeConsentEvidence::fromProperty() records requested_agent_help; Campaign Studio uses the same constant.PASS Integrated Studio Rescue preview qualifies a real property enquiry. Commit 8fa931b85.
4. Continuous RescueRescueDiscoveryService plus email-nurture:discover-rescue, every ten minutes with overlap protection.PASS New-first priority, idempotency, temporary recovery, permanent exclusions, launch race, utility exclusion and scheduler tests. Commit 094cbe9fa.
5. Audience driftNo code change by owner decision. UI now states that the audience is calculated during preparation.ACCEPTED Not treated as frozen; live suppressions and relevance still apply.
6. Large launch1,000-row batches, persisted cursor/counts, UUID claim token, 600-second timeout, three attempts and final failure callback.PASS Exact-multiple, replay counts, stale ownership, transient retry, final failure and completed-race tests. Commits c08db4ffb and f1713fc12.
7. TimingExplicit send_mode=now|schedule; shared validator; invalid schedule throws instead of falling back.PASS Immediate daytime, next-morning window, expired launch, legacy draft and strict save tests. Commit 9e515b09f.
8. Edit/launch raceStatus and enrolment checks happen after lockForUpdate().PASS Stale status and stale enrolment edit tests. Commit c46cde7b8.

Release gates after code remediation

Recommended CEO decision

Approve pushing the eight remediation commits to PR #4464. If CI remains green and Felix finds no new blocker, approve merge to pre_production. Do not combine that decision with deployment or customer sending; those retain the operational gates above.

Open PR #4464