@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.
package/dist/index.cjs CHANGED
@@ -24,7 +24,7 @@ __export(index_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(index_exports);
26
26
 
27
- // ../core/dist/chunk-OLYXZCOK.js
27
+ // ../core/dist/chunk-U5HUTMR5.js
28
28
  function getCrypto() {
29
29
  const c = globalThis.crypto;
30
30
  return c;
@@ -79,6 +79,8 @@ function base64Encode(bytes) {
79
79
  }
80
80
  return out;
81
81
  }
82
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
83
+ var MAX_RETRY_DELAY_MS = 3e4;
82
84
  function wait(ms) {
83
85
  return new Promise((resolve) => setTimeout(resolve, ms));
84
86
  }
@@ -86,6 +88,46 @@ function formatEndpoint(endpoint) {
86
88
  if (!endpoint) return void 0;
87
89
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
88
90
  }
91
+ function redactUrlForLog(url) {
92
+ try {
93
+ const parsed = new URL(url);
94
+ parsed.username = "";
95
+ parsed.password = "";
96
+ parsed.search = "";
97
+ parsed.hash = "";
98
+ return parsed.toString();
99
+ } catch (e) {
100
+ return "<unparseable-url>";
101
+ }
102
+ }
103
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
104
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
105
+ function rateLimitedLog(key, log) {
106
+ const now = Date.now();
107
+ const last = rateLimitedLogLast.get(key);
108
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
109
+ return false;
110
+ }
111
+ rateLimitedLogLast.set(key, now);
112
+ log();
113
+ return true;
114
+ }
115
+ async function raceWithTimeout(promise, timeoutMs) {
116
+ let timer;
117
+ const settledInTime = await Promise.race([
118
+ promise.then(
119
+ () => true,
120
+ () => true
121
+ ),
122
+ new Promise((resolve) => {
123
+ var _a;
124
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
125
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
126
+ })
127
+ ]);
128
+ if (timer) clearTimeout(timer);
129
+ return settledInTime;
130
+ }
89
131
  function parseRetryAfter(headers) {
90
132
  var _a;
91
133
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -102,12 +144,12 @@ function parseRetryAfter(headers) {
102
144
  function getRetryDelayMs(attemptNumber, previousError) {
103
145
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
104
146
  const v = previousError.retryAfterMs;
105
- if (typeof v === "number") return Math.max(0, v);
147
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
106
148
  }
107
149
  if (attemptNumber <= 1) return 0;
108
150
  const base = 500;
109
151
  const factor = Math.pow(2, attemptNumber - 2);
110
- return base * factor;
152
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
111
153
  }
112
154
  async function withRetry(operation, opName, opts) {
113
155
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -142,7 +184,9 @@ async function withRetry(operation, opName, opts) {
142
184
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
143
185
  }
144
186
  async function postJson(url, body, headers, opts) {
145
- const opName = `POST ${url}`;
187
+ var _a;
188
+ const opName = `POST ${redactUrlForLog(url)}`;
189
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
146
190
  await withRetry(
147
191
  async () => {
148
192
  const resp = await fetch(url, {
@@ -151,7 +195,8 @@ async function postJson(url, body, headers, opts) {
151
195
  "Content-Type": "application/json",
152
196
  ...headers
153
197
  },
154
- body: JSON.stringify(body)
198
+ body: JSON.stringify(body),
199
+ signal: AbortSignal.timeout(timeoutMs)
155
200
  });
156
201
  if (!resp.ok) {
157
202
  const text = await resp.text().catch(() => "");
@@ -168,6 +213,27 @@ async function postJson(url, body, headers, opts) {
168
213
  opts
169
214
  );
170
215
  }
216
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
217
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
218
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
219
+ function resolveMaxTextFieldChars(value) {
220
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
221
+ return Math.floor(value);
222
+ }
223
+ return currentDefaultMaxTextFieldChars;
224
+ }
225
+ function truncateToLimit(text, limit) {
226
+ if (limit > TRUNCATION_MARKER.length) {
227
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
228
+ }
229
+ return text.slice(0, Math.max(0, limit));
230
+ }
231
+ function capText(value, limit) {
232
+ if (typeof value !== "string") return value;
233
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
234
+ if (value.length <= max) return value;
235
+ return truncateToLimit(value, max);
236
+ }
171
237
  var SpanStatusCode = {
172
238
  UNSET: 0,
173
239
  OK: 1,
@@ -310,6 +376,8 @@ function mirrorPartialEventToLocalDebugger(event, options = {}) {
310
376
  }).catch(() => {
311
377
  });
312
378
  }
379
+ var SHUTDOWN_DEADLINE_MS = 1e4;
380
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
313
381
  function mergePatches(target, source) {
314
382
  var _a, _b, _c, _d;
315
383
  const out = { ...target, ...source };
@@ -327,6 +395,7 @@ var EventShipper = class {
327
395
  this.sticky = /* @__PURE__ */ new Map();
328
396
  this.timers = /* @__PURE__ */ new Map();
329
397
  this.inFlight = /* @__PURE__ */ new Set();
398
+ this.hasShutdown = false;
330
399
  var _a, _b, _c, _d, _e, _f, _g, _h;
331
400
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
332
401
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -336,6 +405,7 @@ var EventShipper = class {
336
405
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
337
406
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
338
407
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
408
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
339
409
  this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
340
410
  if (this.debug && this.localDebuggerUrl) {
341
411
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
@@ -358,10 +428,52 @@ var EventShipper = class {
358
428
  authHeaders() {
359
429
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
360
430
  }
431
+ /**
432
+ * Build the retry/timeout options for one POST, honoring the shutdown
433
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
434
+ * the caller must drop the payload (with a rate-limited warning) instead
435
+ * of issuing a request that could outlive process exit.
436
+ *
437
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
438
+ * path is mid-drain takes effect immediately: no further retries, and the
439
+ * per-attempt timeout is clamped to the remaining window. After
440
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
441
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
442
+ * run as a single short attempt rather than regaining the full retry
443
+ * schedule.
444
+ */
445
+ requestOpts() {
446
+ if (this.shutdownDeadlineAt !== void 0) {
447
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
448
+ if (remainingMs <= 0) return null;
449
+ return {
450
+ maxAttempts: 1,
451
+ debug: this.debug,
452
+ sdkName: this.sdkName,
453
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
454
+ };
455
+ }
456
+ if (this.hasShutdown) {
457
+ return {
458
+ maxAttempts: 1,
459
+ debug: this.debug,
460
+ sdkName: this.sdkName,
461
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
462
+ };
463
+ }
464
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
465
+ }
361
466
  async patch(eventId, patch) {
362
467
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
363
468
  if (!this.enabled) return;
364
469
  if (!eventId || !eventId.trim()) return;
470
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
471
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
472
+ patch = { ...patch, input: capText(patch.input, maxChars) };
473
+ }
474
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
475
+ patch = { ...patch, output: capText(patch.output, maxChars) };
476
+ }
365
477
  if (this.debug) {
366
478
  console.log(`${this.prefix} queue patch`, {
367
479
  eventId,
@@ -408,9 +520,16 @@ var EventShipper = class {
408
520
  })));
409
521
  }
410
522
  async shutdown() {
411
- for (const t of this.timers.values()) clearTimeout(t);
412
- this.timers.clear();
413
- await this.flush();
523
+ this.hasShutdown = true;
524
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
525
+ try {
526
+ for (const t of this.timers.values()) clearTimeout(t);
527
+ this.timers.clear();
528
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
529
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
530
+ } finally {
531
+ this.shutdownDeadlineAt = void 0;
532
+ }
414
533
  }
415
534
  async trackSignal(signal) {
416
535
  var _a, _b;
@@ -432,15 +551,19 @@ var EventShipper = class {
432
551
  ];
433
552
  if (!this.writeKey) return;
434
553
  const url = `${this.baseUrl}signals/track`;
554
+ const opts = this.requestOpts();
555
+ if (!opts) {
556
+ this.warnShutdownDrop("signal");
557
+ return;
558
+ }
435
559
  try {
436
- await postJson(url, body, this.authHeaders(), {
437
- maxAttempts: 3,
438
- debug: this.debug,
439
- sdkName: this.sdkName
440
- });
560
+ await postJson(url, body, this.authHeaders(), opts);
441
561
  } catch (err) {
442
562
  const msg = err instanceof Error ? err.message : String(err);
443
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
563
+ rateLimitedLog(
564
+ `${this.prefix}.send_signal_failed`,
565
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
566
+ );
444
567
  }
445
568
  }
446
569
  async identify(users) {
@@ -464,17 +587,27 @@ var EventShipper = class {
464
587
  if (!this.writeKey) return;
465
588
  if (body.length === 0) return;
466
589
  const url = `${this.baseUrl}users/identify`;
590
+ const opts = this.requestOpts();
591
+ if (!opts) {
592
+ this.warnShutdownDrop("identify");
593
+ return;
594
+ }
467
595
  try {
468
- await postJson(url, body, this.authHeaders(), {
469
- maxAttempts: 3,
470
- debug: this.debug,
471
- sdkName: this.sdkName
472
- });
596
+ await postJson(url, body, this.authHeaders(), opts);
473
597
  } catch (err) {
474
598
  const msg = err instanceof Error ? err.message : String(err);
475
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
599
+ rateLimitedLog(
600
+ `${this.prefix}.send_identify_failed`,
601
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
602
+ );
476
603
  }
477
604
  }
605
+ warnShutdownDrop(what) {
606
+ rateLimitedLog(
607
+ `${this.prefix}.shutdown_deadline`,
608
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
609
+ );
610
+ }
478
611
  async flushOne(eventId) {
479
612
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
480
613
  if (!this.enabled) return;
@@ -550,11 +683,13 @@ var EventShipper = class {
550
683
  if (!isPending) this.sticky.delete(eventId);
551
684
  return;
552
685
  }
553
- const p = postJson(url, payload, this.authHeaders(), {
554
- maxAttempts: 3,
555
- debug: this.debug,
556
- sdkName: this.sdkName
557
- });
686
+ const opts = this.requestOpts();
687
+ if (!opts) {
688
+ this.warnShutdownDrop(`track_partial ${eventId}`);
689
+ if (!isPending) this.sticky.delete(eventId);
690
+ return;
691
+ }
692
+ const p = postJson(url, payload, this.authHeaders(), opts);
558
693
  this.inFlight.add(p);
559
694
  try {
560
695
  try {
@@ -564,7 +699,10 @@ var EventShipper = class {
564
699
  }
565
700
  } catch (err) {
566
701
  const msg = err instanceof Error ? err.message : String(err);
567
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
702
+ rateLimitedLog(
703
+ `${this.prefix}.send_track_partial_failed`,
704
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
705
+ );
568
706
  }
569
707
  } finally {
570
708
  this.inFlight.delete(p);
@@ -574,10 +712,113 @@ var EventShipper = class {
574
712
  }
575
713
  }
576
714
  };
715
+ var DEFAULT_SECRET_KEY_NAMES = [
716
+ "apikey",
717
+ "apisecret",
718
+ "apitoken",
719
+ "secretaccesskey",
720
+ "sessiontoken",
721
+ "privatekey",
722
+ "privatekeyid",
723
+ "clientsecret",
724
+ "accesstoken",
725
+ "refreshtoken",
726
+ "oauthtoken",
727
+ "bearertoken",
728
+ "authorization",
729
+ "password",
730
+ "passphrase"
731
+ ];
732
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
733
+ function normalizeKeyName(name) {
734
+ return name.toLowerCase().replace(/[-_.]/g, "");
735
+ }
736
+ function redactSecretsInObject(value, options) {
737
+ var _a, _b;
738
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
739
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
740
+ const seen = /* @__PURE__ */ new WeakSet();
741
+ const walk = (node) => {
742
+ if (node === null || typeof node !== "object") return node;
743
+ if (seen.has(node)) return "[CIRCULAR]";
744
+ seen.add(node);
745
+ if (Array.isArray(node)) {
746
+ return node.map((item) => walk(item));
747
+ }
748
+ const out = {};
749
+ for (const [k, v] of Object.entries(node)) {
750
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
751
+ out[k] = placeholder;
752
+ } else {
753
+ out[k] = walk(v);
754
+ }
755
+ }
756
+ return out;
757
+ };
758
+ return walk(value);
759
+ }
760
+ function buildSecretSet(names) {
761
+ const set = /* @__PURE__ */ new Set();
762
+ for (const name of names) set.add(normalizeKeyName(name));
763
+ return set;
764
+ }
765
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
766
+ "ai.request.providerOptions",
767
+ "ai.response.providerMetadata"
768
+ ];
769
+ function defaultTransformSpan(span) {
770
+ const attrs = span.attributes;
771
+ if (!attrs || attrs.length === 0) return span;
772
+ let nextAttrs;
773
+ for (let i = 0; i < attrs.length; i++) {
774
+ const attr = attrs[i];
775
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
776
+ if (redacted === void 0) continue;
777
+ if (!nextAttrs) nextAttrs = attrs.slice();
778
+ nextAttrs[i] = { key: attr.key, value: redacted };
779
+ }
780
+ if (!nextAttrs) return span;
781
+ return { ...span, attributes: nextAttrs };
782
+ }
783
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
784
+ function redactJsonAttributeValue(key, value) {
785
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
786
+ const json = value.stringValue;
787
+ if (typeof json !== "string" || json.length === 0) return void 0;
788
+ let parsed;
789
+ try {
790
+ parsed = JSON.parse(json);
791
+ } catch (e) {
792
+ return void 0;
793
+ }
794
+ const scrubbed = redactSecretsInObject(parsed);
795
+ let scrubbedJson;
796
+ try {
797
+ scrubbedJson = JSON.stringify(scrubbed);
798
+ } catch (e) {
799
+ return void 0;
800
+ }
801
+ if (scrubbedJson === json) return void 0;
802
+ return { stringValue: scrubbedJson };
803
+ }
804
+ function applyOtelSpanAttributeLimit(limit) {
805
+ var _a, _b;
806
+ try {
807
+ 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;
808
+ if (!raw) return limit;
809
+ const parsed = Number.parseInt(raw, 10);
810
+ if (Number.isFinite(parsed) && parsed > 0) {
811
+ return Math.min(limit, parsed);
812
+ }
813
+ } catch (e) {
814
+ }
815
+ return limit;
816
+ }
577
817
  var TraceShipper = class {
578
818
  constructor(opts) {
579
819
  this.queue = [];
580
820
  this.inFlight = /* @__PURE__ */ new Set();
821
+ this.hasShutdown = false;
581
822
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
582
823
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
583
824
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -595,6 +836,75 @@ var TraceShipper = class {
595
836
  if (this.debug && this.localDebuggerUrl) {
596
837
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
597
838
  }
839
+ this.transformSpanHook = opts.transformSpan;
840
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
841
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
842
+ }
843
+ /**
844
+ * Cap every string attribute value on the span. O(#attributes) length
845
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
846
+ * pipeline so the default secret-scrub still sees parseable JSON in
847
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
848
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
849
+ * in the surviving prefix).
850
+ *
851
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
852
+ * for span content, matching the Python SDK and the OTel SDK convention.
853
+ */
854
+ capSpanAttributes(span) {
855
+ var _a;
856
+ const maxChars = applyOtelSpanAttributeLimit(
857
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
858
+ );
859
+ const attrs = span.attributes;
860
+ if (!attrs || attrs.length === 0) return span;
861
+ let nextAttrs;
862
+ for (let i = 0; i < attrs.length; i++) {
863
+ const attr = attrs[i];
864
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
865
+ if (typeof value !== "string" || value.length <= maxChars) continue;
866
+ if (!nextAttrs) nextAttrs = attrs.slice();
867
+ nextAttrs[i] = {
868
+ key: attr.key,
869
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
870
+ };
871
+ }
872
+ if (!nextAttrs) return span;
873
+ return { ...span, attributes: nextAttrs };
874
+ }
875
+ /**
876
+ * Apply the user `transformSpan` hook (if any) followed by the default
877
+ * redactor (unless disabled). Returns either the (possibly new) span to
878
+ * ship, or `null` to drop the span entirely.
879
+ *
880
+ * Ordering: user hook runs first so callers can rewrite the span freely
881
+ * (rename attrs, add new ones, scrub things the default doesn't know
882
+ * about). The default redactor then runs on whatever the user produced,
883
+ * acting as the always-on floor for documented BYOK secrets. If the user
884
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
885
+ *
886
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
887
+ * hook can never accidentally ship raw, un-redacted spans.
888
+ */
889
+ redactSpan(span) {
890
+ let current = span;
891
+ if (this.transformSpanHook) {
892
+ try {
893
+ const result = this.transformSpanHook(current);
894
+ if (result === null) return null;
895
+ if (result !== void 0) current = result;
896
+ } catch (err) {
897
+ if (this.debug) {
898
+ const msg = err instanceof Error ? err.message : String(err);
899
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
900
+ }
901
+ return null;
902
+ }
903
+ }
904
+ if (!this.disableDefaultRedaction) {
905
+ current = defaultTransformSpan(current);
906
+ }
907
+ return this.capSpanAttributes(current);
598
908
  }
599
909
  isDebugEnabled() {
600
910
  return this.debug;
@@ -612,8 +922,8 @@ var TraceShipper = class {
612
922
  ];
613
923
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
614
924
  const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
615
- if (this.localDebuggerUrl) {
616
- const openSpan = buildOtlpSpan({
925
+ this.mirrorToLocalDebugger(
926
+ buildOtlpSpan({
617
927
  ids: span.ids,
618
928
  name: span.name,
619
929
  startTimeUnixNano: span.startTimeUnixNano,
@@ -621,16 +931,21 @@ var TraceShipper = class {
621
931
  // placeholder — will be updated on endSpan
622
932
  attributes: span.attributes,
623
933
  status: { code: SpanStatusCode.UNSET }
624
- });
625
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
626
- mirrorTraceExportToLocalDebugger(body, {
627
- baseUrl: this.localDebuggerUrl,
628
- debug: false,
629
- sdkName: this.sdkName
630
- });
631
- }
934
+ })
935
+ );
632
936
  return span;
633
937
  }
938
+ mirrorToLocalDebugger(span) {
939
+ if (!this.localDebuggerUrl) return;
940
+ const redacted = this.redactSpan(span);
941
+ if (redacted === null) return;
942
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
943
+ mirrorTraceExportToLocalDebugger(body, {
944
+ baseUrl: this.localDebuggerUrl,
945
+ debug: false,
946
+ sdkName: this.sdkName
947
+ });
948
+ }
634
949
  endSpan(span, extra) {
635
950
  var _a, _b;
636
951
  if (span.endTimeUnixNano) return;
@@ -652,14 +967,7 @@ var TraceShipper = class {
652
967
  status
653
968
  });
654
969
  this.enqueue(otlp);
655
- if (this.localDebuggerUrl) {
656
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
657
- mirrorTraceExportToLocalDebugger(body, {
658
- baseUrl: this.localDebuggerUrl,
659
- debug: false,
660
- sdkName: this.sdkName
661
- });
662
- }
970
+ this.mirrorToLocalDebugger(otlp);
663
971
  }
664
972
  createSpan(args) {
665
973
  var _a;
@@ -677,14 +985,7 @@ var TraceShipper = class {
677
985
  status: args.status
678
986
  });
679
987
  this.enqueue(otlp);
680
- if (this.localDebuggerUrl) {
681
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
682
- mirrorTraceExportToLocalDebugger(body, {
683
- baseUrl: this.localDebuggerUrl,
684
- debug: false,
685
- sdkName: this.sdkName
686
- });
687
- }
988
+ this.mirrorToLocalDebugger(otlp);
688
989
  }
689
990
  enqueue(span) {
690
991
  if (!this.enabled) return;
@@ -696,10 +997,12 @@ var TraceShipper = class {
696
997
  )}`
697
998
  );
698
999
  }
1000
+ const redacted = this.redactSpan(span);
1001
+ if (redacted === null) return;
699
1002
  if (this.queue.length >= this.maxQueueSize) {
700
1003
  this.queue.shift();
701
1004
  }
702
- this.queue.push(span);
1005
+ this.queue.push(redacted);
703
1006
  if (this.queue.length >= this.maxBatchSize) {
704
1007
  void this.flush().catch(() => {
705
1008
  });
@@ -722,6 +1025,16 @@ var TraceShipper = class {
722
1025
  while (this.queue.length > 0) {
723
1026
  const batch = this.queue.splice(0, this.maxBatchSize);
724
1027
  if (!this.writeKey) continue;
1028
+ const opts = this.requestOpts();
1029
+ if (!opts) {
1030
+ rateLimitedLog(
1031
+ `${this.prefix}.shutdown_deadline`,
1032
+ () => console.warn(
1033
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1034
+ )
1035
+ );
1036
+ continue;
1037
+ }
725
1038
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
726
1039
  const url = `${this.baseUrl}traces`;
727
1040
  if (this.debug) {
@@ -730,11 +1043,7 @@ var TraceShipper = class {
730
1043
  endpoint: url
731
1044
  });
732
1045
  }
733
- const p = postJson(url, body, this.authHeaders(), {
734
- maxAttempts: 3,
735
- debug: this.debug,
736
- sdkName: this.sdkName
737
- });
1046
+ const p = postJson(url, body, this.authHeaders(), opts);
738
1047
  this.inFlight.add(p);
739
1048
  try {
740
1049
  try {
@@ -742,21 +1051,61 @@ var TraceShipper = class {
742
1051
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
743
1052
  } catch (err) {
744
1053
  const msg = err instanceof Error ? err.message : String(err);
745
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1054
+ rateLimitedLog(
1055
+ `${this.prefix}.send_spans_failed`,
1056
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1057
+ );
746
1058
  }
747
1059
  } finally {
748
1060
  this.inFlight.delete(p);
749
1061
  }
750
1062
  }
751
1063
  }
1064
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1065
+ requestOpts() {
1066
+ if (this.shutdownDeadlineAt !== void 0) {
1067
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1068
+ if (remainingMs <= 0) return null;
1069
+ return {
1070
+ maxAttempts: 1,
1071
+ debug: this.debug,
1072
+ sdkName: this.sdkName,
1073
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1074
+ };
1075
+ }
1076
+ if (this.hasShutdown) {
1077
+ return {
1078
+ maxAttempts: 1,
1079
+ debug: this.debug,
1080
+ sdkName: this.sdkName,
1081
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1082
+ };
1083
+ }
1084
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1085
+ }
752
1086
  async shutdown() {
753
- if (this.timer) {
754
- clearTimeout(this.timer);
755
- this.timer = void 0;
1087
+ this.hasShutdown = true;
1088
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1089
+ try {
1090
+ if (this.timer) {
1091
+ clearTimeout(this.timer);
1092
+ this.timer = void 0;
1093
+ }
1094
+ const drain = async () => {
1095
+ await this.flush();
1096
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1097
+ })));
1098
+ };
1099
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1100
+ if (!settled) {
1101
+ rateLimitedLog(
1102
+ `${this.prefix}.shutdown_deadline`,
1103
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1104
+ );
1105
+ }
1106
+ } finally {
1107
+ this.shutdownDeadlineAt = void 0;
756
1108
  }
757
- await this.flush();
758
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
759
- })));
760
1109
  }
761
1110
  };
762
1111
 
@@ -767,7 +1116,7 @@ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = import_async_hooks.AsyncLocalStorage;
767
1116
  // package.json
768
1117
  var package_default = {
769
1118
  name: "@raindrop-ai/pi-agent",
770
- version: "0.0.3",
1119
+ version: "0.0.5",
771
1120
  description: "Raindrop observability for Pi Agent \u2014 automatic tracing via subscriber or pi-coding-agent extension",
772
1121
  type: "module",
773
1122
  license: "MIT",
@@ -817,18 +1166,18 @@ var package_default = {
817
1166
  "test:watch": "vitest"
818
1167
  },
819
1168
  peerDependencies: {
820
- "@mariozechner/pi-agent-core": ">=0.60.0",
821
- "@mariozechner/pi-coding-agent": ">=0.65.2"
1169
+ "@earendil-works/pi-agent-core": ">=0.74.0",
1170
+ "@earendil-works/pi-coding-agent": ">=0.74.0"
822
1171
  },
823
1172
  peerDependenciesMeta: {
824
- "@mariozechner/pi-coding-agent": {
1173
+ "@earendil-works/pi-coding-agent": {
825
1174
  optional: true
826
1175
  }
827
1176
  },
828
1177
  devDependencies: {
829
1178
  "@raindrop-ai/core": "workspace:*",
830
- "@mariozechner/pi-agent-core": "^0.66.0",
831
- "@mariozechner/pi-coding-agent": "^0.66.0",
1179
+ "@earendil-works/pi-agent-core": "^0.78.0",
1180
+ "@earendil-works/pi-coding-agent": "^0.78.0",
832
1181
  "@types/node": "^20.11.17",
833
1182
  msw: "^2.12.7",
834
1183
  tsup: "^8.5.1",
@@ -965,19 +1314,82 @@ function formatToolSpanName(toolName, args) {
965
1314
  }
966
1315
  return toolName;
967
1316
  }
968
- function safeStringify(value) {
1317
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1318
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1319
+ var MAX_ATTR_LENGTH = 32768;
1320
+ var MAX_BOUNDED_DEPTH = 12;
1321
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1322
+ if (value.length <= limit) return value;
1323
+ if (limit > TRUNCATION_MARKER2.length) {
1324
+ return value.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1325
+ }
1326
+ return value.slice(0, limit);
1327
+ }
1328
+ function boundedClone2(value, budget, depth) {
1329
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1330
+ if (typeof value === "string") {
1331
+ if (value.length > budget.remaining) {
1332
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1333
+ budget.remaining = 0;
1334
+ return taken;
1335
+ }
1336
+ budget.remaining -= Math.max(value.length, 1);
1337
+ return value;
1338
+ }
1339
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1340
+ budget.remaining -= 8;
1341
+ return value;
1342
+ }
1343
+ if (typeof value !== "object") {
1344
+ budget.remaining -= 8;
1345
+ return value;
1346
+ }
1347
+ if (depth >= MAX_BOUNDED_DEPTH) {
1348
+ budget.remaining -= 16;
1349
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1350
+ }
1351
+ const withToJson = value;
1352
+ if (typeof withToJson.toJSON === "function") {
1353
+ try {
1354
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1355
+ } catch (e) {
1356
+ }
1357
+ }
1358
+ if (Array.isArray(value)) {
1359
+ const out2 = [];
1360
+ for (const item of value) {
1361
+ if (budget.remaining <= 0) {
1362
+ out2.push(TRUNCATION_MARKER2);
1363
+ break;
1364
+ }
1365
+ out2.push(boundedClone2(item, budget, depth + 1));
1366
+ }
1367
+ return out2;
1368
+ }
1369
+ const out = {};
1370
+ for (const key of Object.keys(value)) {
1371
+ if (budget.remaining <= 0) {
1372
+ out["..."] = TRUNCATION_MARKER2;
1373
+ break;
1374
+ }
1375
+ budget.remaining -= Math.max(key.length, 1);
1376
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1377
+ }
1378
+ return out;
1379
+ }
1380
+ function safeStringify(value, limit = MAX_ATTR_LENGTH) {
969
1381
  if (value === void 0 || value === null) return void 0;
970
1382
  try {
971
- return JSON.stringify(value);
1383
+ const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
1384
+ return JSON.stringify(pruned);
972
1385
  } catch (e) {
973
1386
  try {
974
- return String(value);
1387
+ return capText2(String(value), limit);
975
1388
  } catch (e2) {
976
1389
  return "[unserializable]";
977
1390
  }
978
1391
  }
979
1392
  }
980
- var MAX_ATTR_LENGTH = 32768;
981
1393
  function truncate(value) {
982
1394
  if (value === void 0) return void 0;
983
1395
  if (value.length <= MAX_ATTR_LENGTH) return value;
@@ -1094,9 +1506,10 @@ function createSubscriber(agent, eventShipper, traceShipper, defaultOptions, opt
1094
1506
  if (!currentRun) return;
1095
1507
  const userText = extractUserText(message);
1096
1508
  if (userText !== void 0) {
1097
- currentRun.currentInput = userText;
1509
+ const cappedInput = capText2(userText);
1510
+ currentRun.currentInput = cappedInput;
1098
1511
  if (eventShipper) {
1099
- eventShipper.patch(currentRun.eventId, { input: userText }).catch(() => {
1512
+ eventShipper.patch(currentRun.eventId, { input: cappedInput }).catch(() => {
1100
1513
  });
1101
1514
  }
1102
1515
  return;
@@ -1265,7 +1678,7 @@ function createSubscriber(agent, eventShipper, traceShipper, defaultOptions, opt
1265
1678
  traceShipper.endSpan(run.currentTurnSpan);
1266
1679
  run.currentTurnSpan = void 0;
1267
1680
  }
1268
- const outputText = run.outputParts.join("");
1681
+ const outputText = capText2(run.outputParts.join(""));
1269
1682
  if (traceShipper && run.rootSpan) {
1270
1683
  const rootAttrs = [
1271
1684
  attrString("ai.operationId", "generateText")