@agenticmail/core 0.5.59 → 0.6.0

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/dist/index.cjs CHANGED
@@ -3653,6 +3653,40 @@ var import_nodemailer2 = __toESM(require("nodemailer"), 1);
3653
3653
  var import_mail_composer2 = __toESM(require("nodemailer/lib/mail-composer/index.js"), 1);
3654
3654
  var import_imapflow3 = require("imapflow");
3655
3655
  var import_mailparser2 = require("mailparser");
3656
+ function formatPollError(err) {
3657
+ if (!err) return "unknown error (no error object)";
3658
+ if (typeof err !== "object") return String(err);
3659
+ const e = err;
3660
+ const parts = [];
3661
+ const head = (e.message ?? String(err)).toString().trim();
3662
+ if (head) parts.push(head);
3663
+ if (e.code && e.code !== head) parts.push(`code=${e.code}`);
3664
+ if (typeof e.errno === "number") parts.push(`errno=${e.errno}`);
3665
+ if (e.syscall) parts.push(`syscall=${e.syscall}`);
3666
+ if (e.hostname) parts.push(`host=${e.hostname}`);
3667
+ if (typeof e.port === "number") parts.push(`port=${e.port}`);
3668
+ if (e.responseText && e.responseText !== head) {
3669
+ parts.push(`response=${truncate(String(e.responseText), 240)}`);
3670
+ } else if (e.response && e.response !== head) {
3671
+ parts.push(`response=${truncate(String(e.response), 240)}`);
3672
+ }
3673
+ if (e.command) parts.push(`command=${e.command}`);
3674
+ if (typeof e.exitCode === "number") parts.push(`exit=${e.exitCode}`);
3675
+ else if (e.code && /^\d+$/.test(String(e.code))) parts.push(`exit=${e.code}`);
3676
+ if (e.signal) parts.push(`signal=${e.signal}`);
3677
+ const stderr = (e.stderr ?? "").toString().trim();
3678
+ if (stderr) parts.push(`stderr=${truncate(stderr, 240)}`);
3679
+ const stdout = (e.stdout ?? "").toString().trim();
3680
+ if (stdout && !stderr) parts.push(`stdout=${truncate(stdout, 240)}`);
3681
+ if (parts.length === 1 && /^command failed$/i.test(head)) {
3682
+ return `${head} (no further detail available \u2014 wrapping error did not carry stderr/code/response)`;
3683
+ }
3684
+ return parts.join(" | ");
3685
+ }
3686
+ function truncate(s, max) {
3687
+ if (s.length <= max) return s;
3688
+ return s.slice(0, max - 1).trimEnd() + "\u2026";
3689
+ }
3656
3690
  var RelayGateway = class {
3657
3691
  smtpTransport = null;
3658
3692
  pollTimer = null;
@@ -3827,7 +3861,7 @@ var RelayGateway = class {
3827
3861
  this.consecutiveFailures = 0;
3828
3862
  } catch (err) {
3829
3863
  this.consecutiveFailures++;
3830
- const msg = err instanceof Error ? err.message : String(err);
3864
+ const msg = formatPollError(err);
3831
3865
  console.error(`[RelayGateway] Poll failed (attempt ${this.consecutiveFailures}): ${msg}`);
3832
3866
  if (this.consecutiveFailures >= 5 && this.consecutiveFailures % 5 === 0) {
3833
3867
  console.error(`[RelayGateway] ${this.consecutiveFailures} consecutive failures \u2014 check IMAP credentials and connectivity (${this.config?.imapHost}:${this.config?.imapPort})`);
@@ -6085,20 +6119,23 @@ var GatewayManager = class {
6085
6119
  }
6086
6120
  /**
6087
6121
  * Resume gateway from saved config (e.g., after server restart).
6122
+ *
6123
+ * Issue #31 — On a Docker container restart the API can come up
6124
+ * before Stalwart / Gmail IMAP / DNS is reachable, so the very first
6125
+ * setup() can fail with a transient network error. Previously that
6126
+ * single failure was logged and never retried, leaving polling
6127
+ * permanently dead until someone noticed and manually revived the
6128
+ * relay. We now schedule background retries with exponential backoff
6129
+ * (5s, 10s, 20s, 40s, 60s cap, indefinite) so the relay
6130
+ * self-recovers as soon as the dependency is reachable again.
6088
6131
  */
6089
6132
  async resume() {
6090
6133
  if (this.config.mode === "relay" && this.config.relay) {
6091
6134
  try {
6092
- await this.relay.setup(this.config.relay);
6093
- const savedUid = this.loadLastSeenUid();
6094
- if (savedUid > 0) {
6095
- this.relay.setLastSeenUid(savedUid);
6096
- console.log(`[GatewayManager] Restored lastSeenUid=${savedUid} from database`);
6097
- }
6098
- this.relay.onUidAdvance = (uid) => this.saveLastSeenUid(uid);
6099
- await this.relay.startPolling();
6135
+ await this._resumeRelayOnce();
6100
6136
  } catch (err) {
6101
- console.error("[GatewayManager] Failed to resume relay:", err);
6137
+ console.error("[GatewayManager] Initial relay resume failed; scheduling retries:", formatPollError(err));
6138
+ this._scheduleRelayResumeRetry();
6102
6139
  }
6103
6140
  }
6104
6141
  if (this.smsManager && this.accountManager) {
@@ -6125,6 +6162,42 @@ var GatewayManager = class {
6125
6162
  }
6126
6163
  }
6127
6164
  }
6165
+ // ─── Issue #31 helpers — resume retry with backoff ───
6166
+ _resumeRetryTimer = null;
6167
+ _resumeRetryAttempt = 0;
6168
+ async _resumeRelayOnce() {
6169
+ if (!this.config.relay) throw new Error("No relay config to resume");
6170
+ await this.relay.setup(this.config.relay);
6171
+ const savedUid = this.loadLastSeenUid();
6172
+ if (savedUid > 0) {
6173
+ this.relay.setLastSeenUid(savedUid);
6174
+ console.log(`[GatewayManager] Restored lastSeenUid=${savedUid} from database`);
6175
+ }
6176
+ this.relay.onUidAdvance = (uid) => this.saveLastSeenUid(uid);
6177
+ await this.relay.startPolling();
6178
+ if (this._resumeRetryAttempt > 0) {
6179
+ console.log(`[GatewayManager] Relay polling resumed after ${this._resumeRetryAttempt} retry attempt${this._resumeRetryAttempt !== 1 ? "s" : ""}`);
6180
+ }
6181
+ this._resumeRetryAttempt = 0;
6182
+ }
6183
+ _scheduleRelayResumeRetry() {
6184
+ if (this._resumeRetryTimer) return;
6185
+ this._resumeRetryAttempt++;
6186
+ const base = Math.min(5e3 * Math.pow(2, this._resumeRetryAttempt - 1), 6e4);
6187
+ const jitter = base * (0.8 + Math.random() * 0.4);
6188
+ const delay = Math.round(jitter);
6189
+ console.log(`[GatewayManager] Will retry relay resume in ${(delay / 1e3).toFixed(1)}s (attempt ${this._resumeRetryAttempt + 1})`);
6190
+ this._resumeRetryTimer = setTimeout(async () => {
6191
+ this._resumeRetryTimer = null;
6192
+ if (this.config.mode !== "relay" || !this.config.relay) return;
6193
+ try {
6194
+ await this._resumeRelayOnce();
6195
+ } catch (err) {
6196
+ console.error(`[GatewayManager] Relay resume retry ${this._resumeRetryAttempt} failed:`, formatPollError(err));
6197
+ this._scheduleRelayResumeRetry();
6198
+ }
6199
+ }, delay);
6200
+ }
6128
6201
  // --- Persistence ---
6129
6202
  loadConfig() {
6130
6203
  const row = this.db.prepare("SELECT * FROM gateway_config WHERE id = ?").get("default");
package/dist/index.d.cts CHANGED
@@ -1244,8 +1244,21 @@ declare class GatewayManager {
1244
1244
  shutdown(): Promise<void>;
1245
1245
  /**
1246
1246
  * Resume gateway from saved config (e.g., after server restart).
1247
+ *
1248
+ * Issue #31 — On a Docker container restart the API can come up
1249
+ * before Stalwart / Gmail IMAP / DNS is reachable, so the very first
1250
+ * setup() can fail with a transient network error. Previously that
1251
+ * single failure was logged and never retried, leaving polling
1252
+ * permanently dead until someone noticed and manually revived the
1253
+ * relay. We now schedule background retries with exponential backoff
1254
+ * (5s, 10s, 20s, 40s, 60s cap, indefinite) so the relay
1255
+ * self-recovers as soon as the dependency is reachable again.
1247
1256
  */
1248
1257
  resume(): Promise<void>;
1258
+ private _resumeRetryTimer;
1259
+ private _resumeRetryAttempt;
1260
+ private _resumeRelayOnce;
1261
+ private _scheduleRelayResumeRetry;
1249
1262
  private loadConfig;
1250
1263
  private saveConfig;
1251
1264
  private saveLastSeenUid;
package/dist/index.d.ts CHANGED
@@ -1244,8 +1244,21 @@ declare class GatewayManager {
1244
1244
  shutdown(): Promise<void>;
1245
1245
  /**
1246
1246
  * Resume gateway from saved config (e.g., after server restart).
1247
+ *
1248
+ * Issue #31 — On a Docker container restart the API can come up
1249
+ * before Stalwart / Gmail IMAP / DNS is reachable, so the very first
1250
+ * setup() can fail with a transient network error. Previously that
1251
+ * single failure was logged and never retried, leaving polling
1252
+ * permanently dead until someone noticed and manually revived the
1253
+ * relay. We now schedule background retries with exponential backoff
1254
+ * (5s, 10s, 20s, 40s, 60s cap, indefinite) so the relay
1255
+ * self-recovers as soon as the dependency is reachable again.
1247
1256
  */
1248
1257
  resume(): Promise<void>;
1258
+ private _resumeRetryTimer;
1259
+ private _resumeRetryAttempt;
1260
+ private _resumeRelayOnce;
1261
+ private _scheduleRelayResumeRetry;
1249
1262
  private loadConfig;
1250
1263
  private saveConfig;
1251
1264
  private saveLastSeenUid;
package/dist/index.js CHANGED
@@ -2896,6 +2896,40 @@ import nodemailer2 from "nodemailer";
2896
2896
  import MailComposer2 from "nodemailer/lib/mail-composer/index.js";
2897
2897
  import { ImapFlow as ImapFlow3 } from "imapflow";
2898
2898
  import { simpleParser as simpleParser2 } from "mailparser";
2899
+ function formatPollError(err) {
2900
+ if (!err) return "unknown error (no error object)";
2901
+ if (typeof err !== "object") return String(err);
2902
+ const e = err;
2903
+ const parts = [];
2904
+ const head = (e.message ?? String(err)).toString().trim();
2905
+ if (head) parts.push(head);
2906
+ if (e.code && e.code !== head) parts.push(`code=${e.code}`);
2907
+ if (typeof e.errno === "number") parts.push(`errno=${e.errno}`);
2908
+ if (e.syscall) parts.push(`syscall=${e.syscall}`);
2909
+ if (e.hostname) parts.push(`host=${e.hostname}`);
2910
+ if (typeof e.port === "number") parts.push(`port=${e.port}`);
2911
+ if (e.responseText && e.responseText !== head) {
2912
+ parts.push(`response=${truncate(String(e.responseText), 240)}`);
2913
+ } else if (e.response && e.response !== head) {
2914
+ parts.push(`response=${truncate(String(e.response), 240)}`);
2915
+ }
2916
+ if (e.command) parts.push(`command=${e.command}`);
2917
+ if (typeof e.exitCode === "number") parts.push(`exit=${e.exitCode}`);
2918
+ else if (e.code && /^\d+$/.test(String(e.code))) parts.push(`exit=${e.code}`);
2919
+ if (e.signal) parts.push(`signal=${e.signal}`);
2920
+ const stderr = (e.stderr ?? "").toString().trim();
2921
+ if (stderr) parts.push(`stderr=${truncate(stderr, 240)}`);
2922
+ const stdout = (e.stdout ?? "").toString().trim();
2923
+ if (stdout && !stderr) parts.push(`stdout=${truncate(stdout, 240)}`);
2924
+ if (parts.length === 1 && /^command failed$/i.test(head)) {
2925
+ return `${head} (no further detail available \u2014 wrapping error did not carry stderr/code/response)`;
2926
+ }
2927
+ return parts.join(" | ");
2928
+ }
2929
+ function truncate(s, max) {
2930
+ if (s.length <= max) return s;
2931
+ return s.slice(0, max - 1).trimEnd() + "\u2026";
2932
+ }
2899
2933
  var RelayGateway = class {
2900
2934
  smtpTransport = null;
2901
2935
  pollTimer = null;
@@ -3070,7 +3104,7 @@ var RelayGateway = class {
3070
3104
  this.consecutiveFailures = 0;
3071
3105
  } catch (err) {
3072
3106
  this.consecutiveFailures++;
3073
- const msg = err instanceof Error ? err.message : String(err);
3107
+ const msg = formatPollError(err);
3074
3108
  console.error(`[RelayGateway] Poll failed (attempt ${this.consecutiveFailures}): ${msg}`);
3075
3109
  if (this.consecutiveFailures >= 5 && this.consecutiveFailures % 5 === 0) {
3076
3110
  console.error(`[RelayGateway] ${this.consecutiveFailures} consecutive failures \u2014 check IMAP credentials and connectivity (${this.config?.imapHost}:${this.config?.imapPort})`);
@@ -5327,20 +5361,23 @@ var GatewayManager = class {
5327
5361
  }
5328
5362
  /**
5329
5363
  * Resume gateway from saved config (e.g., after server restart).
5364
+ *
5365
+ * Issue #31 — On a Docker container restart the API can come up
5366
+ * before Stalwart / Gmail IMAP / DNS is reachable, so the very first
5367
+ * setup() can fail with a transient network error. Previously that
5368
+ * single failure was logged and never retried, leaving polling
5369
+ * permanently dead until someone noticed and manually revived the
5370
+ * relay. We now schedule background retries with exponential backoff
5371
+ * (5s, 10s, 20s, 40s, 60s cap, indefinite) so the relay
5372
+ * self-recovers as soon as the dependency is reachable again.
5330
5373
  */
5331
5374
  async resume() {
5332
5375
  if (this.config.mode === "relay" && this.config.relay) {
5333
5376
  try {
5334
- await this.relay.setup(this.config.relay);
5335
- const savedUid = this.loadLastSeenUid();
5336
- if (savedUid > 0) {
5337
- this.relay.setLastSeenUid(savedUid);
5338
- console.log(`[GatewayManager] Restored lastSeenUid=${savedUid} from database`);
5339
- }
5340
- this.relay.onUidAdvance = (uid) => this.saveLastSeenUid(uid);
5341
- await this.relay.startPolling();
5377
+ await this._resumeRelayOnce();
5342
5378
  } catch (err) {
5343
- console.error("[GatewayManager] Failed to resume relay:", err);
5379
+ console.error("[GatewayManager] Initial relay resume failed; scheduling retries:", formatPollError(err));
5380
+ this._scheduleRelayResumeRetry();
5344
5381
  }
5345
5382
  }
5346
5383
  if (this.smsManager && this.accountManager) {
@@ -5367,6 +5404,42 @@ var GatewayManager = class {
5367
5404
  }
5368
5405
  }
5369
5406
  }
5407
+ // ─── Issue #31 helpers — resume retry with backoff ───
5408
+ _resumeRetryTimer = null;
5409
+ _resumeRetryAttempt = 0;
5410
+ async _resumeRelayOnce() {
5411
+ if (!this.config.relay) throw new Error("No relay config to resume");
5412
+ await this.relay.setup(this.config.relay);
5413
+ const savedUid = this.loadLastSeenUid();
5414
+ if (savedUid > 0) {
5415
+ this.relay.setLastSeenUid(savedUid);
5416
+ console.log(`[GatewayManager] Restored lastSeenUid=${savedUid} from database`);
5417
+ }
5418
+ this.relay.onUidAdvance = (uid) => this.saveLastSeenUid(uid);
5419
+ await this.relay.startPolling();
5420
+ if (this._resumeRetryAttempt > 0) {
5421
+ console.log(`[GatewayManager] Relay polling resumed after ${this._resumeRetryAttempt} retry attempt${this._resumeRetryAttempt !== 1 ? "s" : ""}`);
5422
+ }
5423
+ this._resumeRetryAttempt = 0;
5424
+ }
5425
+ _scheduleRelayResumeRetry() {
5426
+ if (this._resumeRetryTimer) return;
5427
+ this._resumeRetryAttempt++;
5428
+ const base = Math.min(5e3 * Math.pow(2, this._resumeRetryAttempt - 1), 6e4);
5429
+ const jitter = base * (0.8 + Math.random() * 0.4);
5430
+ const delay = Math.round(jitter);
5431
+ console.log(`[GatewayManager] Will retry relay resume in ${(delay / 1e3).toFixed(1)}s (attempt ${this._resumeRetryAttempt + 1})`);
5432
+ this._resumeRetryTimer = setTimeout(async () => {
5433
+ this._resumeRetryTimer = null;
5434
+ if (this.config.mode !== "relay" || !this.config.relay) return;
5435
+ try {
5436
+ await this._resumeRelayOnce();
5437
+ } catch (err) {
5438
+ console.error(`[GatewayManager] Relay resume retry ${this._resumeRetryAttempt} failed:`, formatPollError(err));
5439
+ this._scheduleRelayResumeRetry();
5440
+ }
5441
+ }, delay);
5442
+ }
5370
5443
  // --- Persistence ---
5371
5444
  loadConfig() {
5372
5445
  const row = this.db.prepare("SELECT * FROM gateway_config WHERE id = ?").get("default");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.5.59",
3
+ "version": "0.6.0",
4
4
  "description": "Core SDK for AgenticMail — email, SMS, and phone number access for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",