@niadra/sdk 0.9.1 → 0.10.4

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 +69 -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 +331 -96
  9. package/dist/cli.js.map +1 -1
  10. package/dist/{client-CWMxmJHs.d.cts → client-7mkrdRI0.d.cts} +192 -7
  11. package/dist/{client-CWMxmJHs.d.ts → client-7mkrdRI0.d.ts} +192 -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 +341 -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 +341 -98
  35. package/dist/index.js.map +1 -1
  36. package/dist/{intercept-CGngQI8B.d.cts → intercept-CGQhwAom.d.cts} +1 -1
  37. package/dist/{intercept-Bv0nLpSw.d.ts → intercept-CT0vglsd.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-Bf84jr3Y.d.ts} +1 -1
  61. package/dist/{shared-Cq8mz-7m.d.cts → shared-CxAwOcZI.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-jTx_SegV.d.cts → webhook-DE3HsGkb.d.cts} +1 -1
  79. package/dist/{webhook-z2TjUzjv.d.ts → webhook-QxYgxE6_.d.ts} +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.4";
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,36 @@ 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;
544
+ /** Likely: an answer came within `keepAliveMs`. When not, the next budgeted call gets `coldAllowanceMs`. */
545
+ connectionOpen() {
546
+ return this.answeredAt !== void 0 && Date.now() - this.answeredAt <= (this.config.keepAliveMs ?? 0);
547
+ }
532
548
  async request(spec) {
533
- return spec.retry.kind === "read" ? this.read(spec, spec.retry) : this.write(spec, spec.retry);
549
+ if (spec.activity !== false) this.lastActivityAt = Date.now();
550
+ const budgeted = this.withAllowance(spec);
551
+ return budgeted.retry.kind === "read" ? this.read(budgeted, budgeted.retry) : this.write(budgeted, budgeted.retry);
552
+ }
553
+ /**
554
+ * `spec` with the allowance for opening a connection, when it has a budget and none is likely open. The calls
555
+ * that start while the first one opens it get it too; after that, none does until an answer comes, so an
556
+ * outage costs the allowance once, not on every turn.
557
+ */
558
+ withAllowance(spec) {
559
+ const allowance = this.config.coldAllowanceMs ?? 0;
560
+ const { retry } = spec;
561
+ if (allowance <= 0 || retry.kind === "write" && retry.totalMs === void 0) return spec;
562
+ const now = Date.now();
563
+ const keep = this.config.keepAliveMs ?? 0;
564
+ if (this.answeredAt !== void 0 && now - this.answeredAt <= keep) return spec;
565
+ this.grantedAt ??= now;
566
+ if (now - this.grantedAt > allowance) return spec;
567
+ const total = retry.kind === "write" && retry.totalMs !== void 0 ? { ...retry, totalMs: retry.totalMs + allowance } : retry;
568
+ return { ...spec, timeoutMs: spec.timeoutMs + allowance, retry: total };
534
569
  }
535
570
  async read(spec, policy) {
536
571
  const deadline = new Deadline(spec.timeoutMs, spec.signal);
@@ -601,7 +636,18 @@ var Transport = class {
601
636
  headers["content-type"] = "application/json";
602
637
  init.body = JSON.stringify(spec.body);
603
638
  }
604
- return this.exchange(this.url(spec.path, spec.query), init, deadline);
639
+ try {
640
+ const answer = await this.exchange(this.url(spec.path, spec.query), init, deadline);
641
+ this.answered();
642
+ return answer;
643
+ } catch (error) {
644
+ if (error instanceof NiadraAPIError) this.answered();
645
+ throw error;
646
+ }
647
+ }
648
+ answered() {
649
+ this.answeredAt = Date.now();
650
+ this.grantedAt = void 0;
605
651
  }
606
652
  async exchange(url, init, deadline) {
607
653
  let response;
@@ -4042,8 +4088,7 @@ var TurnSender = class {
4042
4088
  return true;
4043
4089
  }
4044
4090
  }
4045
- const code = error instanceof NiadraAPIError ? error.code : toNiadraError(error).name;
4046
- this.recorder.rejected(batch.frames.length, [code]);
4091
+ this.recorder.rejected(batch.frames.length, [explain(error)]);
4047
4092
  return true;
4048
4093
  }
4049
4094
  accepted(batch, answer) {
@@ -4051,8 +4096,8 @@ var TurnSender = class {
4051
4096
  this.resumeAt = 0;
4052
4097
  this.recorder.sent(answer.accepted, answer.duplicates);
4053
4098
  const again = [];
4054
- const codes2 = /* @__PURE__ */ new Set();
4055
- let refused2 = 0;
4099
+ const reasons = /* @__PURE__ */ new Set();
4100
+ let refused3 = 0;
4056
4101
  for (const error of answer.errors ?? []) {
4057
4102
  const frame = batch.frames[error.index];
4058
4103
  if (frame === void 0) continue;
@@ -4061,12 +4106,12 @@ var TurnSender = class {
4061
4106
  this.recorder.modeRefused();
4062
4107
  again.push(frame);
4063
4108
  } else {
4064
- refused2++;
4065
- codes2.add(error.code);
4109
+ refused3++;
4110
+ reasons.add(error.detail ? `${error.code}: ${error.detail.slice(0, 300)}` : error.code);
4066
4111
  }
4067
4112
  }
4068
4113
  if (again.length > 0) this.queue.requeue(again);
4069
- if (refused2 > 0) this.recorder.rejected(refused2, [...codes2]);
4114
+ if (refused3 > 0) this.recorder.rejected(refused3, [...reasons]);
4070
4115
  }
4071
4116
  };
