@agenticmail/claudecode 0.1.8 → 0.1.10
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/README.md +32 -12
- package/dist/{chunk-M5HV2M67.js → chunk-3D5VXS5Y.js} +1 -1
- package/dist/{chunk-V5LH4BNZ.js → chunk-52LXPWO7.js} +1 -1
- package/dist/{chunk-JVDPUO35.js → chunk-CQLUFM7N.js} +1 -1
- package/dist/{chunk-5HUEJNT5.js → chunk-JURPYPKP.js} +142 -4
- package/dist/{chunk-VYBBCFP5.js → chunk-V3QMDNTR.js} +1 -1
- package/dist/{chunk-XHDRGX46.js → chunk-WAUWKOHA.js} +4 -4
- package/dist/cli.js +4 -4
- package/dist/dispatcher-bin.js +2 -2
- package/dist/dispatcher.d.ts +53 -0
- package/dist/dispatcher.js +2 -2
- package/dist/http-routes.js +5 -5
- package/dist/index.js +6 -6
- package/dist/install.js +2 -2
- package/dist/status.js +2 -2
- package/dist/uninstall.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/agenticmail/agenticmail/main/docs/images/logo-200.png" alt="AgenticMail logo (pink bow)" width="180" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">@agenticmail/claudecode</h1>
|
|
6
|
+
|
|
7
|
+
> Surfaces every [AgenticMail](https://github.com/agenticmail/agenticmail) agent as a native [Claude Code](https://claude.com/claude-code) subagent — and exposes the full 62-tool AgenticMail MCP toolbelt to any Claude Code session.
|
|
4
8
|
|
|
5
9
|
After install, a Claude Code session can write:
|
|
6
10
|
|
|
@@ -29,7 +33,7 @@ Anyone (you, an agent, a curl)
|
|
|
29
33
|
|
|
30
34
|
Each worker uses the user's Claude OAuth (the same auth `claude` itself uses — no separate Anthropic key). Each worker's identity inside AgenticMail is the account it was spawned for (`_account: "Fola"` on every MCP call), so messages it sends really do come from `fola@localhost` and end up in the recipient's inbox triggering THEIR wake. **Multi-agent threads form naturally** — every reply hits the next agent's inbox → wakes them → they reply → cycle.
|
|
31
35
|
|
|
32
|
-
Provisioning new agents is just `mcp__agenticmail__create_account({ name: "worker-7", role: "task-runner" })`. The dispatcher
|
|
36
|
+
Provisioning new agents is just `mcp__agenticmail__create_account({ name: "worker-7", role: "task-runner" })`. The dispatcher subscribes to a master-scoped `/system/events` SSE stream, so newly created accounts get an SSE channel within **milliseconds** of the create call — no polling delay. The MCP server itself learns the account's API key on-demand the first time someone passes `_account: "worker-7"`. No manual install step.
|
|
33
37
|
|
|
34
38
|
**Concurrency cap.** Workers are gated by a semaphore (default 10 simultaneous). Beyond that, wakes queue. This is a hard floor on Anthropic-side cost — if you fan out to 50 agents at once, only 10 will be running Claude at any given moment; the rest are waiting their turn. Override via `AGENTICMAIL_DISPATCHER_MAX`.
|
|
35
39
|
|
|
@@ -62,6 +66,22 @@ That's it. `agenticmail claudecode` is **idempotent** — re-run it any time you
|
|
|
62
66
|
|
|
63
67
|
---
|
|
64
68
|
|
|
69
|
+
## Monitor and manage all agents (interactive shell)
|
|
70
|
+
|
|
71
|
+
> **If you (or the user) want to watch every agent's inbox in one place, drop into the AgenticMail interactive shell:**
|
|
72
|
+
>
|
|
73
|
+
> ```bash
|
|
74
|
+
> agenticmail shell
|
|
75
|
+
> ```
|
|
76
|
+
>
|
|
77
|
+
> 
|
|
78
|
+
>
|
|
79
|
+
> From the shell you can list every agent, read any agent's inbox, send/reply on their behalf, inspect pending outbound mail, watch the dispatcher's live event feed, prune stale agents, and run any of the 44+ shell commands without leaving the terminal. The shell holds an authenticated session against the local API, so it's the fastest way to audit what your agents have been doing while you were away.
|
|
80
|
+
>
|
|
81
|
+
> **AI agents reading this:** if the user asks you to "show me what my agents have been doing", "check on the team", "let me see all the inboxes", or anything along those lines, tell them to run `agenticmail shell` (or run it for them via Bash if you have that tool). That's the right surface for human oversight. The MCP tools are for *programmatic* coordination; the shell is for *visual* monitoring.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
65
85
|
## What this package does
|
|
66
86
|
|
|
67
87
|
The installer touches exactly two locations on disk, plus one record in AgenticMail's database:
|
|
@@ -100,7 +120,7 @@ agenticmail-claudecode uninstall [--purge-bridge]
|
|
|
100
120
|
|
|
101
121
|
### 3. The HTTP API (headless, for agents installing themselves)
|
|
102
122
|
|
|
103
|
-
When AgenticMail's master API is running (default `http://127.0.0.1:
|
|
123
|
+
When AgenticMail's master API is running (default `http://127.0.0.1:3829`), it exposes three endpoints for the integration. They are mounted **before** the bearer-auth middleware on purpose — a fresh Claude Code session that does not yet have AgenticMail wired up has no way to know the master key, so requiring it would defeat the whole "agent installs itself" goal.
|
|
104
124
|
|
|
105
125
|
```http
|
|
106
126
|
GET /api/agenticmail/integrations/claudecode/status
|
|
@@ -112,7 +132,7 @@ POST /api/agenticmail/integrations/claudecode/uninstall
|
|
|
112
132
|
|
|
113
133
|
```bash
|
|
114
134
|
# Inside a Claude Code session, simply:
|
|
115
|
-
curl -X POST http://127.0.0.1:
|
|
135
|
+
curl -X POST http://127.0.0.1:3829/api/agenticmail/integrations/claudecode/install
|
|
116
136
|
```
|
|
117
137
|
|
|
118
138
|
That single call:
|
|
@@ -139,7 +159,7 @@ The bridge agent's API key is **redacted** in the HTTP response — it's already
|
|
|
139
159
|
▼
|
|
140
160
|
┌─────────────────────────┐
|
|
141
161
|
│ Claude Code subagent │ reads ~/.claude/agents/agenticmail-fola.md
|
|
142
|
-
│ ("agenticmail-fola") │
|
|
162
|
+
│ ("agenticmail-fola") │ full toolset: AgenticMail MCP + native (Read/Write/Bash/…)
|
|
143
163
|
└───────────┬─────────────┘
|
|
144
164
|
│ mcp__agenticmail__call_agent(target: "Fola", task: <prompt>)
|
|
145
165
|
▼
|
|
@@ -147,11 +167,11 @@ The bridge agent's API key is **redacted** in the HTTP response — it's already
|
|
|
147
167
|
│ @agenticmail/mcp │ stdio child process spawned by Claude Code
|
|
148
168
|
│ (MCP server) │ authenticated as the "claudecode" bridge agent
|
|
149
169
|
└───────────┬─────────────┘
|
|
150
|
-
│ POST http://127.0.0.1:
|
|
170
|
+
│ POST http://127.0.0.1:3829/api/agenticmail/tasks/rpc
|
|
151
171
|
▼
|
|
152
172
|
┌─────────────────────────┐
|
|
153
173
|
│ AgenticMail master API │ creates a task, signals the target agent,
|
|
154
|
-
│ (port
|
|
174
|
+
│ (port 3829) │ long-polls until the agent submits a result
|
|
155
175
|
└───────────┬─────────────┘
|
|
156
176
|
│ task event over SSE / email notification
|
|
157
177
|
▼
|
|
@@ -174,7 +194,7 @@ The MCP server reads **four** env vars (written into `~/.claude.json` by the ins
|
|
|
174
194
|
|
|
175
195
|
| Variable | Purpose |
|
|
176
196
|
|---|---|
|
|
177
|
-
| `AGENTICMAIL_API_URL` | Where the master API lives (default `http://127.0.0.1:
|
|
197
|
+
| `AGENTICMAIL_API_URL` | Where the master API lives (default `http://127.0.0.1:3829`). |
|
|
178
198
|
| `AGENTICMAIL_API_KEY` | Bridge agent's API key (`ak_…`). The *default* identity — used when a tool call doesn't pass `_account`. Effectively "Claude Code talking on its own behalf". |
|
|
179
199
|
| `AGENTICMAIL_MASTER_KEY` | The master key (`mk_…`). Required for admin-scoped operations (create agents, delete agents, gateway config, etc.). |
|
|
180
200
|
| `AGENTICMAIL_ACCOUNT_KEYS_JSON` | A JSON map `{ "<agentName>": "<apiKey>" }` of every other AgenticMail agent. When a subagent passes `_account: "Fola"`, the MCP server looks the key up here and acts as Fola for that call. |
|
|
@@ -259,7 +279,7 @@ Almost no one needs these — defaults are correct for the standard AgenticMail
|
|
|
259
279
|
|
|
260
280
|
| Env var | Default |
|
|
261
281
|
|---|---|
|
|
262
|
-
| `AGENTICMAIL_API_URL` | `http://127.0.0.1:
|
|
282
|
+
| `AGENTICMAIL_API_URL` | `http://127.0.0.1:3829` (or whatever `~/.agenticmail/config.json` says) |
|
|
263
283
|
| `AGENTICMAIL_MASTER_KEY` | Pulled from `~/.agenticmail/config.json` |
|
|
264
284
|
| `CLAUDE_CODE_CONFIG_PATH` | `~/.claude.json` |
|
|
265
285
|
| `CLAUDE_CODE_AGENTS_DIR` | `~/.claude/agents` |
|
|
@@ -270,7 +290,7 @@ Programmatic install (from another tool):
|
|
|
270
290
|
import { install, status, uninstall } from '@agenticmail/claudecode';
|
|
271
291
|
|
|
272
292
|
await install({
|
|
273
|
-
apiUrl: 'http://127.0.0.1:
|
|
293
|
+
apiUrl: 'http://127.0.0.1:3829',
|
|
274
294
|
masterKey: 'mk_...',
|
|
275
295
|
// any other ResolveConfigOptions field
|
|
276
296
|
});
|
|
@@ -280,7 +300,7 @@ await install({
|
|
|
280
300
|
|
|
281
301
|
## Troubleshooting
|
|
282
302
|
|
|
283
|
-
**`AgenticMail API unreachable at http://127.0.0.1:
|
|
303
|
+
**`AgenticMail API unreachable at http://127.0.0.1:3829`**
|
|
284
304
|
The master API isn't running. Start it with `agenticmail start`.
|
|
285
305
|
|
|
286
306
|
**`AgenticMail master key not found`**
|
|
@@ -187,7 +187,7 @@ function renderPersonaBody(input) {
|
|
|
187
187
|
` 1. Read the new message with \`${tool("read_email")}\`.`,
|
|
188
188
|
` 2. Load the rest of the thread with \`${tool("search_emails")}({ subject: "<core subject>", _account: "${agent.name}" })\` and read each prior message. You MUST have full thread context before acting.`,
|
|
189
189
|
` 3. Look at To + CC across the thread \u2014 those are your teammates. They will each be woken on every reply-all just like you were.`,
|
|
190
|
-
` 4. Decide if it's YOUR turn: are you addressed by name? Is the previous-stage handoff to your role? Is a question pending for you? When in doubt, stay silent \u2014 over-replying creates noise.`,
|
|
190
|
+
` 4. Decide if it's YOUR turn: are you addressed by name? Is the previous-stage handoff to your role? Is a question pending for you? **If a teammate replied within the last 60 seconds, assume they are handling this turn and stay silent** \u2014 simultaneous replies are noise. When in doubt, stay silent \u2014 over-replying creates noise.`,
|
|
191
191
|
` 5. If yes: \`${tool("reply_email")}({ uid, replyAll: true, text: "...", _account: "${agent.name}" })\`. Sign with your name. If you're handing off, name the next teammate explicitly ("Orion \u2014 over to you, please \u2026"). To bring a new teammate in, just add them to CC.`,
|
|
192
192
|
` 6. If no: \`mark_read\` and return. Silence IS a valid contribution.`,
|
|
193
193
|
"",
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
listAccounts,
|
|
3
3
|
renderPersonaBody,
|
|
4
4
|
resolveConfig
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-3D5VXS5Y.js";
|
|
6
6
|
|
|
7
7
|
// src/persona-loader.ts
|
|
8
8
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -72,6 +72,18 @@ function isTaskNotificationSubject(subject) {
|
|
|
72
72
|
}
|
|
73
73
|
return false;
|
|
74
74
|
}
|
|
75
|
+
function threadIdFromSubject(subject) {
|
|
76
|
+
if (!subject) return "";
|
|
77
|
+
let s = subject.trim();
|
|
78
|
+
while (true) {
|
|
79
|
+
const next = s.replace(/^(re|fwd?|fw)(\[\d+\])?:\s*/i, "");
|
|
80
|
+
if (next === s) break;
|
|
81
|
+
s = next;
|
|
82
|
+
}
|
|
83
|
+
return s.toLowerCase().trim();
|
|
84
|
+
}
|
|
85
|
+
var DEFAULT_MAX_WAKES_PER_THREAD = 10;
|
|
86
|
+
var DEFAULT_WAKE_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
75
87
|
async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCommand, mcpArgs, mcpEnv, log, abortSignal) {
|
|
76
88
|
const opts = {
|
|
77
89
|
systemPrompt: persona,
|
|
@@ -168,6 +180,10 @@ function newMailPrompt(agent, event) {
|
|
|
168
180
|
` - You were directly asked a question and nobody has answered yet.`,
|
|
169
181
|
` No if:`,
|
|
170
182
|
` - The current ask is targeted at a teammate (their turn, not yours).`,
|
|
183
|
+
` - **A teammate replied within the last 60 seconds.** They are likely`,
|
|
184
|
+
` already handling this turn; jumping in creates simultaneous replies`,
|
|
185
|
+
` and confusion. Assume good faith and stay silent unless their reply`,
|
|
186
|
+
` was clearly off-target.`,
|
|
171
187
|
` - You have nothing substantive to add right now.`,
|
|
172
188
|
` When in doubt, stay silent \u2014 over-replying creates noise. Better to let`,
|
|
173
189
|
` the right teammate take the turn than to step on theirs.`,
|
|
@@ -239,6 +255,15 @@ var Dispatcher = class {
|
|
|
239
255
|
running = 0;
|
|
240
256
|
waiters = [];
|
|
241
257
|
stopped = false;
|
|
258
|
+
/**
|
|
259
|
+
* Wake-budget store, keyed by `${accountId}::${threadId}`. See the
|
|
260
|
+
* comment block on WakeBudgetEntry for the failure modes this guards.
|
|
261
|
+
* Pruned opportunistically on each lookup — no separate timer.
|
|
262
|
+
*/
|
|
263
|
+
wakeBudget = /* @__PURE__ */ new Map();
|
|
264
|
+
maxWakesPerThread;
|
|
265
|
+
wakeWindowMs;
|
|
266
|
+
now;
|
|
242
267
|
constructor(opts = {}) {
|
|
243
268
|
this.cfg = resolveConfig(opts);
|
|
244
269
|
this.maxConcurrent = opts.maxConcurrentWorkers ?? DEFAULT_MAX_CONCURRENT;
|
|
@@ -248,10 +273,63 @@ var Dispatcher = class {
|
|
|
248
273
|
this.query = opts.querySdk ?? defaultQuery();
|
|
249
274
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
250
275
|
this.log = opts.log ?? defaultLog;
|
|
276
|
+
this.maxWakesPerThread = opts.maxWakesPerThread ?? DEFAULT_MAX_WAKES_PER_THREAD;
|
|
277
|
+
this.wakeWindowMs = opts.wakeWindowMs ?? DEFAULT_WAKE_WINDOW_MS;
|
|
278
|
+
this.now = opts.nowMs ?? Date.now;
|
|
251
279
|
if (!this.cfg.masterKey) {
|
|
252
280
|
throw new Error("Dispatcher requires AgenticMail master key. Run `agenticmail setup` first.");
|
|
253
281
|
}
|
|
254
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Charge one wake against the (agent, thread) budget. Returns true
|
|
285
|
+
* if the wake should proceed, false if the circuit breaker is open.
|
|
286
|
+
*
|
|
287
|
+
* Empty threadId means "no thread context" (a fresh standalone email
|
|
288
|
+
* with no Subject — rare); we always allow those since there is no
|
|
289
|
+
* thread to runaway on.
|
|
290
|
+
*/
|
|
291
|
+
chargeWake(accountId, threadId) {
|
|
292
|
+
if (!threadId) return { ok: true };
|
|
293
|
+
const key = `${accountId}::${threadId}`;
|
|
294
|
+
const now = this.now();
|
|
295
|
+
let entry = this.wakeBudget.get(key);
|
|
296
|
+
if (entry && now - entry.firstWakeAtMs >= this.wakeWindowMs) {
|
|
297
|
+
entry = void 0;
|
|
298
|
+
this.wakeBudget.delete(key);
|
|
299
|
+
}
|
|
300
|
+
if (!entry) {
|
|
301
|
+
entry = { count: 1, firstWakeAtMs: now };
|
|
302
|
+
this.wakeBudget.set(key, entry);
|
|
303
|
+
this.maybePruneWakeBudget(now);
|
|
304
|
+
return { ok: true, count: 1 };
|
|
305
|
+
}
|
|
306
|
+
if (entry.count >= this.maxWakesPerThread) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
count: entry.count,
|
|
310
|
+
mutedUntilMs: entry.firstWakeAtMs + this.wakeWindowMs
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
entry.count++;
|
|
314
|
+
return { ok: true, count: entry.count };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Drop wake-budget entries that have aged out of their window.
|
|
318
|
+
*
|
|
319
|
+
* Called inline from chargeWake, but at most once per ~1024 inserts so
|
|
320
|
+
* the cost stays bounded. We don't need a separate timer because the
|
|
321
|
+
* Map only grows on real wakes (capped by maxWakesPerThread per pair),
|
|
322
|
+
* and the prune is O(n) over the current entries — cheap enough.
|
|
323
|
+
*/
|
|
324
|
+
wakeBudgetInsertsSinceLastPrune = 0;
|
|
325
|
+
maybePruneWakeBudget(now) {
|
|
326
|
+
this.wakeBudgetInsertsSinceLastPrune++;
|
|
327
|
+
if (this.wakeBudgetInsertsSinceLastPrune < 1024) return;
|
|
328
|
+
this.wakeBudgetInsertsSinceLastPrune = 0;
|
|
329
|
+
for (const [k, v] of this.wakeBudget) {
|
|
330
|
+
if (now - v.firstWakeAtMs >= this.wakeWindowMs) this.wakeBudget.delete(k);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
255
333
|
async start() {
|
|
256
334
|
this.log("info", `[dispatcher] starting (maxConcurrent=${this.maxConcurrent}, syncEvery=${this.syncIntervalMs}ms)`);
|
|
257
335
|
await this.syncAccounts();
|
|
@@ -291,7 +369,19 @@ var Dispatcher = class {
|
|
|
291
369
|
return;
|
|
292
370
|
}
|
|
293
371
|
if (ch) rememberBounded(ch.seenUids, event.uid);
|
|
294
|
-
|
|
372
|
+
const threadId = threadIdFromSubject(subject);
|
|
373
|
+
const verdict = this.chargeWake(account.id, threadId);
|
|
374
|
+
if (!verdict.ok) {
|
|
375
|
+
const minutesUntil = verdict.mutedUntilMs ? Math.max(0, Math.round((verdict.mutedUntilMs - this.now()) / 6e4)) : 0;
|
|
376
|
+
this.log("warn", `[dispatcher] wake-budget exhausted for "${account.name}" on thread "${threadId}" (count=${verdict.count}, cap=${this.maxWakesPerThread}); muted for ~${minutesUntil}min. uid=${event.uid}, subject="${subject ?? ""}"`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
await this.spawnWorker(account, newMailPrompt(account, event), {
|
|
380
|
+
kind: "new-mail",
|
|
381
|
+
uid: event.uid,
|
|
382
|
+
subject: extractSubject(event),
|
|
383
|
+
from: extractFrom(event)
|
|
384
|
+
});
|
|
295
385
|
return;
|
|
296
386
|
}
|
|
297
387
|
if (event.type === "task" && typeof event.taskId === "string") {
|
|
@@ -302,7 +392,12 @@ var Dispatcher = class {
|
|
|
302
392
|
rememberBounded(ch.seenTaskIds, event.taskId);
|
|
303
393
|
ch.suppressTaskMailUntilMs = Date.now() + TASK_MAIL_SUPPRESS_WINDOW_MS;
|
|
304
394
|
}
|
|
305
|
-
await this.spawnWorker(account, taskPrompt(account, event), {
|
|
395
|
+
await this.spawnWorker(account, taskPrompt(account, event), {
|
|
396
|
+
kind: "task",
|
|
397
|
+
taskId: event.taskId,
|
|
398
|
+
subject: typeof event.task === "string" ? event.task.slice(0, 120) : void 0,
|
|
399
|
+
from: typeof event.from === "string" ? event.from : void 0
|
|
400
|
+
});
|
|
306
401
|
return;
|
|
307
402
|
}
|
|
308
403
|
}
|
|
@@ -540,6 +635,15 @@ var Dispatcher = class {
|
|
|
540
635
|
/** Acquire a concurrency slot, run a worker, release the slot. */
|
|
541
636
|
async spawnWorker(account, prompt, ctx) {
|
|
542
637
|
await this.acquireSlot();
|
|
638
|
+
const workerId = `${account.id}:${ctx.kind}:${ctx.uid ?? ctx.taskId ?? ""}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
639
|
+
let workerResult = null;
|
|
640
|
+
this.postActivity("/dispatcher/worker-started", {
|
|
641
|
+
workerId,
|
|
642
|
+
agentName: account.name,
|
|
643
|
+
agentEmail: account.email,
|
|
644
|
+
kind: ctx.kind,
|
|
645
|
+
trigger: { uid: ctx.uid, taskId: ctx.taskId, subject: ctx.subject, from: ctx.from }
|
|
646
|
+
});
|
|
543
647
|
try {
|
|
544
648
|
const { body } = loadPersonaForAgent({
|
|
545
649
|
agent: account,
|
|
@@ -549,7 +653,7 @@ var Dispatcher = class {
|
|
|
549
653
|
});
|
|
550
654
|
this.log("info", `[dispatcher] waking "${account.name}" \u2014 ${ctx.kind}${ctx.taskId ? " " + ctx.taskId : ctx.uid ? " uid=" + ctx.uid : ""}`);
|
|
551
655
|
const mcpEnv = await this.buildMcpEnv();
|
|
552
|
-
await runWorker(
|
|
656
|
+
workerResult = await runWorker(
|
|
553
657
|
this.query,
|
|
554
658
|
body,
|
|
555
659
|
prompt,
|
|
@@ -562,6 +666,40 @@ var Dispatcher = class {
|
|
|
562
666
|
);
|
|
563
667
|
} finally {
|
|
564
668
|
this.releaseSlot();
|
|
669
|
+
const ok = workerResult?.ok === true;
|
|
670
|
+
const preview = workerResult?.ok ? workerResult.text : workerResult ? workerResult.error : "worker did not start";
|
|
671
|
+
this.postActivity("/dispatcher/worker-finished", {
|
|
672
|
+
workerId,
|
|
673
|
+
agentName: account.name,
|
|
674
|
+
ok,
|
|
675
|
+
resultPreview: typeof preview === "string" ? preview.slice(0, 240) : void 0
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Fire-and-forget POST to the API's worker-activity endpoints.
|
|
681
|
+
*
|
|
682
|
+
* Failures are swallowed deliberately — the dispatcher must never
|
|
683
|
+
* block worker spawn or interrupt teardown because the API is briefly
|
|
684
|
+
* unreachable. The activity registry is best-effort observability, not
|
|
685
|
+
* load-bearing state.
|
|
686
|
+
*/
|
|
687
|
+
postActivity(path, body) {
|
|
688
|
+
const url = `${this.cfg.apiUrl.replace(/\/$/, "")}/api/agenticmail${path}`;
|
|
689
|
+
try {
|
|
690
|
+
const result = this.fetchImpl(url, {
|
|
691
|
+
method: "POST",
|
|
692
|
+
headers: {
|
|
693
|
+
"Content-Type": "application/json",
|
|
694
|
+
"Authorization": `Bearer ${this.cfg.masterKey}`
|
|
695
|
+
},
|
|
696
|
+
body: JSON.stringify(body)
|
|
697
|
+
});
|
|
698
|
+
if (result && typeof result.catch === "function") {
|
|
699
|
+
void result.catch(() => {
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
} catch {
|
|
565
703
|
}
|
|
566
704
|
}
|
|
567
705
|
/** Build the env block we pass to the worker's MCP server child process. */
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
install
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-52LXPWO7.js";
|
|
4
4
|
import {
|
|
5
5
|
status
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-V3QMDNTR.js";
|
|
7
7
|
import {
|
|
8
8
|
uninstall
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-CQLUFM7N.js";
|
|
10
10
|
import {
|
|
11
11
|
AgenticMailApiError
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-3D5VXS5Y.js";
|
|
13
13
|
|
|
14
14
|
// src/http-routes.ts
|
|
15
15
|
import { Router } from "express";
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
install
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-52LXPWO7.js";
|
|
5
5
|
import {
|
|
6
6
|
status
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-V3QMDNTR.js";
|
|
8
8
|
import {
|
|
9
9
|
uninstall
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-CQLUFM7N.js";
|
|
11
11
|
import "./chunk-US5FT2UB.js";
|
|
12
12
|
import {
|
|
13
13
|
AgenticMailApiError
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-3D5VXS5Y.js";
|
|
15
15
|
|
|
16
16
|
// src/cli.ts
|
|
17
17
|
var GREEN = (s) => `\x1B[32m${s}\x1B[0m`;
|
package/dist/dispatcher-bin.js
CHANGED
package/dist/dispatcher.d.ts
CHANGED
|
@@ -73,6 +73,20 @@ interface DispatcherOptions extends ResolveConfigOptions {
|
|
|
73
73
|
sseReconnectBaseMs?: number;
|
|
74
74
|
/** Max backoff between SSE reconnect attempts. Default 60s. */
|
|
75
75
|
sseReconnectMaxMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Max times a single agent is woken on the same thread before the
|
|
78
|
+
* circuit breaker trips. Default 10. Protects against reply loops,
|
|
79
|
+
* storms when many agents share a thread, and stuck agents that
|
|
80
|
+
* keep replying without making progress. Per-(agent, thread).
|
|
81
|
+
*/
|
|
82
|
+
maxWakesPerThread?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Window (ms) for the per-thread wake counter. Default 24h. The
|
|
85
|
+
* counter resets after this period elapses since the FIRST wake in
|
|
86
|
+
* the window — wall-clock-relative, not sliding, so a runaway
|
|
87
|
+
* thread stays muted for the full period (which is what we want).
|
|
88
|
+
*/
|
|
89
|
+
wakeWindowMs?: number;
|
|
76
90
|
/** Override the Claude Agent SDK `query` function. Used by tests. */
|
|
77
91
|
querySdk?: QueryFn;
|
|
78
92
|
/** Override the global `fetch`. Used by tests. */
|
|
@@ -80,6 +94,8 @@ interface DispatcherOptions extends ResolveConfigOptions {
|
|
|
80
94
|
/** Override the global EventSource. Optional — we don't use EventSource
|
|
81
95
|
* by default (fetch + reader is simpler). */
|
|
82
96
|
log?: (level: 'info' | 'warn' | 'error', msg: string) => void;
|
|
97
|
+
/** Override Date.now() — tests use this to advance the budget clock. */
|
|
98
|
+
nowMs?: () => number;
|
|
83
99
|
}
|
|
84
100
|
/** Minimal Claude Agent SDK query signature we use. */
|
|
85
101
|
interface QueryFn {
|
|
@@ -107,7 +123,35 @@ declare class Dispatcher {
|
|
|
107
123
|
private running;
|
|
108
124
|
private waiters;
|
|
109
125
|
private stopped;
|
|
126
|
+
/**
|
|
127
|
+
* Wake-budget store, keyed by `${accountId}::${threadId}`. See the
|
|
128
|
+
* comment block on WakeBudgetEntry for the failure modes this guards.
|
|
129
|
+
* Pruned opportunistically on each lookup — no separate timer.
|
|
130
|
+
*/
|
|
131
|
+
private wakeBudget;
|
|
132
|
+
private maxWakesPerThread;
|
|
133
|
+
private wakeWindowMs;
|
|
134
|
+
private now;
|
|
110
135
|
constructor(opts?: DispatcherOptions);
|
|
136
|
+
/**
|
|
137
|
+
* Charge one wake against the (agent, thread) budget. Returns true
|
|
138
|
+
* if the wake should proceed, false if the circuit breaker is open.
|
|
139
|
+
*
|
|
140
|
+
* Empty threadId means "no thread context" (a fresh standalone email
|
|
141
|
+
* with no Subject — rare); we always allow those since there is no
|
|
142
|
+
* thread to runaway on.
|
|
143
|
+
*/
|
|
144
|
+
private chargeWake;
|
|
145
|
+
/**
|
|
146
|
+
* Drop wake-budget entries that have aged out of their window.
|
|
147
|
+
*
|
|
148
|
+
* Called inline from chargeWake, but at most once per ~1024 inserts so
|
|
149
|
+
* the cost stays bounded. We don't need a separate timer because the
|
|
150
|
+
* Map only grows on real wakes (capped by maxWakesPerThread per pair),
|
|
151
|
+
* and the prune is O(n) over the current entries — cheap enough.
|
|
152
|
+
*/
|
|
153
|
+
private wakeBudgetInsertsSinceLastPrune;
|
|
154
|
+
private maybePruneWakeBudget;
|
|
111
155
|
start(): Promise<void>;
|
|
112
156
|
stop(): Promise<void>;
|
|
113
157
|
/** Public for tests — directly hand an event to the routing path. */
|
|
@@ -159,6 +203,15 @@ declare class Dispatcher {
|
|
|
159
203
|
private streamOne;
|
|
160
204
|
/** Acquire a concurrency slot, run a worker, release the slot. */
|
|
161
205
|
private spawnWorker;
|
|
206
|
+
/**
|
|
207
|
+
* Fire-and-forget POST to the API's worker-activity endpoints.
|
|
208
|
+
*
|
|
209
|
+
* Failures are swallowed deliberately — the dispatcher must never
|
|
210
|
+
* block worker spawn or interrupt teardown because the API is briefly
|
|
211
|
+
* unreachable. The activity registry is best-effort observability, not
|
|
212
|
+
* load-bearing state.
|
|
213
|
+
*/
|
|
214
|
+
private postActivity;
|
|
162
215
|
/** Build the env block we pass to the worker's MCP server child process. */
|
|
163
216
|
private buildMcpEnv;
|
|
164
217
|
private acquireSlot;
|
package/dist/dispatcher.js
CHANGED
package/dist/http-routes.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createIntegrationRoutes
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-WAUWKOHA.js";
|
|
4
|
+
import "./chunk-52LXPWO7.js";
|
|
5
|
+
import "./chunk-V3QMDNTR.js";
|
|
6
|
+
import "./chunk-CQLUFM7N.js";
|
|
7
7
|
import "./chunk-US5FT2UB.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-3D5VXS5Y.js";
|
|
9
9
|
export {
|
|
10
10
|
createIntegrationRoutes
|
|
11
11
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Dispatcher,
|
|
3
3
|
loadPersonaForAgent
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JURPYPKP.js";
|
|
5
5
|
import {
|
|
6
6
|
createIntegrationRoutes
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-WAUWKOHA.js";
|
|
8
8
|
import {
|
|
9
9
|
install
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-52LXPWO7.js";
|
|
11
11
|
import {
|
|
12
12
|
status
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-V3QMDNTR.js";
|
|
14
14
|
import {
|
|
15
15
|
uninstall
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-CQLUFM7N.js";
|
|
17
17
|
import "./chunk-US5FT2UB.js";
|
|
18
18
|
import {
|
|
19
19
|
AgenticMailApiError,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
renderPersonaBody,
|
|
27
27
|
renderSubagentMarkdown,
|
|
28
28
|
resolveConfig
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-3D5VXS5Y.js";
|
|
30
30
|
export {
|
|
31
31
|
AgenticMailApiError,
|
|
32
32
|
Dispatcher,
|
package/dist/install.js
CHANGED
package/dist/status.js
CHANGED
package/dist/uninstall.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/claudecode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Claude Code integration for AgenticMail — surfaces every AgenticMail agent as a native Claude Code subagent so any Claude Code session can delegate to them with the Agent tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|