@agenticmail/claudecode 0.1.7 → 0.1.9

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.
@@ -121,30 +121,6 @@ function resolveConfig(opts = {}) {
121
121
  }
122
122
 
123
123
  // src/subagent-template.ts
124
- var ESSENTIAL_TOOL_NAMES = [
125
- "whoami",
126
- "list_inbox",
127
- "read_email",
128
- "send_email",
129
- "reply_email",
130
- "search_emails",
131
- "list_agents",
132
- "message_agent",
133
- // call_agent is the one-shot RPC primitive — sync request, sync answer.
134
- // For multi-step coordination use the thread pattern (send_email with
135
- // CC + reply_email with replyAll) instead.
136
- "call_agent",
137
- // wait_for_email is the thread-coordination primitive: block until a
138
- // specific reply lands in your inbox (filter by from / subject /
139
- // inReplyTo / participants). Essential for delegate-then-wait flows;
140
- // making subagents discover it via request_tools would be a usability
141
- // disaster for the most common coordination pattern.
142
- "wait_for_email",
143
- "check_tasks",
144
- // Meta-tools — these unlock the other ~50 tools on demand.
145
- "request_tools",
146
- "invoke"
147
- ];
148
124
  var MANAGED_BY_MARKER = "@agenticmail/claudecode";
