opencode-swarm-plugin 0.18.0 → 0.20.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.
@@ -12,6 +12,16 @@
12
12
  import { z } from "zod";
13
13
  import { calculateDecayedValue } from "./learning";
14
14
 
15
+ // ============================================================================
16
+ // Constants
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Tolerance for floating-point comparisons.
21
+ * Used when comparing success rates to avoid floating-point precision issues.
22
+ */
23
+ const FLOAT_EPSILON = 0.01;
24
+
15
25
  // ============================================================================
16
26
  // Schemas
17
27
  // ============================================================================
@@ -164,26 +174,26 @@ export function calculateMaturityState(
164
174
  );
165
175
 
166
176
  const total = decayedHelpful + decayedHarmful;
167
- const epsilon = 0.01; // Float comparison tolerance
168
- const safeTotal = total > epsilon ? total : 0;
177
+ // Use FLOAT_EPSILON constant (defined at module level)
178
+ const safeTotal = total > FLOAT_EPSILON ? total : 0;
169
179
  const harmfulRatio = safeTotal > 0 ? decayedHarmful / safeTotal : 0;
170
180
 
171
181
  // Deprecated: high harmful ratio with enough feedback
172
182
  if (
173
183
  harmfulRatio > config.deprecationThreshold &&
174
- safeTotal >= config.minFeedback - epsilon
184
+ safeTotal >= config.minFeedback - FLOAT_EPSILON
175
185
  ) {
176
186
  return "deprecated";
177
187
  }
178
188
 
179
189
  // Candidate: not enough feedback yet
