@raindrop-ai/opencode-plugin 0.0.1 → 0.0.2

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
@@ -47,7 +47,7 @@ function loadConfig(projectDirectory) {
47
47
  }
48
48
  return {
49
49
  writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
50
- endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai",
50
+ endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
51
51
  eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
52
52
  userId: merged.user_id,
53
53
  debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false
@@ -100,6 +100,9 @@ var SpanStatusCode = {
100
100
  OK: 1,
101
101
  ERROR: 2
102
102
  };
103
+ function generateId() {
104
+ return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
105
+ }
103
106
  function createSpanIds(parent) {
104
107
  const traceId = parent ? parent.traceIdB64 : base64Encode(randomBytes(16));
105
108
  const spanId = base64Encode(randomBytes(8));
@@ -428,6 +431,27 @@ var TraceShipper = class {
428
431
  });
429
432
  this.enqueue(otlp);
430
433
  }
434
+ /**
435
+ * Create and enqueue a span with both start and end times (atomic insert).
436
+ * Used for tool spans and any span whose full duration is known at write time.
437
+ */
438
+ createSpan(args) {
439
+ var _a;
440
+ const ids = createSpanIds(args.parent);
441
+ const attrs = [
442
+ attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId)
443
+ ];
444
+ if ((_a = args.attributes) == null ? void 0 : _a.length) attrs.push(...args.attributes);
445
+ const otlp = buildOtlpSpan({
446
+ ids,
447
+ name: args.name,
448
+ startTimeUnixNano: args.startTimeUnixNano,
449
+ endTimeUnixNano: args.endTimeUnixNano,
450
+ attributes: attrs,
451
+ status: args.status
452
+ });
453
+ this.enqueue(otlp);
454
+ }
431
455
  enqueue(span) {
432
456
  if (this.queue.length >= this.maxQueueSize) this.queue.shift();
433
457
  this.queue.push(span);
@@ -526,6 +550,8 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
526
550
  })) == null ? void 0 : _a.catch) == null ? void 0 : _b.call(_a, () => {
527
551
  });
528
552
  }
553
+ const hostname = getHostname();
554
+ const os = process.platform;
529
555
  log("Raindrop tracing hooks registered");
530
556
  return {
531
557
  // ------------------------------------------------------------------
@@ -545,21 +571,9 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
545
571
  return;
546
572
  }
547
573
  const userId = (_a = config.userId) != null ? _a : sessionID;
548
- const hostname = getHostname();
549
- const os = process.platform;
550
- const rootSpan = traceShipper.startSpan({
551
- name: `OpenCode: ${getProjectName(worktree)}`,
552
- eventId: sessionID,
553
- attributes: [
554
- attrString("workspace", worktree),
555
- attrString("directory", directory),
556
- attrString("hostname", hostname),
557
- attrString("os", os)
558
- ]
559
- });
560
574
  const state = {
561
- eventId: sessionID,
562
- rootSpan,
575
+ sessionId: sessionID,
576
+ userId,
563
577
  turnNumber: 0,
564
578
  toolCount: 0,
565
579
  startTime: Date.now(),
@@ -569,26 +583,22 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
569
583
  reasoningParts: /* @__PURE__ */ new Map(),
570
584
  toolCalls: /* @__PURE__ */ new Map(),
571
585
  toolStartTimes: /* @__PURE__ */ new Map(),
572
- toolSpans: /* @__PURE__ */ new Map(),
586
+ toolSpanStarts: /* @__PURE__ */ new Map(),
573
587
  toolCallMessageIds: /* @__PURE__ */ new Map(),
574
588
  processedMessages: /* @__PURE__ */ new Set(),
575
589
  attachments: []
576
590
  };
