opencode-swarm-plugin 0.22.0 → 0.23.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.
- package/.turbo/turbo-build.log +9 -0
- package/CHANGELOG.md +20 -0
- package/README.md +109 -429
- package/dist/agent-mail.d.ts +480 -0
- package/dist/agent-mail.d.ts.map +1 -0
- package/dist/anti-patterns.d.ts +257 -0
- package/dist/anti-patterns.d.ts.map +1 -0
- package/dist/beads.d.ts +377 -0
- package/dist/beads.d.ts.map +1 -0
- package/dist/eval-capture.d.ts +206 -0
- package/dist/eval-capture.d.ts.map +1 -0
- package/dist/index.d.ts +1299 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +498 -4246
- package/dist/learning.d.ts +670 -0
- package/dist/learning.d.ts.map +1 -0
- package/dist/mandate-promotion.d.ts +93 -0
- package/dist/mandate-promotion.d.ts.map +1 -0
- package/dist/mandate-storage.d.ts +209 -0
- package/dist/mandate-storage.d.ts.map +1 -0
- package/dist/mandates.d.ts +230 -0
- package/dist/mandates.d.ts.map +1 -0
- package/dist/output-guardrails.d.ts +125 -0
- package/dist/output-guardrails.d.ts.map +1 -0
- package/dist/pattern-maturity.d.ts +246 -0
- package/dist/pattern-maturity.d.ts.map +1 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +493 -4241
- package/dist/rate-limiter.d.ts +218 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/repo-crawl.d.ts +146 -0
- package/dist/repo-crawl.d.ts.map +1 -0
- package/dist/schemas/bead.d.ts +255 -0
- package/dist/schemas/bead.d.ts.map +1 -0
- package/dist/schemas/evaluation.d.ts +161 -0
- package/dist/schemas/evaluation.d.ts.map +1 -0
- package/dist/schemas/index.d.ts +34 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/mandate.d.ts +336 -0
- package/dist/schemas/mandate.d.ts.map +1 -0
- package/dist/schemas/swarm-context.d.ts +131 -0
- package/dist/schemas/swarm-context.d.ts.map +1 -0
- package/dist/schemas/task.d.ts +188 -0
- package/dist/schemas/task.d.ts.map +1 -0
- package/dist/skills.d.ts +471 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/storage.d.ts +260 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/structured.d.ts +196 -0
- package/dist/structured.d.ts.map +1 -0
- package/dist/swarm-decompose.d.ts +201 -0
- package/dist/swarm-decompose.d.ts.map +1 -0
- package/dist/swarm-mail.d.ts +240 -0
- package/dist/swarm-mail.d.ts.map +1 -0
- package/dist/swarm-orchestrate.d.ts +708 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -0
- package/dist/swarm-prompts.d.ts +292 -0
- package/dist/swarm-prompts.d.ts.map +1 -0
- package/dist/swarm-strategies.d.ts +100 -0
- package/dist/swarm-strategies.d.ts.map +1 -0
- package/dist/swarm.d.ts +455 -0
- package/dist/swarm.d.ts.map +1 -0
- package/dist/tool-availability.d.ts +91 -0
- package/dist/tool-availability.d.ts.map +1 -0
- package/docs/planning/ADR-001-monorepo-structure.md +171 -0
- package/docs/planning/ADR-002-package-extraction.md +393 -0
- package/docs/planning/ADR-003-performance-improvements.md +451 -0
- package/docs/planning/ADR-004-message-queue-features.md +187 -0
- package/docs/planning/ADR-005-devtools-observability.md +202 -0
- package/docs/planning/ROADMAP.md +368 -0
- package/package.json +13 -24
- package/src/agent-mail.ts +1 -1
- package/src/beads.ts +1 -2
- package/src/index.ts +2 -2
- package/src/learning.integration.test.ts +66 -11
- package/src/mandate-storage.test.ts +3 -3
- package/src/storage.ts +78 -10
- package/src/swarm-mail.ts +3 -3
- package/src/swarm-orchestrate.ts +7 -7
- package/src/tool-availability.ts +1 -1
- package/tsconfig.json +1 -1
- package/.beads/.local_version +0 -1
- package/.beads/README.md +0 -81
- package/.beads/analysis/skill-architecture-meta-skills.md +0 -1562
- package/.beads/config.yaml +0 -62
- package/.beads/issues.jsonl +0 -2197
- package/.beads/metadata.json +0 -4
- package/.gitattributes +0 -3
- package/.github/workflows/ci.yml +0 -30
- package/.github/workflows/opencode.yml +0 -31
- package/.opencode/skills/tdd/SKILL.md +0 -182
- package/INTEGRATION_EXAMPLE.md +0 -66
- package/VERIFICATION_QUALITY_PATTERNS.md +0 -565
- package/bun.lock +0 -286
- package/dist/pglite.data +0 -0
- package/dist/pglite.wasm +0 -0
- package/src/streams/agent-mail.test.ts +0 -777
- package/src/streams/agent-mail.ts +0 -535
- package/src/streams/debug.test.ts +0 -500
- package/src/streams/debug.ts +0 -727
- package/src/streams/effect/ask.integration.test.ts +0 -314
- package/src/streams/effect/ask.ts +0 -202
- package/src/streams/effect/cursor.integration.test.ts +0 -418
- package/src/streams/effect/cursor.ts +0 -288
- package/src/streams/effect/deferred.test.ts +0 -357
- package/src/streams/effect/deferred.ts +0 -445
- package/src/streams/effect/index.ts +0 -17
- package/src/streams/effect/layers.ts +0 -73
- package/src/streams/effect/lock.test.ts +0 -385
- package/src/streams/effect/lock.ts +0 -399
- package/src/streams/effect/mailbox.test.ts +0 -260
- package/src/streams/effect/mailbox.ts +0 -318
- package/src/streams/events.test.ts +0 -924
- package/src/streams/events.ts +0 -329
- package/src/streams/index.test.ts +0 -229
- package/src/streams/index.ts +0 -578
- package/src/streams/migrations.test.ts +0 -359
- package/src/streams/migrations.ts +0 -362
- package/src/streams/projections.test.ts +0 -611
- package/src/streams/projections.ts +0 -504
- package/src/streams/store.integration.test.ts +0 -658
- package/src/streams/store.ts +0 -1075
- package/src/streams/swarm-mail.ts +0 -552
- package/test-bug-fixes.ts +0 -86
- package/vitest.integration.config.ts +0 -19
- package/vitest.integration.setup.ts +0 -48
- package/workflow-integration-analysis.md +0 -876
|
@@ -1,552 +0,0 @@
|
|
|
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
|
-
}
|
package/test-bug-fixes.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Quick test to verify bug fixes in store.ts
|
|
3
|
-
*
|
|
4
|
-
* Bug 1 (xcavl.5): Transaction rollback error propagation
|
|
5
|
-
* Bug 2 (xcavl.6): File reservation idempotency
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { getDatabase, resetDatabase } from "./src/streams/index";
|
|
9
|
-
import { reserveFiles } from "./src/streams/store";
|
|
10
|
-
|
|
11
|
-
async function testBug1_RollbackErrorPropagation() {
|
|
12
|
-
console.log("\n=== Testing Bug 1: Rollback Error Propagation ===");
|
|
13
|
-
console.log(
|
|
14
|
-
"Note: This is hard to test in isolation without forcing connection loss.",
|
|
15
|
-
);
|
|
16
|
-
console.log(
|
|
17
|
-
"The fix ensures composite errors are thrown when both transaction AND rollback fail.",
|
|
18
|
-
);
|
|
19
|
-
console.log(
|
|
20
|
-
"✅ Code review confirms fix is in place (see lines 141-166 in store.ts)",
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function testBug2_ReservationIdempotency() {
|
|
25
|
-
console.log("\n=== Testing Bug 2: File Reservation Idempotency ===");
|
|
26
|
-
|
|
27
|
-
await resetDatabase(); // Clean slate
|
|
28
|
-
const db = await getDatabase();
|
|
29
|
-
|
|
30
|
-
const projectKey = "test-project";
|
|
31
|
-
const agentName = "TestWorker";
|
|
32
|
-
const paths = ["src/file.ts", "src/other.ts"];
|
|
33
|
-
|
|
34
|
-
// First reservation
|
|
35
|
-
await reserveFiles(projectKey, agentName, paths, {
|
|
36
|
-
reason: "Initial reservation",
|
|
37
|
-
exclusive: true,
|
|
38
|
-
ttlSeconds: 3600,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Query reservations
|
|
42
|
-
const result1 = await db.query<{ count: string }>(
|
|
43
|
-
`SELECT COUNT(*) as count FROM reservations
|
|
44
|
-
WHERE project_key = $1 AND agent_name = $2 AND released_at IS NULL`,
|
|
45
|
-
[projectKey, agentName],
|
|
46
|
-
);
|
|
47
|
-
const count1 = parseInt(result1.rows[0]?.count || "0");
|
|
48
|
-
console.log(`After first reservation: ${count1} active reservations`);
|
|
49
|
-
|
|
50
|
-
// RETRY the same reservation (simulating network timeout + retry)
|
|
51
|
-
await reserveFiles(projectKey, agentName, paths, {
|
|
52
|
-
reason: "Retry after timeout",
|
|
53
|
-
exclusive: true,
|
|
54
|
-
ttlSeconds: 3600,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Query again - should still be 2 (idempotent)
|
|
58
|
-
const result2 = await db.query<{ count: string }>(
|
|
59
|
-
`SELECT COUNT(*) as count FROM reservations
|
|
60
|
-
WHERE project_key = $1 AND agent_name = $2 AND released_at IS NULL`,
|
|
61
|
-
[projectKey, agentName],
|
|
62
|
-
);
|
|
63
|
-
const count2 = parseInt(result2.rows[0]?.count || "0");
|
|
64
|
-
console.log(`After retry: ${count2} active reservations`);
|
|
65
|
-
|
|
66
|
-
if (count1 === count2 && count1 === 2) {
|
|
67
|
-
console.log("✅ PASS: Reservation is idempotent (no duplicates created)");
|
|
68
|
-
} else {
|
|
69
|
-
console.log(
|
|
70
|
-
`❌ FAIL: Expected 2 reservations both times, got ${count1} then ${count2}`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function main() {
|
|
76
|
-
try {
|
|
77
|
-
await testBug1_RollbackErrorPropagation();
|
|
78
|
-
await testBug2_ReservationIdempotency();
|
|
79
|
-
console.log("\n=== All tests complete ===\n");
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.error("Test failed:", error);
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
main();
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
include: ["src/**/*.integration.test.ts"],
|
|
6
|
-
testTimeout: 30000, // Integration tests may be slower
|
|
7
|
-
hookTimeout: 30000,
|
|
8
|
-
// Run serially to avoid race conditions with shared services
|
|
9
|
-
sequence: {
|
|
10
|
-
concurrent: false,
|
|
11
|
-
},
|
|
12
|
-
env: {
|
|
13
|
-
// Enable test-specific collections to isolate test data from production
|
|
14
|
-
TEST_MEMORY_COLLECTIONS: "true",
|
|
15
|
-
},
|
|
16
|
-
// Global setup/teardown hooks
|
|
17
|
-
globalSetup: "./vitest.integration.setup.ts",
|
|
18
|
-
},
|
|
19
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global setup/teardown for integration tests
|
|
3
|
-
*
|
|
4
|
-
* Ensures test-specific semantic-memory collections are cleaned up
|
|
5
|
-
* after all integration tests complete.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export async function setup() {
|
|
9
|
-
console.log("[vitest] Integration test setup: TEST_MEMORY_COLLECTIONS=true");
|
|
10
|
-
// Setup runs before tests - environment variables are already set via vitest config
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function teardown() {
|
|
14
|
-
console.log(
|
|
15
|
-
"[vitest] Integration test teardown: cleaning up test collections",
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
// Clean up test collections
|
|
19
|
-
const testCollections = [
|
|
20
|
-
"swarm-feedback-test",
|
|
21
|
-
"swarm-patterns-test",
|
|
22
|
-
"swarm-maturity-test",
|
|
23
|
-
"swarm-maturity-test-feedback",
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
for (const collection of testCollections) {
|
|
27
|
-
try {
|
|
28
|
-
// Attempt to remove test collection data
|
|
29
|
-
// Note: semantic-memory doesn't have a built-in "delete collection" command,
|
|
30
|
-
// so we'll use the remove command with a wildcard or rely on TTL/manual cleanup
|
|
31
|
-
console.log(`[vitest] Attempting to clean collection: ${collection}`);
|
|
32
|
-
|
|
33
|
-
// List items and remove them (semantic-memory may not support bulk delete)
|
|
34
|
-
// This is a best-effort cleanup - some backends may require manual cleanup
|
|
35
|
-
// For now, we'll just log that cleanup should happen
|
|
36
|
-
console.log(
|
|
37
|
-
`[vitest] Note: Collection "${collection}" may need manual cleanup via semantic-memory CLI`,
|
|
38
|
-
);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.warn(
|
|
41
|
-
`[vitest] Failed to clean collection ${collection}:`,
|
|
42
|
-
error instanceof Error ? error.message : String(error),
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
console.log("[vitest] Integration test teardown complete");
|
|
48
|
-
}
|