opencode-swarm-plugin 0.48.0 → 0.49.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.
@@ -18210,11 +18210,16 @@ echo "Project directory: $1"
18210
18210
  // src/swarm-insights.ts
18211
18211
  var exports_swarm_insights = {};
18212
18212
  __export(exports_swarm_insights, {
18213
+ trackCoordinatorViolation: () => trackCoordinatorViolation,
18214
+ getViolationAnalytics: () => getViolationAnalytics,
18213
18215
  getStrategyInsights: () => getStrategyInsights,
18216
+ getRejectionAnalytics: () => getRejectionAnalytics,
18214
18217
  getPatternInsights: () => getPatternInsights,
18215
18218
  getFileInsights: () => getFileInsights,
18219
+ getFileFailureHistory: () => getFileFailureHistory,
18216
18220
  getCachedInsights: () => getCachedInsights,
18217
18221
  formatInsightsForPrompt: () => formatInsightsForPrompt,
18222
+ formatFileHistoryWarnings: () => formatFileHistoryWarnings,
18218
18223
  clearInsightsCache: () => clearInsightsCache
18219
18224
  });
18220
18225
  async function getStrategyInsights(swarmMail, _task) {
@@ -18286,6 +18291,136 @@ async function getFileInsights(swarmMail, files) {
18286
18291
  async function getFileGotchas(_swarmMail, _file2) {
18287
18292
  return [];
18288
18293
  }
18294
+ async function getFileFailureHistory(swarmMail, files) {
18295
+ if (files.length === 0)
18296
+ return [];
18297
+ const db = await swarmMail.getDatabase();
18298
+ const histories = [];
18299
+ for (const file2 of files) {
18300
+ const query = `
18301
+ SELECT data
18302
+ FROM events
18303
+ WHERE type = 'review_feedback'
18304
+ AND json_extract(data, '$.status') = 'needs_changes'
18305
+ AND json_extract(data, '$.issues') LIKE ?
18306
+ `;
18307
+ const result = await db.query(query, [`%${file2}%`]);
18308
+ if (!result.rows || result.rows.length === 0) {
18309
+ continue;
18310
+ }
18311
+ const issueTexts = [];
18312
+ for (const row of result.rows) {
18313
+ try {
18314
+ const data = JSON.parse(row.data);
18315
+ const issuesStr = data.issues;
18316
+ if (!issuesStr)
18317
+ continue;
18318
+ const issues = JSON.parse(issuesStr);
18319
+ for (const issue2 of issues) {
18320
+ if (issue2.file === file2) {
18321
+ issueTexts.push(issue2.issue);
18322
+ }
18323
+ }
18324
+ } catch (e) {
18325
+ continue;
18326
+ }
18327
+ }
18328
+ if (issueTexts.length === 0) {
18329
+ continue;
18330
+ }
18331
+ const issueCounts = new Map;
18332
+ for (const text of issueTexts) {
18333
+ issueCounts.set(text, (issueCounts.get(text) || 0) + 1);
18334
+ }
18335
+ const topIssues = Array.from(issueCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([text]) => text);
18336
+ histories.push({
18337
+ file: file2,
18338
+ rejectionCount: issueTexts.length,
18339
+ topIssues
18340
+ });
18341
+ }
18342
+ return histories;
18343
+ }
18344
+ async function getRejectionAnalytics(swarmMail) {
18345
+ const db = await swarmMail.getDatabase();
18346
+ const query = `
18347
+ SELECT data
18348
+ FROM events
18349
+ WHERE type = 'review_feedback'
18350
+ ORDER BY timestamp DESC
18351
+ `;
18352
+ const result = await db.query(query, []);
18353
+ if (!result.rows || result.rows.length === 0) {
18354
+ return {
18355
+ totalReviews: 0,
18356
+ approved: 0,
18357
+ rejected: 0,
18358
+ approvalRate: 0,
18359
+ topReasons: []
18360
+ };
18361
+ }
18362
+ let approved = 0;
18363
+ let rejected = 0;
18364
+ const reasonCounts = new Map;
18365
+ for (const row of result.rows) {
18366
+ try {
18367
+ const data = JSON.parse(row.data);
18368
+ if (data.status === "approved") {
18369
+ approved++;
18370
+ } else if (data.status === "needs_changes") {
18371
+ rejected++;
18372
+ if (data.issues) {
18373
+ const issues = JSON.parse(data.issues);
18374
+ for (const issue2 of issues) {
18375
+ const category = categorizeRejectionReason(issue2.issue);
18376
+ reasonCounts.set(category, (reasonCounts.get(category) || 0) + 1);
18377
+ }
18378
+ }
18379
+ }
18380
+ } catch (e) {
18381
+ continue;
18382
+ }
18383
+ }
18384
+ const totalReviews = approved + rejected;
18385
+ const approvalRate = totalReviews > 0 ? approved / totalReviews * 100 : 0;
18386
+ const topReasons = Array.from(reasonCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([category, count]) => ({
18387
+ category,
18388
+ count,
18389
+ percentage: rejected > 0 ? count / rejected * 100 : 0
18390
+ }));
18391
+ return {
18392
+ totalReviews,
18393
+ approved,
18394
+ rejected,
18395
+ approvalRate,
18396
+ topReasons
18397
+ };
18398
+ }
18399
+ function categorizeRejectionReason(reason) {
18400
+ const lowerReason = reason.toLowerCase();
18401
+ if (lowerReason.includes("test") || lowerReason.includes("spec") || lowerReason.includes("coverage")) {
18402
+ return "Missing tests";
18403
+ }
18404
+ if (lowerReason.includes("type") || lowerReason.includes("undefined") || lowerReason.includes("null") || lowerReason.includes("assignable")) {
18405
+ return "Type errors";
18406
+ }
18407
+ if (lowerReason.includes("incomplete") || lowerReason.includes("missing") || lowerReason.includes("forgot") || lowerReason.includes("didn't implement")) {
18408
+ return "Incomplete implementation";
18409
+ }
18410
+ if (lowerReason.includes("wrong file") || lowerReason.includes("modified incorrect") || lowerReason.includes("shouldn't have changed")) {
18411
+ return "Wrong file modified";
18412
+ }
18413
+ if (lowerReason.includes("performance") || lowerReason.includes("slow") || lowerReason.includes("inefficient")) {
18414
+ return "Performance issue";
18415
+ }
18416
+ if (lowerReason.includes("security") || lowerReason.includes("vulnerability") || lowerReason.includes("unsafe")) {
18417
+ return "Security vulnerability";
18418
+ }
18419
+ if (lowerReason.includes("error handling") || lowerReason.includes("try/catch") || lowerReason.includes("exception")) {
18420
+ return "Missing error handling";
18421
+ }
18422
+ return "Other";
18423
+ }
18289
18424
  async function getPatternInsights(swarmMail) {
18290
18425
  const db = await swarmMail.getDatabase();
18291
18426
  const patterns = [];
@@ -18373,6 +18508,107 @@ async function getCachedInsights(_swarmMail, cacheKey, computeFn) {
18373
18508
  function clearInsightsCache() {
18374
18509
  insightsCache.clear();
18375
18510
  }
18511
+ async function trackCoordinatorViolation(swarmMail, violation) {
18512
+ const db = await swarmMail.getDatabase();
18513
+ const query = `
18514
+ INSERT INTO events (type, project_key, timestamp, data)
18515
+ VALUES (?, ?, ?, ?)
18516
+ RETURNING id
18517
+ `;
18518
+ const data = JSON.stringify({
18519
+ session_id: violation.session_id,
18520
+ epic_id: violation.epic_id,
18521
+ event_type: "VIOLATION",
18522
+ violation_type: violation.violation_type,
18523
+ payload: violation.payload
18524
+ });
18525
+ const result = await db.query(query, [
18526
+ "coordinator_violation",
18527
+ violation.project_key,
18528
+ Date.now(),
18529
+ data
18530
+ ]);
18531
+ return result.rows[0].id;
18532
+ }
18533
+ async function getViolationAnalytics(swarmMail, projectKey) {
18534
+ const db = await swarmMail.getDatabase();
18535
+ const violationsQuery = projectKey ? `
18536
+ SELECT data
18537
+ FROM events
18538
+ WHERE type = 'coordinator_violation'
18539
+ AND project_key = ?
18540
+ ORDER BY timestamp DESC
18541
+ ` : `
18542
+ SELECT data
18543
+ FROM events
18544
+ WHERE type = 'coordinator_violation'
18545
+ ORDER BY timestamp DESC
18546
+ `;
18547
+ const params = projectKey ? [projectKey] : [];
18548
+ const result = await db.query(violationsQuery, params);
18549
+ if (!result.rows || result.rows.length === 0) {
18550
+ return {
18551
+ totalViolations: 0,
18552
+ byType: [],
18553
+ violationRate: 0
18554
+ };
18555
+ }
18556
+ const violationCounts = new Map;
18557
+ let totalViolations = 0;
18558
+ for (const row of result.rows) {
18559
+ try {
18560
+ const data = JSON.parse(row.data);
18561
+ const violationType = data.violation_type;
18562
+ if (violationType) {
18563
+ violationCounts.set(violationType, (violationCounts.get(violationType) || 0) + 1);
18564
+ totalViolations++;
18565
+ }
18566
+ } catch (e) {
18567
+ continue;
18568
+ }
18569
+ }
18570
+ const byType = Array.from(violationCounts.entries()).sort((a, b) => b[1] - a[1]).map(([violationType, count]) => ({
18571
+ violationType,
18572
+ count,
18573
+ percentage: count / totalViolations * 100
18574
+ }));
18575
+ const coordinationQuery = projectKey ? `
18576
+ SELECT COUNT(*) as count
18577
+ FROM events
18578
+ WHERE type IN ('worker_spawned', 'review_feedback', 'message_sent')
18579
+ AND project_key = ?
18580
+ ` : `
18581
+ SELECT COUNT(*) as count
18582
+ FROM events
18583
+ WHERE type IN ('worker_spawned', 'review_feedback', 'message_sent')
18584
+ `;
18585
+ const coordResult = await db.query(coordinationQuery, params);
18586
+ const coordinationCount = coordResult.rows[0]?.count || 0;
18587
+ const violationRate = coordinationCount > 0 ? totalViolations / coordinationCount * 100 : 0;
18588
+ return {
18589
+ totalViolations,
18590
+ byType,
18591
+ violationRate
18592
+ };
18593
+ }
18594
+ function formatFileHistoryWarnings(histories) {
18595
+ if (histories.length === 0) {
18596
+ return "";
18597
+ }
18598
+ const lines = ["⚠️ FILE HISTORY WARNINGS:"];
18599
+ for (const history of histories) {
18600
+ const workerText = history.rejectionCount === 1 ? "1 previous worker rejected" : `${history.rejectionCount} previous workers rejected`;
18601
+ const issuesText = history.topIssues.join(", ");
18602
+ lines.push(`- ${history.file}: ${workerText} for ${issuesText}`);
18603
+ }
18604
+ let result = lines.join(`
18605
+ `);
18606
+ const maxChars = 300 * 4;
18607
+ if (result.length > maxChars) {
18608
+ result = result.slice(0, maxChars - 3) + "...";
18609
+ }
18610
+ return result;
18611
+ }
18376
18612
  var insightsCache, CACHE_TTL_MS;
18377
18613
  var init_swarm_insights = __esm(() => {
18378
18614
  insightsCache = new Map;
@@ -20281,7 +20517,8 @@ var CellTreeSchema = exports_external.object({
20281
20517
  title: exports_external.string().min(1),
20282
20518
  description: exports_external.string().optional().default("")
20283
20519
  }),
20284
- subtasks: exports_external.array(SubtaskSpecSchema).min(1)
20520
+ subtasks: exports_external.array(SubtaskSpecSchema).min(1),
20521
+ strategy: exports_external.enum(["file-based", "feature-based", "risk-based", "research-based"]).optional().describe("Decomposition strategy from swarm_select_strategy. If not provided, defaults to feature-based.")
20285
20522
  });
20286
20523
  var EpicCreateArgsSchema = exports_external.object({
20287
20524
  epic_title: exports_external.string().min(1),
@@ -21188,13 +21425,13 @@ async function autoMigrateFromJSONL(adapter, projectKey) {
21188
21425
  skipExisting: true
21189
21426
  });
21190
21427
  if (result.created > 0 || result.updated > 0) {
21191
- console.log(`[hive] Auto-migrated ${result.created} cells from ${jsonlPath} (${result.skipped} skipped, ${result.errors.length} errors)`);
21428
+ console.error(`[hive] Auto-migrated ${result.created} cells from ${jsonlPath} (${result.skipped} skipped, ${result.errors.length} errors)`);
21192
21429
  }
21193
21430
  if (result.errors.length > 0) {
21194
- console.warn(`[hive] Migration errors:`, result.errors.slice(0, 5).map((e) => `${e.cellId}: ${e.error}`));
21431
+ console.error(`[hive] Migration errors:`, result.errors.slice(0, 5).map((e) => `${e.cellId}: ${e.error}`));
21195
21432
  }
21196
21433
  } catch (error45) {
21197
- console.warn(`[hive] Failed to auto-migrate from ${jsonlPath}:`, error45 instanceof Error ? error45.message : String(error45));
21434
+ console.error(`[hive] Failed to auto-migrate from ${jsonlPath}:`, error45 instanceof Error ? error45.message : String(error45));
21198
21435
  }
21199
21436
  }
21200
21437
  function formatCellForOutput(adapterCell) {
@@ -23364,7 +23601,7 @@ var swarm_complete = tool({
23364
23601
  files_touched: tool.schema.array(tool.schema.string()).optional().describe("Files modified - will be verified (typecheck, tests)"),
23365
23602
  skip_verification: tool.schema.boolean().optional().describe("Skip ALL verification (typecheck, tests). Use sparingly! (default: false)"),
23366
23603
  planned_files: tool.schema.array(tool.schema.string()).optional().describe("Files that were originally planned to be modified"),
23367
- start_time: tool.schema.number().optional().describe("Task start timestamp (Unix ms) for duration calculation"),
23604
+ start_time: tool.schema.number().describe("Task start timestamp (Unix ms) for duration calculation - REQUIRED for accurate analytics"),
23368
23605
  error_count: tool.schema.number().optional().describe("Number of errors encountered during task"),
23369
23606
  retry_count: tool.schema.number().optional().describe("Number of retry attempts during task"),
23370
23607
  skip_review: tool.schema.boolean().optional().describe("Skip review gate check (default: false). Use only for tasks that don't require coordinator review.")
@@ -23568,7 +23805,7 @@ This will be recorded as a negative learning signal.`;
23568
23805
  syncError = error45 instanceof Error ? error45.message : String(error45);
23569
23806
  console.warn(`[swarm_complete] Auto-sync failed (non-fatal): ${syncError}`);
23570
23807
  }
23571
- const completionDurationMs = args.start_time ? Date.now() - args.start_time : 0;
23808
+ const completionDurationMs = Date.now() - args.start_time;
23572
23809
  const eventEpicId = cell.parent_id || (args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id);
23573
23810
  try {
23574
23811
  const event = createEvent3("subtask_outcome", {
@@ -23624,12 +23861,18 @@ This will be recorded as a negative learning signal.`;
23624
23861
  memoryError = `Failed to store memory: ${error45 instanceof Error ? error45.message : String(error45)}`;
23625
23862
  console.warn(`[swarm_complete] ${memoryError}`);
23626
23863
  }
23864
+ let reservationsReleased = false;
23865
+ let reservationsReleasedCount = 0;
23866
+ let reservationsReleaseError;
23627
23867
  try {
23628
- await releaseSwarmFiles({
23868
+ const releaseResult = await releaseSwarmFiles({
23629
23869
  projectPath: args.project_key,
23630
23870
  agentName: args.agent_name
23631
23871
  });
23872
+ reservationsReleased = true;
23873
+ reservationsReleasedCount = releaseResult.released;
23632
23874
  } catch (error45) {
23875
+ reservationsReleaseError = error45 instanceof Error ? error45.message : String(error45);
23633
23876
  console.warn(`[swarm] Failed to release file reservations for ${args.agent_name}:`, error45);
23634
23877
  }
23635
23878
  const epicId2 = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
@@ -23665,7 +23908,9 @@ This will be recorded as a negative learning signal.`;
23665
23908
  success: true,
23666
23909
  bead_id: args.bead_id,
23667
23910
  closed: true,
23668
- reservations_released: true,
23911
+ reservations_released: reservationsReleased,
23912
+ reservations_released_count: reservationsReleasedCount,
23913
+ reservations_release_error: reservationsReleaseError,
23669
23914
  synced: syncSuccess,
23670
23915
  sync_error: syncError,
23671
23916
  message_sent: messageSent,
@@ -37885,14 +38130,14 @@ async function maybeAutoMigrate(db) {
37885
38130
  if (memoryCount > 0) {
37886
38131
  return;
37887
38132
  }
37888
- console.log("[memory] Legacy database detected, starting auto-migration...");
38133
+ console.error("[memory] Legacy database detected, starting auto-migration...");
37889
38134
  const result = await migrateLegacyMemories({
37890
38135
  targetDb: db,
37891
38136
  dryRun: false,
37892
- onProgress: console.log
38137
+ onProgress: (msg) => console.error(msg)
37893
38138
  });
37894
38139
  if (result.migrated > 0) {
37895
- console.log(`[memory] Auto-migrated ${result.migrated} memories from legacy database`);
38140
+ console.error(`[memory] Auto-migrated ${result.migrated} memories from legacy database`);
37896
38141
  }
37897
38142
  if (result.failed > 0) {
37898
38143
  console.warn(`[memory] ${result.failed} memories failed to migrate. See errors above.`);
@@ -37961,6 +38206,7 @@ Linked to ${result.links.length} related memor${result.links.length === 1 ? "y"
37961
38206
  async find(args2) {
37962
38207
  const limit = args2.limit ?? 10;
37963
38208
  let results;
38209
+ let usedFallback = false;
37964
38210
  if (args2.fts) {
37965
38211
  results = await store.ftsSearch(args2.query, {
37966
38212
  limit,
@@ -37971,14 +38217,23 @@ Linked to ${result.links.length} related memor${result.links.length === 1 ? "y"
37971
38217
  const ollama = yield* Ollama;
37972
38218
  return yield* ollama.embed(args2.query);
37973
38219
  });
37974
- const queryEmbedding = await exports_Effect.runPromise(program.pipe(exports_Effect.provide(ollamaLayer)));
37975
- results = await store.search(queryEmbedding, {
37976
- limit,
37977
- threshold: 0.3,
37978
- collection: args2.collection
37979
- });
38220
+ try {
38221
+ const queryEmbedding = await exports_Effect.runPromise(program.pipe(exports_Effect.provide(ollamaLayer)));
38222
+ results = await store.search(queryEmbedding, {
38223
+ limit,
38224
+ threshold: 0.3,
38225
+ collection: args2.collection
38226
+ });
38227
+ } catch (e) {
38228
+ console.warn("[hivemind] Ollama unavailable, falling back to full-text search");
38229
+ usedFallback = true;
38230
+ results = await store.ftsSearch(args2.query, {
38231
+ limit,
38232
+ collection: args2.collection
38233
+ });
38234
+ }
37980
38235
  }
37981
- return {
38236
+ const response = {
37982
38237
  results: results.map((r) => ({
37983
38238
  id: r.memory.id,
37984
38239
  content: args2.expand ? r.memory.content : truncateContent(r.memory.content),
@@ -37989,6 +38244,10 @@ Linked to ${result.links.length} related memor${result.links.length === 1 ? "y"
37989
38244
  })),
37990
38245
  count: results.length
37991
38246
  };
38247
+ if (usedFallback) {
38248
+ response.fallback_used = true;
38249
+ }
38250
+ return response;
37992
38251
  },
37993
38252
  async get(args2) {
37994
38253
  return store.get(args2.id);
@@ -38811,6 +39070,74 @@ Your role is **ONLY** to:
38811
39070
 
38812
39071
  **ALWAYS spawn workers, even for sequential tasks.** Sequential just means spawn them in order and wait for each to complete before spawning the next.
38813
39072
 
39073
+ ### Explicit NEVER Rules (With Examples)
39074
+
39075
+ \`\`\`
39076
+ ╔═══════════════════════════════════════════════════════════════════════════╗
39077
+ ║ ║
39078
+ ║ ❌ COORDINATORS NEVER DO THIS: ║
39079
+ ║ ║
39080
+ ║ - Read implementation files (read(), glob src/**, grep for patterns) ║
39081
+ ║ - Edit code (edit(), write() any .ts/.js/.tsx files) ║
39082
+ ║ - Run tests (bash "bun test", "npm test", pytest) ║
39083
+ ║ - Implement features (adding functions, components, logic) ║
39084
+ ║ - Fix bugs (changing code to fix errors) ║
39085
+ ║ - Install packages (bash "bun add", "npm install") ║
39086
+ ║ - Commit changes (bash "git add", "git commit") ║
39087
+ ║ - Reserve files (swarmmail_reserve - workers do this) ║
39088
+ ║ ║
39089
+ ║ ✅ COORDINATORS ONLY DO THIS: ║
39090
+ ║ ║
39091
+ ║ - Clarify task scope (ask questions, understand requirements) ║
39092
+ ║ - Read package.json/tsconfig.json for structure (metadata only) ║
39093
+ ║ - Decompose into subtasks (swarm_plan_prompt, validate_decomposition) ║
39094
+ ║ - Spawn workers (swarm_spawn_subtask, Task(subagent_type="worker")) ║
39095
+ ║ - Monitor progress (swarmmail_inbox, swarm_status) ║
39096
+ ║ - Review completed work (swarm_review, swarm_review_feedback) ║
39097
+ ║ - Verify final state (check all workers completed, hive_sync) ║
39098
+ ║ ║
39099
+ ╚═══════════════════════════════════════════════════════════════════════════╝
39100
+ \`\`\`
39101
+
39102
+ **Examples of Violations:**
39103
+
39104
+ ❌ **WRONG** - Coordinator reading implementation:
39105
+ \`\`\`
39106
+ read("src/auth/login.ts") // NO - spawn worker to analyze
39107
+ glob("src/components/**/*.tsx") // NO - spawn worker to inventory
39108
+ grep(pattern="export", include="*.ts") // NO - spawn worker to search
39109
+ \`\`\`
39110
+
39111
+ ❌ **WRONG** - Coordinator editing code:
39112
+ \`\`\`
39113
+ edit("src/types.ts", ...) // NO - spawn worker to fix
39114
+ write("src/new.ts", ...) // NO - spawn worker to create
39115
+ \`\`\`
39116
+
39117
+ ❌ **WRONG** - Coordinator running tests:
39118
+ \`\`\`
39119
+ bash("bun test src/auth.test.ts") // NO - worker runs tests
39120
+ \`\`\`
39121
+
39122
+ ❌ **WRONG** - Coordinator reserving files:
39123
+ \`\`\`
39124
+ swarmmail_reserve(paths=["src/auth.ts"]) // NO - worker reserves their own files
39125
+ swarm_spawn_subtask(bead_id="...", files=["src/auth.ts"])
39126
+ \`\`\`
39127
+
39128
+ ✅ **CORRECT** - Coordinator spawning worker:
39129
+ \`\`\`
39130
+ // Coordinator delegates ALL work
39131
+ swarm_spawn_subtask(
39132
+ bead_id="fix-auth-bug",
39133
+ epic_id="epic-123",
39134
+ subtask_title="Fix null check in login handler",
39135
+ files=["src/auth/login.ts", "src/auth/login.test.ts"],
39136
+ shared_context="Bug: login fails when username is null"
39137
+ )
39138
+ Task(subagent_type="swarm-worker", prompt="<from above>")
39139
+ \`\`\`
39140
+
38814
39141
  ### Why This Matters
38815
39142
 
38816
39143
  | Coordinator Work | Worker Work | Consequence of Mixing |
@@ -22,8 +22,8 @@ export declare const DecompositionStrategySchema: z.ZodEnum<{
22
22
  "file-based": "file-based";
23
23
  "feature-based": "feature-based";
24
24
  "risk-based": "risk-based";
25
- auto: "auto";
26
25
  "research-based": "research-based";
26
+ auto: "auto";
27
27
  }>;
28
28
  /**
29
29
  * Marker words that indicate positive directives
package/dist/swarm.d.ts CHANGED
@@ -123,7 +123,7 @@ export declare const swarmTools: {
123
123
  files_touched: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
124
124
  skip_verification: import("zod").ZodOptional<import("zod").ZodBoolean>;
125
125
  planned_files: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
126
- start_time: import("zod").ZodOptional<import("zod").ZodNumber>;
126
+ start_time: import("zod").ZodNumber;
127
127
  error_count: import("zod").ZodOptional<import("zod").ZodNumber>;
128
128
  retry_count: import("zod").ZodOptional<import("zod").ZodNumber>;
129
129
  skip_review: import("zod").ZodOptional<import("zod").ZodBoolean>;
@@ -133,11 +133,11 @@ export declare const swarmTools: {
133
133
  agent_name: string;
134
134
  bead_id: string;
135
135
  summary: string;
136
+ start_time: number;
136
137
  evaluation?: string | undefined;
137
138
  files_touched?: string[] | undefined;
138
139
  skip_verification?: boolean | undefined;
139
140
  planned_files?: string[] | undefined;
140
- start_time?: number | undefined;
141
141
  error_count?: number | undefined;
142
142
  retry_count?: number | undefined;
143
143
  skip_review?: boolean | undefined;
@@ -2775,8 +2775,9 @@ const SwarmPlugin: Plugin = async (
2775
2775
  has_projection: !!sessionScan.projection?.isSwarm,
2776
2776
  });
2777
2777
 
2778
- // Hoist snapshot outside try block so it's available in fallback path
2778
+ // Hoist snapshot and queryDuration outside try block so they're available in fallback path
2779
2779
  let snapshot: SwarmStateSnapshot | undefined;
2780
+ let queryDuration = 0; // 0 if using projection, actual duration if using hive query
2780
2781
 
2781
2782
  try {
2782
2783
  // =======================================================================
@@ -2821,7 +2822,7 @@ const SwarmPlugin: Plugin = async (
2821
2822
  // Fallback to hive query (may be stale)
2822
2823
  const queryStart = Date.now();
2823
2824
  snapshot = await querySwarmState(input.sessionID);
2824
- const queryDuration = Date.now() - queryStart;
2825
+ queryDuration = Date.now() - queryStart;
2825
2826
 
2826
2827
  logCompaction("info", "fallback_to_hive_query", {
2827
2828
  session_id: input.sessionID,
@@ -2967,6 +2968,16 @@ const SwarmPlugin: Plugin = async (
2967
2968
  });
2968
2969
  }
2969
2970
 
2971
+ // Guard: Don't double-inject if LLM prompt was already set
2972
+ // This can happen if the error occurred after setting output.prompt but before return
2973
+ if ("prompt" in output && output.prompt) {
2974
+ logCompaction("info", "skipping_static_fallback_prompt_already_set", {
2975
+ session_id: input.sessionID,
2976
+ prompt_length: output.prompt.length,
2977
+ });
2978
+ return;
2979
+ }
2980
+
2970
2981
  // Level 3: Fall back to static context WITH dynamic state from snapshot
2971
2982
  const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
2972
2983
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -64,7 +64,7 @@
64
64
  "minimatch": "^10.1.1",
65
65
  "pino": "^9.6.0",
66
66
  "pino-roll": "^1.3.0",
67
- "swarm-mail": "1.7.0",
67
+ "swarm-mail": "1.7.2",
68
68
  "yaml": "^2.8.2",
69
69
  "zod": "4.1.8"
70
70
  },