Email was the one sidecar built inside out. The platform held ~1,800 lines — the per-account
SQLite store, all 14 HTTP routes, account CRUD, resync, IMAP validation — while the 314-line
sidecar was a scheduler that reached BACK into the platform to do anything
(`import { performResync } from '../../api/email/resync'`).
The sidecar now serves its own HTTP listener and announces `email:server`, and
/api/email/* on the platform is createSidecarProxy like every other one: 1,801 lines down
to 22, with no mail knowledge left in it — not a message, not a folder, not a credential.
The routes moved verbatim, Hono and all. http.ts only reconstructs what the platform's
middleware used to provide: `user` on the context, from the X-Officer-User header the proxy
injects (trusted because this server binds loopback), and an error handler that turns
custom-errors into status codes.
The /email/events SSE stream went with them, which removes a whole round trip: the IDLE
watcher used to send `email:new` over the registration socket so the platform could push to
its SSE clients. Those clients are here now, so it calls broadcastEmailNew in-process and
`email:new` is gone from the wire protocol.
DELIBERATELY NOT DONE YET, and left backwards on purpose rather than half-moved:
- The two sync handlers (email-sync 381 lines, gmail-sync 712) still run in the platform's
queue and now import the store from its new home — a platform → sidecar import, which is
the wrong direction and is temporary. Moving them is option (A) from the plan: the sidecar
schedules its own syncs, independent of the platform Jobs list.
- accounts.ts still imports queue/init to enqueue a sync and to report sync status, and
index.ts still carries the queue-over-WS shim that inversion needs.
- The three channel handlers still open the mail store directly rather than asking over HTTP.
Two things worth knowing while testing: a from-scratch sync holds a proxied request open
well past the 60s idle default, hence timeoutSeconds on the proxy; and `gmail-sync` is
hardcoded in all three channel handlers even though the only account is provider=gmail with
auth_type=password, which routes to IMAP — so "sync emails" from a chat channel is
almost certainly already broken, and folds into the next stage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/email/messages and /email/stats built `labels LIKE '%${folder}%'` by string
interpolation, and `folder` comes straight off the query string. Five statements across
the two handlers were exposed. The fragment is bound now, and it carries its parameters
with it because each handler builds several statements from the same fragment and has
to spread them in order.
Checked against an in-memory table: inbox/INBOX/SENT/all return exactly what they
returned before, and `x' OR 1=1 --` now matches nothing instead of being SQL.
Also: page and limit reached the bindings as NaN for any non-numeric value, so a
mistyped query param was a 500. They fall back to their defaults now.
And deleted src/servers/sidecar/email-cron.ts — 92 lines imported by nothing. The live
cron is sidecar/email/email-cron.ts; this was an older copy that still reached into
queue-runner and google-auth directly, so leaving it there invites someone to fix the
wrong file.
This is the first commit on the email branch; the placement problems (the whole mail
store, both syncs, and the resync coalescing that cannot work across processes) are
untouched and much larger — see SIDECAR_WORK_LOG.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorganizes email storage: the DB moves from DATA_PATH/<user>/emails.db to
DATA_PATH/<user>/email_accounts/<accountEmail>/emails.db, with a shared
email_accounts/attachment_cache/ (was Gmail/emails/attachments). openEmailDb now
takes (owner, account); a new openUserEmailDb(owner, userId) resolves the user's
configured account (first enabled) for read paths. Threads the account through
email.ts, accounts, resync, queue sync, channel handlers, and the email_db MCP
tool path. Drops the dead getUserEmailDir helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The API and email sidecar both open emails.db; without a busy timeout a write
overlap (resync, or the thread_id backfill) throws "database is locked" and
500s a request. Wait up to 5s for the other writer instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hybrid grouping via a new thread_id column: new mail threads exactly on
References/In-Reply-To (id is sha1(Message-Id), so a referenced id hashes to
the ancestor's own id); already-synced mail is backfilled with a
normalized-subject + counterpart key. Folder views collapse to one row per
thread with a count badge; the reader shows the thread as a collapsible stack.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- add getValidGoogleAccessToken helper and use it in email-cron, email account auth resolver, and the new gmail proxy
- POST /api/integrations/google/gmail-proxy forwards arbitrary gmail rest calls server-side, with auto-refreshed oauth
- pi-manager and claude user-instance inject OFFICER_API_URL + per-session JWT so tools can call back as the user
- claude anthropic proxy uses Authorization: Bearer + preserves any anthropic-beta headers (pro oauth tokens are rejected via x-api-key, and overwriting the beta header broke context_management)
- pi --list-models: fall back to stderr when stdout is empty (pi v0.73.1 writes the table to stderr)
- mcp tool server + pi tool loader: accept type: object inputs so json bodies stay structured
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First sync still uses IMAP with app password. Subsequent syncs use
Gmail API history.list + messages.get with OAuth for faster, more
reliable incremental sync. Dispatch gmail-sync handler for gmail
accounts instead of generic email-sync. Show sync button for synced
accounts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Flips the connection model so sidecars register themselves with the API
server via WebSocket at /api/sidecar/register, enabling dynamic discovery,
location independence, and automatic reconnection from either side.
- Add registration protocol types and PTY command/event types
- Create sidecar-registry.ts (replaces sidecar-client.ts) as passive registry
- Create sidecar connector (connect.ts) with exponential backoff reconnect
- Convert process sidecar from WS server to WS client
- Convert PTY sidecar from WS server to multiplexed WS client
- Simplify terminal bridge to thin adapter using registry
- Add PTY sidecar as PM2-managed process
- Update all consumer imports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewired EmailList sync button to call /email/accounts/:id/sync instead
of the old gmail-sync job. Shows sync button for connected and synced
accounts. Allow manual incremental sync for synced accounts.
Added duration logging to queue runner: start/complete/fail markers
with elapsed time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sidecar/queue runner no longer needs job-specific context. API server
resolves account details, IMAP auth, and user email at enqueue time —
all persisted in the job file. Handler reads directly from job meta.
Removed "Load account" step. Sync step auto-reconnects up to 10 times
when Gmail drops the connection, resuming from saved UIDs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Issues:
1. User directories created with chmod 770 (too restrictive)
- Prevents service user from reading/writing even though in group
- Changed to chmod 775 (recursive) to allow group access
2. openEmailDb tries to chmod file it may not own
- If file owned by different user (e.g., andrepadez), chmod fails
- Wrapped chmodSync in try/catch to gracefully skip
Changes:
- src/servers/api/users/provision.ts:
* Changed chmod 770 → 775 (owner/group rwx, others rx)
* Applied recursively to all subdirectories
* Added chmod after seeding files to ensure consistency
- src/servers/api/email/email-db.ts:
* Wrapped chmodSync in try/catch
* Logs silently skip if file not owned by current process
* Database still works even if chmod fails
Fixes /email endpoint 500 errors for users with email data.
- add claude-code as virtual model in channel messaging (telegram/discord/whatsapp)
- new send-claude-code.ts: docker exec claude -p with session resumption
- route claude-code model in sendAndAwait before Pi pipeline
- append claude-code to listPiModels output
- fix container .claude mount (rw for sub-mounts), hooks format (matcher-based)
- pre-seed hasTrustDialogAccepted and bypassPermissions in container settings
- git init in entrypoint to skip workspace trust prompt
- fix ~/~ double-tilde in CommandTerminalWrapper cwd resolution
- remove --continue from claude-code panel command
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- respect charset from Content-Type when decoding body and headers
- detect inline attachments with filenames (not just disposition: attachment)
- fix broken decodeQuotedPrintable reference in parseAttachments
- white background for email iframe (emails designed for light bg)
- sticky header in email list, scrollable message area
- arrow key navigation between emails with scroll-into-view
- /email/:id routing for deep linking to specific emails
- empty folder shows "No emails in this folder" instead of losing header
- fix attachment viewer header overlap with dialog close button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>