@niadra/sdk 0.9.0 → 0.10.3

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +40 -1
  3. package/dist/ai-sdk.d.cts +3 -3
  4. package/dist/ai-sdk.d.ts +3 -3
  5. package/dist/anthropic.d.cts +4 -4
  6. package/dist/anthropic.d.ts +4 -4
  7. package/dist/bedrock.d.cts +4 -4
  8. package/dist/bedrock.d.ts +4 -4
  9. package/dist/cli.js +368 -104
  10. package/dist/cli.js.map +1 -1
  11. package/dist/{client-B_ip8T2o.d.cts → client-rmWhZJOx.d.cts} +212 -21
  12. package/dist/{client-B_ip8T2o.d.ts → client-rmWhZJOx.d.ts} +212 -21
  13. package/dist/cloudflare-agents.d.cts +3 -3
  14. package/dist/cloudflare-agents.d.ts +3 -3
  15. package/dist/elevenlabs.cjs +7 -5
  16. package/dist/elevenlabs.cjs.map +1 -1
  17. package/dist/elevenlabs.d.cts +5 -5
  18. package/dist/elevenlabs.d.ts +5 -5
  19. package/dist/elevenlabs.js +7 -5
  20. package/dist/elevenlabs.js.map +1 -1
  21. package/dist/genkit.d.cts +3 -3
  22. package/dist/genkit.d.ts +3 -3
  23. package/dist/google-adk.cjs +7 -5
  24. package/dist/google-adk.cjs.map +1 -1
  25. package/dist/google-adk.d.cts +3 -3
  26. package/dist/google-adk.d.ts +3 -3
  27. package/dist/google-adk.js +7 -5
  28. package/dist/google-adk.js.map +1 -1
  29. package/dist/google-genai.d.cts +4 -4
  30. package/dist/google-genai.d.ts +4 -4
  31. package/dist/index.cjs +378 -106
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +13 -5
  34. package/dist/index.d.ts +13 -5
  35. package/dist/index.js +378 -106
  36. package/dist/index.js.map +1 -1
  37. package/dist/{intercept-CxmBLfoj.d.cts → intercept-C0XODsy9.d.cts} +1 -1
  38. package/dist/{intercept-CBaPK_-k.d.ts → intercept-veMDudKV.d.ts} +1 -1
  39. package/dist/langchain.cjs.map +1 -1
  40. package/dist/langchain.d.cts +3 -3
  41. package/dist/langchain.d.ts +3 -3
  42. package/dist/langchain.js.map +1 -1
  43. package/dist/livekit.cjs +10 -2
  44. package/dist/livekit.cjs.map +1 -1
  45. package/dist/livekit.d.cts +3 -3
  46. package/dist/livekit.d.ts +3 -3
  47. package/dist/livekit.js +10 -2
  48. package/dist/livekit.js.map +1 -1
  49. package/dist/llamaindex.d.cts +3 -3
  50. package/dist/llamaindex.d.ts +3 -3
  51. package/dist/mastra.d.cts +3 -3
  52. package/dist/mastra.d.ts +3 -3
  53. package/dist/openai-agents.d.cts +3 -3
  54. package/dist/openai-agents.d.ts +3 -3
  55. package/dist/retell.cjs +7 -5
  56. package/dist/retell.cjs.map +1 -1
  57. package/dist/retell.d.cts +5 -5
  58. package/dist/retell.d.ts +5 -5
  59. package/dist/retell.js +7 -5
  60. package/dist/retell.js.map +1 -1
  61. package/dist/{shared-BLkAbvuf.d.ts → shared-Cs7h5C4Y.d.ts} +1 -1
  62. package/dist/{shared-DEkF2Y_z.d.cts → shared-zaTBum17.d.cts} +1 -1
  63. package/dist/strands.d.cts +3 -3
  64. package/dist/strands.d.ts +3 -3
  65. package/dist/twilio.cjs +10 -2
  66. package/dist/twilio.cjs.map +1 -1
  67. package/dist/twilio.d.cts +4 -4
  68. package/dist/twilio.d.ts +4 -4
  69. package/dist/twilio.js +10 -2
  70. package/dist/twilio.js.map +1 -1
  71. package/dist/vapi.cjs +7 -5
  72. package/dist/vapi.cjs.map +1 -1
  73. package/dist/vapi.d.cts +5 -5
  74. package/dist/vapi.d.ts +5 -5
  75. package/dist/vapi.js +7 -5
  76. package/dist/vapi.js.map +1 -1
  77. package/dist/voltagent.d.cts +3 -3
  78. package/dist/voltagent.d.ts +3 -3
  79. package/dist/{webhook-BaraTaA9.d.ts → webhook-2V5QgnKI.d.ts} +1 -1
  80. package/dist/{webhook-CuR0Wckd.d.cts → webhook-CX7nvQig.d.cts} +1 -1
  81. package/dist/whatsapp.cjs +10 -2
  82. package/dist/whatsapp.cjs.map +1 -1
  83. package/dist/whatsapp.d.cts +2 -2
  84. package/dist/whatsapp.d.ts +2 -2
  85. package/dist/whatsapp.js +10 -2
  86. package/dist/whatsapp.js.map +1 -1
  87. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -4,6 +4,46 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // src/ids.ts
8
+ var lastMs = -1;
9
+ var counter = 0;
10
+ function randomBytes(length) {
11
+ const bytes = new Uint8Array(length);
12
+ const webCrypto = globalThis.crypto;
13
+ if (webCrypto?.getRandomValues) {
14
+ webCrypto.getRandomValues(bytes);
15
+ } else {
16
+ for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
17
+ }
18
+ return bytes;
19
+ }
20
+ function uuidv7(now = Date.now()) {
21
+ let ms = now;
22
+ if (ms <= lastMs) {
23
+ ms = lastMs;
24
+ counter = counter + 1 & 4095;
25
+ if (counter === 0) ms += 1;
26
+ } else {
27
+ counter = (randomBytes(2)[0] ?? 0) & 127;
28
+ }
29
+ lastMs = ms;
30
+ const bytes = randomBytes(16);
31
+ const high = Math.floor(ms / 2 ** 16);
32
+ const low = ms % 2 ** 16;
33
+ bytes[0] = high >>> 24 & 255;
34
+ bytes[1] = high >>> 16 & 255;
35
+ bytes[2] = high >>> 8 & 255;
36
+ bytes[3] = high & 255;
37
+ bytes[4] = low >>> 8 & 255;
38
+ bytes[5] = low & 255;
39
+ bytes[6] = 112 | counter >>> 8 & 15;
40
+ bytes[7] = counter & 255;
41
+ bytes[8] = 128 | (bytes[8] ?? 0) & 63;
42
+ let hex2 = "";
43
+ for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
44
+ return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
45
+ }
46
+
7
47
  // src/errors.ts