4072
4117
 
@@ -4188,9 +4233,12 @@ var TurnRecorder = class {
4188
4233
  this.accepted += accepted;
4189
4234
  this.duplicates += duplicates;
4190
4235
  }
4191
- rejected(count3, codes2) {
4236
+ /** `reasons`: the API's code and detail of each refusal (field paths and rules, never a value). */
4237
+ rejected(count3, reasons) {
4192
4238
  this.rejectedTurns += count3;
4193
- this.logger.warn(`${count3} turn records were refused (${[...codes2].sort().join(", ")})`);
4239
+ const shown = [...new Set(reasons)].sort();
4240
+ const more = shown.length > 3 ? ` (and ${shown.length - 3} more)` : "";
4241
+ this.logger.warn(`${count3} turn records were refused: ${shown.slice(0, 3).join("; ")}${more}`);
4194
4242
  }
4195
4243
  /** The space does not record turns: these are dropped, and with `off` (a 404) recording stops a while. */
4196
4244
  notRecorded(count3, off) {
@@ -4260,6 +4308,10 @@ async function hex(text2) {
4260
4308
  }
4261
4309
 
4262
4310
  // src/coordination/client.ts
4311
+ var REFUSED = /* @__PURE__ */ new Set([400, 401, 403, 422]);
4312
+ function refused(error) {
4313
+ return error instanceof NiadraAPIError && REFUSED.has(error.status);
4314
+ }
4263
4315
  var FAIL_CLOSED = /* @__PURE__ */ new Set(["marketing", "retention", "collection"]);
4264
4316
  var CHECK_BUDGET_MS = 200;
4265
4317
  function fallback(request, suppressed, failOpen) {
@@ -4281,15 +4333,22 @@ function claimed(data, error) {
4281
4333
  return { held: false, claim: null, error: code };
4282
4334
  }
4283
4335
  var Coordinator = class {
4284
- constructor(outbox, suppressions, declareNow) {
4336
+ constructor(outbox, suppressions, declareNow, logger) {
4285
4337
  this.outbox = outbox;
4286
4338
  this.suppressions = suppressions;
4287
4339
  this.declareNow = declareNow;
4340
+ this.logger = logger;
4288
4341
  }
4289
4342
  outbox;
4290
4343
  suppressions;
4291
4344
  declareNow;
4345
+ logger;
4292
4346
  async failed(request, failOpen, error) {
4347
+ if (refused(error)) {
4348
+ this.logger?.warn(`the coordination check was refused: ${explain(error)}`);
4349
+ const decision = request.direction === "inbound" ? "allow" : "defer";
4350
+ return recorded({ decision, decision_id: uuidv7(), reasons: ["invalid_request"], valid_for_s: 0 });
4351
+ }
4293
4352
  const off = error instanceof NiadraAPIError && error.status === 404;
4294
4353
  const suppressed = request.subject != null && request.direction === "outbound" ? !await this.suppressions.mayContact(request.subject, request.purpose, { channel: request.channel ?? null, failOpen: true }) : false;
4295
4354
  const plain2 = off ? { ...request, effect_key: null } : request;
@@ -4310,7 +4369,7 @@ var Coordinator = class {
4310
4369
  if (who.subject) body.subject = who.subject;
4311
4370
  if (who.object) body.object = who.object;
4312
4371
  const key2 = uuidv7();
4313
- this.outbox.put({ send: () => this.declareNow(body, key2) });
4372
+ this.outbox.put({ send: () => this.declareNow(body, key2), route: "POST /v1/coordination/declare" });
4314
4373
  return key2;
4315
4374
  }
4316
4375
  };
@@ -4829,7 +4888,7 @@ var Outbox = class {
4829
4888
  return false;
4830
4889
  }
4831
4890
  this.writes.shift();
4832
- this.logger.warn(`a write was refused (${error instanceof NiadraAPIError ? error.code : String(error)})`);
4891
+ this.logger.warn(`${write.route ?? "a write"} was refused: ${explain(error)}`);
4833
4892
  this.settle(write, void 0, error);
4834
4893
  return true;
4835
4894
  }
@@ -5719,9 +5778,17 @@ var handles = {
5719
5778
  appUserId: (value, options) => build("app_user_id", value, void 0, options),
5720
5779
  /** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */
5721
5780
  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. */
5781
+ /**
5782
+ * A person's national document, such as a CPF: `govIdHmac("529.982.247-25", "BR")`. Send the number
5783
+ * itself, never a hash of it: the server checks its check digits, then keeps only a keyed hash with your
5784
+ * space's secret and an encrypted copy, and shows it masked. `country` is its ISO 3166-1 alpha-2 code.
5785
+ */
5723
5786
  govIdHmac: (value, country, options) => build("gov_id_hmac", value, country, options),
5724
- /** An HMAC of a company registry number. Identifies an organization. */
5787
+ /**
5788
+ * A company's registry number, such as a CNPJ: `orgRegistryHmac("11.222.333/0001-81", "BR")`. As with
5789
+ * `govIdHmac`, send the number itself; the server checks it and keeps only a keyed hash. Identifies an
5790
+ * organization.
5791
+ */
5725
5792
  orgRegistryHmac: (value, country) => build("org_registry_hmac", value, country, { subjectKind: "account" }),
5726
5793
  /** An e-mail domain, such as `acme.com`. Identifies an organization. */
5727
5794
  emailDomain: (domain) => build("email_domain", domain, void 0, { subjectKind: "account" }),
@@ -6395,6 +6462,7 @@ var AgentSession = class {
6395
6462
  try {
6396
6463
  return this.host.coordinator.decided(await this.host.check(request, options.timeoutMs ?? CHECK_BUDGET_MS), request, this.checked);
6397
6464
  } catch (error) {
6465
+ if (this.host.strict && refused(error)) throw error;
6398
6466
  return this.host.coordinator.failed(request, options.failOpen, error);
6399
6467
  }
6400
6468
  }
@@ -7006,6 +7074,7 @@ var SessionState = class {
7006
7074
  /** The guards of the last read: they hold the answers to the turn they were written for. */
7007
7075
  guards = /* @__PURE__ */ new Map();
7008
7076
  lastReport = null;
7077
+ unlinkedSaid = false;
7009
7078
  /** After the first pack, every read also asks what changed since. */
7010
7079
  get wantsDelta() {
7011
7080
  return this.etag !== null;
@@ -7034,6 +7103,19 @@ var SessionState = class {
7034
7103
  this.last = absorbed;
7035
7104
  return absorbed;
7036
7105
  }
7106
+ /**
7107
+ * Once per session: the read named an organization (`about`) with no active link to the subject, so it went
7108
+ * on with the subject's own memory, never the organization's.
7109
+ */
7110
+ sayUnlinked(result2, logger) {
7111
+ if (result2.response?.about_unlinked && !this.unlinkedSaid) {
7112
+ this.unlinkedSaid = true;
7113
+ logger.warn(
7114
+ "`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."
7115
+ );
7116
+ }
7117
+ return result2;
7118
+ }
7037
7119
  /** The last backing check of an agent's answer. */
7038
7120
  get lastBacking() {
7039
7121
  return this.lastReport;
@@ -7281,7 +7363,7 @@ var Conversation = class {
7281
7363
  * `query` picks this read's slots by other words than the turn; the pack is the pinned one.
7282
7364
  */
7283
7365
  async context(options = {}) {
7284
- const { query, turn, format, explain, include, ...requestOptions } = options;
7366
+ const { query, turn, format, explain: explain2, include, ...requestOptions } = options;
7285
7367
  const params = {
7286
7368
  subject: this.subject,
7287
7369
  view: this.view,
@@ -7290,7 +7372,7 @@ var Conversation = class {
7290
7372
  ...this.params.about ? { about: this.params.about } : {},
7291
7373
  ...this.params.target ? { target: this.params.target } : {},
7292
7374
  ...format === "json" ? { format } : {},
7293
- ...explain ? { explain } : {}
7375
+ ...explain2 ? { explain: explain2 } : {}
7294
7376
  };
7295
7377
  if (query) params.query = query;
7296
7378
  if (include?.length) params.include = include;
@@ -7298,7 +7380,7 @@ var Conversation = class {
7298
7380
  params.turn = turn === void 0 ? this.turnText : turn;
7299
7381
  const result2 = await this.client.context(params, requestOptions);
7300
7382
  this.features.observe(result2);
7301
- return this.state.observe(this.state.absorb(result2));
7383
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7302
7384
  }
7303
7385
  /**
7304
7386
  * Starts this conversation's first read now, in the background: call it when the call starts
@@ -7478,6 +7560,35 @@ function withHandles(base, extra) {
7478
7560
  return all;
7479
7561
  }
7480
7562
 
7563
+ // src/warm.ts
7564
+ var EVERY_MS = 1e5;
7565
+ var IDLE_MS = 9e4;
7566
+ var WARM_FOR_MS = 6e5;
7567
+ var KeepWarm = class {
7568
+ constructor(enabled) {
7569
+ this.enabled = enabled;
7570
+ }
7571
+ enabled;
7572
+ open = /* @__PURE__ */ new Map();
7573
+ openedAt = 0;
7574
+ /** Notes an open conversation or task. */
7575
+ add(scope, session, now) {
7576
+ if (!this.enabled || typeof WeakRef === "undefined") return;
7577
+ this.open.set(scope, new WeakRef(session));
7578
+ this.openedAt = now;
7579
+ }
7580
+ /** The conversation or task ended. */
7581
+ end(scope) {
7582
+ this.open.delete(scope);
7583
+ }
7584
+ step(now, lastActivityAt, idleMs = IDLE_MS, warmForMs = WARM_FOR_MS) {
7585
+ for (const [scope, ref] of this.open) if (ref.deref() === void 0) this.open.delete(scope);
7586
+ const used = Math.max(lastActivityAt ?? 0, this.openedAt);
7587
+ if (this.open.size === 0 || now - used > warmForMs) return "stop";
7588
+ return now - used >= idleMs ? "ping" : "wait";
7589
+ }
7590
+ };
7591
+
7481
7592
  // src/exit.ts
7482
7593
  var registered = /* @__PURE__ */ new Set();
7483
7594
  var installedOn = null;
@@ -7608,8 +7719,10 @@ var DEFAULT_TIMEOUTS = {
7608
7719
  write: 5e3,
7609
7720
  token: 2e3,
7610
7721
  upload: 6e4,
7611
- prefetch: 1e3
7722
+ prefetch: 1e3,
7723
+ connect: 1e3
7612
7724
  };
7725
+ var FETCH_KEEPALIVE_MS = 4e3;
7613
7726
  var DEFAULT_CACHE = {
7614
7727
  ttlMs: 1e4,
7615
7728
  staleWhileRevalidateMs: 10 * 6e4,
@@ -7716,7 +7829,7 @@ var Task = class {
7716
7829
  * Resolves with an empty result, never rejects, unless the client is strict.
7717
7830
  */
7718
7831
  async context(options = {}) {
7719
- const { query, format, explain, include, ...requestOptions } = options;
7832
+ const { query, format, explain: explain2, include, ...requestOptions } = options;
7720
7833
  const target = this.object ? { object: this.object } : this.params.subject ? { subject: this.params.subject } : {};
7721
7834
  const params = {
7722
7835
  ...target,
@@ -7726,18 +7839,18 @@ var Task = class {
7726
7839
  ...this.level ? { verification: this.level } : {},
7727
7840
  ...this.params.target ? { target: this.params.target } : {},
7728
7841
  ...format === "json" ? { format } : {},
7729
- ...explain ? { explain } : {},
7842
+ ...explain2 ? { explain: explain2 } : {},
7730
7843
  ...include?.length ? { include } : {}
7731
7844
  };
7732
7845
  if (query) {
7733
7846
  const answered = await this.client.context({ ...params, query }, requestOptions);
7734
7847
  this.features.observe(answered);
7735
- return this.state.observe(answered);
7848
+ return this.state.observe(this.state.sayUnlinked(answered, this.logger));
7736
7849
  }
7737
7850
  if (this.state.wantsDelta) params.delta = true;
7738
7851
  const result2 = await this.client.context(params, requestOptions);
7739
7852
  this.features.observe(result2);
7740
- return this.state.observe(this.state.absorb(result2));
7853
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7741
7854
  }
7742
7855
  /**
7743
7856
  * The agent's own working notes for this task's prompt, as `niadra.agentMemory()` with
@@ -7867,14 +7980,14 @@ var AGENT_MEMORY_TOOL_NAMES = {
7867
7980
  search: "search_agent_memory",
7868
7981
  remember: "remember"
7869
7982
  };
7870
- var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
7983
+ var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait", "system_event"];
7871
7984
  var NOTE_KINDS = ["procedure", "tool_note", "process_note", "pitfall"];
7872
7985
  var TOOL_DEFINITIONS = [
7873
7986
  {
7874
7987
  "type": "function",
7875
7988
  "function": {
7876
7989
  "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.",
7990
+ "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
7991
  "parameters": {
7879
7992
  "type": "object",
7880
7993
  "properties": {
@@ -7918,7 +8031,8 @@ var TOOL_DEFINITIONS = [
7918
8031
  "open_item",
7919
8032
  "action",
7920
8033
  "object",
7921
- "trait"
8034
+ "trait",
8035
+ "system_event"
7922
8036
  ]
7923
8037
  }
7924
8038
  },
@@ -7945,7 +8059,7 @@ var TOOL_DEFINITIONS = [
7945
8059
  "type": "function",
7946
8060
  "function": {
7947
8061
  "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.",
8062
+ "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
8063
  "parameters": {
7950
8064
  "type": "object",
7951
8065
  "properties": {
@@ -7993,7 +8107,8 @@ var TOOL_DEFINITIONS = [
7993
8107
  "open_item",
7994
8108
  "action",
7995
8109
  "object",
7996
- "trait"
8110
+ "trait",
8111
+ "system_event"
7997
8112
  ]
7998
8113
  }
7999
8114
  },
@@ -8012,7 +8127,7 @@ var TOOL_DEFINITIONS = [
8012
8127
  "type": "function",
8013
8128
  "function": {
8014
8129
  "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.",
8130
+ "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
8131
  "parameters": {
8017
8132
  "type": "object",
8018
8133
  "properties": {
@@ -8383,6 +8498,13 @@ function compose(body, read) {
8383
8498
  const pack = body.pack ? { pack: { ...body.pack, slots: fetched?.pack?.slots ?? [] } } : {};
8384
8499
  return { ...body, slots: fetched?.slots ?? null, guards: fetched?.guards ?? [], ...pack };
8385
8500
  }
8501
+ var RTT_MARGIN_MS = 50;
8502
+ function budgetWarnings(rttMs, timeouts, explicit) {
8503
+ const ms = Math.round(rttMs);
8504
+ return [...explicit].sort().filter((name) => timeouts[name] < rttMs + RTT_MARGIN_MS).map(
8505
+ (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`
8506
+ );
8507
+ }
8386
8508
  function rttWarnings(rttMs, timeouts) {
8387
8509
  const ms = Math.round(rttMs);
8388
8510
  const found2 = [];
@@ -8420,6 +8542,14 @@ var Niadra = class _Niadra {
8420
8542
  enabled;
8421
8543
  core;
8422
8544
  timeouts;
8545
+ keepWarm;
8546
+ warmTimer;
8547
+ /** The read budgets the caller left at their defaults: they take the measured round trip on top. */
8548
+ defaultReads;
8549
+ voiceStarted = false;
8550
+ /** A probe is on its way: until it ends, default read budgets get `timeouts.connect` on top. */
8551
+ measuring = false;
8552
+ voiceWarned = false;
8423
8553
  strict;
8424
8554
  /** Where the client reports what it swallows in fail-open mode. */
8425
8555
  logger;
@@ -8464,6 +8594,8 @@ var Niadra = class _Niadra {
8464
8594
  this.strict = options.strict ?? false;
8465
8595
  this.logger = options.logger ?? consoleLogger;
8466
8596
  this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
8597
+ this.keepWarm = new KeepWarm(options.keepWarm ?? true);
8598
+ this.defaultReads = new Set(["context", "navigation"].filter((name) => options.timeouts?.[name] === void 0));
8467
8599
  this.voice = new VoiceLines(
8468
8600
  options.voice === false ? { ...DEFAULT_VOICE, enabled: false } : { ...DEFAULT_VOICE, ...options.voice }
8469
8601
  );
@@ -8489,13 +8621,14 @@ var Niadra = class _Niadra {
8489
8621
  this.coordinator = new Coordinator(
8490
8622
  this.outbox,
8491
8623
  this.suppressions,
8492
- (body, key2) => this.api.declare(body, { idempotency_key: key2 })
8624
+ (body, key2) => this.api.declare(body, { idempotency_key: key2 }),
8625
+ this.logger
8493
8626
  );
8494
8627
  this.states = new AgentStates(
8495
8628
  this.outbox,
8496
8629
  {
8497
- read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.timeouts.navigation }),
8498
- write: (write) => this.api.writeAgentState(write, { timeout: this.timeouts.navigation })
8630
+ read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.readBudget("navigation") }),
8631
+ write: (write) => this.api.writeAgentState(write, { timeout: this.readBudget("navigation") })
8499
8632
  },
8500
8633
  this.logger
8501
8634
  );
@@ -8522,6 +8655,7 @@ var Niadra = class _Niadra {
8522
8655
  this.core = setup;
8523
8656
  this.enabled = true;
8524
8657
  this.disabledReason = null;
8658
+ this.probe(setup);
8525
8659
  if (options.flushOnExit ?? true) this.unregisterExit = registerExitFlush(this);
8526
8660
  }
8527
8661
  setup(options) {
@@ -8544,7 +8678,9 @@ var Niadra = class _Niadra {
8544
8678
  apiKey,
8545
8679
  fetch: fetchImpl,
8546
8680
  defaultHeaders: options.defaultHeaders ?? {},
8547
- logger: this.logger
8681
+ logger: this.logger,
8682
+ coldAllowanceMs: this.timeouts.connect,
8683
+ keepAliveMs: options.keepAliveMs ?? FETCH_KEEPALIVE_MS
8548
8684
  });
8549
8685
  const queueOptions = { ...DEFAULT_QUEUE, ...options.queue };
8550
8686
  queueOptions.maxBatchSize = Math.min(queueOptions.maxBatchSize, 499);
@@ -8600,7 +8736,7 @@ var Niadra = class _Niadra {
8600
8736
  return result2;
8601
8737
  }
8602
8738
  readContext(core, params, request, options) {
8603
- const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
8739
+ const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8604
8740
  const { query: own3, ...pinned2 } = request;
8605
8741
  const query = turnText(own3 ?? params.turn);
8606
8742
  const voiceCache = this.voiceCache(core, pinned2, options.cache);
@@ -8616,7 +8752,7 @@ var Niadra = class _Niadra {
8616
8752
  */
8617
8753
  async profile() {
8618
8754
  if (!this.core) return null;
8619
- return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.timeouts.navigation }));
8755
+ return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.readBudget("navigation") }));
8620
8756
  }
8621
8757
  /**
8622
8758
  * Checks outputs against this claim contract instead of the one the profile serves (a company's own copy,
@@ -8641,7 +8777,7 @@ var Niadra = class _Niadra {
8641
8777
  */
8642
8778
  async mayContact(handle, purpose, options = {}) {
8643
8779
  if (this.core && this.suppressions.due()) {
8644
- const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.timeouts.navigation);
8780
+ const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.readBudget("navigation"));
8645
8781
  if (!this.suppressions.held) await read;
8646
8782
  }
8647
8783
  const checkOptions = { channel: options.channel ?? null };
@@ -8651,8 +8787,8 @@ var Niadra = class _Niadra {
8651
8787
  readSuppressions(budgetMs) {
8652
8788
  return this.suppressions.read(
8653
8789
  {
8654
- salt: () => this.api.suppressionSalt({ timeout: this.timeouts.navigation }),
8655
- page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.timeouts.navigation })
8790
+ salt: () => this.api.suppressionSalt({ timeout: this.readBudget("navigation") }),
8791
+ page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.readBudget("navigation") })
8656
8792
  },
8657
8793
  budgetMs
8658
8794
  );
@@ -8731,6 +8867,7 @@ var Niadra = class _Niadra {
8731
8867
  }
8732
8868
  /** What a conversation or a task needs of its client for the agent features. */
8733
8869
  get agentHost() {
8870
+ const client = this;
8734
8871
  return {
8735
8872
  recorder: this.turns,
8736
8873
  coordinator: this.coordinator,
@@ -8744,7 +8881,10 @@ var Niadra = class _Niadra {
8744
8881
  claim: (request, timeoutMs) => this.api.claim(request, {}, { timeout: timeoutMs }),
8745
8882
  verifyClaim: (ref, field, value, options) => this.verifyClaim(ref, field, value, options),
8746
8883
  enabled: this.enabled,
8747
- navigationMs: this.timeouts.navigation
8884
+ strict: this.strict,
8885
+ get navigationMs() {
8886
+ return client.readBudget("navigation");
8887
+ }
8748
8888
  };
8749
8889
  }
8750
8890
  /**
@@ -8772,7 +8912,7 @@ var Niadra = class _Niadra {
8772
8912
  if (!cache) return false;
8773
8913
  const key2 = cacheKey(request);
8774
8914
  const line = this.voice.line(cacheScope(request) ?? "");
8775
- this.probe(core);
8915
+ this.startVoice(core);
8776
8916
  line.request = request;
8777
8917
  if (!cache.has(key2) && line.inFlight().length === 0) {
8778
8918
  this.voiceRead(core, cache, line, key2, request, null, this.timeouts.contextVoiceStart);
@@ -8909,13 +9049,13 @@ var Niadra = class _Niadra {
8909
9049
  if (!params.query || params.query.length > 2e3) {
8910
9050
  throw new NiadraValidationError("query must be 1 to 2000 characters");
8911
9051
  }
8912
- return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
9052
+ return this.readSpec("POST", "/v1/history/search", params, this.readBudget("navigation"), options);
8913
9053
  });
8914
9054
  }
8915
9055
  /** The customer's history, newest first, one line per item, paginated by cursor. */
8916
9056
  async timeline(params, options = {}) {
8917
9057
  return this.navigate(
8918
- () => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
9058
+ () => this.readSpec("POST", "/v1/history/timeline", params, this.readBudget("navigation"), options)
8919
9059
  );
8920
9060
  }
8921
9061
  /**
@@ -8928,9 +9068,10 @@ var Niadra = class _Niadra {
8928
9068
  if (!id) throw new NiadraValidationError("open() needs an item id");
8929
9069
  const body = { item_id: id };
8930
9070
  if (params.subject) body.subject = params.subject;
9071
+ if (params.about) body.about = params.about;
8931
9072
  if (params.verification) body.verification = params.verification;
8932
9073
  if (params.conversation_id) body.conversation_id = params.conversation_id;
8933
- return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
9074
+ return this.readSpec("POST", "/v1/history/open", body, this.readBudget("navigation"), options);
8934
9075
  });
8935
9076
  }
8936
9077
  /**
@@ -8942,7 +9083,7 @@ var Niadra = class _Niadra {
8942
9083
  * const { data: invoice } = await niadra.objectState("invoice:erp:0823");
8943
9084
  */
8944
9085
  async objectState(object, options = {}) {
8945
- return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.timeouts.navigation, options));
9086
+ return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.readBudget("navigation"), options));
8946
9087
  }
8947
9088
  /**
8948
9089
  * System events and agent actions about one object, newest first, one line each and never
@@ -8955,7 +9096,7 @@ var Niadra = class _Niadra {
8955
9096
  throw new NiadraValidationError("limit must be between 1 and 100");
8956
9097
  }
8957
9098
  const path = `${objectPath(object)}/timeline`;
8958
- const spec = this.readSpec("GET", path, void 0, this.timeouts.navigation, options);
9099
+ const spec = this.readSpec("GET", path, void 0, this.readBudget("navigation"), options);
8959
9100
  spec.query = { cursor: params.cursor, limit: String(limit3) };
8960
9101
  return spec;
8961
9102
  });
@@ -8974,6 +9115,7 @@ var Niadra = class _Niadra {
8974
9115
  timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
8975
9116
  open: (id, customer, bound, voice) => {
8976
9117
  const scope = { subject: customer };
9118
+ if (bound.about) scope.about = bound.about;
8977
9119
  if (bound.verification) scope.verification = bound.verification;
8978
9120
  if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
8979
9121
  return this.open(id, scope, this.voiceBudget(voice));
@@ -9008,7 +9150,7 @@ var Niadra = class _Niadra {
9008
9150
  const key2 = AgentMemoryCache.key(params);
9009
9151
  const fresh = cache?.fresh(key2);
9010
9152
  if (fresh) return blockResult(fresh, "cache");
9011
- const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
9153
+ const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
9012
9154
  const spec = this.readSpec("GET", "/v1/agent-memory/block", void 0, timeout, options);
9013
9155
  spec.query = {
9014
9156
  max_tokens: String(params.max_tokens ?? 300),
@@ -9045,7 +9187,7 @@ var Niadra = class _Niadra {
9045
9187
  if (params.limit !== void 0) body.limit = params.limit;
9046
9188
  if (params.conversation_id) body.conversation_id = params.conversation_id;
9047
9189
  if (params.task_id) body.task_id = params.task_id;
9048
- return this.readSpec("POST", "/v1/agent-memory/search", body, this.timeouts.navigation, options);
9190
+ return this.readSpec("POST", "/v1/agent-memory/search", body, this.readBudget("navigation"), options);
9049
9191
  });
9050
9192
  return result2.error ? result2 : { data: result2.data.notes, error: null };
9051
9193
  }
@@ -9139,6 +9281,41 @@ var Niadra = class _Niadra {
9139
9281
  return { ok: false, idempotency_key: key2, error: this.swallow(error, "feedback") };
9140
9282
  }
9141
9283
  }
9284
+ /**
9285
+ * How the space's agents used the context they read (`GET /v1/context-use`): sessions, deliveries, use,
9286
+ * repetition, transfers and recontact, with intervals, grouped by `group_by`. A key of an `analyst` source
9287
+ * with the `analytics` scope reads every source of the space; a key with `admin` reads its own source.
9288
+ */
9289
+ contextUse(params = {}, options = {}) {
9290
+ return this.navigate(() => {
9291
+ const { group_by: groups, ...filters } = params;
9292
+ const query = { ...filters };
9293
+ if (groups?.length) query.group_by = groups;
9294
+ return { ...this.readSpec("GET", "/v1/context-use", void 0, this.timeouts.write, options), query };
9295
+ });
9296
+ }
9297
+ /**
9298
+ * Links a person to the organization they act for (an account or a partner), as a system of record that
9299
+ * knows who works for whom: a CRM, an HR system. Needs a key with the `identity:link` scope (or `admin`);
9300
+ * `can_see_contacts` needs `admin`. Reads with `about` reach the organization through the link.
9301
+ */
9302
+ link(params, options = {}) {
9303
+ const { idempotency_key: key2, ...rest } = params;
9304
+ const body = { can_see_contacts: false, method: "system_import", ...rest };
9305
+ return this.navigate(() => this.writeSpec("/v1/identity/links", body, key2 ?? uuidv7(), options));
9306
+ }
9307
+ /**
9308
+ * Ends a link, from `valid_to` (now when absent): the person no longer acts for the organization, and reads
9309
+ * with `about` for the pair go on with the person's own memory. Needs `identity:link` or `admin`.
9310
+ */
9311
+ endLink(linkId, params = {}, options = {}) {
9312
+ return this.navigate(() => {
9313
+ if (!linkId) throw new NiadraValidationError("endLink() needs a link id");
9314
+ const body = params.valid_to ? { valid_to: params.valid_to } : {};
9315
+ const path = `/v1/identity/links/${encodeURIComponent(linkId)}/end`;
9316
+ return this.writeSpec(path, body, params.idempotency_key ?? uuidv7(), options);
9317
+ });
9318
+ }
9142
9319
  /**
9143
9320
  * Up to 500 corrections in one call, each with its own idempotency key (minted when missing).
9144
9321
  * Resolves with `accepted`, `duplicates` for replayed keys and one error per refused item, by index.
@@ -9231,16 +9408,20 @@ var Niadra = class _Niadra {
9231
9408
  * emits `conversation.ended` when you call `end()`.
9232
9409
  */
9233
9410
  conversation(params) {
9234
- return new Conversation(this, params, {
9411
+ const conversation = new Conversation(this, params, {
9235
9412
  endConversation: (id) => this.endScope(buildConversationEnded(id), `conversation:${id}`)
9236
9413
  });
9414
+ this.warm(`conversation:${conversation.id}`, conversation);
9415
+ return conversation;
9237
9416
  }
9238
9417
  /** A helper for one internal-agent task: binds `task_id` to reads and writes and emits `task.ended`. */
9239
9418
  task(params) {
9240
- return new Task(this, params, {
9419
+ const task = new Task(this, params, {
9241
9420
  endTask: (id) => this.endScope(buildTaskEnded(id), `task:${id}`),
9242
9421
  verifyTask: (verify) => this.verifyWith(verify)
9243
9422
  });
9423
+ this.warm(`task:${task.id}`, task);
9424
+ return task;
9244
9425
  }
9245
9426
  /**
9246
9427
  * Sends every queued event and resolves when done. Call it before a serverless function
@@ -9260,6 +9441,8 @@ var Niadra = class _Niadra {
9260
9441
  */
9261
9442
  async shutdown() {
9262
9443
  this.unregisterExit();
9444
+ clearInterval(this.warmTimer);
9445
+ this.warmTimer = void 0;
9263
9446
  if (!this.core) return;
9264
9447
  await this.turnSender?.stop(this.timeouts.write);
9265
9448
  await this.outbox.stop(this.timeouts.write);
@@ -9363,9 +9546,9 @@ var Niadra = class _Niadra {
9363
9546
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", request, timeout, { signal, headers }));
9364
9547
  return normalizeContext(response.data);
9365
9548
  } catch (error) {
9366
- const refused2 = error instanceof NiadraAPIError && error.status === 404;
9549
+ const refused3 = error instanceof NiadraAPIError && error.status === 404;
9367
9550
  const left = timeout - (Date.now() - started);
9368
- if (!request.include?.length || !refused2 || left <= 0) throw error;
9551
+ if (!request.include?.length || !refused3 || left <= 0) throw error;
9369
9552
  for (const name of request.include) this.refusedBlocks.set(name, Date.now() + BLOCK_RECHECK_AFTER_MS);
9370
9553
  const { include: _dropped, ...plain2 } = request;
9371
9554
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", plain2, left, { signal, headers }));
@@ -9433,6 +9616,7 @@ var Niadra = class _Niadra {
9433
9616
  /** Measures the round trip to the region once per client, in the background. */
9434
9617
  probe(core) {
9435
9618
  if (!this.voice.claimProbe()) return;
9619
+ this.measuring = true;
9436
9620
  void (async () => {
9437
9621
  const samples = [];
9438
9622
  for (let i = 0; i < 2; i++) {
@@ -9446,16 +9630,44 @@ var Niadra = class _Niadra {
9446
9630
  });
9447
9631
  } catch (error) {
9448
9632
  this.logger.debug(`round trip probe failed: ${describe(toNiadraError(error))}`);
9633
+ this.measuring = false;
9449
9634
  return;
9450
9635
  }
9451
9636
  samples.push(Date.now() - started);
9452
9637
  }
9453
9638
  const rtt = Math.min(...samples);
9454
9639
  this.voice.rtt = rtt;
9640
+ this.measuring = false;
9455
9641
  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);
9642
+ const explicit = ["context", "navigation"].filter((name) => !this.defaultReads.has(name));
9643
+ for (const warning of budgetWarnings(rtt, this.timeouts, explicit)) this.logger.warn(warning);
9644
+ if (this.voiceStarted) this.warnVoice();
9457
9645
  })();
9458
9646
  }
9647
+ /**
9648
+ * A read budget: one the caller left at its default is what the API may take, and the measured round trip
9649
+ * to the region goes on top, so an agent far from the region (Sao Paulo, 170 ms from us-east-2) is not
9650
+ * timed out by the network; one the caller set is a ceiling.
9651
+ */
9652
+ readBudget(name) {
9653
+ if (!this.defaultReads.has(name)) return this.timeouts[name];
9654
+ const rtt = this.voice.rtt;
9655
+ if (rtt !== null) return this.timeouts[name] + rtt;
9656
+ const open = this.core?.transport.connectionOpen() ?? false;
9657
+ return this.measuring && open ? this.timeouts[name] + this.timeouts.connect : this.timeouts[name];
9658
+ }
9659
+ /** The client reads in voice: the voice budgets' warnings matter from now on. */
9660
+ startVoice(core) {
9661
+ this.voiceStarted = true;
9662
+ this.probe(core);
9663
+ this.warnVoice();
9664
+ }
9665
+ warnVoice() {
9666
+ const rtt = this.voice.rtt;
9667
+ if (this.voiceWarned || rtt === null) return;
9668
+ this.voiceWarned = true;
9669
+ for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9670
+ }
9459
9671
  /**
9460
9672
  * A voice turn: the pinned body from memory, and the slots of the read of its words when that
9461
9673
  * read lands within `timeout`. See `voice.ts`.
@@ -9465,7 +9677,7 @@ var Niadra = class _Niadra {
9465
9677
  const key2 = cacheKey(request);
9466
9678
  const deadline = Date.now() + timeout;
9467
9679
  const line = this.voice.line(scope);
9468
- this.probe(core);
9680
+ this.startVoice(core);
9469
9681
  line.request = request;
9470
9682
  const words2 = wordsOf(query);
9471
9683
  const background = this.timeouts.prefetch;
@@ -9651,8 +9863,39 @@ var Niadra = class _Niadra {
9651
9863
  core.queue.flushInBackground(true);
9652
9864
  });
9653
9865
  }
9866
+ /** Notes an open conversation or task; the first one starts the keep-warm timer (`warm.ts`). */
9867
+ warm(scope, session) {
9868
+ const core = this.core;
9869
+ if (!core || !this.keepWarm.enabled) return;
9870
+ this.keepWarm.add(scope, session, Date.now());
9871
+ if (this.warmTimer !== void 0) return;
9872
+ const timer = setInterval(() => {
9873
+ this.warmTick(core);
9874
+ }, EVERY_MS);
9875
+ timer.unref?.();
9876
+ this.warmTimer = timer;
9877
+ }
9878
+ warmTick(core) {
9879
+ const step = this.keepWarm.step(Date.now(), core.transport.lastActivityAt);
9880
+ if (step === "stop") {
9881
+ clearInterval(this.warmTimer);
9882
+ this.warmTimer = void 0;
9883
+ return;
9884
+ }
9885
+ if (step !== "ping") return;
9886
+ core.transport.request({
9887
+ method: "GET",
9888
+ path: "/healthz",
9889
+ timeoutMs: 2e3,
9890
+ retry: { kind: "read", maxAttempts: 1 },
9891
+ activity: false
9892
+ }).catch((error) => {
9893
+ this.logger.debug(`keep-warm ping failed: ${describe(toNiadraError(error))}`);
9894
+ });
9895
+ }
9654
9896
  async endScope(item, scope) {
9655
9897
  this.forgetScope(scope);
9898
+ this.keepWarm.end(scope);
9656
9899
  return this.sendNow(() => item);
9657
9900
  }
9658
9901
  async sendBatch(transport, items2, maxAttempts, backoff) {
@@ -11507,7 +11750,7 @@ var Replayer = class {
11507
11750
  try {
11508
11751
  answer = await this.niadra.callRoute({ method: "POST", path: "/v1/scenario-runs", body, idempotencyKey: uuidv7() });
11509
11752
  } catch (error) {
11510
- if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused(scenarioIds, results);
11753
+ if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused2(scenarioIds, results);
11511
11754
  throw error;
11512
11755
  }
11513
11756
  let run = runOf(answer);
@@ -11610,7 +11853,7 @@ var Replayer = class {
11610
11853
  return result(scenarioId, turnId, caseId, run, "completed", { paraphrase: rephrased, assertions, divergent_calls: played.divergent, latency_ms: latency });
11611
11854
  }
11612
11855
  };
11613
- function refused(scenarioIds, results) {
11856
+ function refused2(scenarioIds, results) {
11614
11857
  const scenarios = scenarioIds.map((id) => {
11615
11858
  const mine = results.filter((r) => r.scenario_id === id);
11616
11859
  return {