@senpi/runtime 3.0.6-dev.runtime-phase-2.20260630134307 → 3.0.6-dev.runtime-phase-2.20260630134410

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.
Files changed (177) hide show
  1. package/dist/actions/close-position/close-position.action.d.ts +1 -1
  2. package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
  3. package/dist/actions/close-position/close-position.action.js +15 -14
  4. package/dist/actions/close-position/close-position.action.js.map +1 -1
  5. package/dist/actions/event-derivation.d.ts +60 -0
  6. package/dist/actions/event-derivation.d.ts.map +1 -0
  7. package/dist/actions/event-derivation.js +179 -0
  8. package/dist/actions/event-derivation.js.map +1 -0
  9. package/dist/actions/llm-decision/anthropic-llm-decision.d.ts +5 -9
  10. package/dist/actions/llm-decision/anthropic-llm-decision.d.ts.map +1 -1
  11. package/dist/actions/llm-decision/anthropic-llm-decision.js +6 -87
  12. package/dist/actions/llm-decision/anthropic-llm-decision.js.map +1 -1
  13. package/dist/actions/llm-decision/factory.d.ts +0 -6
  14. package/dist/actions/llm-decision/factory.d.ts.map +1 -1
  15. package/dist/actions/llm-decision/factory.js +0 -2
  16. package/dist/actions/llm-decision/factory.js.map +1 -1
  17. package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
  18. package/dist/actions/llm-decision/openclaw-llm-decision.js +21 -2
  19. package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
  20. package/dist/actions/open-position/open-position.action.d.ts +1 -1
  21. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  22. package/dist/actions/open-position/open-position.action.js +28 -21
  23. package/dist/actions/open-position/open-position.action.js.map +1 -1
  24. package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
  25. package/dist/actions/position-tracker/position-tracker.action.js +2 -4
  26. package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
  27. package/dist/actions/tracking-helpers.d.ts +41 -3
  28. package/dist/actions/tracking-helpers.d.ts.map +1 -1
  29. package/dist/actions/tracking-helpers.js +100 -11
  30. package/dist/actions/tracking-helpers.js.map +1 -1
  31. package/dist/dsl/plugin/index.d.ts +8 -0
  32. package/dist/dsl/plugin/index.d.ts.map +1 -1
  33. package/dist/dsl/plugin/index.js +36 -2
  34. package/dist/dsl/plugin/index.js.map +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +28 -13
  37. package/dist/index.js.map +1 -1
  38. package/dist/recording/otel/attributes.d.ts +2 -0
  39. package/dist/recording/otel/attributes.d.ts.map +1 -1
  40. package/dist/recording/otel/attributes.js +1 -1
  41. package/dist/recording/otel/attributes.js.map +1 -1
  42. package/dist/recording/otel/mappers.d.ts.map +1 -1
  43. package/dist/recording/otel/mappers.js +1 -0
  44. package/dist/recording/otel/mappers.js.map +1 -1
  45. package/dist/recording/records.d.ts +7 -1
  46. package/dist/recording/records.d.ts.map +1 -1
  47. package/dist/recording/wiring/emitter-factory.d.ts +0 -5
  48. package/dist/recording/wiring/emitter-factory.d.ts.map +1 -1
  49. package/dist/recording/wiring/emitter-factory.js +0 -18
  50. package/dist/recording/wiring/emitter-factory.js.map +1 -1
  51. package/dist/recording/wiring/index.d.ts +1 -1
  52. package/dist/recording/wiring/index.d.ts.map +1 -1
  53. package/dist/recording/wiring/index.js +1 -1
  54. package/dist/recording/wiring/index.js.map +1 -1
  55. package/dist/recording/wiring/record-builders.d.ts.map +1 -1
  56. package/dist/recording/wiring/record-builders.js +5 -2
  57. package/dist/recording/wiring/record-builders.js.map +1 -1
  58. package/dist/runtime/auto-update/coordinator.js +2 -2
  59. package/dist/runtime/auto-update/coordinator.js.map +1 -1
  60. package/dist/runtime/create-actions.d.ts.map +1 -1
  61. package/dist/runtime/create-actions.js +0 -10
  62. package/dist/runtime/create-actions.js.map +1 -1
  63. package/dist/runtime/domain-events.d.ts +43 -0
  64. package/dist/runtime/domain-events.d.ts.map +1 -0
  65. package/dist/runtime/domain-events.js +181 -0
  66. package/dist/runtime/domain-events.js.map +1 -0
  67. package/dist/runtime/lifecycle-events.d.ts +43 -0
  68. package/dist/runtime/lifecycle-events.d.ts.map +1 -0
  69. package/dist/runtime/lifecycle-events.js +139 -0
  70. package/dist/runtime/lifecycle-events.js.map +1 -0
  71. package/dist/runtime/notifications.js +2 -2
  72. package/dist/runtime/notifications.js.map +1 -1
  73. package/dist/runtime/runtime-schema.d.ts +124 -173
  74. package/dist/runtime/runtime-schema.d.ts.map +1 -1
  75. package/dist/runtime/runtime-schema.js +4 -4
  76. package/dist/runtime/runtime-schema.js.map +1 -1
  77. package/dist/runtime/runtime.d.ts +1 -0
  78. package/dist/runtime/runtime.d.ts.map +1 -1
  79. package/dist/runtime/runtime.js +62 -52
  80. package/dist/runtime/runtime.js.map +1 -1
  81. package/dist/runtime/strategy-lifecycle.d.ts.map +1 -1
  82. package/dist/runtime/strategy-lifecycle.js +13 -1
  83. package/dist/runtime/strategy-lifecycle.js.map +1 -1
  84. package/dist/runtime-api/routes/signals.d.ts +1 -1
  85. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  86. package/dist/runtime-api/routes/signals.js +2 -8
  87. package/dist/runtime-api/routes/signals.js.map +1 -1
  88. package/dist/senpi/client.d.ts +10 -0
  89. package/dist/senpi/client.d.ts.map +1 -1
  90. package/dist/senpi/client.js +71 -49
  91. package/dist/senpi/client.js.map +1 -1
  92. package/dist/skills-manager/coordinator.js +3 -3
  93. package/dist/skills-manager/coordinator.js.map +1 -1
  94. package/dist/telemetry/metrics.d.ts +47 -0
  95. package/dist/telemetry/metrics.d.ts.map +1 -0
  96. package/dist/telemetry/metrics.js +154 -0
  97. package/dist/telemetry/metrics.js.map +1 -0
  98. package/dist/telemetry/otel-sdk.d.ts +23 -35
  99. package/dist/telemetry/otel-sdk.d.ts.map +1 -1
  100. package/dist/telemetry/otel-sdk.js +83 -54
  101. package/dist/telemetry/otel-sdk.js.map +1 -1
  102. package/dist/telemetry/trace-identity.d.ts +2 -1
  103. package/dist/telemetry/trace-identity.d.ts.map +1 -1
  104. package/dist/telemetry/trace-identity.js +2 -1
  105. package/dist/telemetry/trace-identity.js.map +1 -1
  106. package/dist/telemetry/tracing.d.ts +2 -1
  107. package/dist/telemetry/tracing.d.ts.map +1 -1
  108. package/dist/telemetry/tracing.js +1 -1
  109. package/dist/telemetry/tracing.js.map +1 -1
  110. package/dist/tracking/context.d.ts +6 -0
  111. package/dist/tracking/context.d.ts.map +1 -1
  112. package/dist/tracking/context.js.map +1 -1
  113. package/dist/tracking/spans/constants.d.ts +4 -11
  114. package/dist/tracking/spans/constants.d.ts.map +1 -1
  115. package/dist/tracking/spans/constants.js +4 -23
  116. package/dist/tracking/spans/constants.js.map +1 -1
  117. package/dist/tracking/spans/gen-ai.d.ts +70 -0
  118. package/dist/tracking/spans/gen-ai.d.ts.map +1 -0
  119. package/dist/tracking/spans/gen-ai.js +330 -0
  120. package/dist/tracking/spans/gen-ai.js.map +1 -0
  121. package/dist/tracking/spans/index.d.ts +6 -8
  122. package/dist/tracking/spans/index.d.ts.map +1 -1
  123. package/dist/tracking/spans/index.js +6 -11
  124. package/dist/tracking/spans/index.js.map +1 -1
  125. package/dist/tracking/spans/redact-pii.d.ts +19 -21
  126. package/dist/tracking/spans/redact-pii.d.ts.map +1 -1
  127. package/dist/tracking/spans/redact-pii.js +31 -31
  128. package/dist/tracking/spans/redact-pii.js.map +1 -1
  129. package/dist/tracking/spans/span-hooks.d.ts +0 -17
  130. package/dist/tracking/spans/span-hooks.d.ts.map +1 -1
  131. package/dist/tracking/spans/span-hooks.js +407 -207
  132. package/dist/tracking/spans/span-hooks.js.map +1 -1
  133. package/dist/utils/event-catalog.d.ts +269 -0
  134. package/dist/utils/event-catalog.d.ts.map +1 -0
  135. package/dist/utils/event-catalog.js +91 -0
  136. package/dist/utils/event-catalog.js.map +1 -0
  137. package/dist/utils/logger.d.ts +58 -5
  138. package/dist/utils/logger.d.ts.map +1 -1
  139. package/dist/utils/logger.js +359 -27
  140. package/dist/utils/logger.js.map +1 -1
  141. package/package.json +2 -1
  142. package/dist/tracking/spans/fan-out-span-transport.d.ts +0 -22
  143. package/dist/tracking/spans/fan-out-span-transport.d.ts.map +0 -1
  144. package/dist/tracking/spans/fan-out-span-transport.js +0 -49
  145. package/dist/tracking/spans/fan-out-span-transport.js.map +0 -1
  146. package/dist/tracking/spans/otel/fit-row-budget.d.ts +0 -22
  147. package/dist/tracking/spans/otel/fit-row-budget.d.ts.map +0 -1
  148. package/dist/tracking/spans/otel/fit-row-budget.js +0 -91
  149. package/dist/tracking/spans/otel/fit-row-budget.js.map +0 -1
  150. package/dist/tracking/spans/otel/map-span-to-trace-row.d.ts +0 -16
  151. package/dist/tracking/spans/otel/map-span-to-trace-row.d.ts.map +0 -1
  152. package/dist/tracking/spans/otel/map-span-to-trace-row.js +0 -227
  153. package/dist/tracking/spans/otel/map-span-to-trace-row.js.map +0 -1
  154. package/dist/tracking/spans/otel/recording-span-transport.d.ts +0 -43
  155. package/dist/tracking/spans/otel/recording-span-transport.d.ts.map +0 -1
  156. package/dist/tracking/spans/otel/recording-span-transport.js +0 -67
  157. package/dist/tracking/spans/otel/recording-span-transport.js.map +0 -1
  158. package/dist/tracking/spans/otel/span-ids.d.ts +0 -22
  159. package/dist/tracking/spans/otel/span-ids.d.ts.map +0 -1
  160. package/dist/tracking/spans/otel/span-ids.js +0 -32
  161. package/dist/tracking/spans/otel/span-ids.js.map +0 -1
  162. package/dist/tracking/spans/plugin-span-tracker.d.ts +0 -9
  163. package/dist/tracking/spans/plugin-span-tracker.d.ts.map +0 -1
  164. package/dist/tracking/spans/plugin-span-tracker.js +0 -115
  165. package/dist/tracking/spans/plugin-span-tracker.js.map +0 -1
  166. package/dist/tracking/spans/span-tracker.d.ts +0 -133
  167. package/dist/tracking/spans/span-tracker.d.ts.map +0 -1
  168. package/dist/tracking/spans/span-tracker.js +0 -721
  169. package/dist/tracking/spans/span-tracker.js.map +0 -1
  170. package/dist/tracking/spans/span-transport.d.ts +0 -71
  171. package/dist/tracking/spans/span-transport.d.ts.map +0 -1
  172. package/dist/tracking/spans/span-transport.js +0 -240
  173. package/dist/tracking/spans/span-transport.js.map +0 -1
  174. package/dist/tracking/spans/types.d.ts +0 -101
  175. package/dist/tracking/spans/types.d.ts.map +0 -1
  176. package/dist/tracking/spans/types.js +0 -6
  177. package/dist/tracking/spans/types.js.map +0 -1
