@raindrop-ai/pi-agent 0.0.4 → 0.0.6

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.
package/README.md CHANGED
@@ -39,6 +39,15 @@ pi install npm:@raindrop-ai/pi-agent
39
39
 
40
40
  Set `RAINDROP_WRITE_KEY` in your environment. Traces appear automatically.
41
41
 
42
+ ## Payload size limits
43
+
44
+ Event input/output are capped at **1,000,000 characters per field** (span
45
+ attributes at 32 KB) and truncated with a `...[truncated by raindrop]` marker.
46
+ The cap is enforced before (or during) serialization, so oversized payloads
47
+ cost the cap — not the payload — on the host's event loop, and large events
48
+ land truncated instead of being rejected at the ingest size limit. Hook error
49
+ logs are rate-limited to one line per failure family per 30s.
50
+
42
51
  ## Documentation
43
52
 
44
53
  See the full [Pi Agent docs](https://www.raindrop.ai/docs/integrations/pi-agent/) for configuration, per-subscribe overrides, and extension settings.
@@ -1,4 +1,4 @@
1
- // ../core/dist/chunk-VUNUOE2X.js
1
+ // ../core/dist/chunk-SK6EJEO7.js
2
2
  function getCrypto() {
3
3
  const c = globalThis.crypto;
4
4
  return c;
@@ -56,6 +56,8 @@ function base64Encode(bytes) {
56
56
  function generateId() {
57
57
  return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
58
58
  }
59
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
60
+ var MAX_RETRY_DELAY_MS = 3e4;
59
61
  function wait(ms) {
60
62
  return new Promise((resolve) => setTimeout(resolve, ms));
61
63
  }
@@ -63,6 +65,46 @@ function formatEndpoint(endpoint) {
63
65
  if (!endpoint) return void 0;
64
66
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
65
67
  }
68
+ function redactUrlForLog(url) {
69
+ try {
70
+ const parsed = new URL(url);
71
+ parsed.username = "";
72
+ parsed.password = "";
73
+ parsed.search = "";
74
+ parsed.hash = "";
75
+ return parsed.toString();
76
+ } catch (e) {
77
+ return "<unparseable-url>";
78
+ }
79
+ }
80
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
81
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
82
+ function rateLimitedLog(key, log) {
83
+ const now = Date.now();
84
+ const last = rateLimitedLogLast.get(key);
85
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
86
+ return false;
87
+ }
88
+ rateLimitedLogLast.set(key, now);
89
+ log();
90
+ return true;
91
+ }
92
+ async function raceWithTimeout(promise, timeoutMs) {
93
+ let timer;
94
+ const settledInTime = await Promise.race([
95
+ promise.then(
96
+ () => true,
97
+ () => true
98
+ ),
99
+ new Promise((resolve) => {
100
+ var _a;
101
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
102
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
103
+ })
104
+ ]);
105
+ if (timer) clearTimeout(timer);
106
+ return settledInTime;
107
+ }
66
108
  function parseRetryAfter(headers) {
67
109
  var _a;
68
110
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -79,12 +121,12 @@ function parseRetryAfter(headers) {
79
121
  function getRetryDelayMs(attemptNumber, previousError) {
80
122
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
81
123
  const v = previousError.retryAfterMs;
82
- if (typeof v === "number") return Math.max(0, v);
124
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
83
125
  }
84
126
  if (attemptNumber <= 1) return 0;
85
127
  const base = 500;
86
128
  const factor = Math.pow(2, attemptNumber - 2);
87
- return base * factor;
129
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
88
130
  }
89
131
  async function withRetry(operation, opName, opts) {
90
132
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -119,7 +161,9 @@ async function withRetry(operation, opName, opts) {
119
161
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
120
162
  }
121
163
  async function postJson(url, body, headers, opts) {
122
- const opName = `POST ${url}`;
164
+ var _a;
165
+ const opName = `POST ${redactUrlForLog(url)}`;
166
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
123
167
  await withRetry(
124
168
  async () => {
125
169
  const resp = await fetch(url, {
@@ -128,7 +172,8 @@ async function postJson(url, body, headers, opts) {
128
172
  "Content-Type": "application/json",
129
173
  ...headers
130
174
  },
131
- body: JSON.stringify(body)
175
+ body: JSON.stringify(body),
176
+ signal: AbortSignal.timeout(timeoutMs)
132
177
  });
133
178
  if (!resp.ok) {
134
179
  const text = await resp.text().catch(() => "");
@@ -145,6 +190,27 @@ async function postJson(url, body, headers, opts) {
145
190
  opts
146
191
  );
147
192
  }
193
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
194
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
195
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
196
+ function resolveMaxTextFieldChars(value) {
197
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
198
+ return Math.floor(value);
199
+ }
200
+ return currentDefaultMaxTextFieldChars;
201
+ }
202
+ function truncateToLimit(text, limit) {
203
+ if (limit > TRUNCATION_MARKER.length) {
204
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
205
+ }
206
+ return text.slice(0, Math.max(0, limit));
207
+ }
208
+ function capText(value, limit) {
209
+ if (typeof value !== "string") return value;
210
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
211
+ if (value.length <= max) return value;
212
+ return truncateToLimit(value, max);
213
+ }
148
214
  var SpanStatusCode = {
149
215
  UNSET: 0,
150
216
  OK: 1,
@@ -287,6 +353,27 @@ function mirrorPartialEventToLocalDebugger(event, options = {}) {
287
353
  }).catch(() => {
288
354
  });
289
355
  }
356
+ var PROJECT_ID_HEADER = "X-Raindrop-Project-Id";
357
+ var PROJECT_ID_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
358
+ function isValidProjectIdSlug(value) {
359
+ return PROJECT_ID_SLUG_PATTERN.test(value);
360
+ }
361
+ function normalizeProjectId(raw, opts) {
362
+ if (typeof raw !== "string") return void 0;
363
+ const trimmed = raw.trim();
364
+ if (!trimmed) return void 0;
365
+ if (!isValidProjectIdSlug(trimmed) && opts.debug) {
366
+ console.warn(
367
+ `${opts.prefix} projectId "${trimmed}" does not match slug ${PROJECT_ID_SLUG_PATTERN.source}; sending anyway \u2014 backend may reject with HTTP 400`
368
+ );
369
+ }
370
+ return trimmed;
371
+ }
372
+ function projectIdHeaders(projectId) {
373
+ return projectId ? { [PROJECT_ID_HEADER]: projectId } : {};
374
+ }
375
+ var SHUTDOWN_DEADLINE_MS = 1e4;
376
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
290
377
  function mergePatches(target, source) {
291
378
  var _a, _b, _c, _d;
292
379
  const out = { ...target, ...source };
@@ -304,6 +391,7 @@ var EventShipper = class {
304
391
  this.sticky = /* @__PURE__ */ new Map();
305
392
  this.timers = /* @__PURE__ */ new Map();
306
393
  this.inFlight = /* @__PURE__ */ new Set();
394
+ this.hasShutdown = false;
307
395
  var _a, _b, _c, _d, _e, _f, _g, _h;
308
396
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
309
397
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -313,10 +401,15 @@ var EventShipper = class {
313
401
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
314
402
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
315
403
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
404
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
316
405
  this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
317
406
  if (this.debug && this.localDebuggerUrl) {
318
407
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
319
408
  }
409
+ this.projectId = normalizeProjectId(opts.projectId, {
410
+ debug: this.debug,
411
+ prefix: this.prefix
412
+ });
320
413
  const isNode = typeof process !== "undefined" && typeof process.version === "string";
321
414
  this.context = {
322
415
  library: {
@@ -335,10 +428,55 @@ var EventShipper = class {
335
428
  authHeaders() {
336
429
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
337
430
  }
431
+ requestHeaders() {
432
+ return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
433
+ }
434
+ /**
435
+ * Build the retry/timeout options for one POST, honoring the shutdown
436
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
437
+ * the caller must drop the payload (with a rate-limited warning) instead
438
+ * of issuing a request that could outlive process exit.
439
+ *
440
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
441
+ * path is mid-drain takes effect immediately: no further retries, and the
442
+ * per-attempt timeout is clamped to the remaining window. After
443
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
444
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
445
+ * run as a single short attempt rather than regaining the full retry
446
+ * schedule.
447
+ */
448
+ requestOpts() {
449
+ if (this.shutdownDeadlineAt !== void 0) {
450
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
451
+ if (remainingMs <= 0) return null;
452
+ return {
453
+ maxAttempts: 1,
454
+ debug: this.debug,
455
+ sdkName: this.sdkName,
456
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
457
+ };
458
+ }
459
+ if (this.hasShutdown) {
460
+ return {
461
+ maxAttempts: 1,
462
+ debug: this.debug,
463
+ sdkName: this.sdkName,
464
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
465
+ };
466
+ }
467
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
468
+ }
338
469
  async patch(eventId, patch) {
339
470
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
340
471
  if (!this.enabled) return;
341
472
  if (!eventId || !eventId.trim()) return;
473
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
474
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
475
+ patch = { ...patch, input: capText(patch.input, maxChars) };
476
+ }
477
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
478
+ patch = { ...patch, output: capText(patch.output, maxChars) };
479
+ }
342
480
  if (this.debug) {
343
481
  console.log(`${this.prefix} queue patch`, {
344
482
  eventId,
@@ -385,9 +523,16 @@ var EventShipper = class {
385
523
  })));
386
524
  }
387
525
  async shutdown() {
388
- for (const t of this.timers.values()) clearTimeout(t);
389
- this.timers.clear();
390
- await this.flush();
526
+ this.hasShutdown = true;
527
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
528
+ try {
529
+ for (const t of this.timers.values()) clearTimeout(t);
530
+ this.timers.clear();
531
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
532
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
533
+ } finally {
534
+ this.shutdownDeadlineAt = void 0;
535
+ }
391
536
  }
392
537
  async trackSignal(signal) {
393
538
  var _a, _b;
@@ -409,15 +554,19 @@ var EventShipper = class {
409
554
  ];
410
555
  if (!this.writeKey) return;
411
556
  const url = `${this.baseUrl}signals/track`;
557
+ const opts = this.requestOpts();
558
+ if (!opts) {
559
+ this.warnShutdownDrop("signal");
560
+ return;
561
+ }
412
562
  try {
413
- await postJson(url, body, this.authHeaders(), {
414
- maxAttempts: 3,
415
- debug: this.debug,
416
- sdkName: this.sdkName
417
- });
563
+ await postJson(url, body, this.requestHeaders(), opts);
418
564
  } catch (err) {
419
565
  const msg = err instanceof Error ? err.message : String(err);
420
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
566
+ rateLimitedLog(
567
+ `${this.prefix}.send_signal_failed`,
568
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
569
+ );
421
570
  }
422
571
  }
423
572
  async identify(users) {
@@ -441,17 +590,27 @@ var EventShipper = class {
441
590
  if (!this.writeKey) return;
442
591
  if (body.length === 0) return;
443
592
  const url = `${this.baseUrl}users/identify`;
593
+ const opts = this.requestOpts();
594
+ if (!opts) {
595
+ this.warnShutdownDrop("identify");
596
+ return;
597
+ }
444
598
  try {
445
- await postJson(url, body, this.authHeaders(), {
446
- maxAttempts: 3,
447
- debug: this.debug,
448
- sdkName: this.sdkName
449
- });
599
+ await postJson(url, body, this.requestHeaders(), opts);
450
600
  } catch (err) {
451
601
  const msg = err instanceof Error ? err.message : String(err);
452
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
602
+ rateLimitedLog(
603
+ `${this.prefix}.send_identify_failed`,
604
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
605
+ );
453
606
  }
454
607
  }
608
+ warnShutdownDrop(what) {
609
+ rateLimitedLog(
610
+ `${this.prefix}.shutdown_deadline`,
611
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
612
+ );
613
+ }
455
614
  async flushOne(eventId) {
456
615
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
457
616
  if (!this.enabled) return;
@@ -527,11 +686,13 @@ var EventShipper = class {
527
686
  if (!isPending) this.sticky.delete(eventId);
528
687
  return;
529
688
  }
530
- const p = postJson(url, payload, this.authHeaders(), {
531
- maxAttempts: 3,
532
- debug: this.debug,
533
- sdkName: this.sdkName
534
- });
689
+ const opts = this.requestOpts();
690
+ if (!opts) {
691
+ this.warnShutdownDrop(`track_partial ${eventId}`);
692
+ if (!isPending) this.sticky.delete(eventId);
693
+ return;
694
+ }
695
+ const p = postJson(url, payload, this.requestHeaders(), opts);
535
696
  this.inFlight.add(p);
536
697
  try {
537
698
  try {
@@ -541,7 +702,10 @@ var EventShipper = class {
541
702
  }
542
703
  } catch (err) {
543
704
  const msg = err instanceof Error ? err.message : String(err);
544
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
705
+ rateLimitedLog(
706
+ `${this.prefix}.send_track_partial_failed`,
707
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
708
+ );
545
709
  }
546
710
  } finally {
547
711
  this.inFlight.delete(p);
@@ -640,10 +804,24 @@ function redactJsonAttributeValue(key, value) {
640
804
  if (scrubbedJson === json) return void 0;
641
805
  return { stringValue: scrubbedJson };
642
806
  }
807
+ function applyOtelSpanAttributeLimit(limit) {
808
+ var _a, _b;
809
+ try {
810
+ const raw = (_b = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env) == null ? void 0 : _b.OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT;
811
+ if (!raw) return limit;
812
+ const parsed = Number.parseInt(raw, 10);
813
+ if (Number.isFinite(parsed) && parsed > 0) {
814
+ return Math.min(limit, parsed);
815
+ }
816
+ } catch (e) {
817
+ }
818
+ return limit;
819
+ }
643
820
  var TraceShipper = class {
644
821
  constructor(opts) {
645
822
  this.queue = [];
646
823
  this.inFlight = /* @__PURE__ */ new Set();
824
+ this.hasShutdown = false;
647
825
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
648
826
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
649
827
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -661,8 +839,45 @@ var TraceShipper = class {
661
839
  if (this.debug && this.localDebuggerUrl) {
662
840
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
663
841
  }
842
+ this.projectId = normalizeProjectId(opts.projectId, {
843
+ debug: this.debug,
844
+ prefix: this.prefix
845
+ });
664
846
  this.transformSpanHook = opts.transformSpan;
665
847
  this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
848
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
849
+ }
850
+ /**
851
+ * Cap every string attribute value on the span. O(#attributes) length
852
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
853
+ * pipeline so the default secret-scrub still sees parseable JSON in
854
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
855
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
856
+ * in the surviving prefix).
857
+ *
858
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
859
+ * for span content, matching the Python SDK and the OTel SDK convention.
860
+ */
861
+ capSpanAttributes(span) {
862
+ var _a;
863
+ const maxChars = applyOtelSpanAttributeLimit(
864
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
865
+ );
866
+ const attrs = span.attributes;
867
+ if (!attrs || attrs.length === 0) return span;
868
+ let nextAttrs;
869
+ for (let i = 0; i < attrs.length; i++) {
870
+ const attr = attrs[i];
871
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
872
+ if (typeof value !== "string" || value.length <= maxChars) continue;
873
+ if (!nextAttrs) nextAttrs = attrs.slice();
874
+ nextAttrs[i] = {
875
+ key: attr.key,
876
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
877
+ };
878
+ }
879
+ if (!nextAttrs) return span;
880
+ return { ...span, attributes: nextAttrs };
666
881
  }
667
882
  /**
668
883
  * Apply the user `transformSpan` hook (if any) followed by the default
@@ -696,7 +911,7 @@ var TraceShipper = class {
696
911
  if (!this.disableDefaultRedaction) {
697
912
  current = defaultTransformSpan(current);
698
913
  }
699
- return current;
914
+ return this.capSpanAttributes(current);
700
915
  }
701
916
  isDebugEnabled() {
702
917
  return this.debug;
@@ -704,6 +919,9 @@ var TraceShipper = class {
704
919
  authHeaders() {
705
920
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
706
921
  }
922
+ requestHeaders() {
923
+ return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
924
+ }
707
925
  startSpan(args) {
708
926
  var _a, _b;
709
927
  const ids = createSpanIds(args.parent);
@@ -817,6 +1035,16 @@ var TraceShipper = class {
817
1035
  while (this.queue.length > 0) {
818
1036
  const batch = this.queue.splice(0, this.maxBatchSize);
819
1037
  if (!this.writeKey) continue;
1038
+ const opts = this.requestOpts();
1039
+ if (!opts) {
1040
+ rateLimitedLog(
1041
+ `${this.prefix}.shutdown_deadline`,
1042
+ () => console.warn(
1043
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1044
+ )
1045
+ );
1046
+ continue;
1047
+ }
820
1048
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
821
1049
  const url = `${this.baseUrl}traces`;
822
1050
  if (this.debug) {
@@ -825,11 +1053,7 @@ var TraceShipper = class {
825
1053
  endpoint: url
826
1054
  });
827
1055
  }
828
- const p = postJson(url, body, this.authHeaders(), {
829
- maxAttempts: 3,
830
- debug: this.debug,
831
- sdkName: this.sdkName
832
- });
1056
+ const p = postJson(url, body, this.requestHeaders(), opts);
833
1057
  this.inFlight.add(p);
834
1058
  try {
835
1059
  try {
@@ -837,21 +1061,61 @@ var TraceShipper = class {
837
1061
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
838
1062
  } catch (err) {
839
1063
  const msg = err instanceof Error ? err.message : String(err);
840
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1064
+ rateLimitedLog(
1065
+ `${this.prefix}.send_spans_failed`,
1066
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1067
+ );
841
1068
  }
842
1069
  } finally {
843
1070
  this.inFlight.delete(p);
844
1071
  }
845
1072
  }
846
1073
  }
1074
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1075
+ requestOpts() {
1076
+ if (this.shutdownDeadlineAt !== void 0) {
1077
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1078
+ if (remainingMs <= 0) return null;
1079
+ return {
1080
+ maxAttempts: 1,
1081
+ debug: this.debug,
1082
+ sdkName: this.sdkName,
1083
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1084
+ };
1085
+ }
1086
+ if (this.hasShutdown) {
1087
+ return {
1088
+ maxAttempts: 1,
1089
+ debug: this.debug,
1090
+ sdkName: this.sdkName,
1091
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1092
+ };
1093
+ }
1094
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1095
+ }
847
1096
  async shutdown() {
848
- if (this.timer) {
849
- clearTimeout(this.timer);
850
- this.timer = void 0;
1097
+ this.hasShutdown = true;
1098
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1099
+ try {
1100
+ if (this.timer) {
1101
+ clearTimeout(this.timer);
1102
+ this.timer = void 0;
1103
+ }
1104
+ const drain = async () => {
1105
+ await this.flush();
1106
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1107
+ })));
1108
+ };
1109
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1110
+ if (!settled) {
1111
+ rateLimitedLog(
1112
+ `${this.prefix}.shutdown_deadline`,
1113
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1114
+ );
1115
+ }
1116
+ } finally {
1117
+ this.shutdownDeadlineAt = void 0;
851
1118
  }
852
- await this.flush();
853
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
854
- })));
855
1119
  }
856
1120
  };
857
1121
 
@@ -862,7 +1126,7 @@ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage;
862
1126
  // package.json
863
1127
  var package_default = {
864
1128
  name: "@raindrop-ai/pi-agent",
865
- version: "0.0.4",
1129
+ version: "0.0.6",
866
1130
  description: "Raindrop observability for Pi Agent \u2014 automatic tracing via subscriber or pi-coding-agent extension",
867
1131
  type: "module",
868
1132
  license: "MIT",
@@ -1060,19 +1324,82 @@ function formatToolSpanName(toolName, args) {
1060
1324
  }
1061
1325
  return toolName;
1062
1326
  }
1063
- function safeStringify(value) {
1327
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1328
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1329
+ var MAX_ATTR_LENGTH = 32768;
1330
+ var MAX_BOUNDED_DEPTH = 12;
1331
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1332
+ if (value.length <= limit) return value;
1333
+ if (limit > TRUNCATION_MARKER2.length) {
1334
+ return value.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1335
+ }
1336
+ return value.slice(0, limit);
1337
+ }
1338
+ function boundedClone2(value, budget, depth) {
1339
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1340
+ if (typeof value === "string") {
1341
+ if (value.length > budget.remaining) {
1342
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1343
+ budget.remaining = 0;
1344
+ return taken;
1345
+ }
1346
+ budget.remaining -= Math.max(value.length, 1);
1347
+ return value;
1348
+ }
1349
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1350
+ budget.remaining -= 8;
1351
+ return value;
1352
+ }
1353
+ if (typeof value !== "object") {
1354
+ budget.remaining -= 8;
1355
+ return value;
1356
+ }
1357
+ if (depth >= MAX_BOUNDED_DEPTH) {
1358
+ budget.remaining -= 16;
1359
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1360
+ }
1361
+ const withToJson = value;
1362
+ if (typeof withToJson.toJSON === "function") {
1363
+ try {
1364
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1365
+ } catch (e) {
1366
+ }
1367
+ }
1368
+ if (Array.isArray(value)) {
1369
+ const out2 = [];
1370
+ for (const item of value) {
1371
+ if (budget.remaining <= 0) {
1372
+ out2.push(TRUNCATION_MARKER2);
1373
+ break;
1374
+ }
1375
+ out2.push(boundedClone2(item, budget, depth + 1));
1376
+ }
1377
+ return out2;
1378
+ }
1379
+ const out = {};
1380
+ for (const key of Object.keys(value)) {
1381
+ if (budget.remaining <= 0) {
1382
+ out["..."] = TRUNCATION_MARKER2;
1383
+ break;
1384
+ }
1385
+ budget.remaining -= Math.max(key.length, 1);
1386
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1387
+ }
1388
+ return out;
1389
+ }
1390
+ function safeStringify(value, limit = MAX_ATTR_LENGTH) {
1064
1391
  if (value === void 0 || value === null) return void 0;
1065
1392
  try {
1066
- return JSON.stringify(value);
1393
+ const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
1394
+ return JSON.stringify(pruned);
1067
1395
  } catch (e) {
1068
1396
  try {
1069
- return String(value);
1397
+ return capText2(String(value), limit);
1070
1398
  } catch (e2) {
1071
1399
  return "[unserializable]";
1072
1400
  }
1073
1401
  }
1074
1402
  }
1075
- var MAX_ATTR_LENGTH = 32768;
1076
1403
  function truncate(value) {
1077
1404
  if (value === void 0) return void 0;
1078
1405
  if (value.length <= MAX_ATTR_LENGTH) return value;
@@ -1118,6 +1445,7 @@ export {
1118
1445
  extractAssistantText,
1119
1446
  extractToolCallIds,
1120
1447
  formatToolSpanName,
1448
+ capText2 as capText,
1121
1449
  safeStringify,
1122
1450
  truncate,
1123
1451
  getHostname,