opencode-swarm-plugin 0.12.19 → 0.12.20
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/index.js +116 -4
- package/dist/plugin.js +116 -4
- package/package.json +1 -1
- package/src/agent-mail.integration.test.ts +108 -0
- package/src/agent-mail.ts +182 -0
- package/src/beads.ts +50 -0
- package/src/swarm.ts +18 -10
package/dist/index.js
CHANGED
|
@@ -22153,7 +22153,40 @@ var beads_sync = tool({
|
|
|
22153
22153
|
}
|
|
22154
22154
|
}
|
|
22155
22155
|
if (autoPull) {
|
|
22156
|
+
const dirtyCheckResult = await runGitCommand([
|
|
22157
|
+
"status",
|
|
22158
|
+
"--porcelain",
|
|
22159
|
+
"--untracked-files=no"
|
|
22160
|
+
]);
|
|
22161
|
+
const hasDirtyFiles = dirtyCheckResult.stdout.trim() !== "";
|
|
22162
|
+
let didStash = false;
|
|
22163
|
+
if (hasDirtyFiles) {
|
|
22164
|
+
console.warn("[beads] Detected unstaged changes, stashing before pull...");
|
|
22165
|
+
const stashResult = await runGitCommand([
|
|
22166
|
+
"stash",
|
|
22167
|
+
"push",
|
|
22168
|
+
"-m",
|
|
22169
|
+
"beads_sync: auto-stash before pull",
|
|
22170
|
+
"--include-untracked"
|
|
22171
|
+
]);
|
|
22172
|
+
if (stashResult.exitCode === 0) {
|
|
22173
|
+
didStash = true;
|
|
22174
|
+
console.warn("[beads] Changes stashed successfully");
|
|
22175
|
+
} else {
|
|
22176
|
+
console.warn(`[beads] Stash failed (${stashResult.stderr}), attempting pull anyway...`);
|
|
22177
|
+
}
|
|
22178
|
+
}
|
|
22156
22179
|
const pullResult = await withTimeout(runGitCommand(["pull", "--rebase"]), TIMEOUT_MS, "git pull --rebase");
|
|
22180
|
+
if (didStash) {
|
|
22181
|
+
console.warn("[beads] Restoring stashed changes...");
|
|
22182
|
+
const unstashResult = await runGitCommand(["stash", "pop"]);
|
|
22183
|
+
if (unstashResult.exitCode !== 0) {
|
|
22184
|
+
console.error(`[beads] WARNING: Failed to restore stashed changes: ${unstashResult.stderr}`);
|
|
22185
|
+
console.error("[beads] Your changes are in 'git stash list' - run 'git stash pop' manually");
|
|
22186
|
+
} else {
|
|
22187
|
+
console.warn("[beads] Stashed changes restored");
|
|
22188
|
+
}
|
|
22189
|
+
}
|
|
22157
22190
|
if (pullResult.exitCode !== 0) {
|
|
22158
22191
|
throw new BeadError(`Failed to pull: ${pullResult.stderr}`, "git pull --rebase", pullResult.exitCode);
|
|
22159
22192
|
}
|
|
@@ -23014,6 +23047,20 @@ function isRetryableError(error45) {
|
|
|
23014
23047
|
}
|
|
23015
23048
|
return false;
|
|
23016
23049
|
}
|
|
23050
|
+
function isProjectNotFoundError(error45) {
|
|
23051
|
+
if (error45 instanceof Error) {
|
|
23052
|
+
const message = error45.message.toLowerCase();
|
|
23053
|
+
return message.includes("project") && (message.includes("not found") || message.includes("does not exist"));
|
|
23054
|
+
}
|
|
23055
|
+
return false;
|
|
23056
|
+
}
|
|
23057
|
+
function isAgentNotFoundError(error45) {
|
|
23058
|
+
if (error45 instanceof Error) {
|
|
23059
|
+
const message = error45.message.toLowerCase();
|
|
23060
|
+
return message.includes("agent") && (message.includes("not found") || message.includes("does not exist"));
|
|
23061
|
+
}
|
|
23062
|
+
return false;
|
|
23063
|
+
}
|
|
23017
23064
|
var agentMailAvailable = null;
|
|
23018
23065
|
async function checkAgentMailAvailable() {
|
|
23019
23066
|
if (agentMailAvailable !== null) {
|
|
@@ -23144,6 +23191,68 @@ async function mcpCall(toolName, args) {
|
|
|
23144
23191
|
}
|
|
23145
23192
|
throw lastError || new Error("Unknown error in mcpCall");
|
|
23146
23193
|
}
|
|
23194
|
+
async function reRegisterProject(projectKey) {
|
|
23195
|
+
try {
|
|
23196
|
+
console.warn(`[agent-mail] Re-registering project "${projectKey}" after server restart...`);
|
|
23197
|
+
await mcpCall("ensure_project", {
|
|
23198
|
+
human_key: projectKey
|
|
23199
|
+
});
|
|
23200
|
+
console.warn(`[agent-mail] Project "${projectKey}" re-registered successfully`);
|
|
23201
|
+
return true;
|
|
23202
|
+
} catch (error45) {
|
|
23203
|
+
console.error(`[agent-mail] Failed to re-register project "${projectKey}":`, error45);
|
|
23204
|
+
return false;
|
|
23205
|
+
}
|
|
23206
|
+
}
|
|
23207
|
+
async function reRegisterAgent(projectKey, agentName, taskDescription) {
|
|
23208
|
+
try {
|
|
23209
|
+
console.warn(`[agent-mail] Re-registering agent "${agentName}" for project "${projectKey}"...`);
|
|
23210
|
+
await mcpCall("register_agent", {
|
|
23211
|
+
project_key: projectKey,
|
|
23212
|
+
program: "opencode",
|
|
23213
|
+
model: "claude-opus-4",
|
|
23214
|
+
name: agentName,
|
|
23215
|
+
task_description: taskDescription || "Re-registered after server restart"
|
|
23216
|
+
});
|
|
23217
|
+
console.warn(`[agent-mail] Agent "${agentName}" re-registered successfully`);
|
|
23218
|
+
return true;
|
|
23219
|
+
} catch (error45) {
|
|
23220
|
+
console.error(`[agent-mail] Failed to re-register agent "${agentName}":`, error45);
|
|
23221
|
+
return false;
|
|
23222
|
+
}
|
|
23223
|
+
}
|
|
23224
|
+
async function mcpCallWithAutoInit(toolName, args, options) {
|
|
23225
|
+
const maxAttempts = options?.maxReregistrationAttempts ?? 1;
|
|
23226
|
+
let reregistrationAttempts = 0;
|
|
23227
|
+
while (true) {
|
|
23228
|
+
try {
|
|
23229
|
+
return await mcpCall(toolName, args);
|
|
23230
|
+
} catch (error45) {
|
|
23231
|
+
const isProjectError = isProjectNotFoundError(error45);
|
|
23232
|
+
const isAgentError = isAgentNotFoundError(error45);
|
|
23233
|
+
if (!isProjectError && !isAgentError) {
|
|
23234
|
+
throw error45;
|
|
23235
|
+
}
|
|
23236
|
+
if (reregistrationAttempts >= maxAttempts) {
|
|
23237
|
+
console.error(`[agent-mail] Exhausted ${maxAttempts} re-registration attempt(s) for ${toolName}`);
|
|
23238
|
+
throw error45;
|
|
23239
|
+
}
|
|
23240
|
+
reregistrationAttempts++;
|
|
23241
|
+
console.warn(`[agent-mail] Detected "${isProjectError ? "project" : "agent"} not found" for ${toolName}, ` + `attempting re-registration (attempt ${reregistrationAttempts}/${maxAttempts})...`);
|
|
23242
|
+
const projectOk = await reRegisterProject(args.project_key);
|
|
23243
|
+
if (!projectOk) {
|
|
23244
|
+
throw error45;
|
|
23245
|
+
}
|
|
23246
|
+
if (args.agent_name && (isAgentError || toolName !== "ensure_project")) {
|
|
23247
|
+
const agentOk = await reRegisterAgent(args.project_key, args.agent_name, options?.taskDescription);
|
|
23248
|
+
if (!agentOk) {
|
|
23249
|
+
console.warn(`[agent-mail] Agent re-registration failed, but continuing with retry...`);
|
|
23250
|
+
}
|
|
23251
|
+
}
|
|
23252
|
+
console.warn(`[agent-mail] Retrying ${toolName} after re-registration...`);
|
|
23253
|
+
}
|
|
23254
|
+
}
|
|
23255
|
+
}
|
|
23147
23256
|
function requireState(sessionID) {
|
|
23148
23257
|
let state = sessionStates.get(sessionID);
|
|
23149
23258
|
if (!state) {
|
|
@@ -25217,8 +25326,9 @@ var swarm_progress = tool({
|
|
|
25217
25326
|
await Bun.$`bd update ${args.bead_id} --status ${beadStatus} --json`.quiet().nothrow();
|
|
25218
25327
|
}
|
|
25219
25328
|
const epicId = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
|
|
25220
|
-
await
|
|
25329
|
+
await mcpCallWithAutoInit("send_message", {
|
|
25221
25330
|
project_key: args.project_key,
|
|
25331
|
+
agent_name: args.agent_name,
|
|
25222
25332
|
sender_name: args.agent_name,
|
|
25223
25333
|
to: [],
|
|
25224
25334
|
subject: `Progress: ${args.bead_id} - ${args.status}`,
|
|
@@ -25308,8 +25418,9 @@ ${args.files_affected.map((f) => `- \`${f}\``).join(`
|
|
|
25308
25418
|
].filter(Boolean).join(`
|
|
25309
25419
|
`);
|
|
25310
25420
|
const mailImportance = args.importance === "blocker" ? "urgent" : args.importance === "warning" ? "high" : "normal";
|
|
25311
|
-
await
|
|
25421
|
+
await mcpCallWithAutoInit("send_message", {
|
|
25312
25422
|
project_key: state.projectKey,
|
|
25423
|
+
agent_name: state.agentName,
|
|
25313
25424
|
sender_name: state.agentName,
|
|
25314
25425
|
to: [],
|
|
25315
25426
|
subject: `[${args.importance.toUpperCase()}] Context update from ${state.agentName}`,
|
|
@@ -25381,7 +25492,7 @@ var swarm_complete = tool({
|
|
|
25381
25492
|
throw new SwarmError(`Failed to close bead: ${closeResult.stderr.toString()}`, "complete");
|
|
25382
25493
|
}
|
|
25383
25494
|
try {
|
|
25384
|
-
await
|
|
25495
|
+
await mcpCallWithAutoInit("release_file_reservations", {
|
|
25385
25496
|
project_key: args.project_key,
|
|
25386
25497
|
agent_name: args.agent_name
|
|
25387
25498
|
});
|
|
@@ -25398,8 +25509,9 @@ var swarm_complete = tool({
|
|
|
25398
25509
|
parsedEvaluation?.overall_feedback ? `**Feedback**: ${parsedEvaluation.overall_feedback}` : ""
|
|
25399
25510
|
].filter(Boolean).join(`
|
|
25400
25511
|
`);
|
|
25401
|
-
await
|
|
25512
|
+
await mcpCallWithAutoInit("send_message", {
|
|
25402
25513
|
project_key: args.project_key,
|
|
25514
|
+
agent_name: args.agent_name,
|
|
25403
25515
|
sender_name: args.agent_name,
|
|
25404
25516
|
to: [],
|
|
25405
25517
|
subject: `Complete: ${args.bead_id}`,
|
package/dist/plugin.js
CHANGED
|
@@ -22153,7 +22153,40 @@ var beads_sync = tool({
|
|
|
22153
22153
|
}
|
|
22154
22154
|
}
|
|
22155
22155
|
if (autoPull) {
|
|
22156
|
+
const dirtyCheckResult = await runGitCommand([
|
|
22157
|
+
"status",
|
|
22158
|
+
"--porcelain",
|
|
22159
|
+
"--untracked-files=no"
|
|
22160
|
+
]);
|
|
22161
|
+
const hasDirtyFiles = dirtyCheckResult.stdout.trim() !== "";
|
|
22162
|
+
let didStash = false;
|
|
22163
|
+
if (hasDirtyFiles) {
|
|
22164
|
+
console.warn("[beads] Detected unstaged changes, stashing before pull...");
|
|
22165
|
+
const stashResult = await runGitCommand([
|
|
22166
|
+
"stash",
|
|
22167
|
+
"push",
|
|
22168
|
+
"-m",
|
|
22169
|
+
"beads_sync: auto-stash before pull",
|
|
22170
|
+
"--include-untracked"
|
|
22171
|
+
]);
|
|
22172
|
+
if (stashResult.exitCode === 0) {
|
|
22173
|
+
didStash = true;
|
|
22174
|
+
console.warn("[beads] Changes stashed successfully");
|
|
22175
|
+
} else {
|
|
22176
|
+
console.warn(`[beads] Stash failed (${stashResult.stderr}), attempting pull anyway...`);
|
|
22177
|
+
}
|
|
22178
|
+
}
|
|
22156
22179
|
const pullResult = await withTimeout(runGitCommand(["pull", "--rebase"]), TIMEOUT_MS, "git pull --rebase");
|
|
22180
|
+
if (didStash) {
|
|
22181
|
+
console.warn("[beads] Restoring stashed changes...");
|
|
22182
|
+
const unstashResult = await runGitCommand(["stash", "pop"]);
|
|
22183
|
+
if (unstashResult.exitCode !== 0) {
|
|
22184
|
+
console.error(`[beads] WARNING: Failed to restore stashed changes: ${unstashResult.stderr}`);
|
|
22185
|
+
console.error("[beads] Your changes are in 'git stash list' - run 'git stash pop' manually");
|
|
22186
|
+
} else {
|
|
22187
|
+
console.warn("[beads] Stashed changes restored");
|
|
22188
|
+
}
|
|
22189
|
+
}
|
|
22157
22190
|
if (pullResult.exitCode !== 0) {
|
|
22158
22191
|
throw new BeadError(`Failed to pull: ${pullResult.stderr}`, "git pull --rebase", pullResult.exitCode);
|
|
22159
22192
|
}
|
|
@@ -22984,6 +23017,20 @@ function isRetryableError(error45) {
|
|
|
22984
23017
|
}
|
|
22985
23018
|
return false;
|
|
22986
23019
|
}
|
|
23020
|
+
function isProjectNotFoundError(error45) {
|
|
23021
|
+
if (error45 instanceof Error) {
|
|
23022
|
+
const message = error45.message.toLowerCase();
|
|
23023
|
+
return message.includes("project") && (message.includes("not found") || message.includes("does not exist"));
|
|
23024
|
+
}
|
|
23025
|
+
return false;
|
|
23026
|
+
}
|
|
23027
|
+
function isAgentNotFoundError(error45) {
|
|
23028
|
+
if (error45 instanceof Error) {
|
|
23029
|
+
const message = error45.message.toLowerCase();
|
|
23030
|
+
return message.includes("agent") && (message.includes("not found") || message.includes("does not exist"));
|
|
23031
|
+
}
|
|
23032
|
+
return false;
|
|
23033
|
+
}
|
|
22987
23034
|
var agentMailAvailable = null;
|
|
22988
23035
|
async function checkAgentMailAvailable() {
|
|
22989
23036
|
if (agentMailAvailable !== null) {
|
|
@@ -23114,6 +23161,68 @@ async function mcpCall(toolName, args) {
|
|
|
23114
23161
|
}
|
|
23115
23162
|
throw lastError || new Error("Unknown error in mcpCall");
|
|
23116
23163
|
}
|
|
23164
|
+
async function reRegisterProject(projectKey) {
|
|
23165
|
+
try {
|
|
23166
|
+
console.warn(`[agent-mail] Re-registering project "${projectKey}" after server restart...`);
|
|
23167
|
+
await mcpCall("ensure_project", {
|
|
23168
|
+
human_key: projectKey
|
|
23169
|
+
});
|
|
23170
|
+
console.warn(`[agent-mail] Project "${projectKey}" re-registered successfully`);
|
|
23171
|
+
return true;
|
|
23172
|
+
} catch (error45) {
|
|
23173
|
+
console.error(`[agent-mail] Failed to re-register project "${projectKey}":`, error45);
|
|
23174
|
+
return false;
|
|
23175
|
+
}
|
|
23176
|
+
}
|
|
23177
|
+
async function reRegisterAgent(projectKey, agentName, taskDescription) {
|
|
23178
|
+
try {
|
|
23179
|
+
console.warn(`[agent-mail] Re-registering agent "${agentName}" for project "${projectKey}"...`);
|
|
23180
|
+
await mcpCall("register_agent", {
|
|
23181
|
+
project_key: projectKey,
|
|
23182
|
+
program: "opencode",
|
|
23183
|
+
model: "claude-opus-4",
|
|
23184
|
+
name: agentName,
|
|
23185
|
+
task_description: taskDescription || "Re-registered after server restart"
|
|
23186
|
+
});
|
|
23187
|
+
console.warn(`[agent-mail] Agent "${agentName}" re-registered successfully`);
|
|
23188
|
+
return true;
|
|
23189
|
+
} catch (error45) {
|
|
23190
|
+
console.error(`[agent-mail] Failed to re-register agent "${agentName}":`, error45);
|
|
23191
|
+
return false;
|
|
23192
|
+
}
|
|
23193
|
+
}
|
|
23194
|
+
async function mcpCallWithAutoInit(toolName, args, options) {
|
|
23195
|
+
const maxAttempts = options?.maxReregistrationAttempts ?? 1;
|
|
23196
|
+
let reregistrationAttempts = 0;
|
|
23197
|
+
while (true) {
|
|
23198
|
+
try {
|
|
23199
|
+
return await mcpCall(toolName, args);
|
|
23200
|
+
} catch (error45) {
|
|
23201
|
+
const isProjectError = isProjectNotFoundError(error45);
|
|
23202
|
+
const isAgentError = isAgentNotFoundError(error45);
|
|
23203
|
+
if (!isProjectError && !isAgentError) {
|
|
23204
|
+
throw error45;
|
|
23205
|
+
}
|
|
23206
|
+
if (reregistrationAttempts >= maxAttempts) {
|
|
23207
|
+
console.error(`[agent-mail] Exhausted ${maxAttempts} re-registration attempt(s) for ${toolName}`);
|
|
23208
|
+
throw error45;
|
|
23209
|
+
}
|
|
23210
|
+
reregistrationAttempts++;
|
|
23211
|
+
console.warn(`[agent-mail] Detected "${isProjectError ? "project" : "agent"} not found" for ${toolName}, ` + `attempting re-registration (attempt ${reregistrationAttempts}/${maxAttempts})...`);
|
|
23212
|
+
const projectOk = await reRegisterProject(args.project_key);
|
|
23213
|
+
if (!projectOk) {
|
|
23214
|
+
throw error45;
|
|
23215
|
+
}
|
|
23216
|
+
if (args.agent_name && (isAgentError || toolName !== "ensure_project")) {
|
|
23217
|
+
const agentOk = await reRegisterAgent(args.project_key, args.agent_name, options?.taskDescription);
|
|
23218
|
+
if (!agentOk) {
|
|
23219
|
+
console.warn(`[agent-mail] Agent re-registration failed, but continuing with retry...`);
|
|
23220
|
+
}
|
|
23221
|
+
}
|
|
23222
|
+
console.warn(`[agent-mail] Retrying ${toolName} after re-registration...`);
|
|
23223
|
+
}
|
|
23224
|
+
}
|
|
23225
|
+
}
|
|
23117
23226
|
function requireState(sessionID) {
|
|
23118
23227
|
let state = sessionStates.get(sessionID);
|
|
23119
23228
|
if (!state) {
|
|
@@ -25156,8 +25265,9 @@ var swarm_progress = tool({
|
|
|
25156
25265
|
await Bun.$`bd update ${args.bead_id} --status ${beadStatus} --json`.quiet().nothrow();
|
|
25157
25266
|
}
|
|
25158
25267
|
const epicId = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
|
|
25159
|
-
await
|
|
25268
|
+
await mcpCallWithAutoInit("send_message", {
|
|
25160
25269
|
project_key: args.project_key,
|
|
25270
|
+
agent_name: args.agent_name,
|
|
25161
25271
|
sender_name: args.agent_name,
|
|
25162
25272
|
to: [],
|
|
25163
25273
|
subject: `Progress: ${args.bead_id} - ${args.status}`,
|
|
@@ -25247,8 +25357,9 @@ ${args.files_affected.map((f) => `- \`${f}\``).join(`
|
|
|
25247
25357
|
].filter(Boolean).join(`
|
|
25248
25358
|
`);
|
|
25249
25359
|
const mailImportance = args.importance === "blocker" ? "urgent" : args.importance === "warning" ? "high" : "normal";
|
|
25250
|
-
await
|
|
25360
|
+
await mcpCallWithAutoInit("send_message", {
|
|
25251
25361
|
project_key: state.projectKey,
|
|
25362
|
+
agent_name: state.agentName,
|
|
25252
25363
|
sender_name: state.agentName,
|
|
25253
25364
|
to: [],
|
|
25254
25365
|
subject: `[${args.importance.toUpperCase()}] Context update from ${state.agentName}`,
|
|
@@ -25320,7 +25431,7 @@ var swarm_complete = tool({
|
|
|
25320
25431
|
throw new SwarmError(`Failed to close bead: ${closeResult.stderr.toString()}`, "complete");
|
|
25321
25432
|
}
|
|
25322
25433
|
try {
|
|
25323
|
-
await
|
|
25434
|
+
await mcpCallWithAutoInit("release_file_reservations", {
|
|
25324
25435
|
project_key: args.project_key,
|
|
25325
25436
|
agent_name: args.agent_name
|
|
25326
25437
|
});
|
|
@@ -25337,8 +25448,9 @@ var swarm_complete = tool({
|
|
|
25337
25448
|
parsedEvaluation?.overall_feedback ? `**Feedback**: ${parsedEvaluation.overall_feedback}` : ""
|
|
25338
25449
|
].filter(Boolean).join(`
|
|
25339
25450
|
`);
|
|
25340
|
-
await
|
|
25451
|
+
await mcpCallWithAutoInit("send_message", {
|
|
25341
25452
|
project_key: args.project_key,
|
|
25453
|
+
agent_name: args.agent_name,
|
|
25342
25454
|
sender_name: args.agent_name,
|
|
25343
25455
|
to: [],
|
|
25344
25456
|
subject: `Complete: ${args.bead_id}`,
|
package/package.json
CHANGED
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
import { describe, it, expect, beforeAll } from "vitest";
|
|
12
12
|
import {
|
|
13
13
|
mcpCall,
|
|
14
|
+
mcpCallWithAutoInit,
|
|
14
15
|
sessionStates,
|
|
15
16
|
setState,
|
|
16
17
|
clearState,
|
|
17
18
|
requireState,
|
|
18
19
|
MAX_INBOX_LIMIT,
|
|
19
20
|
AgentMailNotInitializedError,
|
|
21
|
+
isProjectNotFoundError,
|
|
22
|
+
isAgentNotFoundError,
|
|
20
23
|
type AgentMailState,
|
|
21
24
|
} from "./agent-mail";
|
|
22
25
|
|
|
@@ -1318,4 +1321,109 @@ describe("agent-mail integration", () => {
|
|
|
1318
1321
|
clearState(worker2Ctx.sessionID);
|
|
1319
1322
|
});
|
|
1320
1323
|
});
|
|
1324
|
+
|
|
1325
|
+
// ============================================================================
|
|
1326
|
+
// Self-Healing Tests (mcpCallWithAutoInit)
|
|
1327
|
+
// ============================================================================
|
|
1328
|
+
|
|
1329
|
+
describe("self-healing (mcpCallWithAutoInit)", () => {
|
|
1330
|
+
it("detects project not found errors correctly", () => {
|
|
1331
|
+
const projectError = new Error("Project 'migrate-egghead' not found.");
|
|
1332
|
+
const agentError = new Error("Agent 'BlueLake' not found in project");
|
|
1333
|
+
const otherError = new Error("Network timeout");
|
|
1334
|
+
|
|
1335
|
+
expect(isProjectNotFoundError(projectError)).toBe(true);
|
|
1336
|
+
expect(isProjectNotFoundError(agentError)).toBe(false);
|
|
1337
|
+
expect(isProjectNotFoundError(otherError)).toBe(false);
|
|
1338
|
+
|
|
1339
|
+
expect(isAgentNotFoundError(agentError)).toBe(true);
|
|
1340
|
+
expect(isAgentNotFoundError(projectError)).toBe(false);
|
|
1341
|
+
expect(isAgentNotFoundError(otherError)).toBe(false);
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
it("auto-registers project on 'not found' error", async () => {
|
|
1345
|
+
const ctx = createTestContext();
|
|
1346
|
+
|
|
1347
|
+
// First, ensure project exists and register an agent
|
|
1348
|
+
const { state } = await initTestAgent(ctx, `AutoInit_${Date.now()}`);
|
|
1349
|
+
|
|
1350
|
+
// Now use mcpCallWithAutoInit - it should work normally
|
|
1351
|
+
// (no error to recover from, but verifies the wrapper works)
|
|
1352
|
+
await mcpCallWithAutoInit("send_message", {
|
|
1353
|
+
project_key: state.projectKey,
|
|
1354
|
+
agent_name: state.agentName,
|
|
1355
|
+
sender_name: state.agentName,
|
|
1356
|
+
to: [],
|
|
1357
|
+
subject: "Test auto-init wrapper",
|
|
1358
|
+
body_md: "This should work normally",
|
|
1359
|
+
thread_id: "test-thread",
|
|
1360
|
+
importance: "normal",
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
// Verify message was sent by checking inbox
|
|
1364
|
+
const inbox = await mcpCall<Array<{ subject: string }>>("fetch_inbox", {
|
|
1365
|
+
project_key: state.projectKey,
|
|
1366
|
+
agent_name: state.agentName,
|
|
1367
|
+
limit: 5,
|
|
1368
|
+
include_bodies: false,
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
// The message should be in the inbox (sent to empty 'to' = broadcast)
|
|
1372
|
+
// Note: depending on Agent Mail behavior, broadcast might not show in sender's inbox
|
|
1373
|
+
// This test mainly verifies the wrapper doesn't break normal operation
|
|
1374
|
+
|
|
1375
|
+
// Cleanup
|
|
1376
|
+
clearState(ctx.sessionID);
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
it("recovers from simulated project not found by re-registering", async () => {
|
|
1380
|
+
const ctx = createTestContext();
|
|
1381
|
+
|
|
1382
|
+
// Create a fresh project key that doesn't exist yet
|
|
1383
|
+
const freshProjectKey = `/test/fresh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1384
|
+
const agentName = `Recovery_${Date.now()}`;
|
|
1385
|
+
|
|
1386
|
+
// First ensure the project exists (simulating initial setup)
|
|
1387
|
+
await mcpCall("ensure_project", { human_key: freshProjectKey });
|
|
1388
|
+
await mcpCall("register_agent", {
|
|
1389
|
+
project_key: freshProjectKey,
|
|
1390
|
+
program: "opencode-test",
|
|
1391
|
+
model: "test-model",
|
|
1392
|
+
name: agentName,
|
|
1393
|
+
task_description: "Recovery test agent",
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
// Now use mcpCallWithAutoInit for an operation
|
|
1397
|
+
// This should work, and if the project somehow got lost, it would re-register
|
|
1398
|
+
await mcpCallWithAutoInit("send_message", {
|
|
1399
|
+
project_key: freshProjectKey,
|
|
1400
|
+
agent_name: agentName,
|
|
1401
|
+
sender_name: agentName,
|
|
1402
|
+
to: [],
|
|
1403
|
+
subject: "Recovery test",
|
|
1404
|
+
body_md: "Testing self-healing",
|
|
1405
|
+
thread_id: "recovery-test",
|
|
1406
|
+
importance: "normal",
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
// If we got here without error, the wrapper is working
|
|
1410
|
+
// (In a real scenario where the server restarted, it would have re-registered)
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it("passes through non-recoverable errors", async () => {
|
|
1414
|
+
const ctx = createTestContext();
|
|
1415
|
+
const { state } = await initTestAgent(ctx, `ErrorPass_${Date.now()}`);
|
|
1416
|
+
|
|
1417
|
+
// Try to call a non-existent tool - should throw, not retry forever
|
|
1418
|
+
await expect(
|
|
1419
|
+
mcpCallWithAutoInit("nonexistent_tool_xyz", {
|
|
1420
|
+
project_key: state.projectKey,
|
|
1421
|
+
agent_name: state.agentName,
|
|
1422
|
+
}),
|
|
1423
|
+
).rejects.toThrow(/Unknown tool/);
|
|
1424
|
+
|
|
1425
|
+
// Cleanup
|
|
1426
|
+
clearState(ctx.sessionID);
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1321
1429
|
});
|
package/src/agent-mail.ts
CHANGED
|
@@ -541,6 +541,39 @@ function isRetryableError(error: unknown): boolean {
|
|
|
541
541
|
return false;
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Check if an error indicates the project was not found
|
|
546
|
+
*
|
|
547
|
+
* This happens when Agent Mail server restarts and loses project registrations.
|
|
548
|
+
* The fix is to re-register the project and retry the operation.
|
|
549
|
+
*/
|
|
550
|
+
export function isProjectNotFoundError(error: unknown): boolean {
|
|
551
|
+
if (error instanceof Error) {
|
|
552
|
+
const message = error.message.toLowerCase();
|
|
553
|
+
return (
|
|
554
|
+
message.includes("project") &&
|
|
555
|
+
(message.includes("not found") || message.includes("does not exist"))
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Check if an error indicates the agent was not found
|
|
563
|
+
*
|
|
564
|
+
* Similar to project not found - server restart loses agent registrations.
|
|
565
|
+
*/
|
|
566
|
+
export function isAgentNotFoundError(error: unknown): boolean {
|
|
567
|
+
if (error instanceof Error) {
|
|
568
|
+
const message = error.message.toLowerCase();
|
|
569
|
+
return (
|
|
570
|
+
message.includes("agent") &&
|
|
571
|
+
(message.includes("not found") || message.includes("does not exist"))
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
544
577
|
// ============================================================================
|
|
545
578
|
// MCP Client
|
|
546
579
|
// ============================================================================
|
|
@@ -823,6 +856,153 @@ export async function mcpCall<T>(
|
|
|
823
856
|
throw lastError || new Error("Unknown error in mcpCall");
|
|
824
857
|
}
|
|
825
858
|
|
|
859
|
+
/**
|
|
860
|
+
* Re-register a project with Agent Mail server
|
|
861
|
+
*
|
|
862
|
+
* Called when we detect "Project not found" error, indicating server restart.
|
|
863
|
+
* This is a lightweight operation that just ensures the project exists.
|
|
864
|
+
*/
|
|
865
|
+
async function reRegisterProject(projectKey: string): Promise<boolean> {
|
|
866
|
+
try {
|
|
867
|
+
console.warn(
|
|
868
|
+
`[agent-mail] Re-registering project "${projectKey}" after server restart...`,
|
|
869
|
+
);
|
|
870
|
+
await mcpCall<ProjectInfo>("ensure_project", {
|
|
871
|
+
human_key: projectKey,
|
|
872
|
+
});
|
|
873
|
+
console.warn(
|
|
874
|
+
`[agent-mail] Project "${projectKey}" re-registered successfully`,
|
|
875
|
+
);
|
|
876
|
+
return true;
|
|
877
|
+
} catch (error) {
|
|
878
|
+
console.error(
|
|
879
|
+
`[agent-mail] Failed to re-register project "${projectKey}":`,
|
|
880
|
+
error,
|
|
881
|
+
);
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Re-register an agent with Agent Mail server
|
|
888
|
+
*
|
|
889
|
+
* Called when we detect "Agent not found" error, indicating server restart.
|
|
890
|
+
*/
|
|
891
|
+
async function reRegisterAgent(
|
|
892
|
+
projectKey: string,
|
|
893
|
+
agentName: string,
|
|
894
|
+
taskDescription?: string,
|
|
895
|
+
): Promise<boolean> {
|
|
896
|
+
try {
|
|
897
|
+
console.warn(
|
|
898
|
+
`[agent-mail] Re-registering agent "${agentName}" for project "${projectKey}"...`,
|
|
899
|
+
);
|
|
900
|
+
await mcpCall<AgentInfo>("register_agent", {
|
|
901
|
+
project_key: projectKey,
|
|
902
|
+
program: "opencode",
|
|
903
|
+
model: "claude-opus-4",
|
|
904
|
+
name: agentName,
|
|
905
|
+
task_description: taskDescription || "Re-registered after server restart",
|
|
906
|
+
});
|
|
907
|
+
console.warn(
|
|
908
|
+
`[agent-mail] Agent "${agentName}" re-registered successfully`,
|
|
909
|
+
);
|
|
910
|
+
return true;
|
|
911
|
+
} catch (error) {
|
|
912
|
+
console.error(
|
|
913
|
+
`[agent-mail] Failed to re-register agent "${agentName}":`,
|
|
914
|
+
error,
|
|
915
|
+
);
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* MCP call with automatic project/agent re-registration on "not found" errors
|
|
922
|
+
*
|
|
923
|
+
* This is the self-healing wrapper that handles Agent Mail server restarts.
|
|
924
|
+
* When the server restarts, it loses all project and agent registrations.
|
|
925
|
+
* This wrapper detects those errors and automatically re-registers before retrying.
|
|
926
|
+
*
|
|
927
|
+
* Use this instead of raw mcpCall when you have project_key and agent_name context.
|
|
928
|
+
*
|
|
929
|
+
* @param toolName - The MCP tool to call
|
|
930
|
+
* @param args - Arguments including project_key and optionally agent_name
|
|
931
|
+
* @param options - Optional configuration for re-registration
|
|
932
|
+
* @returns The result of the MCP call
|
|
933
|
+
*/
|
|
934
|
+
export async function mcpCallWithAutoInit<T>(
|
|
935
|
+
toolName: string,
|
|
936
|
+
args: Record<string, unknown> & { project_key: string; agent_name?: string },
|
|
937
|
+
options?: {
|
|
938
|
+
/** Task description for agent re-registration */
|
|
939
|
+
taskDescription?: string;
|
|
940
|
+
/** Max re-registration attempts (default: 1) */
|
|
941
|
+
maxReregistrationAttempts?: number;
|
|
942
|
+
},
|
|
943
|
+
): Promise<T> {
|
|
944
|
+
const maxAttempts = options?.maxReregistrationAttempts ?? 1;
|
|
945
|
+
let reregistrationAttempts = 0;
|
|
946
|
+
|
|
947
|
+
while (true) {
|
|
948
|
+
try {
|
|
949
|
+
return await mcpCall<T>(toolName, args);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
// Check if this is a recoverable "not found" error
|
|
952
|
+
const isProjectError = isProjectNotFoundError(error);
|
|
953
|
+
const isAgentError = isAgentNotFoundError(error);
|
|
954
|
+
|
|
955
|
+
if (!isProjectError && !isAgentError) {
|
|
956
|
+
// Not a recoverable error, rethrow
|
|
957
|
+
throw error;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Check if we've exhausted re-registration attempts
|
|
961
|
+
if (reregistrationAttempts >= maxAttempts) {
|
|
962
|
+
console.error(
|
|
963
|
+
`[agent-mail] Exhausted ${maxAttempts} re-registration attempt(s) for ${toolName}`,
|
|
964
|
+
);
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
reregistrationAttempts++;
|
|
969
|
+
console.warn(
|
|
970
|
+
`[agent-mail] Detected "${isProjectError ? "project" : "agent"} not found" for ${toolName}, ` +
|
|
971
|
+
`attempting re-registration (attempt ${reregistrationAttempts}/${maxAttempts})...`,
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
// Re-register project first (always needed)
|
|
975
|
+
const projectOk = await reRegisterProject(args.project_key);
|
|
976
|
+
if (!projectOk) {
|
|
977
|
+
throw error; // Can't recover without project
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Re-register agent if we have one and it was an agent error
|
|
981
|
+
// (or if the original call needs an agent)
|
|
982
|
+
if (args.agent_name && (isAgentError || toolName !== "ensure_project")) {
|
|
983
|
+
const agentOk = await reRegisterAgent(
|
|
984
|
+
args.project_key,
|
|
985
|
+
args.agent_name,
|
|
986
|
+
options?.taskDescription,
|
|
987
|
+
);
|
|
988
|
+
if (!agentOk) {
|
|
989
|
+
// Agent re-registration failed, but project is OK
|
|
990
|
+
// Some operations might still work, so continue
|
|
991
|
+
console.warn(
|
|
992
|
+
`[agent-mail] Agent re-registration failed, but continuing with retry...`,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Retry the original call
|
|
998
|
+
console.warn(
|
|
999
|
+
`[agent-mail] Retrying ${toolName} after re-registration...`,
|
|
1000
|
+
);
|
|
1001
|
+
// Loop continues to retry
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
826
1006
|
/**
|
|
827
1007
|
* Get Agent Mail state for a session, or throw if not initialized
|
|
828
1008
|
*
|
|
@@ -1490,4 +1670,6 @@ export {
|
|
|
1490
1670
|
restartServer,
|
|
1491
1671
|
RETRY_CONFIG,
|
|
1492
1672
|
RECOVERY_CONFIG,
|
|
1673
|
+
// Note: isProjectNotFoundError, isAgentNotFoundError, mcpCallWithAutoInit
|
|
1674
|
+
// are exported at their definitions
|
|
1493
1675
|
};
|
package/src/beads.ts
CHANGED
|
@@ -717,11 +717,61 @@ export const beads_sync = tool({
|
|
|
717
717
|
|
|
718
718
|
// 5. Pull if requested (with rebase to avoid merge commits)
|
|
719
719
|
if (autoPull) {
|
|
720
|
+
// Check for unstaged changes that would block pull --rebase
|
|
721
|
+
const dirtyCheckResult = await runGitCommand([
|
|
722
|
+
"status",
|
|
723
|
+
"--porcelain",
|
|
724
|
+
"--untracked-files=no",
|
|
725
|
+
]);
|
|
726
|
+
const hasDirtyFiles = dirtyCheckResult.stdout.trim() !== "";
|
|
727
|
+
let didStash = false;
|
|
728
|
+
|
|
729
|
+
// Stash dirty files before pull (self-healing for "unstaged changes" error)
|
|
730
|
+
if (hasDirtyFiles) {
|
|
731
|
+
console.warn(
|
|
732
|
+
"[beads] Detected unstaged changes, stashing before pull...",
|
|
733
|
+
);
|
|
734
|
+
const stashResult = await runGitCommand([
|
|
735
|
+
"stash",
|
|
736
|
+
"push",
|
|
737
|
+
"-m",
|
|
738
|
+
"beads_sync: auto-stash before pull",
|
|
739
|
+
"--include-untracked",
|
|
740
|
+
]);
|
|
741
|
+
if (stashResult.exitCode === 0) {
|
|
742
|
+
didStash = true;
|
|
743
|
+
console.warn("[beads] Changes stashed successfully");
|
|
744
|
+
} else {
|
|
745
|
+
// Stash failed - try pull anyway, it might work
|
|
746
|
+
console.warn(
|
|
747
|
+
`[beads] Stash failed (${stashResult.stderr}), attempting pull anyway...`,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
720
752
|
const pullResult = await withTimeout(
|
|
721
753
|
runGitCommand(["pull", "--rebase"]),
|
|
722
754
|
TIMEOUT_MS,
|
|
723
755
|
"git pull --rebase",
|
|
724
756
|
);
|
|
757
|
+
|
|
758
|
+
// Restore stashed changes regardless of pull result
|
|
759
|
+
if (didStash) {
|
|
760
|
+
console.warn("[beads] Restoring stashed changes...");
|
|
761
|
+
const unstashResult = await runGitCommand(["stash", "pop"]);
|
|
762
|
+
if (unstashResult.exitCode !== 0) {
|
|
763
|
+
// Unstash failed - this is bad, user needs to know
|
|
764
|
+
console.error(
|
|
765
|
+
`[beads] WARNING: Failed to restore stashed changes: ${unstashResult.stderr}`,
|
|
766
|
+
);
|
|
767
|
+
console.error(
|
|
768
|
+
"[beads] Your changes are in 'git stash list' - run 'git stash pop' manually",
|
|
769
|
+
);
|
|
770
|
+
} else {
|
|
771
|
+
console.warn("[beads] Stashed changes restored");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
725
775
|
if (pullResult.exitCode !== 0) {
|
|
726
776
|
throw new BeadError(
|
|
727
777
|
`Failed to pull: ${pullResult.stderr}`,
|
package/src/swarm.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type SpawnedAgent,
|
|
26
26
|
type Bead,
|
|
27
27
|
} from "./schemas";
|
|
28
|
-
import { mcpCall, requireState } from "./agent-mail";
|
|
28
|
+
import { mcpCall, mcpCallWithAutoInit, requireState } from "./agent-mail";
|
|
29
29
|
import {
|
|
30
30
|
OutcomeSignalsSchema,
|
|
31
31
|
DecompositionStrategySchema,
|
|
@@ -1222,7 +1222,7 @@ export const swarm_plan_prompt = tool({
|
|
|
1222
1222
|
.number()
|
|
1223
1223
|
.int()
|
|
1224
1224
|
.min(2)
|
|
1225
|
-
|
|
1225
|
+
|
|
1226
1226
|
.default(5)
|
|
1227
1227
|
.describe("Maximum number of subtasks (default: 5)"),
|
|
1228
1228
|
context: tool.schema
|
|
@@ -1237,7 +1237,7 @@ export const swarm_plan_prompt = tool({
|
|
|
1237
1237
|
.number()
|
|
1238
1238
|
.int()
|
|
1239
1239
|
.min(1)
|
|
1240
|
-
|
|
1240
|
+
|
|
1241
1241
|
.optional()
|
|
1242
1242
|
.describe("Max CASS results to include (default: 3)"),
|
|
1243
1243
|
},
|
|
@@ -1352,7 +1352,7 @@ export const swarm_decompose = tool({
|
|
|
1352
1352
|
.number()
|
|
1353
1353
|
.int()
|
|
1354
1354
|
.min(2)
|
|
1355
|
-
|
|
1355
|
+
|
|
1356
1356
|
.default(5)
|
|
1357
1357
|
.describe("Maximum number of subtasks (default: 5)"),
|
|
1358
1358
|
context: tool.schema
|
|
@@ -1367,7 +1367,7 @@ export const swarm_decompose = tool({
|
|
|
1367
1367
|
.number()
|
|
1368
1368
|
.int()
|
|
1369
1369
|
.min(1)
|
|
1370
|
-
|
|
1370
|
+
|
|
1371
1371
|
.optional()
|
|
1372
1372
|
.describe("Max CASS results to include (default: 3)"),
|
|
1373
1373
|
},
|
|
@@ -1721,9 +1721,10 @@ export const swarm_progress = tool({
|
|
|
1721
1721
|
? args.bead_id.split(".")[0]
|
|
1722
1722
|
: args.bead_id;
|
|
1723
1723
|
|
|
1724
|
-
// Send progress message to thread
|
|
1725
|
-
await
|
|
1724
|
+
// Send progress message to thread (with auto-reinit on server restart)
|
|
1725
|
+
await mcpCallWithAutoInit("send_message", {
|
|
1726
1726
|
project_key: args.project_key,
|
|
1727
|
+
agent_name: args.agent_name,
|
|
1727
1728
|
sender_name: args.agent_name,
|
|
1728
1729
|
to: [], // Coordinator will pick it up from thread
|
|
1729
1730
|
subject: `Progress: ${args.bead_id} - ${args.status}`,
|
|
@@ -1892,8 +1893,10 @@ export const swarm_broadcast = tool({
|
|
|
1892
1893
|
: "normal";
|
|
1893
1894
|
|
|
1894
1895
|
// Send as broadcast to thread (empty 'to' = all agents in thread)
|
|
1895
|
-
|
|
1896
|
+
// Uses auto-reinit wrapper to handle server restarts gracefully
|
|
1897
|
+
await mcpCallWithAutoInit("send_message", {
|
|
1896
1898
|
project_key: state.projectKey,
|
|
1899
|
+
agent_name: state.agentName,
|
|
1897
1900
|
sender_name: state.agentName,
|
|
1898
1901
|
to: [], // Broadcast to thread
|
|
1899
1902
|
subject: `[${args.importance.toUpperCase()}] Context update from ${state.agentName}`,
|
|
@@ -2020,12 +2023,16 @@ export const swarm_complete = tool({
|
|
|
2020
2023
|
}
|
|
2021
2024
|
|
|
2022
2025
|
// Release file reservations for this agent
|
|
2026
|
+
// Uses auto-reinit wrapper to handle server restarts - this was the original
|
|
2027
|
+
// failure point that prompted the self-healing implementation
|
|
2023
2028
|
try {
|
|
2024
|
-
await
|
|
2029
|
+
await mcpCallWithAutoInit("release_file_reservations", {
|
|
2025
2030
|
project_key: args.project_key,
|
|
2026
2031
|
agent_name: args.agent_name,
|
|
2027
2032
|
});
|
|
2028
2033
|
} catch (error) {
|
|
2034
|
+
// Even with auto-reinit, release might fail (e.g., no reservations existed)
|
|
2035
|
+
// This is non-fatal - log and continue
|
|
2029
2036
|
console.warn(
|
|
2030
2037
|
`[swarm] Failed to release file reservations for ${args.agent_name}:`,
|
|
2031
2038
|
error,
|
|
@@ -2053,8 +2060,9 @@ export const swarm_complete = tool({
|
|
|
2053
2060
|
.filter(Boolean)
|
|
2054
2061
|
.join("\n");
|
|
2055
2062
|
|
|
2056
|
-
await
|
|
2063
|
+
await mcpCallWithAutoInit("send_message", {
|
|
2057
2064
|
project_key: args.project_key,
|
|
2065
|
+
agent_name: args.agent_name,
|
|
2058
2066
|
sender_name: args.agent_name,
|
|
2059
2067
|
to: [], // Thread broadcast
|
|
2060
2068
|
subject: `Complete: ${args.bead_id}`,
|