@agenticmail/claudecode 0.2.0 → 0.2.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.
@@ -12345,9 +12345,17 @@ async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCo
12345
12345
  }
12346
12346
  }
12347
12347
  }
12348
- if (m.type === "result" && typeof m.result === "string") {
12349
- collectedText.push(m.result);
12350
- if (observer) observer.onMessage("result", m.result.slice(0, 240).replace(/\s+/g, " ").trim());
12348
+ if (m.type === "result") {
12349
+ const r = m;
12350
+ if (typeof r.result === "string") {
12351
+ collectedText.push(r.result);
12352
+ if (observer) observer.onMessage("result", r.result.slice(0, 240).replace(/\s+/g, " ").trim());
12353
+ }
12354
+ if (r.usage && observer) {
12355
+ const u = r.usage;
12356
+ const summary = `in=${u.input_tokens ?? 0} out=${u.output_tokens ?? 0} cacheR=${u.cache_read_input_tokens ?? 0} cacheW=${u.cache_creation_input_tokens ?? 0}${typeof r.total_cost_usd === "number" ? ` cost=$${r.total_cost_usd.toFixed(4)}` : ""}`;
12357
+ observer.onMessage("usage", summary);
12358
+ }
12351
12359
  }
12352
12360
  }
12353
12361
  const text = collectedText.join("\n").trim();
