dev split message identity from placement, made filter evaluation per
placement, and replaced the schema-migration helpers with create-only
schemas. This branch's single-owner filter execution had to be re-seated
on top of all three.
The account gate stays where it was in intent -- a daemon that does not
own an account runs none of that account's rule actions -- but now sits
outside the per-placement fire-once claim rather than beside a per-message
one. On the sync path the gate is checked with the new-arrival test before
`apply_filter_actions` is reached, so a non-owner never takes a claim it
has already decided not to act on; leaving the claim unconsumed keeps it
available to the daemon that does own the account. Triage does the same
one level up: a message whose account this daemon does not own is counted
as scanned but its placements are never even read, and a daemon owning no
account still refuses the RPC outright instead of streaming an empty scan.
`filters_enabled` is now only a column in the created `accounts` schema;
the additive `ALTER TABLE` helper and its two migration tests are dropped,
since dev creates schemas and never upgrades them. The flag's persistence
is still covered -- the identity-rebuild test now asserts filter ownership
survives a rebuild, and the store round-trip assertions are unchanged.
The non-owner sync test keeps every assertion it had, translated to the
placement API: the mail is still stored, the occupancy's read flag is
still unflipped, and no execution log or outbox mutation is written.
Every daemon syncing an account decides for itself that a message is new,
because "new" means "not in this daemon's store". So every daemon fires the
same rules against the same message. MOVE, COPY and FLAG survive that -- ten
moves converge on one outcome -- but FORWARD and CALL WEBHOOK land on a third
party with no shared state to compare against. Ten daemons send ten forwards
and page an on-call engineer ten times per alert. Nothing in the outbox
constrains this either: pending_remote_mutations has no uniqueness of any kind.
The engine cannot fix this by itself. The servers coordinate state, not
actions, so there is no resource to compare-and-set against for an effect that
leaves the system. Until a server-side claim is available, ownership has to be
declared: filters_enabled on the account, and exactly one daemon per account
turns it on.
Default off, including for accounts that already exist. An account already
configured on several machines would otherwise have all of them start
forwarding the moment they upgrade. Off costs one setting to correct; a sent
forward cannot be recalled. The salvage path deliberately does not recover the
flag either, for the same reason.
A non-owning daemon still syncs and stores everything -- it is a full mail
client, it simply does not act. Triage is bound by the same rule since it
produces the identical side effects, and refuses outright when this daemon owns
no account rather than streaming a scan that reports progress and applies
nothing, which would read as "no rules matched".
Webhooks now carry an Idempotency-Key stable across retries of a rule/message
pair, so a redelivery caused by a lost response can be discarded rather than
acted on twice. It is not yet stable across daemons -- rule ids are random per
daemon -- which single-owner execution is what covers today.
Refs #389