180
- if (safeTotal < config.minFeedback - epsilon) {
190
+ if (safeTotal < config.minFeedback - FLOAT_EPSILON) {
181
191
  return "candidate";
182
192
  }
183
193
 
184
194
  // Proven: strong positive signal
185
195
  if (
186
- decayedHelpful >= config.minHelpful - epsilon &&
196
+ decayedHelpful >= config.minHelpful - FLOAT_EPSILON &&
187
197
  harmfulRatio < config.maxHarmful
188
198
  ) {
189
199
  return "proven";
@@ -210,14 +220,23 @@ export function createPatternMaturity(patternId: string): PatternMaturity {
210
220
  }
211
221
 
212
222
  /**
213
- * Update pattern maturity with new feedback
223
+ * Update pattern maturity with new feedback.
224
+ *
225
+ * Side Effects:
226
+ * - Sets `promoted_at` timestamp on first entry into 'proven' status
227
+ * - Sets `deprecated_at` timestamp on first entry into 'deprecated' status
228
+ * - Updates `helpful_count` and `harmful_count` based on feedback events
229
+ * - Recalculates `state` based on decayed feedback counts
214
230
  *
215
- * Records feedback, updates counts, and recalculates state.
231
+ * State Transitions:
232
+ * - candidate → established: After minFeedback observations (default 3)
233
+ * - established → proven: When decayedHelpful >= minHelpful (5) AND harmfulRatio < maxHarmful (15%)
234
+ * - any → deprecated: When harmfulRatio > deprecationThreshold (30%) AND total >= minFeedback
216
235
  *
217
236
  * @param maturity - Current maturity record
218
237
  * @param feedbackEvents - All feedback events for this pattern
219
238
  * @param config - Maturity configuration
220
- * @returns Updated maturity record
239
+ * @returns Updated maturity record with new state
221
240
  */
222
241
  export function updatePatternMaturity(
223
242
  maturity: PatternMaturity,
@@ -269,7 +288,16 @@ export function promotePattern(maturity: PatternMaturity): PatternMaturity {
269
288
  }
270
289
 
271
290
  if (maturity.state === "proven") {
272
- return maturity; // Already proven
291
+ console.warn(
292
+ `[PatternMaturity] Pattern already proven: ${maturity.pattern_id}`,
293
+ );
294
+ return maturity; // No-op but warn
295
+ }
296
+
297
+ if (maturity.state === "candidate" && maturity.helpful_count < 3) {
298
+ console.warn(
299
+ `[PatternMaturity] Promoting candidate with insufficient data: ${maturity.pattern_id} (${maturity.helpful_count} helpful observations)`,
300
+ );
273
301
  }
274
302
 
275
303
  const now = new Date().toISOString();
@@ -309,12 +337,16 @@ export function deprecatePattern(
309
337
  }
310
338
 
311
339
  /**
312
- * Get maturity score multiplier for pattern ranking
340
+ * Get weight multiplier based on pattern maturity status.
313
341
  *
314
- * Higher maturity patterns should be weighted more heavily.
342
+ * Multipliers chosen to:
343
+ * - Heavily penalize deprecated patterns (0x) - never recommend
344
+ * - Slightly boost proven patterns (1.5x) - reward validated success
345
+ * - Penalize unvalidated candidates (0.5x) - reduce impact until proven
346
+ * - Neutral for established (1.0x) - baseline weight
315
347
  *
316
- * @param state - Maturity state
317
- * @returns Score multiplier (0-1.5)
348
+ * @param state - Pattern maturity status
349
+ * @returns Multiplier to apply to pattern weight
318
350
  */
319
351
  export function getMaturityMultiplier(state: MaturityState): number {
320
352
  const multipliers: Record<MaturityState, number> = {
@@ -336,6 +368,12 @@ export function getMaturityMultiplier(state: MaturityState): number {
336
368
  */
337
369
  export function formatMaturityForPrompt(maturity: PatternMaturity): string {
338
370
  const total = maturity.helpful_count + maturity.harmful_count;
371
+
372
+ // Don't show percentages for insufficient data
373
+ if (total < 3) {
374
+ return `[LIMITED DATA - ${total} observation${total !== 1 ? "s" : ""}]`;
375
+ }
376
+
339
377
  const harmfulRatio =
340
378
  total > 0 ? Math.round((maturity.harmful_count / total) * 100) : 0;
341
379
  const helpfulRatio =
package/src/plugin.ts CHANGED
@@ -1,9 +1,21 @@
1
1
  /**
2
2
  * OpenCode Plugin Entry Point
3
3
  *
4
- * This file ONLY exports the plugin function.
5
- * The plugin loader iterates over all exports and calls them as functions,
6
- * so we cannot export anything else here (classes, constants, types, etc.)
4
+ * CRITICAL: Only export the plugin function from this file.
5
+ *
6
+ * OpenCode's plugin loader calls ALL exports as functions during initialization.
7
+ * Exporting classes, constants, or non-function values will cause the plugin
8
+ * to fail to load with cryptic errors.
9
+ *
10
+ * If you need to export utilities for external use, add them to src/index.ts instead.
11
+ *
12
+ * @example
13
+ * // ✅ CORRECT - only export the plugin function
14
+ * export default SwarmPlugin;
15
+ *
16
+ * // ❌ WRONG - will break plugin loading
17
+ * export const VERSION = "1.0.0";
18
+ * export class Helper {}
7
19
  */
8
20
  import { SwarmPlugin } from "./index";
9
21
 
@@ -42,19 +42,42 @@ export type BeadDependency = z.infer<typeof BeadDependencySchema>;
42
42
  * - Custom subtask: `{project}-{custom-id}.{suffix}` (e.g., `migrate-egghead-phase-0.e2e-test`)
43
43
  */
44
44
  export const BeadSchema = z.object({
45
+ /**
46
+ * Bead ID format: project-slug-hash with optional subtask index.
47
+ *
48
+ * Pattern: `project-name-xxxxx` or `project-name-xxxxx.N`
49
+ * Examples:
50
+ * - `my-project-abc12` (main bead)
51
+ * - `my-project-abc12.1` (first subtask)
52
+ * - `my-project-abc12.2` (second subtask)
53
+ */
45
54
  id: z
46
55
  .string()
47
- .regex(/^[a-z0-9]+(-[a-z0-9]+)+(\.[\w-]+)?$/, "Invalid bead ID format"),
56
+ .regex(
57
+ /^[a-z0-9]+(-[a-z0-9]+)+(\.[\w-]+)?$/,
58
+ "Invalid bead ID format (expected: project-slug-hash or project-slug-hash.N)",
59
+ ),
48
60
  title: z.string().min(1, "Title required"),
49
61
  description: z.string().optional().default(""),
50
62
  status: BeadStatusSchema.default("open"),
51
63
  priority: z.number().int().min(0).max(3).default(2),
52
64
  issue_type: BeadTypeSchema.default("task"),
53
- created_at: z.string().datetime({ offset: true }), // ISO-8601 with timezone offset
54
- updated_at: z.string().datetime({ offset: true }).optional(),
65
+ created_at: z.string().datetime({
66
+ offset: true,
67
+ message:
68
+ "Must be ISO-8601 datetime with timezone (e.g., 2024-01-15T10:30:00Z)",
69
+ }),
70
+ updated_at: z
71
+ .string()
72
+ .datetime({
73
+ offset: true,
74
+ message:
75
+ "Must be ISO-8601 datetime with timezone (e.g., 2024-01-15T10:30:00Z)",
76
+ })
77
+ .optional(),
55
78
  closed_at: z.string().datetime({ offset: true }).optional(),
56
79
  parent_id: z.string().optional(),
57
- dependencies: z.array(BeadDependencySchema).optional().default([]),
80
+ dependencies: z.array(BeadDependencySchema).default([]),
58
81
  metadata: z.record(z.string(), z.unknown()).optional(),
59
82
  });
60
83
  export type Bead = z.infer<typeof BeadSchema>;
@@ -111,6 +134,14 @@ export const SubtaskSpecSchema = z.object({
111
134
  description: z.string().optional().default(""),
112
135
  files: z.array(z.string()).default([]),
113
136
  dependencies: z.array(z.number().int().min(0)).default([]), // Indices of other subtasks
137
+ /**
138
+ * Complexity estimate on 1-5 scale:
139
+ * 1 = trivial (typo fix, simple rename)
140
+ * 2 = simple (single function change)
141
+ * 3 = moderate (multi-file, some coordination)
142
+ * 4 = complex (significant refactoring)
143
+ * 5 = very complex (architectural change)
144
+ */
114
145
  estimated_complexity: z.number().int().min(1).max(5).default(3),
115
146
  });
116
147
  export type SubtaskSpec = z.infer<typeof SubtaskSpecSchema>;
@@ -12,9 +12,15 @@
12
12
  import { z } from "zod";
13
13
 
14
14
  /**
15
- * Single criterion evaluation
15
+ * Evaluation of a single criterion.
16
16
  *
17
- * Each criterion (type_safe, no_bugs, etc.) gets its own evaluation.
17
+ * @example
18
+ * // Passing criterion
19
+ * { passed: true, feedback: "All types validated", score: 0.95 }
20
+ *
21
+ * @example
22
+ * // Failing criterion
23
+ * { passed: false, feedback: "Missing error handling in auth flow", score: 0.3 }
18
24
  */
19
25
  export const CriterionEvaluationSchema = z.object({
20
26
  passed: z.boolean(),
@@ -31,7 +37,11 @@ export type CriterionEvaluation = z.infer<typeof CriterionEvaluationSchema>;
31
37
  */
32
38
  export const WeightedCriterionEvaluationSchema =
33
39
  CriterionEvaluationSchema.extend({
34
- /** Current weight after decay (0-1, lower = less reliable) */
40
+ /**
41
+ * Current weight after 90-day half-life decay.
42
+ * Range: 0-1 where 1 = recent/validated, 0 = old/unreliable.
43
+ * Weights decay over time unless revalidated via semantic-memory_validate.
44
+ */
35
45
  weight: z.number().min(0).max(1).default(1),
36
46
  /** Weighted score = score * weight */
37
47
  weighted_score: z.number().min(0).max(1).optional(),
@@ -75,9 +85,11 @@ export type DefaultCriterion = (typeof DEFAULT_CRITERIA)[number];
75
85
  * Evaluation request arguments
76
86
  */
77
87
  export const EvaluationRequestSchema = z.object({
78
- subtask_id: z.string(),
79
- criteria: z.array(z.string()).default([...DEFAULT_CRITERIA]),
80
- context: z.string().optional(),
88
+ bead_id: z.string(),
89
+ subtask_title: z.string(),
90
+ files_touched: z.array(z.string()),
91
+ /** ISO-8601 timestamp when evaluation was requested */
92
+ requested_at: z.string().datetime().optional(),
81
93
  });
82
94
  export type EvaluationRequest = z.infer<typeof EvaluationRequestSchema>;
83
95
 
@@ -1,7 +1,30 @@
1
1
  /**
2
- * Schema exports
2
+ * Schema Definitions - Central export point for all Zod schemas
3
3
  *
4
- * Re-export all schemas for convenient importing.
4
+ * This module re-exports all schema definitions used throughout the plugin.
5
+ * Schemas are organized by domain:
6
+ *
7
+ * ## Bead Schemas (Issue Tracking)
8
+ * - `BeadSchema` - Core bead/issue definition
9
+ * - `BeadStatusSchema` - Status enum (open, in_progress, blocked, closed)
10
+ * - `BeadTypeSchema` - Type enum (bug, feature, task, epic, chore)
11
+ * - `SubtaskSpecSchema` - Subtask specification for epic creation
12
+ *
13
+ * ## Task Schemas (Swarm Decomposition)
14
+ * - `TaskDecompositionSchema` - Full task breakdown
15
+ * - `DecomposedSubtaskSchema` - Individual subtask definition
16
+ * - `BeadTreeSchema` - Epic + subtasks structure
17
+ *
18
+ * ## Evaluation Schemas (Agent Self-Assessment)
19
+ * - `EvaluationSchema` - Complete evaluation with criteria
20
+ * - `CriterionEvaluationSchema` - Single criterion result
21
+ *
22
+ * ## Progress Schemas (Swarm Coordination)
23
+ * - `SwarmStatusSchema` - Overall swarm progress
24
+ * - `AgentProgressSchema` - Individual agent status
25
+ * - `SpawnedAgentSchema` - Spawned agent metadata
26
+ *
27
+ * @module schemas
5
28
  */
6
29
 
7
30
  // Bead schemas
@@ -7,13 +7,19 @@
7
7
  import { z } from "zod";
8
8
 
9
9
  /**
10
- * Effort estimation levels
10
+ * Effort estimation for subtasks.
11
+ *
12
+ * Time ranges:
13
+ * - `trivial`: < 5 minutes (simple rename, typo fix)
14
+ * - `small`: 5-30 minutes (single function, simple feature)
15
+ * - `medium`: 30 min - 2 hours (multi-file change, moderate complexity)
16
+ * - `large`: 2+ hours (significant feature, refactoring)
11
17
  */
12
18
  export const EffortLevelSchema = z.enum([
13
- "trivial", // < 5 min
14
- "small", // 5-30 min
15
- "medium", // 30 min - 2 hours
16
- "large", // 2+ hours
19
+ "trivial",
20
+ "small",
21
+ "medium",
22
+ "large",
17
23
  ]);
18
24
  export type EffortLevel = z.infer<typeof EffortLevelSchema>;
19
25
 
@@ -35,6 +41,7 @@ export const DecomposedSubtaskSchema = z.object({
35
41
  description: z.string(),
36
42
  files: z.array(z.string()), // File paths this subtask will modify
37
43
  estimated_effort: EffortLevelSchema,
44
+ /** Potential risks or complications (e.g., 'tight coupling', 'data migration required', 'breaking change') */
38
45
  risks: z.array(z.string()).optional().default([]),
39
46
  });
40
47
  export type DecomposedSubtask = z.infer<typeof DecomposedSubtaskSchema>;
@@ -43,8 +50,10 @@ export type DecomposedSubtask = z.infer<typeof DecomposedSubtaskSchema>;
43
50
  * Dependency between subtasks
44
51
  */
45
52
  export const SubtaskDependencySchema = z.object({
46
- from: z.number().int().min(0), // Subtask index
47
- to: z.number().int().min(0), // Subtask index
53
+ /** Zero-based index of the dependency source subtask */
54
+ from: z.number().int().min(0),
55
+ /** Zero-based index of the dependency target subtask */
56
+ to: z.number().int().min(0),
48
57
  type: DependencyTypeSchema,
49
58
  });
50
59
  export type SubtaskDependency = z.infer<typeof SubtaskDependencySchema>;
@@ -56,10 +65,15 @@ export type SubtaskDependency = z.infer<typeof SubtaskDependencySchema>;
56
65
  */
57
66
  export const TaskDecompositionSchema = z.object({
58
67
  task: z.string(), // Original task description
59
- reasoning: z.string().optional(), // Why this decomposition
68
+ /** Rationale for this decomposition strategy (why these subtasks, why this order) */
69
+ reasoning: z.string().optional(),
60
70
  subtasks: z.array(DecomposedSubtaskSchema).min(1),
61
71
  dependencies: z.array(SubtaskDependencySchema).optional().default([]),
62
- shared_context: z.string().optional(), // Context to pass to all agents
72
+ /**
73
+ * Context shared with all spawned agents.
74
+ * Examples: API contracts, shared types, project conventions, architectural decisions.
75
+ */
76
+ shared_context: z.string().optional(),
63
77
  });
64
78
  export type TaskDecomposition = z.infer<typeof TaskDecompositionSchema>;
65
79
 
@@ -78,11 +92,19 @@ export type DecomposeArgs = z.infer<typeof DecomposeArgsSchema>;
78
92
  */
79
93
  export const SpawnedAgentSchema = z.object({
80
94
  bead_id: z.string(),
81
- agent_name: z.string(), // Agent Mail name (e.g., "BlueLake")
95
+ /**
96
+ * Agent Mail assigned name (e.g., 'BlueLake', 'CrimsonRiver').
97
+ * Generated by Agent Mail on session init.
98
+ */
99
+ agent_name: z.string(),
82
100
  task_id: z.string().optional(), // OpenCode task ID
83
101
  status: z.enum(["pending", "running", "completed", "failed"]),
84
102
  files: z.array(z.string()), // Reserved files
85
- reservation_ids: z.array(z.number()).optional(), // Agent Mail reservation IDs
103
+ /**
104
+ * Agent Mail reservation IDs for file locking.
105
+ * Used to release locks on task completion via agentmail_release.
106
+ */
107
+ reservation_ids: z.array(z.number()).optional(),
86
108
  });
87
109
  export type SpawnedAgent = z.infer<typeof SpawnedAgentSchema>;
88
110
 
@@ -101,16 +123,22 @@ export type SwarmSpawnResult = z.infer<typeof SwarmSpawnResultSchema>;
101
123
  /**
102
124
  * Progress update from an agent
103
125
  */
104
- export const AgentProgressSchema = z.object({
105
- bead_id: z.string(),
106
- agent_name: z.string(),
107
- status: z.enum(["in_progress", "blocked", "completed", "failed"]),
108
- progress_percent: z.number().min(0).max(100).optional(),
109
- message: z.string().optional(),
110
- files_touched: z.array(z.string()).optional(),
111
- blockers: z.array(z.string()).optional(),
112
- timestamp: z.string().datetime({ offset: true }), // ISO-8601 with timezone
113
- });
126
+ export const AgentProgressSchema = z
127
+ .object({
128
+ bead_id: z.string(),
129
+ agent_name: z.string(),
130
+ status: z.enum(["in_progress", "blocked", "completed", "failed"]),
131
+ progress_percent: z.number().min(0).max(100).optional(),
132
+ message: z.string().optional(),
133
+ files_touched: z.array(z.string()).optional(),
134
+ blockers: z.array(z.string()).optional(),
135
+ timestamp: z.string().datetime({ offset: true }), // ISO-8601 with timezone
136
+ })
137
+ .refine(
138
+ (data) =>
139
+ data.status !== "blocked" || (data.blockers && data.blockers.length > 0),
140
+ { message: "blockers array required when status is 'blocked'" },
141
+ );
114
142
  export type AgentProgress = z.infer<typeof AgentProgressSchema>;
115
143
 
116
144
  /**
@@ -5,7 +5,7 @@
5
5
  * Useful for debugging issues and understanding system behavior.
6
6
  */
7
7
  import { getDatabase, getDatabaseStats } from "./index";
8
- import { readEvents, getLatestSequence } from "./store";
8
+ import { readEvents, getLatestSequence, replayEventsBatched } from "./store";
9
9
  import { getAgent, getActiveReservations, getMessage } from "./projections";
10
10
  import type { AgentEvent } from "./events";
11
11
 
@@ -231,11 +231,27 @@ function getAgentFromEvent(event: AgentEvent): string {
231
231
 
232
232
  /**
233
233
  * Get recent events with filtering
234
+ *
235
+ * For large event logs (>100k events), consider using batchSize option
236
+ * to paginate through results instead of loading all events.
234
237
  */
235
238
  export async function debugEvents(
236
- options: DebugEventsOptions,
239
+ options: DebugEventsOptions & { batchSize?: number },
237
240
  ): Promise<DebugEventsResult> {
238
- const { projectPath, types, agentName, limit = 50, since, until } = options;
241
+ const {
242
+ projectPath,
243
+ types,
244
+ agentName,
245
+ limit = 50,
246
+ since,
247
+ until,
248
+ batchSize,
249
+ } = options;
250
+
251
+ // If batchSize is specified, use pagination to avoid OOM
252
+ if (batchSize && batchSize > 0) {
253
+ return await debugEventsPaginated({ ...options, batchSize });
254
+ }
239
255
 
240
256
  // Get all events first (we'll filter in memory for agent name)
241
257
  const allEvents = await readEvents(
@@ -284,6 +300,88 @@ export async function debugEvents(
284
300
  };
285
301
  }
286
302
 
303
+ /**
304
+ * Get events using pagination to avoid OOM on large logs
305
+ */
306
+ async function debugEventsPaginated(
307
+ options: DebugEventsOptions & { batchSize: number },
308
+ ): Promise<DebugEventsResult> {
309
+ const {
310
+ projectPath,
311
+ types,
312
+ agentName,
313
+ limit = 50,
314
+ since,
315
+ until,
316
+ batchSize,
317
+ } = options;
318
+
319
+ const allEvents: Array<AgentEvent & { id: number; sequence: number }> = [];
320
+ let offset = 0;
321
+ let hasMore = true;
322
+
323
+ // Fetch in batches until we have enough events or run out
324
+ while (hasMore && allEvents.length < limit) {
325
+ const batch = await readEvents(
326
+ {
327
+ projectKey: projectPath,
328
+ types,
329
+ since,
330
+ until,
331
+ limit: batchSize,
332
+ offset,
333
+ },
334
+ projectPath,
335
+ );
336
+
337
+ if (batch.length === 0) {
338
+ hasMore = false;
339
+ break;
340
+ }
341
+
342
+ // Filter by agent name if specified
343
+ const filtered = agentName
344
+ ? batch.filter((e) => {
345
+ if ("agent_name" in e && e.agent_name === agentName) return true;
346
+ if ("from_agent" in e && e.from_agent === agentName) return true;
347
+ if ("to_agents" in e && e.to_agents?.includes(agentName)) return true;
348
+ return false;
349
+ })
350
+ : batch;
351
+
352
+ allEvents.push(...filtered);
353
+ offset += batchSize;
354
+
355
+ console.log(
356
+ `[SwarmMail] Fetched ${allEvents.length} events (batch size: ${batchSize})`,
357
+ );
358
+ }
359
+
360
+ // Sort by sequence descending (most recent first)
361
+ allEvents.sort((a, b) => b.sequence - a.sequence);
362
+
363
+ // Apply limit
364
+ const limitedEvents = allEvents.slice(0, limit);
365
+
366
+ // Format for output
367
+ const events: DebugEventResult[] = limitedEvents.map((e) => {
368
+ const { id, sequence, type, timestamp, project_key, ...rest } = e;
369
+ return {
370
+ id,
371
+ sequence,
372
+ type,
373
+ timestamp,
374
+ timestamp_human: formatTimestamp(timestamp),
375
+ ...rest,
376
+ };
377
+ });
378
+
379
+ return {
380
+ events,
381
+ total: allEvents.length,
382
+ };
383
+ }
384
+
287
385
  /**
288
386
  * Get detailed agent information
289
387
  */
@@ -174,6 +174,28 @@ export type TaskProgressEvent = z.infer<typeof TaskProgressEventSchema>;
174
174
  export type TaskCompletedEvent = z.infer<typeof TaskCompletedEventSchema>;
175
175
  export type TaskBlockedEvent = z.infer<typeof TaskBlockedEventSchema>;
176
176
 
177
+ // ============================================================================
178
+ // Session State Types
179
+ // ============================================================================
180
+
181
+ /**
182
+ * Shared session state for Agent Mail and Swarm Mail
183
+ *
184
+ * Common fields for tracking agent coordination session across both
185
+ * the MCP-based implementation (agent-mail) and the embedded event-sourced
186
+ * implementation (swarm-mail).
187
+ */
188
+ export interface MailSessionState {
189
+ /** Project key (usually absolute path) */
190
+ projectKey: string;
191
+ /** Agent name for this session */
192
+ agentName: string;
193
+ /** Active reservation IDs */
194
+ reservations: number[];
195
+ /** Session start timestamp (ISO-8601) */
196
+ startedAt: string;
197
+ }
198
+
177
199
  // ============================================================================
178
200
  // Event Helpers
179
201
  // ============================================================================
@@ -1,5 +1,30 @@
1
1
  /**
2
- * PGLite Event Store Setup
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
  // ============================================================================