- email_channel.py: add detect_automated_message() that inspects RFC 3834
Auto-Submitted, mailer-daemon/postmaster sender patterns, X-Loop,
multipart/report Content-Type, Precedence, and subject heuristics.
fetch_unread_messages() now includes is_automated / automated_reason
in every message dict.
- main.py: if is_automated is set, call agent.handle_automated_message()
instead of process_message() — no reply is ever sent to a bounce source.
- agent/core.py: add MAX_USER_MESSAGES = 20 cap; process_message() returns
"" without replying once a conversation exceeds the limit and calls
notifier.notify_loop_escalation() on first breach. New public method
handle_automated_message() records the event and triggers the same
one-shot admin alert.
- models/conversation.py: add loop_escalated: bool field (persisted) so
the admin alert fires at most once per conversation.
- notifications/notifier.py: add notify_loop_escalation() which sends a
plain-text warning to the admin CC list (Markus Graf / spielgruppen@).
https://claude.ai/code/session_01KwvR5hDPjSuJg4kvw5b5e5
fetch_unread_messages now returns both `body` (stripped, for the LLM)
and `raw_body` (full with nested quotes, for the outgoing reply).
main.py passes raw_body as quoted_text so each reply carries the
complete conversation thread, not just the single last message.
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Add standard > -prefixed quote block to outbound replies so parents can
see what they wrote in the previous message, matching natural email client
behaviour. The quote header uses the German "Am <date> schrieb <addr>:"
convention (matching Outlook/Thunderbird).
- email_channel.py: add _build_quoted_block() helper; extend send_reply()
with optional quoted_text/quoted_from params
- main.py: pass msg["body"] and msg["from"] as quoted_text/quoted_from
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Closes the gap where parents sending a new email (instead of replying)
would lose their registration progress. All changes follow the
email-based-conversation-matching OpenSpec change.
Key changes
-----------
storage/json_store.py
- normalize_email() helper (lowercase + trim)
- Conversations now keyed by sender email address, not thread ID
- Versioned registration storage: data/registrations/<email>/v<N>_<ts>.json
- current.json always reflects the latest version
- save_registration() returns (email_key, version) tuple
- save_registration_version() for updates with change_summary
- get_registration_history() returns all versions in order
models/conversation.py
- Added last_inbound_message_id field for reply threading (not matching)
channels/email_channel.py
- fetch_unread_messages() no longer exposes thread_id
- Conversation matching removed from channel layer (now in agent)
- Removed _resolve_thread_id() — threading headers kept for SMTP only
agent/core.py
- process_message() takes parent_email + inbound_message_id (no thread ID)
- Looks up conversation by normalized email address
- Post-completion handler: detects intent (question / update / new_child)
- Registration updates: diffs old vs new, versions storage, notifies admin
agent/prompts.py
- build_system_prompt() dispatches to registration or post-completion prompt
- Post-completion prompt guides LLM to return intent field
- Reminder language updated: no expiration threats
notifications/notifier.py
- notify_admin() accepts version parameter
- notify_registration_update() sends "Registration Updated" emails with diff
- _build_update_body() includes field-level old→new change summary
main.py
- Poll loop passes parent_email + inbound_message_id to agent (no thread_id)
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw