opencode-swarm-plugin 0.18.0 → 0.19.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/.beads/issues.jsonl +71 -61
- package/.github/workflows/ci.yml +5 -1
- package/README.md +48 -4
- package/dist/index.js +6643 -6326
- package/dist/plugin.js +2726 -2404
- package/package.json +1 -1
- package/src/agent-mail.ts +13 -0
- package/src/anti-patterns.test.ts +1167 -0
- package/src/anti-patterns.ts +29 -11
- package/src/pattern-maturity.ts +51 -13
- package/src/plugin.ts +15 -3
- package/src/schemas/bead.ts +35 -4
- package/src/schemas/evaluation.ts +18 -6
- package/src/schemas/index.ts +25 -2
- package/src/schemas/task.ts +49 -21
- package/src/streams/debug.ts +101 -3
- package/src/streams/index.ts +58 -1
- package/src/streams/migrations.ts +46 -4
- package/src/streams/store.integration.test.ts +110 -0
- package/src/streams/store.ts +311 -126
- package/src/structured.test.ts +1046 -0
- package/src/structured.ts +74 -27
- package/src/swarm-decompose.ts +912 -0
- package/src/swarm-orchestrate.ts +1869 -0
- package/src/swarm-prompts.ts +756 -0
- package/src/swarm-strategies.ts +407 -0
- package/src/swarm.ts +23 -3876
- package/src/tool-availability.ts +29 -6
- package/test-bug-fixes.ts +86 -0
package/src/streams/index.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SwarmMail Event Store - PGLite-based event sourcing
|
|
3
|
+
*
|
|
4
|
+
* ## Thread Safety
|
|
5
|
+
*
|
|
6
|
+
* PGLite runs in-process as a single-threaded SQLite-compatible database.
|
|
7
|
+
* While Node.js is single-threaded, async operations can interleave.
|
|
8
|
+
*
|
|
9
|
+
* **Concurrency Model:**
|
|
10
|
+
* - Single PGLite instance per project (singleton pattern via LRU cache)
|
|
11
|
+
* - Transactions provide isolation for multi-statement operations
|
|
12
|
+
* - appendEvents uses BEGIN/COMMIT for atomic event batches
|
|
13
|
+
* - Concurrent reads are safe (no locks needed)
|
|
14
|
+
* - Concurrent writes are serialized by PGLite internally
|
|
15
|
+
*
|
|
16
|
+
* **Race Condition Mitigations:**
|
|
17
|
+
* - File reservations use INSERT with conflict detection
|
|
18
|
+
* - Sequence numbers are auto-incremented by database
|
|
19
|
+
* - Materialized views updated within same transaction as events
|
|
20
|
+
* - Pending instance promises prevent duplicate initialization
|
|
21
|
+
*
|
|
22
|
+
* **Known Limitations:**
|
|
23
|
+
* - No distributed locking (single-process only)
|
|
24
|
+
* - Large transactions may block other operations
|
|
25
|
+
* - No connection pooling (embedded database)
|
|
26
|
+
*
|
|
27
|
+
* ## Database Setup
|
|
3
28
|
*
|
|
4
29
|
* Embedded PostgreSQL database for event sourcing.
|
|
5
30
|
* No external server required - runs in-process.
|
|
@@ -41,6 +66,38 @@ export async function withTimeout<T>(
|
|
|
41
66
|
return Promise.race([promise, timeout]);
|
|
42
67
|
}
|
|
43
68
|
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Performance Monitoring
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/** Threshold for slow query warnings in milliseconds */
|
|
74
|
+
const SLOW_QUERY_THRESHOLD_MS = 100;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute a database operation with timing instrumentation.
|
|
78
|
+
* Logs a warning if the operation exceeds SLOW_QUERY_THRESHOLD_MS.
|
|
79
|
+
*
|
|
80
|
+
* @param operation - Name of the operation for logging
|
|
81
|
+
* @param fn - Async function to execute
|
|
82
|
+
* @returns Result of the function
|
|
83
|
+
*/
|
|
84
|
+
export async function withTiming<T>(
|
|
85
|
+
operation: string,
|
|
86
|
+
fn: () => Promise<T>,
|
|
87
|
+
): Promise<T> {
|
|
88
|
+
const start = performance.now();
|
|
89
|
+
try {
|
|
90
|
+
return await fn();
|
|
91
|
+
} finally {
|
|
92
|
+
const duration = performance.now() - start;
|
|
93
|
+
if (duration > SLOW_QUERY_THRESHOLD_MS) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`[SwarmMail] Slow operation: ${operation} took ${duration.toFixed(1)}ms`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
44
101
|
// ============================================================================
|
|
45
102
|
// Debug Logging
|
|
46
103
|
// ============================================================================
|
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Schema Migration System
|
|
2
|
+
* Schema Migration System
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Handles database schema evolution for the PGLite event store.
|
|
5
|
+
*
|
|
6
|
+
* ## How It Works
|
|
7
|
+
*
|
|
8
|
+
* 1. Each migration has a unique version number (incrementing integer)
|
|
9
|
+
* 2. On startup, `runMigrations()` checks current schema version
|
|
10
|
+
* 3. Migrations are applied in order until schema is current
|
|
11
|
+
* 4. Version is stored in `schema_version` table
|
|
12
|
+
*
|
|
13
|
+
* ## Adding a New Migration
|
|
14
|
+
*
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // In migrations.ts
|
|
17
|
+
* export const migrations: Migration[] = [
|
|
18
|
+
* // ... existing migrations
|
|
19
|
+
* {
|
|
20
|
+
* version: 3,
|
|
21
|
+
* description: "add_new_column",
|
|
22
|
+
* up: `ALTER TABLE events ADD COLUMN new_col TEXT`,
|
|
23
|
+
* down: `ALTER TABLE events DROP COLUMN new_col`,
|
|
24
|
+
* },
|
|
25
|
+
* ];
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* ## Rollback
|
|
29
|
+
*
|
|
30
|
+
* Rollback is supported via `rollbackTo(db, targetVersion)`.
|
|
31
|
+
* Note: Some migrations may not be fully reversible (data loss).
|
|
32
|
+
*
|
|
33
|
+
* ## Best Practices
|
|
34
|
+
*
|
|
35
|
+
* - Always test migrations on a copy of production data
|
|
36
|
+
* - Keep migrations small and focused
|
|
37
|
+
* - Include both `up` and `down` SQL
|
|
38
|
+
* - Use transactions for multi-statement migrations
|
|
39
|
+
* - Document any data transformations
|
|
40
|
+
*
|
|
41
|
+
* @module migrations
|
|
7
42
|
*/
|
|
8
43
|
import type { PGlite } from "@electric-sql/pglite";
|
|
9
44
|
|
|
@@ -11,10 +46,17 @@ import type { PGlite } from "@electric-sql/pglite";
|
|
|
11
46
|
// Types
|
|
12
47
|
// ============================================================================
|
|
13
48
|
|
|
49
|
+
/**
|
|
50
|
+
* A database migration definition.
|
|
51
|
+
*/
|
|
14
52
|
export interface Migration {
|
|
53
|
+
/** Unique version number (must be sequential) */
|
|
15
54
|
version: number;
|
|
55
|
+
/** Human-readable migration description */
|
|
16
56
|
description: string;
|
|
57
|
+
/** SQL to apply the migration */
|
|
17
58
|
up: string;
|
|
59
|
+
/** SQL to rollback the migration (best effort) */
|
|
18
60
|
down: string;
|
|
19
61
|
}
|
|
20
62
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
readEvents,
|
|
18
18
|
getLatestSequence,
|
|
19
19
|
replayEvents,
|
|
20
|
+
replayEventsBatched,
|
|
20
21
|
registerAgent,
|
|
21
22
|
sendMessage,
|
|
22
23
|
reserveFiles,
|
|
@@ -343,6 +344,115 @@ describe("Event Store", () => {
|
|
|
343
344
|
});
|
|
344
345
|
});
|
|
345
346
|
|
|
347
|
+
describe("replayEventsBatched", () => {
|
|
348
|
+
it("should replay events in batches with progress tracking", async () => {
|
|
349
|
+
// Create 50 events
|
|
350
|
+
for (let i = 0; i < 50; i++) {
|
|
351
|
+
await registerAgent(
|
|
352
|
+
"test-project",
|
|
353
|
+
`Agent${i}`,
|
|
354
|
+
{ taskDescription: `Agent ${i}` },
|
|
355
|
+
TEST_PROJECT_PATH,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Manually corrupt the views
|
|
360
|
+
const db = await getDatabase(TEST_PROJECT_PATH);
|
|
361
|
+
await db.query("DELETE FROM agents WHERE project_key = 'test-project'");
|
|
362
|
+
|
|
363
|
+
// Verify views are empty
|
|
364
|
+
const empty = await db.query<{ count: string }>(
|
|
365
|
+
"SELECT COUNT(*) as count FROM agents WHERE project_key = 'test-project'",
|
|
366
|
+
);
|
|
367
|
+
expect(parseInt(empty.rows[0]?.count ?? "0")).toBe(0);
|
|
368
|
+
|
|
369
|
+
// Track progress
|
|
370
|
+
const progressUpdates: Array<{
|
|
371
|
+
processed: number;
|
|
372
|
+
total: number;
|
|
373
|
+
percent: number;
|
|
374
|
+
}> = [];
|
|
375
|
+
|
|
376
|
+
// Replay in batches of 10
|
|
377
|
+
const result = await replayEventsBatched(
|
|
378
|
+
"test-project",
|
|
379
|
+
async (_events, progress) => {
|
|
380
|
+
progressUpdates.push(progress);
|
|
381
|
+
},
|
|
382
|
+
{ batchSize: 10, clearViews: false },
|
|
383
|
+
TEST_PROJECT_PATH,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Verify all events replayed
|
|
387
|
+
expect(result.eventsReplayed).toBe(50);
|
|
388
|
+
|
|
389
|
+
// Verify progress updates
|
|
390
|
+
expect(progressUpdates.length).toBe(5); // 50 events / 10 per batch = 5 batches
|
|
391
|
+
expect(progressUpdates[0]).toMatchObject({
|
|
392
|
+
processed: 10,
|
|
393
|
+
total: 50,
|
|
394
|
+
percent: 20,
|
|
395
|
+
});
|
|
396
|
+
expect(progressUpdates[4]).toMatchObject({
|
|
397
|
+
processed: 50,
|
|
398
|
+
total: 50,
|
|
399
|
+
percent: 100,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Verify views are restored
|
|
403
|
+
const restored = await db.query<{ count: string }>(
|
|
404
|
+
"SELECT COUNT(*) as count FROM agents WHERE project_key = 'test-project'",
|
|
405
|
+
);
|
|
406
|
+
expect(parseInt(restored.rows[0]?.count ?? "0")).toBe(50);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should handle zero events gracefully", async () => {
|
|
410
|
+
const progressUpdates: Array<{
|
|
411
|
+
processed: number;
|
|
412
|
+
total: number;
|
|
413
|
+
percent: number;
|
|
414
|
+
}> = [];
|
|
415
|
+
|
|
416
|
+
const result = await replayEventsBatched(
|
|
417
|
+
"test-project",
|
|
418
|
+
async (_events, progress) => {
|
|
419
|
+
progressUpdates.push(progress);
|
|
420
|
+
},
|
|
421
|
+
{ batchSize: 10 },
|
|
422
|
+
TEST_PROJECT_PATH,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
expect(result.eventsReplayed).toBe(0);
|
|
426
|
+
expect(progressUpdates.length).toBe(0);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("should use custom batch size", async () => {
|
|
430
|
+
// Create 25 events
|
|
431
|
+
for (let i = 0; i < 25; i++) {
|
|
432
|
+
await registerAgent("test-project", `Agent${i}`, {}, TEST_PROJECT_PATH);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const progressUpdates: Array<{
|
|
436
|
+
processed: number;
|
|
437
|
+
total: number;
|
|
438
|
+
percent: number;
|
|
439
|
+
}> = [];
|
|
440
|
+
|
|
441
|
+
// Replay with batch size of 5
|
|
442
|
+
await replayEventsBatched(
|
|
443
|
+
"test-project",
|
|
444
|
+
async (_events, progress) => {
|
|
445
|
+
progressUpdates.push(progress);
|
|
446
|
+
},
|
|
447
|
+
{ batchSize: 5, clearViews: false },
|
|
448
|
+
TEST_PROJECT_PATH,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Should have 5 batches (25 events / 5 per batch)
|
|
452
|
+
expect(progressUpdates.length).toBe(5);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
346
456
|
describe("getDatabaseStats", () => {
|
|
347
457
|
it("should return correct counts", async () => {
|
|
348
458
|
await registerAgent("test-project", "Agent1", {}, TEST_PROJECT_PATH);
|