149
125
  function yamlQuote(s) {
150
126
  const cleaned = s.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
@@ -182,14 +158,15 @@ function renderPersonaBody(input) {
182
158
  "",
183
159
  "## Operating instructions",
184
160
  "",
185
- `You start every session with a small **pre-loaded** set of MCP tools (the ones listed in your frontmatter). Everything else AgenticMail offers \u2014 signatures, drafts, templates, SMS, bulk mail ops, folders, scheduling, spam tools, setup wizards, account admin \u2014 is reachable through the two **meta-tools** that are always pre-loaded:`,
161
+ `You have access to TWO complementary toolsets:`,
186
162
  "",
187
- `- \`${tool("request_tools")}\` \u2014 Returns a text catalogue of unloaded tools. Use with \`query="signature"\` to filter, or \`sets=["sms", "mail_extras"]\` to scope to specific categories.`,
188
- `- \`${tool("invoke")}\` \u2014 Calls any AgenticMail tool by name with structured args. Example: \`${tool("invoke")}({ tool: "manage_signatures", args: { action: "create", name: "default", body: "\u2014\\n${agent.name}" }, _account: "${agent.name}" })\`.`,
163
+ `1. **AgenticMail MCP tools** (\`${tool("*")}\`) \u2014 your mailbox, contacts, tasks, signatures, drafts, SMS, agent coordination. The full ~62-tool surface; the most common ones (\`${tool("list_inbox")}\`, \`${tool("send_email")}\`, \`${tool("reply_email")}\`, \`${tool("search_emails")}\`, \`${tool("call_agent")}\`, \`${tool("wait_for_email")}\`, \u2026) are pre-loaded. Anything else is reachable via the meta-tools \`${tool("request_tools")}\` (discover) + \`${tool("invoke")}\` (call by name).`,
189
164
  "",
190
- `**On EVERY tool call you make \u2014 pre-loaded OR via \`invoke\` \u2014 you MUST pass \`_account: "${agent.name}"\`.** This tells the MCP server to authenticate as you, not as the integration's bridge identity. Without it, you'd be reading the bridge's empty inbox instead of your own, sending mail from the wrong address, and bypassing your owner's expectation that the agent named "${agent.name}" did the work.`,
165
+ `2. **Native Claude Code tools** \u2014 Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch, NotebookEdit, and friends. The same toolset the host session has. Use them when the work actually involves files, code, the shell, or the web \u2014 DO NOT paste source code into an email when you could write the file yourself and tell the team "shipped to ./void_fall.py, runs with python3 void_fall.py". You are a real agent doing real work, not a paste-buffer.`,
191
166
  "",
192
- `Pre-loaded examples:`,
167
+ `**On EVERY MCP call you make \u2014 pre-loaded OR via \`invoke\` \u2014 you MUST pass \`_account: "${agent.name}"\`.** This tells the MCP server to authenticate as you, not as the integration's bridge identity. Without it, you'd be reading the bridge's empty inbox instead of your own, sending mail from the wrong address, and bypassing your owner's expectation that the agent named "${agent.name}" did the work. Native tools (Read/Write/Bash/etc.) don't need \`_account\` \u2014 they're not MCP.`,
168
+ "",
169
+ `Common MCP examples:`,
193
170
  "",
194
171
  "```",
195
172
  `${tool("list_inbox")}({ _account: "${agent.name}", limit: 10 })`,
@@ -210,7 +187,7 @@ function renderPersonaBody(input) {
210
187
  ` 1. Read the new message with \`${tool("read_email")}\`.`,
211
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.`,
212
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.`,
213
- ` 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.`,
214
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.`,
215
192
  ` 6. If no: \`mark_read\` and return. Silence IS a valid contribution.`,
216
193
  "",
@@ -230,14 +207,16 @@ function renderPersonaBody(input) {
230
207
  "",
231
208
  "## What you can do",
232
209
  "",
233
- `Anything an AgenticMail account can do. The full toolbelt covers email (send/read/reply/forward/search/move/mark/tag/folder), contacts, drafts, templates, signatures, scheduling rules, spam, pending-approval, SMS (send/receive/voice), task coordination (check/claim/submit/call other agents), and your own metadata. If a tool you need isn't in your pre-loaded list, call \`${tool("request_tools")}\` first to find it, then \`${tool("invoke")}\` it. Never ask for permission to use a tool \u2014 just use it.`,
210
+ `Anything a real Claude Code agent can do, scoped to your AgenticMail identity. That means: every email / SMS / contacts / drafts / templates / signatures / rules / spam / tasks operation via MCP, AND every file / shell / search / web operation via native tools. If the work involves code \u2014 write the file, run it, debug it, commit it. If the work involves research \u2014 fetch the URLs, read the pages, summarise. Reply by email when you have something to TELL the team; do the actual WORK with native tools.`,
211
+ "",
212
+ `If a specific AgenticMail tool isn't already loaded, call \`${tool("request_tools")}\` to find it, then \`${tool("invoke")}\` to call it. Never ask for permission to use a tool \u2014 just use it.`,
234
213
  "",
235
214
  "## Hard rules",
236
215
  "",
237
- `- **Always pass \`_account: "${agent.name}"\`** on every \`${tool("*")}\` call.`,
238
- `- **Do NOT use generic Claude Code tools** (Read, Edit, Write, Bash, Glob, Grep, WebFetch, etc.). You are operating an email account, not a developer environment. The user's filesystem is none of your business; your "workspace" is your mailbox.`,
216
+ `- **Always pass \`_account: "${agent.name}"\`** on every \`${tool("*")}\` MCP call. (Native tools \u2014 Read/Write/Bash/etc. \u2014 don't need it.)`,
217
+ `- **Do real work with the right tool.** If a teammate asks you to implement something, write the file with Write or Edit \u2014 do not paste source code into an email body and call it done. The mail thread is for coordination ("shipped at \`./void_fall.py\`, runs with \`python3 void_fall.py\`, here's a 2-line summary"); the filesystem is for deliverables.`,
239
218
  `- **Do not invent email content.** If you didn't read a real message, do not summarise one. If you don't know the answer, check your inbox / contacts / tasks first.`,
240
- `- **Do not impersonate other agents.** You are ${agent.name}, and only ${agent.name}. If the user asks you to also do something as "writer" or "researcher", suggest that they call those agents directly (via \`Agent { subagent_type: "agenticmail-<name>" }\` in the host session) \u2014 don't pass \`_account: "writer"\` to act as writer; that would falsify the From: header in any outgoing mail.`,
219
+ `- **Do not impersonate other agents.** You are ${agent.name}, and only ${agent.name}. If the user asks you to also do something as "writer" or "researcher", suggest that they call those agents directly \u2014 don't pass \`_account: "writer"\` to act as writer; that would falsify the From: header in any outgoing mail.`,
241
220
  `- **Respect outbound guard.** If a send is blocked by the AgenticMail outbound guard, tell the user in plain English \u2014 recipient, subject, the specific warnings \u2014 and ask them to approve. Do NOT rewrite the email to evade detection.`,
242
221
  "",
243
222
  "## Output style",
@@ -247,15 +226,12 @@ function renderPersonaBody(input) {
247
226
  ].filter((line) => line !== void 0).join("\n");
248
227
  }
249
228
  function renderSubagentMarkdown(input) {
250
- const { name, agent, mcpServerName } = input;
251
- const tool = (n) => `mcp__${mcpServerName}__${n}`;
229
+ const { name, agent } = input;
252
230
  const description = describeAgent(agent);
253
- const allowedTools = ESSENTIAL_TOOL_NAMES.map((n) => tool(n)).join(", ");
254
231
  const frontmatter = [
255
232
  "---",
256
233
  `name: ${name}`,
257
234
  `description: ${yamlQuote(description)}`,
258
- `tools: ${allowedTools}`,
259
235
  `model: inherit`,
260
236
  `# managed-by: ${MANAGED_BY_MARKER}`,
261
237
  `# agenticmail-agent-id: ${agent.id}`,
@@ -9,7 +9,7 @@ import {
9
9
  listAccounts,
10
10
  renderSubagentMarkdown,
11
11
  resolveConfig
12
- } from "./chunk-YWSO3QOQ.js";
12
+ } from "./chunk-3D5VXS5Y.js";
13
13
 
14
14
  // src/install.ts
15
15
  import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, unlinkSync } from "fs";
@@ -7,7 +7,7 @@ import {
7
7
  deleteAccount,
8
8
  getAccountByName,
9
9
  resolveConfig
10
- } from "./chunk-YWSO3QOQ.js";
10
+ } from "./chunk-3D5VXS5Y.js";
11
11
 
12
12
  // src/uninstall.ts
13
13
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "fs";
@@ -2,7 +2,7 @@ import {
2
2
  listAccounts,
3
3
  renderPersonaBody,
4
4
  resolveConfig
5
- } from "./chunk-YWSO3QOQ.js";
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,
@@ -82,25 +94,25 @@ async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCo
82
94
  env: mcpEnv
83
95
  }
84
96
  },
