opencode-swarm-plugin 0.12.4 → 0.12.7
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 +224 -0
- package/README.md +94 -106
- package/bin/swarm.ts +6 -7
- package/dist/index.js +409 -71
- package/dist/plugin.js +376 -58
- package/examples/commands/swarm.md +51 -216
- package/package.json +1 -1
- package/src/agent-mail.ts +183 -37
- package/src/beads.ts +75 -20
- package/src/learning.ts +277 -0
- package/src/rate-limiter.ts +12 -6
- package/src/schemas/bead.ts +4 -4
- package/src/schemas/evaluation.ts +2 -2
- package/src/schemas/task.ts +3 -3
- package/src/storage.ts +37 -15
- package/src/swarm.ts +189 -1
- package/examples/agents/swarm-planner.md +0 -138
package/src/beads.ts
CHANGED
|
@@ -55,6 +55,10 @@ export class BeadValidationError extends Error {
|
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Build a bd create command from args
|
|
58
|
+
*
|
|
59
|
+
* Note: Bun's `$` template literal properly escapes arguments when passed as array.
|
|
60
|
+
* Each array element is treated as a separate argument, preventing shell injection.
|
|
61
|
+
* Example: ["bd", "create", "; rm -rf /"] becomes: bd create "; rm -rf /"
|
|
58
62
|
*/
|
|
59
63
|
function buildCreateCommand(args: BeadCreateArgs): string[] {
|
|
60
64
|
const parts = ["bd", "create", args.title];
|
|
@@ -250,25 +254,40 @@ export const beads_create_epic = tool({
|
|
|
250
254
|
|
|
251
255
|
return JSON.stringify(result, null, 2);
|
|
252
256
|
} catch (error) {
|
|
253
|
-
// Partial failure -
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
// Partial failure - execute rollback automatically
|
|
258
|
+
const rollbackCommands: string[] = [];
|
|
259
|
+
|
|
260
|
+
for (const bead of created) {
|
|
261
|
+
try {
|
|
262
|
+
const closeCmd = [
|
|
263
|
+
"bd",
|
|
264
|
+
"close",
|
|
265
|
+
bead.id,
|
|
266
|
+
"--reason",
|
|
267
|
+
"Rollback partial epic",
|
|
268
|
+
"--json",
|
|
269
|
+
];
|
|
270
|
+
await Bun.$`${closeCmd}`.quiet().nothrow();
|
|
271
|
+
rollbackCommands.push(
|
|
272
|
+
`bd close ${bead.id} --reason "Rollback partial epic"`,
|
|
273
|
+
);
|
|
274
|
+
} catch (rollbackError) {
|
|
275
|
+
// Log rollback failure but continue
|
|
276
|
+
console.error(`Failed to rollback bead ${bead.id}:`, rollbackError);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
257
279
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
280
|
+
// Throw error with rollback info
|
|
281
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
282
|
+
const rollbackInfo =
|
|
283
|
+
rollbackCommands.length > 0
|
|
284
|
+
? `\n\nRolled back ${rollbackCommands.length} bead(s):\n${rollbackCommands.join("\n")}`
|
|
285
|
+
: "\n\nNo beads to rollback.";
|
|
264
286
|
|
|
265
|
-
|
|
266
|
-
{
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
},
|
|
270
|
-
null,
|
|
271
|
-
2,
|
|
287
|
+
throw new BeadError(
|
|
288
|
+
`Epic creation failed: ${errorMsg}${rollbackInfo}`,
|
|
289
|
+
"beads_create_epic",
|
|
290
|
+
1,
|
|
272
291
|
);
|
|
273
292
|
}
|
|
274
293
|
},
|
|
@@ -487,10 +506,38 @@ export const beads_sync = tool({
|
|
|
487
506
|
},
|
|
488
507
|
async execute(args, ctx) {
|
|
489
508
|
const autoPull = args.auto_pull ?? true;
|
|
509
|
+
const TIMEOUT_MS = 30000; // 30 seconds
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Helper to run a command with timeout
|
|
513
|
+
*/
|
|
514
|
+
const withTimeout = async <T>(
|
|
515
|
+
promise: Promise<T>,
|
|
516
|
+
timeoutMs: number,
|
|
517
|
+
operation: string,
|
|
518
|
+
): Promise<T> => {
|
|
519
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
520
|
+
setTimeout(
|
|
521
|
+
() =>
|
|
522
|
+
reject(
|
|
523
|
+
new BeadError(
|
|
524
|
+
`Operation timed out after ${timeoutMs}ms`,
|
|
525
|
+
operation,
|
|
526
|
+
),
|
|
527
|
+
),
|
|
528
|
+
timeoutMs,
|
|
529
|
+
),
|
|
530
|
+
);
|
|
531
|
+
return Promise.race([promise, timeoutPromise]);
|
|
532
|
+
};
|
|
490
533
|
|
|
491
534
|
// 1. Pull if requested
|
|
492
535
|
if (autoPull) {
|
|
493
|
-
const pullResult = await
|
|
536
|
+
const pullResult = await withTimeout(
|
|
537
|
+
Bun.$`git pull --rebase`.quiet().nothrow(),
|
|
538
|
+
TIMEOUT_MS,
|
|
539
|
+
"git pull --rebase",
|
|
540
|
+
);
|
|
494
541
|
if (pullResult.exitCode !== 0) {
|
|
495
542
|
throw new BeadError(
|
|
496
543
|
`Failed to pull: ${pullResult.stderr.toString()}`,
|
|
@@ -501,7 +548,11 @@ export const beads_sync = tool({
|
|
|
501
548
|
}
|
|
502
549
|
|
|
503
550
|
// 2. Sync beads
|
|
504
|
-
const syncResult = await
|
|
551
|
+
const syncResult = await withTimeout(
|
|
552
|
+
Bun.$`bd sync`.quiet().nothrow(),
|
|
553
|
+
TIMEOUT_MS,
|
|
554
|
+
"bd sync",
|
|
555
|
+
);
|
|
505
556
|
if (syncResult.exitCode !== 0) {
|
|
506
557
|
throw new BeadError(
|
|
507
558
|
`Failed to sync beads: ${syncResult.stderr.toString()}`,
|
|
@@ -511,7 +562,11 @@ export const beads_sync = tool({
|
|
|
511
562
|
}
|
|
512
563
|
|
|
513
564
|
// 3. Push
|
|
514
|
-
const pushResult = await
|
|
565
|
+
const pushResult = await withTimeout(
|
|
566
|
+
Bun.$`git push`.quiet().nothrow(),
|
|
567
|
+
TIMEOUT_MS,
|
|
568
|
+
"git push",
|
|
569
|
+
);
|
|
515
570
|
if (pushResult.exitCode !== 0) {
|
|
516
571
|
throw new BeadError(
|
|
517
572
|
`Failed to push: ${pushResult.stderr.toString()}`,
|
package/src/learning.ts
CHANGED
|
@@ -64,6 +64,46 @@ export const CriterionWeightSchema = z.object({
|
|
|
64
64
|
});
|
|
65
65
|
export type CriterionWeight = z.infer<typeof CriterionWeightSchema>;
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Error types that can occur during subtask execution
|
|
69
|
+
*/
|
|
70
|
+
export const ErrorTypeSchema = z.enum([
|
|
71
|
+
"validation",
|
|
72
|
+
"timeout",
|
|
73
|
+
"conflict",
|
|
74
|
+
"tool_failure",
|
|
75
|
+
"unknown",
|
|
76
|
+
]);
|
|
77
|
+
export type ErrorType = z.infer<typeof ErrorTypeSchema>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* An error entry in the error accumulator
|
|
81
|
+
*
|
|
82
|
+
* Errors are accumulated during subtask execution and can be fed
|
|
83
|
+
* into retry prompts to help agents learn from past failures.
|
|
84
|
+
*/
|
|
85
|
+
export const ErrorEntrySchema = z.object({
|
|
86
|
+
/** Unique ID for this error entry */
|
|
87
|
+
id: z.string(),
|
|
88
|
+
/** The bead ID this error relates to */
|
|
89
|
+
bead_id: z.string(),
|
|
90
|
+
/** Type of error encountered */
|
|
91
|
+
error_type: ErrorTypeSchema,
|
|
92
|
+
/** Human-readable error message */
|
|
93
|
+
message: z.string(),
|
|
94
|
+
/** Optional stack trace for debugging */
|
|
95
|
+
stack_trace: z.string().optional(),
|
|
96
|
+
/** Tool that failed, if applicable */
|
|
97
|
+
tool_name: z.string().optional(),
|
|
98
|
+
/** When this error occurred */
|
|
99
|
+
timestamp: z.string(), // ISO-8601
|
|
100
|
+
/** Whether this error was resolved */
|
|
101
|
+
resolved: z.boolean().default(false),
|
|
102
|
+
/** Context about what was happening when error occurred */
|
|
103
|
+
context: z.string().optional(),
|
|
104
|
+
});
|
|
105
|
+
export type ErrorEntry = z.infer<typeof ErrorEntrySchema>;
|
|
106
|
+
|
|
67
107
|
/**
|
|
68
108
|
* Decomposition strategies for tracking which approach was used
|
|
69
109
|
*/
|
|
@@ -437,6 +477,241 @@ export class InMemoryFeedbackStorage implements FeedbackStorage {
|
|
|
437
477
|
}
|
|
438
478
|
}
|
|
439
479
|
|
|
480
|
+
// ============================================================================
|
|
481
|
+
// Error Accumulator
|
|
482
|
+
// ============================================================================
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Storage interface for error entries
|
|
486
|
+
*
|
|
487
|
+
* Similar to FeedbackStorage but for tracking errors during execution.
|
|
488
|
+
*/
|
|
489
|
+
export interface ErrorStorage {
|
|
490
|
+
/** Store an error entry */
|
|
491
|
+
store(entry: ErrorEntry): Promise<void>;
|
|
492
|
+
/** Get all errors for a bead */
|
|
493
|
+
getByBead(beadId: string): Promise<ErrorEntry[]>;
|
|
494
|
+
/** Get unresolved errors for a bead */
|
|
495
|
+
getUnresolvedByBead(beadId: string): Promise<ErrorEntry[]>;
|
|
496
|
+
/** Mark an error as resolved */
|
|
497
|
+
markResolved(id: string): Promise<void>;
|
|
498
|
+
/** Get all errors */
|
|
499
|
+
getAll(): Promise<ErrorEntry[]>;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* In-memory error storage
|
|
504
|
+
*
|
|
505
|
+
* Accumulates errors during subtask execution for feeding into retry prompts.
|
|
506
|
+
*/
|
|
507
|
+
export class InMemoryErrorStorage implements ErrorStorage {
|
|
508
|
+
private errors: ErrorEntry[] = [];
|
|
509
|
+
|
|
510
|
+
async store(entry: ErrorEntry): Promise<void> {
|
|
511
|
+
this.errors.push(entry);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async getByBead(beadId: string): Promise<ErrorEntry[]> {
|
|
515
|
+
return this.errors.filter((e) => e.bead_id === beadId);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async getUnresolvedByBead(beadId: string): Promise<ErrorEntry[]> {
|
|
519
|
+
return this.errors.filter((e) => e.bead_id === beadId && !e.resolved);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async markResolved(id: string): Promise<void> {
|
|
523
|
+
const error = this.errors.find((e) => e.id === id);
|
|
524
|
+
if (error) {
|
|
525
|
+
error.resolved = true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async getAll(): Promise<ErrorEntry[]> {
|
|
530
|
+
return [...this.errors];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Error accumulator for tracking errors during subtask execution
|
|
536
|
+
*
|
|
537
|
+
* Implements patterns from "Patterns for Building AI Agents" p.40:
|
|
538
|
+
* - Examines and corrects errors when something goes wrong
|
|
539
|
+
* - Feeds error context into retry prompts
|
|
540
|
+
* - Tracks error patterns for learning
|
|
541
|
+
*/
|
|
542
|
+
export class ErrorAccumulator {
|
|
543
|
+
private storage: ErrorStorage;
|
|
544
|
+
|
|
545
|
+
constructor(storage?: ErrorStorage) {
|
|
546
|
+
this.storage = storage ?? new InMemoryErrorStorage();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Record an error during subtask execution
|
|
551
|
+
*
|
|
552
|
+
* @param beadId - Bead ID where error occurred
|
|
553
|
+
* @param errorType - Category of error
|
|
554
|
+
* @param message - Human-readable error message
|
|
555
|
+
* @param options - Additional context (stack trace, tool name, etc.)
|
|
556
|
+
* @returns The created error entry
|
|
557
|
+
*/
|
|
558
|
+
async recordError(
|
|
559
|
+
beadId: string,
|
|
560
|
+
errorType: ErrorType,
|
|
561
|
+
message: string,
|
|
562
|
+
options?: {
|
|
563
|
+
stack_trace?: string;
|
|
564
|
+
tool_name?: string;
|
|
565
|
+
context?: string;
|
|
566
|
+
},
|
|
567
|
+
): Promise<ErrorEntry> {
|
|
568
|
+
const entry: ErrorEntry = {
|
|
569
|
+
id: `${beadId}-${errorType}-${Date.now()}`,
|
|
570
|
+
bead_id: beadId,
|
|
571
|
+
error_type: errorType,
|
|
572
|
+
message,
|
|
573
|
+
stack_trace: options?.stack_trace,
|
|
574
|
+
tool_name: options?.tool_name,
|
|
575
|
+
timestamp: new Date().toISOString(),
|
|
576
|
+
resolved: false,
|
|
577
|
+
context: options?.context,
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const validated = ErrorEntrySchema.parse(entry);
|
|
581
|
+
await this.storage.store(validated);
|
|
582
|
+
|
|
583
|
+
return validated;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get all errors for a bead (resolved and unresolved)
|
|
588
|
+
*/
|
|
589
|
+
async getErrors(beadId: string): Promise<ErrorEntry[]> {
|
|
590
|
+
return this.storage.getByBead(beadId);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Get only unresolved errors for a bead
|
|
595
|
+
*/
|
|
596
|
+
async getUnresolvedErrors(beadId: string): Promise<ErrorEntry[]> {
|
|
597
|
+
return this.storage.getUnresolvedByBead(beadId);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Mark an error as resolved
|
|
602
|
+
*/
|
|
603
|
+
async resolveError(errorId: string): Promise<void> {
|
|
604
|
+
await this.storage.markResolved(errorId);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Format errors as context for retry prompts
|
|
609
|
+
*
|
|
610
|
+
* Groups errors by type and provides structured feedback
|
|
611
|
+
* for the agent to learn from.
|
|
612
|
+
*
|
|
613
|
+
* @param beadId - Bead to get error context for
|
|
614
|
+
* @param includeResolved - Include resolved errors (default: false)
|
|
615
|
+
* @returns Formatted error context string
|
|
616
|
+
*/
|
|
617
|
+
async getErrorContext(
|
|
618
|
+
beadId: string,
|
|
619
|
+
includeResolved = false,
|
|
620
|
+
): Promise<string> {
|
|
621
|
+
const errors = includeResolved
|
|
622
|
+
? await this.getErrors(beadId)
|
|
623
|
+
: await this.getUnresolvedErrors(beadId);
|
|
624
|
+
|
|
625
|
+
if (errors.length === 0) {
|
|
626
|
+
return "";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Group errors by type
|
|
630
|
+
const byType = errors.reduce(
|
|
631
|
+
(acc, err) => {
|
|
632
|
+
const type = err.error_type;
|
|
633
|
+
if (!acc[type]) {
|
|
634
|
+
acc[type] = [];
|
|
635
|
+
}
|
|
636
|
+
acc[type].push(err);
|
|
637
|
+
return acc;
|
|
638
|
+
},
|
|
639
|
+
{} as Record<ErrorType, ErrorEntry[]>,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// Format as structured feedback
|
|
643
|
+
const lines = [
|
|
644
|
+
"## Previous Errors",
|
|
645
|
+
"",
|
|
646
|
+
"The following errors were encountered during execution:",
|
|
647
|
+
"",
|
|
648
|
+
];
|
|
649
|
+
|
|
650
|
+
for (const [type, typeErrors] of Object.entries(byType)) {
|
|
651
|
+
lines.push(
|
|
652
|
+
`### ${type} (${typeErrors.length} error${typeErrors.length > 1 ? "s" : ""})`,
|
|
653
|
+
);
|
|
654
|
+
lines.push("");
|
|
655
|
+
|
|
656
|
+
for (const err of typeErrors) {
|
|
657
|
+
lines.push(`- **${err.message}**`);
|
|
658
|
+
if (err.context) {
|
|
659
|
+
lines.push(` - Context: ${err.context}`);
|
|
660
|
+
}
|
|
661
|
+
if (err.tool_name) {
|
|
662
|
+
lines.push(` - Tool: ${err.tool_name}`);
|
|
663
|
+
}
|
|
664
|
+
if (err.stack_trace) {
|
|
665
|
+
lines.push(` - Stack: \`${err.stack_trace.slice(0, 100)}...\``);
|
|
666
|
+
}
|
|
667
|
+
lines.push(
|
|
668
|
+
` - Time: ${new Date(err.timestamp).toLocaleString()}${err.resolved ? " (resolved)" : ""}`,
|
|
669
|
+
);
|
|
670
|
+
lines.push("");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
lines.push(
|
|
675
|
+
"**Action Required**: Address these errors before proceeding. Consider:",
|
|
676
|
+
);
|
|
677
|
+
lines.push("- What caused each error?");
|
|
678
|
+
lines.push("- How can you prevent similar errors?");
|
|
679
|
+
lines.push("- Are there patterns across error types?");
|
|
680
|
+
lines.push("");
|
|
681
|
+
|
|
682
|
+
return lines.join("\n");
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Get error statistics for outcome tracking
|
|
687
|
+
*
|
|
688
|
+
* @param beadId - Bead to get stats for
|
|
689
|
+
* @returns Error counts and patterns
|
|
690
|
+
*/
|
|
691
|
+
async getErrorStats(beadId: string): Promise<{
|
|
692
|
+
total: number;
|
|
693
|
+
unresolved: number;
|
|
694
|
+
by_type: Record<ErrorType, number>;
|
|
695
|
+
}> {
|
|
696
|
+
const allErrors = await this.getErrors(beadId);
|
|
697
|
+
const unresolved = await this.getUnresolvedErrors(beadId);
|
|
698
|
+
|
|
699
|
+
const byType = allErrors.reduce(
|
|
700
|
+
(acc, err) => {
|
|
701
|
+
acc[err.error_type] = (acc[err.error_type] || 0) + 1;
|
|
702
|
+
return acc;
|
|
703
|
+
},
|
|
704
|
+
{} as Record<ErrorType, number>,
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
total: allErrors.length,
|
|
709
|
+
unresolved: unresolved.length,
|
|
710
|
+
by_type: byType,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
440
715
|
// ============================================================================
|
|
441
716
|
// Exports
|
|
442
717
|
// ============================================================================
|
|
@@ -448,4 +723,6 @@ export const learningSchemas = {
|
|
|
448
723
|
OutcomeSignalsSchema,
|
|
449
724
|
ScoredOutcomeSchema,
|
|
450
725
|
DecompositionStrategySchema,
|
|
726
|
+
ErrorTypeSchema,
|
|
727
|
+
ErrorEntrySchema,
|
|
451
728
|
};
|
package/src/rate-limiter.ts
CHANGED
|
@@ -122,9 +122,15 @@ export function getLimitsForEndpoint(endpoint: string): EndpointLimits {
|
|
|
122
122
|
const perHourEnv =
|
|
123
123
|
process.env[`OPENCODE_RATE_LIMIT_${upperEndpoint}_PER_HOUR`];
|
|
124
124
|
|
|
125
|
+
// Parse and validate env vars, fall back to defaults on NaN
|
|
126
|
+
const parsedPerMinute = perMinuteEnv ? parseInt(perMinuteEnv, 10) : NaN;
|
|
127
|
+
const parsedPerHour = perHourEnv ? parseInt(perHourEnv, 10) : NaN;
|
|
128
|
+
|
|
125
129
|
return {
|
|
126
|
-
perMinute:
|
|
127
|
-
|
|
130
|
+
perMinute: Number.isNaN(parsedPerMinute)
|
|
131
|
+
? defaults.perMinute
|
|
132
|
+
: parsedPerMinute,
|
|
133
|
+
perHour: Number.isNaN(parsedPerHour) ? defaults.perHour : parsedPerHour,
|
|
128
134
|
};
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -211,10 +217,11 @@ export class RedisRateLimiter implements RateLimiter {
|
|
|
211
217
|
const windowDuration = this.getWindowDuration(window);
|
|
212
218
|
const windowStart = now - windowDuration;
|
|
213
219
|
|
|
214
|
-
// Remove expired entries
|
|
220
|
+
// Remove expired entries, count current ones, and fetch oldest in a single pipeline
|
|
215
221
|
const pipeline = this.redis.pipeline();
|
|
216
222
|
pipeline.zremrangebyscore(key, 0, windowStart);
|
|
217
223
|
pipeline.zcard(key);
|
|
224
|
+
pipeline.zrange(key, 0, 0, "WITHSCORES"); // Fetch oldest entry atomically
|
|
218
225
|
|
|
219
226
|
const results = await pipeline.exec();
|
|
220
227
|
if (!results) {
|
|
@@ -225,11 +232,10 @@ export class RedisRateLimiter implements RateLimiter {
|
|
|
225
232
|
const remaining = Math.max(0, limit - count);
|
|
226
233
|
const allowed = count < limit;
|
|
227
234
|
|
|
228
|
-
// Calculate reset time based on oldest entry in window
|
|
235
|
+
// Calculate reset time based on oldest entry in window (fetched atomically)
|
|
229
236
|
let resetAt = now + windowDuration;
|
|
230
237
|
if (!allowed) {
|
|
231
|
-
|
|
232
|
-
const oldest = await this.redis.zrange(key, 0, 0, "WITHSCORES");
|
|
238
|
+
const oldest = (results[2]?.[1] as string[]) || [];
|
|
233
239
|
if (oldest.length >= 2) {
|
|
234
240
|
const oldestTimestamp = parseInt(oldest[1], 10);
|
|
235
241
|
resetAt = oldestTimestamp + windowDuration;
|
package/src/schemas/bead.ts
CHANGED
|
@@ -42,15 +42,15 @@ export type BeadDependency = z.infer<typeof BeadDependencySchema>;
|
|
|
42
42
|
export const BeadSchema = z.object({
|
|
43
43
|
id: z
|
|
44
44
|
.string()
|
|
45
|
-
.regex(/^[a-z0-9-
|
|
45
|
+
.regex(/^[a-z0-9]+(-[a-z0-9]+)+(\.\d+)?$/, "Invalid bead ID format"),
|
|
46
46
|
title: z.string().min(1, "Title required"),
|
|
47
47
|
description: z.string().optional().default(""),
|
|
48
48
|
status: BeadStatusSchema.default("open"),
|
|
49
49
|
priority: z.number().int().min(0).max(3).default(2),
|
|
50
50
|
issue_type: BeadTypeSchema.default("task"),
|
|
51
|
-
created_at: z.string(), // ISO-8601
|
|
52
|
-
updated_at: z.string().optional(),
|
|
53
|
-
closed_at: z.string().optional(),
|
|
51
|
+
created_at: z.string().datetime({ offset: true }), // ISO-8601 with timezone offset
|
|
52
|
+
updated_at: z.string().datetime({ offset: true }).optional(),
|
|
53
|
+
closed_at: z.string().datetime({ offset: true }).optional(),
|
|
54
54
|
parent_id: z.string().optional(),
|
|
55
55
|
dependencies: z.array(BeadDependencySchema).optional().default([]),
|
|
56
56
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -53,7 +53,7 @@ export const EvaluationSchema = z.object({
|
|
|
53
53
|
criteria: z.record(z.string(), CriterionEvaluationSchema),
|
|
54
54
|
overall_feedback: z.string(),
|
|
55
55
|
retry_suggestion: z.string().nullable(),
|
|
56
|
-
timestamp: z.string().optional(), // ISO-8601
|
|
56
|
+
timestamp: z.string().datetime({ offset: true }).optional(), // ISO-8601 with timezone
|
|
57
57
|
});
|
|
58
58
|
export type Evaluation = z.infer<typeof EvaluationSchema>;
|
|
59
59
|
|
|
@@ -91,7 +91,7 @@ export const WeightedEvaluationSchema = z.object({
|
|
|
91
91
|
criteria: z.record(z.string(), WeightedCriterionEvaluationSchema),
|
|
92
92
|
overall_feedback: z.string(),
|
|
93
93
|
retry_suggestion: z.string().nullable(),
|
|
94
|
-
timestamp: z.string().optional(), // ISO-8601
|
|
94
|
+
timestamp: z.string().datetime({ offset: true }).optional(), // ISO-8601 with timezone
|
|
95
95
|
/** Average weight across all criteria (indicates overall confidence) */
|
|
96
96
|
average_weight: z.number().min(0).max(1).optional(),
|
|
97
97
|
/** Raw score before weighting */
|
package/src/schemas/task.ts
CHANGED
|
@@ -94,7 +94,7 @@ export const SwarmSpawnResultSchema = z.object({
|
|
|
94
94
|
coordinator_name: z.string(), // Agent Mail name of coordinator
|
|
95
95
|
thread_id: z.string(), // Agent Mail thread for this swarm
|
|
96
96
|
agents: z.array(SpawnedAgentSchema),
|
|
97
|
-
started_at: z.string(), // ISO-8601
|
|
97
|
+
started_at: z.string().datetime({ offset: true }), // ISO-8601 with timezone
|
|
98
98
|
});
|
|
99
99
|
export type SwarmSpawnResult = z.infer<typeof SwarmSpawnResultSchema>;
|
|
100
100
|
|
|
@@ -109,7 +109,7 @@ export const AgentProgressSchema = z.object({
|
|
|
109
109
|
message: z.string().optional(),
|
|
110
110
|
files_touched: z.array(z.string()).optional(),
|
|
111
111
|
blockers: z.array(z.string()).optional(),
|
|
112
|
-
timestamp: z.string(), // ISO-8601
|
|
112
|
+
timestamp: z.string().datetime({ offset: true }), // ISO-8601 with timezone
|
|
113
113
|
});
|
|
114
114
|
export type AgentProgress = z.infer<typeof AgentProgressSchema>;
|
|
115
115
|
|
|
@@ -124,6 +124,6 @@ export const SwarmStatusSchema = z.object({
|
|
|
124
124
|
failed: z.number().int().min(0),
|
|
125
125
|
blocked: z.number().int().min(0),
|
|
126
126
|
agents: z.array(SpawnedAgentSchema),
|
|
127
|
-
last_update: z.string(), // ISO-8601
|
|
127
|
+
last_update: z.string().datetime({ offset: true }), // ISO-8601 with timezone
|
|
128
128
|
});
|
|
129
129
|
export type SwarmStatus = z.infer<typeof SwarmStatusSchema>;
|
package/src/storage.ts
CHANGED
|
@@ -73,20 +73,35 @@ async function resolveSemanticMemoryCommand(): Promise<string[]> {
|
|
|
73
73
|
async function execSemanticMemory(
|
|
74
74
|
args: string[],
|
|
75
75
|
): Promise<{ exitCode: number; stdout: Buffer; stderr: Buffer }> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Use Bun.spawn for dynamic command arrays
|
|
80
|
-
const proc = Bun.spawn(fullCmd, {
|
|
81
|
-
stdout: "pipe",
|
|
82
|
-
stderr: "pipe",
|
|
83
|
-
});
|
|
76
|
+
try {
|
|
77
|
+
const cmd = await resolveSemanticMemoryCommand();
|
|
78
|
+
const fullCmd = [...cmd, ...args];
|
|
84
79
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
// Use Bun.spawn for dynamic command arrays
|
|
81
|
+
const proc = Bun.spawn(fullCmd, {
|
|
82
|
+
stdout: "pipe",
|
|
83
|
+
stderr: "pipe",
|
|
84
|
+
});
|
|
88
85
|
|
|
89
|
-
|
|
86
|
+
try {
|
|
87
|
+
const stdout = Buffer.from(await new Response(proc.stdout).arrayBuffer());
|
|
88
|
+
const stderr = Buffer.from(await new Response(proc.stderr).arrayBuffer());
|
|
89
|
+
const exitCode = await proc.exited;
|
|
90
|
+
|
|
91
|
+
return { exitCode, stdout, stderr };
|
|
92
|
+
} finally {
|
|
93
|
+
// Ensure process cleanup
|
|
94
|
+
proc.kill();
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Return structured error result on exceptions
|
|
98
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
99
|
+
return {
|
|
100
|
+
exitCode: 1,
|
|
101
|
+
stdout: Buffer.from(""),
|
|
102
|
+
stderr: Buffer.from(`Error executing semantic-memory: ${errorMessage}`),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
/**
|
|
@@ -646,17 +661,22 @@ export async function createStorageWithFallback(
|
|
|
646
661
|
// ============================================================================
|
|
647
662
|
|
|
648
663
|
let globalStorage: LearningStorage | null = null;
|
|
664
|
+
let globalStoragePromise: Promise<LearningStorage> | null = null;
|
|
649
665
|
|
|
650
666
|
/**
|
|
651
667
|
* Get or create the global storage instance
|
|
652
668
|
*
|
|
653
669
|
* Uses semantic-memory by default, with automatic fallback to in-memory.
|
|
670
|
+
* Prevents race conditions by storing the initialization promise.
|
|
654
671
|
*/
|
|
655
672
|
export async function getStorage(): Promise<LearningStorage> {
|
|
656
|
-
if (!
|
|
657
|
-
|
|
673
|
+
if (!globalStoragePromise) {
|
|
674
|
+
globalStoragePromise = createStorageWithFallback().then((storage) => {
|
|
675
|
+
globalStorage = storage;
|
|
676
|
+
return storage;
|
|
677
|
+
});
|
|
658
678
|
}
|
|
659
|
-
return
|
|
679
|
+
return globalStoragePromise;
|
|
660
680
|
}
|
|
661
681
|
|
|
662
682
|
/**
|
|
@@ -666,6 +686,7 @@ export async function getStorage(): Promise<LearningStorage> {
|
|
|
666
686
|
*/
|
|
667
687
|
export function setStorage(storage: LearningStorage): void {
|
|
668
688
|
globalStorage = storage;
|
|
689
|
+
globalStoragePromise = Promise.resolve(storage);
|
|
669
690
|
}
|
|
670
691
|
|
|
671
692
|
/**
|
|
@@ -676,4 +697,5 @@ export async function resetStorage(): Promise<void> {
|
|
|
676
697
|
await globalStorage.close();
|
|
677
698
|
globalStorage = null;
|
|
678
699
|
}
|
|
700
|
+
globalStoragePromise = null;
|
|
679
701
|
}
|