@@ -1,721 +0,0 @@
1
- /**
2
- * SpanTracker: stitches start/end hook pairs into one finalized span and
3
- * enqueues it on the transport. Maintains two registries:
4
- *
5
- * - openSpans: Map<spanKey, OpenSpan> (LLM and tool spans)
6
- * - openSubagents: Map<childSessionKey, OpenSubagentSpan>
7
- *
8
- * Lifetimes:
9
- * - LLM span: llm_input → llm_output (single host turn)
10
- * - Tool span: before_tool_call → after_tool_call (single tool call)
11
- * - Subagent: subagent_spawning → … → subagent_ended (may outlive host turns)
12
- *
13
- * Orphan handling — flush spans we couldn't pair up with `data.orphan_reason`:
14
- * - agent_end sweep: any span open under (session,run) when agent_end fires
15
- * - stale sweep: anything older than STALE_TIMEOUT_MS (periodic)
16
- * - shutdown drain: anything still open at shutdown
17
- *
18
- * Never throws — errors are caught and logged.
19
- */
20
- import { randomUUID } from "node:crypto";
21
- import { createLogger } from "../../utils/logger.js";
22
- import { STALE_TIMEOUT_MS, SWEEP_INTERVAL_MS, } from "./constants.js";
23
- const logger = createLogger("[senpi-core][span-tracker]");
24
- const ADDRESS_RE = /^0x[a-f0-9]{40}$/i;
25
- const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
26
- function spanKeyLlm(sessionKey, runId) {
27
- return `session:${sessionKey}:run:${runId}:llm`;
28
- }
29
- function spanKeyTool(sessionKey, runId, toolCallId, toolName, seq) {
30
- if (toolCallId) {
31
- return `session:${sessionKey}:run:${runId}:toolcall:${toolCallId}`;
32
- }
33
- return `session:${sessionKey}:run:${runId}:tool:${toolName}:${seq}`;
34
- }
35
- function sanitizeCorrelationId(raw) {
36
- if (typeof raw !== "string")
37
- return null;
38
- return UUID_V4_RE.test(raw) ? raw : null;
39
- }
40
- function sanitizeAddress(raw) {
41
- if (typeof raw !== "string")
42
- return "";
43
- const lowered = raw.toLowerCase();
44
- return ADDRESS_RE.test(lowered) ? lowered : "";
45
- }
46
- function sanitizeString(raw) {
47
- return typeof raw === "string" && raw.length > 0 ? raw : "";
48
- }
49
- function sanitizeUint(raw) {
50
- return typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : null;
51
- }
52
- /**
53
- * Format a number as a fixed-6-decimal-place string for the cost_usd wire
54
- * field. Returns null when input is absent or non-numeric. The schema expects
55
- * `^\d+(\.\d{1,6})?$` — we emit canonical 6-decimal form so all wire values
56
- * are consistent. Negative values are rejected (return null) to match the
57
- * server-side validator.
58
- */
59
- function sanitizeCostUsd(raw) {
60
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0)
61
- return null;
62
- return raw.toFixed(6);
63
- }
64
- export class SpanTracker {
65
- transport;
66
- now;
67
- staleTimeoutMs;
68
- openSpans = new Map();
69
- openSubagents = new Map();
70
- toolSeq = 0;
71
- sweepTimer = null;
72
- constructor(options) {
73
- this.transport = options.transport;
74
- this.now = options.now ?? Date.now;
75
- this.staleTimeoutMs = options.staleTimeoutMs ?? STALE_TIMEOUT_MS;
76
- const sweepInterval = options.sweepIntervalMs ?? SWEEP_INTERVAL_MS;
77
- if (options.enableSweeper !== false) {
78
- this.sweepTimer = setInterval(() => {
79
- this.sweepStale();
80
- }, sweepInterval);
81
- if (this.sweepTimer && typeof this.sweepTimer === "object" && "unref" in this.sweepTimer) {
82
- this.sweepTimer.unref();
83
- }
84
- }
85
- }
86
- // ─── LLM lifecycle (hook path) ──────────────────────────────────────────
87
- openLlmSpan(input) {
88
- try {
89
- const key = spanKeyLlm(input.sessionKey, input.runId);
90
- const existing = this.openSpans.get(key);
91
- if (existing) {
92
- // Silently discard the prior open span. A duplicate llm_input for the
93
- // same (session, run) usually means OpenClaw's agent loop is starting
94
- // a new turn (e.g., feeding back a tool result) without having fired
95
- // llm_output for the previous turn — emitting an orphan row would
96
- // pollute ClickHouse with partial records. We post only after the
97
- // matching llm_output completes the merge.
98
- logger.debug("openLlmSpan: discarding prior open span", {
99
- sessionKey: input.sessionKey,
100
- runId: input.runId,
101
- });
102
- this.openSpans.delete(key);
103
- }
104
- const id = randomUUID();
105
- this.openSpans.set(key, {
106
- id,
107
- spanType: "llm",
108
- sessionKey: input.sessionKey,
109
- runId: input.runId,
110
- parentSpanId: "",
111
- toolCallId: "",
112
- startedAt: input.startedAt ?? this.now(),
113
- correlationId: sanitizeCorrelationId(input.correlationId),
114
- address: sanitizeAddress(input.address),
115
- recipeName: sanitizeString(input.recipeName),
116
- runtimeId: sanitizeString(input.runtimeId),
117
- runtimeVersion: sanitizeString(input.runtimeVersion),
118
- data: { input: input.input },
119
- emissionSource: input.emissionSource,
120
- });
121
- return id;
122
- }
123
- catch (err) {
124
- logger.warn("openLlmSpan failed", { err: errMsg(err) });
125
- return "";
126
- }
127
- }
128
- finalizeLlmSpan(input) {
129
- try {
130
- const key = spanKeyLlm(input.sessionKey, input.runId);
131
- const open = this.openSpans.get(key);
132
- if (!open) {
133
- logger.warn("finalizeLlmSpan: no open span", {
134
- sessionKey: input.sessionKey,
135
- runId: input.runId,
136
- });
137
- return;
138
- }
139
- this.openSpans.delete(key);
140
- const endedAt = this.now();
141
- const data = {
142
- ...open.data,
143
- ...(input.output !== undefined ? { output: input.output } : {}),
144
- ...(input.error !== undefined ? { error: errMsg(input.error) } : {}),
145
- };
146
- this.transport.enqueue({
147
- id: open.id,
148
- correlationId: sanitizeCorrelationId(input.correlationId) ?? open.correlationId,
149
- sessionKey: open.sessionKey,
150
- runId: open.runId,
151
- childSessionKey: "",
152
- toolCallId: "",
153
- parentSpanId: open.parentSpanId,
154
- spanType: "llm",
155
- emissionSource: open.emissionSource,
156
- timestamp: endedAt,
157
- address: sanitizeAddress(input.address) || open.address,
158
- recipeName: sanitizeString(input.recipeName) || open.recipeName,
159
- runtimeId: sanitizeString(input.runtimeId) || open.runtimeId,
160
- runtimeVersion: sanitizeString(input.runtimeVersion) || open.runtimeVersion,
161
- startedAt: open.startedAt,
162
- endedAt,
163
- durationMs: Math.max(0, endedAt - open.startedAt),
164
- model: sanitizeString(input.model),
165
- tokensIn: sanitizeUint(input.tokensIn),
166
- tokensOut: sanitizeUint(input.tokensOut),
167
- costUsd: sanitizeCostUsd(input.costUsd),
168
- data,
169
- });
170
- }
171
- catch (err) {
172
- logger.warn("finalizeLlmSpan failed", { err: errMsg(err) });
173
- }
174
- }
175
- /** Returns the id of the currently-open LLM span for (session, run), or null. */
176
- getActiveLlmSpanId(sessionKey, runId) {
177
- return this.openSpans.get(spanKeyLlm(sessionKey, runId))?.id ?? null;
178
- }
179
- // ─── Tool lifecycle (hook path) ─────────────────────────────────────────
180
- openToolSpan(input) {
181
- try {
182
- const parentSpanId = this.getActiveLlmSpanId(input.sessionKey, input.runId);
183
- const seq = ++this.toolSeq;
184
- const key = spanKeyTool(input.sessionKey, input.runId, input.toolCallId, input.toolName, seq);
185
- // For toolCallId-keyed entries, silently discard any duplicate so we
186
- // don't emit partial rows. The merge happens at after_tool_call.
187
- if (input.toolCallId) {
188
- const existing = this.openSpans.get(key);
189
- if (existing) {
190
- logger.debug("openToolSpan: discarding prior open span", {
191
- toolCallId: input.toolCallId,
192
- });
193
- this.openSpans.delete(key);
194
- }
195
- }
196
- const id = randomUUID();
197
- this.openSpans.set(key, {
198
- id,
199
- spanType: "tool",
200
- sessionKey: input.sessionKey,
201
- runId: input.runId,
202
- parentSpanId: parentSpanId ?? "",
203
- toolCallId: input.toolCallId ?? "",
204
- startedAt: input.startedAt ?? this.now(),
205
- correlationId: sanitizeCorrelationId(input.correlationId),
206
- address: sanitizeAddress(input.address),
207
- recipeName: sanitizeString(input.recipeName),
208
- runtimeId: sanitizeString(input.runtimeId),
209
- runtimeVersion: sanitizeString(input.runtimeVersion),
210
- // Flat shape matches captureToolSpan (direct path) so all
211
- // tool-typed rows have identical `data` layout:
212
- // { tool_name, input?, output?, error? }
213
- // Downstream queries can rely on JSONExtractString(data, 'tool_name')
214
- // regardless of emission_source.
215
- data: {
216
- tool_name: input.toolName,
217
- ...(input.toolInput !== undefined ? { input: input.toolInput } : {}),
218
- },
219
- emissionSource: input.emissionSource,
220
- });
221
- return id;
222
- }
223
- catch (err) {
224
- logger.warn("openToolSpan failed", { err: errMsg(err) });
225
- return "";
226
- }
227
- }
228
- finalizeToolSpan(input) {
229
- try {
230
- // Find by toolCallId first (most precise — bridges any runId drift).
231
- let foundKey;
232
- if (input.toolCallId) {
233
- const exactKey = spanKeyTool(input.sessionKey, input.runId, input.toolCallId, input.toolName, 0);
234
- if (this.openSpans.has(exactKey)) {
235
- foundKey = exactKey;
236
- }
237
- }
238
- // Next, when we have a toolCallId: (session, *, toolCallId) — bridges
239
- // runId drift between before_tool_call (runId=R1) and after_tool_call
240
- // (runId=R2, e.g. when a fresh llm_input fired mid-tool and updated the
241
- // session's lastActive runId). toolCallId is provider-stamped and unique
242
- // per call, so this match is still precise — only the run scope shifts.
243
- if (!foundKey && input.toolCallId) {
244
- const sessionPrefix = `session:${input.sessionKey}:run:`;
245
- const toolCallSuffix = `:toolcall:${input.toolCallId}`;
246
- for (const key of this.openSpans.keys()) {
247
- if (key.startsWith(sessionPrefix) && key.endsWith(toolCallSuffix)) {
248
- foundKey = key;
249
- break;
250
- }
251
- }
252
- }
253
- // Next: (session, run, toolName) prefix — happy path when runId stable
254
- // and no toolCallId was issued.
255
- //
256
- // Without a toolCallId we can't disambiguate two same-name tools open at
257
- // once. We pick the oldest by insertion order (Map iteration order) and
258
- // log a warn when multiple match so the ambiguity is observable in
259
- // production rather than silently mis-attributing output. If providers
260
- // ever emit parallel tool calls without ids, this surfaces in the logs.
261
- if (!foundKey) {
262
- const prefix = `session:${input.sessionKey}:run:${input.runId}:tool:${input.toolName}:`;
263
- const matches = [];
264
- for (const key of this.openSpans.keys()) {
265
- if (key.startsWith(prefix))
266
- matches.push(key);
267
- }
268
- if (matches.length > 1) {
269
- logger.warn("finalizeToolSpan: ambiguous match (multiple same-name tools open without toolCallId); attributing to oldest", {
270
- sessionKey: input.sessionKey,
271
- runId: input.runId,
272
- toolName: input.toolName,
273
- matchCount: matches.length,
274
- });
275
- }
276
- foundKey = matches[0];
277
- }
278
- // Last resort: (session, *, toolName) — handles runId drift for spans
279
- // without any toolCallId. Common with long-running exec tools. Same
280
- // ambiguity rule: take the oldest matching open span and warn if more
281
- // than one matched.
282
- if (!foundKey && !input.toolCallId) {
283
- const sessionPrefix = `session:${input.sessionKey}:run:`;
284
- const toolSuffix = `:tool:${input.toolName}:`;
285
- const matches = [];
286
- for (const key of this.openSpans.keys()) {
287
- if (key.startsWith(sessionPrefix) && key.includes(toolSuffix)) {
288
- matches.push(key);
289
- }
290
- }
291
- if (matches.length > 1) {
292
- logger.warn("finalizeToolSpan: ambiguous match across runIds (multiple same-name tools open without toolCallId); attributing to oldest", {
293
- sessionKey: input.sessionKey,
294
- toolName: input.toolName,
295
- matchCount: matches.length,
296
- });
297
- }
298
- foundKey = matches[0];
299
- }
300
- if (!foundKey) {
301
- // Likely already orphan-flushed by an agent_end sweep, or never opened.
302
- // Downgrade to debug — warn was too noisy in practice.
303
- logger.debug("finalizeToolSpan: no open span", {
304
- sessionKey: input.sessionKey,
305
- toolName: input.toolName,
306
- toolCallId: input.toolCallId,
307
- });
308
- return;
309
- }
310
- const open = this.openSpans.get(foundKey);
311
- if (!open)
312
- return;
313
- this.openSpans.delete(foundKey);
314
- const endedAt = this.now();
315
- const data = { ...open.data };
316
- if (input.error !== undefined) {
317
- data.error = errMsg(input.error);
318
- }
319
- else if (input.output !== undefined) {
320
- data.output =
321
- input.output && typeof input.output === "object"
322
- ? input.output
323
- : { result: input.output };
324
- }
325
- this.transport.enqueue({
326
- id: open.id,
327
- correlationId: sanitizeCorrelationId(input.correlationId) ?? open.correlationId,
328
- sessionKey: open.sessionKey,
329
- runId: open.runId,
330
- childSessionKey: "",
331
- toolCallId: open.toolCallId,
332
- parentSpanId: open.parentSpanId,
333
- spanType: "tool",
334
- emissionSource: open.emissionSource,
335
- timestamp: endedAt,
336
- address: sanitizeAddress(input.address) || open.address,
337
- recipeName: sanitizeString(input.recipeName) || open.recipeName,
338
- runtimeId: sanitizeString(input.runtimeId) || open.runtimeId,
339
- runtimeVersion: sanitizeString(input.runtimeVersion) || open.runtimeVersion,
340
- startedAt: open.startedAt,
341
- endedAt,
342
- durationMs: input.durationMs ?? Math.max(0, endedAt - open.startedAt),
343
- model: "",
344
- tokensIn: null,
345
- tokensOut: null,
346
- costUsd: null,
347
- data,
348
- });
349
- }
350
- catch (err) {
351
- logger.warn("finalizeToolSpan failed", { err: errMsg(err) });
352
- }
353
- }
354
- // ─── Subagent lifecycle (separate registry, keyed by childSessionKey) ───
355
- openSubagentSpan(input) {
356
- try {
357
- const existing = this.openSubagents.get(input.childSessionKey);
358
- if (existing) {
359
- this.enqueueSubagentOrphan(existing, "subagent_spawning_replaces_active");
360
- this.openSubagents.delete(input.childSessionKey);
361
- }
362
- const id = randomUUID();
363
- this.openSubagents.set(input.childSessionKey, {
364
- id,
365
- hostSessionKey: input.hostSessionKey,
366
- hostRunId: input.hostRunId,
367
- childSessionKey: input.childSessionKey,
368
- parentSpanId: input.parentSpanId ?? "",
369
- startedAt: input.startedAt ?? this.now(),
370
- correlationId: sanitizeCorrelationId(input.correlationId),
371
- address: sanitizeAddress(input.address),
372
- recipeName: sanitizeString(input.recipeName),
373
- runtimeId: sanitizeString(input.runtimeId),
374
- runtimeVersion: sanitizeString(input.runtimeVersion),
375
- data: {
376
- input: input.input,
377
- status: "spawning",
378
- },
379
- });
380
- return id;
381
- }
382
- catch (err) {
383
- logger.warn("openSubagentSpan failed", { err: errMsg(err) });
384
- return "";
385
- }
386
- }
387
- /** Merge fields into an existing subagent (e.g., on _spawned, _delivery_target). Lazy-creates if missing. */
388
- mergeSubagentSpan(input, context) {
389
- try {
390
- let open = this.openSubagents.get(input.childSessionKey);
391
- if (!open) {
392
- // Lazy-create: missed _spawning. Use what context we have.
393
- const id = randomUUID();
394
- open = {
395
- id,
396
- hostSessionKey: "",
397
- hostRunId: "",
398
- childSessionKey: input.childSessionKey,
399
- parentSpanId: "",
400
- startedAt: this.now(),
401
- correlationId: sanitizeCorrelationId(context?.correlationId),
402
- address: sanitizeAddress(context?.address),
403
- recipeName: sanitizeString(context?.recipeName),
404
- runtimeId: sanitizeString(context?.runtimeId),
405
- runtimeVersion: sanitizeString(context?.runtimeVersion),
406
- data: { status: input.status },
407
- };
408
- this.openSubagents.set(input.childSessionKey, open);
409
- }
410
- open.data.status = input.status;
411
- if (input.patch) {
412
- // shallow-merge any provided fields into the data root
413
- for (const [k, v] of Object.entries(input.patch)) {
414
- open.data[k] = v;
415
- }
416
- }
417
- }
418
- catch (err) {
419
- logger.warn("mergeSubagentSpan failed", { err: errMsg(err) });
420
- }
421
- }
422
- finalizeSubagentSpan(input, context) {
423
- try {
424
- const open = this.openSubagents.get(input.childSessionKey);
425
- if (!open) {
426
- logger.warn("finalizeSubagentSpan: no open subagent", {
427
- childSessionKey: input.childSessionKey,
428
- });
429
- return;
430
- }
431
- this.openSubagents.delete(input.childSessionKey);
432
- const endedAt = this.now();
433
- const data = { ...open.data, status: input.status };
434
- if (input.error !== undefined) {
435
- data.output = { error: errMsg(input.error) };
436
- }
437
- else if (input.output !== undefined) {
438
- data.output = input.output;
439
- }
440
- this.transport.enqueue({
441
- id: open.id,
442
- correlationId: sanitizeCorrelationId(context?.correlationId) ?? open.correlationId,
443
- sessionKey: open.hostSessionKey,
444
- runId: open.hostRunId,
445
- childSessionKey: open.childSessionKey,
446
- toolCallId: "",
447
- parentSpanId: open.parentSpanId,
448
- spanType: "subagent",
449
- emissionSource: "hook",
450
- timestamp: endedAt,
451
- address: sanitizeAddress(context?.address) || open.address,
452
- recipeName: sanitizeString(context?.recipeName) || open.recipeName,
453
- runtimeId: sanitizeString(context?.runtimeId) || open.runtimeId,
454
- runtimeVersion: sanitizeString(context?.runtimeVersion) || open.runtimeVersion,
455
- startedAt: open.startedAt,
456
- endedAt,
457
- durationMs: Math.max(0, endedAt - open.startedAt),
458
- model: "",
459
- tokensIn: null,
460
- tokensOut: null,
461
- costUsd: null,
462
- data,
463
- });
464
- }
465
- catch (err) {
466
- logger.warn("finalizeSubagentSpan failed", { err: errMsg(err) });
467
- }
468
- }
469
- // ─── Agent end — emit marker + sweep orphans for this (session, run) ────
470
- captureAgentEnd(input) {
471
- try {
472
- // Sweep orphans for this session/run.
473
- const prefix = `session:${input.sessionKey}:run:${input.runId}:`;
474
- const keysToSweep = [];
475
- for (const key of this.openSpans.keys()) {
476
- if (key.startsWith(prefix))
477
- keysToSweep.push(key);
478
- }
479
- for (const key of keysToSweep) {
480
- const open = this.openSpans.get(key);
481
- if (open) {
482
- this.enqueueOrphan(open, "agent_end_without_close");
483
- this.openSpans.delete(key);
484
- }
485
- }
486
- // Also sweep subagents hosted by this (session, run).
487
- const subKeysToSweep = [];
488
- for (const [k, sub] of this.openSubagents.entries()) {
489
- if (sub.hostSessionKey === input.sessionKey && sub.hostRunId === input.runId) {
490
- subKeysToSweep.push(k);
491
- }
492
- }
493
- for (const k of subKeysToSweep) {
494
- const sub = this.openSubagents.get(k);
495
- if (sub) {
496
- this.enqueueSubagentOrphan(sub, "agent_end_without_close");
497
- this.openSubagents.delete(k);
498
- }
499
- }
500
- const endedAt = this.now();
501
- const data = {};
502
- if (input.outcome !== undefined)
503
- data.outcome = input.outcome;
504
- if (input.error !== undefined)
505
- data.error = errMsg(input.error);
506
- this.transport.enqueue({
507
- id: randomUUID(),
508
- correlationId: sanitizeCorrelationId(input.correlationId),
509
- sessionKey: input.sessionKey,
510
- runId: input.runId,
511
- childSessionKey: "",
512
- toolCallId: "",
513
- parentSpanId: "",
514
- spanType: "agent_end",
515
- emissionSource: "hook",
516
- timestamp: endedAt,
517
- address: sanitizeAddress(input.address),
518
- recipeName: sanitizeString(input.recipeName),
519
- runtimeId: sanitizeString(input.runtimeId),
520
- runtimeVersion: sanitizeString(input.runtimeVersion),
521
- startedAt: endedAt,
522
- endedAt,
523
- durationMs: 0,
524
- model: "",
525
- tokensIn: null,
526
- tokensOut: null,
527
- costUsd: null,
528
- data,
529
- });
530
- }
531
- catch (err) {
532
- logger.warn("captureAgentEnd failed", { err: errMsg(err) });
533
- }
534
- }
535
- // ─── Direct (manual) emission — for non-hook paths like AnthropicLlmDecision ──
536
- captureLlmSpan(input) {
537
- try {
538
- const data = { input: input.input };
539
- if (input.output !== undefined)
540
- data.output = input.output;
541
- if (input.error !== undefined)
542
- data.error = errMsg(input.error);
543
- this.transport.enqueue({
544
- id: randomUUID(),
545
- correlationId: sanitizeCorrelationId(input.correlationId),
546
- sessionKey: input.sessionKey,
547
- runId: input.runId,
548
- childSessionKey: "",
549
- toolCallId: "",
550
- parentSpanId: input.parentSpanId ?? "",
551
- spanType: "llm",
552
- emissionSource: "direct",
553
- timestamp: input.endedAt,
554
- address: sanitizeAddress(input.address),
555
- recipeName: sanitizeString(input.recipeName),
556
- runtimeId: sanitizeString(input.runtimeId),
557
- runtimeVersion: sanitizeString(input.runtimeVersion),
558
- startedAt: input.startedAt,
559
- endedAt: input.endedAt,
560
- durationMs: Math.max(0, input.endedAt - input.startedAt),
561
- model: sanitizeString(input.model),
562
- tokensIn: sanitizeUint(input.tokensIn),
563
- tokensOut: sanitizeUint(input.tokensOut),
564
- costUsd: sanitizeCostUsd(input.costUsd),
565
- data,
566
- });
567
- }
568
- catch (err) {
569
- logger.warn("captureLlmSpan failed", { err: errMsg(err) });
570
- }
571
- }
572
- captureToolSpan(input) {
573
- try {
574
- const data = { tool_name: input.toolName };
575
- if (input.input !== undefined)
576
- data.input = input.input;
577
- if (input.output !== undefined)
578
- data.output = input.output;
579
- if (input.error !== undefined)
580
- data.error = errMsg(input.error);
581
- this.transport.enqueue({
582
- id: randomUUID(),
583
- correlationId: sanitizeCorrelationId(input.correlationId),
584
- sessionKey: input.sessionKey,
585
- runId: input.runId,
586
- childSessionKey: "",
587
- toolCallId: input.toolCallId ?? "",
588
- parentSpanId: input.parentSpanId ?? "",
589
- spanType: "tool",
590
- emissionSource: "direct",
591
- timestamp: input.endedAt,
592
- address: sanitizeAddress(input.address),
593
- recipeName: sanitizeString(input.recipeName),
594
- runtimeId: sanitizeString(input.runtimeId),
595
- runtimeVersion: sanitizeString(input.runtimeVersion),
596
- startedAt: input.startedAt,
597
- endedAt: input.endedAt,
598
- durationMs: Math.max(0, input.endedAt - input.startedAt),
599
- model: "",
600
- tokensIn: null,
601
- tokensOut: null,
602
- costUsd: null,
603
- data,
604
- });
605
- }
606
- catch (err) {
607
- logger.warn("captureToolSpan failed", { err: errMsg(err) });
608
- }
609
- }
610
- // ─── Maintenance ────────────────────────────────────────────────────────
611
- sweepStale() {
612
- try {
613
- const cutoff = this.now() - this.staleTimeoutMs;
614
- const keys = [];
615
- for (const [k, open] of this.openSpans.entries()) {
616
- if (open.startedAt < cutoff)
617
- keys.push(k);
618
- }
619
- for (const k of keys) {
620
- const open = this.openSpans.get(k);
621
- if (open) {
622
- this.enqueueOrphan(open, "stale_timeout");
623
- this.openSpans.delete(k);
624
- }
625
- }
626
- const subKeys = [];
627
- for (const [k, sub] of this.openSubagents.entries()) {
628
- if (sub.startedAt < cutoff)
629
- subKeys.push(k);
630
- }
631
- for (const k of subKeys) {
632
- const sub = this.openSubagents.get(k);
633
- if (sub) {
634
- this.enqueueSubagentOrphan(sub, "stale_timeout");
635
- this.openSubagents.delete(k);
636
- }
637
- }
638
- }
639
- catch (err) {
640
- logger.warn("sweepStale failed", { err: errMsg(err) });
641
- }
642
- }
643
- async shutdown() {
644
- if (this.sweepTimer) {
645
- clearInterval(this.sweepTimer);
646
- this.sweepTimer = null;
647
- }
648
- // Drain remaining open spans as orphans.
649
- for (const [k, open] of this.openSpans.entries()) {
650
- this.enqueueOrphan(open, "shutdown");
651
- this.openSpans.delete(k);
652
- }
653
- for (const [k, sub] of this.openSubagents.entries()) {
654
- this.enqueueSubagentOrphan(sub, "shutdown");
655
- this.openSubagents.delete(k);
656
- }
657
- await this.transport.shutdown();
658
- }
659
- // ─── Internal: orphan enqueue helpers ───────────────────────────────────
660
- enqueueOrphan(open, reason) {
661
- const endedAt = this.now();
662
- const data = { ...open.data, orphan_reason: reason };
663
- const span = {
664
- id: open.id,
665
- correlationId: open.correlationId,
666
- sessionKey: open.sessionKey,
667
- runId: open.runId,
668
- childSessionKey: "",
669
- toolCallId: open.toolCallId,
670
- parentSpanId: open.parentSpanId,
671
- spanType: open.spanType,
672
- emissionSource: open.emissionSource,
673
- timestamp: endedAt,
674
- address: open.address,
675
- recipeName: open.recipeName,
676
- runtimeId: open.runtimeId,
677
- runtimeVersion: open.runtimeVersion,
678
- startedAt: open.startedAt,
679
- endedAt,
680
- durationMs: Math.max(0, endedAt - open.startedAt),
681
- model: "",
682
- tokensIn: null,
683
- tokensOut: null,
684
- costUsd: null,
685
- data,
686
- };
687
- this.transport.enqueue(span);
688
- }
689
- enqueueSubagentOrphan(open, reason) {
690
- const endedAt = this.now();
691
- const data = { ...open.data, orphan_reason: reason };
692
- this.transport.enqueue({
693
- id: open.id,
694
- correlationId: open.correlationId,
695
- sessionKey: open.hostSessionKey,
696
- runId: open.hostRunId,
697
- childSessionKey: open.childSessionKey,
698
- toolCallId: "",
699
- parentSpanId: open.parentSpanId,
700
- spanType: "subagent",
701
- emissionSource: "hook",
702
- timestamp: endedAt,
703
- address: open.address,
704
- recipeName: open.recipeName,
705
- runtimeId: open.runtimeId,
706
- runtimeVersion: open.runtimeVersion,
707
- startedAt: open.startedAt,
708
- endedAt,
709
- durationMs: Math.max(0, endedAt - open.startedAt),
710
- model: "",
711
- tokensIn: null,
712
- tokensOut: null,
713
- costUsd: null,
714
- data,
715
- });
716
- }
717
- }
718
- function errMsg(err) {
719
- return err instanceof Error ? err.message : String(err);
720
- }
721
- //# sourceMappingURL=span-tracker.js.map