@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.cjs CHANGED
@@ -6,6 +6,46 @@ var __export = (target, all) => {
6
6
  __defProp(target, name, { get: all[name], enumerable: true });
7
7
  };
8
8
 
9
+ // src/ids.ts
10
+ var lastMs = -1;
11
+ var counter = 0;
12
+ function randomBytes(length) {
13
+ const bytes = new Uint8Array(length);
14
+ const webCrypto = globalThis.crypto;
15
+ if (webCrypto?.getRandomValues) {
16
+ webCrypto.getRandomValues(bytes);
17
+ } else {
18
+ for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
19
+ }
20
+ return bytes;
21
+ }
22
+ function uuidv7(now = Date.now()) {
23
+ let ms = now;
24
+ if (ms <= lastMs) {
25
+ ms = lastMs;
26
+ counter = counter + 1 & 4095;
27
+ if (counter === 0) ms += 1;
28
+ } else {
29
+ counter = (randomBytes(2)[0] ?? 0) & 127;
30
+ }
31
+ lastMs = ms;
32
+ const bytes = randomBytes(16);
33
+ const high = Math.floor(ms / 2 ** 16);
34
+ const low = ms % 2 ** 16;
35
+ bytes[0] = high >>> 24 & 255;
36
+ bytes[1] = high >>> 16 & 255;
37
+ bytes[2] = high >>> 8 & 255;
38
+ bytes[3] = high & 255;
39
+ bytes[4] = low >>> 8 & 255;
40
+ bytes[5] = low & 255;
41
+ bytes[6] = 112 | counter >>> 8 & 15;
42
+ bytes[7] = counter & 255;
43
+ bytes[8] = 128 | (bytes[8] ?? 0) & 63;
44
+ let hex2 = "";
45
+ for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
46
+ return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
47
+ }
48
+
9
49
  // src/errors.ts
