@agenticmail/claudecode 0.1.6 → 0.1.7

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.
@@ -59,7 +59,7 @@ function rememberBounded(set, item) {
59
59
  }
60
60
  }
61
61
  var DEFAULT_MAX_CONCURRENT = 10;
62
- var DEFAULT_SYNC_INTERVAL_MS = 5e3;
62
+ var DEFAULT_SYNC_INTERVAL_MS = 3e4;
63
63
  var DEFAULT_RECONNECT_BASE_MS = 2e3;
64
64
  var DEFAULT_RECONNECT_MAX_MS = 6e4;
65
65
  var TASK_MAIL_SUPPRESS_WINDOW_MS = 3e4;
@@ -229,6 +229,7 @@ var Dispatcher = class {
229
229
  channels = /* @__PURE__ */ new Map();
230
230
  // keyed by account.id
231
231
  accountSyncTimer = null;
232
+ systemChannelController = null;
232
233
  running = 0;
233
234
  waiters = [];
234
235
  stopped = false;
@@ -251,11 +252,19 @@ var Dispatcher = class {
251
252
  this.accountSyncTimer = setInterval(() => {
252
253
  this.syncAccounts().catch((err) => this.log("warn", `[dispatcher] account sync failed: ${err}`));
253
254
  }, this.syncIntervalMs);
255
+ void this.runSystemChannel();
254
256
  }
255
257
  async stop() {
256
258
  this.stopped = true;
257
259
  if (this.accountSyncTimer) clearInterval(this.accountSyncTimer);
258
260
  this.accountSyncTimer = null;
261
+ if (this.systemChannelController) {
262
+ try {
263
+ this.systemChannelController.abort();
264
+ } catch {
265
+ }
266
+ this.systemChannelController = null;
267
+ }
259
268
  for (const ch of this.channels.values()) {
260
269
  ch.stopping = true;
261
270
  ch.controller?.abort();
@@ -352,6 +361,118 @@ var Dispatcher = class {
352
361
  void this.runChannel(ch);
353
362
  }
354
363
  }
364
+ /**
365
+ * Subscribe to the API's master-scoped system events SSE.
366
+ *
367
+ * Pushes from /system/events arrive as JSON-per-frame just like the
368
+ * per-account stream:
369
+ * { type: "connected" }
370
+ * { type: "account_created", account: { id, name, email, apiKey, ... } }
371
+ * { type: "account_deleted", accountId, name }
372
+ *
373
+ * On `account_created` we eagerly open a per-account SSE channel using
374
+ * the apiKey carried in the event payload — no extra round trip, the
375
+ * channel is live within milliseconds of the POST /accounts response.
376
+ *
377
+ * Reconnect with the same exponential backoff scheme as per-account
378
+ * channels. If the API is older and doesn't expose /system/events
379
+ * (404), we log once and stop trying — polling-only fallback still
380
+ * works.
381
+ */
382
+ async runSystemChannel() {
383
+ let backoff = this.reconnectBaseMs;
384
+ let giveUp = false;
385
+ while (!this.stopped && !giveUp) {
386
+ this.systemChannelController = new AbortController();
387
+ try {
388
+ const url = `${this.cfg.apiUrl.replace(/\/$/, "")}/api/agenticmail/system/events`;
389
+ const res = await this.fetchImpl(url, {
390
+ headers: {
391
+ "Authorization": `Bearer ${this.cfg.masterKey}`,
392
+ "Accept": "text/event-stream"
393
+ },
394
+ signal: this.systemChannelController.signal
395
+ });
396
+ if (res.status === 404) {
397
+ this.log("warn", "[dispatcher] /system/events not available on this API \u2014 falling back to polling-only account discovery (please upgrade @agenticmail/api to >=0.7.3)");
398
+ giveUp = true;
399
+ break;
400
+ }
401
+ if (!res.ok || !res.body) {
402
+ throw new Error(`system/events HTTP ${res.status}`);
403
+ }
404
+ backoff = this.reconnectBaseMs;
405
+ const reader = res.body.getReader();
406
+ const decoder = new TextDecoder();
407
+ let buffer = "";
408
+ while (!this.stopped) {
409
+ const { value, done } = await reader.read();
410
+ if (done) break;
411
+ buffer += decoder.decode(value, { stream: true });
412
+ let boundary;
413
+ while ((boundary = buffer.indexOf("\n\n")) !== -1) {
414
+ const frame = buffer.slice(0, boundary);
415
+ buffer = buffer.slice(boundary + 2);
416
+ for (const line of frame.split("\n")) {
417
+ if (!line.startsWith("data: ")) continue;
418
+ try {
419
+ const event = JSON.parse(line.slice(6));
420
+ this.handleSystemEvent(event);
421
+ } catch {
422
+ }
423
+ }
424
+ }
425
+ }
426
+ } catch (err) {
427
+ if (this.stopped) break;
428
+ this.log("warn", `[dispatcher] system-events stream error: ${err.message}; reconnecting in ${backoff}ms`);
429
+ }
430
+ if (this.stopped || giveUp) break;
431
+ await sleep(backoff);
432
+ backoff = Math.min(backoff * 2, this.reconnectMaxMs);
433
+ }
434
+ }
435
+ /** Apply an account-lifecycle event from /system/events. */
436
+ handleSystemEvent(event) {
437
+ const type = typeof event.type === "string" ? event.type : "";
438
+ if (type === "account_created" && event.account && typeof event.account === "object") {
439
+ const account = event.account;
440
+ if (!account.id || !account.name || !account.apiKey) {
441
+ this.log("warn", "[dispatcher] account_created event missing required fields; ignoring");
442
+ return;
443
+ }
444
+ if (!this.shouldWatch(account)) {
445
+ this.log("info", `[dispatcher] account_created "${account.name}" \u2014 skipping (bridge/role excluded)`);
446
+ return;
447
+ }
448
+ if (this.channels.has(account.id)) return;
449
+ const ch = {
450
+ account,
451
+ controller: null,
452
+ stopping: false,
453
+ backoffMs: this.reconnectBaseMs,
454
+ seenUids: /* @__PURE__ */ new Set(),
455
+ seenTaskIds: /* @__PURE__ */ new Set(),
456
+ suppressTaskMailUntilMs: 0
457
+ };
458
+ this.channels.set(account.id, ch);
459
+ this.log("info", `[dispatcher] account_created "${account.name}" (${account.email}) \u2014 opening SSE channel immediately`);
460
+ void this.runChannel(ch);
461
+ return;
462
+ }
463
+ if (type === "account_deleted" && typeof event.accountId === "string") {
464
+ const ch = this.channels.get(event.accountId);
465
+ if (!ch) return;
466
+ ch.stopping = true;
467
+ try {
468
+ ch.controller?.abort();
469
+ } catch {
470
+ }
471
+ this.channels.delete(event.accountId);
472
+ this.log("info", `[dispatcher] account_deleted "${ch.account.name}" \u2014 closed SSE channel`);
473
+ return;
474
+ }
475
+ }
355
476
  /** Watch one account's SSE stream forever; reconnect with backoff on drop. */
356
477
  async runChannel(ch) {
357
478
  while (!ch.stopping && !this.stopped) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Dispatcher
4
- } from "./chunk-5M43V6CB.js";
4
+ } from "./chunk-3ZBSRXAK.js";
5
5
  import "./chunk-YWSO3QOQ.js";
6
6
 
7
7
  // src/dispatcher-bin.ts
@@ -103,6 +103,7 @@ declare class Dispatcher {
103
103
  private log;
104
104
  private channels;
105
105
  private accountSyncTimer;
106
+ private systemChannelController;
106
107
  private running;
107
108
  private waiters;
108
109
  private stopped;
@@ -131,6 +132,27 @@ declare class Dispatcher {
131
132
  private shouldWatch;
132
133
  /** Re-fetch /accounts; open SSE for new ones, close for vanished ones. */
133
134
  private syncAccounts;
135
+ /**
136
+ * Subscribe to the API's master-scoped system events SSE.
137
+ *
138
+ * Pushes from /system/events arrive as JSON-per-frame just like the
139
+ * per-account stream:
140
+ * { type: "connected" }
141
+ * { type: "account_created", account: { id, name, email, apiKey, ... } }
142
+ * { type: "account_deleted", accountId, name }
143
+ *
144
+ * On `account_created` we eagerly open a per-account SSE channel using
145
+ * the apiKey carried in the event payload — no extra round trip, the
146
+ * channel is live within milliseconds of the POST /accounts response.
147
+ *
148
+ * Reconnect with the same exponential backoff scheme as per-account
149
+ * channels. If the API is older and doesn't expose /system/events
150
+ * (404), we log once and stop trying — polling-only fallback still
151
+ * works.
152
+ */
153
+ private runSystemChannel;
154
+ /** Apply an account-lifecycle event from /system/events. */
155
+ private handleSystemEvent;
134
156
  /** Watch one account's SSE stream forever; reconnect with backoff on drop. */
135
157
  private runChannel;
136
158
  /** Single SSE attach. Returns when the stream closes for any reason. */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Dispatcher
3
- } from "./chunk-5M43V6CB.js";
3
+ } from "./chunk-3ZBSRXAK.js";
4
4
  import "./chunk-YWSO3QOQ.js";
5
5
  export {
6
6
  Dispatcher
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Dispatcher,
3
3
  loadPersonaForAgent
4
- } from "./chunk-5M43V6CB.js";
4
+ } from "./chunk-3ZBSRXAK.js";
5
5
  import {
6
6
  createIntegrationRoutes
7
7
  } from "./chunk-N43A7EQB.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/claudecode",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",