@niadra/sdk 0.9.1 → 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 (86) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/ai-sdk.d.cts +3 -3
  3. package/dist/ai-sdk.d.ts +3 -3
  4. package/dist/anthropic.d.cts +4 -4
  5. package/dist/anthropic.d.ts +4 -4
  6. package/dist/bedrock.d.cts +4 -4
  7. package/dist/bedrock.d.ts +4 -4
  8. package/dist/cli.js +319 -96
  9. package/dist/cli.js.map +1 -1
  10. package/dist/{client-CWMxmJHs.d.cts → client-rmWhZJOx.d.cts} +190 -7
  11. package/dist/{client-CWMxmJHs.d.ts → client-rmWhZJOx.d.ts} +190 -7
  12. package/dist/cloudflare-agents.d.cts +3 -3
  13. package/dist/cloudflare-agents.d.ts +3 -3
  14. package/dist/elevenlabs.cjs +7 -5
  15. package/dist/elevenlabs.cjs.map +1 -1
  16. package/dist/elevenlabs.d.cts +5 -5
  17. package/dist/elevenlabs.d.ts +5 -5
  18. package/dist/elevenlabs.js +7 -5
  19. package/dist/elevenlabs.js.map +1 -1
  20. package/dist/genkit.d.cts +3 -3
  21. package/dist/genkit.d.ts +3 -3
  22. package/dist/google-adk.cjs +7 -5
  23. package/dist/google-adk.cjs.map +1 -1
  24. package/dist/google-adk.d.cts +3 -3
  25. package/dist/google-adk.d.ts +3 -3
  26. package/dist/google-adk.js +7 -5
  27. package/dist/google-adk.js.map +1 -1
  28. package/dist/google-genai.d.cts +4 -4
  29. package/dist/google-genai.d.ts +4 -4
  30. package/dist/index.cjs +329 -98
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +13 -5
  33. package/dist/index.d.ts +13 -5
  34. package/dist/index.js +329 -98
  35. package/dist/index.js.map +1 -1
  36. package/dist/{intercept-CGngQI8B.d.cts → intercept-C0XODsy9.d.cts} +1 -1
  37. package/dist/{intercept-Bv0nLpSw.d.ts → intercept-veMDudKV.d.ts} +1 -1
  38. package/dist/langchain.cjs.map +1 -1
  39. package/dist/langchain.d.cts +3 -3
  40. package/dist/langchain.d.ts +3 -3
  41. package/dist/langchain.js.map +1 -1
  42. package/dist/livekit.cjs +10 -2
  43. package/dist/livekit.cjs.map +1 -1
  44. package/dist/livekit.d.cts +3 -3
  45. package/dist/livekit.d.ts +3 -3
  46. package/dist/livekit.js +10 -2
  47. package/dist/livekit.js.map +1 -1
  48. package/dist/llamaindex.d.cts +3 -3
  49. package/dist/llamaindex.d.ts +3 -3
  50. package/dist/mastra.d.cts +3 -3
  51. package/dist/mastra.d.ts +3 -3
  52. package/dist/openai-agents.d.cts +3 -3
  53. package/dist/openai-agents.d.ts +3 -3
  54. package/dist/retell.cjs +7 -5
  55. package/dist/retell.cjs.map +1 -1
  56. package/dist/retell.d.cts +5 -5
  57. package/dist/retell.d.ts +5 -5
  58. package/dist/retell.js +7 -5
  59. package/dist/retell.js.map +1 -1
  60. package/dist/{shared-B4Ef2Wpg.d.ts → shared-Cs7h5C4Y.d.ts} +1 -1
  61. package/dist/{shared-Cq8mz-7m.d.cts → shared-zaTBum17.d.cts} +1 -1
  62. package/dist/strands.d.cts +3 -3
  63. package/dist/strands.d.ts +3 -3
  64. package/dist/twilio.cjs +10 -2
  65. package/dist/twilio.cjs.map +1 -1
  66. package/dist/twilio.d.cts +4 -4
  67. package/dist/twilio.d.ts +4 -4
  68. package/dist/twilio.js +10 -2
  69. package/dist/twilio.js.map +1 -1
  70. package/dist/vapi.cjs +7 -5
  71. package/dist/vapi.cjs.map +1 -1
  72. package/dist/vapi.d.cts +5 -5
  73. package/dist/vapi.d.ts +5 -5
  74. package/dist/vapi.js +7 -5
  75. package/dist/vapi.js.map +1 -1
  76. package/dist/voltagent.d.cts +3 -3
  77. package/dist/voltagent.d.ts +3 -3
  78. package/dist/{webhook-z2TjUzjv.d.ts → webhook-2V5QgnKI.d.ts} +1 -1
  79. package/dist/{webhook-jTx_SegV.d.cts → webhook-CX7nvQig.d.cts} +1 -1
  80. package/dist/whatsapp.cjs +10 -2
  81. package/dist/whatsapp.cjs.map +1 -1
  82. package/dist/whatsapp.d.cts +2 -2
  83. package/dist/whatsapp.d.ts +2 -2
  84. package/dist/whatsapp.js +10 -2
  85. package/dist/whatsapp.js.map +1 -1
  86. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,6 +4,46 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // src/ids.ts
8
+ var lastMs = -1;
9
+ var counter = 0;
10
+ function randomBytes(length) {
11
+ const bytes = new Uint8Array(length);
12
+ const webCrypto = globalThis.crypto;
13
+ if (webCrypto?.getRandomValues) {
14
+ webCrypto.getRandomValues(bytes);
15
+ } else {
16
+ for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
17
+ }
18
+ return bytes;
19
+ }
20
+ function uuidv7(now = Date.now()) {
21
+ let ms = now;
22
+ if (ms <= lastMs) {
23
+ ms = lastMs;
24
+ counter = counter + 1 & 4095;
25
+ if (counter === 0) ms += 1;
26
+ } else {
27
+ counter = (randomBytes(2)[0] ?? 0) & 127;
28
+ }
29
+ lastMs = ms;
30
+ const bytes = randomBytes(16);
31
+ const high = Math.floor(ms / 2 ** 16);
32
+ const low = ms % 2 ** 16;
33
+ bytes[0] = high >>> 24 & 255;
34
+ bytes[1] = high >>> 16 & 255;
35
+ bytes[2] = high >>> 8 & 255;
36
+ bytes[3] = high & 255;
37
+ bytes[4] = low >>> 8 & 255;
38
+ bytes[5] = low & 255;
39
+ bytes[6] = 112 | counter >>> 8 & 15;
40
+ bytes[7] = counter & 255;
41
+ bytes[8] = 128 | (bytes[8] ?? 0) & 63;
42
+ let hex2 = "";
43
+ for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
44
+ return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
45
+ }
46
+
7
47
  // src/errors.ts
