@agenticmail/claudecode 0.2.0 → 0.2.1

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();
@@ -12565,6 +12573,13 @@ var Dispatcher = class {
12565
12573
  */
12566
12574
  wakeCoalesce = /* @__PURE__ */ new Map();
12567
12575
  wakeCoalesceMs;
12576
+ /** Wall-clock timestamp the dispatcher started. Surfaced via
12577
+ * process-heartbeat so check_activity can show uptime. */
12578
+ startedAtMs = Date.now();
12579
+ /** Periodic timer that posts a process-heartbeat to the API.
12580
+ * Without this, a hung dispatcher looks identical to "no
12581
+ * events to wake on" — the host has no liveness signal. */
12582
+ processHeartbeatTimer = null;
12568
12583
  constructor(opts = {}) {
12569
12584
  this.cfg = resolveConfig(opts);
12570
12585
  this.maxConcurrent = opts.maxConcurrentWorkers ?? DEFAULT_MAX_CONCURRENT;
@@ -12636,16 +12651,38 @@ var Dispatcher = class {
12636
12651
  }
12637
12652
  async start() {
12638
12653
  this.log("info", `[dispatcher] starting (maxConcurrent=${this.maxConcurrent}, syncEvery=${this.syncIntervalMs}ms)`);
12654
+ this.startedAtMs = Date.now();
12639
12655
  await this.syncAccounts();
12640
12656
  this.accountSyncTimer = setInterval(() => {
12641
12657
  this.syncAccounts().catch((err) => this.log("warn", `[dispatcher] account sync failed: ${err}`));
12642
12658
  }, this.syncIntervalMs);
12659
+ this.processHeartbeatTimer = setInterval(() => {
12660
+ this.postActivity("/dispatcher/process-heartbeat", {
12661
+ startedAtMs: this.startedAtMs,
12662
+ uptimeMs: Date.now() - this.startedAtMs,
12663
+ channels: this.channels.size,
12664
+ coalesceQueueSize: this.wakeCoalesce.size,
12665
+ running: this.running,
12666
+ maxConcurrent: this.maxConcurrent
12667
+ });
12668
+ }, 3e4);
12669
+ this.processHeartbeatTimer.unref?.();
12670
+ this.postActivity("/dispatcher/process-heartbeat", {
12671
+ startedAtMs: this.startedAtMs,
12672
+ uptimeMs: 0,
12673
+ channels: this.channels.size,
12674
+ coalesceQueueSize: 0,
12675
+ running: 0,
12676
+ maxConcurrent: this.maxConcurrent
12677
+ });
12643
12678
  void this.runSystemChannel();
12644
12679
  }
12645
12680
  async stop() {
12646
12681
  this.stopped = true;
12647
12682
  if (this.accountSyncTimer) clearInterval(this.accountSyncTimer);
12648
12683
  this.accountSyncTimer = null;
12684
+ if (this.processHeartbeatTimer) clearInterval(this.processHeartbeatTimer);
12685
+ this.processHeartbeatTimer = null;
12649
12686
  if (this.systemChannelController) {
12650
12687
  try {
12651
12688
  this.systemChannelController.abort();
@@ -12695,6 +12732,7 @@ var Dispatcher = class {
12695
12732
  if (ch) rememberBounded(ch.seenUids, event.uid);
12696
12733
  if (isThreadClosedSubject(subject)) {
12697
12734
  this.log("info", `[dispatcher] thread closed (subject="${subject ?? ""}") \u2014 skipping wake for "${account.name}" uid=${event.uid}`);
12735
+ this.postSkipped(account, event, "thread-closed", `subject contains a thread-close marker: "${subject ?? ""}"`);
12698
12736
  try {
12699
12737
  this.threadCache.delete(cacheThreadId);
12700
12738
  } catch {
@@ -12708,8 +12746,18 @@ var Dispatcher = class {
12708
12746
  const allowlist = extractWakeAllowlist(event);
12709
12747
  if (!isAgentOnWakeAllowlist(account.name, allowlist)) {
12710
12748
  this.log("info", `[dispatcher] wake allowlist excludes "${account.name}" (list=${JSON.stringify(allowlist)}) \u2014 mail delivered, no Claude turn`);
12749
+ this.postSkipped(account, event, "allowlist-excluded", `wake list ${JSON.stringify(allowlist)} did not include "${account.name}"`);
12711
12750
  return;
12712
12751
  }
12752
+ const wakeOnCc = account.wakeOnCc !== false;
12753
+ if (!wakeOnCc) {
12754
+ const wasOnTo = event.wasOnTo === true;
12755
+ if (!wasOnTo) {
12756
+ 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})`);
12757
+ this.postSkipped(account, event, "wake-on-cc", `"${account.name}" has wake_on_cc:false; not on To`);
12758
+ return;
12759
+ }
12760
+ }
12713
12761
  const threadId = threadIdFromSubject(subject);
12714
12762
  await this.scheduleCoalescedWake(account, event, threadId);
12715
12763
  return;
@@ -12985,27 +13033,37 @@ var Dispatcher = class {
12985
13033
  }
12986
13034
  const key = `${account.id}::${threadId}`;
12987
13035
  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?.();
13036
+ if (!existing) {
13037
+ const entry = {
13038
+ events: [],
13039
+ // empty first event already fired
13040
+ account,
13041
+ threadId,
13042
+ firstScheduledAt: this.now(),
13043
+ timer: setTimeout(() => this.fireCoalescedWake(key), this.wakeCoalesceMs)
13044
+ };
13045
+ entry.timer.unref?.();
13046
+ this.wakeCoalesce.set(key, entry);
13047
+ await this.fireWakeImmediately(account, event, threadId);
12998
13048
  return;
12999
13049
  }
13000
- const entry = {
13001
- events: [event],
13002
- account,
13050
+ clearTimeout(existing.timer);
13051
+ existing.events.push(event);
13052
+ this.postActivity("/dispatcher/worker-queued", {
13053
+ agentName: account.name,
13054
+ agentId: account.id,
13003
13055
  threadId,
13004
- firstScheduledAt: this.now(),
13005
- timer: setTimeout(() => this.fireCoalescedWake(key), this.wakeCoalesceMs)
13006
- };
13007
- entry.timer.unref?.();
13008
- this.wakeCoalesce.set(key, entry);
13056
+ queuedCount: existing.events.length,
13057
+ fireAtMs: this.now() + this.wakeCoalesceMs,
13058
+ reason: "coalescing subsequent burst events"
13059
+ });
13060
+ existing.timer = setTimeout(() => this.fireCoalescedWake(key), this.wakeCoalesceMs);
13061
+ existing.timer.unref?.();
13062
+ const elapsedFromFirst = this.now() - existing.firstScheduledAt;
13063
+ if (elapsedFromFirst > this.wakeCoalesceMs * 5) {
13064
+ clearTimeout(existing.timer);
13065
+ this.fireCoalescedWake(key);
13066
+ }
13009
13067
  }
13010
13068
  /**
13011
13069
  * Pre-0.9.0 fast path used when coalescing is disabled. Same
@@ -13016,6 +13074,7 @@ var Dispatcher = class {
13016
13074
  const verdict = this.chargeWake(account.id, threadId);
13017
13075
  if (!verdict.ok) {
13018
13076
  this.log("warn", `[dispatcher] wake-budget exhausted for "${account.name}" on thread "${threadId}" \u2014 dropped uid=${event.uid}`);
13077
+ this.postSkipped(account, event, "budget-exhausted", `wake budget exhausted for thread "${threadId}" (count=${verdict.count}, cap=${this.maxWakesPerThread})`);
13019
13078
  return;
13020
13079
  }
13021
13080
  await this.spawnWorker(account, newMailPrompt(account, event), {
@@ -13150,6 +13209,7 @@ var Dispatcher = class {
13150
13209
  }
13151
13210
  let turnCount = 0;
13152
13211
  let lastTool = "";
13212
+ let lastUsage;
13153
13213
  const observer = {
13154
13214
  onMessage: (tag, summary) => {
13155
13215
  writeLog(`${tag} ${summary}`);
@@ -13157,6 +13217,7 @@ var Dispatcher = class {
13157
13217
  lastTool = summary.split(" ")[0];
13158
13218
  turnCount++;
13159
13219
  }
13220
+ if (tag === "usage") lastUsage = summary;
13160
13221
  }
13161
13222
  };
13162
13223
  const heartbeatHandle = setInterval(() => {
@@ -13210,6 +13271,11 @@ var Dispatcher = class {
13210
13271
  agentName: account.name,
13211
13272
  ok,
13212
13273
  turnCount,
13274
+ // Context-budget telemetry: the SDK-reported usage line
13275
+ // (input/output/cache tokens + cost). Forwarded so
13276
+ // check_activity can show real cost per worker and the
13277
+ // cache+memory savings vs pre-0.9.0 become measurable.
13278
+ usage: lastUsage,
13213
13279
  resultPreview: typeof preview === "string" ? preview.slice(0, 240) : void 0
13214
13280
  });
13215
13281
  }
@@ -13240,6 +13306,32 @@ var Dispatcher = class {
13240
13306
  } catch {
13241
13307
  }
13242
13308
  }
13309
+ /**
13310
+ * Post a "skipped wake" notification with the reason the
13311
+ * dispatcher decided not to fire a Claude turn. Surfaced in
13312
+ * `check_activity` so the host can see the decision instead
13313
+ * of just observing silence ("did my mail land? did the
13314
+ * dispatcher skip it? is the dispatcher even alive?").
13315
+ *
13316
+ * Reasons cover every filter that drops a wake:
13317
+ * - thread-closed — subject had [FINAL]/[DONE]/[CLOSED]/[WRAP]
13318
+ * - allowlist-excluded — sender's `wake` list did not include the agent
13319
+ * - wake-on-cc — agent registered wake_on_cc:false and was on Cc
13320
+ * - dedup — duplicate UID seen recently
13321
+ * - rpc-suppress — RPC-notification mail right after a task event
13322
+ * - budget-exhausted — per-(agent, thread) wake budget hit the cap
13323
+ */
13324
+ postSkipped(account, event, reason, detail) {
13325
+ this.postActivity("/dispatcher/worker-skipped", {
13326
+ agentId: account.id,
13327
+ agentName: account.name,
13328
+ uid: event.uid,
13329
+ subject: extractSubject(event),
13330
+ from: extractFrom(event),
13331
+ reason,
13332
+ detail
13333
+ });
13334
+ }
13243
13335
  /** Build the env block we pass to the worker's MCP server child process. */
13244
13336
  async buildMcpEnv() {
13245
13337
  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-TWFORAWT.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-TWFORAWT.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-TWFORAWT.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.1",
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.1",
51
51
  "@anthropic-ai/claude-agent-sdk": "^0.2.140"
52
52
  },
53
53
  "peerDependencies": {