opencode-swarm-plugin 0.44.0 → 0.44.2
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/bin/swarm.serve.test.ts +6 -4
- package/bin/swarm.ts +18 -12
- package/dist/compaction-prompt-scoring.js +139 -0
- package/dist/eval-capture.js +12811 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/hive.js +14834 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7743 -62593
- package/dist/plugin.js +24052 -78907
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-prompts.js +39407 -0
- package/dist/swarm-review.d.ts.map +1 -1
- package/dist/swarm-validation.d.ts +127 -0
- package/dist/swarm-validation.d.ts.map +1 -0
- package/dist/validators/index.d.ts +7 -0
- package/dist/validators/index.d.ts.map +1 -0
- package/dist/validators/schema-validator.d.ts +58 -0
- package/dist/validators/schema-validator.d.ts.map +1 -0
- package/package.json +17 -5
- package/.changeset/swarm-insights-data-layer.md +0 -63
- package/.hive/analysis/eval-failure-analysis-2025-12-25.md +0 -331
- package/.hive/analysis/session-data-quality-audit.md +0 -320
- package/.hive/eval-results.json +0 -483
- package/.hive/issues.jsonl +0 -138
- package/.hive/memories.jsonl +0 -729
- package/.opencode/eval-history.jsonl +0 -327
- package/.turbo/turbo-build.log +0 -9
- package/CHANGELOG.md +0 -2286
- package/SCORER-ANALYSIS.md +0 -598
- package/docs/analysis/subagent-coordination-patterns.md +0 -902
- package/docs/analysis-socratic-planner-pattern.md +0 -504
- package/docs/planning/ADR-001-monorepo-structure.md +0 -171
- package/docs/planning/ADR-002-package-extraction.md +0 -393
- package/docs/planning/ADR-003-performance-improvements.md +0 -451
- package/docs/planning/ADR-004-message-queue-features.md +0 -187
- package/docs/planning/ADR-005-devtools-observability.md +0 -202
- package/docs/planning/ADR-007-swarm-enhancements-worktree-review.md +0 -168
- package/docs/planning/ADR-008-worker-handoff-protocol.md +0 -293
- package/docs/planning/ADR-009-oh-my-opencode-patterns.md +0 -353
- package/docs/planning/ADR-010-cass-inhousing.md +0 -1215
- package/docs/planning/ROADMAP.md +0 -368
- package/docs/semantic-memory-cli-syntax.md +0 -123
- package/docs/swarm-mail-architecture.md +0 -1147
- package/docs/testing/context-recovery-test.md +0 -470
- package/evals/ARCHITECTURE.md +0 -1189
- package/evals/README.md +0 -768
- package/evals/compaction-prompt.eval.ts +0 -149
- package/evals/compaction-resumption.eval.ts +0 -289
- package/evals/coordinator-behavior.eval.ts +0 -307
- package/evals/coordinator-session.eval.ts +0 -154
- package/evals/evalite.config.ts.bak +0 -15
- package/evals/example.eval.ts +0 -31
- package/evals/fixtures/cass-baseline.ts +0 -217
- package/evals/fixtures/compaction-cases.ts +0 -350
- package/evals/fixtures/compaction-prompt-cases.ts +0 -311
- package/evals/fixtures/coordinator-sessions.ts +0 -328
- package/evals/fixtures/decomposition-cases.ts +0 -105
- package/evals/lib/compaction-loader.test.ts +0 -248
- package/evals/lib/compaction-loader.ts +0 -320
- package/evals/lib/data-loader.evalite-test.ts +0 -289
- package/evals/lib/data-loader.test.ts +0 -345
- package/evals/lib/data-loader.ts +0 -281
- package/evals/lib/llm.ts +0 -115
- package/evals/scorers/compaction-prompt-scorers.ts +0 -145
- package/evals/scorers/compaction-scorers.ts +0 -305
- package/evals/scorers/coordinator-discipline.evalite-test.ts +0 -539
- package/evals/scorers/coordinator-discipline.ts +0 -325
- package/evals/scorers/index.test.ts +0 -146
- package/evals/scorers/index.ts +0 -328
- package/evals/scorers/outcome-scorers.evalite-test.ts +0 -27
- package/evals/scorers/outcome-scorers.ts +0 -349
- package/evals/swarm-decomposition.eval.ts +0 -121
- package/examples/commands/swarm.md +0 -745
- package/examples/plugin-wrapper-template.ts +0 -2515
- package/examples/skills/hive-workflow/SKILL.md +0 -212
- package/examples/skills/skill-creator/SKILL.md +0 -223
- package/examples/skills/swarm-coordination/SKILL.md +0 -292
- package/global-skills/cli-builder/SKILL.md +0 -344
- package/global-skills/cli-builder/references/advanced-patterns.md +0 -244
- package/global-skills/learning-systems/SKILL.md +0 -644
- package/global-skills/skill-creator/LICENSE.txt +0 -202
- package/global-skills/skill-creator/SKILL.md +0 -352
- package/global-skills/skill-creator/references/output-patterns.md +0 -82
- package/global-skills/skill-creator/references/workflows.md +0 -28
- package/global-skills/swarm-coordination/SKILL.md +0 -995
- package/global-skills/swarm-coordination/references/coordinator-patterns.md +0 -235
- package/global-skills/swarm-coordination/references/strategies.md +0 -138
- package/global-skills/system-design/SKILL.md +0 -213
- package/global-skills/testing-patterns/SKILL.md +0 -430
- package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +0 -586
- package/opencode-swarm-plugin-0.30.7.tgz +0 -0
- package/opencode-swarm-plugin-0.31.0.tgz +0 -0
- package/scripts/cleanup-test-memories.ts +0 -346
- package/scripts/init-skill.ts +0 -222
- package/scripts/migrate-unknown-sessions.ts +0 -349
- package/scripts/validate-skill.ts +0 -204
- package/src/agent-mail.ts +0 -1724
- package/src/anti-patterns.test.ts +0 -1167
- package/src/anti-patterns.ts +0 -448
- package/src/compaction-capture.integration.test.ts +0 -257
- package/src/compaction-hook.test.ts +0 -838
- package/src/compaction-hook.ts +0 -1204
- package/src/compaction-observability.integration.test.ts +0 -139
- package/src/compaction-observability.test.ts +0 -187
- package/src/compaction-observability.ts +0 -324
- package/src/compaction-prompt-scorers.test.ts +0 -475
- package/src/compaction-prompt-scoring.ts +0 -300
- package/src/contributor-tools.test.ts +0 -133
- package/src/contributor-tools.ts +0 -201
- package/src/dashboard.test.ts +0 -611
- package/src/dashboard.ts +0 -462
- package/src/error-enrichment.test.ts +0 -403
- package/src/error-enrichment.ts +0 -219
- package/src/eval-capture.test.ts +0 -1015
- package/src/eval-capture.ts +0 -929
- package/src/eval-gates.test.ts +0 -306
- package/src/eval-gates.ts +0 -218
- package/src/eval-history.test.ts +0 -508
- package/src/eval-history.ts +0 -214
- package/src/eval-learning.test.ts +0 -378
- package/src/eval-learning.ts +0 -360
- package/src/eval-runner.test.ts +0 -223
- package/src/eval-runner.ts +0 -402
- package/src/export-tools.test.ts +0 -476
- package/src/export-tools.ts +0 -257
- package/src/hive.integration.test.ts +0 -2241
- package/src/hive.ts +0 -1628
- package/src/index.ts +0 -940
- package/src/learning.integration.test.ts +0 -1815
- package/src/learning.ts +0 -1079
- package/src/logger.test.ts +0 -189
- package/src/logger.ts +0 -135
- package/src/mandate-promotion.test.ts +0 -473
- package/src/mandate-promotion.ts +0 -239
- package/src/mandate-storage.integration.test.ts +0 -601
- package/src/mandate-storage.test.ts +0 -578
- package/src/mandate-storage.ts +0 -794
- package/src/mandates.ts +0 -540
- package/src/memory-tools.test.ts +0 -195
- package/src/memory-tools.ts +0 -344
- package/src/memory.integration.test.ts +0 -334
- package/src/memory.test.ts +0 -158
- package/src/memory.ts +0 -527
- package/src/model-selection.test.ts +0 -188
- package/src/model-selection.ts +0 -68
- package/src/observability-tools.test.ts +0 -359
- package/src/observability-tools.ts +0 -871
- package/src/output-guardrails.test.ts +0 -438
- package/src/output-guardrails.ts +0 -381
- package/src/pattern-maturity.test.ts +0 -1160
- package/src/pattern-maturity.ts +0 -525
- package/src/planning-guardrails.test.ts +0 -491
- package/src/planning-guardrails.ts +0 -438
- package/src/plugin.ts +0 -23
- package/src/post-compaction-tracker.test.ts +0 -251
- package/src/post-compaction-tracker.ts +0 -237
- package/src/query-tools.test.ts +0 -636
- package/src/query-tools.ts +0 -324
- package/src/rate-limiter.integration.test.ts +0 -466
- package/src/rate-limiter.ts +0 -774
- package/src/replay-tools.test.ts +0 -496
- package/src/replay-tools.ts +0 -240
- package/src/repo-crawl.integration.test.ts +0 -441
- package/src/repo-crawl.ts +0 -610
- package/src/schemas/cell-events.test.ts +0 -347
- package/src/schemas/cell-events.ts +0 -807
- package/src/schemas/cell.ts +0 -257
- package/src/schemas/evaluation.ts +0 -166
- package/src/schemas/index.test.ts +0 -199
- package/src/schemas/index.ts +0 -286
- package/src/schemas/mandate.ts +0 -232
- package/src/schemas/swarm-context.ts +0 -115
- package/src/schemas/task.ts +0 -161
- package/src/schemas/worker-handoff.test.ts +0 -302
- package/src/schemas/worker-handoff.ts +0 -131
- package/src/sessions/agent-discovery.test.ts +0 -137
- package/src/sessions/agent-discovery.ts +0 -112
- package/src/sessions/index.ts +0 -15
- package/src/skills.integration.test.ts +0 -1192
- package/src/skills.test.ts +0 -643
- package/src/skills.ts +0 -1549
- package/src/storage.integration.test.ts +0 -341
- package/src/storage.ts +0 -884
- package/src/structured.integration.test.ts +0 -817
- package/src/structured.test.ts +0 -1046
- package/src/structured.ts +0 -762
- package/src/swarm-decompose.test.ts +0 -188
- package/src/swarm-decompose.ts +0 -1302
- package/src/swarm-deferred.integration.test.ts +0 -157
- package/src/swarm-deferred.test.ts +0 -38
- package/src/swarm-insights.test.ts +0 -214
- package/src/swarm-insights.ts +0 -459
- package/src/swarm-mail.integration.test.ts +0 -970
- package/src/swarm-mail.ts +0 -739
- package/src/swarm-orchestrate.integration.test.ts +0 -282
- package/src/swarm-orchestrate.test.ts +0 -548
- package/src/swarm-orchestrate.ts +0 -3084
- package/src/swarm-prompts.test.ts +0 -1270
- package/src/swarm-prompts.ts +0 -2077
- package/src/swarm-research.integration.test.ts +0 -701
- package/src/swarm-research.test.ts +0 -698
- package/src/swarm-research.ts +0 -472
- package/src/swarm-review.integration.test.ts +0 -285
- package/src/swarm-review.test.ts +0 -879
- package/src/swarm-review.ts +0 -709
- package/src/swarm-strategies.ts +0 -407
- package/src/swarm-worktree.test.ts +0 -501
- package/src/swarm-worktree.ts +0 -575
- package/src/swarm.integration.test.ts +0 -2377
- package/src/swarm.ts +0 -38
- package/src/tool-adapter.integration.test.ts +0 -1221
- package/src/tool-availability.ts +0 -461
- package/tsconfig.json +0 -28
package/src/agent-mail.ts
DELETED
|
@@ -1,1724 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Mail Module - MCP client for multi-agent coordination
|
|
3
|
-
*
|
|
4
|
-
* ⚠️ DEPRECATED: This MCP-based implementation is deprecated as of v0.14.0.
|
|
5
|
-
*
|
|
6
|
-
* Use the embedded Swarm Mail implementation instead:
|
|
7
|
-
* - swarmmail_* tools in src/streams/swarm-mail.ts
|
|
8
|
-
* - No external MCP server required
|
|
9
|
-
* - Embedded PGLite with event sourcing
|
|
10
|
-
* - Better error messages and recovery
|
|
11
|
-
*
|
|
12
|
-
* This file remains for backward compatibility and will be removed in v1.0.0.
|
|
13
|
-
* See README.md "Migrating from MCP Agent Mail" section for migration guide.
|
|
14
|
-
*
|
|
15
|
-
* ---
|
|
16
|
-
*
|
|
17
|
-
* This module provides type-safe wrappers around the Agent Mail MCP server.
|
|
18
|
-
* It enforces context-preservation defaults to prevent session exhaustion.
|
|
19
|
-
*
|
|
20
|
-
* CRITICAL CONSTRAINTS:
|
|
21
|
-
* - fetch_inbox ALWAYS uses include_bodies: false
|
|
22
|
-
* - fetch_inbox ALWAYS limits to 5 messages max
|
|
23
|
-
* - Use summarize_thread instead of fetching all messages
|
|
24
|
-
* - Auto-release reservations when tasks complete
|
|
25
|
-
*
|
|
26
|
-
* GRACEFUL DEGRADATION:
|
|
27
|
-
* - If Agent Mail server is not running, tools return helpful error messages
|
|
28
|
-
* - Swarm can still function without Agent Mail (just no coordination)
|
|
29
|
-
*/
|
|
30
|
-
import { tool } from "@opencode-ai/plugin";
|
|
31
|
-
import { z } from "zod";
|
|
32
|
-
import { isToolAvailable, warnMissingTool } from "./tool-availability";
|
|
33
|
-
import { getRateLimiter, type RateLimiter } from "./rate-limiter";
|
|
34
|
-
import type { MailSessionState } from "swarm-mail";
|
|
35
|
-
|
|
36
|
-
// ============================================================================
|
|
37
|
-
// Configuration
|
|
38
|
-
// ============================================================================
|
|
39
|
-
|
|
40
|
-
const AGENT_MAIL_URL = "http://127.0.0.1:8765";
|
|
41
|
-
const DEFAULT_TTL_SECONDS = 3600; // 1 hour
|
|
42
|
-
const MAX_INBOX_LIMIT = 5; // HARD CAP - never exceed this
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Default project directory for Agent Mail operations
|
|
46
|
-
*
|
|
47
|
-
* This is set by the plugin init to the actual working directory (from OpenCode).
|
|
48
|
-
* Without this, tools might use the plugin's directory instead of the project's.
|
|
49
|
-
*
|
|
50
|
-
* Set this via setAgentMailProjectDirectory() before using tools.
|
|
51
|
-
*/
|
|
52
|
-
let agentMailProjectDirectory: string | null = null;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Set the default project directory for Agent Mail operations
|
|
56
|
-
*
|
|
57
|
-
* Called during plugin initialization with the actual project directory.
|
|
58
|
-
* This ensures agentmail_init uses the correct project path by default.
|
|
59
|
-
*/
|
|
60
|
-
export function setAgentMailProjectDirectory(directory: string): void {
|
|
61
|
-
agentMailProjectDirectory = directory;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Get the default project directory
|
|
66
|
-
*
|
|
67
|
-
* Returns the configured directory, or falls back to cwd if not set.
|
|
68
|
-
*/
|
|
69
|
-
export function getAgentMailProjectDirectory(): string {
|
|
70
|
-
return agentMailProjectDirectory || process.cwd();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Retry configuration
|
|
74
|
-
const RETRY_CONFIG = {
|
|
75
|
-
maxRetries: parseInt(process.env.OPENCODE_AGENT_MAIL_MAX_RETRIES || "3"),
|
|
76
|
-
baseDelayMs: parseInt(process.env.OPENCODE_AGENT_MAIL_BASE_DELAY_MS || "100"),
|
|
77
|
-
maxDelayMs: parseInt(process.env.OPENCODE_AGENT_MAIL_MAX_DELAY_MS || "5000"),
|
|
78
|
-
timeoutMs: parseInt(process.env.OPENCODE_AGENT_MAIL_TIMEOUT_MS || "10000"),
|
|
79
|
-
jitterPercent: 20,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Server recovery configuration
|
|
83
|
-
const RECOVERY_CONFIG = {
|
|
84
|
-
/** Max consecutive failures before attempting restart (1 = restart on first "unexpected error") */
|
|
85
|
-
failureThreshold: 1,
|
|
86
|
-
/** Cooldown between restart attempts (ms) - 10 seconds */
|
|
87
|
-
restartCooldownMs: 10000,
|
|
88
|
-
/** Whether auto-restart is enabled */
|
|
89
|
-
enabled: process.env.OPENCODE_AGENT_MAIL_AUTO_RESTART !== "false",
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// ============================================================================
|
|
93
|
-
// Types
|
|
94
|
-
// ============================================================================
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Agent Mail session state
|
|
98
|
-
* @deprecated Use MailSessionState from streams/events.ts instead
|
|
99
|
-
* This is kept for backward compatibility and re-exported as an alias
|
|
100
|
-
*/
|
|
101
|
-
export type AgentMailState = MailSessionState;
|
|
102
|
-
|
|
103
|
-
// ============================================================================
|
|
104
|
-
// Module-level state (keyed by sessionID)
|
|
105
|
-
// ============================================================================
|
|
106
|
-
|
|
107
|
-
import {
|
|
108
|
-
existsSync,
|
|
109
|
-
mkdirSync,
|
|
110
|
-
readFileSync,
|
|
111
|
-
writeFileSync,
|
|
112
|
-
unlinkSync,
|
|
113
|
-
} from "fs";
|
|
114
|
-
import { join } from "path";
|
|
115
|
-
import { tmpdir } from "os";
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Directory for persisting session state across CLI invocations
|
|
119
|
-
* This allows `swarm tool` commands to share state
|
|
120
|
-
*/
|
|
121
|
-
const SESSION_STATE_DIR =
|
|
122
|
-
process.env.SWARM_STATE_DIR || join(tmpdir(), "swarm-sessions");
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Get the file path for a session's state
|
|
126
|
-
*/
|
|
127
|
-
function getSessionStatePath(sessionID: string): string {
|
|
128
|
-
// Sanitize sessionID to be filesystem-safe
|
|
129
|
-
const safeID = sessionID.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
130
|
-
return join(SESSION_STATE_DIR, `${safeID}.json`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Load session state from disk
|
|
135
|
-
*/
|
|
136
|
-
function loadSessionState(sessionID: string): AgentMailState | null {
|
|
137
|
-
const path = getSessionStatePath(sessionID);
|
|
138
|
-
try {
|
|
139
|
-
if (existsSync(path)) {
|
|
140
|
-
const data = readFileSync(path, "utf-8");
|
|
141
|
-
return JSON.parse(data) as AgentMailState;
|
|
142
|
-
}
|
|
143
|
-
} catch (error) {
|
|
144
|
-
// File might be corrupted or inaccessible - ignore and return null
|
|
145
|
-
console.warn(`[agent-mail] Could not load session state: ${error}`);
|
|
146
|
-
}
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Save session state to disk
|
|
152
|
-
*
|
|
153
|
-
* @returns true if save succeeded, false if failed
|
|
154
|
-
*/
|
|
155
|
-
function saveSessionState(sessionID: string, state: AgentMailState): boolean {
|
|
156
|
-
try {
|
|
157
|
-
// Ensure directory exists
|
|
158
|
-
if (!existsSync(SESSION_STATE_DIR)) {
|
|
159
|
-
mkdirSync(SESSION_STATE_DIR, { recursive: true });
|
|
160
|
-
}
|
|
161
|
-
const path = getSessionStatePath(sessionID);
|
|
162
|
-
writeFileSync(path, JSON.stringify(state, null, 2));
|
|
163
|
-
return true;
|
|
164
|
-
} catch (error) {
|
|
165
|
-
// Non-fatal - state just won't persist
|
|
166
|
-
console.error(
|
|
167
|
-
`[agent-mail] CRITICAL: Could not save session state: ${error}`,
|
|
168
|
-
);
|
|
169
|
-
console.error(
|
|
170
|
-
`[agent-mail] Session state will not persist across CLI invocations!`,
|
|
171
|
-
);
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Delete session state from disk
|
|
178
|
-
*/
|
|
179
|
-
function deleteSessionState(sessionID: string): void {
|
|
180
|
-
const path = getSessionStatePath(sessionID);
|
|
181
|
-
try {
|
|
182
|
-
if (existsSync(path)) {
|
|
183
|
-
unlinkSync(path);
|
|
184
|
-
}
|
|
185
|
-
} catch {
|
|
186
|
-
// Ignore errors on cleanup
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* State storage keyed by sessionID.
|
|
192
|
-
* In-memory cache that also persists to disk for CLI usage.
|
|
193
|
-
*/
|
|
194
|
-
const sessionStates = new Map<string, AgentMailState>();
|
|
195
|
-
|
|
196
|
-
/** MCP JSON-RPC response */
|
|
197
|
-
interface MCPResponse<T = unknown> {
|
|
198
|
-
jsonrpc: "2.0";
|
|
199
|
-
id: string;
|
|
200
|
-
result?: T;
|
|
201
|
-
error?: {
|
|
202
|
-
code: number;
|
|
203
|
-
message: string;
|
|
204
|
-
data?: unknown;
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Agent registration result */
|
|
209
|
-
interface AgentInfo {
|
|
210
|
-
id: number;
|
|
211
|
-
name: string;
|
|
212
|
-
program: string;
|
|
213
|
-
model: string;
|
|
214
|
-
task_description: string;
|
|
215
|
-
inception_ts: string;
|
|
216
|
-
last_active_ts: string;
|
|
217
|
-
project_id: number;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Project info */
|
|
221
|
-
interface ProjectInfo {
|
|
222
|
-
id: number;
|
|
223
|
-
slug: string;
|
|
224
|
-
human_key: string;
|
|
225
|
-
created_at: string;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Message header (no body) */
|
|
229
|
-
interface MessageHeader {
|
|
230
|
-
id: number;
|
|
231
|
-
subject: string;
|
|
232
|
-
from: string;
|
|
233
|
-
created_ts: string;
|
|
234
|
-
importance: string;
|
|
235
|
-
ack_required: boolean;
|
|
236
|
-
thread_id?: string;
|
|
237
|
-
kind?: string;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** File reservation result */
|
|
241
|
-
interface ReservationResult {
|
|
242
|
-
granted: Array<{
|
|
243
|
-
id: number;
|
|
244
|
-
path_pattern: string;
|
|
245
|
-
exclusive: boolean;
|
|
246
|
-
reason: string;
|
|
247
|
-
expires_ts: string;
|
|
248
|
-
}>;
|
|
249
|
-
conflicts: Array<{
|
|
250
|
-
path: string;
|
|
251
|
-
holders: string[];
|
|
252
|
-
}>;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Thread summary */
|
|
256
|
-
interface ThreadSummary {
|
|
257
|
-
thread_id: string;
|
|
258
|
-
summary: {
|
|
259
|
-
participants: string[];
|
|
260
|
-
key_points: string[];
|
|
261
|
-
action_items: string[];
|
|
262
|
-
total_messages: number;
|
|
263
|
-
};
|
|
264
|
-
examples?: Array<{
|
|
265
|
-
id: number;
|
|
266
|
-
subject: string;
|
|
267
|
-
from: string;
|
|
268
|
-
body_md?: string;
|
|
269
|
-
}>;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// ============================================================================
|
|
273
|
-
// Errors
|
|
274
|
-
// ============================================================================
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* AgentMailError - Custom error for Agent Mail operations
|
|
278
|
-
*
|
|
279
|
-
* Note: Using a factory pattern to avoid "Cannot call a class constructor without |new|"
|
|
280
|
-
* errors in some bundled environments (OpenCode's plugin runtime).
|
|
281
|
-
*/
|
|
282
|
-
export class AgentMailError extends Error {
|
|
283
|
-
public readonly tool: string;
|
|
284
|
-
public readonly code?: number;
|
|
285
|
-
public readonly data?: unknown;
|
|
286
|
-
|
|
287
|
-
constructor(message: string, tool: string, code?: number, data?: unknown) {
|
|
288
|
-
super(message);
|
|
289
|
-
this.tool = tool;
|
|
290
|
-
this.code = code;
|
|
291
|
-
this.data = data;
|
|
292
|
-
this.name = "AgentMailError";
|
|
293
|
-
// Fix prototype chain for instanceof checks
|
|
294
|
-
Object.setPrototypeOf(this, AgentMailError.prototype);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Factory function to create AgentMailError
|
|
300
|
-
* Use this instead of `new AgentMailError()` for compatibility
|
|
301
|
-
*/
|
|
302
|
-
export function createAgentMailError(
|
|
303
|
-
message: string,
|
|
304
|
-
tool: string,
|
|
305
|
-
code?: number,
|
|
306
|
-
data?: unknown,
|
|
307
|
-
): AgentMailError {
|
|
308
|
-
return new AgentMailError(message, tool, code, data);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export class AgentMailNotInitializedError extends Error {
|
|
312
|
-
constructor() {
|
|
313
|
-
super("Agent Mail not initialized. Call agent-mail:init first.");
|
|
314
|
-
this.name = "AgentMailNotInitializedError";
|
|
315
|
-
Object.setPrototypeOf(this, AgentMailNotInitializedError.prototype);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export class FileReservationConflictError extends Error {
|
|
320
|
-
constructor(
|
|
321
|
-
message: string,
|
|
322
|
-
public readonly conflicts: Array<{ path: string; holders: string[] }>,
|
|
323
|
-
) {
|
|
324
|
-
super(message);
|
|
325
|
-
this.name = "FileReservationConflictError";
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export class RateLimitExceededError extends Error {
|
|
330
|
-
constructor(
|
|
331
|
-
public readonly endpoint: string,
|
|
332
|
-
public readonly remaining: number,
|
|
333
|
-
public readonly resetAt: number,
|
|
334
|
-
) {
|
|
335
|
-
const resetDate = new Date(resetAt);
|
|
336
|
-
const waitMs = Math.max(0, resetAt - Date.now());
|
|
337
|
-
const waitSec = Math.ceil(waitMs / 1000);
|
|
338
|
-
super(
|
|
339
|
-
`Rate limit exceeded for ${endpoint}. ` +
|
|
340
|
-
`${remaining} remaining. ` +
|
|
341
|
-
`Retry in ${waitSec}s (at ${resetDate.toISOString()})`,
|
|
342
|
-
);
|
|
343
|
-
this.name = "RateLimitExceededError";
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ============================================================================
|
|
348
|
-
// Server Recovery
|
|
349
|
-
// ============================================================================
|
|
350
|
-
|
|
351
|
-
/** Track consecutive failures for recovery decisions */
|
|
352
|
-
let consecutiveFailures = 0;
|
|
353
|
-
let lastRestartAttempt = 0;
|
|
354
|
-
let isRestarting = false;
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Check if the server is responding to health checks
|
|
358
|
-
*/
|
|
359
|
-
async function isServerHealthy(): Promise<boolean> {
|
|
360
|
-
try {
|
|
361
|
-
const controller = new AbortController();
|
|
362
|
-
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
363
|
-
|
|
364
|
-
const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`, {
|
|
365
|
-
signal: controller.signal,
|
|
366
|
-
});
|
|
367
|
-
clearTimeout(timeout);
|
|
368
|
-
|
|
369
|
-
return response.ok;
|
|
370
|
-
} catch {
|
|
371
|
-
return false;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Test if the server can handle a basic MCP call
|
|
377
|
-
* This catches cases where health is OK but MCP is broken
|
|
378
|
-
*/
|
|
379
|
-
async function isServerFunctional(): Promise<boolean> {
|
|
380
|
-
try {
|
|
381
|
-
const controller = new AbortController();
|
|
382
|
-
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
383
|
-
|
|
384
|
-
const response = await fetch(`${AGENT_MAIL_URL}/mcp/`, {
|
|
385
|
-
method: "POST",
|
|
386
|
-
headers: { "Content-Type": "application/json" },
|
|
387
|
-
body: JSON.stringify({
|
|
388
|
-
jsonrpc: "2.0",
|
|
389
|
-
id: "health-test",
|
|
390
|
-
method: "tools/call",
|
|
391
|
-
params: { name: "health_check", arguments: {} },
|
|
392
|
-
}),
|
|
393
|
-
signal: controller.signal,
|
|
394
|
-
});
|
|
395
|
-
clearTimeout(timeout);
|
|
396
|
-
|
|
397
|
-
if (!response.ok) return false;
|
|
398
|
-
|
|
399
|
-
const json = (await response.json()) as { result?: { isError?: boolean } };
|
|
400
|
-
// Check if it's an error response
|
|
401
|
-
if (json.result?.isError) return false;
|
|
402
|
-
|
|
403
|
-
return true;
|
|
404
|
-
} catch {
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Attempt to restart the Agent Mail server
|
|
411
|
-
*
|
|
412
|
-
* Finds the running process, kills it, and starts a new one.
|
|
413
|
-
* Returns true if restart was successful.
|
|
414
|
-
*/
|
|
415
|
-
async function restartServer(): Promise<boolean> {
|
|
416
|
-
if (!RECOVERY_CONFIG.enabled) {
|
|
417
|
-
console.warn(
|
|
418
|
-
"[agent-mail] Auto-restart disabled via OPENCODE_AGENT_MAIL_AUTO_RESTART=false",
|
|
419
|
-
);
|
|
420
|
-
return false;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Prevent concurrent restart attempts
|
|
424
|
-
if (isRestarting) {
|
|
425
|
-
console.warn("[agent-mail] Restart already in progress");
|
|
426
|
-
return false;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Respect cooldown
|
|
430
|
-
const now = Date.now();
|
|
431
|
-
if (now - lastRestartAttempt < RECOVERY_CONFIG.restartCooldownMs) {
|
|
432
|
-
const waitSec = Math.ceil(
|
|
433
|
-
(RECOVERY_CONFIG.restartCooldownMs - (now - lastRestartAttempt)) / 1000,
|
|
434
|
-
);
|
|
435
|
-
console.warn(`[agent-mail] Restart cooldown active, wait ${waitSec}s`);
|
|
436
|
-
return false;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
isRestarting = true;
|
|
440
|
-
lastRestartAttempt = now;
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
console.warn("[agent-mail] Attempting server restart...");
|
|
444
|
-
|
|
445
|
-
// Find the agent-mail process
|
|
446
|
-
const findProc = Bun.spawn(["lsof", "-i", ":8765", "-t"], {
|
|
447
|
-
stdout: "pipe",
|
|
448
|
-
stderr: "pipe",
|
|
449
|
-
});
|
|
450
|
-
const findOutput = await new Response(findProc.stdout).text();
|
|
451
|
-
await findProc.exited;
|
|
452
|
-
|
|
453
|
-
const pids = findOutput.trim().split("\n").filter(Boolean);
|
|
454
|
-
|
|
455
|
-
if (pids.length > 0) {
|
|
456
|
-
// Kill existing process(es)
|
|
457
|
-
for (const pid of pids) {
|
|
458
|
-
console.warn(`[agent-mail] Killing process ${pid}`);
|
|
459
|
-
Bun.spawn(["kill", pid]);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Wait for process to die
|
|
463
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Find the agent-mail installation directory
|
|
467
|
-
// Try common locations
|
|
468
|
-
const possiblePaths = [
|
|
469
|
-
`${process.env.HOME}/Code/Dicklesworthstone/mcp_agent_mail`,
|
|
470
|
-
`${process.env.HOME}/.local/share/agent-mail`,
|
|
471
|
-
`${process.env.HOME}/mcp_agent_mail`,
|
|
472
|
-
];
|
|
473
|
-
|
|
474
|
-
let serverDir: string | null = null;
|
|
475
|
-
for (const path of possiblePaths) {
|
|
476
|
-
try {
|
|
477
|
-
const stat = await Bun.file(`${path}/pyproject.toml`).exists();
|
|
478
|
-
if (stat) {
|
|
479
|
-
serverDir = path;
|
|
480
|
-
break;
|
|
481
|
-
}
|
|
482
|
-
} catch {
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (!serverDir) {
|
|
488
|
-
console.error(
|
|
489
|
-
"[agent-mail] Could not find agent-mail installation directory",
|
|
490
|
-
);
|
|
491
|
-
return false;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Start the server
|
|
495
|
-
console.warn(`[agent-mail] Starting server from ${serverDir}`);
|
|
496
|
-
Bun.spawn(["python", "-m", "mcp_agent_mail.cli", "serve-http"], {
|
|
497
|
-
cwd: serverDir,
|
|
498
|
-
stdout: "ignore",
|
|
499
|
-
stderr: "ignore",
|
|
500
|
-
// Detach so it survives our process
|
|
501
|
-
detached: true,
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
// Wait for server to come up
|
|
505
|
-
for (let i = 0; i < 10; i++) {
|
|
506
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
507
|
-
if (await isServerHealthy()) {
|
|
508
|
-
console.warn("[agent-mail] Server restarted successfully");
|
|
509
|
-
consecutiveFailures = 0;
|
|
510
|
-
return true;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
console.error("[agent-mail] Server failed to start after restart");
|
|
515
|
-
return false;
|
|
516
|
-
} catch (error) {
|
|
517
|
-
console.error("[agent-mail] Restart failed:", error);
|
|
518
|
-
return false;
|
|
519
|
-
} finally {
|
|
520
|
-
isRestarting = false;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Reset recovery state (for testing)
|
|
526
|
-
*/
|
|
527
|
-
export function resetRecoveryState(): void {
|
|
528
|
-
consecutiveFailures = 0;
|
|
529
|
-
lastRestartAttempt = 0;
|
|
530
|
-
isRestarting = false;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// ============================================================================
|
|
534
|
-
// Retry Logic
|
|
535
|
-
// ============================================================================
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Calculate delay with exponential backoff + jitter
|
|
539
|
-
*/
|
|
540
|
-
function calculateBackoffDelay(attempt: number): number {
|
|
541
|
-
if (attempt === 0) return 0;
|
|
542
|
-
|
|
543
|
-
const exponentialDelay = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt - 1);
|
|
544
|
-
const cappedDelay = Math.min(exponentialDelay, RETRY_CONFIG.maxDelayMs);
|
|
545
|
-
|
|
546
|
-
// Add jitter (±jitterPercent%)
|
|
547
|
-
const jitterRange = cappedDelay * (RETRY_CONFIG.jitterPercent / 100);
|
|
548
|
-
const jitter = (Math.random() * 2 - 1) * jitterRange;
|
|
549
|
-
|
|
550
|
-
return Math.round(cappedDelay + jitter);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Check if an error is retryable (transient network/server issues)
|
|
555
|
-
*/
|
|
556
|
-
function isRetryableError(error: unknown): boolean {
|
|
557
|
-
if (error instanceof Error) {
|
|
558
|
-
const message = error.message.toLowerCase();
|
|
559
|
-
|
|
560
|
-
// Network errors
|
|
561
|
-
if (
|
|
562
|
-
message.includes("econnrefused") ||
|
|
563
|
-
message.includes("econnreset") ||
|
|
564
|
-
message.includes("socket") ||
|
|
565
|
-
message.includes("network") ||
|
|
566
|
-
message.includes("timeout") ||
|
|
567
|
-
message.includes("aborted")
|
|
568
|
-
) {
|
|
569
|
-
return true;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Server errors (but not 500 which is usually a logic bug)
|
|
573
|
-
if (error instanceof AgentMailError && error.code) {
|
|
574
|
-
return error.code === 502 || error.code === 503 || error.code === 504;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Generic "unexpected error" from server - might be recoverable with restart
|
|
578
|
-
if (message.includes("unexpected error")) {
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
return false;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Check if an error indicates the project was not found
|
|
588
|
-
*
|
|
589
|
-
* This happens when Agent Mail server restarts and loses project registrations.
|
|
590
|
-
* The fix is to re-register the project and retry the operation.
|
|
591
|
-
*/
|
|
592
|
-
export function isProjectNotFoundError(error: unknown): boolean {
|
|
593
|
-
if (error instanceof Error) {
|
|
594
|
-
const message = error.message.toLowerCase();
|
|
595
|
-
return (
|
|
596
|
-
message.includes("project") &&
|
|
597
|
-
(message.includes("not found") || message.includes("does not exist"))
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* Check if an error indicates the agent was not found
|
|
605
|
-
*
|
|
606
|
-
* Similar to project not found - server restart loses agent registrations.
|
|
607
|
-
*/
|
|
608
|
-
export function isAgentNotFoundError(error: unknown): boolean {
|
|
609
|
-
if (error instanceof Error) {
|
|
610
|
-
const message = error.message.toLowerCase();
|
|
611
|
-
return (
|
|
612
|
-
message.includes("agent") &&
|
|
613
|
-
(message.includes("not found") || message.includes("does not exist"))
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// ============================================================================
|
|
620
|
-
// MCP Client
|
|
621
|
-
// ============================================================================
|
|
622
|
-
|
|
623
|
-
/** MCP tool result with content wrapper (real Agent Mail format) */
|
|
624
|
-
interface MCPToolResult<T = unknown> {
|
|
625
|
-
content?: Array<{ type: string; text: string }>;
|
|
626
|
-
structuredContent?: T;
|
|
627
|
-
isError?: boolean;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/** Cached availability check result */
|
|
631
|
-
let agentMailAvailable: boolean | null = null;
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Check if Agent Mail server is available (cached)
|
|
635
|
-
*/
|
|
636
|
-
async function checkAgentMailAvailable(): Promise<boolean> {
|
|
637
|
-
if (agentMailAvailable !== null) {
|
|
638
|
-
return agentMailAvailable;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
agentMailAvailable = await isToolAvailable("agent-mail");
|
|
642
|
-
return agentMailAvailable;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Reset availability cache (for testing)
|
|
647
|
-
*/
|
|
648
|
-
export function resetAgentMailCache(): void {
|
|
649
|
-
agentMailAvailable = null;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/** Cached rate limiter instance */
|
|
653
|
-
let rateLimiter: RateLimiter | null = null;
|
|
654
|
-
|
|
655
|
-
/** Whether rate limiting is enabled (can be disabled via env var) */
|
|
656
|
-
const RATE_LIMITING_ENABLED =
|
|
657
|
-
process.env.OPENCODE_RATE_LIMIT_DISABLED !== "true";
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* Check rate limit for an endpoint and throw if exceeded
|
|
661
|
-
*
|
|
662
|
-
* @param agentName - The agent making the request
|
|
663
|
-
* @param endpoint - The endpoint being accessed (e.g., "send", "inbox")
|
|
664
|
-
* @throws RateLimitExceededError if rate limit is exceeded
|
|
665
|
-
*/
|
|
666
|
-
async function checkRateLimit(
|
|
667
|
-
agentName: string,
|
|
668
|
-
endpoint: string,
|
|
669
|
-
): Promise<void> {
|
|
670
|
-
if (!RATE_LIMITING_ENABLED) {
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (!rateLimiter) {
|
|
675
|
-
rateLimiter = await getRateLimiter();
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const result = await rateLimiter.checkLimit(agentName, endpoint);
|
|
679
|
-
if (!result.allowed) {
|
|
680
|
-
throw new RateLimitExceededError(
|
|
681
|
-
endpoint,
|
|
682
|
-
result.remaining,
|
|
683
|
-
result.resetAt,
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Record a request against the rate limit (call after successful request)
|
|
690
|
-
*
|
|
691
|
-
* @param agentName - The agent making the request
|
|
692
|
-
* @param endpoint - The endpoint being accessed
|
|
693
|
-
*/
|
|
694
|
-
async function recordRateLimitedRequest(
|
|
695
|
-
agentName: string,
|
|
696
|
-
endpoint: string,
|
|
697
|
-
): Promise<void> {
|
|
698
|
-
if (!RATE_LIMITING_ENABLED) {
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (!rateLimiter) {
|
|
703
|
-
rateLimiter = await getRateLimiter();
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
await rateLimiter.recordRequest(agentName, endpoint);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* Reset rate limiter (for testing)
|
|
711
|
-
*/
|
|
712
|
-
export async function resetRateLimiterCache(): Promise<void> {
|
|
713
|
-
if (rateLimiter) {
|
|
714
|
-
await rateLimiter.close();
|
|
715
|
-
rateLimiter = null;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Execute a single MCP call (no retry)
|
|
721
|
-
*/
|
|
722
|
-
async function mcpCallOnce<T>(
|
|
723
|
-
toolName: string,
|
|
724
|
-
args: Record<string, unknown>,
|
|
725
|
-
): Promise<T> {
|
|
726
|
-
const controller = new AbortController();
|
|
727
|
-
const timeout = setTimeout(() => controller.abort(), RETRY_CONFIG.timeoutMs);
|
|
728
|
-
|
|
729
|
-
try {
|
|
730
|
-
const response = await fetch(`${AGENT_MAIL_URL}/mcp/`, {
|
|
731
|
-
method: "POST",
|
|
732
|
-
headers: { "Content-Type": "application/json" },
|
|
733
|
-
body: JSON.stringify({
|
|
734
|
-
jsonrpc: "2.0",
|
|
735
|
-
id: crypto.randomUUID(),
|
|
736
|
-
method: "tools/call",
|
|
737
|
-
params: { name: toolName, arguments: args },
|
|
738
|
-
}),
|
|
739
|
-
signal: controller.signal,
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
clearTimeout(timeout);
|
|
743
|
-
|
|
744
|
-
if (!response.ok) {
|
|
745
|
-
throw new AgentMailError(
|
|
746
|
-
`HTTP ${response.status}: ${response.statusText}`,
|
|
747
|
-
toolName,
|
|
748
|
-
response.status,
|
|
749
|
-
);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const json = (await response.json()) as MCPResponse<MCPToolResult<T> | T>;
|
|
753
|
-
|
|
754
|
-
if (json.error) {
|
|
755
|
-
throw new AgentMailError(
|
|
756
|
-
json.error.message,
|
|
757
|
-
toolName,
|
|
758
|
-
json.error.code,
|
|
759
|
-
json.error.data,
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
const result = json.result;
|
|
764
|
-
|
|
765
|
-
// Handle wrapped response format (real Agent Mail server)
|
|
766
|
-
// Check for isError first (error responses don't have structuredContent)
|
|
767
|
-
if (result && typeof result === "object") {
|
|
768
|
-
const wrapped = result as MCPToolResult<T>;
|
|
769
|
-
|
|
770
|
-
// Check for error response (has isError: true but no structuredContent)
|
|
771
|
-
if (wrapped.isError) {
|
|
772
|
-
const errorText = wrapped.content?.[0]?.text || "Unknown error";
|
|
773
|
-
throw new AgentMailError(errorText, toolName);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Check for success response with structuredContent
|
|
777
|
-
if ("structuredContent" in wrapped) {
|
|
778
|
-
return wrapped.structuredContent as T;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// Handle direct response format (mock server)
|
|
783
|
-
return result as T;
|
|
784
|
-
} catch (error) {
|
|
785
|
-
clearTimeout(timeout);
|
|
786
|
-
throw error;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* Call an Agent Mail MCP tool with retry and auto-restart
|
|
792
|
-
*
|
|
793
|
-
* Features:
|
|
794
|
-
* - Exponential backoff with jitter on retryable errors
|
|
795
|
-
* - Auto-restart server after consecutive failures
|
|
796
|
-
* - Timeout handling per request
|
|
797
|
-
*
|
|
798
|
-
* Handles both direct results (mock server) and wrapped results (real server).
|
|
799
|
-
* Real Agent Mail returns: { content: [...], structuredContent: {...} }
|
|
800
|
-
*/
|
|
801
|
-
export async function mcpCall<T>(
|
|
802
|
-
toolName: string,
|
|
803
|
-
args: Record<string, unknown>,
|
|
804
|
-
): Promise<T> {
|
|
805
|
-
let lastError: Error | null = null;
|
|
806
|
-
let restartAttempted = false;
|
|
807
|
-
|
|
808
|
-
for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
|
|
809
|
-
// Apply backoff delay (except first attempt)
|
|
810
|
-
if (attempt > 0) {
|
|
811
|
-
const delay = calculateBackoffDelay(attempt);
|
|
812
|
-
console.warn(
|
|
813
|
-
`[agent-mail] Retry ${attempt}/${RETRY_CONFIG.maxRetries} for ${toolName} after ${delay}ms`,
|
|
814
|
-
);
|
|
815
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
try {
|
|
819
|
-
const result = await mcpCallOnce<T>(toolName, args);
|
|
820
|
-
|
|
821
|
-
// Success - reset failure counter
|
|
822
|
-
consecutiveFailures = 0;
|
|
823
|
-
return result;
|
|
824
|
-
} catch (error) {
|
|
825
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
826
|
-
const errorMessage = lastError.message.toLowerCase();
|
|
827
|
-
|
|
828
|
-
// Track consecutive failures
|
|
829
|
-
consecutiveFailures++;
|
|
830
|
-
|
|
831
|
-
// Check if error is retryable
|
|
832
|
-
const retryable = isRetryableError(error);
|
|
833
|
-
|
|
834
|
-
// AGGRESSIVE: If it's an "unexpected error", restart immediately (once per call)
|
|
835
|
-
const isUnexpectedError = errorMessage.includes("unexpected error");
|
|
836
|
-
if (isUnexpectedError && !restartAttempted && RECOVERY_CONFIG.enabled) {
|
|
837
|
-
console.warn(
|
|
838
|
-
`[agent-mail] "${toolName}" got unexpected error, restarting server immediately...`,
|
|
839
|
-
);
|
|
840
|
-
restartAttempted = true;
|
|
841
|
-
const restarted = await restartServer();
|
|
842
|
-
if (restarted) {
|
|
843
|
-
agentMailAvailable = null;
|
|
844
|
-
consecutiveFailures = 0;
|
|
845
|
-
// Small delay to let server stabilize
|
|
846
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
847
|
-
// Don't count this attempt - retry immediately
|
|
848
|
-
attempt--;
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Standard retry logic for other retryable errors
|
|
854
|
-
if (
|
|
855
|
-
!isUnexpectedError &&
|
|
856
|
-
consecutiveFailures >= RECOVERY_CONFIG.failureThreshold &&
|
|
857
|
-
RECOVERY_CONFIG.enabled &&
|
|
858
|
-
!restartAttempted
|
|
859
|
-
) {
|
|
860
|
-
console.warn(
|
|
861
|
-
`[agent-mail] ${consecutiveFailures} consecutive failures, checking server health...`,
|
|
862
|
-
);
|
|
863
|
-
|
|
864
|
-
const healthy = await isServerFunctional();
|
|
865
|
-
if (!healthy) {
|
|
866
|
-
console.warn("[agent-mail] Server unhealthy, attempting restart...");
|
|
867
|
-
restartAttempted = true;
|
|
868
|
-
const restarted = await restartServer();
|
|
869
|
-
if (restarted) {
|
|
870
|
-
agentMailAvailable = null;
|
|
871
|
-
if (retryable) {
|
|
872
|
-
attempt--;
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// If error is not retryable, throw immediately
|
|
880
|
-
if (!retryable) {
|
|
881
|
-
console.warn(
|
|
882
|
-
`[agent-mail] Non-retryable error for ${toolName}: ${lastError.message}`,
|
|
883
|
-
);
|
|
884
|
-
throw lastError;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// If this was the last retry, throw
|
|
888
|
-
if (attempt === RETRY_CONFIG.maxRetries) {
|
|
889
|
-
console.error(
|
|
890
|
-
`[agent-mail] All ${RETRY_CONFIG.maxRetries} retries exhausted for ${toolName}`,
|
|
891
|
-
);
|
|
892
|
-
throw lastError;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// Should never reach here, but TypeScript needs it
|
|
898
|
-
throw lastError || new Error("Unknown error in mcpCall");
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
/**
|
|
902
|
-
* Re-register a project with Agent Mail server
|
|
903
|
-
*
|
|
904
|
-
* Called when we detect "Project not found" error, indicating server restart.
|
|
905
|
-
* This is a lightweight operation that just ensures the project exists.
|
|
906
|
-
*/
|
|
907
|
-
async function reRegisterProject(projectKey: string): Promise<boolean> {
|
|
908
|
-
try {
|
|
909
|
-
console.warn(
|
|
910
|
-
`[agent-mail] Re-registering project "${projectKey}" after server restart...`,
|
|
911
|
-
);
|
|
912
|
-
await mcpCall<ProjectInfo>("ensure_project", {
|
|
913
|
-
human_key: projectKey,
|
|
914
|
-
});
|
|
915
|
-
console.warn(
|
|
916
|
-
`[agent-mail] Project "${projectKey}" re-registered successfully`,
|
|
917
|
-
);
|
|
918
|
-
return true;
|
|
919
|
-
} catch (error) {
|
|
920
|
-
console.error(
|
|
921
|
-
`[agent-mail] Failed to re-register project "${projectKey}":`,
|
|
922
|
-
error,
|
|
923
|
-
);
|
|
924
|
-
return false;
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
/**
|
|
929
|
-
* Re-register an agent with Agent Mail server
|
|
930
|
-
*
|
|
931
|
-
* Called when we detect "Agent not found" error, indicating server restart.
|
|
932
|
-
*/
|
|
933
|
-
async function reRegisterAgent(
|
|
934
|
-
projectKey: string,
|
|
935
|
-
agentName: string,
|
|
936
|
-
taskDescription?: string,
|
|
937
|
-
): Promise<boolean> {
|
|
938
|
-
try {
|
|
939
|
-
console.warn(
|
|
940
|
-
`[agent-mail] Re-registering agent "${agentName}" for project "${projectKey}"...`,
|
|
941
|
-
);
|
|
942
|
-
await mcpCall<AgentInfo>("register_agent", {
|
|
943
|
-
project_key: projectKey,
|
|
944
|
-
program: "opencode",
|
|
945
|
-
model: "claude-opus-4",
|
|
946
|
-
name: agentName,
|
|
947
|
-
task_description: taskDescription || "Re-registered after server restart",
|
|
948
|
-
});
|
|
949
|
-
console.warn(
|
|
950
|
-
`[agent-mail] Agent "${agentName}" re-registered successfully`,
|
|
951
|
-
);
|
|
952
|
-
return true;
|
|
953
|
-
} catch (error) {
|
|
954
|
-
console.error(
|
|
955
|
-
`[agent-mail] Failed to re-register agent "${agentName}":`,
|
|
956
|
-
error,
|
|
957
|
-
);
|
|
958
|
-
return false;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
/**
|
|
963
|
-
* MCP call with automatic project/agent re-registration on "not found" errors
|
|
964
|
-
*
|
|
965
|
-
* This is the self-healing wrapper that handles Agent Mail server restarts.
|
|
966
|
-
* When the server restarts, it loses all project and agent registrations.
|
|
967
|
-
* This wrapper detects those errors and automatically re-registers before retrying.
|
|
968
|
-
*
|
|
969
|
-
* Use this instead of raw mcpCall when you have project_key and agent_name context.
|
|
970
|
-
*
|
|
971
|
-
* @param toolName - The MCP tool to call
|
|
972
|
-
* @param args - Arguments including project_key and optionally agent_name
|
|
973
|
-
* @param options - Optional configuration for re-registration
|
|
974
|
-
* @returns The result of the MCP call
|
|
975
|
-
*/
|
|
976
|
-
export async function mcpCallWithAutoInit<T>(
|
|
977
|
-
toolName: string,
|
|
978
|
-
args: Record<string, unknown> & { project_key: string; agent_name?: string },
|
|
979
|
-
options?: {
|
|
980
|
-
/** Task description for agent re-registration */
|
|
981
|
-
taskDescription?: string;
|
|
982
|
-
/** Max re-registration attempts (default: 1) */
|
|
983
|
-
maxReregistrationAttempts?: number;
|
|
984
|
-
},
|
|
985
|
-
): Promise<T> {
|
|
986
|
-
const maxAttempts = options?.maxReregistrationAttempts ?? 1;
|
|
987
|
-
let reregistrationAttempts = 0;
|
|
988
|
-
|
|
989
|
-
while (true) {
|
|
990
|
-
try {
|
|
991
|
-
return await mcpCall<T>(toolName, args);
|
|
992
|
-
} catch (error) {
|
|
993
|
-
// Check if this is a recoverable "not found" error
|
|
994
|
-
const isProjectError = isProjectNotFoundError(error);
|
|
995
|
-
const isAgentError = isAgentNotFoundError(error);
|
|
996
|
-
|
|
997
|
-
if (!isProjectError && !isAgentError) {
|
|
998
|
-
// Not a recoverable error, rethrow
|
|
999
|
-
throw error;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// Check if we've exhausted re-registration attempts
|
|
1003
|
-
if (reregistrationAttempts >= maxAttempts) {
|
|
1004
|
-
console.error(
|
|
1005
|
-
`[agent-mail] Exhausted ${maxAttempts} re-registration attempt(s) for ${toolName}`,
|
|
1006
|
-
);
|
|
1007
|
-
throw error;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
reregistrationAttempts++;
|
|
1011
|
-
console.warn(
|
|
1012
|
-
`[agent-mail] Detected "${isProjectError ? "project" : "agent"} not found" for ${toolName}, ` +
|
|
1013
|
-
`attempting re-registration (attempt ${reregistrationAttempts}/${maxAttempts})...`,
|
|
1014
|
-
);
|
|
1015
|
-
|
|
1016
|
-
// Re-register project first (always needed)
|
|
1017
|
-
const projectOk = await reRegisterProject(args.project_key);
|
|
1018
|
-
if (!projectOk) {
|
|
1019
|
-
throw error; // Can't recover without project
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// Re-register agent if we have one and it was an agent error
|
|
1023
|
-
// (or if the original call needs an agent)
|
|
1024
|
-
if (args.agent_name && (isAgentError || toolName !== "ensure_project")) {
|
|
1025
|
-
const agentOk = await reRegisterAgent(
|
|
1026
|
-
args.project_key,
|
|
1027
|
-
args.agent_name,
|
|
1028
|
-
options?.taskDescription,
|
|
1029
|
-
);
|
|
1030
|
-
if (!agentOk) {
|
|
1031
|
-
// Agent re-registration failed, but project is OK
|
|
1032
|
-
// Some operations might still work, so continue
|
|
1033
|
-
console.warn(
|
|
1034
|
-
`[agent-mail] Agent re-registration failed, but continuing with retry...`,
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
// Retry the original call
|
|
1040
|
-
console.warn(
|
|
1041
|
-
`[agent-mail] Retrying ${toolName} after re-registration...`,
|
|
1042
|
-
);
|
|
1043
|
-
// Loop continues to retry
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
/**
|
|
1049
|
-
* Get Agent Mail state for a session, or throw if not initialized
|
|
1050
|
-
*
|
|
1051
|
-
* Checks in-memory cache first, then falls back to disk storage.
|
|
1052
|
-
* This allows CLI invocations to share state across calls.
|
|
1053
|
-
*/
|
|
1054
|
-
function requireState(sessionID: string): AgentMailState {
|
|
1055
|
-
// Check in-memory cache first
|
|
1056
|
-
let state = sessionStates.get(sessionID);
|
|
1057
|
-
|
|
1058
|
-
// If not in memory, try loading from disk
|
|
1059
|
-
if (!state) {
|
|
1060
|
-
state = loadSessionState(sessionID) ?? undefined;
|
|
1061
|
-
if (state) {
|
|
1062
|
-
// Cache in memory for subsequent calls in same process
|
|
1063
|
-
sessionStates.set(sessionID, state);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
if (!state) {
|
|
1068
|
-
throw new AgentMailNotInitializedError();
|
|
1069
|
-
}
|
|
1070
|
-
return state;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
/**
|
|
1074
|
-
* Store Agent Mail state for a session
|
|
1075
|
-
*
|
|
1076
|
-
* Saves to both in-memory cache and disk for CLI persistence.
|
|
1077
|
-
*/
|
|
1078
|
-
function setState(sessionID: string, state: AgentMailState): void {
|
|
1079
|
-
sessionStates.set(sessionID, state);
|
|
1080
|
-
saveSessionState(sessionID, state);
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/**
|
|
1084
|
-
* Get state if exists (for cleanup hooks)
|
|
1085
|
-
*
|
|
1086
|
-
* Checks in-memory cache first, then falls back to disk storage.
|
|
1087
|
-
*/
|
|
1088
|
-
function getState(sessionID: string): AgentMailState | undefined {
|
|
1089
|
-
let state = sessionStates.get(sessionID);
|
|
1090
|
-
if (!state) {
|
|
1091
|
-
state = loadSessionState(sessionID) ?? undefined;
|
|
1092
|
-
if (state) {
|
|
1093
|
-
sessionStates.set(sessionID, state);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
return state;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Clear state for a session
|
|
1101
|
-
*
|
|
1102
|
-
* Removes from both in-memory cache and disk.
|
|
1103
|
-
*/
|
|
1104
|
-
function clearState(sessionID: string): void {
|
|
1105
|
-
sessionStates.delete(sessionID);
|
|
1106
|
-
deleteSessionState(sessionID);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// ============================================================================
|
|
1110
|
-
// Tool Definitions
|
|
1111
|
-
// ============================================================================
|
|
1112
|
-
|
|
1113
|
-
/**
|
|
1114
|
-
* Initialize Agent Mail session
|
|
1115
|
-
*/
|
|
1116
|
-
export const agentmail_init = tool({
|
|
1117
|
-
description:
|
|
1118
|
-
"Initialize Agent Mail session (ensure project + register agent)",
|
|
1119
|
-
args: {
|
|
1120
|
-
project_path: tool.schema
|
|
1121
|
-
.string()
|
|
1122
|
-
.optional()
|
|
1123
|
-
.describe(
|
|
1124
|
-
"Absolute path to the project/repo (defaults to current working directory)",
|
|
1125
|
-
),
|
|
1126
|
-
agent_name: tool.schema
|
|
1127
|
-
.string()
|
|
1128
|
-
.optional()
|
|
1129
|
-
.describe("Agent name (omit for auto-generated adjective+noun)"),
|
|
1130
|
-
task_description: tool.schema
|
|
1131
|
-
.string()
|
|
1132
|
-
.optional()
|
|
1133
|
-
.describe("Description of current task"),
|
|
1134
|
-
},
|
|
1135
|
-
async execute(args, ctx) {
|
|
1136
|
-
// Use provided path or fall back to configured project directory
|
|
1137
|
-
// This prevents using the plugin's directory when working in a different project
|
|
1138
|
-
const projectPath = args.project_path || getAgentMailProjectDirectory();
|
|
1139
|
-
|
|
1140
|
-
// Check if Agent Mail is available
|
|
1141
|
-
const available = await checkAgentMailAvailable();
|
|
1142
|
-
if (!available) {
|
|
1143
|
-
warnMissingTool("agent-mail");
|
|
1144
|
-
return JSON.stringify(
|
|
1145
|
-
{
|
|
1146
|
-
error: "Agent Mail server not available",
|
|
1147
|
-
available: false,
|
|
1148
|
-
hint: "Start Agent Mail with: agent-mail serve",
|
|
1149
|
-
fallback:
|
|
1150
|
-
"Swarm will continue without multi-agent coordination. File conflicts possible if multiple agents active.",
|
|
1151
|
-
},
|
|
1152
|
-
null,
|
|
1153
|
-
2,
|
|
1154
|
-
);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// Retry loop with restart on failure
|
|
1158
|
-
const MAX_INIT_RETRIES = 3;
|
|
1159
|
-
let lastError: Error | null = null;
|
|
1160
|
-
|
|
1161
|
-
for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
|
|
1162
|
-
try {
|
|
1163
|
-
// 1. Ensure project exists
|
|
1164
|
-
const project = await mcpCall<ProjectInfo>("ensure_project", {
|
|
1165
|
-
human_key: projectPath,
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
// 2. Register agent
|
|
1169
|
-
const agent = await mcpCall<AgentInfo>("register_agent", {
|
|
1170
|
-
project_key: projectPath,
|
|
1171
|
-
program: "opencode",
|
|
1172
|
-
model: "claude-opus-4",
|
|
1173
|
-
name: args.agent_name, // undefined = auto-generate
|
|
1174
|
-
task_description: args.task_description || "",
|
|
1175
|
-
});
|
|
1176
|
-
|
|
1177
|
-
// 3. Store state using sessionID
|
|
1178
|
-
const state: AgentMailState = {
|
|
1179
|
-
projectKey: projectPath,
|
|
1180
|
-
agentName: agent.name,
|
|
1181
|
-
reservations: [],
|
|
1182
|
-
startedAt: new Date().toISOString(),
|
|
1183
|
-
};
|
|
1184
|
-
setState(ctx.sessionID, state);
|
|
1185
|
-
|
|
1186
|
-
// Success - if we retried, log it
|
|
1187
|
-
if (attempt > 1) {
|
|
1188
|
-
console.warn(
|
|
1189
|
-
`[agent-mail] Init succeeded on attempt ${attempt} after restart`,
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
return JSON.stringify({ project, agent, available: true }, null, 2);
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1196
|
-
const isUnexpectedError = lastError.message
|
|
1197
|
-
.toLowerCase()
|
|
1198
|
-
.includes("unexpected error");
|
|
1199
|
-
|
|
1200
|
-
console.warn(
|
|
1201
|
-
`[agent-mail] Init attempt ${attempt}/${MAX_INIT_RETRIES} failed: ${lastError.message}`,
|
|
1202
|
-
);
|
|
1203
|
-
|
|
1204
|
-
// If it's an "unexpected error" and we have retries left, restart and retry
|
|
1205
|
-
if (isUnexpectedError && attempt < MAX_INIT_RETRIES) {
|
|
1206
|
-
console.warn(
|
|
1207
|
-
"[agent-mail] Detected 'unexpected error', restarting server...",
|
|
1208
|
-
);
|
|
1209
|
-
const restarted = await restartServer();
|
|
1210
|
-
if (restarted) {
|
|
1211
|
-
// Clear cache and retry
|
|
1212
|
-
agentMailAvailable = null;
|
|
1213
|
-
consecutiveFailures = 0;
|
|
1214
|
-
// Small delay to let server stabilize
|
|
1215
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1216
|
-
continue;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// For non-unexpected errors or if restart failed, don't retry
|
|
1221
|
-
if (!isUnexpectedError) {
|
|
1222
|
-
break;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// All retries exhausted
|
|
1228
|
-
return JSON.stringify(
|
|
1229
|
-
{
|
|
1230
|
-
error: `Agent Mail init failed after ${MAX_INIT_RETRIES} attempts`,
|
|
1231
|
-
available: false,
|
|
1232
|
-
lastError: lastError?.message,
|
|
1233
|
-
hint: "Manually restart Agent Mail: pkill -f agent-mail && agent-mail serve",
|
|
1234
|
-
fallback: "Swarm will continue without multi-agent coordination.",
|
|
1235
|
-
},
|
|
1236
|
-
null,
|
|
1237
|
-
2,
|
|
1238
|
-
);
|
|
1239
|
-
},
|
|
1240
|
-
});
|
|
1241
|
-
|
|
1242
|
-
/**
|
|
1243
|
-
* Send a message to other agents
|
|
1244
|
-
*/
|
|
1245
|
-
export const agentmail_send = tool({
|
|
1246
|
-
description: "Send message to other agents",
|
|
1247
|
-
args: {
|
|
1248
|
-
to: tool.schema
|
|
1249
|
-
.array(tool.schema.string())
|
|
1250
|
-
.describe("Recipient agent names"),
|
|
1251
|
-
subject: tool.schema.string().describe("Message subject"),
|
|
1252
|
-
body: tool.schema.string().describe("Message body (Markdown)"),
|
|
1253
|
-
thread_id: tool.schema
|
|
1254
|
-
.string()
|
|
1255
|
-
.optional()
|
|
1256
|
-
.describe("Thread ID (use bead ID for linking)"),
|
|
1257
|
-
importance: tool.schema
|
|
1258
|
-
.enum(["low", "normal", "high", "urgent"])
|
|
1259
|
-
.optional()
|
|
1260
|
-
.describe("Message importance (default: normal)"),
|
|
1261
|
-
ack_required: tool.schema
|
|
1262
|
-
.boolean()
|
|
1263
|
-
.optional()
|
|
1264
|
-
.describe("Require acknowledgement (default: false)"),
|
|
1265
|
-
},
|
|
1266
|
-
async execute(args, ctx) {
|
|
1267
|
-
const state = requireState(ctx.sessionID);
|
|
1268
|
-
|
|
1269
|
-
// Check rate limit before sending
|
|
1270
|
-
await checkRateLimit(state.agentName, "send");
|
|
1271
|
-
|
|
1272
|
-
await mcpCall("send_message", {
|
|
1273
|
-
project_key: state.projectKey,
|
|
1274
|
-
sender_name: state.agentName,
|
|
1275
|
-
to: args.to,
|
|
1276
|
-
subject: args.subject,
|
|
1277
|
-
body_md: args.body,
|
|
1278
|
-
thread_id: args.thread_id,
|
|
1279
|
-
importance: args.importance || "normal",
|
|
1280
|
-
ack_required: args.ack_required || false,
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
// Record successful request
|
|
1284
|
-
await recordRateLimitedRequest(state.agentName, "send");
|
|
1285
|
-
|
|
1286
|
-
return `Message sent to ${args.to.join(", ")}`;
|
|
1287
|
-
},
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
/**
|
|
1291
|
-
* Fetch inbox (CONTEXT-SAFE: bodies excluded, limit 5)
|
|
1292
|
-
*/
|
|
1293
|
-
export const agentmail_inbox = tool({
|
|
1294
|
-
description: "Fetch inbox (CONTEXT-SAFE: bodies excluded, limit 5)",
|
|
1295
|
-
args: {
|
|
1296
|
-
limit: tool.schema
|
|
1297
|
-
.number()
|
|
1298
|
-
.max(MAX_INBOX_LIMIT)
|
|
1299
|
-
.optional()
|
|
1300
|
-
.describe(`Max messages (hard cap: ${MAX_INBOX_LIMIT})`),
|
|
1301
|
-
urgent_only: tool.schema
|
|
1302
|
-
.boolean()
|
|
1303
|
-
.optional()
|
|
1304
|
-
.describe("Only show urgent messages"),
|
|
1305
|
-
since_ts: tool.schema
|
|
1306
|
-
.string()
|
|
1307
|
-
.optional()
|
|
1308
|
-
.describe("Only messages after this ISO-8601 timestamp"),
|
|
1309
|
-
},
|
|
1310
|
-
async execute(args, ctx) {
|
|
1311
|
-
const state = requireState(ctx.sessionID);
|
|
1312
|
-
|
|
1313
|
-
// Check rate limit
|
|
1314
|
-
await checkRateLimit(state.agentName, "inbox");
|
|
1315
|
-
|
|
1316
|
-
// CRITICAL: Enforce context-safe defaults
|
|
1317
|
-
const limit = Math.min(args.limit || MAX_INBOX_LIMIT, MAX_INBOX_LIMIT);
|
|
1318
|
-
|
|
1319
|
-
const messages = await mcpCall<MessageHeader[]>("fetch_inbox", {
|
|
1320
|
-
project_key: state.projectKey,
|
|
1321
|
-
agent_name: state.agentName,
|
|
1322
|
-
limit,
|
|
1323
|
-
include_bodies: false, // MANDATORY - never include bodies
|
|
1324
|
-
urgent_only: args.urgent_only || false,
|
|
1325
|
-
since_ts: args.since_ts,
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
// Record successful request
|
|
1329
|
-
await recordRateLimitedRequest(state.agentName, "inbox");
|
|
1330
|
-
|
|
1331
|
-
return JSON.stringify(messages, null, 2);
|
|
1332
|
-
},
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
/**
|
|
1336
|
-
* Read a single message body by ID
|
|
1337
|
-
*/
|
|
1338
|
-
export const agentmail_read_message = tool({
|
|
1339
|
-
description: "Fetch ONE message body by ID (use after inbox)",
|
|
1340
|
-
args: {
|
|
1341
|
-
message_id: tool.schema.number().describe("Message ID from inbox"),
|
|
1342
|
-
},
|
|
1343
|
-
async execute(args, ctx) {
|
|
1344
|
-
const state = requireState(ctx.sessionID);
|
|
1345
|
-
|
|
1346
|
-
// Check rate limit
|
|
1347
|
-
await checkRateLimit(state.agentName, "read_message");
|
|
1348
|
-
|
|
1349
|
-
// Mark as read
|
|
1350
|
-
await mcpCall("mark_message_read", {
|
|
1351
|
-
project_key: state.projectKey,
|
|
1352
|
-
agent_name: state.agentName,
|
|
1353
|
-
message_id: args.message_id,
|
|
1354
|
-
});
|
|
1355
|
-
|
|
1356
|
-
// Fetch with body - fetch more messages to find the requested one
|
|
1357
|
-
// Since there's no get_message endpoint, we need to fetch a reasonable batch
|
|
1358
|
-
const messages = await mcpCall<MessageHeader[]>("fetch_inbox", {
|
|
1359
|
-
project_key: state.projectKey,
|
|
1360
|
-
agent_name: state.agentName,
|
|
1361
|
-
limit: 50, // Fetch more messages to increase chance of finding the target
|
|
1362
|
-
include_bodies: true, // Only for single message fetch
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
const message = messages.find((m) => m.id === args.message_id);
|
|
1366
|
-
if (!message) {
|
|
1367
|
-
return `Message ${args.message_id} not found in recent 50 messages. Try using agentmail_search to locate it.`;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
// Record successful request
|
|
1371
|
-
await recordRateLimitedRequest(state.agentName, "read_message");
|
|
1372
|
-
|
|
1373
|
-
return JSON.stringify(message, null, 2);
|
|
1374
|
-
},
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
|
-
/**
|
|
1378
|
-
* Summarize a thread (PREFERRED over fetching all messages)
|
|
1379
|
-
*/
|
|
1380
|
-
export const agentmail_summarize_thread = tool({
|
|
1381
|
-
description: "Summarize thread (PREFERRED over fetching all messages)",
|
|
1382
|
-
args: {
|
|
1383
|
-
thread_id: tool.schema.string().describe("Thread ID (usually bead ID)"),
|
|
1384
|
-
include_examples: tool.schema
|
|
1385
|
-
.boolean()
|
|
1386
|
-
.optional()
|
|
1387
|
-
.describe("Include up to 3 sample messages"),
|
|
1388
|
-
},
|
|
1389
|
-
async execute(args, ctx) {
|
|
1390
|
-
const state = requireState(ctx.sessionID);
|
|
1391
|
-
|
|
1392
|
-
// Check rate limit
|
|
1393
|
-
await checkRateLimit(state.agentName, "summarize_thread");
|
|
1394
|
-
|
|
1395
|
-
const summary = await mcpCall<ThreadSummary>("summarize_thread", {
|
|
1396
|
-
project_key: state.projectKey,
|
|
1397
|
-
thread_id: args.thread_id,
|
|
1398
|
-
include_examples: args.include_examples || false,
|
|
1399
|
-
llm_mode: true, // Use LLM for better summaries
|
|
1400
|
-
});
|
|
1401
|
-
|
|
1402
|
-
// Record successful request
|
|
1403
|
-
await recordRateLimitedRequest(state.agentName, "summarize_thread");
|
|
1404
|
-
|
|
1405
|
-
return JSON.stringify(summary, null, 2);
|
|
1406
|
-
},
|
|
1407
|
-
});
|
|
1408
|
-
|
|
1409
|
-
/**
|
|
1410
|
-
* Reserve file paths for exclusive editing
|
|
1411
|
-
*/
|
|
1412
|
-
export const agentmail_reserve = tool({
|
|
1413
|
-
description: "Reserve file paths for exclusive editing",
|
|
1414
|
-
args: {
|
|
1415
|
-
paths: tool.schema
|
|
1416
|
-
.array(tool.schema.string())
|
|
1417
|
-
.describe("File paths or globs to reserve (e.g., src/auth/**)"),
|
|
1418
|
-
ttl_seconds: tool.schema
|
|
1419
|
-
.number()
|
|
1420
|
-
.optional()
|
|
1421
|
-
.describe(`Time to live in seconds (default: ${DEFAULT_TTL_SECONDS})`),
|
|
1422
|
-
exclusive: tool.schema
|
|
1423
|
-
.boolean()
|
|
1424
|
-
.optional()
|
|
1425
|
-
.describe("Exclusive lock (default: true)"),
|
|
1426
|
-
reason: tool.schema
|
|
1427
|
-
.string()
|
|
1428
|
-
.optional()
|
|
1429
|
-
.describe("Reason for reservation (include bead ID)"),
|
|
1430
|
-
},
|
|
1431
|
-
async execute(args, ctx) {
|
|
1432
|
-
const state = requireState(ctx.sessionID);
|
|
1433
|
-
|
|
1434
|
-
// Check rate limit
|
|
1435
|
-
await checkRateLimit(state.agentName, "reserve");
|
|
1436
|
-
|
|
1437
|
-
const result = await mcpCall<ReservationResult>("file_reservation_paths", {
|
|
1438
|
-
project_key: state.projectKey,
|
|
1439
|
-
agent_name: state.agentName,
|
|
1440
|
-
paths: args.paths,
|
|
1441
|
-
ttl_seconds: args.ttl_seconds || DEFAULT_TTL_SECONDS,
|
|
1442
|
-
exclusive: args.exclusive ?? true,
|
|
1443
|
-
reason: args.reason || "",
|
|
1444
|
-
});
|
|
1445
|
-
|
|
1446
|
-
// Handle unexpected response structure
|
|
1447
|
-
if (!result) {
|
|
1448
|
-
throw new AgentMailError(
|
|
1449
|
-
"Unexpected response: file_reservation_paths returned null/undefined",
|
|
1450
|
-
"file_reservation_paths",
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// Check for conflicts
|
|
1455
|
-
if (result.conflicts && result.conflicts.length > 0) {
|
|
1456
|
-
const conflictDetails = result.conflicts
|
|
1457
|
-
.map((c) => `${c.path}: held by ${c.holders.join(", ")}`)
|
|
1458
|
-
.join("\n");
|
|
1459
|
-
|
|
1460
|
-
throw new FileReservationConflictError(
|
|
1461
|
-
`Cannot reserve files:\n${conflictDetails}`,
|
|
1462
|
-
result.conflicts,
|
|
1463
|
-
);
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// Handle case where granted is undefined/null (alternative response formats)
|
|
1467
|
-
const granted = result.granted ?? [];
|
|
1468
|
-
if (!Array.isArray(granted)) {
|
|
1469
|
-
throw new AgentMailError(
|
|
1470
|
-
`Unexpected response format: expected granted to be an array, got ${typeof granted}`,
|
|
1471
|
-
"file_reservation_paths",
|
|
1472
|
-
);
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// Store reservation IDs for auto-release
|
|
1476
|
-
const reservationIds = granted.map((r) => r.id);
|
|
1477
|
-
state.reservations = [...state.reservations, ...reservationIds];
|
|
1478
|
-
setState(ctx.sessionID, state);
|
|
1479
|
-
|
|
1480
|
-
// Record successful request
|
|
1481
|
-
await recordRateLimitedRequest(state.agentName, "reserve");
|
|
1482
|
-
|
|
1483
|
-
if (granted.length === 0) {
|
|
1484
|
-
return "No paths were reserved (empty granted list)";
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
return `Reserved ${granted.length} path(s):\n${granted
|
|
1488
|
-
.map((r) => ` - ${r.path_pattern} (expires: ${r.expires_ts})`)
|
|
1489
|
-
.join("\n")}`;
|
|
1490
|
-
},
|
|
1491
|
-
});
|
|
1492
|
-
|
|
1493
|
-
/**
|
|
1494
|
-
* Release file reservations
|
|
1495
|
-
*/
|
|
1496
|
-
export const agentmail_release = tool({
|
|
1497
|
-
description: "Release file reservations (auto-called on task completion)",
|
|
1498
|
-
args: {
|
|
1499
|
-
paths: tool.schema
|
|
1500
|
-
.array(tool.schema.string())
|
|
1501
|
-
.optional()
|
|
1502
|
-
.describe("Specific paths to release (omit for all)"),
|
|
1503
|
-
reservation_ids: tool.schema
|
|
1504
|
-
.array(tool.schema.number())
|
|
1505
|
-
.optional()
|
|
1506
|
-
.describe("Specific reservation IDs to release"),
|
|
1507
|
-
},
|
|
1508
|
-
async execute(args, ctx) {
|
|
1509
|
-
const state = requireState(ctx.sessionID);
|
|
1510
|
-
|
|
1511
|
-
// Check rate limit
|
|
1512
|
-
await checkRateLimit(state.agentName, "release");
|
|
1513
|
-
|
|
1514
|
-
const result = await mcpCall<{ released: number; released_at: string }>(
|
|
1515
|
-
"release_file_reservations",
|
|
1516
|
-
{
|
|
1517
|
-
project_key: state.projectKey,
|
|
1518
|
-
agent_name: state.agentName,
|
|
1519
|
-
paths: args.paths,
|
|
1520
|
-
file_reservation_ids: args.reservation_ids,
|
|
1521
|
-
},
|
|
1522
|
-
);
|
|
1523
|
-
|
|
1524
|
-
// Clear stored reservation IDs
|
|
1525
|
-
state.reservations = [];
|
|
1526
|
-
setState(ctx.sessionID, state);
|
|
1527
|
-
|
|
1528
|
-
// Record successful request
|
|
1529
|
-
await recordRateLimitedRequest(state.agentName, "release");
|
|
1530
|
-
|
|
1531
|
-
return `Released ${result.released} reservation(s)`;
|
|
1532
|
-
},
|
|
1533
|
-
});
|
|
1534
|
-
|
|
1535
|
-
/**
|
|
1536
|
-
* Acknowledge a message
|
|
1537
|
-
*/
|
|
1538
|
-
export const agentmail_ack = tool({
|
|
1539
|
-
description: "Acknowledge a message (for ack_required messages)",
|
|
1540
|
-
args: {
|
|
1541
|
-
message_id: tool.schema.number().describe("Message ID to acknowledge"),
|
|
1542
|
-
},
|
|
1543
|
-
async execute(args, ctx) {
|
|
1544
|
-
const state = requireState(ctx.sessionID);
|
|
1545
|
-
|
|
1546
|
-
// Check rate limit
|
|
1547
|
-
await checkRateLimit(state.agentName, "ack");
|
|
1548
|
-
|
|
1549
|
-
await mcpCall("acknowledge_message", {
|
|
1550
|
-
project_key: state.projectKey,
|
|
1551
|
-
agent_name: state.agentName,
|
|
1552
|
-
message_id: args.message_id,
|
|
1553
|
-
});
|
|
1554
|
-
|
|
1555
|
-
// Record successful request
|
|
1556
|
-
await recordRateLimitedRequest(state.agentName, "ack");
|
|
1557
|
-
|
|
1558
|
-
return `Acknowledged message ${args.message_id}`;
|
|
1559
|
-
},
|
|
1560
|
-
});
|
|
1561
|
-
|
|
1562
|
-
/**
|
|
1563
|
-
* Search messages
|
|
1564
|
-
*/
|
|
1565
|
-
export const agentmail_search = tool({
|
|
1566
|
-
description: "Search messages by keyword (FTS5 syntax supported)",
|
|
1567
|
-
args: {
|
|
1568
|
-
query: tool.schema
|
|
1569
|
-
.string()
|
|
1570
|
-
.describe('Search query (e.g., "build plan", plan AND users)'),
|
|
1571
|
-
limit: tool.schema
|
|
1572
|
-
.number()
|
|
1573
|
-
.optional()
|
|
1574
|
-
.describe("Max results (default: 20)"),
|
|
1575
|
-
},
|
|
1576
|
-
async execute(args, ctx) {
|
|
1577
|
-
const state = requireState(ctx.sessionID);
|
|
1578
|
-
|
|
1579
|
-
// Check rate limit
|
|
1580
|
-
await checkRateLimit(state.agentName, "search");
|
|
1581
|
-
|
|
1582
|
-
const results = await mcpCall<MessageHeader[]>("search_messages", {
|
|
1583
|
-
project_key: state.projectKey,
|
|
1584
|
-
query: args.query,
|
|
1585
|
-
limit: args.limit || 20,
|
|
1586
|
-
});
|
|
1587
|
-
|
|
1588
|
-
// Record successful request
|
|
1589
|
-
await recordRateLimitedRequest(state.agentName, "search");
|
|
1590
|
-
|
|
1591
|
-
return JSON.stringify(results, null, 2);
|
|
1592
|
-
},
|
|
1593
|
-
});
|
|
1594
|
-
|
|
1595
|
-
/**
|
|
1596
|
-
* Check Agent Mail health
|
|
1597
|
-
*/
|
|
1598
|
-
export const agentmail_health = tool({
|
|
1599
|
-
description: "Check if Agent Mail server is running",
|
|
1600
|
-
args: {},
|
|
1601
|
-
async execute(args, ctx) {
|
|
1602
|
-
try {
|
|
1603
|
-
const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`);
|
|
1604
|
-
if (response.ok) {
|
|
1605
|
-
// Also check if MCP is functional
|
|
1606
|
-
const functional = await isServerFunctional();
|
|
1607
|
-
if (functional) {
|
|
1608
|
-
return "Agent Mail is running and functional";
|
|
1609
|
-
}
|
|
1610
|
-
return "Agent Mail health OK but MCP not responding - consider restart";
|
|
1611
|
-
}
|
|
1612
|
-
return `Agent Mail returned status ${response.status}`;
|
|
1613
|
-
} catch (error) {
|
|
1614
|
-
return `Agent Mail not reachable: ${error instanceof Error ? error.message : String(error)}`;
|
|
1615
|
-
}
|
|
1616
|
-
},
|
|
1617
|
-
});
|
|
1618
|
-
|
|
1619
|
-
/**
|
|
1620
|
-
* Manually restart Agent Mail server
|
|
1621
|
-
*
|
|
1622
|
-
* Use when server is in bad state (health OK but MCP failing).
|
|
1623
|
-
* This kills the existing process and starts a fresh one.
|
|
1624
|
-
*/
|
|
1625
|
-
export const agentmail_restart = tool({
|
|
1626
|
-
description:
|
|
1627
|
-
"Manually restart Agent Mail server (use when getting 'unexpected error')",
|
|
1628
|
-
args: {
|
|
1629
|
-
force: tool.schema
|
|
1630
|
-
.boolean()
|
|
1631
|
-
.optional()
|
|
1632
|
-
.describe(
|
|
1633
|
-
"Force restart even if server appears healthy (default: false)",
|
|
1634
|
-
),
|
|
1635
|
-
},
|
|
1636
|
-
async execute(args) {
|
|
1637
|
-
// Check if restart is needed
|
|
1638
|
-
if (!args.force) {
|
|
1639
|
-
const functional = await isServerFunctional();
|
|
1640
|
-
if (functional) {
|
|
1641
|
-
return JSON.stringify(
|
|
1642
|
-
{
|
|
1643
|
-
restarted: false,
|
|
1644
|
-
reason: "Server is functional, no restart needed",
|
|
1645
|
-
hint: "Use force=true to restart anyway",
|
|
1646
|
-
},
|
|
1647
|
-
null,
|
|
1648
|
-
2,
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
// Attempt restart
|
|
1654
|
-
console.warn("[agent-mail] Manual restart requested...");
|
|
1655
|
-
const success = await restartServer();
|
|
1656
|
-
|
|
1657
|
-
// Clear caches
|
|
1658
|
-
agentMailAvailable = null;
|
|
1659
|
-
consecutiveFailures = 0;
|
|
1660
|
-
|
|
1661
|
-
if (success) {
|
|
1662
|
-
return JSON.stringify(
|
|
1663
|
-
{
|
|
1664
|
-
restarted: true,
|
|
1665
|
-
success: true,
|
|
1666
|
-
message: "Agent Mail server restarted successfully",
|
|
1667
|
-
},
|
|
1668
|
-
null,
|
|
1669
|
-
2,
|
|
1670
|
-
);
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
return JSON.stringify(
|
|
1674
|
-
{
|
|
1675
|
-
restarted: true,
|
|
1676
|
-
success: false,
|
|
1677
|
-
error: "Restart attempted but server did not come back up",
|
|
1678
|
-
hint: "Check server logs or manually start: agent-mail serve",
|
|
1679
|
-
},
|
|
1680
|
-
null,
|
|
1681
|
-
2,
|
|
1682
|
-
);
|
|
1683
|
-
},
|
|
1684
|
-
});
|
|
1685
|
-
|
|
1686
|
-
// ============================================================================
|
|
1687
|
-
// Export all tools
|
|
1688
|
-
// ============================================================================
|
|
1689
|
-
|
|
1690
|
-
export const agentMailTools = {
|
|
1691
|
-
agentmail_init: agentmail_init,
|
|
1692
|
-
agentmail_send: agentmail_send,
|
|
1693
|
-
agentmail_inbox: agentmail_inbox,
|
|
1694
|
-
agentmail_read_message: agentmail_read_message,
|
|
1695
|
-
agentmail_summarize_thread: agentmail_summarize_thread,
|
|
1696
|
-
agentmail_reserve: agentmail_reserve,
|
|
1697
|
-
agentmail_release: agentmail_release,
|
|
1698
|
-
agentmail_ack: agentmail_ack,
|
|
1699
|
-
agentmail_search: agentmail_search,
|
|
1700
|
-
agentmail_health: agentmail_health,
|
|
1701
|
-
agentmail_restart: agentmail_restart,
|
|
1702
|
-
};
|
|
1703
|
-
|
|
1704
|
-
// ============================================================================
|
|
1705
|
-
// Utility exports for other modules
|
|
1706
|
-
// ============================================================================
|
|
1707
|
-
|
|
1708
|
-
export {
|
|
1709
|
-
requireState,
|
|
1710
|
-
setState,
|
|
1711
|
-
getState,
|
|
1712
|
-
clearState,
|
|
1713
|
-
sessionStates,
|
|
1714
|
-
AGENT_MAIL_URL,
|
|
1715
|
-
MAX_INBOX_LIMIT,
|
|
1716
|
-
// Recovery/retry utilities (resetRecoveryState already exported at definition)
|
|
1717
|
-
isServerHealthy,
|
|
1718
|
-
isServerFunctional,
|
|
1719
|
-
restartServer,
|
|
1720
|
-
RETRY_CONFIG,
|
|
1721
|
-
RECOVERY_CONFIG,
|
|
1722
|
-
// Note: isProjectNotFoundError, isAgentNotFoundError, mcpCallWithAutoInit
|
|
1723
|
-
// are exported at their definitions
|
|
1724
|
-
};
|