577
- sessions.set(sessionID, state);
578
- await eventShipper.patch(sessionID, {
579
- isPending: true,
580
- userId,
581
- convoId: sessionID,
582
- eventName: config.eventName,
583
- properties: {
584
- workspace: worktree,
585
- directory,
586
- hostname,
587
- os,
588
- turn_count: 0,
589
- tool_count: 0
590
- }
591
+ state.currentRootSpan = traceShipper.startSpan({
592
+ name: `OpenCode: ${getProjectName(worktree)}`,
593
+ eventId: sessionID,
594
+ attributes: [
595
+ attrString("workspace", worktree),
596
+ attrString("directory", directory),
597
+ attrString("hostname", hostname),
598
+ attrString("os", os)
599
+ ]
591
600
  });
601
+ sessions.set(sessionID, state);
592
602
  log("Session created", { sessionID, userId });
593
603
  } else if (event.type === "message.part.updated") {
594
604
  const part = props["part"];
@@ -632,7 +642,7 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
632
642
  const time = msgInfo["time"];
633
643
  if (!msgSessionID || !messageId || !(time == null ? void 0 : time["completed"])) return;
634
644
  const state = sessions.get(msgSessionID);
635
- if (!state || !state.currentTurnSpan) return;
645
+ if (!state || !state.currentEventId || !state.currentTurnSpan) return;
636
646
  if (state.processedMessages.has(messageId)) return;
637
647
  state.processedMessages.add(messageId);
638
648
  const tokens = msgInfo["tokens"];
@@ -657,7 +667,7 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
657
667
  const llmSpan = traceShipper.startSpan({
658
668
  name: modelName,
659
669
  parent: state.currentTurnSpan.ids,
660
- eventId: state.eventId,
670
+ eventId: state.currentEventId,
661
671
  attributes: llmAttrs
662
672
  });
663
673
  if (typeof time["created"] === "number") {
@@ -668,11 +678,17 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
668
678
 
669
679
  type: ${(_f = msgError.name) != null ? _f : "UnknownError"}` : void 0;
670
680
  traceShipper.endSpan(llmSpan, { error: errorForSpan });
671
- await eventShipper.patch(state.eventId, {
681
+ await eventShipper.finish(state.currentEventId, {
682
+ userId: state.userId,
672
683
  model: modelName,
673
- output: outputText || void 0
684
+ output: outputText || void 0,
685
+ properties: {
686
+ turn_count: state.turnNumber,
687
+ tool_count: state.toolCount
688
+ }
674
689
  });
675
- log("LLM span created", {
690
+ await traceShipper.flush();
691
+ log("LLM span created, turn/root close on session.idle", {
676
692
  messageId,
677
693
  modelName,
678
694
  inputTokens,
@@ -687,14 +703,23 @@ type: ${(_f = msgError.name) != null ? _f : "UnknownError"}` : void 0;
687
703
  traceShipper.endSpan(state.currentTurnSpan);
688
704
  state.currentTurnSpan = void 0;
689
705
  }
690
- await eventShipper.patch(state.eventId, {
691
- output: state.currentOutput || void 0,
692
- properties: {
693
- turn_count: state.turnNumber,
694
- tool_count: state.toolCount
695
- }
696
- });
697
- log("Session idle \u2014 turn span closed", { sessionID });
706
+ if (state.currentRootSpan) {
707
+ traceShipper.endSpan(state.currentRootSpan);
708
+ state.currentRootSpan = void 0;
709
+ }
710
+ if (state.currentEventId) {
711
+ await eventShipper.finish(state.currentEventId, {
712
+ userId: state.userId,
713
+ output: state.currentOutput || void 0,
714
+ properties: {
715
+ turn_count: state.turnNumber,
716
+ tool_count: state.toolCount
717
+ }
718
+ });
719
+ state.currentEventId = void 0;
720
+ }
721
+ await traceShipper.flush();
722
+ log("Session idle \u2014 turn and root closed", { sessionID });
698
723
  } else if (event.type === "session.deleted") {
699
724
  if (!sessionID) return;
700
725
  const state = sessions.get(String(sessionID));
@@ -703,15 +728,22 @@ type: ${(_f = msgError.name) != null ? _f : "UnknownError"}` : void 0;
703
728
  traceShipper.endSpan(state.currentTurnSpan);
704
729
  state.currentTurnSpan = void 0;
705
730
  }
706
- traceShipper.endSpan(state.rootSpan);
707
- await eventShipper.finish(state.eventId, {
708
- output: state.currentOutput || void 0,
709
- model: state.model,
710
- properties: {
711
- turn_count: state.turnNumber,
712
- tool_count: state.toolCount
713
- }
714
- });
731
+ if (state.currentRootSpan) {
732
+ traceShipper.endSpan(state.currentRootSpan);
733
+ state.currentRootSpan = void 0;
734
+ }
735
+ if (state.currentEventId) {
736
+ await eventShipper.finish(state.currentEventId, {
737
+ userId: state.userId,
738
+ output: state.currentOutput || void 0,
739
+ model: state.model,
740
+ properties: {
741
+ turn_count: state.turnNumber,
742
+ tool_count: state.toolCount
743
+ }
744
+ });
745
+ state.currentEventId = void 0;
746
+ }
715
747
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
716
748
  sessions.delete(String(sessionID));
717
749
  log("Session deleted \u2014 flushed", { sessionID });
@@ -730,14 +762,21 @@ type: ${errorName}`;
730
762
  traceShipper.endSpan(state.currentTurnSpan, { error: errorStr });
731
763
  state.currentTurnSpan = void 0;
732
764
  }
733
- traceShipper.endSpan(state.rootSpan, { error: errorStr });
734
- await eventShipper.finish(state.eventId, {
735
- properties: {
736
- error: errorStr,
737
- turn_count: state.turnNumber,
738
- tool_count: state.toolCount
739
- }
740
- });
765
+ if (state.currentRootSpan) {
766
+ traceShipper.endSpan(state.currentRootSpan, { error: errorStr });
767
+ state.currentRootSpan = void 0;
768
+ }
769
+ if (state.currentEventId) {
770
+ await eventShipper.finish(state.currentEventId, {
771
+ userId: state.userId,
772
+ properties: {
773
+ error: errorStr,
774
+ turn_count: state.turnNumber,
775
+ tool_count: state.toolCount
776
+ }
777
+ });
778
+ state.currentEventId = void 0;
779
+ }
741
780
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
742
781
  sessions.delete(String(errorSessionID));
743
782
  log("Session error handled", { errorSessionID, errorName });
@@ -756,21 +795,53 @@ type: ${errorName}`;
756
795
  }
757
796
  },
758
797
  // ------------------------------------------------------------------
759
- // chat.message — user sent a message; start a new turn span
798
+ // chat.message — user sent a message; start a new turn with a fresh event ID
760
799
  // ------------------------------------------------------------------
761
800
  "chat.message": async (messageInput, output) => {
762
- var _a;
801
+ var _a, _b;
763
802
  try {
764
803
  const { sessionID } = messageInput;
765
- const state = sessions.get(sessionID);
766
- if (!state) return;
804
+ let state = sessions.get(sessionID);
805
+ if (!state) {
806
+ state = {
807
+ sessionId: sessionID,
808
+ userId: (_a = config.userId) != null ? _a : sessionID,
809
+ turnNumber: 0,
810
+ toolCount: 0,
811
+ startTime: Date.now(),
812
+ currentInput: "",
813
+ currentOutput: "",
814
+ outputParts: /* @__PURE__ */ new Map(),
815
+ reasoningParts: /* @__PURE__ */ new Map(),
816
+ toolCalls: /* @__PURE__ */ new Map(),
817
+ toolStartTimes: /* @__PURE__ */ new Map(),
818
+ toolSpanStarts: /* @__PURE__ */ new Map(),
819
+ toolCallMessageIds: /* @__PURE__ */ new Map(),
820
+ processedMessages: /* @__PURE__ */ new Set(),
821
+ attachments: []
822
+ };
823
+ sessions.set(sessionID, state);
824
+ }
767
825
  if (state.currentTurnSpan) {
768
826
  traceShipper.endSpan(state.currentTurnSpan);
769
827
  state.currentTurnSpan = void 0;
770
828
  }
829
+ if (!state.currentRootSpan) {
830
+ state.currentRootSpan = traceShipper.startSpan({
831
+ name: `OpenCode: ${getProjectName(worktree)}`,
832
+ eventId: sessionID,
833
+ attributes: [
834
+ attrString("workspace", worktree),
835
+ attrString("directory", directory),
836
+ attrString("hostname", hostname),
837
+ attrString("os", os)
838
+ ]
839
+ });
840
+ }
771
841
  state.turnNumber++;
772
842
  state.currentOutput = "";
773
- const parts = (_a = output == null ? void 0 : output.parts) != null ? _a : [];
843
+ state.currentEventId = generateId();
844
+ const parts = (_b = output == null ? void 0 : output.parts) != null ? _b : [];
774
845
  const textParts = [];
775
846
  const newAttachments = [];
776
847
  for (const part of parts) {
@@ -790,17 +861,29 @@ type: ${errorName}`;
790
861
  if (newAttachments.length > 0) state.attachments.push(...newAttachments);
791
862
  state.currentTurnSpan = traceShipper.startSpan({
792
863
  name: `Turn ${state.turnNumber}`,
793
- parent: state.rootSpan.ids,
794
- eventId: state.eventId,
864
+ parent: state.currentRootSpan.ids,
865
+ eventId: state.currentEventId,
795
866
  attributes: [attrInt("turn.number", state.turnNumber)]
796
867
  });
797
- await eventShipper.patch(state.eventId, {
868
+ await eventShipper.patch(state.currentEventId, {
869
+ isPending: true,
870
+ userId: state.userId,
871
+ convoId: state.sessionId,
872
+ eventName: config.eventName,
798
873
  input: userText || void 0,
799
874
  attachments: newAttachments.length > 0 ? newAttachments : void 0,
800
- properties: { turn_count: state.turnNumber }
875
+ properties: {
876
+ workspace: worktree,
877
+ directory,
878
+ hostname,
879
+ os,
880
+ turn_count: state.turnNumber,
881
+ tool_count: state.toolCount
882
+ }
801
883
  });
802
884
  log("Chat message \u2014 new turn started", {
803
885
  sessionID,
886
+ eventId: state.currentEventId,
804
887
  turnNumber: state.turnNumber,
805
888
  inputLength: userText.length,
806
889
  attachments: newAttachments.length
@@ -810,50 +893,57 @@ type: ${errorName}`;
810
893
  }
811
894
  },
812
895
  // ------------------------------------------------------------------
813
- // tool.execute.before — record start time and open tool span
896
+ // tool.execute.before — record start time only (span created atomically in after)
814
897
  // ------------------------------------------------------------------
815
898
  "tool.execute.before": async (toolInput, _output) => {
816
899
  try {
817
900
  const { tool, sessionID, callID } = toolInput;
818
901
  const state = sessions.get(sessionID);
819
- if (!state || !state.currentTurnSpan) return;
902
+ if (!state || !state.currentTurnSpan || !state.currentEventId) return;
820
903
  state.toolStartTimes.set(callID, Date.now());
821
- const toolSpan = traceShipper.startSpan({
822
- name: tool,
904
+ state.toolSpanStarts.set(callID, {
905
+ startTimeUnixNano: nowUnixNanoString(),
823
906
  parent: state.currentTurnSpan.ids,
824
- eventId: state.eventId,
825
- attributes: [attrString("tool.name", tool)]
907
+ eventId: state.currentEventId,
908
+ name: tool
826
909
  });
827
- state.toolSpans.set(callID, toolSpan);
828
910
  log("Tool execute before", { tool, sessionID, callID });
829
911
  } catch (err) {
830
912
  log("Error in tool.execute.before hook", { error: String(err) });
831
913
  }
832
914
  },
833
915
  // ------------------------------------------------------------------
834
- // tool.execute.after — finalize tool span
916
+ // tool.execute.after — create tool span atomically with both start and end times
835
917
  // ------------------------------------------------------------------
836
918
  "tool.execute.after": async (toolInput, result) => {
837
919
  var _a;
838
920
  try {
839
921
  const { tool, sessionID, callID } = toolInput;
840
922
  const state = sessions.get(sessionID);
841
- if (!state) return;
923
+ if (!state || !state.currentEventId) return;
842
924
  state.toolCount++;
843
925
  state.toolStartTimes.delete(callID);
844
926
  state.toolCallMessageIds.delete(callID);
845
- const toolSpan = state.toolSpans.get(callID);
846
- state.toolSpans.delete(callID);
847
- if (toolSpan) {
927
+ const startInfo = state.toolSpanStarts.get(callID);
928
+ state.toolSpanStarts.delete(callID);
929
+ if (startInfo) {
930
+ const endTimeUnixNano = nowUnixNanoString();
848
931
  const outputPreview = typeof result.output === "string" ? result.output.slice(0, 200) : JSON.stringify((_a = result.output) != null ? _a : "").slice(0, 200);
849
- traceShipper.endSpan(toolSpan, {
932
+ traceShipper.createSpan({
933
+ name: startInfo.name,
934
+ parent: startInfo.parent,
935
+ eventId: startInfo.eventId,
936
+ startTimeUnixNano: startInfo.startTimeUnixNano,
937
+ endTimeUnixNano,
850
938
  attributes: [
939
+ attrString("tool.name", tool),
851
940
  attrString("tool.title", result.title),
852
941
  attrString("tool.output_preview", outputPreview)
853
942
  ]
854
943
  });
855
944
  }
856
- await eventShipper.patch(state.eventId, {
945
+ await eventShipper.patch(state.currentEventId, {
946
+ userId: state.userId,
857
947
  properties: { tool_count: state.toolCount }
858
948
  });
859
949
  log("Tool execute after", { tool, sessionID, callID });
@@ -885,7 +975,7 @@ async function plugin(input) {
885
975
  "warn",
886
976
  "RAINDROP_WRITE_KEY not set \u2014 Raindrop tracing disabled. Set it in your environment or in ~/.config/opencode/raindrop.json."
887
977
  );
888
- return { hooks: {} };
978
+ return {};
889
979
  }
890
980
  if (config.debug) {
891
981
  appLog("info", `Raindrop tracing enabled (endpoint: ${config.endpoint})`);
@@ -909,5 +999,5 @@ async function plugin(input) {
909
999
  eventShipper,
910
1000
  traceShipper
911
1001
  );
912
- return { hooks };
1002
+ return hooks;
913
1003
  }
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { PluginInput } from '@opencode-ai/plugin';
1
+ import { PluginInput, Hooks } from '@opencode-ai/plugin';
2
2
 
3
3
  /**
4
4
  * Raindrop plugin for OpenCode.
@@ -12,38 +12,6 @@ import { PluginInput } from '@opencode-ai/plugin';
12
12
  *
13
13
  * Optional config file: ~/.config/opencode/raindrop.json or .opencode/raindrop.json
14
14
  */
15
- declare function plugin(input: PluginInput): Promise<{
16
- hooks: {};
17
- } | {
18
- hooks: {
19
- event: ({ event, }: {
20
- event: {
21
- type: string;
22
- properties: Record<string, unknown>;
23
- };
24
- }) => Promise<void>;
25
- "chat.message": (messageInput: {
26
- sessionID: string;
27
- agent?: unknown;
28
- model?: unknown;
29
- }, output: {
30
- parts?: Array<Record<string, unknown>>;
31
- } | undefined) => Promise<void>;
32
- "tool.execute.before": (toolInput: {
33
- tool: string;
34
- sessionID: string;
35
- callID: string;
36
- }, _output: unknown) => Promise<void>;
37
- "tool.execute.after": (toolInput: {
38
- tool: string;
39
- sessionID: string;
40
- callID: string;
41
- }, result: {
42
- output?: unknown;
43
- metadata?: unknown;
44
- title?: string;
45
- }) => Promise<void>;
46
- };
47
- }>;
15
+ declare function plugin(input: PluginInput): Promise<Hooks>;
48
16
 
49
17
  export { plugin as default };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PluginInput } from '@opencode-ai/plugin';
1
+ import { PluginInput, Hooks } from '@opencode-ai/plugin';
2
2
 
3
3
  /**
4
4
  * Raindrop plugin for OpenCode.
@@ -12,38 +12,6 @@ import { PluginInput } from '@opencode-ai/plugin';
12
12
  *
13
13
  * Optional config file: ~/.config/opencode/raindrop.json or .opencode/raindrop.json
14
14
  */
15
- declare function plugin(input: PluginInput): Promise<{
16
- hooks: {};
17
- } | {
18
- hooks: {
19
- event: ({ event, }: {
20
- event: {
21
- type: string;
22
- properties: Record<string, unknown>;
23
- };
24
- }) => Promise<void>;
25
- "chat.message": (messageInput: {
26
- sessionID: string;
27
- agent?: unknown;
28
- model?: unknown;
29
- }, output: {
30
- parts?: Array<Record<string, unknown>>;
31
- } | undefined) => Promise<void>;
32
- "tool.execute.before": (toolInput: {
33
- tool: string;
34
- sessionID: string;
35
- callID: string;
36
- }, _output: unknown) => Promise<void>;
37
- "tool.execute.after": (toolInput: {
38
- tool: string;
39
- sessionID: string;
40
- callID: string;
41
- }, result: {
42
- output?: unknown;
43
- metadata?: unknown;
44
- title?: string;
45
- }) => Promise<void>;
46
- };
47
- }>;
15
+ declare function plugin(input: PluginInput): Promise<Hooks>;
48
16
 
49
17
  export { plugin as default };
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ function loadConfig(projectDirectory) {
28
28
  }
29
29
  return {
30
30
  writeKey: (_b = (_a = process.env["RAINDROP_WRITE_KEY"]) != null ? _a : merged.write_key) != null ? _b : "",
31
- endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai",
31
+ endpoint: (_d = (_c = process.env["RAINDROP_API_URL"]) != null ? _c : merged.api_url) != null ? _d : "https://api.raindrop.ai/v1",
32
32
  eventName: (_e = merged.event_name) != null ? _e : "opencode_session",
33
33
  userId: merged.user_id,
34
34
  debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_f = merged.debug) != null ? _f : false
@@ -81,6 +81,9 @@ var SpanStatusCode = {
81
81
  OK: 1,
82
82
  ERROR: 2
83
83
  };
84
+ function generateId() {
85
+ return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
86
+ }
84
87
  function createSpanIds(parent) {
85
88
  const traceId = parent ? parent.traceIdB64 : base64Encode(randomBytes(16));
86
89
  const spanId = base64Encode(randomBytes(8));
@@ -409,6 +412,27 @@ var TraceShipper = class {
409
412
  });
410
413
  this.enqueue(otlp);
411
414
  }
415
+ /**
416
+ * Create and enqueue a span with both start and end times (atomic insert).
417
+ * Used for tool spans and any span whose full duration is known at write time.
418
+ */
419
+ createSpan(args) {
420
+ var _a;
421
+ const ids = createSpanIds(args.parent);
422
+ const attrs = [
423
+ attrString("ai.telemetry.metadata.raindrop.eventId", args.eventId)
424
+ ];
425
+ if ((_a = args.attributes) == null ? void 0 : _a.length) attrs.push(...args.attributes);
426
+ const otlp = buildOtlpSpan({
427
+ ids,
428
+ name: args.name,
429
+ startTimeUnixNano: args.startTimeUnixNano,
430
+ endTimeUnixNano: args.endTimeUnixNano,
431
+ attributes: attrs,
432
+ status: args.status
433
+ });
434
+ this.enqueue(otlp);
435
+ }
412
436
  enqueue(span) {
413
437
  if (this.queue.length >= this.maxQueueSize) this.queue.shift();
414
438
  this.queue.push(span);
@@ -507,6 +531,8 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
507
531
  })) == null ? void 0 : _a.catch) == null ? void 0 : _b.call(_a, () => {
508
532
  });
509
533
  }
534
+ const hostname = getHostname();
535
+ const os = process.platform;
510
536
  log("Raindrop tracing hooks registered");
511
537
  return {
512
538
  // ------------------------------------------------------------------
@@ -526,21 +552,9 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
526
552
  return;
527
553
  }
528
554
  const userId = (_a = config.userId) != null ? _a : sessionID;
529
- const hostname = getHostname();
530
- const os = process.platform;
531
- const rootSpan = traceShipper.startSpan({
532
- name: `OpenCode: ${getProjectName(worktree)}`,
533
- eventId: sessionID,
534
- attributes: [
535
- attrString("workspace", worktree),
536
- attrString("directory", directory),
537
- attrString("hostname", hostname),
538
- attrString("os", os)
539
- ]
540
- });
541
555
  const state = {
542
- eventId: sessionID,
543
- rootSpan,
556
+ sessionId: sessionID,
557
+ userId,
544
558
  turnNumber: 0,
545
559
  toolCount: 0,
546
560
  startTime: Date.now(),
@@ -550,26 +564,22 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
550
564
  reasoningParts: /* @__PURE__ */ new Map(),
551
565
  toolCalls: /* @__PURE__ */ new Map(),
552
566
  toolStartTimes: /* @__PURE__ */ new Map(),
553
- toolSpans: /* @__PURE__ */ new Map(),
567
+ toolSpanStarts: /* @__PURE__ */ new Map(),
554
568
  toolCallMessageIds: /* @__PURE__ */ new Map(),
555
569
  processedMessages: /* @__PURE__ */ new Set(),
556
570
  attachments: []
557
571
  };
558
- sessions.set(sessionID, state);
559
- await eventShipper.patch(sessionID, {
560
- isPending: true,
561
- userId,
562
- convoId: sessionID,
563
- eventName: config.eventName,
564
- properties: {
565
- workspace: worktree,
566
- directory,
567
- hostname,
568
- os,
569
- turn_count: 0,
570
- tool_count: 0
571
- }
572
+ state.currentRootSpan = traceShipper.startSpan({
573
+ name: `OpenCode: ${getProjectName(worktree)}`,
574
+ eventId: sessionID,
575
+ attributes: [
576
+ attrString("workspace", worktree),
577
+ attrString("directory", directory),
578
+ attrString("hostname", hostname),
579
+ attrString("os", os)
580
+ ]
572
581
  });
582
+ sessions.set(sessionID, state);
573
583
  log("Session created", { sessionID, userId });
574
584
  } else if (event.type === "message.part.updated") {
575
585
  const part = props["part"];
@@ -613,7 +623,7 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
613
623
  const time = msgInfo["time"];
614
624
  if (!msgSessionID || !messageId || !(time == null ? void 0 : time["completed"])) return;
615
625
  const state = sessions.get(msgSessionID);
616
- if (!state || !state.currentTurnSpan) return;
626
+ if (!state || !state.currentEventId || !state.currentTurnSpan) return;
617
627
  if (state.processedMessages.has(messageId)) return;
618
628
  state.processedMessages.add(messageId);
619
629
  const tokens = msgInfo["tokens"];
@@ -638,7 +648,7 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
638
648
  const llmSpan = traceShipper.startSpan({
639
649
  name: modelName,
640
650
  parent: state.currentTurnSpan.ids,
641
- eventId: state.eventId,
651
+ eventId: state.currentEventId,
642
652
  attributes: llmAttrs
643
653
  });
644
654
  if (typeof time["created"] === "number") {
@@ -649,11 +659,17 @@ function createHooks(client, config, worktree, directory, eventShipper, traceShi
649
659
 
650
660
  type: ${(_f = msgError.name) != null ? _f : "UnknownError"}` : void 0;
651
661
  traceShipper.endSpan(llmSpan, { error: errorForSpan });
652
- await eventShipper.patch(state.eventId, {
662
+ await eventShipper.finish(state.currentEventId, {
663
+ userId: state.userId,
653
664
  model: modelName,
654
- output: outputText || void 0
665
+ output: outputText || void 0,
666
+ properties: {
667
+ turn_count: state.turnNumber,
668
+ tool_count: state.toolCount
669
+ }
655
670
  });
656
- log("LLM span created", {
671
+ await traceShipper.flush();
672
+ log("LLM span created, turn/root close on session.idle", {
657
673
  messageId,
658
674
  modelName,
659
675
  inputTokens,
@@ -668,14 +684,23 @@ type: ${(_f = msgError.name) != null ? _f : "UnknownError"}` : void 0;
668
684
  traceShipper.endSpan(state.currentTurnSpan);
669
685
  state.currentTurnSpan = void 0;
670
686
  }
671
- await eventShipper.patch(state.eventId, {
672
- output: state.currentOutput || void 0,
673
- properties: {
674
- turn_count: state.turnNumber,
675
- tool_count: state.toolCount
676
- }
677
- });
678
- log("Session idle \u2014 turn span closed", { sessionID });
687
+ if (state.currentRootSpan) {
688
+ traceShipper.endSpan(state.currentRootSpan);
689
+ state.currentRootSpan = void 0;
690
+ }
691
+ if (state.currentEventId) {
692
+ await eventShipper.finish(state.currentEventId, {
693
+ userId: state.userId,
694
+ output: state.currentOutput || void 0,
695
+ properties: {
696
+ turn_count: state.turnNumber,
697
+ tool_count: state.toolCount
698
+ }
699
+ });
700
+ state.currentEventId = void 0;
701
+ }
702
+ await traceShipper.flush();
703
+ log("Session idle \u2014 turn and root closed", { sessionID });
679
704
  } else if (event.type === "session.deleted") {
680
705
  if (!sessionID) return;
681
706
  const state = sessions.get(String(sessionID));
@@ -684,15 +709,22 @@ type: ${(_f = msgError.name) != null ? _f : "UnknownError"}` : void 0;
684
709
  traceShipper.endSpan(state.currentTurnSpan);
685
710
  state.currentTurnSpan = void 0;
686
711
  }
687
- traceShipper.endSpan(state.rootSpan);
688
- await eventShipper.finish(state.eventId, {
689
- output: state.currentOutput || void 0,
690
- model: state.model,
691
- properties: {
692
- turn_count: state.turnNumber,
693
- tool_count: state.toolCount
694
- }
695
- });
712
+ if (state.currentRootSpan) {
713
+ traceShipper.endSpan(state.currentRootSpan);
714
+ state.currentRootSpan = void 0;
715
+ }
716
+ if (state.currentEventId) {
717
+ await eventShipper.finish(state.currentEventId, {
718
+ userId: state.userId,
719
+ output: state.currentOutput || void 0,
720
+ model: state.model,
721
+ properties: {
722
+ turn_count: state.turnNumber,
723
+ tool_count: state.toolCount
724
+ }
725
+ });
726
+ state.currentEventId = void 0;
727
+ }
696
728
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
697
729
  sessions.delete(String(sessionID));
698
730
  log("Session deleted \u2014 flushed", { sessionID });
@@ -711,14 +743,21 @@ type: ${errorName}`;
711
743
  traceShipper.endSpan(state.currentTurnSpan, { error: errorStr });
712
744
  state.currentTurnSpan = void 0;
713
745
  }
714
- traceShipper.endSpan(state.rootSpan, { error: errorStr });
715
- await eventShipper.finish(state.eventId, {
716
- properties: {
717
- error: errorStr,
718
- turn_count: state.turnNumber,
719
- tool_count: state.toolCount
720
- }
721
- });
746
+ if (state.currentRootSpan) {
747
+ traceShipper.endSpan(state.currentRootSpan, { error: errorStr });
748
+ state.currentRootSpan = void 0;
749
+ }
750
+ if (state.currentEventId) {
751
+ await eventShipper.finish(state.currentEventId, {
752
+ userId: state.userId,
753
+ properties: {
754
+ error: errorStr,
755
+ turn_count: state.turnNumber,
756
+ tool_count: state.toolCount
757
+ }
758
+ });
759
+ state.currentEventId = void 0;
760
+ }
722
761
  await Promise.all([eventShipper.flush(), traceShipper.flush()]);
723
762
  sessions.delete(String(errorSessionID));
724
763
  log("Session error handled", { errorSessionID, errorName });
@@ -737,21 +776,53 @@ type: ${errorName}`;
737
776
  }
738
777
  },
739
778
  // ------------------------------------------------------------------
740
- // chat.message — user sent a message; start a new turn span
779
+ // chat.message — user sent a message; start a new turn with a fresh event ID
741
780
  // ------------------------------------------------------------------
742
781
  "chat.message": async (messageInput, output) => {
743
- var _a;
782
+ var _a, _b;
744
783
  try {
745
784
  const { sessionID } = messageInput;
746
- const state = sessions.get(sessionID);
747
- if (!state) return;
785
+ let state = sessions.get(sessionID);
786
+ if (!state) {
787
+ state = {
788
+ sessionId: sessionID,
789
+ userId: (_a = config.userId) != null ? _a : sessionID,
790
+ turnNumber: 0,
791
+ toolCount: 0,
792
+ startTime: Date.now(),
793
+ currentInput: "",
794
+ currentOutput: "",
795
+ outputParts: /* @__PURE__ */ new Map(),
796
+ reasoningParts: /* @__PURE__ */ new Map(),
797
+ toolCalls: /* @__PURE__ */ new Map(),
798
+ toolStartTimes: /* @__PURE__ */ new Map(),
799
+ toolSpanStarts: /* @__PURE__ */ new Map(),
800
+ toolCallMessageIds: /* @__PURE__ */ new Map(),
801
+ processedMessages: /* @__PURE__ */ new Set(),
802
+ attachments: []
803
+ };
804
+ sessions.set(sessionID, state);
805
+ }
748
806
  if (state.currentTurnSpan) {
749
807
  traceShipper.endSpan(state.currentTurnSpan);
750
808
  state.currentTurnSpan = void 0;
751
809
  }
810
+ if (!state.currentRootSpan) {
811
+ state.currentRootSpan = traceShipper.startSpan({
812
+ name: `OpenCode: ${getProjectName(worktree)}`,
813
+ eventId: sessionID,
814
+ attributes: [
815
+ attrString("workspace", worktree),
816
+ attrString("directory", directory),
817
+ attrString("hostname", hostname),
818
+ attrString("os", os)
819
+ ]
820
+ });
821
+ }
752
822
  state.turnNumber++;
753
823
  state.currentOutput = "";
754
- const parts = (_a = output == null ? void 0 : output.parts) != null ? _a : [];
824
+ state.currentEventId = generateId();
825
+ const parts = (_b = output == null ? void 0 : output.parts) != null ? _b : [];
755
826
  const textParts = [];
756
827
  const newAttachments = [];
757
828
  for (const part of parts) {
@@ -771,17 +842,29 @@ type: ${errorName}`;
771
842
  if (newAttachments.length > 0) state.attachments.push(...newAttachments);
772
843
  state.currentTurnSpan = traceShipper.startSpan({
773
844
  name: `Turn ${state.turnNumber}`,
774
- parent: state.rootSpan.ids,
775
- eventId: state.eventId,
845
+ parent: state.currentRootSpan.ids,
846
+ eventId: state.currentEventId,
776
847
  attributes: [attrInt("turn.number", state.turnNumber)]
777
848
  });
778
- await eventShipper.patch(state.eventId, {
849
+ await eventShipper.patch(state.currentEventId, {
850
+ isPending: true,
851
+ userId: state.userId,
852
+ convoId: state.sessionId,
853
+ eventName: config.eventName,
779
854
  input: userText || void 0,
780
855
  attachments: newAttachments.length > 0 ? newAttachments : void 0,
781
- properties: { turn_count: state.turnNumber }
856
+ properties: {
857
+ workspace: worktree,
858
+ directory,
859
+ hostname,
860
+ os,
861
+ turn_count: state.turnNumber,
862
+ tool_count: state.toolCount
863
+ }
782
864
  });
783
865
  log("Chat message \u2014 new turn started", {
784
866
  sessionID,
867
+ eventId: state.currentEventId,
785
868
  turnNumber: state.turnNumber,
786
869
  inputLength: userText.length,
787
870
  attachments: newAttachments.length
@@ -791,50 +874,57 @@ type: ${errorName}`;
791
874
  }
792
875
  },
793
876
  // ------------------------------------------------------------------
794
- // tool.execute.before — record start time and open tool span
877
+ // tool.execute.before — record start time only (span created atomically in after)
795
878
  // ------------------------------------------------------------------
796
879
  "tool.execute.before": async (toolInput, _output) => {
797
880
  try {
798
881
  const { tool, sessionID, callID } = toolInput;
799
882
  const state = sessions.get(sessionID);
800
- if (!state || !state.currentTurnSpan) return;
883
+ if (!state || !state.currentTurnSpan || !state.currentEventId) return;
801
884
  state.toolStartTimes.set(callID, Date.now());
802
- const toolSpan = traceShipper.startSpan({
803
- name: tool,
885
+ state.toolSpanStarts.set(callID, {
886
+ startTimeUnixNano: nowUnixNanoString(),
804
887
  parent: state.currentTurnSpan.ids,
805
- eventId: state.eventId,
806
- attributes: [attrString("tool.name", tool)]
888
+ eventId: state.currentEventId,
889
+ name: tool
807
890
  });
808
- state.toolSpans.set(callID, toolSpan);
809
891
  log("Tool execute before", { tool, sessionID, callID });
810
892
  } catch (err) {
811
893
  log("Error in tool.execute.before hook", { error: String(err) });
812
894
  }
813
895
  },
814
896
  // ------------------------------------------------------------------
815
- // tool.execute.after — finalize tool span
897
+ // tool.execute.after — create tool span atomically with both start and end times
816
898
  // ------------------------------------------------------------------
817
899
  "tool.execute.after": async (toolInput, result) => {
818
900
  var _a;
819
901
  try {
820
902
  const { tool, sessionID, callID } = toolInput;
821
903
  const state = sessions.get(sessionID);
822
- if (!state) return;
904
+ if (!state || !state.currentEventId) return;
823
905
  state.toolCount++;
824
906
  state.toolStartTimes.delete(callID);
825
907
  state.toolCallMessageIds.delete(callID);
826
- const toolSpan = state.toolSpans.get(callID);
827
- state.toolSpans.delete(callID);
828
- if (toolSpan) {
908
+ const startInfo = state.toolSpanStarts.get(callID);
909
+ state.toolSpanStarts.delete(callID);
910
+ if (startInfo) {
911
+ const endTimeUnixNano = nowUnixNanoString();
829
912
  const outputPreview = typeof result.output === "string" ? result.output.slice(0, 200) : JSON.stringify((_a = result.output) != null ? _a : "").slice(0, 200);
830
- traceShipper.endSpan(toolSpan, {
913
+ traceShipper.createSpan({
914
+ name: startInfo.name,
915
+ parent: startInfo.parent,
916
+ eventId: startInfo.eventId,
917
+ startTimeUnixNano: startInfo.startTimeUnixNano,
918
+ endTimeUnixNano,
831
919
  attributes: [
920
+ attrString("tool.name", tool),
832
921
  attrString("tool.title", result.title),
833
922
  attrString("tool.output_preview", outputPreview)
834
923
  ]
835
924
  });
836
925
  }
837
- await eventShipper.patch(state.eventId, {
926
+ await eventShipper.patch(state.currentEventId, {
927
+ userId: state.userId,
838
928
  properties: { tool_count: state.toolCount }
839
929
  });
840
930
  log("Tool execute after", { tool, sessionID, callID });
@@ -866,7 +956,7 @@ async function plugin(input) {
866
956
  "warn",
867
957
  "RAINDROP_WRITE_KEY not set \u2014 Raindrop tracing disabled. Set it in your environment or in ~/.config/opencode/raindrop.json."
868
958
  );
869
- return { hooks: {} };
959
+ return {};
870
960
  }
871
961
  if (config.debug) {
872
962
  appLog("info", `Raindrop tracing enabled (endpoint: ${config.endpoint})`);
@@ -890,7 +980,7 @@ async function plugin(input) {
890
980
  eventShipper,
891
981
  traceShipper
892
982
  );
893
- return { hooks };
983
+ return hooks;
894
984
  }
895
985
  export {
896
986
  plugin as default
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@raindrop-ai/opencode-plugin",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Raindrop observability plugin for OpenCode — automatic session/event/span tracing",
5
5
  "type": "module",
6
- "main": "dist/index.js",
7
- "module": "dist/index.mjs",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
- "import": "./dist/index.mjs",
13
- "require": "./dist/index.js"
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
14
  }
15
15
  },
16
16
  "sideEffects": false,