Oracle Inbox Watch

SPEC_ORACLE_INBOX_WATCH.md · 2026-04-20

SPECIFICATION: Oracle Inbox Watch

Status: AUTHORIZED

Authorized: α.13, April 16 2026

Version: v1.0


Version: v1.0

PURPOSE

oracle_inbox_watch.py is the autoresponder and alert daemon for oracle@42sisters.ai. It runs every 2 minutes via cron, checks for new unread messages via the Microsoft Graph API, broadcasts all new arrivals to CREW_CHANNEL, and sends a 24-hour acknowledgement autoresponse to eligible senders. It is the first point of contact for inbound oracle traffic and the crew's early-warning system for incoming inquiries.

Source file: /home/nous/oracle_inbox_watch.py

Trigger: cron, every 2 minutes

Dependencies: check_inbox.py (provides check_unread()), send_graph_email.py, check_inbox.crew_radio (provides crew_broadcast())

State file: ~/.oracle_inbox_seen.json


INPUTS

| Input | Source | Format |

|---|---|---|

| Unread messages | check_inbox.check_unread() | List of message dicts with at minimum: sender, subject, receivedDateTime |

| Seen state | ~/.oracle_inbox_seen.json | JSON object: {"seen": [...list of receivedDateTime strings...]} |

| AUTORESPOND_SKIP set | Hardcoded in script | Python set of lowercase email addresses |

AUTORESPOND_SKIP set (canonical):


oracle@42sisters.ai
noreply@stripe.com
no-reply@stripe.com
receipts@stripe.com

Autoresponse body (canonical, verbatim):


Your inquiry has been received by the 42Sisters.AI Oracle. Expect a response within 24 hours. Thank you for reaching out. — 42 Sisters AI · oracle@42sisters.ai

Autoresponse subject rule: Prepend "Re: " to original subject unless subject already starts with "Re:" (case-sensitive check).


OUTPUTS

| Output | Destination | Trigger |

|---|---|---|

| CREW_CHANNEL broadcast | crew_broadcast("ORACLE", ...) | Every new (unseen) message |

| Autoresponse email | send_graph_email | New message where sender NOT in AUTORESPOND_SKIP AND sender address does NOT start with "no-reply" |

| Updated seen state | ~/.oracle_inbox_seen.json | Every run (even if no new messages) |

Broadcast format:


Incoming: from {sender} — subject: {subject}

State file write format:


{"seen": ["2026-04-16T10:00:00Z", "2026-04-16T10:02:00Z", ...]}

Entries are receivedDateTime strings. List is sorted ascending and trimmed to 500 entries max on every write.


INVARIANTS

  1. Non-fatal execution — Any exception (network failure, malformed message, missing state file, Graph API error) must be caught at the top level. Script must exit with code 0 in all cases. Cron health must not be disrupted by transient failures.
  1. First-run flood prevention — If ~/.oracle_inbox_seen.json does not exist, the script seeds the seen set from all currently unread messages WITHOUT sending autoresponses or broadcasting. This prevents a flood on first deployment.
  1. State cap — The seen list must never exceed 500 entries. On every write, sort ascending and trim to the 500 most recent entries.
  1. Self-loop preventionoracle@42sisters.ai must be in AUTORESPOND_SKIP. The script must never autorespond to itself.
  1. no-reply prefix guard — Any sender address that starts with "no-reply" (case-insensitive) is skipped for autoresponse, regardless of AUTORESPOND_SKIP membership. This is a second independent guard.
  1. Broadcast all new messages — Every unseen message is broadcast to CREW_CHANNEL via crew_broadcast(), including those in AUTORESPOND_SKIP. The skip set governs autoresponse only, not alerting.
  1. Seen state is keyed on receivedDateTime — Deduplication uses receivedDateTime as the unique key. Message ID is not used.
  1. Read-only on inbox — The script reads inbox state but does not mark messages as read, move, delete, or otherwise mutate the inbox.
  1. Autoresponse subject prefix — Subject must be prefixed with "Re: " if not already present. No double-prefixing.
  1. Imports — Must import from check_inbox (for check_unread and crew_broadcast) and send_graph_email (for outbound mail). No inline credential handling — credentials loaded by those modules from .env or vault.

VERIFICATION CRITERIA

| # | Criterion | Pass Condition |

|---|---|---|

