@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.
@@ -25,7 +25,7 @@ __export(extension_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(extension_exports);
27
27
 
28
- // ../core/dist/chunk-OLYXZCOK.js
28
+ // ../core/dist/chunk-U5HUTMR5.js
29
29
  function getCrypto() {
30
30
  const c = globalThis.crypto;
31
31
  return c;
@@ -72,6 +72,8 @@ function base64Encode(bytes) {
72
72
  function generateId() {
73
73
  return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
74
74
  }
75
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
76
+ var MAX_RETRY_DELAY_MS = 3e4;
75
77
  function wait(ms) {
76
78
  return new Promise((resolve) => setTimeout(resolve, ms));
77
79
  }
@@ -79,6 +81,46 @@ function formatEndpoint(endpoint) {
79
81
  if (!endpoint) return void 0;
80
82
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
81
83
  }
84
+ function redactUrlForLog(url) {
85
+ try {
86
+ const parsed = new URL(url);
87
+ parsed.username = "";
88
+ parsed.password = "";
89
+ parsed.search = "";
90
+ parsed.hash = "";
91
+ return parsed.toString();
92
+ } catch (e) {
93
+ return "<unparseable-url>";
94
+ }
95
+ }
96
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
97
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
98
+ function rateLimitedLog(key, log) {
99
+ const now = Date.now();
100
+ const last = rateLimitedLogLast.get(key);
101
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
102
+ return false;
103
+ }
104
+ rateLimitedLogLast.set(key, now);
105
+ log();
106
+ return true;
107
+ }
108
+ async function raceWithTimeout(promise, timeoutMs) {
109
+ let timer;
110
+ const settledInTime = await Promise.race([
111
+ promise.then(
112
+ () => true,
113
+ () => true
114
+ ),
115
+ new Promise((resolve) => {
116
+ var _a;
117
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
118
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
119
+ })
120
+ ]);
121
+ if (timer) clearTimeout(timer);
122
+ return settledInTime;
123
+ }
82
124
  function parseRetryAfter(headers) {
83
125
  var _a;
84
126
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -95,12 +137,12 @@ function parseRetryAfter(headers) {
95
137
  function getRetryDelayMs(attemptNumber, previousError) {
96
138
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
97
139
  const v = previousError.retryAfterMs;
98
- if (typeof v === "number") return Math.max(0, v);
140
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
99
141
  }
100
142
  if (attemptNumber <= 1) return 0;
101
143
  const base = 500;
102
144
  const factor = Math.pow(2, attemptNumber - 2);
103
- return base * factor;
145
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
104
146
  }
105
147
  async function withRetry(operation, opName, opts) {
106
148
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -135,7 +177,9 @@ async function withRetry(operation, opName, opts) {
135
177
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
136
178
  }
137
179
  async function postJson(url, body, headers, opts) {
138
- const opName = `POST ${url}`;
180
+ var _a;
181
+ const opName = `POST ${redactUrlForLog(url)}`;
182
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
139
183
  await withRetry(
140
184
  async () => {
141
185
  const resp = await fetch(url, {
@@ -144,7 +188,8 @@ async function postJson(url, body, headers, opts) {
144
188
  "Content-Type": "application/json",
145
189
  ...headers
146
190
  },
147
- body: JSON.stringify(body)
191
+ body: JSON.stringify(body),
192
+ signal: AbortSignal.timeout(timeoutMs)
148
193
  });
149
194
  if (!resp.ok) {
150
195
  const text = await resp.text().catch(() => "");
@@ -161,6 +206,27 @@ async function postJson(url, body, headers, opts) {
161
206
  opts
162
207
  );
163
208
  }
209
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
210
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
211
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
212
+ function resolveMaxTextFieldChars(value) {
213
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
214
+ return Math.floor(value);
215
+ }
216
+ return currentDefaultMaxTextFieldChars;
217
+ }
218
+ function truncateToLimit(text, limit) {
219
+ if (limit > TRUNCATION_MARKER.length) {
220
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
221
+ }
222
+ return text.slice(0, Math.max(0, limit));
223
+ }
224
+ function capText(value, limit) {
225
+ if (typeof value !== "string") return value;
226
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
227
+ if (value.length <= max) return value;
228
+ return truncateToLimit(value, max);
229
+ }
164
230
  var SpanStatusCode = {
165
231
  UNSET: 0,
166
232
  OK: 1,
@@ -303,6 +369,8 @@ function mirrorPartialEventToLocalDebugger(event, options = {}) {
303
369
  }).catch(() => {
304
370
  });
305
371
  }
372
+ var SHUTDOWN_DEADLINE_MS = 1e4;
373
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
306
374
  function mergePatches(target, source) {
307
375
  var _a, _b, _c, _d;
308
376
  const out = { ...target, ...source };
@@ -320,6 +388,7 @@ var EventShipper = class {
320
388
  this.sticky = /* @__PURE__ */ new Map();
321
389
  this.timers = /* @__PURE__ */ new Map();
322
390
  this.inFlight = /* @__PURE__ */ new Set();
391
+ this.hasShutdown = false;
323
392
  var _a, _b, _c, _d, _e, _f, _g, _h;
324
393
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
325
394
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -329,6 +398,7 @@ var EventShipper = class {
329
398
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
330
399
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
331
400
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
401
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
332
402
  this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
333
403
  if (this.debug && this.localDebuggerUrl) {
334
404
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
@@ -351,10 +421,52 @@ var EventShipper = class {
351
421
  authHeaders() {
352
422
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
353
423
  }
424
+ /**
425
+ * Build the retry/timeout options for one POST, honoring the shutdown
426
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
427
+ * the caller must drop the payload (with a rate-limited warning) instead
428
+ * of issuing a request that could outlive process exit.
429
+ *
430
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
431
+ * path is mid-drain takes effect immediately: no further retries, and the
432
+ * per-attempt timeout is clamped to the remaining window. After
433
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
434
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
435
+ * run as a single short attempt rather than regaining the full retry
436
+ * schedule.
437
+ */
438
+ requestOpts() {
439
+ if (this.shutdownDeadlineAt !== void 0) {
440
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
441
+ if (remainingMs <= 0) return null;
442
+ return {
443
+ maxAttempts: 1,
444
+ debug: this.debug,
445
+ sdkName: this.sdkName,
446
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
447
+ };
448
+ }
449
+ if (this.hasShutdown) {
450
+ return {
451
+ maxAttempts: 1,
452
+ debug: this.debug,
453
+ sdkName: this.sdkName,
454
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
455
+ };
456
+ }
457
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
458
+ }
354
459
  async patch(eventId, patch) {
355
460
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
356
461
  if (!this.enabled) return;
357
462
  if (!eventId || !eventId.trim()) return;
463
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
464
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
465
+ patch = { ...patch, input: capText(patch.input, maxChars) };
466
+ }
467
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
468
+ patch = { ...patch, output: capText(patch.output, maxChars) };
469
+ }
358
470
  if (this.debug) {
359
471
  console.log(`${this.prefix} queue patch`, {
360
472
  eventId,
@@ -401,9 +513,16 @@ var EventShipper = class {
401
513
  })));
402
514
  }
403
515
  async shutdown() {
404
- for (const t of this.timers.values()) clearTimeout(t);
405
- this.timers.clear();
406
- await this.flush();
516
+ this.hasShutdown = true;
517
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
518
+ try {
519
+ for (const t of this.timers.values()) clearTimeout(t);
520
+ this.timers.clear();
521
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
522
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
523
+ } finally {
524
+ this.shutdownDeadlineAt = void 0;
525
+ }
407
526
  }
408
527
  async trackSignal(signal) {
409
528
  var _a, _b;
@@ -425,15 +544,19 @@ var EventShipper = class {
425
544
  ];
426
545
  if (!this.writeKey) return;
427
546
  const url = `${this.baseUrl}signals/track`;
547
+ const opts = this.requestOpts();
548
+ if (!opts) {
549
+ this.warnShutdownDrop("signal");
550
+ return;
551
+ }
428
552
  try {
429
- await postJson(url, body, this.authHeaders(), {
430
- maxAttempts: 3,
431
- debug: this.debug,
432
- sdkName: this.sdkName
433
- });
553
+ await postJson(url, body, this.authHeaders(), opts);
434
554
  } catch (err) {
435
555
  const msg = err instanceof Error ? err.message : String(err);
436
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
556
+ rateLimitedLog(
557
+ `${this.prefix}.send_signal_failed`,
558
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
559
+ );
437
560
  }
438
561
  }
439
562
  async identify(users) {
@@ -457,17 +580,27 @@ var EventShipper = class {
457
580
  if (!this.writeKey) return;
458
581
  if (body.length === 0) return;
459
582
  const url = `${this.baseUrl}users/identify`;
583
+ const opts = this.requestOpts();
584
+ if (!opts) {
585
+ this.warnShutdownDrop("identify");
586
+ return;
587
+ }
460
588
  try {
461
- await postJson(url, body, this.authHeaders(), {
462
- maxAttempts: 3,
463
- debug: this.debug,
464
- sdkName: this.sdkName
465
- });
589
+ await postJson(url, body, this.authHeaders(), opts);
466
590
  } catch (err) {
467
591
  const msg = err instanceof Error ? err.message : String(err);
468
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
592
+ rateLimitedLog(
593
+ `${this.prefix}.send_identify_failed`,
594
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
595
+ );
469
596
  }
470
597
  }
598
+ warnShutdownDrop(what) {
599
+ rateLimitedLog(
600
+ `${this.prefix}.shutdown_deadline`,
601
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
602
+ );
603
+ }
471
604
  async flushOne(eventId) {
472
605
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
473
606
  if (!this.enabled) return;
@@ -543,11 +676,13 @@ var EventShipper = class {
543
676
  if (!isPending) this.sticky.delete(eventId);
544
677
  return;
545
678
  }
546
- const p = postJson(url, payload, this.authHeaders(), {
547
- maxAttempts: 3,
548
- debug: this.debug,
549
- sdkName: this.sdkName
550
- });
679
+ const opts = this.requestOpts();
680
+ if (!opts) {
681
+ this.warnShutdownDrop(`track_partial ${eventId}`);
682
+ if (!isPending) this.sticky.delete(eventId);
683
+ return;
684
+ }
685
+ const p = postJson(url, payload, this.authHeaders(), opts);
551
686
  this.inFlight.add(p);
552
687
  try {
553
688
  try {
@@ -557,7 +692,10 @@ var EventShipper = class {
557
692
  }
558
693
  } catch (err) {
559
694
  const msg = err instanceof Error ? err.message : String(err);
560
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
695
+ rateLimitedLog(
696
+ `${this.prefix}.send_track_partial_failed`,
697
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
698
+ );
561
699
  }
562
700
  } finally {
563
701
  this.inFlight.delete(p);
@@ -567,10 +705,113 @@ var EventShipper = class {
567
705
  }
568
706
  }
569
707
  };
708
+ var DEFAULT_SECRET_KEY_NAMES = [
709
+ "apikey",
710
+ "apisecret",
711
+ "apitoken",
712
+ "secretaccesskey",
713
+ "sessiontoken",
714
+ "privatekey",
715
+ "privatekeyid",
716
+ "clientsecret",
717
+ "accesstoken",
718
+ "refreshtoken",
719
+ "oauthtoken",
720
+ "bearertoken",
721
+ "authorization",
722
+ "password",
723
+ "passphrase"
724
+ ];
725
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
726
+ function normalizeKeyName(name) {
727
+ return name.toLowerCase().replace(/[-_.]/g, "");
728
+ }
729
+ function redactSecretsInObject(value, options) {
730
+ var _a, _b;
731
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
732
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
733
+ const seen = /* @__PURE__ */ new WeakSet();
734
+ const walk = (node) => {
735
+ if (node === null || typeof node !== "object") return node;
736
+ if (seen.has(node)) return "[CIRCULAR]";
737
+ seen.add(node);
738
+ if (Array.isArray(node)) {
739
+ return node.map((item) => walk(item));
740
+ }
741
+ const out = {};
742
+ for (const [k, v] of Object.entries(node)) {
743
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
744
+ out[k] = placeholder;
745
+ } else {
746
+ out[k] = walk(v);
747
+ }
748
+ }
749
+ return out;
750
+ };
751
+ return walk(value);
752
+ }
753
+ function buildSecretSet(names) {
754
+ const set = /* @__PURE__ */ new Set();
755
+ for (const name of names) set.add(normalizeKeyName(name));
756
+ return set;
757
+ }
758
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
759
+ "ai.request.providerOptions",
760
+ "ai.response.providerMetadata"
761
+ ];
762
+ function defaultTransformSpan(span) {
763
+ const attrs = span.attributes;
764
+ if (!attrs || attrs.length === 0) return span;
765
+ let nextAttrs;
766
+ for (let i = 0; i < attrs.length; i++) {
767
+ const attr = attrs[i];
768
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
769
+ if (redacted === void 0) continue;
770
+ if (!nextAttrs) nextAttrs = attrs.slice();
771
+ nextAttrs[i] = { key: attr.key, value: redacted };
772
+ }
773
+ if (!nextAttrs) return span;
774
+ return { ...span, attributes: nextAttrs };
775
+ }
776
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
777
+ function redactJsonAttributeValue(key, value) {
778
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
779
+ const json = value.stringValue;
780
+ if (typeof json !== "string" || json.length === 0) return void 0;
781
+ let parsed;
782
+ try {
783
+ parsed = JSON.parse(json);
784
+ } catch (e) {
785
+ return void 0;
786
+ }
787
+ const scrubbed = redactSecretsInObject(parsed);
788
+ let scrubbedJson;
789
+ try {
790
+ scrubbedJson = JSON.stringify(scrubbed);
791
+ } catch (e) {
792
+ return void 0;
793
+ }
794
+ if (scrubbedJson === json) return void 0;
795
+ return { stringValue: scrubbedJson };
796
+ }
797
+ function applyOtelSpanAttributeLimit(limit) {
798
+ var _a, _b;
799
+ try {
800
+ 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;
801
+ if (!raw) return limit;
802
+ const parsed = Number.parseInt(raw, 10);
803
+ if (Number.isFinite(parsed) && parsed > 0) {
804
+ return Math.min(limit, parsed);
805
+ }
806
+ } catch (e) {
807
+ }
808
+ return limit;
809
+ }
570
810
  var TraceShipper = class {
571
811
  constructor(opts) {
572
812
  this.queue = [];
573
813
  this.inFlight = /* @__PURE__ */ new Set();
814
+ this.hasShutdown = false;
574
815
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
575
816
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
576
817
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -588,6 +829,75 @@ var TraceShipper = class {
588
829
  if (this.debug && this.localDebuggerUrl) {
589
830
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
590
831
  }
832
+ this.transformSpanHook = opts.transformSpan;
833
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
834
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
835
+ }
836
+ /**
837
+ * Cap every string attribute value on the span. O(#attributes) length
838
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
839
+ * pipeline so the default secret-scrub still sees parseable JSON in
840
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
841
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
842
+ * in the surviving prefix).
843
+ *
844
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
845
+ * for span content, matching the Python SDK and the OTel SDK convention.
846
+ */
847
+ capSpanAttributes(span) {
848
+ var _a;
849
+ const maxChars = applyOtelSpanAttributeLimit(
850
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
851
+ );
852
+ const attrs = span.attributes;
853
+ if (!attrs || attrs.length === 0) return span;
854
+ let nextAttrs;
855
+ for (let i = 0; i < attrs.length; i++) {
856
+ const attr = attrs[i];
857
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
858
+ if (typeof value !== "string" || value.length <= maxChars) continue;
859
+ if (!nextAttrs) nextAttrs = attrs.slice();
860
+ nextAttrs[i] = {
861
+ key: attr.key,
862
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
863
+ };
864
+ }
865
+ if (!nextAttrs) return span;
866
+ return { ...span, attributes: nextAttrs };
867
+ }
868
+ /**
869
+ * Apply the user `transformSpan` hook (if any) followed by the default
870
+ * redactor (unless disabled). Returns either the (possibly new) span to
871
+ * ship, or `null` to drop the span entirely.
872
+ *
873
+ * Ordering: user hook runs first so callers can rewrite the span freely
874
+ * (rename attrs, add new ones, scrub things the default doesn't know
875
+ * about). The default redactor then runs on whatever the user produced,
876
+ * acting as the always-on floor for documented BYOK secrets. If the user
877
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
878
+ *
879
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
880
+ * hook can never accidentally ship raw, un-redacted spans.
881
+ */
882
+ redactSpan(span) {
883
+ let current = span;
884
+ if (this.transformSpanHook) {
885
+ try {
886
+ const result = this.transformSpanHook(current);
887
+ if (result === null) return null;
888
+ if (result !== void 0) current = result;
889
+ } catch (err) {
890
+ if (this.debug) {
891
+ const msg = err instanceof Error ? err.message : String(err);
892
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
893
+ }
894
+ return null;
895
+ }
896
+ }
897
+ if (!this.disableDefaultRedaction) {
898
+ current = defaultTransformSpan(current);
899
+ }
900
+ return this.capSpanAttributes(current);
591
901
  }
592
902
  isDebugEnabled() {
593
903
  return this.debug;
@@ -605,8 +915,8 @@ var TraceShipper = class {
605
915
  ];
606
916
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
607
917
  const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
608
- if (this.localDebuggerUrl) {
609
- const openSpan = buildOtlpSpan({
918
+ this.mirrorToLocalDebugger(
919
+ buildOtlpSpan({
610
920
  ids: span.ids,
611
921
  name: span.name,
612
922
  startTimeUnixNano: span.startTimeUnixNano,
@@ -614,16 +924,21 @@ var TraceShipper = class {
614
924
  // placeholder — will be updated on endSpan
615
925
  attributes: span.attributes,
616
926
  status: { code: SpanStatusCode.UNSET }
617
- });
618
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
619
- mirrorTraceExportToLocalDebugger(body, {
620
- baseUrl: this.localDebuggerUrl,
621
- debug: false,
622
- sdkName: this.sdkName
623
- });
624
- }
927
+ })
928
+ );
625
929
  return span;
626
930
  }
931
+ mirrorToLocalDebugger(span) {
932
+ if (!this.localDebuggerUrl) return;
933
+ const redacted = this.redactSpan(span);
934
+ if (redacted === null) return;
935
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
936
+ mirrorTraceExportToLocalDebugger(body, {
937
+ baseUrl: this.localDebuggerUrl,
938
+ debug: false,
939
+ sdkName: this.sdkName
940
+ });
941
+ }
627
942
  endSpan(span, extra) {
628
943
  var _a, _b;
629
944
  if (span.endTimeUnixNano) return;
@@ -645,14 +960,7 @@ var TraceShipper = class {
645
960
  status
646
961
  });
647
962
  this.enqueue(otlp);
648
- if (this.localDebuggerUrl) {
649
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
650
- mirrorTraceExportToLocalDebugger(body, {
651
- baseUrl: this.localDebuggerUrl,
652
- debug: false,
653
- sdkName: this.sdkName
654
- });
655
- }
963
+ this.mirrorToLocalDebugger(otlp);
656
964
  }
657
965
  createSpan(args) {
658
966
  var _a;
@@ -670,14 +978,7 @@ var TraceShipper = class {
670
978
  status: args.status
671
979
  });
672
980
  this.enqueue(otlp);
673
- if (this.localDebuggerUrl) {
674
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
675
- mirrorTraceExportToLocalDebugger(body, {
676
- baseUrl: this.localDebuggerUrl,
677
- debug: false,
678
- sdkName: this.sdkName
679
- });
680
- }
981
+ this.mirrorToLocalDebugger(otlp);
681
982
  }
682
983
  enqueue(span) {
683
984
  if (!this.enabled) return;
@@ -689,10 +990,12 @@ var TraceShipper = class {
689
990
  )}`
690
991
  );
691
992
  }
993
+ const redacted = this.redactSpan(span);
994
+ if (redacted === null) return;
692
995
  if (this.queue.length >= this.maxQueueSize) {
693
996
  this.queue.shift();
694
997
  }
695
- this.queue.push(span);
998
+ this.queue.push(redacted);
696
999
  if (this.queue.length >= this.maxBatchSize) {
697
1000
  void this.flush().catch(() => {
698
1001
  });
@@ -715,6 +1018,16 @@ var TraceShipper = class {
715
1018
  while (this.queue.length > 0) {
716
1019
  const batch = this.queue.splice(0, this.maxBatchSize);
717
1020
  if (!this.writeKey) continue;
1021
+ const opts = this.requestOpts();
1022
+ if (!opts) {
1023
+ rateLimitedLog(
1024
+ `${this.prefix}.shutdown_deadline`,
1025
+ () => console.warn(
1026
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1027
+ )
1028
+ );
1029
+ continue;
1030
+ }
718
1031
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
719
1032
  const url = `${this.baseUrl}traces`;
720
1033
  if (this.debug) {
@@ -723,11 +1036,7 @@ var TraceShipper = class {
723
1036
  endpoint: url
724
1037
  });
725
1038
  }
726
- const p = postJson(url, body, this.authHeaders(), {
727
- maxAttempts: 3,
728
- debug: this.debug,
729
- sdkName: this.sdkName
730
- });
1039
+ const p = postJson(url, body, this.authHeaders(), opts);
731
1040
  this.inFlight.add(p);
732
1041
  try {
733
1042
  try {
@@ -735,21 +1044,61 @@ var TraceShipper = class {
735
1044
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
736
1045
  } catch (err) {
737
1046
  const msg = err instanceof Error ? err.message : String(err);
738
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1047
+ rateLimitedLog(
1048
+ `${this.prefix}.send_spans_failed`,
1049
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1050
+ );
739
1051
  }
740
1052
  } finally {
741
1053
  this.inFlight.delete(p);
742
1054
  }
743
1055
  }
744
1056
  }
1057
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1058
+ requestOpts() {
1059
+ if (this.shutdownDeadlineAt !== void 0) {
1060
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1061
+ if (remainingMs <= 0) return null;
1062
+ return {
1063
+ maxAttempts: 1,
1064
+ debug: this.debug,
1065
+ sdkName: this.sdkName,
1066
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1067
+ };
1068
+ }
1069
+ if (this.hasShutdown) {
1070
+ return {
1071
+ maxAttempts: 1,
1072
+ debug: this.debug,
1073
+ sdkName: this.sdkName,
1074
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1075
+ };
1076
+ }
1077
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1078
+ }
745
1079
  async shutdown() {
746
- if (this.timer) {
747
- clearTimeout(this.timer);
748
- this.timer = void 0;
1080
+ this.hasShutdown = true;
1081
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1082
+ try {
1083
+ if (this.timer) {
1084
+ clearTimeout(this.timer);
1085
+ this.timer = void 0;
1086
+ }
1087
+ const drain = async () => {
1088
+ await this.flush();
1089
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1090
+ })));
1091
+ };
1092
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1093
+ if (!settled) {
1094
+ rateLimitedLog(
1095
+ `${this.prefix}.shutdown_deadline`,
1096
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1097
+ );
1098
+ }
1099
+ } finally {
1100
+ this.shutdownDeadlineAt = void 0;
749
1101
  }
750
- await this.flush();
751
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
752
- })));
753
1102
  }
754
1103
  };
755
1104
 
@@ -814,7 +1163,7 @@ function resolveLocalWorkshopUrl(fileValue) {
814
1163
  // package.json
815
1164
  var package_default = {
816
1165
  name: "@raindrop-ai/pi-agent",
817
- version: "0.0.3",
1166
+ version: "0.0.5",
818
1167
  description: "Raindrop observability for Pi Agent \u2014 automatic tracing via subscriber or pi-coding-agent extension",
819
1168
  type: "module",
820
1169
  license: "MIT",
@@ -864,18 +1213,18 @@ var package_default = {
864
1213
  "test:watch": "vitest"
865
1214
  },
866
1215
  peerDependencies: {
867
- "@mariozechner/pi-agent-core": ">=0.60.0",
868
- "@mariozechner/pi-coding-agent": ">=0.65.2"
1216
+ "@earendil-works/pi-agent-core": ">=0.74.0",
1217
+ "@earendil-works/pi-coding-agent": ">=0.74.0"
869
1218
  },
870
1219
  peerDependenciesMeta: {
871
- "@mariozechner/pi-coding-agent": {
1220
+ "@earendil-works/pi-coding-agent": {
872
1221
  optional: true
873
1222
  }
874
1223
  },
875
1224
  devDependencies: {
876
1225
  "@raindrop-ai/core": "workspace:*",
877
- "@mariozechner/pi-agent-core": "^0.66.0",
878
- "@mariozechner/pi-coding-agent": "^0.66.0",
1226
+ "@earendil-works/pi-agent-core": "^0.78.0",
1227
+ "@earendil-works/pi-coding-agent": "^0.78.0",
879
1228
  "@types/node": "^20.11.17",
880
1229
  msw: "^2.12.7",
881
1230
  tsup: "^8.5.1",
@@ -969,19 +1318,82 @@ function formatToolSpanName(toolName, args) {
969
1318
  }
970
1319
  return toolName;
971
1320
  }
972
- function safeStringify(value) {
1321
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1322
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1323
+ var MAX_ATTR_LENGTH = 32768;
1324
+ var MAX_BOUNDED_DEPTH = 12;
1325
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1326
+ if (value.length <= limit) return value;
1327
+ if (limit > TRUNCATION_MARKER2.length) {
1328
+ return value.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1329
+ }
1330
+ return value.slice(0, limit);
1331
+ }
1332
+ function boundedClone2(value, budget, depth) {
1333
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1334
+ if (typeof value === "string") {
1335
+ if (value.length > budget.remaining) {
1336
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1337
+ budget.remaining = 0;
1338
+ return taken;
1339
+ }
1340
+ budget.remaining -= Math.max(value.length, 1);
1341
+ return value;
1342
+ }
1343
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1344
+ budget.remaining -= 8;
1345
+ return value;
1346
+ }
1347
+ if (typeof value !== "object") {
1348
+ budget.remaining -= 8;
1349
+ return value;
1350
+ }
1351
+ if (depth >= MAX_BOUNDED_DEPTH) {
1352
+ budget.remaining -= 16;
1353
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1354
+ }
1355
+ const withToJson = value;
1356
+ if (typeof withToJson.toJSON === "function") {
1357
+ try {
1358
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1359
+ } catch (e) {
1360
+ }
1361
+ }
1362
+ if (Array.isArray(value)) {
1363
+ const out2 = [];
1364
+ for (const item of value) {
1365
+ if (budget.remaining <= 0) {
1366
+ out2.push(TRUNCATION_MARKER2);
1367
+ break;
1368
+ }
1369
+ out2.push(boundedClone2(item, budget, depth + 1));
1370
+ }
1371
+ return out2;
1372
+ }
1373
+ const out = {};
1374
+ for (const key of Object.keys(value)) {
1375
+ if (budget.remaining <= 0) {
1376
+ out["..."] = TRUNCATION_MARKER2;
1377
+ break;
1378
+ }
1379
+ budget.remaining -= Math.max(key.length, 1);
1380
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1381
+ }
1382
+ return out;
1383
+ }
1384
+ function safeStringify(value, limit = MAX_ATTR_LENGTH) {
973
1385
  if (value === void 0 || value === null) return void 0;
974
1386
  try {
975
- return JSON.stringify(value);
1387
+ const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
1388
+ return JSON.stringify(pruned);
976
1389
  } catch (e) {
977
1390
  try {
978
- return String(value);
1391
+ return capText2(String(value), limit);
979
1392
  } catch (e2) {
980
1393
  return "[unserializable]";
981
1394
  }
982
1395
  }
983
1396
  }
984
- var MAX_ATTR_LENGTH = 32768;
985
1397
  function truncate(value) {
986
1398
  if (value === void 0) return void 0;
987
1399
  if (value.length <= MAX_ATTR_LENGTH) return value;
@@ -1020,6 +1432,15 @@ function safeParsArgs(argsStr) {
1020
1432
  }
1021
1433
  }
1022
1434
  var MAX_SYSTEM_PROMPT_LENGTH = 32768;
1435
+ var ERROR_LOG_INTERVAL_MS = 3e4;
1436
+ var lastErrorLogAt = /* @__PURE__ */ new Map();
1437
+ function rateLimitedErrorLog(key, message) {
1438
+ const now = Date.now();
1439
+ const last = lastErrorLogAt.get(key);
1440
+ if (last !== void 0 && now - last < ERROR_LOG_INTERVAL_MS) return;
1441
+ lastErrorLogAt.set(key, now);
1442
+ console.log(message);
1443
+ }
1023
1444
  function createSessionState(sessionId) {
1024
1445
  return {
1025
1446
  sessionId,
@@ -1084,7 +1505,8 @@ function getState(stateRef, ctx) {
1084
1505
  function registerTracing(pi, config, eventShipper, traceShipper) {
1085
1506
  const stateRef = {};
1086
1507
  function logError(hook, err) {
1087
- console.log(
1508
+ rateLimitedErrorLog(
1509
+ hook,
1088
1510
  `[raindrop-ai/pi-agent] [error] Error in ${hook}: ${err instanceof Error ? err.message : String(err)}`
1089
1511
  );
1090
1512
  }
@@ -1137,7 +1559,7 @@ function registerTracing(pi, config, eventShipper, traceShipper) {
1137
1559
  }
1138
1560
  state.toolSpanStarts.clear();
1139
1561
  state.toolCallToLlmParent.clear();
1140
- state.currentInput = event.prompt;
1562
+ state.currentInput = capText2(event.prompt);
1141
1563
  state.turnNumber = 0;
1142
1564
  state.currentSystemPrompt = config.captureSystemPrompt ? truncateSystemPrompt(event.systemPrompt) : void 0;
1143
1565
  state.currentEventRequestId = generateId();
@@ -1158,7 +1580,7 @@ function registerTracing(pi, config, eventShipper, traceShipper) {
1158
1580
  userId: getUserId(state, config.eventMetadata),
1159
1581
  convoId: state.sessionId,
1160
1582
  eventName: getEventName(config),
1161
- input: event.prompt,
1583
+ input: state.currentInput,
1162
1584
  ...attachments.length > 0 ? { attachments } : {},
1163
1585
  properties: getBaseProperties(config, ctx)
1164
1586
  });
@@ -1209,7 +1631,7 @@ function registerTracing(pi, config, eventShipper, traceShipper) {
1209
1631
  const modelId = (_c = message.model) != null ? _c : "";
1210
1632
  const modelName = provider && modelId ? `${provider}/${modelId}` : modelId || "llm";
1211
1633
  const errorForSpan = getAssistantError(message);
1212
- const outputText = getAssistantText(message);
1634
+ const outputText = capText2(getAssistantText(message));
1213
1635
  const inputTokens = (_d = message.usage) == null ? void 0 : _d.input;
1214
1636
  const outputTokens = (_e = message.usage) == null ? void 0 : _e.output;
1215
1637
  const cacheReadTokens = (_f = message.usage) == null ? void 0 : _f.cacheRead;