@raindrop-ai/pi-agent 0.0.3 → 0.0.5

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.
@@ -1,4 +1,4 @@
1
- // ../core/dist/chunk-OLYXZCOK.js
1
+ // ../core/dist/chunk-U5HUTMR5.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,8 @@ function mirrorPartialEventToLocalDebugger(event, options = {}) {
287
353
  }).catch(() => {
288
354
  });
289
355
  }
356
+ var SHUTDOWN_DEADLINE_MS = 1e4;
357
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
290
358
  function mergePatches(target, source) {
291
359
  var _a, _b, _c, _d;
292
360
  const out = { ...target, ...source };
@@ -304,6 +372,7 @@ var EventShipper = class {
304
372
  this.sticky = /* @__PURE__ */ new Map();
305
373
  this.timers = /* @__PURE__ */ new Map();
306
374
  this.inFlight = /* @__PURE__ */ new Set();
375
+ this.hasShutdown = false;
307
376
  var _a, _b, _c, _d, _e, _f, _g, _h;
308
377
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
309
378
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -313,6 +382,7 @@ var EventShipper = class {
313
382
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
314
383
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
315
384
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
385
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
316
386
  this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
317
387
  if (this.debug && this.localDebuggerUrl) {
318
388
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
@@ -335,10 +405,52 @@ var EventShipper = class {
335
405
  authHeaders() {
336
406
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
337
407
  }
408
+ /**
409
+ * Build the retry/timeout options for one POST, honoring the shutdown
410
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
411
+ * the caller must drop the payload (with a rate-limited warning) instead
412
+ * of issuing a request that could outlive process exit.
413
+ *
414
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
415
+ * path is mid-drain takes effect immediately: no further retries, and the
416
+ * per-attempt timeout is clamped to the remaining window. After
417
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
418
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
419
+ * run as a single short attempt rather than regaining the full retry
420
+ * schedule.
421
+ */
422
+ requestOpts() {
423
+ if (this.shutdownDeadlineAt !== void 0) {
424
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
425
+ if (remainingMs <= 0) return null;
426
+ return {
427
+ maxAttempts: 1,
428
+ debug: this.debug,
429
+ sdkName: this.sdkName,
430
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
431
+ };
432
+ }
433
+ if (this.hasShutdown) {
434
+ return {
435
+ maxAttempts: 1,
436
+ debug: this.debug,
437
+ sdkName: this.sdkName,
438
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
439
+ };
440
+ }
441
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
442
+ }
338
443
  async patch(eventId, patch) {
339
444
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
340
445
  if (!this.enabled) return;
341
446
  if (!eventId || !eventId.trim()) return;
447
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
448
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
449
+ patch = { ...patch, input: capText(patch.input, maxChars) };
450
+ }
451
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
452
+ patch = { ...patch, output: capText(patch.output, maxChars) };
453
+ }
342
454
  if (this.debug) {
343
455
  console.log(`${this.prefix} queue patch`, {
344
456
  eventId,
@@ -385,9 +497,16 @@ var EventShipper = class {
385
497
  })));
386
498
  }
387
499
  async shutdown() {
388
- for (const t of this.timers.values()) clearTimeout(t);
389
- this.timers.clear();
390
- await this.flush();
500
+ this.hasShutdown = true;
501
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
502
+ try {
503
+ for (const t of this.timers.values()) clearTimeout(t);
504
+ this.timers.clear();
505
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
506
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
507
+ } finally {
508
+ this.shutdownDeadlineAt = void 0;
509
+ }
391
510
  }
392
511
  async trackSignal(signal) {
393
512
  var _a, _b;
@@ -409,15 +528,19 @@ var EventShipper = class {
409
528
  ];
410
529
  if (!this.writeKey) return;
411
530
  const url = `${this.baseUrl}signals/track`;
531
+ const opts = this.requestOpts();
532
+ if (!opts) {
533
+ this.warnShutdownDrop("signal");
534
+ return;
535
+ }
412
536
  try {
413
- await postJson(url, body, this.authHeaders(), {
414
- maxAttempts: 3,
415
- debug: this.debug,
416
- sdkName: this.sdkName
417
- });
537
+ await postJson(url, body, this.authHeaders(), opts);
418
538
  } catch (err) {
419
539
  const msg = err instanceof Error ? err.message : String(err);
420
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
540
+ rateLimitedLog(
541
+ `${this.prefix}.send_signal_failed`,
542
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
543
+ );
421
544
  }
422
545
  }
423
546
  async identify(users) {
@@ -441,17 +564,27 @@ var EventShipper = class {
441
564
  if (!this.writeKey) return;
442
565
  if (body.length === 0) return;
443
566
  const url = `${this.baseUrl}users/identify`;
567
+ const opts = this.requestOpts();
568
+ if (!opts) {
569
+ this.warnShutdownDrop("identify");
570
+ return;
571
+ }
444
572
  try {
445
- await postJson(url, body, this.authHeaders(), {
446
- maxAttempts: 3,
447
- debug: this.debug,
448
- sdkName: this.sdkName
449
- });
573
+ await postJson(url, body, this.authHeaders(), opts);
450
574
  } catch (err) {
451
575
  const msg = err instanceof Error ? err.message : String(err);
452
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
576
+ rateLimitedLog(
577
+ `${this.prefix}.send_identify_failed`,
578
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
579
+ );
453
580
  }
454
581
  }
582
+ warnShutdownDrop(what) {
583
+ rateLimitedLog(
584
+ `${this.prefix}.shutdown_deadline`,
585
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
586
+ );
587
+ }
455
588
  async flushOne(eventId) {
456
589
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
457
590
  if (!this.enabled) return;
@@ -527,11 +660,13 @@ var EventShipper = class {
527
660
  if (!isPending) this.sticky.delete(eventId);
528
661
  return;
529
662
  }
530
- const p = postJson(url, payload, this.authHeaders(), {
531
- maxAttempts: 3,
532
- debug: this.debug,
533
- sdkName: this.sdkName
534
- });
663
+ const opts = this.requestOpts();
664
+ if (!opts) {
665
+ this.warnShutdownDrop(`track_partial ${eventId}`);
666
+ if (!isPending) this.sticky.delete(eventId);
667
+ return;
668
+ }
669
+ const p = postJson(url, payload, this.authHeaders(), opts);
535
670
  this.inFlight.add(p);
536
671
  try {
537
672
  try {
@@ -541,7 +676,10 @@ var EventShipper = class {
541
676
  }
542
677
  } catch (err) {
543
678
  const msg = err instanceof Error ? err.message : String(err);
544
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
679
+ rateLimitedLog(
680
+ `${this.prefix}.send_track_partial_failed`,
681
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
682
+ );
545
683
  }
546
684
  } finally {
547
685
  this.inFlight.delete(p);
@@ -551,10 +689,113 @@ var EventShipper = class {
551
689
  }
552
690
  }
553
691
  };
692
+ var DEFAULT_SECRET_KEY_NAMES = [
693
+ "apikey",
694
+ "apisecret",
695
+ "apitoken",
696
+ "secretaccesskey",
697
+ "sessiontoken",
698
+ "privatekey",
699
+ "privatekeyid",
700
+ "clientsecret",
701
+ "accesstoken",
702
+ "refreshtoken",
703
+ "oauthtoken",
704
+ "bearertoken",
705
+ "authorization",
706
+ "password",
707
+ "passphrase"
708
+ ];
709
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
710
+ function normalizeKeyName(name) {
711
+ return name.toLowerCase().replace(/[-_.]/g, "");
712
+ }
713
+ function redactSecretsInObject(value, options) {
714
+ var _a, _b;
715
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
716
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
717
+ const seen = /* @__PURE__ */ new WeakSet();
718
+ const walk = (node) => {
719
+ if (node === null || typeof node !== "object") return node;
720
+ if (seen.has(node)) return "[CIRCULAR]";
721
+ seen.add(node);
722
+ if (Array.isArray(node)) {
723
+ return node.map((item) => walk(item));
724
+ }
725
+ const out = {};
726
+ for (const [k, v] of Object.entries(node)) {
727
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
728
+ out[k] = placeholder;
729
+ } else {
730
+ out[k] = walk(v);
731
+ }
732
+ }
733
+ return out;
734
+ };
735
+ return walk(value);
736
+ }
737
+ function buildSecretSet(names) {
738
+ const set = /* @__PURE__ */ new Set();
739
+ for (const name of names) set.add(normalizeKeyName(name));
740
+ return set;
741
+ }
742
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
743
+ "ai.request.providerOptions",
744
+ "ai.response.providerMetadata"
745
+ ];
746
+ function defaultTransformSpan(span) {
747
+ const attrs = span.attributes;
748
+ if (!attrs || attrs.length === 0) return span;
749
+ let nextAttrs;
750
+ for (let i = 0; i < attrs.length; i++) {
751
+ const attr = attrs[i];
752
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
753
+ if (redacted === void 0) continue;
754
+ if (!nextAttrs) nextAttrs = attrs.slice();
755
+ nextAttrs[i] = { key: attr.key, value: redacted };
756
+ }
757
+ if (!nextAttrs) return span;
758
+ return { ...span, attributes: nextAttrs };
759
+ }
760
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
761
+ function redactJsonAttributeValue(key, value) {
762
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
763
+ const json = value.stringValue;
764
+ if (typeof json !== "string" || json.length === 0) return void 0;
765
+ let parsed;
766
+ try {
767
+ parsed = JSON.parse(json);
768
+ } catch (e) {
769
+ return void 0;
770
+ }
771
+ const scrubbed = redactSecretsInObject(parsed);
772
+ let scrubbedJson;
773
+ try {
774
+ scrubbedJson = JSON.stringify(scrubbed);
775
+ } catch (e) {
776
+ return void 0;
777
+ }
778
+ if (scrubbedJson === json) return void 0;
779
+ return { stringValue: scrubbedJson };
780
+ }
781
+ function applyOtelSpanAttributeLimit(limit) {
782
+ var _a, _b;
783
+ try {
784
+ 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;
785
+ if (!raw) return limit;
786
+ const parsed = Number.parseInt(raw, 10);
787
+ if (Number.isFinite(parsed) && parsed > 0) {
788
+ return Math.min(limit, parsed);
789
+ }
790
+ } catch (e) {
791
+ }
792
+ return limit;
793
+ }
554
794
  var TraceShipper = class {
555
795
  constructor(opts) {
556
796
  this.queue = [];
557
797
  this.inFlight = /* @__PURE__ */ new Set();
798
+ this.hasShutdown = false;
558
799
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
559
800
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
560
801
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -572,6 +813,75 @@ var TraceShipper = class {
572
813
  if (this.debug && this.localDebuggerUrl) {
573
814
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
574
815
  }
816
+ this.transformSpanHook = opts.transformSpan;
817
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
818
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
819
+ }
820
+ /**
821
+ * Cap every string attribute value on the span. O(#attributes) length
822
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
823
+ * pipeline so the default secret-scrub still sees parseable JSON in
824
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
825
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
826
+ * in the surviving prefix).
827
+ *
828
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
829
+ * for span content, matching the Python SDK and the OTel SDK convention.
830
+ */
831
+ capSpanAttributes(span) {
832
+ var _a;
833
+ const maxChars = applyOtelSpanAttributeLimit(
834
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
835
+ );
836
+ const attrs = span.attributes;
837
+ if (!attrs || attrs.length === 0) return span;
838
+ let nextAttrs;
839
+ for (let i = 0; i < attrs.length; i++) {
840
+ const attr = attrs[i];
841
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
842
+ if (typeof value !== "string" || value.length <= maxChars) continue;
843
+ if (!nextAttrs) nextAttrs = attrs.slice();
844
+ nextAttrs[i] = {
845
+ key: attr.key,
846
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
847
+ };
848
+ }
849
+ if (!nextAttrs) return span;
850
+ return { ...span, attributes: nextAttrs };
851
+ }
852
+ /**
853
+ * Apply the user `transformSpan` hook (if any) followed by the default
854
+ * redactor (unless disabled). Returns either the (possibly new) span to
855
+ * ship, or `null` to drop the span entirely.
856
+ *
857
+ * Ordering: user hook runs first so callers can rewrite the span freely
858
+ * (rename attrs, add new ones, scrub things the default doesn't know
859
+ * about). The default redactor then runs on whatever the user produced,
860
+ * acting as the always-on floor for documented BYOK secrets. If the user
861
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
862
+ *
863
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
864
+ * hook can never accidentally ship raw, un-redacted spans.
865
+ */
866
+ redactSpan(span) {
867
+ let current = span;
868
+ if (this.transformSpanHook) {
869
+ try {
870
+ const result = this.transformSpanHook(current);
871
+ if (result === null) return null;
872
+ if (result !== void 0) current = result;
873
+ } catch (err) {
874
+ if (this.debug) {
875
+ const msg = err instanceof Error ? err.message : String(err);
876
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
877
+ }
878
+ return null;
879
+ }
880
+ }
881
+ if (!this.disableDefaultRedaction) {
882
+ current = defaultTransformSpan(current);
883
+ }
884
+ return this.capSpanAttributes(current);
575
885
  }
576
886
  isDebugEnabled() {
577
887
  return this.debug;
@@ -589,8 +899,8 @@ var TraceShipper = class {
589
899
  ];
590
900
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
591
901
  const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
592
- if (this.localDebuggerUrl) {
593
- const openSpan = buildOtlpSpan({
902
+ this.mirrorToLocalDebugger(
903
+ buildOtlpSpan({
594
904
  ids: span.ids,
595
905
  name: span.name,
596
906
  startTimeUnixNano: span.startTimeUnixNano,
@@ -598,16 +908,21 @@ var TraceShipper = class {
598
908
  // placeholder — will be updated on endSpan
599
909
  attributes: span.attributes,
600
910
  status: { code: SpanStatusCode.UNSET }
601
- });
602
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
603
- mirrorTraceExportToLocalDebugger(body, {
604
- baseUrl: this.localDebuggerUrl,
605
- debug: false,
606
- sdkName: this.sdkName
607
- });
608
- }
911
+ })
912
+ );
609
913
  return span;
610
914
  }
915
+ mirrorToLocalDebugger(span) {
916
+ if (!this.localDebuggerUrl) return;
917
+ const redacted = this.redactSpan(span);
918
+ if (redacted === null) return;
919
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
920
+ mirrorTraceExportToLocalDebugger(body, {
921
+ baseUrl: this.localDebuggerUrl,
922
+ debug: false,
923
+ sdkName: this.sdkName
924
+ });
925
+ }
611
926
  endSpan(span, extra) {
612
927
  var _a, _b;
613
928
  if (span.endTimeUnixNano) return;
@@ -629,14 +944,7 @@ var TraceShipper = class {
629
944
  status
630
945
  });
631
946
  this.enqueue(otlp);
632
- if (this.localDebuggerUrl) {
633
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
634
- mirrorTraceExportToLocalDebugger(body, {
635
- baseUrl: this.localDebuggerUrl,
636
- debug: false,
637
- sdkName: this.sdkName
638
- });
639
- }
947
+ this.mirrorToLocalDebugger(otlp);
640
948
  }
641
949
  createSpan(args) {
642
950
  var _a;
@@ -654,14 +962,7 @@ var TraceShipper = class {
654
962
  status: args.status
655
963
  });
656
964
  this.enqueue(otlp);
657
- if (this.localDebuggerUrl) {
658
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
659
- mirrorTraceExportToLocalDebugger(body, {
660
- baseUrl: this.localDebuggerUrl,
661
- debug: false,
662
- sdkName: this.sdkName
663
- });
664
- }
965
+ this.mirrorToLocalDebugger(otlp);
665
966
  }
666
967
  enqueue(span) {
667
968
  if (!this.enabled) return;
@@ -673,10 +974,12 @@ var TraceShipper = class {
673
974
  )}`
674
975
  );
675
976
  }
977
+ const redacted = this.redactSpan(span);
978
+ if (redacted === null) return;
676
979
  if (this.queue.length >= this.maxQueueSize) {
677
980
  this.queue.shift();
678
981
  }
679
- this.queue.push(span);
982
+ this.queue.push(redacted);
680
983
  if (this.queue.length >= this.maxBatchSize) {
681
984
  void this.flush().catch(() => {
682
985
  });
@@ -699,6 +1002,16 @@ var TraceShipper = class {
699
1002
  while (this.queue.length > 0) {
700
1003
  const batch = this.queue.splice(0, this.maxBatchSize);
701
1004
  if (!this.writeKey) continue;
1005
+ const opts = this.requestOpts();
1006
+ if (!opts) {
1007
+ rateLimitedLog(
1008
+ `${this.prefix}.shutdown_deadline`,
1009
+ () => console.warn(
1010
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1011
+ )
1012
+ );
1013
+ continue;
1014
+ }
702
1015
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
703
1016
  const url = `${this.baseUrl}traces`;
704
1017
  if (this.debug) {
@@ -707,11 +1020,7 @@ var TraceShipper = class {
707
1020
  endpoint: url
708
1021
  });
709
1022
  }
710
- const p = postJson(url, body, this.authHeaders(), {
711
- maxAttempts: 3,
712
- debug: this.debug,
713
- sdkName: this.sdkName
714
- });
1023
+ const p = postJson(url, body, this.authHeaders(), opts);
715
1024
  this.inFlight.add(p);
716
1025
  try {
717
1026
  try {
@@ -719,21 +1028,61 @@ var TraceShipper = class {
719
1028
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
720
1029
  } catch (err) {
721
1030
  const msg = err instanceof Error ? err.message : String(err);
722
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1031
+ rateLimitedLog(
1032
+ `${this.prefix}.send_spans_failed`,
1033
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1034
+ );
723
1035
  }
724
1036
  } finally {
725
1037
  this.inFlight.delete(p);
726
1038
  }
727
1039
  }
728
1040
  }
1041
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1042
+ requestOpts() {
1043
+ if (this.shutdownDeadlineAt !== void 0) {
1044
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1045
+ if (remainingMs <= 0) return null;
1046
+ return {
1047
+ maxAttempts: 1,
1048
+ debug: this.debug,
1049
+ sdkName: this.sdkName,
1050
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1051
+ };
1052
+ }
1053
+ if (this.hasShutdown) {
1054
+ return {
1055
+ maxAttempts: 1,
1056
+ debug: this.debug,
1057
+ sdkName: this.sdkName,
1058
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1059
+ };
1060
+ }
1061
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1062
+ }
729
1063
  async shutdown() {
730
- if (this.timer) {
731
- clearTimeout(this.timer);
732
- this.timer = void 0;
1064
+ this.hasShutdown = true;
1065
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1066
+ try {
1067
+ if (this.timer) {
1068
+ clearTimeout(this.timer);
1069
+ this.timer = void 0;
1070
+ }
1071
+ const drain = async () => {
1072
+ await this.flush();
1073
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1074
+ })));
1075
+ };
1076
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1077
+ if (!settled) {
1078
+ rateLimitedLog(
1079
+ `${this.prefix}.shutdown_deadline`,
1080
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1081
+ );
1082
+ }
1083
+ } finally {
1084
+ this.shutdownDeadlineAt = void 0;
733
1085
  }
734
- await this.flush();
735
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
736
- })));
737
1086
  }
738
1087
  };
739
1088
 
@@ -744,7 +1093,7 @@ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage;
744
1093
  // package.json
745
1094
  var package_default = {
746
1095
  name: "@raindrop-ai/pi-agent",
747
- version: "0.0.3",
1096
+ version: "0.0.5",
748
1097
  description: "Raindrop observability for Pi Agent \u2014 automatic tracing via subscriber or pi-coding-agent extension",
749
1098
  type: "module",
750
1099
  license: "MIT",
@@ -794,18 +1143,18 @@ var package_default = {
794
1143
  "test:watch": "vitest"
795
1144
  },
796
1145
  peerDependencies: {
797
- "@mariozechner/pi-agent-core": ">=0.60.0",
798
- "@mariozechner/pi-coding-agent": ">=0.65.2"
1146
+ "@earendil-works/pi-agent-core": ">=0.74.0",
1147
+ "@earendil-works/pi-coding-agent": ">=0.74.0"
799
1148
  },
800
1149
  peerDependenciesMeta: {
801
- "@mariozechner/pi-coding-agent": {
1150
+ "@earendil-works/pi-coding-agent": {
802
1151
  optional: true
803
1152
  }
804
1153
  },
805
1154
  devDependencies: {
806
1155
  "@raindrop-ai/core": "workspace:*",
807
- "@mariozechner/pi-agent-core": "^0.66.0",
808
- "@mariozechner/pi-coding-agent": "^0.66.0",
1156
+ "@earendil-works/pi-agent-core": "^0.78.0",
1157
+ "@earendil-works/pi-coding-agent": "^0.78.0",
809
1158
  "@types/node": "^20.11.17",
810
1159
  msw: "^2.12.7",
811
1160
  tsup: "^8.5.1",
@@ -942,19 +1291,82 @@ function formatToolSpanName(toolName, args) {
942
1291
  }
943
1292
  return toolName;
944
1293
  }
945
- function safeStringify(value) {
1294
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1295
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1296
+ var MAX_ATTR_LENGTH = 32768;
1297
+ var MAX_BOUNDED_DEPTH = 12;
1298
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1299
+ if (value.length <= limit) return value;
1300
+ if (limit > TRUNCATION_MARKER2.length) {
1301
+ return value.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1302
+ }
1303
+ return value.slice(0, limit);
1304
+ }
1305
+ function boundedClone2(value, budget, depth) {
1306
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1307
+ if (typeof value === "string") {
1308
+ if (value.length > budget.remaining) {
1309
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1310
+ budget.remaining = 0;
1311
+ return taken;
1312
+ }
1313
+ budget.remaining -= Math.max(value.length, 1);
1314
+ return value;
1315
+ }
1316
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1317
+ budget.remaining -= 8;
1318
+ return value;
1319
+ }
1320
+ if (typeof value !== "object") {
1321
+ budget.remaining -= 8;
1322
+ return value;
1323
+ }
1324
+ if (depth >= MAX_BOUNDED_DEPTH) {
1325
+ budget.remaining -= 16;
1326
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1327
+ }
1328
+ const withToJson = value;
1329
+ if (typeof withToJson.toJSON === "function") {
1330
+ try {
1331
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1332
+ } catch (e) {
1333
+ }
1334
+ }
1335
+ if (Array.isArray(value)) {
1336
+ const out2 = [];
1337
+ for (const item of value) {
1338
+ if (budget.remaining <= 0) {
1339
+ out2.push(TRUNCATION_MARKER2);
1340
+ break;
1341
+ }
1342
+ out2.push(boundedClone2(item, budget, depth + 1));
1343
+ }
1344
+ return out2;
1345
+ }
1346
+ const out = {};
1347
+ for (const key of Object.keys(value)) {
1348
+ if (budget.remaining <= 0) {
1349
+ out["..."] = TRUNCATION_MARKER2;
1350
+ break;
1351
+ }
1352
+ budget.remaining -= Math.max(key.length, 1);
1353
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1354
+ }
1355
+ return out;
1356
+ }
1357
+ function safeStringify(value, limit = MAX_ATTR_LENGTH) {
946
1358
  if (value === void 0 || value === null) return void 0;
947
1359
  try {
948
- return JSON.stringify(value);
1360
+ const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
1361
+ return JSON.stringify(pruned);
949
1362
  } catch (e) {
950
1363
  try {
951
- return String(value);
1364
+ return capText2(String(value), limit);
952
1365
  } catch (e2) {
953
1366
  return "[unserializable]";
954
1367
  }
955
1368
  }
956
1369
  }
957
- var MAX_ATTR_LENGTH = 32768;
958
1370
  function truncate(value) {
959
1371
  if (value === void 0) return void 0;
960
1372
  if (value.length <= MAX_ATTR_LENGTH) return value;
@@ -1000,6 +1412,7 @@ export {
1000
1412
  extractAssistantText,
1001
1413
  extractToolCallIds,
1002
1414
  formatToolSpanName,
1415
+ capText2 as capText,
1003
1416
  safeStringify,
1004
1417
  truncate,
1005
1418
  getHostname,