@agenticmail/core 0.9.13 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -711,6 +711,7 @@ __export(index_exports, {
711
711
  AGENT_ROLES: () => AGENT_ROLES,
712
712
  AccountManager: () => AccountManager,
713
713
  AgentDeletionService: () => AgentDeletionService,
714
+ AgentMemoryManager: () => AgentMemoryManager,
714
715
  AgentMemoryStore: () => AgentMemoryStore,
715
716
  AgenticMailClient: () => AgenticMailClient,
716
717
  BRIDGE_OPERATOR_LIVE_WINDOW_MS: () => BRIDGE_OPERATOR_LIVE_WINDOW_MS,
@@ -723,12 +724,28 @@ __export(index_exports, {
723
724
  DependencyInstaller: () => DependencyInstaller,
724
725
  DomainManager: () => DomainManager,
725
726
  DomainPurchaser: () => DomainPurchaser,
727
+ ELKS_REALTIME_AUDIO_FORMATS: () => ELKS_REALTIME_AUDIO_FORMATS,
726
728
  EmailSearchIndex: () => EmailSearchIndex,
727
729
  GatewayManager: () => GatewayManager,
728
730
  InboxWatcher: () => InboxWatcher,
731
+ MEMORY_CATEGORIES: () => MEMORY_CATEGORIES,
729
732
  MailReceiver: () => MailReceiver,
730
733
  MailSender: () => MailSender,
734
+ MemorySearchIndex: () => MemorySearchIndex,
735
+ PHONE_MAX_CONCURRENT_MISSIONS: () => PHONE_MAX_CONCURRENT_MISSIONS,
736
+ PHONE_MIN_WEBHOOK_SECRET_LENGTH: () => PHONE_MIN_WEBHOOK_SECRET_LENGTH,
737
+ PHONE_MISSION_STATES: () => PHONE_MISSION_STATES,
738
+ PHONE_RATE_LIMIT_PER_HOUR: () => PHONE_RATE_LIMIT_PER_HOUR,
739
+ PHONE_RATE_LIMIT_PER_MINUTE: () => PHONE_RATE_LIMIT_PER_MINUTE,
740
+ PHONE_REGION_SCOPES: () => PHONE_REGION_SCOPES,
741
+ PHONE_SERVER_MAX_ATTEMPTS: () => PHONE_SERVER_MAX_ATTEMPTS,
742
+ PHONE_SERVER_MAX_CALL_DURATION_SECONDS: () => PHONE_SERVER_MAX_CALL_DURATION_SECONDS,
743
+ PHONE_SERVER_MAX_COST_PER_MISSION: () => PHONE_SERVER_MAX_COST_PER_MISSION,
744
+ PHONE_TASK_MAX_LENGTH: () => PHONE_TASK_MAX_LENGTH,
731
745
  PathTraversalError: () => PathTraversalError,
746
+ PhoneManager: () => PhoneManager,
747
+ PhoneRateLimitError: () => PhoneRateLimitError,
748
+ PhoneWebhookAuthError: () => PhoneWebhookAuthError,
732
749
  REDACTED: () => REDACTED,
733
750
  RELAY_PRESETS: () => RELAY_PRESETS,
734
751
  RelayBridge: () => RelayBridge,
@@ -739,6 +756,7 @@ __export(index_exports, {
739
756
  SmsManager: () => SmsManager,
740
757
  SmsPoller: () => SmsPoller,
741
758
  StalwartAdmin: () => StalwartAdmin,
759
+ TELEPHONY_TRANSPORT_CAPABILITIES: () => TELEPHONY_TRANSPORT_CAPABILITIES,
742
760
  ThreadCache: () => ThreadCache,
743
761
  TunnelManager: () => TunnelManager,
744
762
  UnsafeApiUrlError: () => UnsafeApiUrlError,
@@ -747,8 +765,16 @@ __export(index_exports, {
747
765
  bridgeWakeErrorMessage: () => bridgeWakeErrorMessage,
748
766
  bridgeWakeLastSeenAgeMs: () => bridgeWakeLastSeenAgeMs,
749
767
  buildApiUrl: () => buildApiUrl,
768
+ buildElksAudioMessage: () => buildElksAudioMessage,
769
+ buildElksByeMessage: () => buildElksByeMessage,
770
+ buildElksHandshakeMessages: () => buildElksHandshakeMessages,
771
+ buildElksInterruptMessage: () => buildElksInterruptMessage,
772
+ buildElksListeningMessage: () => buildElksListeningMessage,
773
+ buildElksSendingMessage: () => buildElksSendingMessage,
750
774
  buildInboundSecurityAdvisory: () => buildInboundSecurityAdvisory,
775
+ buildPhoneTransportConfig: () => buildPhoneTransportConfig,
751
776
  classifyEmailRoute: () => classifyEmailRoute,
777
+ classifyPhoneNumberRisk: () => classifyPhoneNumberRisk,
752
778
  classifyResumeError: () => classifyResumeError,
753
779
  closeDatabase: () => closeDatabase,
754
780
  composeBridgeWakePrompt: () => composeBridgeWakePrompt,
@@ -763,8 +789,10 @@ __export(index_exports, {
763
789
  getOperatorEmail: () => getOperatorEmail,
764
790
  getSmsProvider: () => getSmsProvider,
765
791
  hostSessionStoragePath: () => hostSessionStoragePath,
792
+ inferPhoneRegion: () => inferPhoneRegion,
766
793
  isInternalEmail: () => isInternalEmail,
767
794
  isLoopbackMailHost: () => isLoopbackMailHost,
795
+ isPhoneRegionAllowed: () => isPhoneRegionAllowed,
768
796
  isSessionFresh: () => isSessionFresh,
769
797
  isValidPhoneNumber: () => isValidPhoneNumber,
770
798
  loadHostSession: () => loadHostSession,
@@ -773,11 +801,13 @@ __export(index_exports, {
773
801
  normalizePhoneNumber: () => normalizePhoneNumber,
774
802
  normalizeSubject: () => normalizeSubject,
775
803
  operatorPrefsStoragePath: () => operatorPrefsStoragePath,
804
+ parseElksRealtimeMessage: () => parseElksRealtimeMessage,
776
805
  parseEmail: () => parseEmail,
777
806
  parseGoogleVoiceSms: () => parseGoogleVoiceSms,
778
807
  planBridgeWake: () => planBridgeWake,
779
808
  recordToolCall: () => recordToolCall,
780
809
  redactObject: () => redactObject,
810
+ redactPhoneTransportConfig: () => redactPhoneTransportConfig,
781
811
  redactSecret: () => redactSecret,
782
812
  redactSmsConfig: () => redactSmsConfig,
783
813
  resolveConfig: () => resolveConfig,
@@ -792,9 +822,14 @@ __export(index_exports, {
792
822
  setTelemetryVersion: () => setTelemetryVersion,
793
823
  shouldSkipBridgeWakeForLiveOperator: () => shouldSkipBridgeWakeForLiveOperator,
794
824
  startRelayBridge: () => startRelayBridge,
825
+ stem: () => stem,
795
826
  threadIdFor: () => threadIdFor,
827
+ tokenize: () => tokenize,
796
828
  tryJoin: () => tryJoin,
797
- validateApiUrl: () => validateApiUrl
829
+ validateApiUrl: () => validateApiUrl,
830
+ validatePhoneMissionPolicy: () => validatePhoneMissionPolicy,
831
+ validatePhoneMissionStart: () => validatePhoneMissionStart,
832
+ validatePhoneTransportProfile: () => validatePhoneTransportProfile
798
833
  });
799
834
  module.exports = __toCommonJS(index_exports);
800
835
 
@@ -2191,6 +2226,10 @@ var AccountManager = class {
2191
2226
  }
2192
2227
  const stmt = this.db.prepare("DELETE FROM agents WHERE id = ?");
2193
2228
  const result = stmt.run(id);
2229
+ try {
2230
+ this.db.prepare("DELETE FROM agent_memory WHERE agent_id = ?").run(id);
2231
+ } catch {
2232
+ }
2194
2233
  return result.changes > 0;
2195
2234
  }
2196
2235
  /**
@@ -7020,6 +7059,967 @@ var RELAY_PRESETS = {
7020
7059
  }
7021
7060
  };
7022
7061
 
7062
+ // src/phone/realtime.ts
7063
+ var ELKS_REALTIME_AUDIO_FORMATS = ["ulaw", "pcm_16000", "pcm_24000", "wav"];
7064
+ function asRecord(value) {
7065
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) ? value : {};
7066
+ }
7067
+ function asString2(value) {
7068
+ return typeof value === "string" ? value.trim() : "";
7069
+ }
7070
+ function isAudioFormat(value) {
7071
+ return typeof value === "string" && ELKS_REALTIME_AUDIO_FORMATS.includes(value);
7072
+ }
7073
+ function assertAudioFormat(format) {
7074
+ if (!isAudioFormat(format)) {
7075
+ throw new Error(`Unsupported 46elks realtime audio format: ${String(format)}`);
7076
+ }
7077
+ return format;
7078
+ }
7079
+ function looksLikeBase64(value) {
7080
+ return value.length > 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value) && value.length % 4 === 0;
7081
+ }
7082
+ function decodeJsonMessage(input) {
7083
+ if (typeof input === "string") {
7084
+ try {
7085
+ return asRecord(JSON.parse(input));
7086
+ } catch {
7087
+ throw new Error("Invalid 46elks realtime message: expected JSON object string");
7088
+ }
7089
+ }
7090
+ return asRecord(input);
7091
+ }
7092
+ function parseElksRealtimeMessage(input) {
7093
+ const msg = decodeJsonMessage(input);
7094
+ const type = asString2(msg.t);
7095
+ if (type === "hello") {
7096
+ const callid = asString2(msg.callid);
7097
+ const from = asString2(msg.from);
7098
+ const to = asString2(msg.to);
7099
+ if (!callid || !from || !to) {
7100
+ throw new Error("Invalid 46elks realtime hello: callid, from, and to are required");
7101
+ }
7102
+ return { ...msg, t: "hello", callid, from, to };
7103
+ }
7104
+ if (type === "audio") {
7105
+ const data = asString2(msg.data);
7106
+ if (!looksLikeBase64(data)) {
7107
+ throw new Error("Invalid 46elks realtime audio: data must be non-empty base64");
7108
+ }
7109
+ return { t: "audio", data };
7110
+ }
7111
+ if (type === "bye") {
7112
+ const reason = asString2(msg.reason) || void 0;
7113
+ const message = asString2(msg.message) || void 0;
7114
+ return { ...msg, t: "bye", reason, message };
7115
+ }
7116
+ throw new Error(`Unsupported 46elks realtime message type: ${type || "(missing)"}`);
7117
+ }
7118
+ function buildElksListeningMessage(format = "pcm_24000") {
7119
+ return { t: "listening", format: assertAudioFormat(format) };
7120
+ }
7121
+ function buildElksSendingMessage(format = "pcm_24000") {
7122
+ return { t: "sending", format: assertAudioFormat(format) };
7123
+ }
7124
+ function buildElksAudioMessage(data) {
7125
+ const encoded = typeof data === "string" ? data : Buffer.from(data).toString("base64");
7126
+ if (!looksLikeBase64(encoded)) {
7127
+ throw new Error("46elks realtime audio data must be base64 or bytes");
7128
+ }
7129
+ return { t: "audio", data: encoded };
7130
+ }
7131
+ function buildElksInterruptMessage() {
7132
+ return { t: "interrupt" };
7133
+ }
7134
+ function buildElksByeMessage() {
7135
+ return { t: "bye" };
7136
+ }
7137
+ function buildElksHandshakeMessages(options = {}) {
7138
+ return [
7139
+ buildElksListeningMessage(options.listenFormat ?? "pcm_24000"),
7140
+ buildElksSendingMessage(options.sendFormat ?? "pcm_24000")
7141
+ ];
7142
+ }
7143
+
7144
+ // src/phone/manager.ts
7145
+ var import_node_crypto3 = require("crypto");
7146
+
7147
+ // src/phone/mission.ts
7148
+ var PHONE_REGION_SCOPES = ["AT", "DE", "EU", "WORLD"];
7149
+ var TELEPHONY_TRANSPORT_CAPABILITIES = [
7150
+ "sms",
7151
+ "call_control",
7152
+ "realtime_media",
7153
+ "recording_supported"
7154
+ ];
7155
+ var PHONE_MISSION_STATES = [
7156
+ "draft",
7157
+ "approved",
7158
+ "dialing",
7159
+ "connected",
7160
+ "conversing",
7161
+ "needs_operator",
7162
+ "completed",
7163
+ "failed",
7164
+ "cancelled"
7165
+ ];
7166
+ var PHONE_SERVER_MAX_CALL_DURATION_SECONDS = 3600;
7167
+ var PHONE_SERVER_MAX_COST_PER_MISSION = 5;
7168
+ var PHONE_SERVER_MAX_ATTEMPTS = 3;
7169
+ var PHONE_TASK_MAX_LENGTH = 2e3;
7170
+ var EU_DIAL_PREFIXES = [
7171
+ "+30",
7172
+ "+31",
7173
+ "+32",
7174
+ "+33",
7175
+ "+34",
7176
+ "+351",
7177
+ "+352",
7178
+ "+353",
7179
+ "+354",
7180
+ "+356",
7181
+ "+357",
7182
+ "+358",
7183
+ "+359",
7184
+ "+36",
7185
+ "+370",
7186
+ "+371",
7187
+ "+372",
7188
+ "+385",
7189
+ "+386",
7190
+ "+39",
7191
+ "+40",
7192
+ "+420",
7193
+ "+421",
7194
+ "+43",
7195
+ "+45",
7196
+ "+46",
7197
+ "+48",
7198
+ "+49"
7199
+ ];
7200
+ var PREMIUM_OR_SPECIAL_PREFIXES = [
7201
+ "+1900",
7202
+ "+1976",
7203
+ "+43810",
7204
+ "+43820",
7205
+ "+43821",
7206
+ "+43828",
7207
+ "+43900",
7208
+ "+43901",
7209
+ "+43930",
7210
+ "+43931",
7211
+ "+49190",
7212
+ "+49900"
7213
+ ];
7214
+ function issue(code, field, message) {
7215
+ return { code, field, message };
7216
+ }
7217
+ function isRecord(value) {
7218
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7219
+ }
7220
+ function isPhoneRegionScope(value) {
7221
+ return typeof value === "string" && PHONE_REGION_SCOPES.includes(value);
7222
+ }
7223
+ function isTelephonyTransportCapability(value) {
7224
+ return typeof value === "string" && TELEPHONY_TRANSPORT_CAPABILITIES.includes(value);
7225
+ }
7226
+ function readPositiveInteger(value) {
7227
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
7228
+ }
7229
+ function readNonNegativeNumber(value) {
7230
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
7231
+ }
7232
+ function readBoolean(value) {
7233
+ return typeof value === "boolean" ? value : null;
7234
+ }
7235
+ function readRegionList(value) {
7236
+ if (!Array.isArray(value) || value.length === 0) return null;
7237
+ const regions = value.filter(isPhoneRegionScope);
7238
+ if (regions.length !== value.length) return null;
7239
+ return Array.from(new Set(regions));
7240
+ }
7241
+ function readCapabilityList(value) {
7242
+ if (!Array.isArray(value) || value.length === 0) return null;
7243
+ const capabilities = value.filter(isTelephonyTransportCapability);
7244
+ if (capabilities.length !== value.length) return null;
7245
+ return Array.from(new Set(capabilities));
7246
+ }
7247
+ function validateConfirmPolicy(value) {
7248
+ const issues = [];
7249
+ if (!isRecord(value)) {
7250
+ return [issue("confirm-policy-required", "policy.confirmPolicy", "confirmPolicy is required")];
7251
+ }
7252
+ const required = {
7253
+ paymentDetails: "never",
7254
+ contractCommitment: "never",
7255
+ costOverLimit: "needs_operator",
7256
+ sensitivePersonalData: "needs_operator",
7257
+ unclearAlternative: "needs_operator"
7258
+ };
7259
+ for (const [field, expected] of Object.entries(required)) {
7260
+ if (value[field] !== expected) {
7261
+ issues.push(issue(
7262
+ "unsafe-confirm-policy",
7263
+ `policy.confirmPolicy.${field}`,
7264
+ `${field} must be ${expected}`
7265
+ ));
7266
+ }
7267
+ }
7268
+ return issues;
7269
+ }
7270
+ function validateAlternativePolicy(value) {
7271
+ if (!isRecord(value)) {
7272
+ return [issue("alternative-policy-required", "policy.alternativePolicy", "alternativePolicy is required")];
7273
+ }
7274
+ const maxTimeShiftMinutes = readNonNegativeNumber(value.maxTimeShiftMinutes);
7275
+ if (maxTimeShiftMinutes === null || !Number.isInteger(maxTimeShiftMinutes)) {
7276
+ return [issue(
7277
+ "invalid-alternative-policy",
7278
+ "policy.alternativePolicy.maxTimeShiftMinutes",
7279
+ "maxTimeShiftMinutes must be a non-negative integer"
7280
+ )];
7281
+ }
7282
+ return [];
7283
+ }
7284
+ function validatePhoneMissionPolicy(policy) {
7285
+ const issues = [];
7286
+ if (!isRecord(policy)) {
7287
+ return { ok: false, issues: [issue("policy-required", "policy", "policy is required")] };
7288
+ }
7289
+ if (policy.policyVersion !== 1) {
7290
+ issues.push(issue("unsupported-policy-version", "policy.policyVersion", "policyVersion must be 1"));
7291
+ }
7292
+ const regionAllowlist = readRegionList(policy.regionAllowlist);
7293
+ if (!regionAllowlist) {
7294
+ issues.push(issue("invalid-region-allowlist", "policy.regionAllowlist", "regionAllowlist must contain at least one supported region"));
7295
+ }
7296
+ const maxCallDurationSeconds = readPositiveInteger(policy.maxCallDurationSeconds);
7297
+ if (maxCallDurationSeconds === null) {
7298
+ issues.push(issue("invalid-max-duration", "policy.maxCallDurationSeconds", "maxCallDurationSeconds must be a positive integer"));
7299
+ }
7300
+ const maxCostPerMission = readNonNegativeNumber(policy.maxCostPerMission);
7301
+ if (maxCostPerMission === null) {
7302
+ issues.push(issue("invalid-max-cost", "policy.maxCostPerMission", "maxCostPerMission must be a non-negative number"));
7303
+ }
7304
+ const maxAttempts = readPositiveInteger(policy.maxAttempts);
7305
+ if (maxAttempts === null) {
7306
+ issues.push(issue("invalid-max-attempts", "policy.maxAttempts", "maxAttempts must be a positive integer"));
7307
+ }
7308
+ const transcriptEnabled = readBoolean(policy.transcriptEnabled);
7309
+ if (transcriptEnabled === null) {
7310
+ issues.push(issue("invalid-transcript-enabled", "policy.transcriptEnabled", "transcriptEnabled must be boolean"));
7311
+ }
7312
+ const recordingEnabled = readBoolean(policy.recordingEnabled);
7313
+ if (recordingEnabled === null) {
7314
+ issues.push(issue("invalid-recording-enabled", "policy.recordingEnabled", "recordingEnabled must be boolean"));
7315
+ }
7316
+ issues.push(...validateConfirmPolicy(policy.confirmPolicy));
7317
+ issues.push(...validateAlternativePolicy(policy.alternativePolicy));
7318
+ if (issues.length > 0) return { ok: false, issues };
7319
+ return {
7320
+ ok: true,
7321
+ policy: {
7322
+ policyVersion: 1,
7323
+ regionAllowlist,
7324
+ maxCallDurationSeconds: Math.min(maxCallDurationSeconds, PHONE_SERVER_MAX_CALL_DURATION_SECONDS),
7325
+ maxCostPerMission: Math.min(maxCostPerMission, PHONE_SERVER_MAX_COST_PER_MISSION),
7326
+ maxAttempts: Math.min(maxAttempts, PHONE_SERVER_MAX_ATTEMPTS),
7327
+ transcriptEnabled,
7328
+ recordingEnabled,
7329
+ confirmPolicy: policy.confirmPolicy,
7330
+ alternativePolicy: {
7331
+ maxTimeShiftMinutes: policy.alternativePolicy.maxTimeShiftMinutes
7332
+ }
7333
+ },
7334
+ issues: []
7335
+ };
7336
+ }
7337
+ function validatePhoneTransportProfile(transport) {
7338
+ const issues = [];
7339
+ if (!isRecord(transport)) {
7340
+ return { ok: false, issues: [issue("transport-required", "transport", "transport profile is required")] };
7341
+ }
7342
+ const provider = typeof transport.provider === "string" ? transport.provider.trim() : "";
7343
+ if (!provider) {
7344
+ issues.push(issue("invalid-provider", "transport.provider", "provider is required"));
7345
+ }
7346
+ const phoneNumber = typeof transport.phoneNumber === "string" ? normalizePhoneNumber(transport.phoneNumber) : null;
7347
+ if (!phoneNumber) {
7348
+ issues.push(issue("invalid-transport-number", "transport.phoneNumber", "transport phoneNumber must be valid E.164"));
7349
+ }
7350
+ const capabilities = readCapabilityList(transport.capabilities);
7351
+ if (!capabilities) {
7352
+ issues.push(issue("invalid-capabilities", "transport.capabilities", "capabilities must contain supported transport capabilities"));
7353
+ } else if (!capabilities.includes("call_control")) {
7354
+ issues.push(issue("missing-call-control", "transport.capabilities", "transport must support call_control to start phone missions"));
7355
+ }
7356
+ const supportedRegions = readRegionList(transport.supportedRegions);
7357
+ if (!supportedRegions) {
7358
+ issues.push(issue("invalid-supported-regions", "transport.supportedRegions", "supportedRegions must contain at least one supported region"));
7359
+ }
7360
+ if (issues.length > 0) return { ok: false, issues };
7361
+ return {
7362
+ ok: true,
7363
+ transport: {
7364
+ provider,
7365
+ phoneNumber,
7366
+ capabilities,
7367
+ supportedRegions
7368
+ },
7369
+ issues: []
7370
+ };
7371
+ }
7372
+ function inferPhoneRegion(phoneNumber) {
7373
+ const normalized = normalizePhoneNumber(phoneNumber);
7374
+ if (!normalized) return null;
7375
+ if (normalized.startsWith("+43")) return "AT";
7376
+ if (normalized.startsWith("+49")) return "DE";
7377
+ if (EU_DIAL_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return "EU";
7378
+ return "WORLD";
7379
+ }
7380
+ function isPhoneRegionAllowed(region, allowlist) {
7381
+ if (allowlist.includes("WORLD")) return true;
7382
+ if (allowlist.includes(region)) return true;
7383
+ if ((region === "AT" || region === "DE" || region === "EU") && allowlist.includes("EU")) return true;
7384
+ return false;
7385
+ }
7386
+ function classifyPhoneNumberRisk(phoneNumber) {
7387
+ const normalized = normalizePhoneNumber(phoneNumber);
7388
+ if (!normalized) return "invalid";
7389
+ if (PREMIUM_OR_SPECIAL_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
7390
+ return "premium_or_special";
7391
+ }
7392
+ return "standard";
7393
+ }
7394
+ function validatePhoneMissionStart(input, transport, options = {}) {
7395
+ const issues = [];
7396
+ if (!isRecord(input)) {
7397
+ return { ok: false, issues: [issue("start-input-required", "input", "start input is required")] };
7398
+ }
7399
+ const to = typeof input.to === "string" ? normalizePhoneNumber(input.to) : null;
7400
+ if (!to) {
7401
+ issues.push(issue("invalid-target-number", "input.to", "target number must be valid E.164"));
7402
+ }
7403
+ const rawTask = typeof input.task === "string" ? input.task : "";
7404
+ const task = rawTask.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").trim();
7405
+ if (!task) {
7406
+ issues.push(issue("task-required", "input.task", "task is required"));
7407
+ } else if (task.length > PHONE_TASK_MAX_LENGTH) {
7408
+ issues.push(issue("task-too-long", "input.task", `task must be ${PHONE_TASK_MAX_LENGTH} characters or fewer`));
7409
+ }
7410
+ const policyResult = validatePhoneMissionPolicy(input.policy);
7411
+ if (!policyResult.ok) issues.push(...policyResult.issues);
7412
+ const transportResult = validatePhoneTransportProfile(transport);
7413
+ if (!transportResult.ok) issues.push(...transportResult.issues);
7414
+ const risk = typeof input.to === "string" ? classifyPhoneNumberRisk(input.to) : "invalid";
7415
+ if (risk === "premium_or_special" && !options.allowPremiumOrSpecialNumbers) {
7416
+ issues.push(issue("premium-number-blocked", "input.to", "premium or special-rate numbers require an explicit allowlist"));
7417
+ }
7418
+ const targetRegion = to ? inferPhoneRegion(to) : null;
7419
+ if (!targetRegion) {
7420
+ issues.push(issue("unknown-target-region", "input.to", "target region could not be inferred"));
7421
+ }
7422
+ if (policyResult.ok && targetRegion && !isPhoneRegionAllowed(targetRegion, policyResult.policy.regionAllowlist)) {
7423
+ issues.push(issue("region-not-allowed", "input.to", "target number is outside the mission policy regionAllowlist"));
7424
+ }
7425
+ if (transportResult.ok && targetRegion && !isPhoneRegionAllowed(targetRegion, transportResult.transport.supportedRegions)) {
7426
+ issues.push(issue("transport-region-unsupported", "transport.supportedRegions", "target number is outside the transport supportedRegions"));
7427
+ }
7428
+ const capabilities = transportResult.ok ? transportResult.transport.capabilities : [];
7429
+ if (policyResult.ok && policyResult.policy.recordingEnabled && !capabilities.includes("recording_supported")) {
7430
+ issues.push(issue("recording-unsupported", "policy.recordingEnabled", "recordingEnabled requires transport recording_supported capability"));
7431
+ }
7432
+ if (issues.length > 0 || !policyResult.ok || !transportResult.ok || !to || !targetRegion) {
7433
+ return { ok: false, issues };
7434
+ }
7435
+ return {
7436
+ ok: true,
7437
+ mission: {
7438
+ to,
7439
+ task,
7440
+ policy: policyResult.policy,
7441
+ targetRegion,
7442
+ transport: transportResult.transport,
7443
+ voiceRuntimeRef: typeof input.voiceRuntimeRef === "string" && input.voiceRuntimeRef.trim() ? input.voiceRuntimeRef.trim() : void 0
7444
+ },
7445
+ issues: []
7446
+ };
7447
+ }
7448
+
7449
+ // src/phone/manager.ts
7450
+ var PHONE_RATE_LIMIT_PER_MINUTE = 5;
7451
+ var PHONE_RATE_LIMIT_PER_HOUR = 30;
7452
+ var PHONE_MAX_CONCURRENT_MISSIONS = 3;
7453
+ var PHONE_MIN_WEBHOOK_SECRET_LENGTH = 24;
7454
+ var TERMINAL_MISSION_STATES = ["completed", "failed", "cancelled"];
7455
+ var PhoneWebhookAuthError = class extends Error {
7456
+ isPhoneWebhookAuthError = true;
7457
+ constructor() {
7458
+ super("Invalid phone webhook request");
7459
+ this.name = "PhoneWebhookAuthError";
7460
+ }
7461
+ };
7462
+ var PhoneRateLimitError = class extends Error {
7463
+ isPhoneRateLimitError = true;
7464
+ constructor(message) {
7465
+ super(message);
7466
+ this.name = "PhoneRateLimitError";
7467
+ }
7468
+ };
7469
+ var PHONE_SECRET_FIELDS = ["password", "webhookSecret"];
7470
+ var MAX_PHONE_WEBHOOK_EVENT_KEYS = 50;
7471
+ function asString3(value) {
7472
+ return typeof value === "string" ? value.trim() : "";
7473
+ }
7474
+ function asRecord2(value) {
7475
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
7476
+ }
7477
+ function defaultApiUrl2(config) {
7478
+ const url = (config.apiUrl || "https://api.46elks.com/a1").replace(/\/+$/, "");
7479
+ if (!/^https:\/\//i.test(url)) {
7480
+ throw new Error("46elks apiUrl must use https:// \u2014 refusing to send credentials over a non-TLS connection");
7481
+ }
7482
+ return url;
7483
+ }
7484
+ function basicAuth2(username, password) {
7485
+ return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
7486
+ }
7487
+ function secretMatches(provided, expected) {
7488
+ const a = Buffer.from(provided);
7489
+ const b = Buffer.from(expected);
7490
+ return a.length === b.length && (0, import_node_crypto3.timingSafeEqual)(a, b);
7491
+ }
7492
+ function apiBaseUrl(webhookBaseUrl) {
7493
+ const root = webhookBaseUrl.replace(/\/+$/, "");
7494
+ return root.endsWith("/api/agenticmail") ? root : `${root}/api/agenticmail`;
7495
+ }
7496
+ function webhookToken(webhookSecret, missionId) {
7497
+ return (0, import_node_crypto3.createHmac)("sha256", webhookSecret).update(missionId).digest("hex");
7498
+ }
7499
+ function buildWebhookUrl(config, path2, missionId) {
7500
+ const url = new URL(`${apiBaseUrl(config.webhookBaseUrl)}${path2}`);
7501
+ url.searchParams.set("missionId", missionId);
7502
+ url.searchParams.set("token", webhookToken(config.webhookSecret, missionId));
7503
+ return url.toString();
7504
+ }
7505
+ function redactWebhookUrl(value) {
7506
+ try {
7507
+ const url = new URL(value);
7508
+ if (url.searchParams.has("token")) url.searchParams.set("token", "***");
7509
+ if (url.searchParams.has("secret")) url.searchParams.set("secret", "***");
7510
+ return url.toString();
7511
+ } catch {
7512
+ return "[redacted-url]";
7513
+ }
7514
+ }
7515
+ function redactProviderRequest(request) {
7516
+ return {
7517
+ url: request.url,
7518
+ body: {
7519
+ ...request.body,
7520
+ voice_start: redactWebhookUrl(request.body.voice_start),
7521
+ whenhangup: redactWebhookUrl(request.body.whenhangup)
7522
+ }
7523
+ };
7524
+ }
7525
+ function stableFlatJson(value) {
7526
+ return JSON.stringify(Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))));
7527
+ }
7528
+ function phoneWebhookEventKey(kind, payload) {
7529
+ const callId = asString3(payload.callid) || asString3(payload.id) || asString3(payload.call_id);
7530
+ const result = asString3(payload.result) || asString3(payload.status) || asString3(payload.why);
7531
+ const fingerprint = (0, import_node_crypto3.createHash)("sha256").update(stableFlatJson(payload)).digest("hex").slice(0, 16);
7532
+ return [kind, callId || fingerprint, result].filter(Boolean).join(":");
7533
+ }
7534
+ function processedWebhookEventKeys(mission) {
7535
+ const value = mission.metadata.phoneWebhookEvents;
7536
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
7537
+ }
7538
+ function hasProcessedWebhookEvent(mission, eventKey) {
7539
+ return processedWebhookEventKeys(mission).includes(eventKey);
7540
+ }
7541
+ function appendProcessedWebhookEvent(mission, eventKey) {
7542
+ return [...processedWebhookEventKeys(mission), eventKey].slice(-MAX_PHONE_WEBHOOK_EVENT_KEYS);
7543
+ }
7544
+ function parseJson(value, fallback) {
7545
+ if (!value) return fallback;
7546
+ try {
7547
+ return JSON.parse(value);
7548
+ } catch {
7549
+ return fallback;
7550
+ }
7551
+ }
7552
+ function rowToMission(row) {
7553
+ return {
7554
+ id: row.id,
7555
+ agentId: row.agent_id,
7556
+ status: row.status,
7557
+ from: row.from_phone,
7558
+ to: row.to_phone,
7559
+ task: row.task,
7560
+ policy: parseJson(row.policy_json, {}),
7561
+ transport: parseJson(row.transport_json, {}),
7562
+ provider: row.provider,
7563
+ providerCallId: row.provider_call_id ?? void 0,
7564
+ transcript: parseJson(row.transcript_json, []),
7565
+ metadata: parseJson(row.metadata_json, {}),
7566
+ createdAt: row.created_at,
7567
+ updatedAt: row.updated_at
7568
+ };
7569
+ }
7570
+ function redactPhoneTransportConfig(config) {
7571
+ return {
7572
+ ...config,
7573
+ password: config.password ? "***" : "",
7574
+ webhookSecret: config.webhookSecret ? "***" : ""
7575
+ };
7576
+ }
7577
+ var PhoneManager = class {
7578
+ constructor(db2, encryptionKey) {
7579
+ this.db = db2;
7580
+ this.encryptionKey = encryptionKey;
7581
+ this.ensureTables();
7582
+ }
7583
+ initialized = false;
7584
+ /** Per-agent outbound-call timestamps (ms) for the in-memory rate limiter. */
7585
+ callTimestamps = /* @__PURE__ */ new Map();
7586
+ /**
7587
+ * Abuse / cost gate for /calls/start (#43-H1). Each non-dry-run call is
7588
+ * a real billed outbound call, so before dialing we enforce:
7589
+ * - a hard cap on concurrently-active (non-terminal) missions, and
7590
+ * - a per-agent token-bucket rate limit (per-minute + per-hour).
7591
+ * Throws {@link PhoneRateLimitError} (-> HTTP 429) when a limit is hit.
7592
+ * Call only on the real path — dry runs place no call and are exempt.
7593
+ */
7594
+ enforceCallLimits(agentId, nowMs) {
7595
+ const activeRow = this.db.prepare(
7596
+ `SELECT COUNT(*) AS cnt FROM phone_missions
7597
+ WHERE agent_id = ? AND status NOT IN ('completed', 'failed', 'cancelled')`
7598
+ ).get(agentId);
7599
+ if ((activeRow?.cnt ?? 0) >= PHONE_MAX_CONCURRENT_MISSIONS) {
7600
+ throw new PhoneRateLimitError(
7601
+ `Too many active phone missions (max ${PHONE_MAX_CONCURRENT_MISSIONS}). Wait for an active call to end before starting another.`
7602
+ );
7603
+ }
7604
+ const recent = (this.callTimestamps.get(agentId) ?? []).filter((ts) => nowMs - ts < 36e5);
7605
+ const lastMinute = recent.filter((ts) => nowMs - ts < 6e4).length;
7606
+ if (lastMinute >= PHONE_RATE_LIMIT_PER_MINUTE) {
7607
+ throw new PhoneRateLimitError(
7608
+ `Phone call rate limit reached (max ${PHONE_RATE_LIMIT_PER_MINUTE}/minute). Try again shortly.`
7609
+ );
7610
+ }
7611
+ if (recent.length >= PHONE_RATE_LIMIT_PER_HOUR) {
7612
+ throw new PhoneRateLimitError(
7613
+ `Phone call rate limit reached (max ${PHONE_RATE_LIMIT_PER_HOUR}/hour). Try again later.`
7614
+ );
7615
+ }
7616
+ recent.push(nowMs);
7617
+ this.callTimestamps.set(agentId, recent);
7618
+ }
7619
+ encryptConfig(config) {
7620
+ if (!this.encryptionKey) return config;
7621
+ const out = { ...config };
7622
+ for (const field of PHONE_SECRET_FIELDS) {
7623
+ const value = out[field];
7624
+ if (typeof value === "string" && value && !isEncryptedSecret(value)) {
7625
+ out[field] = encryptSecret(value, this.encryptionKey);
7626
+ }
7627
+ }
7628
+ return out;
7629
+ }
7630
+ decryptConfig(config) {
7631
+ if (!this.encryptionKey) return config;
7632
+ const out = { ...config };
7633
+ for (const field of PHONE_SECRET_FIELDS) {
7634
+ const value = out[field];
7635
+ if (typeof value === "string" && isEncryptedSecret(value)) {
7636
+ try {
7637
+ out[field] = decryptSecret(value, this.encryptionKey);
7638
+ } catch {
7639
+ }
7640
+ }
7641
+ }
7642
+ return out;
7643
+ }
7644
+ ensureTables() {
7645
+ if (this.initialized) return;
7646
+ this.db.exec(`
7647
+ CREATE TABLE IF NOT EXISTS phone_missions (
7648
+ id TEXT PRIMARY KEY,
7649
+ agent_id TEXT NOT NULL,
7650
+ status TEXT NOT NULL,
7651
+ from_phone TEXT NOT NULL,
7652
+ to_phone TEXT NOT NULL,
7653
+ task TEXT NOT NULL,
7654
+ policy_json TEXT NOT NULL,
7655
+ transport_json TEXT NOT NULL,
7656
+ provider TEXT NOT NULL,
7657
+ provider_call_id TEXT,
7658
+ transcript_json TEXT NOT NULL DEFAULT '[]',
7659
+ metadata_json TEXT NOT NULL DEFAULT '{}',
7660
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
7661
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
7662
+ )
7663
+ `);
7664
+ try {
7665
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_phone_missions_agent ON phone_missions(agent_id)");
7666
+ } catch {
7667
+ }
7668
+ try {
7669
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_phone_missions_status ON phone_missions(status)");
7670
+ } catch {
7671
+ }
7672
+ this.initialized = true;
7673
+ }
7674
+ getPhoneTransportConfig(agentId) {
7675
+ const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
7676
+ if (!row) return null;
7677
+ const meta = parseJson(row.metadata, {});
7678
+ const config = meta.phoneTransport;
7679
+ if (!config || typeof config !== "object") return null;
7680
+ return this.decryptConfig(config);
7681
+ }
7682
+ savePhoneTransportConfig(agentId, config) {
7683
+ const row = this.db.prepare("SELECT metadata FROM agents WHERE id = ?").get(agentId);
7684
+ if (!row) throw new Error(`Agent ${agentId} not found`);
7685
+ const transportCheck = validatePhoneTransportProfile(config);
7686
+ if (!transportCheck.ok) {
7687
+ throw new Error(`Invalid phone transport config: ${transportCheck.issues.map((item) => `${item.field}: ${item.message}`).join("; ")}`);
7688
+ }
7689
+ const meta = parseJson(row.metadata, {});
7690
+ meta.phoneTransport = this.encryptConfig({
7691
+ ...config,
7692
+ phoneNumber: transportCheck.transport.phoneNumber,
7693
+ capabilities: transportCheck.transport.capabilities,
7694
+ supportedRegions: transportCheck.transport.supportedRegions
7695
+ });
7696
+ this.db.prepare("UPDATE agents SET metadata = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(meta), agentId);
7697
+ return config;
7698
+ }
7699
+ getMission(missionId, agentId) {
7700
+ const row = agentId ? this.db.prepare("SELECT * FROM phone_missions WHERE id = ? AND agent_id = ?").get(missionId, agentId) : this.db.prepare("SELECT * FROM phone_missions WHERE id = ?").get(missionId);
7701
+ return row ? rowToMission(row) : null;
7702
+ }
7703
+ listMissions(agentId, opts = {}) {
7704
+ const limit = Math.min(Math.max(opts.limit ?? 20, 1), 100);
7705
+ const offset = Math.max(opts.offset ?? 0, 0);
7706
+ const params = [agentId];
7707
+ let sql = "SELECT * FROM phone_missions WHERE agent_id = ?";
7708
+ if (opts.status && PHONE_MISSION_STATES.includes(opts.status)) {
7709
+ sql += " AND status = ?";
7710
+ params.push(opts.status);
7711
+ }
7712
+ sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
7713
+ params.push(limit, offset);
7714
+ return this.db.prepare(sql).all(...params).map(rowToMission);
7715
+ }
7716
+ async startMission(agentId, input, options = {}) {
7717
+ const config = this.getPhoneTransportConfig(agentId);
7718
+ if (!config) {
7719
+ throw new Error("Phone transport is not configured. Use phone_transport_setup first.");
7720
+ }
7721
+ if (config.provider !== "46elks") {
7722
+ throw new Error(`Phone provider ${config.provider} does not support call_control yet`);
7723
+ }
7724
+ const validation = validatePhoneMissionStart(input, config);
7725
+ if (!validation.ok) {
7726
+ throw new Error(`Invalid phone mission: ${validation.issues.map((item) => `${item.code} (${item.field})`).join(", ")}`);
7727
+ }
7728
+ const now = options.now ?? /* @__PURE__ */ new Date();
7729
+ if (!options.dryRun) {
7730
+ this.enforceCallLimits(agentId, now.getTime());
7731
+ }
7732
+ const missionId = `call_${(0, import_node_crypto3.randomUUID)()}`;
7733
+ const transcript = [{
7734
+ at: now.toISOString(),
7735
+ source: "system",
7736
+ text: "Phone mission created; outbound carrier call requested."
7737
+ }];
7738
+ const metadata = {
7739
+ voiceRuntimeRef: validation.mission.voiceRuntimeRef,
7740
+ targetRegion: validation.mission.targetRegion,
7741
+ dryRun: !!options.dryRun,
7742
+ // Attempt counter (#43-H2) — wired for breach detection; there is
7743
+ // no automatic retry loop today, so a fresh mission is attempt 1.
7744
+ attempts: 1
7745
+ };
7746
+ const mission = {
7747
+ id: missionId,
7748
+ agentId,
7749
+ status: "dialing",
7750
+ from: config.phoneNumber,
7751
+ to: validation.mission.to,
7752
+ task: validation.mission.task,
7753
+ policy: validation.mission.policy,
7754
+ transport: validation.mission.transport,
7755
+ provider: config.provider,
7756
+ transcript,
7757
+ metadata,
7758
+ createdAt: now.toISOString(),
7759
+ updatedAt: now.toISOString()
7760
+ };
7761
+ this.insertMission(mission);
7762
+ const providerRequest = this.build46ElksCallRequest(config, mission);
7763
+ if (options.dryRun) {
7764
+ const updated2 = this.updateProviderCall(missionId, "dryrun-call", {
7765
+ dryRun: true,
7766
+ providerRequest: redactProviderRequest(providerRequest)
7767
+ });
7768
+ return { mission: updated2, providerRequest };
7769
+ }
7770
+ const fetchFn = options.fetchFn ?? fetch;
7771
+ let response;
7772
+ try {
7773
+ response = await fetchFn(providerRequest.url, {
7774
+ method: "POST",
7775
+ headers: {
7776
+ "Authorization": `Basic ${basicAuth2(config.username, config.password)}`,
7777
+ "Content-Type": "application/x-www-form-urlencoded"
7778
+ },
7779
+ body: new URLSearchParams(providerRequest.body),
7780
+ signal: AbortSignal.timeout(15e3)
7781
+ });
7782
+ } catch (err) {
7783
+ const message = err?.message ?? String(err);
7784
+ this.updateMissionStatus(missionId, "failed", {
7785
+ providerError: message
7786
+ }, [{
7787
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7788
+ source: "provider",
7789
+ text: "46elks call start failed \u2014 the provider request threw before any response.",
7790
+ metadata: { error: message }
7791
+ }]);
7792
+ throw err;
7793
+ }
7794
+ const text = await response.text();
7795
+ let raw = text;
7796
+ try {
7797
+ raw = JSON.parse(text);
7798
+ } catch {
7799
+ }
7800
+ if (!response.ok) {
7801
+ const failed = this.updateMissionStatus(missionId, "failed", {
7802
+ providerStatus: response.status,
7803
+ providerResponse: raw
7804
+ }, [{
7805
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7806
+ source: "provider",
7807
+ text: `46elks call start failed with HTTP ${response.status}.`,
7808
+ metadata: { providerResponse: raw }
7809
+ }]);
7810
+ throw new Error(`46elks call start failed (${response.status}) for mission ${failed.id}`);
7811
+ }
7812
+ const providerCallId = asRecord2(raw).id ? String(asRecord2(raw).id) : void 0;
7813
+ const updated = this.updateProviderCall(missionId, providerCallId, { providerResponse: raw });
7814
+ return { mission: updated, providerRequest, providerResponse: raw };
7815
+ }
7816
+ /**
7817
+ * Verify a webhook request and return the mission, or throw a uniform
7818
+ * {@link PhoneWebhookAuthError} for ANY failure (unknown mission, no
7819
+ * token, wrong token). Uniform on purpose — no 404-vs-403 oracle.
7820
+ */
7821
+ authenticateWebhook(missionId, providedToken) {
7822
+ const mission = missionId ? this.getMission(missionId) : null;
7823
+ const config = mission ? this.getPhoneTransportConfig(mission.agentId) : null;
7824
+ if (!mission || !config || !providedToken || !secretMatches(providedToken, webhookToken(config.webhookSecret, mission.id))) {
7825
+ throw new PhoneWebhookAuthError();
7826
+ }
7827
+ return mission;
7828
+ }
7829
+ handleVoiceStartWebhook(missionId, providedToken, payload = {}) {
7830
+ const mission = this.authenticateWebhook(missionId, providedToken);
7831
+ if (TERMINAL_MISSION_STATES.includes(mission.status)) {
7832
+ return { mission, action: this.buildVoiceStartAction() };
7833
+ }
7834
+ const eventKey = phoneWebhookEventKey("voice_start", payload);
7835
+ if (hasProcessedWebhookEvent(mission, eventKey)) {
7836
+ return {
7837
+ mission,
7838
+ action: this.buildVoiceStartAction()
7839
+ };
7840
+ }
7841
+ const updated = this.updateMissionStatus(mission.id, "connected", {
7842
+ lastVoiceStartPayload: payload,
7843
+ phoneWebhookEvents: appendProcessedWebhookEvent(mission, eventKey)
7844
+ }, [{
7845
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7846
+ source: "provider",
7847
+ text: "46elks voice_start webhook received. Realtime voice runtime is not connected in this slice.",
7848
+ metadata: { payload }
7849
+ }]);
7850
+ return {
7851
+ mission: updated,
7852
+ action: this.buildVoiceStartAction()
7853
+ };
7854
+ }
7855
+ handleHangupWebhook(missionId, providedToken, payload = {}) {
7856
+ const mission = this.authenticateWebhook(missionId, providedToken);
7857
+ const eventKey = phoneWebhookEventKey("hangup", payload);
7858
+ if (hasProcessedWebhookEvent(mission, eventKey)) {
7859
+ return mission;
7860
+ }
7861
+ const costPatch = this.buildCostMetadataPatch(mission, payload);
7862
+ const nextStatus = TERMINAL_MISSION_STATES.includes(mission.status) ? mission.status : "failed";
7863
+ const transcript = [{
7864
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7865
+ source: "provider",
7866
+ text: nextStatus === "failed" ? "46elks hangup webhook received before a conversation runtime completed the mission." : "46elks hangup webhook received.",
7867
+ metadata: { payload }
7868
+ }];
7869
+ if (costPatch.costExceeded) {
7870
+ transcript.push({
7871
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7872
+ source: "system",
7873
+ text: `Mission cost ${costPatch.totalCost} exceeded the policy cap of ${mission.policy.maxCostPerMission}.`
7874
+ });
7875
+ }
7876
+ return this.updateMissionStatus(mission.id, nextStatus, {
7877
+ lastHangupPayload: payload,
7878
+ hangupReason: nextStatus === "failed" ? "call-ended-before-conversation-runtime" : void 0,
7879
+ phoneWebhookEvents: appendProcessedWebhookEvent(mission, eventKey),
7880
+ ...costPatch
7881
+ }, transcript);
7882
+ }
7883
+ /**
7884
+ * Read the call cost off a 46elks hangup payload, add it to the
7885
+ * mission's running total, and flag a policy-cap breach (#43-H2).
7886
+ * Cost is only knowable post-call from the provider — the preventive
7887
+ * cost controls are the duration ceiling, rate limit, and concurrency
7888
+ * cap; this is the after-the-fact accounting + alerting.
7889
+ */
7890
+ buildCostMetadataPatch(mission, payload) {
7891
+ const rawCost = payload.cost;
7892
+ const callCost = typeof rawCost === "number" && Number.isFinite(rawCost) && rawCost >= 0 ? rawCost : Number.parseFloat(asString3(rawCost)) || 0;
7893
+ const priorCost = typeof mission.metadata.totalCost === "number" ? mission.metadata.totalCost : 0;
7894
+ const totalCost = Math.round((priorCost + callCost) * 1e6) / 1e6;
7895
+ const cap = mission.policy?.maxCostPerMission;
7896
+ const costExceeded = typeof cap === "number" && totalCost > cap;
7897
+ return { totalCost, costExceeded };
7898
+ }
7899
+ buildVoiceStartAction() {
7900
+ return {
7901
+ play: "AgenticMail has received this call mission. The live voice runtime is not connected yet; the operator will follow up."
7902
+ };
7903
+ }
7904
+ cancelMission(agentId, missionId) {
7905
+ const mission = this.getMission(missionId, agentId);
7906
+ if (!mission) throw new Error("Phone mission not found");
7907
+ return this.updateMissionStatus(mission.id, "cancelled", {}, [{
7908
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7909
+ source: "operator",
7910
+ text: "Phone mission cancelled."
7911
+ }]);
7912
+ }
7913
+ build46ElksCallRequest(config, mission) {
7914
+ const timeout = Math.min(Math.max(mission.policy.maxCallDurationSeconds, 1), PHONE_SERVER_MAX_CALL_DURATION_SECONDS);
7915
+ return {
7916
+ url: `${defaultApiUrl2(config)}/calls`,
7917
+ body: {
7918
+ from: config.phoneNumber,
7919
+ to: mission.to,
7920
+ voice_start: buildWebhookUrl(config, "/calls/webhook/46elks/voice-start", mission.id),
7921
+ whenhangup: buildWebhookUrl(config, "/calls/webhook/46elks/hangup", mission.id),
7922
+ timeout: String(timeout)
7923
+ }
7924
+ };
7925
+ }
7926
+ insertMission(mission) {
7927
+ this.db.prepare(`
7928
+ INSERT INTO phone_missions (
7929
+ id, agent_id, status, from_phone, to_phone, task,
7930
+ policy_json, transport_json, provider, provider_call_id,
7931
+ transcript_json, metadata_json, created_at, updated_at
7932
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7933
+ `).run(
7934
+ mission.id,
7935
+ mission.agentId,
7936
+ mission.status,
7937
+ mission.from,
7938
+ mission.to,
7939
+ mission.task,
7940
+ JSON.stringify(mission.policy),
7941
+ JSON.stringify(mission.transport),
7942
+ mission.provider,
7943
+ mission.providerCallId ?? null,
7944
+ JSON.stringify(mission.transcript),
7945
+ JSON.stringify(mission.metadata),
7946
+ mission.createdAt,
7947
+ mission.updatedAt
7948
+ );
7949
+ }
7950
+ updateProviderCall(missionId, providerCallId, metadata) {
7951
+ const mission = this.getMission(missionId);
7952
+ if (!mission) throw new Error("Phone mission not found");
7953
+ const nextMetadata = { ...mission.metadata, ...metadata };
7954
+ this.db.prepare(`
7955
+ UPDATE phone_missions
7956
+ SET provider_call_id = ?, metadata_json = ?, updated_at = ?
7957
+ WHERE id = ?
7958
+ `).run(providerCallId ?? null, JSON.stringify(nextMetadata), (/* @__PURE__ */ new Date()).toISOString(), missionId);
7959
+ return this.getMission(missionId);
7960
+ }
7961
+ updateMissionStatus(missionId, status, metadata, transcriptEntries = []) {
7962
+ const mission = this.getMission(missionId);
7963
+ if (!mission) throw new Error("Phone mission not found");
7964
+ const nextTranscript = [...mission.transcript, ...transcriptEntries];
7965
+ const nextMetadata = Object.fromEntries(
7966
+ Object.entries({ ...mission.metadata, ...metadata }).filter(([, value]) => value !== void 0)
7967
+ );
7968
+ this.db.prepare(`
7969
+ UPDATE phone_missions
7970
+ SET status = ?, transcript_json = ?, metadata_json = ?, updated_at = ?
7971
+ WHERE id = ?
7972
+ `).run(status, JSON.stringify(nextTranscript), JSON.stringify(nextMetadata), (/* @__PURE__ */ new Date()).toISOString(), missionId);
7973
+ return this.getMission(missionId);
7974
+ }
7975
+ };
7976
+ function buildPhoneTransportConfig(input) {
7977
+ const provider = asString3(input.provider) || "46elks";
7978
+ if (provider !== "46elks") throw new Error('provider must be "46elks"');
7979
+ const phoneNumber = normalizePhoneNumber(asString3(input.phoneNumber));
7980
+ if (!phoneNumber) throw new Error("phoneNumber must be a valid E.164 phone number");
7981
+ const username = asString3(input.username);
7982
+ const password = asString3(input.password);
7983
+ const webhookBaseUrl = asString3(input.webhookBaseUrl);
7984
+ const webhookSecret = asString3(input.webhookSecret);
7985
+ if (!username || !password) throw new Error('username and password are required for provider "46elks"');
7986
+ if (!webhookBaseUrl) throw new Error("webhookBaseUrl is required");
7987
+ if (!webhookSecret) throw new Error("webhookSecret is required");
7988
+ if (webhookSecret.length < PHONE_MIN_WEBHOOK_SECRET_LENGTH) {
7989
+ throw new Error(`webhookSecret must be at least ${PHONE_MIN_WEBHOOK_SECRET_LENGTH} characters`);
7990
+ }
7991
+ const parsedWebhookBaseUrl = new URL(webhookBaseUrl);
7992
+ if (parsedWebhookBaseUrl.protocol !== "https:" && parsedWebhookBaseUrl.hostname !== "127.0.0.1" && parsedWebhookBaseUrl.hostname !== "localhost") {
7993
+ throw new Error("webhookBaseUrl must use https:// unless it points at localhost");
7994
+ }
7995
+ const apiUrl = asString3(input.apiUrl);
7996
+ if (apiUrl) {
7997
+ const parsedApiUrl = new URL(apiUrl);
7998
+ if (parsedApiUrl.protocol !== "https:") {
7999
+ throw new Error("apiUrl must use https:// \u2014 credentials are sent on every request");
8000
+ }
8001
+ }
8002
+ const capabilities = Array.isArray(input.capabilities) ? input.capabilities.filter((item) => typeof item === "string" && ["sms", "call_control", "realtime_media", "recording_supported"].includes(item)) : ["call_control"];
8003
+ const supportedRegions = Array.isArray(input.supportedRegions) ? input.supportedRegions.filter((item) => typeof item === "string" && ["AT", "DE", "EU", "WORLD"].includes(item)) : ["EU"];
8004
+ const config = {
8005
+ provider,
8006
+ phoneNumber,
8007
+ username,
8008
+ password,
8009
+ webhookBaseUrl,
8010
+ webhookSecret,
8011
+ apiUrl: apiUrl || void 0,
8012
+ capabilities: Array.from(/* @__PURE__ */ new Set(["call_control", ...capabilities])),
8013
+ supportedRegions: supportedRegions.length ? Array.from(new Set(supportedRegions)) : ["EU"],
8014
+ configuredAt: input.configuredAt ?? (/* @__PURE__ */ new Date()).toISOString()
8015
+ };
8016
+ const validation = validatePhoneTransportProfile(config);
8017
+ if (!validation.ok) {
8018
+ throw new Error(`Invalid phone transport config: ${validation.issues.map((item) => `${item.field}: ${item.message}`).join("; ")}`);
8019
+ }
8020
+ return config;
8021
+ }
8022
+
7023
8023
  // src/telemetry.ts
7024
8024
  var import_crypto = require("crypto");
7025
8025
  var import_fs = require("fs");
@@ -7485,7 +8485,7 @@ function buildApiUrl(baseOrigin, pathAndQuery) {
7485
8485
  }
7486
8486
 
7487
8487
  // src/setup/index.ts
7488
- var import_node_crypto3 = require("crypto");
8488
+ var import_node_crypto4 = require("crypto");
7489
8489
  var import_node_fs9 = require("fs");
7490
8490
  var import_node_path11 = require("path");
7491
8491
  var import_node_os9 = require("os");
@@ -8696,8 +9696,8 @@ var SetupManager = class {
8696
9696
  if (!(0, import_node_fs9.existsSync)(dataDir)) {
8697
9697
  (0, import_node_fs9.mkdirSync)(dataDir, { recursive: true });
8698
9698
  }
8699
- const masterKey = `mk_${(0, import_node_crypto3.randomBytes)(24).toString("hex")}`;
8700
- const stalwartPassword = (0, import_node_crypto3.randomBytes)(16).toString("hex");
9699
+ const masterKey = `mk_${(0, import_node_crypto4.randomBytes)(24).toString("hex")}`;
9700
+ const stalwartPassword = (0, import_node_crypto4.randomBytes)(16).toString("hex");
8701
9701
  const config = {
8702
9702
  masterKey,
8703
9703
  stalwart: {
@@ -8835,7 +9835,7 @@ secret = "${password}"
8835
9835
  };
8836
9836
 
8837
9837
  // src/threading/thread-id.ts
8838
- var import_node_crypto4 = require("crypto");
9838
+ var import_node_crypto5 = require("crypto");
8839
9839
  function stripReplyPrefixes(subject) {
8840
9840
  let s = subject.length > 1e3 ? subject.slice(0, 1e3) : subject;
8841
9841
  for (; ; ) {
@@ -8864,7 +9864,7 @@ function normalizeAddress(addr) {
8864
9864
  }
8865
9865
  function threadIdFor(input) {
8866
9866
  const subject = normalizeSubject(input.subject);
8867
- return (0, import_node_crypto4.createHash)("sha256").update(subject).digest("base64url").slice(0, 16);
9867
+ return (0, import_node_crypto5.createHash)("sha256").update(subject).digest("base64url").slice(0, 16);
8868
9868
  }
8869
9869
 
8870
9870
  // src/threading/thread-cache.ts
@@ -9108,11 +10108,891 @@ function parse(raw) {
9108
10108
  }
9109
10109
  return out;
9110
10110
  }
10111
+
10112
+ // src/memory/manager.ts
10113
+ var import_node_crypto6 = require("crypto");
10114
+
10115
+ // src/memory/text-search.ts
10116
+ var BM25_K1 = 1.2;
10117
+ var BM25_B = 0.75;
10118
+ var FIELD_WEIGHT_TITLE = 3;
10119
+ var FIELD_WEIGHT_TAGS = 2;
10120
+ var FIELD_WEIGHT_CONTENT = 1;
10121
+ var PREFIX_MATCH_PENALTY = 0.7;
10122
+ var STOP_WORDS = /* @__PURE__ */ new Set([
10123
+ "a",
10124
+ "about",
10125
+ "above",
10126
+ "after",
10127
+ "again",
10128
+ "against",
10129
+ "all",
10130
+ "am",
10131
+ "an",
10132
+ "and",
10133
+ "any",
10134
+ "are",
10135
+ "as",
10136
+ "at",
10137
+ "be",
10138
+ "because",
10139
+ "been",
10140
+ "before",
10141
+ "being",
10142
+ "below",
10143
+ "between",
10144
+ "both",
10145
+ "but",
10146
+ "by",
10147
+ "can",
10148
+ "could",
10149
+ "did",
10150
+ "do",
10151
+ "does",
10152
+ "doing",
10153
+ "down",
10154
+ "during",
10155
+ "each",
10156
+ "either",
10157
+ "every",
10158
+ "few",
10159
+ "for",
10160
+ "from",
10161
+ "further",
10162
+ "get",
10163
+ "got",
10164
+ "had",
10165
+ "has",
10166
+ "have",
10167
+ "having",
10168
+ "he",
10169
+ "her",
10170
+ "here",
10171
+ "hers",
10172
+ "herself",
10173
+ "him",
10174
+ "himself",
10175
+ "his",
10176
+ "how",
10177
+ "i",
10178
+ "if",
10179
+ "in",
10180
+ "into",
10181
+ "is",
10182
+ "it",
10183
+ "its",
10184
+ "itself",
10185
+ "just",
10186
+ "may",
10187
+ "me",
10188
+ "might",
10189
+ "more",
10190
+ "most",
10191
+ "must",
10192
+ "my",
10193
+ "myself",
10194
+ "neither",
10195
+ "no",
10196
+ "nor",
10197
+ "not",
10198
+ "now",
10199
+ "of",
10200
+ "off",
10201
+ "on",
10202
+ "once",
10203
+ "only",
10204
+ "or",
10205
+ "other",
10206
+ "ought",
10207
+ "our",
10208
+ "ours",
10209
+ "ourselves",
10210
+ "out",
10211
+ "over",
10212
+ "own",
10213
+ "same",
10214
+ "shall",
10215
+ "she",
10216
+ "should",
10217
+ "so",
10218
+ "some",
10219
+ "such",
10220
+ "than",
10221
+ "that",
10222
+ "the",
10223
+ "their",
10224
+ "theirs",
10225
+ "them",
10226
+ "themselves",
10227
+ "then",
10228
+ "there",
10229
+ "these",
10230
+ "they",
10231
+ "this",
10232
+ "those",
10233
+ "through",
10234
+ "to",
10235
+ "too",
10236
+ "under",
10237
+ "until",
10238
+ "up",
10239
+ "us",
10240
+ "very",
10241
+ "was",
10242
+ "we",
10243
+ "were",
10244
+ "what",
10245
+ "when",
10246
+ "where",
10247
+ "which",
10248
+ "while",
10249
+ "who",
10250
+ "whom",
10251
+ "why",
10252
+ "will",
10253
+ "with",
10254
+ "would",
10255
+ "yet",
10256
+ "you",
10257
+ "your",
10258
+ "yours",
10259
+ "yourself",
10260
+ "yourselves"
10261
+ ]);
10262
+ var STEM_RULES = [
10263
+ // Step 1: plurals and past participles
10264
+ [/ies$/, "i", 3],
10265
+ // policies → polici,eries → eri
10266
+ [/sses$/, "ss", 4],
10267
+ // addresses → address
10268
+ [/([^s])s$/, "$1", 3],
10269
+ // items → item, but not "ss"
10270
+ [/eed$/, "ee", 4],
10271
+ // agreed → agree
10272
+ [/ed$/, "", 3],
10273
+ // configured → configur, but min length 3
10274
+ [/ing$/, "", 4],
10275
+ // running → runn → run (handled below)
10276
+ // Step 2: derivational suffixes
10277
+ [/ational$/, "ate", 6],
10278
+ // relational → relate
10279
+ [/tion$/, "t", 5],
10280
+ // adoption → adopt
10281
+ [/ness$/, "", 5],
10282
+ // awareness → aware
10283
+ [/ment$/, "", 5],
10284
+ // deployment → deploy
10285
+ [/able$/, "", 5],
10286
+ // configurable → configur
10287
+ [/ible$/, "", 5],
10288
+ // accessible → access
10289
+ [/ful$/, "", 5],
10290
+ // powerful → power
10291
+ [/ous$/, "", 5],
10292
+ // dangerous → danger
10293
+ [/ive$/, "", 5],
10294
+ // interactive → interact
10295
+ [/ize$/, "", 4],
10296
+ // normalize → normal
10297
+ [/ise$/, "", 4],
10298
+ // organise → organ
10299
+ [/ally$/, "", 5],
10300
+ // automatically → automat
10301
+ [/ly$/, "", 4],
10302
+ // quickly → quick
10303
+ [/er$/, "", 4]
10304
+ // handler → handl
10305
+ ];
10306
+ var DOUBLE_CONSONANT = /([^aeiou])\1$/;
10307
+ function stem(word) {
10308
+ if (word.length < 3) return word;
10309
+ let stemmed = word;
10310
+ for (const [pattern, replacement, minLen] of STEM_RULES) {
10311
+ if (stemmed.length >= minLen && pattern.test(stemmed)) {
10312
+ stemmed = stemmed.replace(pattern, replacement);
10313
+ break;
10314
+ }
10315
+ }
10316
+ if (stemmed.length > 2 && DOUBLE_CONSONANT.test(stemmed)) {
10317
+ stemmed = stemmed.slice(0, -1);
10318
+ }
10319
+ return stemmed;
10320
+ }
10321
+ function tokenize(text) {
10322
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t)).map(stem);
10323
+ }
10324
+ var MemorySearchIndex = class {
10325
+ /** Posting lists: stemmed term → Set of memory IDs containing it */
10326
+ postings = /* @__PURE__ */ new Map();
10327
+ /** Per-document metadata for BM25 scoring */
10328
+ docs = /* @__PURE__ */ new Map();
10329
+ /** Pre-computed IDF values. Stale flag triggers lazy recomputation. */
10330
+ idf = /* @__PURE__ */ new Map();
10331
+ idfStale = true;
10332
+ /** 3-character prefix map for prefix matching: prefix → Set of full stems */
10333
+ prefixMap = /* @__PURE__ */ new Map();
10334
+ /** Total weighted document length (for computing average) */
10335
+ totalWeightedLen = 0;
10336
+ get docCount() {
10337
+ return this.docs.size;
10338
+ }
10339
+ get avgDocLen() {
10340
+ return this.docs.size > 0 ? this.totalWeightedLen / this.docs.size : 1;
10341
+ }
10342
+ /**
10343
+ * Index a memory entry. Extracts stems from title, content, and tags
10344
+ * with field-specific weighting and builds posting lists.
10345
+ */
10346
+ addDocument(id, entry) {
10347
+ if (this.docs.has(id)) this.removeDocument(id);
10348
+ const titleTokens = tokenize(entry.title);
10349
+ const contentTokens = tokenize(entry.content);
10350
+ const tagTokens = entry.tags.flatMap((t) => tokenize(t));
10351
+ const weightedTf = /* @__PURE__ */ new Map();
10352
+ for (const t of titleTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TITLE);
10353
+ for (const t of tagTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TAGS);
10354
+ for (const t of contentTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_CONTENT);
10355
+ const weightedLen = titleTokens.length * FIELD_WEIGHT_TITLE + tagTokens.length * FIELD_WEIGHT_TAGS + contentTokens.length * FIELD_WEIGHT_CONTENT;
10356
+ const allStems = /* @__PURE__ */ new Set();
10357
+ for (const t of weightedTf.keys()) allStems.add(t);
10358
+ const stemSequence = [...titleTokens, ...contentTokens];
10359
+ const docRecord = { weightedTf, weightedLen, allStems, stemSequence };
10360
+ this.docs.set(id, docRecord);
10361
+ this.totalWeightedLen += weightedLen;
10362
+ for (const term of allStems) {
10363
+ let posting = this.postings.get(term);
10364
+ if (!posting) {
10365
+ posting = /* @__PURE__ */ new Set();
10366
+ this.postings.set(term, posting);
10367
+ }
10368
+ posting.add(id);
10369
+ if (term.length >= 3) {
10370
+ const prefix = term.slice(0, 3);
10371
+ let prefixSet = this.prefixMap.get(prefix);
10372
+ if (!prefixSet) {
10373
+ prefixSet = /* @__PURE__ */ new Set();
10374
+ this.prefixMap.set(prefix, prefixSet);
10375
+ }
10376
+ prefixSet.add(term);
10377
+ }
10378
+ }
10379
+ this.idfStale = true;
10380
+ }
10381
+ /** Remove a document from the index. */
10382
+ removeDocument(id) {
10383
+ const doc = this.docs.get(id);
10384
+ if (!doc) return;
10385
+ this.totalWeightedLen -= doc.weightedLen;
10386
+ this.docs.delete(id);
10387
+ for (const term of doc.allStems) {
10388
+ const posting = this.postings.get(term);
10389
+ if (posting) {
10390
+ posting.delete(id);
10391
+ if (posting.size === 0) {
10392
+ this.postings.delete(term);
10393
+ if (term.length >= 3) {
10394
+ const prefixSet = this.prefixMap.get(term.slice(0, 3));
10395
+ if (prefixSet) {
10396
+ prefixSet.delete(term);
10397
+ if (prefixSet.size === 0) this.prefixMap.delete(term.slice(0, 3));
10398
+ }
10399
+ }
10400
+ }
10401
+ }
10402
+ }
10403
+ this.idfStale = true;
10404
+ }
10405
+ /** Recompute IDF values for all terms. Called lazily before search. */
10406
+ refreshIdf() {
10407
+ if (!this.idfStale) return;
10408
+ const N = this.docs.size;
10409
+ this.idf.clear();
10410
+ for (const [term, posting] of this.postings) {
10411
+ const df = posting.size;
10412
+ this.idf.set(term, Math.log((N - df + 0.5) / (df + 0.5) + 1));
10413
+ }
10414
+ this.idfStale = false;
10415
+ }
10416
+ /**
10417
+ * Expand query terms with prefix matches.
10418
+ * "deploy" → ["deploy", "deployment", "deploying", ...] (if they exist in the index)
10419
+ */
10420
+ expandQueryTerms(queryStems) {
10421
+ const expanded = /* @__PURE__ */ new Map();
10422
+ for (const qs of queryStems) {
10423
+ if (this.postings.has(qs)) {
10424
+ expanded.set(qs, Math.max(expanded.get(qs) || 0, 1));
10425
+ }
10426
+ if (qs.length >= 3) {
10427
+ const prefix = qs.slice(0, 3);
10428
+ const candidates = this.prefixMap.get(prefix);
10429
+ if (candidates) {
10430
+ for (const candidate of candidates) {
10431
+ if (candidate !== qs && candidate.startsWith(qs)) {
10432
+ expanded.set(candidate, Math.max(expanded.get(candidate) || 0, PREFIX_MATCH_PENALTY));
10433
+ }
10434
+ }
10435
+ }
10436
+ }
10437
+ }
10438
+ return expanded;
10439
+ }
10440
+ /**
10441
+ * Compute bigram proximity boost: if two query terms appear adjacent
10442
+ * in the document's stem sequence, boost the score.
10443
+ */
10444
+ bigramProximityBoost(docId, queryStems) {
10445
+ if (queryStems.length < 2) return 0;
10446
+ const doc = this.docs.get(docId);
10447
+ if (!doc || doc.stemSequence.length < 2) return 0;
10448
+ let boost = 0;
10449
+ const seq = doc.stemSequence;
10450
+ const querySet = new Set(queryStems);
10451
+ for (let i = 0; i < seq.length - 1; i++) {
10452
+ if (querySet.has(seq[i]) && querySet.has(seq[i + 1]) && seq[i] !== seq[i + 1]) {
10453
+ boost += 0.5;
10454
+ }
10455
+ }
10456
+ return Math.min(boost, 2);
10457
+ }
10458
+ /**
10459
+ * Search the index for documents matching a query.
10460
+ * Returns scored results sorted by BM25F relevance.
10461
+ *
10462
+ * @param query - Raw query string
10463
+ * @param candidateIds - Optional: only score these document IDs (for agent-scoped search)
10464
+ * @returns Array of { id, score } sorted by descending score
10465
+ */
10466
+ search(query, candidateIds) {
10467
+ const queryStems = tokenize(query);
10468
+ if (queryStems.length === 0) return [];
10469
+ this.refreshIdf();
10470
+ const expandedTerms = this.expandQueryTerms(queryStems);
10471
+ if (expandedTerms.size === 0) return [];
10472
+ const avgDl = this.avgDocLen;
10473
+ const candidates = /* @__PURE__ */ new Set();
10474
+ for (const term of expandedTerms.keys()) {
10475
+ const posting = this.postings.get(term);
10476
+ if (posting) {
10477
+ for (const docId of posting) {
10478
+ if (!candidateIds || candidateIds.has(docId)) candidates.add(docId);
10479
+ }
10480
+ }
10481
+ }
10482
+ const results = [];
10483
+ for (const docId of candidates) {
10484
+ const doc = this.docs.get(docId);
10485
+ if (!doc) continue;
10486
+ let score = 0;
10487
+ for (const [term, weight] of expandedTerms) {
10488
+ const tf = doc.weightedTf.get(term) || 0;
10489
+ if (tf === 0) continue;
10490
+ const termIdf = this.idf.get(term) || 0;
10491
+ const numerator = tf * (BM25_K1 + 1);
10492
+ const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (doc.weightedLen / avgDl));
10493
+ score += termIdf * (numerator / denominator) * weight;
10494
+ }
10495
+ score += this.bigramProximityBoost(docId, queryStems);
10496
+ if (score > 0) results.push({ id: docId, score });
10497
+ }
10498
+ results.sort((a, b) => b.score - a.score);
10499
+ return results;
10500
+ }
10501
+ /** Check if a document exists in the index. */
10502
+ has(id) {
10503
+ return this.docs.has(id);
10504
+ }
10505
+ };
10506
+
10507
+ // src/memory/manager.ts
10508
+ function sj(v, fb = {}) {
10509
+ if (!v) return fb;
10510
+ try {
10511
+ return JSON.parse(v);
10512
+ } catch {
10513
+ return fb;
10514
+ }
10515
+ }
10516
+ var MEMORY_CATEGORIES = {
10517
+ knowledge: {
10518
+ label: "Knowledge",
10519
+ description: "Facts, procedures, and reference information the agent has learned"
10520
+ },
10521
+ interaction_pattern: {
10522
+ label: "Interaction Patterns",
10523
+ description: "Learned patterns from past interactions"
10524
+ },
10525
+ preference: {
10526
+ label: "Preferences",
10527
+ description: "User and counterparty preferences"
10528
+ },
10529
+ correction: {
10530
+ label: "Corrections",
10531
+ description: "Corrections and feedback received"
10532
+ },
10533
+ skill: {
10534
+ label: "Skills",
10535
+ description: "Learned abilities and competencies"
10536
+ },
10537
+ context: {
10538
+ label: "Context",
10539
+ description: "Contextual information and background knowledge"
10540
+ },
10541
+ reflection: {
10542
+ label: "Reflections",
10543
+ description: "Self-reflective insights and learnings"
10544
+ },
10545
+ session_learning: {
10546
+ label: "Session Learnings",
10547
+ description: "Insights captured during conversation sessions"
10548
+ },
10549
+ system_notice: {
10550
+ label: "System Notices",
10551
+ description: "System-generated notifications about configuration changes"
10552
+ }
10553
+ };
10554
+ var VALID_CATEGORIES = new Set(Object.keys(MEMORY_CATEGORIES));
10555
+ var VALID_IMPORTANCE = /* @__PURE__ */ new Set(["critical", "high", "normal", "low"]);
10556
+ var IMPORTANCE_WEIGHT = {
10557
+ critical: 4,
10558
+ high: 3,
10559
+ normal: 2,
10560
+ low: 1
10561
+ };
10562
+ var AgentMemoryManager = class {
10563
+ constructor(db2) {
10564
+ this.db = db2;
10565
+ this.ensureTable();
10566
+ this.loadFromDb();
10567
+ }
10568
+ memories = /* @__PURE__ */ new Map();
10569
+ /** Per-agent index: agentId → Set of memory IDs for O(1) agent lookups */
10570
+ agentIndex = /* @__PURE__ */ new Map();
10571
+ /** Full-text search index (BM25F + stemming + inverted index) */
10572
+ searchIndex = new MemorySearchIndex();
10573
+ initialized = false;
10574
+ // ─── Database layer ─────────────────────────────────
10575
+ ensureTable() {
10576
+ if (this.initialized) return;
10577
+ this.db.exec(`
10578
+ CREATE TABLE IF NOT EXISTS agent_memory (
10579
+ id TEXT PRIMARY KEY,
10580
+ agent_id TEXT NOT NULL,
10581
+ category TEXT NOT NULL,
10582
+ title TEXT NOT NULL,
10583
+ content TEXT NOT NULL,
10584
+ source TEXT NOT NULL DEFAULT 'interaction',
10585
+ importance TEXT NOT NULL DEFAULT 'normal',
10586
+ confidence REAL NOT NULL DEFAULT 1.0,
10587
+ access_count INTEGER NOT NULL DEFAULT 0,
10588
+ last_accessed_at TEXT,
10589
+ expires_at TEXT,
10590
+ tags TEXT NOT NULL DEFAULT '[]',
10591
+ metadata TEXT NOT NULL DEFAULT '{}',
10592
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
10593
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
10594
+ )
10595
+ `);
10596
+ try {
10597
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_agent_memory_agent ON agent_memory(agent_id)");
10598
+ } catch {
10599
+ }
10600
+ try {
10601
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_agent_memory_category ON agent_memory(category)");
10602
+ } catch {
10603
+ }
10604
+ this.initialized = true;
10605
+ }
10606
+ /** Run a write statement, swallowing errors with a log (memory must never crash a caller). */
10607
+ dbRun(sql, params) {
10608
+ try {
10609
+ this.db.prepare(sql).run(...params);
10610
+ } catch (err) {
10611
+ console.error("[agent-memory] DB write failed:", err.message);
10612
+ }
10613
+ }
10614
+ dbAll(sql, params = []) {
10615
+ try {
10616
+ return this.db.prepare(sql).all(...params);
10617
+ } catch (err) {
10618
+ console.error("[agent-memory] DB read failed:", err.message);
10619
+ return [];
10620
+ }
10621
+ }
10622
+ loadFromDb() {
10623
+ const rows = this.dbAll("SELECT * FROM agent_memory");
10624
+ for (const r of rows) {
10625
+ try {
10626
+ const entry = this.rowToEntry(r);
10627
+ this.memories.set(entry.id, entry);
10628
+ this.indexAdd(entry.agentId, entry.id);
10629
+ this.searchIndex.addDocument(entry.id, entry);
10630
+ } catch {
10631
+ }
10632
+ }
10633
+ }
10634
+ /** Add a memory ID to the per-agent index. */
10635
+ indexAdd(agentId, memoryId) {
10636
+ let set = this.agentIndex.get(agentId);
10637
+ if (!set) {
10638
+ set = /* @__PURE__ */ new Set();
10639
+ this.agentIndex.set(agentId, set);
10640
+ }
10641
+ set.add(memoryId);
10642
+ }
10643
+ /** Remove a memory ID from the per-agent index. */
10644
+ indexRemove(agentId, memoryId) {
10645
+ const set = this.agentIndex.get(agentId);
10646
+ if (set) {
10647
+ set.delete(memoryId);
10648
+ if (set.size === 0) this.agentIndex.delete(agentId);
10649
+ }
10650
+ }
10651
+ /** Get all memory entries for an agent via the index. */
10652
+ getAgentMemories(agentId) {
10653
+ const ids = this.agentIndex.get(agentId);
10654
+ if (!ids || ids.size === 0) return [];
10655
+ const result = [];
10656
+ for (const id of ids) {
10657
+ const entry = this.memories.get(id);
10658
+ if (entry) result.push(entry);
10659
+ }
10660
+ return result;
10661
+ }
10662
+ // ─── Convenience Methods ─────────────────────────────
10663
+ /** Store a memory with minimal input — the common "just remember this" case. */
10664
+ async storeMemory(agentId, opts) {
10665
+ const category = opts.category && VALID_CATEGORIES.has(opts.category) ? opts.category : "context";
10666
+ const importance = opts.importance && VALID_IMPORTANCE.has(opts.importance) ? opts.importance : "normal";
10667
+ return this.createMemory({
10668
+ agentId,
10669
+ content: opts.content,
10670
+ category,
10671
+ importance,
10672
+ confidence: opts.confidence ?? 1,
10673
+ title: opts.title || opts.content.slice(0, 80),
10674
+ source: "system",
10675
+ tags: opts.tags ?? [],
10676
+ metadata: {}
10677
+ });
10678
+ }
10679
+ /** Search memories by text query, sorted by relevance. */
10680
+ async recall(agentId, query, limit = 5) {
10681
+ return this.queryMemories({ agentId, query, limit });
10682
+ }
10683
+ // ─── CRUD Operations ────────────────────────────────
10684
+ /** Create a new memory entry with auto-generated id + timestamps. */
10685
+ async createMemory(input) {
10686
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10687
+ const entry = {
10688
+ ...input,
10689
+ confidence: input.confidence ?? 0.8,
10690
+ tags: input.tags ?? [],
10691
+ metadata: input.metadata ?? {},
10692
+ id: (0, import_node_crypto6.randomUUID)(),
10693
+ accessCount: 0,
10694
+ createdAt: now,
10695
+ updatedAt: now
10696
+ };
10697
+ this.memories.set(entry.id, entry);
10698
+ this.indexAdd(entry.agentId, entry.id);
10699
+ this.searchIndex.addDocument(entry.id, entry);
10700
+ this.dbRun(
10701
+ `INSERT INTO agent_memory (id, agent_id, category, title, content, source, importance, confidence, access_count, last_accessed_at, expires_at, tags, metadata, created_at, updated_at)
10702
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
10703
+ [
10704
+ entry.id,
10705
+ entry.agentId,
10706
+ entry.category,
10707
+ entry.title,
10708
+ entry.content,
10709
+ entry.source,
10710
+ entry.importance,
10711
+ entry.confidence,
10712
+ entry.accessCount,
10713
+ entry.lastAccessedAt || null,
10714
+ entry.expiresAt || null,
10715
+ JSON.stringify(entry.tags),
10716
+ JSON.stringify(entry.metadata),
10717
+ entry.createdAt,
10718
+ entry.updatedAt
10719
+ ]
10720
+ );
10721
+ return entry;
10722
+ }
10723
+ /** Update an existing memory entry by merging provided fields. */
10724
+ async updateMemory(id, updates) {
10725
+ const existing = this.memories.get(id);
10726
+ if (!existing) return null;
10727
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10728
+ const updated = {
10729
+ ...existing,
10730
+ ...updates,
10731
+ id: existing.id,
10732
+ agentId: existing.agentId,
10733
+ createdAt: existing.createdAt,
10734
+ updatedAt: now
10735
+ };
10736
+ this.memories.set(id, updated);
10737
+ if (updates.title !== void 0 || updates.content !== void 0 || updates.tags !== void 0) {
10738
+ this.searchIndex.addDocument(id, updated);
10739
+ }
10740
+ this.dbRun(
10741
+ `UPDATE agent_memory SET
10742
+ category = ?, title = ?, content = ?, source = ?,
10743
+ importance = ?, confidence = ?, access_count = ?,
10744
+ last_accessed_at = ?, expires_at = ?, tags = ?,
10745
+ metadata = ?, updated_at = ?
10746
+ WHERE id = ?`,
10747
+ [
10748
+ updated.category,
10749
+ updated.title,
10750
+ updated.content,
10751
+ updated.source,
10752
+ updated.importance,
10753
+ updated.confidence,
10754
+ updated.accessCount,
10755
+ updated.lastAccessedAt || null,
10756
+ updated.expiresAt || null,
10757
+ JSON.stringify(updated.tags),
10758
+ JSON.stringify(updated.metadata),
10759
+ updated.updatedAt,
10760
+ id
10761
+ ]
10762
+ );
10763
+ return updated;
10764
+ }
10765
+ /** Delete a single memory entry. Returns true if it existed. */
10766
+ async deleteMemory(id) {
10767
+ const entry = this.memories.get(id);
10768
+ const existed = this.memories.delete(id);
10769
+ if (entry) this.indexRemove(entry.agentId, id);
10770
+ this.searchIndex.removeDocument(id);
10771
+ this.dbRun("DELETE FROM agent_memory WHERE id = ?", [id]);
10772
+ return existed;
10773
+ }
10774
+ /**
10775
+ * Purge every memory entry belonging to an agent — Map, per-agent
10776
+ * index, search index, and the database row. Called when an agent is
10777
+ * deleted so no orphaned memory is left behind.
10778
+ * Returns the number of entries removed.
10779
+ */
10780
+ async deleteAgentMemories(agentId) {
10781
+ const ids = Array.from(this.agentIndex.get(agentId) ?? []);
10782
+ for (const id of ids) {
10783
+ this.memories.delete(id);
10784
+ this.searchIndex.removeDocument(id);
10785
+ }
10786
+ this.agentIndex.delete(agentId);
10787
+ this.dbRun("DELETE FROM agent_memory WHERE agent_id = ?", [agentId]);
10788
+ return ids.length;
10789
+ }
10790
+ /** Retrieve a single memory entry by id. */
10791
+ async getMemory(id) {
10792
+ return this.memories.get(id);
10793
+ }
10794
+ // ─── Query Operations ───────────────────────────────
10795
+ /** Query an agent's memory with optional category/importance/source filters + text search. */
10796
+ async queryMemories(opts) {
10797
+ let results = this.getAgentMemories(opts.agentId);
10798
+ if (opts.category) results = results.filter((m) => m.category === opts.category);
10799
+ if (opts.importance) results = results.filter((m) => m.importance === opts.importance);
10800
+ if (opts.source) results = results.filter((m) => m.source === opts.source);
10801
+ if (opts.query) {
10802
+ const candidateIds = new Set(results.map((m) => m.id));
10803
+ const searchResults = this.searchIndex.search(opts.query, candidateIds);
10804
+ if (searchResults.length > 0) {
10805
+ const scored = searchResults.map((r) => {
10806
+ const entry = this.memories.get(r.id);
10807
+ return entry ? { entry, score: r.score * IMPORTANCE_WEIGHT[entry.importance] } : null;
10808
+ }).filter((r) => r !== null);
10809
+ scored.sort((a, b) => b.score - a.score);
10810
+ return scored.slice(0, opts.limit || 100).map((d) => d.entry);
10811
+ }
10812
+ return [];
10813
+ }
10814
+ results.sort((a, b) => {
10815
+ const weightDiff = IMPORTANCE_WEIGHT[b.importance] - IMPORTANCE_WEIGHT[a.importance];
10816
+ if (weightDiff !== 0) return weightDiff;
10817
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
10818
+ });
10819
+ return results.slice(0, opts.limit || 100);
10820
+ }
10821
+ /** Memories created within the last N hours for an agent. */
10822
+ async getRecentMemories(agentId, hours = 24) {
10823
+ const cutoff = new Date(Date.now() - hours * 36e5).toISOString();
10824
+ return this.getAgentMemories(agentId).filter((m) => m.createdAt >= cutoff).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
10825
+ }
10826
+ // ─── Access Tracking ────────────────────────────────
10827
+ /** Bump access count + lastAccessedAt for a memory entry. */
10828
+ async recordAccess(memoryId) {
10829
+ const entry = this.memories.get(memoryId);
10830
+ if (!entry) return;
10831
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10832
+ entry.accessCount += 1;
10833
+ entry.lastAccessedAt = now;
10834
+ entry.updatedAt = now;
10835
+ this.dbRun(
10836
+ "UPDATE agent_memory SET access_count = ?, last_accessed_at = ?, updated_at = ? WHERE id = ?",
10837
+ [entry.accessCount, entry.lastAccessedAt, entry.updatedAt, memoryId]
10838
+ );
10839
+ }
10840
+ // ─── Context Generation ─────────────────────────────
10841
+ /**
10842
+ * Render an agent's memory as a markdown block for prompt injection.
10843
+ * Ranks entries by confidence × access × recency × importance, with a
10844
+ * BM25F relevance boost when a query is supplied, groups by category,
10845
+ * and truncates to ~maxTokens (estimated at 4 chars/token).
10846
+ */
10847
+ async generateMemoryContext(agentId, query, maxTokens = 1500) {
10848
+ const entries = this.getAgentMemories(agentId).filter((m) => m.confidence >= 0.1);
10849
+ if (entries.length === 0) return "";
10850
+ const now = Date.now();
10851
+ let relevanceMap;
10852
+ if (query) {
10853
+ const candidateIds = new Set(entries.map((e) => e.id));
10854
+ const searchResults = this.searchIndex.search(query, candidateIds);
10855
+ if (searchResults.length > 0) {
10856
+ relevanceMap = /* @__PURE__ */ new Map();
10857
+ const maxScore = searchResults[0].score;
10858
+ for (const r of searchResults) {
10859
+ relevanceMap.set(r.id, maxScore > 0 ? r.score / maxScore : 0);
10860
+ }
10861
+ }
10862
+ }
10863
+ const scored = entries.map((entry) => {
10864
+ const accessWeight = 1 + Math.log1p(entry.accessCount) * 0.3;
10865
+ const lastTouch = entry.lastAccessedAt || entry.createdAt;
10866
+ const ageHours = Math.max(1, (now - new Date(lastTouch).getTime()) / 36e5);
10867
+ const recencyWeight = 1 / (1 + Math.log1p(ageHours / 24) * 0.2);
10868
+ let score = entry.confidence * accessWeight * recencyWeight;
10869
+ score *= IMPORTANCE_WEIGHT[entry.importance];
10870
+ if (relevanceMap) {
10871
+ const relevance = relevanceMap.get(entry.id) || 0;
10872
+ if (relevance > 0) score *= 1 + relevance * 3;
10873
+ }
10874
+ return { entry, score };
10875
+ });
10876
+ scored.sort((a, b) => b.score - a.score);
10877
+ const grouped = /* @__PURE__ */ new Map();
10878
+ for (const { entry } of scored) {
10879
+ const group = grouped.get(entry.category) || [];
10880
+ group.push(entry);
10881
+ grouped.set(entry.category, group);
10882
+ }
10883
+ const maxChars = maxTokens * 4;
10884
+ const lines = ["## Agent Memory", ""];
10885
+ let charCount = lines.join("\n").length;
10886
+ for (const [category, categoryEntries] of Array.from(grouped.entries())) {
10887
+ const meta = MEMORY_CATEGORIES[category];
10888
+ if (!meta) continue;
10889
+ const header2 = `### ${meta.label}`;
10890
+ if (charCount + header2.length + 2 > maxChars) break;
10891
+ lines.push(header2, "");
10892
+ charCount += header2.length + 2;
10893
+ for (const entry of categoryEntries) {
10894
+ const badge = entry.importance === "critical" ? "[CRITICAL] " : entry.importance === "high" ? "[HIGH] " : "";
10895
+ const entryLine = `- **${badge}${entry.title}**: ${entry.content}`;
10896
+ if (charCount + entryLine.length + 1 > maxChars) break;
10897
+ lines.push(entryLine);
10898
+ charCount += entryLine.length + 1;
10899
+ }
10900
+ lines.push("");
10901
+ charCount += 1;
10902
+ }
10903
+ return lines.join("\n").trim();
10904
+ }
10905
+ // ─── Memory Lifecycle ───────────────────────────────
10906
+ /** Decay confidence for entries unaccessed for 7+ days. Critical entries are exempt. */
10907
+ async decayConfidence(agentId, decayRate = 0.05) {
10908
+ const cutoff = new Date(Date.now() - 7 * 864e5).toISOString();
10909
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10910
+ let decayed = 0;
10911
+ for (const entry of this.getAgentMemories(agentId)) {
10912
+ if (entry.importance === "critical") continue;
10913
+ const lastTouch = entry.lastAccessedAt || entry.createdAt;
10914
+ if (lastTouch >= cutoff) continue;
10915
+ const newConfidence = Math.max(0, entry.confidence - decayRate);
10916
+ if (newConfidence === entry.confidence) continue;
10917
+ entry.confidence = parseFloat(newConfidence.toFixed(4));
10918
+ entry.updatedAt = now;
10919
+ this.dbRun(
10920
+ "UPDATE agent_memory SET confidence = ?, updated_at = ? WHERE id = ?",
10921
+ [entry.confidence, now, entry.id]
10922
+ );
10923
+ decayed += 1;
10924
+ }
10925
+ return decayed;
10926
+ }
10927
+ /** Prune entries with confidence < 0.1 or past their expiresAt. */
10928
+ async pruneExpired(agentId) {
10929
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10930
+ const toDelete = [];
10931
+ const entries = agentId ? this.getAgentMemories(agentId) : Array.from(this.memories.values());
10932
+ for (const entry of entries) {
10933
+ const isLowConfidence = entry.confidence < 0.1;
10934
+ const isExpired = !!entry.expiresAt && entry.expiresAt <= now;
10935
+ if (isLowConfidence || isExpired) toDelete.push({ id: entry.id, agentId: entry.agentId });
10936
+ }
10937
+ for (const item of toDelete) {
10938
+ this.memories.delete(item.id);
10939
+ this.indexRemove(item.agentId, item.id);
10940
+ this.searchIndex.removeDocument(item.id);
10941
+ this.dbRun("DELETE FROM agent_memory WHERE id = ?", [item.id]);
10942
+ }
10943
+ return toDelete.length;
10944
+ }
10945
+ // ─── Statistics ─────────────────────────────────────
10946
+ /** Aggregate statistics for a specific agent's memory. */
10947
+ async getStats(agentId) {
10948
+ return this.computeStats(this.getAgentMemories(agentId));
10949
+ }
10950
+ computeStats(entries) {
10951
+ const byCategory = {};
10952
+ const byImportance = {};
10953
+ const bySource = {};
10954
+ let totalConfidence = 0;
10955
+ for (const entry of entries) {
10956
+ byCategory[entry.category] = (byCategory[entry.category] || 0) + 1;
10957
+ byImportance[entry.importance] = (byImportance[entry.importance] || 0) + 1;
10958
+ bySource[entry.source] = (bySource[entry.source] || 0) + 1;
10959
+ totalConfidence += entry.confidence;
10960
+ }
10961
+ return {
10962
+ totalEntries: entries.length,
10963
+ byCategory,
10964
+ byImportance,
10965
+ bySource,
10966
+ avgConfidence: entries.length > 0 ? parseFloat((totalConfidence / entries.length).toFixed(4)) : 0
10967
+ };
10968
+ }
10969
+ // ─── Row Mapper ─────────────────────────────────────
10970
+ rowToEntry(row) {
10971
+ return {
10972
+ id: row.id,
10973
+ agentId: row.agent_id,
10974
+ category: row.category,
10975
+ title: row.title,
10976
+ content: row.content,
10977
+ source: row.source,
10978
+ importance: row.importance,
10979
+ confidence: row.confidence,
10980
+ accessCount: row.access_count || 0,
10981
+ lastAccessedAt: row.last_accessed_at || void 0,
10982
+ expiresAt: row.expires_at || void 0,
10983
+ tags: Array.isArray(sj(row.tags)) ? sj(row.tags) : [],
10984
+ metadata: sj(row.metadata || "{}"),
10985
+ createdAt: row.created_at,
10986
+ updatedAt: row.updated_at
10987
+ };
10988
+ }
10989
+ };
9111
10990
  // Annotate the CommonJS export names for ESM import in node:
9112
10991
  0 && (module.exports = {
9113
10992
  AGENT_ROLES,
9114
10993
  AccountManager,
9115
10994
  AgentDeletionService,
10995
+ AgentMemoryManager,
9116
10996
  AgentMemoryStore,
9117
10997
  AgenticMailClient,
9118
10998
  BRIDGE_OPERATOR_LIVE_WINDOW_MS,
@@ -9125,12 +11005,28 @@ function parse(raw) {
9125
11005
  DependencyInstaller,
9126
11006
  DomainManager,
9127
11007
  DomainPurchaser,
11008
+ ELKS_REALTIME_AUDIO_FORMATS,
9128
11009
  EmailSearchIndex,
9129
11010
  GatewayManager,
9130
11011
  InboxWatcher,
11012
+ MEMORY_CATEGORIES,
9131
11013
  MailReceiver,
9132
11014
  MailSender,
11015
+ MemorySearchIndex,
11016
+ PHONE_MAX_CONCURRENT_MISSIONS,
11017
+ PHONE_MIN_WEBHOOK_SECRET_LENGTH,
11018
+ PHONE_MISSION_STATES,
11019
+ PHONE_RATE_LIMIT_PER_HOUR,
11020
+ PHONE_RATE_LIMIT_PER_MINUTE,
11021
+ PHONE_REGION_SCOPES,
11022
+ PHONE_SERVER_MAX_ATTEMPTS,
11023
+ PHONE_SERVER_MAX_CALL_DURATION_SECONDS,
11024
+ PHONE_SERVER_MAX_COST_PER_MISSION,
11025
+ PHONE_TASK_MAX_LENGTH,
9133
11026
  PathTraversalError,
11027
+ PhoneManager,
11028
+ PhoneRateLimitError,
11029
+ PhoneWebhookAuthError,
9134
11030
  REDACTED,
9135
11031
  RELAY_PRESETS,
9136
11032
  RelayBridge,
@@ -9141,6 +11037,7 @@ function parse(raw) {
9141
11037
  SmsManager,
9142
11038
  SmsPoller,
9143
11039
  StalwartAdmin,
11040
+ TELEPHONY_TRANSPORT_CAPABILITIES,
9144
11041
  ThreadCache,
9145
11042
  TunnelManager,
9146
11043
  UnsafeApiUrlError,
@@ -9149,8 +11046,16 @@ function parse(raw) {
9149
11046
  bridgeWakeErrorMessage,
9150
11047
  bridgeWakeLastSeenAgeMs,
9151
11048
  buildApiUrl,
11049
+ buildElksAudioMessage,
11050
+ buildElksByeMessage,
11051
+ buildElksHandshakeMessages,
11052
+ buildElksInterruptMessage,
11053
+ buildElksListeningMessage,
11054
+ buildElksSendingMessage,
9152
11055
  buildInboundSecurityAdvisory,
11056
+ buildPhoneTransportConfig,
9153
11057
  classifyEmailRoute,
11058
+ classifyPhoneNumberRisk,
9154
11059
  classifyResumeError,
9155
11060
  closeDatabase,
9156
11061
  composeBridgeWakePrompt,
@@ -9165,8 +11070,10 @@ function parse(raw) {
9165
11070
  getOperatorEmail,
9166
11071
  getSmsProvider,
9167
11072
  hostSessionStoragePath,
11073
+ inferPhoneRegion,
9168
11074
  isInternalEmail,
9169
11075
  isLoopbackMailHost,
11076
+ isPhoneRegionAllowed,
9170
11077
  isSessionFresh,
9171
11078
  isValidPhoneNumber,
9172
11079
  loadHostSession,
@@ -9175,11 +11082,13 @@ function parse(raw) {
9175
11082
  normalizePhoneNumber,
9176
11083
  normalizeSubject,
9177
11084
  operatorPrefsStoragePath,
11085
+ parseElksRealtimeMessage,
9178
11086
  parseEmail,
9179
11087
  parseGoogleVoiceSms,
9180
11088
  planBridgeWake,
9181
11089
  recordToolCall,
9182
11090
  redactObject,
11091
+ redactPhoneTransportConfig,
9183
11092
  redactSecret,
9184
11093
  redactSmsConfig,
9185
11094
  resolveConfig,
@@ -9194,7 +11103,12 @@ function parse(raw) {
9194
11103
  setTelemetryVersion,
9195
11104
  shouldSkipBridgeWakeForLiveOperator,
9196
11105
  startRelayBridge,
11106
+ stem,
9197
11107
  threadIdFor,
11108
+ tokenize,
9198
11109
  tryJoin,
9199
- validateApiUrl
11110
+ validateApiUrl,
11111
+ validatePhoneMissionPolicy,
11112
+ validatePhoneMissionStart,
11113
+ validatePhoneTransportProfile
9200
11114
  });