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,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent 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
|
+
import { registerAgent, sendMessage, reserveFiles, appendEvent } from "./store";
|
|
14
|
+
import {
|
|
15
|
+
getAgents,
|
|
16
|
+
getAgent,
|
|
17
|
+
getInbox,
|
|
18
|
+
getMessage,
|
|
19
|
+
getActiveReservations,
|
|
20
|
+
checkConflicts,
|
|
21
|
+
} from "./projections";
|
|
22
|
+
import { createEvent } from "./events";
|
|
23
|
+
import { isDatabaseHealthy, getDatabaseStats } from "./index";
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Constants
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
const MAX_INBOX_LIMIT = 5; // HARD CAP - context preservation
|
|
30
|
+
const DEFAULT_TTL_SECONDS = 3600; // 1 hour
|
|
31
|
+
|
|
32
|
+
// Agent name generation
|
|
33
|
+
const ADJECTIVES = [
|
|
34
|
+
"Blue",
|
|
35
|
+
"Red",
|
|
36
|
+
"Green",
|
|
37
|
+
"Gold",
|
|
38
|
+
"Silver",
|
|
39
|
+
"Swift",
|
|
40
|
+
"Bright",
|
|
41
|
+
"Dark",
|
|
42
|
+
"Calm",
|
|
43
|
+
"Bold",
|
|
44
|
+
"Wise",
|
|
45
|
+
"Quick",
|
|
46
|
+
"Warm",
|
|
47
|
+
"Cool",
|
|
48
|
+
"Pure",
|
|
49
|
+
"Wild",
|
|
50
|
+
];
|
|
51
|
+
const NOUNS = [
|
|
52
|
+
"Lake",
|
|
53
|
+
"Stone",
|
|
54
|
+
"River",
|
|
55
|
+
"Mountain",
|
|
56
|
+
"Forest",
|
|
57
|
+
"Ocean",
|
|
58
|
+
"Star",
|
|
59
|
+
"Moon",
|
|
60
|
+
"Wind",
|
|
61
|
+
"Fire",
|
|
62
|
+
"Cloud",
|
|
63
|
+
"Storm",
|
|
64
|
+
"Dawn",
|
|
65
|
+
"Dusk",
|
|
66
|
+
"Hawk",
|
|
67
|
+
"Wolf",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function generateAgentName(): string {
|
|
71
|
+
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
|
72
|
+
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
73
|
+
return `${adj}${noun}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Types
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
export interface AgentMailContext {
|
|
81
|
+
projectKey: string;
|
|
82
|
+
agentName: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface InitAgentOptions {
|
|
86
|
+
projectPath: string;
|
|
87
|
+
agentName?: string;
|
|
88
|
+
program?: string;
|
|
89
|
+
model?: string;
|
|
90
|
+
taskDescription?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface SendMessageOptions {
|
|
94
|
+
projectPath: string;
|
|
95
|
+
fromAgent: string;
|
|
96
|
+
toAgents: string[];
|
|
97
|
+
subject: string;
|
|
98
|
+
body: string;
|
|
99
|
+
threadId?: string;
|
|
100
|
+
importance?: "low" | "normal" | "high" | "urgent";
|
|
101
|
+
ackRequired?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SendMessageResult {
|
|
105
|
+
success: boolean;
|
|
106
|
+
messageId: number;
|
|
107
|
+
threadId?: string;
|
|
108
|
+
recipientCount: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface GetInboxOptions {
|
|
112
|
+
projectPath: string;
|
|
113
|
+
agentName: string;
|
|
114
|
+
limit?: number;
|
|
115
|
+
urgentOnly?: boolean;
|
|
116
|
+
unreadOnly?: boolean;
|
|
117
|
+
includeBodies?: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface InboxMessage {
|
|
121
|
+
id: number;
|
|
122
|
+
from_agent: string;
|
|
123
|
+
subject: string;
|
|
124
|
+
body?: string;
|
|
125
|
+
thread_id: string | null;
|
|
126
|
+
importance: string;
|
|
127
|
+
created_at: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface InboxResult {
|
|
131
|
+
messages: InboxMessage[];
|
|
132
|
+
total: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ReadMessageOptions {
|
|
136
|
+
projectPath: string;
|
|
137
|
+
messageId: number;
|
|
138
|
+
agentName?: string;
|
|
139
|
+
markAsRead?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface ReserveFilesOptions {
|
|
143
|
+
projectPath: string;
|
|
144
|
+
agentName: string;
|
|
145
|
+
paths: string[];
|
|
146
|
+
reason?: string;
|
|
147
|
+
exclusive?: boolean;
|
|
148
|
+
ttlSeconds?: number;
|
|
149
|
+
force?: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface GrantedReservation {
|
|
153
|
+
id: number;
|
|
154
|
+
path: string;
|
|
155
|
+
expiresAt: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface ReservationConflict {
|
|
159
|
+
path: string;
|
|
160
|
+
holder: string;
|
|
161
|
+
pattern: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface ReserveFilesResult {
|
|
165
|
+
granted: GrantedReservation[];
|
|
166
|
+
conflicts: ReservationConflict[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface ReleaseFilesOptions {
|
|
170
|
+
projectPath: string;
|
|
171
|
+
agentName: string;
|
|
172
|
+
paths?: string[];
|
|
173
|
+
reservationIds?: number[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ReleaseFilesResult {
|
|
177
|
+
released: number;
|
|
178
|
+
releasedAt: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface AcknowledgeOptions {
|
|
182
|
+
projectPath: string;
|
|
183
|
+
messageId: number;
|
|
184
|
+
agentName: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface AcknowledgeResult {
|
|
188
|
+
acknowledged: boolean;
|
|
189
|
+
acknowledgedAt: string | null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface HealthResult {
|
|
193
|
+
healthy: boolean;
|
|
194
|
+
database: "connected" | "disconnected";
|
|
195
|
+
stats?: {
|
|
196
|
+
events: number;
|
|
197
|
+
agents: number;
|
|
198
|
+
messages: number;
|
|
199
|
+
reservations: number;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// Agent Operations
|
|
205
|
+
// ============================================================================
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Initialize an agent for this session
|
|
209
|
+
*/
|
|
210
|
+
export async function initAgent(
|
|
211
|
+
options: InitAgentOptions,
|
|
212
|
+
): Promise<AgentMailContext> {
|
|
213
|
+
const {
|
|
214
|
+
projectPath,
|
|
215
|
+
agentName = generateAgentName(),
|
|
216
|
+
program = "opencode",
|
|
217
|
+
model = "unknown",
|
|
218
|
+
taskDescription,
|
|
219
|
+
} = options;
|
|
220
|
+
|
|
221
|
+
// Register the agent (creates event + updates view)
|
|
222
|
+
await registerAgent(
|
|
223
|
+
projectPath, // Use projectPath as projectKey
|
|
224
|
+
agentName,
|
|
225
|
+
{ program, model, taskDescription },
|
|
226
|
+
projectPath,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
projectKey: projectPath,
|
|
231
|
+
agentName,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Message Operations
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Send a message to other agents
|
|
241
|
+
*/
|
|
242
|
+
export async function sendAgentMessage(
|
|
243
|
+
options: SendMessageOptions,
|
|
244
|
+
): Promise<SendMessageResult> {
|
|
245
|
+
const {
|
|
246
|
+
projectPath,
|
|
247
|
+
fromAgent,
|
|
248
|
+
toAgents,
|
|
249
|
+
subject,
|
|
250
|
+
body,
|
|
251
|
+
threadId,
|
|
252
|
+
importance = "normal",
|
|
253
|
+
ackRequired = false,
|
|
254
|
+
} = options;
|
|
255
|
+
|
|
256
|
+
await sendMessage(
|
|
257
|
+
projectPath,
|
|
258
|
+
fromAgent,
|
|
259
|
+
toAgents,
|
|
260
|
+
subject,
|
|
261
|
+
body,
|
|
262
|
+
{ threadId, importance, ackRequired },
|
|
263
|
+
projectPath,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Get the message ID from the messages table (not the event ID)
|
|
267
|
+
const { getDatabase } = await import("./index");
|
|
268
|
+
const db = await getDatabase(projectPath);
|
|
269
|
+
const result = await db.query<{ id: number }>(
|
|
270
|
+
`SELECT id FROM messages
|
|
271
|
+
WHERE project_key = $1 AND from_agent = $2 AND subject = $3
|
|
272
|
+
ORDER BY created_at DESC LIMIT 1`,
|
|
273
|
+
[projectPath, fromAgent, subject],
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const messageId = result.rows[0]?.id ?? 0;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
success: true,
|
|
280
|
+
messageId,
|
|
281
|
+
threadId,
|
|
282
|
+
recipientCount: toAgents.length,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get inbox messages for an agent
|
|
288
|
+
*/
|
|
289
|
+
export async function getAgentInbox(
|
|
290
|
+
options: GetInboxOptions,
|
|
291
|
+
): Promise<InboxResult> {
|
|
292
|
+
const {
|
|
293
|
+
projectPath,
|
|
294
|
+
agentName,
|
|
295
|
+
limit = MAX_INBOX_LIMIT,
|
|
296
|
+
urgentOnly = false,
|
|
297
|
+
unreadOnly = false,
|
|
298
|
+
includeBodies = false,
|
|
299
|
+
} = options;
|
|
300
|
+
|
|
301
|
+
// Enforce max limit
|
|
302
|
+
const effectiveLimit = Math.min(limit, MAX_INBOX_LIMIT);
|
|
303
|
+
|
|
304
|
+
const messages = await getInbox(
|
|
305
|
+
projectPath,
|
|
306
|
+
agentName,
|
|
307
|
+
{
|
|
308
|
+
limit: effectiveLimit,
|
|
309
|
+
urgentOnly,
|
|
310
|
+
unreadOnly,
|
|
311
|
+
includeBodies,
|
|
312
|
+
},
|
|
313
|
+
projectPath,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
messages: messages.map((m) => ({
|
|
318
|
+
id: m.id,
|
|
319
|
+
from_agent: m.from_agent,
|
|
320
|
+
subject: m.subject,
|
|
321
|
+
body: includeBodies ? m.body : undefined,
|
|
322
|
+
thread_id: m.thread_id,
|
|
323
|
+
importance: m.importance,
|
|
324
|
+
created_at: m.created_at,
|
|
325
|
+
})),
|
|
326
|
+
total: messages.length,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Read a single message with full body
|
|
332
|
+
*/
|
|
333
|
+
export async function readAgentMessage(
|
|
334
|
+
options: ReadMessageOptions,
|
|
335
|
+
): Promise<InboxMessage | null> {
|
|
336
|
+
const { projectPath, messageId, agentName, markAsRead = false } = options;
|
|
337
|
+
|
|
338
|
+
const message = await getMessage(projectPath, messageId, projectPath);
|
|
339
|
+
|
|
340
|
+
if (!message) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Mark as read if requested
|
|
345
|
+
if (markAsRead && agentName) {
|
|
346
|
+
await appendEvent(
|
|
347
|
+
createEvent("message_read", {
|
|
348
|
+
project_key: projectPath,
|
|
349
|
+
message_id: messageId,
|
|
350
|
+
agent_name: agentName,
|
|
351
|
+
}),
|
|
352
|
+
projectPath,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
id: message.id,
|
|
358
|
+
from_agent: message.from_agent,
|
|
359
|
+
subject: message.subject,
|
|
360
|
+
body: message.body,
|
|
361
|
+
thread_id: message.thread_id,
|
|
362
|
+
importance: message.importance,
|
|
363
|
+
created_at: message.created_at,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ============================================================================
|
|
368
|
+
// Reservation Operations
|
|
369
|
+
// ============================================================================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Reserve files for exclusive editing
|
|
373
|
+
*/
|
|
374
|
+
export async function reserveAgentFiles(
|
|
375
|
+
options: ReserveFilesOptions,
|
|
376
|
+
): Promise<ReserveFilesResult> {
|
|
377
|
+
const {
|
|
378
|
+
projectPath,
|
|
379
|
+
agentName,
|
|
380
|
+
paths,
|
|
381
|
+
reason,
|
|
382
|
+
exclusive = true,
|
|
383
|
+
ttlSeconds = DEFAULT_TTL_SECONDS,
|
|
384
|
+
force = false,
|
|
385
|
+
} = options;
|
|
386
|
+
|
|
387
|
+
// Check for conflicts first
|
|
388
|
+
const conflicts = await checkConflicts(
|
|
389
|
+
projectPath,
|
|
390
|
+
agentName,
|
|
391
|
+
paths,
|
|
392
|
+
projectPath,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// If conflicts exist and not forcing, reject reservation
|
|
396
|
+
if (conflicts.length > 0 && !force) {
|
|
397
|
+
return {
|
|
398
|
+
granted: [],
|
|
399
|
+
conflicts: conflicts.map((c) => ({
|
|
400
|
+
path: c.path,
|
|
401
|
+
holder: c.holder,
|
|
402
|
+
pattern: c.pattern,
|
|
403
|
+
})),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Only create reservations if no conflicts or force=true
|
|
408
|
+
const event = await reserveFiles(
|
|
409
|
+
projectPath,
|
|
410
|
+
agentName,
|
|
411
|
+
paths,
|
|
412
|
+
{ reason, exclusive, ttlSeconds },
|
|
413
|
+
projectPath,
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Build granted list
|
|
417
|
+
const granted: GrantedReservation[] = paths.map((path, index) => ({
|
|
418
|
+
id: event.id + index, // Approximate - each path gets a reservation
|
|
419
|
+
path,
|
|
420
|
+
expiresAt: event.expires_at,
|
|
421
|
+
}));
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
granted,
|
|
425
|
+
conflicts: conflicts.map((c) => ({
|
|
426
|
+
path: c.path,
|
|
427
|
+
holder: c.holder,
|
|
428
|
+
pattern: c.pattern,
|
|
429
|
+
})),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Release file reservations
|
|
435
|
+
*/
|
|
436
|
+
export async function releaseAgentFiles(
|
|
437
|
+
options: ReleaseFilesOptions,
|
|
438
|
+
): Promise<ReleaseFilesResult> {
|
|
439
|
+
const { projectPath, agentName, paths, reservationIds } = options;
|
|
440
|
+
|
|
441
|
+
// Get current reservations to count what we're releasing
|
|
442
|
+
const currentReservations = await getActiveReservations(
|
|
443
|
+
projectPath,
|
|
444
|
+
projectPath,
|
|
445
|
+
agentName,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
let releaseCount = 0;
|
|
449
|
+
|
|
450
|
+
if (paths && paths.length > 0) {
|
|
451
|
+
// Release specific paths
|
|
452
|
+
releaseCount = currentReservations.filter((r) =>
|
|
453
|
+
paths.includes(r.path_pattern),
|
|
454
|
+
).length;
|
|
455
|
+
} else if (reservationIds && reservationIds.length > 0) {
|
|
456
|
+
// Release by ID
|
|
457
|
+
releaseCount = currentReservations.filter((r) =>
|
|
458
|
+
reservationIds.includes(r.id),
|
|
459
|
+
).length;
|
|
460
|
+
} else {
|
|
461
|
+
// Release all
|
|
462
|
+
releaseCount = currentReservations.length;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Create release event
|
|
466
|
+
await appendEvent(
|
|
467
|
+
createEvent("file_released", {
|
|
468
|
+
project_key: projectPath,
|
|
469
|
+
agent_name: agentName,
|
|
470
|
+
paths,
|
|
471
|
+
reservation_ids: reservationIds,
|
|
472
|
+
}),
|
|
473
|
+
projectPath,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
released: releaseCount,
|
|
478
|
+
releasedAt: Date.now(),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ============================================================================
|
|
483
|
+
// Acknowledgement Operations
|
|
484
|
+
// ============================================================================
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Acknowledge a message
|
|
488
|
+
*/
|
|
489
|
+
export async function acknowledgeMessage(
|
|
490
|
+
options: AcknowledgeOptions,
|
|
491
|
+
): Promise<AcknowledgeResult> {
|
|
492
|
+
const { projectPath, messageId, agentName } = options;
|
|
493
|
+
|
|
494
|
+
const timestamp = Date.now();
|
|
495
|
+
|
|
496
|
+
await appendEvent(
|
|
497
|
+
createEvent("message_acked", {
|
|
498
|
+
project_key: projectPath,
|
|
499
|
+
message_id: messageId,
|
|
500
|
+
agent_name: agentName,
|
|
501
|
+
}),
|
|
502
|
+
projectPath,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
acknowledged: true,
|
|
507
|
+
acknowledgedAt: new Date(timestamp).toISOString(),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ============================================================================
|
|
512
|
+
// Health Check
|
|
513
|
+
// ============================================================================
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Check if the agent mail store is healthy
|
|
517
|
+
*/
|
|
518
|
+
export async function checkHealth(projectPath?: string): Promise<HealthResult> {
|
|
519
|
+
const healthy = await isDatabaseHealthy(projectPath);
|
|
520
|
+
|
|
521
|
+
if (!healthy) {
|
|
522
|
+
return {
|
|
523
|
+
healthy: false,
|
|
524
|
+
database: "disconnected",
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const stats = await getDatabaseStats(projectPath);
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
healthy: true,
|
|
532
|
+
database: "connected",
|
|
533
|
+
stats,
|
|
534
|
+
};
|
|
535
|
+
}
|