opencode-swarm-plugin 0.12.6 → 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/dist/plugin.js CHANGED
@@ -22606,8 +22606,8 @@ var RETRY_CONFIG = {
22606
22606
  jitterPercent: 20
22607
22607
  };
22608
22608
  var RECOVERY_CONFIG = {
22609
- failureThreshold: 2,
22610
- restartCooldownMs: 30000,
22609
+ failureThreshold: 1,
22610
+ restartCooldownMs: 1e4,
22611
22611
  enabled: process.env.OPENCODE_AGENT_MAIL_AUTO_RESTART !== "false"
22612
22612
  };
22613
22613
  var SESSION_STATE_DIR = process.env.SWARM_STATE_DIR || join2(tmpdir(), "swarm-sessions");
@@ -22981,24 +22981,57 @@ var agentmail_init = tool({
22981
22981
  fallback: "Swarm will continue without multi-agent coordination. File conflicts possible if multiple agents active."
22982
22982
  }, null, 2);
22983
22983
  }
22984
- const project = await mcpCall("ensure_project", {
22985
- human_key: args.project_path
22986
- });
22987
- const agent = await mcpCall("register_agent", {
22988
- project_key: args.project_path,
22989
- program: "opencode",
22990
- model: "claude-opus-4",
22991
- name: args.agent_name,
22992
- task_description: args.task_description || ""
22993
- });
22994
- const state = {
22995
- projectKey: args.project_path,
22996
- agentName: agent.name,
22997
- reservations: [],
22998
- startedAt: new Date().toISOString()
22999
- };
23000
- setState(ctx.sessionID, state);
23001
- return JSON.stringify({ project, agent, available: true }, null, 2);
22984
+ const MAX_INIT_RETRIES = 3;
22985
+ let lastError = null;
22986
+ for (let attempt = 1;attempt <= MAX_INIT_RETRIES; attempt++) {
22987
+ try {
22988
+ const project = await mcpCall("ensure_project", {
22989
+ human_key: args.project_path
22990
+ });
22991
+ const agent = await mcpCall("register_agent", {
22992
+ project_key: args.project_path,
22993
+ program: "opencode",
22994
+ model: "claude-opus-4",
22995
+ name: args.agent_name,
22996
+ task_description: args.task_description || ""
22997
+ });
22998
+ const state = {
22999
+ projectKey: args.project_path,
23000
+ agentName: agent.name,
23001
+ reservations: [],
23002
+ startedAt: new Date().toISOString()
23003
+ };
23004
+ setState(ctx.sessionID, state);
23005
+ if (attempt > 1) {
23006
+ console.warn(`[agent-mail] Init succeeded on attempt ${attempt} after restart`);
23007
+ }
23008
+ return JSON.stringify({ project, agent, available: true }, null, 2);
23009
+ } catch (error45) {
23010
+ lastError = error45 instanceof Error ? error45 : new Error(String(error45));
23011
+ const isUnexpectedError = lastError.message.toLowerCase().includes("unexpected error");
23012
+ console.warn(`[agent-mail] Init attempt ${attempt}/${MAX_INIT_RETRIES} failed: ${lastError.message}`);
23013
+ if (isUnexpectedError && attempt < MAX_INIT_RETRIES) {
23014
+ console.warn("[agent-mail] Detected 'unexpected error', restarting server...");
23015
+ const restarted = await restartServer();
23016
+ if (restarted) {
23017
+ agentMailAvailable = null;
23018
+ consecutiveFailures = 0;
23019
+ await new Promise((resolve) => setTimeout(resolve, 1000));
23020
+ continue;
23021
+ }
23022
+ }
23023
+ if (!isUnexpectedError) {
23024
+ break;
23025
+ }
23026
+ }
23027
+ }
23028
+ return JSON.stringify({
23029
+ error: `Agent Mail init failed after ${MAX_INIT_RETRIES} attempts`,
23030
+ available: false,
23031
+ lastError: lastError?.message,
23032
+ hint: "Manually restart Agent Mail: pkill -f agent-mail && agent-mail serve",
23033
+ fallback: "Swarm will continue without multi-agent coordination."
23034
+ }, null, 2);
23002
23035
  }
