@cool-ai/beach-missives
v0.0.6
Published
Universal message record and pluggable storage adapters for Beach applications.
Readme
@cool-ai/beach-missives
Owns the universal message record and its pluggable storage. Missives are the permanent history every Beach application writes alongside the event log — what the user said, what the assistant replied, and every outbound draft waiting for a batched edge to pick up. The storage backend is swappable (in-memory, Redis, file); the shape is not.
Home: cool-ai.org · Documentation: cool-ai.org/docs
Why "missive"
A missive is a message or letter — singular, un-ambiguous, covers any channel. The term was chosen over "correspondence" (ambiguous: means both letter and matching between things) and "exchange" (ambiguous: finance, currency, software). Readers unfamiliar with the word learn it once.
Install
npm install @cool-ai/beach-missivesFor Redis-backed storage, install the optional peer:
npm install ioredisConcern
@cool-ai/beach-missives provides:
- The
Missivetype — the universal message record (channel-agnostic). Every inbound and outbound message through anyChannelAdapteris saved as one. - The
MissiveStoreinterface —save,getById,getByThread,findByChannelAddress,search. - Thread ID normalisation —
normaliseThreadId(subject)stripsRe:/Fwd:, lowercases, trims. Shared logic so every adapter threads consistently. ArtifactStoreinterface andMissiveAttachmentreferences — inbound/outbound files are stored as artifacts; missives carry references, not bytes (see BF-007 in the meta-plan).- Reference storage adapters:
InMemoryStore— for tests and ephemeral use.RedisStore— Redis-backed with optional RediSearch for search. Requires Redis with AOF persistence for durability.JsonFileStore— single-file or per-thread JSON on disk. Zero-infra; suitable for prototypes and low-volume use.SqliteStore— SQLite with FTS5 for text search. Single-file; production-ready for single-node deployments. ThrowsMissiveDeserialiseError(exported) when a stored row fails shape validation — allows callers to distinguish corrupted records from other store errors.
- Extension hook — consumers provide their own adapter for other stores (MariaDB, Postgres, DynamoDB, etc.) by implementing the interface.
What the missive record does
- Cross-channel continuity. A conversation started on email continues in chat — same
threadId, same linked entities. - Searchable history. "What did we discuss last month about Corfu?" resolves through the store's search.
- Entity linkage. Missive records link to the consumer's domain entities (tasks, projects, basket items) via application-defined join tables.
- Audit trail. Every external exchange — including peer-agent MCP/A2A calls — is recorded.
- Multi-modal body. Missives can carry text, structured data, and references to artifact attachments (images, audio, files).
Record shape
interface Missive {
id: string; // UUID assigned at creation, immutable
sessionId: string; // links to the session/conversation
turnId?: string; // links to the turn that produced this missive
channelId?: string; // e.g. 'email', 'chat', 'whatsapp' — populated by channel edges
threadId?: string; // thread key for multi-message conversations; defaults to sessionId
externalId?: string; // channel-native message ID (IMAP UID, WhatsApp msgId, etc.)
tenantId?: string; // reserved for multi-tenancy; enforcement is v2+
origin: MissiveOrigin; // sender identity — required
destination?: MissiveDestination; // counterpart — populated by channel edges
parts: MissivePart[]; // the payload — multi-modal
subject?: string; // email subject, Teams thread title, etc.
inReplyTo?: string; // ID of the missive this is a direct reply to
references?: string[]; // RFC 5322-style ancestor chain for multi-hop threading, oldest → newest
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}
interface MissiveOrigin {
address?: string; // email address, phone number, UPN, etc.
messageId?: string; // native message ID from the originating system
displayName?: string;
}
interface MissiveDestination {
to?: string[];
cc?: string[];
bcc?: string[]; // typically populated on outbound only
metadata?: Record<string, unknown>; // opaque channel-specific fields the outbound edge may need
}
interface MissivePart {
partType: string; // open string validated against PartTypeRegistry
text?: string;
data?: unknown;
metadata?: Record<string, unknown>;
}Channel fields (channelId, threadId, externalId, destination, references) are optional and left unset by observability-only writers; channel inbound edges populate them fully so outbound edges can reconstruct channel artifacts.
Choosing a storage adapter
| Adapter | When to use |
|---------|-------------|
| InMemoryStore | Tests, ephemeral agents with no need for cross-restart history |
| RedisStore | When the consumer already runs Redis and wants zero new infrastructure |
| JsonFileStore | Prototypes and low-volume agents; easy backup via git or rsync |
| SqliteStore | Single-node production agents wanting durable search |
| Custom adapter | Production agents with existing RDBMS or specialised stores |
The library imposes no adapter. A consumer picks one, configures it, and passes it to @cool-ai/beach-transport's adapter wiring.
Artifact storage
Artifacts (files, images, audio) live in an ArtifactStore, separate from the missive record. Reference adapters:
InMemoryArtifactStore— tests only.FilesystemArtifactStore— local disk.S3ArtifactStore— S3-compatible object storage.
The missive record carries only the artifactId reference. This keeps the missive store lean and allows storage of large files to scale independently.
Not in this package
- Channel adapters themselves (
@cool-ai/beach-transport). - The envelope format (
@cool-ai/beach-protocol).
Consumers
Any agent that wants persistent cross-session or cross-channel message history. An agent that only serves stateless peer calls can skip this package.
Related
- ../transport/ — adapters that read/write missives.
- https://cool-ai.org/docs/design-principles — principle 2.7 (channel-agnostic actors) depends on a consistent record model.
