agent-inbox 0.0.1 → 0.1.1

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.
Files changed (126) hide show
  1. package/CLAUDE.md +113 -0
  2. package/README.md +195 -1
  3. package/dist/federation/address.d.ts +24 -0
  4. package/dist/federation/address.d.ts.map +1 -0
  5. package/dist/federation/address.js +54 -0
  6. package/dist/federation/address.js.map +1 -0
  7. package/dist/federation/connection-manager.d.ts +118 -0
  8. package/dist/federation/connection-manager.d.ts.map +1 -0
  9. package/dist/federation/connection-manager.js +369 -0
  10. package/dist/federation/connection-manager.js.map +1 -0
  11. package/dist/federation/delivery-queue.d.ts +66 -0
  12. package/dist/federation/delivery-queue.d.ts.map +1 -0
  13. package/dist/federation/delivery-queue.js +199 -0
  14. package/dist/federation/delivery-queue.js.map +1 -0
  15. package/dist/federation/index.d.ts +7 -0
  16. package/dist/federation/index.d.ts.map +1 -0
  17. package/dist/federation/index.js +6 -0
  18. package/dist/federation/index.js.map +1 -0
  19. package/dist/federation/routing-engine.d.ts +74 -0
  20. package/dist/federation/routing-engine.d.ts.map +1 -0
  21. package/dist/federation/routing-engine.js +158 -0
  22. package/dist/federation/routing-engine.js.map +1 -0
  23. package/dist/federation/trust.d.ts +39 -0
  24. package/dist/federation/trust.d.ts.map +1 -0
  25. package/dist/federation/trust.js +64 -0
  26. package/dist/federation/trust.js.map +1 -0
  27. package/dist/index.d.ts +60 -2
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +217 -18
  30. package/dist/index.js.map +1 -1
  31. package/dist/ipc/ipc-server.d.ts +20 -0
  32. package/dist/ipc/ipc-server.d.ts.map +1 -0
  33. package/dist/ipc/ipc-server.js +152 -0
  34. package/dist/ipc/ipc-server.js.map +1 -0
  35. package/dist/jsonrpc/mail-server.d.ts +45 -0
  36. package/dist/jsonrpc/mail-server.d.ts.map +1 -0
  37. package/dist/jsonrpc/mail-server.js +284 -0
  38. package/dist/jsonrpc/mail-server.js.map +1 -0
  39. package/dist/map/map-client.d.ts +91 -0
  40. package/dist/map/map-client.d.ts.map +1 -0
  41. package/dist/map/map-client.js +202 -0
  42. package/dist/map/map-client.js.map +1 -0
  43. package/dist/mcp/mcp-server.d.ts +23 -0
  44. package/dist/mcp/mcp-server.d.ts.map +1 -0
  45. package/dist/mcp/mcp-server.js +226 -0
  46. package/dist/mcp/mcp-server.js.map +1 -0
  47. package/dist/push/notifier.d.ts +49 -0
  48. package/dist/push/notifier.d.ts.map +1 -0
  49. package/dist/push/notifier.js +150 -0
  50. package/dist/push/notifier.js.map +1 -0
  51. package/dist/registry/warm-registry.d.ts +63 -0
  52. package/dist/registry/warm-registry.d.ts.map +1 -0
  53. package/dist/registry/warm-registry.js +173 -0
  54. package/dist/registry/warm-registry.js.map +1 -0
  55. package/dist/router/message-router.d.ts +44 -0
  56. package/dist/router/message-router.d.ts.map +1 -0
  57. package/dist/router/message-router.js +137 -0
  58. package/dist/router/message-router.js.map +1 -0
  59. package/dist/storage/interface.d.ts +31 -0
  60. package/dist/storage/interface.d.ts.map +1 -0
  61. package/dist/storage/interface.js +2 -0
  62. package/dist/storage/interface.js.map +1 -0
  63. package/dist/storage/memory.d.ts +28 -0
  64. package/dist/storage/memory.d.ts.map +1 -0
  65. package/dist/storage/memory.js +118 -0
  66. package/dist/storage/memory.js.map +1 -0
  67. package/dist/storage/sqlite.d.ts +35 -0
  68. package/dist/storage/sqlite.d.ts.map +1 -0
  69. package/dist/storage/sqlite.js +445 -0
  70. package/dist/storage/sqlite.js.map +1 -0
  71. package/dist/traceability/traceability.d.ts +29 -0
  72. package/dist/traceability/traceability.d.ts.map +1 -0
  73. package/dist/traceability/traceability.js +150 -0
  74. package/dist/traceability/traceability.js.map +1 -0
  75. package/dist/types.d.ts +253 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +3 -0
  78. package/dist/types.js.map +1 -0
  79. package/docs/DESIGN.md +1156 -0
  80. package/docs/PLAN.md +545 -0
  81. package/hooks/inbox-hook.mjs +119 -0
  82. package/hooks/register-hook.mjs +69 -0
  83. package/package.json +33 -25
  84. package/rules/agent-inbox.md +78 -0
  85. package/src/federation/address.ts +61 -0
  86. package/src/federation/connection-manager.ts +458 -0
  87. package/src/federation/delivery-queue.ts +222 -0
  88. package/src/federation/index.ts +6 -0
  89. package/src/federation/routing-engine.ts +188 -0
  90. package/src/federation/trust.ts +71 -0
  91. package/src/index.ts +299 -0
  92. package/src/ipc/ipc-server.ts +180 -0
  93. package/src/jsonrpc/mail-server.ts +356 -0
  94. package/src/map/map-client.ts +260 -0
  95. package/src/mcp/mcp-server.ts +272 -0
  96. package/src/push/notifier.ts +192 -0
  97. package/src/registry/warm-registry.ts +210 -0
  98. package/src/router/message-router.ts +175 -0
  99. package/src/storage/interface.ts +48 -0
  100. package/src/storage/memory.ts +145 -0
  101. package/src/storage/sqlite.ts +645 -0
  102. package/src/traceability/traceability.ts +183 -0
  103. package/src/types.ts +287 -0
  104. package/test/federation/address.test.ts +101 -0
  105. package/test/federation/connection-manager.test.ts +546 -0
  106. package/test/federation/delivery-queue.test.ts +159 -0
  107. package/test/federation/integration.test.ts +823 -0
  108. package/test/federation/routing-engine.test.ts +117 -0
  109. package/test/federation/sdk-integration.test.ts +748 -0
  110. package/test/federation/trust.test.ts +89 -0
  111. package/test/ipc-jsonrpc.test.ts +113 -0
  112. package/test/ipc-server.test.ts +138 -0
  113. package/test/mail-server.test.ts +208 -0
  114. package/test/map-client.test.ts +408 -0
  115. package/test/message-router.test.ts +184 -0
  116. package/test/push-notifier.test.ts +139 -0
  117. package/test/registry/warm-registry.test.ts +171 -0
  118. package/test/sqlite-storage.test.ts +243 -0
  119. package/test/storage.test.ts +196 -0
  120. package/test/traceability.test.ts +123 -0
  121. package/tsconfig.json +20 -0
  122. package/tsup.config.ts +10 -0
  123. package/vitest.config.ts +8 -0
  124. package/dist/index.d.mts +0 -2
  125. package/dist/index.mjs +0 -1
  126. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,645 @@