@@ -12454,15 +12462,26 @@ function newMailPrompt(agent, event) {
12454
12462
  ` see ./foo.py, runs with \`python3 foo.py\`"); the filesystem is for`,
12455
12463
  ` DELIVERABLES. Then:`,
12456
12464
  ` reply_email({ uid: ${uid ?? "<uid>"}, replyAll: true, text: "...", _account: "${agent.name}" })`,
12457
- ` Sign with your name. Be substantive but concise. If you are handing off`,
12458
- ` to the next teammate, name them explicitly in your reply ("Orion \u2014 over to you, please \u2026").`,
12459
- ` **NAME the next actor in the \`wake\` parameter** so the dispatcher only`,
12460
- ` gives them a Claude turn \u2014 every other CC'd teammate still receives the`,
12461
- ` mail in their inbox but stays asleep, saving the project a lot of tokens.`,
12462
- ` Example: \`reply_email({ uid, replyAll: true, text: "Orion \u2014 your turn \u2026",`,
12463
- ` wake: ["orion"], _account: "${agent.name}" })\`. If nobody specific is`,
12464
- ` next (the work is complete and you're just signing off), pass \`wake: []\``,
12465
- ` to deliver silently with zero Claude turns spawned.`,
12465
+ ` Sign with your name. Be substantive but concise.`,
12466
+ ``,
12467
+ ` ## Reply addressing \u2014 CRITICAL for wake control`,
12468
+ ` reply_email({ replyAll: true }) automatically builds the right shape:`,
12469
+ ` the ORIGINAL SENDER ends up on To (so they wake by default),`,
12470
+ ` every other participant ends up on Cc (so they see it without`,
12471
+ ` waking). DO NOT pass a hand-rolled comma-separated address list`,
12472
+ ` via send_email \u2014 that puts every recipient on To and re-wakes`,
12473
+ ` the whole thread, defeating the wake gating. Trust replyAll.`,
12474
+ ``,
12475
+ ` If you want to wake someone OTHER than the original sender`,
12476
+ ` (e.g. you are handing off to a different next actor), name them`,
12477
+ ` explicitly in the reply body ("Orion \u2014 over to you, please\u2026")`,
12478
+ ` AND pass \`wake: ["orion"]\` so the dispatcher gives them a`,
12479
+ ` Claude turn instead. Example:`,
12480
+ ` reply_email({ uid, replyAll: true, text: "Orion \u2014 your turn \u2026",`,
12481
+ ` wake: ["orion"], _account: "${agent.name}" })`,
12482
+ ` If nobody specific is next (the work is complete and you're just`,
12483
+ ` signing off), pass \`wake: []\` to deliver silently \u2014 every`,
12484
+ ` participant still sees the reply, no Claude turn is spawned.`,
12466
12485
  ``,
12467
12486
  `7. **If you need additional help from a teammate not yet on the thread,**`,
12468
12487
  ` include them by CC'ing in your reply-all \u2014 DO NOT spin up a separate`,
@@ -12565,6 +12584,13 @@ var Dispatcher = class {
12565
12584
  */
12566
12585
  wakeCoalesce = /* @__PURE__ */ new Map();
12567
12586
  wakeCoalesceMs;
12587
+ /** Wall-clock timestamp the dispatcher started. Surfaced via
12588
+ * process-heartbeat so check_activity can show uptime. */
12589
+ startedAtMs = Date.now();
12590
+ /** Periodic timer that posts a process-heartbeat to the API.
12591
+ * Without this, a hung dispatcher looks identical to "no
12592
+ * events to wake on" — the host has no liveness signal. */
12593
+ processHeartbeatTimer = null;
12568
12594
  constructor(opts = {}) {
12569
12595
  this.cfg = resolveConfig(opts);
12570
12596
  this.maxConcurrent = opts.maxConcurrentWorkers ?? DEFAULT_MAX_CONCURRENT;
@@ -12636,16 +12662,38 @@ var Dispatcher = class {
12636
12662
  }
12637
12663
  async start() {
12638
12664
  this.log("info", `[dispatcher] starting (maxConcurrent=${this.maxConcurrent}, syncEvery=${this.syncIntervalMs}ms)`);
12665
+ this.startedAtMs = Date.now();
12639
12666
  await this.syncAccounts();
12640
12667
  this.accountSyncTimer = setInterval(() => {
12641
12668
  this.syncAccounts().catch((err) => this.log("warn", `[dispatcher] account sync failed: ${err}`));
12642
12669
  }, this.syncIntervalMs);
12670
+ this.processHeartbeatTimer = setInterval(() => {
12671
+ this.postActivity("/dispatcher/process-heartbeat", {
12672
+ startedAtMs: this.startedAtMs,
12673
+ uptimeMs: Date.now() - this.startedAtMs,
12674
+ channels: this.channels.size,
12675
+ coalesceQueueSize: this.wakeCoalesce.size,
12676
+ running: this.running,
12677
+ maxConcurrent: this.maxConcurrent
12678
+ });
12679
+ }, 3e4);
12680
+ this.processHeartbeatTimer.unref?.();
12681
+ this.postActivity("/dispatcher/process-heartbeat", {
12682
+ startedAtMs: this.startedAtMs,
12683
+ uptimeMs: 0,
12684
+ channels: this.channels.size,
12685
+ coalesceQueueSize: 0,
12686
+ running: 0,
12687
+ maxConcurrent: this.maxConcurrent
12688
+ });
12643
12689
  void this.runSystemChannel();
12644
12690
  }
12645
12691
  async stop() {
12646
12692
  this.stopped = true;
12647
12693
  if (this.accountSyncTimer) clearInterval(this.accountSyncTimer);
12648
12694
  this.accountSyncTimer = null;
12695
+ if (this.processHeartbeatTimer) clearInterval(this.processHeartbeatTimer);
12696
+ this.processHeartbeatTimer = null;
12649
12697
  if (this.systemChannelController) {
12650
12698
  try {
12651
12699
  this.systemChannelController.abort();
@@ -12695,6 +12743,7 @@ var Dispatcher = class {
12695
12743
  if (ch) rememberBounded(ch.seenUids, event.uid);
12696
12744
  if (isThreadClosedSubject(subject)) {
12697
12745
  this.log("info", `[dispatcher] thread closed (subject="${subject ?? ""}") \u2014 skipping wake for "${account.name}" uid=${event.uid}`);
12746
+ this.postSkipped(account, event, "thread-closed", `subject contains a thread-close marker: "${subject ?? ""}"`);
12698
12747
  try {
12699
12748
  this.threadCache.delete(cacheThreadId);
12700
12749
  } catch {
@@ -12708,8 +12757,18 @@ var Dispatcher = class {
12708
12757
  const allowlist = extractWakeAllowlist(event);
12709
12758
  if (!isAgentOnWakeAllowlist(account.name, allowlist)) {
12710
12759
  this.log("info", `[dispatcher] wake allowlist excludes "${account.name}" (list=${JSON.stringify(allowlist)}) \u2014 mail delivered, no Claude turn`);
12760
+ this.postSkipped(account, event, "allowlist-excluded", `wake list ${JSON.stringify(allowlist)} did not include "${account.name}"`);
12711
12761
  return;
12712
12762
  }
12763
+ const wakeOnCc = account.wakeOnCc !== false;
12764
+ if (!wakeOnCc) {
12765
+ const wasOnTo = event.wasOnTo === true;
12766
+ if (!wasOnTo) {
12767
+ this.log("info", `[dispatcher] "${account.name}" has wake_on_cc:false and was not on To \u2014 mail delivered, no Claude turn (uid=${event.uid})`);
12768
+ this.postSkipped(account, event, "wake-on-cc", `"${account.name}" has wake_on_cc:false; not on To`);
12769
+ return;
12770
+ }
12771
+ }
12713
12772
  const threadId = threadIdFromSubject(subject);
12714
12773
  await this.scheduleCoalescedWake(account, event, threadId);
12715
12774
  return;
@@ -12985,27 +13044,37 @@ var Dispatcher = class {
12985
13044
  }
12986
13045
  const key = `${account.id}::${threadId}`;
12987
13046
  const existing = this.wakeCoalesce.get(key);
12988
- if (existing) {
12989
- clearTimeout(existing.timer);
12990
- existing.events.push(event);
12991
- existing.timer = setTimeout(() => this.fireCoalescedWake(key), this.wakeCoalesceMs);
12992
- const elapsedFromFirst = this.now() - existing.firstScheduledAt;
12993
- if (elapsedFromFirst > this.wakeCoalesceMs * 5) {
12994
- clearTimeout(existing.timer);
12995
- this.fireCoalescedWake(key);
12996
- }
12997
- existing.timer.unref?.();
13047
+ if (!existing) {
13048
+ const entry = {
13049
+ events: [],
13050
+ // empty first event already fired
13051
+ account,
13052
+ threadId,
13053
+ firstScheduledAt: this.now(),
13054
+ timer: setTimeout(() => this.fireCoalescedWake(key), this.wakeCoalesceMs)
13055
+ };
13056
+ entry.timer.unref?.();
13057
+ this.wakeCoalesce.set(key, entry);
13058
+ await this.fireWakeImmediately(account, event, threadId);
12998
13059
  return;
12999
13060
  }
13000
- const entry = {
13001
- events: [event],
13002
- account,
13061
+ clearTimeout(existing.timer);
13062
+ existing.events.push(event);
13063
+ this.postActivity("/dispatcher/worker-queued", {
13064
+ agentName: account.name,
13065
+ agentId: account.id,
13003
13066
  threadId,
13004
- firstScheduledAt: this.now(),
13005
- timer: setTimeout(() => this.fireCoalescedWake(key), this.wakeCoalesceMs)
13006
- };
13007
- entry.timer.unref?.();
13008
- this.wakeCoalesce.set(key, entry);
13067
+ queuedCount: existing.events.length,
13068
+ fireAtMs: this.now() + this.wakeCoalesceMs,
13069
+ reason: "coalescing subsequent burst events"
13070
+ });
13071
+ existing.timer = setTimeout(() => this.fireCoalescedWake(key), this.wakeCoalesceMs);
13072
+ existing.timer.unref?.();
13073
+ const elapsedFromFirst = this.now() - existing.firstScheduledAt;
13074
+ if (elapsedFromFirst > this.wakeCoalesceMs * 5) {
13075
+ clearTimeout(existing.timer);
13076
+ this.fireCoalescedWake(key);
13077
+ }
13009
13078
  }
13010
13079
  /**
13011
13080
  * Pre-0.9.0 fast path used when coalescing is disabled. Same
@@ -13016,6 +13085,7 @@ var Dispatcher = class {
13016
13085
  const verdict = this.chargeWake(account.id, threadId);
13017
13086
  if (!verdict.ok) {
13018
13087
  this.log("warn", `[dispatcher] wake-budget exhausted for "${account.name}" on thread "${threadId}" \u2014 dropped uid=${event.uid}`);
13088
+ this.postSkipped(account, event, "budget-exhausted", `wake budget exhausted for thread "${threadId}" (count=${verdict.count}, cap=${this.maxWakesPerThread})`);
13019
13089
  return;
13020
13090
  }
13021
13091
  await this.spawnWorker(account, newMailPrompt(account, event), {
@@ -13150,6 +13220,7 @@ var Dispatcher = class {
13150
13220
  }
13151
13221
  let turnCount = 0;
13152
13222
  let lastTool = "";
13223
+ let lastUsage;
13153
13224
  const observer = {
13154
13225
  onMessage: (tag, summary) => {
13155
13226
  writeLog(`${tag} ${summary}`);
@@ -13157,6 +13228,7 @@ var Dispatcher = class {
13157
13228
  lastTool = summary.split(" ")[0];
13158
13229
  turnCount++;
13159
13230
  }
13231
+ if (tag === "usage") lastUsage = summary;
13160
13232
  }
13161
13233
  };
13162
13234
  const heartbeatHandle = setInterval(() => {
@@ -13210,6 +13282,11 @@ var Dispatcher = class {
13210
13282
  agentName: account.name,
13211
13283
  ok,
13212
13284
  turnCount,
13285
+ // Context-budget telemetry: the SDK-reported usage line
13286
+ // (input/output/cache tokens + cost). Forwarded so
13287
+ // check_activity can show real cost per worker and the
13288
+ // cache+memory savings vs pre-0.9.0 become measurable.
13289
+ usage: lastUsage,
13213
13290
  resultPreview: typeof preview === "string" ? preview.slice(0, 240) : void 0
13214
13291
  });
13215
13292
  }
@@ -13240,6 +13317,32 @@ var Dispatcher = class {
13240
13317
  } catch {
13241
13318
  }
13242
13319
  }
13320
+ /**
13321
+ * Post a "skipped wake" notification with the reason the
13322
+ * dispatcher decided not to fire a Claude turn. Surfaced in
13323
+ * `check_activity` so the host can see the decision instead
13324
+ * of just observing silence ("did my mail land? did the
13325
+ * dispatcher skip it? is the dispatcher even alive?").
13326
+ *
13327
+ * Reasons cover every filter that drops a wake:
13328
+ * - thread-closed — subject had [FINAL]/[DONE]/[CLOSED]/[WRAP]
13329
+ * - allowlist-excluded — sender's `wake` list did not include the agent
13330
+ * - wake-on-cc — agent registered wake_on_cc:false and was on Cc
13331
+ * - dedup — duplicate UID seen recently
13332
+ * - rpc-suppress — RPC-notification mail right after a task event
13333
+ * - budget-exhausted — per-(agent, thread) wake budget hit the cap
13334
+ */
13335
+ postSkipped(account, event, reason, detail) {
13336
+ this.postActivity("/dispatcher/worker-skipped", {
13337
+ agentId: account.id,
13338
+ agentName: account.name,
13339
+ uid: event.uid,
13340
+ subject: extractSubject(event),
13341
+ from: extractFrom(event),
13342
+ reason,
13343
+ detail
13344
+ });
13345
+ }
13243
13346
  /** Build the env block we pass to the worker's MCP server child process. */
13244
13347
  async buildMcpEnv() {
13245
13348
  return {
@@ -12,6 +12,10 @@ interface AgenticMailAccount {
12
12
  apiKey: string;
13
13
  role?: string;
14
14
  metadata?: Record<string, unknown>;
15
+ /** Per-agent wake preference. When false, the dispatcher
16
+ * drops wakes where this agent was on Cc/Bcc but not To,
17
+ * regardless of the sender's wake list. Defaults to true. */
18
+ wakeOnCc?: boolean;
15
19
  }
16
20
  /** Resolved configuration for everything the package does. */
17
21
  interface ClaudeCodeIntegrationConfig {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Dispatcher
4
- } from "./chunk-UCI2HLHM.js";
4
+ } from "./chunk-T3J2WZMM.js";
5
5
  import "./chunk-DDJNA5HP.js";
6
6
  import "./chunk-B276KPVO.js";
7
7
  import "./chunk-RB5MGRT3.js";
@@ -1,4 +1,4 @@
1
- import { R as ResolveConfigOptions, A as AgenticMailAccount } from './config-CjEDSvVy.js';
1
+ import { R as ResolveConfigOptions, A as AgenticMailAccount } from './config-CXW3gXFC.js';
2
2
 
3
3
  /**
4
4
  * AgenticMail → Claude Code event dispatcher.
@@ -68,6 +68,11 @@ interface SSEEvent {
68
68
  * Claude turn. When absent, every CC'd recipient wakes (v0.8.x default).
69
69
  */
70
70
  wakeAllowlist?: string[];
71
+ /** Per-recipient "was I on the To field?" flag emitted by the
72
+ * API in 0.9.1+. Pairs with the recipient's `wake_on_cc`
73
+ * preference: when the agent registered with wake_on_cc:false
74
+ * and `wasOnTo !== true`, the dispatcher drops the wake. */
75
+ wasOnTo?: boolean;
71
76
  [key: string]: unknown;
72
77
  }
73
78
  interface DispatcherOptions extends ResolveConfigOptions {
@@ -191,6 +196,13 @@ declare class Dispatcher {
191
196
  */
192
197
  private wakeCoalesce;
193
198
  private wakeCoalesceMs;
199
+ /** Wall-clock timestamp the dispatcher started. Surfaced via
200
+ * process-heartbeat so check_activity can show uptime. */
201
+ private startedAtMs;
202
+ /** Periodic timer that posts a process-heartbeat to the API.
203
+ * Without this, a hung dispatcher looks identical to "no
204
+ * events to wake on" — the host has no liveness signal. */
205
+ private processHeartbeatTimer;
194
206
  constructor(opts?: DispatcherOptions);
195
207
  /**
196
208
  * Charge one wake against the (agent, thread) budget. Returns true
@@ -316,6 +328,22 @@ declare class Dispatcher {
316
328
  * load-bearing state.
317
329
  */
318
330
  private postActivity;
331
+ /**
332
+ * Post a "skipped wake" notification with the reason the
333
+ * dispatcher decided not to fire a Claude turn. Surfaced in
334
+ * `check_activity` so the host can see the decision instead
335
+ * of just observing silence ("did my mail land? did the
336
+ * dispatcher skip it? is the dispatcher even alive?").
337
+ *
338
+ * Reasons cover every filter that drops a wake:
339
+ * - thread-closed — subject had [FINAL]/[DONE]/[CLOSED]/[WRAP]
340
+ * - allowlist-excluded — sender's `wake` list did not include the agent
341
+ * - wake-on-cc — agent registered wake_on_cc:false and was on Cc
342
+ * - dedup — duplicate UID seen recently
343
+ * - rpc-suppress — RPC-notification mail right after a task event
344
+ * - budget-exhausted — per-(agent, thread) wake budget hit the cap
345
+ */
346
+ private postSkipped;
319
347
  /** Build the env block we pass to the worker's MCP server child process. */
320
348
  private buildMcpEnv;
321
349
  private acquireSlot;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Dispatcher
3
- } from "./chunk-UCI2HLHM.js";
3
+ } from "./chunk-T3J2WZMM.js";
4
4
  import "./chunk-DDJNA5HP.js";
5
5
  import "./chunk-B276KPVO.js";
6
6
  import "./chunk-RB5MGRT3.js";
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { install } from './install.js';
2
2
  export { UninstallOptions, uninstall } from './uninstall.js';
3
3
  export { status } from './status.js';
4
- import { A as AgenticMailAccount } from './config-CjEDSvVy.js';
5
- export { C as ClaudeCodeIntegrationConfig, I as InstallResult, a as InstallStatus, R as ResolveConfigOptions, U as UninstallResult, r as resolveConfig } from './config-CjEDSvVy.js';
4
+ import { A as AgenticMailAccount } from './config-CXW3gXFC.js';
5
+ export { C as ClaudeCodeIntegrationConfig, I as InstallResult, a as InstallStatus, R as ResolveConfigOptions, U as UninstallResult, r as resolveConfig } from './config-CXW3gXFC.js';
6
6
  export { createIntegrationRoutes } from './http-routes.js';
7
7
  export { Dispatcher, DispatcherOptions, QueryFn } from './dispatcher.js';
8
8
  import 'express';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Dispatcher,
3
3
  loadPersonaForAgent
4
- } from "./chunk-UCI2HLHM.js";
4
+ } from "./chunk-T3J2WZMM.js";
5
5
  import "./chunk-DDJNA5HP.js";
6
6
  import "./chunk-B276KPVO.js";
7
7
  import "./chunk-RB5MGRT3.js";
package/dist/install.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as ResolveConfigOptions, I as InstallResult, A as AgenticMailAccount, C as ClaudeCodeIntegrationConfig } from './config-CjEDSvVy.js';
1
+ import { R as ResolveConfigOptions, I as InstallResult, A as AgenticMailAccount, C as ClaudeCodeIntegrationConfig } from './config-CXW3gXFC.js';
2
2
 
3
3
  /**
4
4
  * Install AgenticMail into Claude Code.
package/dist/status.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as ResolveConfigOptions, a as InstallStatus } from './config-CjEDSvVy.js';
1
+ import { R as ResolveConfigOptions, a as InstallStatus } from './config-CXW3gXFC.js';
2
2
 
3
3
  /**
4
4
  * Inspect the current install state of @agenticmail/claudecode.
@@ -1,4 +1,4 @@
1
- import { R as ResolveConfigOptions, U as UninstallResult } from './config-CjEDSvVy.js';
1
+ import { R as ResolveConfigOptions, U as UninstallResult } from './config-CXW3gXFC.js';
2
2
 
3
3
  /**
4
4
  * Uninstall AgenticMail from Claude Code.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/claudecode",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
@@ -47,7 +47,7 @@
47
47
  "prepublishOnly": "npm run build"
48
48
  },
49
49
  "dependencies": {
50
- "@agenticmail/mcp": "^0.9.0",
50
+ "@agenticmail/mcp": "^0.9.2",
51
51
  "@anthropic-ai/claude-agent-sdk": "^0.2.140"
52
52
  },
53
53
  "peerDependencies": {