Antispam: bootstrap every account, not just the ones we happen to see go live
Warming Bayes hung on a single event — `account.state_changed` reaching "live". That covers an account added from now on and nothing else. An account that went live before this plugin existed never gets one; neither does one whose Junk folder was empty at the time and filled up later, or one whose owner enabled spam-folder sync afterwards. A worker restarting at the wrong moment loses it too. The result was a feature that quietly required someone to run it by hand per account, which is not a feature. An hourly sweep now walks every live account and offers each a bootstrap. That works because the per-account job was already cheap to repeat: it returns at once for an account already done, and declines — without marking it done — when the corpus is still too small to learn from. So the sweep simply keeps asking until there is something worth learning, then does it once. `s.accounts.live()` is the new SDK surface it needs: cross-user by design, because a sweep runs for the instance rather than a request. Ids and state only, so reading anything still goes through the user-scoped accessors. One failing account cannot stop the sweep — the mailbox that raises is logged and the rest still get their turn. docs/plugins.md now says this in general terms next to `JobSpec`: a state-change hook is one-shot, so pair it with a sweep when the work must eventually happen for everything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
This commit is contained in:
@@ -100,6 +100,7 @@ s = sdk("myplugin")
|
||||
| `await s.http_get(url)` | HTTPS GET through the SSRF guard (blocks private IPs) |
|
||||
| `s.messages` | read a message: `envelope`, `header`, `raw`, `sample` |
|
||||
| `s.actions` | act on a message: `set_flags`, `trash`, `move_to_special` |
|
||||
| `s.accounts` | `live()` → `(user_id, account_id)` for every account past its initial import — cross-user, for scheduled jobs |
|
||||
|
||||
Plus, for routers: `CurrentUserId` (auth dependency → the user's uuid) and
|
||||
`require_csrf` (guard mutating routes).
|
||||
@@ -243,6 +244,12 @@ jobs = [
|
||||
enqueue-on-demand via `await sdk("myplugin").enqueue(sync_now, user_id=...)`.
|
||||
- `singleton=True`: at most one running instance per `(plugin, user_id)` (Redis lock).
|
||||
|
||||
A hook on a state change is a **one-shot** event: it cannot help anything that
|
||||
already happened before your plugin was installed, and a worker restart can miss
|
||||
it. If a job must eventually run for every account, pair the hook with a
|
||||
scheduled sweep over `s.accounts.live()` and make the per-account work cheap to
|
||||
repeat — the antispam bootstrap does exactly this.
|
||||
|
||||
Jobs share the worker pool + retry semantics and log under `plg.myplugin.*`.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user