getpatter 0.6.4 → 0.6.6

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.js CHANGED
@@ -5,10 +5,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __glob = (map) => (path6) => {
9
- var fn = map[path6];
8
+ var __glob = (map) => (path7) => {
9
+ var fn = map[path7];
10
10
  if (fn) return fn();
11
- throw new Error("Module not found in bundle: " + path6);
11
+ throw new Error("Module not found in bundle: " + path7);
12
12
  };
13
13
  var __esm = (fn, res) => function __init() {
14
14
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
@@ -118,6 +118,110 @@ var init_errors = __esm({
118
118
  }
119
119
  });
120
120
 
121
+ // src/telemetry/call-metrics.ts
122
+ function engineFromMode(mode) {
123
+ if (mode === "openai_realtime" || mode === "openai_realtime_2") return "realtime";
124
+ if (mode === "elevenlabs_convai") return "convai";
125
+ if (mode === "pipeline") return "pipeline";
126
+ return "other";
127
+ }
128
+ function providerFromMetrics(m) {
129
+ const mode = m.provider_mode;
130
+ if (mode === "openai_realtime" || mode === "openai_realtime_2") return "openai";
131
+ if (mode === "elevenlabs_convai") return "elevenlabs";
132
+ for (const key of ["llm_provider", "stt_provider", "tts_provider"]) {
133
+ const v = m[key];
134
+ if (typeof v === "string" && v) return v.toLowerCase();
135
+ }
136
+ return "other";
137
+ }
138
+ function providerFromMode(mode) {
139
+ if (mode === "openai_realtime" || mode === "openai_realtime_2") return "openai";
140
+ if (mode === "elevenlabs_convai") return "elevenlabs";
141
+ return "other";
142
+ }
143
+ function carrierFamily(tp) {
144
+ return typeof tp === "string" && tp ? tp.toLowerCase() : "none";
145
+ }
146
+ function direction(value) {
147
+ const v = typeof value === "string" ? value.toLowerCase() : "";
148
+ return v === "inbound" || v === "outbound" ? v : void 0;
149
+ }
150
+ function turnCountBucket(n) {
151
+ if (n <= 0) return "0";
152
+ if (n === 1) return "1";
153
+ if (n <= 3) return "2_3";
154
+ if (n <= 6) return "4_6";
155
+ if (n <= 12) return "7_12";
156
+ return "13_plus";
157
+ }
158
+ function latencyMs(m) {
159
+ const p95 = m.latency_p95;
160
+ if (p95 && typeof p95 === "object") {
161
+ return p95.agent_response_ms;
162
+ }
163
+ return void 0;
164
+ }
165
+ function recordCallStarted(telemetry, opts) {
166
+ if (!telemetry) return;
167
+ try {
168
+ const dims = {
169
+ engine: engineFromMode(opts.providerMode),
170
+ provider: providerFromMode(opts.providerMode),
171
+ carrier: carrierFamily(opts.telephonyProvider)
172
+ };
173
+ const d = direction(opts.direction);
174
+ if (d !== void 0) dims.direction = d;
175
+ telemetry.record("call_started", dims);
176
+ } catch {
177
+ }
178
+ }
179
+ function recordCallCompleted(telemetry, opts) {
180
+ if (!telemetry) return;
181
+ try {
182
+ const dims = { outcome: opts.outcome };
183
+ const d = direction(opts.direction);
184
+ if (d !== void 0) dims.direction = d;
185
+ const metrics = opts.metrics;
186
+ if (metrics && typeof metrics === "object") {
187
+ const m = metrics;
188
+ dims.engine = engineFromMode(m.provider_mode);
189
+ dims.provider = providerFromMetrics(m);
190
+ dims.carrier = carrierFamily(m.telephony_provider);
191
+ if (typeof m.duration_seconds === "number") {
192
+ dims.duration_seconds = Math.max(0, Math.round(m.duration_seconds));
193
+ }
194
+ const lat = latencyMs(m);
195
+ if (typeof lat === "number") dims.latency_ms = Math.max(0, Math.round(lat));
196
+ const cost = m.cost;
197
+ if (cost && typeof cost === "object") {
198
+ const total = cost.total;
199
+ if (typeof total === "number" && Number.isFinite(total)) {
200
+ dims.cost_usd = Math.max(0, Math.round(total * 1e4) / 1e4);
201
+ }
202
+ }
203
+ if (Array.isArray(m.turns)) {
204
+ dims.turn_count_bucket = turnCountBucket(m.turns.length);
205
+ }
206
+ const errorCode = m.error_code;
207
+ if (typeof errorCode === "string" && errorCode) {
208
+ dims.error_code = errorCode;
209
+ dims.outcome = "error";
210
+ }
211
+ } else if (opts.carrier !== void 0) {
212
+ dims.carrier = carrierFamily(opts.carrier);
213
+ }
214
+ telemetry.record("call_completed", dims);
215
+ } catch {
216
+ }
217
+ }
218
+ var init_call_metrics = __esm({
219
+ "src/telemetry/call-metrics.ts"() {
220
+ "use strict";
221
+ init_cjs_shims();
222
+ }
223
+ });
224
+
121
225
  // src/logger.ts
122
226
  function getLogger() {
123
227
  return currentLogger;
@@ -2171,10 +2275,10 @@ var init_plivo_adapter = __esm({
2171
2275
  this.baseUrl = `${PLIVO_API_BASE}/Account/${encodeURIComponent(authId)}`;
2172
2276
  this.authHeader = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
2173
2277
  }
2174
- async request(method, path6, jsonBody) {
2278
+ async request(method, path7, jsonBody) {
2175
2279
  const headers = { Authorization: this.authHeader };
2176
2280
  if (jsonBody !== void 0) headers["Content-Type"] = "application/json";
2177
- const response = await fetch(`${this.baseUrl}${path6}`, {
2281
+ const response = await fetch(`${this.baseUrl}${path7}`, {
2178
2282
  method,
2179
2283
  headers,
2180
2284
  body: jsonBody !== void 0 ? JSON.stringify(jsonBody) : void 0,
@@ -2182,7 +2286,7 @@ var init_plivo_adapter = __esm({
2182
2286
  });
2183
2287
  const text = await response.text();
2184
2288
  if (!response.ok && response.status !== 404) {
2185
- throw new Error(`Plivo ${method} ${path6} failed: ${response.status} ${text}`);
2289
+ throw new Error(`Plivo ${method} ${path7} failed: ${response.status} ${text}`);
2186
2290
  }
2187
2291
  let data = {};
2188
2292
  if (text) {
@@ -3705,9 +3809,9 @@ function loadDashboardHtml() {
3705
3809
  (0, import_node_path.join)(here, "dashboard", "ui.html"),
3706
3810
  (0, import_node_path.join)(here, "..", "dashboard", "ui.html")
3707
3811
  ];
3708
- for (const path6 of candidates) {
3812
+ for (const path7 of candidates) {
3709
3813
  try {
3710
- return (0, import_node_fs.readFileSync)(path6, "utf8");
3814
+ return (0, import_node_fs.readFileSync)(path7, "utf8");
3711
3815
  } catch {
3712
3816
  }
3713
3817
  }
@@ -4599,6 +4703,9 @@ var init_metrics = __esm({
4599
4703
  ttsModel;
4600
4704
  realtimeModel;
4601
4705
  _pricing;
4706
+ // Terminal error code (lowercased ErrorCode value or "other"); set by
4707
+ // recordError when the call ends abnormally. Empty for a clean call.
4708
+ _errorCode = "";
4602
4709
  _callStart;
4603
4710
  _turns = [];
4604
4711
  // mutable internal array; immutable when exposed via TurnMetrics[] → readonly TurnMetrics[]
@@ -5169,11 +5276,35 @@ var init_metrics = __esm({
5169
5276
  telephony_provider: this.telephonyProvider,
5170
5277
  stt_model: this.sttModel,
5171
5278
  tts_model: this.ttsModel,
5172
- llm_model: this._llmModel
5279
+ llm_model: this._llmModel,
5280
+ error_code: this._errorCode
5173
5281
  };
5174
5282
  this._eventBus?.emit("call_ended", { callId: this.callId, metrics });
5175
5283
  return metrics;
5176
5284
  }
5285
+ /**
5286
+ * Record the call's terminal error as a coarse, anonymous code. Stores the
5287
+ * PatterError `.code` lowercased; maps common timeout/connection errors; falls
5288
+ * back to "other". Never stores the message. Last write wins.
5289
+ */
5290
+ recordError(err) {
5291
+ const code = err?.code;
5292
+ const name = err?.name;
5293
+ const sys = typeof code === "string" ? code : "";
5294
+ if (sys.startsWith("ECONN") || sys === "EHOSTUNREACH" || sys === "ENETUNREACH" || sys === "EPIPE") {
5295
+ this._errorCode = "connection";
5296
+ return;
5297
+ }
5298
+ if (typeof code === "string" && code) {
5299
+ this._errorCode = code.toLowerCase();
5300
+ return;
5301
+ }
5302
+ if (name === "TimeoutError" || name === "AbortError") {
5303
+ this._errorCode = "timeout";
5304
+ } else {
5305
+ this._errorCode = "other";
5306
+ }
5307
+ }
5177
5308
  /** Return the cost breakdown for the call so far without ending it. */
5178
5309
  getCostSoFar() {
5179
5310
  const duration3 = (hrTimeMs() - this._callStart) / 1e3;
@@ -5963,7 +6094,7 @@ var init_llm_loop = __esm({
5963
6094
  });
5964
6095
  if (!response.ok) {
5965
6096
  const errText = await response.text();
5966
- getLogger().error(`LLM API error: ${response.status} ${errText}`);
6097
+ getLogger().error(`LLM API error: ${response.status} ${errText.slice(0, 200)}`);
5967
6098
  throw new PatterConnectionError(
5968
6099
  `LLM API returned ${response.status}: ${errText.slice(0, 200)}`
5969
6100
  );
@@ -6131,12 +6262,22 @@ ${systemPrompt}` : DEFAULT_PHONE_PREAMBLE;
6131
6262
  const hasAfterLlmResponse = Boolean(hookExecutor?.hasAfterLlmResponse() && hookCtx);
6132
6263
  const hasAfterLlmChunk = Boolean(hookExecutor?.hasAfterLlmChunk());
6133
6264
  const allEmittedText = [];
6265
+ const callId = callContext.call_id;
6266
+ const caller = callContext.caller;
6267
+ const callee = callContext.callee;
6268
+ const hasContext = typeof callId === "string" && callId.length > 0 || typeof caller === "string" && caller.length > 0 || typeof callee === "string" && callee.length > 0;
6269
+ const streamOpts = hasContext ? {
6270
+ ...opts,
6271
+ ...typeof callId === "string" && callId.length > 0 ? { callId } : {},
6272
+ ...typeof caller === "string" && caller.length > 0 ? { caller } : {},
6273
+ ...typeof callee === "string" && callee.length > 0 ? { callee } : {}
6274
+ } : opts;
6134
6275
  for (let iter = 0; iter < maxIterations; iter++) {
6135
6276
  const toolCallsAccumulated = /* @__PURE__ */ new Map();
6136
6277
  const textParts = [];
6137
6278
  let hasToolCalls = false;
6138
6279
  let usageChunkReceived = false;
6139
- for await (const chunk of this.provider.stream(messages, this.openaiTools, opts)) {
6280
+ for await (const chunk of this.provider.stream(messages, this.openaiTools, streamOpts)) {
6140
6281
  if (chunk.type === "text" && chunk.content) {
6141
6282
  const content = hasAfterLlmChunk && hookExecutor ? hookExecutor.runAfterLlmChunk(chunk.content) : chunk.content;
6142
6283
  textParts.push(content);
@@ -6264,6 +6405,7 @@ ${systemPrompt}` : DEFAULT_PHONE_PREAMBLE;
6264
6405
  { role: "system", content: this.systemPrompt }
6265
6406
  ];
6266
6407
  for (const entry of history) {
6408
+ if (entry.role === "tool") continue;
6267
6409
  messages.push({
6268
6410
  role: entry.role === "assistant" ? "assistant" : "user",
6269
6411
  content: entry.text
@@ -6541,10 +6683,10 @@ function mergeDefs(...defs) {
6541
6683
  function cloneDef(schema) {
6542
6684
  return mergeDefs(schema._zod.def);
6543
6685
  }
6544
- function getElementAtPath(obj, path6) {
6545
- if (!path6)
6686
+ function getElementAtPath(obj, path7) {
6687
+ if (!path7)
6546
6688
  return obj;
6547
- return path6.reduce((acc, key) => acc?.[key], obj);
6689
+ return path7.reduce((acc, key) => acc?.[key], obj);
6548
6690
  }
6549
6691
  function promiseAllObject(promisesObj) {
6550
6692
  const keys = Object.keys(promisesObj);
@@ -6872,11 +7014,11 @@ function explicitlyAborted(x, startIndex = 0) {
6872
7014
  }
6873
7015
  return false;
6874
7016
  }
6875
- function prefixIssues(path6, issues) {
7017
+ function prefixIssues(path7, issues) {
6876
7018
  return issues.map((iss) => {
6877
7019
  var _a3;
6878
7020
  (_a3 = iss).path ?? (_a3.path = []);
6879
- iss.path.unshift(path6);
7021
+ iss.path.unshift(path7);
6880
7022
  return iss;
6881
7023
  });
6882
7024
  }
@@ -7095,16 +7237,16 @@ function flattenError(error2, mapper = (issue2) => issue2.message) {
7095
7237
  }
7096
7238
  function formatError(error2, mapper = (issue2) => issue2.message) {
7097
7239
  const fieldErrors = { _errors: [] };
7098
- const processError = (error3, path6 = []) => {
7240
+ const processError = (error3, path7 = []) => {
7099
7241
  for (const issue2 of error3.issues) {
7100
7242
  if (issue2.code === "invalid_union" && issue2.errors.length) {
7101
- issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
7243
+ issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
7102
7244
  } else if (issue2.code === "invalid_key") {
7103
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
7245
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
7104
7246
  } else if (issue2.code === "invalid_element") {
7105
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
7247
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
7106
7248
  } else {
7107
- const fullpath = [...path6, ...issue2.path];
7249
+ const fullpath = [...path7, ...issue2.path];
7108
7250
  if (fullpath.length === 0) {
7109
7251
  fieldErrors._errors.push(mapper(issue2));
7110
7252
  } else {
@@ -17908,20 +18050,20 @@ var require_compile = __commonJS({
17908
18050
  var util_1 = require_util();
17909
18051
  var validate_1 = require_validate();
17910
18052
  var SchemaEnv = class {
17911
- constructor(env) {
18053
+ constructor(env2) {
17912
18054
  var _a3;
17913
18055
  this.refs = {};
17914
18056
  this.dynamicAnchors = {};
17915
18057
  let schema;
17916
- if (typeof env.schema == "object")
17917
- schema = env.schema;
17918
- this.schema = env.schema;
17919
- this.schemaId = env.schemaId;
17920
- this.root = env.root || this;
17921
- this.baseId = (_a3 = env.baseId) !== null && _a3 !== void 0 ? _a3 : (0, resolve_1.normalizeId)(schema === null || schema === void 0 ? void 0 : schema[env.schemaId || "$id"]);
17922
- this.schemaPath = env.schemaPath;
17923
- this.localRefs = env.localRefs;
17924
- this.meta = env.meta;
18058
+ if (typeof env2.schema == "object")
18059
+ schema = env2.schema;
18060
+ this.schema = env2.schema;
18061
+ this.schemaId = env2.schemaId;
18062
+ this.root = env2.root || this;
18063
+ this.baseId = (_a3 = env2.baseId) !== null && _a3 !== void 0 ? _a3 : (0, resolve_1.normalizeId)(schema === null || schema === void 0 ? void 0 : schema[env2.schemaId || "$id"]);
18064
+ this.schemaPath = env2.schemaPath;
18065
+ this.localRefs = env2.localRefs;
18066
+ this.meta = env2.meta;
17925
18067
  this.$async = schema === null || schema === void 0 ? void 0 : schema.$async;
17926
18068
  this.refs = {};
17927
18069
  }
@@ -18105,15 +18247,15 @@ var require_compile = __commonJS({
18105
18247
  baseId = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schId);
18106
18248
  }
18107
18249
  }
18108
- let env;
18250
+ let env2;
18109
18251
  if (typeof schema != "boolean" && schema.$ref && !(0, util_1.schemaHasRulesButRef)(schema, this.RULES)) {
18110
18252
  const $ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schema.$ref);
18111
- env = resolveSchema.call(this, root, $ref);
18253
+ env2 = resolveSchema.call(this, root, $ref);
18112
18254
  }
18113
18255
  const { schemaId } = this.opts;
18114
- env = env || new SchemaEnv({ schema, schemaId, root, baseId });
18115
- if (env.schema !== env.root.schema)
18116
- return env;
18256
+ env2 = env2 || new SchemaEnv({ schema, schemaId, root, baseId });
18257
+ if (env2.schema !== env2.root.schema)
18258
+ return env2;
18117
18259
  return void 0;
18118
18260
  }
18119
18261
  }
@@ -18265,8 +18407,8 @@ var require_utils = __commonJS({
18265
18407
  }
18266
18408
  return ind;
18267
18409
  }
18268
- function removeDotSegments(path6) {
18269
- let input = path6;
18410
+ function removeDotSegments(path7) {
18411
+ let input = path7;
18270
18412
  const output = [];
18271
18413
  let nextSlash = -1;
18272
18414
  let len = 0;
@@ -18519,8 +18661,8 @@ var require_schemes = __commonJS({
18519
18661
  wsComponent.secure = void 0;
18520
18662
  }
18521
18663
  if (wsComponent.resourceName) {
18522
- const [path6, query] = wsComponent.resourceName.split("?");
18523
- wsComponent.path = path6 && path6 !== "/" ? path6 : void 0;
18664
+ const [path7, query] = wsComponent.resourceName.split("?");
18665
+ wsComponent.path = path7 && path7 !== "/" ? path7 : void 0;
18524
18666
  wsComponent.query = query;
18525
18667
  wsComponent.resourceName = void 0;
18526
18668
  }
@@ -19608,8 +19750,8 @@ var require_ref = __commonJS({
19608
19750
  schemaType: "string",
19609
19751
  code(cxt) {
19610
19752
  const { gen, schema: $ref, it } = cxt;
19611
- const { baseId, schemaEnv: env, validateName, opts, self } = it;
19612
- const { root } = env;
19753
+ const { baseId, schemaEnv: env2, validateName, opts, self } = it;
19754
+ const { root } = env2;
19613
19755
  if (($ref === "#" || $ref === "#/") && baseId === root.baseId)
19614
19756
  return callRootRef();
19615
19757
  const schOrEnv = compile_1.resolveRef.call(self, root, baseId, $ref);
@@ -19619,8 +19761,8 @@ var require_ref = __commonJS({
19619
19761
  return callValidate(schOrEnv);
19620
19762
  return inlineRefSchema(schOrEnv);
19621
19763
  function callRootRef() {
19622
- if (env === root)
19623
- return callRef(cxt, validateName, env, env.$async);
19764
+ if (env2 === root)
19765
+ return callRef(cxt, validateName, env2, env2.$async);
19624
19766
  const rootName = gen.scopeValue("root", { ref: root });
19625
19767
  return callRef(cxt, (0, codegen_1._)`${rootName}.validate`, root, root.$async);
19626
19768
  }
@@ -19650,14 +19792,14 @@ var require_ref = __commonJS({
19650
19792
  exports2.getValidate = getValidate;
19651
19793
  function callRef(cxt, v, sch, $async) {
19652
19794
  const { gen, it } = cxt;
19653
- const { allErrors, schemaEnv: env, opts } = it;
19795
+ const { allErrors, schemaEnv: env2, opts } = it;
19654
19796
  const passCxt = opts.passContext ? names_1.default.this : codegen_1.nil;
19655
19797
  if ($async)
19656
19798
  callAsyncRef();
19657
19799
  else
19658
19800
  callSyncRef();
19659
19801
  function callAsyncRef() {
19660
- if (!env.$async)
19802
+ if (!env2.$async)
19661
19803
  throw new Error("async schema referenced by sync schema");
19662
19804
  const valid = gen.let("valid");
19663
19805
  gen.try(() => {
@@ -21959,12 +22101,12 @@ var require_dist = __commonJS({
21959
22101
  throw new Error(`Unknown format "${name}"`);
21960
22102
  return f;
21961
22103
  };
21962
- function addFormats(ajv, list, fs6, exportName) {
22104
+ function addFormats(ajv, list, fs8, exportName) {
21963
22105
  var _a3;
21964
22106
  var _b;
21965
22107
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
21966
22108
  for (const f of list)
21967
- ajv.addFormat(f, fs6[f]);
22109
+ ajv.addFormat(f, fs8[f]);
21968
22110
  }
21969
22111
  module2.exports = exports2 = formatsPlugin;
21970
22112
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -27781,6 +27923,26 @@ function isSttHallucination(text) {
27781
27923
  const pieces = stripped.split(/[.!?…。!?]+/u).map((p) => p.trim()).filter((p) => p.length > 0);
27782
27924
  return pieces.length > 1 && pieces.every((p) => HALLUCINATIONS.has(p));
27783
27925
  }
27926
+ function normalizeForEcho(text) {
27927
+ return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/u, " ").trim().replace(/\s+/gu, " ");
27928
+ }
27929
+ function looksLikeEcho(candidate, agentText) {
27930
+ const a = normalizeForEcho(agentText);
27931
+ const c = normalizeForEcho(candidate);
27932
+ if (!a || !c) return false;
27933
+ const words = c.split(" ").filter(Boolean);
27934
+ if (words.length < ECHO_MIN_CANDIDATE_WORDS) return false;
27935
+ if (a.includes(c)) return true;
27936
+ const agentWords = new Set(a.split(" "));
27937
+ const overlap = words.filter((w) => agentWords.has(w)).length / words.length;
27938
+ return overlap >= ECHO_WORD_OVERLAP_THRESHOLD;
27939
+ }
27940
+ function isNearDuplicate(a, b) {
27941
+ if (!a || !b) return false;
27942
+ if (a === b) return true;
27943
+ const [shorter, longer] = a.length <= b.length ? [a, b] : [b, a];
27944
+ return longer.startsWith(shorter + " ");
27945
+ }
27784
27946
  async function queryDeepgramCost(metricsAcc, deepgramKey, deepgramRequestId) {
27785
27947
  try {
27786
27948
  const projResp = await fetch("https://api.deepgram.com/v1/projects", {
@@ -27811,7 +27973,7 @@ async function queryDeepgramCost(metricsAcc, deepgramKey, deepgramRequestId) {
27811
27973
  } catch {
27812
27974
  }
27813
27975
  }
27814
- var DEFAULT_TOOL_CALL_PREAMBLE_BLOCK, HALLUCINATIONS, StreamHandler;
27976
+ var DEFAULT_TOOL_CALL_PREAMBLE_BLOCK, HALLUCINATIONS, ECHO_WORD_OVERLAP_THRESHOLD, ECHO_MIN_CANDIDATE_WORDS, StreamHandler;
27815
27977
  var init_stream_handler = __esm({
27816
27978
  "src/stream-handler.ts"() {
27817
27979
  "use strict";
@@ -27924,6 +28086,8 @@ Avoid:
27924
28086
  "[blank_audio]",
27925
28087
  "(silence)"
27926
28088
  ]);
28089
+ ECHO_WORD_OVERLAP_THRESHOLD = 0.6;
28090
+ ECHO_MIN_CANDIDATE_WORDS = 4;
27927
28091
  StreamHandler = class _StreamHandler {
27928
28092
  deps;
27929
28093
  ws;
@@ -27936,6 +28100,17 @@ Avoid:
27936
28100
  stt = null;
27937
28101
  tts = null;
27938
28102
  isSpeaking = false;
28103
+ /**
28104
+ * True only while the post-TTS tail-grace window is pending: the agent has
28105
+ * finished its turn but ``isSpeaking`` is still held for
28106
+ * ``PATTER_TTS_TAIL_GRACE_MS`` to swallow the fading echo tail. A VAD
28107
+ * ``speech_start`` (or a transcript) during this window is the user's NEXT
28108
+ * turn, not a barge-in — there is nothing left to interrupt. Set by
28109
+ * ``endSpeakingWithGrace``; cleared by ``beginSpeaking``, the grace flip,
28110
+ * ``cancelSpeaking``, and ``endTailGraceForNewTurn``. Parity with Python
28111
+ * ``_tail_grace_active``.
28112
+ */
28113
+ tailGraceActive = false;
27939
28114
  /**
27940
28115
  * Ring buffer of inbound PCM16 16 kHz frames captured while the agent
27941
28116
  * is speaking and the self-hearing guard is dropping audio. On
@@ -28011,6 +28186,35 @@ Avoid:
28011
28186
  * ``isSpeaking=false``, and silently cut the agent's first turn.
28012
28187
  */
28013
28188
  firstAudioSentAt = null;
28189
+ /**
28190
+ * Estimated wall-clock (ms) when the LAST audio byte pushed to the carrier
28191
+ * finishes PLAYING on the phone. The pipeline pushes TTS audio as fast as
28192
+ * the provider synthesizes it (no pacing) and the carrier buffers + plays
28193
+ * at realtime, so "we finished pushing" and "the caller finished hearing"
28194
+ * can diverge by tens of seconds — especially with agent-runtime LLMs
28195
+ * (Hermes/OpenClaw) that deliver a long reply all at once after a thinking
28196
+ * pause. ``endSpeakingWithGrace`` holds ``isSpeaking=true`` (with
28197
+ * ``tailGraceActive=false``) until this cursor passes, so a barge-in during
28198
+ * the audible backlog still takes the cancel path (``sendClear`` drops the
28199
+ * carrier buffer) instead of being treated as a calm next turn. Advanced by
28200
+ * ``trackOutboundPlayback``; reset by ``cancelSpeaking`` (the buffer is
28201
+ * cleared) and ``endTailGraceForNewTurn``.
28202
+ */
28203
+ playbackBufferedUntil = 0;
28204
+ /**
28205
+ * Per-turn playback timeline used to estimate the response prefix the
28206
+ * caller actually HEARD when a barge-in lands. ``turnPlaybackTotalMs``
28207
+ * accumulates the playout duration of every chunk pushed this turn
28208
+ * (including filler audio, which keeps the timeline aligned);
28209
+ * ``turnSpokenSegments`` records ``{text, startMs}`` for each RESPONSE
28210
+ * sentence at its first audible chunk (filler / error-fallback audio
28211
+ * advances the clock but adds no segment). ``heard = total - backlog``
28212
+ * then maps to a sentence-granular prefix — see ``heardResponsePrefix``.
28213
+ * Both reset at ``beginSpeaking``. Mirrors Python
28214
+ * ``_turn_playback_total_s`` / ``_turn_spoken_segments``.
28215
+ */
28216
+ turnPlaybackTotalMs = 0;
28217
+ turnSpokenSegments = [];
28014
28218
  /**
28015
28219
  * Optional barge-in confirmation strategies. With an empty array the
28016
28220
  * SDK falls back to the legacy "cancel on first VAD speech_start"
@@ -28128,11 +28332,15 @@ Avoid:
28128
28332
  }
28129
28333
  this.speakingGeneration++;
28130
28334
  this.isSpeaking = true;
28335
+ this.tailGraceActive = false;
28131
28336
  this.speakingStartedAt = Date.now();
28132
28337
  this.suppressedSpeechPending = false;
28133
28338
  void isFirstMessage;
28134
28339
  this.firstAudioSentAt = Date.now();
28135
28340
  this.inboundAudioRing = [];
28341
+ this.currentAgentSpokenText = "";
28342
+ this.turnPlaybackTotalMs = 0;
28343
+ this.turnSpokenSegments = [];
28136
28344
  this.resetVad();
28137
28345
  }
28138
28346
  /**
@@ -28147,6 +28355,87 @@ Avoid:
28147
28355
  this.firstAudioSentAt = Date.now();
28148
28356
  }
28149
28357
  }
28358
+ /**
28359
+ * Advance ``playbackBufferedUntil`` by the playout duration of an outbound
28360
+ * TTS chunk. ``numBytes`` is the size of the chunk BEFORE carrier encoding
28361
+ * (the same buffer handed to ``encodePipelineAudio``): PCM16 @ 16 kHz in
28362
+ * the default path (32 bytes/ms), or the carrier's native μ-law @ 8 kHz
28363
+ * (8 bytes/ms) when the TTS adapter emits wire format directly
28364
+ * (``ttsOutputFormatNativeForCarrier`` — Twilio/Plivo ``ulaw_8000``;
28365
+ * Telnyx native is ``pcm_16000`` so it stays at 32 bytes/ms).
28366
+ */
28367
+ trackOutboundPlayback(numBytes) {
28368
+ if (numBytes <= 0) return;
28369
+ const bytesPerMs = this.ttsOutputFormatNativeForCarrier && this.deps.bridge.telephonyProvider !== "telnyx" ? 8 : 32;
28370
+ const now = Date.now();
28371
+ const chunkMs = numBytes / bytesPerMs;
28372
+ const base = this.playbackBufferedUntil > now ? this.playbackBufferedUntil : now;
28373
+ this.playbackBufferedUntil = base + chunkMs;
28374
+ this.turnPlaybackTotalMs += chunkMs;
28375
+ }
28376
+ /**
28377
+ * Estimate the response prefix the caller actually HEARD this turn.
28378
+ *
28379
+ * The pipeline pushes audio faster than realtime, so at barge-in time
28380
+ * ``heard = totalPushed - carrierBacklog`` ms of audio have actually
28381
+ * played. Mapped at sentence granularity against ``turnSpokenSegments``:
28382
+ * a sentence counts as heard once its playback has STARTED
28383
+ * (``startMs <= heardMs``), so the sentence playing at the moment of
28384
+ * interruption is included.
28385
+ *
28386
+ * Returns ``null`` when no segments were tracked this turn (nothing
28387
+ * synthesized through the tracked path — callers fall back to the legacy
28388
+ * full-text behaviour). Mirrors Python ``_heard_response_prefix``.
28389
+ */
28390
+ heardResponsePrefix() {
28391
+ if (this.turnSpokenSegments.length === 0) return null;
28392
+ const remainingMs = Math.max(0, this.playbackBufferedUntil - Date.now());
28393
+ const heardMs = Math.max(0, this.turnPlaybackTotalMs - remainingMs);
28394
+ const heard = this.turnSpokenSegments.filter((s) => s.startMs <= heardMs);
28395
+ return {
28396
+ text: heard.map((s) => s.text).join(" "),
28397
+ heardEverything: heard.length === this.turnSpokenSegments.length
28398
+ };
28399
+ }
28400
+ /**
28401
+ * Replace the text of the most recent assistant entry in the conversation
28402
+ * history. No-op when the last entry is not an assistant turn (e.g. the
28403
+ * caller's next turn was already committed).
28404
+ */
28405
+ rewriteLastAssistantEntry(text) {
28406
+ const entries = this.history.entries;
28407
+ const last = entries[entries.length - 1];
28408
+ if (last && last.role === "assistant") {
28409
+ entries[entries.length - 1] = { ...last, text };
28410
+ }
28411
+ }
28412
+ /**
28413
+ * LiveKit-style "heard prefix" semantics for a barge-in that lands AFTER
28414
+ * the turn completed, while the carrier is still playing the buffered
28415
+ * tail.
28416
+ *
28417
+ * The completed turn already recorded its FULL reply in history, but the
28418
+ * caller only heard part of it before interrupting — a stateful agent
28419
+ * runtime (Hermes / OpenClaw) would otherwise "remember saying" things
28420
+ * the caller never heard. Rewrites the last assistant entry to the heard
28421
+ * prefix + ``[interrupted by caller]``.
28422
+ *
28423
+ * MUST run BEFORE ``cancelSpeaking`` resets ``playbackBufferedUntil``
28424
+ * (the backlog is the heard-prefix input). No-op when a turn is still in
28425
+ * flight (the streaming path applies its own marker), when there is no
28426
+ * backlog, or when everything was already heard. Mirrors Python
28427
+ * ``_maybe_truncate_completed_turn_history``.
28428
+ */
28429
+ maybeTruncateCompletedTurnHistory() {
28430
+ if (this.dispatchTask !== null) return;
28431
+ const remainingMs = this.playbackBufferedUntil - Date.now();
28432
+ if (remainingMs <= 0) return;
28433
+ const heard = this.heardResponsePrefix();
28434
+ if (heard === null || heard.heardEverything) return;
28435
+ this.rewriteLastAssistantEntry(
28436
+ heard.text ? `${heard.text} [interrupted by caller]` : "[interrupted by caller]"
28437
+ );
28438
+ }
28150
28439
  /**
28151
28440
  * Atomically end speaking AND invalidate any pending grace timer.
28152
28441
  * Use instead of ``this.isSpeaking = false`` at barge-in sites.
@@ -28157,10 +28446,12 @@ Avoid:
28157
28446
  cancelSpeaking() {
28158
28447
  this.speakingGeneration++;
28159
28448
  this.isSpeaking = false;
28449
+ this.tailGraceActive = false;
28160
28450
  this.speakingStartedAt = null;
28161
28451
  this.firstAudioSentAt = null;
28162
28452
  this.lastCancelAt = Date.now();
28163
28453
  this.suppressedSpeechPending = false;
28454
+ this.playbackBufferedUntil = 0;
28164
28455
  this.drainPendingMarks();
28165
28456
  if (this.llmAbort !== null) {
28166
28457
  try {
@@ -28233,23 +28524,37 @@ Avoid:
28233
28524
  if (grace > 0) {
28234
28525
  const gen = this.speakingGeneration;
28235
28526
  this.clearGraceTimer();
28236
- this.graceTimer = setTimeout(() => {
28237
- this.graceTimer = null;
28238
- if (this.speakingGeneration === gen) {
28239
- this.isSpeaking = false;
28240
- this.speakingStartedAt = null;
28241
- this.firstAudioSentAt = null;
28242
- this.clearPendingBargeIn();
28243
- void this.resetBargeInStrategies();
28244
- if (this.suppressedSpeechPending) {
28245
- this.suppressedSpeechPending = false;
28246
- this.flushInboundAudioRing();
28527
+ const startTailGrace = () => {
28528
+ this.tailGraceActive = true;
28529
+ this.graceTimer = setTimeout(() => {
28530
+ this.graceTimer = null;
28531
+ if (this.speakingGeneration === gen) {
28532
+ this.isSpeaking = false;
28533
+ this.tailGraceActive = false;
28534
+ this.speakingStartedAt = null;
28535
+ this.firstAudioSentAt = null;
28536
+ this.clearPendingBargeIn();
28537
+ void this.resetBargeInStrategies();
28538
+ if (this.suppressedSpeechPending) {
28539
+ this.suppressedSpeechPending = false;
28540
+ this.flushInboundAudioRing();
28541
+ }
28542
+ this.resetVad();
28247
28543
  }
28248
- this.resetVad();
28249
- }
28250
- }, grace);
28544
+ }, grace);
28545
+ };
28546
+ const bufferedMs = Math.max(0, this.playbackBufferedUntil - Date.now());
28547
+ if (bufferedMs <= 0) {
28548
+ startTailGrace();
28549
+ } else {
28550
+ this.graceTimer = setTimeout(() => {
28551
+ this.graceTimer = null;
28552
+ if (this.speakingGeneration === gen) startTailGrace();
28553
+ }, bufferedMs);
28554
+ }
28251
28555
  } else {
28252
28556
  this.isSpeaking = false;
28557
+ this.tailGraceActive = false;
28253
28558
  this.speakingStartedAt = null;
28254
28559
  this.firstAudioSentAt = null;
28255
28560
  this.clearPendingBargeIn();
@@ -28261,6 +28566,35 @@ Avoid:
28261
28566
  this.resetVad();
28262
28567
  }
28263
28568
  }
28569
+ /**
28570
+ * End the post-TTS tail-grace window because the user has begun their next
28571
+ * turn. Unlike a barge-in, the agent's response already played out in full
28572
+ * — there is nothing to cancel and no turn was interrupted. We flip the
28573
+ * speaking flag off (bumping ``speakingGeneration`` so the scheduled grace
28574
+ * timer no-ops), recover any leading audio the self-hearing guard captured
28575
+ * into the ring (the user's first ~250 ms, which VAD needed before it could
28576
+ * emit ``speech_start``), and let the live STT stream take over. We do NOT
28577
+ * call ``sendClear``, ``recordBargeinDetected`` or ``recordTurnInterrupted``
28578
+ * — none apply to a turn that completed normally.
28579
+ *
28580
+ * Without this, fast next-turn speech (humans reply in 200-700 ms, well
28581
+ * inside the 1500 ms default grace) is withheld from STT and recorded as an
28582
+ * empty ``[interrupted]`` turn, after which the agent goes silent for the
28583
+ * rest of the call. Parity with Python ``_end_tail_grace_for_new_turn``.
28584
+ */
28585
+ endTailGraceForNewTurn() {
28586
+ this.isSpeaking = false;
28587
+ this.tailGraceActive = false;
28588
+ this.speakingStartedAt = null;
28589
+ this.firstAudioSentAt = null;
28590
+ this.playbackBufferedUntil = 0;
28591
+ this.speakingGeneration++;
28592
+ this.clearGraceTimer();
28593
+ this.clearPendingBargeIn();
28594
+ void this.resetBargeInStrategies();
28595
+ this.suppressedSpeechPending = false;
28596
+ this.flushInboundAudioRing();
28597
+ }
28264
28598
  async resetBargeInStrategies() {
28265
28599
  if (this.bargeInStrategies.length === 0) return;
28266
28600
  const { resetStrategies: resetStrategies2 } = await Promise.resolve().then(() => (init_barge_in_strategies(), barge_in_strategies_exports));
@@ -28396,9 +28730,43 @@ Avoid:
28396
28730
  maxDurationTimer = null;
28397
28731
  transcriptProcessing = false;
28398
28732
  transcriptQueue = [];
28733
+ /**
28734
+ * The in-flight turn dispatch (LLM + TTS) runs as a SINGLE tracked promise
28735
+ * so the transcript drain loop keeps running ``handleBargeIn`` against the
28736
+ * LIVE turn during a long (30-90 s) agent-runtime response, instead of
28737
+ * head-of-line-blocking on it. Exactly one is in flight: the launcher awaits
28738
+ * the previous one to settle (fast — a barge-in already aborted it) before
28739
+ * starting the next, preserving history/metrics ordering. Parity with
28740
+ * Python ``_dispatch_task``.
28741
+ */
28742
+ dispatchTask = null;
28743
+ /**
28744
+ * Cap (ms) on how long teardown waits for the backgrounded dispatch to
28745
+ * settle. JS promises are not cancellable, so a user-supplied ``onMessage``
28746
+ * (which receives no AbortSignal) parked on a hung external call could block
28747
+ * call cleanup indefinitely — `llmAbort.abort()` only unblocks the built-in
28748
+ * LLM/TTS paths. We bound the WAIT (Python hard-cancels the task instead).
28749
+ * 30 s matches the webhook ceiling.
28750
+ */
28751
+ static DISPATCH_SETTLE_TIMEOUT_MS = 3e4;
28752
+ /**
28753
+ * Opt-in (default OFF): forward inbound audio to STT even while the agent is
28754
+ * speaking, so the transcript barge-in path can receive a transcript on
28755
+ * echo-masked PSTN links where the VAD never fires. ECHO RISK without AEC.
28756
+ * Parity with Python ``_forward_stt_while_speaking``.
28757
+ */
28758
+ forwardSttWhileSpeaking = ["1", "true", "yes"].includes(
28759
+ (process.env.PATTER_FORWARD_STT_WHILE_SPEAKING ?? "").trim().toLowerCase()
28760
+ );
28399
28761
  // Throttle state for back-to-back STT finals — see ``commitTranscript``.
28400
28762
  lastCommitText = "";
28401
28763
  lastCommitAt = 0;
28764
+ /** The agent's spoken text for the CURRENT turn, accumulated as tokens stream.
28765
+ * The echo guard rejects transcripts matching it (the agent's own TTS bleeding
28766
+ * back into STT when audio is forwarded during TTS without effective AEC).
28767
+ * Reset in ``beginSpeaking``; only consulted while ``forwardSttWhileSpeaking``.
28768
+ * Parity with Python ``_current_agent_spoken_text``. */
28769
+ currentAgentSpokenText = "";
28402
28770
  // PCM16 byte-alignment carry for TTS streaming (pipeline mode).
28403
28771
  // HTTP streams from ElevenLabs / OpenAI / Cartesia can yield chunks of any
28404
28772
  // size, including odd byte counts. Silently dropping the trailing odd byte
@@ -28418,6 +28786,11 @@ Avoid:
28418
28786
  this.ws = ws;
28419
28787
  this.caller = caller;
28420
28788
  this.callee = callee;
28789
+ if (this.forwardSttWhileSpeaking) {
28790
+ getLogger().warn(
28791
+ "PATTER_FORWARD_STT_WHILE_SPEAKING=on: inbound audio is sent to STT during TTS so transcript barge-in works on echo-masked links. Without AEC the agent's own voice may be transcribed as a phantom interruption \u2014 pair with agent.bargeInStrategies."
28792
+ );
28793
+ }
28421
28794
  this.bargeInStrategies = (deps.agent.bargeInStrategies ?? []).slice();
28422
28795
  const confirmMs = deps.agent.bargeInConfirmMs;
28423
28796
  this.bargeInConfirmMs = typeof confirmMs === "number" && Number.isFinite(confirmMs) && confirmMs > 0 ? confirmMs : 1500;
@@ -28617,12 +28990,12 @@ Avoid:
28617
28990
  } catch {
28618
28991
  }
28619
28992
  if (this.deps.onCallStart) {
28620
- const direction = this.deps.metricsStore.getActive(callId)?.direction ?? "inbound";
28993
+ const direction2 = this.deps.metricsStore.getActive(callId)?.direction ?? "inbound";
28621
28994
  await this.deps.onCallStart({
28622
28995
  call_id: callId,
28623
28996
  caller: this.caller,
28624
28997
  callee: this.callee,
28625
- direction,
28998
+ direction: direction2,
28626
28999
  telephony_provider: this.deps.bridge.telephonyProvider,
28627
29000
  ...Object.keys(customParams).length > 0 ? { custom_params: customParams } : {}
28628
29001
  });
@@ -28689,6 +29062,17 @@ Avoid:
28689
29062
  setStreamSid(sid) {
28690
29063
  this.streamSid = sid;
28691
29064
  }
29065
+ /**
29066
+ * Record a terminal/processing error as a coarse, anonymous code on the call
29067
+ * metrics (code only, never the message). Surfaced via `call_completed`
29068
+ * telemetry. Safe to call with any value; last write wins.
29069
+ */
29070
+ recordError(err) {
29071
+ try {
29072
+ this.metricsAcc.recordError(err);
29073
+ } catch {
29074
+ }
29075
+ }
28692
29076
  /** Handle an incoming audio chunk (already decoded from base64). */
28693
29077
  /** Forward inbound audio bytes to the AI adapter and (in pipeline mode) the STT provider. */
28694
29078
  async handleAudio(audioBuffer) {
@@ -28715,6 +29099,9 @@ Avoid:
28715
29099
  );
28716
29100
  }
28717
29101
  if (evt?.type === "speech_start") {
29102
+ if (this.isSpeaking && this.tailGraceActive) {
29103
+ this.endTailGraceForNewTurn();
29104
+ }
28718
29105
  const phantomSuppressed = this.isSpeaking && !this.canBargeIn();
28719
29106
  if (phantomSuppressed) {
28720
29107
  getLogger().info(
@@ -28722,7 +29109,8 @@ Avoid:
28722
29109
  );
28723
29110
  this.suppressedSpeechPending = true;
28724
29111
  } else if (this.isSpeaking) {
28725
- if (this.bargeInStrategies.length > 0) {
29112
+ const deferCancel = this.bargeInStrategies.length > 0 || this.forwardSttWhileSpeaking && !this.aec;
29113
+ if (deferCancel) {
28726
29114
  this.startPendingBargeIn();
28727
29115
  this.metricsAcc.anchorUserSpeechStart();
28728
29116
  return;
@@ -28732,6 +29120,7 @@ Avoid:
28732
29120
  this.metricsAcc.recordBargeinDetected();
28733
29121
  const bargeinSpan = startSpan(SPAN_BARGEIN, { "patter.call.id": this.callId });
28734
29122
  try {
29123
+ this.maybeTruncateCompletedTurnHistory();
28735
29124
  this.cancelSpeaking();
28736
29125
  try {
28737
29126
  this.deps.bridge.sendClear(this.ws, this.streamSid);
@@ -28776,9 +29165,10 @@ Avoid:
28776
29165
  if (this.inboundAudioRing.length > _StreamHandler.INBOUND_AUDIO_RING_FRAMES) {
28777
29166
  this.inboundAudioRing.shift();
28778
29167
  }
29168
+ if (!this.forwardSttWhileSpeaking) return;
29169
+ } else if ((this.deps.agent.bargeInThresholdMs ?? 300) === 0) {
28779
29170
  return;
28780
29171
  }
28781
- if ((this.deps.agent.bargeInThresholdMs ?? 300) === 0) return;
28782
29172
  }
28783
29173
  const hooks = this.deps.agent.hooks;
28784
29174
  if (hooks?.beforeSendToStt) {
@@ -28840,6 +29230,27 @@ Avoid:
28840
29230
  }
28841
29231
  }
28842
29232
  }
29233
+ /**
29234
+ * Await the backgrounded turn dispatch during teardown, but never block
29235
+ * longer than ``DISPATCH_SETTLE_TIMEOUT_MS``. The earlier ``llmAbort.abort()``
29236
+ * settles the built-in LLM/TTS paths immediately; the cap only bites a
29237
+ * misbehaving user ``onMessage`` parked on a hung external call (JS promises
29238
+ * can't be cancelled). No-op when nothing is in flight.
29239
+ */
29240
+ async settleDispatchForTeardown() {
29241
+ if (!this.dispatchTask) return;
29242
+ const settle = this.dispatchTask.catch(() => {
29243
+ });
29244
+ let timer;
29245
+ const cap = new Promise((resolve2) => {
29246
+ timer = setTimeout(resolve2, _StreamHandler.DISPATCH_SETTLE_TIMEOUT_MS);
29247
+ });
29248
+ try {
29249
+ await Promise.race([settle, cap]);
29250
+ } finally {
29251
+ if (timer) clearTimeout(timer);
29252
+ }
29253
+ }
28843
29254
  /** Handle call stop / stream end. */
28844
29255
  /** Handle a carrier-emitted `stop` event signalling the call has ended. */
28845
29256
  async handleStop() {
@@ -28856,6 +29267,7 @@ Avoid:
28856
29267
  } catch {
28857
29268
  }
28858
29269
  }
29270
+ await this.settleDispatchForTeardown();
28859
29271
  this.clearPendingBargeIn();
28860
29272
  this.drainPendingMarks();
28861
29273
  this.clearGraceTimer();
@@ -28883,6 +29295,7 @@ Avoid:
28883
29295
  } catch {
28884
29296
  }
28885
29297
  }
29298
+ await this.settleDispatchForTeardown();
28886
29299
  this.clearPendingBargeIn();
28887
29300
  this.drainPendingMarks();
28888
29301
  this.clearGraceTimer();
@@ -29277,7 +29690,7 @@ Avoid:
29277
29690
  };
29278
29691
  }
29279
29692
  /** Synthesize a single sentence through TTS with hooks, sending audio to telephony. */
29280
- async synthesizeSentence(sentence, hookExecutor, hookCtx, ttsFirstByteSent) {
29693
+ async synthesizeSentence(sentence, hookExecutor, hookCtx, ttsFirstByteSent, recordSegment = true) {
29281
29694
  if (!this.tts || !this.isSpeaking) return;
29282
29695
  let transformed = sentence;
29283
29696
  const transforms = this.deps.agent.textTransforms;
@@ -29303,8 +29716,16 @@ Avoid:
29303
29716
  if (this.aec) {
29304
29717
  this.aec.pushFarEnd(processedAudio);
29305
29718
  }
29719
+ if (recordSegment) {
29720
+ this.turnSpokenSegments.push({
29721
+ text: processedText,
29722
+ startMs: this.turnPlaybackTotalMs
29723
+ });
29724
+ recordSegment = false;
29725
+ }
29306
29726
  const encoded = this.encodePipelineAudio(processedAudio);
29307
29727
  this.deps.bridge.sendAudio(this.ws, encoded, this.streamSid);
29728
+ this.trackOutboundPlayback(processedAudio.length);
29308
29729
  this.markFirstAudioSent();
29309
29730
  }
29310
29731
  } catch (e) {
@@ -29379,64 +29800,101 @@ Avoid:
29379
29800
  return;
29380
29801
  }
29381
29802
  this.history.push({ role: "user", text: filteredTranscript, timestamp: Date.now() });
29382
- let responseText = "";
29383
29803
  this.metricsAcc.recordOnUserTurnCompletedDelay(0);
29384
29804
  this.metricsAcc.recordTurnCommitted();
29385
29805
  closeEndpointSpan();
29386
- if (this.deps.onMessage && typeof this.deps.onMessage === "function") {
29387
- try {
29388
- responseText = await this.deps.onMessage({
29806
+ await this.dispatchTask?.catch(() => {
29807
+ });
29808
+ const historySnapshot = [...this.history.entries];
29809
+ this.dispatchTask = this.dispatchTurn(
29810
+ filteredTranscript,
29811
+ hookExecutor,
29812
+ hookCtx,
29813
+ interrupted,
29814
+ historySnapshot
29815
+ );
29816
+ }
29817
+ /**
29818
+ * Post-commit turn body (LLM dispatch → TTS → turn-complete) run as a
29819
+ * tracked background task so the transcript drain loop is not blocked for
29820
+ * the whole (possibly 30-90 s) agent-runtime turn. A barge-in — transcript
29821
+ * (now reachable mid-turn) or VAD — aborts the in-flight ``llmAbort`` and
29822
+ * flips ``isSpeaking``, which the LLM/TTS loops here observe and break on.
29823
+ * Parity with Python ``_dispatch_turn``.
29824
+ */
29825
+ async dispatchTurn(filteredTranscript, hookExecutor, hookCtx, interrupted, historySnapshot) {
29826
+ const label = this.deps.bridge.label;
29827
+ let responseText = "";
29828
+ try {
29829
+ if (this.deps.onMessage && typeof this.deps.onMessage === "function") {
29830
+ try {
29831
+ responseText = await this.deps.onMessage({
29832
+ text: filteredTranscript,
29833
+ call_id: this.callId,
29834
+ caller: this.caller,
29835
+ callee: this.callee,
29836
+ history: historySnapshot
29837
+ });
29838
+ } catch (e) {
29839
+ getLogger().error(`onMessage error (${label}):`, e);
29840
+ return;
29841
+ }
29842
+ if (!responseText) {
29843
+ getLogger().warn(
29844
+ `onMessage returned empty/void (${label}) \u2014 no TTS will play. If you intended to observe transcripts, use onTranscript instead; if you meant to answer via the built-in LLM, remove onMessage and pass openaiKey.`
29845
+ );
29846
+ }
29847
+ } else if (this.deps.onMessage && isRemoteUrl(this.deps.onMessage)) {
29848
+ const msgData = {
29389
29849
  text: filteredTranscript,
29390
29850
  call_id: this.callId,
29391
29851
  caller: this.caller,
29392
29852
  callee: this.callee,
29393
- history: [...this.history.entries]
29394
- });
29395
- } catch (e) {
29396
- getLogger().error(`onMessage error (${label}):`, e);
29397
- return;
29398
- }
29399
- if (!responseText) {
29853
+ history: historySnapshot
29854
+ };
29855
+ if (isWebSocketUrl(this.deps.onMessage)) {
29856
+ await this.handleWebSocketResponse(msgData);
29857
+ return;
29858
+ }
29859
+ try {
29860
+ responseText = await this.deps.remoteHandler.callWebhook(this.deps.onMessage, msgData);
29861
+ } catch (e) {
29862
+ getLogger().error(`Webhook remote error (${label}):`, e);
29863
+ return;
29864
+ }
29865
+ } else if (this.llmLoop) {
29866
+ const llmResult = await this.runPipelineLlm(
29867
+ filteredTranscript,
29868
+ hookExecutor,
29869
+ hookCtx,
29870
+ historySnapshot
29871
+ );
29872
+ responseText = llmResult.text;
29873
+ interrupted = interrupted || llmResult.interrupted;
29874
+ } else {
29400
29875
  getLogger().warn(
29401
- `onMessage returned empty/void (${label}) \u2014 no TTS will play. If you intended to observe transcripts, use onTranscript instead; if you meant to answer via the built-in LLM, remove onMessage and pass openaiKey.`
29876
+ `Pipeline (${label}) has no llm/onMessage handler \u2014 transcript "${sanitizeLogValue(filteredTranscript.slice(0, 60))}" dropped. Check that agent.llm or onMessage is configured.`
29402
29877
  );
29403
- }
29404
- } else if (this.deps.onMessage && isRemoteUrl(this.deps.onMessage)) {
29405
- const msgData = {
29406
- text: filteredTranscript,
29407
- call_id: this.callId,
29408
- caller: this.caller,
29409
- callee: this.callee,
29410
- history: [...this.history.entries]
29411
- };
29412
- if (isWebSocketUrl(this.deps.onMessage)) {
29413
- await this.handleWebSocketResponse(msgData);
29414
29878
  return;
29415
29879
  }
29416
- try {
29417
- responseText = await this.deps.remoteHandler.callWebhook(this.deps.onMessage, msgData);
29418
- } catch (e) {
29419
- getLogger().error(`Webhook remote error (${label}):`, e);
29420
- return;
29880
+ if (!responseText) return;
29881
+ if (this.llmLoop) {
29882
+ let spokenText = responseText;
29883
+ if (interrupted) {
29884
+ const heard = this.heardResponsePrefix();
29885
+ spokenText = heard === null ? `${responseText} [interrupted by caller]` : heard.text ? `${heard.text} [interrupted by caller]` : "[interrupted by caller]";
29886
+ }
29887
+ await this.emitAssistantTranscript(spokenText);
29888
+ if (!interrupted) this.metricsAcc.recordTtsComplete(responseText);
29889
+ } else {
29890
+ interrupted = await this.runRegularLlm(responseText, hookExecutor, hookCtx) || interrupted;
29891
+ responseText = this.history.entries[this.history.entries.length - 1]?.text ?? responseText;
29421
29892
  }
29422
- } else if (this.llmLoop) {
29423
- responseText = await this.runPipelineLlm(filteredTranscript, hookExecutor, hookCtx);
29424
- } else {
29425
- getLogger().warn(
29426
- `Pipeline (${label}) has no llm/onMessage handler \u2014 transcript "${sanitizeLogValue(filteredTranscript.slice(0, 60))}" dropped. Check that agent.llm or onMessage is configured.`
29427
- );
29428
- return;
29429
- }
29430
- if (!responseText) return;
29431
- if (this.llmLoop) {
29432
- await this.emitAssistantTranscript(responseText);
29433
- this.metricsAcc.recordTtsComplete(responseText);
29434
- } else {
29435
- interrupted = await this.runRegularLlm(responseText, hookExecutor, hookCtx) || interrupted;
29436
- responseText = this.history.entries[this.history.entries.length - 1]?.text ?? responseText;
29437
- }
29438
- if (!interrupted) {
29439
- await this.emitTurnMetrics(this.metricsAcc.recordTurnComplete(responseText));
29893
+ if (!interrupted) {
29894
+ await this.emitTurnMetrics(this.metricsAcc.recordTurnComplete(responseText));
29895
+ }
29896
+ } finally {
29897
+ this.dispatchTask = null;
29440
29898
  }
29441
29899
  }
29442
29900
  /**
@@ -29447,6 +29905,18 @@ Avoid:
29447
29905
  */
29448
29906
  async handleBargeInAsync(transcript) {
29449
29907
  if (!transcript.text || !this.isSpeaking) return false;
29908
+ if (this.tailGraceActive) {
29909
+ this.endTailGraceForNewTurn();
29910
+ return false;
29911
+ }
29912
+ if (this.forwardSttWhileSpeaking && looksLikeEcho(transcript.text, this.currentAgentSpokenText)) {
29913
+ getLogger().info(
29914
+ `Barge-in suppressed: transcript matches agent's own speech (echo) \u2014 ${sanitizeLogValue(
29915
+ transcript.text.slice(0, 40)
29916
+ )}`
29917
+ );
29918
+ return false;
29919
+ }
29450
29920
  if (!this.canBargeIn()) {
29451
29921
  getLogger().info(
29452
29922
  `Barge-in transcript suppressed (agent speaking < gate, aec=${this.aec ? "on" : "off"})`
@@ -29486,6 +29956,18 @@ Avoid:
29486
29956
  */
29487
29957
  handleBargeIn(transcript) {
29488
29958
  if (!transcript.text || !this.isSpeaking) return false;
29959
+ if (this.tailGraceActive) {
29960
+ this.endTailGraceForNewTurn();
29961
+ return false;
29962
+ }
29963
+ if (this.forwardSttWhileSpeaking && looksLikeEcho(transcript.text, this.currentAgentSpokenText)) {
29964
+ getLogger().info(
29965
+ `Barge-in suppressed: transcript matches agent's own speech (echo) \u2014 ${sanitizeLogValue(
29966
+ transcript.text.slice(0, 40)
29967
+ )}`
29968
+ );
29969
+ return false;
29970
+ }
29489
29971
  if (this.bargeInStrategies.length === 0) {
29490
29972
  if (!this.canBargeIn()) {
29491
29973
  getLogger().info(
@@ -29517,6 +29999,7 @@ Avoid:
29517
29999
  this.metricsAcc.recordBargeinDetected();
29518
30000
  const bargeinSpan = startSpan(SPAN_BARGEIN, { "patter.call.id": this.callId });
29519
30001
  try {
30002
+ this.maybeTruncateCompletedTurnHistory();
29520
30003
  this.cancelSpeaking();
29521
30004
  try {
29522
30005
  this.deps.bridge.sendClear(this.ws, this.streamSid);
@@ -29580,15 +30063,21 @@ Avoid:
29580
30063
  getLogger().debug(`Dropped likely STT hallucination: ${sanitizeLogValue(normalised.slice(0, 40))}`);
29581
30064
  return false;
29582
30065
  }
30066
+ if (this.forwardSttWhileSpeaking && this.isSpeaking && looksLikeEcho(text, this.currentAgentSpokenText)) {
30067
+ getLogger().debug(
30068
+ `Dropped agent-echo transcript (not a user turn): ${sanitizeLogValue(normalised.slice(0, 40))}`
30069
+ );
30070
+ return false;
30071
+ }
29583
30072
  if (sinceLastMs < 2e3 && normalised === this.lastCommitText) {
29584
30073
  getLogger().debug(
29585
30074
  `Dropped duplicate final transcript (${(sinceLastMs / 1e3).toFixed(1)}s since last): ${sanitizeLogValue(normalised.slice(0, 40))}`
29586
30075
  );
29587
30076
  return false;
29588
30077
  }
29589
- if (sinceLastMs < 500) {
30078
+ if (sinceLastMs < 500 && isNearDuplicate(normalised, this.lastCommitText)) {
29590
30079
  getLogger().debug(
29591
- `Dropped back-to-back final transcript (${(sinceLastMs / 1e3).toFixed(2)}s since last): ${sanitizeLogValue(normalised.slice(0, 40))}`
30080
+ `Dropped back-to-back near-duplicate final (${(sinceLastMs / 1e3).toFixed(2)}s since last): ${sanitizeLogValue(normalised.slice(0, 40))}`
29592
30081
  );
29593
30082
  return false;
29594
30083
  }
@@ -29596,11 +30085,63 @@ Avoid:
29596
30085
  this.lastCommitAt = now;
29597
30086
  return true;
29598
30087
  }
30088
+ /**
30089
+ * Schedule the opt-in long-turn filler and return its async ``clear()``.
30090
+ *
30091
+ * When ``agent.longTurnMessage`` is unset / empty the returned clear is a
30092
+ * no-op (byte-identical to today's behaviour). Otherwise a one-shot timer
30093
+ * fires after ``agent.longTurnMessageAfterS`` seconds and, IFF no audio has
30094
+ * reached the carrier this turn (``!ttsFirstByteSent.value``) AND we still own
30095
+ * the floor (``this.isSpeaking``), synthesizes the filler ONCE via the same
30096
+ * per-sentence TTS primitive every sentence uses.
30097
+ *
30098
+ * The returned ``clear()`` is **async**: it stops the timer AND, if the filler
30099
+ * already started synthesizing (its ``setTimeout`` callback runs in a separate
30100
+ * macro-task, so it can fire just before the first real sentence), AWAITS the
30101
+ * in-flight synthesis so the filler audio can never interleave with the real
30102
+ * sentence that follows. Idempotent; self-synthesis failure degrades to
30103
+ * silence (never crashes the turn). The caller must clear on first real audio,
30104
+ * on the error branch, and in the finally.
30105
+ */
30106
+ scheduleLongTurnFiller(ttsFirstByteSent, hookExecutor, hookCtx, label) {
30107
+ const message = this.deps.agent.longTurnMessage;
30108
+ if (!message) return async () => {
30109
+ };
30110
+ const afterS = this.deps.agent.longTurnMessageAfterS ?? 4;
30111
+ let cancelled = false;
30112
+ let inFlight = null;
30113
+ const timer = setTimeout(() => {
30114
+ if (cancelled || ttsFirstByteSent.value || !this.isSpeaking) return;
30115
+ inFlight = this.synthesizeSentence(
30116
+ message,
30117
+ hookExecutor,
30118
+ hookCtx,
30119
+ ttsFirstByteSent,
30120
+ false
30121
+ ).catch((err) => {
30122
+ getLogger().error(
30123
+ `longTurnMessage filler synthesis failed (${label}):`,
30124
+ err
30125
+ );
30126
+ });
30127
+ }, Math.max(0, afterS * 1e3));
30128
+ return async () => {
30129
+ cancelled = true;
30130
+ clearTimeout(timer);
30131
+ if (inFlight !== null) {
30132
+ const pending = inFlight;
30133
+ inFlight = null;
30134
+ await pending;
30135
+ }
30136
+ };
30137
+ }
29599
30138
  /**
29600
30139
  * Streaming built-in LLM path with sentence chunking and per-sentence
29601
- * guardrails/TTS. Returns the concatenated response text.
30140
+ * guardrails/TTS. Returns the concatenated (plain) response text plus whether
30141
+ * the turn was cut short by a barge-in — the caller applies the interrupted
30142
+ * marker to history only, keeping metrics on the plain text.
29602
30143
  */
29603
- async runPipelineLlm(filteredTranscript, hookExecutor, hookCtx) {
30144
+ async runPipelineLlm(filteredTranscript, hookExecutor, hookCtx, historySnapshot) {
29604
30145
  const label = this.deps.bridge.label;
29605
30146
  const callCtx = { call_id: this.callId, caller: this.caller, callee: this.callee };
29606
30147
  const chunker = new SentenceChunker({
@@ -29613,6 +30154,12 @@ Avoid:
29613
30154
  this.llmAbort = new AbortController();
29614
30155
  const llmSignal = this.llmAbort.signal;
29615
30156
  let llmError = false;
30157
+ const clearLongTurnFiller = this.scheduleLongTurnFiller(
30158
+ ttsFirstByteSent,
30159
+ hookExecutor,
30160
+ hookCtx,
30161
+ label
30162
+ );
29616
30163
  const llmSpan = startSpan(SPAN_LLM, { "patter.call.id": this.callId });
29617
30164
  const guardAndSpeak = async (sentence, isFirst) => {
29618
30165
  if (isFirst) this.metricsAcc.recordLlmFirstSentenceComplete();
@@ -29623,6 +30170,7 @@ Avoid:
29623
30170
  if (transformed === null) return;
29624
30171
  sentenceText = transformed;
29625
30172
  }
30173
+ await clearLongTurnFiller();
29626
30174
  await this.synthesizeSentence(sentenceText, hookExecutor, hookCtx, ttsFirstByteSent);
29627
30175
  };
29628
30176
  let firstSentenceEmitted = false;
@@ -29630,7 +30178,7 @@ Avoid:
29630
30178
  try {
29631
30179
  for await (const token of this.llmLoop.run(
29632
30180
  filteredTranscript,
29633
- this.history.entries,
30181
+ historySnapshot,
29634
30182
  callCtx,
29635
30183
  this.metricsAcc,
29636
30184
  hookExecutor,
@@ -29641,6 +30189,7 @@ Avoid:
29641
30189
  this.metricsAcc.recordLlmFirstToken();
29642
30190
  await this.emitLlmFirstToken();
29643
30191
  allParts.push(token);
30192
+ this.currentAgentSpokenText = allParts.join("");
29644
30193
  for (const sentence of chunker.push(token)) {
29645
30194
  if (!this.isSpeaking) break;
29646
30195
  await guardAndSpeak(sentence, !firstSentenceEmitted);
@@ -29650,11 +30199,20 @@ Avoid:
29650
30199
  }
29651
30200
  } catch (e) {
29652
30201
  const isAbort = e?.name === "AbortError" || llmSignal.aborted;
30202
+ await clearLongTurnFiller();
29653
30203
  if (!isAbort) {
29654
30204
  llmError = true;
29655
30205
  chunker.reset();
29656
30206
  getLogger().error(`LLM loop error (${label}):`, e);
29657
30207
  this.metricsAcc.recordTurnInterrupted();
30208
+ const fallback = this.deps.agent.llmErrorMessage;
30209
+ if (fallback && !ttsFirstByteSent.value && this.isSpeaking) {
30210
+ try {
30211
+ await this.synthesizeSentence(fallback, hookExecutor, hookCtx, ttsFirstByteSent, false);
30212
+ } catch (err) {
30213
+ getLogger().error(`llmErrorMessage fallback synthesis failed (${label}):`, err);
30214
+ }
30215
+ }
29658
30216
  }
29659
30217
  }
29660
30218
  this.metricsAcc.recordLlmComplete();
@@ -29666,6 +30224,7 @@ Avoid:
29666
30224
  }
29667
30225
  }
29668
30226
  } finally {
30227
+ await clearLongTurnFiller();
29669
30228
  this.endSpeakingWithGrace();
29670
30229
  this.llmAbort = null;
29671
30230
  try {
@@ -29673,7 +30232,7 @@ Avoid:
29673
30232
  } catch {
29674
30233
  }
29675
30234
  }
29676
- return allParts.join("");
30235
+ return { text: allParts.join(""), interrupted: llmSignal.aborted };
29677
30236
  }
29678
30237
  /**
29679
30238
  * Non-streaming path (onMessage function / webhook): apply output guardrails,
@@ -30760,7 +31319,7 @@ function validateTelnyxSignature(rawBody, signature, timestamp, publicKey, toler
30760
31319
  if (!Number.isFinite(ts)) return false;
30761
31320
  const tsMs = ts < 1e12 ? ts * 1e3 : ts;
30762
31321
  const ageMs = Date.now() - tsMs;
30763
- if (ageMs < 0 || ageMs > toleranceSec * 1e3) return false;
31322
+ if (ageMs > toleranceSec * 1e3 || ageMs < -TELNYX_FUTURE_SKEW_MS) return false;
30764
31323
  const payload = `${timestamp}|${rawBody}`;
30765
31324
  const keyBuffer = Buffer.from(publicKey, "base64");
30766
31325
  const keyObject = import_node_crypto4.default.createPublicKey({
@@ -30806,7 +31365,7 @@ function sanitizeVariables(raw) {
30806
31365
  for (const key of Object.keys(raw)) {
30807
31366
  if (BLOCKED_KEYS.has(key)) continue;
30808
31367
  const val = raw[key];
30809
- safe[key] = typeof val === "string" ? val : String(val ?? "");
31368
+ safe[key] = (typeof val === "string" ? val : String(val ?? "")).replace(/[\x00-\x1f\x7f]/g, "").slice(0, 500);
30810
31369
  }
30811
31370
  return safe;
30812
31371
  }
@@ -30901,7 +31460,7 @@ async function sleep(ms) {
30901
31460
  if (ms <= 0) return;
30902
31461
  await new Promise((resolve2) => setTimeout(resolve2, ms));
30903
31462
  }
30904
- var import_node_crypto4, import_express, import_http, import_ws5, TRANSFER_CALL_TOOL, END_CALL_TOOL, TwilioBridge, TELNYX_DTMF_ALLOWED, TELNYX_DTMF_DURATION_MS, TelnyxBridge, GRACEFUL_SHUTDOWN_TIMEOUT_MS, EmbeddedServer;
31463
+ var import_node_crypto4, import_express, import_http, import_ws5, TRANSFER_CALL_TOOL, END_CALL_TOOL, TELNYX_FUTURE_SKEW_MS, TwilioBridge, TELNYX_DTMF_ALLOWED, TELNYX_DTMF_DURATION_MS, TelnyxBridge, GRACEFUL_SHUTDOWN_TIMEOUT_MS, EmbeddedServer;
30905
31464
  var init_server = __esm({
30906
31465
  "src/server.ts"() {
30907
31466
  "use strict";
@@ -30910,6 +31469,7 @@ var init_server = __esm({
30910
31469
  import_express = __toESM(require("express"));
30911
31470
  import_http = require("http");
30912
31471
  import_ws5 = require("ws");
31472
+ init_call_metrics();
30913
31473
  init_openai_realtime_2();
30914
31474
  init_elevenlabs_convai();
30915
31475
  init_plivo_adapter();
@@ -30949,6 +31509,7 @@ var init_server = __esm({
30949
31509
  }
30950
31510
  }
30951
31511
  };
31512
+ TELNYX_FUTURE_SKEW_MS = 3e4;
30952
31513
  TwilioBridge = class {
30953
31514
  constructor(config2) {
30954
31515
  this.config = config2;
@@ -31250,6 +31811,9 @@ var init_server = __esm({
31250
31811
  twilioTokenWarningLogged = false;
31251
31812
  telnyxSigWarningLogged = false;
31252
31813
  metricsStore;
31814
+ /** Anonymous telemetry client, set by ``client.ts`` ``serve()``; emits the
31815
+ * per-call ``call_completed`` event from the call-end path. */
31816
+ telemetry;
31253
31817
  pricing;
31254
31818
  remoteHandler = new RemoteMessageHandler();
31255
31819
  /**
@@ -31353,6 +31917,12 @@ var init_server = __esm({
31353
31917
  * Mirrors Python's ``_resolve_completion``.
31354
31918
  */
31355
31919
  resolveCompletion(callId, args) {
31920
+ if (args.outcome === "no_answer" || args.outcome === "busy" || args.outcome === "failed") {
31921
+ recordCallCompleted(this.telemetry, {
31922
+ outcome: args.outcome,
31923
+ carrier: this.config.telephonyProvider
31924
+ });
31925
+ }
31356
31926
  const entry = this.completions.get(callId);
31357
31927
  if (!entry || entry.done) return;
31358
31928
  const data = args.data;
@@ -32101,7 +32671,13 @@ var init_server = __esm({
32101
32671
  return Object.fromEntries(Object.entries(snap).filter(([, v]) => v !== void 0));
32102
32672
  };
32103
32673
  const store = this.metricsStore;
32674
+ const telemetry = this.telemetry;
32104
32675
  const wrappedStart = async (data) => {
32676
+ recordCallStarted(telemetry, {
32677
+ providerMode: agent.provider ?? void 0,
32678
+ telephonyProvider: bridge.telephonyProvider,
32679
+ direction: data.direction
32680
+ });
32105
32681
  if (logger2.enabled) {
32106
32682
  const callId = typeof data.call_id === "string" ? data.call_id : "";
32107
32683
  const dataCaller = typeof data.caller === "string" ? data.caller : "";
@@ -32132,6 +32708,11 @@ var init_server = __esm({
32132
32708
  if (userMetrics) await userMetrics(data);
32133
32709
  };
32134
32710
  const wrappedEnd = async (data) => {
32711
+ recordCallCompleted(this.telemetry, {
32712
+ outcome: "completed",
32713
+ metrics: data.metrics,
32714
+ direction: data.direction
32715
+ });
32135
32716
  if (logger2.enabled) {
32136
32717
  const callId = typeof data.call_id === "string" ? data.call_id : "";
32137
32718
  const metricsObj = data.metrics ?? null;
@@ -32187,7 +32768,7 @@ var init_server = __esm({
32187
32768
  await handler.handleCallStart(callSid, customParameters);
32188
32769
  } else if (event === "media") {
32189
32770
  const payload = data.media?.payload ?? "";
32190
- handler.handleAudio(Buffer.from(payload, "base64"));
32771
+ await handler.handleAudio(Buffer.from(payload, "base64"));
32191
32772
  } else if (event === "mark") {
32192
32773
  const markName = String(data.mark?.name ?? "");
32193
32774
  if (markName) await handler.onMark(markName);
@@ -32199,6 +32780,7 @@ var init_server = __esm({
32199
32780
  }
32200
32781
  } catch (err) {
32201
32782
  getLogger().error("Stream handler error:", err);
32783
+ handler.recordError(err);
32202
32784
  }
32203
32785
  });
32204
32786
  ws.on("close", async () => {
@@ -32243,7 +32825,7 @@ var init_server = __esm({
32243
32825
  if (track !== "inbound") return;
32244
32826
  const audioChunk = data.media?.payload ?? "";
32245
32827
  if (!audioChunk) return;
32246
- handler.handleAudio(Buffer.from(audioChunk, "base64"));
32828
+ await handler.handleAudio(Buffer.from(audioChunk, "base64"));
32247
32829
  } else if (event === "dtmf") {
32248
32830
  const digit = String(data.dtmf?.digit ?? "").trim();
32249
32831
  if (digit) {
@@ -32257,9 +32839,11 @@ var init_server = __esm({
32257
32839
  }
32258
32840
  } catch (err) {
32259
32841
  getLogger().error("Stream handler error (Telnyx):", err);
32842
+ handler.recordError(err);
32260
32843
  }
32261
32844
  });
32262
32845
  ws.on("close", async () => {
32846
+ this.activeCallIds.delete(ws);
32263
32847
  await handler.handleWsClose();
32264
32848
  });
32265
32849
  }
@@ -32288,7 +32872,7 @@ var init_server = __esm({
32288
32872
  await handler.handleCallStart(callId);
32289
32873
  } else if (event === "media") {
32290
32874
  const payload = data.media?.payload ?? "";
32291
- if (payload) handler.handleAudio(Buffer.from(payload, "base64"));
32875
+ if (payload) await handler.handleAudio(Buffer.from(payload, "base64"));
32292
32876
  } else if (event === "playedStream") {
32293
32877
  const markName = String(data.name ?? "");
32294
32878
  if (markName) await handler.onMark(markName);
@@ -32302,6 +32886,7 @@ var init_server = __esm({
32302
32886
  }
32303
32887
  } catch (err) {
32304
32888
  getLogger().error("Stream handler error (Plivo):", err);
32889
+ handler.recordError(err);
32305
32890
  }
32306
32891
  });
32307
32892
  ws.on("close", async () => {
@@ -34182,6 +34767,7 @@ __export(index_exports, {
34182
34767
  CerebrasLLM: () => LLM4,
34183
34768
  ChatContext: () => ChatContext,
34184
34769
  CloudflareTunnel: () => CloudflareTunnel,
34770
+ CustomLLM: () => LLM7,
34185
34771
  DEFAULT_MIN_SENTENCE_LEN: () => DEFAULT_MIN_SENTENCE_LEN,
34186
34772
  DEFAULT_PRICING: () => DEFAULT_PRICING,
34187
34773
  DTMF_EVENTS: () => DTMF_EVENTS,
@@ -34205,6 +34791,7 @@ __export(index_exports, {
34205
34791
  GoogleLLM: () => LLM5,
34206
34792
  GroqLLM: () => LLM3,
34207
34793
  Guardrail: () => Guardrail,
34794
+ HermesLLM: () => LLM8,
34208
34795
  IVRActivity: () => IVRActivity,
34209
34796
  InworldTTS: () => TTS7,
34210
34797
  KrispFrameDuration: () => KrispFrameDuration,
@@ -34215,6 +34802,8 @@ __export(index_exports, {
34215
34802
  MetricsStore: () => MetricsStore,
34216
34803
  MinWordsStrategy: () => MinWordsStrategy,
34217
34804
  Ngrok: () => Ngrok,
34805
+ OpenAICompatibleLLM: () => LLM6,
34806
+ OpenAICompatibleLLMProvider: () => OpenAICompatibleLLMProvider,
34218
34807
  OpenAILLM: () => LLM,
34219
34808
  OpenAILLMProvider: () => OpenAILLMProvider,
34220
34809
  OpenAIRealtime: () => Realtime,
@@ -34228,6 +34817,7 @@ __export(index_exports, {
34228
34817
  OpenAITranscribeSTT: () => STT3,
34229
34818
  OpenAITranscriptionModel: () => OpenAITranscriptionModel,
34230
34819
  OpenAIVoice: () => OpenAIVoice,
34820
+ OpenClawLLM: () => LLM9,
34231
34821
  PRICING_LAST_UPDATED: () => PRICING_LAST_UPDATED,
34232
34822
  PRICING_VERSION: () => PRICING_VERSION,
34233
34823
  PartialStreamError: () => PartialStreamError,
@@ -34296,6 +34886,7 @@ __export(index_exports, {
34296
34886
  createResampler24kTo16k: () => createResampler24kTo16k,
34297
34887
  createResampler24kTo8k: () => createResampler24kTo8k,
34298
34888
  createResampler8kTo16k: () => createResampler8kTo16k,
34889
+ custom: () => custom2,
34299
34890
  deepgram: () => deepgram,
34300
34891
  defineTool: () => defineTool,
34301
34892
  elevenlabs: () => elevenlabs,
@@ -34307,6 +34898,8 @@ __export(index_exports, {
34307
34898
  geminiLive: () => geminiLive,
34308
34899
  getLogger: () => getLogger,
34309
34900
  guardrail: () => guardrail,
34901
+ hashCaller: () => hashCaller,
34902
+ hermes: () => hermes,
34310
34903
  initTracing: () => initTracing,
34311
34904
  isRemoteUrl: () => isRemoteUrl,
34312
34905
  isTracingEnabled: () => isTracingEnabled,
@@ -34319,7 +34912,9 @@ __export(index_exports, {
34319
34912
  mountDashboard: () => mountDashboard,
34320
34913
  mulawToPcm16: () => mulawToPcm16,
34321
34914
  notifyDashboard: () => notifyDashboard,
34915
+ openaiCompatible: () => openaiCompatible,
34322
34916
  openaiTts: () => openaiTts,
34917
+ openclaw: () => openclaw,
34323
34918
  openclawConsult: () => openclawConsult,
34324
34919
  openclawPostCallNotifier: () => openclawPostCallNotifier,
34325
34920
  pcm16ToMulaw: () => pcm16ToMulaw,
@@ -34350,6 +34945,60 @@ init_cjs_shims();
34350
34945
  init_errors();
34351
34946
  init_server();
34352
34947
 
34948
+ // src/telephony/twilio.ts
34949
+ init_cjs_shims();
34950
+ var Carrier2 = class {
34951
+ kind = "twilio";
34952
+ accountSid;
34953
+ authToken;
34954
+ constructor(opts = {}) {
34955
+ const sid = opts.accountSid ?? process.env.TWILIO_ACCOUNT_SID;
34956
+ const tok = opts.authToken ?? process.env.TWILIO_AUTH_TOKEN;
34957
+ if (!sid) {
34958
+ throw new Error(
34959
+ "Twilio carrier requires accountSid. Pass { accountSid: 'AC...' } or set TWILIO_ACCOUNT_SID in the environment."
34960
+ );
34961
+ }
34962
+ if (!tok) {
34963
+ throw new Error(
34964
+ "Twilio carrier requires authToken. Pass { authToken: '...' } or set TWILIO_AUTH_TOKEN in the environment."
34965
+ );
34966
+ }
34967
+ this.accountSid = sid;
34968
+ this.authToken = tok;
34969
+ }
34970
+ };
34971
+
34972
+ // src/telephony/telnyx.ts
34973
+ init_cjs_shims();
34974
+ var Carrier3 = class {
34975
+ kind = "telnyx";
34976
+ apiKey;
34977
+ connectionId;
34978
+ publicKey;
34979
+ constructor(opts = {}) {
34980
+ const key = opts.apiKey ?? process.env.TELNYX_API_KEY;
34981
+ const conn = opts.connectionId ?? process.env.TELNYX_CONNECTION_ID;
34982
+ const pub = opts.publicKey ?? process.env.TELNYX_PUBLIC_KEY;
34983
+ if (!key) {
34984
+ throw new Error(
34985
+ "Telnyx carrier requires apiKey. Pass { apiKey: '...' } or set TELNYX_API_KEY in the environment."
34986
+ );
34987
+ }
34988
+ if (!conn) {
34989
+ throw new Error(
34990
+ "Telnyx carrier requires connectionId. Pass { connectionId: '...' } or set TELNYX_CONNECTION_ID in the environment."
34991
+ );
34992
+ }
34993
+ this.apiKey = key;
34994
+ this.connectionId = conn;
34995
+ this.publicKey = pub;
34996
+ }
34997
+ };
34998
+
34999
+ // src/client.ts
35000
+ init_plivo();
35001
+
34353
35002
  // src/engines/openai.ts
34354
35003
  init_cjs_shims();
34355
35004
  init_openai_realtime();
@@ -34576,6 +35225,570 @@ function validateAllToolSchemas(tools) {
34576
35225
  // src/client.ts
34577
35226
  init_logger();
34578
35227
 
35228
+ // src/telemetry/index.ts
35229
+ init_cjs_shims();
35230
+
35231
+ // src/telemetry/client.ts
35232
+ init_cjs_shims();
35233
+ init_logger();
35234
+
35235
+ // src/telemetry/consent.ts
35236
+ init_cjs_shims();
35237
+
35238
+ // src/telemetry/env.ts
35239
+ init_cjs_shims();
35240
+ var CI_ENV_VARS = [
35241
+ "CI",
35242
+ "CONTINUOUS_INTEGRATION",
35243
+ "GITHUB_ACTIONS",
35244
+ "GITLAB_CI",
35245
+ "TRAVIS",
35246
+ "CIRCLECI",
35247
+ "APPVEYOR",
35248
+ "TF_BUILD",
35249
+ "TEAMCITY_VERSION",
35250
+ "BUILDKITE",
35251
+ "DRONE",
35252
+ "JENKINS_URL",
35253
+ "HUDSON_URL",
35254
+ "BAMBOO_BUILDKEY",
35255
+ "CODEBUILD_BUILD_ID"
35256
+ ];
35257
+ var TEST_ENV_VARS = ["VITEST", "JEST_WORKER_ID"];
35258
+ function isTruthy(value) {
35259
+ if (value === void 0) return false;
35260
+ const v = value.trim().toLowerCase();
35261
+ return v !== "" && v !== "0" && v !== "false" && v !== "no" && v !== "off";
35262
+ }
35263
+ function isCi() {
35264
+ return CI_ENV_VARS.some((name) => isTruthy(process.env[name]));
35265
+ }
35266
+ function isTest() {
35267
+ if (TEST_ENV_VARS.some((name) => process.env[name] !== void 0)) return true;
35268
+ const node = (process.env.NODE_ENV ?? "").trim().toLowerCase();
35269
+ const patter = (process.env.PATTER_ENV ?? "").trim().toLowerCase();
35270
+ return node === "test" || patter === "test";
35271
+ }
35272
+
35273
+ // src/telemetry/install-id.ts
35274
+ init_cjs_shims();
35275
+ var import_node_crypto5 = require("crypto");
35276
+ var fs5 = __toESM(require("fs"));
35277
+ var os2 = __toESM(require("os"));
35278
+ var path5 = __toESM(require("path"));
35279
+ var RUN_ID = (0, import_node_crypto5.randomUUID)().replace(/-/g, "");
35280
+ var HEX32 = /^[0-9a-f]{32}$/;
35281
+ var VERSION_RE = /^[0-9][0-9a-z.+-]{0,31}$/;
35282
+ var cachedInstallId = null;
35283
+ function runId() {
35284
+ return RUN_ID;
35285
+ }
35286
+ function statePath() {
35287
+ const base = process.env.PATTER_TELEMETRY_STATE_DIR || process.env.XDG_STATE_HOME;
35288
+ const root = base && base.length > 0 ? base : path5.join(os2.homedir(), ".getpatter");
35289
+ return path5.join(root, "install-id");
35290
+ }
35291
+ function installId() {
35292
+ if (cachedInstallId !== null) return cachedInstallId;
35293
+ const p = statePath();
35294
+ try {
35295
+ const existing = fs5.readFileSync(p, "utf8").trim();
35296
+ if (HEX32.test(existing)) {
35297
+ cachedInstallId = existing;
35298
+ return cachedInstallId;
35299
+ }
35300
+ } catch {
35301
+ }
35302
+ const newId = (0, import_node_crypto5.randomUUID)().replace(/-/g, "");
35303
+ try {
35304
+ fs5.mkdirSync(path5.dirname(p), { recursive: true });
35305
+ fs5.writeFileSync(p, newId, "utf8");
35306
+ cachedInstallId = newId;
35307
+ } catch {
35308
+ cachedInstallId = RUN_ID;
35309
+ }
35310
+ return cachedInstallId;
35311
+ }
35312
+ function versionPath() {
35313
+ return path5.join(path5.dirname(statePath()), "version");
35314
+ }
35315
+ function previousVersion(current) {
35316
+ const p = versionPath();
35317
+ let prev = "";
35318
+ try {
35319
+ prev = fs5.readFileSync(p, "utf8").trim();
35320
+ } catch {
35321
+ prev = "";
35322
+ }
35323
+ try {
35324
+ fs5.mkdirSync(path5.dirname(p), { recursive: true });
35325
+ fs5.writeFileSync(p, current, "utf8");
35326
+ } catch {
35327
+ }
35328
+ return VERSION_RE.test(prev) ? prev : "";
35329
+ }
35330
+ function daysSinceInstallBucket() {
35331
+ let mtimeMs;
35332
+ try {
35333
+ mtimeMs = fs5.statSync(statePath()).mtimeMs;
35334
+ } catch {
35335
+ return "0";
35336
+ }
35337
+ const days = Math.max(0, Math.floor((Date.now() - mtimeMs) / 864e5));
35338
+ if (days === 0) return "0";
35339
+ if (days <= 7) return "1_7";
35340
+ if (days <= 30) return "8_30";
35341
+ return "30_plus";
35342
+ }
35343
+ function firstRunPath() {
35344
+ return path5.join(path5.dirname(statePath()), "first-run");
35345
+ }
35346
+ function isFirstRun() {
35347
+ const p = firstRunPath();
35348
+ try {
35349
+ if (fs5.existsSync(p)) return false;
35350
+ } catch {
35351
+ return false;
35352
+ }
35353
+ try {
35354
+ fs5.mkdirSync(path5.dirname(p), { recursive: true });
35355
+ fs5.writeFileSync(p, "1", "utf8");
35356
+ return true;
35357
+ } catch {
35358
+ return false;
35359
+ }
35360
+ }
35361
+ function optOutPath() {
35362
+ return path5.join(path5.dirname(statePath()), "telemetry-disabled");
35363
+ }
35364
+ function isOptedOut() {
35365
+ try {
35366
+ return fs5.existsSync(optOutPath());
35367
+ } catch {
35368
+ return false;
35369
+ }
35370
+ }
35371
+
35372
+ // src/telemetry/consent.ts
35373
+ function isEnabled(flag) {
35374
+ if (isTruthy(process.env.DO_NOT_TRACK)) return false;
35375
+ if (isTruthy(process.env.PATTER_TELEMETRY_DISABLED)) return false;
35376
+ if (isOptedOut()) return false;
35377
+ if (flag === false) return false;
35378
+ if (isCi() || isTest()) return false;
35379
+ return true;
35380
+ }
35381
+
35382
+ // src/telemetry/events.ts
35383
+ init_cjs_shims();
35384
+ var os3 = __toESM(require("os"));
35385
+
35386
+ // src/telemetry/stack.ts
35387
+ init_cjs_shims();
35388
+ var STACK_VENDORS = /* @__PURE__ */ new Set([
35389
+ "openai",
35390
+ "anthropic",
35391
+ "google",
35392
+ "cerebras",
35393
+ "groq",
35394
+ "deepgram",
35395
+ "elevenlabs",
35396
+ "cartesia",
35397
+ "whisper",
35398
+ "soniox",
35399
+ "assemblyai",
35400
+ "speechmatics",
35401
+ "lmnt",
35402
+ "rime",
35403
+ "inworld",
35404
+ "telnyx",
35405
+ "other"
35406
+ ]);
35407
+ var VENDOR_ALIASES = {
35408
+ cartesia_stt: "cartesia",
35409
+ cartesia_tts: "cartesia",
35410
+ openai_tts: "openai",
35411
+ openai_transcribe: "openai",
35412
+ elevenlabs_ws: "elevenlabs",
35413
+ telnyx_stt: "telnyx",
35414
+ telnyx_tts: "telnyx"
35415
+ };
35416
+ var RAW_UNSAFE_RE = /[^a-z0-9._-]/;
35417
+ var DATE_SUFFIX_RE = /-\d{8}$/;
35418
+ function vendorOf(providerKey) {
35419
+ if (!providerKey) return "other";
35420
+ const v = VENDOR_ALIASES[providerKey] ?? providerKey;
35421
+ return STACK_VENDORS.has(v) ? v : "other";
35422
+ }
35423
+ function modelToken(vendor, rawModel) {
35424
+ if (!rawModel) return `${vendor}-other`;
35425
+ const m = rawModel.trim().toLowerCase();
35426
+ if (m.length > 40 || RAW_UNSAFE_RE.test(m)) return `${vendor}-other`;
35427
+ const token = m.replace(/_/g, "-").replace(DATE_SUFFIX_RE, "").replace(/^[-.]+|[-.]+$/g, "");
35428
+ return token ? `${vendor}-${token}` : `${vendor}-other`;
35429
+ }
35430
+ function readProviderKey(obj) {
35431
+ const ctor = obj?.constructor;
35432
+ const key = ctor?.providerKey;
35433
+ return typeof key === "string" && key ? key : null;
35434
+ }
35435
+ function readModel(obj) {
35436
+ const rec = obj;
35437
+ for (const attr of ["model", "modelId", "_model"]) {
35438
+ const v = rec?.[attr];
35439
+ if (typeof v === "string" && v) return v;
35440
+ }
35441
+ return "";
35442
+ }
35443
+ function layerDims(obj, providerField, modelField) {
35444
+ if (obj === null || obj === void 0) return {};
35445
+ const vendor = vendorOf(readProviderKey(obj));
35446
+ return { [providerField]: vendor, [modelField]: modelToken(vendor, readModel(obj)) };
35447
+ }
35448
+ function stackDimensions(stt, tts, llm) {
35449
+ return {
35450
+ ...layerDims(stt, "stt_provider", "stt_model"),
35451
+ ...layerDims(tts, "tts_provider", "tts_model"),
35452
+ ...layerDims(llm, "llm_provider", "llm_model")
35453
+ };
35454
+ }
35455
+
35456
+ // src/telemetry/events.ts
35457
+ var SCHEMA_VERSION2 = 5;
35458
+ var EVENT_SDK_INITIALIZED = "sdk_initialized";
35459
+ var EVENT_FIRST_RUN = "first_run";
35460
+ var EVENT_CLI_COMMAND = "cli_command";
35461
+ var EVENT_FEATURE_USED = "feature_used";
35462
+ var EVENT_AGENT_CONFIGURED = "agent_configured";
35463
+ var EVENT_CALL_STARTED = "call_started";
35464
+ var EVENT_CALL_COMPLETED = "call_completed";
35465
+ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
35466
+ EVENT_SDK_INITIALIZED,
35467
+ EVENT_FIRST_RUN,
35468
+ EVENT_CLI_COMMAND,
35469
+ EVENT_FEATURE_USED,
35470
+ EVENT_AGENT_CONFIGURED,
35471
+ EVENT_CALL_STARTED,
35472
+ EVENT_CALL_COMPLETED
35473
+ ]);
35474
+ var DIMENSION_VALUES = {
35475
+ carrier: /* @__PURE__ */ new Set(["twilio", "telnyx", "plivo", "none"]),
35476
+ tunnel: /* @__PURE__ */ new Set(["static", "configured", "none"]),
35477
+ engine: /* @__PURE__ */ new Set(["realtime", "convai", "pipeline"]),
35478
+ provider: /* @__PURE__ */ new Set([
35479
+ "openai",
35480
+ "elevenlabs",
35481
+ "deepgram",
35482
+ "cartesia",
35483
+ "cerebras",
35484
+ "anthropic",
35485
+ "google",
35486
+ "whisper",
35487
+ "other"
35488
+ ]),
35489
+ // agent_configured dimensions
35490
+ custom_tool_count_bucket: /* @__PURE__ */ new Set(["0", "1", "2_3", "4_6", "7_12", "13_plus"]),
35491
+ integration: /* @__PURE__ */ new Set(["openclaw", "mcp", "hermes", "other", "none"]),
35492
+ integration_kind: /* @__PURE__ */ new Set(["consult", "mcp", "none"]),
35493
+ mcp_server_count_bucket: /* @__PURE__ */ new Set(["0", "1", "2_3", "4_plus"]),
35494
+ // call_started / call_completed: inbound vs outbound — a core usage split.
35495
+ direction: /* @__PURE__ */ new Set(["inbound", "outbound", "none"]),
35496
+ // cli_command: which CLI subcommand was invoked (never args/flags values).
35497
+ cli_command: /* @__PURE__ */ new Set(["dashboard", "eval", "telemetry", "none", "other"]),
35498
+ // call_completed: the call's terminal outcome
35499
+ outcome: /* @__PURE__ */ new Set(["completed", "error", "no_answer", "busy", "failed"]),
35500
+ // call_completed: terminal error code (mirrors ErrorCode, plus "other"). Never
35501
+ // the error message.
35502
+ error_code: /* @__PURE__ */ new Set([
35503
+ "config",
35504
+ "connection",
35505
+ "auth",
35506
+ "timeout",
35507
+ "rate_limit",
35508
+ "webhook_verification",
35509
+ "input_validation",
35510
+ "provider_error",
35511
+ "provision",
35512
+ "internal",
35513
+ "other"
35514
+ ]),
35515
+ // feature_used (pipeline): per-layer vendor of the composed stack. A
35516
+ // providerKey not on the closed allowlist collapses to "other"; an absent layer
35517
+ // is omitted (the value set keeps "none" only as a safety token).
35518
+ stt_provider: /* @__PURE__ */ new Set([...STACK_VENDORS, "none"]),
35519
+ tts_provider: /* @__PURE__ */ new Set([...STACK_VENDORS, "none"]),
35520
+ llm_provider: /* @__PURE__ */ new Set([...STACK_VENDORS, "none"]),
35521
+ // sdk_initialized: anonymous deploy-shape (presence-only env/file probes).
35522
+ invoked_by_agent: /* @__PURE__ */ new Set(["claude", "cursor", "copilot", "gemini", "windsurf", "other", "none"]),
35523
+ serverless: /* @__PURE__ */ new Set(["lambda", "cloud_run", "vercel", "azure_functions", "none"]),
35524
+ cloud: /* @__PURE__ */ new Set(["aws", "gcp", "azure", "fly", "none"]),
35525
+ package_manager: /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun", "pip", "uv", "poetry", "pipenv", "conda", "none"]),
35526
+ days_since_install_bucket: /* @__PURE__ */ new Set(["0", "1_7", "8_30", "30_plus"]),
35527
+ // agent_configured: feature-adoption (Realtime tuning).
35528
+ noise_reduction: /* @__PURE__ */ new Set(["near_field", "far_field", "none"]),
35529
+ turn_detection: /* @__PURE__ */ new Set(["default", "custom", "none"]),
35530
+ // call_completed: how many conversational turns the call had.
35531
+ turn_count_bucket: /* @__PURE__ */ new Set(["0", "1", "2_3", "4_6", "7_12", "13_plus"])
35532
+ };
35533
+ var NUMERIC_DIMENSIONS = /* @__PURE__ */ new Set([
35534
+ "builtin_tool_count",
35535
+ "latency_ms",
35536
+ "duration_seconds",
35537
+ "cost_usd"
35538
+ ]);
35539
+ var STRING_DIMENSIONS = /* @__PURE__ */ new Set([
35540
+ "stt_model",
35541
+ "tts_model",
35542
+ "llm_model",
35543
+ "previous_sdk_version"
35544
+ ]);
35545
+ var MODEL_TOKEN_RE = /^[a-z0-9][a-z0-9.-]{0,40}$/;
35546
+ var BOOL_DIMENSIONS = /* @__PURE__ */ new Set([
35547
+ "container",
35548
+ "preambles_used",
35549
+ "per_tool_timeouts_set",
35550
+ "llm_fallback_configured"
35551
+ ]);
35552
+ var ALLOWED_DIMENSIONS = /* @__PURE__ */ new Set([
35553
+ ...Object.keys(DIMENSION_VALUES),
35554
+ ...NUMERIC_DIMENSIONS,
35555
+ ...STRING_DIMENSIONS,
35556
+ ...BOOL_DIMENSIONS
35557
+ ]);
35558
+ function osFamily() {
35559
+ const p = os3.platform();
35560
+ if (p === "win32") return "windows";
35561
+ return p || "unknown";
35562
+ }
35563
+ function arch2() {
35564
+ const a = os3.arch();
35565
+ if (a === "x64") return "x86_64";
35566
+ if (a === "arm64") return "arm64";
35567
+ return "other";
35568
+ }
35569
+ function runtimeVersion() {
35570
+ const parts = (process.versions.node ?? "0.0").split(".");
35571
+ return `${parts[0] ?? "0"}.${parts[1] ?? "0"}`;
35572
+ }
35573
+ function buildEvent(name, opts) {
35574
+ if (!ALLOWED_EVENTS.has(name)) {
35575
+ throw new Error(`unknown telemetry event: ${name}`);
35576
+ }
35577
+ const event = {
35578
+ event: name,
35579
+ schema_version: SCHEMA_VERSION2,
35580
+ run_id: runId(),
35581
+ install_id: installId(),
35582
+ sdk: "typescript",
35583
+ sdk_version: opts.sdkVersion,
35584
+ os: osFamily(),
35585
+ arch: arch2(),
35586
+ runtime: "node",
35587
+ runtime_version: runtimeVersion(),
35588
+ ci: isCi() || isTest()
35589
+ };
35590
+ for (const [key, raw] of Object.entries(opts.dimensions ?? {})) {
35591
+ if (!ALLOWED_DIMENSIONS.has(key) || raw === null || raw === void 0) {
35592
+ continue;
35593
+ }
35594
+ let value = raw;
35595
+ const allowed = DIMENSION_VALUES[key];
35596
+ if (allowed && !(typeof value === "string" && allowed.has(value))) {
35597
+ value = "other";
35598
+ } else if (STRING_DIMENSIONS.has(key)) {
35599
+ if (!(typeof value === "string" && MODEL_TOKEN_RE.test(value))) {
35600
+ continue;
35601
+ }
35602
+ } else if (BOOL_DIMENSIONS.has(key) && typeof value !== "boolean") {
35603
+ continue;
35604
+ }
35605
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
35606
+ event[key] = value;
35607
+ }
35608
+ }
35609
+ return event;
35610
+ }
35611
+
35612
+ // src/telemetry/client.ts
35613
+ var DEFAULT_ENDPOINT = "https://telemetry.getpatter.com/v1/ingest";
35614
+ var TIMEOUT_MS = 3e3;
35615
+ var BUFFER_MAX = 256;
35616
+ var noticeShown = false;
35617
+ var liveClients = /* @__PURE__ */ new Set();
35618
+ var exitHookRegistered = false;
35619
+ function showNoticeOnce() {
35620
+ if (noticeShown) return;
35621
+ noticeShown = true;
35622
+ getLogger().info(
35623
+ "Anonymous usage telemetry is on (no PII, no call content). Collected: a random anonymous install id, SDK version, language, OS family, runtime version, coarse feature flags, the composed stack (provider + model per layer), tool counts, integration category, and per-call duration, latency, cost, and error codes (no call content, no message text). Disable with PATTER_TELEMETRY_DISABLED=1, DO_NOT_TRACK=1, or telemetry: false. Details: https://docs.getpatter.com/telemetry"
35624
+ );
35625
+ }
35626
+ function registerExitHook() {
35627
+ if (exitHookRegistered) return;
35628
+ exitHookRegistered = true;
35629
+ process.once("beforeExit", () => {
35630
+ for (const ref of [...liveClients]) {
35631
+ const client = ref.deref();
35632
+ if (client) void client.close();
35633
+ else liveClients.delete(ref);
35634
+ }
35635
+ });
35636
+ }
35637
+ var TelemetryClient = class {
35638
+ sdkVersion;
35639
+ enabledFlag;
35640
+ endpoint;
35641
+ debug;
35642
+ buffer = [];
35643
+ flushing = false;
35644
+ closed = false;
35645
+ selfRef = new WeakRef(this);
35646
+ constructor(options) {
35647
+ this.sdkVersion = options.sdkVersion;
35648
+ this.enabledFlag = isEnabled(options.flag);
35649
+ this.endpoint = options.endpoint ?? process.env.PATTER_TELEMETRY_ENDPOINT ?? DEFAULT_ENDPOINT;
35650
+ this.debug = isTruthy(process.env.PATTER_TELEMETRY_DEBUG);
35651
+ if (this.enabledFlag && !this.debug) {
35652
+ showNoticeOnce();
35653
+ registerExitHook();
35654
+ liveClients.add(this.selfRef);
35655
+ }
35656
+ }
35657
+ get enabled() {
35658
+ return this.enabledFlag;
35659
+ }
35660
+ /** Enqueue an event. Fire-and-forget; never throws, never blocks. */
35661
+ record(name, dimensions) {
35662
+ if (!this.enabledFlag || this.closed) return;
35663
+ let event;
35664
+ try {
35665
+ event = buildEvent(name, { sdkVersion: this.sdkVersion, dimensions });
35666
+ } catch (err) {
35667
+ getLogger().debug("telemetry buildEvent failed", err);
35668
+ return;
35669
+ }
35670
+ if (this.debug) {
35671
+ try {
35672
+ process.stderr.write(`[patter telemetry] ${JSON.stringify(event)}
35673
+ `);
35674
+ } catch {
35675
+ }
35676
+ return;
35677
+ }
35678
+ try {
35679
+ if (this.buffer.length >= BUFFER_MAX) this.buffer.shift();
35680
+ this.buffer.push(event);
35681
+ this.scheduleFlush();
35682
+ } catch (err) {
35683
+ getLogger().debug("telemetry enqueue failed", err);
35684
+ }
35685
+ }
35686
+ /**
35687
+ * Schedule a flush of any buffered events. Events recorded before the server
35688
+ * is running (e.g. at `new Patter(...)`) sit in the buffer; call this once the
35689
+ * server is up so they ship promptly. Cheap when disabled or buffer is empty.
35690
+ */
35691
+ flushPending() {
35692
+ if (!this.enabledFlag || this.debug) return;
35693
+ try {
35694
+ this.scheduleFlush();
35695
+ } catch (err) {
35696
+ getLogger().debug("telemetry flushPending failed", err);
35697
+ }
35698
+ }
35699
+ /** Flush remaining events (graceful shutdown). Never throws. */
35700
+ async close() {
35701
+ if (this.closed) return;
35702
+ this.closed = true;
35703
+ liveClients.delete(this.selfRef);
35704
+ if (!this.enabledFlag || this.debug) return;
35705
+ try {
35706
+ await this.flush();
35707
+ } catch (err) {
35708
+ getLogger().debug("telemetry close flush failed", err);
35709
+ }
35710
+ }
35711
+ scheduleFlush() {
35712
+ if (this.flushing) return;
35713
+ this.flushing = true;
35714
+ void this.flush().finally(() => {
35715
+ this.flushing = false;
35716
+ });
35717
+ }
35718
+ async flush() {
35719
+ if (this.buffer.length === 0) return;
35720
+ const events = this.buffer.splice(0, this.buffer.length);
35721
+ const controller = new AbortController();
35722
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
35723
+ timer.unref?.();
35724
+ try {
35725
+ await fetch(this.endpoint, {
35726
+ method: "POST",
35727
+ headers: { "content-type": "application/json" },
35728
+ body: JSON.stringify(events),
35729
+ signal: controller.signal
35730
+ });
35731
+ } catch (err) {
35732
+ getLogger().debug("telemetry flush failed", err);
35733
+ } finally {
35734
+ clearTimeout(timer);
35735
+ }
35736
+ }
35737
+ };
35738
+
35739
+ // src/telemetry/environment.ts
35740
+ init_cjs_shims();
35741
+ var fs6 = __toESM(require("fs"));
35742
+ var env = process.env;
35743
+ function invokedByAgent() {
35744
+ if ("CLAUDECODE" in env || "CLAUDE_CODE" in env || "CLAUDE_CODE_ENTRYPOINT" in env)
35745
+ return "claude";
35746
+ if ("CURSOR_TRACE_ID" in env || "CURSOR_AGENT" in env) return "cursor";
35747
+ if ("GITHUB_COPILOT_AGENT" in env || "COPILOT_AGENT_ID" in env) return "copilot";
35748
+ if ("GEMINI_CLI" in env || "GEMINI_AGENT" in env) return "gemini";
35749
+ if ("WINDSURF" in env || "WINDSURF_AGENT" in env) return "windsurf";
35750
+ if ("AIDER" in env || "OPENAI_AGENT" in env) return "other";
35751
+ return "none";
35752
+ }
35753
+ function inContainer() {
35754
+ try {
35755
+ if (fs6.existsSync("/.dockerenv")) return true;
35756
+ } catch {
35757
+ }
35758
+ if (env.KUBERNETES_SERVICE_HOST) return true;
35759
+ try {
35760
+ const blob = fs6.readFileSync("/proc/1/cgroup", "utf8");
35761
+ return blob.includes("docker") || blob.includes("containerd") || blob.includes("kubepods");
35762
+ } catch {
35763
+ return false;
35764
+ }
35765
+ }
35766
+ function serverless() {
35767
+ if (env.AWS_LAMBDA_FUNCTION_NAME) return "lambda";
35768
+ if (env.K_SERVICE) return "cloud_run";
35769
+ if (env.VERCEL) return "vercel";
35770
+ if (env.AZURE_FUNCTIONS_ENVIRONMENT || env.FUNCTIONS_WORKER_RUNTIME) return "azure_functions";
35771
+ return "none";
35772
+ }
35773
+ function cloud() {
35774
+ if (env.AWS_REGION || env.AWS_EXECUTION_ENV || env.AWS_LAMBDA_FUNCTION_NAME) return "aws";
35775
+ if (env.K_SERVICE || env.GOOGLE_CLOUD_PROJECT || env.GCP_PROJECT) return "gcp";
35776
+ if (env.WEBSITE_INSTANCE_ID || env.AZURE_FUNCTIONS_ENVIRONMENT) return "azure";
35777
+ if (env.FLY_APP_NAME) return "fly";
35778
+ return "none";
35779
+ }
35780
+ function packageManager() {
35781
+ const ua = env.npm_config_user_agent ?? "";
35782
+ if (ua.startsWith("pnpm")) return "pnpm";
35783
+ if (ua.startsWith("yarn")) return "yarn";
35784
+ if (ua.startsWith("bun")) return "bun";
35785
+ if (ua.startsWith("npm")) return "npm";
35786
+ return "none";
35787
+ }
35788
+
35789
+ // src/client.ts
35790
+ init_version();
35791
+
34579
35792
  // src/_speech-events.ts
34580
35793
  init_cjs_shims();
34581
35794
  init_logger();
@@ -34881,6 +36094,79 @@ function closeParkedConnections(slot) {
34881
36094
  }
34882
36095
  }
34883
36096
  }
36097
+ function carrierFamily2(carrier) {
36098
+ if (carrier instanceof Carrier2) return "twilio";
36099
+ if (carrier instanceof Carrier3) return "telnyx";
36100
+ if (carrier instanceof Carrier) return "plivo";
36101
+ return "none";
36102
+ }
36103
+ function telemetryEngineFamily(opts) {
36104
+ if (opts.engine) {
36105
+ return opts.engine.constructor.name.toLowerCase().includes("convai") ? "convai" : "realtime";
36106
+ }
36107
+ if (opts.provider === "elevenlabs_convai") return "convai";
36108
+ if (opts.provider === "pipeline") return "pipeline";
36109
+ if (opts.provider === "openai_realtime") return "realtime";
36110
+ if (opts.stt || opts.tts) return "pipeline";
36111
+ return "realtime";
36112
+ }
36113
+ function telemetryProviderFamily(family) {
36114
+ if (family === "realtime") return "openai";
36115
+ if (family === "convai") return "elevenlabs";
36116
+ return "other";
36117
+ }
36118
+ function telemetryBucketCustomTools(n) {
36119
+ if (n <= 0) return "0";
36120
+ if (n === 1) return "1";
36121
+ if (n <= 3) return "2_3";
36122
+ if (n <= 6) return "4_6";
36123
+ if (n <= 12) return "7_12";
36124
+ return "13_plus";
36125
+ }
36126
+ function telemetryBucketMcp(n) {
36127
+ if (n <= 0) return "0";
36128
+ if (n === 1) return "1";
36129
+ if (n <= 3) return "2_3";
36130
+ return "4_plus";
36131
+ }
36132
+ function telemetryIntegration(opts) {
36133
+ const nMcp = opts.mcpServers?.length ?? 0;
36134
+ if (nMcp > 0) {
36135
+ return { integration: "mcp", integrationKind: "mcp", mcpBucket: telemetryBucketMcp(nMcp) };
36136
+ }
36137
+ if (opts.consult) {
36138
+ let isOpenclaw = false;
36139
+ const oc = opts.consult.openaiCompatible;
36140
+ if (oc) {
36141
+ const model = oc.model ?? "";
36142
+ const baseUrl = oc.baseUrl ?? "";
36143
+ isOpenclaw = model.startsWith("openclaw/") || baseUrl.includes(":18789");
36144
+ }
36145
+ return {
36146
+ integration: isOpenclaw ? "openclaw" : "other",
36147
+ integrationKind: "consult",
36148
+ mcpBucket: "0"
36149
+ };
36150
+ }
36151
+ return { integration: "none", integrationKind: "none", mcpBucket: "0" };
36152
+ }
36153
+ function telemetryEnvironmentDims() {
36154
+ try {
36155
+ const dims = {
36156
+ invoked_by_agent: invokedByAgent(),
36157
+ container: inContainer(),
36158
+ serverless: serverless(),
36159
+ cloud: cloud(),
36160
+ package_manager: packageManager(),
36161
+ days_since_install_bucket: daysSinceInstallBucket()
36162
+ };
36163
+ const prev = previousVersion(VERSION);
36164
+ if (prev) dims.previous_sdk_version = prev;
36165
+ return dims;
36166
+ } catch {
36167
+ return {};
36168
+ }
36169
+ }
34884
36170
  var Patter = class {
34885
36171
  localConfig;
34886
36172
  embeddedServer = null;
@@ -34901,6 +36187,14 @@ var Patter = class {
34901
36187
  * ``Cannot use both tunnel: true and webhookUrl``.
34902
36188
  */
34903
36189
  tunnelOwnsWebhookUrl = false;
36190
+ /**
36191
+ * Anonymous usage telemetry (opt-out, default ON). Separate from
36192
+ * ``./observability`` (user-facing OTel). Fire-and-forget and fail-safe — it
36193
+ * can never block or break a call. See ``./telemetry``.
36194
+ */
36195
+ telemetry;
36196
+ telemetrySeenEngines = /* @__PURE__ */ new Set();
36197
+ telemetrySeenAgentShapes = /* @__PURE__ */ new Set();
34904
36198
  /**
34905
36199
  * Pre-rendered first-message TTS audio per outbound call_id. Populated
34906
36200
  * by :meth:`call` when ``agent.prewarmFirstMessage`` is true; consumed
@@ -35106,6 +36400,22 @@ var Patter = class {
35106
36400
  openaiKey: options.openaiKey,
35107
36401
  persistRoot: resolvePersistRoot(options.persist)
35108
36402
  };
36403
+ this.telemetry = new TelemetryClient({
36404
+ sdkVersion: VERSION,
36405
+ flag: options.telemetry
36406
+ });
36407
+ const initDims = {
36408
+ carrier: carrierFamily2(carrier),
36409
+ tunnel: tunnel instanceof Static ? "static" : options.tunnel ? "configured" : "none",
36410
+ ...telemetryEnvironmentDims()
36411
+ };
36412
+ if (this.telemetry.enabled) {
36413
+ try {
36414
+ if (isFirstRun()) this.telemetry.record("first_run", initDims);
36415
+ } catch {
36416
+ }
36417
+ }
36418
+ this.telemetry.record("sdk_initialized", initDims);
35109
36419
  this._tunnelReady = new Promise((resolve2, reject) => {
35110
36420
  this._tunnelReadyResolve = resolve2;
35111
36421
  this._tunnelReadyReject = reject;
@@ -35125,6 +36435,55 @@ var Patter = class {
35125
36435
  // === Agent definition ===
35126
36436
  /** Resolve user-supplied agent options against engine defaults and return the merged config. */
35127
36437
  agent(opts) {
36438
+ const family = telemetryEngineFamily(opts);
36439
+ const stack = stackDimensions(opts.stt, opts.tts, opts.llm);
36440
+ const featureKey = family + "|" + Object.entries(stack).sort().map(([k, v]) => `${k}=${v}`).join(",");
36441
+ if (!this.telemetrySeenEngines.has(featureKey)) {
36442
+ this.telemetrySeenEngines.add(featureKey);
36443
+ this.telemetry.record("feature_used", {
36444
+ engine: family,
36445
+ provider: telemetryProviderFamily(family),
36446
+ ...stack
36447
+ });
36448
+ }
36449
+ const builtin = opts.consult ? 1 : 0;
36450
+ const customBucket = telemetryBucketCustomTools(opts.tools?.length ?? 0);
36451
+ const { integration, integrationKind, mcpBucket } = telemetryIntegration(opts);
36452
+ const engineObj = opts.engine;
36453
+ const nr = opts.openaiRealtimeNoiseReduction ?? engineObj?.noiseReduction;
36454
+ const noiseReduction = nr === "near_field" || nr === "far_field" ? nr : "none";
36455
+ const td = opts.realtimeTurnDetection ?? engineObj?.turnDetection;
36456
+ const turnDetection = td != null ? "custom" : "none";
36457
+ const preamblesUsed = Boolean(opts.toolCallPreambles);
36458
+ const perToolTimeoutsSet = Array.isArray(opts.tools) && opts.tools.some((t) => t.timeoutMs !== void 0);
36459
+ const llmFallbackConfigured = typeof opts.llm?.getAvailability === "function";
36460
+ const shapeKey = [
36461
+ builtin,
36462
+ customBucket,
36463
+ integration,
36464
+ integrationKind,
36465
+ mcpBucket,
36466
+ noiseReduction,
36467
+ turnDetection,
36468
+ preamblesUsed,
36469
+ perToolTimeoutsSet,
36470
+ llmFallbackConfigured
36471
+ ].join("|");
36472
+ if (!this.telemetrySeenAgentShapes.has(shapeKey)) {
36473
+ this.telemetrySeenAgentShapes.add(shapeKey);
36474
+ this.telemetry.record("agent_configured", {
36475
+ builtin_tool_count: builtin,
36476
+ custom_tool_count_bucket: customBucket,
36477
+ integration,
36478
+ integration_kind: integrationKind,
36479
+ mcp_server_count_bucket: mcpBucket,
36480
+ noise_reduction: noiseReduction,
36481
+ turn_detection: turnDetection,
36482
+ preambles_used: preamblesUsed,
36483
+ per_tool_timeouts_set: perToolTimeoutsSet,
36484
+ llm_fallback_configured: llmFallbackConfigured
36485
+ });
36486
+ }
35128
36487
  let working = { ...opts };
35129
36488
  if (opts.engine) {
35130
36489
  if (opts.provider) {
@@ -35314,6 +36673,7 @@ var Patter = class {
35314
36673
  opts.dashboardToken ?? "",
35315
36674
  opts.allowInsecureDashboard ?? false
35316
36675
  );
36676
+ this.embeddedServer.telemetry = this.telemetry;
35317
36677
  this.embeddedServer.popPrewarmAudio = this.popPrewarmAudio;
35318
36678
  this.embeddedServer.popPrewarmedConnections = this.popPrewarmedConnections;
35319
36679
  this.embeddedServer.recordPrewarmWaste = this.recordPrewarmWaste;
@@ -35323,6 +36683,7 @@ var Patter = class {
35323
36683
  await waitForTunnelPubliclyReachable(webhookUrl);
35324
36684
  }
35325
36685
  this._readyResolve(webhookUrl);
36686
+ this.telemetry.flushPending();
35326
36687
  } catch (err) {
35327
36688
  const e = err instanceof Error ? err : new Error(String(err));
35328
36689
  this._readyReject(e);
@@ -36005,6 +37366,7 @@ var Patter = class {
36005
37366
  * entries leak across ``serve`` / ``disconnect`` cycles. See FIX #93.
36006
37367
  */
36007
37368
  async disconnect() {
37369
+ this.telemetry.flushPending();
36008
37370
  for (const handle of this.prewarmTtlTimers.values()) {
36009
37371
  clearTimeout(handle);
36010
37372
  }
@@ -36606,6 +37968,7 @@ var PatterTool = class {
36606
37968
  maxDurationSec;
36607
37969
  recording;
36608
37970
  started = false;
37971
+ hermesTelemetryEmitted = false;
36609
37972
  /** Cached in-progress (or completed) start promise so concurrent execute()
36610
37973
  * callers all await the same boot sequence instead of each racing into
36611
37974
  * phone.serve(). Reset to null on failure so callers can retry after a
@@ -36762,6 +38125,20 @@ var PatterTool = class {
36762
38125
  * the same wire contract.
36763
38126
  */
36764
38127
  hermesHandler() {
38128
+ if (!this.hermesTelemetryEmitted) {
38129
+ this.hermesTelemetryEmitted = true;
38130
+ try {
38131
+ const tel = this.phone.telemetry;
38132
+ tel?.record("agent_configured", {
38133
+ builtin_tool_count: 0,
38134
+ custom_tool_count_bucket: "0",
38135
+ integration: "hermes",
38136
+ integration_kind: "none",
38137
+ mcp_server_count_bucket: "0"
38138
+ });
38139
+ } catch {
38140
+ }
38141
+ }
36765
38142
  return async (args) => {
36766
38143
  try {
36767
38144
  const result = await this.execute(args);
@@ -41852,6 +43229,314 @@ var LLM5 = class extends GoogleLLMProvider {
41852
43229
  }
41853
43230
  };
41854
43231
 
43232
+ // src/llm/openai-compatible.ts
43233
+ init_cjs_shims();
43234
+ var import_node_crypto6 = require("crypto");
43235
+ init_llm_loop();
43236
+ init_errors();
43237
+ init_logger();
43238
+ init_version();
43239
+ var DEFAULT_TIMEOUT_S = 60;
43240
+ function hashCaller(caller) {
43241
+ if (!caller) return void 0;
43242
+ return (0, import_node_crypto6.createHash)("sha256").update(caller, "utf8").digest("hex").slice(0, 16);
43243
+ }
43244
+ var OpenAICompatibleLLMProvider = class {
43245
+ /**
43246
+ * Stable pricing/dashboard key — read by stream-handler/metrics. Typed as
43247
+ * ``string`` (not the narrowed literal) so the Hermes / OpenClaw presets can
43248
+ * override it with their own key while still extending this class.
43249
+ */
43250
+ static providerKey = "openai_compatible";
43251
+ /** Resolved bearer; undefined for keyless gateways. */
43252
+ apiKey;
43253
+ model;
43254
+ baseUrl;
43255
+ timeoutMs;
43256
+ extraHeaders;
43257
+ sessionUserPrefix;
43258
+ sessionIdHeader;
43259
+ sessionIdPrefix;
43260
+ sessionKeyHeader;
43261
+ sessionKey;
43262
+ sessionKeyFactory;
43263
+ temperature;
43264
+ maxTokens;
43265
+ responseFormat;
43266
+ parallelToolCalls;
43267
+ toolChoice;
43268
+ seed;
43269
+ topP;
43270
+ frequencyPenalty;
43271
+ presencePenalty;
43272
+ stop;
43273
+ constructor(options) {
43274
+ if (!options.baseUrl) {
43275
+ throw new Error(
43276
+ 'OpenAICompatibleLLMProvider requires a baseUrl (e.g. "http://127.0.0.1:11434/v1").'
43277
+ );
43278
+ }
43279
+ if (!options.model) {
43280
+ throw new Error("OpenAICompatibleLLMProvider requires a model.");
43281
+ }
43282
+ this.apiKey = options.apiKey ?? (options.apiKeyEnv ? process.env[options.apiKeyEnv] : void 0);
43283
+ this.model = options.model;
43284
+ this.baseUrl = options.baseUrl;
43285
+ this.timeoutMs = (options.timeout ?? DEFAULT_TIMEOUT_S) * 1e3;
43286
+ this.extraHeaders = options.extraHeaders;
43287
+ this.sessionUserPrefix = options.sessionUserPrefix;
43288
+ this.sessionIdHeader = options.sessionIdHeader;
43289
+ this.sessionIdPrefix = options.sessionIdPrefix;
43290
+ this.sessionKeyHeader = options.sessionKeyHeader;
43291
+ this.sessionKey = options.sessionKey;
43292
+ let sessionKeyFactory = options.sessionKeyFactory;
43293
+ if (!sessionKeyFactory && options.sessionKeyFrom === "caller_hash") {
43294
+ sessionKeyFactory = (ctx) => ctx.callerHash ? `patter-caller-${ctx.callerHash}` : void 0;
43295
+ } else if (options.sessionKeyFrom !== void 0 && options.sessionKeyFrom !== "caller_hash") {
43296
+ throw new Error(
43297
+ `sessionKeyFrom must be 'caller_hash' or undefined, got ${JSON.stringify(
43298
+ options.sessionKeyFrom
43299
+ )}`
43300
+ );
43301
+ }
43302
+ this.sessionKeyFactory = sessionKeyFactory;
43303
+ this.temperature = options.temperature;
43304
+ this.maxTokens = options.maxTokens;
43305
+ this.responseFormat = options.responseFormat;
43306
+ this.parallelToolCalls = options.parallelToolCalls;
43307
+ this.toolChoice = options.toolChoice;
43308
+ this.seed = options.seed;
43309
+ this.topP = options.topP;
43310
+ this.frequencyPenalty = options.frequencyPenalty;
43311
+ this.presencePenalty = options.presencePenalty;
43312
+ this.stop = options.stop;
43313
+ }
43314
+ /**
43315
+ * Assemble the request headers. ``User-Agent`` is set first so any
43316
+ * ``extraHeaders`` (and the per-call session headers) layer on top without
43317
+ * silently dropping the SDK attribution, and the ``Authorization`` header is
43318
+ * only added when a key is present (keyless gateways omit it).
43319
+ *
43320
+ * The two session headers are emitted INDEPENDENTLY, each gated on its own
43321
+ * config (decoupled from ``sessionUserPrefix`` and from each other):
43322
+ * - ``sessionIdHeader`` (+ ``callId``) → ``` `${sessionIdPrefix}${callId}` ```
43323
+ * - ``sessionKeyHeader`` (+ ``sessionKey``) → the static ``sessionKey`` value.
43324
+ * ``sessionKey`` is a credential-grade memory scope and is never logged.
43325
+ */
43326
+ buildHeaders(callId, caller, callee) {
43327
+ const headers = {
43328
+ "Content-Type": "application/json",
43329
+ "User-Agent": `getpatter/${VERSION}`,
43330
+ ...this.extraHeaders ?? {}
43331
+ };
43332
+ if (this.apiKey) {
43333
+ headers.Authorization = `Bearer ${this.apiKey}`;
43334
+ }
43335
+ if (this.sessionIdHeader && callId) {
43336
+ headers[this.sessionIdHeader] = `${this.sessionIdPrefix ?? ""}${callId}`;
43337
+ }
43338
+ if (this.sessionKeyHeader) {
43339
+ const sessionKeyValue = this.resolveSessionKey(callId, caller, callee);
43340
+ if (sessionKeyValue) {
43341
+ headers[this.sessionKeyHeader] = sessionKeyValue;
43342
+ }
43343
+ }
43344
+ return headers;
43345
+ }
43346
+ /**
43347
+ * Resolve the ``sessionKeyHeader`` VALUE for this call. When a
43348
+ * ``sessionKeyFactory`` is configured it is called with a
43349
+ * {@link SessionContext} (the raw ``caller`` plus its non-reversible
43350
+ * {@link hashCaller}) and its return value wins — a falsy return omits the
43351
+ * header. Otherwise the static ``sessionKey`` is used. Never logged.
43352
+ */
43353
+ resolveSessionKey(callId, caller, callee) {
43354
+ if (this.sessionKeyFactory) {
43355
+ const ctx = {
43356
+ callId,
43357
+ caller,
43358
+ callee,
43359
+ callerHash: hashCaller(caller)
43360
+ };
43361
+ return this.sessionKeyFactory(ctx);
43362
+ }
43363
+ return this.sessionKey;
43364
+ }
43365
+ /**
43366
+ * Pre-call DNS / TLS warmup for the configured endpoint. Best-effort:
43367
+ * 5 s timeout, all exceptions swallowed at debug level. The ``Authorization``
43368
+ * header is only sent when a key is present so the operator-grade bearer is
43369
+ * never echoed for keyless gateways (and the key is never logged).
43370
+ */
43371
+ async warmup() {
43372
+ try {
43373
+ const headers = {};
43374
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
43375
+ await fetch(`${this.baseUrl}/models`, {
43376
+ method: "GET",
43377
+ headers,
43378
+ signal: AbortSignal.timeout(5e3)
43379
+ });
43380
+ } catch (err) {
43381
+ getLogger().debug(
43382
+ `OpenAI-compatible LLM warmup failed (best-effort): ${String(err)}`
43383
+ );
43384
+ }
43385
+ }
43386
+ /**
43387
+ * Build the request body. Mirrors the base OpenAI provider's sampling-kwarg
43388
+ * assembly and additionally sets ``user`` for session continuity when
43389
+ * ``sessionUserPrefix`` is set AND a ``callId`` is available — so the default
43390
+ * (prefix unset) behaviour is byte-identical to the base provider.
43391
+ */
43392
+ buildBody(messages, tools, callId) {
43393
+ const body = {
43394
+ model: this.model,
43395
+ messages,
43396
+ stream: true,
43397
+ stream_options: { include_usage: true }
43398
+ };
43399
+ if (this.temperature !== void 0) body.temperature = this.temperature;
43400
+ if (this.maxTokens !== void 0) body.max_completion_tokens = this.maxTokens;
43401
+ if (this.responseFormat !== void 0) body.response_format = this.responseFormat;
43402
+ if (this.parallelToolCalls !== void 0) body.parallel_tool_calls = this.parallelToolCalls;
43403
+ if (this.toolChoice !== void 0) body.tool_choice = this.toolChoice;
43404
+ if (this.seed !== void 0) body.seed = this.seed;
43405
+ if (this.topP !== void 0) body.top_p = this.topP;
43406
+ if (this.frequencyPenalty !== void 0) body.frequency_penalty = this.frequencyPenalty;
43407
+ if (this.presencePenalty !== void 0) body.presence_penalty = this.presencePenalty;
43408
+ if (this.stop !== void 0) body.stop = this.stop;
43409
+ if (tools) body.tools = tools;
43410
+ if (this.sessionUserPrefix !== void 0 && callId) {
43411
+ body.user = `${this.sessionUserPrefix}${callId}`;
43412
+ }
43413
+ return body;
43414
+ }
43415
+ /** Stream Patter-format LLM chunks from the configured chat completions API. */
43416
+ async *stream(messages, tools, opts) {
43417
+ const callId = opts?.callId;
43418
+ const caller = opts?.caller;
43419
+ const callee = opts?.callee;
43420
+ const body = this.buildBody(messages, tools, callId);
43421
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
43422
+ method: "POST",
43423
+ headers: this.buildHeaders(callId, caller, callee),
43424
+ body: JSON.stringify(body),
43425
+ signal: mergeAbortSignals(opts?.signal, AbortSignal.timeout(this.timeoutMs))
43426
+ });
43427
+ if (!response.ok) {
43428
+ const errText = await response.text();
43429
+ getLogger().error(
43430
+ `OpenAI-compatible API error: ${response.status} ${errText}`
43431
+ );
43432
+ throw new PatterConnectionError(
43433
+ `LLM API returned ${response.status}: ${errText.slice(0, 200)}`
43434
+ );
43435
+ }
43436
+ yield* parseOpenAISseStream(response);
43437
+ }
43438
+ };
43439
+ var LLM6 = class extends OpenAICompatibleLLMProvider {
43440
+ static providerKey = "openai_compatible";
43441
+ };
43442
+
43443
+ // src/llm/custom.ts
43444
+ init_cjs_shims();
43445
+ var LLM7 = class extends OpenAICompatibleLLMProvider {
43446
+ /** Stable pricing/dashboard key — read by stream-handler/metrics. */
43447
+ static providerKey = "custom";
43448
+ };
43449
+
43450
+ // src/llm/hermes.ts
43451
+ init_cjs_shims();
43452
+ var BASE_URL = "http://127.0.0.1:8642/v1";
43453
+ var DEFAULT_MODEL5 = "hermes-agent";
43454
+ var API_KEY_ENV = "API_SERVER_KEY";
43455
+ var MODEL_ENV = "API_SERVER_MODEL_NAME";
43456
+ var SESSION_USER_PREFIX = "patter-call-";
43457
+ var SESSION_ID_HEADER = "X-Hermes-Session-Id";
43458
+ var SESSION_ID_PREFIX = "patter-call-";
43459
+ var SESSION_KEY_HEADER = "X-Hermes-Session-Key";
43460
+ var DEFAULT_TIMEOUT_S2 = 120;
43461
+ var LLM8 = class extends OpenAICompatibleLLMProvider {
43462
+ static providerKey = "hermes";
43463
+ constructor(opts = {}) {
43464
+ const model = opts.model ?? process.env[MODEL_ENV] ?? DEFAULT_MODEL5;
43465
+ const options = {
43466
+ apiKey: opts.apiKey,
43467
+ apiKeyEnv: API_KEY_ENV,
43468
+ baseUrl: opts.baseUrl ?? BASE_URL,
43469
+ model,
43470
+ timeout: opts.timeout ?? DEFAULT_TIMEOUT_S2,
43471
+ sessionUserPrefix: SESSION_USER_PREFIX,
43472
+ sessionIdHeader: SESSION_ID_HEADER,
43473
+ sessionIdPrefix: SESSION_ID_PREFIX,
43474
+ sessionKeyHeader: SESSION_KEY_HEADER,
43475
+ sessionKey: opts.sessionKey,
43476
+ sessionKeyFrom: opts.sessionKeyFrom,
43477
+ sessionKeyFactory: opts.sessionKeyFactory,
43478
+ extraHeaders: opts.extraHeaders,
43479
+ temperature: opts.temperature,
43480
+ maxTokens: opts.maxTokens,
43481
+ responseFormat: opts.responseFormat,
43482
+ parallelToolCalls: opts.parallelToolCalls,
43483
+ toolChoice: opts.toolChoice,
43484
+ seed: opts.seed,
43485
+ topP: opts.topP,
43486
+ frequencyPenalty: opts.frequencyPenalty,
43487
+ presencePenalty: opts.presencePenalty,
43488
+ stop: opts.stop
43489
+ };
43490
+ super(options);
43491
+ }
43492
+ };
43493
+
43494
+ // src/llm/openclaw.ts
43495
+ init_cjs_shims();
43496
+ var BASE_URL2 = "http://127.0.0.1:18789/v1";
43497
+ var API_KEY_ENV2 = "OPENCLAW_API_KEY";
43498
+ var SESSION_HEADER = "x-openclaw-session-key";
43499
+ var SESSION_USER_PREFIX2 = "patter-call-";
43500
+ var DEFAULT_TIMEOUT_S3 = 120;
43501
+ var OPENCLAW_AGENT_RE2 = /^[A-Za-z0-9._:/-]+$/;
43502
+ var LLM9 = class extends OpenAICompatibleLLMProvider {
43503
+ static providerKey = "openclaw";
43504
+ constructor(opts) {
43505
+ const agent = opts?.agent;
43506
+ if (!agent || !OPENCLAW_AGENT_RE2.test(agent)) {
43507
+ throw new Error(
43508
+ `Invalid OpenClaw agent id: ${JSON.stringify(agent)}. Allowed characters: letters, digits, dot, underscore, colon, slash, dash.`
43509
+ );
43510
+ }
43511
+ const model = agent.includes("/") || agent.includes(":") ? agent : `openclaw/${agent}`;
43512
+ const options = {
43513
+ apiKey: opts.apiKey,
43514
+ apiKeyEnv: API_KEY_ENV2,
43515
+ baseUrl: opts.baseUrl ?? BASE_URL2,
43516
+ model,
43517
+ timeout: opts.timeout ?? DEFAULT_TIMEOUT_S3,
43518
+ sessionUserPrefix: SESSION_USER_PREFIX2,
43519
+ // Wire-identical to the prior behaviour: header value is the raw call id
43520
+ // (empty prefix), and OpenClaw's gateway also derives the session from
43521
+ // the ``user`` field above. No separate memory-scope header.
43522
+ sessionIdHeader: SESSION_HEADER,
43523
+ sessionIdPrefix: "",
43524
+ extraHeaders: opts.extraHeaders,
43525
+ temperature: opts.temperature,
43526
+ maxTokens: opts.maxTokens,
43527
+ responseFormat: opts.responseFormat,
43528
+ parallelToolCalls: opts.parallelToolCalls,
43529
+ toolChoice: opts.toolChoice,
43530
+ seed: opts.seed,
43531
+ topP: opts.topP,
43532
+ frequencyPenalty: opts.frequencyPenalty,
43533
+ presencePenalty: opts.presencePenalty,
43534
+ stop: opts.stop
43535
+ };
43536
+ super(options);
43537
+ }
43538
+ };
43539
+
41855
43540
  // src/index.ts
41856
43541
  init_silero_vad();
41857
43542
 
@@ -42088,57 +43773,6 @@ var KrispVivaFilter = class {
42088
43773
  }
42089
43774
  };
42090
43775
 
42091
- // src/telephony/twilio.ts
42092
- init_cjs_shims();
42093
- var Carrier2 = class {
42094
- kind = "twilio";
42095
- accountSid;
42096
- authToken;
42097
- constructor(opts = {}) {
42098
- const sid = opts.accountSid ?? process.env.TWILIO_ACCOUNT_SID;
42099
- const tok = opts.authToken ?? process.env.TWILIO_AUTH_TOKEN;
42100
- if (!sid) {
42101
- throw new Error(
42102
- "Twilio carrier requires accountSid. Pass { accountSid: 'AC...' } or set TWILIO_ACCOUNT_SID in the environment."
42103
- );
42104
- }
42105
- if (!tok) {
42106
- throw new Error(
42107
- "Twilio carrier requires authToken. Pass { authToken: '...' } or set TWILIO_AUTH_TOKEN in the environment."
42108
- );
42109
- }
42110
- this.accountSid = sid;
42111
- this.authToken = tok;
42112
- }
42113
- };
42114
-
42115
- // src/telephony/telnyx.ts
42116
- init_cjs_shims();
42117
- var Carrier3 = class {
42118
- kind = "telnyx";
42119
- apiKey;
42120
- connectionId;
42121
- publicKey;
42122
- constructor(opts = {}) {
42123
- const key = opts.apiKey ?? process.env.TELNYX_API_KEY;
42124
- const conn = opts.connectionId ?? process.env.TELNYX_CONNECTION_ID;
42125
- const pub = opts.publicKey ?? process.env.TELNYX_PUBLIC_KEY;
42126
- if (!key) {
42127
- throw new Error(
42128
- "Telnyx carrier requires apiKey. Pass { apiKey: '...' } or set TELNYX_API_KEY in the environment."
42129
- );
42130
- }
42131
- if (!conn) {
42132
- throw new Error(
42133
- "Telnyx carrier requires connectionId. Pass { connectionId: '...' } or set TELNYX_CONNECTION_ID in the environment."
42134
- );
42135
- }
42136
- this.apiKey = key;
42137
- this.connectionId = conn;
42138
- this.publicKey = pub;
42139
- }
42140
- };
42141
-
42142
43776
  // src/index.ts
42143
43777
  init_plivo();
42144
43778
  init_openai_realtime_2();
@@ -42213,9 +43847,9 @@ init_tunnel();
42213
43847
 
42214
43848
  // src/chat-context.ts
42215
43849
  init_cjs_shims();
42216
- var import_node_crypto5 = require("crypto");
43850
+ var import_node_crypto7 = require("crypto");
42217
43851
  function generateId() {
42218
- return (0, import_node_crypto5.randomUUID)().replace(/-/g, "").slice(0, 12);
43852
+ return (0, import_node_crypto7.randomUUID)().replace(/-/g, "").slice(0, 12);
42219
43853
  }
42220
43854
  function createMessage(role, content, options) {
42221
43855
  return Object.freeze({
@@ -42864,8 +44498,8 @@ var TwilioAdapter = class _TwilioAdapter {
42864
44498
  this.baseUrl = opts.region ? `https://api.${opts.region}.twilio.com/2010-04-01` : TWILIO_API_BASE2;
42865
44499
  this.authHeader = `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString("base64")}`;
42866
44500
  }
42867
- async request(method, path6, body) {
42868
- const url2 = `${this.baseUrl}/Accounts/${encodeURIComponent(this.accountSid)}${path6}`;
44501
+ async request(method, path7, body) {
44502
+ const url2 = `${this.baseUrl}/Accounts/${encodeURIComponent(this.accountSid)}${path7}`;
42869
44503
  const headers = { Authorization: this.authHeader };
42870
44504
  if (body) headers["Content-Type"] = "application/x-www-form-urlencoded";
42871
44505
  const response = await fetch(url2, {
@@ -42876,7 +44510,7 @@ var TwilioAdapter = class _TwilioAdapter {
42876
44510
  });
42877
44511
  const text = await response.text();
42878
44512
  if (!response.ok) {
42879
- throw new Error(`Twilio ${method} ${path6} failed: ${response.status} ${text}`);
44513
+ throw new Error(`Twilio ${method} ${path7} failed: ${response.status} ${text}`);
42880
44514
  }
42881
44515
  if (!text) return {};
42882
44516
  try {
@@ -42894,8 +44528,8 @@ var TwilioAdapter = class _TwilioAdapter {
42894
44528
  const country = encodeURIComponent(opts.countryCode);
42895
44529
  const queryParts = ["PageSize=1"];
42896
44530
  if (opts.areaCode) queryParts.push(`AreaCode=${encodeURIComponent(opts.areaCode)}`);
42897
- const path6 = `/AvailablePhoneNumbers/${country}/Local.json?${queryParts.join("&")}`;
42898
- const available = await this.request("GET", path6);
44531
+ const path7 = `/AvailablePhoneNumbers/${country}/Local.json?${queryParts.join("&")}`;
44532
+ const available = await this.request("GET", path7);
42899
44533
  const first = available.available_phone_numbers?.[0]?.phone_number;
42900
44534
  if (!first) {
42901
44535
  throw new Error(`TwilioAdapter: no numbers available for country ${opts.countryCode}`);
@@ -42995,7 +44629,7 @@ var TwilioAdapter = class _TwilioAdapter {
42995
44629
 
42996
44630
  // src/providers/telnyx-adapter.ts
42997
44631
  init_cjs_shims();
42998
- var import_node_crypto6 = require("crypto");
44632
+ var import_node_crypto8 = require("crypto");
42999
44633
  init_logger();
43000
44634
  var TELNYX_API_BASE2 = "https://api.telnyx.com/v2";
43001
44635
  var TelnyxAdapter = class {
@@ -43007,8 +44641,8 @@ var TelnyxAdapter = class {
43007
44641
  this.apiKey = apiKey;
43008
44642
  this.connectionId = connectionId;
43009
44643
  }
43010
- async request(method, path6, body) {
43011
- const url2 = `${this.baseUrl}${path6}`;
44644
+ async request(method, path7, body) {
44645
+ const url2 = `${this.baseUrl}${path7}`;
43012
44646
  const headers = {
43013
44647
  Authorization: `Bearer ${this.apiKey}`
43014
44648
  };
@@ -43021,7 +44655,7 @@ var TelnyxAdapter = class {
43021
44655
  });
43022
44656
  const text = await response.text();
43023
44657
  if (!response.ok) {
43024
- throw new Error(`Telnyx ${method} ${path6} failed: ${response.status} ${text}`);
44658
+ throw new Error(`Telnyx ${method} ${path7} failed: ${response.status} ${text}`);
43025
44659
  }
43026
44660
  if (!text) return {};
43027
44661
  try {
@@ -43105,7 +44739,7 @@ var TelnyxAdapter = class {
43105
44739
  if (!callControlId) throw new Error("TelnyxAdapter: callControlId is required");
43106
44740
  const encoded = encodeURIComponent(callControlId);
43107
44741
  const body = {
43108
- command_id: opts.commandId ?? (0, import_node_crypto6.randomUUID)()
44742
+ command_id: opts.commandId ?? (0, import_node_crypto8.randomUUID)()
43109
44743
  };
43110
44744
  try {
43111
44745
  await this.request(
@@ -43386,6 +45020,12 @@ var TelnyxTTS = class {
43386
45020
  init_cjs_shims();
43387
45021
  init_tracing();
43388
45022
  init_event_bus();
45023
+
45024
+ // src/index.ts
45025
+ var hermes = Object.freeze({ LLM: LLM8 });
45026
+ var openclaw = Object.freeze({ LLM: LLM9 });
45027
+ var openaiCompatible = Object.freeze({ LLM: LLM6 });
45028
+ var custom2 = Object.freeze({ LLM: LLM7 });
43389
45029
  // Annotate the CommonJS export names for ESM import in node:
43390
45030
  0 && (module.exports = {
43391
45031
  AllProvidersFailedError,
@@ -43402,6 +45042,7 @@ init_event_bus();
43402
45042
  CerebrasLLM,
43403
45043
  ChatContext,
43404
45044
  CloudflareTunnel,
45045
+ CustomLLM,
43405
45046
  DEFAULT_MIN_SENTENCE_LEN,
43406
45047
  DEFAULT_PRICING,
43407
45048
  DTMF_EVENTS,
@@ -43425,6 +45066,7 @@ init_event_bus();
43425
45066
  GoogleLLM,
43426
45067
  GroqLLM,
43427
45068
  Guardrail,
45069
+ HermesLLM,
43428
45070
  IVRActivity,
43429
45071
  InworldTTS,
43430
45072
  KrispFrameDuration,
@@ -43435,6 +45077,8 @@ init_event_bus();
43435
45077
  MetricsStore,
43436
45078
  MinWordsStrategy,
43437
45079
  Ngrok,
45080
+ OpenAICompatibleLLM,
45081
+ OpenAICompatibleLLMProvider,
43438
45082
  OpenAILLM,
43439
45083
  OpenAILLMProvider,
43440
45084
  OpenAIRealtime,
@@ -43448,6 +45092,7 @@ init_event_bus();
43448
45092
  OpenAITranscribeSTT,
43449
45093
  OpenAITranscriptionModel,
43450
45094
  OpenAIVoice,
45095
+ OpenClawLLM,
43451
45096
  PRICING_LAST_UPDATED,
43452
45097
  PRICING_VERSION,
43453
45098
  PartialStreamError,
@@ -43516,6 +45161,7 @@ init_event_bus();
43516
45161
  createResampler24kTo16k,
43517
45162
  createResampler24kTo8k,
43518
45163
  createResampler8kTo16k,
45164
+ custom,
43519
45165
  deepgram,
43520
45166
  defineTool,
43521
45167
  elevenlabs,
@@ -43527,6 +45173,8 @@ init_event_bus();
43527
45173
  geminiLive,
43528
45174
  getLogger,
43529
45175
  guardrail,
45176
+ hashCaller,
45177
+ hermes,
43530
45178
  initTracing,
43531
45179
  isRemoteUrl,
43532
45180
  isTracingEnabled,
@@ -43539,7 +45187,9 @@ init_event_bus();
43539
45187
  mountDashboard,
43540
45188
  mulawToPcm16,
43541
45189
  notifyDashboard,
45190
+ openaiCompatible,
43542
45191
  openaiTts,
45192
+ openclaw,
43543
45193
  openclawConsult,
43544
45194
  openclawPostCallNotifier,
43545
45195
  pcm16ToMulaw,