opencode-swarm-plugin 0.6.3 → 0.10.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 +7 -80
- package/README.md +232 -589
- package/bin/swarm.ts +693 -0
- package/bun.lock +7 -0
- package/dist/index.js +232 -26
- package/dist/plugin.js +232 -26
- package/package.json +3 -2
- package/src/agent-mail.ts +401 -44
- package/scripts/setup.ts +0 -371
package/bun.lock
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "opencode-swarm-plugin",
|
|
7
7
|
"dependencies": {
|
|
8
|
+
"@clack/prompts": "^0.11.0",
|
|
8
9
|
"@opencode-ai/plugin": "^1.0.134",
|
|
9
10
|
"ioredis": "^5.4.1",
|
|
10
11
|
"zod": "4.1.8",
|
|
@@ -20,6 +21,10 @@
|
|
|
20
21
|
},
|
|
21
22
|
},
|
|
22
23
|
"packages": {
|
|
24
|
+
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
|
|
25
|
+
|
|
26
|
+
"@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="],
|
|
27
|
+
|
|
23
28
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
|
24
29
|
|
|
25
30
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
|
@@ -204,6 +209,8 @@
|
|
|
204
209
|
|
|
205
210
|
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
|
206
211
|
|
|
212
|
+
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
|
213
|
+
|
|
207
214
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
|
208
215
|
|
|
209
216
|
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
package/dist/index.js
CHANGED
|
@@ -22593,6 +22593,18 @@ async function getRateLimiter() {
|
|
|
22593
22593
|
var AGENT_MAIL_URL = "http://127.0.0.1:8765";
|
|
22594
22594
|
var DEFAULT_TTL_SECONDS = 3600;
|
|
22595
22595
|
var MAX_INBOX_LIMIT = 5;
|
|
22596
|
+
var RETRY_CONFIG = {
|
|
22597
|
+
maxRetries: parseInt(process.env.OPENCODE_AGENT_MAIL_MAX_RETRIES || "3"),
|
|
22598
|
+
baseDelayMs: parseInt(process.env.OPENCODE_AGENT_MAIL_BASE_DELAY_MS || "100"),
|
|
22599
|
+
maxDelayMs: parseInt(process.env.OPENCODE_AGENT_MAIL_MAX_DELAY_MS || "5000"),
|
|
22600
|
+
timeoutMs: parseInt(process.env.OPENCODE_AGENT_MAIL_TIMEOUT_MS || "10000"),
|
|
22601
|
+
jitterPercent: 20
|
|
22602
|
+
};
|
|
22603
|
+
var RECOVERY_CONFIG = {
|
|
22604
|
+
failureThreshold: 2,
|
|
22605
|
+
restartCooldownMs: 30000,
|
|
22606
|
+
enabled: process.env.OPENCODE_AGENT_MAIL_AUTO_RESTART !== "false"
|
|
22607
|
+
};
|
|
22596
22608
|
var sessionStates = new Map;
|
|
22597
22609
|
|
|
22598
22610
|
class AgentMailError extends Error {
|
|
@@ -22639,6 +22651,151 @@ class RateLimitExceededError extends Error {
|
|
|
22639
22651
|
this.name = "RateLimitExceededError";
|
|
22640
22652
|
}
|
|
22641
22653
|
}
|
|
22654
|
+
var consecutiveFailures = 0;
|
|
22655
|
+
var lastRestartAttempt = 0;
|
|
22656
|
+
var isRestarting = false;
|
|
22657
|
+
async function isServerHealthy() {
|
|
22658
|
+
try {
|
|
22659
|
+
const controller = new AbortController;
|
|
22660
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
22661
|
+
const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`, {
|
|
22662
|
+
signal: controller.signal
|
|
22663
|
+
});
|
|
22664
|
+
clearTimeout(timeout);
|
|
22665
|
+
return response.ok;
|
|
22666
|
+
} catch {
|
|
22667
|
+
return false;
|
|
22668
|
+
}
|
|
22669
|
+
}
|
|
22670
|
+
async function isServerFunctional() {
|
|
22671
|
+
try {
|
|
22672
|
+
const controller = new AbortController;
|
|
22673
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
22674
|
+
const response = await fetch(`${AGENT_MAIL_URL}/mcp/`, {
|
|
22675
|
+
method: "POST",
|
|
22676
|
+
headers: { "Content-Type": "application/json" },
|
|
22677
|
+
body: JSON.stringify({
|
|
22678
|
+
jsonrpc: "2.0",
|
|
22679
|
+
id: "health-test",
|
|
22680
|
+
method: "tools/call",
|
|
22681
|
+
params: { name: "health_check", arguments: {} }
|
|
22682
|
+
}),
|
|
22683
|
+
signal: controller.signal
|
|
22684
|
+
});
|
|
22685
|
+
clearTimeout(timeout);
|
|
22686
|
+
if (!response.ok)
|
|
22687
|
+
return false;
|
|
22688
|
+
const json2 = await response.json();
|
|
22689
|
+
if (json2.result?.isError)
|
|
22690
|
+
return false;
|
|
22691
|
+
return true;
|
|
22692
|
+
} catch {
|
|
22693
|
+
return false;
|
|
22694
|
+
}
|
|
22695
|
+
}
|
|
22696
|
+
async function restartServer() {
|
|
22697
|
+
if (!RECOVERY_CONFIG.enabled) {
|
|
22698
|
+
console.warn("[agent-mail] Auto-restart disabled via OPENCODE_AGENT_MAIL_AUTO_RESTART=false");
|
|
22699
|
+
return false;
|
|
22700
|
+
}
|
|
22701
|
+
if (isRestarting) {
|
|
22702
|
+
console.warn("[agent-mail] Restart already in progress");
|
|
22703
|
+
return false;
|
|
22704
|
+
}
|
|
22705
|
+
const now = Date.now();
|
|
22706
|
+
if (now - lastRestartAttempt < RECOVERY_CONFIG.restartCooldownMs) {
|
|
22707
|
+
const waitSec = Math.ceil((RECOVERY_CONFIG.restartCooldownMs - (now - lastRestartAttempt)) / 1000);
|
|
22708
|
+
console.warn(`[agent-mail] Restart cooldown active, wait ${waitSec}s`);
|
|
22709
|
+
return false;
|
|
22710
|
+
}
|
|
22711
|
+
isRestarting = true;
|
|
22712
|
+
lastRestartAttempt = now;
|
|
22713
|
+
try {
|
|
22714
|
+
console.warn("[agent-mail] Attempting server restart...");
|
|
22715
|
+
const findProc = Bun.spawn(["lsof", "-i", ":8765", "-t"], {
|
|
22716
|
+
stdout: "pipe",
|
|
22717
|
+
stderr: "pipe"
|
|
22718
|
+
});
|
|
22719
|
+
const findOutput = await new Response(findProc.stdout).text();
|
|
22720
|
+
await findProc.exited;
|
|
22721
|
+
const pids = findOutput.trim().split(`
|
|
22722
|
+
`).filter(Boolean);
|
|
22723
|
+
if (pids.length > 0) {
|
|
22724
|
+
for (const pid of pids) {
|
|
22725
|
+
console.warn(`[agent-mail] Killing process ${pid}`);
|
|
22726
|
+
Bun.spawn(["kill", pid]);
|
|
22727
|
+
}
|
|
22728
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
22729
|
+
}
|
|
22730
|
+
const possiblePaths = [
|
|
22731
|
+
`${process.env.HOME}/Code/Dicklesworthstone/mcp_agent_mail`,
|
|
22732
|
+
`${process.env.HOME}/.local/share/agent-mail`,
|
|
22733
|
+
`${process.env.HOME}/mcp_agent_mail`
|
|
22734
|
+
];
|
|
22735
|
+
let serverDir = null;
|
|
22736
|
+
for (const path of possiblePaths) {
|
|
22737
|
+
try {
|
|
22738
|
+
const stat = await Bun.file(`${path}/pyproject.toml`).exists();
|
|
22739
|
+
if (stat) {
|
|
22740
|
+
serverDir = path;
|
|
22741
|
+
break;
|
|
22742
|
+
}
|
|
22743
|
+
} catch {
|
|
22744
|
+
continue;
|
|
22745
|
+
}
|
|
22746
|
+
}
|
|
22747
|
+
if (!serverDir) {
|
|
22748
|
+
console.error("[agent-mail] Could not find agent-mail installation directory");
|
|
22749
|
+
return false;
|
|
22750
|
+
}
|
|
22751
|
+
console.warn(`[agent-mail] Starting server from ${serverDir}`);
|
|
22752
|
+
Bun.spawn(["python", "-m", "mcp_agent_mail.cli", "serve-http"], {
|
|
22753
|
+
cwd: serverDir,
|
|
22754
|
+
stdout: "ignore",
|
|
22755
|
+
stderr: "ignore",
|
|
22756
|
+
detached: true
|
|
22757
|
+
});
|
|
22758
|
+
for (let i = 0;i < 10; i++) {
|
|
22759
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
22760
|
+
if (await isServerHealthy()) {
|
|
22761
|
+
console.warn("[agent-mail] Server restarted successfully");
|
|
22762
|
+
consecutiveFailures = 0;
|
|
22763
|
+
return true;
|
|
22764
|
+
}
|
|
22765
|
+
}
|
|
22766
|
+
console.error("[agent-mail] Server failed to start after restart");
|
|
22767
|
+
return false;
|
|
22768
|
+
} catch (error45) {
|
|
22769
|
+
console.error("[agent-mail] Restart failed:", error45);
|
|
22770
|
+
return false;
|
|
22771
|
+
} finally {
|
|
22772
|
+
isRestarting = false;
|
|
22773
|
+
}
|
|
22774
|
+
}
|
|
22775
|
+
function calculateBackoffDelay(attempt) {
|
|
22776
|
+
if (attempt === 0)
|
|
22777
|
+
return 0;
|
|
22778
|
+
const exponentialDelay = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt - 1);
|
|
22779
|
+
const cappedDelay = Math.min(exponentialDelay, RETRY_CONFIG.maxDelayMs);
|
|
22780
|
+
const jitterRange = cappedDelay * (RETRY_CONFIG.jitterPercent / 100);
|
|
22781
|
+
const jitter = (Math.random() * 2 - 1) * jitterRange;
|
|
22782
|
+
return Math.round(cappedDelay + jitter);
|
|
22783
|
+
}
|
|
22784
|
+
function isRetryableError(error45) {
|
|
22785
|
+
if (error45 instanceof Error) {
|
|
22786
|
+
const message = error45.message.toLowerCase();
|
|
22787
|
+
if (message.includes("econnrefused") || message.includes("econnreset") || message.includes("socket") || message.includes("network") || message.includes("timeout") || message.includes("aborted")) {
|
|
22788
|
+
return true;
|
|
22789
|
+
}
|
|
22790
|
+
if (error45 instanceof AgentMailError && error45.code) {
|
|
22791
|
+
return error45.code === 502 || error45.code === 503 || error45.code === 504;
|
|
22792
|
+
}
|
|
22793
|
+
if (message.includes("unexpected error")) {
|
|
22794
|
+
return true;
|
|
22795
|
+
}
|
|
22796
|
+
}
|
|
22797
|
+
return false;
|
|
22798
|
+
}
|
|
22642
22799
|
var agentMailAvailable = null;
|
|
22643
22800
|
async function checkAgentMailAvailable() {
|
|
22644
22801
|
if (agentMailAvailable !== null) {
|
|
@@ -22670,36 +22827,85 @@ async function recordRateLimitedRequest(agentName, endpoint) {
|
|
|
22670
22827
|
}
|
|
22671
22828
|
await rateLimiter.recordRequest(agentName, endpoint);
|
|
22672
22829
|
}
|
|
22673
|
-
async function
|
|
22674
|
-
const
|
|
22675
|
-
|
|
22676
|
-
|
|
22677
|
-
|
|
22678
|
-
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
|
|
22683
|
-
|
|
22684
|
-
|
|
22685
|
-
|
|
22686
|
-
|
|
22687
|
-
|
|
22688
|
-
|
|
22689
|
-
|
|
22830
|
+
async function mcpCallOnce(toolName, args) {
|
|
22831
|
+
const controller = new AbortController;
|
|
22832
|
+
const timeout = setTimeout(() => controller.abort(), RETRY_CONFIG.timeoutMs);
|
|
22833
|
+
try {
|
|
22834
|
+
const response = await fetch(`${AGENT_MAIL_URL}/mcp/`, {
|
|
22835
|
+
method: "POST",
|
|
22836
|
+
headers: { "Content-Type": "application/json" },
|
|
22837
|
+
body: JSON.stringify({
|
|
22838
|
+
jsonrpc: "2.0",
|
|
22839
|
+
id: crypto.randomUUID(),
|
|
22840
|
+
method: "tools/call",
|
|
22841
|
+
params: { name: toolName, arguments: args }
|
|
22842
|
+
}),
|
|
22843
|
+
signal: controller.signal
|
|
22844
|
+
});
|
|
22845
|
+
clearTimeout(timeout);
|
|
22846
|
+
if (!response.ok) {
|
|
22847
|
+
throw new AgentMailError(`HTTP ${response.status}: ${response.statusText}`, toolName, response.status);
|
|
22848
|
+
}
|
|
22849
|
+
const json2 = await response.json();
|
|
22850
|
+
if (json2.error) {
|
|
22851
|
+
throw new AgentMailError(json2.error.message, toolName, json2.error.code, json2.error.data);
|
|
22852
|
+
}
|
|
22853
|
+
const result = json2.result;
|
|
22854
|
+
if (result && typeof result === "object") {
|
|
22855
|
+
const wrapped = result;
|
|
22856
|
+
if (wrapped.isError) {
|
|
22857
|
+
const errorText = wrapped.content?.[0]?.text || "Unknown error";
|
|
22858
|
+
throw new AgentMailError(errorText, toolName);
|
|
22859
|
+
}
|
|
22860
|
+
if ("structuredContent" in wrapped) {
|
|
22861
|
+
return wrapped.structuredContent;
|
|
22862
|
+
}
|
|
22863
|
+
}
|
|
22864
|
+
return result;
|
|
22865
|
+
} catch (error45) {
|
|
22866
|
+
clearTimeout(timeout);
|
|
22867
|
+
throw error45;
|
|
22690
22868
|
}
|
|
22691
|
-
|
|
22692
|
-
|
|
22693
|
-
|
|
22694
|
-
|
|
22695
|
-
|
|
22696
|
-
|
|
22869
|
+
}
|
|
22870
|
+
async function mcpCall(toolName, args) {
|
|
22871
|
+
let lastError = null;
|
|
22872
|
+
for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
|
|
22873
|
+
if (attempt > 0) {
|
|
22874
|
+
const delay = calculateBackoffDelay(attempt);
|
|
22875
|
+
console.warn(`[agent-mail] Retry ${attempt}/${RETRY_CONFIG.maxRetries} for ${toolName} after ${delay}ms`);
|
|
22876
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
22697
22877
|
}
|
|
22698
|
-
|
|
22699
|
-
|
|
22878
|
+
try {
|
|
22879
|
+
const result = await mcpCallOnce(toolName, args);
|
|
22880
|
+
consecutiveFailures = 0;
|
|
22881
|
+
return result;
|
|
22882
|
+
} catch (error45) {
|
|
22883
|
+
lastError = error45 instanceof Error ? error45 : new Error(String(error45));
|
|
22884
|
+
consecutiveFailures++;
|
|
22885
|
+
if (consecutiveFailures >= RECOVERY_CONFIG.failureThreshold && RECOVERY_CONFIG.enabled) {
|
|
22886
|
+
console.warn(`[agent-mail] ${consecutiveFailures} consecutive failures, checking server health...`);
|
|
22887
|
+
const healthy = await isServerFunctional();
|
|
22888
|
+
if (!healthy) {
|
|
22889
|
+
console.warn("[agent-mail] Server unhealthy, attempting restart...");
|
|
22890
|
+
const restarted = await restartServer();
|
|
22891
|
+
if (restarted) {
|
|
22892
|
+
agentMailAvailable = null;
|
|
22893
|
+
attempt--;
|
|
22894
|
+
continue;
|
|
22895
|
+
}
|
|
22896
|
+
}
|
|
22897
|
+
}
|
|
22898
|
+
if (!isRetryableError(error45)) {
|
|
22899
|
+
console.warn(`[agent-mail] Non-retryable error for ${toolName}: ${lastError.message}`);
|
|
22900
|
+
throw lastError;
|
|
22901
|
+
}
|
|
22902
|
+
if (attempt === RETRY_CONFIG.maxRetries) {
|
|
22903
|
+
console.error(`[agent-mail] All ${RETRY_CONFIG.maxRetries} retries exhausted for ${toolName}`);
|
|
22904
|
+
throw lastError;
|
|
22905
|
+
}
|
|
22700
22906
|
}
|
|
22701
22907
|
}
|
|
22702
|
-
|
|
22908
|
+
throw lastError || new Error("Unknown error in mcpCall");
|
|
22703
22909
|
}
|
|
22704
22910
|
function requireState(sessionID) {
|
|
22705
22911
|
const state = sessionStates.get(sessionID);
|
package/dist/plugin.js
CHANGED
|
@@ -22567,6 +22567,18 @@ async function getRateLimiter() {
|
|
|
22567
22567
|
var AGENT_MAIL_URL = "http://127.0.0.1:8765";
|
|
22568
22568
|
var DEFAULT_TTL_SECONDS = 3600;
|
|
22569
22569
|
var MAX_INBOX_LIMIT = 5;
|
|
22570
|
+
var RETRY_CONFIG = {
|
|
22571
|
+
maxRetries: parseInt(process.env.OPENCODE_AGENT_MAIL_MAX_RETRIES || "3"),
|
|
22572
|
+
baseDelayMs: parseInt(process.env.OPENCODE_AGENT_MAIL_BASE_DELAY_MS || "100"),
|
|
22573
|
+
maxDelayMs: parseInt(process.env.OPENCODE_AGENT_MAIL_MAX_DELAY_MS || "5000"),
|
|
22574
|
+
timeoutMs: parseInt(process.env.OPENCODE_AGENT_MAIL_TIMEOUT_MS || "10000"),
|
|
22575
|
+
jitterPercent: 20
|
|
22576
|
+
};
|
|
22577
|
+
var RECOVERY_CONFIG = {
|
|
22578
|
+
failureThreshold: 2,
|
|
22579
|
+
restartCooldownMs: 30000,
|
|
22580
|
+
enabled: process.env.OPENCODE_AGENT_MAIL_AUTO_RESTART !== "false"
|
|
22581
|
+
};
|
|
22570
22582
|
var sessionStates = new Map;
|
|
22571
22583
|
|
|
22572
22584
|
class AgentMailError extends Error {
|
|
@@ -22613,6 +22625,151 @@ class RateLimitExceededError extends Error {
|
|
|
22613
22625
|
this.name = "RateLimitExceededError";
|
|
22614
22626
|
}
|
|
22615
22627
|
}
|
|
22628
|
+
var consecutiveFailures = 0;
|
|
22629
|
+
var lastRestartAttempt = 0;
|
|
22630
|
+
var isRestarting = false;
|
|
22631
|
+
async function isServerHealthy() {
|
|
22632
|
+
try {
|
|
22633
|
+
const controller = new AbortController;
|
|
22634
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
22635
|
+
const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`, {
|
|
22636
|
+
signal: controller.signal
|
|
22637
|
+
});
|
|
22638
|
+
clearTimeout(timeout);
|
|
22639
|
+
return response.ok;
|
|
22640
|
+
} catch {
|
|
22641
|
+
return false;
|
|
22642
|
+
}
|
|
22643
|
+
}
|
|
22644
|
+
async function isServerFunctional() {
|
|
22645
|
+
try {
|
|
22646
|
+
const controller = new AbortController;
|
|
22647
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
22648
|
+
const response = await fetch(`${AGENT_MAIL_URL}/mcp/`, {
|
|
22649
|
+
method: "POST",
|
|
22650
|
+
headers: { "Content-Type": "application/json" },
|
|
22651
|
+
body: JSON.stringify({
|
|
22652
|
+
jsonrpc: "2.0",
|
|
22653
|
+
id: "health-test",
|
|
22654
|
+
method: "tools/call",
|
|
22655
|
+
params: { name: "health_check", arguments: {} }
|
|
22656
|
+
}),
|
|
22657
|
+
signal: controller.signal
|
|
22658
|
+
});
|
|
22659
|
+
clearTimeout(timeout);
|
|
22660
|
+
if (!response.ok)
|
|
22661
|
+
return false;
|
|
22662
|
+
const json2 = await response.json();
|
|
22663
|
+
if (json2.result?.isError)
|
|
22664
|
+
return false;
|
|
22665
|
+
return true;
|
|
22666
|
+
} catch {
|
|
22667
|
+
return false;
|
|
22668
|
+
}
|
|
22669
|
+
}
|
|
22670
|
+
async function restartServer() {
|
|
22671
|
+
if (!RECOVERY_CONFIG.enabled) {
|
|
22672
|
+
console.warn("[agent-mail] Auto-restart disabled via OPENCODE_AGENT_MAIL_AUTO_RESTART=false");
|
|
22673
|
+
return false;
|
|
22674
|
+
}
|
|
22675
|
+
if (isRestarting) {
|
|
22676
|
+
console.warn("[agent-mail] Restart already in progress");
|
|
22677
|
+
return false;
|
|
22678
|
+
}
|
|
22679
|
+
const now = Date.now();
|
|
22680
|
+
if (now - lastRestartAttempt < RECOVERY_CONFIG.restartCooldownMs) {
|
|
22681
|
+
const waitSec = Math.ceil((RECOVERY_CONFIG.restartCooldownMs - (now - lastRestartAttempt)) / 1000);
|
|
22682
|
+
console.warn(`[agent-mail] Restart cooldown active, wait ${waitSec}s`);
|
|
22683
|
+
return false;
|
|
22684
|
+
}
|
|
22685
|
+
isRestarting = true;
|
|
22686
|
+
lastRestartAttempt = now;
|
|
22687
|
+
try {
|
|
22688
|
+
console.warn("[agent-mail] Attempting server restart...");
|
|
22689
|
+
const findProc = Bun.spawn(["lsof", "-i", ":8765", "-t"], {
|
|
22690
|
+
stdout: "pipe",
|
|
22691
|
+
stderr: "pipe"
|
|
22692
|
+
});
|
|
22693
|
+
const findOutput = await new Response(findProc.stdout).text();
|
|
22694
|
+
await findProc.exited;
|
|
22695
|
+
const pids = findOutput.trim().split(`
|
|
22696
|
+
`).filter(Boolean);
|
|
22697
|
+
if (pids.length > 0) {
|
|
22698
|
+
for (const pid of pids) {
|
|
22699
|
+
console.warn(`[agent-mail] Killing process ${pid}`);
|
|
22700
|
+
Bun.spawn(["kill", pid]);
|
|
22701
|
+
}
|
|
22702
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
22703
|
+
}
|
|
22704
|
+
const possiblePaths = [
|
|
22705
|
+
`${process.env.HOME}/Code/Dicklesworthstone/mcp_agent_mail`,
|
|
22706
|
+
`${process.env.HOME}/.local/share/agent-mail`,
|
|
22707
|
+
`${process.env.HOME}/mcp_agent_mail`
|
|
22708
|
+
];
|
|
22709
|
+
let serverDir = null;
|
|
22710
|
+
for (const path of possiblePaths) {
|
|
22711
|
+
try {
|
|
22712
|
+
const stat = await Bun.file(`${path}/pyproject.toml`).exists();
|
|
22713
|
+
if (stat) {
|
|
22714
|
+
serverDir = path;
|
|
22715
|
+
break;
|
|
22716
|
+
}
|
|
22717
|
+
} catch {
|
|
22718
|
+
continue;
|
|
22719
|
+
}
|
|
22720
|
+
}
|
|
22721
|
+
if (!serverDir) {
|
|
22722
|
+
console.error("[agent-mail] Could not find agent-mail installation directory");
|
|
22723
|
+
return false;
|
|
22724
|
+
}
|
|
22725
|
+
console.warn(`[agent-mail] Starting server from ${serverDir}`);
|
|
22726
|
+
Bun.spawn(["python", "-m", "mcp_agent_mail.cli", "serve-http"], {
|
|
22727
|
+
cwd: serverDir,
|
|
22728
|
+
stdout: "ignore",
|
|
22729
|
+
stderr: "ignore",
|
|
22730
|
+
detached: true
|
|
22731
|
+
});
|
|
22732
|
+
for (let i = 0;i < 10; i++) {
|
|
22733
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
22734
|
+
if (await isServerHealthy()) {
|
|
22735
|
+
console.warn("[agent-mail] Server restarted successfully");
|
|
22736
|
+
consecutiveFailures = 0;
|
|
22737
|
+
return true;
|
|
22738
|
+
}
|
|
22739
|
+
}
|
|
22740
|
+
console.error("[agent-mail] Server failed to start after restart");
|
|
22741
|
+
return false;
|
|
22742
|
+
} catch (error45) {
|
|
22743
|
+
console.error("[agent-mail] Restart failed:", error45);
|
|
22744
|
+
return false;
|
|
22745
|
+
} finally {
|
|
22746
|
+
isRestarting = false;
|
|
22747
|
+
}
|
|
22748
|
+
}
|
|
22749
|
+
function calculateBackoffDelay(attempt) {
|
|
22750
|
+
if (attempt === 0)
|
|
22751
|
+
return 0;
|
|
22752
|
+
const exponentialDelay = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt - 1);
|
|
22753
|
+
const cappedDelay = Math.min(exponentialDelay, RETRY_CONFIG.maxDelayMs);
|
|
22754
|
+
const jitterRange = cappedDelay * (RETRY_CONFIG.jitterPercent / 100);
|
|
22755
|
+
const jitter = (Math.random() * 2 - 1) * jitterRange;
|
|
22756
|
+
return Math.round(cappedDelay + jitter);
|
|
22757
|
+
}
|
|
22758
|
+
function isRetryableError(error45) {
|
|
22759
|
+
if (error45 instanceof Error) {
|
|
22760
|
+
const message = error45.message.toLowerCase();
|
|
22761
|
+
if (message.includes("econnrefused") || message.includes("econnreset") || message.includes("socket") || message.includes("network") || message.includes("timeout") || message.includes("aborted")) {
|
|
22762
|
+
return true;
|
|
22763
|
+
}
|
|
22764
|
+
if (error45 instanceof AgentMailError && error45.code) {
|
|
22765
|
+
return error45.code === 502 || error45.code === 503 || error45.code === 504;
|
|
22766
|
+
}
|
|
22767
|
+
if (message.includes("unexpected error")) {
|
|
22768
|
+
return true;
|
|
22769
|
+
}
|
|
22770
|
+
}
|
|
22771
|
+
return false;
|
|
22772
|
+
}
|
|
22616
22773
|
var agentMailAvailable = null;
|
|
22617
22774
|
async function checkAgentMailAvailable() {
|
|
22618
22775
|
if (agentMailAvailable !== null) {
|
|
@@ -22644,36 +22801,85 @@ async function recordRateLimitedRequest(agentName, endpoint) {
|
|
|
22644
22801
|
}
|
|
22645
22802
|
await rateLimiter.recordRequest(agentName, endpoint);
|
|
22646
22803
|
}
|
|
22647
|
-
async function
|
|
22648
|
-
const
|
|
22649
|
-
|
|
22650
|
-
|
|
22651
|
-
|
|
22652
|
-
|
|
22653
|
-
|
|
22654
|
-
|
|
22655
|
-
|
|
22656
|
-
|
|
22657
|
-
|
|
22658
|
-
|
|
22659
|
-
|
|
22660
|
-
|
|
22661
|
-
|
|
22662
|
-
|
|
22663
|
-
|
|
22804
|
+
async function mcpCallOnce(toolName, args) {
|
|
22805
|
+
const controller = new AbortController;
|
|
22806
|
+
const timeout = setTimeout(() => controller.abort(), RETRY_CONFIG.timeoutMs);
|
|
22807
|
+
try {
|
|
22808
|
+
const response = await fetch(`${AGENT_MAIL_URL}/mcp/`, {
|
|
22809
|
+
method: "POST",
|
|
22810
|
+
headers: { "Content-Type": "application/json" },
|
|
22811
|
+
body: JSON.stringify({
|
|
22812
|
+
jsonrpc: "2.0",
|
|
22813
|
+
id: crypto.randomUUID(),
|
|
22814
|
+
method: "tools/call",
|
|
22815
|
+
params: { name: toolName, arguments: args }
|
|
22816
|
+
}),
|
|
22817
|
+
signal: controller.signal
|
|
22818
|
+
});
|
|
22819
|
+
clearTimeout(timeout);
|
|
22820
|
+
if (!response.ok) {
|
|
22821
|
+
throw new AgentMailError(`HTTP ${response.status}: ${response.statusText}`, toolName, response.status);
|
|
22822
|
+
}
|
|
22823
|
+
const json2 = await response.json();
|
|
22824
|
+
if (json2.error) {
|
|
22825
|
+
throw new AgentMailError(json2.error.message, toolName, json2.error.code, json2.error.data);
|
|
22826
|
+
}
|
|
22827
|
+
const result = json2.result;
|
|
22828
|
+
if (result && typeof result === "object") {
|
|
22829
|
+
const wrapped = result;
|
|
22830
|
+
if (wrapped.isError) {
|
|
22831
|
+
const errorText = wrapped.content?.[0]?.text || "Unknown error";
|
|
22832
|
+
throw new AgentMailError(errorText, toolName);
|
|
22833
|
+
}
|
|
22834
|
+
if ("structuredContent" in wrapped) {
|
|
22835
|
+
return wrapped.structuredContent;
|
|
22836
|
+
}
|
|
22837
|
+
}
|
|
22838
|
+
return result;
|
|
22839
|
+
} catch (error45) {
|
|
22840
|
+
clearTimeout(timeout);
|
|
22841
|
+
throw error45;
|
|
22664
22842
|
}
|
|
22665
|
-
|
|
22666
|
-
|
|
22667
|
-
|
|
22668
|
-
|
|
22669
|
-
|
|
22670
|
-
|
|
22843
|
+
}
|
|
22844
|
+
async function mcpCall(toolName, args) {
|
|
22845
|
+
let lastError = null;
|
|
22846
|
+
for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
|
|
22847
|
+
if (attempt > 0) {
|
|
22848
|
+
const delay = calculateBackoffDelay(attempt);
|
|
22849
|
+
console.warn(`[agent-mail] Retry ${attempt}/${RETRY_CONFIG.maxRetries} for ${toolName} after ${delay}ms`);
|
|
22850
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
22671
22851
|
}
|
|
22672
|
-
|
|
22673
|
-
|
|
22852
|
+
try {
|
|
22853
|
+
const result = await mcpCallOnce(toolName, args);
|
|
22854
|
+
consecutiveFailures = 0;
|
|
22855
|
+
return result;
|
|
22856
|
+
} catch (error45) {
|
|
22857
|
+
lastError = error45 instanceof Error ? error45 : new Error(String(error45));
|
|
22858
|
+
consecutiveFailures++;
|
|
22859
|
+
if (consecutiveFailures >= RECOVERY_CONFIG.failureThreshold && RECOVERY_CONFIG.enabled) {
|
|
22860
|
+
console.warn(`[agent-mail] ${consecutiveFailures} consecutive failures, checking server health...`);
|
|
22861
|
+
const healthy = await isServerFunctional();
|
|
22862
|
+
if (!healthy) {
|
|
22863
|
+
console.warn("[agent-mail] Server unhealthy, attempting restart...");
|
|
22864
|
+
const restarted = await restartServer();
|
|
22865
|
+
if (restarted) {
|
|
22866
|
+
agentMailAvailable = null;
|
|
22867
|
+
attempt--;
|
|
22868
|
+
continue;
|
|
22869
|
+
}
|
|
22870
|
+
}
|
|
22871
|
+
}
|
|
22872
|
+
if (!isRetryableError(error45)) {
|
|
22873
|
+
console.warn(`[agent-mail] Non-retryable error for ${toolName}: ${lastError.message}`);
|
|
22874
|
+
throw lastError;
|
|
22875
|
+
}
|
|
22876
|
+
if (attempt === RETRY_CONFIG.maxRetries) {
|
|
22877
|
+
console.error(`[agent-mail] All ${RETRY_CONFIG.maxRetries} retries exhausted for ${toolName}`);
|
|
22878
|
+
throw lastError;
|
|
22879
|
+
}
|
|
22674
22880
|
}
|
|
22675
22881
|
}
|
|
22676
|
-
|
|
22882
|
+
throw lastError || new Error("Unknown error in mcpCall");
|
|
22677
22883
|
}
|
|
22678
22884
|
function requireState(sessionID) {
|
|
22679
22885
|
const state = sessionStates.get(sessionID);
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"
|
|
9
|
+
"swarm": "./bin/swarm.ts"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"clean": "rm -rf dist"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"@clack/prompts": "^0.11.0",
|
|
28
29
|
"@opencode-ai/plugin": "^1.0.134",
|
|
29
30
|
"ioredis": "^5.4.1",
|
|
30
31
|
"zod": "4.1.8"
|