1
+ import Database from "better-sqlite3";
2
+ import type {
3
+ Agent,
4
+ Message,
5
+ Conversation,
6
+ Turn,
7
+ Thread,
8
+ Recipient,
9
+ MessageContent,
10
+ Participant,
11
+ } from "../types.js";
12
+ import type { Storage, InboxQuery, ThreadQuery } from "./interface.js";
13
+
14
+ export interface SqliteStorageOptions {
15
+ path: string; // ":memory:" for in-memory, or file path
16
+ }
17
+
18
+ export class SqliteStorage implements Storage {
19
+ private db: Database.Database;
20
+
21
+ constructor(opts: SqliteStorageOptions) {
22
+ this.db = new Database(opts.path);
23
+ this.db.pragma("journal_mode = WAL");
24
+ this.db.pragma("foreign_keys = ON");
25
+ this.migrate();
26
+ }
27
+
28
+ private migrate(): void {
29
+ this.db.exec(`
30
+ CREATE TABLE IF NOT EXISTS agents (
31
+ agent_id TEXT PRIMARY KEY,
32
+ display_name TEXT,
33
+ program TEXT,
34
+ model TEXT,
35
+ scope TEXT NOT NULL DEFAULT 'default',
36
+ status TEXT NOT NULL DEFAULT 'active',
37
+ metadata TEXT NOT NULL DEFAULT '{}',
38
+ registered_at TEXT NOT NULL,
39
+ last_active_at TEXT NOT NULL
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS messages (
43
+ id TEXT PRIMARY KEY,
44
+ scope TEXT NOT NULL DEFAULT 'default',
45
+ sender_id TEXT NOT NULL,
46
+ subject TEXT,
47
+ content TEXT NOT NULL,
48
+ thread_tag TEXT,
49
+ in_reply_to TEXT,
50
+ conversation_id TEXT,
51
+ importance TEXT NOT NULL DEFAULT 'normal',
52
+ metadata TEXT NOT NULL DEFAULT '{}',
53
+ created_at TEXT NOT NULL
54
+ );
55
+
56
+ CREATE TABLE IF NOT EXISTS recipients (
57
+ message_id TEXT NOT NULL,
58
+ agent_id TEXT NOT NULL,
59
+ kind TEXT NOT NULL DEFAULT 'to',
60
+ delivered_at TEXT,
61
+ read_at TEXT,
62
+ ack_at TEXT,
63
+ PRIMARY KEY (message_id, agent_id),
64
+ FOREIGN KEY (message_id) REFERENCES messages(id)
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS conversations (
68
+ id TEXT PRIMARY KEY,
69
+ scope TEXT NOT NULL DEFAULT 'default',
70
+ subject TEXT,
71
+ status TEXT NOT NULL DEFAULT 'active',
72
+ metadata TEXT NOT NULL DEFAULT '{}',
73
+ created_at TEXT NOT NULL,
74
+ updated_at TEXT NOT NULL
75
+ );
76
+
77
+ CREATE TABLE IF NOT EXISTS participants (
78
+ conversation_id TEXT NOT NULL,
79
+ agent_id TEXT NOT NULL,
80
+ role TEXT,
81
+ joined_at TEXT NOT NULL,
82
+ PRIMARY KEY (conversation_id, agent_id),
83
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id)
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS turns (
87
+ id TEXT PRIMARY KEY,
88
+ conversation_id TEXT NOT NULL,
89
+ participant_id TEXT NOT NULL,
90
+ source_message_id TEXT,
91
+ content_type TEXT NOT NULL,
92
+ content TEXT NOT NULL,
93
+ thread_id TEXT,
94
+ in_reply_to TEXT,
95
+ created_at TEXT NOT NULL,
96
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id)
97
+ );
98
+
99
+ CREATE TABLE IF NOT EXISTS threads (
100
+ id TEXT PRIMARY KEY,
101
+ conversation_id TEXT NOT NULL,
102
+ root_turn_id TEXT NOT NULL,
103
+ parent_thread_id TEXT,
104
+ subject TEXT,
105
+ created_at TEXT NOT NULL,
106
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id)
107
+ );
108
+
109
+ -- Indexes for common queries
110
+ CREATE INDEX IF NOT EXISTS idx_messages_scope ON messages(scope);
111
+ CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id);
112
+ CREATE INDEX IF NOT EXISTS idx_messages_thread_tag ON messages(thread_tag, scope);
113
+ CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
114
+ CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at);
115
+ CREATE INDEX IF NOT EXISTS idx_recipients_agent ON recipients(agent_id);
116
+ CREATE INDEX IF NOT EXISTS idx_turns_conversation ON turns(conversation_id);
117
+ CREATE INDEX IF NOT EXISTS idx_agents_scope ON agents(scope);
118
+
119
+ -- FTS5 virtual table for full-text search
120
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
121
+ id UNINDEXED,
122
+ subject,
123
+ text_content,
124
+ content='messages',
125
+ content_rowid='rowid'
126
+ );
127
+
128
+ -- Triggers to keep FTS in sync
129
+ CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
130
+ INSERT INTO messages_fts(rowid, id, subject, text_content)
131
+ VALUES (NEW.rowid, NEW.id, NEW.subject,
132
+ CASE WHEN json_extract(NEW.content, '$.type') = 'text'
133
+ THEN json_extract(NEW.content, '$.text')
134
+ ELSE '' END);
135
+ END;
136
+
137
+ CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
138
+ INSERT INTO messages_fts(messages_fts, rowid, id, subject, text_content)
139
+ VALUES ('delete', OLD.rowid, OLD.id, OLD.subject,
140
+ CASE WHEN json_extract(OLD.content, '$.type') = 'text'
141
+ THEN json_extract(OLD.content, '$.text')
142
+ ELSE '' END);
143
+ END;
144
+
145
+ CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
146
+ INSERT INTO messages_fts(messages_fts, rowid, id, subject, text_content)
147
+ VALUES ('delete', OLD.rowid, OLD.id, OLD.subject,
148
+ CASE WHEN json_extract(OLD.content, '$.type') = 'text'
149
+ THEN json_extract(OLD.content, '$.text')
150
+ ELSE '' END);
151
+ INSERT INTO messages_fts(rowid, id, subject, text_content)
152
+ VALUES (NEW.rowid, NEW.id, NEW.subject,
153
+ CASE WHEN json_extract(NEW.content, '$.type') = 'text'
154
+ THEN json_extract(NEW.content, '$.text')
155
+ ELSE '' END);
156
+ END;
157
+ `);
158
+ }
159
+
160
+ // --- Agents ---
161
+
162
+ getAgent(agentId: string): Agent | undefined {
163
+ const row = this.db
164
+ .prepare("SELECT * FROM agents WHERE agent_id = ?")
165
+ .get(agentId) as AgentRow | undefined;
166
+ return row ? rowToAgent(row) : undefined;
167
+ }
168
+
169
+ putAgent(agent: Agent): void {
170
+ this.db
171
+ .prepare(
172
+ `INSERT OR REPLACE INTO agents
173
+ (agent_id, display_name, program, model, scope, status, metadata, registered_at, last_active_at)
174
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
175
+ )
176
+ .run(
177
+ agent.agent_id,
178
+ agent.display_name ?? null,
179
+ agent.program ?? null,
180
+ agent.model ?? null,
181
+ agent.scope,
182
+ agent.status,
183
+ JSON.stringify(agent.metadata),
184
+ agent.registered_at,
185
+ agent.last_active_at
186
+ );
187
+ }
188
+
189
+ listAgents(scope?: string): Agent[] {
190
+ if (scope) {
191
+ return (
192
+ this.db.prepare("SELECT * FROM agents WHERE scope = ?").all(scope) as AgentRow[]
193
+ ).map(rowToAgent);
194
+ }
195
+ return (this.db.prepare("SELECT * FROM agents").all() as AgentRow[]).map(rowToAgent);
196
+ }
197
+
198
+ removeAgent(agentId: string): boolean {
199
+ const result = this.db
200
+ .prepare("DELETE FROM agents WHERE agent_id = ?")
201
+ .run(agentId);
202
+ return result.changes > 0;
203
+ }
204
+
205
+ // --- Messages ---
206
+
207
+ getMessage(id: string): Message | undefined {
208
+ const row = this.db
209
+ .prepare("SELECT * FROM messages WHERE id = ?")
210
+ .get(id) as MessageRow | undefined;
211
+ if (!row) return undefined;
212
+ return this.rowToMessage(row);
213
+ }
214
+
215
+ putMessage(message: Message): Message {
216
+ const upsertMsg = this.db.prepare(
217
+ `INSERT OR REPLACE INTO messages
218
+ (id, scope, sender_id, subject, content, thread_tag, in_reply_to, conversation_id, importance, metadata, created_at)
219
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
220
+ );
221
+ const upsertRecipient = this.db.prepare(
222
+ `INSERT OR REPLACE INTO recipients
223
+ (message_id, agent_id, kind, delivered_at, read_at, ack_at)
224
+ VALUES (?, ?, ?, ?, ?, ?)`
225
+ );
226
+
227
+ this.db.transaction(() => {
228
+ upsertMsg.run(
229
+ message.id,
230
+ message.scope,
231
+ message.sender_id,
232
+ message.subject ?? null,
233
+ JSON.stringify(message.content),
234
+ message.thread_tag ?? null,
235
+ message.in_reply_to ?? null,
236
+ message.conversation_id ?? null,
237
+ message.importance,
238
+ JSON.stringify(message.metadata),
239
+ message.created_at
240
+ );
241
+ // Delete old recipients and re-insert
242
+ this.db
243
+ .prepare("DELETE FROM recipients WHERE message_id = ?")
244
+ .run(message.id);
245
+ for (const r of message.recipients) {
246
+ upsertRecipient.run(
247
+ message.id,
248
+ r.agent_id,
249
+ r.kind,
250
+ r.delivered_at ?? null,
251
+ r.read_at ?? null,
252
+ r.ack_at ?? null
253
+ );
254
+ }
255
+ })();
256
+
257
+ return message;
258
+ }
259
+
260
+ getInbox(agentId: string, opts?: InboxQuery): Message[] {
261
+ let sql = `
262
+ SELECT m.* FROM messages m
263
+ JOIN recipients r ON r.message_id = m.id
264
+ WHERE r.agent_id = ?
265
+ `;
266
+ const params: unknown[] = [agentId];
267
+
268
+ if (opts?.unreadOnly) {
269
+ sql += " AND r.read_at IS NULL";
270
+ }
271
+ if (opts?.since) {
272
+ sql += " AND m.created_at >= ?";
273
+ params.push(opts.since);
274
+ }
275
+ sql += " ORDER BY m.created_at ASC";
276
+ if (opts?.limit) {
277
+ sql += " LIMIT ?";
278
+ params.push(opts.limit);
279
+ }
280
+
281
+ const rows = this.db.prepare(sql).all(...params) as MessageRow[];
282
+ return rows.map((row) => this.rowToMessage(row));
283
+ }
284
+
285
+ getThread(query: ThreadQuery): Message[] {
286
+ const rows = this.db
287
+ .prepare(
288
+ "SELECT * FROM messages WHERE thread_tag = ? AND scope = ? ORDER BY created_at ASC"
289
+ )
290
+ .all(query.threadTag, query.scope) as MessageRow[];
291
+ return rows.map((row) => this.rowToMessage(row));
292
+ }
293
+
294
+ getSentMessages(agentId: string, limit?: number): Message[] {
295
+ let sql = "SELECT * FROM messages WHERE sender_id = ? ORDER BY created_at ASC";
296
+ const params: unknown[] = [agentId];
297
+ if (limit) {
298
+ sql += " LIMIT ?";
299
+ params.push(limit);
300
+ }
301
+ const rows = this.db.prepare(sql).all(...params) as MessageRow[];
302
+ return rows.map((row) => this.rowToMessage(row));
303
+ }
304
+
305
+ searchMessages(query: string, scope?: string): Message[] {
306
+ let sql: string;
307
+ let params: unknown[];
308
+
309
+ if (scope) {
310
+ sql = `
311
+ SELECT m.* FROM messages m
312
+ JOIN messages_fts fts ON fts.id = m.id
313
+ WHERE messages_fts MATCH ? AND m.scope = ?
314
+ ORDER BY m.created_at ASC
315
+ `;
316
+ params = [query, scope];
317
+ } else {
318
+ sql = `
319
+ SELECT m.* FROM messages m
320
+ JOIN messages_fts fts ON fts.id = m.id
321
+ WHERE messages_fts MATCH ?
322
+ ORDER BY m.created_at ASC
323
+ `;
324
+ params = [query];
325
+ }
326
+
327
+ try {
328
+ const rows = this.db.prepare(sql).all(...params) as MessageRow[];
329
+ return rows.map((row) => this.rowToMessage(row));
330
+ } catch {
331
+ // FTS query syntax error — fall back to LIKE
332
+ return this.searchMessagesLike(query, scope);
333
+ }
334
+ }
335
+
336
+ private searchMessagesLike(query: string, scope?: string): Message[] {
337
+ const pattern = `%${query}%`;
338
+ let sql = `
339
+ SELECT * FROM messages
340
+ WHERE (subject LIKE ? OR content LIKE ?)
341
+ `;
342
+ const params: unknown[] = [pattern, pattern];
343
+ if (scope) {
344
+ sql += " AND scope = ?";
345
+ params.push(scope);
346
+ }
347
+ sql += " ORDER BY created_at ASC";
348
+ const rows = this.db.prepare(sql).all(...params) as MessageRow[];
349
+ return rows.map((row) => this.rowToMessage(row));
350
+ }
351
+
352
+ // --- Conversations ---
353
+
354
+ getConversation(id: string): Conversation | undefined {
355
+ const row = this.db
356
+ .prepare("SELECT * FROM conversations WHERE id = ?")
357
+ .get(id) as ConversationRow | undefined;
358
+ if (!row) return undefined;
359
+ return this.rowToConversation(row);
360
+ }
361
+
362
+ putConversation(conversation: Conversation): Conversation {
363
+ this.db.transaction(() => {
364
+ this.db
365
+ .prepare(
366
+ `INSERT OR REPLACE INTO conversations
367
+ (id, scope, subject, status, metadata, created_at, updated_at)
368
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
369
+ )
370
+ .run(
371
+ conversation.id,
372
+ conversation.scope,
373
+ conversation.subject ?? null,
374
+ conversation.status,
375
+ JSON.stringify(conversation.metadata),
376
+ conversation.created_at,
377
+ conversation.updated_at
378
+ );
379
+ // Re-sync participants
380
+ this.db
381
+ .prepare("DELETE FROM participants WHERE conversation_id = ?")
382
+ .run(conversation.id);
383
+ const insertP = this.db.prepare(
384
+ `INSERT INTO participants (conversation_id, agent_id, role, joined_at) VALUES (?, ?, ?, ?)`
385
+ );
386
+ for (const p of conversation.participants) {
387
+ insertP.run(conversation.id, p.agent_id, p.role ?? null, p.joined_at);
388
+ }
389
+ })();
390
+ return conversation;
391
+ }
392
+
393
+ listConversations(scope?: string): Conversation[] {
394
+ let rows: ConversationRow[];
395
+ if (scope) {
396
+ rows = this.db
397
+ .prepare("SELECT * FROM conversations WHERE scope = ? ORDER BY updated_at DESC")
398
+ .all(scope) as ConversationRow[];
399
+ } else {
400
+ rows = this.db
401
+ .prepare("SELECT * FROM conversations ORDER BY updated_at DESC")
402
+ .all() as ConversationRow[];
403
+ }
404
+ return rows.map((row) => this.rowToConversation(row));
405
+ }
406
+
407
+ // --- Turns ---
408
+
409
+ addTurn(turn: Turn): void {
410
+ this.db
411
+ .prepare(
412
+ `INSERT INTO turns
413
+ (id, conversation_id, participant_id, source_message_id, content_type, content, thread_id, in_reply_to, created_at)
414
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
415
+ )
416
+ .run(
417
+ turn.id,
418
+ turn.conversation_id,
419
+ turn.participant_id,
420
+ turn.source_message_id ?? null,
421
+ turn.content_type,
422
+ JSON.stringify(turn.content),
423
+ turn.thread_id ?? null,
424
+ turn.in_reply_to ?? null,
425
+ turn.created_at
426
+ );
427
+ }
428
+
429
+ getTurns(conversationId: string): Turn[] {
430
+ const rows = this.db
431
+ .prepare(
432
+ "SELECT * FROM turns WHERE conversation_id = ? ORDER BY created_at ASC"
433
+ )
434
+ .all(conversationId) as TurnRow[];
435
+ return rows.map(rowToTurn);
436
+ }
437
+
438
+ // --- Threads ---
439
+
440
+ getThread2(id: string): Thread | undefined {
441
+ const row = this.db
442
+ .prepare("SELECT * FROM threads WHERE id = ?")
443
+ .get(id) as ThreadRow | undefined;
444
+ return row ? rowToThread(row) : undefined;
445
+ }
446
+
447
+ putThread(thread: Thread): Thread {
448
+ this.db
449
+ .prepare(
450
+ `INSERT OR REPLACE INTO threads
451
+ (id, conversation_id, root_turn_id, parent_thread_id, subject, created_at)
452
+ VALUES (?, ?, ?, ?, ?, ?)`
453
+ )
454
+ .run(
455
+ thread.id,
456
+ thread.conversation_id,
457
+ thread.root_turn_id,
458
+ thread.parent_thread_id ?? null,
459
+ thread.subject ?? null,
460
+ thread.created_at
461
+ );
462
+ return thread;
463
+ }
464
+
465
+ getThreadsByConversation(conversationId: string): Thread[] {
466
+ const rows = this.db
467
+ .prepare("SELECT * FROM threads WHERE conversation_id = ?")
468
+ .all(conversationId) as ThreadRow[];
469
+ return rows.map(rowToThread);
470
+ }
471
+
472
+ // --- Helpers ---
473
+
474
+ private getRecipients(messageId: string): Recipient[] {
475
+ const rows = this.db
476
+ .prepare("SELECT * FROM recipients WHERE message_id = ?")
477
+ .all(messageId) as RecipientRow[];
478
+ return rows.map((r) => ({
479
+ agent_id: r.agent_id,
480
+ kind: r.kind as Recipient["kind"],
481
+ delivered_at: r.delivered_at ?? undefined,
482
+ read_at: r.read_at ?? undefined,
483
+ ack_at: r.ack_at ?? undefined,
484
+ }));
485
+ }
486
+
487
+ private getParticipants(conversationId: string): Participant[] {
488
+ const rows = this.db
489
+ .prepare("SELECT * FROM participants WHERE conversation_id = ?")
490
+ .all(conversationId) as ParticipantRow[];
491
+ return rows.map((r) => ({
492
+ agent_id: r.agent_id,
493
+ role: r.role ?? undefined,
494
+ joined_at: r.joined_at,
495
+ }));
496
+ }
497
+
498
+ private rowToMessage(row: MessageRow): Message {
499
+ return {
500
+ id: row.id,
501
+ scope: row.scope,
502
+ sender_id: row.sender_id,
503
+ recipients: this.getRecipients(row.id),
504
+ subject: row.subject ?? undefined,
505
+ content: JSON.parse(row.content) as MessageContent,
506
+ thread_tag: row.thread_tag ?? undefined,
507
+ in_reply_to: row.in_reply_to ?? undefined,
508
+ conversation_id: row.conversation_id ?? undefined,
509
+ importance: row.importance as Message["importance"],
510
+ metadata: JSON.parse(row.metadata),
511
+ created_at: row.created_at,
512
+ };
513
+ }
514
+
515
+ private rowToConversation(row: ConversationRow): Conversation {
516
+ return {
517
+ id: row.id,
518
+ scope: row.scope,
519
+ subject: row.subject ?? undefined,
520
+ status: row.status as Conversation["status"],
521
+ participants: this.getParticipants(row.id),
522
+ metadata: JSON.parse(row.metadata),
523
+ created_at: row.created_at,
524
+ updated_at: row.updated_at,
525
+ };
526
+ }
527
+
528
+ close(): void {
529
+ this.db.close();
530
+ }
531
+ }
532
+
533
+ // --- Row types ---
534
+
535
+ interface AgentRow {
536
+ agent_id: string;
537
+ display_name: string | null;
538
+ program: string | null;
539
+ model: string | null;
540
+ scope: string;
541
+ status: string;
542
+ metadata: string;
543
+ registered_at: string;
544
+ last_active_at: string;
545
+ }
546
+
547
+ interface MessageRow {
548
+ id: string;
549
+ scope: string;
550
+ sender_id: string;
551
+ subject: string | null;
552
+ content: string;
553
+ thread_tag: string | null;
554
+ in_reply_to: string | null;
555
+ conversation_id: string | null;
556
+ importance: string;
557
+ metadata: string;
558
+ created_at: string;
559
+ }
560
+
561
+ interface RecipientRow {
562
+ message_id: string;
563
+ agent_id: string;
564
+ kind: string;
565
+ delivered_at: string | null;
566
+ read_at: string | null;
567
+ ack_at: string | null;
568
+ }
569
+
570
+ interface ConversationRow {
571
+ id: string;
572
+ scope: string;
573
+ subject: string | null;
574
+ status: string;
575
+ metadata: string;
576
+ created_at: string;
577
+ updated_at: string;
578
+ }
579
+
580
+ interface ParticipantRow {
581
+ conversation_id: string;
582
+ agent_id: string;
583
+ role: string | null;
584
+ joined_at: string;
585
+ }
586
+
587
+ interface TurnRow {
588
+ id: string;
589
+ conversation_id: string;
590
+ participant_id: string;
591
+ source_message_id: string | null;
592
+ content_type: string;
593
+ content: string;
594
+ thread_id: string | null;
595
+ in_reply_to: string | null;
596
+ created_at: string;
597
+ }
598
+
599
+ interface ThreadRow {
600
+ id: string;
601
+ conversation_id: string;
602
+ root_turn_id: string;
603
+ parent_thread_id: string | null;
604
+ subject: string | null;
605
+ created_at: string;
606
+ }
607
+
608
+ function rowToAgent(row: AgentRow): Agent {
609
+ return {
610
+ agent_id: row.agent_id,
611
+ display_name: row.display_name ?? undefined,
612
+ program: row.program ?? undefined,
613
+ model: row.model ?? undefined,
614
+ scope: row.scope,
615
+ status: row.status as Agent["status"],
616
+ metadata: JSON.parse(row.metadata),
617
+ registered_at: row.registered_at,
618
+ last_active_at: row.last_active_at,
619
+ };
620
+ }
621
+
622
+ function rowToTurn(row: TurnRow): Turn {
623
+ return {
624
+ id: row.id,
625
+ conversation_id: row.conversation_id,
626
+ participant_id: row.participant_id,
627
+ source_message_id: row.source_message_id ?? undefined,
628
+ content_type: row.content_type,
629
+ content: JSON.parse(row.content) as MessageContent,
630
+ thread_id: row.thread_id ?? undefined,
631
+ in_reply_to: row.in_reply_to ?? undefined,
632
+ created_at: row.created_at,
633
+ };
634
+ }
635
+
636
+ function rowToThread(row: ThreadRow): Thread {
637
+ return {
638
+ id: row.id,
639
+ conversation_id: row.conversation_id,
640
+ root_turn_id: row.root_turn_id,
641
+ parent_thread_id: row.parent_thread_id ?? undefined,
642
+ subject: row.subject ?? undefined,
643
+ created_at: row.created_at,
644
+ };
645
+ }