agent-mailbox-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,556 @@
1
+ // @bun
2
+ // src/database.ts
3
+ import { Database } from "bun:sqlite";
4
+ var SCHEMA_VERSION = 1;
5
+ function initDatabase(config) {
6
+ const db = new Database(config.dbPath);
7
+ if (config.walMode) {
8
+ db.exec("PRAGMA journal_mode=WAL");
9
+ db.exec("PRAGMA synchronous=NORMAL");
10
+ }
11
+ db.exec("PRAGMA foreign_keys=ON");
12
+ db.exec(`
13
+ CREATE TABLE IF NOT EXISTS schema_version (
14
+ version INTEGER NOT NULL
15
+ );
16
+
17
+ CREATE TABLE IF NOT EXISTS messages (
18
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
19
+ from_agent TEXT NOT NULL,
20
+ to_agent TEXT NOT NULL,
21
+ subject TEXT NOT NULL,
22
+ body TEXT NOT NULL,
23
+ thread_id TEXT NOT NULL,
24
+ priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('high', 'normal', 'low')),
25
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'delivered', 'read', 'acked', 'expired', 'dead')),
26
+ ttl_seconds INTEGER NOT NULL DEFAULT 86400,
27
+ idempotency_key TEXT,
28
+ trace_id TEXT,
29
+ receive_count INTEGER NOT NULL DEFAULT 0,
30
+ visible_after TEXT,
31
+ session_id TEXT NOT NULL DEFAULT '',
32
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
33
+ read_at TEXT,
34
+ ack_at TEXT,
35
+ expires_at TEXT NOT NULL DEFAULT (datetime('now', '+86400 seconds'))
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_messages_to_agent_status ON messages(to_agent, status);
39
+ CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id);
40
+ CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at);
41
+ CREATE INDEX IF NOT EXISTS idx_messages_expires ON messages(expires_at);
42
+ CREATE INDEX IF NOT EXISTS idx_messages_visible ON messages(visible_after);
43
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_idempotency ON messages(idempotency_key) WHERE idempotency_key IS NOT NULL;
44
+
45
+ CREATE TABLE IF NOT EXISTS threads (
46
+ id TEXT PRIMARY KEY,
47
+ subject TEXT NOT NULL,
48
+ participants TEXT NOT NULL DEFAULT '[]',
49
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
50
+ last_message_at TEXT NOT NULL DEFAULT (datetime('now'))
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS dead_letters (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ original_message_id INTEGER NOT NULL,
56
+ from_agent TEXT NOT NULL,
57
+ to_agent TEXT NOT NULL,
58
+ subject TEXT NOT NULL,
59
+ body TEXT NOT NULL,
60
+ thread_id TEXT NOT NULL,
61
+ reason TEXT NOT NULL,
62
+ moved_at TEXT NOT NULL DEFAULT (datetime('now'))
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS rate_limits (
66
+ agent TEXT NOT NULL,
67
+ window_start TEXT NOT NULL,
68
+ message_count INTEGER NOT NULL DEFAULT 1,
69
+ PRIMARY KEY (agent, window_start)
70
+ );
71
+
72
+ CREATE TABLE IF NOT EXISTS agent_registry (
73
+ name TEXT PRIMARY KEY,
74
+ role TEXT,
75
+ registered_at TEXT NOT NULL DEFAULT (datetime('now')),
76
+ last_active TEXT NOT NULL DEFAULT (datetime('now'))
77
+ );
78
+ `);
79
+ try {
80
+ db.exec(`
81
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
82
+ subject, body, content=messages, content_rowid=id
83
+ );
84
+
85
+ CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
86
+ INSERT INTO messages_fts(rowid, subject, body) VALUES (new.id, new.subject, new.body);
87
+ END;
88
+
89
+ CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
90
+ INSERT INTO messages_fts(messages_fts, rowid, subject, body) VALUES ('delete', old.id, old.subject, old.body);
91
+ END;
92
+ `);
93
+ } catch {}
94
+ const currentVersion = db.prepare("SELECT version FROM schema_version LIMIT 1").get();
95
+ if (!currentVersion) {
96
+ db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(SCHEMA_VERSION);
97
+ }
98
+ return db;
99
+ }
100
+
101
+ // src/mailbox.ts
102
+ var DEFAULT_CONFIG = {
103
+ dbPath: ":memory:",
104
+ defaultTTL: 86400,
105
+ visibilityTimeout: 300,
106
+ maxRetries: 3,
107
+ maxBodySize: 65536,
108
+ rateLimitPerMinute: 60,
109
+ walMode: true,
110
+ cleanupInterval: 300
111
+ };
112
+ function resolveConfig(config) {
113
+ return { ...DEFAULT_CONFIG, ...config };
114
+ }
115
+ function generateThreadId() {
116
+ return `thread-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
117
+ }
118
+ class Mailbox {
119
+ db;
120
+ config;
121
+ cleanupTimer = null;
122
+ stmts;
123
+ constructor(config) {
124
+ this.config = resolveConfig(config);
125
+ this.db = initDatabase(this.config);
126
+ this.stmts = this.prepareStatements();
127
+ if (this.config.cleanupInterval > 0) {
128
+ this.cleanupTimer = setInterval(() => this.cleanup(), this.config.cleanupInterval * 1000);
129
+ }
130
+ }
131
+ prepareStatements() {
132
+ return {
133
+ insertMessage: this.db.prepare(`
134
+ INSERT INTO messages (from_agent, to_agent, subject, body, thread_id, priority, ttl_seconds, idempotency_key, trace_id, session_id, expires_at)
135
+ VALUES ($from, $to, $subject, $body, $thread_id, $priority, $ttl, $idem_key, $trace_id, $session_id, datetime('now', '+' || $ttl || ' seconds'))
136
+ `),
137
+ insertThread: this.db.prepare(`
138
+ INSERT OR IGNORE INTO threads (id, subject, participants) VALUES ($id, $subject, $participants)
139
+ `),
140
+ updateThreadTimestamp: this.db.prepare(`
141
+ UPDATE threads SET last_message_at = datetime('now') WHERE id = $id
142
+ `),
143
+ getInbox: this.db.prepare(`
144
+ SELECT * FROM messages
145
+ WHERE (to_agent = $agent OR to_agent = 'broadcast')
146
+ AND from_agent != $agent
147
+ AND status IN ('pending', 'delivered')
148
+ AND (visible_after IS NULL OR visible_after <= datetime('now'))
149
+ AND expires_at > datetime('now')
150
+ ORDER BY
151
+ CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 WHEN 'low' THEN 2 END,
152
+ created_at DESC
153
+ LIMIT $limit
154
+ `),
155
+ getInboxIncludeRead: this.db.prepare(`
156
+ SELECT * FROM messages
157
+ WHERE (to_agent = $agent OR to_agent = 'broadcast')
158
+ AND from_agent != $agent
159
+ AND status NOT IN ('dead', 'expired')
160
+ AND expires_at > datetime('now')
161
+ ORDER BY created_at DESC
162
+ LIMIT $limit
163
+ `),
164
+ claimMessage: this.db.prepare(`
165
+ UPDATE messages
166
+ SET status = 'delivered',
167
+ receive_count = receive_count + 1,
168
+ visible_after = datetime('now', '+' || $timeout || ' seconds')
169
+ WHERE id = $id
170
+ `),
171
+ markRead: this.db.prepare(`
172
+ UPDATE messages SET status = 'read', read_at = datetime('now'), visible_after = NULL WHERE id = $id
173
+ `),
174
+ markAcked: this.db.prepare(`
175
+ UPDATE messages SET status = 'acked', ack_at = datetime('now'), visible_after = NULL WHERE id = $id
176
+ `),
177
+ getMessage: this.db.prepare(`SELECT * FROM messages WHERE id = $id`),
178
+ searchFTS: this.db.prepare(`
179
+ SELECT m.* FROM messages m
180
+ JOIN messages_fts fts ON m.id = fts.rowid
181
+ WHERE messages_fts MATCH $query
182
+ AND m.expires_at > datetime('now')
183
+ ORDER BY m.created_at DESC
184
+ LIMIT $limit
185
+ `),
186
+ searchLIKE: this.db.prepare(`
187
+ SELECT * FROM messages
188
+ WHERE (subject LIKE $q OR body LIKE $q)
189
+ AND expires_at > datetime('now')
190
+ ORDER BY created_at DESC
191
+ LIMIT $limit
192
+ `),
193
+ listThreads: this.db.prepare(`
194
+ SELECT t.*,
195
+ COUNT(m.id) as message_count,
196
+ SUM(CASE WHEN m.status IN ('pending', 'delivered') AND (m.to_agent = $agent OR m.to_agent = 'broadcast') THEN 1 ELSE 0 END) as unread_count
197
+ FROM threads t
198
+ LEFT JOIN messages m ON m.thread_id = t.id
199
+ GROUP BY t.id
200
+ ORDER BY t.last_message_at DESC
201
+ LIMIT $limit
202
+ `),
203
+ getThreadMessages: this.db.prepare(`
204
+ SELECT * FROM messages WHERE thread_id = $thread_id ORDER BY created_at ASC
205
+ `),
206
+ getReply: this.db.prepare(`
207
+ SELECT * FROM messages
208
+ WHERE thread_id = $thread_id AND from_agent = $from AND to_agent = $to AND id > $after_id
209
+ ORDER BY created_at ASC LIMIT 1
210
+ `),
211
+ moveToDLQ: this.db.prepare(`
212
+ INSERT INTO dead_letters (original_message_id, from_agent, to_agent, subject, body, thread_id, reason)
213
+ SELECT id, from_agent, to_agent, subject, body, thread_id, $reason
214
+ FROM messages WHERE id = $id
215
+ `),
216
+ markDead: this.db.prepare(`
217
+ UPDATE messages SET status = 'dead' WHERE id = $id
218
+ `),
219
+ getDeadLetters: this.db.prepare(`
220
+ SELECT * FROM dead_letters ORDER BY moved_at DESC LIMIT $limit
221
+ `),
222
+ replayDeadLetter: this.db.prepare(`
223
+ SELECT * FROM dead_letters WHERE id = $id
224
+ `),
225
+ deleteDeadLetter: this.db.prepare(`
226
+ DELETE FROM dead_letters WHERE id = $id
227
+ `),
228
+ checkRate: this.db.prepare(`
229
+ SELECT message_count FROM rate_limits
230
+ WHERE agent = $agent AND window_start = $window
231
+ `),
232
+ upsertRate: this.db.prepare(`
233
+ INSERT INTO rate_limits (agent, window_start, message_count)
234
+ VALUES ($agent, $window, 1)
235
+ ON CONFLICT(agent, window_start)
236
+ DO UPDATE SET message_count = message_count + 1
237
+ `),
238
+ upsertAgent: this.db.prepare(`
239
+ INSERT INTO agent_registry (name, role, last_active)
240
+ VALUES ($name, $role, datetime('now'))
241
+ ON CONFLICT(name)
242
+ DO UPDATE SET role = COALESCE($role, role), last_active = datetime('now')
243
+ `),
244
+ listAgents: this.db.prepare(`
245
+ SELECT ar.name, ar.role, ar.last_active,
246
+ (SELECT COUNT(*) FROM messages WHERE from_agent = ar.name) as message_count
247
+ FROM agent_registry ar
248
+ ORDER BY ar.last_active DESC
249
+ `),
250
+ expireMessages: this.db.prepare(`
251
+ UPDATE messages SET status = 'expired' WHERE expires_at <= datetime('now') AND status NOT IN ('acked', 'expired', 'dead')
252
+ `),
253
+ requeueTimedOut: this.db.prepare(`
254
+ UPDATE messages SET status = 'pending', visible_after = NULL
255
+ WHERE status = 'delivered'
256
+ AND visible_after IS NOT NULL
257
+ AND visible_after <= datetime('now')
258
+ AND receive_count < $max_retries
259
+ `),
260
+ moveExhaustedToDLQ: this.db.prepare(`
261
+ SELECT id FROM messages
262
+ WHERE status = 'delivered'
263
+ AND visible_after IS NOT NULL
264
+ AND visible_after <= datetime('now')
265
+ AND receive_count >= $max_retries
266
+ `),
267
+ cleanRateLimits: this.db.prepare(`
268
+ DELETE FROM rate_limits WHERE window_start < $cutoff
269
+ `),
270
+ countByStatus: this.db.prepare(`
271
+ SELECT status, COUNT(*) as cnt FROM messages GROUP BY status
272
+ `),
273
+ countDeadLetters: this.db.prepare(`
274
+ SELECT COUNT(*) as cnt FROM dead_letters
275
+ `),
276
+ countActiveThreads: this.db.prepare(`
277
+ SELECT COUNT(*) as cnt FROM threads WHERE last_message_at > datetime('now', '-1 hour')
278
+ `),
279
+ messagesPerAgent: this.db.prepare(`
280
+ SELECT from_agent, COUNT(*) as cnt FROM messages GROUP BY from_agent ORDER BY cnt DESC
281
+ `),
282
+ avgDeliveryTime: this.db.prepare(`
283
+ SELECT AVG((julianday(read_at) - julianday(created_at)) * 86400000) as avg_ms
284
+ FROM messages WHERE read_at IS NOT NULL
285
+ `),
286
+ checkIdempotency: this.db.prepare(`
287
+ SELECT id, thread_id FROM messages WHERE idempotency_key = $key
288
+ `)
289
+ };
290
+ }
291
+ send(opts) {
292
+ if (Buffer.byteLength(opts.body, "utf-8") > this.config.maxBodySize) {
293
+ throw new Error(`Message body exceeds max size of ${this.config.maxBodySize} bytes`);
294
+ }
295
+ this.checkRateLimit(opts.from);
296
+ if (opts.idempotencyKey) {
297
+ const existing = this.stmts.checkIdempotency.get({ $key: opts.idempotencyKey });
298
+ if (existing) {
299
+ return { messageId: existing.id, threadId: existing.thread_id, idempotencyKey: opts.idempotencyKey };
300
+ }
301
+ }
302
+ const threadId = opts.threadId ?? generateThreadId();
303
+ const ttl = opts.ttlSeconds ?? this.config.defaultTTL;
304
+ this.stmts.insertThread.run({
305
+ $id: threadId,
306
+ $subject: opts.subject,
307
+ $participants: JSON.stringify([opts.from, opts.to])
308
+ });
309
+ this.stmts.updateThreadTimestamp.run({ $id: threadId });
310
+ const result = this.stmts.insertMessage.run({
311
+ $from: opts.from,
312
+ $to: opts.to,
313
+ $subject: opts.subject,
314
+ $body: opts.body,
315
+ $thread_id: threadId,
316
+ $priority: opts.priority ?? "normal",
317
+ $ttl: ttl,
318
+ $idem_key: opts.idempotencyKey ?? null,
319
+ $trace_id: opts.traceId ?? null,
320
+ $session_id: opts.sessionId ?? ""
321
+ });
322
+ this.stmts.upsertAgent.run({ $name: opts.from, $role: null });
323
+ return {
324
+ messageId: Number(result.lastInsertRowid),
325
+ threadId,
326
+ idempotencyKey: opts.idempotencyKey ?? null
327
+ };
328
+ }
329
+ broadcast(opts) {
330
+ return this.send({ ...opts, to: "broadcast" });
331
+ }
332
+ readInbox(opts) {
333
+ const limit = opts.limit ?? 20;
334
+ if (opts.includeRead) {
335
+ return this.stmts.getInboxIncludeRead.all({ $agent: opts.agent, $limit: limit });
336
+ }
337
+ const rows = this.stmts.getInbox.all({ $agent: opts.agent, $limit: limit });
338
+ for (const row of rows) {
339
+ this.stmts.claimMessage.run({
340
+ $id: row.id,
341
+ $timeout: this.config.visibilityTimeout
342
+ });
343
+ }
344
+ return rows;
345
+ }
346
+ markRead(messageId) {
347
+ this.stmts.markRead.run({ $id: messageId });
348
+ }
349
+ acknowledge(messageId, response) {
350
+ this.stmts.markAcked.run({ $id: messageId });
351
+ if (response) {
352
+ const original = this.stmts.getMessage.get({ $id: messageId });
353
+ if (original) {
354
+ return this.send({
355
+ from: response.from,
356
+ to: original.from_agent,
357
+ subject: `Re: ${original.subject}`,
358
+ body: response.body,
359
+ threadId: original.thread_id,
360
+ priority: "normal",
361
+ sessionId: response.sessionId
362
+ });
363
+ }
364
+ }
365
+ return null;
366
+ }
367
+ search(opts) {
368
+ const limit = opts.limit ?? 10;
369
+ try {
370
+ const rows = this.stmts.searchFTS.all({ $query: opts.query, $limit: limit });
371
+ return { messages: rows, usedFallback: false };
372
+ } catch {
373
+ const rows = this.stmts.searchLIKE.all({ $q: `%${opts.query}%`, $limit: limit });
374
+ return { messages: rows, usedFallback: true };
375
+ }
376
+ }
377
+ listThreads(agent, limit = 10) {
378
+ return this.stmts.listThreads.all({ $agent: agent, $limit: limit });
379
+ }
380
+ getThread(threadId) {
381
+ return this.stmts.getThreadMessages.all({ $thread_id: threadId });
382
+ }
383
+ async request(opts) {
384
+ const timeout = opts.timeoutMs ?? 120000;
385
+ const { messageId, threadId } = this.send({
386
+ ...opts,
387
+ priority: "high",
388
+ body: opts.body + `
389
+
390
+ ---
391
+ REPLY REQUESTED \u2014 sender is waiting.`
392
+ });
393
+ const startTime = Date.now();
394
+ let delay = 500;
395
+ while (Date.now() - startTime < timeout) {
396
+ const reply = this.stmts.getReply.get({
397
+ $thread_id: threadId,
398
+ $from: opts.to,
399
+ $to: opts.from,
400
+ $after_id: messageId
401
+ });
402
+ if (reply) {
403
+ this.markRead(reply.id);
404
+ return { reply };
405
+ }
406
+ await new Promise((r) => setTimeout(r, delay));
407
+ delay = Math.min(delay * 1.5, 1e4);
408
+ }
409
+ return { timeout: true, messageId, threadId };
410
+ }
411
+ registerAgent(name, role) {
412
+ this.stmts.upsertAgent.run({ $name: name, $role: role ?? null });
413
+ }
414
+ listAgents() {
415
+ return this.stmts.listAgents.all();
416
+ }
417
+ getDeadLetters(limit = 20) {
418
+ return this.stmts.getDeadLetters.all({ $limit: limit });
419
+ }
420
+ replayDeadLetter(dlqId) {
421
+ const dl = this.stmts.replayDeadLetter.get({ $id: dlqId });
422
+ if (!dl)
423
+ return null;
424
+ const result = this.send({
425
+ from: dl.from_agent,
426
+ to: dl.to_agent,
427
+ subject: dl.subject,
428
+ body: dl.body,
429
+ threadId: dl.thread_id
430
+ });
431
+ this.stmts.deleteDeadLetter.run({ $id: dlqId });
432
+ return result;
433
+ }
434
+ metrics() {
435
+ const statusCounts = this.stmts.countByStatus.all();
436
+ const statusMap = {};
437
+ let total = 0;
438
+ for (const row of statusCounts) {
439
+ statusMap[row.status] = row.cnt;
440
+ total += row.cnt;
441
+ }
442
+ const dlCount = this.stmts.countDeadLetters.get().cnt;
443
+ const threadCount = this.stmts.countActiveThreads.get().cnt;
444
+ const perAgent = this.stmts.messagesPerAgent.all();
445
+ const avgDel = this.stmts.avgDeliveryTime.get();
446
+ return {
447
+ totalMessages: total,
448
+ pendingMessages: statusMap["pending"] ?? 0,
449
+ deliveredMessages: statusMap["delivered"] ?? 0,
450
+ deadLetters: dlCount,
451
+ activeThreads: threadCount,
452
+ messagesPerAgent: Object.fromEntries(perAgent.map((r) => [r.from_agent, r.cnt])),
453
+ avgDeliveryTimeMs: avgDel.avg_ms
454
+ };
455
+ }
456
+ cleanup() {
457
+ const expired = this.stmts.expireMessages.run().changes;
458
+ const requeued = this.stmts.requeueTimedOut.run({ $max_retries: this.config.maxRetries }).changes;
459
+ const exhausted = this.stmts.moveExhaustedToDLQ.all({ $max_retries: this.config.maxRetries });
460
+ for (const { id } of exhausted) {
461
+ this.stmts.moveToDLQ.run({ $id: id, $reason: `Max retries (${this.config.maxRetries}) exceeded` });
462
+ this.stmts.markDead.run({ $id: id });
463
+ }
464
+ const cutoff = new Date(Date.now() - 120000).toISOString().slice(0, 16);
465
+ this.stmts.cleanRateLimits.run({ $cutoff: cutoff });
466
+ return { expired, requeued, deadLettered: exhausted.length };
467
+ }
468
+ close() {
469
+ if (this.cleanupTimer) {
470
+ clearInterval(this.cleanupTimer);
471
+ this.cleanupTimer = null;
472
+ }
473
+ this.db.close();
474
+ }
475
+ checkRateLimit(agent) {
476
+ const window = new Date().toISOString().slice(0, 16);
477
+ const current = this.stmts.checkRate.get({ $agent: agent, $window: window });
478
+ if (current && current.message_count >= this.config.rateLimitPerMinute) {
479
+ throw new Error(`Rate limit exceeded for agent '${agent}': ${this.config.rateLimitPerMinute}/min`);
480
+ }
481
+ this.stmts.upsertRate.run({ $agent: agent, $window: window });
482
+ }
483
+ }
484
+ // src/format.ts
485
+ function formatMessages(rows) {
486
+ if (!rows || rows.length === 0)
487
+ return "No messages found.";
488
+ return rows.map((m) => `[#${m.id}] ${m.priority === "high" ? "!! " : m.priority === "low" ? "-- " : ""}` + `From: @${m.from_agent} -> To: @${m.to_agent}
489
+ ` + `Subject: ${m.subject}
490
+ ` + `Thread: ${m.thread_id} | Status: ${m.status} | Receives: ${m.receive_count}
491
+ ` + `Time: ${m.created_at} | Expires: ${m.expires_at}` + `${m.read_at ? ` | Read: ${m.read_at}` : ""}` + `${m.ack_at ? ` | Acked: ${m.ack_at}` : ""}
492
+ ` + `${m.trace_id ? `Trace: ${m.trace_id}
493
+ ` : ""}` + `---
494
+ ${m.body}
495
+ `).join(`
496
+ ` + "=".repeat(50) + `
497
+
498
+ `);
499
+ }
500
+ function formatThreads(threads) {
501
+ if (!threads || threads.length === 0)
502
+ return "No active threads.";
503
+ return threads.map((t) => `[${t.id}] ${t.subject}
504
+ ` + ` Messages: ${t.message_count} | Unread: ${t.unread_count}
505
+ ` + ` Last activity: ${t.last_message_at}`).join(`
506
+
507
+ `);
508
+ }
509
+ function formatAgents(agents) {
510
+ if (!agents || agents.length === 0)
511
+ return "No registered agents.";
512
+ return agents.map((a) => `@${a.name} [${a.messageCount} msgs]` + `${a.lastActive ? ` last active: ${a.lastActive}` : " (never active)"}
513
+ ` + ` ${a.role ?? "No role defined"}`).join(`
514
+
515
+ `);
516
+ }
517
+ function formatDeadLetters(dls) {
518
+ if (!dls || dls.length === 0)
519
+ return "Dead letter queue is empty.";
520
+ return dls.map((d) => `[DLQ #${d.id}] Original: #${d.original_message_id}
521
+ ` + `From: @${d.from_agent} -> To: @${d.to_agent}
522
+ ` + `Subject: ${d.subject}
523
+ ` + `Reason: ${d.reason}
524
+ ` + `Moved: ${d.moved_at}
525
+ ` + `---
526
+ ${d.body}
527
+ `).join(`
528
+ ` + "=".repeat(50) + `
529
+
530
+ `);
531
+ }
532
+ function formatMetrics(m) {
533
+ const lines = [
534
+ `Mailbox Metrics`,
535
+ `${"=".repeat(40)}`,
536
+ `Total messages: ${m.totalMessages}`,
537
+ `Pending: ${m.pendingMessages}`,
538
+ `Delivered (in-flight): ${m.deliveredMessages}`,
539
+ `Dead letters: ${m.deadLetters}`,
540
+ `Active threads (last 1h): ${m.activeThreads}`,
541
+ `Avg delivery time: ${m.avgDeliveryTimeMs ? `${Math.round(m.avgDeliveryTimeMs)}ms` : "N/A"}`,
542
+ ``,
543
+ `Messages per agent:`,
544
+ ...Object.entries(m.messagesPerAgent).map(([a, c]) => ` @${a}: ${c}`)
545
+ ];
546
+ return lines.join(`
547
+ `);
548
+ }
549
+ export {
550
+ formatThreads,
551
+ formatMetrics,
552
+ formatMessages,
553
+ formatDeadLetters,
554
+ formatAgents,
555
+ Mailbox
556
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Core Mailbox class — the main API for agent-mailbox
3
+ */
4
+ import { Database } from "bun:sqlite";
5
+ import type { MailboxConfig, ResolvedConfig, Message, SendOptions, SendResult, InboxOptions, SearchOptions, Thread, DeadLetter, MailboxMetrics, AgentInfo } from "./types.js";
6
+ export declare class Mailbox {
7
+ readonly db: Database;
8
+ readonly config: ResolvedConfig;
9
+ private cleanupTimer;
10
+ private stmts;
11
+ constructor(config?: MailboxConfig);
12
+ private prepareStatements;
13
+ /** Send a message to an agent */
14
+ send(opts: SendOptions): SendResult;
15
+ /** Broadcast a message to all agents */
16
+ broadcast(opts: Omit<SendOptions, "to">): SendResult;
17
+ /** Read inbox with visibility timeout */
18
+ readInbox(opts: InboxOptions): Message[];
19
+ /** Mark a message as read (clears visibility timeout) */
20
+ markRead(messageId: number): void;
21
+ /** Acknowledge a message (confirms processing complete) */
22
+ acknowledge(messageId: number, response?: {
23
+ from: string;
24
+ body: string;
25
+ sessionId?: string;
26
+ }): SendResult | null;
27
+ /** Search messages using FTS5 with LIKE fallback */
28
+ search(opts: SearchOptions): {
29
+ messages: Message[];
30
+ usedFallback: boolean;
31
+ };
32
+ /** List conversation threads */
33
+ listThreads(agent: string, limit?: number): Thread[];
34
+ /** Get all messages in a thread */
35
+ getThread(threadId: string): Message[];
36
+ /** Send a request and poll for reply with exponential backoff */
37
+ request(opts: SendOptions & {
38
+ timeoutMs?: number;
39
+ }): Promise<{
40
+ reply: Message;
41
+ } | {
42
+ timeout: true;
43
+ messageId: number;
44
+ threadId: string;
45
+ }>;
46
+ /** Register an agent (upsert) */
47
+ registerAgent(name: string, role?: string): void;
48
+ /** List all registered agents */
49
+ listAgents(): AgentInfo[];
50
+ /** Get messages in the dead letter queue */
51
+ getDeadLetters(limit?: number): DeadLetter[];
52
+ /** Replay a dead letter (re-send the original message) */
53
+ replayDeadLetter(dlqId: number): SendResult | null;
54
+ /** Get mailbox metrics snapshot */
55
+ metrics(): MailboxMetrics;
56
+ /** Run cleanup: expire messages, requeue timed-out, move exhausted to DLQ */
57
+ cleanup(): {
58
+ expired: number;
59
+ requeued: number;
60
+ deadLettered: number;
61
+ };
62
+ /** Close the database and stop cleanup timer */
63
+ close(): void;
64
+ private checkRateLimit;
65
+ }
66
+ //# sourceMappingURL=mailbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailbox.d.ts","sourceRoot":"","sources":["../src/mailbox.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,OAAO,EACP,WAAW,EACX,UAAU,EACV,YAAY,EACZ,aAAa,EACb,MAAM,EACN,UAAU,EACV,cAAc,EACd,SAAS,EAEV,MAAM,YAAY,CAAC;AAyBpB,qBAAa,OAAO;IAClB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,OAAO,CAAC,YAAY,CAA+C;IAGnE,OAAO,CAAC,KAAK,CAA4C;gBAE7C,MAAM,CAAC,EAAE,aAAa;IAalC,OAAO,CAAC,iBAAiB;IA2MzB,iCAAiC;IACjC,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU;IAoDnC,wCAAwC;IACxC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,UAAU;IAIpD,yCAAyC;IACzC,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,EAAE;IAoBxC,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIjC,2DAA2D;IAC3D,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,UAAU,GAAG,IAAI;IAoBhH,oDAAoD;IACpD,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG;QAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE;IAY3E,gCAAgC;IAChC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,EAAE;IAIhD,mCAAmC;IACnC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE;IAItC,iEAAiE;IAC3D,OAAO,CACX,IAAI,EAAE,WAAW,GAAG;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAiCvF,iCAAiC;IACjC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAIhD,iCAAiC;IACjC,UAAU,IAAI,SAAS,EAAE;IAMzB,4CAA4C;IAC5C,cAAc,CAAC,KAAK,SAAK,GAAG,UAAU,EAAE;IAIxC,0DAA0D;IAC1D,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAkBlD,mCAAmC;IACnC,OAAO,IAAI,cAAc;IA2BzB,6EAA6E;IAC7E,OAAO,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE;IAqBtE,gDAAgD;IAChD,KAAK,IAAI,IAAI;IAUb,OAAO,CAAC,cAAc;CAUvB"}