Implement email-address-based conversation matching

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
This commit is contained in:
Claude
2026-02-20 22:15:49 +00:00
parent c37862b75e
commit 0c3b5a9033
7 changed files with 594 additions and 241 deletions
+2 -6
View File
@@ -105,16 +105,12 @@ def run_poll_loop(agent: EmailAgent, channel: EmailChannel, poll_interval: int)
messages = channel.fetch_unread_messages()
for msg in messages:
logger.info(
"Processing message from %s (thread: %s)",
msg["from"],
msg["thread_id"],
)
logger.info("Processing message from %s", msg["from"])
try:
reply = agent.process_message(
conversation_id=msg["thread_id"],
parent_email=msg["from"],
message_text=msg["body"],
inbound_message_id=msg["message_id"],
)
if reply:
channel.send_reply(