8
48
  var NiadraError = class extends Error {
9
49
  name = "NiadraError";
@@ -47,11 +87,12 @@ var NiadraAPIError = class extends NiadraError {
47
87
  constructor(status, problem, requestId, retryAfterMs = null) {
48
88
  const code = problem?.code ?? `http_${status}`;
49
89
  const detail = problem?.detail ? `: ${problem.detail}` : "";
50
- super(`${status} ${code}${detail}`);
90
+ const id = requestId ?? problem?.request_id ?? null;
91
+ super(`${status} ${code}${detail}${id ? ` (request ${id})` : ""}`);
51
92
  this.status = status;
52
93
  this.code = code;
53
94
  this.problem = problem;
54
- this.requestId = requestId ?? problem?.request_id ?? null;
95
+ this.requestId = id;
55
96
  this.retryAfterMs = retryAfterMs;
56
97
  }
57
98
  };
@@ -75,45 +116,11 @@ function toNiadraError(error) {
75
116
  const message = error instanceof Error ? error.message : String(error);
76
117
  return new NiadraError(message, { cause: error });
77
118
  }
78
-
79
- // src/ids.ts
80
- var lastMs = -1;
81
- var counter = 0;
82
- function randomBytes(length) {
83
- const bytes = new Uint8Array(length);
84
- const webCrypto = globalThis.crypto;
85
- if (webCrypto?.getRandomValues) {
86
- webCrypto.getRandomValues(bytes);
87
- } else {
88
- for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
89
- }
90
- return bytes;
91
- }
92
- function uuidv7(now = Date.now()) {
93
- let ms = now;
94
- if (ms <= lastMs) {
95
- ms = lastMs;
96
- counter = counter + 1 & 4095;
97
- if (counter === 0) ms += 1;
98
- } else {
99
- counter = (randomBytes(2)[0] ?? 0) & 127;
100
- }
101
- lastMs = ms;
102
- const bytes = randomBytes(16);
103
- const high = Math.floor(ms / 2 ** 16);
104
- const low = ms % 2 ** 16;
105
- bytes[0] = high >>> 24 & 255;
106
- bytes[1] = high >>> 16 & 255;
107
- bytes[2] = high >>> 8 & 255;
108
- bytes[3] = high & 255;
109
- bytes[4] = low >>> 8 & 255;
110
- bytes[5] = low & 255;
111
- bytes[6] = 112 | counter >>> 8 & 15;
112
- bytes[7] = counter & 255;
113
- bytes[8] = 128 | (bytes[8] ?? 0) & 63;
114
- let hex2 = "";
115
- for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
116
- return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
119
+ function explain(error) {
120
+ if (!(error instanceof NiadraAPIError)) return error instanceof Error ? error.name : String(error);
121
+ const detail = error.problem?.detail?.trim();
122
+ const said = `${error.status} ${error.code}${detail ? `: ${detail.slice(0, 300)}` : ""}`;
123
+ return error.requestId ? `${said} (request ${error.requestId})` : said;
117
124
  }
118
125
 
119
126
  // src/admin.ts
@@ -516,7 +523,7 @@ function internalRecord(span, ref, act) {
516
523
  }
517
524
 
518
525
  // src/version.ts
519
- var VERSION = "0.9.1";
526
+ var VERSION = "0.10.3";
520
527
 
521
528
  // src/transport.ts
522
529
  var RETRYABLE_WRITE_STATUS = /* @__PURE__ */ new Set([408, 421, 429, 500, 502, 503, 504]);
@@ -529,8 +536,32 @@ var Transport = class {
529
536
  }
530
537
  config;
531
538
  baseURL;
539
+ answeredAt;
540
+ /** When the allowance was first given since the last answer; it holds for the calls of that moment only. */
541
+ grantedAt;
542
+ /** When the client last sent a request of its own (`RequestSpec.activity`), in epoch milliseconds. */
543
+ lastActivityAt;
532
544
  async request(spec) {
533
- return spec.retry.kind === "read" ? this.read(spec, spec.retry) : this.write(spec, spec.retry);
545
+ if (spec.activity !== false) this.lastActivityAt = Date.now();
546
+ const budgeted = this.withAllowance(spec);
547
+ return budgeted.retry.kind === "read" ? this.read(budgeted, budgeted.retry) : this.write(budgeted, budgeted.retry);
548
+ }
549
+ /**
550
+ * `spec` with the allowance for opening a connection, when it has a budget and none is likely open. The calls
551
+ * that start while the first one opens it get it too; after that, none does until an answer comes, so an
552
+ * outage costs the allowance once, not on every turn.
553
+ */
554
+ withAllowance(spec) {
555
+ const allowance = this.config.coldAllowanceMs ?? 0;
556
+ const { retry } = spec;
557
+ if (allowance <= 0 || retry.kind === "write" && retry.totalMs === void 0) return spec;
558
+ const now = Date.now();
559
+ const keep = this.config.keepAliveMs ?? 0;
560
+ if (this.answeredAt !== void 0 && now - this.answeredAt <= keep) return spec;
561
+ this.grantedAt ??= now;
562
+ if (now - this.grantedAt > allowance) return spec;
563
+ const total = retry.kind === "write" && retry.totalMs !== void 0 ? { ...retry, totalMs: retry.totalMs + allowance } : retry;
564
+ return { ...spec, timeoutMs: spec.timeoutMs + allowance, retry: total };
534
565
  }
535
566
  async read(spec, policy) {
536
567
  const deadline = new Deadline(spec.timeoutMs, spec.signal);
@@ -601,7 +632,18 @@ var Transport = class {
601
632
  headers["content-type"] = "application/json";
602
633
  init.body = JSON.stringify(spec.body);
603
634
  }
604
- return this.exchange(this.url(spec.path, spec.query), init, deadline);
635
+ try {
636
+ const answer = await this.exchange(this.url(spec.path, spec.query), init, deadline);
637
+ this.answered();
638
+ return answer;
639
+ } catch (error) {
640
+ if (error instanceof NiadraAPIError) this.answered();
641
+ throw error;
642
+ }
643
+ }
644
+ answered() {
645
+ this.answeredAt = Date.now();
646
+ this.grantedAt = void 0;
605
647
  }
606
648
  async exchange(url, init, deadline) {
607
649
  let response;
@@ -4042,8 +4084,7 @@ var TurnSender = class {
4042
4084
  return true;
4043
4085
  }
4044
4086
  }
4045
- const code = error instanceof NiadraAPIError ? error.code : toNiadraError(error).name;
4046
- this.recorder.rejected(batch.frames.length, [code]);
4087
+ this.recorder.rejected(batch.frames.length, [explain(error)]);
4047
4088
  return true;
4048
4089
  }
4049
4090
  accepted(batch, answer) {
@@ -4051,8 +4092,8 @@ var TurnSender = class {
4051
4092
  this.resumeAt = 0;
4052
4093
  this.recorder.sent(answer.accepted, answer.duplicates);
4053
4094
  const again = [];
4054
- const codes2 = /* @__PURE__ */ new Set();
4055
- let refused2 = 0;
4095
+ const reasons = /* @__PURE__ */ new Set();
4096
+ let refused3 = 0;
4056
4097
  for (const error of answer.errors ?? []) {
4057
4098
  const frame = batch.frames[error.index];
4058
4099
  if (frame === void 0) continue;
@@ -4061,12 +4102,12 @@ var TurnSender = class {
4061
4102
  this.recorder.modeRefused();
4062
4103
  again.push(frame);
4063
4104
  } else {
4064
- refused2++;
4065
- codes2.add(error.code);
4105
+ refused3++;
4106
+ reasons.add(error.detail ? `${error.code}: ${error.detail.slice(0, 300)}` : error.code);
4066
4107
  }
4067
4108
  }
4068
4109
  if (again.length > 0) this.queue.requeue(again);
4069
- if (refused2 > 0) this.recorder.rejected(refused2, [...codes2]);
4110
+ if (refused3 > 0) this.recorder.rejected(refused3, [...reasons]);
4070
4111
  }
4071
4112
  };
4072
4113
 
@@ -4188,9 +4229,12 @@ var TurnRecorder = class {
4188
4229
  this.accepted += accepted;
4189
4230
  this.duplicates += duplicates;
4190
4231
  }
4191
- rejected(count3, codes2) {
4232
+ /** `reasons`: the API's code and detail of each refusal (field paths and rules, never a value). */
4233
+ rejected(count3, reasons) {
4192
4234
  this.rejectedTurns += count3;
4193
- this.logger.warn(`${count3} turn records were refused (${[...codes2].sort().join(", ")})`);
4235
+ const shown = [...new Set(reasons)].sort();
4236
+ const more = shown.length > 3 ? ` (and ${shown.length - 3} more)` : "";
4237
+ this.logger.warn(`${count3} turn records were refused: ${shown.slice(0, 3).join("; ")}${more}`);
4194
4238
  }
4195
4239
  /** The space does not record turns: these are dropped, and with `off` (a 404) recording stops a while. */
4196
4240
  notRecorded(count3, off) {
@@ -4260,6 +4304,10 @@ async function hex(text2) {
4260
4304
  }
4261
4305
 
4262
4306
  // src/coordination/client.ts
4307
+ var REFUSED = /* @__PURE__ */ new Set([400, 401, 403, 422]);
4308
+ function refused(error) {
4309
+ return error instanceof NiadraAPIError && REFUSED.has(error.status);
4310
+ }
4263
4311
  var FAIL_CLOSED = /* @__PURE__ */ new Set(["marketing", "retention", "collection"]);
4264
4312
  var CHECK_BUDGET_MS = 200;
4265
4313
  function fallback(request, suppressed, failOpen) {
@@ -4281,15 +4329,22 @@ function claimed(data, error) {
4281
4329
  return { held: false, claim: null, error: code };
4282
4330
  }
4283
4331
  var Coordinator = class {
4284
- constructor(outbox, suppressions, declareNow) {
4332
+ constructor(outbox, suppressions, declareNow, logger) {
4285
4333
  this.outbox = outbox;
4286
4334
  this.suppressions = suppressions;
4287
4335
  this.declareNow = declareNow;
4336
+ this.logger = logger;
4288
4337
  }
4289
4338
  outbox;
4290
4339
  suppressions;
4291
4340
  declareNow;
4341
+ logger;
4292
4342
  async failed(request, failOpen, error) {
4343
+ if (refused(error)) {
4344
+ this.logger?.warn(`the coordination check was refused: ${explain(error)}`);
4345
+ const decision = request.direction === "inbound" ? "allow" : "defer";
4346
+ return recorded({ decision, decision_id: uuidv7(), reasons: ["invalid_request"], valid_for_s: 0 });
4347
+ }
4293
4348
  const off = error instanceof NiadraAPIError && error.status === 404;
4294
4349
  const suppressed = request.subject != null && request.direction === "outbound" ? !await this.suppressions.mayContact(request.subject, request.purpose, { channel: request.channel ?? null, failOpen: true }) : false;
4295
4350
  const plain2 = off ? { ...request, effect_key: null } : request;
@@ -4310,7 +4365,7 @@ var Coordinator = class {
4310
4365
  if (who.subject) body.subject = who.subject;
4311
4366
  if (who.object) body.object = who.object;
4312
4367
  const key2 = uuidv7();
4313
- this.outbox.put({ send: () => this.declareNow(body, key2) });
4368
+ this.outbox.put({ send: () => this.declareNow(body, key2), route: "POST /v1/coordination/declare" });
4314
4369
  return key2;
4315
4370
  }
4316
4371
  };
@@ -4829,7 +4884,7 @@ var Outbox = class {
4829
4884
  return false;
4830
4885
  }
4831
4886
  this.writes.shift();
4832
- this.logger.warn(`a write was refused (${error instanceof NiadraAPIError ? error.code : String(error)})`);
4887
+ this.logger.warn(`${write.route ?? "a write"} was refused: ${explain(error)}`);
4833
4888
  this.settle(write, void 0, error);
4834
4889
  return true;
4835
4890
  }
@@ -5719,9 +5774,17 @@ var handles = {
5719
5774
  appUserId: (value, options) => build("app_user_id", value, void 0, options),
5720
5775
  /** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */
5721
5776
  systemId: (value, system, options) => build("system_id", value, system, options),
5722
- /** An HMAC of a national document number; `country` is its ISO 3166-1 alpha-2 code. */
5777
+ /**
5778
+ * A person's national document, such as a CPF: `govIdHmac("529.982.247-25", "BR")`. Send the number
5779
+ * itself, never a hash of it: the server checks its check digits, then keeps only a keyed hash with your
5780
+ * space's secret and an encrypted copy, and shows it masked. `country` is its ISO 3166-1 alpha-2 code.
5781
+ */
5723
5782
  govIdHmac: (value, country, options) => build("gov_id_hmac", value, country, options),
5724
- /** An HMAC of a company registry number. Identifies an organization. */
5783
+ /**
5784
+ * A company's registry number, such as a CNPJ: `orgRegistryHmac("11.222.333/0001-81", "BR")`. As with
5785
+ * `govIdHmac`, send the number itself; the server checks it and keeps only a keyed hash. Identifies an
5786
+ * organization.
5787
+ */
5725
5788
  orgRegistryHmac: (value, country) => build("org_registry_hmac", value, country, { subjectKind: "account" }),
5726
5789
  /** An e-mail domain, such as `acme.com`. Identifies an organization. */
5727
5790
  emailDomain: (domain) => build("email_domain", domain, void 0, { subjectKind: "account" }),
@@ -6395,6 +6458,7 @@ var AgentSession = class {
6395
6458
  try {
6396
6459
  return this.host.coordinator.decided(await this.host.check(request, options.timeoutMs ?? CHECK_BUDGET_MS), request, this.checked);
6397
6460
  } catch (error) {
6461
+ if (this.host.strict && refused(error)) throw error;
6398
6462
  return this.host.coordinator.failed(request, options.failOpen, error);
6399
6463
  }
6400
6464
  }
@@ -7006,6 +7070,7 @@ var SessionState = class {
7006
7070
  /** The guards of the last read: they hold the answers to the turn they were written for. */
7007
7071
  guards = /* @__PURE__ */ new Map();
7008
7072
  lastReport = null;
7073
+ unlinkedSaid = false;
7009
7074
  /** After the first pack, every read also asks what changed since. */
7010
7075
  get wantsDelta() {
7011
7076
  return this.etag !== null;
@@ -7034,6 +7099,19 @@ var SessionState = class {
7034
7099
  this.last = absorbed;
7035
7100
  return absorbed;
7036
7101
  }
7102
+ /**
7103
+ * Once per session: the read named an organization (`about`) with no active link to the subject, so it went
7104
+ * on with the subject's own memory, never the organization's.
7105
+ */
7106
+ sayUnlinked(result2, logger) {
7107
+ if (result2.response?.about_unlinked && !this.unlinkedSaid) {
7108
+ this.unlinkedSaid = true;
7109
+ logger.warn(
7110
+ "`about` names an organization with no active link to the subject; the context is the subject's own, without that organization's memory. Create the link to read it."
7111
+ );
7112
+ }
7113
+ return result2;
7114
+ }
7037
7115
  /** The last backing check of an agent's answer. */
7038
7116
  get lastBacking() {
7039
7117
  return this.lastReport;
@@ -7281,7 +7359,7 @@ var Conversation = class {
7281
7359
  * `query` picks this read's slots by other words than the turn; the pack is the pinned one.
7282
7360
  */
7283
7361
  async context(options = {}) {
7284
- const { query, turn, format, explain, include, ...requestOptions } = options;
7362
+ const { query, turn, format, explain: explain2, include, ...requestOptions } = options;
7285
7363
  const params = {
7286
7364
  subject: this.subject,
7287
7365
  view: this.view,
@@ -7290,7 +7368,7 @@ var Conversation = class {
7290
7368
  ...this.params.about ? { about: this.params.about } : {},
7291
7369
  ...this.params.target ? { target: this.params.target } : {},
7292
7370
  ...format === "json" ? { format } : {},
7293
- ...explain ? { explain } : {}
7371
+ ...explain2 ? { explain: explain2 } : {}
7294
7372
  };
7295
7373
  if (query) params.query = query;
7296
7374
  if (include?.length) params.include = include;
@@ -7298,7 +7376,7 @@ var Conversation = class {
7298
7376
  params.turn = turn === void 0 ? this.turnText : turn;
7299
7377
  const result2 = await this.client.context(params, requestOptions);
7300
7378
  this.features.observe(result2);
7301
- return this.state.observe(this.state.absorb(result2));
7379
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7302
7380
  }
7303
7381
  /**
7304
7382
  * Starts this conversation's first read now, in the background: call it when the call starts
@@ -7478,6 +7556,35 @@ function withHandles(base, extra) {
7478
7556
  return all;
7479
7557
  }
7480
7558
 
7559
+ // src/warm.ts
7560
+ var EVERY_MS = 1e5;
7561
+ var IDLE_MS = 9e4;
7562
+ var WARM_FOR_MS = 6e5;
7563
+ var KeepWarm = class {
7564
+ constructor(enabled) {
7565
+ this.enabled = enabled;
7566
+ }
7567
+ enabled;
7568
+ open = /* @__PURE__ */ new Map();
7569
+ openedAt = 0;
7570
+ /** Notes an open conversation or task. */
7571
+ add(scope, session, now) {
7572
+ if (!this.enabled || typeof WeakRef === "undefined") return;
7573
+ this.open.set(scope, new WeakRef(session));
7574
+ this.openedAt = now;
7575
+ }
7576
+ /** The conversation or task ended. */
7577
+ end(scope) {
7578
+ this.open.delete(scope);
7579
+ }
7580
+ step(now, lastActivityAt, idleMs = IDLE_MS, warmForMs = WARM_FOR_MS) {
7581
+ for (const [scope, ref] of this.open) if (ref.deref() === void 0) this.open.delete(scope);
7582
+ const used = Math.max(lastActivityAt ?? 0, this.openedAt);
7583
+ if (this.open.size === 0 || now - used > warmForMs) return "stop";
7584
+ return now - used >= idleMs ? "ping" : "wait";
7585
+ }
7586
+ };
7587
+
7481
7588
  // src/exit.ts
7482
7589
  var registered = /* @__PURE__ */ new Set();
7483
7590
  var installedOn = null;
@@ -7608,8 +7715,10 @@ var DEFAULT_TIMEOUTS = {
7608
7715
  write: 5e3,
7609
7716
  token: 2e3,
7610
7717
  upload: 6e4,
7611
- prefetch: 1e3
7718
+ prefetch: 1e3,
7719
+ connect: 1e3
7612
7720
  };
7721
+ var FETCH_KEEPALIVE_MS = 4e3;
7613
7722
  var DEFAULT_CACHE = {
7614
7723
  ttlMs: 1e4,
7615
7724
  staleWhileRevalidateMs: 10 * 6e4,
@@ -7716,7 +7825,7 @@ var Task = class {
7716
7825
  * Resolves with an empty result, never rejects, unless the client is strict.
7717
7826
  */
7718
7827
  async context(options = {}) {
7719
- const { query, format, explain, include, ...requestOptions } = options;
7828
+ const { query, format, explain: explain2, include, ...requestOptions } = options;
7720
7829
  const target = this.object ? { object: this.object } : this.params.subject ? { subject: this.params.subject } : {};
7721
7830
  const params = {
7722
7831
  ...target,
@@ -7726,18 +7835,18 @@ var Task = class {
7726
7835
  ...this.level ? { verification: this.level } : {},
7727
7836
  ...this.params.target ? { target: this.params.target } : {},
7728
7837
  ...format === "json" ? { format } : {},
7729
- ...explain ? { explain } : {},
7838
+ ...explain2 ? { explain: explain2 } : {},
7730
7839
  ...include?.length ? { include } : {}
7731
7840
  };
7732
7841
  if (query) {
7733
7842
  const answered = await this.client.context({ ...params, query }, requestOptions);
7734
7843
  this.features.observe(answered);
7735
- return this.state.observe(answered);
7844
+ return this.state.observe(this.state.sayUnlinked(answered, this.logger));
7736
7845
  }
7737
7846
  if (this.state.wantsDelta) params.delta = true;
7738
7847
  const result2 = await this.client.context(params, requestOptions);
7739
7848
  this.features.observe(result2);
7740
- return this.state.observe(this.state.absorb(result2));
7849
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7741
7850
  }
7742
7851
  /**
7743
7852
  * The agent's own working notes for this task's prompt, as `niadra.agentMemory()` with
@@ -7867,14 +7976,14 @@ var AGENT_MEMORY_TOOL_NAMES = {
7867
7976
  search: "search_agent_memory",
7868
7977
  remember: "remember"
7869
7978
  };
7870
- var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
7979
+ var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait", "system_event"];
7871
7980
  var NOTE_KINDS = ["procedure", "tool_note", "process_note", "pitfall"];
7872
7981
  var TOOL_DEFINITIONS = [
7873
7982
  {
7874
7983
  "type": "function",
7875
7984
  "function": {
7876
7985
  "name": "search_customer_history",
7877
- "description": "Search everything that already happened with this customer: past conversations, promises, agent actions, orders and invoices. Use it when the customer refers to something earlier or asks whether a problem happened before. Do not call it when the answer is already in the context block, including its 'Do hist\xF3rico' line, which already counts recurrences. Returns short items with date, channel and outcome, plus a recurrence count. Open one with open_history_item.",
7986
+ "description": "Search everything that already happened with this customer: past conversations, promises, agent actions, orders and invoices. Use it when the customer refers to something earlier or asks whether a problem happened before. Do not call it when the answer is already in the context block, including its 'Hist\xF3rico' line, which already counts recurrences. Returns short items with date, channel and outcome, plus a recurrence count. Open a conversation (`episode:`) or an object (`object:`) with open_history_item; the other items are complete as listed.",
7878
7987
  "parameters": {
7879
7988
  "type": "object",
7880
7989
  "properties": {
@@ -7918,7 +8027,8 @@ var TOOL_DEFINITIONS = [
7918
8027
  "open_item",
7919
8028
  "action",
7920
8029
  "object",
7921
- "trait"
8030
+ "trait",
8031
+ "system_event"
7922
8032
  ]
7923
8033
  }
7924
8034
  },
@@ -7945,7 +8055,7 @@ var TOOL_DEFINITIONS = [
7945
8055
  "type": "function",
7946
8056
  "function": {
7947
8057
  "name": "get_customer_timeline",
7948
- "description": "List this customer's conversations and agent actions in order, newest first, one line each. Use it to leaf through the history when you do not know what to search for. Pass next_cursor to continue. Prefer search_customer_history for a specific question.",
8058
+ "description": "List this customer's conversations, agent actions and system events in order, newest first, one line each. Use it to leaf through the history when you do not know what to search for; filters.item_kinds also lists the open items, facts, patterns or objects it names. Pass next_cursor to continue. Prefer search_customer_history for a specific question.",
7949
8059
  "parameters": {
7950
8060
  "type": "object",
7951
8061
  "properties": {
@@ -7993,7 +8103,8 @@ var TOOL_DEFINITIONS = [
7993
8103
  "open_item",
7994
8104
  "action",
7995
8105
  "object",
7996
- "trait"
8106
+ "trait",
8107
+ "system_event"
7997
8108
  ]
7998
8109
  }
7999
8110
  },
@@ -8012,7 +8123,7 @@ var TOOL_DEFINITIONS = [
8012
8123
  "type": "function",
8013
8124
  "function": {
8014
8125
  "name": "open_history_item",
8015
- "description": "Open one conversation or business object returned by search_customer_history or get_customer_timeline: what was asked, what was promised and by whom, the outcome and what memory came from it. Use it only after a search or timeline pointed to the item.",
8126
+ "description": "Open one conversation (`episode:`) or business object (`object:`) returned by search_customer_history or get_customer_timeline: what was asked, what was promised and by whom, the outcome and what memory came from it. Actions, events, facts and promises are complete as listed and do not open. Use it only after a search or timeline pointed to the item.",
8016
8127
  "parameters": {
8017
8128
  "type": "object",
8018
8129
  "properties": {
@@ -8383,6 +8494,13 @@ function compose(body, read) {
8383
8494
  const pack = body.pack ? { pack: { ...body.pack, slots: fetched?.pack?.slots ?? [] } } : {};
8384
8495
  return { ...body, slots: fetched?.slots ?? null, guards: fetched?.guards ?? [], ...pack };
8385
8496
  }
8497
+ var RTT_MARGIN_MS = 50;
8498
+ function budgetWarnings(rttMs, timeouts, explicit) {
8499
+ const ms = Math.round(rttMs);
8500
+ return [...explicit].sort().filter((name) => timeouts[name] < rttMs + RTT_MARGIN_MS).map(
8501
+ (name) => `timeouts.${name} (${timeouts[name]} ms) is shorter than the round trip to the region (${ms} ms) plus ${RTT_MARGIN_MS} ms for the API: its reads will run out of time. Leave it at its default, which adds the measured round trip, or raise it`
8502
+ );
8503
+ }
8386
8504
  function rttWarnings(rttMs, timeouts) {
8387
8505
  const ms = Math.round(rttMs);
8388
8506
  const found2 = [];
@@ -8420,6 +8538,12 @@ var Niadra = class _Niadra {
8420
8538
  enabled;
8421
8539
  core;
8422
8540
  timeouts;
8541
+ keepWarm;
8542
+ warmTimer;
8543
+ /** The read budgets the caller left at their defaults: they take the measured round trip on top. */
8544
+ defaultReads;
8545
+ voiceStarted = false;
8546
+ voiceWarned = false;
8423
8547
  strict;
8424
8548
  /** Where the client reports what it swallows in fail-open mode. */
8425
8549
  logger;
@@ -8464,6 +8588,8 @@ var Niadra = class _Niadra {
8464
8588
  this.strict = options.strict ?? false;
8465
8589
  this.logger = options.logger ?? consoleLogger;
8466
8590
  this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
8591
+ this.keepWarm = new KeepWarm(options.keepWarm ?? true);
8592
+ this.defaultReads = new Set(["context", "navigation"].filter((name) => options.timeouts?.[name] === void 0));
8467
8593
  this.voice = new VoiceLines(
8468
8594
  options.voice === false ? { ...DEFAULT_VOICE, enabled: false } : { ...DEFAULT_VOICE, ...options.voice }
8469
8595
  );
@@ -8489,13 +8615,14 @@ var Niadra = class _Niadra {
8489
8615
  this.coordinator = new Coordinator(
8490
8616
  this.outbox,
8491
8617
  this.suppressions,
8492
- (body, key2) => this.api.declare(body, { idempotency_key: key2 })
8618
+ (body, key2) => this.api.declare(body, { idempotency_key: key2 }),
8619
+ this.logger
8493
8620
  );
8494
8621
  this.states = new AgentStates(
8495
8622
  this.outbox,
8496
8623
  {
8497
- read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.timeouts.navigation }),
8498
- write: (write) => this.api.writeAgentState(write, { timeout: this.timeouts.navigation })
8624
+ read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.readBudget("navigation") }),
8625
+ write: (write) => this.api.writeAgentState(write, { timeout: this.readBudget("navigation") })
8499
8626
  },
8500
8627
  this.logger
8501
8628
  );
@@ -8522,6 +8649,7 @@ var Niadra = class _Niadra {
8522
8649
  this.core = setup;
8523
8650
  this.enabled = true;
8524
8651
  this.disabledReason = null;
8652
+ this.probe(setup);
8525
8653
  if (options.flushOnExit ?? true) this.unregisterExit = registerExitFlush(this);
8526
8654
  }
8527
8655
  setup(options) {
@@ -8544,7 +8672,9 @@ var Niadra = class _Niadra {
8544
8672
  apiKey,
8545
8673
  fetch: fetchImpl,
8546
8674
  defaultHeaders: options.defaultHeaders ?? {},
8547
- logger: this.logger
8675
+ logger: this.logger,
8676
+ coldAllowanceMs: this.timeouts.connect,
8677
+ keepAliveMs: options.keepAliveMs ?? FETCH_KEEPALIVE_MS
8548
8678
  });
8549
8679
  const queueOptions = { ...DEFAULT_QUEUE, ...options.queue };
8550
8680
  queueOptions.maxBatchSize = Math.min(queueOptions.maxBatchSize, 499);
@@ -8600,7 +8730,7 @@ var Niadra = class _Niadra {
8600
8730
  return result2;
8601
8731
  }
8602
8732
  readContext(core, params, request, options) {
8603
- const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
8733
+ const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8604
8734
  const { query: own3, ...pinned2 } = request;
8605
8735
  const query = turnText(own3 ?? params.turn);
8606
8736
  const voiceCache = this.voiceCache(core, pinned2, options.cache);
@@ -8616,7 +8746,7 @@ var Niadra = class _Niadra {
8616
8746
  */
8617
8747
  async profile() {
8618
8748
  if (!this.core) return null;
8619
- return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.timeouts.navigation }));
8749
+ return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.readBudget("navigation") }));
8620
8750
  }
8621
8751
  /**
8622
8752
  * Checks outputs against this claim contract instead of the one the profile serves (a company's own copy,
@@ -8641,7 +8771,7 @@ var Niadra = class _Niadra {
8641
8771
  */
8642
8772
  async mayContact(handle, purpose, options = {}) {
8643
8773
  if (this.core && this.suppressions.due()) {
8644
- const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.timeouts.navigation);
8774
+ const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.readBudget("navigation"));
8645
8775
  if (!this.suppressions.held) await read;
8646
8776
  }
8647
8777
  const checkOptions = { channel: options.channel ?? null };
@@ -8651,8 +8781,8 @@ var Niadra = class _Niadra {
8651
8781
  readSuppressions(budgetMs) {
8652
8782
  return this.suppressions.read(
8653
8783
  {
8654
- salt: () => this.api.suppressionSalt({ timeout: this.timeouts.navigation }),
8655
- page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.timeouts.navigation })
8784
+ salt: () => this.api.suppressionSalt({ timeout: this.readBudget("navigation") }),
8785
+ page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.readBudget("navigation") })
8656
8786
  },
8657
8787
  budgetMs
8658
8788
  );
@@ -8731,6 +8861,7 @@ var Niadra = class _Niadra {
8731
8861
  }
8732
8862
  /** What a conversation or a task needs of its client for the agent features. */
8733
8863
  get agentHost() {
8864
+ const client = this;
8734
8865
  return {
8735
8866
  recorder: this.turns,
8736
8867
  coordinator: this.coordinator,
@@ -8744,7 +8875,10 @@ var Niadra = class _Niadra {
8744
8875
  claim: (request, timeoutMs) => this.api.claim(request, {}, { timeout: timeoutMs }),
8745
8876
  verifyClaim: (ref, field, value, options) => this.verifyClaim(ref, field, value, options),
8746
8877
  enabled: this.enabled,
8747
- navigationMs: this.timeouts.navigation
8878
+ strict: this.strict,
8879
+ get navigationMs() {
8880
+ return client.readBudget("navigation");
8881
+ }
8748
8882
  };
8749
8883
  }
8750
8884
  /**
@@ -8772,7 +8906,7 @@ var Niadra = class _Niadra {
8772
8906
  if (!cache) return false;
8773
8907
  const key2 = cacheKey(request);
8774
8908
  const line = this.voice.line(cacheScope(request) ?? "");
8775
- this.probe(core);
8909
+ this.startVoice(core);
8776
8910
  line.request = request;
8777
8911
  if (!cache.has(key2) && line.inFlight().length === 0) {
8778
8912
  this.voiceRead(core, cache, line, key2, request, null, this.timeouts.contextVoiceStart);
@@ -8909,13 +9043,13 @@ var Niadra = class _Niadra {
8909
9043
  if (!params.query || params.query.length > 2e3) {
8910
9044
  throw new NiadraValidationError("query must be 1 to 2000 characters");
8911
9045
  }
8912
- return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
9046
+ return this.readSpec("POST", "/v1/history/search", params, this.readBudget("navigation"), options);
8913
9047
  });
8914
9048
  }
8915
9049
  /** The customer's history, newest first, one line per item, paginated by cursor. */
8916
9050
  async timeline(params, options = {}) {
8917
9051
  return this.navigate(
8918
- () => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
9052
+ () => this.readSpec("POST", "/v1/history/timeline", params, this.readBudget("navigation"), options)
8919
9053
  );
8920
9054
  }
8921
9055
  /**
@@ -8928,9 +9062,10 @@ var Niadra = class _Niadra {
8928
9062
  if (!id) throw new NiadraValidationError("open() needs an item id");
8929
9063
  const body = { item_id: id };
8930
9064
  if (params.subject) body.subject = params.subject;
9065
+ if (params.about) body.about = params.about;
8931
9066
  if (params.verification) body.verification = params.verification;
8932
9067
  if (params.conversation_id) body.conversation_id = params.conversation_id;
8933
- return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
9068
+ return this.readSpec("POST", "/v1/history/open", body, this.readBudget("navigation"), options);
8934
9069
  });
8935
9070
  }
8936
9071
  /**
@@ -8942,7 +9077,7 @@ var Niadra = class _Niadra {
8942
9077
  * const { data: invoice } = await niadra.objectState("invoice:erp:0823");
8943
9078
  */
8944
9079
  async objectState(object, options = {}) {
8945
- return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.timeouts.navigation, options));
9080
+ return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.readBudget("navigation"), options));
8946
9081
  }
8947
9082
  /**
8948
9083
  * System events and agent actions about one object, newest first, one line each and never
@@ -8955,7 +9090,7 @@ var Niadra = class _Niadra {
8955
9090
  throw new NiadraValidationError("limit must be between 1 and 100");
8956
9091
  }
8957
9092
  const path = `${objectPath(object)}/timeline`;
8958
- const spec = this.readSpec("GET", path, void 0, this.timeouts.navigation, options);
9093
+ const spec = this.readSpec("GET", path, void 0, this.readBudget("navigation"), options);
8959
9094
  spec.query = { cursor: params.cursor, limit: String(limit3) };
8960
9095
  return spec;
8961
9096
  });
@@ -8974,6 +9109,7 @@ var Niadra = class _Niadra {
8974
9109
  timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
8975
9110
  open: (id, customer, bound, voice) => {
8976
9111
  const scope = { subject: customer };
9112
+ if (bound.about) scope.about = bound.about;
8977
9113
  if (bound.verification) scope.verification = bound.verification;
8978
9114
  if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
8979
9115
  return this.open(id, scope, this.voiceBudget(voice));
@@ -9008,7 +9144,7 @@ var Niadra = class _Niadra {
9008
9144
  const key2 = AgentMemoryCache.key(params);
9009
9145
  const fresh = cache?.fresh(key2);
9010
9146
  if (fresh) return blockResult(fresh, "cache");
9011
- const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
9147
+ const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
9012
9148
  const spec = this.readSpec("GET", "/v1/agent-memory/block", void 0, timeout, options);
9013
9149
  spec.query = {
9014
9150
  max_tokens: String(params.max_tokens ?? 300),
@@ -9045,7 +9181,7 @@ var Niadra = class _Niadra {
9045
9181
  if (params.limit !== void 0) body.limit = params.limit;
9046
9182
  if (params.conversation_id) body.conversation_id = params.conversation_id;
9047
9183
  if (params.task_id) body.task_id = params.task_id;
9048
- return this.readSpec("POST", "/v1/agent-memory/search", body, this.timeouts.navigation, options);
9184
+ return this.readSpec("POST", "/v1/agent-memory/search", body, this.readBudget("navigation"), options);
9049
9185
  });
9050
9186
  return result2.error ? result2 : { data: result2.data.notes, error: null };
9051
9187
  }
@@ -9139,6 +9275,41 @@ var Niadra = class _Niadra {
9139
9275
  return { ok: false, idempotency_key: key2, error: this.swallow(error, "feedback") };
9140
9276
  }
9141
9277
  }
9278
+ /**
9279
+ * How the space's agents used the context they read (`GET /v1/context-use`): sessions, deliveries, use,
9280
+ * repetition, transfers and recontact, with intervals, grouped by `group_by`. A key of an `analyst` source
9281
+ * with the `analytics` scope reads every source of the space; a key with `admin` reads its own source.
9282
+ */
9283
+ contextUse(params = {}, options = {}) {
9284
+ return this.navigate(() => {
9285
+ const { group_by: groups, ...filters } = params;
9286
+ const query = { ...filters };
9287
+ if (groups?.length) query.group_by = groups;
9288
+ return { ...this.readSpec("GET", "/v1/context-use", void 0, this.timeouts.write, options), query };
9289
+ });
9290
+ }
9291
+ /**
9292
+ * Links a person to the organization they act for (an account or a partner), as a system of record that
9293
+ * knows who works for whom: a CRM, an HR system. Needs a key with the `identity:link` scope (or `admin`);
9294
+ * `can_see_contacts` needs `admin`. Reads with `about` reach the organization through the link.
9295
+ */
9296
+ link(params, options = {}) {
9297
+ const { idempotency_key: key2, ...rest } = params;
9298
+ const body = { can_see_contacts: false, method: "system_import", ...rest };
9299
+ return this.navigate(() => this.writeSpec("/v1/identity/links", body, key2 ?? uuidv7(), options));
9300
+ }
9301
+ /**
9302
+ * Ends a link, from `valid_to` (now when absent): the person no longer acts for the organization, and reads
9303
+ * with `about` for the pair go on with the person's own memory. Needs `identity:link` or `admin`.
9304
+ */
9305
+ endLink(linkId, params = {}, options = {}) {
9306
+ return this.navigate(() => {
9307
+ if (!linkId) throw new NiadraValidationError("endLink() needs a link id");
9308
+ const body = params.valid_to ? { valid_to: params.valid_to } : {};
9309
+ const path = `/v1/identity/links/${encodeURIComponent(linkId)}/end`;
9310
+ return this.writeSpec(path, body, params.idempotency_key ?? uuidv7(), options);
9311
+ });
9312
+ }
9142
9313
  /**
9143
9314
  * Up to 500 corrections in one call, each with its own idempotency key (minted when missing).
9144
9315
  * Resolves with `accepted`, `duplicates` for replayed keys and one error per refused item, by index.
@@ -9231,16 +9402,20 @@ var Niadra = class _Niadra {
9231
9402
  * emits `conversation.ended` when you call `end()`.
9232
9403
  */
9233
9404
  conversation(params) {
9234
- return new Conversation(this, params, {
9405
+ const conversation = new Conversation(this, params, {
9235
9406
  endConversation: (id) => this.endScope(buildConversationEnded(id), `conversation:${id}`)
9236
9407
  });
9408
+ this.warm(`conversation:${conversation.id}`, conversation);
9409
+ return conversation;
9237
9410
  }
9238
9411
  /** A helper for one internal-agent task: binds `task_id` to reads and writes and emits `task.ended`. */
9239
9412
  task(params) {
9240
- return new Task(this, params, {
9413
+ const task = new Task(this, params, {
9241
9414
  endTask: (id) => this.endScope(buildTaskEnded(id), `task:${id}`),
9242
9415
  verifyTask: (verify) => this.verifyWith(verify)
9243
9416
  });
9417
+ this.warm(`task:${task.id}`, task);
9418
+ return task;
9244
9419
  }
9245
9420
  /**
9246
9421
  * Sends every queued event and resolves when done. Call it before a serverless function
@@ -9260,6 +9435,8 @@ var Niadra = class _Niadra {
9260
9435
  */
9261
9436
  async shutdown() {
9262
9437
  this.unregisterExit();
9438
+ clearInterval(this.warmTimer);
9439
+ this.warmTimer = void 0;
9263
9440
  if (!this.core) return;
9264
9441
  await this.turnSender?.stop(this.timeouts.write);
9265
9442
  await this.outbox.stop(this.timeouts.write);
@@ -9363,9 +9540,9 @@ var Niadra = class _Niadra {
9363
9540
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", request, timeout, { signal, headers }));
9364
9541
  return normalizeContext(response.data);
9365
9542
  } catch (error) {
9366
- const refused2 = error instanceof NiadraAPIError && error.status === 404;
9543
+ const refused3 = error instanceof NiadraAPIError && error.status === 404;
9367
9544
  const left = timeout - (Date.now() - started);
9368
- if (!request.include?.length || !refused2 || left <= 0) throw error;
9545
+ if (!request.include?.length || !refused3 || left <= 0) throw error;
9369
9546
  for (const name of request.include) this.refusedBlocks.set(name, Date.now() + BLOCK_RECHECK_AFTER_MS);
9370
9547
  const { include: _dropped, ...plain2 } = request;
9371
9548
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", plain2, left, { signal, headers }));
@@ -9453,9 +9630,32 @@ var Niadra = class _Niadra {
9453
9630
  const rtt = Math.min(...samples);
9454
9631
  this.voice.rtt = rtt;
9455
9632
  this.logger.debug(`round trip to the region ${Math.round(rtt)} ms`);
9456
- for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9633
+ const explicit = ["context", "navigation"].filter((name) => !this.defaultReads.has(name));
9634
+ for (const warning of budgetWarnings(rtt, this.timeouts, explicit)) this.logger.warn(warning);
9635
+ if (this.voiceStarted) this.warnVoice();
9457
9636
  })();
9458
9637
  }
9638
+ /**
9639
+ * A read budget: one the caller left at its default is what the API may take, and the measured round trip
9640
+ * to the region goes on top, so an agent far from the region (Sao Paulo, 170 ms from us-east-2) is not
9641
+ * timed out by the network; one the caller set is a ceiling.
9642
+ */
9643
+ readBudget(name) {
9644
+ const rtt = this.voice.rtt;
9645
+ return rtt !== null && this.defaultReads.has(name) ? this.timeouts[name] + rtt : this.timeouts[name];
9646
+ }
9647
+ /** The client reads in voice: the voice budgets' warnings matter from now on. */
9648
+ startVoice(core) {
9649
+ this.voiceStarted = true;
9650
+ this.probe(core);
9651
+ this.warnVoice();
9652
+ }
9653
+ warnVoice() {
9654
+ const rtt = this.voice.rtt;
9655
+ if (this.voiceWarned || rtt === null) return;
9656
+ this.voiceWarned = true;
9657
+ for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9658
+ }
9459
9659
  /**
9460
9660
  * A voice turn: the pinned body from memory, and the slots of the read of its words when that
9461
9661
  * read lands within `timeout`. See `voice.ts`.
@@ -9465,7 +9665,7 @@ var Niadra = class _Niadra {
9465
9665
  const key2 = cacheKey(request);
9466
9666
  const deadline = Date.now() + timeout;
9467
9667
  const line = this.voice.line(scope);
9468
- this.probe(core);
9668
+ this.startVoice(core);
9469
9669
  line.request = request;
9470
9670
  const words2 = wordsOf(query);
9471
9671
  const background = this.timeouts.prefetch;
@@ -9651,8 +9851,39 @@ var Niadra = class _Niadra {
9651
9851
  core.queue.flushInBackground(true);
9652
9852
  });
9653
9853
  }
9854
+ /** Notes an open conversation or task; the first one starts the keep-warm timer (`warm.ts`). */
9855
+ warm(scope, session) {
9856
+ const core = this.core;
9857
+ if (!core || !this.keepWarm.enabled) return;
9858
+ this.keepWarm.add(scope, session, Date.now());
9859
+ if (this.warmTimer !== void 0) return;
9860
+ const timer = setInterval(() => {
9861
+ this.warmTick(core);
9862
+ }, EVERY_MS);
9863
+ timer.unref?.();
9864
+ this.warmTimer = timer;
9865
+ }
9866
+ warmTick(core) {
9867
+ const step = this.keepWarm.step(Date.now(), core.transport.lastActivityAt);
9868
+ if (step === "stop") {
9869
+ clearInterval(this.warmTimer);
9870
+ this.warmTimer = void 0;
9871
+ return;
9872
+ }
9873
+ if (step !== "ping") return;
9874
+ core.transport.request({
9875
+ method: "GET",
9876
+ path: "/healthz",
9877
+ timeoutMs: 2e3,
9878
+ retry: { kind: "read", maxAttempts: 1 },
9879
+ activity: false
9880
+ }).catch((error) => {
9881
+ this.logger.debug(`keep-warm ping failed: ${describe(toNiadraError(error))}`);
9882
+ });
9883
+ }
9654
9884
  async endScope(item, scope) {
9655
9885
  this.forgetScope(scope);
9886
+ this.keepWarm.end(scope);
9656
9887
  return this.sendNow(() => item);
9657
9888
  }
9658
9889
  async sendBatch(transport, items2, maxAttempts, backoff) {
@@ -11507,7 +11738,7 @@ var Replayer = class {
11507
11738
  try {
11508
11739
  answer = await this.niadra.callRoute({ method: "POST", path: "/v1/scenario-runs", body, idempotencyKey: uuidv7() });
11509
11740
  } catch (error) {
11510
- if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused(scenarioIds, results);
11741
+ if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused2(scenarioIds, results);
11511
11742
  throw error;
11512
11743
  }
11513
11744
  let run = runOf(answer);
@@ -11610,7 +11841,7 @@ var Replayer = class {
11610
11841
  return result(scenarioId, turnId, caseId, run, "completed", { paraphrase: rephrased, assertions, divergent_calls: played.divergent, latency_ms: latency });
11611
11842
  }
11612
11843
  };
11613
- function refused(scenarioIds, results) {
11844
+ function refused2(scenarioIds, results) {
11614
11845
  const scenarios = scenarioIds.map((id) => {
11615
11846
  const mine = results.filter((r) => r.scenario_id === id);
11616
11847
  return {