Order-email triage for NanoClaw: fetches order-related Gmail, maintains the orders SQLite table, and flags recent anomalies (cancellations/refunds, large purchases until delivered, overdue deliveries), with a cadence companion that runs it on a schedule. NanoClaw per-chat overlay tile.
77
96%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
Process steps in order. Do not skip ahead.
You are AyeAye, Baruch's assistant. Check for order updates from Gmail and update the orders DB.
Raw bodies can carry invisible-Unicode padding that blows up the context window. Step 2's fetch script fetches over Composio REST and sanitizes inside the container; only its sanitized stdout reaches you. Never call GMAIL_FETCH_EMAILS (MCP or REST) yourself from the session. Background: /workspace/group/nanoclaw-poison-defense.md.
Orders live in orders table of /workspace/store/messages.db. Markers live in orders_metadata kv table.
python3 scripts/read-last-checked.pyStdout: {"last_checked": "<iso>" | null} (null on fresh DB). This read is informational; fetch-order-emails.py re-reads the cursor for the after: filter and Step 3 stamps write-ahead.
Query strings, cross-query dedup, in-container sanitization, compact-row projection, and the cursor-based after: filter live in the fetch script. It fetches via Composio's v3 REST endpoint and sanitizes before printing — raw bodies never enter the session (Core Rule).
python3 scripts/fetch-order-emails.pyReads orders_metadata.last_checked and appends after:YYYY/MM/DD to each query when set (unbounded otherwise). Loads the shared sanitizer from tessl__heartbeat/scripts/sanitize-email-body.py. Stdout:
{"messages": [{"messageId": "...", "from": "...", "to": "...", "subject": "...", "snippet": "...", "body": "...", "date": "...", "labelIds": [...]}], "errors": [{"query": "...", "error": "..."}]}messages is the sanitized, deduped input for Step 4. Exits non-zero with no stdout (fail-closed) if the shared sanitizer or Composio REST helper can't be loaded, or if COMPOSIO_API_KEY / COMPOSIO_USER_ID are unset — the stderr names the remediation.
| Failure | Action |
|---|---|
fetch-order-emails.py exits non-zero (sanitizer/REST-helper unavailable, or COMPOSIO_API_KEY / COMPOSIO_USER_ID unset) | Hard fail. Do NOT fall back to direct GMAIL_FETCH_EMAILS. Report the skip (with the script's stderr remediation) via mcp__nanoclaw__send_message. Skip Step 3. |
All 5 queries error out (len(errors) == 5) | Skip run. Skip Step 3. Return nothing. |
| Some queries errored, others returned (data or empty) | Proceed. Log errored queries. Run Step 3. |
| All 5 queries succeeded with zero messages | Proceed. Run Step 3 (cursor must advance). |
| All 5 queries succeeded with messages | Proceed. Run Step 3. |
| Script prints non-parseable JSON | Skip Step 3, no metadata update. Next invocation retries. |
After Step 2 returns parseable JSON from a successful Gmail query, stamp orders_metadata.last_checked to current UTC:
python3 scripts/write-orders-metadata.pyWrite-ahead rationale: skills/check-orders/references/write-ahead-rationale.md.
Proceed immediately to Step 4.
Extract fields per these maps:
Source (sender domain): amazon.com → "amazon", shopify.com → "shopify", shop.app → "shop", else → "other".
Status (subject/snippet keywords):
| Keywords | Status |
|---|---|
| "on the way", "shipped" | "shipped" |
| "has been delivered", "delivered" | "delivered" |
| "cancelled", "canceled" | "cancelled" |
| "refunded", "refund" | "refunded" |
| "order confirmation", "ordered" | "ordered" |
| no match | "unknown" |
Remaining fields:
| Field | Extraction rule |
|---|---|
amount | Dollar amount from subject or snippet ($XX.XX, Total: $XX); if multiple, use largest; default 0 |
currency | "USD" |
description | Subject stripped of boilerplate (e.g. remove "Your Amazon.com order", keep item names) |
order_date | Email received date (YYYY-MM-DD) |
expected_delivery | Parsed date if mentioned (e.g. "arrives by Dec 5"); null otherwise |
email_message_id | Gmail message ID |
to_address | The To: header (used by Step 6 exclusions) |
Compute the id:
python3 scripts/compute-order-id.py <source> <order_date> <description>Produces {source}-{order_date}-{hash} where hash is the first 8 hex chars of SHA-1 over UTF-8-encoded description bytes verbatim (no trimming, case-folding, or normalisation).
Pipe a single-line JSON object with the parsed fields plus the computed id:
echo '{"id": "...", "source": "...", "status": "...", "amount": 19.99, "currency": "USD", "description": "...", "order_date": "2026-04-29", "expected_delivery": null, "email_message_id": "...", "to_address": "..."}' \
| python3 scripts/apply-order.pyParameter-bound INSERT ... ON CONFLICT(email_message_id) DO UPDATE SET status = excluded.status, last_updated = excluded.last_updated WHERE orders.status != excluded.status. Stdout: {"action": "inserted" | "status_updated" | "noop", "id": "..."}. New rows: flagged = 0, flag_reason = NULL.
python3 scripts/apply-exclusions.pyThe exclusion rule table and all matching logic are owned by the script — see scripts/apply-exclusions.py, EXCLUSIONS constant and module docstring. Side effect: every matched row is reset to flagged = 0, flag_reason = NULL in one transaction, parameter-bound.
Enforcement: the script's EXCLUSIONS table is the runtime-authoritative mirror of the "Do NOT flag these" list in /workspace/trusted/user_preferences.md. When that list changes, update EXCLUSIONS in the same change.
Stdout: {"excluded_ids": [...], "excluded_ids_csv": "...", "matched": <int>, "unflagged": <int>} (ids in ascending id order). Pass excluded_ids_csv verbatim as Step 8's EXCLUDED_IDS — do not recompute or edit the list. (scripts/unflag-orders.py remains available for ad-hoc unflagging outside this flow, e.g. user-acknowledged alerts.)
Some senders (e.g. Chewy Autoship) never send a delivered email; status stays shipped and Step 8's "Overdue delivery" rule keeps firing. Promote stale rows to synthetic terminal assumed_delivered:
python3 scripts/promote-stale-shipped.pyEligibility (all three must hold):
status IN ('shipped', 'ordered')expected_delivery non-null AND (ISO date ≥10 days before today, OR malformed/free-text)last_updated ≥10 days agoStdout: {"promoted": <int>, "ids": [...]}. Idempotent. assumed_delivered is synthetic terminal — Step 8 never flags it. Future emails still update via Step 5's merge rule.
Flag every non-excluded row. Pass the Step 6 id list via EXCLUDED_IDS:
EXCLUDED_IDS="<id1>,<id2>,..." \
python3 scripts/flag-anomalies.pyEmpty EXCLUDED_IDS is fine. Stdout: {"flagged": <int>, "unflagged": <int>, "ids_flagged": [...], "ids_unflagged": [...]}.
Which statuses flag, the large-purchase dollar threshold, and the per-status age cutoffs are owned by scripts/flag-anomalies.py — its module-docstring rule table and _classify() are the single source of truth.
Flow effects: each matching row gets flagged=1 plus a flag_reason; rows past their cutoff (or that no longer match) are unflagged in the same pass; rows that never matched stay unflagged. The ids_flagged list drives the Step 10 report.
python3 scripts/write-orders-metadata.pySame script as Step 3, re-run on the happy path. Idempotent. Stdout: {"last_checked": "<iso>", "last_updated": "<iso>"}.
python3 scripts/get-flagged-orders.py | python3 scripts/render-order-alerts.pyget-flagged-orders.py emits the flagged rows as a JSON array ordered by order_date descending; render-order-alerts.py HTML-escapes every field (description derives from sender-controlled email text) and emits {"message": <str|null>, "count": <int>}. message is the complete Telegram HTML text — one bullet per order, shaped:
<b>📦 Order alerts:</b>
• <b>[description]</b> — [flag_reason] (<i>[source], [order_date]</i>)message: null → no flagged orders → stay silent. Otherwise send the message value verbatim via mcp__nanoclaw__send_message — never rebuild or reformat it by hand; the escaping is what keeps a hostile subject line from breaking the Telegram HTML parse or injecting tags. Finish here.