10
50
  var NiadraError = class extends Error {
11
51
  name = "NiadraError";
@@ -49,11 +89,12 @@ var NiadraAPIError = class extends NiadraError {
49
89
  constructor(status, problem, requestId, retryAfterMs = null) {
50
90
  const code = problem?.code ?? `http_${status}`;
51
91
  const detail = problem?.detail ? `: ${problem.detail}` : "";
52
- super(`${status} ${code}${detail}`);
92
+ const id = requestId ?? problem?.request_id ?? null;
93
+ super(`${status} ${code}${detail}${id ? ` (request ${id})` : ""}`);
53
94
  this.status = status;
54
95
  this.code = code;
55
96
  this.problem = problem;
56
- this.requestId = requestId ?? problem?.request_id ?? null;
97
+ this.requestId = id;
57
98
  this.retryAfterMs = retryAfterMs;
58
99
  }
59
100
  };
@@ -77,45 +118,11 @@ function toNiadraError(error) {
77
118
  const message = error instanceof Error ? error.message : String(error);
78
119
  return new NiadraError(message, { cause: error });
79
120
  }
80
-
81
- // src/ids.ts
82
- var lastMs = -1;
83
- var counter = 0;
84
- function randomBytes(length) {
85
- const bytes = new Uint8Array(length);
86
- const webCrypto = globalThis.crypto;
87
- if (webCrypto?.getRandomValues) {
88
- webCrypto.getRandomValues(bytes);
89
- } else {
90
- for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
91
- }
92
- return bytes;
93
- }
94
- function uuidv7(now = Date.now()) {
95
- let ms = now;
96
- if (ms <= lastMs) {
97
- ms = lastMs;
98
- counter = counter + 1 & 4095;
99
- if (counter === 0) ms += 1;
100
- } else {
101
- counter = (randomBytes(2)[0] ?? 0) & 127;
102
- }
103
- lastMs = ms;
104
- const bytes = randomBytes(16);
105
- const high = Math.floor(ms / 2 ** 16);
106
- const low = ms % 2 ** 16;
107
- bytes[0] = high >>> 24 & 255;
108
- bytes[1] = high >>> 16 & 255;
109
- bytes[2] = high >>> 8 & 255;
110
- bytes[3] = high & 255;
111
- bytes[4] = low >>> 8 & 255;
112
- bytes[5] = low & 255;
113
- bytes[6] = 112 | counter >>> 8 & 15;
114
- bytes[7] = counter & 255;
115
- bytes[8] = 128 | (bytes[8] ?? 0) & 63;
116
- let hex2 = "";
117
- for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
118
- return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
121
+ function explain(error) {
122
+ if (!(error instanceof NiadraAPIError)) return error instanceof Error ? error.name : String(error);
123
+ const detail = error.problem?.detail?.trim();
124
+ const said = `${error.status} ${error.code}${detail ? `: ${detail.slice(0, 300)}` : ""}`;
125
+ return error.requestId ? `${said} (request ${error.requestId})` : said;
119
126
  }
120
127
 
121
128
  // src/admin.ts
@@ -518,7 +525,7 @@ function internalRecord(span, ref, act) {
518
525
  }
519
526
 
520
527
  // src/version.ts
521
- var VERSION = "0.9.0";
528
+ var VERSION = "0.10.3";
522
529
 
523
530
  // src/transport.ts
524
531
  var RETRYABLE_WRITE_STATUS = /* @__PURE__ */ new Set([408, 421, 429, 500, 502, 503, 504]);
@@ -531,8 +538,32 @@ var Transport = class {
531
538
  }
532
539
  config;
533
540
  baseURL;
541
+ answeredAt;
542
+ /** When the allowance was first given since the last answer; it holds for the calls of that moment only. */
543
+ grantedAt;
544
+ /** When the client last sent a request of its own (`RequestSpec.activity`), in epoch milliseconds. */
545
+ lastActivityAt;
534
546
  async request(spec) {
535
- return spec.retry.kind === "read" ? this.read(spec, spec.retry) : this.write(spec, spec.retry);
547
+ if (spec.activity !== false) this.lastActivityAt = Date.now();
548
+ const budgeted = this.withAllowance(spec);
549
+ return budgeted.retry.kind === "read" ? this.read(budgeted, budgeted.retry) : this.write(budgeted, budgeted.retry);
550
+ }
551
+ /**
552
+ * `spec` with the allowance for opening a connection, when it has a budget and none is likely open. The calls
553
+ * that start while the first one opens it get it too; after that, none does until an answer comes, so an
554
+ * outage costs the allowance once, not on every turn.
555
+ */
556
+ withAllowance(spec) {
557
+ const allowance = this.config.coldAllowanceMs ?? 0;
558
+ const { retry } = spec;
559
+ if (allowance <= 0 || retry.kind === "write" && retry.totalMs === void 0) return spec;
560
+ const now = Date.now();
561
+ const keep = this.config.keepAliveMs ?? 0;
562
+ if (this.answeredAt !== void 0 && now - this.answeredAt <= keep) return spec;
563
+ this.grantedAt ??= now;
564
+ if (now - this.grantedAt > allowance) return spec;
565
+ const total = retry.kind === "write" && retry.totalMs !== void 0 ? { ...retry, totalMs: retry.totalMs + allowance } : retry;
566
+ return { ...spec, timeoutMs: spec.timeoutMs + allowance, retry: total };
536
567
  }
537
568
  async read(spec, policy) {
538
569
  const deadline = new Deadline(spec.timeoutMs, spec.signal);
@@ -603,7 +634,18 @@ var Transport = class {
603
634
  headers["content-type"] = "application/json";
604
635
  init.body = JSON.stringify(spec.body);
605
636
  }
606
- return this.exchange(this.url(spec.path, spec.query), init, deadline);
637
+ try {
638
+ const answer = await this.exchange(this.url(spec.path, spec.query), init, deadline);
639
+ this.answered();
640
+ return answer;
641
+ } catch (error) {
642
+ if (error instanceof NiadraAPIError) this.answered();
643
+ throw error;
644
+ }
645
+ }
646
+ answered() {
647
+ this.answeredAt = Date.now();
648
+ this.grantedAt = void 0;
607
649
  }
608
650
  async exchange(url, init, deadline) {
609
651
  let response;
@@ -612,6 +654,7 @@ var Transport = class {
612
654
  } catch (error) {
613
655
  throw deadline.explain(error);
614
656
  }
657
+ warnIfDeprecated(this.config.logger, init.method ?? "GET", url, response.headers);
615
658
  const requestId = response.headers.get("x-request-id");
616
659
  let payload;
617
660
  try {
@@ -681,6 +724,29 @@ var Deadline = class {
681
724
  return new NiadraConnectionError(`connection failed: ${message}`, { cause: error });
682
725
  }
683
726
  };
727
+ var VERSIONING_DOCS = "https://docs.niadra.com/en/security/api-versioning";
728
+ var DEPRECATION_LINK = /<([^>]*)>[^,]*;\s*rel="?deprecation"?/gi;
729
+ var deprecationsSeen = /* @__PURE__ */ new Set();
730
+ function warnIfDeprecated(logger, method, url, headers) {
731
+ const since = headers.get("deprecation");
732
+ if (since === null) return;
733
+ const links = [...(headers.get("link") ?? "").matchAll(DEPRECATION_LINK)].map((match) => match[1] ?? "");
734
+ const key2 = `${method} ${links.join(" ") || (url.split("?")[0] ?? url)}`;
735
+ if (deprecationsSeen.has(key2)) return;
736
+ deprecationsSeen.add(key2);
737
+ const sunset = headers.get("sunset");
738
+ logger.warn(
739
+ `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}`
740
+ );
741
+ }
742
+ function deprecatedSince(value) {
743
+ const seconds = Number(value.trim().replace(/^@/, ""));
744
+ return Number.isInteger(seconds) ? new Date(seconds * 1e3).toISOString().slice(0, 10) : value;
745
+ }
746
+ function sunsetDay(value) {
747
+ const time = Date.parse(value);
748
+ return Number.isNaN(time) ? value : new Date(time).toISOString().slice(0, 10);
749
+ }
684
750
  async function readBody(response) {
685
751
  const text2 = await response.text();
686
752
  if (text2.length === 0) return null;
@@ -4020,8 +4086,7 @@ var TurnSender = class {
4020
4086
  return true;
4021
4087
  }
4022
4088
  }
4023
- const code = error instanceof NiadraAPIError ? error.code : toNiadraError(error).name;
4024
- this.recorder.rejected(batch.frames.length, [code]);
4089
+ this.recorder.rejected(batch.frames.length, [explain(error)]);
4025
4090
  return true;
4026
4091
  }
4027
4092
  accepted(batch, answer) {
@@ -4029,8 +4094,8 @@ var TurnSender = class {
4029
4094
  this.resumeAt = 0;
4030
4095
  this.recorder.sent(answer.accepted, answer.duplicates);
4031
4096
  const again = [];
4032
- const codes2 = /* @__PURE__ */ new Set();
4033
- let refused2 = 0;
4097
+ const reasons = /* @__PURE__ */ new Set();
4098
+ let refused3 = 0;
4034
4099
  for (const error of answer.errors ?? []) {
4035
4100
  const frame = batch.frames[error.index];
4036
4101
  if (frame === void 0) continue;
@@ -4039,12 +4104,12 @@ var TurnSender = class {
4039
4104
  this.recorder.modeRefused();
4040
4105
  again.push(frame);
4041
4106
  } else {
4042
- refused2++;
4043
- codes2.add(error.code);
4107
+ refused3++;
4108
+ reasons.add(error.detail ? `${error.code}: ${error.detail.slice(0, 300)}` : error.code);
4044
4109
  }
4045
4110
  }
4046
4111
  if (again.length > 0) this.queue.requeue(again);
4047
- if (refused2 > 0) this.recorder.rejected(refused2, [...codes2]);
4112
+ if (refused3 > 0) this.recorder.rejected(refused3, [...reasons]);
4048
4113
  }
4049
4114
  };
4050
4115
 
@@ -4166,9 +4231,12 @@ var TurnRecorder = class {
4166
4231
  this.accepted += accepted;
4167
4232
  this.duplicates += duplicates;
4168
4233
  }
4169
- rejected(count3, codes2) {
4234
+ /** `reasons`: the API's code and detail of each refusal (field paths and rules, never a value). */
4235
+ rejected(count3, reasons) {
4170
4236
  this.rejectedTurns += count3;
4171
- this.logger.warn(`${count3} turn records were refused (${[...codes2].sort().join(", ")})`);
4237
+ const shown = [...new Set(reasons)].sort();
4238
+ const more = shown.length > 3 ? ` (and ${shown.length - 3} more)` : "";
4239
+ this.logger.warn(`${count3} turn records were refused: ${shown.slice(0, 3).join("; ")}${more}`);
4172
4240
  }
4173
4241
  /** The space does not record turns: these are dropped, and with `off` (a 404) recording stops a while. */
4174
4242
  notRecorded(count3, off) {
@@ -4238,6 +4306,10 @@ async function hex(text2) {
4238
4306
  }
4239
4307
 
4240
4308
  // src/coordination/client.ts
4309
+ var REFUSED = /* @__PURE__ */ new Set([400, 401, 403, 422]);
4310
+ function refused(error) {
4311
+ return error instanceof NiadraAPIError && REFUSED.has(error.status);
4312
+ }
4241
4313
  var FAIL_CLOSED = /* @__PURE__ */ new Set(["marketing", "retention", "collection"]);
4242
4314
  var CHECK_BUDGET_MS = 200;
4243
4315
  function fallback(request, suppressed, failOpen) {
@@ -4259,15 +4331,22 @@ function claimed(data, error) {
4259
4331
  return { held: false, claim: null, error: code };
4260
4332
  }
4261
4333
  var Coordinator = class {
4262
- constructor(outbox, suppressions, declareNow) {
4334
+ constructor(outbox, suppressions, declareNow, logger) {
4263
4335
  this.outbox = outbox;
4264
4336
  this.suppressions = suppressions;
4265
4337
  this.declareNow = declareNow;
4338
+ this.logger = logger;
4266
4339
  }
4267
4340
  outbox;
4268
4341
  suppressions;
4269
4342
  declareNow;
4343
+ logger;
4270
4344
  async failed(request, failOpen, error) {
4345
+ if (refused(error)) {
4346
+ this.logger?.warn(`the coordination check was refused: ${explain(error)}`);
4347
+ const decision = request.direction === "inbound" ? "allow" : "defer";
4348
+ return recorded({ decision, decision_id: uuidv7(), reasons: ["invalid_request"], valid_for_s: 0 });
4349
+ }
4271
4350
  const off = error instanceof NiadraAPIError && error.status === 404;
4272
4351
  const suppressed = request.subject != null && request.direction === "outbound" ? !await this.suppressions.mayContact(request.subject, request.purpose, { channel: request.channel ?? null, failOpen: true }) : false;
4273
4352
  const plain2 = off ? { ...request, effect_key: null } : request;
@@ -4288,7 +4367,7 @@ var Coordinator = class {
4288
4367
  if (who.subject) body.subject = who.subject;
4289
4368
  if (who.object) body.object = who.object;
4290
4369
  const key2 = uuidv7();
4291
- this.outbox.put({ send: () => this.declareNow(body, key2) });
4370
+ this.outbox.put({ send: () => this.declareNow(body, key2), route: "POST /v1/coordination/declare" });
4292
4371
  return key2;
4293
4372
  }
4294
4373
  };
@@ -4395,26 +4474,42 @@ async function suppressionKey(salt, canonical2) {
4395
4474
  const key2 = await subtle.importKey("raw", saltBytes(salt), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
4396
4475
  return toBase64url(new Uint8Array(await subtle.sign("HMAC", key2, new TextEncoder().encode(canonical2))));
4397
4476
  }
4477
+ var AREA_CODES = new Set(
4478
+ "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(" ")
4479
+ );
4480
+ var FORMAT_CHARACTERS = /\p{Cf}/gu;
4398
4481
  function phone(value) {
4399
- const raw = value.trim().replace(SEPARATORS2, "");
4400
- const international = raw.startsWith("+") || raw.startsWith("00");
4482
+ let text2 = value.normalize("NFKC").replace(FORMAT_CHARACTERS, "").trim();
4483
+ if (text2.slice(0, 4).toLowerCase() === "tel:") text2 = text2.slice(4);
4484
+ const international = text2.startsWith("+") || text2.startsWith("00");
4485
+ if (international) text2 = text2.replaceAll("(0)", "");
4486
+ const raw = text2.replace(SEPARATORS2, "");
4401
4487
  let digits = raw.startsWith("+") ? raw.slice(1) : international ? raw.slice(2) : raw;
4402
4488
  if (!DIGITS2.test(digits)) throw new NiadraDestinationError("invalid_handle");
4403
- const national = digits.replace(/^0+/, "");
4404
- if (!international && (national.length === 10 || national.length === 11) && isArea(national.slice(0, 2))) {
4405
- digits = `55${national}`;
4489
+ if (!international) {
4490
+ const national = digits.replace(/^0+/, "");
4491
+ if (isBrazilianNational(national)) {
4492
+ digits = `55${national}`;
4493
+ } else if (digits.startsWith("0") && (national.length === 12 || national.length === 13)) {
4494
+ if (isBrazilianNational(national.slice(2))) digits = `55${national.slice(2)}`;
4495
+ }
4406
4496
  }
4407
4497
  if (digits.length < 8 || digits.length > 15 || digits.startsWith("0")) {
4408
4498
  throw new NiadraDestinationError("invalid_handle");
4409
4499
  }
4410
4500
  const rest = digits.slice(2);
4411
- if (digits.startsWith("55") && rest.length === 10 && isArea(rest.slice(0, 2)) && "6789".includes(rest.charAt(2))) {
4501
+ if (digits.startsWith("55") && rest.length === 10 && AREA_CODES.has(rest.slice(0, 2)) && "6789".includes(rest.charAt(2))) {
4412
4502
  digits = `55${rest.slice(0, 2)}9${rest.slice(2)}`;
4413
4503
  }
4504
+ if (digits.length === 13 && (digits.startsWith("521") || digits.startsWith("549"))) {
4505
+ digits = digits.slice(0, 2) + digits.slice(3);
4506
+ }
4414
4507
  return `phone:+${digits}`;
4415
4508
  }
4416
- function isArea(code) {
4417
- return /^[1-9]{2}$/.test(code);
4509
+ function isBrazilianNational(national) {
4510
+ if (!AREA_CODES.has(national.slice(0, 2))) return false;
4511
+ if (national.length === 10) return "23456789".includes(national.charAt(2));
4512
+ return national.length === 11 && national.charAt(2) === "9";
4418
4513
  }
4419
4514
  function saltBytes(salt) {
4420
4515
  const bytes = fromBase64url(salt);
@@ -4791,7 +4886,7 @@ var Outbox = class {
4791
4886
  return false;
4792
4887
  }
4793
4888
  this.writes.shift();
4794
- this.logger.warn(`a write was refused (${error instanceof NiadraAPIError ? error.code : String(error)})`);
4889
+ this.logger.warn(`${write.route ?? "a write"} was refused: ${explain(error)}`);
4795
4890
  this.settle(write, void 0, error);
4796
4891
  return true;
4797
4892
  }
@@ -5681,9 +5776,17 @@ var handles = {
5681
5776
  appUserId: (value, options) => build("app_user_id", value, void 0, options),
5682
5777
  /** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */
5683
5778
  systemId: (value, system, options) => build("system_id", value, system, options),
5684
- /** An HMAC of a national document number; `country` is its ISO 3166-1 alpha-2 code. */
5779
+ /**
5780
+ * A person's national document, such as a CPF: `govIdHmac("529.982.247-25", "BR")`. Send the number
5781
+ * itself, never a hash of it: the server checks its check digits, then keeps only a keyed hash with your
5782
+ * space's secret and an encrypted copy, and shows it masked. `country` is its ISO 3166-1 alpha-2 code.
5783
+ */
5685
5784
  govIdHmac: (value, country, options) => build("gov_id_hmac", value, country, options),
5686
- /** An HMAC of a company registry number. Identifies an organization. */
5785
+ /**
5786
+ * A company's registry number, such as a CNPJ: `orgRegistryHmac("11.222.333/0001-81", "BR")`. As with
5787
+ * `govIdHmac`, send the number itself; the server checks it and keeps only a keyed hash. Identifies an
5788
+ * organization.
5789
+ */
5687
5790
  orgRegistryHmac: (value, country) => build("org_registry_hmac", value, country, { subjectKind: "account" }),
5688
5791
  /** An e-mail domain, such as `acme.com`. Identifies an organization. */
5689
5792
  emailDomain: (domain) => build("email_domain", domain, void 0, { subjectKind: "account" }),
@@ -6357,6 +6460,7 @@ var AgentSession = class {
6357
6460
  try {
6358
6461
  return this.host.coordinator.decided(await this.host.check(request, options.timeoutMs ?? CHECK_BUDGET_MS), request, this.checked);
6359
6462
  } catch (error) {
6463
+ if (this.host.strict && refused(error)) throw error;
6360
6464
  return this.host.coordinator.failed(request, options.failOpen, error);
6361
6465
  }
6362
6466
  }
@@ -6968,6 +7072,7 @@ var SessionState = class {
6968
7072
  /** The guards of the last read: they hold the answers to the turn they were written for. */
6969
7073
  guards = /* @__PURE__ */ new Map();
6970
7074
  lastReport = null;
7075
+ unlinkedSaid = false;
6971
7076
  /** After the first pack, every read also asks what changed since. */
6972
7077
  get wantsDelta() {
6973
7078
  return this.etag !== null;
@@ -6996,6 +7101,19 @@ var SessionState = class {
6996
7101
  this.last = absorbed;
6997
7102
  return absorbed;
6998
7103
  }
7104
+ /**
7105
+ * Once per session: the read named an organization (`about`) with no active link to the subject, so it went
7106
+ * on with the subject's own memory, never the organization's.
7107
+ */
7108
+ sayUnlinked(result2, logger) {
7109
+ if (result2.response?.about_unlinked && !this.unlinkedSaid) {
7110
+ this.unlinkedSaid = true;
7111
+ logger.warn(
7112
+ "`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."
7113
+ );
7114
+ }
7115
+ return result2;
7116
+ }
6999
7117
  /** The last backing check of an agent's answer. */
7000
7118
  get lastBacking() {
7001
7119
  return this.lastReport;
@@ -7243,7 +7361,7 @@ var Conversation = class {
7243
7361
  * `query` picks this read's slots by other words than the turn; the pack is the pinned one.
7244
7362
  */
7245
7363
  async context(options = {}) {
7246
- const { query, turn, format, explain, include, ...requestOptions } = options;
7364
+ const { query, turn, format, explain: explain2, include, ...requestOptions } = options;
7247
7365
  const params = {
7248
7366
  subject: this.subject,
7249
7367
  view: this.view,
@@ -7252,7 +7370,7 @@ var Conversation = class {
7252
7370
  ...this.params.about ? { about: this.params.about } : {},
7253
7371
  ...this.params.target ? { target: this.params.target } : {},
7254
7372
  ...format === "json" ? { format } : {},
7255
- ...explain ? { explain } : {}
7373
+ ...explain2 ? { explain: explain2 } : {}
7256
7374
  };
7257
7375
  if (query) params.query = query;
7258
7376
  if (include?.length) params.include = include;
@@ -7260,7 +7378,7 @@ var Conversation = class {
7260
7378
  params.turn = turn === void 0 ? this.turnText : turn;
7261
7379
  const result2 = await this.client.context(params, requestOptions);
7262
7380
  this.features.observe(result2);
7263
- return this.state.observe(this.state.absorb(result2));
7381
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7264
7382
  }
7265
7383
  /**
7266
7384
  * Starts this conversation's first read now, in the background: call it when the call starts
@@ -7440,6 +7558,35 @@ function withHandles(base, extra) {
7440
7558
  return all;
7441
7559
  }
7442
7560
 
7561
+ // src/warm.ts
7562
+ var EVERY_MS = 1e5;
7563
+ var IDLE_MS = 9e4;
7564
+ var WARM_FOR_MS = 6e5;
7565
+ var KeepWarm = class {
7566
+ constructor(enabled) {
7567
+ this.enabled = enabled;
7568
+ }
7569
+ enabled;
7570
+ open = /* @__PURE__ */ new Map();
7571
+ openedAt = 0;
7572
+ /** Notes an open conversation or task. */
7573
+ add(scope, session, now) {
7574
+ if (!this.enabled || typeof WeakRef === "undefined") return;
7575
+ this.open.set(scope, new WeakRef(session));
7576
+ this.openedAt = now;
7577
+ }
7578
+ /** The conversation or task ended. */
7579
+ end(scope) {
7580
+ this.open.delete(scope);
7581
+ }
7582
+ step(now, lastActivityAt, idleMs = IDLE_MS, warmForMs = WARM_FOR_MS) {
7583
+ for (const [scope, ref] of this.open) if (ref.deref() === void 0) this.open.delete(scope);
7584
+ const used = Math.max(lastActivityAt ?? 0, this.openedAt);
7585
+ if (this.open.size === 0 || now - used > warmForMs) return "stop";
7586
+ return now - used >= idleMs ? "ping" : "wait";
7587
+ }
7588
+ };
7589
+
7443
7590
  // src/exit.ts
7444
7591
  var registered = /* @__PURE__ */ new Set();
7445
7592
  var installedOn = null;
@@ -7570,8 +7717,10 @@ var DEFAULT_TIMEOUTS = {
7570
7717
  write: 5e3,
7571
7718
  token: 2e3,
7572
7719
  upload: 6e4,
7573
- prefetch: 1e3
7720
+ prefetch: 1e3,
7721
+ connect: 1e3
7574
7722
  };
7723
+ var FETCH_KEEPALIVE_MS = 4e3;
7575
7724
  var DEFAULT_CACHE = {
7576
7725
  ttlMs: 1e4,
7577
7726
  staleWhileRevalidateMs: 10 * 6e4,
@@ -7678,7 +7827,7 @@ var Task = class {
7678
7827
  * Resolves with an empty result, never rejects, unless the client is strict.
7679
7828
  */
7680
7829
  async context(options = {}) {
7681
- const { query, format, explain, include, ...requestOptions } = options;
7830
+ const { query, format, explain: explain2, include, ...requestOptions } = options;
7682
7831
  const target = this.object ? { object: this.object } : this.params.subject ? { subject: this.params.subject } : {};
7683
7832
  const params = {
7684
7833
  ...target,
@@ -7688,18 +7837,18 @@ var Task = class {
7688
7837
  ...this.level ? { verification: this.level } : {},
7689
7838
  ...this.params.target ? { target: this.params.target } : {},
7690
7839
  ...format === "json" ? { format } : {},
7691
- ...explain ? { explain } : {},
7840
+ ...explain2 ? { explain: explain2 } : {},
7692
7841
  ...include?.length ? { include } : {}
7693
7842
  };
7694
7843
  if (query) {
7695
7844
  const answered = await this.client.context({ ...params, query }, requestOptions);
7696
7845
  this.features.observe(answered);
7697
- return this.state.observe(answered);
7846
+ return this.state.observe(this.state.sayUnlinked(answered, this.logger));
7698
7847
  }
7699
7848
  if (this.state.wantsDelta) params.delta = true;
7700
7849
  const result2 = await this.client.context(params, requestOptions);
7701
7850
  this.features.observe(result2);
7702
- return this.state.observe(this.state.absorb(result2));
7851
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7703
7852
  }
7704
7853
  /**
7705
7854
  * The agent's own working notes for this task's prompt, as `niadra.agentMemory()` with
@@ -7829,14 +7978,14 @@ var AGENT_MEMORY_TOOL_NAMES = {
7829
7978
  search: "search_agent_memory",
7830
7979
  remember: "remember"
7831
7980
  };
7832
- var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
7981
+ var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait", "system_event"];
7833
7982
  var NOTE_KINDS = ["procedure", "tool_note", "process_note", "pitfall"];
7834
7983
  var TOOL_DEFINITIONS = [
7835
7984
  {
7836
7985
  "type": "function",
7837
7986
  "function": {
7838
7987
  "name": "search_customer_history",
7839
- "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.",
7988
+ "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.",
7840
7989
  "parameters": {
7841
7990
  "type": "object",
7842
7991
  "properties": {
@@ -7880,7 +8029,8 @@ var TOOL_DEFINITIONS = [
7880
8029
  "open_item",
7881
8030
  "action",
7882
8031
  "object",
7883
- "trait"
8032
+ "trait",
8033
+ "system_event"
7884
8034
  ]
7885
8035
  }
7886
8036
  },
@@ -7907,7 +8057,7 @@ var TOOL_DEFINITIONS = [
7907
8057
  "type": "function",
7908
8058
  "function": {
7909
8059
  "name": "get_customer_timeline",
7910
- "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.",
8060
+ "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.",
7911
8061
  "parameters": {
7912
8062
  "type": "object",
7913
8063
  "properties": {
@@ -7955,7 +8105,8 @@ var TOOL_DEFINITIONS = [
7955
8105
  "open_item",
7956
8106
  "action",
7957
8107
  "object",
7958
- "trait"
8108
+ "trait",
8109
+ "system_event"
7959
8110
  ]
7960
8111
  }
7961
8112
  },
@@ -7974,7 +8125,7 @@ var TOOL_DEFINITIONS = [
7974
8125
  "type": "function",
7975
8126
  "function": {
7976
8127
  "name": "open_history_item",
7977
- "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.",
8128
+ "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.",
7978
8129
  "parameters": {
7979
8130
  "type": "object",
7980
8131
  "properties": {
@@ -8345,6 +8496,13 @@ function compose(body, read) {
8345
8496
  const pack = body.pack ? { pack: { ...body.pack, slots: fetched?.pack?.slots ?? [] } } : {};
8346
8497
  return { ...body, slots: fetched?.slots ?? null, guards: fetched?.guards ?? [], ...pack };
8347
8498
  }
8499
+ var RTT_MARGIN_MS = 50;
8500
+ function budgetWarnings(rttMs, timeouts, explicit) {
8501
+ const ms = Math.round(rttMs);
8502
+ return [...explicit].sort().filter((name) => timeouts[name] < rttMs + RTT_MARGIN_MS).map(
8503
+ (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`
8504
+ );
8505
+ }
8348
8506
  function rttWarnings(rttMs, timeouts) {
8349
8507
  const ms = Math.round(rttMs);
8350
8508
  const found2 = [];
@@ -8382,6 +8540,12 @@ var Niadra = class _Niadra {
8382
8540
  enabled;
8383
8541
  core;
8384
8542
  timeouts;
8543
+ keepWarm;
8544
+ warmTimer;
8545
+ /** The read budgets the caller left at their defaults: they take the measured round trip on top. */
8546
+ defaultReads;
8547
+ voiceStarted = false;
8548
+ voiceWarned = false;
8385
8549
  strict;
8386
8550
  /** Where the client reports what it swallows in fail-open mode. */
8387
8551
  logger;
@@ -8426,6 +8590,8 @@ var Niadra = class _Niadra {
8426
8590
  this.strict = options.strict ?? false;
8427
8591
  this.logger = options.logger ?? consoleLogger;
8428
8592
  this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
8593
+ this.keepWarm = new KeepWarm(options.keepWarm ?? true);
8594
+ this.defaultReads = new Set(["context", "navigation"].filter((name) => options.timeouts?.[name] === void 0));
8429
8595
  this.voice = new VoiceLines(
8430
8596
  options.voice === false ? { ...DEFAULT_VOICE, enabled: false } : { ...DEFAULT_VOICE, ...options.voice }
8431
8597
  );
@@ -8451,13 +8617,14 @@ var Niadra = class _Niadra {
8451
8617
  this.coordinator = new Coordinator(
8452
8618
  this.outbox,
8453
8619
  this.suppressions,
8454
- (body, key2) => this.api.declare(body, { idempotency_key: key2 })
8620
+ (body, key2) => this.api.declare(body, { idempotency_key: key2 }),
8621
+ this.logger
8455
8622
  );
8456
8623
  this.states = new AgentStates(
8457
8624
  this.outbox,
8458
8625
  {
8459
- read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.timeouts.navigation }),
8460
- write: (write) => this.api.writeAgentState(write, { timeout: this.timeouts.navigation })
8626
+ read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.readBudget("navigation") }),
8627
+ write: (write) => this.api.writeAgentState(write, { timeout: this.readBudget("navigation") })
8461
8628
  },
8462
8629
  this.logger
8463
8630
  );
@@ -8484,6 +8651,7 @@ var Niadra = class _Niadra {
8484
8651
  this.core = setup;
8485
8652
  this.enabled = true;
8486
8653
  this.disabledReason = null;
8654
+ this.probe(setup);
8487
8655
  if (options.flushOnExit ?? true) this.unregisterExit = registerExitFlush(this);
8488
8656
  }
8489
8657
  setup(options) {
@@ -8505,7 +8673,10 @@ var Niadra = class _Niadra {
8505
8673
  baseURL,
8506
8674
  apiKey,
8507
8675
  fetch: fetchImpl,
8508
- defaultHeaders: options.defaultHeaders ?? {}
8676
+ defaultHeaders: options.defaultHeaders ?? {},
8677
+ logger: this.logger,
8678
+ coldAllowanceMs: this.timeouts.connect,
8679
+ keepAliveMs: options.keepAliveMs ?? FETCH_KEEPALIVE_MS
8509
8680
  });
8510
8681
  const queueOptions = { ...DEFAULT_QUEUE, ...options.queue };
8511
8682
  queueOptions.maxBatchSize = Math.min(queueOptions.maxBatchSize, 499);
@@ -8561,7 +8732,7 @@ var Niadra = class _Niadra {
8561
8732
  return result2;
8562
8733
  }
8563
8734
  readContext(core, params, request, options) {
8564
- const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
8735
+ const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8565
8736
  const { query: own3, ...pinned2 } = request;
8566
8737
  const query = turnText(own3 ?? params.turn);
8567
8738
  const voiceCache = this.voiceCache(core, pinned2, options.cache);
@@ -8577,7 +8748,7 @@ var Niadra = class _Niadra {
8577
8748
  */
8578
8749
  async profile() {
8579
8750
  if (!this.core) return null;
8580
- return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.timeouts.navigation }));
8751
+ return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.readBudget("navigation") }));
8581
8752
  }
8582
8753
  /**
8583
8754
  * Checks outputs against this claim contract instead of the one the profile serves (a company's own copy,
@@ -8602,7 +8773,7 @@ var Niadra = class _Niadra {
8602
8773
  */
8603
8774
  async mayContact(handle, purpose, options = {}) {
8604
8775
  if (this.core && this.suppressions.due()) {
8605
- const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.timeouts.navigation);
8776
+ const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.readBudget("navigation"));
8606
8777
  if (!this.suppressions.held) await read;
8607
8778
  }
8608
8779
  const checkOptions = { channel: options.channel ?? null };
@@ -8612,8 +8783,8 @@ var Niadra = class _Niadra {
8612
8783
  readSuppressions(budgetMs) {
8613
8784
  return this.suppressions.read(
8614
8785
  {
8615
- salt: () => this.api.suppressionSalt({ timeout: this.timeouts.navigation }),
8616
- page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.timeouts.navigation })
8786
+ salt: () => this.api.suppressionSalt({ timeout: this.readBudget("navigation") }),
8787
+ page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.readBudget("navigation") })
8617
8788
  },
8618
8789
  budgetMs
8619
8790
  );
@@ -8692,6 +8863,7 @@ var Niadra = class _Niadra {
8692
8863
  }
8693
8864
  /** What a conversation or a task needs of its client for the agent features. */
8694
8865
  get agentHost() {
8866
+ const client = this;
8695
8867
  return {
8696
8868
  recorder: this.turns,
8697
8869
  coordinator: this.coordinator,
@@ -8705,7 +8877,10 @@ var Niadra = class _Niadra {
8705
8877
  claim: (request, timeoutMs) => this.api.claim(request, {}, { timeout: timeoutMs }),
8706
8878
  verifyClaim: (ref, field, value, options) => this.verifyClaim(ref, field, value, options),
8707
8879
  enabled: this.enabled,
8708
- navigationMs: this.timeouts.navigation
8880
+ strict: this.strict,
8881
+ get navigationMs() {
8882
+ return client.readBudget("navigation");
8883
+ }
8709
8884
  };
8710
8885
  }
8711
8886
  /**
@@ -8733,7 +8908,7 @@ var Niadra = class _Niadra {
8733
8908
  if (!cache) return false;
8734
8909
  const key2 = cacheKey(request);
8735
8910
  const line = this.voice.line(cacheScope(request) ?? "");
8736
- this.probe(core);
8911
+ this.startVoice(core);
8737
8912
  line.request = request;
8738
8913
  if (!cache.has(key2) && line.inFlight().length === 0) {
8739
8914
  this.voiceRead(core, cache, line, key2, request, null, this.timeouts.contextVoiceStart);
@@ -8870,13 +9045,13 @@ var Niadra = class _Niadra {
8870
9045
  if (!params.query || params.query.length > 2e3) {
8871
9046
  throw new NiadraValidationError("query must be 1 to 2000 characters");
8872
9047
  }
8873
- return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
9048
+ return this.readSpec("POST", "/v1/history/search", params, this.readBudget("navigation"), options);
8874
9049
  });
8875
9050
  }
8876
9051
  /** The customer's history, newest first, one line per item, paginated by cursor. */
8877
9052
  async timeline(params, options = {}) {
8878
9053
  return this.navigate(
8879
- () => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
9054
+ () => this.readSpec("POST", "/v1/history/timeline", params, this.readBudget("navigation"), options)
8880
9055
  );
8881
9056
  }
8882
9057
  /**
@@ -8889,9 +9064,10 @@ var Niadra = class _Niadra {
8889
9064
  if (!id) throw new NiadraValidationError("open() needs an item id");
8890
9065
  const body = { item_id: id };
8891
9066
  if (params.subject) body.subject = params.subject;
9067
+ if (params.about) body.about = params.about;
8892
9068
  if (params.verification) body.verification = params.verification;
8893
9069
  if (params.conversation_id) body.conversation_id = params.conversation_id;
8894
- return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
9070
+ return this.readSpec("POST", "/v1/history/open", body, this.readBudget("navigation"), options);
8895
9071
  });
8896
9072
  }
8897
9073
  /**
@@ -8903,7 +9079,7 @@ var Niadra = class _Niadra {
8903
9079
  * const { data: invoice } = await niadra.objectState("invoice:erp:0823");
8904
9080
  */
8905
9081
  async objectState(object, options = {}) {
8906
- return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.timeouts.navigation, options));
9082
+ return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.readBudget("navigation"), options));
8907
9083
  }
8908
9084
  /**
8909
9085
  * System events and agent actions about one object, newest first, one line each and never
@@ -8916,7 +9092,7 @@ var Niadra = class _Niadra {
8916
9092
  throw new NiadraValidationError("limit must be between 1 and 100");
8917
9093
  }
8918
9094
  const path = `${objectPath(object)}/timeline`;
8919
- const spec = this.readSpec("GET", path, void 0, this.timeouts.navigation, options);
9095
+ const spec = this.readSpec("GET", path, void 0, this.readBudget("navigation"), options);
8920
9096
  spec.query = { cursor: params.cursor, limit: String(limit3) };
8921
9097
  return spec;
8922
9098
  });
@@ -8935,6 +9111,7 @@ var Niadra = class _Niadra {
8935
9111
  timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
8936
9112
  open: (id, customer, bound, voice) => {
8937
9113
  const scope = { subject: customer };
9114
+ if (bound.about) scope.about = bound.about;
8938
9115
  if (bound.verification) scope.verification = bound.verification;
8939
9116
  if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
8940
9117
  return this.open(id, scope, this.voiceBudget(voice));
@@ -8969,7 +9146,7 @@ var Niadra = class _Niadra {
8969
9146
  const key2 = AgentMemoryCache.key(params);
8970
9147
  const fresh = cache?.fresh(key2);
8971
9148
  if (fresh) return blockResult(fresh, "cache");
8972
- const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
9149
+ const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8973
9150
  const spec = this.readSpec("GET", "/v1/agent-memory/block", void 0, timeout, options);
8974
9151
  spec.query = {
8975
9152
  max_tokens: String(params.max_tokens ?? 300),
@@ -9006,7 +9183,7 @@ var Niadra = class _Niadra {
9006
9183
  if (params.limit !== void 0) body.limit = params.limit;
9007
9184
  if (params.conversation_id) body.conversation_id = params.conversation_id;
9008
9185
  if (params.task_id) body.task_id = params.task_id;
9009
- return this.readSpec("POST", "/v1/agent-memory/search", body, this.timeouts.navigation, options);
9186
+ return this.readSpec("POST", "/v1/agent-memory/search", body, this.readBudget("navigation"), options);
9010
9187
  });
9011
9188
  return result2.error ? result2 : { data: result2.data.notes, error: null };
9012
9189
  }
@@ -9100,6 +9277,41 @@ var Niadra = class _Niadra {
9100
9277
  return { ok: false, idempotency_key: key2, error: this.swallow(error, "feedback") };
9101
9278
  }
9102
9279
  }
9280
+ /**
9281
+ * How the space's agents used the context they read (`GET /v1/context-use`): sessions, deliveries, use,
9282
+ * repetition, transfers and recontact, with intervals, grouped by `group_by`. A key of an `analyst` source
9283
+ * with the `analytics` scope reads every source of the space; a key with `admin` reads its own source.
9284
+ */
9285
+ contextUse(params = {}, options = {}) {
9286
+ return this.navigate(() => {
9287
+ const { group_by: groups, ...filters } = params;
9288
+ const query = { ...filters };
9289
+ if (groups?.length) query.group_by = groups;
9290
+ return { ...this.readSpec("GET", "/v1/context-use", void 0, this.timeouts.write, options), query };
9291
+ });
9292
+ }
9293
+ /**
9294
+ * Links a person to the organization they act for (an account or a partner), as a system of record that
9295
+ * knows who works for whom: a CRM, an HR system. Needs a key with the `identity:link` scope (or `admin`);
9296
+ * `can_see_contacts` needs `admin`. Reads with `about` reach the organization through the link.
9297
+ */
9298
+ link(params, options = {}) {
9299
+ const { idempotency_key: key2, ...rest } = params;
9300
+ const body = { can_see_contacts: false, method: "system_import", ...rest };
9301
+ return this.navigate(() => this.writeSpec("/v1/identity/links", body, key2 ?? uuidv7(), options));
9302
+ }
9303
+ /**
9304
+ * Ends a link, from `valid_to` (now when absent): the person no longer acts for the organization, and reads
9305
+ * with `about` for the pair go on with the person's own memory. Needs `identity:link` or `admin`.
9306
+ */
9307
+ endLink(linkId, params = {}, options = {}) {
9308
+ return this.navigate(() => {
9309
+ if (!linkId) throw new NiadraValidationError("endLink() needs a link id");
9310
+ const body = params.valid_to ? { valid_to: params.valid_to } : {};
9311
+ const path = `/v1/identity/links/${encodeURIComponent(linkId)}/end`;
9312
+ return this.writeSpec(path, body, params.idempotency_key ?? uuidv7(), options);
9313
+ });
9314
+ }
9103
9315
  /**
9104
9316
  * Up to 500 corrections in one call, each with its own idempotency key (minted when missing).
9105
9317
  * Resolves with `accepted`, `duplicates` for replayed keys and one error per refused item, by index.
@@ -9192,16 +9404,20 @@ var Niadra = class _Niadra {
9192
9404
  * emits `conversation.ended` when you call `end()`.
9193
9405
  */
9194
9406
  conversation(params) {
9195
- return new Conversation(this, params, {
9407
+ const conversation = new Conversation(this, params, {
9196
9408
  endConversation: (id) => this.endScope(buildConversationEnded(id), `conversation:${id}`)
9197
9409
  });
9410
+ this.warm(`conversation:${conversation.id}`, conversation);
9411
+ return conversation;
9198
9412
  }
9199
9413
  /** A helper for one internal-agent task: binds `task_id` to reads and writes and emits `task.ended`. */
9200
9414
  task(params) {
9201
- return new Task(this, params, {
9415
+ const task = new Task(this, params, {
9202
9416
  endTask: (id) => this.endScope(buildTaskEnded(id), `task:${id}`),
9203
9417
  verifyTask: (verify) => this.verifyWith(verify)
9204
9418
  });
9419
+ this.warm(`task:${task.id}`, task);
9420
+ return task;
9205
9421
  }
9206
9422
  /**
9207
9423
  * Sends every queued event and resolves when done. Call it before a serverless function
@@ -9221,6 +9437,8 @@ var Niadra = class _Niadra {
9221
9437
  */
9222
9438
  async shutdown() {
9223
9439
  this.unregisterExit();
9440
+ clearInterval(this.warmTimer);
9441
+ this.warmTimer = void 0;
9224
9442
  if (!this.core) return;
9225
9443
  await this.turnSender?.stop(this.timeouts.write);
9226
9444
  await this.outbox.stop(this.timeouts.write);
@@ -9324,9 +9542,9 @@ var Niadra = class _Niadra {
9324
9542
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", request, timeout, { signal, headers }));
9325
9543
  return normalizeContext(response.data);
9326
9544
  } catch (error) {
9327
- const refused2 = error instanceof NiadraAPIError && error.status === 404;
9545
+ const refused3 = error instanceof NiadraAPIError && error.status === 404;
9328
9546
  const left = timeout - (Date.now() - started);
9329
- if (!request.include?.length || !refused2 || left <= 0) throw error;
9547
+ if (!request.include?.length || !refused3 || left <= 0) throw error;
9330
9548
  for (const name of request.include) this.refusedBlocks.set(name, Date.now() + BLOCK_RECHECK_AFTER_MS);
9331
9549
  const { include: _dropped, ...plain2 } = request;
9332
9550
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", plain2, left, { signal, headers }));
@@ -9414,9 +9632,32 @@ var Niadra = class _Niadra {
9414
9632
  const rtt = Math.min(...samples);
9415
9633
  this.voice.rtt = rtt;
9416
9634
  this.logger.debug(`round trip to the region ${Math.round(rtt)} ms`);
9417
- for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9635
+ const explicit = ["context", "navigation"].filter((name) => !this.defaultReads.has(name));
9636
+ for (const warning of budgetWarnings(rtt, this.timeouts, explicit)) this.logger.warn(warning);
9637
+ if (this.voiceStarted) this.warnVoice();
9418
9638
  })();
9419
9639
  }
9640
+ /**
9641
+ * A read budget: one the caller left at its default is what the API may take, and the measured round trip
9642
+ * to the region goes on top, so an agent far from the region (Sao Paulo, 170 ms from us-east-2) is not
9643
+ * timed out by the network; one the caller set is a ceiling.
9644
+ */
9645
+ readBudget(name) {
9646
+ const rtt = this.voice.rtt;
9647
+ return rtt !== null && this.defaultReads.has(name) ? this.timeouts[name] + rtt : this.timeouts[name];
9648
+ }
9649
+ /** The client reads in voice: the voice budgets' warnings matter from now on. */
9650
+ startVoice(core) {
9651
+ this.voiceStarted = true;
9652
+ this.probe(core);
9653
+ this.warnVoice();
9654
+ }
9655
+ warnVoice() {
9656
+ const rtt = this.voice.rtt;
9657
+ if (this.voiceWarned || rtt === null) return;
9658
+ this.voiceWarned = true;
9659
+ for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9660
+ }
9420
9661
  /**
9421
9662
  * A voice turn: the pinned body from memory, and the slots of the read of its words when that
9422
9663
  * read lands within `timeout`. See `voice.ts`.
@@ -9426,7 +9667,7 @@ var Niadra = class _Niadra {
9426
9667
  const key2 = cacheKey(request);
9427
9668
  const deadline = Date.now() + timeout;
9428
9669
  const line = this.voice.line(scope);
9429
- this.probe(core);
9670
+ this.startVoice(core);
9430
9671
  line.request = request;
9431
9672
  const words2 = wordsOf(query);
9432
9673
  const background = this.timeouts.prefetch;
@@ -9612,8 +9853,39 @@ var Niadra = class _Niadra {
9612
9853
  core.queue.flushInBackground(true);
9613
9854
  });
9614
9855
  }
9856
+ /** Notes an open conversation or task; the first one starts the keep-warm timer (`warm.ts`). */
9857
+ warm(scope, session) {
9858
+ const core = this.core;
9859
+ if (!core || !this.keepWarm.enabled) return;
9860
+ this.keepWarm.add(scope, session, Date.now());
9861
+ if (this.warmTimer !== void 0) return;
9862
+ const timer = setInterval(() => {
9863
+ this.warmTick(core);
9864
+ }, EVERY_MS);
9865
+ timer.unref?.();
9866
+ this.warmTimer = timer;
9867
+ }
9868
+ warmTick(core) {
9869
+ const step = this.keepWarm.step(Date.now(), core.transport.lastActivityAt);
9870
+ if (step === "stop") {
9871
+ clearInterval(this.warmTimer);
9872
+ this.warmTimer = void 0;
9873
+ return;
9874
+ }
9875
+ if (step !== "ping") return;
9876
+ core.transport.request({
9877
+ method: "GET",
9878
+ path: "/healthz",
9879
+ timeoutMs: 2e3,
9880
+ retry: { kind: "read", maxAttempts: 1 },
9881
+ activity: false
9882
+ }).catch((error) => {
9883
+ this.logger.debug(`keep-warm ping failed: ${describe(toNiadraError(error))}`);
9884
+ });
9885
+ }
9615
9886
  async endScope(item, scope) {
9616
9887
  this.forgetScope(scope);
9888
+ this.keepWarm.end(scope);
9617
9889
  return this.sendNow(() => item);
9618
9890
  }
9619
9891
  async sendBatch(transport, items2, maxAttempts, backoff) {
@@ -11468,7 +11740,7 @@ var Replayer = class {
11468
11740
  try {
11469
11741
  answer = await this.niadra.callRoute({ method: "POST", path: "/v1/scenario-runs", body, idempotencyKey: uuidv7() });
11470
11742
  } catch (error) {
11471
- if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused(scenarioIds, results);
11743
+ if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused2(scenarioIds, results);
11472
11744
  throw error;
11473
11745
  }
11474
11746
  let run = runOf(answer);
@@ -11571,7 +11843,7 @@ var Replayer = class {
11571
11843
  return result(scenarioId, turnId, caseId, run, "completed", { paraphrase: rephrased, assertions, divergent_calls: played.divergent, latency_ms: latency });
11572
11844
  }
11573
11845
  };
11574
- function refused(scenarioIds, results) {
11846
+ function refused2(scenarioIds, results) {
11575
11847
  const scenarios = scenarioIds.map((id) => {
11576
11848
  const mine = results.filter((r) => r.scenario_id === id);
11577
11849
  return {