8
48
  var NiadraError = class extends Error {
9
49
  name = "NiadraError";
@@ -47,11 +87,12 @@ var NiadraAPIError = class extends NiadraError {
47
87
  constructor(status, problem, requestId, retryAfterMs = null) {
48
88
  const code = problem?.code ?? `http_${status}`;
49
89
  const detail = problem?.detail ? `: ${problem.detail}` : "";
50
- super(`${status} ${code}${detail}`);
90
+ const id = requestId ?? problem?.request_id ?? null;
91
+ super(`${status} ${code}${detail}${id ? ` (request ${id})` : ""}`);
51
92
  this.status = status;
52
93
  this.code = code;
53
94
  this.problem = problem;
54
- this.requestId = requestId ?? problem?.request_id ?? null;
95
+ this.requestId = id;
55
96
  this.retryAfterMs = retryAfterMs;
56
97
  }
57
98
  };
@@ -75,45 +116,11 @@ function toNiadraError(error) {
75
116
  const message = error instanceof Error ? error.message : String(error);
76
117
  return new NiadraError(message, { cause: error });
77
118
  }
78
-
79
- // src/ids.ts
80
- var lastMs = -1;
81
- var counter = 0;
82
- function randomBytes(length) {
83
- const bytes = new Uint8Array(length);
84
- const webCrypto = globalThis.crypto;
85
- if (webCrypto?.getRandomValues) {
86
- webCrypto.getRandomValues(bytes);
87
- } else {
88
- for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
89
- }
90
- return bytes;
91
- }
92
- function uuidv7(now = Date.now()) {
93
- let ms = now;
94
- if (ms <= lastMs) {
95
- ms = lastMs;
96
- counter = counter + 1 & 4095;
97
- if (counter === 0) ms += 1;
98
- } else {
99
- counter = (randomBytes(2)[0] ?? 0) & 127;
100
- }
101
- lastMs = ms;
102
- const bytes = randomBytes(16);
103
- const high = Math.floor(ms / 2 ** 16);
104
- const low = ms % 2 ** 16;
105
- bytes[0] = high >>> 24 & 255;
106
- bytes[1] = high >>> 16 & 255;
107
- bytes[2] = high >>> 8 & 255;
108
- bytes[3] = high & 255;
109
- bytes[4] = low >>> 8 & 255;
110
- bytes[5] = low & 255;
111
- bytes[6] = 112 | counter >>> 8 & 15;
112
- bytes[7] = counter & 255;
113
- bytes[8] = 128 | (bytes[8] ?? 0) & 63;
114
- let hex2 = "";
115
- for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
116
- return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
119
+ function explain(error) {
120
+ if (!(error instanceof NiadraAPIError)) return error instanceof Error ? error.name : String(error);
121
+ const detail = error.problem?.detail?.trim();
122
+ const said = `${error.status} ${error.code}${detail ? `: ${detail.slice(0, 300)}` : ""}`;
123
+ return error.requestId ? `${said} (request ${error.requestId})` : said;
117
124
  }
118
125
 
119
126
  // src/admin.ts
@@ -516,7 +523,7 @@ function internalRecord(span, ref, act) {
516
523
  }
517
524
 
518
525
  // src/version.ts
519
- var VERSION = "0.9.0";
526
+ var VERSION = "0.10.3";
520
527
 
521
528
  // src/transport.ts
522
529
  var RETRYABLE_WRITE_STATUS = /* @__PURE__ */ new Set([408, 421, 429, 500, 502, 503, 504]);
@@ -529,8 +536,32 @@ var Transport = class {
529
536
  }
530
537
  config;
531
538
  baseURL;
539
+ answeredAt;
540
+ /** When the allowance was first given since the last answer; it holds for the calls of that moment only. */
541
+ grantedAt;
542
+ /** When the client last sent a request of its own (`RequestSpec.activity`), in epoch milliseconds. */
543
+ lastActivityAt;
532
544
  async request(spec) {
533
- return spec.retry.kind === "read" ? this.read(spec, spec.retry) : this.write(spec, spec.retry);
545
+ if (spec.activity !== false) this.lastActivityAt = Date.now();
546
+ const budgeted = this.withAllowance(spec);
547
+ return budgeted.retry.kind === "read" ? this.read(budgeted, budgeted.retry) : this.write(budgeted, budgeted.retry);
548
+ }
549
+ /**
550
+ * `spec` with the allowance for opening a connection, when it has a budget and none is likely open. The calls
551
+ * that start while the first one opens it get it too; after that, none does until an answer comes, so an
552
+ * outage costs the allowance once, not on every turn.
553
+ */
554
+ withAllowance(spec) {
555
+ const allowance = this.config.coldAllowanceMs ?? 0;
556
+ const { retry } = spec;
557
+ if (allowance <= 0 || retry.kind === "write" && retry.totalMs === void 0) return spec;
558
+ const now = Date.now();
559
+ const keep = this.config.keepAliveMs ?? 0;
560
+ if (this.answeredAt !== void 0 && now - this.answeredAt <= keep) return spec;
561
+ this.grantedAt ??= now;
562
+ if (now - this.grantedAt > allowance) return spec;
563
+ const total = retry.kind === "write" && retry.totalMs !== void 0 ? { ...retry, totalMs: retry.totalMs + allowance } : retry;
564
+ return { ...spec, timeoutMs: spec.timeoutMs + allowance, retry: total };
534
565
  }
535
566
  async read(spec, policy) {
536
567
  const deadline = new Deadline(spec.timeoutMs, spec.signal);
@@ -601,7 +632,18 @@ var Transport = class {
601
632
  headers["content-type"] = "application/json";
602
633
  init.body = JSON.stringify(spec.body);
603
634
  }
604
- return this.exchange(this.url(spec.path, spec.query), init, deadline);
635
+ try {
636
+ const answer = await this.exchange(this.url(spec.path, spec.query), init, deadline);
637
+ this.answered();
638
+ return answer;
639
+ } catch (error) {
640
+ if (error instanceof NiadraAPIError) this.answered();
641
+ throw error;
642
+ }
643
+ }
644
+ answered() {
645
+ this.answeredAt = Date.now();
646
+ this.grantedAt = void 0;
605
647
  }
606
648
  async exchange(url, init, deadline) {
607
649
  let response;
@@ -610,6 +652,7 @@ var Transport = class {
610
652
  } catch (error) {
611
653
  throw deadline.explain(error);
612
654
  }
655
+ warnIfDeprecated(this.config.logger, init.method ?? "GET", url, response.headers);
613
656
  const requestId = response.headers.get("x-request-id");
614
657
  let payload;
615
658
  try {
@@ -679,6 +722,29 @@ var Deadline = class {
679
722
  return new NiadraConnectionError(`connection failed: ${message}`, { cause: error });
680
723
  }
681
724
  };
725
+ var VERSIONING_DOCS = "https://docs.niadra.com/en/security/api-versioning";
726
+ var DEPRECATION_LINK = /<([^>]*)>[^,]*;\s*rel="?deprecation"?/gi;
727
+ var deprecationsSeen = /* @__PURE__ */ new Set();
728
+ function warnIfDeprecated(logger, method, url, headers) {
729
+ const since = headers.get("deprecation");
730
+ if (since === null) return;
731
+ const links = [...(headers.get("link") ?? "").matchAll(DEPRECATION_LINK)].map((match) => match[1] ?? "");
732
+ const key2 = `${method} ${links.join(" ") || (url.split("?")[0] ?? url)}`;
733
+ if (deprecationsSeen.has(key2)) return;
734
+ deprecationsSeen.add(key2);
735
+ const sunset = headers.get("sunset");
736
+ logger.warn(
737
+ `the API deprecated a ${method} route this client calls, since ${deprecatedSince(since)}; it stops answering on ${sunset === null ? "a date not announced yet" : sunsetDay(sunset)}. See ${links.join(", ") || VERSIONING_DOCS}`
738
+ );
739
+ }
740
+ function deprecatedSince(value) {
741
+ const seconds = Number(value.trim().replace(/^@/, ""));
742
+ return Number.isInteger(seconds) ? new Date(seconds * 1e3).toISOString().slice(0, 10) : value;
743
+ }
744
+ function sunsetDay(value) {
745
+ const time = Date.parse(value);
746
+ return Number.isNaN(time) ? value : new Date(time).toISOString().slice(0, 10);
747
+ }
682
748
  async function readBody(response) {
683
749
  const text2 = await response.text();
684
750
  if (text2.length === 0) return null;
@@ -4018,8 +4084,7 @@ var TurnSender = class {
4018
4084
  return true;
4019
4085
  }
4020
4086
  }
4021
- const code = error instanceof NiadraAPIError ? error.code : toNiadraError(error).name;
4022
- this.recorder.rejected(batch.frames.length, [code]);
4087
+ this.recorder.rejected(batch.frames.length, [explain(error)]);
4023
4088
  return true;
4024
4089
  }
4025
4090
  accepted(batch, answer) {
@@ -4027,8 +4092,8 @@ var TurnSender = class {
4027
4092
  this.resumeAt = 0;
4028
4093
  this.recorder.sent(answer.accepted, answer.duplicates);
4029
4094
  const again = [];
4030
- const codes2 = /* @__PURE__ */ new Set();
4031
- let refused2 = 0;
4095
+ const reasons = /* @__PURE__ */ new Set();
4096
+ let refused3 = 0;
4032
4097
  for (const error of answer.errors ?? []) {
4033
4098
  const frame = batch.frames[error.index];
4034
4099
  if (frame === void 0) continue;
@@ -4037,12 +4102,12 @@ var TurnSender = class {
4037
4102
  this.recorder.modeRefused();
4038
4103
  again.push(frame);
4039
4104
  } else {
4040
- refused2++;
4041
- codes2.add(error.code);
4105
+ refused3++;
4106
+ reasons.add(error.detail ? `${error.code}: ${error.detail.slice(0, 300)}` : error.code);
4042
4107
  }
4043
4108
  }
4044
4109
  if (again.length > 0) this.queue.requeue(again);
4045
- if (refused2 > 0) this.recorder.rejected(refused2, [...codes2]);
4110
+ if (refused3 > 0) this.recorder.rejected(refused3, [...reasons]);
4046
4111
  }
4047
4112
  };
4048
4113
 
@@ -4164,9 +4229,12 @@ var TurnRecorder = class {
4164
4229
  this.accepted += accepted;
4165
4230
  this.duplicates += duplicates;
4166
4231
  }
4167
- rejected(count3, codes2) {
4232
+ /** `reasons`: the API's code and detail of each refusal (field paths and rules, never a value). */
4233
+ rejected(count3, reasons) {
4168
4234
  this.rejectedTurns += count3;
4169
- this.logger.warn(`${count3} turn records were refused (${[...codes2].sort().join(", ")})`);
4235
+ const shown = [...new Set(reasons)].sort();
4236
+ const more = shown.length > 3 ? ` (and ${shown.length - 3} more)` : "";
4237
+ this.logger.warn(`${count3} turn records were refused: ${shown.slice(0, 3).join("; ")}${more}`);
4170
4238
  }
4171
4239
  /** The space does not record turns: these are dropped, and with `off` (a 404) recording stops a while. */
4172
4240
  notRecorded(count3, off) {
@@ -4236,6 +4304,10 @@ async function hex(text2) {
4236
4304
  }
4237
4305
 
4238
4306
  // src/coordination/client.ts
4307
+ var REFUSED = /* @__PURE__ */ new Set([400, 401, 403, 422]);
4308
+ function refused(error) {
4309
+ return error instanceof NiadraAPIError && REFUSED.has(error.status);
4310
+ }
4239
4311
  var FAIL_CLOSED = /* @__PURE__ */ new Set(["marketing", "retention", "collection"]);
4240
4312
  var CHECK_BUDGET_MS = 200;
4241
4313
  function fallback(request, suppressed, failOpen) {
@@ -4257,15 +4329,22 @@ function claimed(data, error) {
4257
4329
  return { held: false, claim: null, error: code };
4258
4330
  }
4259
4331
  var Coordinator = class {
4260
- constructor(outbox, suppressions, declareNow) {
4332
+ constructor(outbox, suppressions, declareNow, logger) {
4261
4333
  this.outbox = outbox;
4262
4334
  this.suppressions = suppressions;
4263
4335
  this.declareNow = declareNow;
4336
+ this.logger = logger;
4264
4337
  }
4265
4338
  outbox;
4266
4339
  suppressions;
4267
4340
  declareNow;
4341
+ logger;
4268
4342
  async failed(request, failOpen, error) {
4343
+ if (refused(error)) {
4344
+ this.logger?.warn(`the coordination check was refused: ${explain(error)}`);
4345
+ const decision = request.direction === "inbound" ? "allow" : "defer";
4346
+ return recorded({ decision, decision_id: uuidv7(), reasons: ["invalid_request"], valid_for_s: 0 });
4347
+ }
4269
4348
  const off = error instanceof NiadraAPIError && error.status === 404;
4270
4349
  const suppressed = request.subject != null && request.direction === "outbound" ? !await this.suppressions.mayContact(request.subject, request.purpose, { channel: request.channel ?? null, failOpen: true }) : false;
4271
4350
  const plain2 = off ? { ...request, effect_key: null } : request;
@@ -4286,7 +4365,7 @@ var Coordinator = class {
4286
4365
  if (who.subject) body.subject = who.subject;
4287
4366
  if (who.object) body.object = who.object;
4288
4367
  const key2 = uuidv7();
4289
- this.outbox.put({ send: () => this.declareNow(body, key2) });
4368
+ this.outbox.put({ send: () => this.declareNow(body, key2), route: "POST /v1/coordination/declare" });
4290
4369
  return key2;
4291
4370
  }
4292
4371
  };
@@ -4393,26 +4472,42 @@ async function suppressionKey(salt, canonical2) {
4393
4472
  const key2 = await subtle.importKey("raw", saltBytes(salt), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
4394
4473
  return toBase64url(new Uint8Array(await subtle.sign("HMAC", key2, new TextEncoder().encode(canonical2))));
4395
4474
  }
4475
+ var AREA_CODES = new Set(
4476
+ "11 12 13 14 15 16 17 18 19 21 22 24 27 28 31 32 33 34 35 37 38 41 42 43 44 45 46 47 48 49 51 53 54 55 61 62 63 64 65 66 67 68 69 71 73 74 75 77 79 81 82 83 84 85 86 87 88 89 91 92 93 94 95 96 97 98 99".split(" ")
4477
+ );
4478
+ var FORMAT_CHARACTERS = /\p{Cf}/gu;
4396
4479
  function phone(value) {
4397
- const raw = value.trim().replace(SEPARATORS2, "");
4398
- const international = raw.startsWith("+") || raw.startsWith("00");
4480
+ let text2 = value.normalize("NFKC").replace(FORMAT_CHARACTERS, "").trim();
4481
+ if (text2.slice(0, 4).toLowerCase() === "tel:") text2 = text2.slice(4);
4482
+ const international = text2.startsWith("+") || text2.startsWith("00");
4483
+ if (international) text2 = text2.replaceAll("(0)", "");
4484
+ const raw = text2.replace(SEPARATORS2, "");
4399
4485
  let digits = raw.startsWith("+") ? raw.slice(1) : international ? raw.slice(2) : raw;
4400
4486
  if (!DIGITS2.test(digits)) throw new NiadraDestinationError("invalid_handle");
4401
- const national = digits.replace(/^0+/, "");
4402
- if (!international && (national.length === 10 || national.length === 11) && isArea(national.slice(0, 2))) {
4403
- digits = `55${national}`;
4487
+ if (!international) {
4488
+ const national = digits.replace(/^0+/, "");
4489
+ if (isBrazilianNational(national)) {
4490
+ digits = `55${national}`;
4491
+ } else if (digits.startsWith("0") && (national.length === 12 || national.length === 13)) {
4492
+ if (isBrazilianNational(national.slice(2))) digits = `55${national.slice(2)}`;
4493
+ }
4404
4494
  }
4405
4495
  if (digits.length < 8 || digits.length > 15 || digits.startsWith("0")) {
4406
4496
  throw new NiadraDestinationError("invalid_handle");
4407
4497
  }
4408
4498
  const rest = digits.slice(2);
4409
- if (digits.startsWith("55") && rest.length === 10 && isArea(rest.slice(0, 2)) && "6789".includes(rest.charAt(2))) {
4499
+ if (digits.startsWith("55") && rest.length === 10 && AREA_CODES.has(rest.slice(0, 2)) && "6789".includes(rest.charAt(2))) {
4410
4500
  digits = `55${rest.slice(0, 2)}9${rest.slice(2)}`;
4411
4501
  }
4502
+ if (digits.length === 13 && (digits.startsWith("521") || digits.startsWith("549"))) {
4503
+ digits = digits.slice(0, 2) + digits.slice(3);
4504
+ }
4412
4505
  return `phone:+${digits}`;
4413
4506
  }
4414
- function isArea(code) {
4415
- return /^[1-9]{2}$/.test(code);
4507
+ function isBrazilianNational(national) {
4508
+ if (!AREA_CODES.has(national.slice(0, 2))) return false;
4509
+ if (national.length === 10) return "23456789".includes(national.charAt(2));
4510
+ return national.length === 11 && national.charAt(2) === "9";
4416
4511
  }
4417
4512
  function saltBytes(salt) {
4418
4513
  const bytes = fromBase64url(salt);
@@ -4789,7 +4884,7 @@ var Outbox = class {
4789
4884
  return false;
4790
4885
  }
4791
4886
  this.writes.shift();
4792
- this.logger.warn(`a write was refused (${error instanceof NiadraAPIError ? error.code : String(error)})`);
4887
+ this.logger.warn(`${write.route ?? "a write"} was refused: ${explain(error)}`);
4793
4888
  this.settle(write, void 0, error);
4794
4889
  return true;
4795
4890
  }
@@ -5679,9 +5774,17 @@ var handles = {
5679
5774
  appUserId: (value, options) => build("app_user_id", value, void 0, options),
5680
5775
  /** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */
5681
5776
  systemId: (value, system, options) => build("system_id", value, system, options),
5682
- /** An HMAC of a national document number; `country` is its ISO 3166-1 alpha-2 code. */
5777
+ /**
5778
+ * A person's national document, such as a CPF: `govIdHmac("529.982.247-25", "BR")`. Send the number
5779
+ * itself, never a hash of it: the server checks its check digits, then keeps only a keyed hash with your
5780
+ * space's secret and an encrypted copy, and shows it masked. `country` is its ISO 3166-1 alpha-2 code.
5781
+ */
5683
5782
  govIdHmac: (value, country, options) => build("gov_id_hmac", value, country, options),
5684
- /** An HMAC of a company registry number. Identifies an organization. */
5783
+ /**
5784
+ * A company's registry number, such as a CNPJ: `orgRegistryHmac("11.222.333/0001-81", "BR")`. As with
5785
+ * `govIdHmac`, send the number itself; the server checks it and keeps only a keyed hash. Identifies an
5786
+ * organization.
5787
+ */
5685
5788
  orgRegistryHmac: (value, country) => build("org_registry_hmac", value, country, { subjectKind: "account" }),
5686
5789
  /** An e-mail domain, such as `acme.com`. Identifies an organization. */
5687
5790
  emailDomain: (domain) => build("email_domain", domain, void 0, { subjectKind: "account" }),
@@ -6355,6 +6458,7 @@ var AgentSession = class {
6355
6458
  try {
6356
6459
  return this.host.coordinator.decided(await this.host.check(request, options.timeoutMs ?? CHECK_BUDGET_MS), request, this.checked);
6357
6460
  } catch (error) {
6461
+ if (this.host.strict && refused(error)) throw error;
6358
6462
  return this.host.coordinator.failed(request, options.failOpen, error);
6359
6463
  }
6360
6464
  }
@@ -6966,6 +7070,7 @@ var SessionState = class {
6966
7070
  /** The guards of the last read: they hold the answers to the turn they were written for. */
6967
7071
  guards = /* @__PURE__ */ new Map();
6968
7072
  lastReport = null;
7073
+ unlinkedSaid = false;
6969
7074
  /** After the first pack, every read also asks what changed since. */
6970
7075
  get wantsDelta() {
6971
7076
  return this.etag !== null;
@@ -6994,6 +7099,19 @@ var SessionState = class {
6994
7099
  this.last = absorbed;
6995
7100
  return absorbed;
6996
7101
  }
7102
+ /**
7103
+ * Once per session: the read named an organization (`about`) with no active link to the subject, so it went
7104
+ * on with the subject's own memory, never the organization's.
7105
+ */
7106
+ sayUnlinked(result2, logger) {
7107
+ if (result2.response?.about_unlinked && !this.unlinkedSaid) {
7108
+ this.unlinkedSaid = true;
7109
+ logger.warn(
7110
+ "`about` names an organization with no active link to the subject; the context is the subject's own, without that organization's memory. Create the link to read it."
7111
+ );
7112
+ }
7113
+ return result2;
7114
+ }
6997
7115
  /** The last backing check of an agent's answer. */
6998
7116
  get lastBacking() {
6999
7117
  return this.lastReport;
@@ -7241,7 +7359,7 @@ var Conversation = class {
7241
7359
  * `query` picks this read's slots by other words than the turn; the pack is the pinned one.
7242
7360
  */
7243
7361
  async context(options = {}) {
7244
- const { query, turn, format, explain, include, ...requestOptions } = options;
7362
+ const { query, turn, format, explain: explain2, include, ...requestOptions } = options;
7245
7363
  const params = {
7246
7364
  subject: this.subject,
7247
7365
  view: this.view,
@@ -7250,7 +7368,7 @@ var Conversation = class {
7250
7368
  ...this.params.about ? { about: this.params.about } : {},
7251
7369
  ...this.params.target ? { target: this.params.target } : {},
7252
7370
  ...format === "json" ? { format } : {},
7253
- ...explain ? { explain } : {}
7371
+ ...explain2 ? { explain: explain2 } : {}
7254
7372
  };
7255
7373
  if (query) params.query = query;
7256
7374
  if (include?.length) params.include = include;
@@ -7258,7 +7376,7 @@ var Conversation = class {
7258
7376
  params.turn = turn === void 0 ? this.turnText : turn;
7259
7377
  const result2 = await this.client.context(params, requestOptions);
7260
7378
  this.features.observe(result2);
7261
- return this.state.observe(this.state.absorb(result2));
7379
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7262
7380
  }
7263
7381
  /**
7264
7382
  * Starts this conversation's first read now, in the background: call it when the call starts
@@ -7438,6 +7556,35 @@ function withHandles(base, extra) {
7438
7556
  return all;
7439
7557
  }
7440
7558
 
7559
+ // src/warm.ts
7560
+ var EVERY_MS = 1e5;
7561
+ var IDLE_MS = 9e4;
7562
+ var WARM_FOR_MS = 6e5;
7563
+ var KeepWarm = class {
7564
+ constructor(enabled) {
7565
+ this.enabled = enabled;
7566
+ }
7567
+ enabled;
7568
+ open = /* @__PURE__ */ new Map();
7569
+ openedAt = 0;
7570
+ /** Notes an open conversation or task. */
7571
+ add(scope, session, now) {
7572
+ if (!this.enabled || typeof WeakRef === "undefined") return;
7573
+ this.open.set(scope, new WeakRef(session));
7574
+ this.openedAt = now;
7575
+ }
7576
+ /** The conversation or task ended. */
7577
+ end(scope) {
7578
+ this.open.delete(scope);
7579
+ }
7580
+ step(now, lastActivityAt, idleMs = IDLE_MS, warmForMs = WARM_FOR_MS) {
7581
+ for (const [scope, ref] of this.open) if (ref.deref() === void 0) this.open.delete(scope);
7582
+ const used = Math.max(lastActivityAt ?? 0, this.openedAt);
7583
+ if (this.open.size === 0 || now - used > warmForMs) return "stop";
7584
+ return now - used >= idleMs ? "ping" : "wait";
7585
+ }
7586
+ };
7587
+
7441
7588
  // src/exit.ts
7442
7589
  var registered = /* @__PURE__ */ new Set();
7443
7590
  var installedOn = null;
@@ -7568,8 +7715,10 @@ var DEFAULT_TIMEOUTS = {
7568
7715
  write: 5e3,
7569
7716
  token: 2e3,
7570
7717
  upload: 6e4,
7571
- prefetch: 1e3
7718
+ prefetch: 1e3,
7719
+ connect: 1e3
7572
7720
  };
7721
+ var FETCH_KEEPALIVE_MS = 4e3;
7573
7722
  var DEFAULT_CACHE = {
7574
7723
  ttlMs: 1e4,
7575
7724
  staleWhileRevalidateMs: 10 * 6e4,
@@ -7676,7 +7825,7 @@ var Task = class {
7676
7825
  * Resolves with an empty result, never rejects, unless the client is strict.
7677
7826
  */
7678
7827
  async context(options = {}) {
7679
- const { query, format, explain, include, ...requestOptions } = options;
7828
+ const { query, format, explain: explain2, include, ...requestOptions } = options;
7680
7829
  const target = this.object ? { object: this.object } : this.params.subject ? { subject: this.params.subject } : {};
7681
7830
  const params = {
7682
7831
  ...target,
@@ -7686,18 +7835,18 @@ var Task = class {
7686
7835
  ...this.level ? { verification: this.level } : {},
7687
7836
  ...this.params.target ? { target: this.params.target } : {},
7688
7837
  ...format === "json" ? { format } : {},
7689
- ...explain ? { explain } : {},
7838
+ ...explain2 ? { explain: explain2 } : {},
7690
7839
  ...include?.length ? { include } : {}
7691
7840
  };
7692
7841
  if (query) {
7693
7842
  const answered = await this.client.context({ ...params, query }, requestOptions);
7694
7843
  this.features.observe(answered);
7695
- return this.state.observe(answered);
7844
+ return this.state.observe(this.state.sayUnlinked(answered, this.logger));
7696
7845
  }
7697
7846
  if (this.state.wantsDelta) params.delta = true;
7698
7847
  const result2 = await this.client.context(params, requestOptions);
7699
7848
  this.features.observe(result2);
7700
- return this.state.observe(this.state.absorb(result2));
7849
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7701
7850
  }
7702
7851
  /**
7703
7852
  * The agent's own working notes for this task's prompt, as `niadra.agentMemory()` with
@@ -7827,14 +7976,14 @@ var AGENT_MEMORY_TOOL_NAMES = {
7827
7976
  search: "search_agent_memory",
7828
7977
  remember: "remember"
7829
7978
  };
7830
- var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
7979
+ var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait", "system_event"];
7831
7980
  var NOTE_KINDS = ["procedure", "tool_note", "process_note", "pitfall"];
7832
7981
  var TOOL_DEFINITIONS = [
7833
7982
  {
7834
7983
  "type": "function",
7835
7984
  "function": {
7836
7985
  "name": "search_customer_history",
7837
- "description": "Search everything that already happened with this customer: past conversations, promises, agent actions, orders and invoices. Use it when the customer refers to something earlier or asks whether a problem happened before. Do not call it when the answer is already in the context block, including its 'Do hist\xF3rico' line, which already counts recurrences. Returns short items with date, channel and outcome, plus a recurrence count. Open one with open_history_item.",
7986
+ "description": "Search everything that already happened with this customer: past conversations, promises, agent actions, orders and invoices. Use it when the customer refers to something earlier or asks whether a problem happened before. Do not call it when the answer is already in the context block, including its 'Hist\xF3rico' line, which already counts recurrences. Returns short items with date, channel and outcome, plus a recurrence count. Open a conversation (`episode:`) or an object (`object:`) with open_history_item; the other items are complete as listed.",
7838
7987
  "parameters": {
7839
7988
  "type": "object",
7840
7989
  "properties": {
@@ -7878,7 +8027,8 @@ var TOOL_DEFINITIONS = [
7878
8027
  "open_item",
7879
8028
  "action",
7880
8029
  "object",
7881
- "trait"
8030
+ "trait",
8031
+ "system_event"
7882
8032
  ]
7883
8033
  }
7884
8034
  },
@@ -7905,7 +8055,7 @@ var TOOL_DEFINITIONS = [
7905
8055
  "type": "function",
7906
8056
  "function": {
7907
8057
  "name": "get_customer_timeline",
7908
- "description": "List this customer's conversations and agent actions in order, newest first, one line each. Use it to leaf through the history when you do not know what to search for. Pass next_cursor to continue. Prefer search_customer_history for a specific question.",
8058
+ "description": "List this customer's conversations, agent actions and system events in order, newest first, one line each. Use it to leaf through the history when you do not know what to search for; filters.item_kinds also lists the open items, facts, patterns or objects it names. Pass next_cursor to continue. Prefer search_customer_history for a specific question.",
7909
8059
  "parameters": {
7910
8060
  "type": "object",
7911
8061
  "properties": {
@@ -7953,7 +8103,8 @@ var TOOL_DEFINITIONS = [
7953
8103
  "open_item",
7954
8104
  "action",
7955
8105
  "object",
7956
- "trait"
8106
+ "trait",
8107
+ "system_event"
7957
8108
  ]
7958
8109
  }
7959
8110
  },
@@ -7972,7 +8123,7 @@ var TOOL_DEFINITIONS = [
7972
8123
  "type": "function",
7973
8124
  "function": {
7974
8125
  "name": "open_history_item",
7975
- "description": "Open one conversation or business object returned by search_customer_history or get_customer_timeline: what was asked, what was promised and by whom, the outcome and what memory came from it. Use it only after a search or timeline pointed to the item.",
8126
+ "description": "Open one conversation (`episode:`) or business object (`object:`) returned by search_customer_history or get_customer_timeline: what was asked, what was promised and by whom, the outcome and what memory came from it. Actions, events, facts and promises are complete as listed and do not open. Use it only after a search or timeline pointed to the item.",
7976
8127
  "parameters": {
7977
8128
  "type": "object",
7978
8129
  "properties": {
@@ -8343,6 +8494,13 @@ function compose(body, read) {
8343
8494
  const pack = body.pack ? { pack: { ...body.pack, slots: fetched?.pack?.slots ?? [] } } : {};
8344
8495
  return { ...body, slots: fetched?.slots ?? null, guards: fetched?.guards ?? [], ...pack };
8345
8496
  }
8497
+ var RTT_MARGIN_MS = 50;
8498
+ function budgetWarnings(rttMs, timeouts, explicit) {
8499
+ const ms = Math.round(rttMs);
8500
+ return [...explicit].sort().filter((name) => timeouts[name] < rttMs + RTT_MARGIN_MS).map(
8501
+ (name) => `timeouts.${name} (${timeouts[name]} ms) is shorter than the round trip to the region (${ms} ms) plus ${RTT_MARGIN_MS} ms for the API: its reads will run out of time. Leave it at its default, which adds the measured round trip, or raise it`
8502
+ );
8503
+ }
8346
8504
  function rttWarnings(rttMs, timeouts) {
8347
8505
  const ms = Math.round(rttMs);
8348
8506
  const found2 = [];
@@ -8380,6 +8538,12 @@ var Niadra = class _Niadra {
8380
8538
  enabled;
8381
8539
  core;
8382
8540
  timeouts;
8541
+ keepWarm;
8542
+ warmTimer;
8543
+ /** The read budgets the caller left at their defaults: they take the measured round trip on top. */
8544
+ defaultReads;
8545
+ voiceStarted = false;
8546
+ voiceWarned = false;
8383
8547
  strict;
8384
8548
  /** Where the client reports what it swallows in fail-open mode. */
8385
8549
  logger;
@@ -8424,6 +8588,8 @@ var Niadra = class _Niadra {
8424
8588
  this.strict = options.strict ?? false;
8425
8589
  this.logger = options.logger ?? consoleLogger;
8426
8590
  this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
8591
+ this.keepWarm = new KeepWarm(options.keepWarm ?? true);
8592
+ this.defaultReads = new Set(["context", "navigation"].filter((name) => options.timeouts?.[name] === void 0));
8427
8593
  this.voice = new VoiceLines(
8428
8594
  options.voice === false ? { ...DEFAULT_VOICE, enabled: false } : { ...DEFAULT_VOICE, ...options.voice }
8429
8595
  );
@@ -8449,13 +8615,14 @@ var Niadra = class _Niadra {
8449
8615
  this.coordinator = new Coordinator(
8450
8616
  this.outbox,
8451
8617
  this.suppressions,
8452
- (body, key2) => this.api.declare(body, { idempotency_key: key2 })
8618
+ (body, key2) => this.api.declare(body, { idempotency_key: key2 }),
8619
+ this.logger
8453
8620
  );
8454
8621
  this.states = new AgentStates(
8455
8622
  this.outbox,
8456
8623
  {
8457
- read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.timeouts.navigation }),
8458
- write: (write) => this.api.writeAgentState(write, { timeout: this.timeouts.navigation })
8624
+ read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.readBudget("navigation") }),
8625
+ write: (write) => this.api.writeAgentState(write, { timeout: this.readBudget("navigation") })
8459
8626
  },
8460
8627
  this.logger
8461
8628
  );
@@ -8482,6 +8649,7 @@ var Niadra = class _Niadra {
8482
8649
  this.core = setup;
8483
8650
  this.enabled = true;
8484
8651
  this.disabledReason = null;
8652
+ this.probe(setup);
8485
8653
  if (options.flushOnExit ?? true) this.unregisterExit = registerExitFlush(this);
8486
8654
  }
8487
8655
  setup(options) {
@@ -8503,7 +8671,10 @@ var Niadra = class _Niadra {
8503
8671
  baseURL,
8504
8672
  apiKey,
8505
8673
  fetch: fetchImpl,
8506
- defaultHeaders: options.defaultHeaders ?? {}
8674
+ defaultHeaders: options.defaultHeaders ?? {},
8675
+ logger: this.logger,
8676
+ coldAllowanceMs: this.timeouts.connect,
8677
+ keepAliveMs: options.keepAliveMs ?? FETCH_KEEPALIVE_MS
8507
8678
  });
8508
8679
  const queueOptions = { ...DEFAULT_QUEUE, ...options.queue };
8509
8680
  queueOptions.maxBatchSize = Math.min(queueOptions.maxBatchSize, 499);
@@ -8559,7 +8730,7 @@ var Niadra = class _Niadra {
8559
8730
  return result2;
8560
8731
  }
8561
8732
  readContext(core, params, request, options) {
8562
- const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
8733
+ const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8563
8734
  const { query: own3, ...pinned2 } = request;
8564
8735
  const query = turnText(own3 ?? params.turn);
8565
8736
  const voiceCache = this.voiceCache(core, pinned2, options.cache);
@@ -8575,7 +8746,7 @@ var Niadra = class _Niadra {
8575
8746
  */
8576
8747
  async profile() {
8577
8748
  if (!this.core) return null;
8578
- return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.timeouts.navigation }));
8749
+ return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.readBudget("navigation") }));
8579
8750
  }
8580
8751
  /**
8581
8752
  * Checks outputs against this claim contract instead of the one the profile serves (a company's own copy,
@@ -8600,7 +8771,7 @@ var Niadra = class _Niadra {
8600
8771
  */
8601
8772
  async mayContact(handle, purpose, options = {}) {
8602
8773
  if (this.core && this.suppressions.due()) {
8603
- const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.timeouts.navigation);
8774
+ const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.readBudget("navigation"));
8604
8775
  if (!this.suppressions.held) await read;
8605
8776
  }
8606
8777
  const checkOptions = { channel: options.channel ?? null };
@@ -8610,8 +8781,8 @@ var Niadra = class _Niadra {
8610
8781
  readSuppressions(budgetMs) {
8611
8782
  return this.suppressions.read(
8612
8783
  {
8613
- salt: () => this.api.suppressionSalt({ timeout: this.timeouts.navigation }),
8614
- page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.timeouts.navigation })
8784
+ salt: () => this.api.suppressionSalt({ timeout: this.readBudget("navigation") }),
8785
+ page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.readBudget("navigation") })
8615
8786
  },
8616
8787
  budgetMs
8617
8788
  );
@@ -8690,6 +8861,7 @@ var Niadra = class _Niadra {
8690
8861
  }
8691
8862
  /** What a conversation or a task needs of its client for the agent features. */
8692
8863
  get agentHost() {
8864
+ const client = this;
8693
8865
  return {
8694
8866
  recorder: this.turns,
8695
8867
  coordinator: this.coordinator,
@@ -8703,7 +8875,10 @@ var Niadra = class _Niadra {
8703
8875
  claim: (request, timeoutMs) => this.api.claim(request, {}, { timeout: timeoutMs }),
8704
8876
  verifyClaim: (ref, field, value, options) => this.verifyClaim(ref, field, value, options),
8705
8877
  enabled: this.enabled,
8706
- navigationMs: this.timeouts.navigation
8878
+ strict: this.strict,
8879
+ get navigationMs() {
8880
+ return client.readBudget("navigation");
8881
+ }
8707
8882
  };
8708
8883
  }
8709
8884
  /**
@@ -8731,7 +8906,7 @@ var Niadra = class _Niadra {
8731
8906
  if (!cache) return false;
8732
8907
  const key2 = cacheKey(request);
8733
8908
  const line = this.voice.line(cacheScope(request) ?? "");
8734
- this.probe(core);
8909
+ this.startVoice(core);
8735
8910
  line.request = request;
8736
8911
  if (!cache.has(key2) && line.inFlight().length === 0) {
8737
8912
  this.voiceRead(core, cache, line, key2, request, null, this.timeouts.contextVoiceStart);
@@ -8868,13 +9043,13 @@ var Niadra = class _Niadra {
8868
9043
  if (!params.query || params.query.length > 2e3) {
8869
9044
  throw new NiadraValidationError("query must be 1 to 2000 characters");
8870
9045
  }
8871
- return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
9046
+ return this.readSpec("POST", "/v1/history/search", params, this.readBudget("navigation"), options);
8872
9047
  });
8873
9048
  }
8874
9049
  /** The customer's history, newest first, one line per item, paginated by cursor. */
8875
9050
  async timeline(params, options = {}) {
8876
9051
  return this.navigate(
8877
- () => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
9052
+ () => this.readSpec("POST", "/v1/history/timeline", params, this.readBudget("navigation"), options)
8878
9053
  );
8879
9054
  }
8880
9055
  /**
@@ -8887,9 +9062,10 @@ var Niadra = class _Niadra {
8887
9062
  if (!id) throw new NiadraValidationError("open() needs an item id");
8888
9063
  const body = { item_id: id };
8889
9064
  if (params.subject) body.subject = params.subject;
9065
+ if (params.about) body.about = params.about;
8890
9066
  if (params.verification) body.verification = params.verification;
8891
9067
  if (params.conversation_id) body.conversation_id = params.conversation_id;
8892
- return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
9068
+ return this.readSpec("POST", "/v1/history/open", body, this.readBudget("navigation"), options);
8893
9069
  });
8894
9070
  }
8895
9071
  /**
@@ -8901,7 +9077,7 @@ var Niadra = class _Niadra {
8901
9077
  * const { data: invoice } = await niadra.objectState("invoice:erp:0823");
8902
9078
  */
8903
9079
  async objectState(object, options = {}) {
8904
- return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.timeouts.navigation, options));
9080
+ return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.readBudget("navigation"), options));
8905
9081
  }
8906
9082
  /**
8907
9083
  * System events and agent actions about one object, newest first, one line each and never
@@ -8914,7 +9090,7 @@ var Niadra = class _Niadra {
8914
9090
  throw new NiadraValidationError("limit must be between 1 and 100");
8915
9091
  }
8916
9092
  const path = `${objectPath(object)}/timeline`;
8917
- const spec = this.readSpec("GET", path, void 0, this.timeouts.navigation, options);
9093
+ const spec = this.readSpec("GET", path, void 0, this.readBudget("navigation"), options);
8918
9094
  spec.query = { cursor: params.cursor, limit: String(limit3) };
8919
9095
  return spec;
8920
9096
  });
@@ -8933,6 +9109,7 @@ var Niadra = class _Niadra {
8933
9109
  timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
8934
9110
  open: (id, customer, bound, voice) => {
8935
9111
  const scope = { subject: customer };
9112
+ if (bound.about) scope.about = bound.about;
8936
9113
  if (bound.verification) scope.verification = bound.verification;
8937
9114
  if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
8938
9115
  return this.open(id, scope, this.voiceBudget(voice));
@@ -8967,7 +9144,7 @@ var Niadra = class _Niadra {
8967
9144
  const key2 = AgentMemoryCache.key(params);
8968
9145
  const fresh = cache?.fresh(key2);
8969
9146
  if (fresh) return blockResult(fresh, "cache");
8970
- const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
9147
+ const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8971
9148
  const spec = this.readSpec("GET", "/v1/agent-memory/block", void 0, timeout, options);
8972
9149
  spec.query = {
8973
9150
  max_tokens: String(params.max_tokens ?? 300),
@@ -9004,7 +9181,7 @@ var Niadra = class _Niadra {
9004
9181
  if (params.limit !== void 0) body.limit = params.limit;
9005
9182
  if (params.conversation_id) body.conversation_id = params.conversation_id;
9006
9183
  if (params.task_id) body.task_id = params.task_id;
9007
- return this.readSpec("POST", "/v1/agent-memory/search", body, this.timeouts.navigation, options);
9184
+ return this.readSpec("POST", "/v1/agent-memory/search", body, this.readBudget("navigation"), options);
9008
9185
  });
9009
9186
  return result2.error ? result2 : { data: result2.data.notes, error: null };
9010
9187
  }
@@ -9098,6 +9275,41 @@ var Niadra = class _Niadra {
9098
9275
  return { ok: false, idempotency_key: key2, error: this.swallow(error, "feedback") };
9099
9276
  }
9100
9277
  }
9278
+ /**
9279
+ * How the space's agents used the context they read (`GET /v1/context-use`): sessions, deliveries, use,
9280
+ * repetition, transfers and recontact, with intervals, grouped by `group_by`. A key of an `analyst` source
9281
+ * with the `analytics` scope reads every source of the space; a key with `admin` reads its own source.
9282
+ */
9283
+ contextUse(params = {}, options = {}) {
9284
+ return this.navigate(() => {
9285
+ const { group_by: groups, ...filters } = params;
9286
+ const query = { ...filters };
9287
+ if (groups?.length) query.group_by = groups;
9288
+ return { ...this.readSpec("GET", "/v1/context-use", void 0, this.timeouts.write, options), query };
9289
+ });
9290
+ }
9291
+ /**
9292
+ * Links a person to the organization they act for (an account or a partner), as a system of record that
9293
+ * knows who works for whom: a CRM, an HR system. Needs a key with the `identity:link` scope (or `admin`);
9294
+ * `can_see_contacts` needs `admin`. Reads with `about` reach the organization through the link.
9295
+ */
9296
+ link(params, options = {}) {
9297
+ const { idempotency_key: key2, ...rest } = params;
9298
+ const body = { can_see_contacts: false, method: "system_import", ...rest };
9299
+ return this.navigate(() => this.writeSpec("/v1/identity/links", body, key2 ?? uuidv7(), options));
9300
+ }
9301
+ /**
9302
+ * Ends a link, from `valid_to` (now when absent): the person no longer acts for the organization, and reads
9303
+ * with `about` for the pair go on with the person's own memory. Needs `identity:link` or `admin`.
9304
+ */
9305
+ endLink(linkId, params = {}, options = {}) {
9306
+ return this.navigate(() => {
9307
+ if (!linkId) throw new NiadraValidationError("endLink() needs a link id");
9308
+ const body = params.valid_to ? { valid_to: params.valid_to } : {};
9309
+ const path = `/v1/identity/links/${encodeURIComponent(linkId)}/end`;
9310
+ return this.writeSpec(path, body, params.idempotency_key ?? uuidv7(), options);
9311
+ });
9312
+ }
9101
9313
  /**
9102
9314
  * Up to 500 corrections in one call, each with its own idempotency key (minted when missing).
9103
9315
  * Resolves with `accepted`, `duplicates` for replayed keys and one error per refused item, by index.
@@ -9190,16 +9402,20 @@ var Niadra = class _Niadra {
9190
9402
  * emits `conversation.ended` when you call `end()`.
9191
9403
  */
9192
9404
  conversation(params) {
9193
- return new Conversation(this, params, {
9405
+ const conversation = new Conversation(this, params, {
9194
9406
  endConversation: (id) => this.endScope(buildConversationEnded(id), `conversation:${id}`)
9195
9407
  });
9408
+ this.warm(`conversation:${conversation.id}`, conversation);
9409
+ return conversation;
9196
9410
  }
9197
9411
  /** A helper for one internal-agent task: binds `task_id` to reads and writes and emits `task.ended`. */
9198
9412
  task(params) {
9199
- return new Task(this, params, {
9413
+ const task = new Task(this, params, {
9200
9414
  endTask: (id) => this.endScope(buildTaskEnded(id), `task:${id}`),
9201
9415
  verifyTask: (verify) => this.verifyWith(verify)
9202
9416
  });
9417
+ this.warm(`task:${task.id}`, task);
9418
+ return task;
9203
9419
  }
9204
9420
  /**
9205
9421
  * Sends every queued event and resolves when done. Call it before a serverless function
@@ -9219,6 +9435,8 @@ var Niadra = class _Niadra {
9219
9435
  */
9220
9436
  async shutdown() {
9221
9437
  this.unregisterExit();
9438
+ clearInterval(this.warmTimer);
9439
+ this.warmTimer = void 0;
9222
9440
  if (!this.core) return;
9223
9441
  await this.turnSender?.stop(this.timeouts.write);
9224
9442
  await this.outbox.stop(this.timeouts.write);
@@ -9322,9 +9540,9 @@ var Niadra = class _Niadra {
9322
9540
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", request, timeout, { signal, headers }));
9323
9541
  return normalizeContext(response.data);
9324
9542
  } catch (error) {
9325
- const refused2 = error instanceof NiadraAPIError && error.status === 404;
9543
+ const refused3 = error instanceof NiadraAPIError && error.status === 404;
9326
9544
  const left = timeout - (Date.now() - started);
9327
- if (!request.include?.length || !refused2 || left <= 0) throw error;
9545
+ if (!request.include?.length || !refused3 || left <= 0) throw error;
9328
9546
  for (const name of request.include) this.refusedBlocks.set(name, Date.now() + BLOCK_RECHECK_AFTER_MS);
9329
9547
  const { include: _dropped, ...plain2 } = request;
9330
9548
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", plain2, left, { signal, headers }));
@@ -9412,9 +9630,32 @@ var Niadra = class _Niadra {
9412
9630
  const rtt = Math.min(...samples);
9413
9631
  this.voice.rtt = rtt;
9414
9632
  this.logger.debug(`round trip to the region ${Math.round(rtt)} ms`);
9415
- for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9633
+ const explicit = ["context", "navigation"].filter((name) => !this.defaultReads.has(name));
9634
+ for (const warning of budgetWarnings(rtt, this.timeouts, explicit)) this.logger.warn(warning);
9635
+ if (this.voiceStarted) this.warnVoice();
9416
9636
  })();
9417
9637
  }
9638
+ /**
9639
+ * A read budget: one the caller left at its default is what the API may take, and the measured round trip
9640
+ * to the region goes on top, so an agent far from the region (Sao Paulo, 170 ms from us-east-2) is not
9641
+ * timed out by the network; one the caller set is a ceiling.
9642
+ */
9643
+ readBudget(name) {
9644
+ const rtt = this.voice.rtt;
9645
+ return rtt !== null && this.defaultReads.has(name) ? this.timeouts[name] + rtt : this.timeouts[name];
9646
+ }
9647
+ /** The client reads in voice: the voice budgets' warnings matter from now on. */
9648
+ startVoice(core) {
9649
+ this.voiceStarted = true;
9650
+ this.probe(core);
9651
+ this.warnVoice();
9652
+ }
9653
+ warnVoice() {
9654
+ const rtt = this.voice.rtt;
9655
+ if (this.voiceWarned || rtt === null) return;
9656
+ this.voiceWarned = true;
9657
+ for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9658
+ }
9418
9659
  /**
9419
9660
  * A voice turn: the pinned body from memory, and the slots of the read of its words when that
9420
9661
  * read lands within `timeout`. See `voice.ts`.
@@ -9424,7 +9665,7 @@ var Niadra = class _Niadra {
9424
9665
  const key2 = cacheKey(request);
9425
9666
  const deadline = Date.now() + timeout;
9426
9667
  const line = this.voice.line(scope);
9427
- this.probe(core);
9668
+ this.startVoice(core);
9428
9669
  line.request = request;
9429
9670
  const words2 = wordsOf(query);
9430
9671
  const background = this.timeouts.prefetch;
@@ -9610,8 +9851,39 @@ var Niadra = class _Niadra {
9610
9851
  core.queue.flushInBackground(true);
9611
9852
  });
9612
9853
  }
9854
+ /** Notes an open conversation or task; the first one starts the keep-warm timer (`warm.ts`). */
9855
+ warm(scope, session) {
9856
+ const core = this.core;
9857
+ if (!core || !this.keepWarm.enabled) return;
9858
+ this.keepWarm.add(scope, session, Date.now());
9859
+ if (this.warmTimer !== void 0) return;
9860
+ const timer = setInterval(() => {
9861
+ this.warmTick(core);
9862
+ }, EVERY_MS);
9863
+ timer.unref?.();
9864
+ this.warmTimer = timer;
9865
+ }
9866
+ warmTick(core) {
9867
+ const step = this.keepWarm.step(Date.now(), core.transport.lastActivityAt);
9868
+ if (step === "stop") {
9869
+ clearInterval(this.warmTimer);
9870
+ this.warmTimer = void 0;
9871
+ return;
9872
+ }
9873
+ if (step !== "ping") return;
9874
+ core.transport.request({
9875
+ method: "GET",
9876
+ path: "/healthz",
9877
+ timeoutMs: 2e3,
9878
+ retry: { kind: "read", maxAttempts: 1 },
9879
+ activity: false
9880
+ }).catch((error) => {
9881
+ this.logger.debug(`keep-warm ping failed: ${describe(toNiadraError(error))}`);
9882
+ });
9883
+ }
9613
9884
  async endScope(item, scope) {
9614
9885
  this.forgetScope(scope);
9886
+ this.keepWarm.end(scope);
9615
9887
  return this.sendNow(() => item);
9616
9888
  }
9617
9889
  async sendBatch(transport, items2, maxAttempts, backoff) {
@@ -11466,7 +11738,7 @@ var Replayer = class {
11466
11738
  try {
11467
11739
  answer = await this.niadra.callRoute({ method: "POST", path: "/v1/scenario-runs", body, idempotencyKey: uuidv7() });
11468
11740
  } catch (error) {
11469
- if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused(scenarioIds, results);
11741
+ if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused2(scenarioIds, results);
11470
11742
  throw error;
11471
11743
  }
11472
11744
  let run = runOf(answer);
@@ -11569,7 +11841,7 @@ var Replayer = class {
11569
11841
  return result(scenarioId, turnId, caseId, run, "completed", { paraphrase: rephrased, assertions, divergent_calls: played.divergent, latency_ms: latency });
11570
11842
  }
11571
11843
  };
11572
- function refused(scenarioIds, results) {
11844
+ function refused2(scenarioIds, results) {
11573
11845
  const scenarios = scenarioIds.map((id) => {
11574
11846
  const mine = results.filter((r) => r.scenario_id === id);
11575
11847
  return {