Files
2026-05-27 23:29:53 +01:00

5.5 KiB

Enterprise migration: @suaveplan/imessage-adapter

  • Change ID: enterprise-imessage-adapter
  • Status: PROPOSED
  • Capability: imessage-adapter
  • Target package: @suaveplan/imessage-adapter at 0.1.0
  • Target path: packages/communication/messaging/imessage-adapter/
  • Design: ./design.md
  • Spec delta: ./specs/imessage-adapter/spec.md
  • Source on main: /mnt/s/web/suaveplan/packages/universal/imessage-adapter (~718 LOC)
  • Enterprise plan: /mnt/s/web/suaveplan/enterprise/imessage-adapter.md

Why

@suaveplan/imessage-adapter is the macOS-side ingress for any genesis messaging surface that wants to read from Apple's Messages app. It polls the ~/Library/Messages/chat.db SQLite database, decodes the Apple-epoch / nanosecond timestamps, resolves handle + chat + attachment joins, and emits ParsedIMessage payloads — and a downstream InboundMessage-shaped projection — that any inbound-message router can consume without coupling to Apple's schema.

The upstream package (v1.0.0) is functional but not enterprise-grade:

  • require("bun:sqlite") is reached through an any-cast cast at every call site, defeating type-checking and blocking testability.
  • No structured error codes — every failure is a swallowed console-less error event with a bare Error.
  • No Zod boundary on config; no schema-registry hook.
  • No OpenTelemetry instrumentation — no counters for messages emitted, no histogram for poll duration, no error counter.
  • No AbortSignal propagation on start() — stop() only.
  • The internal listener registry duplicates @suaveplan/events's EventBus; the package should ride the shared bus.
  • Tests don't exercise the watcher loop end-to-end against a real SQLite chat.db fixture — they only construct the adapter and assert on the un-initialised error path.

This change rewrites the package on genesis at enterprise quality without removing a single export from the upstream surface, preserving the contract that any future consumer (e.g. an inbound-message router, the existing @suaveplan/api-plugin-messaging plugin family, or a standalone agent) can rely on.

What

A net-new package at packages/communication/messaging/imessage-adapter/ at 0.1.0, preserving the upstream public surface:

  • Classes: IMessageAdapter, IMessageWatcher, IMessageParser
  • Types: IMessageRow, HandleRow, ChatRow, AttachmentRow, ParsedIMessage, ParsedAttachment, IMessageAdapterConfig, IMessageWatcherConfig, InboundMessage, MessageSourceAdapter, IMessageWatcherEvents

Enterprise additions on top:

  1. Structured errors via defineCodes("IMESSAGE", [...]) — 10 stable codes (DB_OPEN_FAILED, DB_QUERY_FAILED, WATCHER_NOT_RUNNING, WATCHER_ALREADY_RUNNING, ADAPTER_NOT_INITIALIZED, INVALID_CONFIG, POLL_ABORTED, POLL_FAILED, ATTACHMENT_PARSE_FAILED, APPLE_DATE_PARSE_FAILED), each reachable from a real call site and asserted in tests.
  2. Zod-validated config boundary at IMessageWatcherConfigSchema and IMessageAdapterConfigSchema, used by the constructor; invalid config throws IMessageInvalidConfigError.
  3. Injectable SqliteReader seam (structural type) replaces the require("bun:sqlite") as any cast. Production wiring uses bun:sqlite lazily; tests use a real in-process bun:sqlite database populated from a chat.db-shaped fixture (no mocks; rule 2).
  4. Injectable Clock + Timers + IdGenerator for deterministic polling, last-rowid pointer management, and envelope IDs.
  5. OTel instrumentation — counters imessage.messages_emitted_total, imessage.poll_errors_total, imessage.attachments_emitted_total; histogram imessage.poll_duration_ms.
  6. AbortSignal propagation — start({ signal }), poll({ signal }), and the internal scheduler all honour cancellation; aborted polls throw IMessagePollAbortedError.
  7. Bus bridge — IMessageWatcher exposes an EventBus-shaped surface in parallel with its legacy on/off API so downstream subscribers can compose with @suaveplan/events directly. The legacy on/off API is preserved for back-compat.
  8. Idempotent rowid pointer — lastRowId is settable + readable, and the watcher refuses to re-emit messages with ROWID <= lastRowId so persisted-pointer consumers can resume safely.
  9. 100/100/100/100 coverage with real SQLite fixtures (no mocks), sibling .md ≥200 words per implementation file, root README.md.

Impact

Affected dependents

Package Impact Action
@suaveplan/events New consumer None — additive
@suaveplan/error New consumer None — additive
@suaveplan/opentelemetry (via @opentelemetry/api) New consumer None — additive
@suaveplan/types New consumer of Clock, IdGenerator, LoggerLike None — additive
(future) @suaveplan/api-plugin-messaging / inbound-message router Will consume IMessageAdapter as a MessageSourceAdapter No live consumer today; future package can depend on ^0.1.0

No genesis package currently imports @suaveplan/imessage-adapter, so this change has zero blast radius beyond adding the package to the manifest + flipping the STATUS row.

Out of scope

Nothing. Per feedback_no_deferrals_on_migration.md and AGENTS.md §2b, no "phase 2" is permitted. The whole upstream surface plus the enterprise additions land in this change, feature-complete, with 100/100/100/100 coverage.