@agenticmail/core 0.7.5 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,16 @@ This is the foundation layer that everything else builds on. If the API server,
10
10
 
11
11
  Every other AgenticMail package depends on this one.
12
12
 
13
- ## ✨ What's new in 0.7.4
13
+ ## ✨ What's new in 0.9.0
14
+
15
+ - **🧠 New `threading/` module.** Three exported primitives for the dispatcher's wake-context layer:
16
+ - **`threadIdFor({subject})`** — stable, subject-only thread id (replies collapse into the same thread regardless of sender). SHA-256 base64url truncated to 16 chars. Strips chained `Re:`/`Fwd:` prefixes + `[FINAL]/[DONE]/[CLOSED]/[WRAP]` markers + collapses whitespace + lower-cases.
17
+ - **`ThreadCache`** — per-thread ring buffer of the last K message envelopes + previews on disk at `~/.agenticmail/thread-cache/<t>.json`. Configurable K, LRU-bounded directory eviction (default cap 5000 threads ≈ 25 MB). Atomic writes via rename.
18
+ - **`AgentMemoryStore`** — per-`(agent, thread)` markdown files at `~/.agenticmail/agent-memory/<agentId>/<t>.md`. Agent writes structured fields (summary, commitments, openQuestions, lastAction, lastUid) at end-of-wake; on the next wake the dispatcher reads the raw markdown back into the prompt. Per-agent isolation — one agent's memory is invisible to another sharing the same thread.
19
+
20
+ 23 new unit tests covering normalization, hash stability, push/dedup/cap/delete, write/read round-trip, and per-agent isolation.
21
+
22
+ ## ✨ Earlier — 0.7.4
14
23
 
15
24
  - **⭐ `MailReceiver.setStarred(uid, starred, mailbox?)`** — toggles IMAP's `\Flagged` flag via `messageFlagsAdd` / `messageFlagsRemove`. Same on-disk bit Gmail's star uses. Underpins the new `POST /mail/messages/:uid/star` route in `@agenticmail/api`.
16
25
  - **📐 Task `output_schema` column** — migration `014_task_output_schema.sql` adds an optional `output_schema TEXT` column to `agent_tasks`. Stores the assigner-supplied JSON Schema for typed task contracts. `NULL` means "no schema, accept anything", fully back-compat with the v0.8.x task model.
