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.
- package/.beads/issues.jsonl +74 -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 +20 -7
- package/src/anti-patterns.test.ts +1167 -0
- package/src/anti-patterns.ts +29 -11
- package/src/index.ts +8 -0
- package/src/pattern-maturity.test.ts +1160 -0
- 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/events.ts +22 -0
- 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-mail.ts +7 -7
- 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/pattern-maturity.ts
CHANGED
|
@@ -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
|
-
|
|
168
|
-
const safeTotal = total >
|
|
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 -
|
|
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 -
|
|
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 -
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
340
|
+
* Get weight multiplier based on pattern maturity status.
|
|
313
341
|
*
|
|
314
|
-
*
|
|
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 -
|
|
317
|
-
* @returns
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
package/src/schemas/bead.ts
CHANGED
|
@@ -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(
|
|
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({
|
|
54
|
-
|
|
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).
|
|
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
|
-
*
|
|
15
|
+
* Evaluation of a single criterion.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
package/src/schemas/index.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Schema
|
|
2
|
+
* Schema Definitions - Central export point for all Zod schemas
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
package/src/schemas/task.ts
CHANGED
|
@@ -7,13 +7,19 @@
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Effort estimation
|
|
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",
|
|
14
|
-
"small",
|
|
15
|
-
"medium",
|
|
16
|
-
"large",
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
/**
|
package/src/streams/debug.ts
CHANGED
|
@@ -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 {
|
|
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
|
*/
|
package/src/streams/events.ts
CHANGED
|
@@ -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
|
// ============================================================================
|
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
|
// ============================================================================
|