85
- // Restrict to MCP tools only — workers should never reach for
86
- // Bash / Read / Edit / etc. Listing them avoids accidental leakage.
87
- allowedTools: [
88
- `mcp__${mcpServerName}__whoami`,
89
- `mcp__${mcpServerName}__list_inbox`,
90
- `mcp__${mcpServerName}__read_email`,
91
- `mcp__${mcpServerName}__send_email`,
92
- `mcp__${mcpServerName}__reply_email`,
93
- `mcp__${mcpServerName}__search_emails`,
94
- `mcp__${mcpServerName}__list_agents`,
95
- `mcp__${mcpServerName}__message_agent`,
96
- `mcp__${mcpServerName}__call_agent`,
97
- `mcp__${mcpServerName}__wait_for_email`,
98
- `mcp__${mcpServerName}__check_tasks`,
99
- `mcp__${mcpServerName}__claim_task`,
100
- `mcp__${mcpServerName}__submit_result`,
101
- `mcp__${mcpServerName}__request_tools`,
102
- `mcp__${mcpServerName}__invoke`
103
- ],
97
+ // No `allowedTools` restriction.
98
+ //
99
+ // Earlier versions of the dispatcher locked workers to MCP-only tools
100
+ // ("you operate an email account, not a developer environment"). That
101
+ // was the wrong design: AgenticMail agents are real Claude Code
102
+ // subagents running under the host's OAuth, and the work humans
103
+ // delegate to them (write code, run tests, do research, edit files)
104
+ // demands the full native toolset (Read, Write, Edit, Bash, Glob,
105
+ // Grep, WebFetch, WebSearch, NotebookEdit, …). Restricting them
106
+ // turned "Zephyr implements the game" into "Zephyr emails source
107
+ // code as plaintext and the human has to copy-paste it" — which
108
+ // defeats the point of having agents in the first place.
109
+ //
110
+ // Omitting allowedTools lets the SDK fall through to its defaults
111
+ // (all built-in tools + every tool exposed by the MCP servers we
112
+ // declare above). Outbound mail is still guarded by AgenticMail's
113
+ // own outbound guard (HIGH-severity sends held for owner approval)
114
+ // and the worker is sandboxed by Claude Code's permission system
115
+ // just like any other subagent.
104
116
  permissionMode: "bypassPermissions",
