@agenticmail/core 0.9.8 → 0.9.10

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
@@ -713,6 +713,7 @@ __export(index_exports, {
713
713
  AgentDeletionService: () => AgentDeletionService,
714
714
  AgentMemoryStore: () => AgentMemoryStore,
715
715
  AgenticMailClient: () => AgenticMailClient,
716
+ BRIDGE_OPERATOR_LIVE_WINDOW_MS: () => BRIDGE_OPERATOR_LIVE_WINDOW_MS,
716
717
  CloudflareClient: () => CloudflareClient,
717
718
  DEFAULT_AGENT_NAME: () => DEFAULT_AGENT_NAME,
718
719
  DEFAULT_AGENT_ROLE: () => DEFAULT_AGENT_ROLE,
@@ -743,10 +744,14 @@ __export(index_exports, {
743
744
  UnsafeApiUrlError: () => UnsafeApiUrlError,
744
745
  WARNING_THRESHOLD: () => WARNING_THRESHOLD,
745
746
  assertWithinBase: () => assertWithinBase,
747
+ bridgeWakeErrorMessage: () => bridgeWakeErrorMessage,
748
+ bridgeWakeLastSeenAgeMs: () => bridgeWakeLastSeenAgeMs,
746
749
  buildApiUrl: () => buildApiUrl,
747
750
  buildInboundSecurityAdvisory: () => buildInboundSecurityAdvisory,
748
751
  classifyEmailRoute: () => classifyEmailRoute,
752
+ classifyResumeError: () => classifyResumeError,
749
753
  closeDatabase: () => closeDatabase,
754
+ composeBridgeWakePrompt: () => composeBridgeWakePrompt,
750
755
  createTestDatabase: () => createTestDatabase,
751
756
  debug: () => debug,
752
757
  debugWarn: () => debugWarn,
@@ -756,11 +761,13 @@ __export(index_exports, {
756
761
  forgetHostSession: () => forgetHostSession,
757
762
  getDatabase: () => getDatabase,
758
763
  getOperatorEmail: () => getOperatorEmail,
764
+ getSmsProvider: () => getSmsProvider,
759
765
  hostSessionStoragePath: () => hostSessionStoragePath,
760
766
  isInternalEmail: () => isInternalEmail,
761
767
  isSessionFresh: () => isSessionFresh,
762
768
  isValidPhoneNumber: () => isValidPhoneNumber,
763
769
  loadHostSession: () => loadHostSession,
770
+ mapProviderSmsStatus: () => mapProviderSmsStatus,
764
771
  normalizeAddress: () => normalizeAddress,
765
772
  normalizePhoneNumber: () => normalizePhoneNumber,
766
773
  normalizeSubject: () => normalizeSubject,
@@ -770,6 +777,7 @@ __export(index_exports, {
770
777
  recordToolCall: () => recordToolCall,
771
778
  redactObject: () => redactObject,
772
779
  redactSecret: () => redactSecret,
780
+ redactSmsConfig: () => redactSmsConfig,
773
781
  resolveConfig: () => resolveConfig,
774
782
  safeJoin: () => safeJoin,
775
783
  sanitizeEmail: () => sanitizeEmail,
@@ -779,6 +787,7 @@ __export(index_exports, {
779
787
  scoreEmail: () => scoreEmail,
780
788
  setOperatorEmail: () => setOperatorEmail,
781
789
  setTelemetryVersion: () => setTelemetryVersion,
790
+ shouldSkipBridgeWakeForLiveOperator: () => shouldSkipBridgeWakeForLiveOperator,
782
791
  startRelayBridge: () => startRelayBridge,
783
792
  threadIdFor: () => threadIdFor,
784
793
  tryJoin: () => tryJoin,
@@ -802,8 +811,7 @@ var MailSender = class {
802
811
  pass: options.password
803
812
  },
804
813
  tls: {
805
- rejectUnauthorized: false
806
- // Local dev — no TLS
814
+ rejectUnauthorized: options.tlsRejectUnauthorized ?? true
807
815
  },
808
816
  connectionTimeout: 1e4,
809
817
  // 10s to establish TCP connection
@@ -3879,7 +3887,47 @@ var DomainManager = class {
3879
3887
 
3880
3888
  // src/gateway/manager.ts
3881
3889
  var import_node_path4 = require("path");
3890
+
3891
+ // src/crypto/secrets.ts
3882
3892
  var import_node_crypto2 = require("crypto");
3893
+ function deriveKey(key, salt) {
3894
+ return (0, import_node_crypto2.scryptSync)(key, salt, 32, { N: 16384, r: 8, p: 1 });
3895
+ }
3896
+ function encryptSecret(plaintext, key) {
3897
+ const salt = (0, import_node_crypto2.randomBytes)(16);
3898
+ const derivedKey = deriveKey(key, salt);
3899
+ const iv = (0, import_node_crypto2.randomBytes)(12);
3900
+ const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", derivedKey, iv);
3901
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3902
+ const authTag = cipher.getAuthTag();
3903
+ return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
3904
+ }
3905
+ function decryptSecret(value, key) {
3906
+ if (value.startsWith("enc2:")) {
3907
+ const parts = value.split(":");
3908
+ if (parts.length !== 5) return value;
3909
+ const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
3910
+ const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
3911
+ const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
3912
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
3913
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
3914
+ }
3915
+ if (value.startsWith("enc:")) {
3916
+ const parts = value.split(":");
3917
+ if (parts.length !== 4) return value;
3918
+ const [, ivHex, authTagHex, ciphertextHex] = parts;
3919
+ const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest();
3920
+ const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
3921
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
3922
+ return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
3923
+ }
3924
+ return value;
3925
+ }
3926
+ function isEncryptedSecret(value) {
3927
+ return typeof value === "string" && (value.startsWith("enc2:") || value.startsWith("enc:"));
3928
+ }
3929
+
3930
+ // src/gateway/manager.ts
3883
3931
  var import_nodemailer3 = __toESM(require("nodemailer"), 1);
3884
3932
 
3885
3933
  // src/debug.ts
@@ -5157,6 +5205,130 @@ var import_mail_composer3 = __toESM(require("nodemailer/lib/mail-composer/index.
5157
5205
  init_spam_filter();
5158
5206
 
5159
5207
  // src/sms/manager.ts
5208
+ function asString(value) {
5209
+ return typeof value === "string" ? value.trim() : "";
5210
+ }
5211
+ function defaultApiUrl(config) {
5212
+ const url = (config.apiUrl || "https://api.46elks.com/a1").replace(/\/+$/, "");
5213
+ if (!/^https:\/\//i.test(url)) {
5214
+ throw new Error("46elks apiUrl must use https:// \u2014 refusing to send credentials over a non-TLS connection");
5215
+ }
5216
+ return url;
5217
+ }
5218
+ function basicAuth(username, password) {
5219
+ return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
5220
+ }
5221
+ function redactSmsConfig(config) {
5222
+ return {
5223
+ ...config,
5224
+ forwardingPassword: config.forwardingPassword ? "***" : void 0,
5225
+ password: config.password ? "***" : void 0,
5226
+ webhookSecret: config.webhookSecret ? "***" : void 0
5227
+ };
5228
+ }
5229
+ var GoogleVoiceSmsProvider = class {
5230
+ id = "google_voice";
5231
+ async sendSms(config, input) {
5232
+ const to = normalizePhoneNumber(input.to);
5233
+ const from = normalizePhoneNumber(config.phoneNumber);
5234
+ if (!to) throw new Error("Invalid recipient phone number");
5235
+ if (!from) throw new Error("Invalid configured Google Voice phone number");
5236
+ return {
5237
+ provider: this.id,
5238
+ status: "pending",
5239
+ from,
5240
+ to,
5241
+ body: input.body,
5242
+ raw: {
5243
+ delivery: "manual_google_voice_web",
5244
+ url: "https://voice.google.com"
5245
+ }
5246
+ };
5247
+ }
5248
+ parseInboundSms() {
5249
+ return null;
5250
+ }
5251
+ };
5252
+ var FortySixElksSmsProvider = class {
5253
+ id = "46elks";
5254
+ async sendSms(config, input) {
5255
+ const username = asString(config.username);
5256
+ const password = asString(config.password);
5257
+ if (!username || !password) {
5258
+ throw new Error("46elks username and password are required");
5259
+ }
5260
+ const to = normalizePhoneNumber(input.to);
5261
+ const from = normalizePhoneNumber(config.phoneNumber);
5262
+ if (!to) throw new Error("Invalid recipient phone number");
5263
+ if (!from) throw new Error("Invalid configured 46elks phone number");
5264
+ const form = new URLSearchParams();
5265
+ form.set("to", to);
5266
+ form.set("from", from);
5267
+ form.set("message", input.body);
5268
+ if (input.dryRun) form.set("dryrun", "yes");
5269
+ const response = await fetch(`${defaultApiUrl(config)}/sms`, {
5270
+ method: "POST",
5271
+ headers: {
5272
+ "Authorization": `Basic ${basicAuth(username, password)}`,
5273
+ "Content-Type": "application/x-www-form-urlencoded"
5274
+ },
5275
+ body: form,
5276
+ signal: AbortSignal.timeout(15e3)
5277
+ });
5278
+ const text = await response.text();
5279
+ let raw = text;
5280
+ try {
5281
+ raw = JSON.parse(text);
5282
+ } catch {
5283
+ }
5284
+ if (!response.ok) {
5285
+ const message = typeof raw === "object" && raw && ("message" in raw || "error" in raw) ? String(raw.message ?? raw.error) : text.slice(0, 200);
5286
+ throw new Error(`46elks SMS failed (${response.status}): ${message}`);
5287
+ }
5288
+ const providerId = typeof raw === "object" && raw && "id" in raw ? String(raw.id) : void 0;
5289
+ const providerStatus = typeof raw === "object" && raw && "status" in raw ? String(raw.status) : "sent";
5290
+ return {
5291
+ provider: this.id,
5292
+ id: providerId,
5293
+ status: providerStatus,
5294
+ from,
5295
+ to,
5296
+ body: input.body,
5297
+ raw
5298
+ };
5299
+ }
5300
+ parseInboundSms(payload) {
5301
+ const direction = asString(payload.direction).toLowerCase();
5302
+ if (direction && direction !== "incoming") return null;
5303
+ const from = normalizePhoneNumber(asString(payload.from));
5304
+ const to = normalizePhoneNumber(asString(payload.to));
5305
+ const body = asString(payload.message);
5306
+ if (!from || !to || !body) return null;
5307
+ return {
5308
+ provider: this.id,
5309
+ id: asString(payload.id) || void 0,
5310
+ from,
5311
+ to,
5312
+ body,
5313
+ timestamp: asString(payload.created) || (/* @__PURE__ */ new Date()).toISOString(),
5314
+ raw: payload
5315
+ };
5316
+ }
5317
+ };
5318
+ var PROVIDERS = {
5319
+ google_voice: new GoogleVoiceSmsProvider(),
5320
+ "46elks": new FortySixElksSmsProvider()
5321
+ };
5322
+ function getSmsProvider(provider) {
5323
+ return PROVIDERS[provider];
5324
+ }
5325
+ function mapProviderSmsStatus(status) {
5326
+ const normalized = status.toLowerCase();
5327
+ if (normalized === "delivered") return "delivered";
5328
+ if (normalized === "failed" || normalized === "error") return "failed";
5329
+ if (normalized === "created" || normalized === "queued" || normalized === "sent") return "sent";
5330
+ return "sent";
5331
+ }
5160
5332
  function normalizePhoneNumber(raw) {
5161
5333
  const cleaned = raw.replace(/[^+\d]/g, "");
5162
5334
  if (!cleaned) return null;
@@ -5263,12 +5435,48 @@ function extractVerificationCode(smsBody) {
5263
5435
  }
5264
5436
  return null;
5265
5437
  }
5438
+ var SMS_SECRET_FIELDS = ["password", "webhookSecret", "forwardingPassword"];
5266
5439
  var SmsManager = class {
5267
- constructor(db2) {
5440
+ /**
5441
+ * Optional master key used to encrypt SMS credentials at rest (same
5442
+ * AES-256-GCM scheme GatewayManager uses for relay/domain secrets).
5443
+ * When absent (e.g. tests, or a deployment with no master key) configs
5444
+ * are stored as-is and reads tolerate plaintext — so upgrades and
5445
+ * downgrades both stay safe.
5446
+ */
5447
+ constructor(db2, encryptionKey) {
5268
5448
  this.db = db2;
5449
+ this.encryptionKey = encryptionKey;
5269
5450
  this.ensureTable();
5270
5451
  }
5271
5452
  initialized = false;
5453
+ /** Encrypt the credential fields of an SMS config before persisting. */
5454
+ encryptConfig(config) {
5455
+ if (!this.encryptionKey) return config;
5456
+ const out = { ...config };
5457
+ for (const field of SMS_SECRET_FIELDS) {
5458
+ const value = out[field];
5459
+ if (typeof value === "string" && value && !isEncryptedSecret(value)) {
5460
+ out[field] = encryptSecret(value, this.encryptionKey);
5461
+ }
5462
+ }
5463
+ return out;
5464
+ }
5465
+ /** Decrypt the credential fields of an SMS config after loading. */
5466
+ decryptConfig(config) {
5467
+ if (!this.encryptionKey) return config;
5468
+ const out = { ...config };
5469
+ for (const field of SMS_SECRET_FIELDS) {
5470
+ const value = out[field];
5471
+ if (typeof value === "string" && isEncryptedSecret(value)) {
5472
+ try {
5473
+ out[field] = decryptSecret(value, this.encryptionKey);
5474
+ } catch {
5475
+ }
5476
+ }
5477
+ }
5478
+ return out;
5479
+ }
5272
5480
  ensureTable() {
5273
5481
  if (this.initialized) return;
5274
5482
  try {
@@ -5301,18 +5509,19 @@ var SmsManager = class {
5301
5509
  this.initialized = true;
5302
5510
  }
5303
5511
  }
5304
- /** Get SMS config from agent metadata */
5512
+ /** Get SMS config from agent metadata (credential fields decrypted). */
5305
5513
  getSmsConfig(agentId) {
5306
5514
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
5307
5515
  if (!row) return null;
5308
5516
  try {
5309
5517
  const meta = JSON.parse(row.metadata || "{}");
5310
- return meta.sms && meta.sms.enabled !== void 0 ? meta.sms : null;
5518
+ if (!meta.sms || meta.sms.enabled === void 0) return null;
5519
+ return this.decryptConfig(meta.sms);
5311
5520
  } catch {
5312
5521
  return null;
5313
5522
  }
5314
5523
  }
5315
- /** Save SMS config to agent metadata */
5524
+ /** Save SMS config to agent metadata (credential fields encrypted). */
5316
5525
  saveSmsConfig(agentId, config) {
5317
5526
  const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
5318
5527
  if (!row) throw new Error(`Agent ${agentId} not found`);
@@ -5322,7 +5531,7 @@ var SmsManager = class {
5322
5531
  } catch {
5323
5532
  meta = {};
5324
5533
  }
5325
- meta.sms = config;
5534
+ meta.sms = this.encryptConfig(config);
5326
5535
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
5327
5536
  }
5328
5537
  /**
@@ -5365,27 +5574,50 @@ var SmsManager = class {
5365
5574
  delete meta.sms;
5366
5575
  this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
5367
5576
  }
5368
- /** Record an inbound SMS (parsed from email) */
5369
- recordInbound(agentId, parsed) {
5577
+ /** Find the agent whose SMS config owns a phone number. */
5578
+ findAgentBySmsNumber(phoneNumber, provider) {
5579
+ const normalized = normalizePhoneNumber(phoneNumber);
5580
+ if (!normalized) return null;
5581
+ const rows = this.db.prepare("SELECT id, metadata FROM agents").all();
5582
+ for (const row of rows) {
5583
+ try {
5584
+ const meta = JSON.parse(row.metadata || "{}");
5585
+ const cfg = meta.sms;
5586
+ if (!cfg?.enabled) continue;
5587
+ if (provider && cfg.provider !== provider) continue;
5588
+ if (normalizePhoneNumber(cfg.phoneNumber) === normalized) {
5589
+ return { agentId: row.id, config: this.decryptConfig(cfg) };
5590
+ }
5591
+ } catch {
5592
+ }
5593
+ }
5594
+ return null;
5595
+ }
5596
+ /** Record an inbound SMS (parsed from email or provider webhook) */
5597
+ recordInbound(agentId, parsed, metadata) {
5370
5598
  const id = `sms_in_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5371
5599
  const createdAt = parsed.timestamp || (/* @__PURE__ */ new Date()).toISOString();
5372
5600
  this.db.prepare(
5373
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
5374
- ).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt);
5375
- return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt };
5601
+ "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
5602
+ ).run(id, agentId, "inbound", parsed.from, parsed.body, "received", createdAt, JSON.stringify(metadata ?? {}));
5603
+ return { id, agentId, direction: "inbound", phoneNumber: parsed.from, body: parsed.body, status: "received", createdAt, metadata };
5376
5604
  }
5377
5605
  /** Record an outbound SMS attempt */
5378
- recordOutbound(agentId, phoneNumber, body, status = "pending") {
5606
+ recordOutbound(agentId, phoneNumber, body, status = "pending", metadata) {
5379
5607
  const id = `sms_out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5380
5608
  const now = (/* @__PURE__ */ new Date()).toISOString();
5381
5609
  const normalized = normalizePhoneNumber(phoneNumber) || phoneNumber;
5382
5610
  this.db.prepare(
5383
- "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
5384
- ).run(id, agentId, "outbound", normalized, body, status, now);
5385
- return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now };
5386
- }
5387
- /** Update SMS status */
5388
- updateStatus(id, status) {
5611
+ "INSERT INTO sms_messages (id, agent_id, direction, phone_number, body, status, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
5612
+ ).run(id, agentId, "outbound", normalized, body, status, now, JSON.stringify(metadata ?? {}));
5613
+ return { id, agentId, direction: "outbound", phoneNumber: normalized, body, status, createdAt: now, metadata };
5614
+ }
5615
+ /** Update SMS status and optional provider metadata */
5616
+ updateStatus(id, status, metadata) {
5617
+ if (metadata) {
5618
+ this.db.prepare("UPDATE sms_messages SET status = ?, metadata = ? WHERE id = ?").run(status, JSON.stringify(metadata), id);
5619
+ return;
5620
+ }
5389
5621
  this.db.prepare("UPDATE sms_messages SET status = ? WHERE id = ?").run(status, id);
5390
5622
  }
5391
5623
  /** List SMS messages for an agent */
@@ -5585,39 +5817,6 @@ var SmsPoller = class {
5585
5817
  };
5586
5818
 
5587
5819
  // src/gateway/manager.ts
5588
- function deriveKey(key, salt) {
5589
- return (0, import_node_crypto2.scryptSync)(key, salt, 32, { N: 16384, r: 8, p: 1 });
5590
- }
5591
- function encryptSecret(plaintext, key) {
5592
- const salt = (0, import_node_crypto2.randomBytes)(16);
5593
- const derivedKey = deriveKey(key, salt);
5594
- const iv = (0, import_node_crypto2.randomBytes)(12);
5595
- const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", derivedKey, iv);
5596
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
5597
- const authTag = cipher.getAuthTag();
5598
- return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
5599
- }
5600
- function decryptSecret(value, key) {
5601
- if (value.startsWith("enc2:")) {
5602
- const parts = value.split(":");
5603
- if (parts.length !== 5) return value;
5604
- const [, saltHex, ivHex, authTagHex, ciphertextHex] = parts;
5605
- const derivedKey = deriveKey(key, Buffer.from(saltHex, "hex"));
5606
- const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", derivedKey, Buffer.from(ivHex, "hex"));
5607
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
5608
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
5609
- }
5610
- if (value.startsWith("enc:")) {
5611
- const parts = value.split(":");
5612
- if (parts.length !== 4) return value;
5613
- const [, ivHex, authTagHex, ciphertextHex] = parts;
5614
- const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest();
5615
- const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", keyHash, Buffer.from(ivHex, "hex"));
5616
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
5617
- return Buffer.concat([decipher.update(Buffer.from(ciphertextHex, "hex")), decipher.final()]).toString("utf8");
5618
- }
5619
- return value;
5620
- }
5621
5820
  var GatewayManager = class {
5622
5821
  constructor(options) {
5623
5822
  this.options = options;
@@ -7039,6 +7238,69 @@ function hostSessionStoragePath() {
7039
7238
  return storagePath();
7040
7239
  }
7041
7240
 
7241
+ // src/host-bridge.ts
7242
+ var BRIDGE_OPERATOR_LIVE_WINDOW_MS = 3e4;
7243
+ var DEFAULT_EXPIRED_MARKERS = [
7244
+ "session not found",
7245
+ "invalid session",
7246
+ "session expired",
7247
+ "no such session",
7248
+ "unknown session",
7249
+ "thread not found",
7250
+ "invalid thread",
7251
+ "thread expired",
7252
+ "no such thread",
7253
+ "unknown thread"
7254
+ ];
7255
+ var DEFAULT_SDK_MISSING_MARKERS = [
7256
+ "cannot find module",
7257
+ "could not be found",
7258
+ "command not found"
7259
+ ];
7260
+ function bridgeWakeErrorMessage(err) {
7261
+ return err?.message ?? String(err);
7262
+ }
7263
+ function classifyResumeError(err, options = {}) {
7264
+ const msg = bridgeWakeErrorMessage(err).toLowerCase();
7265
+ const expiredMarkers = options.expiredMarkers ?? DEFAULT_EXPIRED_MARKERS;
7266
+ const sdkMissingMarkers = options.sdkMissingMarkers ?? DEFAULT_SDK_MISSING_MARKERS;
7267
+ if (expiredMarkers.some((marker) => msg.includes(marker))) return "session-expired";
7268
+ if (sdkMissingMarkers.some((marker) => msg.includes(marker))) return "sdk-missing";
7269
+ return "other";
7270
+ }
7271
+ function bridgeWakeLastSeenAgeMs(session, nowMs = Date.now()) {
7272
+ if (!session) return null;
7273
+ return nowMs - session.lastSeenMs;
7274
+ }
7275
+ function shouldSkipBridgeWakeForLiveOperator(session, nowMs = Date.now(), liveWindowMs = BRIDGE_OPERATOR_LIVE_WINDOW_MS) {
7276
+ const ageMs = bridgeWakeLastSeenAgeMs(session, nowMs);
7277
+ return ageMs !== null && ageMs < liveWindowMs;
7278
+ }
7279
+ function composeBridgeWakePrompt(args) {
7280
+ const subject = args.subject ?? "(no subject)";
7281
+ const from = args.from ?? "unknown";
7282
+ const preview = (args.preview ?? "").slice(0, 600);
7283
+ return [
7284
+ `\u{1F380} Bridge mail arrived \u2014 headless wake.`,
7285
+ "",
7286
+ `You are being resumed against your last session because new mail landed in your bridge inbox (${args.bridgeName}@localhost) and you weren't actively at the keyboard.`,
7287
+ "",
7288
+ `Trigger:`,
7289
+ ` UID: ${args.uid}`,
7290
+ ` From: ${from}`,
7291
+ ` Subject: ${subject}`,
7292
+ ` Preview: ${preview}`,
7293
+ "",
7294
+ `Read it with mcp__agenticmail__read_email({ uid: ${args.uid} }) and decide:`,
7295
+ ` \xB7 Does it need a reply from YOU (the operator's session)? Reply via mcp__agenticmail__reply_email.`,
7296
+ ` \xB7 Does it need a teammate to act? Forward / re-route by replying with wake: ["<teammate>"].`,
7297
+ ` \xB7 Is it [NEEDS OPERATOR] / [BLOCKED]? Then it's actually for the human \u2014 mark it unread, and the operator will see it on their next keystroke.`,
7298
+ ` \xB7 Is it FYI noise? mark_read and exit.`,
7299
+ "",
7300
+ `Keep this turn SHORT. You're being resumed to handle ONE piece of mail, not to continue the prior conversation.`
7301
+ ].join("\n");
7302
+ }
7303
+
7042
7304
  // src/util/safe-url.ts
7043
7305
  var UnsafeApiUrlError = class extends Error {
7044
7306
  constructor(raw, reason) {
@@ -8716,6 +8978,7 @@ function parse(raw) {
8716
8978
  AgentDeletionService,
8717
8979
  AgentMemoryStore,
8718
8980
  AgenticMailClient,
8981
+ BRIDGE_OPERATOR_LIVE_WINDOW_MS,
8719
8982
  CloudflareClient,
8720
8983
  DEFAULT_AGENT_NAME,
8721
8984
  DEFAULT_AGENT_ROLE,
@@ -8746,10 +9009,14 @@ function parse(raw) {
8746
9009
  UnsafeApiUrlError,
8747
9010
  WARNING_THRESHOLD,
8748
9011
  assertWithinBase,
9012
+ bridgeWakeErrorMessage,
9013
+ bridgeWakeLastSeenAgeMs,
8749
9014
  buildApiUrl,
8750
9015
  buildInboundSecurityAdvisory,
8751
9016
  classifyEmailRoute,
9017
+ classifyResumeError,
8752
9018
  closeDatabase,
9019
+ composeBridgeWakePrompt,
8753
9020
  createTestDatabase,
8754
9021
  debug,
8755
9022
  debugWarn,
@@ -8759,11 +9026,13 @@ function parse(raw) {
8759
9026
  forgetHostSession,
8760
9027
  getDatabase,
8761
9028
  getOperatorEmail,
9029
+ getSmsProvider,
8762
9030
  hostSessionStoragePath,
8763
9031
  isInternalEmail,
8764
9032
  isSessionFresh,
8765
9033
  isValidPhoneNumber,
8766
9034
  loadHostSession,
9035
+ mapProviderSmsStatus,
8767
9036
  normalizeAddress,
8768
9037
  normalizePhoneNumber,
8769
9038
  normalizeSubject,
@@ -8773,6 +9042,7 @@ function parse(raw) {
8773
9042
  recordToolCall,
8774
9043
  redactObject,
8775
9044
  redactSecret,
9045
+ redactSmsConfig,
8776
9046
  resolveConfig,
8777
9047
  safeJoin,
8778
9048
  sanitizeEmail,
@@ -8782,6 +9052,7 @@ function parse(raw) {
8782
9052
  scoreEmail,
8783
9053
  setOperatorEmail,
8784
9054
  setTelemetryVersion,
9055
+ shouldSkipBridgeWakeForLiveOperator,
8785
9056
  startRelayBridge,
8786
9057
  threadIdFor,
8787
9058
  tryJoin,