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