105
117
  abortController: abortSignal ? wrapSignal(abortSignal) : void 0
106
118
  };
@@ -168,12 +180,22 @@ 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.`,
174
190
  ``,
175
- `5. **If it's your turn \u2014 reply-all so the whole thread sees it.**`,
176
- ` reply_email({ uid: ${uid ?? "<uid>"}, replyAll: true, text: "...", _account: "${agent.name}" })`,
191
+ `5. **If it's your turn \u2014 do the actual work, THEN reply-all about it.**`,
192
+ ` You have full native tools: Read, Write, Edit, Bash, Glob, Grep, WebFetch,`,
193
+ ` WebSearch, NotebookEdit, etc. If the task is "implement X", write the file`,
194
+ ` with Write or Edit and verify with Bash \u2014 do NOT paste source code into an`,
195
+ ` email body and call it shipped. The thread is for COORDINATION ("done,`,
196
+ ` see ./foo.py, runs with \`python3 foo.py\`"); the filesystem is for`,
197
+ ` DELIVERABLES. Then:`,
198
+ ` reply_email({ uid: ${uid ?? "<uid>"}, replyAll: true, text: "...", _account: "${agent.name}" })`,
177
199
  ` Sign with your name. Be substantive but concise. If you are handing off`,
178
200
  ` to the next teammate, name them explicitly in your reply ("Orion \u2014 over to you, please \u2026").`,
179
201
  ``,
@@ -233,6 +255,15 @@ var Dispatcher = class {
233
255
  running = 0;
234
256
  waiters = [];
235
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;
236
267
  constructor(opts = {}) {
237
268
  this.cfg = resolveConfig(opts);
238
269
  this.maxConcurrent = opts.maxConcurrentWorkers ?? DEFAULT_MAX_CONCURRENT;
@@ -242,10 +273,63 @@ var Dispatcher = class {
242
273
  this.query = opts.querySdk ?? defaultQuery();
243
274
  this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
244
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;
245
279
  if (!this.cfg.masterKey) {
246
280
  throw new Error("Dispatcher requires AgenticMail master key. Run `agenticmail setup` first.");
247
281
  }
248
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
+ }
249
333
  async start() {
250
334
  this.log("info", `[dispatcher] starting (maxConcurrent=${this.maxConcurrent}, syncEvery=${this.syncIntervalMs}ms)`);
251
335
  await this.syncAccounts();
@@ -285,6 +369,13 @@ var Dispatcher = class {
285
369
  return;
286
370
  }
287
371
  if (ch) rememberBounded(ch.seenUids, event.uid);
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
+ }
288
379
  await this.spawnWorker(account, newMailPrompt(account, event), { kind: "new-mail", uid: event.uid });
289
380
  return;
290
381
  }
@@ -7,7 +7,7 @@ import {
7
7
  MANAGED_BY_MARKER,
8
8
  getAccountByName,
9
9
  resolveConfig
10
- } from "./chunk-YWSO3QOQ.js";
10
+ } from "./chunk-3D5VXS5Y.js";
11
11
 
12
12
  // src/status.ts
13
13
  import { existsSync, readFileSync, readdirSync } from "fs";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  install
