@raindrop-ai/opencode-plugin 0.0.12 → 0.0.13

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;
@@ -71,6 +71,8 @@ function base64Encode(bytes) {
71
71
  function generateId() {
72
72
  return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
73
73
  }
74
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
75
+ var MAX_RETRY_DELAY_MS = 3e4;
74
76
  function wait(ms) {
75
77
  return new Promise((resolve) => setTimeout(resolve, ms));
76
78
  }
@@ -78,6 +80,46 @@ function formatEndpoint(endpoint) {
78
80
  if (!endpoint) return void 0;
79
81
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
80
82
  }
83
+ function redactUrlForLog(url) {
84
+ try {
85
+ const parsed = new URL(url);
86
+ parsed.username = "";
87
+ parsed.password = "";
88
+ parsed.search = "";
89
+ parsed.hash = "";
90
+ return parsed.toString();
91
+ } catch (e) {
92
+ return "<unparseable-url>";
93
+ }
94
+ }
95
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
96
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
97
+ function rateLimitedLog(key, log) {
98
+ const now = Date.now();
99
+ const last = rateLimitedLogLast.get(key);
100
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
101
+ return false;
102
+ }
103
+ rateLimitedLogLast.set(key, now);
104
+ log();
105
+ return true;
106
+ }
107
+ async function raceWithTimeout(promise, timeoutMs) {
108
+ let timer;
109
+ const settledInTime = await Promise.race([
110
+ promise.then(
111
+ () => true,
112
+ () => true
113
+ ),
114
+ new Promise((resolve) => {
115
+ var _a;
116
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
117
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
118
+ })
119
+ ]);
120
+ if (timer) clearTimeout(timer);
121
+ return settledInTime;
122
+ }
81
123
  function parseRetryAfter(headers) {
82
124
  var _a;
83
125
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -94,12 +136,12 @@ function parseRetryAfter(headers) {
94
136
  function getRetryDelayMs(attemptNumber, previousError) {
95
137
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
96
138
  const v = previousError.retryAfterMs;
97
- if (typeof v === "number") return Math.max(0, v);
139
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
98
140
  }
99
141
  if (attemptNumber <= 1) return 0;
100
142
  const base = 500;
101
143
  const factor = Math.pow(2, attemptNumber - 2);
102
- return base * factor;
144
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
103
145
  }
104
146
  async function withRetry(operation, opName, opts) {
105
147
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -134,7 +176,9 @@ async function withRetry(operation, opName, opts) {
134
176
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
135
177
  }
136
178
  async function postJson(url, body, headers, opts) {
137
- const opName = `POST ${url}`;
179
+ var _a;
180
+ const opName = `POST ${redactUrlForLog(url)}`;
181
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
138
182
  await withRetry(
139
183
  async () => {
140
184
  const resp = await fetch(url, {
@@ -143,7 +187,8 @@ async function postJson(url, body, headers, opts) {
143
187
  "Content-Type": "application/json",
144
188
  ...headers
145
189
  },
146
- body: JSON.stringify(body)
190
+ body: JSON.stringify(body),
191
+ signal: AbortSignal.timeout(timeoutMs)
147
192
  });
148
193
  if (!resp.ok) {
149
194
  const text = await resp.text().catch(() => "");
@@ -160,6 +205,27 @@ async function postJson(url, body, headers, opts) {
160
205
  opts
161
206
  );
162
207
  }
208
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
209
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
210
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
211
+ function resolveMaxTextFieldChars(value) {
212
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
213
+ return Math.floor(value);
214
+ }
215
+ return currentDefaultMaxTextFieldChars;
216
+ }
217
+ function truncateToLimit(text, limit) {
218
+ if (limit > TRUNCATION_MARKER.length) {
219
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
220
+ }
221
+ return text.slice(0, Math.max(0, limit));
222
+ }
223
+ function capText(value, limit) {
224
+ if (typeof value !== "string") return value;
225
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
226
+ if (value.length <= max) return value;
227
+ return truncateToLimit(value, max);
228
+ }
163
229
  var SpanStatusCode = {
164
230
  UNSET: 0,
165
231
  OK: 1,
@@ -302,6 +368,8 @@ function mirrorPartialEventToLocalDebugger(event, options = {}) {
302
368
  }).catch(() => {
303
369
  });
304
370
  }
371
+ var SHUTDOWN_DEADLINE_MS = 1e4;
372
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
305
373
  function mergePatches(target, source) {
306
374
  var _a, _b, _c, _d;
307
375
  const out = { ...target, ...source };
@@ -319,6 +387,7 @@ var EventShipper = class {
319
387
  this.sticky = /* @__PURE__ */ new Map();
320
388
  this.timers = /* @__PURE__ */ new Map();
321
389
  this.inFlight = /* @__PURE__ */ new Set();
390
+ this.hasShutdown = false;
322
391
  var _a, _b, _c, _d, _e, _f, _g, _h;
323
392
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
324
393
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -328,6 +397,7 @@ var EventShipper = class {
328
397
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
329
398
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
330
399
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
400
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
331
401
  this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
332
402
  if (this.debug && this.localDebuggerUrl) {
333
403
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
@@ -350,10 +420,52 @@ var EventShipper = class {
350
420
  authHeaders() {
351
421
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
352
422
  }
423
+ /**
424
+ * Build the retry/timeout options for one POST, honoring the shutdown
425
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
426
+ * the caller must drop the payload (with a rate-limited warning) instead
427
+ * of issuing a request that could outlive process exit.
428
+ *
429
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
430
+ * path is mid-drain takes effect immediately: no further retries, and the
431
+ * per-attempt timeout is clamped to the remaining window. After
432
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
433
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
434
+ * run as a single short attempt rather than regaining the full retry
435
+ * schedule.
436
+ */
437
+ requestOpts() {
438
+ if (this.shutdownDeadlineAt !== void 0) {
439
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
440
+ if (remainingMs <= 0) return null;
441
+ return {
442
+ maxAttempts: 1,
443
+ debug: this.debug,
444
+ sdkName: this.sdkName,
445
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
446
+ };
447
+ }
448
+ if (this.hasShutdown) {
449
+ return {
450
+ maxAttempts: 1,
451
+ debug: this.debug,
452
+ sdkName: this.sdkName,
453
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
454
+ };
455
+ }
456
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
457
+ }
353
458
  async patch(eventId, patch) {
354
459
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
355
460
  if (!this.enabled) return;
356
461
  if (!eventId || !eventId.trim()) return;
462
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
463
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
464
+ patch = { ...patch, input: capText(patch.input, maxChars) };
465
+ }
466
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
467
+ patch = { ...patch, output: capText(patch.output, maxChars) };
468
+ }
357
469
  if (this.debug) {
358
470
  console.log(`${this.prefix} queue patch`, {
359
471
  eventId,
@@ -400,9 +512,16 @@ var EventShipper = class {
400
512
  })));
401
513
  }
402
514
  async shutdown() {
403
- for (const t of this.timers.values()) clearTimeout(t);
404
- this.timers.clear();
405
- await this.flush();
515
+ this.hasShutdown = true;
516
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
517
+ try {
518
+ for (const t of this.timers.values()) clearTimeout(t);
519
+ this.timers.clear();
520
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
521
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
522
+ } finally {
523
+ this.shutdownDeadlineAt = void 0;
524
+ }
406
525
  }
407
526
  async trackSignal(signal) {
408
527
  var _a, _b;
@@ -424,15 +543,19 @@ var EventShipper = class {
424
543
  ];
425
544
  if (!this.writeKey) return;
426
545
  const url = `${this.baseUrl}signals/track`;
546
+ const opts = this.requestOpts();
547
+ if (!opts) {
548
+ this.warnShutdownDrop("signal");
549
+ return;
550
+ }
427
551
  try {
428
- await postJson(url, body, this.authHeaders(), {
429
- maxAttempts: 3,
430
- debug: this.debug,
431
- sdkName: this.sdkName
432
- });
552
+ await postJson(url, body, this.authHeaders(), opts);
433
553
  } catch (err) {
434
554
  const msg = err instanceof Error ? err.message : String(err);
435
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
555
+ rateLimitedLog(
556
+ `${this.prefix}.send_signal_failed`,
557
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
558
+ );
436
559
  }
437
560
  }
438
561
  async identify(users) {
@@ -456,17 +579,27 @@ var EventShipper = class {
456
579
  if (!this.writeKey) return;
457
580
  if (body.length === 0) return;
458
581
  const url = `${this.baseUrl}users/identify`;
582
+ const opts = this.requestOpts();
583
+ if (!opts) {
584
+ this.warnShutdownDrop("identify");
585
+ return;
586
+ }
459
587
  try {
460
- await postJson(url, body, this.authHeaders(), {
461
- maxAttempts: 3,
462
- debug: this.debug,
463
- sdkName: this.sdkName
464
- });
588
+ await postJson(url, body, this.authHeaders(), opts);
465
589
  } catch (err) {
466
590
  const msg = err instanceof Error ? err.message : String(err);
467
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
591
+ rateLimitedLog(
592
+ `${this.prefix}.send_identify_failed`,
593
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
594
+ );
468
595
  }
469
596
  }
597
+ warnShutdownDrop(what) {
598
+ rateLimitedLog(
599
+ `${this.prefix}.shutdown_deadline`,
600
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
601
+ );
602
+ }
470
603
  async flushOne(eventId) {
471
604
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
472
605
  if (!this.enabled) return;
@@ -542,11 +675,13 @@ var EventShipper = class {
542
675
  if (!isPending) this.sticky.delete(eventId);
543
676
  return;
544
677
  }
545
- const p = postJson(url, payload, this.authHeaders(), {
546
- maxAttempts: 3,
547
- debug: this.debug,
548
- sdkName: this.sdkName
549
- });
678
+ const opts = this.requestOpts();
679
+ if (!opts) {
680
+ this.warnShutdownDrop(`track_partial ${eventId}`);
681
+ if (!isPending) this.sticky.delete(eventId);
682
+ return;
683
+ }
684
+ const p = postJson(url, payload, this.authHeaders(), opts);
550
685
  this.inFlight.add(p);
551
686
  try {
552
687
  try {
@@ -556,7 +691,10 @@ var EventShipper = class {
556
691
  }
557
692
  } catch (err) {
558
693
  const msg = err instanceof Error ? err.message : String(err);
559
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
694
+ rateLimitedLog(
695
+ `${this.prefix}.send_track_partial_failed`,
696
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
697
+ );
560
698
  }
561
699
  } finally {
562
700
  this.inFlight.delete(p);
@@ -566,10 +704,113 @@ var EventShipper = class {
566
704
  }
567
705
  }
568
706
  };
707
+ var DEFAULT_SECRET_KEY_NAMES = [
708
+ "apikey",
709
+ "apisecret",
710
+ "apitoken",
711
+ "secretaccesskey",
712
+ "sessiontoken",
713
+ "privatekey",
714
+ "privatekeyid",
715
+ "clientsecret",
716
+ "accesstoken",
717
+ "refreshtoken",
718
+ "oauthtoken",
719
+ "bearertoken",
720
+ "authorization",
721
+ "password",
722
+ "passphrase"
723
+ ];
724
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
725
+ function normalizeKeyName(name) {
726
+ return name.toLowerCase().replace(/[-_.]/g, "");
727
+ }
728
+ function redactSecretsInObject(value, options) {
729
+ var _a, _b;
730
+ const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
731
+ const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
732
+ const seen = /* @__PURE__ */ new WeakSet();
733
+ const walk = (node) => {
734
+ if (node === null || typeof node !== "object") return node;
735
+ if (seen.has(node)) return "[CIRCULAR]";
736
+ seen.add(node);
737
+ if (Array.isArray(node)) {
738
+ return node.map((item) => walk(item));
739
+ }
740
+ const out = {};
741
+ for (const [k, v] of Object.entries(node)) {
742
+ if (normalizedSecretSet.has(normalizeKeyName(k))) {
743
+ out[k] = placeholder;
744
+ } else {
745
+ out[k] = walk(v);
746
+ }
747
+ }
748
+ return out;
749
+ };
750
+ return walk(value);
751
+ }
752
+ function buildSecretSet(names) {
753
+ const set = /* @__PURE__ */ new Set();
754
+ for (const name of names) set.add(normalizeKeyName(name));
755
+ return set;
756
+ }
757
+ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
758
+ "ai.request.providerOptions",
759
+ "ai.response.providerMetadata"
760
+ ];
761
+ function defaultTransformSpan(span) {
762
+ const attrs = span.attributes;
763
+ if (!attrs || attrs.length === 0) return span;
764
+ let nextAttrs;
765
+ for (let i = 0; i < attrs.length; i++) {
766
+ const attr = attrs[i];
767
+ const redacted = redactJsonAttributeValue(attr.key, attr.value);
768
+ if (redacted === void 0) continue;
769
+ if (!nextAttrs) nextAttrs = attrs.slice();
770
+ nextAttrs[i] = { key: attr.key, value: redacted };
771
+ }
772
+ if (!nextAttrs) return span;
773
+ return { ...span, attributes: nextAttrs };
774
+ }
775
+ var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
776
+ function redactJsonAttributeValue(key, value) {
777
+ if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
778
+ const json = value.stringValue;
779
+ if (typeof json !== "string" || json.length === 0) return void 0;
780
+ let parsed;
781
+ try {
782
+ parsed = JSON.parse(json);
783
+ } catch (e) {
784
+ return void 0;
785
+ }
786
+ const scrubbed = redactSecretsInObject(parsed);
787
+ let scrubbedJson;
788
+ try {
789
+ scrubbedJson = JSON.stringify(scrubbed);
790
+ } catch (e) {
791
+ return void 0;
792
+ }
793
+ if (scrubbedJson === json) return void 0;
794
+ return { stringValue: scrubbedJson };
795
+ }
796
+ function applyOtelSpanAttributeLimit(limit) {
797
+ var _a, _b;
798
+ try {
799
+ 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;
800
+ if (!raw) return limit;
801
+ const parsed = Number.parseInt(raw, 10);
802
+ if (Number.isFinite(parsed) && parsed > 0) {
803
+ return Math.min(limit, parsed);
804
+ }
805
+ } catch (e) {
806
+ }
807
+ return limit;
808
+ }
569
809
  var TraceShipper = class {
570
810
  constructor(opts) {
571
811
  this.queue = [];
572
812
  this.inFlight = /* @__PURE__ */ new Set();
813
+ this.hasShutdown = false;
573
814
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
574
815
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
575
816
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -587,6 +828,75 @@ var TraceShipper = class {
587
828
  if (this.debug && this.localDebuggerUrl) {
588
829
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
589
830
  }
831
+ this.transformSpanHook = opts.transformSpan;
832
+ this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
833
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
834
+ }
835
+ /**
836
+ * Cap every string attribute value on the span. O(#attributes) length
837
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
838
+ * pipeline so the default secret-scrub still sees parseable JSON in
839
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
840
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
841
+ * in the surviving prefix).
842
+ *
843
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
844
+ * for span content, matching the Python SDK and the OTel SDK convention.
845
+ */
846
+ capSpanAttributes(span) {
847
+ var _a;
848
+ const maxChars = applyOtelSpanAttributeLimit(
849
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
850
+ );
851
+ const attrs = span.attributes;
852
+ if (!attrs || attrs.length === 0) return span;
853
+ let nextAttrs;
854
+ for (let i = 0; i < attrs.length; i++) {
855
+ const attr = attrs[i];
856
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
857
+ if (typeof value !== "string" || value.length <= maxChars) continue;
858
+ if (!nextAttrs) nextAttrs = attrs.slice();
859
+ nextAttrs[i] = {
860
+ key: attr.key,
861
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
862
+ };
863
+ }
864
+ if (!nextAttrs) return span;
865
+ return { ...span, attributes: nextAttrs };
866
+ }
867
+ /**
868
+ * Apply the user `transformSpan` hook (if any) followed by the default
869
+ * redactor (unless disabled). Returns either the (possibly new) span to
870
+ * ship, or `null` to drop the span entirely.
871
+ *
872
+ * Ordering: user hook runs first so callers can rewrite the span freely
873
+ * (rename attrs, add new ones, scrub things the default doesn't know
874
+ * about). The default redactor then runs on whatever the user produced,
875
+ * acting as the always-on floor for documented BYOK secrets. If the user
876
+ * sets `disableDefaultRedaction: true`, the floor is skipped.
877
+ *
878
+ * Fail-closed: if the user hook throws, the span is dropped — a buggy
879
+ * hook can never accidentally ship raw, un-redacted spans.
880
+ */
881
+ redactSpan(span) {
882
+ let current = span;
883
+ if (this.transformSpanHook) {
884
+ try {
885
+ const result = this.transformSpanHook(current);
886
+ if (result === null) return null;
887
+ if (result !== void 0) current = result;
888
+ } catch (err) {
889
+ if (this.debug) {
890
+ const msg = err instanceof Error ? err.message : String(err);
891
+ console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
892
+ }
893
+ return null;
894
+ }
895
+ }
896
+ if (!this.disableDefaultRedaction) {
897
+ current = defaultTransformSpan(current);
898
+ }
899
+ return this.capSpanAttributes(current);
590
900
  }
591
901
  isDebugEnabled() {
592
902
  return this.debug;
@@ -604,8 +914,8 @@ var TraceShipper = class {
604
914
  ];
605
915
  if ((_b = args.attributes) == null ? void 0 : _b.length) attrs.push(...args.attributes);
606
916
  const span = { ids, name: args.name, startTimeUnixNano: started, attributes: attrs };
607
- if (this.localDebuggerUrl) {
608
- const openSpan = buildOtlpSpan({
917
+ this.mirrorToLocalDebugger(
918
+ buildOtlpSpan({
609
919
  ids: span.ids,
610
920
  name: span.name,
611
921
  startTimeUnixNano: span.startTimeUnixNano,
@@ -613,16 +923,21 @@ var TraceShipper = class {
613
923
  // placeholder — will be updated on endSpan
614
924
  attributes: span.attributes,
615
925
  status: { code: SpanStatusCode.UNSET }
616
- });
617
- const body = buildExportTraceServiceRequest([openSpan], this.serviceName, this.serviceVersion);
618
- mirrorTraceExportToLocalDebugger(body, {
619
- baseUrl: this.localDebuggerUrl,
620
- debug: false,
621
- sdkName: this.sdkName
622
- });
623
- }
926
+ })
927
+ );
624
928
  return span;
625
929
  }
930
+ mirrorToLocalDebugger(span) {
931
+ if (!this.localDebuggerUrl) return;
932
+ const redacted = this.redactSpan(span);
933
+ if (redacted === null) return;
934
+ const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
935
+ mirrorTraceExportToLocalDebugger(body, {
936
+ baseUrl: this.localDebuggerUrl,
937
+ debug: false,
938
+ sdkName: this.sdkName
939
+ });
940
+ }
626
941
  endSpan(span, extra) {
627
942
  var _a, _b;
628
943
  if (span.endTimeUnixNano) return;
@@ -644,14 +959,7 @@ var TraceShipper = class {
644
959
  status
645
960
  });
646
961
  this.enqueue(otlp);
647
- if (this.localDebuggerUrl) {
648
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
649
- mirrorTraceExportToLocalDebugger(body, {
650
- baseUrl: this.localDebuggerUrl,
651
- debug: false,
652
- sdkName: this.sdkName
653
- });
654
- }
962
+ this.mirrorToLocalDebugger(otlp);
655
963
  }
656
964
  createSpan(args) {
657
965
  var _a;
@@ -669,14 +977,7 @@ var TraceShipper = class {
669
977
  status: args.status
670
978
  });
671
979
  this.enqueue(otlp);
672
- if (this.localDebuggerUrl) {
673
- const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
674
- mirrorTraceExportToLocalDebugger(body, {
675
- baseUrl: this.localDebuggerUrl,
676
- debug: false,
677
- sdkName: this.sdkName
678
- });
679
- }
980
+ this.mirrorToLocalDebugger(otlp);
680
981
  }
681
982
  enqueue(span) {
682
983
  if (!this.enabled) return;
@@ -688,10 +989,12 @@ var TraceShipper = class {
688
989
  )}`
689
990
  );
690
991
  }
992
+ const redacted = this.redactSpan(span);
993
+ if (redacted === null) return;
691
994
  if (this.queue.length >= this.maxQueueSize) {
692
995
  this.queue.shift();
693
996
  }
694
- this.queue.push(span);
997
+ this.queue.push(redacted);
695
998
  if (this.queue.length >= this.maxBatchSize) {
696
999
  void this.flush().catch(() => {
697
1000
  });
@@ -714,6 +1017,16 @@ var TraceShipper = class {
714
1017
  while (this.queue.length > 0) {
715
1018
  const batch = this.queue.splice(0, this.maxBatchSize);
716
1019
  if (!this.writeKey) continue;
1020
+ const opts = this.requestOpts();
1021
+ if (!opts) {
1022
+ rateLimitedLog(
1023
+ `${this.prefix}.shutdown_deadline`,
1024
+ () => console.warn(
1025
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1026
+ )
1027
+ );
1028
+ continue;
1029
+ }
717
1030
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
718
1031
  const url = `${this.baseUrl}traces`;
719
1032
  if (this.debug) {
@@ -722,11 +1035,7 @@ var TraceShipper = class {
722
1035
  endpoint: url
723
1036
  });
724
1037
  }
725
- const p = postJson(url, body, this.authHeaders(), {
726
- maxAttempts: 3,
727
- debug: this.debug,
728
- sdkName: this.sdkName
729
- });
1038
+ const p = postJson(url, body, this.authHeaders(), opts);
730
1039
  this.inFlight.add(p);
731
1040
  try {
732
1041
  try {
@@ -734,21 +1043,61 @@ var TraceShipper = class {
734
1043
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
735
1044
  } catch (err) {
736
1045
  const msg = err instanceof Error ? err.message : String(err);
737
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1046
+ rateLimitedLog(
1047
+ `${this.prefix}.send_spans_failed`,
1048
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1049
+ );
738
1050
  }
739
1051
  } finally {
740
1052
  this.inFlight.delete(p);
741
1053
  }
742
1054
  }
743
1055
  }
1056
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1057
+ requestOpts() {
1058
+ if (this.shutdownDeadlineAt !== void 0) {
1059
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1060
+ if (remainingMs <= 0) return null;
1061
+ return {
1062
+ maxAttempts: 1,
1063
+ debug: this.debug,
1064
+ sdkName: this.sdkName,
1065
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1066
+ };
1067
+ }
1068
+ if (this.hasShutdown) {
1069
+ return {
1070
+ maxAttempts: 1,
1071
+ debug: this.debug,
1072
+ sdkName: this.sdkName,
1073
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1074
+ };
1075
+ }
1076
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1077
+ }
744
1078
  async shutdown() {
745
- if (this.timer) {
746
- clearTimeout(this.timer);
747
- this.timer = void 0;
1079
+ this.hasShutdown = true;
1080
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1081
+ try {
1082
+ if (this.timer) {
1083
+ clearTimeout(this.timer);
1084
+ this.timer = void 0;
1085
+ }
1086
+ const drain = async () => {
1087
+ await this.flush();
1088
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1089
+ })));
1090
+ };
1091
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1092
+ if (!settled) {
1093
+ rateLimitedLog(
1094
+ `${this.prefix}.shutdown_deadline`,
1095
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1096
+ );
1097
+ }
1098
+ } finally {
1099
+ this.shutdownDeadlineAt = void 0;
748
1100
  }
749
- await this.flush();
750
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
751
- })));
752
1101
  }
753
1102
  };
754
1103
 
@@ -809,7 +1158,7 @@ function resolveLocalWorkshopUrl(fileValue) {
809
1158
  // package.json
810
1159
  var package_default = {
811
1160
  name: "@raindrop-ai/opencode-plugin",
812
- version: "0.0.12",
1161
+ version: "0.0.13",
813
1162
  description: "Raindrop observability plugin for OpenCode \u2014 automatic session/event/span tracing",
814
1163
  type: "module",
815
1164
  main: "dist/index.js",
@@ -919,6 +1268,86 @@ var TraceShipper2 = class extends TraceShipper {
919
1268
  }
920
1269
  };
921
1270
 
1271
+ // src/bounded.ts
1272
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1273
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1274
+ var MAX_BOUNDED_DEPTH = 12;
1275
+ function truncateToLimit2(text, limit) {
1276
+ if (text.length <= limit) return text;
1277
+ if (limit > TRUNCATION_MARKER2.length) {
1278
+ return text.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1279
+ }
1280
+ return text.slice(0, limit);
1281
+ }
1282
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1283
+ if (value.length <= limit) return value;
1284
+ return truncateToLimit2(value, limit);
1285
+ }
1286
+ function boundedClone2(value, budget, depth) {
1287
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1288
+ if (typeof value === "string") {
1289
+ if (value.length > budget.remaining) {
1290
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1291
+ budget.remaining = 0;
1292
+ return taken;
1293
+ }
1294
+ budget.remaining -= Math.max(value.length, 1);
1295
+ return value;
1296
+ }
1297
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1298
+ budget.remaining -= 8;
1299
+ return value;
1300
+ }
1301
+ if (typeof value !== "object") {
1302
+ budget.remaining -= 8;
1303
+ return value;
1304
+ }
1305
+ if (depth >= MAX_BOUNDED_DEPTH) {
1306
+ budget.remaining -= 16;
1307
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1308
+ }
1309
+ const withToJson = value;
1310
+ if (typeof withToJson.toJSON === "function") {
1311
+ try {
1312
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1313
+ } catch (e) {
1314
+ }
1315
+ }
1316
+ if (Array.isArray(value)) {
1317
+ const out2 = [];
1318
+ for (const item of value) {
1319
+ if (budget.remaining <= 0) {
1320
+ out2.push(TRUNCATION_MARKER2);
1321
+ break;
1322
+ }
1323
+ out2.push(boundedClone2(item, budget, depth + 1));
1324
+ }
1325
+ return out2;
1326
+ }
1327
+ const out = {};
1328
+ for (const key of Object.keys(value)) {
1329
+ if (budget.remaining <= 0) {
1330
+ out["..."] = TRUNCATION_MARKER2;
1331
+ break;
1332
+ }
1333
+ budget.remaining -= Math.max(key.length, 1);
1334
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1335
+ }
1336
+ return out;
1337
+ }
1338
+ function boundedStringify(value, limit = MAX_TEXT_FIELD_CHARS) {
1339
+ var _a;
1340
+ try {
1341
+ if (typeof value === "string") {
1342
+ return truncateToLimit2(JSON.stringify(capText2(value, limit)), limit);
1343
+ }
1344
+ const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
1345
+ return truncateToLimit2((_a = JSON.stringify(pruned)) != null ? _a : "", limit);
1346
+ } catch (e) {
1347
+ return capText2(String(value), limit);
1348
+ }
1349
+ }
1350
+
922
1351
  // src/tracing-linkage-helpers.ts
923
1352
  function createSessionParentMapHelpers({
924
1353
  sessions: sessions2,
@@ -996,6 +1425,15 @@ function createSessionParentMapHelpers({
996
1425
  }
997
1426
 
998
1427
  // src/tracing.ts
1428
+ var ERROR_LOG_INTERVAL_MS = 3e4;
1429
+ var lastErrorLogAt = /* @__PURE__ */ new Map();
1430
+ function rateLimitedErrorLog(key, message) {
1431
+ const now = Date.now();
1432
+ const last = lastErrorLogAt.get(key);
1433
+ if (last !== void 0 && now - last < ERROR_LOG_INTERVAL_MS) return;
1434
+ lastErrorLogAt.set(key, now);
1435
+ console.log(message);
1436
+ }
999
1437
  var sessions = /* @__PURE__ */ new Map();
1000
1438
  var taskContexts = /* @__PURE__ */ new Map();
1001
1439
  var runningTaskCallsBySession = /* @__PURE__ */ new Map();
@@ -1148,7 +1586,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1148
1586
  const state = sessions.get(partSessionID);
1149
1587
  if (!state) return;
1150
1588
  if (partObj["type"] === "text" && typeof partObj["text"] === "string") {
1151
- state.outputParts.set(messageId, partObj["text"]);
1589
+ state.outputParts.set(messageId, capText2(partObj["text"]));
1152
1590
  } else if (partObj["type"] === "tool" && messageId) {
1153
1591
  const tool = partObj["tool"];
1154
1592
  const callID = partObj["callID"];
@@ -1178,7 +1616,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1178
1616
  if (typeof errorMessage !== "string" || errorMessage === "")
1179
1617
  throw new Error("message.part.updated: error tool state must have error string");
1180
1618
  const input = toolState["input"];
1181
- const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? input : JSON.stringify(input);
1619
+ const toolCallArgs = input === void 0 ? void 0 : typeof input === "string" ? capText2(input) : boundedStringify(input);
1182
1620
  if (endPendingToolSpanWithError(
1183
1621
  state,
1184
1622
  callID,
@@ -1191,7 +1629,7 @@ function createHooks(config, worktree, directory, eventShipper, traceShipper) {
1191
1629
  }
1192
1630
  }
1193
1631
  } else if (partObj["type"] === "reasoning" && typeof partObj["text"] === "string") {
1194
- state.reasoningParts.set(messageId, partObj["text"]);
1632
+ state.reasoningParts.set(messageId, capText2(partObj["text"]));
1195
1633
  }
1196
1634
  } else if (event.type === "message.updated") {
1197
1635
  const msgInfo = props["info"];
@@ -1458,7 +1896,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1458
1896
  sessions.delete(String(errorSessionID));
1459
1897
  }
1460
1898
  } catch (err) {
1461
- console.log(
1899
+ rateLimitedErrorLog(
1900
+ "event",
1462
1901
  `[raindrop-ai/opencode-plugin] [error] Error in event hook: ${err instanceof Error ? err.message : String(err)}`
1463
1902
  );
1464
1903
  }
@@ -1514,7 +1953,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1514
1953
  ...promptMeta,
1515
1954
  properties: baseMeta.properties || promptMeta.properties ? { ...(_a = baseMeta.properties) != null ? _a : {}, ...(_b = promptMeta.properties) != null ? _b : {} } : void 0
1516
1955
  };
1517
- const userText = textParts.join("\n");
1956
+ const userText = capText2(textParts.join("\n"));
1518
1957
  state.currentInput = userText;
1519
1958
  if (state.parentId) {
1520
1959
  if (!state.parentTaskSpanIds || !state.parentEventContext) {
@@ -1623,7 +2062,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1623
2062
  });
1624
2063
  }
1625
2064
  } catch (err) {
1626
- console.log(
2065
+ rateLimitedErrorLog(
2066
+ "tool.execute.before",
1627
2067
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.before hook: ${err instanceof Error ? err.message : String(err)}`
1628
2068
  );
1629
2069
  }
@@ -1638,7 +2078,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1638
2078
  if (!state || !state.currentEventId) return;
1639
2079
  if (args === void 0 || args === null)
1640
2080
  throw new Error("tool.execute.after: args is required");
1641
- const toolCallArgs = typeof args === "string" ? args : JSON.stringify(args);
2081
+ const toolCallArgs = typeof args === "string" ? capText2(args) : boundedStringify(args);
1642
2082
  const startInfo = state.toolSpanStarts.get(callID);
1643
2083
  state.toolSpanStarts.delete(callID);
1644
2084
  const resultMetadata = result.metadata;
@@ -1651,13 +2091,7 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1651
2091
  const endTimeUnixNano = nowUnixNanoString();
1652
2092
  if (!("output" in result) || result.output === void 0)
1653
2093
  throw new Error("tool.execute.after: result.output is required");
1654
- let toolResult;
1655
- try {
1656
- toolResult = JSON.stringify(result.output);
1657
- } catch (err) {
1658
- const msg = err instanceof Error ? err.message : String(err);
1659
- throw new Error(`tool.execute.after: result.output must be JSON-serializable: ${msg}`);
1660
- }
2094
+ const toolResult = boundedStringify(result.output);
1661
2095
  if (startInfo.liveSpan) {
1662
2096
  traceShipper.endSpan(startInfo.liveSpan, {
1663
2097
  attributes: [
@@ -1686,7 +2120,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1686
2120
  markTaskCallFinished(sessionID, callID);
1687
2121
  }
1688
2122
  } catch (err) {
1689
- console.log(
2123
+ rateLimitedErrorLog(
2124
+ "tool.execute.after",
1690
2125
  `[raindrop-ai/opencode-plugin] [error] Error in tool.execute.after hook: ${err instanceof Error ? err.message : String(err)}`
1691
2126
  );
1692
2127
  }
@@ -1703,7 +2138,8 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1703
2138
  state.isCompacting = true;
1704
2139
  }
1705
2140
  } catch (err) {
1706
- console.log(
2141
+ rateLimitedErrorLog(
2142
+ "experimental.session.compacting",
1707
2143
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.session.compacting hook: ${err instanceof Error ? err.message : String(err)}`
1708
2144
  );
1709
2145
  }
@@ -1722,11 +2158,12 @@ type: ${errorName != null ? errorName : "UnknownError"}`;
1722
2158
  sessions.set(sessionID, state);
1723
2159
  }
1724
2160
  const MAX_SYSTEM_PROMPT_LENGTH = 32768;
1725
- const TRUNCATION_MARKER = "\n...[truncated]";
2161
+ const TRUNCATION_MARKER3 = "\n...[truncated]";
1726
2162
  const joined = output.system.join("\n\n");
1727
- state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER : joined;
2163
+ state.currentSystemPrompt = joined.length > MAX_SYSTEM_PROMPT_LENGTH ? joined.slice(0, MAX_SYSTEM_PROMPT_LENGTH - TRUNCATION_MARKER3.length) + TRUNCATION_MARKER3 : joined;
1728
2164
  } catch (err) {
1729
- console.log(
2165
+ rateLimitedErrorLog(
2166
+ "experimental.chat.system.transform",
1730
2167
  `[raindrop-ai/opencode-plugin] [error] Error in experimental.chat.system.transform hook: ${err instanceof Error ? err.message : String(err)}`
1731
2168
  );
1732
2169
  }