@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/cli.js CHANGED
@@ -5,6 +5,46 @@ import { resolve as resolvePath } from "path";
5
5
  import { pathToFileURL } from "url";
6
6
  import { parseArgs as parseArgs4 } from "util";
7
7
 
8
+ // src/ids.ts
9
+ var lastMs = -1;
10
+ var counter = 0;
11
+ function randomBytes(length) {
12
+ const bytes = new Uint8Array(length);
13
+ const webCrypto = globalThis.crypto;
14
+ if (webCrypto?.getRandomValues) {
15
+ webCrypto.getRandomValues(bytes);
16
+ } else {
17
+ for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
18
+ }
19
+ return bytes;
20
+ }
21
+ function uuidv7(now = Date.now()) {
22
+ let ms = now;
23
+ if (ms <= lastMs) {
24
+ ms = lastMs;
25
+ counter = counter + 1 & 4095;
26
+ if (counter === 0) ms += 1;
27
+ } else {
28
+ counter = (randomBytes(2)[0] ?? 0) & 127;
29
+ }
30
+ lastMs = ms;
31
+ const bytes = randomBytes(16);
32
+ const high = Math.floor(ms / 2 ** 16);
33
+ const low = ms % 2 ** 16;
34
+ bytes[0] = high >>> 24 & 255;
35
+ bytes[1] = high >>> 16 & 255;
36
+ bytes[2] = high >>> 8 & 255;
37
+ bytes[3] = high & 255;
38
+ bytes[4] = low >>> 8 & 255;
39
+ bytes[5] = low & 255;
40
+ bytes[6] = 112 | counter >>> 8 & 15;
41
+ bytes[7] = counter & 255;
42
+ bytes[8] = 128 | (bytes[8] ?? 0) & 63;
43
+ let hex2 = "";
44
+ for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
45
+ return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
46
+ }
47
+
8
48
  // src/errors.ts
