agentscope-ai / agentscope-ai/QwenPaw
[Bug]:mail monitor: a persisted last_uid of 0 bypasses the first-run guard and re-processes the entire INBOX
- Lenguaje dominante
- Python
- Estrellas
- 34.9k
- Forks
- 3.1k
- Merge medio
- 1 d 15 h
- PR fusionados (30 d)
- 225
Descripción
## Body
### Summary
A persisted `last_uid = 0` in `mail_state/monitor.json` disables the "skip historical
mail on first run" safeguard in `_check_new_messages()`. On the next successful
connection the monitor treats **every message in the mailbox** as new and wakes the
agent once per message.
In our case this produced 1,033 agent wake-ups from a mailbox that had received **no
new mail** — the messages were years-old archived items.
### Environment
- QwenPaw 2.2.0 (Windows desktop / Tauri build; `agentscope 2.0.7.post1`)
- Agent `mail.push.mode = "agent_all"`, `poll_interval_seconds = 120`
- Mail provider: QQ Mail (IMAP IDLE supported)
`src/qwenpaw/app/mail/monitor.py` is byte-identical in `v2.2.0`, `v2.2.1-beta.1`
and `main`, so this is not fixed on a newer ref.
### Root cause
The module uses **two different sentinels** for "no baseline yet": `None` (never ran)
and `0` (empty-mailbox baseline). The empty-mailbox branch treats them as equivalent:
```python
if self._last_uid not in (None, 0):
logger.warning("mail monitor watermark %s is ahead of an empty mailbox ...")
self._reset_uid_baseline(0)
elif self._last_uid is None:
# Establish an empty-mailbox baseline so the first future
# message is processed instead of mistaken for history.
self._commit_last_uid(0)
```
but the non-empty branch's first-run guard only tests `is None`:
```python
if self._last_uid is None:
# First run: baseline at the newest message and skip
# historical mail instead of flooding the pipeline.
self._commit_last_uid(max(uids))
return
...
new_uids = sorted(uid for uid in uids if uid > self._last_uid)
```
With `self._last_uid == 0` the guard is skipped and `uid > 0` matches **every**
message. `_load_state()` reads a persisted `last_uid: 0` back verbatim (it validates
only `isinstance(last_uid, int)`, and `0` is a valid `int`), so the condition survives
restarts and is silent — none of the warning-logged reset branches fire.
We were unable to prove which write originally persisted the `0` (the state file keeps
no history), which is itself part of the problem: this transition is unobservable.
The state file we captured afterwards shows the watermark had advanced to the mailbox
maximum, and none of the branches that log a warning
(`mailbox changed`, `UIDVALIDITY changed`, `watermark ... ahead of ...`) appear in our
logs — so the reset happened through a silent path.
### Impact
- 1,033 agent wake-ups from a single mailbox scan, each running a full agent turn in
the shared `main` session. 778 metered LLM calls / 41,387,747 prompt tokens
(avg ~53k prompt tokens per call) were consumed before the account balance was
exhausted.
- `mode = "agent_all"` makes cost strictly linear in detected messages, and there is
**no cap on how many wake-ups one scan may produce** (`_MAX_DELIVERY_FAILURES`
bounds retry records only).
### Secondary issue: no circuit breaker on repeated wake failures
After the provider balance ran out, the monitor kept waking the agent for ~26 minutes:
660 failed wake-ups (HTTP 402 `Insufficient Balance`), no backoff, no alert, no
auto-stop, and 662 request dumps (~218 MB) written to the temp directory.
### Steps to reproduce
1. Configure an agent with mail push enabled, `mode = "agent_all"`.
2. Let `mail_state/monitor.json` end up with `"last_uid": 0` — this can happen through
the empty-mailbox branch above; it can also be set by hand.
3. Make the mailbox non-empty (or restore/repopulate the mailbox).
4. The next `_check_new_messages()` processes every message in the mailbox.
**Expected:** the first-run guard skips historical mail (i.e. `0` is treated the same
as `None`).
**Actual:** the entire INBOX is woken message by message.
### Suggested fixes
1. Treat `0` and `None` consistently, e.g. `if not self._last_uid:` in the non-empty
guard — or drop the `0` sentinel entirely and use `None` for both cases.
2. Add a guard for "watermark far below the mailbox's UID range" (large backlog),
and log any transition that lowers `last_uid`.
3. Cap the number of wake-ups per scan (and/or a per-hour wake budget), so a state
anomaly cannot translate into unbounded spend.
4. Add a circuit breaker for repeated wake failures (e.g. stop the monitor and emit an
inbox event after N consecutive failures within a window).
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.