@agenticmail/core 0.9.9 → 0.9.11
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 +228 -26
- package/dist/index.d.cts +100 -5
- package/dist/index.d.ts +100 -5
- package/dist/index.js +221 -26
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -713,6 +713,7 @@ __export(index_exports, {
|
|
|
713
713
|
AgentDeletionService: () => AgentDeletionService,
|
|
714
714
|
AgentMemoryStore: () => AgentMemoryStore,
|
|
715
715
|
AgenticMailClient: () => AgenticMailClient,
|
|
716
|
+
BRIDGE_OPERATOR_LIVE_WINDOW_MS: () => BRIDGE_OPERATOR_LIVE_WINDOW_MS,
|
|
716
717
|
CloudflareClient: () => CloudflareClient,
|
|
717
718
|
DEFAULT_AGENT_NAME: () => DEFAULT_AGENT_NAME,
|
|
718
719
|
DEFAULT_AGENT_ROLE: () => DEFAULT_AGENT_ROLE,
|
|
@@ -743,10 +744,14 @@ __export(index_exports, {
|
|
|
743
744
|
UnsafeApiUrlError: () => UnsafeApiUrlError,
|
|
744
745
|
WARNING_THRESHOLD: () => WARNING_THRESHOLD,
|
|
745
746
|
assertWithinBase: () => assertWithinBase,
|
|
747
|
+
bridgeWakeErrorMessage: () => bridgeWakeErrorMessage,
|
|
748
|
+
bridgeWakeLastSeenAgeMs: () => bridgeWakeLastSeenAgeMs,
|
|
746
749
|
buildApiUrl: () => buildApiUrl,
|
|
747
750
|
buildInboundSecurityAdvisory: () => buildInboundSecurityAdvisory,
|
|
748
751
|
classifyEmailRoute: () => classifyEmailRoute,
|
|
752
|
+
classifyResumeError: () => classifyResumeError,
|
|
749
753
|
closeDatabase: () => closeDatabase,
|
|
754
|
+
composeBridgeWakePrompt: () => composeBridgeWakePrompt,
|
|
750
755
|
createTestDatabase: () => createTestDatabase,
|
|
751
756
|
debug: () => debug,
|
|
752
757
|
debugWarn: () => debugWarn,
|
|
@@ -769,6 +774,7 @@ __export(index_exports, {
|
|
|
769
774
|
operatorPrefsStoragePath: () => operatorPrefsStoragePath,
|
|
770
775
|
parseEmail: () => parseEmail,
|
|
771
776
|
parseGoogleVoiceSms: () => parseGoogleVoiceSms,
|
|
777
|
+
planBridgeWake: () => planBridgeWake,
|
|
772
778
|
recordToolCall: () => recordToolCall,
|
|
773
779
|
redactObject: () => redactObject,
|
|
774
780
|
redactSecret: () => redactSecret,
|
|
@@ -782,6 +788,7 @@ __export(index_exports, {
|
|
|
782
788
|
scoreEmail: () => scoreEmail,
|
|
783
789
|
setOperatorEmail: () => setOperatorEmail,
|
|
784
790
|
setTelemetryVersion: () => setTelemetryVersion,
|
|
791
|
+
shouldSkipBridgeWakeForLiveOperator: () => shouldSkipBridgeWakeForLiveOperator,
|
|
785
792
|
startRelayBridge: () => startRelayBridge,
|
|
786
793
|
threadIdFor: () => threadIdFor,
|
|
787
794
|
tryJoin: () => tryJoin,
|
|
@@ -805,8 +812,7 @@ var MailSender = class {
|
|
|
805
812
|
pass: options.password
|
|
806
813
|
},
|
|
807
814
|
tls: {
|
|
808
|
-
rejectUnauthorized:
|
|
809
|
-
// Local dev — no TLS
|
|
815
|
+
rejectUnauthorized: options.tlsRejectUnauthorized ?? true
|
|
810
816
|
},
|
|
811
817
|
connectionTimeout: 1e4,
|
|
812
818
|
// 10s to establish TCP connection
|
|
@@ -2074,7 +2080,13 @@ function rowToAgent(row) {
|
|
|
2074
2080
|
// Old rows (pre-migration-016) have undefined `wake_on_cc`;
|
|
2075
2081
|
// treat that as the default-true (respect sender's wake list
|
|
2076
2082
|
// as-is). Only explicit 0 disables CC wakes for this agent.
|
|
2077
|
-
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
|
|
2078
2090
|
};
|
|
2079
2091
|
}
|
|
2080
2092
|
var AccountManager = class {
|
|
@@ -3692,6 +3704,22 @@ ALTER TABLE drafts ADD COLUMN attachments TEXT;
|
|
|
3692
3704
|
-- explicitly named" preference from the wake-thrash feedback.
|
|
3693
3705
|
-- Defaults to 1 (respect the senders wake list as-is).
|
|
3694
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;
|
|
3695
3723
|
`
|
|
3696
3724
|
};
|
|
3697
3725
|
function runMigrations(database) {
|
|
@@ -3965,9 +3993,67 @@ function formatPollError(err) {
|
|
|
3965
3993
|
const stdout = (e.stdout ?? "").toString().trim();
|
|
3966
3994
|
if (stdout && !stderr) parts.push(`stdout=${truncate(stdout, 240)}`);
|
|
3967
3995
|
if (parts.length === 1 && /^command failed$/i.test(head)) {
|
|
3968
|
-
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)`);
|
|
3969
3997
|
}
|
|
3970
|
-
return parts.join(" | ");
|
|
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}`;
|
|
4049
|
+
}
|
|
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(" ");
|
|
3971
4057
|
}
|
|
3972
4058
|
function truncate(s, max) {
|
|
3973
4059
|
if (s.length <= max) return s;
|
|
@@ -4022,7 +4108,11 @@ var RelayGateway = class {
|
|
|
4022
4108
|
pass: config.password
|
|
4023
4109
|
}
|
|
4024
4110
|
});
|
|
4025
|
-
|
|
4111
|
+
try {
|
|
4112
|
+
await this.smtpTransport.verify();
|
|
4113
|
+
} catch (err) {
|
|
4114
|
+
throw new Error(formatRelayError(err, config, "SMTP verification"));
|
|
4115
|
+
}
|
|
4026
4116
|
const imap = new import_imapflow3.ImapFlow({
|
|
4027
4117
|
host: config.imapHost,
|
|
4028
4118
|
port: config.imapPort,
|
|
@@ -4033,8 +4123,16 @@ var RelayGateway = class {
|
|
|
4033
4123
|
},
|
|
4034
4124
|
logger: false
|
|
4035
4125
|
});
|
|
4036
|
-
|
|
4037
|
-
|
|
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
|
+
}
|
|
4038
4136
|
}
|
|
4039
4137
|
/**
|
|
4040
4138
|
* Send an email through the relay SMTP server.
|
|
@@ -4044,9 +4142,10 @@ var RelayGateway = class {
|
|
|
4044
4142
|
if (!this.config || !this.smtpTransport) {
|
|
4045
4143
|
throw new Error("Relay not configured. Call setup() first.");
|
|
4046
4144
|
}
|
|
4047
|
-
const
|
|
4048
|
-
const
|
|
4049
|
-
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);
|
|
4050
4149
|
const relayFrom = `${localPart2}+${agentName}@${domain}`;
|
|
4051
4150
|
const displayName = mail.fromName || agentName;
|
|
4052
4151
|
const mailOpts = {
|
|
@@ -4070,7 +4169,12 @@ var RelayGateway = class {
|
|
|
4070
4169
|
};
|
|
4071
4170
|
const composer = new import_mail_composer2.default(mailOpts);
|
|
4072
4171
|
const raw = await composer.compile().build();
|
|
4073
|
-
|
|
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
|
+
}
|
|
4074
4178
|
if (result.messageId) {
|
|
4075
4179
|
this.sentMessageIds.set(result.messageId, agentName);
|
|
4076
4180
|
if (this.sentMessageIds.size > 1e4) {
|
|
@@ -4147,7 +4251,7 @@ var RelayGateway = class {
|
|
|
4147
4251
|
this.consecutiveFailures = 0;
|
|
4148
4252
|
} catch (err) {
|
|
4149
4253
|
this.consecutiveFailures++;
|
|
4150
|
-
const msg = formatPollError(err);
|
|
4254
|
+
const msg = this.config ? formatRelayError(err, this.config, "IMAP poll") : formatPollError(err);
|
|
4151
4255
|
console.error(`[RelayGateway] Poll failed (attempt ${this.consecutiveFailures}): ${msg}`);
|
|
4152
4256
|
if (this.consecutiveFailures >= 5 && this.consecutiveFailures % 5 === 0) {
|
|
4153
4257
|
console.error(`[RelayGateway] ${this.consecutiveFailures} consecutive failures \u2014 check IMAP credentials and connectivity (${this.config?.imapHost}:${this.config?.imapPort})`);
|
|
@@ -4380,13 +4484,14 @@ var RelayGateway = class {
|
|
|
4380
4484
|
*/
|
|
4381
4485
|
async searchRelay(criteria, maxResults = 50) {
|
|
4382
4486
|
if (!this.config) throw new Error("Relay not configured");
|
|
4487
|
+
const relayConfig = this.config;
|
|
4383
4488
|
const imap = new import_imapflow3.ImapFlow({
|
|
4384
|
-
host:
|
|
4385
|
-
port:
|
|
4386
|
-
secure:
|
|
4489
|
+
host: relayConfig.imapHost,
|
|
4490
|
+
port: relayConfig.imapPort,
|
|
4491
|
+
secure: relayConfig.imapPort === 993,
|
|
4387
4492
|
auth: {
|
|
4388
|
-
user:
|
|
4389
|
-
pass:
|
|
4493
|
+
user: relayConfig.email,
|
|
4494
|
+
pass: relayConfig.password
|
|
4390
4495
|
},
|
|
4391
4496
|
logger: false
|
|
4392
4497
|
});
|
|
@@ -4423,7 +4528,7 @@ var RelayGateway = class {
|
|
|
4423
4528
|
results.push({
|
|
4424
4529
|
uid,
|
|
4425
4530
|
source: "relay",
|
|
4426
|
-
account:
|
|
4531
|
+
account: relayConfig.email,
|
|
4427
4532
|
messageId: envelope.messageId ?? "",
|
|
4428
4533
|
subject: envelope.subject ?? "",
|
|
4429
4534
|
from: (envelope.from ?? []).map((a) => ({ name: a.name, address: a.address ?? "" })),
|
|
@@ -4439,7 +4544,7 @@ var RelayGateway = class {
|
|
|
4439
4544
|
lock.release();
|
|
4440
4545
|
}
|
|
4441
4546
|
} catch (err) {
|
|
4442
|
-
console.error("[RelayGateway] Relay search failed:", err
|
|
4547
|
+
console.error("[RelayGateway] Relay search failed:", formatRelayError(err, relayConfig, "IMAP search"));
|
|
4443
4548
|
return [];
|
|
4444
4549
|
} finally {
|
|
4445
4550
|
try {
|
|
@@ -4454,13 +4559,14 @@ var RelayGateway = class {
|
|
|
4454
4559
|
*/
|
|
4455
4560
|
async fetchRelayMessage(uid) {
|
|
4456
4561
|
if (!this.config) throw new Error("Relay not configured");
|
|
4562
|
+
const relayConfig = this.config;
|
|
4457
4563
|
const imap = new import_imapflow3.ImapFlow({
|
|
4458
|
-
host:
|
|
4459
|
-
port:
|
|
4460
|
-
secure:
|
|
4564
|
+
host: relayConfig.imapHost,
|
|
4565
|
+
port: relayConfig.imapPort,
|
|
4566
|
+
secure: relayConfig.imapPort === 993,
|
|
4461
4567
|
auth: {
|
|
4462
|
-
user:
|
|
4463
|
-
pass:
|
|
4568
|
+
user: relayConfig.email,
|
|
4569
|
+
pass: relayConfig.password
|
|
4464
4570
|
},
|
|
4465
4571
|
logger: false
|
|
4466
4572
|
});
|
|
@@ -4497,7 +4603,7 @@ var RelayGateway = class {
|
|
|
4497
4603
|
lock.release();
|
|
4498
4604
|
}
|
|
4499
4605
|
} catch (err) {
|
|
4500
|
-
console.error("[RelayGateway] Fetch relay message failed:", err
|
|
4606
|
+
console.error("[RelayGateway] Fetch relay message failed:", formatRelayError(err, relayConfig, "IMAP fetch"));
|
|
4501
4607
|
return null;
|
|
4502
4608
|
} finally {
|
|
4503
4609
|
try {
|
|
@@ -7233,6 +7339,95 @@ function hostSessionStoragePath() {
|
|
|
7233
7339
|
return storagePath();
|
|
7234
7340
|
}
|
|
7235
7341
|
|
|
7342
|
+
// src/host-bridge.ts
|
|
7343
|
+
var BRIDGE_OPERATOR_LIVE_WINDOW_MS = 3e4;
|
|
7344
|
+
var DEFAULT_EXPIRED_MARKERS = [
|
|
7345
|
+
"session not found",
|
|
7346
|
+
"invalid session",
|
|
7347
|
+
"session expired",
|
|
7348
|
+
"no such session",
|
|
7349
|
+
"unknown session",
|
|
7350
|
+
"thread not found",
|
|
7351
|
+
"invalid thread",
|
|
7352
|
+
"thread expired",
|
|
7353
|
+
"no such thread",
|
|
7354
|
+
"unknown thread"
|
|
7355
|
+
];
|
|
7356
|
+
var DEFAULT_SDK_MISSING_MARKERS = [
|
|
7357
|
+
"cannot find module",
|
|
7358
|
+
"could not be found",
|
|
7359
|
+
"command not found"
|
|
7360
|
+
];
|
|
7361
|
+
function bridgeWakeErrorMessage(err) {
|
|
7362
|
+
return err?.message ?? String(err);
|
|
7363
|
+
}
|
|
7364
|
+
function classifyResumeError(err, options = {}) {
|
|
7365
|
+
const msg = bridgeWakeErrorMessage(err).toLowerCase();
|
|
7366
|
+
const expiredMarkers = options.expiredMarkers ?? DEFAULT_EXPIRED_MARKERS;
|
|
7367
|
+
const sdkMissingMarkers = options.sdkMissingMarkers ?? DEFAULT_SDK_MISSING_MARKERS;
|
|
7368
|
+
if (expiredMarkers.some((marker) => msg.includes(marker))) return "session-expired";
|
|
7369
|
+
if (sdkMissingMarkers.some((marker) => msg.includes(marker))) return "sdk-missing";
|
|
7370
|
+
return "other";
|
|
7371
|
+
}
|
|
7372
|
+
function bridgeWakeLastSeenAgeMs(session, nowMs = Date.now()) {
|
|
7373
|
+
if (!session) return null;
|
|
7374
|
+
return nowMs - session.lastSeenMs;
|
|
7375
|
+
}
|
|
7376
|
+
function shouldSkipBridgeWakeForLiveOperator(session, nowMs = Date.now(), liveWindowMs = BRIDGE_OPERATOR_LIVE_WINDOW_MS) {
|
|
7377
|
+
const ageMs = bridgeWakeLastSeenAgeMs(session, nowMs);
|
|
7378
|
+
return ageMs !== null && ageMs < liveWindowMs;
|
|
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
|
+
}
|
|
7406
|
+
function composeBridgeWakePrompt(args) {
|
|
7407
|
+
const subject = args.subject ?? "(no subject)";
|
|
7408
|
+
const from = args.from ?? "unknown";
|
|
7409
|
+
const preview = (args.preview ?? "").slice(0, 600);
|
|
7410
|
+
return [
|
|
7411
|
+
`\u{1F380} Bridge mail arrived \u2014 headless wake.`,
|
|
7412
|
+
"",
|
|
7413
|
+
`You are being resumed against your last session because new mail landed in your bridge inbox (${args.bridgeName}@localhost) and you weren't actively at the keyboard.`,
|
|
7414
|
+
"",
|
|
7415
|
+
`Trigger:`,
|
|
7416
|
+
` UID: ${args.uid}`,
|
|
7417
|
+
` From: ${from}`,
|
|
7418
|
+
` Subject: ${subject}`,
|
|
7419
|
+
` Preview: ${preview}`,
|
|
7420
|
+
"",
|
|
7421
|
+
`Read it with mcp__agenticmail__read_email({ uid: ${args.uid} }) and decide:`,
|
|
7422
|
+
` \xB7 Does it need a reply from YOU (the operator's session)? Reply via mcp__agenticmail__reply_email.`,
|
|
7423
|
+
` \xB7 Does it need a teammate to act? Forward / re-route by replying with wake: ["<teammate>"].`,
|
|
7424
|
+
` \xB7 Is it [NEEDS OPERATOR] / [BLOCKED]? Then it's actually for the human \u2014 mark it unread, and the operator will see it on their next keystroke.`,
|
|
7425
|
+
` \xB7 Is it FYI noise? mark_read and exit.`,
|
|
7426
|
+
"",
|
|
7427
|
+
`Keep this turn SHORT. You're being resumed to handle ONE piece of mail, not to continue the prior conversation.`
|
|
7428
|
+
].join("\n");
|
|
7429
|
+
}
|
|
7430
|
+
|
|
7236
7431
|
// src/util/safe-url.ts
|
|
7237
7432
|
var UnsafeApiUrlError = class extends Error {
|
|
7238
7433
|
constructor(raw, reason) {
|
|
@@ -8910,6 +9105,7 @@ function parse(raw) {
|
|
|
8910
9105
|
AgentDeletionService,
|
|
8911
9106
|
AgentMemoryStore,
|
|
8912
9107
|
AgenticMailClient,
|
|
9108
|
+
BRIDGE_OPERATOR_LIVE_WINDOW_MS,
|
|
8913
9109
|
CloudflareClient,
|
|
8914
9110
|
DEFAULT_AGENT_NAME,
|
|
8915
9111
|
DEFAULT_AGENT_ROLE,
|
|
@@ -8940,10 +9136,14 @@ function parse(raw) {
|
|
|
8940
9136
|
UnsafeApiUrlError,
|
|
8941
9137
|
WARNING_THRESHOLD,
|
|
8942
9138
|
assertWithinBase,
|
|
9139
|
+
bridgeWakeErrorMessage,
|
|
9140
|
+
bridgeWakeLastSeenAgeMs,
|
|
8943
9141
|
buildApiUrl,
|
|
8944
9142
|
buildInboundSecurityAdvisory,
|
|
8945
9143
|
classifyEmailRoute,
|
|
9144
|
+
classifyResumeError,
|
|
8946
9145
|
closeDatabase,
|
|
9146
|
+
composeBridgeWakePrompt,
|
|
8947
9147
|
createTestDatabase,
|
|
8948
9148
|
debug,
|
|
8949
9149
|
debugWarn,
|
|
@@ -8966,6 +9166,7 @@ function parse(raw) {
|
|
|
8966
9166
|
operatorPrefsStoragePath,
|
|
8967
9167
|
parseEmail,
|
|
8968
9168
|
parseGoogleVoiceSms,
|
|
9169
|
+
planBridgeWake,
|
|
8969
9170
|
recordToolCall,
|
|
8970
9171
|
redactObject,
|
|
8971
9172
|
redactSecret,
|
|
@@ -8979,6 +9180,7 @@ function parse(raw) {
|
|
|
8979
9180
|
scoreEmail,
|
|
8980
9181
|
setOperatorEmail,
|
|
8981
9182
|
setTelemetryVersion,
|
|
9183
|
+
shouldSkipBridgeWakeForLiveOperator,
|
|
8982
9184
|
startRelayBridge,
|
|
8983
9185
|
threadIdFor,
|
|
8984
9186
|
tryJoin,
|
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;
|
|
@@ -481,6 +499,7 @@ interface MailSenderOptions {
|
|
|
481
499
|
password: string;
|
|
482
500
|
authUser?: string;
|
|
483
501
|
secure?: boolean;
|
|
502
|
+
tlsRejectUnauthorized?: boolean;
|
|
484
503
|
}
|
|
485
504
|
interface SendResultWithRaw extends SendResult {
|
|
486
505
|
/** Raw RFC822 message bytes (for appending to Sent folder) */
|
|
@@ -1899,8 +1918,8 @@ declare function operatorPrefsStoragePath(): string;
|
|
|
1899
1918
|
*
|
|
1900
1919
|
* # What this is for
|
|
1901
1920
|
*
|
|
1902
|
-
* When a sub-agent replies into
|
|
1903
|
-
* (`claudecode@localhost` / `codex@localhost`), the dispatcher
|
|
1921
|
+
* When a sub-agent replies into a host bridge inbox
|
|
1922
|
+
* (`claudecode@localhost` / `codex@localhost` / etc.), the dispatcher
|
|
1904
1923
|
* historically had no way to react: bridges are skipped by
|
|
1905
1924
|
* `shouldWatch` because they belong to the human operator's host CLI,
|
|
1906
1925
|
* not to an automated worker. The mail would sit unread until the
|
|
@@ -1933,6 +1952,12 @@ declare function operatorPrefsStoragePath(): string;
|
|
|
1933
1952
|
* "sessionId": "019a2b3c-…",
|
|
1934
1953
|
* "workspace": "/Users/ope/Desktop/facebook-project",
|
|
1935
1954
|
* "lastSeenMs": 1778905100000
|
|
1955
|
+
* },
|
|
1956
|
+
* "openclaw": {
|
|
1957
|
+
* "sessionId": "openclaw-session-key",
|
|
1958
|
+
* "workspace": "/Users/ope/Desktop/facebook-project",
|
|
1959
|
+
* "lastSeenMs": 1778905000000,
|
|
1960
|
+
* "resumeMode": "wake"
|
|
1936
1961
|
* }
|
|
1937
1962
|
* }
|
|
1938
1963
|
* }
|
|
@@ -1963,14 +1988,24 @@ declare function operatorPrefsStoragePath(): string;
|
|
|
1963
1988
|
* that crashes the next reader. Same shape as `dispatcher-state.ts`.
|
|
1964
1989
|
*/
|
|
1965
1990
|
/** Canonical names for the host integrations that own bridge inboxes. */
|
|
1966
|
-
type HostName = 'claudecode' | 'codex';
|
|
1991
|
+
type HostName = 'claudecode' | 'codex' | 'openclaw' | 'gemini' | 'hermes';
|
|
1992
|
+
/**
|
|
1993
|
+
* How a host can be woken from a persisted session record.
|
|
1994
|
+
*
|
|
1995
|
+
* - `resume`: the host can resume a durable prior conversation/thread.
|
|
1996
|
+
* - `wake`: the host can target a live or recently known session key, but does
|
|
1997
|
+
* not guarantee full headless resume semantics.
|
|
1998
|
+
* - `wake-only`: the host can receive a wake notification, but the dispatcher
|
|
1999
|
+
* must not treat it as a resumed worker turn.
|
|
2000
|
+
*/
|
|
2001
|
+
type HostSessionResumeMode = 'resume' | 'wake' | 'wake-only';
|
|
1967
2002
|
/**
|
|
1968
2003
|
* A snapshot of one host CLI's last-known session. Persisted to disk
|
|
1969
2004
|
* by the mail-hook on every fire; loaded by the dispatcher when
|
|
1970
2005
|
* bridge mail arrives so a resume can be attempted.
|
|
1971
2006
|
*/
|
|
1972
2007
|
interface HostSession {
|
|
1973
|
-
/** Stable session_id from the host CLI
|
|
2008
|
+
/** Stable session_id/thread_id/session key from the host CLI/runtime. */
|
|
1974
2009
|
sessionId: string;
|
|
1975
2010
|
/** Wall-clock timestamp of the last hook fire on this session. */
|
|
1976
2011
|
lastSeenMs: number;
|
|
@@ -1980,6 +2015,10 @@ interface HostSession {
|
|
|
1980
2015
|
/** Optional: model name the host session was using, surfaced
|
|
1981
2016
|
* in logs for diagnostic context. */
|
|
1982
2017
|
model?: string;
|
|
2018
|
+
/** Optional: describes whether this host supports true resume or only wake. */
|
|
2019
|
+
resumeMode?: HostSessionResumeMode;
|
|
2020
|
+
/** Optional host-specific metadata. Must not contain secrets. */
|
|
2021
|
+
hostMetadata?: Record<string, unknown>;
|
|
1983
2022
|
}
|
|
1984
2023
|
/** Default freshness window — sessions older than this are skipped. */
|
|
1985
2024
|
declare const DEFAULT_SESSION_MAX_AGE_MS: number;
|
|
@@ -2017,6 +2056,62 @@ declare function forgetHostSession(host: HostName): void;
|
|
|
2017
2056
|
/** Exposed for tests + the `agenticmail status` diagnostic command. */
|
|
2018
2057
|
declare function hostSessionStoragePath(): string;
|
|
2019
2058
|
|
|
2059
|
+
type BridgeWakeError = 'session-expired' | 'sdk-missing' | 'timeout' | 'other';
|
|
2060
|
+
interface BridgeWakeResult {
|
|
2061
|
+
ok: boolean;
|
|
2062
|
+
text?: string;
|
|
2063
|
+
error?: BridgeWakeError;
|
|
2064
|
+
errorMessage?: string;
|
|
2065
|
+
durationMs?: number;
|
|
2066
|
+
}
|
|
2067
|
+
interface BridgeWakePromptArgs {
|
|
2068
|
+
bridgeName: string;
|
|
2069
|
+
uid: number;
|
|
2070
|
+
subject?: string;
|
|
2071
|
+
from?: string;
|
|
2072
|
+
preview?: string;
|
|
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
|
+
}
|
|
2097
|
+
interface ResumeErrorClassificationOptions {
|
|
2098
|
+
expiredMarkers?: readonly string[];
|
|
2099
|
+
sdkMissingMarkers?: readonly string[];
|
|
2100
|
+
}
|
|
2101
|
+
declare const BRIDGE_OPERATOR_LIVE_WINDOW_MS = 30000;
|
|
2102
|
+
declare function bridgeWakeErrorMessage(err: unknown): string;
|
|
2103
|
+
declare function classifyResumeError(err: unknown, options?: ResumeErrorClassificationOptions): BridgeWakeError;
|
|
2104
|
+
declare function bridgeWakeLastSeenAgeMs(session: Pick<HostSession, 'lastSeenMs'> | null | undefined, nowMs?: number): number | null;
|
|
2105
|
+
declare function shouldSkipBridgeWakeForLiveOperator(session: Pick<HostSession, 'lastSeenMs'> | null | undefined, nowMs?: number, liveWindowMs?: number): boolean;
|
|
2106
|
+
declare function planBridgeWake(args: PlanBridgeWakeArgs): BridgeWakeRoute;
|
|
2107
|
+
/**
|
|
2108
|
+
* Build the prompt a host session sees on bridge wake. Host adapters
|
|
2109
|
+
* keep their own SDK resume call, but share this operator-facing
|
|
2110
|
+
* instruction shape so Claude Code, Codex, OpenClaw and later hosts
|
|
2111
|
+
* do not drift semantically.
|
|
2112
|
+
*/
|
|
2113
|
+
declare function composeBridgeWakePrompt(args: BridgeWakePromptArgs): string;
|
|
2114
|
+
|
|
2020
2115
|
/**
|
|
2021
2116
|
* SSRF-safe URL validation for the AgenticMail API base URL.
|
|
2022
2117
|
*
|
|
@@ -2624,4 +2719,4 @@ declare class AgentMemoryStore {
|
|
|
2624
2719
|
renderForPrompt(memory: AgentMemoryRead | null): string;
|
|
2625
2720
|
}
|
|
2626
2721
|
|
|
2627
|
-
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, 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 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, 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, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, closeDatabase, 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, 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;
|
|
@@ -481,6 +499,7 @@ interface MailSenderOptions {
|
|
|
481
499
|
password: string;
|
|
482
500
|
authUser?: string;
|
|
483
501
|
secure?: boolean;
|
|
502
|
+
tlsRejectUnauthorized?: boolean;
|
|
484
503
|
}
|
|
485
504
|
interface SendResultWithRaw extends SendResult {
|
|
486
505
|
/** Raw RFC822 message bytes (for appending to Sent folder) */
|
|
@@ -1899,8 +1918,8 @@ declare function operatorPrefsStoragePath(): string;
|
|
|
1899
1918
|
*
|
|
1900
1919
|
* # What this is for
|
|
1901
1920
|
*
|
|
1902
|
-
* When a sub-agent replies into
|
|
1903
|
-
* (`claudecode@localhost` / `codex@localhost`), the dispatcher
|
|
1921
|
+
* When a sub-agent replies into a host bridge inbox
|
|
1922
|
+
* (`claudecode@localhost` / `codex@localhost` / etc.), the dispatcher
|
|
1904
1923
|
* historically had no way to react: bridges are skipped by
|
|
1905
1924
|
* `shouldWatch` because they belong to the human operator's host CLI,
|
|
1906
1925
|
* not to an automated worker. The mail would sit unread until the
|
|
@@ -1933,6 +1952,12 @@ declare function operatorPrefsStoragePath(): string;
|
|
|
1933
1952
|
* "sessionId": "019a2b3c-…",
|
|
1934
1953
|
* "workspace": "/Users/ope/Desktop/facebook-project",
|
|
1935
1954
|
* "lastSeenMs": 1778905100000
|
|
1955
|
+
* },
|
|
1956
|
+
* "openclaw": {
|
|
1957
|
+
* "sessionId": "openclaw-session-key",
|
|
1958
|
+
* "workspace": "/Users/ope/Desktop/facebook-project",
|
|
1959
|
+
* "lastSeenMs": 1778905000000,
|
|
1960
|
+
* "resumeMode": "wake"
|
|
1936
1961
|
* }
|
|
1937
1962
|
* }
|
|
1938
1963
|
* }
|
|
@@ -1963,14 +1988,24 @@ declare function operatorPrefsStoragePath(): string;
|
|
|
1963
1988
|
* that crashes the next reader. Same shape as `dispatcher-state.ts`.
|
|
1964
1989
|
*/
|
|
1965
1990
|
/** Canonical names for the host integrations that own bridge inboxes. */
|
|
1966
|
-
type HostName = 'claudecode' | 'codex';
|
|
1991
|
+
type HostName = 'claudecode' | 'codex' | 'openclaw' | 'gemini' | 'hermes';
|
|
1992
|
+
/**
|
|
1993
|
+
* How a host can be woken from a persisted session record.
|
|
1994
|
+
*
|
|
1995
|
+
* - `resume`: the host can resume a durable prior conversation/thread.
|
|
1996
|
+
* - `wake`: the host can target a live or recently known session key, but does
|
|
1997
|
+
* not guarantee full headless resume semantics.
|
|
1998
|
+
* - `wake-only`: the host can receive a wake notification, but the dispatcher
|
|
1999
|
+
* must not treat it as a resumed worker turn.
|
|
2000
|
+
*/
|
|
2001
|
+
type HostSessionResumeMode = 'resume' | 'wake' | 'wake-only';
|
|
1967
2002
|
/**
|
|
1968
2003
|
* A snapshot of one host CLI's last-known session. Persisted to disk
|
|
1969
2004
|
* by the mail-hook on every fire; loaded by the dispatcher when
|
|
1970
2005
|
* bridge mail arrives so a resume can be attempted.
|
|
1971
2006
|
*/
|
|
1972
2007
|
interface HostSession {
|
|
1973
|
-
/** Stable session_id from the host CLI
|
|
2008
|
+
/** Stable session_id/thread_id/session key from the host CLI/runtime. */
|
|
1974
2009
|
sessionId: string;
|
|
1975
2010
|
/** Wall-clock timestamp of the last hook fire on this session. */
|
|
1976
2011
|
lastSeenMs: number;
|
|
@@ -1980,6 +2015,10 @@ interface HostSession {
|
|
|
1980
2015
|
/** Optional: model name the host session was using, surfaced
|
|
1981
2016
|
* in logs for diagnostic context. */
|
|
1982
2017
|
model?: string;
|
|
2018
|
+
/** Optional: describes whether this host supports true resume or only wake. */
|
|
2019
|
+
resumeMode?: HostSessionResumeMode;
|
|
2020
|
+
/** Optional host-specific metadata. Must not contain secrets. */
|
|
2021
|
+
hostMetadata?: Record<string, unknown>;
|
|
1983
2022
|
}
|
|
1984
2023
|
/** Default freshness window — sessions older than this are skipped. */
|
|
1985
2024
|
declare const DEFAULT_SESSION_MAX_AGE_MS: number;
|
|
@@ -2017,6 +2056,62 @@ declare function forgetHostSession(host: HostName): void;
|
|
|
2017
2056
|
/** Exposed for tests + the `agenticmail status` diagnostic command. */
|
|
2018
2057
|
declare function hostSessionStoragePath(): string;
|
|
2019
2058
|
|
|
2059
|
+
type BridgeWakeError = 'session-expired' | 'sdk-missing' | 'timeout' | 'other';
|
|
2060
|
+
interface BridgeWakeResult {
|
|
2061
|
+
ok: boolean;
|
|
2062
|
+
text?: string;
|
|
2063
|
+
error?: BridgeWakeError;
|
|
2064
|
+
errorMessage?: string;
|
|
2065
|
+
durationMs?: number;
|
|
2066
|
+
}
|
|
2067
|
+
interface BridgeWakePromptArgs {
|
|
2068
|
+
bridgeName: string;
|
|
2069
|
+
uid: number;
|
|
2070
|
+
subject?: string;
|
|
2071
|
+
from?: string;
|
|
2072
|
+
preview?: string;
|
|
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
|
+
}
|
|
2097
|
+
interface ResumeErrorClassificationOptions {
|
|
2098
|
+
expiredMarkers?: readonly string[];
|
|
2099
|
+
sdkMissingMarkers?: readonly string[];
|
|
2100
|
+
}
|
|
2101
|
+
declare const BRIDGE_OPERATOR_LIVE_WINDOW_MS = 30000;
|
|
2102
|
+
declare function bridgeWakeErrorMessage(err: unknown): string;
|
|
2103
|
+
declare function classifyResumeError(err: unknown, options?: ResumeErrorClassificationOptions): BridgeWakeError;
|
|
2104
|
+
declare function bridgeWakeLastSeenAgeMs(session: Pick<HostSession, 'lastSeenMs'> | null | undefined, nowMs?: number): number | null;
|
|
2105
|
+
declare function shouldSkipBridgeWakeForLiveOperator(session: Pick<HostSession, 'lastSeenMs'> | null | undefined, nowMs?: number, liveWindowMs?: number): boolean;
|
|
2106
|
+
declare function planBridgeWake(args: PlanBridgeWakeArgs): BridgeWakeRoute;
|
|
2107
|
+
/**
|
|
2108
|
+
* Build the prompt a host session sees on bridge wake. Host adapters
|
|
2109
|
+
* keep their own SDK resume call, but share this operator-facing
|
|
2110
|
+
* instruction shape so Claude Code, Codex, OpenClaw and later hosts
|
|
2111
|
+
* do not drift semantically.
|
|
2112
|
+
*/
|
|
2113
|
+
declare function composeBridgeWakePrompt(args: BridgeWakePromptArgs): string;
|
|
2114
|
+
|
|
2020
2115
|
/**
|
|
2021
2116
|
* SSRF-safe URL validation for the AgenticMail API base URL.
|
|
2022
2117
|
*
|
|
@@ -2624,4 +2719,4 @@ declare class AgentMemoryStore {
|
|
|
2624
2719
|
renderForPrompt(memory: AgentMemoryRead | null): string;
|
|
2625
2720
|
}
|
|
2626
2721
|
|
|
2627
|
-
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, 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 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, 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, buildApiUrl, buildInboundSecurityAdvisory, classifyEmailRoute, closeDatabase, 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, 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
|
@@ -24,8 +24,7 @@ var MailSender = class {
|
|
|
24
24
|
pass: options.password
|
|
25
25
|
},
|
|
26
26
|
tls: {
|
|
27
|
-
rejectUnauthorized:
|
|
28
|
-
// Local dev — no TLS
|
|
27
|
+
rejectUnauthorized: options.tlsRejectUnauthorized ?? true
|
|
29
28
|
},
|
|
30
29
|
connectionTimeout: 1e4,
|
|
31
30
|
// 10s to establish TCP connection
|
|
@@ -1293,7 +1292,13 @@ function rowToAgent(row) {
|
|
|
1293
1292
|
// Old rows (pre-migration-016) have undefined `wake_on_cc`;
|
|
1294
1293
|
// treat that as the default-true (respect sender's wake list
|
|
1295
1294
|
// as-is). Only explicit 0 disables CC wakes for this agent.
|
|
1296
|
-
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
|
|
1297
1302
|
};
|
|
1298
1303
|
}
|
|
1299
1304
|
var AccountManager = class {
|
|
@@ -2907,6 +2912,22 @@ ALTER TABLE drafts ADD COLUMN attachments TEXT;
|
|
|
2907
2912
|
-- explicitly named" preference from the wake-thrash feedback.
|
|
2908
2913
|
-- Defaults to 1 (respect the senders wake list as-is).
|
|
2909
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;
|
|
2910
2931
|
`
|
|
2911
2932
|
};
|
|
2912
2933
|
function runMigrations(database) {
|
|
@@ -3180,9 +3201,67 @@ function formatPollError(err) {
|
|
|
3180
3201
|
const stdout = (e.stdout ?? "").toString().trim();
|
|
3181
3202
|
if (stdout && !stderr) parts.push(`stdout=${truncate(stdout, 240)}`);
|
|
3182
3203
|
if (parts.length === 1 && /^command failed$/i.test(head)) {
|
|
3183
|
-
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)`);
|
|
3184
3205
|
}
|
|
3185
|
-
return parts.join(" | ");
|
|
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}`;
|
|
3257
|
+
}
|
|
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(" ");
|
|
3186
3265
|
}
|
|
3187
3266
|
function truncate(s, max) {
|
|
3188
3267
|
if (s.length <= max) return s;
|
|
@@ -3237,7 +3316,11 @@ var RelayGateway = class {
|
|
|
3237
3316
|
pass: config.password
|
|
3238
3317
|
}
|
|
3239
3318
|
});
|
|
3240
|
-
|
|
3319
|
+
try {
|
|
3320
|
+
await this.smtpTransport.verify();
|
|
3321
|
+
} catch (err) {
|
|
3322
|
+
throw new Error(formatRelayError(err, config, "SMTP verification"));
|
|
3323
|
+
}
|
|
3241
3324
|
const imap = new ImapFlow3({
|
|
3242
3325
|
host: config.imapHost,
|
|
3243
3326
|
port: config.imapPort,
|
|
@@ -3248,8 +3331,16 @@ var RelayGateway = class {
|
|
|
3248
3331
|
},
|
|
3249
3332
|
logger: false
|
|
3250
3333
|
});
|
|
3251
|
-
|
|
3252
|
-
|
|
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
|
+
}
|
|
3253
3344
|
}
|
|
3254
3345
|
/**
|
|
3255
3346
|
* Send an email through the relay SMTP server.
|
|
@@ -3259,9 +3350,10 @@ var RelayGateway = class {
|
|
|
3259
3350
|
if (!this.config || !this.smtpTransport) {
|
|
3260
3351
|
throw new Error("Relay not configured. Call setup() first.");
|
|
3261
3352
|
}
|
|
3262
|
-
const
|
|
3263
|
-
const
|
|
3264
|
-
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);
|
|
3265
3357
|
const relayFrom = `${localPart2}+${agentName}@${domain}`;
|
|
3266
3358
|
const displayName = mail.fromName || agentName;
|
|
3267
3359
|
const mailOpts = {
|
|
@@ -3285,7 +3377,12 @@ var RelayGateway = class {
|
|
|
3285
3377
|
};
|
|
3286
3378
|
const composer = new MailComposer2(mailOpts);
|
|
3287
3379
|
const raw = await composer.compile().build();
|
|
3288
|
-
|
|
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
|
+
}
|
|
3289
3386
|
if (result.messageId) {
|
|
3290
3387
|
this.sentMessageIds.set(result.messageId, agentName);
|
|
3291
3388
|
if (this.sentMessageIds.size > 1e4) {
|
|
@@ -3362,7 +3459,7 @@ var RelayGateway = class {
|
|
|
3362
3459
|
this.consecutiveFailures = 0;
|
|
3363
3460
|
} catch (err) {
|
|
3364
3461
|
this.consecutiveFailures++;
|
|
3365
|
-
const msg = formatPollError(err);
|
|
3462
|
+
const msg = this.config ? formatRelayError(err, this.config, "IMAP poll") : formatPollError(err);
|
|
3366
3463
|
console.error(`[RelayGateway] Poll failed (attempt ${this.consecutiveFailures}): ${msg}`);
|
|
3367
3464
|
if (this.consecutiveFailures >= 5 && this.consecutiveFailures % 5 === 0) {
|
|
3368
3465
|
console.error(`[RelayGateway] ${this.consecutiveFailures} consecutive failures \u2014 check IMAP credentials and connectivity (${this.config?.imapHost}:${this.config?.imapPort})`);
|
|
@@ -3595,13 +3692,14 @@ var RelayGateway = class {
|
|
|
3595
3692
|
*/
|
|
3596
3693
|
async searchRelay(criteria, maxResults = 50) {
|
|
3597
3694
|
if (!this.config) throw new Error("Relay not configured");
|
|
3695
|
+
const relayConfig = this.config;
|
|
3598
3696
|
const imap = new ImapFlow3({
|
|
3599
|
-
host:
|
|
3600
|
-
port:
|
|
3601
|
-
secure:
|
|
3697
|
+
host: relayConfig.imapHost,
|
|
3698
|
+
port: relayConfig.imapPort,
|
|
3699
|
+
secure: relayConfig.imapPort === 993,
|
|
3602
3700
|
auth: {
|
|
3603
|
-
user:
|
|
3604
|
-
pass:
|
|
3701
|
+
user: relayConfig.email,
|
|
3702
|
+
pass: relayConfig.password
|
|
3605
3703
|
},
|
|
3606
3704
|
logger: false
|
|
3607
3705
|
});
|
|
@@ -3638,7 +3736,7 @@ var RelayGateway = class {
|
|
|
3638
3736
|
results.push({
|
|
3639
3737
|
uid,
|
|
3640
3738
|
source: "relay",
|
|
3641
|
-
account:
|
|
3739
|
+
account: relayConfig.email,
|
|
3642
3740
|
messageId: envelope.messageId ?? "",
|
|
3643
3741
|
subject: envelope.subject ?? "",
|
|
3644
3742
|
from: (envelope.from ?? []).map((a) => ({ name: a.name, address: a.address ?? "" })),
|
|
@@ -3654,7 +3752,7 @@ var RelayGateway = class {
|
|
|
3654
3752
|
lock.release();
|
|
3655
3753
|
}
|
|
3656
3754
|
} catch (err) {
|
|
3657
|
-
console.error("[RelayGateway] Relay search failed:", err
|
|
3755
|
+
console.error("[RelayGateway] Relay search failed:", formatRelayError(err, relayConfig, "IMAP search"));
|
|
3658
3756
|
return [];
|
|
3659
3757
|
} finally {
|
|
3660
3758
|
try {
|
|
@@ -3669,13 +3767,14 @@ var RelayGateway = class {
|
|
|
3669
3767
|
*/
|
|
3670
3768
|
async fetchRelayMessage(uid) {
|
|
3671
3769
|
if (!this.config) throw new Error("Relay not configured");
|
|
3770
|
+
const relayConfig = this.config;
|
|
3672
3771
|
const imap = new ImapFlow3({
|
|
3673
|
-
host:
|
|
3674
|
-
port:
|
|
3675
|
-
secure:
|
|
3772
|
+
host: relayConfig.imapHost,
|
|
3773
|
+
port: relayConfig.imapPort,
|
|
3774
|
+
secure: relayConfig.imapPort === 993,
|
|
3676
3775
|
auth: {
|
|
3677
|
-
user:
|
|
3678
|
-
pass:
|
|
3776
|
+
user: relayConfig.email,
|
|
3777
|
+
pass: relayConfig.password
|
|
3679
3778
|
},
|
|
3680
3779
|
logger: false
|
|
3681
3780
|
});
|
|
@@ -3712,7 +3811,7 @@ var RelayGateway = class {
|
|
|
3712
3811
|
lock.release();
|
|
3713
3812
|
}
|
|
3714
3813
|
} catch (err) {
|
|
3715
|
-
console.error("[RelayGateway] Fetch relay message failed:", err
|
|
3814
|
+
console.error("[RelayGateway] Fetch relay message failed:", formatRelayError(err, relayConfig, "IMAP fetch"));
|
|
3716
3815
|
return null;
|
|
3717
3816
|
} finally {
|
|
3718
3817
|
try {
|
|
@@ -6447,6 +6546,95 @@ function hostSessionStoragePath() {
|
|
|
6447
6546
|
return storagePath();
|
|
6448
6547
|
}
|
|
6449
6548
|
|
|
6549
|
+
// src/host-bridge.ts
|
|
6550
|
+
var BRIDGE_OPERATOR_LIVE_WINDOW_MS = 3e4;
|
|
6551
|
+
var DEFAULT_EXPIRED_MARKERS = [
|
|
6552
|
+
"session not found",
|
|
6553
|
+
"invalid session",
|
|
6554
|
+
"session expired",
|
|
6555
|
+
"no such session",
|
|
6556
|
+
"unknown session",
|
|
6557
|
+
"thread not found",
|
|
6558
|
+
"invalid thread",
|
|
6559
|
+
"thread expired",
|
|
6560
|
+
"no such thread",
|
|
6561
|
+
"unknown thread"
|
|
6562
|
+
];
|
|
6563
|
+
var DEFAULT_SDK_MISSING_MARKERS = [
|
|
6564
|
+
"cannot find module",
|
|
6565
|
+
"could not be found",
|
|
6566
|
+
"command not found"
|
|
6567
|
+
];
|
|
6568
|
+
function bridgeWakeErrorMessage(err) {
|
|
6569
|
+
return err?.message ?? String(err);
|
|
6570
|
+
}
|
|
6571
|
+
function classifyResumeError(err, options = {}) {
|
|
6572
|
+
const msg = bridgeWakeErrorMessage(err).toLowerCase();
|
|
6573
|
+
const expiredMarkers = options.expiredMarkers ?? DEFAULT_EXPIRED_MARKERS;
|
|
6574
|
+
const sdkMissingMarkers = options.sdkMissingMarkers ?? DEFAULT_SDK_MISSING_MARKERS;
|
|
6575
|
+
if (expiredMarkers.some((marker) => msg.includes(marker))) return "session-expired";
|
|
6576
|
+
if (sdkMissingMarkers.some((marker) => msg.includes(marker))) return "sdk-missing";
|
|
6577
|
+
return "other";
|
|
6578
|
+
}
|
|
6579
|
+
function bridgeWakeLastSeenAgeMs(session, nowMs = Date.now()) {
|
|
6580
|
+
if (!session) return null;
|
|
6581
|
+
return nowMs - session.lastSeenMs;
|
|
6582
|
+
}
|
|
6583
|
+
function shouldSkipBridgeWakeForLiveOperator(session, nowMs = Date.now(), liveWindowMs = BRIDGE_OPERATOR_LIVE_WINDOW_MS) {
|
|
6584
|
+
const ageMs = bridgeWakeLastSeenAgeMs(session, nowMs);
|
|
6585
|
+
return ageMs !== null && ageMs < liveWindowMs;
|
|
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
|
+
}
|
|
6613
|
+
function composeBridgeWakePrompt(args) {
|
|
6614
|
+
const subject = args.subject ?? "(no subject)";
|
|
6615
|
+
const from = args.from ?? "unknown";
|
|
6616
|
+
const preview = (args.preview ?? "").slice(0, 600);
|
|
6617
|
+
return [
|
|
6618
|
+
`\u{1F380} Bridge mail arrived \u2014 headless wake.`,
|
|
6619
|
+
"",
|
|
6620
|
+
`You are being resumed against your last session because new mail landed in your bridge inbox (${args.bridgeName}@localhost) and you weren't actively at the keyboard.`,
|
|
6621
|
+
"",
|
|
6622
|
+
`Trigger:`,
|
|
6623
|
+
` UID: ${args.uid}`,
|
|
6624
|
+
` From: ${from}`,
|
|
6625
|
+
` Subject: ${subject}`,
|
|
6626
|
+
` Preview: ${preview}`,
|
|
6627
|
+
"",
|
|
6628
|
+
`Read it with mcp__agenticmail__read_email({ uid: ${args.uid} }) and decide:`,
|
|
6629
|
+
` \xB7 Does it need a reply from YOU (the operator's session)? Reply via mcp__agenticmail__reply_email.`,
|
|
6630
|
+
` \xB7 Does it need a teammate to act? Forward / re-route by replying with wake: ["<teammate>"].`,
|
|
6631
|
+
` \xB7 Is it [NEEDS OPERATOR] / [BLOCKED]? Then it's actually for the human \u2014 mark it unread, and the operator will see it on their next keystroke.`,
|
|
6632
|
+
` \xB7 Is it FYI noise? mark_read and exit.`,
|
|
6633
|
+
"",
|
|
6634
|
+
`Keep this turn SHORT. You're being resumed to handle ONE piece of mail, not to continue the prior conversation.`
|
|
6635
|
+
].join("\n");
|
|
6636
|
+
}
|
|
6637
|
+
|
|
6450
6638
|
// src/util/safe-url.ts
|
|
6451
6639
|
var UnsafeApiUrlError = class extends Error {
|
|
6452
6640
|
constructor(raw, reason) {
|
|
@@ -8138,6 +8326,7 @@ export {
|
|
|
8138
8326
|
AgentDeletionService,
|
|
8139
8327
|
AgentMemoryStore,
|
|
8140
8328
|
AgenticMailClient,
|
|
8329
|
+
BRIDGE_OPERATOR_LIVE_WINDOW_MS,
|
|
8141
8330
|
CloudflareClient,
|
|
8142
8331
|
DEFAULT_AGENT_NAME,
|
|
8143
8332
|
DEFAULT_AGENT_ROLE,
|
|
@@ -8168,10 +8357,14 @@ export {
|
|
|
8168
8357
|
UnsafeApiUrlError,
|
|
8169
8358
|
WARNING_THRESHOLD,
|
|
8170
8359
|
assertWithinBase,
|
|
8360
|
+
bridgeWakeErrorMessage,
|
|
8361
|
+
bridgeWakeLastSeenAgeMs,
|
|
8171
8362
|
buildApiUrl,
|
|
8172
8363
|
buildInboundSecurityAdvisory,
|
|
8173
8364
|
classifyEmailRoute,
|
|
8365
|
+
classifyResumeError,
|
|
8174
8366
|
closeDatabase,
|
|
8367
|
+
composeBridgeWakePrompt,
|
|
8175
8368
|
createTestDatabase,
|
|
8176
8369
|
debug,
|
|
8177
8370
|
debugWarn,
|
|
@@ -8194,6 +8387,7 @@ export {
|
|
|
8194
8387
|
operatorPrefsStoragePath,
|
|
8195
8388
|
parseEmail,
|
|
8196
8389
|
parseGoogleVoiceSms,
|
|
8390
|
+
planBridgeWake,
|
|
8197
8391
|
recordToolCall,
|
|
8198
8392
|
redactObject,
|
|
8199
8393
|
redactSecret,
|
|
@@ -8207,6 +8401,7 @@ export {
|
|
|
8207
8401
|
scoreEmail,
|
|
8208
8402
|
setOperatorEmail,
|
|
8209
8403
|
setTelemetryVersion,
|
|
8404
|
+
shouldSkipBridgeWakeForLiveOperator,
|
|
8210
8405
|
startRelayBridge,
|
|
8211
8406
|
threadIdFor,
|
|
8212
8407
|
tryJoin,
|