Paginate C-25 owner-return reply collection (STATE-WP-0094-T03)

GET /messages/ accepts offset with stable created_at,id ordering; C-25 pages
every reply per counterpart, dedups by message id and warns explicitly when
collection is incomplete instead of capping at 200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 671493@bnt-lap001
Assistant-Session: 60d7b11f-78f9-4731-9367-918511b2762d
This commit is contained in:
tegwick 2026-10-04 12:54:57 +02:00
parent fceb5f27f0
commit 3b9a5c58f5
3 changed files with 154 additions and 7 deletions

View file

@ -2786,6 +2786,48 @@ def collect_inbox_hygiene(api_base: str, repo_slug: str) -> dict[str, Any]:
}
C25_PAGE_SIZE = 200
C25_MAX_PAGES = 50
def _collect_counterpart_messages(
api_base: str, repo_slug: str, counterpart: str
) -> tuple[list[dict], bool]:
"""Page through every reply from ``counterpart`` (STATE-WP-0094-T03).
Returns ``(messages, complete)``. Messages are deduplicated by id, so a
reply shifting between pages while new mail arrives is counted once and the
finding's message ids stay stable. ``complete`` is False when the hub
ignored ``offset`` (a page repeated only known ids) or the page bound was
hit; the caller reports that instead of silently capping.
"""
seen: dict[str, dict] = {}
anonymous: list[dict] = []
for page in range(C25_MAX_PAGES):
rows = _api_get(api_base, "/messages", {
"to_agent": repo_slug, "from_agent": counterpart,
"unread_only": False, "limit": C25_PAGE_SIZE, "offset": page * C25_PAGE_SIZE,
}) or []
if not isinstance(rows, list):
rows = []
new = 0
for row in rows:
if not isinstance(row, dict):
continue
message_id = str(row.get("id") or "")
if not message_id:
anonymous.append(row)
new += 1
elif message_id not in seen:
seen[message_id] = row
new += 1
if len(rows) < C25_PAGE_SIZE:
return list(seen.values()) + anonymous, True
if new == 0:
return list(seen.values()) + anonymous, False
return list(seen.values()) + anonymous, False
def _check_blocked_unblock_sweep(
api_base: str,
repo_slug: str,
@ -2846,11 +2888,18 @@ def _check_blocked_unblock_sweep(
if not counterpart:
continue
if counterpart not in messages_by_agent:
rows = _api_get(api_base, "/messages", {
"to_agent": repo_slug, "from_agent": counterpart,
"unread_only": False, "limit": 200,
}) or []
messages_by_agent[counterpart] = rows if isinstance(rows, list) else []
rows, complete = _collect_counterpart_messages(api_base, repo_slug, counterpart)
messages_by_agent[counterpart] = rows
if not complete:
report.add(
severity="WARN", check_id="C-25",
message=(f"Owner-return reply collection from {counterpart} is incomplete "
f"({len(rows)} message(s) read; hub ignored paging or the "
f"{C25_MAX_PAGES}-page bound was reached) — returns may be missing"),
file_path=workplan_display_path(repo_dir, wp_file),
file_value=f"message-from:{counterpart}",
fixable=False,
)
# Dated notes are the available acknowledgement watermark. Date-only
# notes cover the whole UTC day, avoiding same-day re-notification.
note_dates = []