23003
23036
  });
23004
23037
  var agentmail_send = tool({
@@ -23204,7 +23237,11 @@ var agentmail_health = tool({
23204
23237
  try {
23205
23238
  const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`);
23206
23239
  if (response.ok) {
23207
- return "Agent Mail is running";
23240
+ const functional = await isServerFunctional();
23241
+ if (functional) {
23242
+ return "Agent Mail is running and functional";
23243
+ }
23244
+ return "Agent Mail health OK but MCP not responding - consider restart";
23208
23245
  }
23209
23246
  return `Agent Mail returned status ${response.status}`;
23210
23247
  } catch (error45) {
@@ -23212,6 +23249,41 @@ var agentmail_health = tool({
23212
23249
  }
23213
23250
  }
23214
23251
  });
23252
+ var agentmail_restart = tool({
23253
+ description: "Manually restart Agent Mail server (use when getting 'unexpected error')",
23254
+ args: {
23255
+ force: tool.schema.boolean().optional().describe("Force restart even if server appears healthy (default: false)")
23256
+ },
23257
+ async execute(args) {
23258
+ if (!args.force) {
23259
+ const functional = await isServerFunctional();
23260
+ if (functional) {
23261
+ return JSON.stringify({
23262
+ restarted: false,
23263
+ reason: "Server is functional, no restart needed",
23264
+ hint: "Use force=true to restart anyway"
23265
+ }, null, 2);
23266
+ }
23267
+ }
23268
+ console.warn("[agent-mail] Manual restart requested...");
23269
+ const success2 = await restartServer();
23270
+ agentMailAvailable = null;
23271
+ consecutiveFailures = 0;
23272
+ if (success2) {
23273
+ return JSON.stringify({
23274
+ restarted: true,
23275
+ success: true,
23276
+ message: "Agent Mail server restarted successfully"
23277
+ }, null, 2);
23278
+ }
23279
+ return JSON.stringify({
23280
+ restarted: true,
23281
+ success: false,
23282
+ error: "Restart attempted but server did not come back up",
23283
+ hint: "Check server logs or manually start: agent-mail serve"
23284
+ }, null, 2);
23285
+ }
23286
+ });
23215
23287
  var agentMailTools = {
23216
23288
  agentmail_init,
23217
23289
  agentmail_send,
@@ -23222,7 +23294,8 @@ var agentMailTools = {
23222
23294
  agentmail_release,
23223
23295
  agentmail_ack,
23224
23296
  agentmail_search,
23225
- agentmail_health
23297
+ agentmail_health,
23298
+ agentmail_restart
23226
23299
  };
23227
23300
 
23228
23301
  // src/structured.ts
@@ -23638,6 +23711,24 @@ var CriterionWeightSchema = exports_external.object({
23638
23711
  last_validated: exports_external.string().optional(),
23639
23712
  half_life_days: exports_external.number().positive().default(90)
23640
23713
  });
23714
+ var ErrorTypeSchema = exports_external.enum([
23715
+ "validation",
23716
+ "timeout",
23717
+ "conflict",
23718
+ "tool_failure",
23719
+ "unknown"
23720
+ ]);
23721
+ var ErrorEntrySchema = exports_external.object({
23722
+ id: exports_external.string(),
23723
+ bead_id: exports_external.string(),
23724
+ error_type: ErrorTypeSchema,
23725
+ message: exports_external.string(),
23726
+ stack_trace: exports_external.string().optional(),
23727
+ tool_name: exports_external.string().optional(),
23728
+ timestamp: exports_external.string(),
23729
+ resolved: exports_external.boolean().default(false),
23730
+ context: exports_external.string().optional()
23731
+ });
23641
23732
  var DecompositionStrategySchema = exports_external.enum([
23642
23733
  "file-based",
23643
23734
  "feature-based",
@@ -23711,6 +23802,117 @@ function outcomeToFeedback(outcome, criterion) {
23711
23802
  raw_value: outcome.decayed_value
23712
23803
  };
23713
23804
  }
23805
+ class InMemoryErrorStorage {
23806
+ errors = [];
23807
+ async store(entry) {
23808
+ this.errors.push(entry);
23809
+ }
23810
+ async getByBead(beadId) {
23811
+ return this.errors.filter((e) => e.bead_id === beadId);
23812
+ }
23813
+ async getUnresolvedByBead(beadId) {
23814
+ return this.errors.filter((e) => e.bead_id === beadId && !e.resolved);
23815
+ }
23816
+ async markResolved(id) {
23817
+ const error45 = this.errors.find((e) => e.id === id);
23818
+ if (error45) {
23819
+ error45.resolved = true;
23820
+ }
23821
+ }
23822
+ async getAll() {
23823
+ return [...this.errors];
23824
+ }
23825
+ }
23826
+
23827
+ class ErrorAccumulator {
23828
+ storage;
23829
+ constructor(storage) {
23830
+ this.storage = storage ?? new InMemoryErrorStorage;
23831
+ }
23832
+ async recordError(beadId, errorType, message, options) {
23833
+ const entry = {
23834
+ id: `${beadId}-${errorType}-${Date.now()}`,
23835
+ bead_id: beadId,
23836
+ error_type: errorType,
23837
+ message,
23838
+ stack_trace: options?.stack_trace,
23839
+ tool_name: options?.tool_name,
23840
+ timestamp: new Date().toISOString(),
23841
+ resolved: false,
23842
+ context: options?.context
23843
+ };
23844
+ const validated = ErrorEntrySchema.parse(entry);
23845
+ await this.storage.store(validated);
23846
+ return validated;
23847
+ }
23848
+ async getErrors(beadId) {
23849
+ return this.storage.getByBead(beadId);
23850
+ }
23851
+ async getUnresolvedErrors(beadId) {
23852
+ return this.storage.getUnresolvedByBead(beadId);
23853
+ }
23854
+ async resolveError(errorId) {
23855
+ await this.storage.markResolved(errorId);
23856
+ }
23857
+ async getErrorContext(beadId, includeResolved = false) {
23858
+ const errors3 = includeResolved ? await this.getErrors(beadId) : await this.getUnresolvedErrors(beadId);
23859
+ if (errors3.length === 0) {
23860
+ return "";
23861
+ }
23862
+ const byType = errors3.reduce((acc, err) => {
23863
+ const type = err.error_type;
23864
+ if (!acc[type]) {
23865
+ acc[type] = [];
23866
+ }
23867
+ acc[type].push(err);
23868
+ return acc;
23869
+ }, {});
23870
+ const lines = [
23871
+ "## Previous Errors",
23872
+ "",
23873
+ "The following errors were encountered during execution:",
23874
+ ""
23875
+ ];
23876
+ for (const [type, typeErrors] of Object.entries(byType)) {
23877
+ lines.push(`### ${type} (${typeErrors.length} error${typeErrors.length > 1 ? "s" : ""})`);
23878
+ lines.push("");
23879
+ for (const err of typeErrors) {
23880
+ lines.push(`- **${err.message}**`);
23881
+ if (err.context) {
23882
+ lines.push(` - Context: ${err.context}`);
23883
+ }
23884
+ if (err.tool_name) {
23885
+ lines.push(` - Tool: ${err.tool_name}`);
23886
+ }
23887
+ if (err.stack_trace) {
23888
+ lines.push(` - Stack: \`${err.stack_trace.slice(0, 100)}...\``);
23889
+ }
23890
+ lines.push(` - Time: ${new Date(err.timestamp).toLocaleString()}${err.resolved ? " (resolved)" : ""}`);
23891
+ lines.push("");
23892
+ }
23893
+ }
23894
+ lines.push("**Action Required**: Address these errors before proceeding. Consider:");
23895
+ lines.push("- What caused each error?");
23896
+ lines.push("- How can you prevent similar errors?");
23897
+ lines.push("- Are there patterns across error types?");
23898
+ lines.push("");
23899
+ return lines.join(`
23900
+ `);
23901
+ }
23902
+ async getErrorStats(beadId) {
23903
+ const allErrors = await this.getErrors(beadId);
23904
+ const unresolved = await this.getUnresolvedErrors(beadId);
23905
+ const byType = allErrors.reduce((acc, err) => {
23906
+ acc[err.error_type] = (acc[err.error_type] || 0) + 1;
23907
+ return acc;
23908
+ }, {});
23909
+ return {
23910
+ total: allErrors.length,
23911
+ unresolved: unresolved.length,
23912
+ by_type: byType
23913
+ };
23914
+ }
23915
+ }
23714
23916
 
23715
23917
  // src/swarm.ts
23716
23918
  var POSITIVE_MARKERS = [
@@ -24122,6 +24324,10 @@ Only modify these files. Need others? Message the coordinator.
24122
24324
  ## Context
24123
24325
  {shared_context}
24124
24326
 
24327
+ {compressed_context}
24328
+
24329
+ {error_context}
24330
+
24125
24331
  ## MANDATORY: Use These Tools
24126
24332
 
24127
24333
  ### Agent Mail - communicate with the swarm
@@ -24155,7 +24361,9 @@ Begin now.`;
24155
24361
  function formatSubtaskPromptV2(params) {
24156
24362
  const fileList = params.files.length > 0 ? params.files.map((f) => `- \`${f}\``).join(`
24157
24363
  `) : "(no specific files - use judgment)";
24158
- return SUBTASK_PROMPT_V2.replace(/{bead_id}/g, params.bead_id).replace(/{epic_id}/g, params.epic_id).replace("{subtask_title}", params.subtask_title).replace("{subtask_description}", params.subtask_description || "(see title)").replace("{file_list}", fileList).replace("{shared_context}", params.shared_context || "(none)");
24364
+ const compressedSection = params.compressed_context ? params.compressed_context : "";
24365
+ const errorSection = params.error_context ? params.error_context : "";
24366
+ return SUBTASK_PROMPT_V2.replace(/{bead_id}/g, params.bead_id).replace(/{epic_id}/g, params.epic_id).replace("{subtask_title}", params.subtask_title).replace("{subtask_description}", params.subtask_description || "(see title)").replace("{file_list}", fileList).replace("{shared_context}", params.shared_context || "(none)").replace("{compressed_context}", compressedSection).replace("{error_context}", errorSection);
24159
24367
  }
24160
24368
  var EVALUATION_PROMPT = `Evaluate the work completed for this subtask.
24161
24369
 
@@ -24853,6 +25061,7 @@ var swarm_record_outcome = tool({
24853
25061
  };
24854
25062
  const validated = OutcomeSignalsSchema.parse(signals);
24855
25063
  const scored = scoreImplicitFeedback(validated, DEFAULT_LEARNING_CONFIG);
25064
+ const errorStats = await globalErrorAccumulator.getErrorStats(args.bead_id);
24856
25065
  const criteriaToScore = args.criteria ?? [
24857
25066
  "type_safe",
24858
25067
  "no_bugs",
@@ -24864,6 +25073,10 @@ var swarm_record_outcome = tool({
24864
25073
  if (args.strategy) {
24865
25074
  event.context = `${event.context || ""} [strategy: ${args.strategy}]`.trim();
24866
25075
  }
25076
+ if (errorStats.total > 0) {
25077
+ const errorSummary = Object.entries(errorStats.by_type).map(([type, count]) => `${type}:${count}`).join(", ");
25078
+ event.context = `${event.context || ""} [errors: ${errorSummary}]`.trim();
25079
+ }
24867
25080
  return event;
24868
25081
  });
24869
25082
  return JSON.stringify({
@@ -24877,13 +25090,16 @@ var swarm_record_outcome = tool({
24877
25090
  }
24878
25091
  },
24879
25092
  feedback_events: feedbackEvents,
25093
+ error_patterns: errorStats,
24880
25094
  summary: {
24881
25095
  feedback_type: scored.type,
24882
25096
  duration_seconds: Math.round(args.duration_ms / 1000),
24883
25097
  error_count: args.error_count ?? 0,
24884
25098
  retry_count: args.retry_count ?? 0,
24885
25099
  success: args.success,
24886
- strategy: args.strategy
25100
+ strategy: args.strategy,
25101
+ accumulated_errors: errorStats.total,
25102
+ unresolved_errors: errorStats.unresolved
24887
25103
  },
24888
25104
  note: "Feedback events should be stored for criterion weight calculation. Use learning.ts functions to apply weights."
24889
25105
  }, null, 2);
@@ -25053,6 +25269,70 @@ var swarm_evaluation_prompt = tool({
25053
25269
  }, null, 2);
25054
25270
  }
25055
25271
  });
25272
+ var globalErrorAccumulator = new ErrorAccumulator;
25273
+ var swarm_accumulate_error = tool({
25274
+ description: "Record an error during subtask execution. Errors feed into retry prompts.",
25275
+ args: {
25276
+ bead_id: tool.schema.string().describe("Bead ID where error occurred"),
25277
+ error_type: tool.schema.enum(["validation", "timeout", "conflict", "tool_failure", "unknown"]).describe("Category of error"),
25278
+ message: tool.schema.string().describe("Human-readable error message"),
25279
+ stack_trace: tool.schema.string().optional().describe("Stack trace for debugging"),
25280
+ tool_name: tool.schema.string().optional().describe("Tool that failed"),
25281
+ context: tool.schema.string().optional().describe("What was happening when error occurred")
25282
+ },
25283
+ async execute(args) {
25284
+ const entry = await globalErrorAccumulator.recordError(args.bead_id, args.error_type, args.message, {
25285
+ stack_trace: args.stack_trace,
25286
+ tool_name: args.tool_name,
25287
+ context: args.context
25288
+ });
25289
+ return JSON.stringify({
25290
+ success: true,
25291
+ error_id: entry.id,
25292
+ bead_id: entry.bead_id,
25293
+ error_type: entry.error_type,
25294
+ message: entry.message,
25295
+ timestamp: entry.timestamp,
25296
+ note: "Error recorded for retry context. Use swarm_get_error_context to retrieve accumulated errors."
25297
+ }, null, 2);
25298
+ }
25299
+ });
25300
+ var swarm_get_error_context = tool({
25301
+ description: "Get accumulated errors for a bead. Returns formatted context for retry prompts.",
25302
+ args: {
25303
+ bead_id: tool.schema.string().describe("Bead ID to get errors for"),
25304
+ include_resolved: tool.schema.boolean().optional().describe("Include resolved errors (default: false)")
25305
+ },
25306
+ async execute(args) {
25307
+ const errorContext = await globalErrorAccumulator.getErrorContext(args.bead_id, args.include_resolved ?? false);
25308
+ const stats = await globalErrorAccumulator.getErrorStats(args.bead_id);
25309
+ return JSON.stringify({
25310
+ bead_id: args.bead_id,
25311
+ error_context: errorContext,
25312
+ stats: {
25313
+ total_errors: stats.total,
25314
+ unresolved: stats.unresolved,
25315
+ by_type: stats.by_type
25316
+ },
25317
+ has_errors: errorContext.length > 0,
25318
+ usage: "Inject error_context into retry prompt using {error_context} placeholder"
25319
+ }, null, 2);
25320
+ }
25321
+ });
25322
+ var swarm_resolve_error = tool({
25323
+ description: "Mark an error as resolved after fixing it. Updates error accumulator state.",
25324
+ args: {
25325
+ error_id: tool.schema.string().describe("Error ID to mark as resolved")
25326
+ },
25327
+ async execute(args) {
25328
+ await globalErrorAccumulator.resolveError(args.error_id);
25329
+ return JSON.stringify({
25330
+ success: true,
25331
+ error_id: args.error_id,
25332
+ resolved: true
25333
+ }, null, 2);
25334
+ }
25335
+ });
25056
25336
  var swarm_init = tool({
25057
25337
  description: "Initialize swarm session and check tool availability. Call at swarm start to see what features are available.",
25058
25338
  args: {
@@ -25114,7 +25394,10 @@ var swarmTools = {
25114
25394
  swarm_subtask_prompt,
25115
25395
  swarm_spawn_subtask,
25116
25396
  swarm_complete_subtask,
25117
- swarm_evaluation_prompt
25397
+ swarm_evaluation_prompt,
25398
+ swarm_accumulate_error,
25399
+ swarm_get_error_context,
25400
+ swarm_resolve_error
25118
25401
  };
25119
25402
  // src/anti-patterns.ts
25120
25403
  var PatternKindSchema = exports_external.enum(["pattern", "anti_pattern"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.12.6",
3
+ "version": "0.12.7",
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",
package/src/agent-mail.ts CHANGED
@@ -38,10 +38,10 @@ const RETRY_CONFIG = {
38
38
 
39
39
  // Server recovery configuration
40
40
  const RECOVERY_CONFIG = {
41
- /** Max consecutive failures before attempting restart */
42
- failureThreshold: 2,
43
- /** Cooldown between restart attempts (ms) */
44
- restartCooldownMs: 30000,
41
+ /** Max consecutive failures before attempting restart (1 = restart on first "unexpected error") */
42
+ failureThreshold: 1,
43
+ /** Cooldown between restart attempts (ms) - 10 seconds */
44
+ restartCooldownMs: 10000,
45
45
  /** Whether auto-restart is enabled */
46
46
  enabled: process.env.OPENCODE_AGENT_MAIL_AUTO_RESTART !== "false",
47
47
  };
@@ -880,30 +880,88 @@ export const agentmail_init = tool({
880
880
  );
881
881
  }
882
882
 
883
- // 1. Ensure project exists
884
- const project = await mcpCall<ProjectInfo>("ensure_project", {
885
- human_key: args.project_path,
886
- });
883
+ // Retry loop with restart on failure
884
+ const MAX_INIT_RETRIES = 3;
885
+ let lastError: Error | null = null;
887
886
 
888
- // 2. Register agent
889
- const agent = await mcpCall<AgentInfo>("register_agent", {
890
- project_key: args.project_path,
891
- program: "opencode",
892
- model: "claude-opus-4",
893
- name: args.agent_name, // undefined = auto-generate
894
- task_description: args.task_description || "",
895
- });
887
+ for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
888
+ try {
889
+ // 1. Ensure project exists
890
+ const project = await mcpCall<ProjectInfo>("ensure_project", {
891
+ human_key: args.project_path,
892
+ });
893
+
894
+ // 2. Register agent
895
+ const agent = await mcpCall<AgentInfo>("register_agent", {
896
+ project_key: args.project_path,
897
+ program: "opencode",
898
+ model: "claude-opus-4",
899
+ name: args.agent_name, // undefined = auto-generate
900
+ task_description: args.task_description || "",
901
+ });
902
+
903
+ // 3. Store state using sessionID
904
+ const state: AgentMailState = {
905
+ projectKey: args.project_path,
906
+ agentName: agent.name,
907
+ reservations: [],
908
+ startedAt: new Date().toISOString(),
909
+ };
910
+ setState(ctx.sessionID, state);
911
+
912
+ // Success - if we retried, log it
913
+ if (attempt > 1) {
914
+ console.warn(
915
+ `[agent-mail] Init succeeded on attempt ${attempt} after restart`,
916
+ );
917
+ }
896
918
 
897
- // 3. Store state using sessionID
898
- const state: AgentMailState = {
899
- projectKey: args.project_path,
900
- agentName: agent.name,
901
- reservations: [],
902
- startedAt: new Date().toISOString(),
903
- };
904
- setState(ctx.sessionID, state);
919
+ return JSON.stringify({ project, agent, available: true }, null, 2);
920
+ } catch (error) {
921
+ lastError = error instanceof Error ? error : new Error(String(error));
922
+ const isUnexpectedError = lastError.message
923
+ .toLowerCase()
924
+ .includes("unexpected error");
925
+
926
+ console.warn(
927
+ `[agent-mail] Init attempt ${attempt}/${MAX_INIT_RETRIES} failed: ${lastError.message}`,
928
+ );
929
+
930
+ // If it's an "unexpected error" and we have retries left, restart and retry
931
+ if (isUnexpectedError && attempt < MAX_INIT_RETRIES) {
932
+ console.warn(
933
+ "[agent-mail] Detected 'unexpected error', restarting server...",
934
+ );
935
+ const restarted = await restartServer();
936
+ if (restarted) {
937
+ // Clear cache and retry
938
+ agentMailAvailable = null;
939
+ consecutiveFailures = 0;
940
+ // Small delay to let server stabilize
941
+ await new Promise((resolve) => setTimeout(resolve, 1000));
942
+ continue;
943
+ }
944
+ }
905
945
 
906
- return JSON.stringify({ project, agent, available: true }, null, 2);
946
+ // For non-unexpected errors or if restart failed, don't retry
947
+ if (!isUnexpectedError) {
948
+ break;
949
+ }
950
+ }
951
+ }
952
+
953
+ // All retries exhausted
954
+ return JSON.stringify(
955
+ {
956
+ error: `Agent Mail init failed after ${MAX_INIT_RETRIES} attempts`,
957
+ available: false,
958
+ lastError: lastError?.message,
959
+ hint: "Manually restart Agent Mail: pkill -f agent-mail && agent-mail serve",
960
+ fallback: "Swarm will continue without multi-agent coordination.",
961
+ },
962
+ null,
963
+ 2,
964
+ );
907
965
  },
908
966
  });
909
967
 
@@ -1270,7 +1328,12 @@ export const agentmail_health = tool({
1270
1328
  try {
1271
1329
  const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`);
