swarm-mail 0.1.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/README.md +201 -0
- package/package.json +28 -0
- package/src/adapter.ts +306 -0
- package/src/index.ts +57 -0
- package/src/pglite.ts +189 -0
- package/src/streams/agent-mail.test.ts +777 -0
- package/src/streams/agent-mail.ts +535 -0
- package/src/streams/debug.test.ts +500 -0
- package/src/streams/debug.ts +727 -0
- package/src/streams/effect/ask.integration.test.ts +314 -0
- package/src/streams/effect/ask.ts +202 -0
- package/src/streams/effect/cursor.integration.test.ts +418 -0
- package/src/streams/effect/cursor.ts +288 -0
- package/src/streams/effect/deferred.test.ts +357 -0
- package/src/streams/effect/deferred.ts +445 -0
- package/src/streams/effect/index.ts +17 -0
- package/src/streams/effect/layers.ts +73 -0
- package/src/streams/effect/lock.test.ts +385 -0
- package/src/streams/effect/lock.ts +399 -0
- package/src/streams/effect/mailbox.test.ts +260 -0
- package/src/streams/effect/mailbox.ts +318 -0
- package/src/streams/events.test.ts +924 -0
- package/src/streams/events.ts +329 -0
- package/src/streams/index.test.ts +229 -0
- package/src/streams/index.ts +578 -0
- package/src/streams/migrations.test.ts +359 -0
- package/src/streams/migrations.ts +362 -0
- package/src/streams/projections.test.ts +611 -0
- package/src/streams/projections.ts +564 -0
- package/src/streams/store.integration.test.ts +658 -0
- package/src/streams/store.ts +1129 -0
- package/src/streams/swarm-mail.ts +552 -0
- package/src/types/adapter.ts +392 -0
- package/src/types/database.ts +127 -0
- package/src/types/index.ts +26 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Mail - Embedded event-sourced implementation
|
|
3
|
+
*
|
|
4
|
+
* Replaces the MCP-based agent-mail with embedded PGLite storage.
|
|
5
|
+
* Same API surface, but no external server dependency.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Event sourcing for full audit trail
|
|
9
|
+
* - Offset-based resumability (Durable Streams inspired)
|
|
10
|
+
* - Materialized views for fast queries
|
|
11
|
+
* - File reservation with conflict detection
|
|
12
|
+
*
|
|
13
|
+
* Effect-TS Integration:
|
|
14
|
+
* - DurableMailbox for message send/receive (envelope pattern)
|
|
15
|
+
* - DurableCursor for positioned inbox consumption with checkpointing
|
|
16
|
+
* - DurableLock for file reservations (mutual exclusion via CAS)
|
|
17
|
+
* - DurableDeferred for request/response messaging
|
|
18
|
+
*/
|
|
19
|
+
import { createEvent } from "./events";
|
|
20
|
+
import { isDatabaseHealthy, getDatabaseStats } from "./index";
|
|
21
|
+
import {
|
|
22
|
+
checkConflicts,
|
|
23
|
+
getActiveReservations,
|
|
24
|
+
getInbox,
|
|
25
|
+
getMessage,
|
|
26
|
+
} from "./projections";
|
|
27
|
+
import { appendEvent, registerAgent, reserveFiles, sendMessage } from "./store";
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Constants
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const MAX_INBOX_LIMIT = 5; // HARD CAP - context preservation
|
|
34
|
+
const DEFAULT_TTL_SECONDS = 3600; // 1 hour
|
|
35
|
+
|
|
36
|
+
// Agent name generation
|
|
37
|
+
const ADJECTIVES = [
|
|
38
|
+
"Blue",
|
|
39
|
+
"Red",
|
|
40
|
+
"Green",
|
|
41
|
+
"Gold",
|
|
42
|
+
"Silver",
|
|
43
|
+
"Swift",
|
|
44
|
+
"Bright",
|
|
45
|
+
"Dark",
|
|
46
|
+
"Calm",
|
|
47
|
+
"Bold",
|
|
48
|
+
"Wise",
|
|
49
|
+
"Quick",
|
|
50
|
+
"Warm",
|
|
51
|
+
"Cool",
|
|
52
|
+
"Pure",
|
|
53
|
+
"Wild",
|
|
54
|
+
];
|
|
55
|
+
const NOUNS = [
|
|
56
|
+
"Lake",
|
|
57
|
+
"Stone",
|
|
58
|
+
"River",
|
|
59
|
+
"Mountain",
|
|
60
|
+
"Forest",
|
|
61
|
+
"Ocean",
|
|
62
|
+
"Star",
|
|
63
|
+
"Moon",
|
|
64
|
+
"Wind",
|
|
65
|
+
"Fire",
|
|
66
|
+
"Cloud",
|
|
67
|
+
"Storm",
|
|
68
|
+
"Dawn",
|
|
69
|
+
"Dusk",
|
|
70
|
+
"Hawk",
|
|
71
|
+
"Wolf",
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function generateSwarmAgentName(): string {
|
|
75
|
+
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
|
76
|
+
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
77
|
+
return `${adj}${noun}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Types
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
export interface SwarmMailContext {
|
|
85
|
+
projectKey: string;
|
|
86
|
+
agentName: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface InitSwarmAgentOptions {
|
|
90
|
+
projectPath: string;
|
|
91
|
+
agentName?: string;
|
|
92
|
+
program?: string;
|
|
93
|
+
model?: string;
|
|
94
|
+
taskDescription?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SendSwarmMessageOptions {
|
|
98
|
+
projectPath: string;
|
|
99
|
+
fromAgent: string;
|
|
100
|
+
toAgents: string[];
|
|
101
|
+
subject: string;
|
|
102
|
+
body: string;
|
|
103
|
+
threadId?: string;
|
|
104
|
+
importance?: "low" | "normal" | "high" | "urgent";
|
|
105
|
+
ackRequired?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SendSwarmMessageResult {
|
|
109
|
+
success: boolean;
|
|
110
|
+
messageId: number;
|
|
111
|
+
threadId?: string;
|
|
112
|
+
recipientCount: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface GetSwarmInboxOptions {
|
|
116
|
+
projectPath: string;
|
|
117
|
+
agentName: string;
|
|
118
|
+
limit?: number;
|
|
119
|
+
urgentOnly?: boolean;
|
|
120
|
+
unreadOnly?: boolean;
|
|
121
|
+
includeBodies?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface SwarmInboxMessage {
|
|
125
|
+
id: number;
|
|
126
|
+
from_agent: string;
|
|
127
|
+
subject: string;
|
|
128
|
+
body?: string;
|
|
129
|
+
thread_id: string | null;
|
|
130
|
+
importance: string;
|
|
131
|
+
created_at: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface SwarmInboxResult {
|
|
135
|
+
messages: SwarmInboxMessage[];
|
|
136
|
+
total: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ReadSwarmMessageOptions {
|
|
140
|
+
projectPath: string;
|
|
141
|
+
messageId: number;
|
|
142
|
+
agentName?: string;
|
|
143
|
+
markAsRead?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ReserveSwarmFilesOptions {
|
|
147
|
+
projectPath: string;
|
|
148
|
+
agentName: string;
|
|
149
|
+
paths: string[];
|
|
150
|
+
reason?: string;
|
|
151
|
+
exclusive?: boolean;
|
|
152
|
+
ttlSeconds?: number;
|
|
153
|
+
force?: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface GrantedSwarmReservation {
|
|
157
|
+
id: number;
|
|
158
|
+
path_pattern: string;
|
|
159
|
+
exclusive: boolean;
|
|
160
|
+
expiresAt: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface SwarmReservationConflict {
|
|
164
|
+
path: string;
|
|
165
|
+
holder: string;
|
|
166
|
+
pattern: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface ReserveSwarmFilesResult {
|
|
170
|
+
granted: GrantedSwarmReservation[];
|
|
171
|
+
conflicts: SwarmReservationConflict[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface ReleaseSwarmFilesOptions {
|
|
175
|
+
projectPath: string;
|
|
176
|
+
agentName: string;
|
|
177
|
+
paths?: string[];
|
|
178
|
+
reservationIds?: number[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface ReleaseSwarmFilesResult {
|
|
182
|
+
released: number;
|
|
183
|
+
releasedAt: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface AcknowledgeSwarmOptions {
|
|
187
|
+
projectPath: string;
|
|
188
|
+
messageId: number;
|
|
189
|
+
agentName: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface AcknowledgeSwarmResult {
|
|
193
|
+
acknowledged: boolean;
|
|
194
|
+
acknowledgedAt: string | null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface SwarmHealthResult {
|
|
198
|
+
healthy: boolean;
|
|
199
|
+
database: "connected" | "disconnected";
|
|
200
|
+
stats?: {
|
|
201
|
+
events: number;
|
|
202
|
+
agents: number;
|
|
203
|
+
messages: number;
|
|
204
|
+
reservations: number;
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// Agent Operations
|
|
210
|
+
// ============================================================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Initialize a swarm agent for this session
|
|
214
|
+
*
|
|
215
|
+
* Future: Can use DurableMailbox.create() for actor-style message consumption
|
|
216
|
+
*/
|
|
217
|
+
export async function initSwarmAgent(
|
|
218
|
+
options: InitSwarmAgentOptions,
|
|
219
|
+
): Promise<SwarmMailContext> {
|
|
220
|
+
const {
|
|
221
|
+
projectPath,
|
|
222
|
+
agentName = generateSwarmAgentName(),
|
|
223
|
+
program = "opencode",
|
|
224
|
+
model = "unknown",
|
|
225
|
+
taskDescription,
|
|
226
|
+
} = options;
|
|
227
|
+
|
|
228
|
+
// Register the agent (creates event + updates view)
|
|
229
|
+
await registerAgent(
|
|
230
|
+
projectPath, // Use projectPath as projectKey
|
|
231
|
+
agentName,
|
|
232
|
+
{ program, model, taskDescription },
|
|
233
|
+
projectPath,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
projectKey: projectPath,
|
|
238
|
+
agentName,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// Message Operations
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Send a message to other swarm agents
|
|
248
|
+
*
|
|
249
|
+
* Future: Use DurableMailbox.send() for envelope pattern with replyTo support
|
|
250
|
+
*/
|
|
251
|
+
export async function sendSwarmMessage(
|
|
252
|
+
options: SendSwarmMessageOptions,
|
|
253
|
+
): Promise<SendSwarmMessageResult> {
|
|
254
|
+
const {
|
|
255
|
+
projectPath,
|
|
256
|
+
fromAgent,
|
|
257
|
+
toAgents,
|
|
258
|
+
subject,
|
|
259
|
+
body,
|
|
260
|
+
threadId,
|
|
261
|
+
importance = "normal",
|
|
262
|
+
ackRequired = false,
|
|
263
|
+
} = options;
|
|
264
|
+
|
|
265
|
+
await sendMessage(
|
|
266
|
+
projectPath,
|
|
267
|
+
fromAgent,
|
|
268
|
+
toAgents,
|
|
269
|
+
subject,
|
|
270
|
+
body,
|
|
271
|
+
{ threadId, importance, ackRequired },
|
|
272
|
+
projectPath,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Get the message ID from the messages table (not the event ID)
|
|
276
|
+
const { getDatabase } = await import("./index");
|
|
277
|
+
const db = await getDatabase(projectPath);
|
|
278
|
+
const result = await db.query<{ id: number }>(
|
|
279
|
+
`SELECT id FROM messages
|
|
280
|
+
WHERE project_key = $1 AND from_agent = $2 AND subject = $3
|
|
281
|
+
ORDER BY created_at DESC LIMIT 1`,
|
|
282
|
+
[projectPath, fromAgent, subject],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const messageId = result.rows[0]?.id ?? 0;
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
success: true,
|
|
289
|
+
messageId,
|
|
290
|
+
threadId,
|
|
291
|
+
recipientCount: toAgents.length,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get inbox messages for a swarm agent
|
|
297
|
+
*
|
|
298
|
+
* Future: Use DurableCursor.consume() for positioned consumption with checkpointing
|
|
299
|
+
*/
|
|
300
|
+
export async function getSwarmInbox(
|
|
301
|
+
options: GetSwarmInboxOptions,
|
|
302
|
+
): Promise<SwarmInboxResult> {
|
|
303
|
+
const {
|
|
304
|
+
projectPath,
|
|
305
|
+
agentName,
|
|
306
|
+
limit = MAX_INBOX_LIMIT,
|
|
307
|
+
urgentOnly = false,
|
|
308
|
+
unreadOnly = false,
|
|
309
|
+
includeBodies = false,
|
|
310
|
+
} = options;
|
|
311
|
+
|
|
312
|
+
// Enforce max limit
|
|
313
|
+
const effectiveLimit = Math.min(limit, MAX_INBOX_LIMIT);
|
|
314
|
+
|
|
315
|
+
const messages = await getInbox(
|
|
316
|
+
projectPath,
|
|
317
|
+
agentName,
|
|
318
|
+
{
|
|
319
|
+
limit: effectiveLimit,
|
|
320
|
+
urgentOnly,
|
|
321
|
+
unreadOnly,
|
|
322
|
+
includeBodies,
|
|
323
|
+
},
|
|
324
|
+
projectPath,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
messages: messages.map((m) => ({
|
|
329
|
+
id: m.id,
|
|
330
|
+
from_agent: m.from_agent,
|
|
331
|
+
subject: m.subject,
|
|
332
|
+
body: includeBodies ? m.body : undefined,
|
|
333
|
+
thread_id: m.thread_id,
|
|
334
|
+
importance: m.importance,
|
|
335
|
+
created_at: m.created_at,
|
|
336
|
+
})),
|
|
337
|
+
total: messages.length,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Read a single message with full body
|
|
343
|
+
*/
|
|
344
|
+
export async function readSwarmMessage(
|
|
345
|
+
options: ReadSwarmMessageOptions,
|
|
346
|
+
): Promise<SwarmInboxMessage | null> {
|
|
347
|
+
const { projectPath, messageId, agentName, markAsRead = false } = options;
|
|
348
|
+
|
|
349
|
+
const message = await getMessage(projectPath, messageId, projectPath);
|
|
350
|
+
|
|
351
|
+
if (!message) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Mark as read if requested
|
|
356
|
+
if (markAsRead && agentName) {
|
|
357
|
+
await appendEvent(
|
|
358
|
+
createEvent("message_read", {
|
|
359
|
+
project_key: projectPath,
|
|
360
|
+
message_id: messageId,
|
|
361
|
+
agent_name: agentName,
|
|
362
|
+
}),
|
|
363
|
+
projectPath,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
id: message.id,
|
|
369
|
+
from_agent: message.from_agent,
|
|
370
|
+
subject: message.subject,
|
|
371
|
+
body: message.body,
|
|
372
|
+
thread_id: message.thread_id,
|
|
373
|
+
importance: message.importance,
|
|
374
|
+
created_at: message.created_at,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// Reservation Operations
|
|
380
|
+
// ============================================================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Reserve files for exclusive editing
|
|
384
|
+
*
|
|
385
|
+
* Always grants reservations (even with conflicts) - conflicts are warnings, not blockers.
|
|
386
|
+
* This matches the test expectations and allows agents to proceed with awareness.
|
|
387
|
+
*
|
|
388
|
+
* Future: Use DurableLock.acquire() for distributed mutex with automatic expiry
|
|
389
|
+
*/
|
|
390
|
+
export async function reserveSwarmFiles(
|
|
391
|
+
options: ReserveSwarmFilesOptions,
|
|
392
|
+
): Promise<ReserveSwarmFilesResult> {
|
|
393
|
+
const {
|
|
394
|
+
projectPath,
|
|
395
|
+
agentName,
|
|
396
|
+
paths,
|
|
397
|
+
reason,
|
|
398
|
+
exclusive = true,
|
|
399
|
+
ttlSeconds = DEFAULT_TTL_SECONDS,
|
|
400
|
+
} = options;
|
|
401
|
+
|
|
402
|
+
// Check for conflicts first
|
|
403
|
+
const conflicts = await checkConflicts(
|
|
404
|
+
projectPath,
|
|
405
|
+
agentName,
|
|
406
|
+
paths,
|
|
407
|
+
projectPath,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Always create reservations - conflicts are warnings, not blockers
|
|
411
|
+
await reserveFiles(
|
|
412
|
+
projectPath,
|
|
413
|
+
agentName,
|
|
414
|
+
paths,
|
|
415
|
+
{ reason, exclusive, ttlSeconds },
|
|
416
|
+
projectPath,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Query the actual reservation IDs from the database
|
|
420
|
+
const reservations = await getActiveReservations(
|
|
421
|
+
projectPath,
|
|
422
|
+
projectPath,
|
|
423
|
+
agentName,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Filter to just the paths we reserved (most recent ones)
|
|
427
|
+
const granted: GrantedSwarmReservation[] = reservations
|
|
428
|
+
.filter((r) => paths.includes(r.path_pattern))
|
|
429
|
+
.map((r) => ({
|
|
430
|
+
id: r.id,
|
|
431
|
+
path_pattern: r.path_pattern,
|
|
432
|
+
exclusive: r.exclusive,
|
|
433
|
+
expiresAt: r.expires_at,
|
|
434
|
+
}));
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
granted,
|
|
438
|
+
conflicts: conflicts.map((c) => ({
|
|
439
|
+
path: c.path,
|
|
440
|
+
holder: c.holder,
|
|
441
|
+
pattern: c.pattern,
|
|
442
|
+
})),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Release file reservations
|
|
448
|
+
*
|
|
449
|
+
* Future: Use DurableLock.release() for automatic cleanup
|
|
450
|
+
*/
|
|
451
|
+
export async function releaseSwarmFiles(
|
|
452
|
+
options: ReleaseSwarmFilesOptions,
|
|
453
|
+
): Promise<ReleaseSwarmFilesResult> {
|
|
454
|
+
const { projectPath, agentName, paths, reservationIds } = options;
|
|
455
|
+
|
|
456
|
+
// Get current reservations to count what we're releasing
|
|
457
|
+
const currentReservations = await getActiveReservations(
|
|
458
|
+
projectPath,
|
|
459
|
+
projectPath,
|
|
460
|
+
agentName,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
let releaseCount = 0;
|
|
464
|
+
|
|
465
|
+
if (paths && paths.length > 0) {
|
|
466
|
+
// Release specific paths
|
|
467
|
+
releaseCount = currentReservations.filter((r) =>
|
|
468
|
+
paths.includes(r.path_pattern),
|
|
469
|
+
).length;
|
|
470
|
+
} else if (reservationIds && reservationIds.length > 0) {
|
|
471
|
+
// Release by ID
|
|
472
|
+
releaseCount = currentReservations.filter((r) =>
|
|
473
|
+
reservationIds.includes(r.id),
|
|
474
|
+
).length;
|
|
475
|
+
} else {
|
|
476
|
+
// Release all
|
|
477
|
+
releaseCount = currentReservations.length;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Create release event
|
|
481
|
+
await appendEvent(
|
|
482
|
+
createEvent("file_released", {
|
|
483
|
+
project_key: projectPath,
|
|
484
|
+
agent_name: agentName,
|
|
485
|
+
paths,
|
|
486
|
+
reservation_ids: reservationIds,
|
|
487
|
+
}),
|
|
488
|
+
projectPath,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
released: releaseCount,
|
|
493
|
+
releasedAt: Date.now(),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ============================================================================
|
|
498
|
+
// Acknowledgement Operations
|
|
499
|
+
// ============================================================================
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Acknowledge a swarm message
|
|
503
|
+
*/
|
|
504
|
+
export async function acknowledgeSwarmMessage(
|
|
505
|
+
options: AcknowledgeSwarmOptions,
|
|
506
|
+
): Promise<AcknowledgeSwarmResult> {
|
|
507
|
+
const { projectPath, messageId, agentName } = options;
|
|
508
|
+
|
|
509
|
+
const timestamp = Date.now();
|
|
510
|
+
|
|
511
|
+
await appendEvent(
|
|
512
|
+
createEvent("message_acked", {
|
|
513
|
+
project_key: projectPath,
|
|
514
|
+
message_id: messageId,
|
|
515
|
+
agent_name: agentName,
|
|
516
|
+
}),
|
|
517
|
+
projectPath,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
acknowledged: true,
|
|
522
|
+
acknowledgedAt: new Date(timestamp).toISOString(),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ============================================================================
|
|
527
|
+
// Health Check
|
|
528
|
+
// ============================================================================
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Check if the swarm mail store is healthy
|
|
532
|
+
*/
|
|
533
|
+
export async function checkSwarmHealth(
|
|
534
|
+
projectPath?: string,
|
|
535
|
+
): Promise<SwarmHealthResult> {
|
|
536
|
+
const healthy = await isDatabaseHealthy(projectPath);
|
|
537
|
+
|
|
538
|
+
if (!healthy) {
|
|
539
|
+
return {
|
|
540
|
+
healthy: false,
|
|
541
|
+
database: "disconnected",
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const stats = await getDatabaseStats(projectPath);
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
healthy: true,
|
|
549
|
+
database: "connected",
|
|
550
|
+
stats,
|
|
551
|
+
};
|
|
552
|
+
}
|