3
- } from "./chunk-WP2ELPRM.js";
3
+ } from "./chunk-52LXPWO7.js";
4
4
  import {
5
5
  status
6
- } from "./chunk-UC63VEBP.js";
6
+ } from "./chunk-V3QMDNTR.js";
7
7
  import {
8
8
  uninstall
9
- } from "./chunk-XGBVWZ3M.js";
9
+ } from "./chunk-CQLUFM7N.js";
10
10
  import {
11
11
  AgenticMailApiError
12
- } from "./chunk-YWSO3QOQ.js";
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-WP2ELPRM.js";
4
+ } from "./chunk-52LXPWO7.js";
5
5
  import {
6
6
  status
7
- } from "./chunk-UC63VEBP.js";
7
+ } from "./chunk-V3QMDNTR.js";
8
8
  import {
9
9
  uninstall
10
- } from "./chunk-XGBVWZ3M.js";
10
+ } from "./chunk-CQLUFM7N.js";
11
11
  import "./chunk-US5FT2UB.js";
12
12
  import {
13
13
  AgenticMailApiError
14
- } from "./chunk-YWSO3QOQ.js";
14
+ } from "./chunk-3D5VXS5Y.js";
15
15
 
16
16
  // src/cli.ts
17
17
  var GREEN = (s) => `\x1B[32m${s}\x1B[0m`;
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Dispatcher
4
- } from "./chunk-3ZBSRXAK.js";
5
- import "./chunk-YWSO3QOQ.js";
4
+ } from "./chunk-FBO6F4IC.js";
5
+ import "./chunk-3D5VXS5Y.js";
6
6
 
7
7
  // src/dispatcher-bin.ts
8
8
  async function main() {
@@ -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. */
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Dispatcher
3
- } from "./chunk-3ZBSRXAK.js";
4
- import "./chunk-YWSO3QOQ.js";
3
+ } from "./chunk-FBO6F4IC.js";
4
+ import "./chunk-3D5VXS5Y.js";
5
5
  export {
6
6
  Dispatcher
7
7
  };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createIntegrationRoutes
3
- } from "./chunk-N43A7EQB.js";
4
- import "./chunk-WP2ELPRM.js";
5
- import "./chunk-UC63VEBP.js";
6
- import "./chunk-XGBVWZ3M.js";
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-YWSO3QOQ.js";
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-3ZBSRXAK.js";
4
+ } from "./chunk-FBO6F4IC.js";
5
5
  import {
6
6
  createIntegrationRoutes
7
- } from "./chunk-N43A7EQB.js";
7
+ } from "./chunk-WAUWKOHA.js";
8
8
  import {
9
9
  install
10
- } from "./chunk-WP2ELPRM.js";
10
+ } from "./chunk-52LXPWO7.js";
11
11
  import {
12
12
  status
13
- } from "./chunk-UC63VEBP.js";
13
+ } from "./chunk-V3QMDNTR.js";
14
14
  import {
15
15
  uninstall
16
- } from "./chunk-XGBVWZ3M.js";
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-YWSO3QOQ.js";
29
+ } from "./chunk-3D5VXS5Y.js";
30
30
  export {
31
31
  AgenticMailApiError,
32
32
  Dispatcher,
package/dist/install.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  install,
3
3
  selectExposableAgents
4
- } from "./chunk-WP2ELPRM.js";
4
+ } from "./chunk-52LXPWO7.js";
5
5
  import "./chunk-US5FT2UB.js";
6
- import "./chunk-YWSO3QOQ.js";
6
+ import "./chunk-3D5VXS5Y.js";
7
7
  export {
8
8
  install,
9
9
  selectExposableAgents
package/dist/status.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  status
3
- } from "./chunk-UC63VEBP.js";
3
+ } from "./chunk-V3QMDNTR.js";
4
4
  import "./chunk-US5FT2UB.js";
5
- import "./chunk-YWSO3QOQ.js";
5
+ import "./chunk-3D5VXS5Y.js";
6
6
  export {
7
7
  status
8
8
  };
package/dist/uninstall.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  uninstall
3
- } from "./chunk-XGBVWZ3M.js";
3
+ } from "./chunk-CQLUFM7N.js";
4
4
  import "./chunk-US5FT2UB.js";
5
- import "./chunk-YWSO3QOQ.js";
5
+ import "./chunk-3D5VXS5Y.js";
6
6
  export {
7
7
  uninstall
8
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/claudecode",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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",