@agenticmail/core 0.9.10 → 0.9.12

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
@@ -774,6 +774,7 @@ __export(index_exports, {
774
774
  operatorPrefsStoragePath: () => operatorPrefsStoragePath,
775
775
  parseEmail: () => parseEmail,
776
776
  parseGoogleVoiceSms: () => parseGoogleVoiceSms,
777
+ planBridgeWake: () => planBridgeWake,
777
778
  recordToolCall: () => recordToolCall,
778
779
  redactObject: () => redactObject,
779
780
  redactSecret: () => redactSecret,
@@ -2079,7 +2080,13 @@ function rowToAgent(row) {
2079
2080
  // Old rows (pre-migration-016) have undefined `wake_on_cc`;
2080
2081
  // treat that as the default-true (respect sender's wake list
2081
2082
  // as-is). Only explicit 0 disables CC wakes for this agent.
2082
- wakeOnCc: row.wake_on_cc !== void 0 ? row.wake_on_cc !== 0 : true
2083
+ wakeOnCc: row.wake_on_cc !== void 0 ? row.wake_on_cc !== 0 : true,
2084
+ // Pre-migration-017 rows have undefined `stopped`; treat as
2085
+ // not-stopped. The dispatcher only blocks wakes when this is
2086
+ // explicitly truthy, so the default mirrors back-compat.
2087
+ stopped: row.stopped !== void 0 ? row.stopped !== 0 : false,
2088
+ stoppedAt: row.stopped_at ?? null,
2089
+ stoppedReason: row.stopped_reason ?? null
2083
2090
  };
2084
2091
  }
2085
2092
  var AccountManager = class {
@@ -3697,6 +3704,22 @@ ALTER TABLE drafts ADD COLUMN attachments TEXT;
3697
3704
  -- explicitly named" preference from the wake-thrash feedback.
3698
3705
  -- Defaults to 1 (respect the senders wake list as-is).
3699
3706
  ALTER TABLE agents ADD COLUMN wake_on_cc INTEGER NOT NULL DEFAULT 1;
3707
+ `,
3708
+ "017_agent_stopped.sql": `
3709
+ -- Soft-stop for an agent mid-task. When 1, the dispatcher refuses
3710
+ -- to wake this agent for ANY reason \u2014 allowlists, To/Cc, task
3711
+ -- notifications, all of it. Mail still gets delivered to the
3712
+ -- mailbox so the audit trail of the thread stays intact. This
3713
+ -- is the non-destructive counterpart to delete_agent: stops an
3714
+ -- agent that's currently churning without losing its inbox or
3715
+ -- the thread history.
3716
+ --
3717
+ -- Companion columns capture WHEN it was stopped and the OPTIONAL
3718
+ -- reason the caller passed (e.g. "task superseded", "budget
3719
+ -- exhausted") so an operator can audit later.
3720
+ ALTER TABLE agents ADD COLUMN stopped INTEGER NOT NULL DEFAULT 0;
3721
+ ALTER TABLE agents ADD COLUMN stopped_at TEXT;
3722
+ ALTER TABLE agents ADD COLUMN stopped_reason TEXT;
3700
3723
  `
3701
3724
  };
3702
3725
  function runMigrations(database) {
@@ -3970,9 +3993,67 @@ function formatPollError(err) {
3970
3993
  const stdout = (e.stdout ?? "").toString().trim();
3971
3994
  if (stdout && !stderr) parts.push(`stdout=${truncate(stdout, 240)}`);
3972
3995
  if (parts.length === 1 && /^command failed$/i.test(head)) {
3973
- return `${head} (no further detail available \u2014 wrapping error did not carry stderr/code/response)`;
3996
+ return redactCredentialTokens(`${head} (no further detail available \u2014 wrapping error did not carry stderr/code/response)`);
3997
+ }
3998
+ return redactCredentialTokens(parts.join(" | "));
3999
+ }
4000
+ function redactCredentialTokens(text) {
4001
+ return text.replace(/\b(AUTH(?:ENTICATE)?)\s+(PLAIN|LOGIN|XOAUTH2|CRAM-MD5|EXTERNAL)\s+\S+/gi, "$1 $2 [redacted]").replace(/\b(AUTH(?:ENTICATE)?)\s+([A-Za-z0-9+/]{16,}={0,2})\b/gi, "$1 [redacted]");
4002
+ }
4003
+ function isRelayCredentialError(err) {
4004
+ if (!err) return false;
4005
+ const haystack = typeof err === "object" ? (() => {
4006
+ const e = err;
4007
+ return [
4008
+ e.message,
4009
+ e.code,
4010
+ e.responseCode,
4011
+ e.response,
4012
+ e.responseText,
4013
+ e.command,
4014
+ e.serverResponse
4015
+ ].filter(Boolean).join(" ").toLowerCase();
4016
+ })() : String(err).toLowerCase();
4017
+ return [
4018
+ "eauth",
4019
+ "authenticationfailed",
4020
+ "authentication failed",
4021
+ "authenticate failed",
4022
+ "invalid credentials",
4023
+ "invalid login",
4024
+ "login failed",
4025
+ "username and password not accepted",
4026
+ "application-specific password",
4027
+ "app password",
4028
+ "badcredentials",
4029
+ "invalid_grant",
4030
+ "invalid_token",
4031
+ "expired token",
4032
+ "token expired",
4033
+ "access token has expired",
4034
+ "token has expired",
4035
+ "token is expired",
4036
+ "token is invalid",
4037
+ "token revoked",
4038
+ "xoauth2",
4039
+ "aadsts",
4040
+ "535",
4041
+ "534",
4042
+ "5.7.8"
4043
+ ].some((marker) => haystack.includes(marker));
4044
+ }
4045
+ function formatRelayError(err, config, phase) {
4046
+ const detail = formatPollError(err);
4047
+ if (!isRelayCredentialError(err)) {
4048
+ return `Relay ${phase} failed: ${detail}`;
3974
4049
  }
3975
- return parts.join(" | ");
4050
+ const provider = config.provider === "gmail" ? "Gmail" : config.provider === "outlook" ? "Outlook/Microsoft 365" : "custom";
4051
+ const action = config.provider === "gmail" ? "Create a fresh Gmail app password or reconnect the relay, then run agenticmail setup-relay again." : config.provider === "outlook" ? "Refresh/recreate the Microsoft relay credential or OAuth token, then run agenticmail setup-relay again." : "Refresh the relay credential, then run agenticmail setup-relay again.";
4052
+ return [
4053
+ `Relay ${phase} failed: ${provider} relay authentication for ${config.email} is invalid, expired, or revoked.`,
4054
+ action,
4055
+ `Original error: ${detail}`
4056
+ ].join(" ");
3976
4057
  }
3977
4058
  function truncate(s, max) {
3978
4059
  if (s.length <= max) return s;
@@ -4027,7 +4108,11 @@ var RelayGateway = class {
4027
4108
  pass: config.password
4028
4109
  }
4029
4110
  });
4030
- await this.smtpTransport.verify();
4111
+ try {
4112
+ await this.smtpTransport.verify();
4113
+ } catch (err) {
4114
+ throw new Error(formatRelayError(err, config, "SMTP verification"));
4115
+ }
4031
4116
  const imap = new import_imapflow3.ImapFlow({
4032
4117
  host: config.imapHost,
4033
4118
  port: config.imapPort,
@@ -4038,8 +4123,16 @@ var RelayGateway = class {
4038
4123
  },
4039
4124
  logger: false
4040
4125
  });
4041
- await imap.connect();
4042
- await imap.logout();
4126
+ try {
4127
+ await imap.connect();
4128
+ } catch (err) {
4129
+ throw new Error(formatRelayError(err, config, "IMAP verification"));
4130
+ } finally {
4131
+ try {
4132
+ await imap.logout();
4133
+ } catch {
4134
+ }
4135
+ }
4043
4136
  }
4044
4137
  /**
4045
4138
  * Send an email through the relay SMTP server.
@@ -4049,9 +4142,10 @@ var RelayGateway = class {
4049
4142
  if (!this.config || !this.smtpTransport) {
4050
4143
  throw new Error("Relay not configured. Call setup() first.");
4051
4144
  }
4052
- const atIdx = this.config.email.lastIndexOf("@");
4053
- const localPart2 = this.config.email.slice(0, atIdx);
4054
- const domain = this.config.email.slice(atIdx + 1);
4145
+ const relayConfig = this.config;
4146
+ const atIdx = relayConfig.email.lastIndexOf("@");
4147
+ const localPart2 = relayConfig.email.slice(0, atIdx);
4148
+ const domain = relayConfig.email.slice(atIdx + 1);
4055
4149
  const relayFrom = `${localPart2}+${agentName}@${domain}`;
4056
4150
  const displayName = mail.fromName || agentName;
4057
4151
  const mailOpts = {
@@ -4075,7 +4169,12 @@ var RelayGateway = class {
4075
4169
  };
4076
4170
  const composer = new import_mail_composer2.default(mailOpts);
4077
4171
  const raw = await composer.compile().build();
4078
- const result = await this.smtpTransport.sendMail(mailOpts);
4172
+ let result;
4173
+ try {
4174
+ result = await this.smtpTransport.sendMail(mailOpts);
4175
+ } catch (err) {
4176
+ throw new Error(formatRelayError(err, relayConfig, "SMTP send"));
4177
+ }
4079
4178
  if (result.messageId) {
4080
4179
  this.sentMessageIds.set(result.messageId, agentName);
4081
4180
  if (this.sentMessageIds.size > 1e4) {
@@ -4152,7 +4251,7 @@ var RelayGateway = class {
4152
4251
  this.consecutiveFailures = 0;
4153
4252
  } catch (err) {
4154
4253
  this.consecutiveFailures++;
4155
- const msg = formatPollError(err);
4254
+ const msg = this.config ? formatRelayError(err, this.config, "IMAP poll") : formatPollError(err);
4156
4255
  console.error(`[RelayGateway] Poll failed (attempt ${this.consecutiveFailures}): ${msg}`);
4157
4256
  if (this.consecutiveFailures >= 5 && this.consecutiveFailures % 5 === 0) {
4158
4257
  console.error(`[RelayGateway] ${this.consecutiveFailures} consecutive failures \u2014 check IMAP credentials and connectivity (${this.config?.imapHost}:${this.config?.imapPort})`);
@@ -4385,13 +4484,14 @@ var RelayGateway = class {
4385
4484
  */
4386
4485
  async searchRelay(criteria, maxResults = 50) {
4387
4486
  if (!this.config) throw new Error("Relay not configured");
4487
+ const relayConfig = this.config;
4388
4488
  const imap = new import_imapflow3.ImapFlow({
4389
- host: this.config.imapHost,
4390
- port: this.config.imapPort,
4391
- secure: this.config.imapPort === 993,
4489
+ host: relayConfig.imapHost,
4490
+ port: relayConfig.imapPort,
4491
+ secure: relayConfig.imapPort === 993,
4392
4492
  auth: {
4393
- user: this.config.email,
4394
- pass: this.config.password
4493
+ user: relayConfig.email,
4494
+ pass: relayConfig.password
4395
4495
  },
4396
4496
  logger: false
4397
4497
  });
@@ -4428,7 +4528,7 @@ var RelayGateway = class {
4428
4528
  results.push({
4429
4529
  uid,
4430
4530
  source: "relay",
4431
- account: this.config.email,
4531
+ account: relayConfig.email,
4432
4532
  messageId: envelope.messageId ?? "",
4433
4533
  subject: envelope.subject ?? "",
4434
4534
  from: (envelope.from ?? []).map((a) => ({ name: a.name, address: a.address ?? "" })),
@@ -4444,7 +4544,7 @@ var RelayGateway = class {
4444
4544
  lock.release();
4445
4545
  }
4446
4546
  } catch (err) {
4447
- console.error("[RelayGateway] Relay search failed:", err instanceof Error ? err.message : err);
4547
+ console.error("[RelayGateway] Relay search failed:", formatRelayError(err, relayConfig, "IMAP search"));
4448
4548
  return [];
4449
4549
  } finally {
4450
4550
  try {
@@ -4459,13 +4559,14 @@ var RelayGateway = class {
4459
4559
  */
4460
4560
  async fetchRelayMessage(uid) {
4461
4561
  if (!this.config) throw new Error("Relay not configured");
4562
+ const relayConfig = this.config;
4462
4563
  const imap = new import_imapflow3.ImapFlow({
4463
- host: this.config.imapHost,
4464
- port: this.config.imapPort,
4465
- secure: this.config.imapPort === 993,
4564
+ host: relayConfig.imapHost,
4565
+ port: relayConfig.imapPort,
4566
+ secure: relayConfig.imapPort === 993,
4466
4567
  auth: {
4467
- user: this.config.email,
4468
- pass: this.config.password
4568
+ user: relayConfig.email,
4569
+ pass: relayConfig.password
4469
4570
  },
4470
4571
  logger: false
4471
4572
  });
@@ -4502,7 +4603,7 @@ var RelayGateway = class {
4502
4603
  lock.release();
4503
4604
  }
4504
4605
  } catch (err) {
4505
- console.error("[RelayGateway] Fetch relay message failed:", err instanceof Error ? err.message : err);
4606
+ console.error("[RelayGateway] Fetch relay message failed:", formatRelayError(err, relayConfig, "IMAP fetch"));
4506
4607
  return null;
4507
4608
  } finally {
4508
4609
  try {
@@ -7276,6 +7377,32 @@ function shouldSkipBridgeWakeForLiveOperator(session, nowMs = Date.now(), liveWi
7276
7377
  const ageMs = bridgeWakeLastSeenAgeMs(session, nowMs);
7277
7378
  return ageMs !== null && ageMs < liveWindowMs;
7278
7379
  }
7380
+ function planBridgeWake(args) {
7381
+ const nowMs = args.nowMs ?? Date.now();
7382
+ const liveWindowMs = args.liveWindowMs ?? BRIDGE_OPERATOR_LIVE_WINDOW_MS;
7383
+ const ageMs = bridgeWakeLastSeenAgeMs(args.session, nowMs);
7384
+ if (ageMs !== null && ageMs < liveWindowMs) {
7385
+ return {
7386
+ action: "skip-live",
7387
+ reason: "operator-live",
7388
+ ageMs,
7389
+ mail: args.mail
7390
+ };
7391
+ }
7392
+ if (!args.session) {
7393
+ return {
7394
+ action: "escalate",
7395
+ reason: "no-fresh-session",
7396
+ mail: args.mail
7397
+ };
7398
+ }
7399
+ return {
7400
+ action: "resume",
7401
+ session: args.session,
7402
+ prompt: composeBridgeWakePrompt(args.mail),
7403
+ mail: args.mail
7404
+ };
7405
+ }
7279
7406
  function composeBridgeWakePrompt(args) {
7280
7407
  const subject = args.subject ?? "(no subject)";
7281
7408
  const from = args.from ?? "unknown";
@@ -9039,6 +9166,7 @@ function parse(raw) {
9039
9166
  operatorPrefsStoragePath,
9040
9167
  parseEmail,
9041
9168
  parseGoogleVoiceSms,
9169
+ planBridgeWake,
9042
9170
  recordToolCall,
9043
9171
  redactObject,
9044
9172
  redactSecret,
package/dist/index.d.cts CHANGED
@@ -358,6 +358,24 @@ interface Agent {
358
358
  * including them never wastes a Claude turn. Defaults to true
359
359
  * (preserves the 0.9.0 wake-list-respecting behaviour). */
360
360
  wakeOnCc?: boolean;
361
+ /** Soft-stop flag. When true, the dispatcher refuses to wake
362
+ * this agent for ANY reason (allowlist, To/Cc, task events).
363
+ * Mail still lands in the mailbox so the audit trail of the
364
+ * thread is preserved — only Claude/Codex turns are blocked.
365
+ * This is the non-destructive counterpart to delete_agent for
366
+ * stopping a churning agent mid-task without losing context. */
367
+ stopped?: boolean;
368
+ /** ISO timestamp of when `stopped` was set to true. NULL when
369
+ * the agent has never been stopped (or has since been resumed
370
+ * — `resume_agent` clears both `stopped` and the audit fields
371
+ * is a policy decision; the current implementation clears
372
+ * `stopped` only and leaves the timestamp / reason in place so
373
+ * operators can see the most-recent stop history). */
374
+ stoppedAt?: string | null;
375
+ /** Optional free-form reason supplied by the caller when the
376
+ * agent was stopped (e.g. "task superseded by new requirements"
377
+ * or "stop all sub-agents — user request 2025-12-09"). */
378
+ stoppedReason?: string | null;
361
379
  }
362
380
  interface CreateAgentOptions {
363
381
  name: string;
@@ -2053,6 +2071,29 @@ interface BridgeWakePromptArgs {
2053
2071
  from?: string;
2054
2072
  preview?: string;
2055
2073
  }
2074
+ interface BridgeMailContext extends BridgeWakePromptArgs {
2075
+ }
2076
+ type BridgeWakeRoute = {
2077
+ action: 'skip-live';
2078
+ reason: 'operator-live';
2079
+ ageMs: number;
2080
+ mail: BridgeMailContext;
2081
+ } | {
2082
+ action: 'escalate';
2083
+ reason: 'no-fresh-session';
2084
+ mail: BridgeMailContext;
2085
+ } | {
2086
+ action: 'resume';
2087
+ session: HostSession;
2088
+ prompt: string;
2089
+ mail: BridgeMailContext;
2090
+ };
2091
+ interface PlanBridgeWakeArgs {
2092
+ session: HostSession | null;
2093
+ mail: BridgeMailContext;
2094
+ nowMs?: number;
2095
+ liveWindowMs?: number;
2096
+ }
2056
2097
  interface ResumeErrorClassificationOptions {
2057
2098
  expiredMarkers?: readonly string[];
2058
2099
  sdkMissingMarkers?: readonly string[];
@@ -2062,6 +2103,7 @@ declare function bridgeWakeErrorMessage(err: unknown): string;
2062
2103
  declare function classifyResumeError(err: unknown, options?: ResumeErrorClassificationOptions): BridgeWakeError;
2063
2104
  declare function bridgeWakeLastSeenAgeMs(session: Pick<HostSession, 'lastSeenMs'> | null | undefined, nowMs?: number): number | null;
2064
2105
  declare function shouldSkipBridgeWakeForLiveOperator(session: Pick<HostSession, 'lastSeenMs'> | null | undefined, nowMs?: number, liveWindowMs?: number): boolean;
2106
+ declare function planBridgeWake(args: PlanBridgeWakeArgs): BridgeWakeRoute;
2065
2107
  /**
2066
2108
  * Build the prompt a host session sees on bridge wake. Host adapters
2067
2109
  * keep their own SDK resume call, but share this operator-facing
@@ -2677,4 +2719,4 @@ declare class AgentMemoryStore {
2677
2719
  renderForPrompt(memory: AgentMemoryRead | null): string;
2678
2720
  }
2679
2721
 
2680
- export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentMemoryFields, type AgentMemoryOptions, type AgentMemoryRead, AgentMemoryStore, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, BRIDGE_OPERATOR_LIVE_WINDOW_MS, type BridgeWakeError, type BridgeWakePromptArgs, type BridgeWakeResult, type CachedMessage, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DEFAULT_SESSION_MAX_AGE_MS, DNSConfigurator, type Database, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type HostName, type HostSession, type HostSessionResumeMode, type InboundEmail, type InboundSmsEvent, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, PathTraversalError, type PurchasedDomain, REDACTED, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, type ResumeErrorClassificationOptions, SPAM_THRESHOLD, type SafeJoinOptions, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, type SendSmsInput, type SendSmsResult, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SmsProvider, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, ThreadCache, type ThreadCacheEntry, type ThreadCacheOptions, type ThreadIdInput, type TunnelConfig, TunnelManager, UnsafeApiUrlError, WARNING_THRESHOLD, type WatcherOptions, assertWithinBase, bridgeWakeErrorMessage, bridgeWakeLastSeenAgeMs, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, classifyResumeError, closeDatabase, composeBridgeWakePrompt, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, forgetHostSession, getDatabase, getOperatorEmail, getSmsProvider, hostSessionStoragePath, isInternalEmail, isSessionFresh, isValidPhoneNumber, loadHostSession, mapProviderSmsStatus, normalizeAddress, normalizePhoneNumber, normalizeSubject, operatorPrefsStoragePath, parseEmail, parseGoogleVoiceSms, recordToolCall, redactObject, redactSecret, redactSmsConfig, resolveConfig, safeJoin, sanitizeEmail, saveConfig, saveHostSession, scanOutboundEmail, scoreEmail, setOperatorEmail, setTelemetryVersion, shouldSkipBridgeWakeForLiveOperator, startRelayBridge, threadIdFor, tryJoin, validateApiUrl };
2722
+ export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentMemoryFields, type AgentMemoryOptions, type AgentMemoryRead, AgentMemoryStore, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, BRIDGE_OPERATOR_LIVE_WINDOW_MS, type BridgeMailContext, type BridgeWakeError, type BridgeWakePromptArgs, type BridgeWakeResult, type BridgeWakeRoute, type CachedMessage, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DEFAULT_SESSION_MAX_AGE_MS, DNSConfigurator, type Database, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type HostName, type HostSession, type HostSessionResumeMode, type InboundEmail, type InboundSmsEvent, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, PathTraversalError, type PlanBridgeWakeArgs, type PurchasedDomain, REDACTED, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, type ResumeErrorClassificationOptions, SPAM_THRESHOLD, type SafeJoinOptions, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, type SendSmsInput, type SendSmsResult, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SmsProvider, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, ThreadCache, type ThreadCacheEntry, type ThreadCacheOptions, type ThreadIdInput, type TunnelConfig, TunnelManager, UnsafeApiUrlError, WARNING_THRESHOLD, type WatcherOptions, assertWithinBase, bridgeWakeErrorMessage, bridgeWakeLastSeenAgeMs, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, classifyResumeError, closeDatabase, composeBridgeWakePrompt, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, forgetHostSession, getDatabase, getOperatorEmail, getSmsProvider, hostSessionStoragePath, isInternalEmail, isSessionFresh, isValidPhoneNumber, loadHostSession, mapProviderSmsStatus, normalizeAddress, normalizePhoneNumber, normalizeSubject, operatorPrefsStoragePath, parseEmail, parseGoogleVoiceSms, planBridgeWake, recordToolCall, redactObject, redactSecret, redactSmsConfig, resolveConfig, safeJoin, sanitizeEmail, saveConfig, saveHostSession, scanOutboundEmail, scoreEmail, setOperatorEmail, setTelemetryVersion, shouldSkipBridgeWakeForLiveOperator, startRelayBridge, threadIdFor, tryJoin, validateApiUrl };
package/dist/index.d.ts CHANGED
@@ -358,6 +358,24 @@ interface Agent {
358
358
  * including them never wastes a Claude turn. Defaults to true
359
359
  * (preserves the 0.9.0 wake-list-respecting behaviour). */
360
360
  wakeOnCc?: boolean;
361
+ /** Soft-stop flag. When true, the dispatcher refuses to wake
362
+ * this agent for ANY reason (allowlist, To/Cc, task events).
363
+ * Mail still lands in the mailbox so the audit trail of the
364
+ * thread is preserved — only Claude/Codex turns are blocked.
365
+ * This is the non-destructive counterpart to delete_agent for
366
+ * stopping a churning agent mid-task without losing context. */
367
+ stopped?: boolean;
368
+ /** ISO timestamp of when `stopped` was set to true. NULL when
369
+ * the agent has never been stopped (or has since been resumed
370
+ * — `resume_agent` clears both `stopped` and the audit fields
371
+ * is a policy decision; the current implementation clears
372
+ * `stopped` only and leaves the timestamp / reason in place so
373
+ * operators can see the most-recent stop history). */
374
+ stoppedAt?: string | null;
375
+ /** Optional free-form reason supplied by the caller when the
376
+ * agent was stopped (e.g. "task superseded by new requirements"
377
+ * or "stop all sub-agents — user request 2025-12-09"). */
378
+ stoppedReason?: string | null;
361
379
  }
362
380
  interface CreateAgentOptions {
363
381
  name: string;
@@ -2053,6 +2071,29 @@ interface BridgeWakePromptArgs {
2053
2071
  from?: string;
2054
2072
  preview?: string;
2055
2073
  }
2074
+ interface BridgeMailContext extends BridgeWakePromptArgs {
2075
+ }
2076
+ type BridgeWakeRoute = {
2077
+ action: 'skip-live';
2078
+ reason: 'operator-live';
2079
+ ageMs: number;
2080
+ mail: BridgeMailContext;
2081
+ } | {
2082
+ action: 'escalate';
2083
+ reason: 'no-fresh-session';
2084
+ mail: BridgeMailContext;
2085
+ } | {
2086
+ action: 'resume';
2087
+ session: HostSession;
2088
+ prompt: string;
2089
+ mail: BridgeMailContext;
2090
+ };
2091
+ interface PlanBridgeWakeArgs {
2092
+ session: HostSession | null;
2093
+ mail: BridgeMailContext;
2094
+ nowMs?: number;
2095
+ liveWindowMs?: number;
2096
+ }
2056
2097
  interface ResumeErrorClassificationOptions {
2057
2098
  expiredMarkers?: readonly string[];
2058
2099
  sdkMissingMarkers?: readonly string[];
@@ -2062,6 +2103,7 @@ declare function bridgeWakeErrorMessage(err: unknown): string;
2062
2103
  declare function classifyResumeError(err: unknown, options?: ResumeErrorClassificationOptions): BridgeWakeError;
2063
2104
  declare function bridgeWakeLastSeenAgeMs(session: Pick<HostSession, 'lastSeenMs'> | null | undefined, nowMs?: number): number | null;
2064
2105
  declare function shouldSkipBridgeWakeForLiveOperator(session: Pick<HostSession, 'lastSeenMs'> | null | undefined, nowMs?: number, liveWindowMs?: number): boolean;
2106
+ declare function planBridgeWake(args: PlanBridgeWakeArgs): BridgeWakeRoute;
2065
2107
  /**
2066
2108
  * Build the prompt a host session sees on bridge wake. Host adapters
2067
2109
  * keep their own SDK resume call, but share this operator-facing
@@ -2677,4 +2719,4 @@ declare class AgentMemoryStore {
2677
2719
  renderForPrompt(memory: AgentMemoryRead | null): string;
2678
2720
  }
2679
2721
 
2680
- export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentMemoryFields, type AgentMemoryOptions, type AgentMemoryRead, AgentMemoryStore, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, BRIDGE_OPERATOR_LIVE_WINDOW_MS, type BridgeWakeError, type BridgeWakePromptArgs, type BridgeWakeResult, type CachedMessage, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DEFAULT_SESSION_MAX_AGE_MS, DNSConfigurator, type Database, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type HostName, type HostSession, type HostSessionResumeMode, type InboundEmail, type InboundSmsEvent, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, PathTraversalError, type PurchasedDomain, REDACTED, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, type ResumeErrorClassificationOptions, SPAM_THRESHOLD, type SafeJoinOptions, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, type SendSmsInput, type SendSmsResult, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SmsProvider, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, ThreadCache, type ThreadCacheEntry, type ThreadCacheOptions, type ThreadIdInput, type TunnelConfig, TunnelManager, UnsafeApiUrlError, WARNING_THRESHOLD, type WatcherOptions, assertWithinBase, bridgeWakeErrorMessage, bridgeWakeLastSeenAgeMs, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, classifyResumeError, closeDatabase, composeBridgeWakePrompt, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, forgetHostSession, getDatabase, getOperatorEmail, getSmsProvider, hostSessionStoragePath, isInternalEmail, isSessionFresh, isValidPhoneNumber, loadHostSession, mapProviderSmsStatus, normalizeAddress, normalizePhoneNumber, normalizeSubject, operatorPrefsStoragePath, parseEmail, parseGoogleVoiceSms, recordToolCall, redactObject, redactSecret, redactSmsConfig, resolveConfig, safeJoin, sanitizeEmail, saveConfig, saveHostSession, scanOutboundEmail, scoreEmail, setOperatorEmail, setTelemetryVersion, shouldSkipBridgeWakeForLiveOperator, startRelayBridge, threadIdFor, tryJoin, validateApiUrl };
2722
+ export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentMemoryFields, type AgentMemoryOptions, type AgentMemoryRead, AgentMemoryStore, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, BRIDGE_OPERATOR_LIVE_WINDOW_MS, type BridgeMailContext, type BridgeWakeError, type BridgeWakePromptArgs, type BridgeWakeResult, type BridgeWakeRoute, type CachedMessage, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DEFAULT_SESSION_MAX_AGE_MS, DNSConfigurator, type Database, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, type EmailRouteAction, type EmailRouteClass, type EmailRouteClassification, type EmailRouteInput, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type HostName, type HostSession, type HostSessionResumeMode, type InboundEmail, type InboundSmsEvent, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, PathTraversalError, type PlanBridgeWakeArgs, type PurchasedDomain, REDACTED, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, type ResumeErrorClassificationOptions, SPAM_THRESHOLD, type SafeJoinOptions, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, type SendSmsInput, type SendSmsResult, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SmsProvider, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, ThreadCache, type ThreadCacheEntry, type ThreadCacheOptions, type ThreadIdInput, type TunnelConfig, TunnelManager, UnsafeApiUrlError, WARNING_THRESHOLD, type WatcherOptions, assertWithinBase, bridgeWakeErrorMessage, bridgeWakeLastSeenAgeMs, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, classifyResumeError, closeDatabase, composeBridgeWakePrompt, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, flushTelemetry, forgetHostSession, getDatabase, getOperatorEmail, getSmsProvider, hostSessionStoragePath, isInternalEmail, isSessionFresh, isValidPhoneNumber, loadHostSession, mapProviderSmsStatus, normalizeAddress, normalizePhoneNumber, normalizeSubject, operatorPrefsStoragePath, parseEmail, parseGoogleVoiceSms, planBridgeWake, recordToolCall, redactObject, redactSecret, redactSmsConfig, resolveConfig, safeJoin, sanitizeEmail, saveConfig, saveHostSession, scanOutboundEmail, scoreEmail, setOperatorEmail, setTelemetryVersion, shouldSkipBridgeWakeForLiveOperator, startRelayBridge, threadIdFor, tryJoin, validateApiUrl };
package/dist/index.js CHANGED
@@ -1292,7 +1292,13 @@ function rowToAgent(row) {
1292
1292
  // Old rows (pre-migration-016) have undefined `wake_on_cc`;
1293
1293
  // treat that as the default-true (respect sender's wake list
1294
1294
  // as-is). Only explicit 0 disables CC wakes for this agent.
1295
- wakeOnCc: row.wake_on_cc !== void 0 ? row.wake_on_cc !== 0 : true
1295
+ wakeOnCc: row.wake_on_cc !== void 0 ? row.wake_on_cc !== 0 : true,
1296
+ // Pre-migration-017 rows have undefined `stopped`; treat as
1297
+ // not-stopped. The dispatcher only blocks wakes when this is
1298
+ // explicitly truthy, so the default mirrors back-compat.
1299
+ stopped: row.stopped !== void 0 ? row.stopped !== 0 : false,
1300
+ stoppedAt: row.stopped_at ?? null,
1301
+ stoppedReason: row.stopped_reason ?? null
1296
1302
  };
1297
1303
  }
1298
1304
  var AccountManager = class {
@@ -2906,6 +2912,22 @@ ALTER TABLE drafts ADD COLUMN attachments TEXT;
2906
2912
  -- explicitly named" preference from the wake-thrash feedback.
2907
2913
  -- Defaults to 1 (respect the senders wake list as-is).
2908
2914
  ALTER TABLE agents ADD COLUMN wake_on_cc INTEGER NOT NULL DEFAULT 1;
2915
+ `,
2916
+ "017_agent_stopped.sql": `
2917
+ -- Soft-stop for an agent mid-task. When 1, the dispatcher refuses
2918
+ -- to wake this agent for ANY reason \u2014 allowlists, To/Cc, task
2919
+ -- notifications, all of it. Mail still gets delivered to the
2920
+ -- mailbox so the audit trail of the thread stays intact. This
2921
+ -- is the non-destructive counterpart to delete_agent: stops an
2922
+ -- agent that's currently churning without losing its inbox or
2923
+ -- the thread history.
2924
+ --
2925
+ -- Companion columns capture WHEN it was stopped and the OPTIONAL
2926
+ -- reason the caller passed (e.g. "task superseded", "budget
2927
+ -- exhausted") so an operator can audit later.
2928
+ ALTER TABLE agents ADD COLUMN stopped INTEGER NOT NULL DEFAULT 0;
2929
+ ALTER TABLE agents ADD COLUMN stopped_at TEXT;
2930
+ ALTER TABLE agents ADD COLUMN stopped_reason TEXT;
2909
2931
  `
2910
2932
  };
2911
2933
  function runMigrations(database) {
@@ -3179,9 +3201,67 @@ function formatPollError(err) {
3179
3201
  const stdout = (e.stdout ?? "").toString().trim();
3180
3202
  if (stdout && !stderr) parts.push(`stdout=${truncate(stdout, 240)}`);
3181
3203
  if (parts.length === 1 && /^command failed$/i.test(head)) {
3182
- return `${head} (no further detail available \u2014 wrapping error did not carry stderr/code/response)`;
3204
+ return redactCredentialTokens(`${head} (no further detail available \u2014 wrapping error did not carry stderr/code/response)`);
3205
+ }
3206
+ return redactCredentialTokens(parts.join(" | "));
3207
+ }
3208
+ function redactCredentialTokens(text) {
3209
+ return text.replace(/\b(AUTH(?:ENTICATE)?)\s+(PLAIN|LOGIN|XOAUTH2|CRAM-MD5|EXTERNAL)\s+\S+/gi, "$1 $2 [redacted]").replace(/\b(AUTH(?:ENTICATE)?)\s+([A-Za-z0-9+/]{16,}={0,2})\b/gi, "$1 [redacted]");
3210
+ }
3211
+ function isRelayCredentialError(err) {
3212
+ if (!err) return false;
3213
+ const haystack = typeof err === "object" ? (() => {
3214
+ const e = err;
3215
+ return [
3216
+ e.message,
3217
+ e.code,
3218
+ e.responseCode,
3219
+ e.response,
3220
+ e.responseText,
3221
+ e.command,
3222
+ e.serverResponse
3223
+ ].filter(Boolean).join(" ").toLowerCase();
3224
+ })() : String(err).toLowerCase();
3225
+ return [
3226
+ "eauth",
3227
+ "authenticationfailed",
3228
+ "authentication failed",
3229
+ "authenticate failed",
3230
+ "invalid credentials",
3231
+ "invalid login",
3232
+ "login failed",
3233
+ "username and password not accepted",
3234
+ "application-specific password",
3235
+ "app password",
3236
+ "badcredentials",
3237
+ "invalid_grant",
3238
+ "invalid_token",
3239
+ "expired token",
3240
+ "token expired",
3241
+ "access token has expired",
3242
+ "token has expired",
3243
+ "token is expired",
3244
+ "token is invalid",
3245
+ "token revoked",
3246
+ "xoauth2",
3247
+ "aadsts",
3248
+ "535",
3249
+ "534",
3250
+ "5.7.8"
3251
+ ].some((marker) => haystack.includes(marker));
3252
+ }
3253
+ function formatRelayError(err, config, phase) {
3254
+ const detail = formatPollError(err);
3255
+ if (!isRelayCredentialError(err)) {
3256
+ return `Relay ${phase} failed: ${detail}`;
3183
3257
  }
3184
- return parts.join(" | ");
3258
+ const provider = config.provider === "gmail" ? "Gmail" : config.provider === "outlook" ? "Outlook/Microsoft 365" : "custom";
3259
+ const action = config.provider === "gmail" ? "Create a fresh Gmail app password or reconnect the relay, then run agenticmail setup-relay again." : config.provider === "outlook" ? "Refresh/recreate the Microsoft relay credential or OAuth token, then run agenticmail setup-relay again." : "Refresh the relay credential, then run agenticmail setup-relay again.";
3260
+ return [
3261
+ `Relay ${phase} failed: ${provider} relay authentication for ${config.email} is invalid, expired, or revoked.`,
3262
+ action,
3263
+ `Original error: ${detail}`
3264
+ ].join(" ");
3185
3265
  }
3186
3266
  function truncate(s, max) {
3187
3267
  if (s.length <= max) return s;
@@ -3236,7 +3316,11 @@ var RelayGateway = class {
3236
3316
  pass: config.password
3237
3317
  }
3238
3318
  });
3239
- await this.smtpTransport.verify();
3319
+ try {
3320
+ await this.smtpTransport.verify();
3321
+ } catch (err) {
3322
+ throw new Error(formatRelayError(err, config, "SMTP verification"));
3323
+ }
3240
3324
  const imap = new ImapFlow3({
3241
3325
  host: config.imapHost,
3242
3326
  port: config.imapPort,
@@ -3247,8 +3331,16 @@ var RelayGateway = class {
3247
3331
  },
3248
3332
  logger: false
3249
3333
  });
3250
- await imap.connect();
3251
- await imap.logout();
3334
+ try {
3335
+ await imap.connect();
3336
+ } catch (err) {
3337
+ throw new Error(formatRelayError(err, config, "IMAP verification"));
3338
+ } finally {
3339
+ try {
3340
+ await imap.logout();
3341
+ } catch {
3342
+ }
3343
+ }
3252
3344
  }
3253
3345
  /**
3254
3346
  * Send an email through the relay SMTP server.
@@ -3258,9 +3350,10 @@ var RelayGateway = class {
3258
3350
  if (!this.config || !this.smtpTransport) {
3259
3351
  throw new Error("Relay not configured. Call setup() first.");
3260
3352
  }
3261
- const atIdx = this.config.email.lastIndexOf("@");
3262
- const localPart2 = this.config.email.slice(0, atIdx);
3263
- const domain = this.config.email.slice(atIdx + 1);
3353
+ const relayConfig = this.config;
3354
+ const atIdx = relayConfig.email.lastIndexOf("@");
3355
+ const localPart2 = relayConfig.email.slice(0, atIdx);
3356
+ const domain = relayConfig.email.slice(atIdx + 1);
3264
3357
  const relayFrom = `${localPart2}+${agentName}@${domain}`;
3265
3358
  const displayName = mail.fromName || agentName;
3266
3359
  const mailOpts = {
@@ -3284,7 +3377,12 @@ var RelayGateway = class {
3284
3377
  };
3285
3378
  const composer = new MailComposer2(mailOpts);
3286
3379
  const raw = await composer.compile().build();
3287
- const result = await this.smtpTransport.sendMail(mailOpts);
3380
+ let result;
3381
+ try {
3382
+ result = await this.smtpTransport.sendMail(mailOpts);
3383
+ } catch (err) {
3384
+ throw new Error(formatRelayError(err, relayConfig, "SMTP send"));
3385
+ }
3288
3386
  if (result.messageId) {
3289
3387
  this.sentMessageIds.set(result.messageId, agentName);
3290
3388
  if (this.sentMessageIds.size > 1e4) {
@@ -3361,7 +3459,7 @@ var RelayGateway = class {
3361
3459
  this.consecutiveFailures = 0;
3362
3460
  } catch (err) {
3363
3461
  this.consecutiveFailures++;
3364
- const msg = formatPollError(err);
3462
+ const msg = this.config ? formatRelayError(err, this.config, "IMAP poll") : formatPollError(err);
3365
3463
  console.error(`[RelayGateway] Poll failed (attempt ${this.consecutiveFailures}): ${msg}`);
3366
3464
  if (this.consecutiveFailures >= 5 && this.consecutiveFailures % 5 === 0) {
3367
3465
  console.error(`[RelayGateway] ${this.consecutiveFailures} consecutive failures \u2014 check IMAP credentials and connectivity (${this.config?.imapHost}:${this.config?.imapPort})`);
@@ -3594,13 +3692,14 @@ var RelayGateway = class {
3594
3692
  */
3595
3693
  async searchRelay(criteria, maxResults = 50) {
3596
3694
  if (!this.config) throw new Error("Relay not configured");
3695
+ const relayConfig = this.config;
3597
3696
  const imap = new ImapFlow3({
3598
- host: this.config.imapHost,
3599
- port: this.config.imapPort,
3600
- secure: this.config.imapPort === 993,
3697
+ host: relayConfig.imapHost,
3698
+ port: relayConfig.imapPort,
3699
+ secure: relayConfig.imapPort === 993,
3601
3700
  auth: {
3602
- user: this.config.email,
3603
- pass: this.config.password
3701
+ user: relayConfig.email,
3702
+ pass: relayConfig.password
3604
3703
  },
3605
3704
  logger: false
3606
3705
  });
@@ -3637,7 +3736,7 @@ var RelayGateway = class {
3637
3736
  results.push({
3638
3737
  uid,
3639
3738
  source: "relay",
3640
- account: this.config.email,
3739
+ account: relayConfig.email,
3641
3740
  messageId: envelope.messageId ?? "",
3642
3741
  subject: envelope.subject ?? "",
3643
3742
  from: (envelope.from ?? []).map((a) => ({ name: a.name, address: a.address ?? "" })),
@@ -3653,7 +3752,7 @@ var RelayGateway = class {
3653
3752
  lock.release();
3654
3753
  }
3655
3754
  } catch (err) {
3656
- console.error("[RelayGateway] Relay search failed:", err instanceof Error ? err.message : err);
3755
+ console.error("[RelayGateway] Relay search failed:", formatRelayError(err, relayConfig, "IMAP search"));
3657
3756
  return [];
3658
3757
  } finally {
3659
3758
  try {
@@ -3668,13 +3767,14 @@ var RelayGateway = class {
3668
3767
  */
3669
3768
  async fetchRelayMessage(uid) {
3670
3769
  if (!this.config) throw new Error("Relay not configured");
3770
+ const relayConfig = this.config;
3671
3771
  const imap = new ImapFlow3({
3672
- host: this.config.imapHost,
3673
- port: this.config.imapPort,
3674
- secure: this.config.imapPort === 993,
3772
+ host: relayConfig.imapHost,
3773
+ port: relayConfig.imapPort,
3774
+ secure: relayConfig.imapPort === 993,
3675
3775
  auth: {
3676
- user: this.config.email,
3677
- pass: this.config.password
3776
+ user: relayConfig.email,
3777
+ pass: relayConfig.password
3678
3778
  },
3679
3779
  logger: false
3680
3780
  });
@@ -3711,7 +3811,7 @@ var RelayGateway = class {
3711
3811
  lock.release();
3712
3812
  }
3713
3813
  } catch (err) {
3714
- console.error("[RelayGateway] Fetch relay message failed:", err instanceof Error ? err.message : err);
3814
+ console.error("[RelayGateway] Fetch relay message failed:", formatRelayError(err, relayConfig, "IMAP fetch"));
3715
3815
  return null;
3716
3816
  } finally {
3717
3817
  try {
@@ -6484,6 +6584,32 @@ function shouldSkipBridgeWakeForLiveOperator(session, nowMs = Date.now(), liveWi
6484
6584
  const ageMs = bridgeWakeLastSeenAgeMs(session, nowMs);
6485
6585
  return ageMs !== null && ageMs < liveWindowMs;
6486
6586
  }
6587
+ function planBridgeWake(args) {
6588
+ const nowMs = args.nowMs ?? Date.now();
6589
+ const liveWindowMs = args.liveWindowMs ?? BRIDGE_OPERATOR_LIVE_WINDOW_MS;
6590
+ const ageMs = bridgeWakeLastSeenAgeMs(args.session, nowMs);
6591
+ if (ageMs !== null && ageMs < liveWindowMs) {
6592
+ return {
6593
+ action: "skip-live",
6594
+ reason: "operator-live",
6595
+ ageMs,
6596
+ mail: args.mail
6597
+ };
6598
+ }
6599
+ if (!args.session) {
6600
+ return {
6601
+ action: "escalate",
6602
+ reason: "no-fresh-session",
6603
+ mail: args.mail
6604
+ };
6605
+ }
6606
+ return {
6607
+ action: "resume",
6608
+ session: args.session,
6609
+ prompt: composeBridgeWakePrompt(args.mail),
6610
+ mail: args.mail
6611
+ };
6612
+ }
6487
6613
  function composeBridgeWakePrompt(args) {
6488
6614
  const subject = args.subject ?? "(no subject)";
6489
6615
  const from = args.from ?? "unknown";
@@ -8261,6 +8387,7 @@ export {
8261
8387
  operatorPrefsStoragePath,
8262
8388
  parseEmail,
8263
8389
  parseGoogleVoiceSms,
8390
+ planBridgeWake,
8264
8391
  recordToolCall,
8265
8392
  redactObject,
8266
8393
  redactSecret,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
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",