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

Abierto
#7,668 1 comentario 0 reacciones 1 asignado Reclamado por @Luohh5 Ver en GitHub
bug
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

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.