Repository navigation
Conversation
This was referenced Oct 27, 2026
Django records the pause state of the shared sandbox SES tenant in SandboxSenderTenantState, from the tenant-state webhook and the daily reconciliation, and announces each committed change to the workers. Nothing reads the table yet; the worker gate follows in its own PR. Refs Silthus/personal#199 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
force-pushed
the
workflows-sandbox-delivery/daily-caps
branch
from
October 28, 2026 10:00
d68a209 to
e189abd
Compare
Silthus
force-pushed
the
workflows-sandbox-delivery/tenant-pause-mirror
branch
from
October 28, 2026 10:00
e8da316 to
c8ee93b
Compare
Silthus
changed the base branch from
workflows-sandbox-delivery/daily-caps
to
workflows-sandbox-product/sender-confirmations
October 28, 2026 10:01
This was referenced Oct 28, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
All sandbox sends go out through one shared PostHog SES tenant.
When AWS pauses that tenant, the email worker needs to know, so it can stop sending through it.
This PR makes Django record the shared tenant's pause state, so the worker gate in the next PR can read it.
Part of Silthus/personal#199 (sandbox delivery). Stacked on #616.
Replaces the Django half of #605, which mixed a migration with
nodejs/changes. Originally #452 and #453.Stack
workflows-sandbox-product/sender-confirmations(feat(workflows): confirm before the sandbox sender replaces senders #616). Lands after slice V2, whose top is feat(workflows): confirm before the sandbox sender replaces senders #616.c8ee93b2755: ruff clean, Workflows and CDP sandbox suites 484 passed. Review: GPT-6.1 Sol through a delegated review of the restack (each PR's diff, its conflict resolutions, the slice seams and the save gate), no findings.c8ee93b2755, 2026-10-28: live run below. Proof gate: fails only on the fork-onlycigap (exit 3); every other check passes.Impact
Grow Workflows, activation rate: the pause gate is a condition for enabling the sandbox sender in production.
No production team sees this yet:
WORKFLOWS_SANDBOX_SENDER_ENABLEDstays off, andSES_SANDBOX_TENANT_NAMEis empty.Merge Danger
Warning
🟡 Merge risk: medium · one-way door (merge-risk rubric)
workflows_sandboxsendertenantstate(migration0039). It is a plainCREATE TABLEwith no foreign keys, so the migration takes no lock on an existing table.SES_SANDBOX_TENANT_NAME. With the setting empty, they do nothing new.get_tenant_sending_state. Team tenant tests pin the same results.reload-sandbox-sender-statemessage reaches no subscriber before then, so Redis drops it.Production lines: 191 changed (
git diff --numstatagainst #616; tests, docs and the migration excluded).Changes
SandboxSenderTenantState:reload-sandbox-sender-statefor the workers, throughtransaction.on_commit(..., robust=True).SES_SANDBOX_TENANT_NAMEis a new Django setting.How did you test this code?
Live run on the slice tip
Live run of the V3 tip
7e95863f7a0on a PostHog preview. Stubs: a local SES endpoint that records each call, andexample.comseeded as a valid MX domain; a local SNS signing certificate, so a signed delivery webhook verifies; the trigger event produced straight onto the CDP events topic; personhog pointed at the persons database.SES tenant
DISABLED, then the Django sync: the mirror row readsDISABLED, and the next sandbox send is skipped with "the sandbox sender is paused right now".ENABLEDand a sync: the next send goes out. The worker picked up each change without a restart.A live workflow send on the sandbox sender recorded
email_sentandemail_sandbox_sentonce each, the pair the sending tier subtracts.A signed SES
Deliverynotification for that message on the sandbox configuration set returned 200 and capturedworkflows sandbox email delivered. The same envelope with a forged signature returned 403.Lane gate on HEAD: the pre-push
hogli ci:preflight --strictpasses (ruff, format, type-check, migrations, semgrep-devex). The lane classifier puts this PR on the Python side only, and it has nonodejs/file.Red: on feat(workflows): cap sandbox sends per project and recipient each day #604's code,
test_ses_tenant_state.pycannot load (ImportError: cannot import name 'sync_sandbox_tenant_state_task').Green on HEAD:
test_ses_tenant_state.pyandtest_ses_tenant_events.py, 48 passed.Test seams:
sync_sandbox_tenant_state, the tenant-state webhook, andreconcile_ses_tenant_states, with the SES client mocked.Proofs owed (
fleet proof gate):69afe3e4b1f), recorded on every delivery head. One real email worker process (cdp-cyclotron-worker-email) on scratch Postgres, with real Redis, Kafka and ClickHouse: a member received the email, a non-member was skipped, a paused tenant was skipped, the daily team cap held, and each delivered send recordedemail_sandbox_sent, which the tier's own ClickHouse read then left out. Stubbed: the SES endpoint (a local HTTP stub behind the real SESv2 client), the MX verdict for the reservedexample.comdomain, and the tenant-state rows the Django sync would write.master, so it reads as mixed until the Node layers below land. Land the stack bottom-first, not with onetrunk mergeon the top layer.Silthus/posthog.Test rationale: the Django tests cover the webhook and sweep paths that write the mirror, including a failed sandbox sync that must not stop the team sweep.
Release status
Docs update
docs/internal/workflows-email-sending-tiers.mddocuments the Django mirror andSES_SANDBOX_TENANT_NAME.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (implementation). Review by Codex, GPT-6.1 Sol.
Split out of #605 and #606 so each PR passes upstream's merge rules: a migration does not share a PR with
nodejs/, and Python does not share a PR with Node. The code is carved by file from those reviewed heads without changes; the stack's tip has the same tree as #606.Skills:
tdd,/writing-tests,pr-description,merge-risk.Public artifact: all test data is invented.
Decisions
master, so each layer passes once the layers below it landSES_SANDBOX_TENANT_NAMElives0039, after master's0038🤖 Generated with Claude Code