package/dist/index.cjs CHANGED
@@ -711,6 +711,7 @@ __export(index_exports, {
711
711
  AGENT_ROLES: () => AGENT_ROLES,
712
712
  AccountManager: () => AccountManager,
713
713
  AgentDeletionService: () => AgentDeletionService,
714
+ AgentMemoryStore: () => AgentMemoryStore,
714
715
  AgenticMailClient: () => AgenticMailClient,
715
716
  CloudflareClient: () => CloudflareClient,
716
717
  DEFAULT_AGENT_NAME: () => DEFAULT_AGENT_NAME,
@@ -734,6 +735,7 @@ __export(index_exports, {
734
735
  SmsManager: () => SmsManager,
735
736
  SmsPoller: () => SmsPoller,
736
737
  StalwartAdmin: () => StalwartAdmin,
738
+ ThreadCache: () => ThreadCache,
737
739
  TunnelManager: () => TunnelManager,
738
740
  WARNING_THRESHOLD: () => WARNING_THRESHOLD,
739
741
  buildInboundSecurityAdvisory: () => buildInboundSecurityAdvisory,
@@ -748,7 +750,9 @@ __export(index_exports, {
748
750
  getDatabase: () => getDatabase,
749
751
  isInternalEmail: () => isInternalEmail,
750
752
  isValidPhoneNumber: () => isValidPhoneNumber,
753
+ normalizeAddress: () => normalizeAddress,
751
754
  normalizePhoneNumber: () => normalizePhoneNumber,
755
+ normalizeSubject: () => normalizeSubject,
752
756
  parseEmail: () => parseEmail,
753
757
  parseGoogleVoiceSms: () => parseGoogleVoiceSms,
754
758
  recordToolCall: () => recordToolCall,
@@ -758,7 +762,8 @@ __export(index_exports, {
758
762
  scanOutboundEmail: () => scanOutboundEmail,
759
763
  scoreEmail: () => scoreEmail,
760
764
  setTelemetryVersion: () => setTelemetryVersion,
761
- startRelayBridge: () => startRelayBridge
765
+ startRelayBridge: () => startRelayBridge,
766
+ threadIdFor: () => threadIdFor
762
767
  });
763
768
  module.exports = __toCommonJS(index_exports);
764
769
 
@@ -1841,14 +1846,14 @@ var StalwartAdmin = class {
1841
1846
  if (!isValidDomain(domain)) {
1842
1847
  throw new Error(`Invalid domain format: "${domain}"`);
1843
1848
  }
1844
- const { readFileSync: readFileSync5, writeFileSync: writeFileSync6 } = await import("fs");
1845
- const { homedir: homedir9 } = await import("os");
1846
- const { join: join10 } = await import("path");
1847
- const configPath = join10(homedir9(), ".agenticmail", "stalwart.toml");
1849
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync8 } = await import("fs");
1850
+ const { homedir: homedir11 } = await import("os");
1851
+ const { join: join12 } = await import("path");
1852
+ const configPath = join12(homedir11(), ".agenticmail", "stalwart.toml");
1848
1853
  try {
1849
- let config = readFileSync5(configPath, "utf-8");
1854
+ let config = readFileSync7(configPath, "utf-8");
1850
1855
  config = config.replace(/^hostname\s*=\s*"[^"]*"/m, `hostname = "${escapeTomlString(domain)}"`);
1851
- writeFileSync6(configPath, config);
1856
+ writeFileSync8(configPath, config);
1852
1857
  console.log(`[Stalwart] Updated hostname to "${domain}" in stalwart.toml`);
1853
1858
  } catch (err) {
1854
1859
  throw new Error(`Failed to set config server.hostname=${domain}`);
@@ -1857,15 +1862,15 @@ var StalwartAdmin = class {
1857
1862
  // --- DKIM ---
1858
1863
  /** Path to the host-side stalwart.toml (mounted read-only into container) */
1859
1864
  get configPath() {
1860
- const { homedir: homedir9 } = require("os");
1861
- const { join: join10 } = require("path");
1862
- return join10(homedir9(), ".agenticmail", "stalwart.toml");
1865
+ const { homedir: homedir11 } = require("os");
1866
+ const { join: join12 } = require("path");
1867
+ return join12(homedir11(), ".agenticmail", "stalwart.toml");
1863
1868
  }
1864
1869
  /** Path to host-side DKIM key directory */
1865
1870
  get dkimDir() {
1866
- const { homedir: homedir9 } = require("os");
1867
- const { join: join10 } = require("path");
1868
- return join10(homedir9(), ".agenticmail");
1871
+ const { homedir: homedir11 } = require("os");
1872
+ const { join: join12 } = require("path");
1873
+ return join12(homedir11(), ".agenticmail");
1869
1874
  }
1870
1875
  /**
1871
1876
  * Create/reuse a DKIM signing key for a domain.
@@ -1966,12 +1971,12 @@ var StalwartAdmin = class {
1966
1971
  * This bypasses the need for a PTR record on the sending IP.
1967
1972
  */
1968
1973
  async configureOutboundRelay(config) {
1969
- const { readFileSync: readFileSync5, writeFileSync: writeFileSync6 } = await import("fs");
1970
- const { homedir: homedir9 } = await import("os");
1971
- const { join: join10 } = await import("path");
1974
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync8 } = await import("fs");
1975
+ const { homedir: homedir11 } = await import("os");
1976
+ const { join: join12 } = await import("path");
1972
1977
  const routeName = config.routeName ?? "gmail";
1973
- const tomlPath = join10(homedir9(), ".agenticmail", "stalwart.toml");
1974
- let toml = readFileSync5(tomlPath, "utf-8");
1978
+ const tomlPath = join12(homedir11(), ".agenticmail", "stalwart.toml");
1979
+ let toml = readFileSync7(tomlPath, "utf-8");
1975
1980
  toml = toml.replace(/\n\[queue\.route\.gmail\][\s\S]*?(?=\n\[|$)/, "");
1976
1981
  toml = toml.replace(/\n\[queue\.strategy\][\s\S]*?(?=\n\[|$)/, "");
1977
1982
  const safeRouteName = routeName.replace(/[^a-zA-Z0-9_-]/g, "");
@@ -1991,7 +1996,7 @@ auth.secret = "${escapeTomlString(config.password)}"
1991
1996
  route = [ { if = "is_local_domain('', rcpt_domain)", then = "'local'" },
1992
1997
  { else = "'${safeRouteName}'" } ]
1993
1998
  `;
1994
- writeFileSync6(tomlPath, toml, "utf-8");
1999
+ writeFileSync8(tomlPath, toml, "utf-8");
1995
2000
  await this.restartContainer();
1996
2001
  }
1997
2002
  };
@@ -3604,6 +3609,18 @@ CREATE INDEX IF NOT EXISTS idx_pending_notification ON pending_outbound(notifica
3604
3609
  -- Column is optional; NULL means "no schema, accept anything" (the
3605
3610
  -- v0.8.x behaviour, fully back-compat).
3606
3611
  ALTER TABLE agent_tasks ADD COLUMN output_schema TEXT;
3612
+ `,
3613
+ "015_draft_attachments.sql": `
3614
+ -- Persist attachments alongside their draft.
3615
+ --
3616
+ -- Stored as a JSON array on the row: each entry is
3617
+ -- { filename, contentType, content (base64), size }. The web UI
3618
+ -- cap is 20 MB total per draft, which SQLite handles fine without
3619
+ -- bloating other queries \u2014 the column is only fetched on the
3620
+ -- per-draft GET (not on the list endpoint) so the Drafts sidebar
3621
+ -- stays snappy. NULL means "no attachments", fully back-compat
3622
+ -- with rows from before this migration.
3623
+ ALTER TABLE drafts ADD COLUMN attachments TEXT;
3607
3624
  `
3608
3625
  };
3609
3626
  function runMigrations(database) {
@@ -5852,12 +5869,12 @@ var GatewayManager = class {
5852
5869
  zone = await this.cfClient.createZone(domain);
5853
5870
  }
5854
5871
  const existingRecords = await this.cfClient.listDnsRecords(zone.id);
5855
- const { homedir: homedir9 } = await import("os");
5856
- const backupDir = (0, import_node_path4.join)(homedir9(), ".agenticmail");
5872
+ const { homedir: homedir11 } = await import("os");
5873
+ const backupDir = (0, import_node_path4.join)(homedir11(), ".agenticmail");
5857
5874
  const backupPath = (0, import_node_path4.join)(backupDir, `dns-backup-${domain}-${Date.now()}.json`);
5858
- const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync7 } = await import("fs");
5859
- mkdirSync7(backupDir, { recursive: true });
5860
- writeFileSync6(backupPath, JSON.stringify({
5875
+ const { writeFileSync: writeFileSync8, mkdirSync: mkdirSync9 } = await import("fs");
5876
+ mkdirSync9(backupDir, { recursive: true });
5877
+ writeFileSync8(backupPath, JSON.stringify({
5861
5878
  domain,
5862
5879
  zoneId: zone.id,
5863
5880
  backedUpAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8040,11 +8057,286 @@ secret = "${password}"
8040
8057
  return (0, import_node_fs7.existsSync)(configPath);
8041
8058
  }
8042
8059
  };
8060
+
8061
+ // src/threading/thread-id.ts
8062
+ var import_node_crypto4 = require("crypto");
8063
+ function stripReplyPrefixes(subject) {
8064
+ let s = subject;
8065
+ for (; ; ) {
8066
+ const next = s.replace(/^\s*(?:re|fwd?|fw)\s*(?:\[\d+\])?\s*:\s*/i, "");
8067
+ if (next === s) break;
8068
+ s = next;
8069
+ }
8070
+ return s;
8071
+ }
8072
+ function stripCoordinationMarkers(subject) {
8073
+ return subject.replace(/\[\s*(?:final|done|closed|wrap)\s*\]/gi, " ");
8074
+ }
8075
+ function normalizeSubject(subject) {
8076
+ if (!subject) return "(no subject)";
8077
+ let s = stripReplyPrefixes(subject);
8078
+ s = stripCoordinationMarkers(s);
8079
+ s = s.replace(/\s+/g, " ").trim().toLowerCase();
8080
+ return s || "(no subject)";
8081
+ }
8082
+ function normalizeAddress(addr) {
8083
+ if (!addr) return "(unknown)";
8084
+ const m = addr.match(/<([^>]+)>/);
8085
+ const raw = m ? m[1] : addr;
8086
+ return raw.trim().toLowerCase();
8087
+ }
8088
+ function threadIdFor(input) {
8089
+ const subject = normalizeSubject(input.subject);
8090
+ return (0, import_node_crypto4.createHash)("sha256").update(subject).digest("base64url").slice(0, 16);
8091
+ }
8092
+
8093
+ // src/threading/thread-cache.ts
8094
+ var import_node_fs8 = require("fs");
8095
+ var import_node_os8 = require("os");
8096
+ var import_node_path9 = require("path");
8097
+ var CACHE_DIR_DEFAULT = (0, import_node_path9.join)((0, import_node_os8.homedir)(), ".agenticmail", "thread-cache");
8098
+ var DEFAULT_K_MESSAGES = 10;
8099
+ var DEFAULT_LRU_CAP = 5e3;
8100
+ var PREVIEW_MAX_CHARS = 240;
8101
+ var ThreadCache = class {
8102
+ dir;
8103
+ k;
8104
+ lruCap;
8105
+ constructor(opts = {}) {
8106
+ this.dir = opts.cacheDir ?? CACHE_DIR_DEFAULT;
8107
+ this.k = opts.k ?? DEFAULT_K_MESSAGES;
8108
+ this.lruCap = opts.lruCap ?? DEFAULT_LRU_CAP;
8109
+ try {
8110
+ (0, import_node_fs8.mkdirSync)(this.dir, { recursive: true });
8111
+ } catch {
8112
+ }
8113
+ }
8114
+ pathFor(threadId) {
8115
+ return (0, import_node_path9.join)(this.dir, `${threadId}.json`);
8116
+ }
8117
+ read(threadId) {
8118
+ const p = this.pathFor(threadId);
8119
+ if (!(0, import_node_fs8.existsSync)(p)) return null;
8120
+ try {
8121
+ const raw = (0, import_node_fs8.readFileSync)(p, "utf-8");
8122
+ return JSON.parse(raw);
8123
+ } catch {
8124
+ try {
8125
+ (0, import_node_fs8.rmSync)(p, { force: true });
8126
+ } catch {
8127
+ }
8128
+ return null;
8129
+ }
8130
+ }
8131
+ /**
8132
+ * Append a message to the thread's cache, pruning to the K
8133
+ * newest entries. Creates the cache entry on first write.
8134
+ *
8135
+ * `rootFromAddr` is the sender of the ROOT message on the
8136
+ * thread; on a brand-new thread this is just `env.fromAddr`,
8137
+ * on a reply it's read off the existing cache entry (callers
8138
+ * should pass the existing entry's rootFromAddr when known).
8139
+ */
8140
+ pushMessage(threadId, env, meta) {
8141
+ const existing = this.read(threadId);
8142
+ const entry = existing ? {
8143
+ ...existing,
8144
+ // We re-affirm subject + rootFromAddr to existing values —
8145
+ // the first-seen values win. Reply messages carry the
8146
+ // replier's `from`, not the original sender's, so we'd
8147
+ // corrupt the thread root if we overwrote here.
8148
+ subject: existing.subject,
8149
+ rootFromAddr: existing.rootFromAddr,
8150
+ lastUpdated: Date.now(),
8151
+ messages: dedupAndCap([env, ...existing.messages], this.k)
8152
+ } : {
8153
+ threadId,
8154
+ subject: meta.subject,
8155
+ rootFromAddr: meta.rootFromAddr,
8156
+ lastUpdated: Date.now(),
8157
+ messages: [env]
8158
+ };
8159
+ this.writeAtomic(threadId, entry);
8160
+ this.maybeEvict();
8161
+ return entry;
8162
+ }
8163
+ /** Permanently remove a thread's cache (called on [FINAL] / [DONE] / [CLOSED] / [WRAP]). */
8164
+ delete(threadId) {
8165
+ try {
8166
+ (0, import_node_fs8.rmSync)(this.pathFor(threadId), { force: true });
8167
+ } catch {
8168
+ }
8169
+ }
8170
+ /**
8171
+ * Render the cache as a compact text block for the wake prompt.
8172
+ * One line per message, newest first. Empty string when the
8173
+ * cache is empty — caller decides whether to suppress the
8174
+ * header in that case.
8175
+ */
8176
+ renderForPrompt(entry) {
8177
+ if (!entry || entry.messages.length === 0) return "";
8178
+ return entry.messages.map((m) => {
8179
+ const preview = m.preview.replace(/\s+/g, " ").slice(0, PREVIEW_MAX_CHARS);
8180
+ return `- UID ${m.uid} \xB7 ${m.from} \xB7 ${m.date} \xB7 "${m.subject}" \xB7 ${preview}`;
8181
+ }).join("\n");
8182
+ }
8183
+ writeAtomic(threadId, entry) {
8184
+ const p = this.pathFor(threadId);
8185
+ const tmp = `${p}.tmp`;
8186
+ (0, import_node_fs8.writeFileSync)(tmp, JSON.stringify(entry), "utf-8");
8187
+ (0, import_node_fs8.renameSync)(tmp, p);
8188
+ }
8189
+ /**
8190
+ * Best-effort LRU eviction. Runs at most every 256 writes (we
8191
+ * don't track a precise counter — `Math.random()` sampling keeps
8192
+ * the write path cheap). When the directory has more files than
8193
+ * `lruCap`, sort by mtime ascending and delete the oldest 10%.
8194
+ */
8195
+ maybeEvict() {
8196
+ if (Math.random() > 1 / 256) return;
8197
+ let files;
8198
+ try {
8199
+ files = (0, import_node_fs8.readdirSync)(this.dir).filter((f) => f.endsWith(".json"));
8200
+ } catch {
8201
+ return;
8202
+ }
8203
+ if (files.length <= this.lruCap) return;
8204
+ const stats = files.map((f) => {
8205
+ const p = (0, import_node_path9.join)(this.dir, f);
8206
+ try {
8207
+ return { p, mtime: (0, import_node_fs8.statSync)(p).mtimeMs };
8208
+ } catch {
8209
+ return { p, mtime: 0 };
8210
+ }
8211
+ });
8212
+ stats.sort((a, b) => a.mtime - b.mtime);
8213
+ const dropCount = Math.max(1, Math.floor(this.lruCap * 0.1));
8214
+ for (let i = 0; i < dropCount; i++) {
8215
+ try {
8216
+ (0, import_node_fs8.rmSync)(stats[i].p, { force: true });
8217
+ } catch {
8218
+ }
8219
+ }
8220
+ }
8221
+ };
8222
+ function dedupAndCap(messages, k) {
8223
+ const seen = /* @__PURE__ */ new Set();
8224
+ const out = [];
8225
+ for (const m of messages) {
8226
+ if (seen.has(m.uid)) continue;
8227
+ seen.add(m.uid);
8228
+ out.push(m);
8229
+ if (out.length >= k) break;
8230
+ }
8231
+ return out;
8232
+ }
8233
+
8234
+ // src/threading/agent-memory.ts
8235
+ var import_node_fs9 = require("fs");
8236
+ var import_node_os9 = require("os");
8237
+ var import_node_path10 = require("path");
8238
+ var MEMORY_DIR_DEFAULT = (0, import_node_path10.join)((0, import_node_os9.homedir)(), ".agenticmail", "agent-memory");
8239
+ var AgentMemoryStore = class {
8240
+ dir;
8241
+ constructor(opts = {}) {
8242
+ this.dir = opts.memoryDir ?? MEMORY_DIR_DEFAULT;
8243
+ try {
8244
+ (0, import_node_fs9.mkdirSync)(this.dir, { recursive: true });
8245
+ } catch {
8246
+ }
8247
+ }
8248
+ dirFor(agentId) {
8249
+ return (0, import_node_path10.join)(this.dir, sanitizeId(agentId));
8250
+ }
8251
+ pathFor(agentId, threadId) {
8252
+ return (0, import_node_path10.join)(this.dirFor(agentId), `${sanitizeId(threadId)}.md`);
8253
+ }
8254
+ read(agentId, threadId) {
8255
+ const p = this.pathFor(agentId, threadId);
8256
+ if (!(0, import_node_fs9.existsSync)(p)) return null;
8257
+ try {
8258
+ const raw = (0, import_node_fs9.readFileSync)(p, "utf-8");
8259
+ const parsed = parse(raw);
8260
+ return { ...parsed, raw };
8261
+ } catch {
8262
+ return null;
8263
+ }
8264
+ }
8265
+ write(agentId, threadId, fields) {
8266
+ const agentDir = this.dirFor(agentId);
8267
+ try {
8268
+ (0, import_node_fs9.mkdirSync)(agentDir, { recursive: true });
8269
+ } catch {
8270
+ }
8271
+ const body = render({ ...fields, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
8272
+ const p = this.pathFor(agentId, threadId);
8273
+ const tmp = `${p}.tmp`;
8274
+ (0, import_node_fs9.writeFileSync)(tmp, body, "utf-8");
8275
+ (0, import_node_fs9.renameSync)(tmp, p);
8276
+ }
8277
+ delete(agentId, threadId) {
8278
+ try {
8279
+ (0, import_node_fs9.rmSync)(this.pathFor(agentId, threadId), { force: true });
8280
+ } catch {
8281
+ }
8282
+ }
8283
+ /** Render an agent's memory for injection into a wake prompt.
8284
+ * Returns the raw markdown if present; empty string when there's
8285
+ * no prior memory (the caller decides whether to suppress the
8286
+ * whole "Your own memory" block). */
8287
+ renderForPrompt(memory) {
8288
+ if (!memory) return "";
8289
+ return memory.raw;
8290
+ }
8291
+ };
8292
+ function sanitizeId(id) {
8293
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
8294
+ }
8295
+ function render(fields) {
8296
+ const fm = ["---"];
8297
+ fm.push(`updated_at: ${fields.updatedAt}`);
8298
+ if (typeof fields.lastUid === "number") fm.push(`last_uid: ${fields.lastUid}`);
8299
+ fm.push("---", "");
8300
+ const sections = [];
8301
+ if (fields.summary && fields.summary.trim()) {
8302
+ sections.push(fields.summary.trim());
8303
+ }
8304
+ if (fields.commitments && fields.commitments.length > 0) {
8305
+ sections.push(`### Commitments
8306
+ ${fields.commitments.map((c) => `- ${c}`).join("\n")}`);
8307
+ }
8308
+ if (fields.openQuestions && fields.openQuestions.length > 0) {
8309
+ sections.push(`### Open
8310
+ ${fields.openQuestions.map((q) => `- ${q}`).join("\n")}`);
8311
+ }
8312
+ if (fields.lastAction && fields.lastAction.trim()) {
8313
+ sections.push(`### Last action
8314
+ ${fields.lastAction.trim()}`);
8315
+ }
8316
+ return fm.join("\n") + sections.join("\n\n") + "\n";
8317
+ }
8318
+ function parse(raw) {
8319
+ const out = {};
8320
+ const m = raw.match(/^---\n([\s\S]*?)\n---\n/);
8321
+ if (m) {
8322
+ for (const line of m[1].split("\n")) {
8323
+ const kv = line.match(/^(\w+):\s*(.*)$/);
8324
+ if (!kv) continue;
8325
+ if (kv[1] === "updated_at") out.updatedAt = kv[2].trim();
8326
+ else if (kv[1] === "last_uid") {
8327
+ const n = parseInt(kv[2], 10);
8328
+ if (!isNaN(n)) out.lastUid = n;
8329
+ }
8330
+ }
8331
+ }
8332
+ return out;
8333
+ }
8043
8334
  // Annotate the CommonJS export names for ESM import in node:
8044
8335
  0 && (module.exports = {
8045
8336
  AGENT_ROLES,
8046
8337
  AccountManager,
8047
8338
  AgentDeletionService,
8339
+ AgentMemoryStore,
8048
8340
  AgenticMailClient,
8049
8341
  CloudflareClient,
8050
8342
  DEFAULT_AGENT_NAME,
@@ -8068,6 +8360,7 @@ secret = "${password}"
8068
8360
  SmsManager,
8069
8361
  SmsPoller,
8070
8362
  StalwartAdmin,
8363
+ ThreadCache,
8071
8364
  TunnelManager,
8072
8365
  WARNING_THRESHOLD,
8073
8366
  buildInboundSecurityAdvisory,
@@ -8082,7 +8375,9 @@ secret = "${password}"
8082
8375
  getDatabase,
8083
8376
  isInternalEmail,
8084
8377
  isValidPhoneNumber,
8378
+ normalizeAddress,
8085
8379
  normalizePhoneNumber,
8380
+ normalizeSubject,
8086
8381
  parseEmail,
8087
8382
  parseGoogleVoiceSms,
8088
8383
  recordToolCall,
@@ -8092,5 +8387,6 @@ secret = "${password}"
8092
8387
  scanOutboundEmail,
8093
8388
  scoreEmail,
8094
8389
  setTelemetryVersion,
8095
- startRelayBridge
8390
+ startRelayBridge,
8391
+ threadIdFor
8096
8392
  });