9.6 KiB
Telepathy Port Status: Python -> Elixir/OTP
Date: 2026-05-18 Status: NOT STARTED -- Blueprint complete, ready for implementation Blocks: Elimination of the last major Python dependency in the critical path
Executive Summary
Telepathy is Muse's email communication layer -- 635 lines of Python across 3 files, running as 2 separate processes (FastAPI on port 8114, aiosmtpd on port 25). Zero Elixir code exists for this service. A detailed migration blueprint was created 2026-05-08 but no implementation work has begun. The Elixir host (symbiont_ex) has no email-related modules, dependencies, or config.
Current Python Architecture
Running Services
| Service | Process | Port | Code |
|---|---|---|---|
| Telepathy API | uvicorn (FastAPI) | 8114 | /data/telepathy/app.py (142 lines) |
| Telepathy Mailer | (imported by app.py) | -- | /data/telepathy/mailer.py (203 lines) |
| Muse Inbox (SMTP) | aiosmtpd | 25 | /data/muse/smtp_handler.py (290 lines) |
Data Flow
Inbound: SMTP:25 -> parse MIME -> inbox.jsonl -> POST /messages (Telepathy)
-> POST /task (Symbiont/Elixir)
-> If reply: POST /email (Telepathy)
-> JMAP send via Fastmail
Outbound: Any caller -> POST /email (Telepathy:8114) -> mailer.py -> Fastmail JMAP API
Python Module Responsibilities
app.py -- Message store API + email dispatch interface
POST /messages-- Store message (from email-in, reflection, system)GET /messages-- List messages (with limit/unread filter)GET /messages/unread-- Unread messagesPOST /messages/{id}/read-- Mark readPOST /email-- Send email via Fastmail JMAPGET /health-- Status with total/unread counts- Data:
messages.jsonl(id, timestamp, source, subject, content, read)
mailer.py -- Fastmail JMAP client
- Session discovery (
GET /jmap/session) - Identity/get, Mailbox/get (find drafts mailbox)
- Email/set (create draft) + EmailSubmission/set (send)
- Draft cleanup after send
- Token from env
FASTMAIL_API_TOKENorconfig.json
smtp_handler.py -- Inbound SMTP receiver + orchestration
- aiosmtpd server on port 25 (
cortex.hydrascale.net) - Accepts:
muse@hydrascale.net,muse@cortex.hydrascale.net - MIME parsing via Python
emailstdlib - Async pipeline: store -> dispatch to Claude -> send reply -> mark read
- Data:
inbox.jsonl(timestamp, from, to, subject, message_id, body, processed)
Elixir Port Status
Completed Components: NONE
No Telepathy-related code exists in /data/symbiont_ex:
- No modules with telepathy/email/smtp/inbox in name
- No email deps in mix.exs (no req, gen_smtp, swoosh, etc.)
- No email config in runtime.exs
- No email endpoints in api.ex or router.ex
- No supervision tree entries for email services
Existing Elixir Infrastructure (Reusable)
| Component | Location | Reuse for Telepathy |
|---|---|---|
| Plug + Bandit (HTTP) | api.ex, port 8111 |
Add /messages and /email endpoints directly |
| JSONL GenServer pattern | queue.ex |
Clone for MessageStore |
| Task.Supervisor | application.ex |
Spawn email pipeline tasks |
| Jason (JSON) | Already in deps | JMAP request/response encoding |
| Router | router.ex |
Route inbound emails through existing task classification |
Planned Elixir Architecture (from 2026-05-08 Blueprint)
New Supervision Subtree
Symbiont.Telepathy.Supervisor (one_for_one)
+-- Symbiont.Telepathy.JMAP (GenServer -- Fastmail session + API)
+-- Symbiont.Telepathy.SMTP (gen_smtp server on port 25)
+-- Symbiont.Telepathy.MessageStore (GenServer -- messages.jsonl)
+-- Symbiont.Telepathy.Pipeline (stateless orchestration module)
New Dependencies Required
{:req, "~> 0.5"}, # HTTP client for JMAP API
{:gen_smtp, "~> 1.2"} # SMTP server + :mimemail for MIME parsing
Module Estimates
| Module | Lines | Complexity | Notes |
|---|---|---|---|
| Telepathy.JMAP | 150-200 | HIGH | Session mgmt, Email/set, EmailSubmission/set, mark_read |
| Telepathy.SMTP | 80-120 | MEDIUM | gen_smtp_server_session behaviour callbacks |
| Telepathy.MessageStore | 80-100 | LOW | Clone Queue pattern, JSONL-backed |
| Telepathy.Pipeline | 40-60 | LOW | Orchestrate: store -> dispatch -> reply -> mark_read |
| API endpoints | 40-60 | LOW | Add to existing api.ex |
| Tests | 150-200 | MEDIUM | Unit + integration |
| Total | 540-740 |
Remaining Work (Ordered)
Phase 1: JMAP Client (P0)
- Add
{:req, "~> 0.5"}to mix.exs - Implement
Symbiont.Telepathy.JMAPGenServer- Session discovery (GET /jmap/session, cache result)
send_email/4(Email/set + EmailSubmission/set)mark_read/1(Email/set with$seenkeyword) -- fixes "19 unread" bug- Auto-refresh on 401
- Config:
FASTMAIL_TOKENin runtime.exs - Unit tests with mock HTTP responses
Phase 2: Message Store (P0)
- Implement
Symbiont.Telepathy.MessageStoreGenServer- JSONL read/append (same pattern as Queue)
- Compatible with existing
/data/telepathy/messages.jsonlschema - Status transitions: unread -> processing -> replied -> read
- API endpoints in existing
api.ex:POST /messages,GET /messages,GET /messages/unreadPOST /messages/:id/read,GET /health(include message counts)
Phase 3: SMTP + Pipeline (P1)
- Add
{:gen_smtp, "~> 1.2"}to mix.exs - Implement
Symbiont.Telepathy.SMTP(gen_smtp_server_session behaviour)- handle_MAIL, handle_RCPT (validate recipients), handle_DATA
- MIME parsing via
:mimemail.decode/1 - Extract From, Subject, body (prefer text/plain)
- Implement
Symbiont.Telepathy.Pipeline- Spawn under Task.Supervisor
- Steps: store -> dispatch to Claude -> send reply -> mark read (local + Fastmail)
- Port
inbox.jsonllogging
Phase 4: Integration + Cutover (P2)
- Add
Symbiont.Telepathy.Supervisorto Application supervision tree - Feature flag:
TELEPATHY_ENABLEDenv var - Full pipeline integration test (SMTP in -> Claude dispatch -> JMAP out)
- Migrate data: point at existing messages.jsonl / inbox.jsonl
- Stop Python processes (systemd units for 8114 + SMTP)
- Remove Python services from startup
Blockers
| Blocker | Severity | Details | Mitigation |
|---|---|---|---|
| No Elixir JMAP library | MEDIUM | Must implement JMAP protocol from scratch using Req | Python mailer.py is only 203 lines; JMAP calls are well-documented in blueprint |
| Port 25 binding | LOW | Requires root/setcap for SMTP | gen_smtp handles this; same constraint as Python |
| Fastmail token access | LOW | Token in /data/telepathy/config.json |
Read at startup via runtime.exs or env var |
| Sandbox write permissions | HIGH | Previous attempts to write Elixir code were blocked by sandbox | Requires manual implementation or permission grant |
:mimemail parsing gaps |
LOW | Less polished than Python email stdlib for multipart |
Only need text/plain extraction; add helpers if needed |
Design Gaps Identified
-
HTML email handling: Python only extracts text/plain, falls back to "[HTML content...]" placeholder. Elixir port should match this behavior initially but consider stripping HTML to text as enhancement.
-
Attachment support: Neither Python nor planned Elixir handles attachments. Not blocking but worth noting.
-
Error propagation: Python smtp_handler fires
_process_email_async()as fire-and-forget. Pipeline failures are silent. Elixir port should log failures and consider retry semantics via Task.Supervisor. -
Message store scalability: Both Python and planned Elixir use JSONL append-only files loaded into memory. Fine for current volume (<1000 messages) but not scalable. Could migrate to Engram (SQLite) later.
-
JMAP session lifecycle: Python caches session globally. Elixir GenServer state is cleaner but needs refresh logic on token expiry or session invalidation.
-
Queue "done" ambiguity: Queue marks blocked tasks as "done" (no success/failure distinction). Pipeline failures for email tasks will be invisible to the next reflection cycle. This is a pre-existing systemic issue, not specific to the port.
Dependencies on Other Systems
- Symbiont API (port 8111): SMTP handler dispatches to
/taskendpoint -- already exists in Elixir - Router: Email tasks routed through existing
router.ex-- already exists - Engram: MessageStore could eventually migrate to SQLite (engram.ex) -- optional
- Fastmail: External dependency, no changes needed on their side
- DNS/MX:
cortex.hydrascale.netMX records point to this host -- no changes needed
Known Bugs to Fix During Port
-
"19 unread" bug -- After sending a reply via JMAP, Python never calls
Email/setwithkeywords/$seento mark the email read on Fastmail. The blueprint Pipeline explicitly includesJMAP.mark_read/1as a pipeline step, fixing this by design. -
Silent pipeline failures -- Python's
_process_email_async()swallows exceptions. Elixir Pipeline should use Task.Supervisor with proper error logging.
Estimated Scope
- Lines of Elixir: 540-740 (excluding tests)
- New dependencies: 2 (req, gen_smtp)
- New modules: 4-5
- Complexity: The JMAP client is the hardest part (~40% of effort). Everything else maps cleanly to existing Elixir patterns.
- Risk: MEDIUM overall. JMAP protocol correctness is the primary risk; mitigated by the fact that Python mailer.py already documents the exact JMAP calls needed.