@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 +152 -24
- package/dist/index.d.cts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +151 -24
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4042
|
-
|
|
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
|
|
4053
|
-
const
|
|
4054
|
-
const
|
|
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
|
-
|
|
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:
|
|
4390
|
-
port:
|
|
4391
|
-
secure:
|
|
4489
|
+
host: relayConfig.imapHost,
|
|
4490
|
+
port: relayConfig.imapPort,
|
|
4491
|
+
secure: relayConfig.imapPort === 993,
|
|
4392
4492
|
auth: {
|
|
4393
|
-
user:
|
|
4394
|
-
pass:
|
|
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:
|
|
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
|
|
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:
|
|
4464
|
-
port:
|
|
4465
|
-
secure:
|
|
4564
|
+
host: relayConfig.imapHost,
|
|
4565
|
+
port: relayConfig.imapPort,
|
|
4566
|
+
secure: relayConfig.imapPort === 993,
|
|
4466
4567
|
auth: {
|
|
4467
|
-
user:
|
|
4468
|
-
pass:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3251
|
-
|
|
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
|
|
3262
|
-
const
|
|
3263
|
-
const
|
|
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
|
-
|
|
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:
|
|
3599
|
-
port:
|
|
3600
|
-
secure:
|
|
3697
|
+
host: relayConfig.imapHost,
|
|
3698
|
+
port: relayConfig.imapPort,
|
|
3699
|
+
secure: relayConfig.imapPort === 993,
|
|
3601
3700
|
auth: {
|
|
3602
|
-
user:
|
|
3603
|
-
pass:
|
|
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:
|
|
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
|
|
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:
|
|
3673
|
-
port:
|
|
3674
|
-
secure:
|
|
3772
|
+
host: relayConfig.imapHost,
|
|
3773
|
+
port: relayConfig.imapPort,
|
|
3774
|
+
secure: relayConfig.imapPort === 993,
|
|
3675
3775
|
auth: {
|
|
3676
|
-
user:
|
|
3677
|
-
pass:
|
|
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
|
|
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,
|