9
49
  var NiadraError = class extends Error {
10
50
  name = "NiadraError";
@@ -40,11 +80,12 @@ var NiadraAPIError = class extends NiadraError {
40
80
  constructor(status, problem, requestId, retryAfterMs = null) {
41
81
  const code = problem?.code ?? `http_${status}`;
42
82
  const detail = problem?.detail ? `: ${problem.detail}` : "";
43
- super(`${status} ${code}${detail}`);
83
+ const id = requestId ?? problem?.request_id ?? null;
84
+ super(`${status} ${code}${detail}${id ? ` (request ${id})` : ""}`);
44
85
  this.status = status;
45
86
  this.code = code;
46
87
  this.problem = problem;
47
- this.requestId = requestId ?? problem?.request_id ?? null;
88
+ this.requestId = id;
48
89
  this.retryAfterMs = retryAfterMs;
49
90
  }
50
91
  };
@@ -68,45 +109,11 @@ function toNiadraError(error) {
68
109
  const message = error instanceof Error ? error.message : String(error);
69
110
  return new NiadraError(message, { cause: error });
70
111
  }
71
-
72
- // src/ids.ts
73
- var lastMs = -1;
74
- var counter = 0;
75
- function randomBytes(length) {
76
- const bytes = new Uint8Array(length);
77
- const webCrypto = globalThis.crypto;
78
- if (webCrypto?.getRandomValues) {
79
- webCrypto.getRandomValues(bytes);
80
- } else {
81
- for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
82
- }
83
- return bytes;
84
- }
85
- function uuidv7(now = Date.now()) {
86
- let ms = now;
87
- if (ms <= lastMs) {
88
- ms = lastMs;
89
- counter = counter + 1 & 4095;
90
- if (counter === 0) ms += 1;
91
- } else {
92
- counter = (randomBytes(2)[0] ?? 0) & 127;
93
- }
94
- lastMs = ms;
95
- const bytes = randomBytes(16);
96
- const high = Math.floor(ms / 2 ** 16);
97
- const low = ms % 2 ** 16;
98
- bytes[0] = high >>> 24 & 255;
99
- bytes[1] = high >>> 16 & 255;
100
- bytes[2] = high >>> 8 & 255;
101
- bytes[3] = high & 255;
102
- bytes[4] = low >>> 8 & 255;
103
- bytes[5] = low & 255;
104
- bytes[6] = 112 | counter >>> 8 & 15;
105
- bytes[7] = counter & 255;
106
- bytes[8] = 128 | (bytes[8] ?? 0) & 63;
107
- let hex2 = "";
108
- for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
109
- return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
112
+ function explain(error) {
113
+ if (!(error instanceof NiadraAPIError)) return error instanceof Error ? error.name : String(error);
114
+ const detail = error.problem?.detail?.trim();
115
+ const said = `${error.status} ${error.code}${detail ? `: ${detail.slice(0, 300)}` : ""}`;
116
+ return error.requestId ? `${said} (request ${error.requestId})` : said;
110
117
  }
111
118
 
112
119
  // src/admin.ts
@@ -503,7 +510,7 @@ function internalRecord(span, ref, act) {
503
510
  }
504
511
 
505
512
  // src/version.ts
506
- var VERSION = "0.9.0";
513
+ var VERSION = "0.10.3";
507
514
 
508
515
  // src/transport.ts
509
516
  var RETRYABLE_WRITE_STATUS = /* @__PURE__ */ new Set([408, 421, 429, 500, 502, 503, 504]);
@@ -516,8 +523,32 @@ var Transport = class {
516
523
  }
517
524
  config;
518
525
  baseURL;
526
+ answeredAt;
527
+ /** When the allowance was first given since the last answer; it holds for the calls of that moment only. */
528
+ grantedAt;
529
+ /** When the client last sent a request of its own (`RequestSpec.activity`), in epoch milliseconds. */
530
+ lastActivityAt;
519
531
  async request(spec) {
520
- return spec.retry.kind === "read" ? this.read(spec, spec.retry) : this.write(spec, spec.retry);
532
+ if (spec.activity !== false) this.lastActivityAt = Date.now();
533
+ const budgeted = this.withAllowance(spec);
534
+ return budgeted.retry.kind === "read" ? this.read(budgeted, budgeted.retry) : this.write(budgeted, budgeted.retry);
535
+ }
536
+ /**
537
+ * `spec` with the allowance for opening a connection, when it has a budget and none is likely open. The calls
538
+ * that start while the first one opens it get it too; after that, none does until an answer comes, so an
539
+ * outage costs the allowance once, not on every turn.
540
+ */
541
+ withAllowance(spec) {
542
+ const allowance = this.config.coldAllowanceMs ?? 0;
543
+ const { retry } = spec;
544
+ if (allowance <= 0 || retry.kind === "write" && retry.totalMs === void 0) return spec;
545
+ const now = Date.now();
546
+ const keep = this.config.keepAliveMs ?? 0;
547
+ if (this.answeredAt !== void 0 && now - this.answeredAt <= keep) return spec;
548
+ this.grantedAt ??= now;
549
+ if (now - this.grantedAt > allowance) return spec;
550
+ const total = retry.kind === "write" && retry.totalMs !== void 0 ? { ...retry, totalMs: retry.totalMs + allowance } : retry;
551
+ return { ...spec, timeoutMs: spec.timeoutMs + allowance, retry: total };
521
552
  }
522
553
  async read(spec, policy) {
523
554
  const deadline = new Deadline(spec.timeoutMs, spec.signal);
@@ -588,7 +619,18 @@ var Transport = class {
588
619
  headers["content-type"] = "application/json";
589
620
  init.body = JSON.stringify(spec.body);
590
621
  }
591
- return this.exchange(this.url(spec.path, spec.query), init, deadline);
622
+ try {
623
+ const answer = await this.exchange(this.url(spec.path, spec.query), init, deadline);
624
+ this.answered();
625
+ return answer;
626
+ } catch (error) {
627
+ if (error instanceof NiadraAPIError) this.answered();
628
+ throw error;
629
+ }
630
+ }
631
+ answered() {
632
+ this.answeredAt = Date.now();
633
+ this.grantedAt = void 0;
592
634
  }
593
635
  async exchange(url, init, deadline) {
594
636
  let response;
@@ -597,6 +639,7 @@ var Transport = class {
597
639
  } catch (error) {
598
640
  throw deadline.explain(error);
599
641
  }
642
+ warnIfDeprecated(this.config.logger, init.method ?? "GET", url, response.headers);
600
643
  const requestId = response.headers.get("x-request-id");
601
644
  let payload;
602
645
  try {
@@ -666,6 +709,29 @@ var Deadline = class {
666
709
  return new NiadraConnectionError(`connection failed: ${message}`, { cause: error });
667
710
  }
668
711
  };
712
+ var VERSIONING_DOCS = "https://docs.niadra.com/en/security/api-versioning";
713
+ var DEPRECATION_LINK = /<([^>]*)>[^,]*;\s*rel="?deprecation"?/gi;
714
+ var deprecationsSeen = /* @__PURE__ */ new Set();
715
+ function warnIfDeprecated(logger, method, url, headers) {
716
+ const since = headers.get("deprecation");
717
+ if (since === null) return;
718
+ const links = [...(headers.get("link") ?? "").matchAll(DEPRECATION_LINK)].map((match) => match[1] ?? "");
719
+ const key2 = `${method} ${links.join(" ") || (url.split("?")[0] ?? url)}`;
720
+ if (deprecationsSeen.has(key2)) return;
721
+ deprecationsSeen.add(key2);
722
+ const sunset = headers.get("sunset");
723
+ logger.warn(
724
+ `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}`
725
+ );
726
+ }
727
+ function deprecatedSince(value) {
728
+ const seconds = Number(value.trim().replace(/^@/, ""));
729
+ return Number.isInteger(seconds) ? new Date(seconds * 1e3).toISOString().slice(0, 10) : value;
730
+ }
731
+ function sunsetDay(value) {
732
+ const time = Date.parse(value);
733
+ return Number.isNaN(time) ? value : new Date(time).toISOString().slice(0, 10);
734
+ }
669
735
  async function readBody(response) {
670
736
  const text2 = await response.text();
671
737
  if (text2.length === 0) return null;
@@ -4001,8 +4067,7 @@ var TurnSender = class {
4001
4067
  return true;
4002
4068
  }
4003
4069
  }
4004
- const code = error instanceof NiadraAPIError ? error.code : toNiadraError(error).name;
4005
- this.recorder.rejected(batch.frames.length, [code]);
4070
+ this.recorder.rejected(batch.frames.length, [explain(error)]);
4006
4071
  return true;
4007
4072
  }
4008
4073
  accepted(batch, answer) {
@@ -4010,8 +4075,8 @@ var TurnSender = class {
4010
4075
  this.resumeAt = 0;
4011
4076
  this.recorder.sent(answer.accepted, answer.duplicates);
4012
4077
  const again = [];
4013
- const codes2 = /* @__PURE__ */ new Set();
4014
- let refused2 = 0;
4078
+ const reasons = /* @__PURE__ */ new Set();
4079
+ let refused3 = 0;
4015
4080
  for (const error of answer.errors ?? []) {
4016
4081
  const frame = batch.frames[error.index];
4017
4082
  if (frame === void 0) continue;
@@ -4020,12 +4085,12 @@ var TurnSender = class {
4020
4085
  this.recorder.modeRefused();
4021
4086
  again.push(frame);
4022
4087
  } else {
4023
- refused2++;
4024
- codes2.add(error.code);
4088
+ refused3++;
4089
+ reasons.add(error.detail ? `${error.code}: ${error.detail.slice(0, 300)}` : error.code);
4025
4090
  }
4026
4091
  }
4027
4092
  if (again.length > 0) this.queue.requeue(again);
4028
- if (refused2 > 0) this.recorder.rejected(refused2, [...codes2]);
4093
+ if (refused3 > 0) this.recorder.rejected(refused3, [...reasons]);
4029
4094
  }
4030
4095
  };
4031
4096
 
@@ -4147,9 +4212,12 @@ var TurnRecorder = class {
4147
4212
  this.accepted += accepted;
4148
4213
  this.duplicates += duplicates;
4149
4214
  }
4150
- rejected(count2, codes2) {
4215
+ /** `reasons`: the API's code and detail of each refusal (field paths and rules, never a value). */
4216
+ rejected(count2, reasons) {
4151
4217
  this.rejectedTurns += count2;
4152
- this.logger.warn(`${count2} turn records were refused (${[...codes2].sort().join(", ")})`);
4218
+ const shown = [...new Set(reasons)].sort();
4219
+ const more = shown.length > 3 ? ` (and ${shown.length - 3} more)` : "";
4220
+ this.logger.warn(`${count2} turn records were refused: ${shown.slice(0, 3).join("; ")}${more}`);
4153
4221
  }
4154
4222
  /** The space does not record turns: these are dropped, and with `off` (a 404) recording stops a while. */
4155
4223
  notRecorded(count2, off) {
@@ -4219,6 +4287,10 @@ async function hex(text2) {
4219
4287
  }
4220
4288
 
4221
4289
  // src/coordination/client.ts
4290
+ var REFUSED = /* @__PURE__ */ new Set([400, 401, 403, 422]);
4291
+ function refused(error) {
4292
+ return error instanceof NiadraAPIError && REFUSED.has(error.status);
4293
+ }
4222
4294
  var FAIL_CLOSED = /* @__PURE__ */ new Set(["marketing", "retention", "collection"]);
4223
4295
  var CHECK_BUDGET_MS = 200;
4224
4296
  function fallback(request, suppressed, failOpen) {
@@ -4240,15 +4312,22 @@ function claimed(data, error) {
4240
4312
  return { held: false, claim: null, error: code };
4241
4313
  }
4242
4314
  var Coordinator = class {
4243
- constructor(outbox, suppressions, declareNow) {
4315
+ constructor(outbox, suppressions, declareNow, logger) {
4244
4316
  this.outbox = outbox;
4245
4317
  this.suppressions = suppressions;
4246
4318
  this.declareNow = declareNow;
4319
+ this.logger = logger;
4247
4320
  }
4248
4321
  outbox;
4249
4322
  suppressions;
4250
4323
  declareNow;
4324
+ logger;
4251
4325
  async failed(request, failOpen, error) {
4326
+ if (refused(error)) {
4327
+ this.logger?.warn(`the coordination check was refused: ${explain(error)}`);
4328
+ const decision = request.direction === "inbound" ? "allow" : "defer";
4329
+ return recorded({ decision, decision_id: uuidv7(), reasons: ["invalid_request"], valid_for_s: 0 });
4330
+ }
4252
4331
  const off = error instanceof NiadraAPIError && error.status === 404;
4253
4332
  const suppressed = request.subject != null && request.direction === "outbound" ? !await this.suppressions.mayContact(request.subject, request.purpose, { channel: request.channel ?? null, failOpen: true }) : false;
4254
4333
  const plain2 = off ? { ...request, effect_key: null } : request;
@@ -4269,7 +4348,7 @@ var Coordinator = class {
4269
4348
  if (who.subject) body.subject = who.subject;
4270
4349
  if (who.object) body.object = who.object;
4271
4350
  const key2 = uuidv7();
4272
- this.outbox.put({ send: () => this.declareNow(body, key2) });
4351
+ this.outbox.put({ send: () => this.declareNow(body, key2), route: "POST /v1/coordination/declare" });
4273
4352
  return key2;
4274
4353
  }
4275
4354
  };
@@ -4376,26 +4455,42 @@ async function suppressionKey(salt, canonical) {
4376
4455
  const key2 = await subtle.importKey("raw", saltBytes(salt), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
4377
4456
  return toBase64url(new Uint8Array(await subtle.sign("HMAC", key2, new TextEncoder().encode(canonical))));
4378
4457
  }
4458
+ var AREA_CODES = new Set(
4459
+ "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(" ")
4460
+ );
4461
+ var FORMAT_CHARACTERS = new RegExp("\\p{Cf}", "gu");
4379
4462
  function phone(value) {
4380
- const raw = value.trim().replace(SEPARATORS2, "");
4381
- const international = raw.startsWith("+") || raw.startsWith("00");
4463
+ let text2 = value.normalize("NFKC").replace(FORMAT_CHARACTERS, "").trim();
4464
+ if (text2.slice(0, 4).toLowerCase() === "tel:") text2 = text2.slice(4);
4465
+ const international = text2.startsWith("+") || text2.startsWith("00");
4466
+ if (international) text2 = text2.replaceAll("(0)", "");
4467
+ const raw = text2.replace(SEPARATORS2, "");
4382
4468
  let digits = raw.startsWith("+") ? raw.slice(1) : international ? raw.slice(2) : raw;
4383
4469
  if (!DIGITS2.test(digits)) throw new NiadraDestinationError("invalid_handle");
4384
- const national = digits.replace(/^0+/, "");
4385
- if (!international && (national.length === 10 || national.length === 11) && isArea(national.slice(0, 2))) {
4386
- digits = `55${national}`;
4470
+ if (!international) {
4471
+ const national = digits.replace(/^0+/, "");
4472
+ if (isBrazilianNational(national)) {
4473
+ digits = `55${national}`;
4474
+ } else if (digits.startsWith("0") && (national.length === 12 || national.length === 13)) {
4475
+ if (isBrazilianNational(national.slice(2))) digits = `55${national.slice(2)}`;
4476
+ }
4387
4477
  }
4388
4478
  if (digits.length < 8 || digits.length > 15 || digits.startsWith("0")) {
4389
4479
  throw new NiadraDestinationError("invalid_handle");
4390
4480
  }
4391
4481
  const rest = digits.slice(2);
4392
- if (digits.startsWith("55") && rest.length === 10 && isArea(rest.slice(0, 2)) && "6789".includes(rest.charAt(2))) {
4482
+ if (digits.startsWith("55") && rest.length === 10 && AREA_CODES.has(rest.slice(0, 2)) && "6789".includes(rest.charAt(2))) {
4393
4483
  digits = `55${rest.slice(0, 2)}9${rest.slice(2)}`;
4394
4484
  }
4485
+ if (digits.length === 13 && (digits.startsWith("521") || digits.startsWith("549"))) {
4486
+ digits = digits.slice(0, 2) + digits.slice(3);
4487
+ }
4395
4488
  return `phone:+${digits}`;
4396
4489
  }
4397
- function isArea(code) {
4398
- return /^[1-9]{2}$/.test(code);
4490
+ function isBrazilianNational(national) {
4491
+ if (!AREA_CODES.has(national.slice(0, 2))) return false;
4492
+ if (national.length === 10) return "23456789".includes(national.charAt(2));
4493
+ return national.length === 11 && national.charAt(2) === "9";
4399
4494
  }
4400
4495
  function saltBytes(salt) {
4401
4496
  const bytes = fromBase64url(salt);
@@ -4772,7 +4867,7 @@ var Outbox = class {
4772
4867
  return false;
4773
4868
  }
4774
4869
  this.writes.shift();
4775
- this.logger.warn(`a write was refused (${error instanceof NiadraAPIError ? error.code : String(error)})`);
4870
+ this.logger.warn(`${write.route ?? "a write"} was refused: ${explain(error)}`);
4776
4871
  this.settle(write, void 0, error);
4777
4872
  return true;
4778
4873
  }
@@ -6309,6 +6404,7 @@ var AgentSession = class {
6309
6404
  try {
6310
6405
  return this.host.coordinator.decided(await this.host.check(request, options.timeoutMs ?? CHECK_BUDGET_MS), request, this.checked);
6311
6406
  } catch (error) {
6407
+ if (this.host.strict && refused(error)) throw error;
6312
6408
  return this.host.coordinator.failed(request, options.failOpen, error);
6313
6409
  }
6314
6410
  }
@@ -6920,6 +7016,7 @@ var SessionState = class {
6920
7016
  /** The guards of the last read: they hold the answers to the turn they were written for. */
6921
7017
  guards = /* @__PURE__ */ new Map();
6922
7018
  lastReport = null;
7019
+ unlinkedSaid = false;
6923
7020
  /** After the first pack, every read also asks what changed since. */
6924
7021
  get wantsDelta() {
6925
7022
  return this.etag !== null;
@@ -6948,6 +7045,19 @@ var SessionState = class {
6948
7045
  this.last = absorbed;
6949
7046
  return absorbed;
6950
7047
  }
7048
+ /**
7049
+ * Once per session: the read named an organization (`about`) with no active link to the subject, so it went
7050
+ * on with the subject's own memory, never the organization's.
7051
+ */
7052
+ sayUnlinked(result2, logger) {
7053
+ if (result2.response?.about_unlinked && !this.unlinkedSaid) {
7054
+ this.unlinkedSaid = true;
7055
+ logger.warn(
7056
+ "`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."
7057
+ );
7058
+ }
7059
+ return result2;
7060
+ }
6951
7061
  /** The last backing check of an agent's answer. */
6952
7062
  get lastBacking() {
6953
7063
  return this.lastReport;
@@ -7195,7 +7305,7 @@ var Conversation = class {
7195
7305
  * `query` picks this read's slots by other words than the turn; the pack is the pinned one.
7196
7306
  */
7197
7307
  async context(options = {}) {
7198
- const { query, turn: turn2, format, explain, include, ...requestOptions } = options;
7308
+ const { query, turn: turn2, format, explain: explain2, include, ...requestOptions } = options;
7199
7309
  const params = {
7200
7310
  subject: this.subject,
7201
7311
  view: this.view,
@@ -7204,7 +7314,7 @@ var Conversation = class {
7204
7314
  ...this.params.about ? { about: this.params.about } : {},
7205
7315
  ...this.params.target ? { target: this.params.target } : {},
7206
7316
  ...format === "json" ? { format } : {},
7207
- ...explain ? { explain } : {}
7317
+ ...explain2 ? { explain: explain2 } : {}
7208
7318
  };
7209
7319
  if (query) params.query = query;
7210
7320
  if (include?.length) params.include = include;
@@ -7212,7 +7322,7 @@ var Conversation = class {
7212
7322
  params.turn = turn2 === void 0 ? this.turnText : turn2;
7213
7323
  const result2 = await this.client.context(params, requestOptions);
7214
7324
  this.features.observe(result2);
7215
- return this.state.observe(this.state.absorb(result2));
7325
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7216
7326
  }
7217
7327
  /**
7218
7328
  * Starts this conversation's first read now, in the background: call it when the call starts
@@ -7392,6 +7502,35 @@ function withHandles(base, extra) {
7392
7502
  return all;
7393
7503
  }
7394
7504
 
7505
+ // src/warm.ts
7506
+ var EVERY_MS = 1e5;
7507
+ var IDLE_MS = 9e4;
7508
+ var WARM_FOR_MS = 6e5;
7509
+ var KeepWarm = class {
7510
+ constructor(enabled) {
7511
+ this.enabled = enabled;
7512
+ }
7513
+ enabled;
7514
+ open = /* @__PURE__ */ new Map();
7515
+ openedAt = 0;
7516
+ /** Notes an open conversation or task. */
7517
+ add(scope, session, now) {
7518
+ if (!this.enabled || typeof WeakRef === "undefined") return;
7519
+ this.open.set(scope, new WeakRef(session));
7520
+ this.openedAt = now;
7521
+ }
7522
+ /** The conversation or task ended. */
7523
+ end(scope) {
7524
+ this.open.delete(scope);
7525
+ }
7526
+ step(now, lastActivityAt, idleMs = IDLE_MS, warmForMs = WARM_FOR_MS) {
7527
+ for (const [scope, ref] of this.open) if (ref.deref() === void 0) this.open.delete(scope);
7528
+ const used = Math.max(lastActivityAt ?? 0, this.openedAt);
7529
+ if (this.open.size === 0 || now - used > warmForMs) return "stop";
7530
+ return now - used >= idleMs ? "ping" : "wait";
7531
+ }
7532
+ };
7533
+
7395
7534
  // src/exit.ts
7396
7535
  var registered = /* @__PURE__ */ new Set();
7397
7536
  var installedOn = null;
@@ -7521,8 +7660,10 @@ var DEFAULT_TIMEOUTS = {
7521
7660
  write: 5e3,
7522
7661
  token: 2e3,
7523
7662
  upload: 6e4,
7524
- prefetch: 1e3
7663
+ prefetch: 1e3,
7664
+ connect: 1e3
7525
7665
  };
7666
+ var FETCH_KEEPALIVE_MS = 4e3;
7526
7667
  var DEFAULT_CACHE = {
7527
7668
  ttlMs: 1e4,
7528
7669
  staleWhileRevalidateMs: 10 * 6e4,
@@ -7629,7 +7770,7 @@ var Task = class {
7629
7770
  * Resolves with an empty result, never rejects, unless the client is strict.
7630
7771
  */
7631
7772
  async context(options = {}) {
7632
- const { query, format, explain, include, ...requestOptions } = options;
7773
+ const { query, format, explain: explain2, include, ...requestOptions } = options;
7633
7774
  const target = this.object ? { object: this.object } : this.params.subject ? { subject: this.params.subject } : {};
7634
7775
  const params = {
7635
7776
  ...target,
@@ -7639,18 +7780,18 @@ var Task = class {
7639
7780
  ...this.level ? { verification: this.level } : {},
7640
7781
  ...this.params.target ? { target: this.params.target } : {},
7641
7782
  ...format === "json" ? { format } : {},
7642
- ...explain ? { explain } : {},
7783
+ ...explain2 ? { explain: explain2 } : {},
7643
7784
  ...include?.length ? { include } : {}
7644
7785
  };
7645
7786
  if (query) {
7646
7787
  const answered = await this.client.context({ ...params, query }, requestOptions);
7647
7788
  this.features.observe(answered);
7648
- return this.state.observe(answered);
7789
+ return this.state.observe(this.state.sayUnlinked(answered, this.logger));
7649
7790
  }
7650
7791
  if (this.state.wantsDelta) params.delta = true;
7651
7792
  const result2 = await this.client.context(params, requestOptions);
7652
7793
  this.features.observe(result2);
7653
- return this.state.observe(this.state.absorb(result2));
7794
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7654
7795
  }
7655
7796
  /**
7656
7797
  * The agent's own working notes for this task's prompt, as `niadra.agentMemory()` with
@@ -7780,14 +7921,14 @@ var AGENT_MEMORY_TOOL_NAMES = {
7780
7921
  search: "search_agent_memory",
7781
7922
  remember: "remember"
7782
7923
  };
7783
- var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
7924
+ var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait", "system_event"];
7784
7925
  var NOTE_KINDS = ["procedure", "tool_note", "process_note", "pitfall"];
7785
7926
  var TOOL_DEFINITIONS = [
7786
7927
  {
7787
7928
  "type": "function",
7788
7929
  "function": {
7789
7930
  "name": "search_customer_history",
7790
- "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.",
7931
+ "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.",
7791
7932
  "parameters": {
7792
7933
  "type": "object",
7793
7934
  "properties": {
@@ -7831,7 +7972,8 @@ var TOOL_DEFINITIONS = [
7831
7972
  "open_item",
7832
7973
  "action",
7833
7974
  "object",
7834
- "trait"
7975
+ "trait",
7976
+ "system_event"
7835
7977
  ]
7836
7978
  }
7837
7979
  },
@@ -7858,7 +8000,7 @@ var TOOL_DEFINITIONS = [
7858
8000
  "type": "function",
7859
8001
  "function": {
7860
8002
  "name": "get_customer_timeline",
7861
- "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.",
8003
+ "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.",
7862
8004
  "parameters": {
7863
8005
  "type": "object",
7864
8006
  "properties": {
@@ -7906,7 +8048,8 @@ var TOOL_DEFINITIONS = [
7906
8048
  "open_item",
7907
8049
  "action",
7908
8050
  "object",
7909
- "trait"
8051
+ "trait",
8052
+ "system_event"
7910
8053
  ]
7911
8054
  }
7912
8055
  },
@@ -7925,7 +8068,7 @@ var TOOL_DEFINITIONS = [
7925
8068
  "type": "function",
7926
8069
  "function": {
7927
8070
  "name": "open_history_item",
7928
- "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.",
8071
+ "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.",
7929
8072
  "parameters": {
7930
8073
  "type": "object",
7931
8074
  "properties": {
@@ -8296,6 +8439,13 @@ function compose(body, read2) {
8296
8439
  const pack = body.pack ? { pack: { ...body.pack, slots: fetched?.pack?.slots ?? [] } } : {};
8297
8440
  return { ...body, slots: fetched?.slots ?? null, guards: fetched?.guards ?? [], ...pack };
8298
8441
  }
8442
+ var RTT_MARGIN_MS = 50;
8443
+ function budgetWarnings(rttMs, timeouts, explicit) {
8444
+ const ms = Math.round(rttMs);
8445
+ return [...explicit].sort().filter((name) => timeouts[name] < rttMs + RTT_MARGIN_MS).map(
8446
+ (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`
8447
+ );
8448
+ }
8299
8449
  function rttWarnings(rttMs, timeouts) {
8300
8450
  const ms = Math.round(rttMs);
8301
8451
  const found2 = [];
@@ -8333,6 +8483,12 @@ var Niadra = class _Niadra {
8333
8483
  enabled;
8334
8484
  core;
8335
8485
  timeouts;
8486
+ keepWarm;
8487
+ warmTimer;
8488
+ /** The read budgets the caller left at their defaults: they take the measured round trip on top. */
8489
+ defaultReads;
8490
+ voiceStarted = false;
8491
+ voiceWarned = false;
8336
8492
  strict;
8337
8493
  /** Where the client reports what it swallows in fail-open mode. */
8338
8494
  logger;
@@ -8377,6 +8533,8 @@ var Niadra = class _Niadra {
8377
8533
  this.strict = options.strict ?? false;
8378
8534
  this.logger = options.logger ?? consoleLogger;
8379
8535
  this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
8536
+ this.keepWarm = new KeepWarm(options.keepWarm ?? true);
8537
+ this.defaultReads = new Set(["context", "navigation"].filter((name) => options.timeouts?.[name] === void 0));
8380
8538
  this.voice = new VoiceLines(
8381
8539
  options.voice === false ? { ...DEFAULT_VOICE, enabled: false } : { ...DEFAULT_VOICE, ...options.voice }
8382
8540
  );
@@ -8402,13 +8560,14 @@ var Niadra = class _Niadra {
8402
8560
  this.coordinator = new Coordinator(
8403
8561
  this.outbox,
8404
8562
  this.suppressions,
8405
- (body, key2) => this.api.declare(body, { idempotency_key: key2 })
8563
+ (body, key2) => this.api.declare(body, { idempotency_key: key2 }),
8564
+ this.logger
8406
8565
  );
8407
8566
  this.states = new AgentStates(
8408
8567
  this.outbox,
8409
8568
  {
8410
- read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.timeouts.navigation }),
8411
- write: (write) => this.api.writeAgentState(write, { timeout: this.timeouts.navigation })
8569
+ read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.readBudget("navigation") }),
8570
+ write: (write) => this.api.writeAgentState(write, { timeout: this.readBudget("navigation") })
8412
8571
  },
8413
8572
  this.logger
8414
8573
  );
@@ -8435,6 +8594,7 @@ var Niadra = class _Niadra {
8435
8594
  this.core = setup;
8436
8595
  this.enabled = true;
8437
8596
  this.disabledReason = null;
8597
+ this.probe(setup);
8438
8598
  if (options.flushOnExit ?? true) this.unregisterExit = registerExitFlush(this);
8439
8599
  }
8440
8600
  setup(options) {
@@ -8456,7 +8616,10 @@ var Niadra = class _Niadra {
8456
8616
  baseURL,
8457
8617
  apiKey,
8458
8618
  fetch: fetchImpl,
8459
- defaultHeaders: options.defaultHeaders ?? {}
8619
+ defaultHeaders: options.defaultHeaders ?? {},
8620
+ logger: this.logger,
8621
+ coldAllowanceMs: this.timeouts.connect,
8622
+ keepAliveMs: options.keepAliveMs ?? FETCH_KEEPALIVE_MS
8460
8623
  });
8461
8624
  const queueOptions = { ...DEFAULT_QUEUE, ...options.queue };
8462
8625
  queueOptions.maxBatchSize = Math.min(queueOptions.maxBatchSize, 499);
@@ -8512,7 +8675,7 @@ var Niadra = class _Niadra {
8512
8675
  return result2;
8513
8676
  }
8514
8677
  readContext(core, params, request, options) {
8515
- const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
8678
+ const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8516
8679
  const { query: own2, ...pinned2 } = request;
8517
8680
  const query = turnText(own2 ?? params.turn);
8518
8681
  const voiceCache = this.voiceCache(core, pinned2, options.cache);
@@ -8528,7 +8691,7 @@ var Niadra = class _Niadra {
8528
8691
  */
8529
8692
  async profile() {
8530
8693
  if (!this.core) return null;
8531
- return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.timeouts.navigation }));
8694
+ return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.readBudget("navigation") }));
8532
8695
  }
8533
8696
  /**
8534
8697
  * Checks outputs against this claim contract instead of the one the profile serves (a company's own copy,
@@ -8553,7 +8716,7 @@ var Niadra = class _Niadra {
8553
8716
  */
8554
8717
  async mayContact(handle, purpose, options = {}) {
8555
8718
  if (this.core && this.suppressions.due()) {
8556
- const read2 = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.timeouts.navigation);
8719
+ const read2 = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.readBudget("navigation"));
8557
8720
  if (!this.suppressions.held) await read2;
8558
8721
  }
8559
8722
  const checkOptions = { channel: options.channel ?? null };
@@ -8563,8 +8726,8 @@ var Niadra = class _Niadra {
8563
8726
  readSuppressions(budgetMs) {
8564
8727
  return this.suppressions.read(
8565
8728
  {
8566
- salt: () => this.api.suppressionSalt({ timeout: this.timeouts.navigation }),
8567
- page: (cursor, limit2) => this.api.suppressions({ cursor, limit: limit2 }, { timeout: this.timeouts.navigation })
8729
+ salt: () => this.api.suppressionSalt({ timeout: this.readBudget("navigation") }),
8730
+ page: (cursor, limit2) => this.api.suppressions({ cursor, limit: limit2 }, { timeout: this.readBudget("navigation") })
8568
8731
  },
8569
8732
  budgetMs
8570
8733
  );
@@ -8643,6 +8806,7 @@ var Niadra = class _Niadra {
8643
8806
  }
8644
8807
  /** What a conversation or a task needs of its client for the agent features. */
8645
8808
  get agentHost() {
8809
+ const client = this;
8646
8810
  return {
8647
8811
  recorder: this.turns,
8648
8812
  coordinator: this.coordinator,
@@ -8656,7 +8820,10 @@ var Niadra = class _Niadra {
8656
8820
  claim: (request, timeoutMs) => this.api.claim(request, {}, { timeout: timeoutMs }),
8657
8821
  verifyClaim: (ref, field, value, options) => this.verifyClaim(ref, field, value, options),
8658
8822
  enabled: this.enabled,
8659
- navigationMs: this.timeouts.navigation
8823
+ strict: this.strict,
8824
+ get navigationMs() {
8825
+ return client.readBudget("navigation");
8826
+ }
8660
8827
  };
8661
8828
  }
8662
8829
  /**
@@ -8684,7 +8851,7 @@ var Niadra = class _Niadra {
8684
8851
  if (!cache) return false;
8685
8852
  const key2 = cacheKey(request);
8686
8853
  const line = this.voice.line(cacheScope(request) ?? "");
8687
- this.probe(core);
8854
+ this.startVoice(core);
8688
8855
  line.request = request;
8689
8856
  if (!cache.has(key2) && line.inFlight().length === 0) {
8690
8857
  this.voiceRead(core, cache, line, key2, request, null, this.timeouts.contextVoiceStart);
@@ -8821,13 +8988,13 @@ var Niadra = class _Niadra {
8821
8988
  if (!params.query || params.query.length > 2e3) {
8822
8989
  throw new NiadraValidationError("query must be 1 to 2000 characters");
8823
8990
  }
8824
- return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
8991
+ return this.readSpec("POST", "/v1/history/search", params, this.readBudget("navigation"), options);
8825
8992
  });
8826
8993
  }
8827
8994
  /** The customer's history, newest first, one line per item, paginated by cursor. */
8828
8995
  async timeline(params, options = {}) {
8829
8996
  return this.navigate(
8830
- () => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
8997
+ () => this.readSpec("POST", "/v1/history/timeline", params, this.readBudget("navigation"), options)
8831
8998
  );
8832
8999
  }
8833
9000
  /**
@@ -8840,9 +9007,10 @@ var Niadra = class _Niadra {
8840
9007
  if (!id) throw new NiadraValidationError("open() needs an item id");
8841
9008
  const body = { item_id: id };
8842
9009
  if (params.subject) body.subject = params.subject;
9010
+ if (params.about) body.about = params.about;
8843
9011
  if (params.verification) body.verification = params.verification;
8844
9012
  if (params.conversation_id) body.conversation_id = params.conversation_id;
8845
- return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
9013
+ return this.readSpec("POST", "/v1/history/open", body, this.readBudget("navigation"), options);
8846
9014
  });
8847
9015
  }
8848
9016
  /**
@@ -8854,7 +9022,7 @@ var Niadra = class _Niadra {
8854
9022
  * const { data: invoice } = await niadra.objectState("invoice:erp:0823");
8855
9023
  */
8856
9024
  async objectState(object, options = {}) {
8857
- return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.timeouts.navigation, options));
9025
+ return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.readBudget("navigation"), options));
8858
9026
  }
8859
9027
  /**
8860
9028
  * System events and agent actions about one object, newest first, one line each and never
@@ -8867,7 +9035,7 @@ var Niadra = class _Niadra {
8867
9035
  throw new NiadraValidationError("limit must be between 1 and 100");
8868
9036
  }
8869
9037
  const path = `${objectPath(object)}/timeline`;
8870
- const spec = this.readSpec("GET", path, void 0, this.timeouts.navigation, options);
9038
+ const spec = this.readSpec("GET", path, void 0, this.readBudget("navigation"), options);
8871
9039
  spec.query = { cursor: params.cursor, limit: String(limit2) };
8872
9040
  return spec;
8873
9041
  });
@@ -8886,6 +9054,7 @@ var Niadra = class _Niadra {
8886
9054
  timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
8887
9055
  open: (id, customer, bound, voice) => {
8888
9056
  const scope = { subject: customer };
9057
+ if (bound.about) scope.about = bound.about;
8889
9058
  if (bound.verification) scope.verification = bound.verification;
8890
9059
  if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
8891
9060
  return this.open(id, scope, this.voiceBudget(voice));
@@ -8920,7 +9089,7 @@ var Niadra = class _Niadra {
8920
9089
  const key2 = AgentMemoryCache.key(params);
8921
9090
  const fresh = cache?.fresh(key2);
8922
9091
  if (fresh) return blockResult(fresh, "cache");
8923
- const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
9092
+ const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8924
9093
  const spec = this.readSpec("GET", "/v1/agent-memory/block", void 0, timeout, options);
8925
9094
  spec.query = {
8926
9095
  max_tokens: String(params.max_tokens ?? 300),
@@ -8957,7 +9126,7 @@ var Niadra = class _Niadra {
8957
9126
  if (params.limit !== void 0) body.limit = params.limit;
8958
9127
  if (params.conversation_id) body.conversation_id = params.conversation_id;
8959
9128
  if (params.task_id) body.task_id = params.task_id;
8960
- return this.readSpec("POST", "/v1/agent-memory/search", body, this.timeouts.navigation, options);
9129
+ return this.readSpec("POST", "/v1/agent-memory/search", body, this.readBudget("navigation"), options);
8961
9130
  });
8962
9131
  return result2.error ? result2 : { data: result2.data.notes, error: null };
8963
9132
  }
@@ -9051,6 +9220,41 @@ var Niadra = class _Niadra {
9051
9220
  return { ok: false, idempotency_key: key2, error: this.swallow(error, "feedback") };
9052
9221
  }
9053
9222
  }
9223
+ /**
9224
+ * How the space's agents used the context they read (`GET /v1/context-use`): sessions, deliveries, use,
9225
+ * repetition, transfers and recontact, with intervals, grouped by `group_by`. A key of an `analyst` source
9226
+ * with the `analytics` scope reads every source of the space; a key with `admin` reads its own source.
9227
+ */
9228
+ contextUse(params = {}, options = {}) {
9229
+ return this.navigate(() => {
9230
+ const { group_by: groups, ...filters } = params;
9231
+ const query = { ...filters };
9232
+ if (groups?.length) query.group_by = groups;
9233
+ return { ...this.readSpec("GET", "/v1/context-use", void 0, this.timeouts.write, options), query };
9234
+ });
9235
+ }
9236
+ /**
9237
+ * Links a person to the organization they act for (an account or a partner), as a system of record that
9238
+ * knows who works for whom: a CRM, an HR system. Needs a key with the `identity:link` scope (or `admin`);
9239
+ * `can_see_contacts` needs `admin`. Reads with `about` reach the organization through the link.
9240
+ */
9241
+ link(params, options = {}) {
9242
+ const { idempotency_key: key2, ...rest } = params;
9243
+ const body = { can_see_contacts: false, method: "system_import", ...rest };
9244
+ return this.navigate(() => this.writeSpec("/v1/identity/links", body, key2 ?? uuidv7(), options));
9245
+ }
9246
+ /**
9247
+ * Ends a link, from `valid_to` (now when absent): the person no longer acts for the organization, and reads
9248
+ * with `about` for the pair go on with the person's own memory. Needs `identity:link` or `admin`.
9249
+ */
9250
+ endLink(linkId, params = {}, options = {}) {
9251
+ return this.navigate(() => {
9252
+ if (!linkId) throw new NiadraValidationError("endLink() needs a link id");
9253
+ const body = params.valid_to ? { valid_to: params.valid_to } : {};
9254
+ const path = `/v1/identity/links/${encodeURIComponent(linkId)}/end`;
9255
+ return this.writeSpec(path, body, params.idempotency_key ?? uuidv7(), options);
9256
+ });
9257
+ }
9054
9258
  /**
9055
9259
  * Up to 500 corrections in one call, each with its own idempotency key (minted when missing).
9056
9260
  * Resolves with `accepted`, `duplicates` for replayed keys and one error per refused item, by index.
@@ -9143,16 +9347,20 @@ var Niadra = class _Niadra {
9143
9347
  * emits `conversation.ended` when you call `end()`.
9144
9348
  */
9145
9349
  conversation(params) {
9146
- return new Conversation(this, params, {
9350
+ const conversation = new Conversation(this, params, {
9147
9351
  endConversation: (id) => this.endScope(buildConversationEnded(id), `conversation:${id}`)
9148
9352
  });
9353
+ this.warm(`conversation:${conversation.id}`, conversation);
9354
+ return conversation;
9149
9355
  }
9150
9356
  /** A helper for one internal-agent task: binds `task_id` to reads and writes and emits `task.ended`. */
9151
9357
  task(params) {
9152
- return new Task(this, params, {
9358
+ const task = new Task(this, params, {
9153
9359
  endTask: (id) => this.endScope(buildTaskEnded(id), `task:${id}`),
9154
9360
  verifyTask: (verify) => this.verifyWith(verify)
9155
9361
  });
9362
+ this.warm(`task:${task.id}`, task);
9363
+ return task;
9156
9364
  }
9157
9365
  /**
9158
9366
  * Sends every queued event and resolves when done. Call it before a serverless function
@@ -9172,6 +9380,8 @@ var Niadra = class _Niadra {
9172
9380
  */
9173
9381
  async shutdown() {
9174
9382
  this.unregisterExit();
9383
+ clearInterval(this.warmTimer);
9384
+ this.warmTimer = void 0;
9175
9385
  if (!this.core) return;
9176
9386
  await this.turnSender?.stop(this.timeouts.write);
9177
9387
  await this.outbox.stop(this.timeouts.write);
@@ -9275,9 +9485,9 @@ var Niadra = class _Niadra {
9275
9485
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", request, timeout, { signal, headers }));
9276
9486
  return normalizeContext(response.data);
9277
9487
  } catch (error) {
9278
- const refused2 = error instanceof NiadraAPIError && error.status === 404;
9488
+ const refused3 = error instanceof NiadraAPIError && error.status === 404;
9279
9489
  const left = timeout - (Date.now() - started);
9280
- if (!request.include?.length || !refused2 || left <= 0) throw error;
9490
+ if (!request.include?.length || !refused3 || left <= 0) throw error;
9281
9491
  for (const name of request.include) this.refusedBlocks.set(name, Date.now() + BLOCK_RECHECK_AFTER_MS);
9282
9492
  const { include: _dropped, ...plain2 } = request;
9283
9493
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", plain2, left, { signal, headers }));
@@ -9365,9 +9575,32 @@ var Niadra = class _Niadra {
9365
9575
  const rtt = Math.min(...samples);
9366
9576
  this.voice.rtt = rtt;
9367
9577
  this.logger.debug(`round trip to the region ${Math.round(rtt)} ms`);
9368
- for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9578
+ const explicit = ["context", "navigation"].filter((name) => !this.defaultReads.has(name));
9579
+ for (const warning of budgetWarnings(rtt, this.timeouts, explicit)) this.logger.warn(warning);
9580
+ if (this.voiceStarted) this.warnVoice();
9369
9581
  })();
9370
9582
  }
9583
+ /**
9584
+ * A read budget: one the caller left at its default is what the API may take, and the measured round trip
9585
+ * to the region goes on top, so an agent far from the region (Sao Paulo, 170 ms from us-east-2) is not
9586
+ * timed out by the network; one the caller set is a ceiling.
9587
+ */
9588
+ readBudget(name) {
9589
+ const rtt = this.voice.rtt;
9590
+ return rtt !== null && this.defaultReads.has(name) ? this.timeouts[name] + rtt : this.timeouts[name];
9591
+ }
9592
+ /** The client reads in voice: the voice budgets' warnings matter from now on. */
9593
+ startVoice(core) {
9594
+ this.voiceStarted = true;
9595
+ this.probe(core);
9596
+ this.warnVoice();
9597
+ }
9598
+ warnVoice() {
9599
+ const rtt = this.voice.rtt;
9600
+ if (this.voiceWarned || rtt === null) return;
9601
+ this.voiceWarned = true;
9602
+ for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9603
+ }
9371
9604
  /**
9372
9605
  * A voice turn: the pinned body from memory, and the slots of the read of its words when that
9373
9606
  * read lands within `timeout`. See `voice.ts`.
@@ -9377,7 +9610,7 @@ var Niadra = class _Niadra {
9377
9610
  const key2 = cacheKey(request);
9378
9611
  const deadline = Date.now() + timeout;
9379
9612
  const line = this.voice.line(scope);
9380
- this.probe(core);
9613
+ this.startVoice(core);
9381
9614
  line.request = request;
9382
9615
  const words2 = wordsOf(query);
9383
9616
  const background = this.timeouts.prefetch;
@@ -9563,8 +9796,39 @@ var Niadra = class _Niadra {
9563
9796
  core.queue.flushInBackground(true);
9564
9797
  });
9565
9798
  }
9799
+ /** Notes an open conversation or task; the first one starts the keep-warm timer (`warm.ts`). */
9800
+ warm(scope, session) {
9801
+ const core = this.core;
9802
+ if (!core || !this.keepWarm.enabled) return;
9803
+ this.keepWarm.add(scope, session, Date.now());
9804
+ if (this.warmTimer !== void 0) return;
9805
+ const timer = setInterval(() => {
9806
+ this.warmTick(core);
9807
+ }, EVERY_MS);
9808
+ timer.unref?.();
9809
+ this.warmTimer = timer;
9810
+ }
9811
+ warmTick(core) {
9812
+ const step = this.keepWarm.step(Date.now(), core.transport.lastActivityAt);
9813
+ if (step === "stop") {
9814
+ clearInterval(this.warmTimer);
9815
+ this.warmTimer = void 0;
9816
+ return;
9817
+ }
9818
+ if (step !== "ping") return;
9819
+ core.transport.request({
9820
+ method: "GET",
9821
+ path: "/healthz",
9822
+ timeoutMs: 2e3,
9823
+ retry: { kind: "read", maxAttempts: 1 },
9824
+ activity: false
9825
+ }).catch((error) => {
9826
+ this.logger.debug(`keep-warm ping failed: ${describe(toNiadraError(error))}`);
9827
+ });
9828
+ }
9566
9829
  async endScope(item, scope) {
9567
9830
  this.forgetScope(scope);
9831
+ this.keepWarm.end(scope);
9568
9832
  return this.sendNow(() => item);
9569
9833
  }
9570
9834
  async sendBatch(transport, items, maxAttempts, backoff) {
@@ -9869,7 +10133,7 @@ var Replayer = class {
9869
10133
  try {
9870
10134
  answer = await this.niadra.callRoute({ method: "POST", path: "/v1/scenario-runs", body, idempotencyKey: uuidv7() });
9871
10135
  } catch (error) {
9872
- if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused(scenarioIds, results);
10136
+ if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused2(scenarioIds, results);
9873
10137
  throw error;
9874
10138
  }
9875
10139
  let run = runOf(answer);
@@ -9972,7 +10236,7 @@ var Replayer = class {
9972
10236
  return result(scenarioId, turnId, caseId, run, "completed", { paraphrase: rephrased, assertions, divergent_calls: played.divergent, latency_ms: latency });
9973
10237
  }
9974
10238
  };
9975
- function refused(scenarioIds, results) {
10239
+ function refused2(scenarioIds, results) {
9976
10240
  const scenarios = scenarioIds.map((id) => {
9977
10241
  const mine = results.filter((r) => r.scenario_id === id);
9978
10242
  return {