1272
1330
  if (response.ok) {
1273
- return "Agent Mail is running";
1331
+ // Also check if MCP is functional
1332
+ const functional = await isServerFunctional();
1333
+ if (functional) {
1334
+ return "Agent Mail is running and functional";
1335
+ }
1336
+ return "Agent Mail health OK but MCP not responding - consider restart";
1274
1337
  }
1275
1338
  return `Agent Mail returned status ${response.status}`;
1276
1339
  } catch (error) {
@@ -1279,6 +1342,73 @@ export const agentmail_health = tool({
1279
1342
  },
1280
1343
  });
1281
1344
 
1345
+ /**
1346
+ * Manually restart Agent Mail server
1347
+ *
1348
+ * Use when server is in bad state (health OK but MCP failing).
1349
+ * This kills the existing process and starts a fresh one.
1350
+ */
1351
+ export const agentmail_restart = tool({
1352
+ description:
1353
+ "Manually restart Agent Mail server (use when getting 'unexpected error')",
1354
+ args: {
1355
+ force: tool.schema
1356
+ .boolean()
1357
+ .optional()
1358
+ .describe(
1359
+ "Force restart even if server appears healthy (default: false)",
1360
+ ),
1361
+ },
1362
+ async execute(args) {
1363
+ // Check if restart is needed
1364
+ if (!args.force) {
1365
+ const functional = await isServerFunctional();
1366
+ if (functional) {
1367
+ return JSON.stringify(
1368
+ {
1369
+ restarted: false,
1370
+ reason: "Server is functional, no restart needed",
1371
+ hint: "Use force=true to restart anyway",
1372
+ },
1373
+ null,
1374
+ 2,
1375
+ );
1376
+ }
1377
+ }
1378
+
1379
+ // Attempt restart
1380
+ console.warn("[agent-mail] Manual restart requested...");
1381
+ const success = await restartServer();
1382
+
1383
+ // Clear caches
1384
+ agentMailAvailable = null;
1385
+ consecutiveFailures = 0;
1386
+
1387
+ if (success) {
1388
+ return JSON.stringify(
1389
+ {
1390
+ restarted: true,
1391
+ success: true,
1392
+ message: "Agent Mail server restarted successfully",
1393
+ },
1394
+ null,
1395
+ 2,
1396
+ );
1397
+ }
1398
+
1399
+ return JSON.stringify(
1400
+ {
1401
+ restarted: true,
1402
+ success: false,
1403
+ error: "Restart attempted but server did not come back up",
1404
+ hint: "Check server logs or manually start: agent-mail serve",
1405
+ },
1406
+ null,
1407
+ 2,
1408
+ );
1409
+ },
1410
+ });
1411
+
1282
1412
  // ============================================================================
1283
1413
  // Export all tools
1284
1414
  // ============================================================================
@@ -1294,6 +1424,7 @@ export const agentMailTools = {
1294
1424
  agentmail_ack: agentmail_ack,
1295
1425
  agentmail_search: agentmail_search,
1296
1426
  agentmail_health: agentmail_health,
1427
+ agentmail_restart: agentmail_restart,
1297
1428
  };
1298
1429
 
1299
1430
  // ============================================================================