| V1 | First-run seed | On empty state file: existing unread messages are seeded into seen set; zero autoresponses sent; zero crew broadcasts emitted |

| V2 | New message broadcast | Introduce 1 new unread message not in seen state → exactly 1 crew_broadcast("ORACLE", ...) call with correct sender and subject |

| V3 | Autoresponse sent | New message from user@example.com (not in AUTORESPOND_SKIP, not no-reply prefix) → exactly 1 autoresponse email sent with canonical body and correct Re: subject |

| V4 | AUTORESPOND_SKIP respected | New message from noreply@stripe.com → broadcast emitted, autoresponse NOT sent |

| V5 | no-reply prefix guard | New message from no-reply@anything.com → broadcast emitted, autoresponse NOT sent |

| V6 | Self-loop blocked | New message from oracle@42sisters.ai → broadcast emitted, autoresponse NOT sent |

| V7 | State cap | Inject 600 receivedDateTime entries into state → after write, state file contains exactly 500 entries (most recent 500) |

| V8 | Non-fatal exception | Simulate check_unread() raising an exception → script exits with code 0; state file not corrupted |

| V9 | No double Re: | Message with subject "Re: Hello" → autoresponse subject is "Re: Hello", not "Re: Re: Hello" |

| V10 | Seen state updated | After processing 3 new messages, all 3 receivedDateTimes appear in ~/.oracle_inbox_seen.json |


FAILURE MODES

| Mode | Symptom | Consequence | Mitigation |

|---|---|---|---|

| FM-1 | Graph API auth token expired | check_unread() raises auth exception | INV-1: exit 0; alert not delivered; next run retries |

| FM-2 | State file corrupted (non-JSON) | json.load() raises exception | INV-1: exit 0; treat as if no state; risk of first-run flood suppressed by guard |

| FM-3 | State file missing on non-first-run (deleted externally) | Re-seeds from current unread without autoresponding | First-run seed guard prevents autoresponse flood; broadcasts lost for that run |

| FM-4 | send_graph_email fails mid-loop | Autoresponse not delivered for that message | Message already marked seen; autoresponse not retried on next run |

| FM-5 | New message has null/missing receivedDateTime | KeyError during deduplication | Should be caught by top-level exception handler; INV-1 applies |

| FM-6 | Same sender sends 100 emails/day | 100 autoresponses sent | GAP: no rate limiting (see GAPS) |

| FM-7 | seen list grows unbounded if sort/trim not applied | State file bloats | INV-3 caps at 500; failure only if trim logic is bypassed |

| FM-8 | Cron fires overlapping (2-min interval, slow API) | Two instances run simultaneously | No lock file; both processes may send duplicate autoresponses for same message; low-risk given 2-min interval |


GAPS

| # | Gap | Risk | Recommended Mitigation |

|---|---|---|---|

| G1 | No per-sender rate limiting | A single sender can receive unlimited autoresponses per day if they send repeatedly | Add per-sender cooldown (e.g., 24h) to state file |

| G2 | No subject-level deduplication | If seen state is cleared, the same message may trigger a duplicate autoresponse | Add message ID (Graph API id field) as secondary dedup key |

| G3 | No archiving of responded messages | No audit trail of which emails received autoresponses | Append responded message metadata to ~/.oracle_autoresponded.jsonl |

| G4 | Cron interval vs Graph API rate limits | 2-minute cron may approach Microsoft Graph throttling limits under high volume | Verify Graph API rate limits for the mail read endpoint; add exponential backoff |

| G5 | No crew alert on autoresponse failure | If send_graph_email fails, crew is not notified | Catch send failures separately and emit a crew_broadcast with failure notice |

| G6 | Seen state keyed on receivedDateTime only | Two messages received at identical timestamps (edge case) would be conflated | Use receivedDateTime + sender composite key or switch to Graph message ID |

| G7 | First-run behavior is implicit | No flag or log entry indicates when a first-run seed occurred | Write a seed event to SESSIONS.md or crew_broadcast on first-run detection |

| G8 | no-reply prefix check is not canonicalized | Case sensitivity of prefix check (no-reply vs NO-REPLY) not formally defined | Enforce sender.lower().startswith("no-reply") explicitly in spec and code |


Jeremy Zlabis

Chronogeometer · Visionary · Disruptor · Chief

42 Sisters AI · East York, Toronto

🍁 Φ 0.042